# 발굴(Discovery) 전용 페이지 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 수집한 영상 중 "재가공할 떡상 후보"를 필터·정렬로 빠르게 골라내는 발굴 전용 페이지(`/discover`)를 추가한다. **Architecture:** 기존 `ChannelVideo` + 큐레이션 API 위에 발굴 조회 쿼리 1개·서비스 메서드 1개·엔드포인트 1개·페이지 1개·사이드바 링크 1개를 더한다. 행별 액션(상태변경·북마크)은 기존 엔드포인트를 재사용한다. 새 외부 호출·새 데이터 없음(비용 0). **Tech Stack:** Spring Boot 3.4 / Java 21 / Spring Data JPA(JPQL) / Thymeleaf(layout-dialect) / 바닐라 JS(fetch). > **검증 방식 주의:** 이 프로젝트는 테스트 프레임워크가 없고(`src/test` 없음) 원격 PostgreSQL에 연결된다. 따라서 각 태스크의 검증은 단위테스트 대신 **컴파일 + 기동 + 수동 확인**으로 한다. JAVA_HOME 은 `D:\Development\app\JDK\jdk-21.0.5`. --- ## File Structure - **수정** `src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoRepository.java` — `discover(...)` JPQL 쿼리 추가. - **수정** `src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java` — `discover(...)` 메서드 추가(파라미터 정규화·기본값·limit 캡·nullsLast 정렬). - **수정** `src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java` — `GET /discover` 엔드포인트 추가. - **수정** `src/main/java/com/hlab/yanalyst/web/WebController.java` — `GET /discover` 페이지 라우트 추가. - **생성** `src/main/resources/templates/discover.html` — 발굴 페이지(필터 바 + 결과 테이블 + 행 액션 + 영상 모달). - **수정** `src/main/resources/templates/layout/sidebar.html` — "발굴" 네비 링크 추가. --- ## Task 1: 발굴 조회 쿼리 (Repository) **Files:** - Modify: `src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoRepository.java` - [ ] **Step 1: `discover` 쿼리 메서드 추가** `search(...)` 메서드 정의 바로 뒤(파일 내 마지막 메서드 다음, 닫는 `}` 앞)에 아래를 추가한다: ```java /** * 발굴(Discovery) 조회. EXCLUDED 는 항상 제외. null/false 조건은 무시. * 정렬·개수 제한은 호출부의 Pageable(sort + size)로 처리한다. * @param publishedAfter 이 시각 이후 업로드분만 (null이면 전체) * @param minRatio viewsPerSubRatio 하한 (null이면 미적용; null 배율 행은 자동 제외) * @param shortsOnly true면 Shorts만 * @param source CHANNEL | SEARCH (null이면 전체) * @param unprocessedOnly true면 상태가 NEW/REVIEWING 인 것만 */ @Query("select v from ChannelVideo v where " + "v.interestStatus <> 'EXCLUDED' and " + "(:publishedAfter is null or v.publishedAt >= :publishedAfter) and " + "(:minRatio is null or v.viewsPerSubRatio >= :minRatio) and " + "(:source is null or v.source = :source) and " + "(:shortsOnly = false or v.isShorts = true) and " + "(:unprocessedOnly = false or v.interestStatus in ('NEW','REVIEWING'))") java.util.List discover(@Param("publishedAfter") java.time.LocalDateTime publishedAfter, @Param("minRatio") java.math.BigDecimal minRatio, @Param("source") String source, @Param("shortsOnly") boolean shortsOnly, @Param("unprocessedOnly") boolean unprocessedOnly, org.springframework.data.domain.Pageable pageable); ``` - [ ] **Step 2: 컴파일로 검증** Run: `$env:JAVA_HOME="D:\Development\app\JDK\jdk-21.0.5"; .\gradlew.bat compileJava --console=plain` Expected: `BUILD SUCCESSFUL` (deprecated API 경고는 무시). - [ ] **Step 3: 커밋** ```bash git add src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoRepository.java git commit -m "feat(discover): add ChannelVideo discovery query" ``` --- ## Task 2: 발굴 서비스 메서드 (Service) **Files:** - Modify: `src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java` - [ ] **Step 1: `discover` 메서드 추가** `findOutperformers(...)` 메서드 정의 바로 뒤에 아래를 추가한다. (이미 import 된 `Sort`, `StringUtils`, `PageRequest`(`org.springframework.data.domain.PageRequest` 는 풀패키지로 사용), `BigDecimal` 사용) ```java /** * 발굴(Discovery) 조회 — 필터 + 정렬 + 개수 제한. * @param periodDays 최근 N일만 (null/0이면 전체) * @param minRatio viewsPerSubRatio 하한 (null이면 미적용) * @param shortsOnly Shorts만 * @param source CHANNEL | SEARCH (빈값이면 전체) * @param unprocessedOnly true면 NEW/REVIEWING 만 * @param sortBy 정렬 필드(ALLOWED_SORT, 기본 viewsPerSubRatio 내림차순, null은 항상 마지막) * @param limit 최대 개수(기본 100, 최대 200) */ public List discover(Integer periodDays, java.math.BigDecimal minRatio, boolean shortsOnly, String source, boolean unprocessedOnly, String sortBy, Integer limit) { java.time.LocalDateTime publishedAfter = (periodDays == null || periodDays <= 0) ? null : java.time.LocalDateTime.now().minusDays(periodDays); String sourceFilter = StringUtils.hasText(source) ? source : null; String sortField = StringUtils.hasText(sortBy) && ALLOWED_SORT.contains(sortBy) ? sortBy : "viewsPerSubRatio"; int size = (limit == null || limit <= 0) ? 100 : Math.min(limit, 200); Sort sort = Sort.by(Sort.Order.desc(sortField).nullsLast()); return channelVideoRepository.discover(publishedAfter, minRatio, sourceFilter, shortsOnly, unprocessedOnly, org.springframework.data.domain.PageRequest.of(0, size, sort)); } ``` - [ ] **Step 2: 컴파일로 검증** Run: `$env:JAVA_HOME="D:\Development\app\JDK\jdk-21.0.5"; .\gradlew.bat compileJava --console=plain` Expected: `BUILD SUCCESSFUL`. - [ ] **Step 3: 커밋** ```bash git add src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java git commit -m "feat(discover): add discover() service method with defaults and limit cap" ``` --- ## Task 3: 발굴 엔드포인트 (Controller) **Files:** - Modify: `src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java` - [ ] **Step 1: `GET /discover` 추가** `/outperformers` 핸들러(`outperformers(...)`) 바로 뒤에 아래를 추가한다: ```java @GetMapping("/discover") @Operation(summary = "발굴(Discovery) 조회", description = "필터: periodDays(최근 N일), minRatio(배율 하한), shortsOnly, source(CHANNEL|SEARCH), " + "unprocessedOnly(NEW/REVIEWING만). 정렬 sortBy(기본 viewsPerSubRatio↓), limit(기본100). " + "EXCLUDED 는 항상 제외.") public ApiResponse> discover( @RequestParam(required = false) Integer periodDays, @RequestParam(required = false) java.math.BigDecimal minRatio, @RequestParam(defaultValue = "false") boolean shortsOnly, @RequestParam(required = false) String source, @RequestParam(defaultValue = "false") boolean unprocessedOnly, @RequestParam(required = false) String sortBy, @RequestParam(required = false) Integer limit) { return ApiResponse.ok(curationService.discover(periodDays, minRatio, shortsOnly, source, unprocessedOnly, sortBy, limit)); } ``` - [ ] **Step 2: 컴파일로 검증** Run: `$env:JAVA_HOME="D:\Development\app\JDK\jdk-21.0.5"; .\gradlew.bat compileJava --console=plain` Expected: `BUILD SUCCESSFUL`. - [ ] **Step 3: 커밋** ```bash git add src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java git commit -m "feat(discover): add GET /api/v1/channel-videos/discover endpoint" ``` --- ## Task 4: 페이지 라우트 (WebController) **Files:** - Modify: `src/main/java/com/hlab/yanalyst/web/WebController.java` - [ ] **Step 1: `/discover` 라우트 추가** `board(...)` 핸들러 바로 뒤에 아래를 추가한다: ```java @GetMapping("/discover") public String discover(Model model) { model.addAttribute("currentPage", "discover"); return "discover"; } ``` - [ ] **Step 2: 컴파일로 검증** Run: `$env:JAVA_HOME="D:\Development\app\JDK\jdk-21.0.5"; .\gradlew.bat compileJava --console=plain` Expected: `BUILD SUCCESSFUL`. - [ ] **Step 3: 커밋** ```bash git add src/main/java/com/hlab/yanalyst/web/WebController.java git commit -m "feat(discover): add /discover page route" ``` --- ## Task 5: 발굴 페이지 템플릿 (discover.html) **Files:** - Create: `src/main/resources/templates/discover.html` - [ ] **Step 1: 템플릿 작성** 아래 내용 그대로 새 파일로 생성한다: ```html h-lab - 발굴

