# CLAUDE.md — abcvideo 프로젝트

이 파일은 Claude Code가 abcvideo 프로젝트에서 작업할 때 참조하는 가이드입니다.

---

## 응답/소통 규칙 (항상 적용)

- **모든 답변은 한글로 작성한다.** (코드·로그·식별자 등 원문 유지가 필요한 부분은 예외)
- 보고는 결론 → 근거 → 다음 행동 순으로 간결하게.

---

## 작업 완료 시 필수 — 히스토리 기록

작업(대화 세션)이 끝날 때 반드시 아래 형식으로 히스토리 파일을 작성한다.

```
파일 경로: docs/history/YYYY-MM-DD_HHMM_{작업명}.md
```

필수 항목 (누락 시 Stop 훅이 응답을 차단함):

```markdown
**소요 시간**: X분
**Context 사용량**: 작업분 input Xk / output Xk tokens · 누적 input Xk / output Xk tokens
```

- 날짜는 오늘 날짜(YYYY-MM-DD), 시간은 작업 완료 시각 24시간제(HHMM), 작업명은 작업 내용을 한글 또는 영문으로 간략히
- 같은 날 여러 작업이면 각각 별도 파일 (예: `2026-04-01_1430_StationOverlay-분석.md`)
- 소요 시간: 해당 작업(요청 접수 → 완료)의 실제 경과 시간
- Context 사용량: **작업분(그 작업을 처리하며 사용된 토큰)과 누적(대화 전체) 둘 다 기록**
  (정확한 값을 모르면 추정값 기재)

---

## 에이전트 협업 규칙

### 필수 파일

| 파일 | 용도 | 누가 관리 |
|------|------|----------|
| `CLAUDE.md` | 프로젝트 가이드 (아키텍처, 규칙, 기술 스택) | 사용자/아키텍트 |
| `PLAN.md` | 전체 구현 계획 + 각 단계별 세부 태스크 | 계획 수립 시 작성, 에이전트가 필요 시 업데이트 |
| `PROGRESS.md` | 각 단계의 진행 상태 + 완료/미완료/이슈 기록 | 작업하는 에이전트가 실시간 업데이트 |

### 에이전트 작업 시작 프로토콜

모든 에이전트는 작업 시작 시 반드시 아래 순서를 따른다:

```
1. CLAUDE.md 읽기    → 프로젝트 규칙/아키텍처 파악
2. PLAN.md 읽기      → 전체 계획에서 자신이 담당할 단계 확인
3. PROGRESS.md 읽기  → 이전 단계 완료 여부, 현재 상태, 알려진 이슈 확인
4. 작업 시작 전 PROGRESS.md에 자신의 단계를 "🔄 진행 중"으로 업데이트
5. 작업 완료 후 PROGRESS.md에 결과 기록 (완료 항목, 남은 이슈, 다음 단계 참고사항)
```

### PLAN.md 구조

```markdown
# PLAN.md
## 단계 N: 단계 이름
### 목표
### 세부 태스크
- [ ] 태스크 1
- [ ] 태스크 2
### 산출물 (완료 기준)
### 다음 단계 의존성
```

- 각 단계는 독립적으로 실행 가능한 단위로 분할
- 태스크는 체크박스(`- [ ]` / `- [x]`)로 관리
- 완료 기준(산출물)이 명확해야 다음 에이전트가 판단 가능

### PROGRESS.md 구조

```markdown
# PROGRESS.md
## 현재 상태 요약
- 마지막 완료 단계: N
- 현재 진행 단계: N+1
- 블로커: (있으면 기록)

## 단계별 진행 기록
### 단계 N: 단계 이름
- 상태: ✅ 완료 | 🔄 진행 중 | ⏳ 대기 | ❌ 실패
- 완료일: YYYY-MM-DD
- 완료 항목:
  - 항목 1
  - 항목 2
- 미완료/이슈:
  - 이슈 설명
- 다음 단계 참고:
  - 후속 에이전트가 알아야 할 사항
```

