diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 605cee3..1dd1293 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,11 @@ "Bash(git rm *)", "Bash(git add *)", "Bash(git -c user.name=hehih -c user.email=hehihoho86@gmail.com commit -q -m 'Retire Opal/YtVideo pipeline; ChannelVideo is the single video master *)", - "Bash(git -c user.name=hehih -c user.email=hehihoho86@gmail.com commit -q -m 'docs: update CLAUDE.md for ChannelVideo single-master architecture *)" + "Bash(git -c user.name=hehih -c user.email=hehihoho86@gmail.com commit -q -m 'docs: update CLAUDE.md for ChannelVideo single-master architecture *)", + "Bash(export JAVA_HOME=\"D:/Development/app/JDK/jdk-21.0.5\")", + "Bash(./gradlew.bat bootRun --console=plain)", + "Bash(echo \"bootRun started in background, PID $!\")", + "Bash(git -c user.name=\"hehih\" -c user.email=\"hehihoho86@gmail.com\" commit -q -m \"docs: add discovery page design spec\")" ] } } diff --git a/docs/superpowers/specs/2026-05-30-discovery-page-design.md b/docs/superpowers/specs/2026-05-30-discovery-page-design.md new file mode 100644 index 0000000..a867ec2 --- /dev/null +++ b/docs/superpowers/specs/2026-05-30-discovery-page-design.md @@ -0,0 +1,102 @@ +# 발굴(Discovery) 전용 페이지 — 설계 + +작성일: 2026-05-30 +상태: 승인됨 (구현 대기) + +## 목적 + +수집된 영상(`ChannelVideo`) 중 "재가공할 만한 떡상 후보"를 빠르게 골라내는 일일 작업 화면을 제공한다. +파이프라인 "수집→**분류/발굴**→재가공→유통"에서 발굴 단계를 전용 화면으로 끌어올린다. + +**제약: 비용 0** — 새로운 외부 API 호출이나 신규 수집 없이, 이미 보유한 파생 지표(`viewsPerHour`, `viewsPerSubRatio`, `viewCount`, `publishedAt`)만으로 랭킹/필터한다. + +## 배경 / 현재 상태 + +- 떡상 발굴은 현재 `GET /api/v1/channel-videos/outperformers` 하나뿐 — `viewsPerSubRatio >= minRatio`인 Shorts를 비율순으로만 반환하며, 대시보드 TOP 리스트로만 노출된다. **전용 작업 화면이 없다.** +- 큐레이션 액션(북마크/상태변경/메모/재가공 이동)은 `ChannelVideoCurationController`(`/api/v1/channel-videos/...`)에 이미 구현되어 있다 — 재사용한다. +- UI 패턴은 `collection.html`(필터 바 + 테이블 + 행별 인라인 액션)을 따른다. + +## 비범위 (YAGNI) + +- 복합 "발굴 점수"(가중치 합산) — 도입하지 않음. 기존 단일 지표 정렬로 충분. +- AI/외부 호출 — 없음. +- 새 액션 엔드포인트 — 없음(기존 큐레이션 API 재사용). + +## 아키텍처 / 컴포넌트 + +신규로 추가되는 단위는 다음 5개이며 각각 책임이 단일하다. + +### 1. `ChannelVideoRepository.discover(...)` (신규 쿼리) +수집 영상을 발굴 조건으로 필터링하는 조회 쿼리. + +입력(모두 nullable, null이면 해당 필터 미적용): +- `publishedAfter: LocalDateTime` — 이 시각 이후 업로드분만 (최근성 필터) +- `minRatio: BigDecimal` — `viewsPerSubRatio >= minRatio` +- `shortsOnly: boolean` — true면 `isShorts = true` +- `source: String` — `CHANNEL` | `SEARCH` +- `unprocessedOnly: boolean` — true면 상태가 `NEW` 또는 `REVIEWING`인 것만 +- `sort: Sort` — 정렬 기준 + +규칙: **`interestStatus = 'EXCLUDED'`는 항상 제외**한다. + +### 2. `ChannelVideoCurationService.discover(...)` (신규 서비스 메서드) +파라미터 검증·기본값 적용 후 리포지토리 호출. +- `periodDays`(Integer) → `publishedAfter = now - periodDays일` (null/0이면 전체) +- `sortBy` → 기존 `ALLOWED_SORT` 화이트리스트(`viewsPerHour|viewsPerSubRatio|viewCount|publishedAt|durationSec`) 재사용, 기본 `viewsPerSubRatio` 내림차순 +- `minRatio` 그대로 전달(null이면 미적용) +- 결과 개수는 `limit`(기본 100, 최대 200)으로 캡 + +### 3. `GET /api/v1/channel-videos/discover` (신규 엔드포인트) +`ChannelVideoCurationController`에 추가. +쿼리 파라미터: `periodDays`, `minRatio`, `shortsOnly`(기본 false), `source`, `unprocessedOnly`(기본 false), `sortBy`, `limit`. +응답: `ApiResponse>`. + +### 4. `discover.html` + `/discover` 라우트 +- `WebController`에 `GET /discover` 추가 (`currentPage = "discover"`, 뷰 `discover`). +- 템플릿은 `collection.html` 구조를 따른다(공유 `layout/base`). +- **필터 바**: 기간(전체/7/14/30일), 최소 배율(전체/≥2x/≥3x/≥5x), Shorts만 토글, 출처(전체/CHANNEL/SEARCH), 미처리만 토글, 정렬 드롭다운(떡상속도/배율/조회수/최신). +- **결과 테이블**: 썸네일 · 제목 · 채널 · 조회수 · 구독자 · 배율(viewsPerSubRatio) · 떡상속도(viewsPerHour) · 업로드일 · 상태뱃지. +- **행별 액션**(기존 API 재사용): 북마크 토글(`POST /{id}/bookmark`), TARGET 지정(`POST /{id}/status`), 🪄 재가공(`/rework/{id}` 이동), YouTube 열기(외부 링크). + +### 5. 사이드바 링크 +`layout/sidebar.html`에 "발굴" 항목 추가(수집함과 보드 사이 권장). 아이콘 후보: lucide `radar` 또는 `telescope`. `currentPage == 'discover'`일 때 active. + +## 데이터 흐름 + +``` +discover.html (필터 변경/진입) + → GET /api/v1/channel-videos/discover?필터 + → CurationService.discover() → Repository.discover() + → 행 렌더링 + → 행별 액션 → 기존 POST 엔드포인트(bookmark/status) → 낙관적 UI 갱신 +``` + +## 기본값 (페이지 진입 시) + +- 기간: 전체 +- 최소 배율: 전체(미적용) +- Shorts만: off +- 출처: 전체 +- 미처리만: off (단, EXCLUDED는 항상 숨김) +- 정렬: 구독자 대비 배율(viewsPerSubRatio) 내림차순 + +→ 진입 즉시 "구독자 적은데 조회수 터진" 영상이 상단에 온다. + +## 에러 / 빈 상태 + +- 결과 0건: "조건에 맞는 발굴 후보가 없습니다" 안내 행. +- API 실패: 테이블에 에러 메시지(`ApiResponse.message`) 표시. (`collection.html` 방식과 동일) +- 백필 안 된 옛 데이터(`viewsPerSubRatio = null`)는 배율 정렬/필터에서 자연히 하단·제외됨 — 별도 처리 불필요(기존 백필 기능으로 보정 가능). + +## 검증 방법 + +이 프로젝트는 테스트 인프라가 없고(`src/test` 부재) 원격 운영 DB에 붙으므로, 기존 관행대로 검증한다: +1. `gradlew.bat clean compileJava` 통과. +2. `gradlew.bat bootRun` 기동(빈 와이어링·엔티티 매핑 정상 확인). +3. Swagger(`/swagger-ui.html`)에서 `/discover` 응답 확인 + 브라우저에서 `/discover` 페이지 필터/정렬/액션 동작 수동 확인. + +## 영향 / 변경 파일 요약 + +신규: `discover.html`, 스펙 문서. +수정: `ChannelVideoRepository`(메서드 1), `ChannelVideoCurationService`(메서드 1), `ChannelVideoCurationController`(엔드포인트 1), `WebController`(라우트 1), `layout/sidebar.html`(링크 1). +기존 액션 API·엔티티·다른 페이지는 변경 없음.