발굴 (Discovery)

수집한 영상 중 재가공할 떡상 후보를 빠르게 골라냅니다. (제외 처리한 영상은 숨겨집니다)

썸네일 제목 채널 구독자 조회수 시간당 배율 업로드 상태 관리
로딩 중...
``` - [ ] **Step 2: 커밋** ```bash git add src/main/resources/templates/discover.html git commit -m "feat(discover): add discovery page template" ``` --- ## Task 6: 사이드바 링크 **Files:** - Modify: `src/main/resources/templates/layout/sidebar.html` - [ ] **Step 1: "발굴" 네비 항목 추가** "수집함" `
  • `(닫는 `
  • `)와 "보드" `
  • ` 사이에 아래 블록을 삽입한다: ```html
  • 발굴
  • ``` - [ ] **Step 2: 커밋** ```bash git add src/main/resources/templates/layout/sidebar.html git commit -m "feat(discover): add sidebar nav link" ``` --- ## Task 7: 통합 검증 (기동 + 수동 확인) **Files:** 없음 (검증 전용) - [ ] **Step 1: 클린 컴파일** Run: `$env:JAVA_HOME="D:\Development\app\JDK\jdk-21.0.5"; .\gradlew.bat clean compileJava --console=plain` Expected: `BUILD SUCCESSFUL`. - [ ] **Step 2: 기동 확인 (포트 8088 점유 시 기존 인스턴스 종료 후)** Run: `$env:JAVA_HOME="D:\Development\app\JDK\jdk-21.0.5"; .\gradlew.bat bootRun --console=plain` Expected: 로그에 `Started HLabApplication` 또는 최소한 `Tomcat started on port 8088` / `JPA EntityManagerFactory` 정상. 빈 에러(`UnsatisfiedDependency`, `BeanCreationException`) 없음. (이미 8088을 다른 인스턴스가 점유 중이면 그 프로세스가 곧 새 코드로 재기동되었는지 확인하거나, 점유 인스턴스를 종료 후 재기동.) - [ ] **Step 3: 수동 확인 (브라우저)** 1. `http://localhost:8088/discover` 접속 → 사이드바 "발굴" active, 진입 즉시 배율 내림차순으로 영상 표시. 2. 기간/최소배율/출처/Shorts/미처리만/정렬 변경 시 목록 갱신 확인. 3. 행에서 상태 드롭다운 변경 → 새로고침해도 유지(저장 확인). 북마크 토글 → 별 색 변경. 🪄 재가공 클릭 → `/rework/{id}` 이동. 썸네일 클릭 → 모달 재생. 4. EXCLUDED 로 바꾼 영상이 새로고침 후 목록에서 사라지는지 확인. 5. (선택) Swagger `http://localhost:8088/swagger-ui.html` 에서 `/api/v1/channel-videos/discover` 응답 확인. - [ ] **Step 4: (이상 없으면) 마무리 커밋 / 정리** 추가 변경이 없으면 커밋할 것 없음. 기동 로그 파일 등 임시 산출물은 `build/`(gitignore) 에 두거나 삭제. --- ## Self-Review 결과 - **스펙 커버리지:** 백엔드(쿼리 T1 / 서비스 T2 / 엔드포인트 T3), 프런트(라우트 T4 / 페이지 T5 / 사이드바 T6), 검증(T7) — 스펙의 5개 컴포넌트 + 기본값 + 에러/빈 상태 + 검증방법 모두 태스크에 매핑됨. - **기본값 일치:** 서비스 기본 sortField=`viewsPerSubRatio`, UI 기본 정렬 옵션도 `viewsPerSubRatio` 첫번째. unprocessed/shorts 기본 off, 기간/배율 기본 전체 — 스펙과 일치. - **타입/시그니처 일치:** `discover` 시그니처가 Controller→Service→Repository 3계층에서 동일(파라미터 순서: periodDays/minRatio/shortsOnly/source/unprocessedOnly/sortBy/limit → 서비스 → publishedAfter/minRatio/source/shortsOnly/unprocessedOnly/Pageable). `EXCLUDED` 제외는 Repository에서 처리. `ALLOWED_SORT`/`Sort`/`StringUtils` 는 서비스에 이미 존재. - **플레이스홀더:** 없음(모든 스텝에 실제 코드/명령 포함). - **null 정렬:** `Sort.Order.desc(field).nullsLast()` 로 배율 null 행이 상단에 뜨지 않게 처리(Postgres 기본 NULLS FIRST 회피).