- 에이전트는 작업 중 **의미 있는 마일스톤마다** PROGRESS.md를 업데이트
- 실패 시 원인과 시도한 방법을 기록하여 다음 에이전트가 같은 실수를 반복하지 않도록 함
- 이전 에이전트가 남긴 "다음 단계 참고"를 반드시 확인 후 작업 시작

### 에이전트 간 인계 규칙

1. **이전 단계 산출물 검증**: 코드가 빌드/실행 가능한 상태인지 확인 후 작업 시작
2. **기존 코드 존중**: 이전 에이전트가 작성한 코드를 이유 없이 대폭 변경하지 않음
3. **충돌 방지**: 같은 파일을 동시에 수정하는 병렬 에이전트 실행 금지
4. **이슈 에스컬레이션**: 블로커 발견 시 PROGRESS.md에 기록하고 사용자에게 보고

---

## 프로젝트 개요

웹 브라우저(PC)에서 **2GB 이상의 대용량 동영상**을 안정적으로 재생하는 특화 기능 탑재 플레이어.
단순 재생을 넘어 **프레임 추출, 단위 이동, 텍스트 오버레이** 등 영상 분석/편집 보조 도구로 기능.

### 핵심 요구사항

1. 2GB 이상 동영상 안정 재생 (서버 파일 + 로컬 파일 모두)
2. 프레임 단위 추출 (정확도 우선)
3. 입력 단위 기준 앞뒤 이동 (프레임 / 초 / 장면)
4. 영상 위 텍스트 입력 (자막형 + 메모형 병행 지원)
5. 확장 가능한 플러그인 아키텍처

### 설계 결정 (2026-03-24 확정)

| 항목 | 결정 | 비고 |
|------|------|------|
| 사용자 모델 | **단일 사용자 도구** | 인증/세션/큐 불필요, 구현 단순화 |
| 로컬 파일 | **업로드 없이 직접 재생** | File API → `URL.createObjectURL()` |
| 프레임 정확도 | **표준 수준** | CFR: `1/fps` 산술, VFR: best-effort |
| HLS 변환 전 재생 | **원본 즉시 재생** | Range Request로 바로 재생, HLS는 백그라운드 변환 |
| 브라우저 지원 | **PC 웹 우선** | Chrome/Firefox/Edge, iOS Safari 무시 |

---

## 아키텍처 결정 (ADR)

### 재생 이중 경로

```
서버 파일:  GET /api/stream/:videoId (Range Request) → 즉시 재생
            POST /api/hls/:videoId/convert → 백그라운드 HLS 변환
            HLS 준비 완료 시 → hls.js로 전환 (seek 안정성 향상)

로컬 파일:  File API → URL.createObjectURL() → <video src> 직접 재생
            프레임 추출 필요 시 → 서버 업로드 또는 Canvas 폴백
```

- 로컬 파일은 HLS 변환 없이 브라우저가 직접 재생
- 서버 파일은 Range Request로 즉시 재생 후, HLS 변환 완료 시 전환
- 2GB 이상 로컬 파일은 `URL.createObjectURL()`이 메모리 효율적 (Blob URL은 참조만 유지)

### 스트리밍 방식: HLS

- Progressive Download는 2GB+ seek 불안정으로 제외
- HLS 세그먼트 분할로 파일 크기 제한 완전 우회
- **세그먼트 크기: 6초** (분석 도구 특성상 seek 반응성 우선)
- **키프레임 간격: 2초** (`-force_key_frames "expr:gte(t,n_forced*2)"`)
- hls.js v1.6.15로 Chrome/Firefox/Edge 지원 (iOS Safari 네이티브는 비대상)

### hls.js 필수 설정

```javascript
const hls = new Hls({
  maxBufferLength: 30,              // 전방 버퍼: 30초
  maxMaxBufferLength: 600,          // 최대 10분
  maxBufferSize: 60 * 1024 * 1024,  // 60MB
  backBufferLength: 30,             // *** 필수: 재생 완료 버퍼 30초 후 해제 ***
                                    // (기본값 Infinity → 장시간 재생 시 메모리 누수)
  enableWorker: true,               // Web Worker 트랜스먹싱
});
```

