# 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//`** — 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`로 감쌉니다 (`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 추적에서는 제외). 이 값들을 로그나 외부 서비스에 노출하지 말고, 추가 시크릿을 커밋하라는 요청이 있으면 경고하세요.