Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ functions/dist

npm-debug.log*
.env
!.env.example

# firebase
.firebase/
.firebase/
firebase-debug.log*
169 changes: 105 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,96 +1,137 @@
# Today I Learned Alarm

GitHub 커밋 데이터를 기반으로 학습용 플래시카드를 생성하는 PWA 애플리케이션입니다.
매일 학습한 내용을 정리하고 알림을 받는 앱입니다.

## 🏗️ 프로젝트 구조
## 🚀 빠른 시작

### 1. 환경 설정
```bash
# Firebase 프로젝트 정보를 사용해 app/.env에 Functions 프록시 주소 생성/추가
pnpm env:setup

# (옵션) 수동 설정 시 app/.env에 다음 키들을 추가하세요
# Firebase Web 설정 (Console > 프로젝트 설정 > 일반 > 웹 앱 구성에서 복사)
VITE_API_KEY=...
VITE_AUTH_DOMAIN=...
VITE_PROJECT_ID=...
VITE_STORAGE_BUCKET=...
VITE_MESSAGING_SENDER_ID=...
VITE_APP_ID=...
VITE_MEASUREMENT_ID=...

# Functions 호출용 (env:setup가 자동 추가)
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_REGION=us-central1
VITE_FUNCTIONS_URL_LOCAL=http://localhost:5001/your-project-id/us-central1
VITE_FUNCTIONS_URL_PROD=https://us-central1-your-project-id.cloudfunctions.net
```
repo/
├── app/ # React + Vite + TypeScript (PWA)
├── functions/ # Cloud Functions (TypeScript + tsup)
├── package.json # 워크스페이스 루트
├── pnpm-workspace.yaml
└── firebase.json
```

## 🚀 기술 스택

- **패키지 매니저**: pnpm (워크스페이스 지원)
- **프론트엔드**: Vite + React + TypeScript + PWA
- **백엔드**: Firebase Cloud Functions (TypeScript + tsup)
- **데이터베이스**: IndexedDB (클라이언트)
- **배포**: Firebase Hosting + Functions
- **스케줄링**: Firebase Functions v2 onSchedule

## 📦 설치 및 실행

### 사전 요구사항
- Node.js 20+
- pnpm 9+
- Firebase CLI

### 설치
### 2. 개발 서버 시작
```bash
# 의존성 설치
pnpm install

# 개발 서버 실행
pnpm dev
```

# 빌드
pnpm build
### 3. Firebase Functions 설정
```bash
cd functions

# 배포
pnpm deploy
# 환경변수 설정 (로컬 개발용)
echo "GITHUB_TOKEN=your_github_token_here" > .env

# Functions 실행
pnpm serve
```

### 환경 변수 설정
`app/env.example`을 참고하여 `.env` 파일을 생성하세요:
## 📁 프로젝트 구조

```bash
cp app/env.example app/.env
```
├── app/ # React 앱 (프론트엔드)
│ ├── src/
│ │ ├── api/ # API 호출 함수들
│ │ ├── modules/ # 유틸리티 (axios 등)
│ │ └── pages/ # 페이지 컴포넌트들
│ └── vite.config.ts # Vite 설정 (프록시 포함)
├── functions/ # Firebase Functions (백엔드)
│ ├── src/
│ │ ├── github.ts # GitHub API Functions
│ │ ├── schedule.ts # 스케줄러 Functions
│ │ └── hypercloax.ts # Hypercloax API Functions
│ └── package.json
└── scripts/
└── setup-proxy.js # app/.env에 Functions URL 자동 추가/보강 스크립트
```

## 🔧 개발
## 🔧 환경변수

### 웹 앱 개발
### 앱 환경변수 (app/.env)
```bash
cd app
pnpm dev
# Firebase Web 설정 (콘솔에서 복사)
VITE_API_KEY=...
VITE_AUTH_DOMAIN=...
VITE_PROJECT_ID=...
VITE_STORAGE_BUCKET=...
VITE_MESSAGING_SENDER_ID=...
VITE_APP_ID=...
VITE_MEASUREMENT_ID=...

# Functions 호출 설정 (env:setup 실행 시 자동 추가/보강)
VITE_FIREBASE_PROJECT_ID=til-alarm
VITE_FIREBASE_REGION=us-central1
VITE_FUNCTIONS_URL_LOCAL=http://localhost:5001/til-alarm/us-central1
VITE_FUNCTIONS_URL_PROD=https://us-central1-til-alarm.cloudfunctions.net
```

### Functions 개발
### Functions 환경변수 (functions/.env)
```bash
cd functions
pnpm serve # 에뮬레이터 실행
GITHUB_TOKEN=your_github_token_here
CLOVA_API_KEY=your_clova_api_key
NCLOUD_API_KEY=your_ncloud_api_key
```

## 📱 PWA 기능
## 🚀 배포

- 오프라인 지원
- 웹 푸시 알림
- 설치 가능한 앱
- 백그라운드 동기화
### Functions 개별 배포
```bash
cd functions

## 🔔 알림 기능
# GitHub API만 배포
pnpm deploy:github

- 매일 오전 8시(KST) 자동 알림
- Firebase Cloud Messaging 사용
- 토픽 기반 브로드캐스트
# Schedule만 배포
pnpm deploy:schedule

## 🚀 배포
# Hypercloax만 배포
pnpm deploy:hypercloax

### Firebase 설정
# 전체 배포
pnpm deploy
```

### 앱 배포
```bash
firebase login
firebase init hosting functions
# 루트에서 전체 배포
pnpm deploy
```

### CI/CD
GitHub Actions를 통한 자동 배포:
- `main` 브랜치 푸시 시 자동 배포
- Firebase Hosting + Functions 동시 배포
## 🔄 API 구조