> `backBufferLength: 30` 미설정 시 2GB 영상 30분 재생 후 브라우저 크래시 위험.

### 플레이어: Video.js 8.23.x

- 플러그인 200개+ 생태계 → 확장성 우선
- `@videojs/http-streaming` 내장으로 HLS 기본 지원
- 전체화면 시 오버레이 유지를 위해 **컨테이너 전체를 fullscreen**으로 처리
- Video.js v10 (2026 중반 GA 예정)으로 향후 마이그레이션 가능하도록 플레이어 로직을 훅/서비스 레이어로 분리

### 프레임 추출: 서버 FFmpeg (child_process.spawn)

- Canvas API는 키프레임 의존으로 정확도 불충분
- ffmpeg.wasm은 ~30MB 로딩 + iOS 미지원 + VFR 불안정
- 서버 FFmpeg + `-accurate_seek` 플래그로 정확한 프레임 추출
- **fluent-ffmpeg 아카이브됨 (2025-05)** → `child_process.spawn()` + 얇은 TypeScript 래퍼 사용
- 로컬 파일 프레임 추출: Canvas API 즉시 미리보기 + 정확 추출 시 서버 업로드

### 백엔드: Node.js + Express

- 서버 영상 스트리밍 (Range Request)
- HLS 변환 API (FFmpeg child_process.spawn)
- 프레임 추출 API
- 대용량 업로드 (**@tus/server v2.3.0** — 자동 재개/재시도)
- 주석/메모 저장 (**better-sqlite3 v12.8.0**)
- FFmpeg 변환 진행률: **SSE (Server-Sent Events)**

---

## 기술 스택

### 프론트엔드

```
React 18 + TypeScript
Video.js 8.23.x + @videojs/http-streaming (HLS 내장)
hls.js 1.6.x (Video.js VHS 보완/대체 가능)
Tailwind CSS
Vite
Zustand (상태 관리)
interact.js (메모형 오버레이 드래그/리사이즈)
subsrt (자막 포맷 변환: SRT, VTT, ASS 등)
```

### 백엔드

```
Node.js 20+ LTS + Express
child_process.spawn (FFmpeg 래퍼 — fluent-ffmpeg 대체)
@tus/server 2.3.x + @tus/file-store (대용량 업로드)
better-sqlite3 12.8.x (주석 저장)
check-disk-space 3.4.x (디스크 모니터링)
```

### 인프라/개발 환경

```
FFmpeg 6.x+ (시스템 설치 필요 — PATH 등록)
Node.js 20+ LTS
```

---

## 프로젝트 구조

