diff --git a/docs/20260623_메인_콘텐츠_랭킹_탭_API/plan-task.md b/docs/20260623_메인_콘텐츠_랭킹_탭_API/plan-task.md index 04d140b6..1db510da 100644 --- a/docs/20260623_메인_콘텐츠_랭킹_탭_API/plan-task.md +++ b/docs/20260623_메인_콘텐츠_랭킹_탭_API/plan-task.md @@ -357,7 +357,7 @@ data class AudioRankingSnapshotRecord( ### Phase 5: 스냅샷 생성 job과 분산 scheduler -- [ ] **Task 5.1: 랭킹 타입별 refresh service 구현** +- [x] **Task 5.1: 랭킹 타입별 refresh service 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingSnapshotRefreshService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingSnapshotRefreshServiceTest.kt` @@ -367,7 +367,7 @@ data class AudioRankingSnapshotRecord( - REFACTOR: 특정 타입 실패가 다른 타입 refresh를 막지 않도록 job service에서 타입 단위 실행을 분리한다. - 기대 결과: 랭킹 타입별 독립 스냅샷 생성이 가능하다. -- [ ] **Task 5.2: job service와 fallback 3회 제한 구현** +- [x] **Task 5.2: job service와 fallback 3회 제한 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingSnapshotJobService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingSnapshotJobServiceTest.kt` @@ -377,7 +377,7 @@ data class AudioRankingSnapshotRecord( - REFACTOR: job 이력 저장은 `REQUIRES_NEW` 트랜잭션 패턴을 검토해 크리에이터 랭킹 job service와 맞춘다. - 기대 결과: fallback과 scheduled refresh가 같은 job 이력 구조를 사용한다. -- [ ] **Task 5.3: 01:00~07:30 분산 scheduler 구현** +- [x] **Task 5.3: 01:00~07:30 분산 scheduler 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/scheduler/AudioRankingSnapshotScheduler.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/scheduler/AudioRankingSnapshotSchedulerTest.kt` @@ -399,7 +399,7 @@ data class AudioRankingSnapshotRecord( - REFACTOR: 순위 변화 계산은 별도 private 함수로 분리한다. - 기대 결과: 크리에이터 랭킹과 같은 의미의 `rank`, `rankChange`, `isNew`가 콘텐츠 랭킹에도 적용된다. -- [ ] **Task 6.2: 차단/성인 콘텐츠 정책 반영** +- [x] **Task 6.2: 차단/성인 콘텐츠 정책 반영** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/port/out/AudioRankingBlockPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingBlockRepository.kt` @@ -410,7 +410,7 @@ data class AudioRankingSnapshotRecord( - REFACTOR: 기존 콘텐츠 추천 탭의 성인 콘텐츠 조회 가능 여부 계산 경로를 재사용한다. - 기대 결과: 공개 조회 정책이 기존 v2 콘텐츠 추천/랭킹 정책과 어긋나지 않는다. -- [ ] **Task 6.3: 스냅샷 없음 fallback 조회 보강** +- [x] **Task 6.3: 스냅샷 없음 fallback 조회 보강** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/application/AudioRankingQueryServiceTest.kt` @@ -422,7 +422,7 @@ data class AudioRankingSnapshotRecord( ### Phase 7: 통합 검증과 문서 정리 -- [ ] **Task 7.1: controller/facade/query 통합 테스트** +- [x] **Task 7.1: controller/facade/query 통합 테스트** - Files: - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/ranking/adapter/in/web/AudioRankingControllerTest.kt` - RED: `GET /api/v2/audio/rankings?type=RISING`이 `showRankChange`, `type`, `items[].contentId`, `rank`, `rankChange`, `isNew`를 반환하는 MockMvc 테스트를 작성한다. @@ -431,7 +431,7 @@ data class AudioRankingSnapshotRecord( - REFACTOR: 공개 response에 점수, 집계 기간, fallback 여부가 노출되지 않는지 확인한다. - 기대 결과: 공개 API 계약이 end-to-end로 검증된다. -- [ ] **Task 7.2: 문서와 DDL 최종 정합성 확인** +- [x] **Task 7.2: 문서와 DDL 최종 정합성 확인** - Files: - Modify: `docs/20260623_메인_콘텐츠_랭킹_탭_API/prd.md` - Modify: `docs/20260623_메인_콘텐츠_랭킹_탭_API/plan-task.md` @@ -442,7 +442,7 @@ data class AudioRankingSnapshotRecord( - Run: `./gradlew tasks --all` - 기대 결과: PRD, 계획 문서, DDL, 코드가 같은 정책을 설명한다. -- [ ] **Task 7.3: 전체 회귀 검증** +- [x] **Task 7.3: 전체 회귀 검증** - Files: - Verify: `build.gradle.kts` - Verify: `docs/20260623_메인_콘텐츠_랭킹_탭_API/plan-task.md` @@ -492,3 +492,20 @@ data class AudioRankingSnapshotRecord( - 2026-06-24 리뷰 반영 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.ranking.*'`, `./gradlew ktlintCheck` 통과. - 2026-06-24 리뷰 재반영: `AudioRankingQueryService`가 최신/직전 visible snapshot을 조회해 `rankChange`, `isNew`, `showRankChange`를 계산하도록 구현했다. - 2026-06-24 리뷰 재반영: 비회원/비성인 조회자를 위해 스냅샷에 `isAdult`를 저장하고, snapshot refresh는 전체 상위 20개와 비성인 상위 20개 후보의 합집합을 보존하도록 변경했다. +- 2026-06-24 Phase 7 구현: `AudioRankingControllerTest`를 Spring context 기반 MockMvc 통합 테스트로 전환해 `Controller -> Facade -> QueryService -> SnapshotRepository` 경로로 `GET /api/v2/audio/rankings?type=RISING` 응답의 `showRankChange`, `type`, `contentId`, `rank`, `rankChange`, `isNew`를 검증했다. +- 2026-06-24 Phase 7 검증: 공개 response에 `finalScore`, `aggregationStartAtUtc`, `aggregationEndAtUtc`, `visibleFromAtUtc`, `fallback`이 노출되지 않음을 통합 테스트로 확인했다. +- 2026-06-24 Phase 7 문서/DDL 정합성 검증: `rg -n "visibleFromAt|visible_from_at|09:00:00|01:00:00|07:30:00|AudioRankingType" docs/20260623_메인_콘텐츠_랭킹_탭_API src/main/kotlin src/test/kotlin`, `./gradlew tasks --all` 통과. +- 2026-06-24 Phase 7 회귀 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.content.ranking.adapter.in.web.AudioRankingControllerTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.ranking.*'`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.ranking.*'`, `./gradlew ktlintCheck` 통과. +- 2026-06-24 Phase 5 구현: `AudioRankingSnapshotJobService`와 `AudioRankingSnapshotScheduler`를 추가해 타입별 scheduled/fallback job 이력, fallback 3회 제한, 타입/기간 기반 Redisson lock, 02:00~07:00 KST 분산 스케줄을 구현했다. +- 2026-06-24 Phase 6 구현: `AudioRankingBlockPort`, `DefaultAudioRankingBlockRepository`를 추가하고 `AudioRankingQueryService`가 회원 차단 관계 콘텐츠를 제외하며 최신 visible snapshot 공백 시 fallback job 실행 후 재조회하도록 보강했다. +- 2026-06-24 RED/GREEN: `AudioRankingSnapshotJobServiceTest`는 service 미존재 컴파일 실패를 확인한 뒤 GREEN 전환했고, `AudioRankingSnapshotSchedulerTest`는 scheduler 미존재 컴파일 실패를 확인한 뒤 GREEN 전환했다. `AudioRankingQueryServiceTest`는 `AudioRankingBlockPort`와 query service 의존성 미구현 컴파일 실패를 확인한 뒤 차단/fallback 구현으로 GREEN 전환했다. +- 2026-06-24 Phase 5/6 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingSnapshotRefreshServiceTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingSnapshotJobServiceTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.scheduler.AudioRankingSnapshotSchedulerTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryServiceTest` 통과. +- 2026-06-24 Phase 5/6 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.ranking.*'`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.ranking.*'`, `./gradlew ktlintCheck` 통과. 병렬 실행 중 `kaptTestKotlin`에서 `StreamCorruptedException: unexpected EOF in middle of data block`이 1회 발생했으나, 동일 content ranking 테스트 단독 재실행은 통과했다. +- 2026-06-24 Phase 5/6 리뷰 반영: snapshot refresh 실패 시 `FAILED` job 이력이 rollback되지 않도록 job 생성/상태 변경을 각각 `REQUIRES_NEW` 트랜잭션으로 분리했다. 공개 조회 fallback 실행 중 예외가 발생해도 응답 스키마를 유지하도록 보강했고, 차단 creator가 직전 스냅샷에만 있는 경우도 `rankChange` 계산에서 제외되도록 latest/previous creator 합집합 기준으로 차단 관계를 조회한다. +- 2026-06-24 Phase 5/6 리뷰 반영 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingSnapshotJobServiceTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryServiceTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.ranking.*'`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.ranking.*'`, `./gradlew ktlintCheck` 통과. +- 2026-06-24 Phase 5/6 코드 리뷰 추가 반영: class-level `@Transactional(readOnly = true)` 경계에서 snapshot replace write가 실행되지 않도록 `refreshService.refreshLastCompletedWeek(...)` 호출 자체를 `REQUIRES_NEW` 트랜잭션으로 감쌌다. fallback job 생성 이전 또는 lock/transaction 단계 예외도 추적 가능하도록 `AudioRankingQueryService`에 `event=audio_ranking_query_fallback_failure` warn 로그를 추가했다. +- 2026-06-24 Phase 5/6 코드 리뷰 추가 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingSnapshotJobServiceTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryServiceTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.ranking.*'`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.ranking.*'`, `./gradlew ktlintCheck` 통과. +- 2026-06-24 Phase 6 잔여 리스크 반영: `DefaultAudioRankingBlockRepositoryTest` DB slice 테스트를 추가해 실제 QueryDSL 양방향 활성 차단 조회, 비활성 차단 제외, 입력 목록 외 차단 제외, 빈 입력 반환을 검증하도록 보강했다. +- 2026-06-24 Phase 6 잔여 리스크 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence.DefaultAudioRankingBlockRepositoryTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.ranking.*'` 통과. +- 2026-06-24 Phase 6 트랜잭션 가시성 리뷰 반영: MySQL `REPEATABLE READ`에서 fallback `REQUIRES_NEW` 커밋 후 같은 read-only 트랜잭션 재조회가 새 스냅샷을 보지 못할 수 있어, `AudioRankingQueryService.getRankings()`의 외부 `@Transactional(readOnly = true)` 경계를 제거했다. +- 2026-06-24 Phase 6 트랜잭션 가시성 검증: `getRankings()`에 `@Transactional`이 다시 붙지 않도록 회귀 테스트를 추가했다. RED 확인 후 수정했고, `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryServiceTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.ranking.*'`, `./gradlew ktlintCheck` 통과.