h-lab/CLAUDE.md

6.3 KiB

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/ 디렉토리가 있다고 되어 있으나 실제로는 없음).

# 앱 실행 (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, video, opal, production, script). 각 패키지가 Entity + Repository + Service + @RestController(Swagger 문서화, /api/v1/... 경로) + dto/를 함께 묶습니다.
  2. web/ + service/ — 이후에 추가된 두 번째 레이어. web/에는 Thymeleaf 페이지 컨트롤러(WebController, ChannelDetailController)와 추가 JSON @RestController들(/api/... 경로, v1 없음)이 있습니다. service/에는 횡단 관심사인 AnalysisWorkflowServiceYtVideoService가 있습니다.

video 엔티티가 두 개라는 점에 유의: domain/video/Video.java(/api/v1/videos) vs domain/video/YtVideo.java(테이블 yt_video, web/YtVideoController + web/VideoActionController를 통해 /api/videos에서 구동). 실제 콘텐츠 제작 워크플로우는 YtVideo 기반으로 동작하며, Video는 구버전 읽기 모델입니다. 기능을 수정하기 전에 어느 쪽을 대상으로 하는지 확인하세요.

Opal 콘텐츠 제작 워크플로우

service/AnalysisWorkflowService가 오케스트레이터이며 앱의 핵심입니다. 이 파이프라인은 YtVideo.status 필드를 CRAWLED → SCRIPT_READY → DRAFTING → FINALIZED 순으로 진행시킵니다. 단계:

  1. generateScript → 트랜스크립트를 가져와 ScriptGen 저장 (비디오당 1개, PK = videoId).
  2. generateDraft → 버전이 매겨진 OpalDraft 행 생성 (비디오별 versionNo 자동 증가), 사용자 피드백을 반영 가능.
  3. acceptDraft → draft를 활성 OpalFinal로 승격 (비디오당 isActive=true는 하나만 유지, 이전 것은 비활성화).
  4. generateFinalAsset → 최종 스크립트를 가져와 OpalFinalAsset 생성 (title/summary/timeline/video_prompt/image_urls를 JSON 컬럼으로 저장).

web/VideoActionController를 통해 구동됩니다 (POST /api/videos/{id}/script|drafts|final-asset 등).

외부 연동 (service/external/)

ExternalApiService는 인터페이스이며 구현체가 두 개입니다:

  • ExternalApiServiceImpl (@Primary) — 실제 구현. Python 트랜스크립트 마이크로서비스(http://h-python.tolag.shop/transcript)를 호출하고, Google Docs API로 Google Docs를 읽고 비웁니다. Google Doc ID는 모드별(TRUE_STORY vs STRUCTURE_CHANGE) 및 워크플로우 단계별로 하드코딩되어 있습니다. generateScript는 실패 시 예외를 던지지 않고 fallback 플레이스홀더 트랜스크립트로 degrade하며, generateFinalAsset은 아직 플레이스홀더 데이터를 반환하는 스텁입니다.
  • ExternalApiServiceStub — fallback/테스트용 더미.

Google Docs 인증은 OAuth installed-app 플로우를 사용합니다: 클라이언트 시크릿은 src/main/resources/credentials.json, 리프레시 토큰은 tokens/ 디렉토리(StoredCredential)에 캐싱됩니다. 최초 실행 시 포트 8888에서 브라우저를 열어 인증을 진행합니다. 이 구현체는 읽은 후 원본 Google Doc의 내용을 삭제합니다(clearGoogleDoc) — 테스트 시 파괴적이므로 주의하세요.

production/ 도메인은 랭킹/크롤 데이터를 위해 외부 n8n webhook과 연동합니다.

컨벤션

  • 영속성: 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 허용).

보안 참고

src/main/resources/application.yml에 PostgreSQL 비밀번호가 하드코딩되어 있고, credentials.json / tokens/StoredCredential에 Google OAuth 시크릿이 저장되어 저장소에 커밋되어 있습니다. 이 값들을 로그나 외부 서비스에 노출하지 말고, 추가 시크릿을 커밋하라는 요청이 있으면 경고하세요.