```
abcvideo/
├── client/                    # React 프론트엔드
│   ├── src/
│   │   ├── components/
│   │   │   ├── player/        # Video.js 플레이어 컴포넌트
│   │   │   ├── overlay/       # 텍스트 오버레이 (자막형/메모형)
│   │   │   ├── controls/      # 커스텀 컨트롤 (단위 이동, 속도 등)
│   │   │   └── toolbar/       # 프레임 추출, 주석 관리 툴바
│   │   ├── hooks/
│   │   │   ├── useVideoPlayer.ts   # Video.js 초기화/제어
│   │   │   ├── useFrameStep.ts     # 프레임 단위 이동
│   │   │   ├── useAnnotations.ts   # 주석 CRUD
│   │   │   └── useHls.ts           # HLS 로드/에러 처리
│   │   ├── store/             # Zustand 스토어
│   │   ├── types/             # 공유 타입 정의
│   │   ├── utils/
│   │   │   ├── timecode.ts    # 타임코드 변환 유틸
│   │   │   ├── vtt.ts         # WebVTT 생성/파싱
│   │   │   └── frameCapture.ts # Canvas 프레임 캡처 (로컬 파일용)
│   │   └── App.tsx
│   ├── index.html
│   ├── vite.config.ts
│   └── tsconfig.json
│
├── server/                    # Node.js 백엔드
│   ├── src/
│   │   ├── routes/
│   │   │   ├── stream.ts      # Range Request 스트리밍
│   │   │   ├── hls.ts         # HLS 변환/서빙
│   │   │   ├── frame.ts       # 프레임 추출 API
│   │   │   ├── upload.ts      # tus 업로드 연동
│   │   │   ├── local.ts       # 로컬 파일 프록시 (선택)
│   │   │   └── annotations.ts # 주석 CRUD API
│   │   ├── services/
│   │   │   ├── ffmpeg.ts      # FFmpeg spawn 래퍼 (HLS 변환, 프레임 추출)
│   │   │   ├── streaming.ts   # Range Request 로직
│   │   │   └── storage.ts     # 파일/DB 접근
│   │   ├── middleware/
│   │   │   ├── security.ts    # Path traversal 방어, CORS
│   │   │   └── upload.ts      # tus 서버 설정
│   │   ├── db/
│   │   │   └── schema.sql     # SQLite 스키마
│   │   └── app.ts
│   └── tsconfig.json
│
├── shared/                    # 클라이언트/서버 공유 타입
│   └── types.ts
│
├── storage/                   # 런타임 생성 (gitignore)
│   ├── videos/                # 업로드된 원본 영상
│   ├── hls/                   # HLS 세그먼트 (.m3u8, .ts)
│   ├── frames/                # 추출된 프레임 이미지
│   └── thumbnails/            # 탐색바 썸네일 스프라이트
│
├── CLAUDE.md                  # 프로젝트 가이드 (아키텍처, 규칙)
├── PLAN.md                    # 전체 구현 계획 + 단계별 세부 태스크
├── PROGRESS.md                # 진행 상태 추적 (에이전트 실시간 업데이트)
├── README.md
└── package.json               # 루트 (워크스페이스)
```

---

## API 설계

### 스트리밍

```
GET  /api/stream/:videoId          # Range Request 스트리밍 (원본 즉시 재생)
GET  /api/hls/:videoId/index.m3u8  # HLS 플레이리스트
GET  /api/hls/:videoId/:segment    # HLS 세그먼트 (.ts)
POST /api/hls/:videoId/convert     # HLS 변환 시작 (비동기)
GET  /api/hls/:videoId/progress    # 변환 진행 상태 (SSE)
```

### 프레임 추출

```
GET  /api/frame/:videoId?time=00:01:30.000   # 특정 시간 프레임
GET  /api/frame/:videoId?frame=1234          # 프레임 번호로 추출
GET  /api/thumbnails/:videoId/sprite.jpg     # 탐색바 썸네일 스프라이트
GET  /api/thumbnails/:videoId/sprite.vtt     # 썸네일 위치 VTT
```

### 업로드 (tus 프로토콜)

```
# tus 엔드포인트 — @tus/server가 자동 처리
POST   /api/upload                 # 업로드 생성 (tus Creation)
PATCH  /api/upload/:uploadId       # 청크 전송 (tus 자동 재개)
HEAD   /api/upload/:uploadId       # 업로드 상태 조회
DELETE /api/upload/:uploadId       # 업로드 취소
```

### 주석/메모

```
GET    /api/annotations/:videoId        # 영상별 주석 목록
POST   /api/annotations/:videoId        # 주석 추가
PUT    /api/annotations/:videoId/:id    # 주석 수정
DELETE /api/annotations/:videoId/:id    # 주석 삭제
GET    /api/annotations/:videoId/export?format=vtt|srt|json|csv  # 내보내기
```

### 메타데이터

```
GET  /api/meta/:videoId            # 영상 메타데이터 (fps, duration, resolution 등)
GET  /api/videos                   # 업로드된 영상 목록
DELETE /api/videos/:videoId        # 영상 삭제 (원본 + HLS + 프레임 + 썸네일 일괄)
```

---

## 핵심 구현 규칙

### 대용량 파일 처리

- Range Request: `fs.createReadStream({ start, end, highWaterMark: 1024 * 1024 })` 사용
- 응답 시 반드시 `Accept-Ranges: bytes` 헤더 포함 (Safari seek 호환)
- `stream/promises`의 `pipeline()` 사용 (에러 핸들링 + 백프레셔 자동 처리)
- 4GB 이상 파일: JavaScript `Number`는 `2^53-1`까지 안전 → BigInt 불필요
- HLS 변환은 원본 파일을 직접 수정하지 않고 `storage/hls/{videoId}/`에 출력
- 소스가 이미 H.264인 경우 `-c copy`로 재인코딩 없이 세그먼트 분할만 수행 (수 초 내 완료)