### GitHub API
- `GET /api/getCommits?since={date}&until={date}` - 커밋 목록
- `GET /api/getFilename?commit_sha={sha}` - 커밋 상세
- `GET /api/getMarkdown?filename={filename}` - 마크다운 내용

### Hypercloax API
- `POST /api/chatCompletions` - CLOVA Studio 질문 생성
- `POST /api/registerDeviceToken` - FCM 토큰 등록
- `POST /api/removeDeviceToken` - FCM 토큰 삭제
- `POST /api/registerSchedule` - 스케줄 등록

### Schedule
- 자동 실행 (매일 오전 8시 KST)

## 📝 라이선스
## 🛠️ 개발 도구

MIT License
- **프론트엔드**: React + TypeScript + Vite
- **백엔드**: Firebase Functions + TypeScript
- **API 통신**: Axios
- **배포**: Firebase Hosting + Functions
11 changes: 7 additions & 4 deletions app/env.example → app/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Firebase 설정
# Firebase auth
VITE_API_KEY=your_firebase_api_key_here
VITE_AUTH_DOMAIN=your_project.firebaseapp.com
VITE_PROJECT_ID=your_project_id
Expand All @@ -8,9 +8,12 @@ VITE_APP_ID=your_app_id
VITE_MEASUREMENT_ID=your_measurement_id
VITE_VAPID_KEY=your_vapid_key_here

# Firebase Functions (pnpm proxy 실행시 자동 생성)
VITE_FIREBASE_PROJECT_ID=
VITE_FIREBASE_REGION=
VITE_FUNCTIONS_URL_LOCAL=
VITE_FUNCTIONS_URL_PROD=

# 사용자 설정
VITE_USER_ID=your_user_id_here
VITE_SCHEDULE_CODE=your_schedule_code_here

# Naver Cloud Platform
VITE_NCLOUD_HYPERCLOVAX_URL=your_ncloud_hyperclovax_url_here
1 change: 1 addition & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"build": "tsc && vite build"
},
"dependencies": {
"axios": "^1.12.2",
"firebase": "^10.4.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
63 changes: 62 additions & 1 deletion app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ import { chatCompletions } from './api/ncloud-api';
import { getCurrentDate } from './modules/utils';
import FlashCardViewer from './pages/FlashCardViewer';
import Login from './pages/Login';
import Settings from './pages/Settings';
import { getGithubData } from './services/github-service';

initDB(DBConfig);
const dates = [1, 7, 30]; // days ago list

type Page = 'flashcard' | 'settings';

const App: React.FC = () => {
const { add, getByID } = useIndexedDB("data");
const [loading, setLoading] = useState<boolean>(true);
const [user, setUser] = useState<User | null>(null);
const [authLoading, setAuthLoading] = useState<boolean>(true);
const [currentPage, setCurrentPage] = useState<Page>('flashcard');

// 인증 상태 감지
useEffect(() => {
Expand Down Expand Up @@ -104,7 +108,64 @@ const App: React.FC = () => {
// 메인 앱 렌더링
return (
<main>
<FlashCardViewer />
{/* 네비게이션 */}
<nav style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
zIndex: 1000,
padding: '12px 20px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => setCurrentPage('flashcard')}
style={{
padding: '8px 16px',
background: currentPage === 'flashcard' ? '#667eea' : 'transparent',
color: currentPage === 'flashcard' ? 'white' : '#667eea',
border: '2px solid #667eea',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 600,
transition: 'all 0.2s',
}}
>
📚 학습하기
</button>
<button
onClick={() => setCurrentPage('settings')}
style={{
padding: '8px 16px',
background: currentPage === 'settings' ? '#667eea' : 'transparent',
color: currentPage === 'settings' ? 'white' : '#667eea',
border: '2px solid #667eea',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 600,
transition: 'all 0.2s',
}}
>
⚙️ 설정
</button>
</div>

<div style={{ fontSize: '0.9rem', color: '#666' }}>
👤 {user.displayName || user.email}
</div>
</nav>

{/* 페이지 컨텐츠 */}
<div style={{ paddingTop: '60px' }}>
{currentPage === 'flashcard' && <FlashCardViewer />}
{currentPage === 'settings' && <Settings />}
</div>
</main>
);
};
Expand Down
37 changes: 22 additions & 15 deletions app/src/api/github-api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { apiClient } from '../modules/axios';

interface Commit {
sha: string;
commit: {
Expand All @@ -14,29 +16,34 @@ interface CommitDetail {
}>;
}

interface MarkdownResponse {
content: string;
}

export async function getCommits(since: Date, until: Date): Promise<Commit[]> {
const sinceISO = since.toISOString();
const untilISO = until.toISOString();

const response = await fetch(`https://api.github.com/repos/hssuh/TIL/commits?since=${sinceISO}&until=${untilISO}`);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
return await response.json();
const response = await apiClient.get('/getCommits', {
params: { since: sinceISO, until: untilISO }
});

return response.data;
}

export async function getFilename(sha: string): Promise<CommitDetail> {
const response = await fetch(`https://api.github.com/repos/hssuh/TIL/commits/${sha}`);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
return await response.json();
const response = await apiClient.get('/getFilename', {
params: { commit_sha: sha }
});

return response.data;
}

export async function getMarkdown(filename: string): Promise<string> {
const response = await fetch(`https://raw.githubusercontent.com/hssuh/TIL/main/${filename}`);
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
return await response.text();
const response = await apiClient.get('/getMarkdown', {
params: { filename }
});

const data: MarkdownResponse = response.data;
return data.content;
}
Loading