# 개발/배포 가이드

## 개발 모드

### 실행
```bash
cd /docker/fromis_9
docker compose up -d --build
```

### 컨테이너 구성
| 컨테이너 | 포트 | 설명 |
|---------|------|------|
| `fromis9-frontend` | 80 | Vite 개발 서버, HMR 지원 |
| `fromis9-backend` | 80 | Fastify API, --watch 모드 |
| `fromis9-meilisearch` | 7700 | 검색 엔진 |
| `fromis9-redis` | 6379 | 캐시 |

- Vite가 `/api`, `/docs` 요청을 백엔드로 프록시

### 로그 확인
```bash
# 전체 로그
docker compose logs -f

# 백엔드만
docker compose logs -f fromis9-backend

# 프론트엔드만
docker compose logs -f fromis9-frontend
```

### 코드 수정
- `frontend/`, `backend/` 폴더가 컨테이너에 마운트됨
- `node_modules`도 호스트 폴더에 직접 설치됨
- 코드 수정 시 자동 반영 (HMR, watch)

### 재시작
```bash
# 백엔드만 재시작
docker compose restart fromis9-backend

# 프론트엔드만 재시작
docker compose restart fromis9-frontend

# 전체 재시작
docker compose restart
```

---

## 배포 모드 전환

### 1. Dockerfile 수정

**backend/Dockerfile:**
```dockerfile
# 개발 모드 주석처리
# FROM node:20-alpine
# ...

# 배포 모드 주석해제
FROM node:20-alpine
WORKDIR /app
RUN apk add --no-cache ffmpeg
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
```

**frontend/Dockerfile:**
```dockerfile
# 개발 모드 주석처리
# FROM node:20-alpine
# ...

# 배포 모드 주석해제
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```

### 2. 빌드 및 실행
```bash
docker compose up -d --build
```

---

## 환경 변수 (.env)

```env
# 서버
PORT=80

# 데이터베이스
DB_HOST=mariadb
DB_PORT=3306
DB_USER=...
DB_PASSWORD=...
DB_NAME=fromis9

# Redis
REDIS_HOST=fromis9-redis
REDIS_PORT=6379

# Meilisearch
MEILI_HOST=http://fromis9-meilisearch:7700
MEILI_MASTER_KEY=...

# JWT
JWT_SECRET=...

# AWS S3
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=...
S3_BUCKET=...

# YouTube API
YOUTUBE_API_KEY=...
```

---

## Caddy 설정

위치: `/docker/caddy/Caddyfile`

### fromis_9 사이트 설정
```caddyfile
fromis9.caadiq.co.kr {
    import custom_errors
    reverse_proxy fromis9-frontend:80
}
```

### 설정 설명
- `import custom_errors`: 공통 에러 페이지 (403, 404, 500, 502, 503)
- `reverse_proxy fromis9-frontend:80`: Docker 네트워크로 프론트엔드 컨테이너에 연결
- 업로드 크기 제한 없음 (Caddy 기본값)

### Caddy 재시작
```bash
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
```

### 네트워크 구조
```
인터넷 → Caddy (:443) → fromis9-frontend (:80) → fromis9-backend (:80)
                                               ↓
                         MariaDB, Redis, Meilisearch (내부 네트워크)
```

---

## 프론트엔드 개발 가이드

### API 클라이언트 구조

```
src/api/
├── index.js          # 전체 export
├── client.js         # api, authApi 헬퍼 (에러 처리, 토큰 주입)
├── public/           # 공개 API (인증 불필요)
│   ├── albums.js     # getAlbums, getAlbumByName, getTrack
│   ├── members.js    # getMembers
│   └── schedules.js  # getSchedules, getSchedule, getCategories
└── admin/            # 관리자 API (인증 필요)
    ├── auth.js       # login, verifyToken
    ├── albums.js     # createAlbum, updateAlbum, deleteAlbum, ...
    ├── bots.js       # getBots, startBot, stopBot, syncBot, getXBot, createXBot, updateXBot, deleteXBot, lookupXProfile
    ├── categories.js # getCategories, createCategory, updateCategory, ...
    ├── members.js    # updateMember
    ├── schedules.js  # getYoutubeInfo, saveYoutube, getXInfo, saveX, ...
    ├── stats.js      # getStats
    └── suggestions.js # getDict, saveDict
```

**client.js 헬퍼:**
```jsx
// 공개 API 헬퍼 (인증 불필요)
import { api } from '@/api/client';

api.get('/albums');
api.post('/schedules/suggestions/save', { query: '검색어' });

// 인증 API 헬퍼 (토큰 자동 주입)
import { authApi } from '@/api/client';

authApi.get('/admin/stats');
authApi.post('/admin/schedules', data);
authApi.put('/admin/albums/1', data);
authApi.del('/admin/schedules/1');
```