### Range Request 구현 패턴

```typescript
// 핵심: 한 응답에 전체 파일을 보내지 않고 청크 크기 제한
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
const start = requestedStart;
const end = Math.min(requestedEnd || (start + CHUNK_SIZE - 1), fileSize - 1);

res.writeHead(206, {
  'Content-Range': `bytes ${start}-${end}/${fileSize}`,
  'Accept-Ranges': 'bytes',
  'Content-Length': end - start + 1,
  'Content-Type': 'video/mp4',
});
```

### HLS 변환 FFmpeg 명령

```bash
# H.264 소스 → 재인코딩 없이 세그먼트 분할 (빠름)
ffmpeg -i input.mp4 -c copy \
  -f hls -hls_time 6 -hls_list_size 0 \
  -hls_segment_filename 'output/segment%03d.ts' \
  -hls_playlist_type vod \
  output/index.m3u8

# 비-H.264 소스 → 트랜스코딩 (느림)
ffmpeg -i input.mkv \
  -c:v libx264 -preset medium -crf 23 -profile:v high -level 4.1 \
  -c:a aac -b:a 128k -ar 48000 \
  -f hls -hls_time 6 -hls_list_size 0 \
  -hls_segment_filename 'output/segment%03d.ts' \
  -hls_playlist_type vod \
  -force_key_frames "expr:gte(t,n_forced*2)" \
  output/index.m3u8
```

### FFmpeg spawn 래퍼 패턴

```typescript
import { spawn } from 'child_process';

function runFFmpeg(args: string[]): Promise<void> {
  return new Promise((resolve, reject) => {
    const proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
    let stderr = '';
    proc.stderr.on('data', (chunk) => {
      stderr += chunk.toString();
      // 진행률 파싱: "time=00:01:30.00" 패턴
    });
    proc.on('close', (code) =>
      code === 0 ? resolve() : reject(new Error(`FFmpeg exit ${code}: ${stderr.slice(-500)}`))
    );
  });
}
```

### 프레임 정확도

- `requestVideoFrameCallback` API로 실제 FPS 동적 감지 (fallback: 30fps)
- 프레임 번호 → 시간 변환: `frameNumber / fps` (currentTime 누적 연산 금지 — 부동소수점 드리프트)
- 서버 FFmpeg 프레임 추출: `-accurate_seek -ss {time} -i {file} -frames:v 1` 순서 준수
- VFR 영상은 best-effort (FFprobe 메타데이터 기반 평균 FPS 사용)

### 타임코드 표현

- 내부 저장: **초 단위 float** (예: `90.033`)
- 표시: `HH:MM:SS.mmm` 형식
- VTT 내보내기: `HH:MM:SS.mmm --> HH:MM:SS.mmm` (점 사용)
- SRT 내보내기: `HH:MM:SS,mmm --> HH:MM:SS,mmm` (콤마 사용)

### 보안

- 서버 파일 경로: `path.resolve()` 정규화 후 허용 디렉토리(`storage/videos/`) 내부 검증 (Path Traversal 방어)
- 업로드 파일 검증: MIME 타입 체크 + **FFprobe로 실제 코덱/컨테이너 확인**
- 허용 MIME: `video/mp4`, `video/quicktime`, `video/x-matroska`, `video/webm`
- 업로드 크기 상한: 환경변수 `MAX_UPLOAD_SIZE` (기본 20GB)
- CORS 설정: 개발 시 `localhost:5173` ↔ `localhost:3001` 허용
- `Cross-Origin-Opener-Policy` / `Cross-Origin-Embedder-Policy` 헤더 (SharedArrayBuffer 대비)

### 전체화면

