h-lab/CLAUDE.md
hehih 487dfc6d0f docs: update CLAUDE.md for ChannelVideo single-master architecture
Reflect removal of Video/YtVideo/Opal: ChannelVideo is now the single
video master with the collect->curate->rework->publish pipeline. Update
external-integration section (Python transcript via ChannelService,
YouTube API via YoutubeSearchService; Google Docs/Opal gone) and the
security note (OAuth creds now unused, gitignored).
2026-05-30 19:59:38 +09:00

67 lines
6.5 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 개요
h-lab(아티팩트명 `yanalyst`)은 유튜브 데이터를 수집·분석하고, 이를 바탕으로 다단계 콘텐츠 제작 파이프라인을 구동하는 개인용 웹 서비스입니다. Spring Boot 3.4.0 / Java 21 백엔드에 **서버 사이드 렌더링(Thymeleaf)** UI를 사용합니다 (README.md에 언급된 React/Vite 프론트엔드는 SSR로 대체되어 제거됨). 모든 UI는 동일한 Spring Boot 앱에서 제공됩니다.
## 명령어
Windows에서는 `gradlew.bat`을 사용합니다. Gradle wrapper는 저장소 루트에 있습니다 (README.md에는 `backend/` 디렉토리가 있다고 되어 있으나 실제로는 없음).
```powershell
# 앱 실행 (UI + API를 http://localhost:8088 에서 제공)
.\gradlew.bat bootRun
# 빌드
.\gradlew.bat build
# 테스트 실행
.\gradlew.bat test
# 단일 테스트 클래스 / 메서드 실행
.\gradlew.bat test --tests "com.hlab.yanalyst.SomeTest"
.\gradlew.bat test --tests "com.hlab.yanalyst.SomeTest.someMethod"
```
- **서버 포트는 8088** (`application.yml`)입니다. README의 8080이 아닙니다. Swagger UI: `http://localhost:8088/swagger-ui.html`.
- 현재 **`src/test` 디렉토리가 없습니다** — `.\gradlew.bat test`는 사실상 빈 상태로 통과합니다. 테스트는 `src/test/java/com/hlab/yanalyst/` 아래에 추가하세요.
## 아키텍처
### 두 가지 패키지 컨벤션이 공존 (중요)
코드베이스에 두 가지 구조 스타일이 섞여 있습니다. 코드를 추가하기 전에 어느 쪽인지 파악하세요:
1. **`domain/<aggregate>/`** — DDD 스타일 패키지 (`channel`, `production`, `category`, `publish`). 각 패키지가 Entity + Repository + Service + `@RestController`(Swagger 문서화, `/api/v1/...` 경로) + `dto/`를 함께 묶습니다.
2. **`web/`** + **`service/`** — 이후에 추가된 두 번째 레이어. `web/`에는 Thymeleaf 페이지 컨트롤러(`WebController`, `ChannelDetailController`)와 추가 JSON `@RestController`들(`/api/...` 경로, `v1` 없음)이 있습니다. `service/`에는 횡단 관심사인 `YoutubeSearchService`(YouTube Data API 검색)가 있습니다.
**영상 모델은 `domain/channel/ChannelVideo`(테이블 `channel_videos`)가 단일 마스터**입니다. 수집(채널 동기화 + 조회수 검색)→큐레이션→재가공→발행 전 과정이 이 엔티티 하나를 중심으로 돕니다. (과거 `Video`/`YtVideo` 및 Opal 파이프라인은 제거됨 — DB에는 `videos`/`yt_video`/`scriptgen`/`opal_*` 테이블이 ddl-auto:update 특성상 고아 상태로 남아있을 수 있으나 더 이상 매핑되지 않음.) `domain/production/ProductionVideo`(테이블 `production_video`)는 별개 개념으로, n8n 크롤 랭킹 스냅샷을 담습니다 — 수집 영상과 혼동하지 마세요.
### ChannelVideo 콘텐츠 파이프라인 (앱의 핵심)
`ChannelVideo`를 "수집→분류→재가공→유통" 흐름으로 진행시키는 것이 앱의 핵심입니다.
1. **수집** → 등록 채널 동기화(`source=CHANNEL`) 또는 조회수 검색 수집(`source=SEARCH`, `ChannelVideo.fromSearch`). 검색은 `service/YoutubeSearchService`(`POST /api/youtube/search`), 일괄 저장은 `domain/channel/SearchCollectionService`(`POST /api/youtube/collect`, 중복 자동 스킵). 정기 자동 수집은 `global/schedule/ScheduledCollectionService`.
2. **큐레이션**`domain/channel/ChannelVideoCurationService`. `interestStatus`(NEW/REVIEWING/TARGET/DONE/EXCLUDED), 카테고리(`domain/category`), 북마크, 떡상 지표(`viewsPerSubRatio` 등). 수집함 `/collection`, 칸반 보드 `/board`.
3. **재가공** → 재가공 작업공간 `/rework/{id}`. 원본 자막 추출(`ChannelService.extractScript` → Python transcript 서비스 호출, `channel_video_scripts` 저장) + 재작성 에디터(`ChannelVideo.reworkText`, 저장 시 status TARGET 승격).
4. **발행(유통)**`domain/publish/PublishService` + `PublishPackage`(ChannelVideo와 1:1). 발행 큐 `/publish`, 재가공 화면 하단 "발행 준비" 섹션. 실제 업로드는 수동(URL 기록) — 플랫폼 API 미연동.
### 외부 연동
- **Python transcript 마이크로서비스** (`http://h-python.tolag.shop/transcript`) — `domain/channel/ChannelService.extractScript``RestTemplate`로 직접 호출해 자막을 가져와 `channel_video_scripts`에 저장합니다.
- **YouTube Data API** — `service/YoutubeSearchService`(검색)와 `domain/channel/ChannelService`(채널/영상 메타 수집)가 `youtube.api.key`(env `YOUTUBE_API_KEY`)로 호출. 일일 쿼터는 `global/schedule/YoutubeQuotaGuard`로 가드.
- **n8n webhook** — `production/` 도메인이 랭킹/크롤 데이터를 위해 연동.
### 컨벤션
- **영속성**: PostgreSQL(원격, `application.yml`에 설정), JPA `ddl-auto: update` — 스키마가 엔티티에서 자동 관리되며 마이그레이션 파일은 없습니다. Lombok 전면 사용, `@EnableJpaAuditing``@CreationTimestamp`/`@UpdateTimestamp`. p6spy가 `global/config/P6SpyFormatter`로 포맷된 SQL을 로깅합니다.
- **API 응답**: JSON 결과는 `global/common/ApiResponse<T>`로 감쌉니다 (`ApiResponse.ok(...)` / `.created(...)` / `.error(...)`). 에러는 `global/error/GlobalExceptionHandler`에서 중앙 처리됩니다.
- **Thymeleaf**: 템플릿은 `src/main/resources/templates/`에 있고, 공유 `layout/base.html` + `layout/sidebar.html`을 사용합니다(thymeleaf-layout-dialect). 페이지 컨트롤러는 사이드바 하이라이트를 위해 `currentPage` 모델 속성을 설정합니다. 정적 CSS/JS는 `static/`에 있으며 다크모드 디자인은 `variables.css` 기반입니다.
- CORS는 `global/config/WebMvcConfig`에서 전면 개방되어 있습니다 (`allowedOriginPatterns("*")`, credentials 허용).
## 보안 참고
`application.yml`은 시크릿을 환경변수로 받되 fallback 값이 하드코딩되어 있습니다(`DB_URL/DB_USERNAME/DB_PASSWORD`, `YOUTUBE_API_KEY`). 또한 `credentials.json` / `tokens/StoredCredential`에 Google OAuth 시크릿이 남아있습니다 — 이는 제거된 Opal(Google Docs 연동) 전용이라 **현재 코드에서는 미사용**이지만 파일은 여전히 저장소에 존재할 수 있습니다(`.gitignore`에 추가되어 git 추적에서는 제외). 이 값들을 로그나 외부 서비스에 노출하지 말고, 추가 시크릿을 커밋하라는 요청이 있으면 경고하세요.