**사용 예시:**
```jsx
// 공개 API
import { getSchedules, getSchedule } from '@/api/public/schedules';

// 관리자 API
import * as botsApi from '@/api/admin/bots';
```

### React Query 사용 (데이터 페칭)

데이터 페칭 시 `useEffect` 대신 `useQuery`를 사용합니다.

**이유:**
- `useEffect`는 React StrictMode에서 2번 실행됨 (개발 모드)
- `useQuery`는 자동 캐싱, 중복 요청 방지, 에러/로딩 상태 관리 제공

**예시:**
```jsx
// ❌ Bad - useEffect 사용
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  fetch('/api/data')
    .then(res => res.json())
    .then(data => setData(data))
    .finally(() => setLoading(false));
}, []);

// ✅ Good - useQuery 사용
import { useQuery } from '@tanstack/react-query';

const { data, isLoading } = useQuery({
  queryKey: ['data'],
  queryFn: () => fetch('/api/data').then(res => res.json()),
});
```

**캐시 무효화:**
```jsx
import { useQueryClient } from '@tanstack/react-query';

const queryClient = useQueryClient();

// 특정 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['schedules'] });

// 모든 쿼리 무효화
queryClient.invalidateQueries();
```

---

## YouTube 봇 동기화

### 동기화 흐름 (syncNewVideos)
1. `fetchRecentVideoIds()` — Activities API로 최근 영상 ID 목록만 조회 (1 unit)
2. DB에서 이미 존재하는 video_id 필터링
3. 새 영상만 `fetchVideoInfo()` — Videos API로 상세 정보 조회 (새 영상당 1 unit)
4. `saveVideo()` — DB 저장 + Meilisearch 동기화

### API 할당량
- 일일 할당량: 10,000 units
- 새 영상 없을 때: activities.list 1 unit만 소비
- 새 영상 있을 때: 1 + 새 영상 수 units
- 1분 간격, 3채널 기준: ~4,320 units/일 (43%)

### 주요 API 함수 (services/youtube/api.js)
| 함수 | YouTube API | 용도 |
|------|-----------|------|
| `fetchRecentVideoIds()` | activities.list (1 unit) | 최근 영상 ID 목록 조회 |
| `fetchVideoInfo()` | videos.list (1 unit) | 단일 영상 상세 정보 |
| `fetchAllVideos()` | playlistItems.list + videos.list | 전체 영상 초기 동기화 |
| `getChannelByHandle()` | channels.list (1 unit) | 핸들로 채널 조회 |
| `getChannelInfo()` | channels.list (1 unit) | 채널 정보 (배너 등) |

---

## 활동 로그 시스템

관리자/봇의 모든 활동을 `logs` 테이블에 기록하고 관리자 페이지에서 조회.

### 로그 기록 방법

```js
import { logActivity } from '../utils/log.js';

// fire-and-forget: 로그 실패가 비즈니스 로직에 영향 주지 않음
logActivity(db, {
  actor: 'admin',              // "admin" 또는 봇 ID ("youtube-3", "x-1")
  action: 'create',            // create, update, delete, upload, start, stop, sync_complete, error
  category: 'album',           // album, schedule, member, bot, category, dict, concert, sync
  targetType: 'album',         // 대상 타입 (optional)
  targetId: 12,                // 대상 DB ID (optional)
  summary: '앨범 생성: 제목',   // 한 줄 요약
  details: { key: 'value' },   // 추가 정보 JSON (optional)
});
```

### 새 기능 추가 시
로그는 자동 수집이 아니므로, 새로운 라우트나 기능을 추가할 때 `logActivity` 호출을 직접 넣어야 합니다.

### 로그 대상
- **관리자 라우트**: 앨범/일정/멤버/봇/카테고리/사전/콘서트 CRUD
- **봇 스케줄러**: 동기화 완료(addedCount > 0), 동기화 에러
- **봇 서비스**: YouTube 영상 추가, X 트윗 추가

---

## 유용한 명령어

```bash
# 컨테이너 상태 확인
docker compose ps

# 완전 재시작
docker compose down && docker compose up -d --build

# Meilisearch 동기화
curl -X POST https://fromis9.caadiq.co.kr/api/schedules/sync-search \
  -H "Authorization: Bearer <token>"

# Redis 확인 (SCAN 사용 권장)
docker exec fromis9-redis redis-cli SCAN 0 MATCH "*" COUNT 100
```