- `document.fullscreenElement`가 `<video>`가 아닌 **플레이어 컨테이너 div**여야 오버레이 유지
- Video.js `fluid: true` 모드로 컨테이너 기준 비율 유지
- Safari webkit 접두사: `webkitRequestFullscreen` 분기 불필요 (PC 웹 대상)

### 오버레이 성능

- `timeupdate` 이벤트에서 직접 DOM 조작 금지 (~250ms 간격, 부정확)
- **`requestVideoFrameCallback`** 또는 `requestAnimationFrame` 루프로 현재 시간 읽고 표시할 주석 필터링
- 주석 배열은 `time` 기준 정렬 유지 → **이진 탐색 O(log n)** 조회
- 오버레이 요소에 `will-change: transform` 적용 (GPU 가속)
- 동시 표시 오버레이 50개 미만 유지 (DOM 레이아웃 스래싱 방지)

### 메모리 관리

- `URL.createObjectURL()` 사용 후 반드시 `URL.revokeObjectURL()` 호출
- Canvas 프레임 캡처 시 단일 Canvas 재사용 (매번 새로 생성 금지)
- hls.js `backBufferLength: 30` 필수 설정
- QuotaExceededError 발생 시: `abort()` → `remove(0, currentTime - 30)` → 재시도

---

## 자막형 vs 메모형 동작 정의

### 자막형 (Subtitle Mode)

- WebVTT `<track kind="subtitles">` 요소 사용
- 시작 시간 ~ 종료 시간 범위 표시
- 위치: 하단 고정 (WebVTT `position`, `line` 속성으로 조정 가능)
- 내보내기: `.vtt`, `.srt`

### 메모형 (Memo Mode)

- 커스텀 DOM 오버레이 레이어 + **interact.js** 드래그/리사이즈
- 특정 시점(포인트) + 표시 지속 시간 (기본 3초)
- 위치: 드래그로 자유 배치 (**x%, y% 정규화 좌표** — 해상도 독립적)
- 크기: width%, height% 정규화 (리사이즈 가능)
- 내보내기: `.json`, `.csv`

### 주석 데이터 모델 (SQLite)

```json
{
  "id": "uuid",
  "videoId": "string",
  "type": "subtitle | memo",
  "timeStart": 90.033,
  "timeEnd": 95.500,
  "position": { "x": 50.0, "y": 75.0 },
  "size": { "width": 30.0, "height": 10.0 },
  "text": "주석 내용",
  "style": {
    "fontSize": 16,
    "color": "#ffffff",
    "backgroundColor": "rgba(0,0,0,0.7)"
  },
  "createdAt": "ISO-8601",
  "updatedAt": "ISO-8601"
}
```

---

## 키보드 단축키

| 키 | 동작 | 비고 |
|----|------|------|
| `Space` | 재생 / 일시정지 | 플레이어 포커스 시에만 |
| `←` / `→` | 5초 뒤로 / 앞으로 | |
| `J` / `L` | 10초 뒤로 / 앞으로 | |
| `,` / `.` | 이전 프레임 / 다음 프레임 | 일시정지 상태에서만 |
| `[` / `]` | 이전 장면 / 다음 장면 | |
| `F` | 전체화면 토글 | |
| `M` | 음소거 토글 | |
| `0`~`9` | 10% 단위 탐색 | |
| `Shift + S` | 현재 프레임 캡처 | |
| `Shift + M` | 현재 시간에 메모 추가 | |
| `+` / `-` | 재생 속도 증가 / 감소 | |

> 텍스트 입력 모드(메모 작성 중)에서는 모든 플레이어 단축키 비활성화.

---

## Video.js React 통합 패턴

```tsx
// ref + useEffect 패턴 (Video.js 공식 권장)
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player | null>(null);

useEffect(() => {
  if (!playerRef.current) {
    const videoElement = document.createElement('video-js');
    videoElement.classList.add('vjs-big-play-centered');
    videoRef.current!.appendChild(videoElement);
    playerRef.current = videojs(videoElement, options, onReady);
  }
}, [options]);

useEffect(() => {
  return () => {
    if (playerRef.current && !playerRef.current.isDisposed()) {
      playerRef.current.dispose();
      playerRef.current = null;
    }
  };
}, []);

return <div data-vjs-player ref={videoRef} />;
```

