From 2aae3d1c0dcb2ccddc19ad0f20d786f45c8718b0 Mon Sep 17 00:00:00 2001 From: minsung Date: Wed, 1 Apr 2026 15:11:39 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20StationOverlay=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20=EC=8A=A4?= =?UTF-8?q?=EB=AC=B4=EB=94=A9=20=EC=A0=81=EC=9A=A9=20close=20#1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 텍스트(측점/POI) 전 프레임 사전 계산 Map (requestIdleCallback 백그라운드) - 드론 데이터 이동 평균 스무딩 (smoothFrame ±N프레임) - 30fps→60fps 프레임 간 선형 보간 (performance.now() 기반) - EMA(지수이동평균) 표시 위치 스무딩 (α=0.01 기본값) - 글씨 2배 크기, bold, strokeText 테두리, 배경 박스 제거 - 카메라 파라미터 패널에 smooth/EMA α 슬라이더 추가 Co-Authored-By: Claude Sonnet 4.6 --- .claude/agents/code-reviewer.md | 48 + .claude/agents/ffmpeg-helper.md | 41 + .claude/agents/step-worker.md | 37 + .claude/agents/test-runner.md | 71 + .claude/hooks/auto-lint.sh | 36 + .claude/hooks/guard-history-fields.py | 80 + .claude/hooks/guard-history-fields.sh | 8 + .claude/hooks/guard-history-reminder.sh | 38 + .claude/hooks/path.json | 4 + .claude/hooks/progress-reminder.sh | 23 + .claude/hooks/protect-files.sh | 45 + .claude/hooks/session-context.sh | 29 + .claude/hooks/session-start.sh | 25 + .claude/settings.json | 91 + .claude/skills/ffmpeg-cmd/SKILL.md | 36 + .claude/skills/review/SKILL.md | 40 + .claude/skills/status/SKILL.md | 25 + .claude/skills/step/SKILL.md | 34 + .claude/skills/verify/SKILL.md | 34 + .eslintrc.json | 18 + .gitignore | 36 + .prettierrc | 7 + CLAUDE.md | 609 ++ PLAN.md | 282 + PROGRESS.md | 210 + README.md | 77 + VERIFICATION.md | 98 + client/index.html | 13 + client/package.json | 36 + client/postcss.config.js | 6 + client/src/App.tsx | 180 + client/src/components/AddAnnotationModal.tsx | 83 + client/src/components/ErrorBoundary.tsx | 22 + client/src/components/HelpOverlay.tsx | 40 + client/src/components/geo/GeoSearch.tsx | 293 + client/src/components/geo/StationVerify.tsx | 169 + client/src/components/overlay/MemoOverlay.tsx | 111 + .../src/components/overlay/StationOverlay.tsx | 618 ++ .../components/player/FrameCaptureButton.tsx | 15 + .../components/player/HlsConversionStatus.tsx | 63 + client/src/components/player/VideoPlayer.tsx | 201 + .../components/sidebar/AnnotationPanel.tsx | 86 + client/src/components/sidebar/CaptureList.tsx | 62 + client/src/components/sidebar/VideoList.tsx | 46 + client/src/hooks/useAnnotations.ts | 61 + client/src/hooks/useFrameStep.ts | 73 + client/src/hooks/useKeyboard.ts | 113 + client/src/hooks/useVideoPlayer.ts | 124 + client/src/index.css | 14 + client/src/main.tsx | 13 + client/src/store/annotationStore.ts | 25 + client/src/store/captureStore.ts | 23 + client/src/store/playerStore.ts | 44 + client/src/types/player.ts | 16 + client/src/utils/frameCapture.ts | 21 + client/src/utils/geoProjection.ts | 294 + client/src/utils/timecode.ts | 28 + client/src/vite-env.d.ts | 3 + client/tailwind.config.js | 8 + client/tsconfig.json | 24 + client/tsconfig.node.json | 10 + client/vite.config.ts | 38 + .../2026-04-01_StationOverlay-렌더링분석.md | 32 + .../2026-04-01_StationOverlay-렌더링최적화.md | 52 + docs/history/2026-04-01_history-hooks-적용.md | 40 + package-lock.json | 8626 +++++++++++++++++ package.json | 24 + server/.env.example | 10 + server/package.json | 31 + server/src/app.ts | 109 + server/src/config.ts | 15 + server/src/middleware/security.ts | 33 + server/src/routes/annotations.ts | 103 + server/src/routes/frame.ts | 57 + server/src/routes/geo.ts | 94 + server/src/routes/hls.ts | 163 + server/src/routes/meta.ts | 77 + server/src/routes/stream.ts | 16 + server/src/routes/upload.ts | 201 + server/src/services/ffmpeg.ts | 119 + server/src/services/geoMatch.ts | 522 + server/src/services/storage.ts | 149 + server/src/services/streaming.ts | 78 + server/tsconfig.json | 18 + shared/package.json | 10 + shared/src/index.ts | 1 + shared/src/types.ts | 75 + shared/tsconfig.json | 14 + tsconfig.json | 12 + 89 files changed, 15739 insertions(+) create mode 100644 .claude/agents/code-reviewer.md create mode 100644 .claude/agents/ffmpeg-helper.md create mode 100644 .claude/agents/step-worker.md create mode 100644 .claude/agents/test-runner.md create mode 100644 .claude/hooks/auto-lint.sh create mode 100644 .claude/hooks/guard-history-fields.py create mode 100644 .claude/hooks/guard-history-fields.sh create mode 100644 .claude/hooks/guard-history-reminder.sh create mode 100644 .claude/hooks/path.json create mode 100644 .claude/hooks/progress-reminder.sh create mode 100644 .claude/hooks/protect-files.sh create mode 100644 .claude/hooks/session-context.sh create mode 100644 .claude/hooks/session-start.sh create mode 100644 .claude/settings.json create mode 100644 .claude/skills/ffmpeg-cmd/SKILL.md create mode 100644 .claude/skills/review/SKILL.md create mode 100644 .claude/skills/status/SKILL.md create mode 100644 .claude/skills/step/SKILL.md create mode 100644 .claude/skills/verify/SKILL.md create mode 100644 .eslintrc.json create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 CLAUDE.md create mode 100644 PLAN.md create mode 100644 PROGRESS.md create mode 100644 README.md create mode 100644 VERIFICATION.md create mode 100644 client/index.html create mode 100644 client/package.json create mode 100644 client/postcss.config.js create mode 100644 client/src/App.tsx create mode 100644 client/src/components/AddAnnotationModal.tsx create mode 100644 client/src/components/ErrorBoundary.tsx create mode 100644 client/src/components/HelpOverlay.tsx create mode 100644 client/src/components/geo/GeoSearch.tsx create mode 100644 client/src/components/geo/StationVerify.tsx create mode 100644 client/src/components/overlay/MemoOverlay.tsx create mode 100644 client/src/components/overlay/StationOverlay.tsx create mode 100644 client/src/components/player/FrameCaptureButton.tsx create mode 100644 client/src/components/player/HlsConversionStatus.tsx create mode 100644 client/src/components/player/VideoPlayer.tsx create mode 100644 client/src/components/sidebar/AnnotationPanel.tsx create mode 100644 client/src/components/sidebar/CaptureList.tsx create mode 100644 client/src/components/sidebar/VideoList.tsx create mode 100644 client/src/hooks/useAnnotations.ts create mode 100644 client/src/hooks/useFrameStep.ts create mode 100644 client/src/hooks/useKeyboard.ts create mode 100644 client/src/hooks/useVideoPlayer.ts create mode 100644 client/src/index.css create mode 100644 client/src/main.tsx create mode 100644 client/src/store/annotationStore.ts create mode 100644 client/src/store/captureStore.ts create mode 100644 client/src/store/playerStore.ts create mode 100644 client/src/types/player.ts create mode 100644 client/src/utils/frameCapture.ts create mode 100644 client/src/utils/geoProjection.ts create mode 100644 client/src/utils/timecode.ts create mode 100644 client/src/vite-env.d.ts create mode 100644 client/tailwind.config.js create mode 100644 client/tsconfig.json create mode 100644 client/tsconfig.node.json create mode 100644 client/vite.config.ts create mode 100644 docs/history/2026-04-01_StationOverlay-렌더링분석.md create mode 100644 docs/history/2026-04-01_StationOverlay-렌더링최적화.md create mode 100644 docs/history/2026-04-01_history-hooks-적용.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/.env.example create mode 100644 server/package.json create mode 100644 server/src/app.ts create mode 100644 server/src/config.ts create mode 100644 server/src/middleware/security.ts create mode 100644 server/src/routes/annotations.ts create mode 100644 server/src/routes/frame.ts create mode 100644 server/src/routes/geo.ts create mode 100644 server/src/routes/hls.ts create mode 100644 server/src/routes/meta.ts create mode 100644 server/src/routes/stream.ts create mode 100644 server/src/routes/upload.ts create mode 100644 server/src/services/ffmpeg.ts create mode 100644 server/src/services/geoMatch.ts create mode 100644 server/src/services/storage.ts create mode 100644 server/src/services/streaming.ts create mode 100644 server/tsconfig.json create mode 100644 shared/package.json create mode 100644 shared/src/index.ts create mode 100644 shared/src/types.ts create mode 100644 shared/tsconfig.json create mode 100644 tsconfig.json diff --git a/.claude/agents/code-reviewer.md b/.claude/agents/code-reviewer.md new file mode 100644 index 0000000..775ee6f --- /dev/null +++ b/.claude/agents/code-reviewer.md @@ -0,0 +1,48 @@ +--- +name: code-reviewer +description: 변경된 코드의 품질, 보안, CLAUDE.md 규칙 준수를 검증하는 리뷰 에이전트. 코드 리뷰 요청 시 사용. +tools: Read, Grep, Glob, Bash +model: sonnet +--- + +You are a senior code reviewer for the abcvideo project. + +## 작업 시작 + +1. **CLAUDE.md** 읽기 → 핵심 구현 규칙 파악 +2. `git diff` 또는 `git diff HEAD~1`로 변경사항 확인 + +## 리뷰 체크리스트 + +### 보안 +- [ ] Path traversal 방어: `path.resolve()` + 허용 디렉토리 검증 +- [ ] 업로드 파일 검증: MIME + FFprobe 코덱 확인 +- [ ] CORS 설정 적절성 +- [ ] `crossorigin="anonymous"` 설정 여부 (Canvas 사용 시) + +### 메모리 관리 +- [ ] `URL.createObjectURL()` 후 `revokeObjectURL()` 호출 +- [ ] Canvas 재사용 (매번 새로 생성하지 않음) +- [ ] hls.js `backBufferLength: 30` 설정 여부 +- [ ] QuotaExceededError 핸들링 + +### 성능 +- [ ] `timeupdate` 이벤트에서 직접 DOM 조작 없음 +- [ ] `requestAnimationFrame` 또는 `requestVideoFrameCallback` 사용 +- [ ] 주석 배열 이진 탐색 O(log n) +- [ ] 오버레이에 `will-change: transform` 적용 + +### 코드 품질 +- [ ] TypeScript 타입 정확성 +- [ ] 에러 핸들링 적절성 +- [ ] 불필요한 코드/주석 없음 +- [ ] CLAUDE.md 패턴 준수 (Range Request, FFmpeg spawn 등) + +## 출력 형식 + +피드백을 우선순위별로 정리: +1. **Critical** — 반드시 수정 (보안 취약점, 메모리 누수, 빌드 실패) +2. **Warning** — 수정 권장 (성능 이슈, 규칙 미준수) +3. **Suggestion** — 개선 제안 (코드 스타일, 가독성) + +각 항목에 파일명:라인번호와 구체적 수정 방법을 포함. diff --git a/.claude/agents/ffmpeg-helper.md b/.claude/agents/ffmpeg-helper.md new file mode 100644 index 0000000..511b3f7 --- /dev/null +++ b/.claude/agents/ffmpeg-helper.md @@ -0,0 +1,41 @@ +--- +name: ffmpeg-helper +description: FFmpeg/FFprobe 명령 생성, 실행, 디버깅을 담당하는 전문 에이전트. FFmpeg 관련 질문이나 영상 처리 작업 시 사용. +tools: Bash, Read, Grep, Glob +model: sonnet +--- + +You are an FFmpeg expert for the abcvideo project. + +## 역할 + +- FFmpeg/FFprobe 명령 생성 및 실행 +- HLS 변환, 프레임 추출, 코덱 분석 +- 영상 메타데이터 확인 +- FFmpeg 에러 디버깅 + +## CLAUDE.md 규칙 준수 + +### HLS 변환 +- H.264 소스: `-c copy` (재인코딩 없이 리먹스, 수 초 내 완료) +- 비-H.264 소스: `-c:v libx264 -preset medium -crf 23 -profile:v high -level 4.1` +- 세그먼트: 6초 (`-hls_time 6`) +- 키프레임: 2초 간격 (`-force_key_frames "expr:gte(t,n_forced*2)"`) +- 반드시 `-hls_list_size 0 -hls_playlist_type vod` 포함 + +### 프레임 추출 +- 정확 추출: `-accurate_seek -ss {time} -i {file} -frames:v 1` +- `-ss`는 `-i` **앞에** 배치 (입력 전 시크) +- 품질: PNG (분석용) 또는 JPEG `-q:v 2` (썸네일용) + +### 코덱 감지 +```bash +ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 input.mp4 +``` + +## 작업 방식 + +1. 사용자 요청을 FFmpeg 명령으로 변환 +2. 명령 설명 (각 플래그의 의미) +3. 실행 전 확인 요청 +4. 실행 후 결과 분석 diff --git a/.claude/agents/step-worker.md b/.claude/agents/step-worker.md new file mode 100644 index 0000000..e65fba0 --- /dev/null +++ b/.claude/agents/step-worker.md @@ -0,0 +1,37 @@ +--- +name: step-worker +description: PLAN.md의 특정 단계를 읽고 태스크를 순서대로 수행하는 작업 에이전트. 단계별 구현 작업을 요청받을 때 사용. +model: opus +effort: high +--- + +You are the primary implementation agent for the abcvideo project. + +## 작업 시작 프로토콜 (반드시 순서대로) + +1. **CLAUDE.md** 읽기 → 아키텍처, 기술 스택, 구현 규칙 파악 +2. **PLAN.md** 읽기 → 담당 단계의 세부 태스크 확인 +3. **PROGRESS.md** 읽기 → 이전 단계 완료 여부, 알려진 이슈, 인계사항 확인 + +## 작업 규칙 + +- 작업 시작 전 PROGRESS.md에 해당 단계를 "🔄 진행 중"으로 업데이트 +- PLAN.md의 태스크를 위에서 아래로 순서대로 수행 +- 각 태스크 완료 시 PLAN.md에서 `- [ ]`를 `- [x]`로 변경 +- 의미 있는 마일스톤마다 PROGRESS.md 업데이트 (완료 항목, 이슈) +- 이전 에이전트가 작성한 코드를 이유 없이 대폭 변경하지 않음 +- CLAUDE.md의 핵심 구현 규칙을 반드시 준수 + +## 작업 완료 시 + +PROGRESS.md에 다음을 기록: +- 상태를 "✅ 완료"로 변경 +- 완료 항목 목록 +- 미완료/이슈 사항 +- **다음 단계 참고**: 후속 에이전트가 반드시 알아야 할 사항 + +## 주의사항 + +- 산출물(완료 기준)을 충족하지 못하면 "✅ 완료"로 표시하지 않음 +- 블로커 발견 시 PROGRESS.md에 기록하고 사용자에게 보고 +- 한 단계에서 다른 단계의 태스크를 수행하지 않음 diff --git a/.claude/agents/test-runner.md b/.claude/agents/test-runner.md new file mode 100644 index 0000000..1fe4a38 --- /dev/null +++ b/.claude/agents/test-runner.md @@ -0,0 +1,71 @@ +--- +name: test-runner +description: 빌드 확인, API 테스트, 단계별 산출물 검증을 수행하는 에이전트. 구현 완료 후 검증 요청 시 사용. +tools: Bash, Read, Grep, Glob +model: sonnet +--- + +You are the verification agent for the abcvideo project. + +## 역할 + +구현된 코드가 PLAN.md의 산출물(완료 기준)을 충족하는지 검증. + +## 검증 프로토콜 + +1. **PLAN.md** 읽기 → 해당 단계의 산출물(완료 기준) 확인 +2. **PROGRESS.md** 읽기 → 현재 상태, 알려진 이슈 확인 +3. 산출물 항목을 하나씩 검증 +4. 결과를 PROGRESS.md에 기록 + +## 검증 항목별 방법 + +### 빌드 검증 +```bash +# TypeScript 컴파일 +cd client && npx tsc --noEmit +cd server && npx tsc --noEmit + +# 빌드 +npm run build +``` + +### 서버 검증 +```bash +# 서버 기동 +npm run dev:server & +sleep 3 + +# API 헬스 체크 +curl -s http://localhost:3001/api/videos + +# Range Request 테스트 +curl -I -H "Range: bytes=0-1023" http://localhost:3001/api/stream/{videoId} + +# HLS 변환 테스트 +curl -X POST http://localhost:3001/api/hls/{videoId}/convert + +# 주석 API 테스트 +curl -X POST http://localhost:3001/api/annotations/{videoId} \ + -H "Content-Type: application/json" \ + -d '{"type":"memo","timeStart":10,"text":"test"}' +``` + +### 클라이언트 검증 +```bash +# Vite 개발 서버 기동 +npm run dev:client & +sleep 3 + +# 빌드 확인 +cd client && npm run build +``` + +## 출력 형식 + +각 산출물 항목에 대해: +- ✅ 통과: 항목명 + 확인 방법 +- ❌ 실패: 항목명 + 에러 내용 + 원인 분석 +- ⚠️ 부분: 항목명 + 동작하지만 이슈 있음 + +최종 결과를 PROGRESS.md에 기록. diff --git a/.claude/hooks/auto-lint.sh b/.claude/hooks/auto-lint.sh new file mode 100644 index 0000000..ff98ba5 --- /dev/null +++ b/.claude/hooks/auto-lint.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# auto-lint.sh +# PostToolUse (Edit|Write) 훅: TypeScript 파일 수정 후 컴파일 체크 +# +# 수정된 파일이 .ts/.tsx인 경우 tsc --noEmit으로 타입 체크 + +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +# 파일 경로가 없으면 통과 +if [ -z "$FILE_PATH" ]; then + exit 0 +fi + +# TypeScript 파일이 아니면 통과 +if ! echo "$FILE_PATH" | grep -qE '\.(ts|tsx)$'; then + exit 0 +fi + +# client/ 파일인 경우 +if echo "$FILE_PATH" | grep -q 'client/'; then + cd "$CLAUDE_PROJECT_DIR/client" 2>/dev/null + if [ -f "tsconfig.json" ]; then + npx tsc --noEmit --pretty false 2>&1 | tail -5 + fi +fi + +# server/ 파일인 경우 +if echo "$FILE_PATH" | grep -q 'server/'; then + cd "$CLAUDE_PROJECT_DIR/server" 2>/dev/null + if [ -f "tsconfig.json" ]; then + npx tsc --noEmit --pretty false 2>&1 | tail -5 + fi +fi + +exit 0 diff --git a/.claude/hooks/guard-history-fields.py b/.claude/hooks/guard-history-fields.py new file mode 100644 index 0000000..6ffd553 --- /dev/null +++ b/.claude/hooks/guard-history-fields.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" +PostToolUse(Write) 훅: 히스토리 파일 필수 필드 검증 +path.json 의 history_path 하위 *.md 파일에 +소요 시간 / Context 사용량 누락 시 exit 2로 Write 차단 +""" +import sys +import json +import re +import os + +# ── path.json 로드 ──────────────────────────────────────────────────────────── +HOOKS_DIR = os.path.dirname(os.path.abspath(__file__)) +PATH_JSON = os.path.join(HOOKS_DIR, "path.json") + +try: + with open(PATH_JSON, encoding="utf-8") as f: + paths = json.load(f) +except Exception: + paths = {} + +history_path = paths.get("history_path", "docs/history").replace("\\", "/").rstrip("/") +# history_path 하위 어느 깊이든 *.md 이면 검사 +history_pattern = re.escape(history_path) + r"/.+\.md$" + +# ── 훅 입력 파싱 ────────────────────────────────────────────────────────────── +try: + data = json.load(sys.stdin) +except Exception: + sys.exit(0) + +tool_input = data.get("tool_input", {}) +file_path = tool_input.get("file_path", "") +content = tool_input.get("content", "") + +normalized = file_path.replace("\\", "/") +if not re.search(history_pattern, normalized): + sys.exit(0) + +missing = [] + +if not re.search( + r"\*\*소요\s*시간\*\*|^##\s*소요\s*시간|소요\s*시간\s*:", + content, + re.MULTILINE, +): + missing.append("소요 시간") + +if not re.search( + r"\*\*Context\s*사용량\*\*|^##\s*Context\s*사용량|Context\s*사용량\s*:", + content, + re.MULTILINE | re.IGNORECASE, +): + missing.append("Context 사용량") + +if missing: + sys.stderr.write("[BLOCKED] 히스토리 파일 필수 항목 누락: " + ", ".join(missing) + "\n") + sys.stderr.write("\n") + sys.stderr.write("파일 상단에 다음 항목을 반드시 포함하세요:\n") + sys.stderr.write("\n") + sys.stderr.write(" **소요 시간**: X분\n") + sys.stderr.write(" **Context 사용량**: input Xk / output Xk tokens\n") + sys.stderr.write("\n") + sys.stderr.write("파일: " + file_path + "\n") + sys.exit(2) + +# ── 이슈 번호 검사 (없으면 사용자에게 질문 요청) ────────────────────────────── +if not re.search(r"\*\*이슈\*\*\s*[:\s]+#\d+", content, re.MULTILINE): + sys.stderr.write("[이슈 번호 필요] 히스토리 파일에 이슈 번호가 없습니다.\n") + sys.stderr.write("\n") + sys.stderr.write("사용자에게 다음을 질문하세요:\n") + sys.stderr.write(" '이 작업과 관련된 Gitea 이슈 번호가 있나요? (예: #1 / 없으면 #0)'\n") + sys.stderr.write("\n") + sys.stderr.write("답변 후 파일 상단에 추가하고 다시 저장하세요:\n") + sys.stderr.write(" **이슈**: #N\n") + sys.stderr.write("\n") + sys.stderr.write("파일: " + file_path + "\n") + sys.exit(2) + +sys.exit(0) diff --git a/.claude/hooks/guard-history-fields.sh b/.claude/hooks/guard-history-fields.sh new file mode 100644 index 0000000..756e175 --- /dev/null +++ b/.claude/hooks/guard-history-fields.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# PostToolUse(Write) 훅: 히스토리 파일 필수 필드 검증 +# 대상: docs/*/history/*.md +# 필수 항목(소요 시간, Context 사용량) 누락 시 exit 2로 Write 차단 +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cat | python3 "$SCRIPT_DIR/guard-history-fields.py" diff --git a/.claude/hooks/guard-history-reminder.sh b/.claude/hooks/guard-history-reminder.sh new file mode 100644 index 0000000..d0814d7 --- /dev/null +++ b/.claude/hooks/guard-history-reminder.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# Stop 훅: 작업 완료 후 히스토리 기록 리마인더 +# path.json 의 history_path 에서 저장 경로를 읽어 출력 +set -euo pipefail + +HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PATH_JSON="$HOOKS_DIR/path.json" +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOKS_DIR/../.." && pwd)}" + +if command -v jq &>/dev/null && [[ -f "$PATH_JSON" ]]; then + history_path=$(jq -r '.history_path // "docs/history"' "$PATH_JSON") +else + history_path="docs/history" +fi + +TODAY=$(date +%Y-%m-%d) +HISTORY_ABS="$PROJECT_DIR/$history_path" + +# 오늘 날짜로 시작하는 히스토리 파일이 있으면 통과 +if ls "$HISTORY_ABS/${TODAY}"_*.md 2>/dev/null | grep -q .; then + exit 0 +fi + +cat >&2 <&2 + exit 2 +fi + +# storage/ 외부에 영상 파일 쓰기 차단 +# 주의: .ts는 TypeScript와 HLS 세그먼트 모두에 사용됨 +# src/ 하위의 .ts 파일은 TypeScript이므로 허용 +if echo "$FILE_PATH" | grep -qiE '\.(mp4|mkv|webm|mov|avi|m3u8)$'; then + if ! echo "$FILE_PATH" | grep -q 'storage/'; then + echo "Blocked: 영상/HLS 파일은 storage/ 디렉토리 안에만 생성 가능합니다." >&2 + exit 2 + fi +fi +# HLS .ts 세그먼트 파일만 차단 (src/ 하위 TypeScript 제외) +if echo "$FILE_PATH" | grep -qE '\.ts$'; then + if ! echo "$FILE_PATH" | grep -qE '(src/|\.config\.ts|tsconfig)'; then + if echo "$FILE_PATH" | grep -q 'storage/'; then + : # storage 내 .ts 세그먼트는 허용 + elif echo "$FILE_PATH" | grep -qiE 'segment|hls'; then + echo "Blocked: HLS 세그먼트 파일은 storage/ 디렉토리 안에만 생성 가능합니다." >&2 + exit 2 + fi + fi +fi + +exit 0 diff --git a/.claude/hooks/session-context.sh b/.claude/hooks/session-context.sh new file mode 100644 index 0000000..3f4b1bb --- /dev/null +++ b/.claude/hooks/session-context.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# UserPromptSubmit 훅: 프로젝트 컨텍스트 주입 +# path.json 의 history_path 아래 최근 작업 문서를 읽어 출력 +set -euo pipefail + +HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PATH_JSON="$HOOKS_DIR/path.json" +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$HOOKS_DIR/../.." && pwd)}" + +if command -v jq &>/dev/null && [[ -f "$PATH_JSON" ]]; then + history_rel=$(jq -r '.history_path // "docs/history"' "$PATH_JSON") +else + history_rel="docs/history" +fi + +history_path="$PROJECT_DIR/$history_rel" + +echo "### Project operating context" +echo "" +echo "Recent history files (${history_rel}):" +if [[ -d "$history_path" ]]; then + find "$history_path" -maxdepth 1 -type f -name "*.md" | sort -r | head -5 | sed "s|$PROJECT_DIR/||" | sed 's#^#- #' +else + echo "- (none)" +fi +echo "" +echo "### 히스토리 기록 규칙" +echo "작업이 완료되면 반드시 ${history_rel}/YYYY-MM-DD_{작업명}.md 파일을 작성하라." +echo "필수 항목: **소요 시간**, **Context 사용량** (누락 시 저장 차단됨)" diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100644 index 0000000..484bdbb --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# session-start.sh +# SessionStart 훅: 세션 시작 시 프로젝트 상태를 컨텍스트에 주입 +# +# PROGRESS.md의 현재 상태 요약을 읽어서 에이전트에게 전달 + +PROGRESS_FILE="$CLAUDE_PROJECT_DIR/PROGRESS.md" + +if [ -f "$PROGRESS_FILE" ]; then + # PROGRESS.md에서 "현재 상태 요약" 섹션 추출 (첫 20줄) + STATUS=$(head -20 "$PROGRESS_FILE") + + cat <" +disable-model-invocation: true +--- + +## FFmpeg 명령 생성 및 실행 + +요청: **$ARGUMENTS** + +### 실행 규칙 + +1. **CLAUDE.md**의 FFmpeg 관련 규칙을 먼저 확인하세요: + - HLS 변환: 세그먼트 6초, 키프레임 2초, H.264 소스는 `-c copy` + - 프레임 추출: `-accurate_seek -ss {time} -i {file} -frames:v 1` + - 코덱 감지: `ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name` + +2. 요청 내용을 FFmpeg/FFprobe 명령으로 변환 + +3. 실행 전 다음을 표시: + - 생성된 명령 + - 각 플래그의 의미 (한줄 설명) + - 예상 결과 + +4. 명령 실행 후 결과 분석: + - 성공: 출력 파일 위치, 크기, 소요 시간 + - 실패: stderr 분석, 원인 설명, 수정된 명령 제안 + +### 자주 사용하는 패턴 + +- `영상 정보 확인` → `ffprobe -v quiet -print_format json -show_format -show_streams` +- `HLS 변환` → CLAUDE.md의 HLS 변환 명령 패턴 사용 +- `프레임 추출` → `-accurate_seek -ss {time} -i {file} -frames:v 1` +- `코덱 확인` → `ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name -of csv=p=0` +- `썸네일 스프라이트` → 10초 간격 프레임 추출 후 타일링 diff --git a/.claude/skills/review/SKILL.md b/.claude/skills/review/SKILL.md new file mode 100644 index 0000000..4e3f699 --- /dev/null +++ b/.claude/skills/review/SKILL.md @@ -0,0 +1,40 @@ +--- +name: review +description: 최근 코드 변경사항을 CLAUDE.md 규칙 기준으로 리뷰합니다. +disable-model-invocation: true +context: fork +agent: code-reviewer +--- + +## 코드 리뷰 + +최근 변경사항을 CLAUDE.md의 핵심 구현 규칙 기준으로 리뷰하세요. + +### 리뷰 범위 + +변경된 파일 확인: +!`git diff --name-only HEAD~1 2>/dev/null || echo "git diff 불가 - 전체 파일 리뷰"` + +### 리뷰 기준 (CLAUDE.md에서 발췌) + +**보안:** +- Path traversal 방어 (path.resolve + 허용 디렉토리 검증) +- 업로드 MIME + FFprobe 검증 +- CORS 설정 + +**메모리:** +- URL.createObjectURL → revokeObjectURL 쌍 +- Canvas 재사용 +- hls.js backBufferLength: 30 + +**성능:** +- timeupdate 직접 DOM 조작 금지 +- requestAnimationFrame/requestVideoFrameCallback 사용 +- 이진 탐색 O(log n) + +**패턴 준수:** +- Range Request: createReadStream + pipeline +- FFmpeg: child_process.spawn 래퍼 +- Video.js: ref + useEffect 패턴 + +피드백을 Critical / Warning / Suggestion 으로 분류하세요. diff --git a/.claude/skills/status/SKILL.md b/.claude/skills/status/SKILL.md new file mode 100644 index 0000000..c221df1 --- /dev/null +++ b/.claude/skills/status/SKILL.md @@ -0,0 +1,25 @@ +--- +name: status +description: 프로젝트 진행 상태를 확인하고 다음 작업을 제안합니다. +disable-model-invocation: true +--- + +## 프로젝트 진행 상태 확인 + +아래 파일들을 읽고 현재 상태를 요약하세요: + +1. **PROGRESS.md** 읽기 — 현재 상태 요약, 단계별 진행 기록 + +2. **PLAN.md** 읽기 — 미완료 태스크 (`- [ ]`) 개수 파악 + +3. 다음 내용을 표 형태로 보고: + +| 단계 | 상태 | 완료 태스크 | 남은 태스크 | +|------|------|------------|------------| + +4. **다음에 할 작업** 제안: + - 의존성이 충족된 다음 단계 식별 + - 블로커가 있으면 강조 + - 병렬 진행 가능한 단계가 있으면 알림 + +5. 블로커/이슈가 있으면 빨간색으로 강조 diff --git a/.claude/skills/step/SKILL.md b/.claude/skills/step/SKILL.md new file mode 100644 index 0000000..cb2ee9c --- /dev/null +++ b/.claude/skills/step/SKILL.md @@ -0,0 +1,34 @@ +--- +name: step +description: PLAN.md의 특정 단계를 실행합니다. 단계 번호를 인자로 전달합니다. +argument-hint: "<단계번호>" +disable-model-invocation: true +--- + +## 단계 $ARGUMENTS 실행 + +step-worker 에이전트를 사용하여 PLAN.md의 **단계 $ARGUMENTS**를 수행합니다. + +### 실행 프로토콜 + +1. 먼저 아래 3개 파일을 순서대로 읽으세요: + - `CLAUDE.md` — 프로젝트 규칙/아키텍처 + - `PLAN.md` — 단계 $ARGUMENTS의 세부 태스크 확인 + - `PROGRESS.md` — 이전 단계 완료 여부, 블로커, 인계사항 + +2. PROGRESS.md에서 단계 $ARGUMENTS의 상태를 "🔄 진행 중"으로 업데이트 + +3. PLAN.md의 단계 $ARGUMENTS 태스크를 위에서 아래로 순서대로 수행: + - 각 태스크 완료 시 PLAN.md에서 `- [ ]`를 `- [x]`로 변경 + - 의미 있는 마일스톤마다 PROGRESS.md 업데이트 + +4. 모든 태스크 완료 후: + - PLAN.md의 **산출물(완료 기준)**을 직접 검증 + - PROGRESS.md에 최종 결과 기록 (완료 항목, 미완료/이슈, 다음 단계 참고) + - 산출물 충족 시 상태를 "✅ 완료"로 변경 + +### 주의사항 + +- 이전 단계가 완료되지 않았으면 사용자에게 알리고 중단 +- 블로커 발견 시 PROGRESS.md에 기록 후 사용자에게 보고 +- 이전 에이전트가 작성한 코드를 이유 없이 변경하지 않음 diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md new file mode 100644 index 0000000..3d79904 --- /dev/null +++ b/.claude/skills/verify/SKILL.md @@ -0,0 +1,34 @@ +--- +name: verify +description: 특정 단계의 산출물(완료 기준)을 검증합니다. 단계 번호를 인자로 전달합니다. +argument-hint: "<단계번호>" +disable-model-invocation: true +--- + +## 단계 $ARGUMENTS 검증 + +PLAN.md의 **단계 $ARGUMENTS**에 정의된 산출물(완료 기준)을 하나씩 검증합니다. + +### 검증 프로토콜 + +1. **PLAN.md** 읽기 → 단계 $ARGUMENTS의 "산출물 (완료 기준)" 섹션 확인 +2. **PROGRESS.md** 읽기 → 현재 상태, 알려진 이슈 확인 + +3. 각 산출물 항목을 실제로 실행하여 검증: + - 빌드 확인: `npx tsc --noEmit`, `npm run build` + - 서버 기동: 서버 시작 후 API 엔드포인트 curl 테스트 + - 클라이언트: 개발 서버 기동 확인, 빌드 성공 확인 + - 기능 테스트: 해당 단계의 핵심 기능 동작 확인 + +4. 각 항목에 대해 결과 보고: + - ✅ 통과: 확인 방법 + 결과 + - ❌ 실패: 에러 내용 + 원인 분석 + 수정 제안 + - ⚠️ 부분 통과: 동작하지만 이슈 있음 + +5. 종합 결과를 PROGRESS.md에 기록 + +### 주의사항 + +- 실행 중인 프로세스는 검증 후 반드시 종료 +- 서버 포트 충돌 시 기존 프로세스 확인 후 처리 +- 검증만 수행하고 코드 수정은 하지 않음 (수정이 필요하면 보고) diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3b3c0fb --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95ef4cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +node_modules/ +dist/ +build/ +.env +.env.local +storage/videos/ +storage/hls/ +storage/frames/ +storage/thumbnails/ +storage/*.db +*.log +.DS_Store + +# 대용량 미디어 파일 +*.MP4 +*.mp4 +*.mkv +*.avi +*.mov +*.webm +*.srt + +# 샘플 데이터 (대용량) +samplevideo/ + +# Python 소스 입력 대용량 파일 +pythonsource/input/*.MP4 +pythonsource/input/*.mp4 +pythonsource/input/*.srt + +# SQLite WAL/SHM +storage/*.db-wal +storage/*.db-shm + +# cloudflared log +storage/cloudflared.log diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1f4c4bb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..11555d6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,609 @@ +# CLAUDE.md — abcvideo 프로젝트 + +이 파일은 Claude Code가 abcvideo 프로젝트에서 작업할 때 참조하는 가이드입니다. + +--- + +## 작업 완료 시 필수 — 히스토리 기록 + +작업(대화 세션)이 끝날 때 반드시 아래 형식으로 히스토리 파일을 작성한다. + +``` +파일 경로: docs/history/YYYY-MM-DD_{작업명}.md +``` + +필수 항목 (누락 시 Stop 훅이 응답을 차단함): + +```markdown +**소요 시간**: X분 +**Context 사용량**: input Xk / output Xk tokens +``` + +- 날짜는 오늘 날짜(YYYY-MM-DD), 작업명은 작업 내용을 한글 또는 영문으로 간략히 +- 같은 날 여러 작업이면 각각 별도 파일 (예: `2026-04-01_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() →