# 크리에이터 랭킹 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`로 task 단위 구현을 진행한다. 각 단계는 체크박스(`- [ ]`)로 진행 상태를 갱신한다. **Goal:** 홈 내부 랭킹 탭에서 `GET /api/v2/home/rankings/creators`로 KST 기준 지난 주 크리에이터 랭킹 중 `visibleFromAt <= now` 조건을 만족하는 최신 공개 스냅샷의 상위 20명을 조회한다. **Architecture:** 공개 endpoint는 home 하위 URL을 사용하고, 클라이언트 API 표면(Controller, API 조합 Facade, DTO)은 기존 홈 API 관례에 맞춰 `kr.co.vividnext.sodalive.v2.api.home` 하위에 둔다. 랭킹 기능 본체(domain/application/port/persistence/scheduler)는 추천 기능과 분리된 `kr.co.vividnext.sodalive.v2.ranking` 하위에 둔다. 주간 스냅샷 생성 작업이 KST 기간을 UTC DB 조회 조건으로 변환해 원천 데이터를 집계하고, 조회 API는 최신 생성 스냅샷이 아니라 `visibleFromAt <= now` 조건을 만족하는 최신 공개 스냅샷을 읽어 응답을 조립한다. 단, 스냅샷 테이블이 완전히 비어 있는 초기 상태에서만 제한적 원천 데이터 fallback 집계를 시도할 수 있고, fallback 응답도 공개 노출 전환 시각을 넘긴 기간에만 허용한다. **Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring Data JPA, QueryDSL 또는 native SQL, JUnit 5, Gradle Wrapper --- ## 0. 구현 전 확정 사항 - API endpoint: `GET /api/v2/home/rankings/creators` - 랭킹 기능 본체 패키지: `kr.co.vividnext.sodalive.v2.ranking` - 홈 공개 API 조립 패키지: `kr.co.vividnext.sodalive.v2.api.home` - 집계 기준 시각: 매주 월요일 00:00:00 KST. 이 시각을 주간 집계 종료 경계로 사용한다. - 집계 기간: 생성 시점 기준 KST 지난 주 월요일 00:00:00 이상, 이번 주 월요일 00:00:00 미만 - DB 조회 기간: KST 집계 기간을 UTC 기준 `LocalDateTime` 또는 프로젝트 표준 시간 타입으로 변환한 기간 - 스냅샷 생성 스케줄 후보: 매주 월요일 KST 01:00, `@Scheduled(cron = "0 0 1 * * MON", zone = "Asia/Seoul")` - 스냅샷 노출 전환 시각: 매주 월요일 KST 09:00. 스냅샷과 job 이력에 `visibleFromAt`으로 저장한다. - 현재 기본 크리에이터 랭킹 타입: `WEEKLY`. 스냅샷과 job 이력에 `rankingType`으로 저장한다. - 다중 서버 인스턴스에서 스냅샷 스케줄러가 중복 실행되지 않도록 기존 Redisson 기반 분산 lock을 사용한다. - 랭킹 스냅샷 lock key는 `lock:creator-ranking-snapshot-refresh`로 고정하고, lock 획득 실패 인스턴스는 정상 skip한다. - 조회 API는 스냅샷 기반 응답을 기본으로 하며, 최신 생성 스냅샷이 아니라 `visibleFromAt <= now` 조건을 만족하는 최신 공개 스냅샷을 응답한다. - 조회 시 09:00 KST 전에는 01:00 KST에 생성된 새 주차 스냅샷이 있어도 직전 공개 스냅샷을 유지한다. - 스냅샷 테이블이 완전히 비어 있는 초기 상태에서만 제한적 원천 데이터 fallback 집계를 시도할 수 있다. - fallback 응답도 fallback 대상 기간의 `visibleFromAt <= now` 조건을 만족할 때만 공개한다. - 스냅샷 테이블에 과거 스냅샷이 하나라도 있으면 원천 데이터 fallback을 시도하지 않고 기존 최신 공개 스냅샷 기준 응답을 유지한다. - 스냅샷 테이블이 완전히 비어 있는 cold-start fallback 성공 시 조회 API는 fallback 응답을 반환하고, 같은 집계 기간의 스냅샷 생성은 조회 서비스가 직접 저장하지 않고 `CreatorRankingSnapshotJobService`/`CreatorRankingSnapshotRefreshService` 책임으로 위임한다. - cold-start fallback 스냅샷 생성 트리거는 운영 배포 직후 내부 테스트 등 초기 검증 보강책이며, 동일 집계 기간에 대해 한 번만 실행되도록 기간 기반 Redisson lock을 사용한다. - 스냅샷 생성 직전 집계 시작/종료 시각을 포함한 job 이력을 생성하고, 스케줄 실행과 관리자 수동 생성 모두 성공/실패 상태를 기록한다. - 관리자는 날짜 범위를 직접 선택해 스냅샷 생성 job을 만들 수 있으며, 실패한 job은 관리자 전용 재시도 API로 대기 상태로 되돌려 재처리할 수 있어야 한다. - 스냅샷은 현재 누적 저장하며, 보존 기간/정리 배치는 운영 데이터 규모 확인 후 별도 결정한다. - API 응답은 `showRankChange`, `items[].rank`, `items[].rankChange`, `items[].isNew`, `items[].creatorId`, `items[].nickname`, `items[].profileImageUrl`만 포함한다. - API 응답에는 집계 기간 날짜와 `finalScore`를 포함하지 않는다. - raw value 방식으로 계산하며 0~100 정규화는 하지 않는다. - 스냅샷 저장 대상은 20위 점수보다 높은 후보와 20위 점수에 동점인 후보 전체로 제한한다. - 동점자는 조회 시 랜덤 정렬로 상위 20명을 추출하고, 별도 `randomTieBreaker`는 저장하지 않는다. - 직전 공개 스냅샷이 없으면 `showRankChange=false`, `rankChange=null`, `isNew=false`로 응답한다. - 비활성 및 탈퇴 크리에이터는 랭킹에 노출하지 않는다. - 차단 관계가 있으면 row는 유지하되 `creatorId=0`, `nickname=""`, `profileImageUrl=기본 이미지 URL`로 마스킹한다. - 신규 팔로우 수는 `CreatorFollowing.createdAt` 기준, 언팔로우 수는 `CreatorFollowing.isActive == false` 및 `CreatorFollowing.updatedAt` 기준으로 계산한다. --- ## 1. 파일 구조 계획 ### 신규 ranking domain/application - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreSpec.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingSnapshotCandidate.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingItem.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingAggregationPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingBlockPort.kt` ### 신규 홈 API 조립 계층 - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/ranking/CreatorRankingResponse.kt` ### 신규 scheduler / persistence - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshot.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingBlockRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotJob.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotJobRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotJobPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobService.kt` ### 신규 관리자 API - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobResponse.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobRequest.kt` ### 문서 산출물 - Create: `docs/20260608_크리에이터_랭킹/create-ranking-tables.sql` - Modify: `docs/20260608_크리에이터_랭킹/plan-task.md` ### 테스트 - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt` --- ## 1.1 DDL 영향도: `visible_from_at`, `ranking_type` - `creator_ranking_snapshot`에는 `ranking_type varchar(30) not null`, `visible_from_at timestamp not null`을 추가한다. - `creator_ranking_snapshot_job`에는 `ranking_type varchar(30) not null`, `visible_from_at timestamp not null`을 추가한다. - 현재 기본 타입 값은 `WEEKLY`로 문서화하고, 코드 구현 시 `CreatorRankingType` 또는 동등한 enum/상수로 고정한다. - `visible_from_at`은 집계 종료일 월요일 09:00:00 KST를 UTC로 변환한 값이다. 예: 2026-06-08 09:00:00 KST는 2026-06-08 00:00:00 UTC다. - `docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`의 기존 CREATE DDL은 이미 적용된 기준으로 유지하고, 하단에 운영 반영용 ALTER DDL을 추가한다. - 운영 DB 변경은 `ADD nullable column -> backfill -> MODIFY NOT NULL -> index 보강/교체` 순서로 적용한다. - backfill은 `ranking_type='WEEKLY'`, `visible_from_at=aggregation_end_at_utc + interval 9 hour` 기준으로 수행한다. - 같은 타입/기간 재생성 삭제 기준은 `ranking_type + aggregation_start_at_utc + aggregation_end_at_utc`다. - 중복 방지 기준은 `ranking_type + aggregation_start_at_utc + aggregation_end_at_utc + creator_id` unique index다. - 최신 공개 스냅샷 조회는 `ranking_type = WEEKLY and visible_from_at <= nowUtc` 조건에서 가장 큰 `visible_from_at`을 찾은 뒤 해당 스냅샷 row를 `final_score desc` 기준으로 읽는다. - 직전 공개 스냅샷 조회는 최신 공개 스냅샷보다 작은 `visible_from_at` 중 가장 큰 값을 기준으로 읽는다. - job 목록/재시도 조회는 `ranking_type + aggregation period + status`, `ranking_type + visible_from_at + status`, `ranking_type + aggregation period + trigger_type + created_at` 인덱스를 사용한다. - 공개 API 응답 DTO에는 `rankingType`, `visibleFromAt`, 집계 기간, fallback 여부를 노출하지 않는다. ### Phase 1: 기간/점수 도메인 정책 - [x] **Task 1.1: KST 주간 기간 산출과 UTC 조회 기간 변환 정책 작성** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicy.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicyTest.kt` - RED: 월요일 KST 기준 지난 주 기간, 월/연도 경계, 서버 timezone UTC와 무관한 기간 산출, KST 2026-06-01 00:00:00~2026-06-08 00:00:00이 UTC 2026-05-31 15:00:00~2026-06-07 15:00:00으로 변환되는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicyTest` - GREEN: `CreatorRankingPeriodPolicy.resolveLastCompletedWeek(now: ZonedDateTime)`와 `toUtcRange(period)`를 구현한다. - REFACTOR: 기간 경계는 종료 미만(`< end`) 조건으로 사용할 수 있도록 `startInclusiveUtc`, `endExclusiveUtc` 명칭을 유지한다. - 기대 결과: KST 기준 기간 산출과 UTC 변환이 테스트로 고정된다. - [x] **Task 1.2: raw value 기반 점수 정책 작성** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreSpec.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicy.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicyTest.kt` - RED: 콘텐츠/라이브 점수, 참여 반응 점수, 응원 점수, 팬 충성도 점수, 최종 점수 산식 테스트를 작성한다. 0~100 정규화 없이 캔/건수/팔로우 원천값이 그대로 가중합되는지 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest` - GREEN: 가중치 상수와 `calculateContentLiveScore`, `calculateEngagementScore`, `calculateSupportScore`, `calculateFanLoyaltyScore`, `calculateFinalScore`를 구현한다. - REFACTOR: 소수 계산 비교는 `assertEquals(expected, actual, 0.0001)` 기준을 사용한다. - 기대 결과: PRD의 raw value 정책과 음수 팔로우 증가 반영이 테스트로 고정된다. - [x] **Task 1.3: 스냅샷 후보/응답 내부 모델 작성** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingSnapshotCandidate.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingItem.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - RED: `rankChange` 양수/음수/null과 `isNew`를 담을 수 있는 내부 item 모델이 없으면 컴파일 실패하는 테스트 골격을 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` - GREEN: 스냅샷 후보와 조회 item 내부 모델을 작성한다. - REFACTOR: API DTO와 domain model을 분리해 Controller가 persistence entity에 의존하지 않도록 한다. - 기대 결과: 이후 service/controller task가 같은 타입을 재사용할 수 있다. ### Phase 2: 스냅샷 저장소와 DDL - [x] **Task 2.1: 랭킹 스냅샷 엔티티/리포지토리 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshot.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt` - RED: 같은 집계 기간의 스냅샷 replace, 최신 완료 주차 조회, 직전 완료 주차 조회, 20위 경계 동점 후보 저장 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest` - GREEN: 스냅샷 엔티티에 `aggregationStartAtUtc`, `aggregationEndAtUtc`, `creatorId`, `finalScore`, 카테고리별 점수, 원천 지표, `createdAt`을 저장한다. 저장 전 같은 기간 row를 삭제하고 새 후보를 저장한다. - REFACTOR: 스냅샷 조회 port는 domain model만 반환하고 JPA entity를 application 계층으로 노출하지 않는다. - 기대 결과: 같은 기간 재생성 시 중복 노출되지 않고 최신/직전 주차를 구분해 조회할 수 있다. - [x] **Task 2.2: 운영 DB 반영용 스냅샷 DDL 문서 작성** - Files: - Create: `docs/20260608_크리에이터_랭킹/create-ranking-tables.sql` - Modify: `docs/20260608_크리에이터_랭킹/plan-task.md` - RED: 테스트 작성 예외. `TDD 예외 사유`: SQL 운영 반영 문서 작성 task로, 실행 대상 DB가 현재 workspace에 없다. - 대체 검증 방법: `rg -n "creator_ranking_snapshot|aggregation_start_at_utc|creator_id|final_score" docs/20260608_크리에이터_랭킹/create-ranking-tables.sql` - GREEN: `creator_ranking_snapshot` 테이블 생성 SQL, 기간/점수 조회용 index, 같은 기간 재생성 시 삭제 기준을 문서에 작성한다. - REFACTOR: 컬럼명은 JPA entity와 1:1로 대응하도록 정리한다. - 기대 결과: 운영 배포 전 DB 테이블 생성 SQL을 검토할 수 있다. ### Phase 3: 원천 지표 집계 repository - [x] **Task 3.1: 콘텐츠/라이브 캔 집계 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingAggregationPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt` - RED: `CanUsage.DONATION`, `LIVE`, `SPIN_ROULETTE`는 라이브 계열 캔으로, `ORDER_CONTENT`는 콘텐츠 구매 캔으로 집계되고 환불 row가 제외되는 repository 통합 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` - GREEN: KST에서 변환한 UTC 기간으로 `UseCan` 계열 데이터를 조회하고 크리에이터별 캔 합계를 반환한다. - REFACTOR: can usage 조건은 private 함수 또는 enum set으로 분리해 산식과 조회 조건이 섞이지 않도록 한다. - 기대 결과: 콘텐츠/라이브 카테고리의 원천 지표가 정확히 집계된다. - [x] **Task 3.2: 콘텐츠 좋아요/댓글 반응 집계 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt` - RED: 활성 콘텐츠 좋아요 수, 댓글+대댓글 수, 크리에이터 본인 댓글/대댓글 제외, 비활성/삭제 정책 제외를 검증하는 repository 통합 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` - GREEN: `AudioContentLike`, `AudioContentComment`, `AudioContent`를 기준으로 크리에이터별 좋아요/댓글 원천 지표를 반환한다. - REFACTOR: 댓글 작성자가 콘텐츠 소유 크리에이터와 같은 경우 제외하는 조건을 테스트 fixture 이름에 드러나게 정리한다. - 기대 결과: 참여 반응 점수 입력값이 PRD 조건과 일치한다. - [x] **Task 3.3: 채널 후원/팬 Talk 응원 집계 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt` - RED: `CanUsage.CHANNEL_DONATION` 캔 합계와 건수, 환불 제외, `CreatorCheers` 최상위 row만 팬 Talk로 집계하고 답글은 제외하는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` - GREEN: 채널 후원 원천 지표와 팬 Talk 원천 지표를 크리에이터별로 반환한다. - REFACTOR: 팬 Talk 답글 제외 조건은 `parent is null` 또는 기존 엔티티 구조에 맞는 조건으로 명확히 둔다. - 기대 결과: 응원 점수 입력값이 캔/건수/최상위 팬 Talk 기준으로 집계된다. - [x] **Task 3.4: 팔로우 최종 수/증가 수 집계 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt` - RED: 최종 팔로우 수는 기간 종료 시점 활성 row, 신규 팔로우 수는 `createdAt` 기간 내, 언팔로우 수는 `isActive=false` 및 `updatedAt` 기간 내, 기간 내 재팔로우는 신규/언팔로우 이벤트로 별도 복원하지 않는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` - GREEN: `CreatorFollowing` 기준 최종 팔로우 수와 팔로우 증가 수를 반환한다. - REFACTOR: 현재 row만으로 계산하는 정책 한계를 테스트명과 주석 한 줄로 남긴다. - 기대 결과: 팬 충성도 점수 입력값이 PRD의 `createdAt`/`updatedAt` 정책과 일치한다. - [x] **Task 3.5: 랭킹 후보 통합 집계와 비활성/탈퇴 제외 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt` - RED: 여러 원천 지표를 크리에이터별로 합쳐 후보를 만들고, 비활성/탈퇴 크리에이터와 최종 점수 1점 미만 후보가 제외되는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` - GREEN: 원천 지표 aggregate를 크리에이터 id 기준으로 합쳐 `CreatorRankingSnapshotCandidate`를 반환한다. - REFACTOR: 복잡한 집계가 QueryDSL로 과도해지면 native SQL을 사용하되, 테스트로 H2 호환성을 고정한다. - 기대 결과: 스냅샷 생성 서비스가 별도 원천 조회를 여러 번 조합하지 않고 후보 목록을 받을 수 있다. ### Phase 4: 스냅샷 생성 서비스와 스케줄러 - [x] **Task 4.1: 주간 스냅샷 생성 서비스 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt` - RED: KST 기간 산출, UTC 조회 기간 전달, raw value 점수 계산, 20위 점수 경계 동점 후보 전체 저장, 같은 기간 replace를 검증하는 service 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest` - GREEN: aggregation port에서 후보를 조회하고 score policy로 점수를 계산한 뒤 저장 대상 후보만 snapshot port에 저장한다. - REFACTOR: service는 계산 흐름만 담당하고 DB 조회 조건/저장 구현은 port 뒤로 숨긴다. - 기대 결과: 스냅샷 저장 대상이 “20위 초과 점수 + 20위 동점 전체” 규칙을 만족한다. - [x] **Task 4.2: 매주 월요일 07:30 KST 스케줄러 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt` - RED: scheduler method에 `@Scheduled(cron = "0 30 7 * * MON", zone = "Asia/Seoul")`가 선언되어 있는지 reflection 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest` - GREEN: 스케줄러가 `CreatorRankingSnapshotRefreshService.refreshLastCompletedWeek()`를 호출하도록 구현한다. - REFACTOR: 스케줄러에는 기간/점수/DB 로직을 두지 않는다. - 기대 결과: 주간 스냅샷 생성 트리거가 KST 기준으로 고정된다. - [x] **Task 4.3: 주간 스냅샷 스케줄러 Redisson lock 적용** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt` - RED: Redisson lock 획득 성공 시 `CreatorRankingSnapshotRefreshService.refreshLastCompletedWeek()`를 1회 호출하고, 획득 실패 시 호출하지 않는 테스트를 작성한다. lock key가 `lock:creator-ranking-snapshot-refresh`인지도 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest` - GREEN: 기존 `RedissonClient` bean을 스케줄러에 주입하고 `tryLock`으로 lock을 획득한 인스턴스만 refresh service를 호출한다. lock 획득 실패는 정상 skip으로 처리한다. - REFACTOR: DB 기반 scheduler lock 테이블은 추가하지 않고, 기존 `AudioContentReleaseScheduledTask`의 Redisson lock 패턴을 참고하되 스케줄러에는 lock 획득/해제와 service 호출만 둔다. - 기대 결과: 여러 서버 인스턴스에서 같은 cron이 동시에 실행돼도 클러스터 전체에서 한 인스턴스만 주간 랭킹 스냅샷을 생성한다. ### Phase 5: 조회 서비스, 순위 변화, 차단 마스킹 - [x] **Task 5.1: 최신/직전 스냅샷 기반 조회 서비스 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - RED: 최신 완료 주차 스냅샷 없음 빈 결과, 직전 주차 없음 `showRankChange=false`, 직전 주차 있음 `rankChange` 양수/음수/null 및 `isNew` 계산 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` - GREEN: 최신 스냅샷 후보를 최종 점수 내림차순과 동점 랜덤 정렬로 최대 20명 선정하고, 직전 스냅샷 순위와 비교해 순위 변화를 계산한다. - REFACTOR: 동점 랜덤으로 인해 같은 동점 구간의 순위 변화가 조회마다 달라질 수 있음을 테스트에서 허용 범위로 표현한다. - 기대 결과: 홈 API Facade가 사용할 `showRankChange`와 item 목록이 ranking application service에서 완성된다. - [x] **Task 5.2: 차단 관계 마스킹 port 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingBlockPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingBlockRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - RED: 조회자와 랭킹 크리에이터 사이에 차단 관계가 있으면 row는 유지되고 `creatorId=0`, `nickname=""`, `profileImageUrl=기본 이미지 URL`로 마스킹되는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` - GREEN: block port로 차단 대상 creator id를 조회하고, service에서 응답 item을 마스킹한다. - REFACTOR: 기본 이미지 URL은 기존 프로젝트 상수/설정이 있으면 재사용하고, 없으면 ranking service 내부 상수로 분리한다. - 기대 결과: 차단 관계가 있어도 순위 row 수는 유지되고 개인 식별 정보만 가려진다. ### Phase 6: 홈 API endpoint, Facade, DTO - [x] **Task 6.1: 랭킹 조회 DTO, 홈 API Facade, Controller 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/ranking/CreatorRankingResponse.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt` - RED: `GET /api/v2/home/rankings/creators`가 `showRankChange`, `items[].rank`, `rankChange`, `isNew`, `creatorId`, `nickname`, `profileImageUrl`만 반환하고 날짜와 `finalScore`를 반환하지 않는 controller 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest` - GREEN: controller, API Facade, response DTO를 구현하고 Facade가 `CreatorRankingQueryService`를 호출해 홈 API 응답으로 변환한다. - REFACTOR: URL과 클라이언트 API 표면은 `v2.api.home` 하위에 두고, 랭킹 DTO는 `v2.api.home.dto.ranking` 하위에 둔다. 랭킹 계산/조회 본체는 `v2.ranking`에 유지한다. - 기대 결과: 클라이언트 홈 랭킹 탭에서 사용할 공개 API 계약이 테스트로 고정된다. - [x] **Task 6.2: 인증/비인증 조회와 차단 마스킹 연결** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt` - RED: 비회원 조회는 기본 랭킹을 반환하고, 인증 회원 조회는 차단 관계 마스킹을 적용하는 controller 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest` - GREEN: 기존 인증 주입 패턴을 확인해 member nullable 흐름을 service에 전달한다. - REFACTOR: 기존 API 응답 wrapper 관례와 상태 코드를 맞춘다. - 기대 결과: 인증 여부에 따라 차단 마스킹만 달라지고 endpoint 계약은 동일하다. ### Phase 7: 관측/문서/회귀 검증 - [x] **Task 7.1: 스냅샷 생성/조회 로그 추가** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - RED: 스냅샷 생성 성공/실패, 후보 수, 저장 수, 조회 성공/실패 로그가 남는지 output capture 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` - GREEN: 기존 프로젝트 관례대로 `LoggerFactory` 기반 구조화 로그를 추가한다. - REFACTOR: 로그에 개인정보를 직접 남기지 않고 creator id/count/period만 남긴다. - 기대 결과: PRD metrics 확인에 필요한 최소 로그가 남는다. - [x] **Task 7.2: 전체 ranking 테스트와 포맷 검증** - Files: - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/**` - Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/**` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/**` - Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/**` - Modify: `docs/20260608_크리에이터_랭킹/plan-task.md` - RED: 테스트 작성 예외. `TDD 예외 사유`: 구현 완료 후 회귀 검증 task다. - 대체 검증 방법: - `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` - `./gradlew ktlintCheck` - `./gradlew test` - GREEN: 실패하는 테스트가 있으면 해당 phase task로 돌아가 수정하고, 모든 명령을 통과시킨다. - REFACTOR: plan-task 하단 검증 기록에 실행 명령, 목적, 결과를 누적한다. - 기대 결과: ranking 기능 본체와 홈 API 조립 계층 테스트, 포맷, 전체 회귀 테스트가 통과한다. ### Phase 8: 스냅샷 job 이력과 스케줄 기록 - [x] **Task 8.1: 스냅샷 job 이력 모델/DDL 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotJob.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotJobRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotJobPort.kt` - Modify: `docs/20260608_크리에이터_랭킹/create-ranking-tables.sql` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepositoryTest.kt` - RED: 집계 시작/종료 시각, 실행 트리거, 상태(`PENDING`, `PROCESSING`, `DONE`, `FAILED`), 실패 사유, 처리 시작/완료 시각을 저장하고 조회할 수 있는 repository 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotJobRepositoryTest` - GREEN: 기존 `charge_event_job` 관례를 참고해 스냅샷 job entity/repository/port와 운영 반영용 DDL을 작성한다. - REFACTOR: 컬럼명은 관리자 목록과 worker 처리에 필요한 최소 필드로 제한하고 공개 API DTO와 분리한다. - 기대 결과: 스냅샷 생성 이력이 기간/상태 기준으로 추적 가능해진다. - [x] **Task 8.2: 스케줄 실행 전 job 생성과 성공/실패 기록 연결** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobServiceTest.kt` - RED: 스케줄러가 스냅샷 생성 직전 집계 기간을 포함한 `SCHEDULED` job을 만들고, refresh 성공 시 `DONE`, 예외 발생 시 `FAILED`와 실패 사유를 기록하는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` - GREEN: 스케줄러는 lock 획득 후 job service를 통해 job 생성/실행/상태 기록을 위임하고, refresh service는 기존 스냅샷 생성 책임을 유지한다. - REFACTOR: lock 획득 실패는 job 실패로 기록하지 않고 기존 정상 skip 정책을 유지한다. - 기대 결과: 매주 스케줄 실행 여부와 성공/실패가 관리자에서 추적 가능한 job 이력으로 남는다. ### Phase 9: 관리자 수동 생성과 실패 job 재시도 API - [x] **Task 9.1: 관리자 날짜 범위 수동 생성 API 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobRequest.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobResponse.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobControllerTest.kt` - RED: `POST /admin/rankings/creators/snapshot-jobs`가 관리자 권한에서 날짜 범위를 받아 `MANUAL` job을 생성하고, 비관리자 요청은 거부되는 controller/service 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.ranking.creator.AdminCreatorRankingSnapshotJobControllerTest` - GREEN: 기존 관리자 API 관례대로 `@PreAuthorize("hasRole('ADMIN')")`와 `ApiResponse.ok(...)`를 사용해 수동 생성 job id와 상태를 반환한다. - REFACTOR: 날짜 범위 validation은 KST 주차/UTC 변환 정책과 중복되지 않도록 application service에 모은다. - 기대 결과: 운영자가 별도 DB 확인 없이 필요한 날짜 범위의 스냅샷 생성을 요청할 수 있다. - [x] **Task 9.2: 관리자 job 목록/실패 job 재시도 API 추가** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/ranking/creator/AdminCreatorRankingSnapshotJobControllerTest.kt` - RED: `GET /admin/rankings/creators/snapshot-jobs`가 날짜 범위/상태/실패 사유/재시도 가능 여부를 반환하고, `POST /admin/rankings/creators/snapshot-jobs/{jobId}/retry`가 `FAILED` job만 `PENDING`으로 되돌리는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.ranking.creator.AdminCreatorRankingSnapshotJobControllerTest` - GREEN: 기존 `AdminChargeEventJobController`/`AdminChargeEventJobService` 패턴을 참고해 관리자 목록과 재시도 API를 구현한다. - REFACTOR: `PENDING`, `PROCESSING`, `DONE` 상태 job은 재시도 대상으로 변경하지 않고 명확한 실패 응답을 반환한다. - 기대 결과: 실패한 스냅샷 job을 관리자 버튼/API로 재시도할 수 있다. ### Phase 10: 스냅샷 완전 공백 fallback - [x] **Task 10.1: 스냅샷 테이블 완전 공백 여부 조회 port 추가** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt` - RED: 스냅샷 row가 하나도 없을 때만 true를 반환하고, 과거 주차 스냅샷이 하나라도 있으면 false를 반환하는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest` - GREEN: snapshot port에 `isSnapshotTableEmpty()` 또는 동등한 메서드를 추가해 조회 서비스가 fallback 조건을 판단할 수 있게 한다. - REFACTOR: “최신 주차 스냅샷 없음”과 “테이블 완전 공백”을 서로 다른 조건으로 유지한다. - 기대 결과: cold-start fallback이 과거 스냅샷 존재 시 실행되지 않도록 조건이 고정된다. - [x] **Task 10.2: 조회 API cold-start fallback 연결** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - RED: 최신 스냅샷이 없고 스냅샷 테이블이 완전히 비어 있을 때만 fallback 집계를 시도하고, 과거 스냅샷이 있으면 fallback을 시도하지 않는 테스트를 작성한다. 공개 응답 스키마가 `showRankChange`와 `items`로 유지되는지도 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` - GREEN: query service가 snapshot-first 흐름을 유지하면서 완전 공백 상태에서만 제한적 fallback 집계를 호출하고 결과를 기존 ranking result로 변환한다. - REFACTOR: fallback은 장기 실시간 랭킹 경로가 아니라 초기 스냅샷 부재 안전장치임을 service 경계와 테스트명에 드러낸다. - 기대 결과: 초기 운영 상태에서는 빈 화면을 줄이고, 운영 중에는 기존 스냅샷 기반 정책을 유지한다. - [x] **Task 10.3: fallback/job 관측 로그와 회귀 검증** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobServiceTest.kt` - RED: fallback 시도/성공/실패와 job 상태 변경 로그가 남는지 output capture 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` - GREEN: 개인정보 없이 period, jobId, trigger, status, count, elapsedMs 중심의 구조화 로그를 추가한다. - REFACTOR: 기존 Phase 7 로그와 이벤트명 충돌이 없도록 prefix를 정리한다. - 기대 결과: 관리자 job과 cold-start fallback 상태를 운영 로그/메트릭으로 추적할 수 있다. ### Phase 11: cold-start fallback 스냅샷 생성 트리거 - [x] **Task 11.1: cold-start fallback 전용 기간 기반 lock 실행 경계 추가** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobServiceTest.kt` - RED: 스냅샷 테이블이 완전히 비어 있는 초기 상태에서 같은 KST 지난 주 기간에 대해 lock을 획득한 경우에만 refresh 책임을 실행하고, lock 획득 실패 시 refresh를 호출하지 않는 테스트를 작성한다. lock key는 집계 시작/종료 UTC 시각을 포함한 `lock:creator-ranking-snapshot-refresh:{start}:{end}` 형식으로 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` - GREEN: `CreatorRankingSnapshotJobService`에 `ensureLastCompletedWeekSnapshotForColdStart()` 또는 동등한 메서드를 추가한다. 이 메서드는 `CreatorRankingPeriodPolicy`로 기간을 산출하고, Redisson lock을 `tryLock(0, -1, TimeUnit.SECONDS)`로 획득한 경우에만 기존 refresh service를 호출한다. - REFACTOR: 조회 API가 직접 `creator_ranking_snapshot`을 저장하지 않도록 하고, lock 획득/해제와 refresh 위임 책임은 job service에 둔다. 스케줄러의 고정 lock key 정책은 유지하고, cold-start 전용 메서드에서만 기간 기반 lock key를 사용한다. - 기대 결과: 운영 배포 직후 내부 테스트 등 초기 cold-start 상황에서 같은 기간 스냅샷 생성이 중복 실행되지 않는다. - [x] **Task 11.2: fallback 성공 후 스냅샷 생성 책임 위임 연결** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobServiceTest.kt` - RED: `getCreatorRankings()`가 최신 스냅샷 없음 + 스냅샷 테이블 완전 공백 상태에서 fallback 결과를 응답하면서 cold-start 스냅샷 생성 위임 메서드를 호출하는 테스트를 작성한다. 과거 스냅샷이 있거나 fallback 후보가 없으면 cold-start 생성 위임을 호출하지 않는 테스트도 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` - GREEN: query service는 fallback 응답 조립 후 job service에 스냅샷 생성 책임을 위임한다. 위임 실패는 공개 API 응답을 깨지 않도록 catch 후 구조화 로그로 남기고, fallback 응답 스키마는 `showRankChange`와 `items` 그대로 유지한다. - REFACTOR: fallback은 장기 실시간 랭킹 경로가 아니라 초기 상태 보강책임을 테스트명과 로그 이벤트명에 드러낸다. - 기대 결과: 첫 내부 조회에서 fallback 응답을 내려주면서 이후 조회가 스냅샷 기반으로 전환될 수 있다. - [x] **Task 11.3: cold-start 스냅샷 생성 트리거 회귀 검증** - Files: - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/**` - Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/**` - Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt` - Modify: `docs/20260608_크리에이터_랭킹/plan-task.md` - RED: 테스트 작성 예외. `TDD 예외 사유`: 구현 완료 후 회귀 검증 task다. - 대체 검증 방법: - `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` - `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` - `./gradlew ktlintCheck` - GREEN: cold-start fallback, 스케줄러, 관리자 job, 차단 마스킹, CDN profile image 응답 테스트가 모두 통과해야 한다. - REFACTOR: 검증 기록에 실행 명령, 목적, 결과를 누적한다. - 기대 결과: cold-start 스냅샷 생성 보강이 기존 스케줄/관리자/조회 경로를 깨지 않는다. ### Phase 12: 크리에이터 랭킹 시간 정책 변경 > Phase 1~11은 완료 당시의 구현 이력이다. 시간 정책 변경은 완료된 task를 다시 수행하는 방식이 아니라, Phase 12에서 기존 07:30 생성 스케줄, 최신 완료 주차 조회, 기존 DDL/엔티티/port 구조를 `01:00 생성 후보`, `09:00 노출 전환`, `visibleFromAt <= now` 최신 공개 스냅샷 조회 기준으로 변경한다. - [x] **Task 12.1: 집계/생성/노출 시각 분리 정책 추가** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicy.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicyTest.kt` - RED: 월요일 00:00:00 KST를 집계 종료 경계로 유지하고, 집계 종료일 월요일 09:00:00 KST가 `visibleFromAtUtc`로 변환되는 테스트를 작성한다. 2026-06-08 09:00:00 KST가 2026-06-08 00:00:00 UTC로 변환되는지 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicyTest` - GREEN: `resolveVisibleFromAtUtc(aggregationEndAtKst)` 또는 동등한 메서드를 추가하고, 기존 집계 기간 산출은 변경하지 않는다. - REFACTOR: 생성 후보 시각(01:00 KST)은 scheduler 책임으로 두고, period policy는 집계 기간과 공개 노출 시각 계산에 집중한다. - 기대 결과: 집계 기준 시각과 공개 노출 전환 시각이 코드와 테스트에서 분리된다. - [x] **Task 12.2: `rankingType`, `visibleFromAt` 스냅샷/job 저장 구조 반영** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshot.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotJob.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotJobPort.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepository.kt` - Modify: `docs/20260608_크리에이터_랭킹/create-ranking-tables.sql` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepositoryTest.kt` - RED: 스냅샷과 job record가 `rankingType=WEEKLY`, `visibleFromAtUtc`를 저장하고, 같은 타입/기간/크리에이터 중복 저장이 불가능하며, 같은 타입/기간 replace가 기존 row를 제거하는 repository 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotJobRepositoryTest` - GREEN: entity/record/port에 `rankingType`, `visibleFromAtUtc`를 추가하고, 운영 DB 변경용 ALTER DDL을 문서화한다. 기본 타입 `WEEKLY`를 생성/조회 경로에 전달한다. - REFACTOR: DDL 컬럼명은 `ranking_type`, `visible_from_at`으로 유지하고, Kotlin 필드명은 기존 시간 필드 관례에 맞춰 `visibleFromAtUtc`로 둔다. - 기대 결과: 스냅샷과 job 이력이 공개 노출 기준으로 조회될 수 있는 데이터를 가진다. - [x] **Task 12.3: 스냅샷 생성 스케줄을 월요일 01:00 KST로 변경** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobServiceTest.kt` - RED: scheduler method에 `@Scheduled(cron = "0 0 1 * * MON", zone = "Asia/Seoul")`가 선언되어 있는지 검증하고, 스케줄 job이 `visibleFromAtUtc`를 월요일 09:00 KST 기준으로 저장하는 테스트를 작성한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` - GREEN: 기존 07:30 cron을 01:00 cron으로 변경하고, refresh/job 생성 경로에 `visibleFromAtUtc`를 전달한다. - REFACTOR: lock key는 기존 중복 실행 방지 정책을 유지하되, 기간 기반 lock 내부에서 `rankingType`이 필요한 경우 lock key에 포함할지 테스트로 고정한다. - 기대 결과: 생성 후보 시각이 집계 종료 1시간 뒤로 당겨져도 공개 노출은 09:00까지 지연된다. - [x] **Task 12.4: 조회 API를 최신 생성 스냅샷이 아닌 최신 공개 스냅샷 기준으로 변경** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - RED: 01:00 KST에 새 스냅샷이 생성되어도 08:59:59 KST 조회는 직전 공개 스냅샷을 반환하고, 09:00:00 KST 조회는 새 스냅샷을 반환하는 테스트를 작성한다. 직전 공개 스냅샷 기준 `rankChange`, `isNew`, `showRankChange` 계산도 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` - GREEN: snapshot port에 `findLatestVisibleSnapshots(rankingType, nowUtc)`와 `findPreviousVisibleSnapshots(rankingType, nowUtc)` 또는 동등한 메서드를 추가하고, query service가 이 메서드만 사용하도록 변경한다. - REFACTOR: 기존 `findLatestSnapshots()`/`findPreviousCompletedSnapshots()`가 더 이상 공개 조회에 쓰이지 않으면 제거하거나 관리자/테스트 전용으로 명확히 제한한다. - 기대 결과: 공개 API가 latest generated가 아니라 latest visible 스냅샷만 응답한다. - [x] **Task 12.5: cold-start fallback 공개 노출 조건 보강과 회귀 검증** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryServiceTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobServiceTest.kt` - RED: 스냅샷 테이블이 완전히 비어 있어도 fallback 대상 기간의 `visibleFromAtUtc > nowUtc`이면 새 주차 결과를 응답하지 않는 테스트를 작성한다. `visibleFromAtUtc <= nowUtc`이면 기존 fallback 응답과 스냅샷 생성 위임이 유지되는지도 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` - GREEN: fallback 집계 전에 공개 가능 여부를 검사하고, 공개 불가 시 빈 응답 또는 직전 공개 스냅샷 응답을 유지한다. - REFACTOR: 공개 API 응답 DTO에는 `visibleFromAtUtc`, `rankingType`, fallback 여부를 추가하지 않는다. - 기대 결과: 초기 상태 보강책도 09:00 공개 전환 정책을 우회하지 않는다. - [x] **Task 12.6: 시간 정책 변경 문서/DDL 정합성 검증** - Files: - Verify: `docs/20260608_크리에이터_랭킹/prd.md` - Verify: `docs/20260608_크리에이터_랭킹/plan-task.md` - Verify: `docs/20260608_크리에이터_랭킹/create-ranking-tables.sql` - Modify: `docs/20260608_크리에이터_랭킹/plan-task.md` - RED: 테스트 작성 예외. `TDD 예외 사유`: 문서와 DDL 변경 범위 검증 task다. - 대체 검증 방법: - `rg -n "07:30|01:00|09:00|visibleFromAt|visible_from_at|rankingType|ranking_type|latest visible|최신 공개|최신 생성" docs/20260608_크리에이터_랭킹` - `rg -n "visible_from_at|ranking_type|creator_ranking_snapshot|creator_ranking_snapshot_job" docs/20260608_크리에이터_랭킹/create-ranking-tables.sql` - `./gradlew tasks --all` - GREEN: 문서에 남은 07:30 표현은 과거 검증 기록 또는 기존 정책 언급인지 확인하고, 현재 목표/신규 task에는 01:00 생성과 09:00 노출 전환만 남긴다. - REFACTOR: 검증 기록에 실행 명령, 목적, 결과를 누적한다. - 기대 결과: PRD, 구현 계획, DDL이 같은 시간 정책과 공개 조회 기준을 설명한다. --- ## 2. PRD 요구사항 추적 - Feature A: Task 1.1, Task 4.1에서 KST 기간 산출과 UTC DB 조회 변환을 검증한다. - Feature B: Task 1.2, Task 3.1, Task 4.1에서 콘텐츠/라이브 raw can 산식을 검증한다. - Feature C: Task 1.2, Task 3.2, Task 4.1에서 좋아요/댓글/대댓글 및 크리에이터 본인 댓글 제외를 검증한다. - Feature D: Task 1.2, Task 3.3, Task 4.1에서 채널 후원 캔/건수와 최상위 팬 Talk 집계를 검증한다. - Feature E: Task 1.2, Task 3.4, Task 4.1에서 최종 팔로우 수와 `createdAt`/`updatedAt` 기반 팔로우 증가 수를 검증한다. - Feature F: Task 1.2, Task 4.1, Task 5.1에서 raw value 최종 점수, 1점 미만 제외, 20위 동점 후보 저장, 동점 랜덤 조회를 검증한다. - Feature G: Task 5.1, Task 5.2에서 ranking 조회 결과와 차단 마스킹을 검증하고, Task 6.1, Task 6.2에서 홈 API endpoint, 응답 스키마, 인증/비인증 연결을 검증한다. Task 10.1, Task 10.2에서 스냅샷 테이블 완전 공백 상태의 제한적 fallback과 공개 응답 스키마 유지를 검증하고, Task 11.2에서 fallback 성공 후 응답을 깨지 않고 스냅샷 생성 책임을 위임하는 흐름을 검증한다. Task 12.4, Task 12.5에서 조회 API가 최신 생성 스냅샷이 아니라 `visibleFromAt <= now` 조건을 만족하는 최신 공개 스냅샷만 응답하도록 검증한다. - Feature H: Task 2.1, Task 2.2, Task 4.1, Task 4.2, Task 4.3에서 주간 스냅샷 저장, 스케줄, 클러스터 단일 실행 lock을 검증한다. Task 8.1, Task 8.2에서 스케줄 job 이력과 성공/실패 기록을 검증하고, Task 9.1, Task 9.2에서 관리자 날짜 범위 수동 생성과 실패 job 재시도 API를 검증한다. Task 11.1, Task 11.2에서 cold-start fallback 성공 후 기간 기반 lock으로 동일 기간 스냅샷 생성 중복을 방지하는 보강책을 검증한다. Task 12.1~12.6에서 집계 기준 00:00 KST, 생성 후보 01:00 KST, 노출 전환 09:00 KST, `rankingType`/`visibleFromAt` DDL 영향과 최신 공개 스냅샷 조회 정책을 검증한다. - Feature I: Phase 5의 ranking 기능 본체는 `v2.ranking` 패키지 경계를 유지하고, Phase 6의 클라이언트 API 표면은 `v2.api.home` 하위에 둔다. Phase 8~10의 관리자/job/fallback 기능도 공개 API 응답 DTO를 변경하지 않는다. --- ## 3. 검증 기록 - 2026-06-08: PRD 기준 구현 계획/TASK 문서를 작성했다. 구현 시작 전 문서 산출물이므로 코드 테스트는 실행하지 않았고, 문서 규칙에 따라 `./gradlew tasks --all`로 Gradle 명령 유효성을 확인한다. - 2026-06-08: `rg -n "TBD|TODO|작성 예정|fill in|placeholder|similar|위와 동일|적절한|나중" docs/20260608_크리에이터_랭킹/plan-task.md`로 placeholder 문구가 없음을 확인했다. - 2026-06-08: `./gradlew tasks --all`은 sandbox 기본 권한에서 `~/.gradle` wrapper lock 파일 접근 권한 문제로 실패했고, 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 778ms`를 확인했다. - 2026-06-08: Phase 1 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicyTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` 실행 시 신규 ranking domain 타입 미정의로 `compileTestKotlin` 실패를 확인했다. - 2026-06-08: Phase 1 GREEN 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest`와 `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest`는 `BUILD SUCCESSFUL`을 확인했다. 병렬 실행한 period 단일 테스트 1건은 Kotlin/kapt cache 경합으로 실패해 후속 통합 검증에서 재확인한다. - 2026-06-08: Phase 2 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest`는 production persistence 추가 전 실행했으나 Kotlin daemon heap 오류로 컴파일 단계에서 중단됐다. 당시 테스트가 참조하는 `CreatorRankingSnapshotRepository` 등 production 타입은 미구현 상태였다. - 2026-06-08: Phase 2 GREEN 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest` 재실행 결과 `BUILD SUCCESSFUL in 1m 49s`를 확인했다. - 2026-06-08: DDL 대체 검증: `rg -n "creator_ranking_snapshot|aggregation_start_at_utc|creator_id|final_score" docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`로 테이블명, 기간 컬럼, 크리에이터 id, 최종 점수 컬럼 및 index 문구를 확인했다. - 2026-06-08: Phase 1~2 ranking 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'` 재실행 결과 `BUILD SUCCESSFUL in 22s`를 확인했다. - 2026-06-08: 포맷 검증: `./gradlew ktlintCheck`는 최초 신규 테스트 긴 줄로 실패했고, 줄바꿈 수정 후 재실행해 `BUILD SUCCESSFUL in 10s`를 확인했다. - 2026-06-08: 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 16s`를 확인했다. - 2026-06-08: Phase 3 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` 실행 결과 `DefaultCreatorRankingAggregationRepository` 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-08: Phase 3 GREEN 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` 재실행 결과 `BUILD SUCCESSFUL in 13s`를 확인했다. - 2026-06-08: Phase 3 focused 재검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` 재실행 결과 `BUILD SUCCESSFUL in 14s`를 확인했다. - 2026-06-08: Phase 3 ranking 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'` 실행 결과 `BUILD SUCCESSFUL in 12s`를 확인했다. - 2026-06-08: Phase 3 포맷 검증: `./gradlew ktlintCheck`는 최초 신규 테스트 긴 줄로 실패했고, 줄바꿈 수정 후 재실행해 `BUILD SUCCESSFUL in 5s`를 확인했다. - 2026-06-08: Phase 4 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest` 실행 결과 `CreatorRankingSnapshotRefreshService`, `CreatorRankingSnapshotScheduler` 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-08: Phase 4 GREEN 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest` 재실행 결과 `BUILD SUCCESSFUL in 3s`를 확인했다. - 2026-06-08: Phase 4 ranking 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'` 실행 결과 `BUILD SUCCESSFUL in 15s`를 확인했다. - 2026-06-08: Phase 4 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 22s`를 확인했다. - 2026-06-08: Phase 4 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 56s`를 확인했다. - 2026-06-08: Phase 4 reviewer gate: 스냅샷 생성 서비스/스케줄러/테스트/문서 변경에 대해 strict review를 수행했고 `PASS` 판정을 확인했다. - 2026-06-08: Task 4.3 및 07:30 스케줄 변경 focused 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest` 실행 결과 `BUILD SUCCESSFUL in 16s`를 확인했다. - 2026-06-08: Task 4.3 ranking 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'` 실행 결과 `BUILD SUCCESSFUL in 18s`를 확인했다. - 2026-06-08: Task 4.3 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 26s`를 확인했다. - 2026-06-08: Phase 5 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` 실행 결과 `CreatorRankingBlockPort`, `CreatorRankingQueryService` 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-08: Phase 5 GREEN 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` 재실행 결과 `BUILD SUCCESSFUL in 29s`를 확인했다. - 2026-06-08: Phase 5 ranking 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'` 실행 결과 `BUILD SUCCESSFUL in 18s`를 확인했다. - 2026-06-08: Phase 5 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 25s`를 확인했다. - 2026-06-08: Phase 5 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 1s`를 확인했다. - 2026-06-08: Phase 5 reviewer gate: 조회 서비스/차단 마스킹/테스트/문서 변경에 대해 strict review를 수행했고 `PASS` 판정을 확인했다. - 2026-06-08: Phase 6 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest` 실행 결과 신규 endpoint/permit rule 미구현으로 비회원 요청 401, 인증 요청 404 등 신규 controller 테스트 3건 실패를 확인했다. - 2026-06-08: Phase 6 GREEN 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest` 재실행 결과 `BUILD SUCCESSFUL in 33s`를 확인했다. - 2026-06-08: Phase 6 ranking/API 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` 실행 결과 `BUILD SUCCESSFUL in 36s`를 확인했다. - 2026-06-08: Phase 6 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 19s`를 확인했다. - 2026-06-08: Phase 6 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 10s`를 확인했다. - 2026-06-08: Phase 7 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` 실행 결과 신규 로그 assertion 4건이 이벤트 로그 부재로 실패하는 것을 확인했다. - 2026-06-08: Phase 7 GREEN 확인: 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 40s`를 확인했다. - 2026-06-08: Phase 7 ranking/API 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` 실행 결과 `BUILD SUCCESSFUL in 39s`를 확인했다. - 2026-06-08: Phase 7 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 21s`를 확인했다. - 2026-06-08: Phase 7 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 9s`를 확인했다. - 2026-06-08: Phase 7 reviewer gate 1차 검토: 스냅샷 생성 성공 로그가 transaction commit 이전에 기록되는 점과 PRD Metrics의 최종 점수 1점 미만 제외 수 관측 누락으로 `FAIL` 판정을 확인했다. - 2026-06-08: Phase 7 reviewer 수정 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest` 실행 결과 신규 `lowScoreExcludedCount` 테스트가 fake 미구현으로 `compileTestKotlin` 실패하는 것을 확인했다. - 2026-06-08: Phase 7 reviewer 수정 GREEN 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest` 실행 결과 `BUILD SUCCESSFUL in 50s`를 확인했다. - 2026-06-08: Phase 7 reviewer 수정 후 ranking/API 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` 실행 결과 `BUILD SUCCESSFUL in 37s`를 확인했다. - 2026-06-08: Phase 7 reviewer 수정 후 포맷 검증: `./gradlew ktlintCheck`는 최초 import 순서 위반으로 실패했고, import 정렬 후 재실행해 `BUILD SUCCESSFUL in 18s`를 확인했다. - 2026-06-08: Phase 7 reviewer 수정 후 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 28s`를 확인했다. - 후속 구현 중 각 task 완료 시 실행 명령, 목적, 결과를 이 섹션에 누적한다. - 2026-06-09: 사용자 추가 요구에 따라 PRD와 plan-task에 스냅샷 job 이력, 스케줄 job 기록, 관리자 날짜 범위 수동 생성, 실패 job 관리자 전용 재시도 API, 스냅샷 테이블 완전 공백 시 제한적 fallback 계획을 문서화했다. - 2026-06-09: Phase 8 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotJobRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 신규 job port/entity/service 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-09: Phase 8 GREEN 확인: 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 15s`를 확인했다. - 2026-06-09: Phase 8 스케줄러 연결 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `BUILD SUCCESSFUL in 4s`를 확인했다. - 2026-06-09: Phase 8 DDL 대체 검증: `rg -n "creator_ranking_snapshot_job|aggregation_start_at_utc|aggregation_end_at_utc|trigger_type|status|processing_started_at|processed_at|last_error" docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`로 job 테이블명, 기간/트리거/상태/처리 시각/실패 사유 컬럼 및 index 문구를 확인했다. - 2026-06-09: Phase 8 ranking 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'` 실행 결과 `BUILD SUCCESSFUL in 19s`를 확인했다. - 2026-06-09: Phase 8 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 14s`를 확인했다. - 2026-06-09: Phase 8 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 51s`를 확인했다. - 2026-06-09: Phase 8 reviewer gate 1차 검토: repository 테스트가 `PENDING` 저장 상태와 `PROCESSING` 전이를 직접 검증하지 않아 `FAIL` 판정을 확인했다. - 2026-06-09: Phase 8 reviewer 수정 후 focused 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotJobRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `BUILD SUCCESSFUL in 16s`를 확인했다. - 2026-06-09: Phase 8 reviewer 수정 후 포맷 검증: `./gradlew ktlintCheck`는 최초 unused import로 실패했고, import 제거 후 재실행해 `BUILD SUCCESSFUL in 6s`를 확인했다. - 2026-06-09: Phase 8 reviewer 수정 후 ranking 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'` 실행 결과 `BUILD SUCCESSFUL in 15s`를 확인했다. - 2026-06-09: Phase 8 reviewer 수정 후 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 51s`를 확인했다. - 2026-06-09: Phase 9 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotJobRepositoryTest --tests kr.co.vividnext.sodalive.v2.admin.ranking.creator.AdminCreatorRankingSnapshotJobControllerTest` 실행 결과 신규 관리자 API 클래스, `createManualJob`/`findJobs`/`retryFailedJob`, `markPending` 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-09: Phase 9 focused GREEN 및 관리자 API 표면 검증: retry 전이 guard 보강 후 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 1m 21s`를 확인했다. `AdminCreatorRankingSnapshotJobControllerTest`의 `MockMvc` 요청으로 `POST /admin/rankings/creators/snapshot-jobs`, `GET /admin/rankings/creators/snapshot-jobs`, `POST /admin/rankings/creators/snapshot-jobs/{jobId}/retry`의 성공 응답과 비관리자 403/익명 401을 검증했다. - 2026-06-09: Phase 9 ranking/admin 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.admin.ranking.creator.*'` 실행 결과 최초 병렬 Gradle 실행 중 Kotlin/kapt cache 경합으로 실패했고, 단독 재실행해 `BUILD SUCCESSFUL in 23s`를 확인했다. - 2026-06-09: Phase 9 포맷 검증: `./gradlew ktlintCheck`는 최초 테스트 파일 닫는 brace 앞 공백과 main import 순서 위반으로 실패했고, 정리 후 재실행해 `BUILD SUCCESSFUL in 11s`를 확인했다. - 2026-06-09: Phase 9 전체 회귀 검증: retry 전이 guard 보강 후 `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 20s`를 확인했다. - 2026-06-09: Phase 10 Task 10.1 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest` 실행 결과 `isSnapshotTableEmpty` 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-09: Phase 10 Task 10.1 GREEN 확인: 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 27s`를 확인했다. - 2026-06-09: Phase 10 Task 10.2 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` 실행 결과 `aggregationPort`, `nowProvider` 생성자 파라미터 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-09: Phase 10 Task 10.2 GREEN 확인: 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 42s`를 확인했다. - 2026-06-09: Phase 10 Task 10.3 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 fallback/job 로그 이벤트 부재로 신규 로그 테스트 4건 실패를 확인했다. - 2026-06-09: Phase 10 Task 10.3 GREEN 확인: 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 10s`를 확인했다. - 2026-06-09: Phase 10 ranking/API 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` 실행 결과 `BUILD SUCCESSFUL in 40s`를 확인했다. - 2026-06-09: Phase 10 포맷 검증: `./gradlew ktlintCheck`는 최초 테스트 import 순서 위반으로 실패했고, import 정렬 후 재실행해 `BUILD SUCCESSFUL in 6s`를 확인했다. - 2026-06-09: Phase 10 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 59s`를 확인했다. - 2026-06-09: Phase 10 reviewer gate 1차 검토: cold-start fallback 경로에서 인증 회원의 차단 크리에이터 마스킹이 누락되어 `FAIL` 판정을 확인했다. - 2026-06-09: Phase 10 reviewer 수정 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` 실행 결과 fallback 차단 마스킹 신규 테스트 1건 실패를 확인했다. - 2026-06-09: Phase 10 reviewer 수정 GREEN 확인: fallback 결과에도 기존 차단 마스킹을 적용한 뒤 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 6s`를 확인했다. - 2026-06-09: Phase 10 reviewer 수정 후 ranking/API 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` 실행 결과 `BUILD SUCCESSFUL in 30s`를 확인했다. - 2026-06-09: Phase 10 reviewer 수정 후 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 15s`를 확인했다. - 2026-06-09: Phase 10 reviewer 수정 후 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 56s`를 확인했다. - 2026-06-09: Phase 10 reviewer 수정 후 follow-up gate: 목표/보안 검증, 코드 품질 검토, QA focused 검증이 모두 `PASS` 판정을 반환했고 blocking issue가 없음을 확인했다. - 2026-06-09: 사용자 후속 요청에 따라 cold-start fallback 성공 시 조회 API가 직접 스냅샷을 저장하지 않고 `CreatorRankingSnapshotJobService`/`CreatorRankingSnapshotRefreshService` 책임으로 위임하도록 PRD와 plan-task를 갱신했다. 동일 집계 기간 중복 생성을 막기 위해 기간 기반 Redisson lock key(`lock:creator-ranking-snapshot-refresh:{start}:{end}`)와 신규 Phase 11 Task 11.1~11.3을 추가했다. 문서 변경 검증으로 `rg -n "cold-start|ensureLastCompletedWeekSnapshotForColdStart|lock:creator-ranking-snapshot-refresh|Task 11|fallback 성공" docs/20260608_크리에이터_랭킹/prd.md docs/20260608_크리에이터_랭킹/plan-task.md` 및 `git diff -- docs/20260608_크리에이터_랭킹/prd.md docs/20260608_크리에이터_랭킹/plan-task.md`를 실행해 반영 범위를 확인했다. - 2026-06-09: creator_ranking_snapshot 최신/직전 조회 기준 확인: `rg -n "max\(latest\.aggregation_end_at_utc\)|max\(previous\.aggregation_end_at_utc\)|order by .*id|findLatestSnapshots|findPreviousCompletedSnapshots" src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence` 및 repository 코드 확인 결과 최신/직전 조회는 `id`가 아니라 `aggregation_end_at_utc`의 max/previous max 기준이며, 기간 내 정렬은 `final_score desc`임을 확인했다. - 2026-06-09: Phase 11 Task 11.1 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `RedissonClient` 생성자 인자와 `ensureLastCompletedWeekSnapshotForColdStart` 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-09: Phase 11 Task 11.1 GREEN 확인: 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 12s`를 확인했다. - 2026-06-09: Phase 11 Task 11.2 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `snapshotJobService` 생성자 파라미터 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-09: Phase 11 Task 11.2 GREEN 확인: 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 16s`를 확인했다. - 2026-06-09: Phase 11 focused 재검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `BUILD SUCCESSFUL in 2s`를 확인했다. - 2026-06-09: Phase 11 ranking/API 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` 실행 결과 `BUILD SUCCESSFUL in 1m 9s`를 확인했다. - 2026-06-09: Phase 11 포맷 검증: `./gradlew ktlintCheck`는 최초 main import 순서 위반으로 실패했고, import 정렬 후 재실행해 `BUILD SUCCESSFUL in 23s`를 확인했다. - 2026-06-09: Phase 11 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 2m 52s`를 확인했다. - 2026-06-09: Phase 11 reviewer gate 1차 Code Quality 검토: 스케줄러 고정 lock과 cold-start 기간 lock이 달라 동일 기간 refresh가 동시에 실행될 수 있어 `FAIL` 판정을 확인했다. - 2026-06-09: Phase 11 reviewer 수정 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 스케줄 job이 cold-start와 같은 기간 lock을 사용하지 않아 신규 테스트 2건 실패를 확인했다. - 2026-06-09: Phase 11 reviewer 수정 GREEN 확인: 스케줄 job refresh와 cold-start refresh가 공통 기간 기반 lock 경계를 사용하도록 수정한 뒤 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 56s`를 확인했다. - 2026-06-09: Phase 11 reviewer 수정 후 focused 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `BUILD SUCCESSFUL in 9s`를 확인했다. - 2026-06-09: Phase 11 reviewer 수정 후 ranking/API 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` 실행 결과 `BUILD SUCCESSFUL in 43s`를 확인했다. - 2026-06-09: Phase 11 reviewer 수정 후 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 27s`를 확인했다. - 2026-06-09: Phase 11 reviewer 수정 후 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 10s`를 확인했다. - 2026-06-09: Phase 11 reviewer 2차 Code Quality 검토: 공통 period lock은 적용됐지만 transaction commit 전에 lock이 해제될 수 있어 `FAIL` 판정을 확인했다. - 2026-06-09: Phase 11 reviewer 2차 수정 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `TransactionTemplate`/transaction manager 생성자 인자 미구현으로 `compileTestKotlin` 실패를 확인했다. - 2026-06-09: Phase 11 reviewer 2차 수정 GREEN 확인: `PlatformTransactionManager`로 `PROPAGATION_REQUIRES_NEW` `TransactionTemplate`을 내부 생성하고, period lock 안의 transaction commit 이후 unlock되도록 수정한 뒤 job service focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 12s`를 확인했다. - 2026-06-09: Phase 11 reviewer 2차 수정 후 focused 검증: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `BUILD SUCCESSFUL in 3s`를 확인했다. - 2026-06-09: Phase 11 reviewer 2차 수정 후 ranking/API 범위 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*'` 실행 결과 `BUILD SUCCESSFUL in 45s`를 확인했다. - 2026-06-09: Phase 11 reviewer 2차 수정 후 포맷 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 17s`를 확인했다. - 2026-06-09: Phase 11 reviewer 2차 수정 후 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 9s`를 확인했다. - 2026-06-09: Phase 11 reviewer 2차 수정 후 Code Quality 재검토 결과 이전 blocking issue가 해소되어 `PASS` 판정을 확인했다. - 2026-06-24: 크리에이터 랭킹 시간 정책 변경 문서 작업을 시작해 PRD에 집계 기준 00:00:00 KST, 생성 후보 01:00:00 KST, 노출 전환 09:00:00 KST, 최신 공개 스냅샷(`visibleFromAt <= now`) 조회 정책을 반영했다. `plan-task.md`에는 `visible_from_at`/`ranking_type` DDL 영향도와 신규 Phase 12 Task 12.1~12.6을 추가했다. - 2026-06-24: 문서 정합성 확인: `rg -n "07:30|0 30 7|최신 완료|완료 주차" docs/20260608_크리에이터_랭킹/prd.md docs/20260608_크리에이터_랭킹/plan-task.md` 실행 결과 현재 PRD에는 변경 전 정책 표현이 남아 있지 않고, `plan-task.md`의 남은 07:30/최신 완료 표현은 Phase 1~11 완료 당시 이력 또는 과거 검증 기록이며 Phase 12 note에서 신규 변경 범위를 구분했음을 확인했다. - 2026-06-24: 시간 정책/DDL 키워드 확인: `rg -n "00:00:00 KST|01:00|09:00|visibleFromAt|visible_from_at|rankingType|ranking_type|최신 공개|최신 생성" docs/20260608_크리에이터_랭킹/prd.md docs/20260608_크리에이터_랭킹/plan-task.md docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`로 PRD, plan-task, DDL에 신규 시간 정책과 컬럼명이 반영됐음을 확인했다. - 2026-06-24: DDL 핵심 컬럼 확인: `rg -n "visible_from_at|ranking_type|creator_ranking_snapshot|creator_ranking_snapshot_job" docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`로 두 테이블의 `ranking_type`, `visible_from_at` 컬럼과 조회/관리 인덱스를 확인했다. - 2026-06-24: 문서 변경 후 Gradle 명령 유효성 확인: `./gradlew tasks --all`은 sandbox 기본 권한에서 `~/.gradle` wrapper lock 파일 접근 권한 문제로 실패했고, 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 2s`를 확인했다. - 2026-06-24: 사용자 피드백에 따라 이미 적용된 `create-ranking-tables.sql`의 CREATE DDL 변경을 되돌리고, 파일 하단에 기존 적용 DB 변경용 ALTER DDL을 추가했다. 컬럼 추가는 기존 row를 고려해 nullable로 추가한 뒤 `WEEKLY`와 `aggregation_end_at_utc + 9시간` 기준으로 backfill하고, 이후 `MODIFY NOT NULL` 및 인덱스 보강/교체를 수행하는 순서로 정리했다. - 2026-06-24: 피드백 반영 후 문서/DDL 재검증: `git diff -- docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`로 CREATE DDL 본문은 변경하지 않고 하단 ALTER 섹션만 추가됐음을 확인했다. `rg -n "이미 위 CREATE DDL|alter table creator_ranking_snapshot|add column ranking_type|update creator_ranking_snapshot|modify column ranking_type|drop index|create index idx_creator_ranking_snapshot_visible_score|alter table creator_ranking_snapshot_job|idx_creator_ranking_snapshot_job_visible_status" docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`로 ALTER/backfill/modify/index 변경 순서를 확인했다. - 2026-06-24: 피드백 반영 후 Gradle 명령 유효성 확인: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL in 808ms`를 확인했다. - 2026-06-24: 완료된 Phase 본문 수정에 대한 혼동을 줄이기 위해 Phase 2.1, Phase 4.2, Phase 5.1의 완료 task 문구는 기존 이력대로 되돌리고, Phase 12 시작부에 “Phase 1~11은 완료 당시 구현 이력이며 시간 정책 변경은 Phase 12에서 수행한다”는 note를 추가했다. - 2026-06-24: 완료 Phase 문구 원복 후 Gradle 명령 유효성 확인: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL in 823ms`를 확인했다. - 2026-06-24: PRD/plan-task 크로스 체크 결과, PRD Feature A의 변경 전 기간 기준 표현이 09:00 공개 노출 전환 전 응답 정책과 충돌할 수 있어 “스냅샷 생성 또는 fallback 집계 기준 시점” 기준으로 수정했다. `rg -n "조회 시점 기준|2026-06-08 월요일 KST에 조회하면" docs/20260608_크리에이터_랭킹/prd.md`로 PRD 본문에 변경 전 표현이 남지 않았고, `rg -n "집계 기준 시각|생성 후보 시각|노출 전환 시각|visibleFromAt <= now|rankingType|visible_from_at|ranking_type|운영 반영용 ALTER" docs/20260608_크리에이터_랭킹/prd.md docs/20260608_크리에이터_랭킹/plan-task.md docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`로 PRD, plan-task, DDL의 요구사항 반영 지점을 확인했다. - 2026-06-24: PRD/plan-task 크로스 체크 수정 후 Gradle 명령 유효성 확인: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL in 762ms`를 확인했다. - 2026-06-24: Phase 12 Task 12.1 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicyTest` 실행 결과 `resolveVisibleFromAtUtc` 미구현으로 `compileTestKotlin` 실패를 확인했다. GREEN 확인: 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 17s`를 확인했다. - 2026-06-24: Phase 12 Task 12.2 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotJobRepositoryTest` 실행 결과 `CreatorRankingType`, `rankingType`, `visibleFromAtUtc` 미구현으로 `compileTestKotlin` 실패를 확인했다. GREEN 확인: 스냅샷/job 저장 구조 반영 후 동일 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 51s`를 확인했다. - 2026-06-24: Phase 12 Task 12.3~12.5 RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.scheduler.CreatorRankingSnapshotSchedulerTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest` 실행 결과 `findLatestVisibleSnapshots`/`findPreviousVisibleSnapshots` 미구현으로 `compileTestKotlin` 실패를 확인했다. GREEN 확인: 01:00 KST cron, 최신 공개 스냅샷 조회, cold-start fallback 공개 전 차단 반영 후 Phase 12 focused 테스트 재실행 결과 `BUILD SUCCESSFUL in 18s`를 확인했다. - 2026-06-24: Phase 12 문서/DDL 정합성 검증: `rg -n "07:30|01:00|09:00|visibleFromAt|visible_from_at|rankingType|ranking_type|latest visible|최신 공개|최신 생성" docs/20260608_크리에이터_랭킹` 실행 결과 남은 07:30 표현은 완료된 Phase 4/과거 검증 기록과 Phase 12 변경 note임을 확인했고, 현재 목표/신규 task에는 01:00 생성과 09:00 노출 전환이 반영됐음을 확인했다. `rg -n "visible_from_at|ranking_type|creator_ranking_snapshot|creator_ranking_snapshot_job" docs/20260608_크리에이터_랭킹/create-ranking-tables.sql`로 ALTER/backfill/not-null/index 변경 순서를 확인했다. - 2026-06-24: Phase 12 ranking/API/admin 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*' --tests 'kr.co.vividnext.sodalive.v2.admin.ranking.creator.*'` 실행 결과 `BUILD SUCCESSFUL in 42s`를 확인했다. - 2026-06-24: Phase 12 포맷 검증: `./gradlew ktlintCheck`는 최초 신규 테스트 긴 줄과 import 순서 위반으로 실패했고, 포맷 정리 후 재실행해 `BUILD SUCCESSFUL in 10s`를 확인했다. - 2026-06-24: Phase 12 전체 회귀 검증: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 2m 18s`를 확인했다. - 2026-06-24: Phase 12 reviewer gate 1차 Code Quality 검토: 스케줄 job 생성/PROCESSING/refresh/DONE/FAILED 기록이 하나의 `REQUIRES_NEW` 트랜잭션에 묶여 refresh 실패 시 `FAILED` 기록도 롤백될 수 있어 `FAIL` 판정을 확인했다. - 2026-06-24: Phase 12 reviewer 수정 focused 검증: `CreatorRankingSnapshotJobService`의 scheduled job 상태 전이를 content ranking 패턴처럼 `savePendingJob`, `markProcessing`, `refresh`, `markDone`, `markFailed` 별도 transaction으로 분리하고, refresh rollback 이후 FAILED 상태 commit 순서 테스트를 추가했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest` 실행 결과 `BUILD SUCCESSFUL in 29s`를 확인했다. - 2026-06-24: Phase 12 reviewer 수정 후 ranking/API/admin 회귀 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*' --tests 'kr.co.vividnext.sodalive.v2.admin.ranking.creator.*'` 실행 결과 `BUILD SUCCESSFUL in 49s`를 확인했다. - 2026-06-24: Phase 12 reviewer 수정 후 포맷/전체 회귀 검증: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL in 32s`, `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 43s`를 확인했다. - 2026-06-24: Phase 12 reviewer 수정 후 Code Quality 재검토 결과 이전 blocking issue가 해소되어 `PASS` 판정을 확인했다. - 2026-06-24: Phase 12 코드 리뷰 및 재검증: 공개 조회 경로가 `findLatestVisibleSnapshots(WEEKLY, nowUtc)`/`findPreviousVisibleSnapshots(WEEKLY, currentAggregationStartAtUtc, nowUtc)`를 사용하고, 01:00 KST scheduler, 09:00 KST `visibleFromAtUtc`, cold-start fallback 공개 전 차단, scheduled job 실패 시 `FAILED` 상태 별도 transaction commit 흐름을 재확인했다. blocking issue는 발견하지 않았다. - 2026-06-24: Phase 12 코드 리뷰 후 fresh 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*' --tests 'kr.co.vividnext.sodalive.v2.admin.ranking.creator.*'` 실행 결과 `BUILD SUCCESSFUL in 38s`를 확인했다. `./gradlew ktlintCheck`는 sandbox 기본 권한에서 `~/.gradle` wrapper lock 파일 접근 권한 문제로 실패했고, 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 855ms`를 확인했다. `./gradlew test` 실행 결과 `BUILD SUCCESSFUL in 1m 41s`를 확인했다.