> `data-vjs-player` 래퍼로 Video.js 추가 DOM 래핑 방지.
> React 18 Strict Mode에서 `playerRef.current` 가드 필수 (이중 초기화 방지).

---

## 환경변수 (server/.env)

```env
PORT=3001
VIDEOS_DIR=../storage/videos
HLS_DIR=../storage/hls
FRAMES_DIR=../storage/frames
THUMBNAILS_DIR=../storage/thumbnails
DB_PATH=../storage/annotations.db
MAX_UPLOAD_SIZE=21474836480    # 20GB (bytes)
FFMPEG_PATH=ffmpeg             # 시스템 PATH에 등록된 경우 기본값
HLS_SEGMENT_TIME=6             # HLS 세그먼트 길이 (초)
THUMBNAIL_INTERVAL=10          # 썸네일 추출 간격 (초)
```

---

## 개발 단계 (에이전트 계획)

> 세부 태스크와 완료 기준은 `PLAN.md` 참조. 진행 상태는 `PROGRESS.md` 참조.

| 단계 | 내용 | 의존성 |
|------|------|--------|
| 1. 프로젝트 구조 | 모노레포 세팅, TypeScript 설정, 폴더 골격, 패키지 설치 | — |
| 2. 백엔드 서버 | Express + Range Request 스트리밍 + FFmpeg spawn 래퍼 + tus 업로드 + HLS 변환 + SSE 진행률 | FFmpeg 설치 |
| 3. 기본 플레이어 | Video.js + 이중 경로 재생 (로컬 File API + 서버 HLS) + 기본 UI | 단계 2 |
| 4. 프레임 추출 | Canvas 즉시 미리보기 + 서버 FFmpeg API (정확 추출) | 단계 2, 3 |
| 5. 단위 이동 | 프레임/초/장면 seek + requestVideoFrameCallback FPS 감지 + 키보드 단축키 | 단계 3, 4 |
| 6. 텍스트 오버레이 | 자막형(WebVTT track) + 메모형(interact.js DOM 오버레이) + 내보내기(subsrt) | 단계 3 |
| 7. UI/UX 통합 | 전체 통합, 반응형 레이아웃, 썸네일 미리보기, 디스크 정리 | 단계 2~6 |

```
단계 1 → 단계 2 → 단계 4 ──┐
   └──→ 단계 3 → 단계 5 ──┤→ 단계 7
                 → 단계 6 ──┘
※ 단계 5와 6은 병렬 진행 가능
```

---

## 파일 정리 정책

- 업로드 실패/중단 임시 파일: **24시간 후 자동 삭제** (서버 시작 시 + 주기적 스캔)
- 영상 삭제 시: 원본 + HLS 세그먼트 + 프레임 + 썸네일 + 주석 DB 레코드 일괄 삭제
- 디스크 잔여 공간 **10GB 미만 시 경고** (check-disk-space)
- 영상 1개당 예상 디스크 사용량: 원본 + HLS ≈ 원본 x 2 + 프레임/썸네일 ~50MB

---

## 알려진 제약 / 주의사항

- **VFR 영상**: 가변 프레임율 영상은 프레임 번호 계산 부정확 → FFprobe 평균 FPS 사용 (best-effort)
- **CORS + Canvas**: 크로스 오리진 영상에서 `toDataURL()` 보안 오류 → `crossorigin="anonymous"` 필수
- **HLS 변환 시간**: 2GB 영상 트랜스코딩 시 수 분~십수 분 소요 → SSE 진행률로 클라이언트 통보
- **H.264 소스 감지**: FFprobe로 코덱 확인 후 `-c copy` (재인코딩 불필요) vs 트랜스코딩 분기
- **WebVTT 리전**: Chrome/Edge에서 미지원 → 리전 기능 사용 금지, 기본 positioning만 사용
- **Video.js v10**: 2026 중반 GA 예정, 플러그인 호환 불확실 → 플레이어 로직을 훅으로 분리하여 마이그레이션 비용 최소화
