33 KiB
메인 홈 크리에이터 랭킹 개편 Implementation Plan
Goal: GET /api/v2/home/rankings/creators 공개 API 스키마는 유지하면서, 크리에이터 랭킹 fallback을 콘텐츠 랭킹과 같은 refresh 후 재조회 방식으로 변경하고, 스냅샷 산식을 순위 기반 인기 점수로 교체한다.
Architecture: DB는 후보 크리에이터의 raw metric만 집계한다. Kotlin application/domain 계층이 metric별 rank(), rank score, weighted score, 최종 점수, tie-breaker 정렬, 상위 20명 제한, score_detail_json 생성을 담당한다. creator_ranking_snapshot은 고정 결과 컬럼(rank_no, final_score, score_policy_version)과 산식 상세 JSON만 저장하며, 공개 조회는 rank_no asc를 기준으로 한다.
Tech Stack: Kotlin, Spring Boot 2.7.14, Java 17, Spring Data JPA/native SQL, Redisson, Jackson JSON serialization, JUnit 5, MockMvc, Gradle Wrapper
0. 구현 전 확정 사항
- 공개 API endpoint:
GET /api/v2/home/rankings/creators - 공개 API 응답 DTO 변경 없음:
showRankChange,items[].rank,rankChange,isNew,creatorId,nickname,profileImageUrl - 집계 기준: KST 기준 지난 완료 주차
- 집계 구간: 직전 월요일
00:00:00 KST이상, 이번 주 월요일00:00:00 KST미만 - DB 조회 구간: 위 집계 구간을 UTC로 변환한
created_at >= startInclusiveUtcandcreated_at < endExclusiveUtc - 공개 시각: 집계 종료 KST + 9시간
- 후보:
member.role = 'CREATOR',member.is_active = true - 후보 포함 metric:
liveCanAmount,contentPurchaseCanAmount,followIncrease,aiChatCount >= 1 followIncrease는 음수이면 0으로 보정한다.- metric 값이 0이면 해당 metric rank score는 0점이다.
- 최종 산식:
liveRevenueRankScore * 0.6 + contentRevenueRankScore * 0.2 + followIncreaseRankScore * 0.1 + aiChatRankScore * 0.1 - metric 순위 점수:
max(100 - (5 * (rank - 1)), 0) - 동률 metric은 SQL
rank()의미를 따른다. - 최종 tie-breaker:
creatorPopularityScore desc,creatorDebutAt desc,creatorId desc - 크리에이터 데뷔일은 기존
ExplorerService기준과 맞춰firstLiveBeginDateTime,firstContentReleaseDate중 빠른 값으로 본다. 둘 다 없으면 tie-breaker에서 가장 뒤로 둔다. - 스냅샷 저장은 최종 20명으로 제한하고 20위 동점자를 추가 저장하지 않는다.
- 기존
creator_ranking_snapshot,creator_ranking_snapshot_job데이터는 폐기하고 신규 구조로 다시 쌓는다. - 기존 old semantic score/raw metric 컬럼은 삭제한다.
- 미래 산식 변경은 고정 컬럼 추가가 꼭 필요한 경우가 아니라면
score_policy_version과score_detail_jsonschema 변경으로 처리한다.
1. 파일 구조 계획
문서/DDL
- Modify:
docs/20260710_메인_홈_크리에이터_랭킹_개편/prd.md - Modify:
docs/20260710_메인_홈_크리에이터_랭킹_개편/plan-task.md - Modify:
docs/20260710_메인_홈_크리에이터_랭킹_개편/alter-creator-ranking-snapshot-score-detail.sql
API 조립 계층
- Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/ranking/CreatorRankingResponse.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt
랭킹 domain/application
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreSpec.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicy.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingSnapshotCandidate.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreDetail.kt - 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 - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicyTest.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 - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobServiceTest.kt
랭킹 port/persistence/scheduler
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingAggregationPort.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/CreatorRankingSnapshot.kt - 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/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.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/adapter/out/persistence/CreatorRankingSnapshotJobRepository.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepository.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.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/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepositoryTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotSchedulerTest.kt
2. score_detail_json schema 초안
구현 시 CreatorRankingScoreDetail에서 schema를 고정한다. JSON 문자열을 직접 이어 붙이지 않고 Jackson/ObjectMapper 또는 JPA converter를 사용한다.
{
"scorePolicyVersion": "CREATOR_WEEKLY_POPULARITY_V2",
"formula": "liveRevenueRankScore * 0.6 + contentRevenueRankScore * 0.2 + followIncreaseRankScore * 0.1 + aiChatRankScore * 0.1",
"metrics": {
"liveRevenue": {
"rawValue": 12000,
"rank": 1,
"rankScore": 100.0,
"weight": 0.6,
"weightedScore": 60.0
},
"contentRevenue": {
"rawValue": 3000,
"rank": 4,
"rankScore": 85.0,
"weight": 0.2,
"weightedScore": 17.0
},
"followIncrease": {
"rawValue": 5,
"rank": 8,
"rankScore": 65.0,
"weight": 0.1,
"weightedScore": 6.5
},
"aiChat": {
"rawValue": 10,
"rank": 2,
"rankScore": 95.0,
"weight": 0.1,
"weightedScore": 9.5
}
},
"tieBreakers": {
"creatorDebutAt": "2026-06-01T00:00:00",
"creatorId": 123
}
}
Phase 1: 점수 정책과 score detail 모델
-
Task 1.1: 순위 기반 점수 정책으로 교체
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreSpec.kt - Modify:
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
- Modify:
- RED: rank 1/2/20/21, 동률 rank, metric 값 0일 때 0점, 최종 가중합
60/20/10/10,followIncrease음수 보정 테스트를 먼저 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest - GREEN: raw value 기반 기존 score 함수를 순위 기반 함수로 교체하거나 신규 함수로 대체한다.
- REFACTOR: weight 상수명은 신규 산식 의미(
LIVE_REVENUE_WEIGHT,CONTENT_REVENUE_WEIGHT,FOLLOW_INCREASE_WEIGHT,AI_CHAT_WEIGHT)와 일치시킨다. - 기대 결과: 산식 경계값과 가중치가 domain test로 고정된다.
- Files:
-
Task 1.2: 후보/스냅샷 domain model을 신규 metric 중심으로 변경
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingSnapshotCandidate.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreDetail.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicyTest.kt
- Modify:
- RED:
CreatorRankingScoreDetail이 PRD의 JSON schema에 맞는 metric key, raw value, rank, rank score, weight, weighted score, tie-breaker 값을 보존하는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest - GREEN: 후보에는
creatorDebutAt,liveCanAmount,contentPurchaseCanAmount,followIncrease,aiChatCount만 산식 raw metric으로 남긴다. - REFACTOR: old semantic 필드(
contentLiveScore,engagementScore,supportScore,fanLoyaltyScore, old raw counts)는 domain/port record에서 제거한다. - 기대 결과: 신규 산식에 필요한 데이터 계약만 domain/port에 남는다.
- Files:
-
Task 1.3: 후보 목록에 rank, score, tie-breaker, score detail을 적용하는 assembler 작성
- 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
- Modify:
- RED: 여러 후보의 metric 동률, 최종 점수 동률, 데뷔일 동률,
creatorId동률 해소, 최종 점수 0 제외, 상위 20명 제한을 검증하는 service 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest - GREEN: raw 후보 목록을 받아 metric별 rank score와
score_detail_json을 만든 뒤finalScore desc,creatorDebutAt desc nulls last,creatorId desc로 정렬하고rankNo를 부여한다. - REFACTOR: cold-start용 중복
toSnapshotRecord로직이 남지 않도록 refresh/query 공통 변환 책임을 정리한다. - 기대 결과: DB 계산 여부와 무관하게 Kotlin 정책 테스트가 최종 저장 순서를 고정한다.
- Files:
Phase 2: 스냅샷 테이블 구조와 persistence 변경
-
Task 2.1: 스냅샷 entity/record를 고정 결과 컬럼 + JSON 상세 구조로 변경
- 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/port/out/CreatorRankingSnapshotPort.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt
- Modify:
- RED:
rankNo,scorePolicyVersion,scoreDetailJson저장/조회와 old semantic 컬럼 미사용을 검증하는 repository 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest - GREEN: entity와 mapper에서 삭제 컬럼을 제거하고 신규 컬럼을 추가한다.
- REFACTOR: JSON은 string passthrough로 둘지 converter로 둘지 기존 코드 스타일에 맞춰 한 곳에서만 직렬화한다.
- 기대 결과: JPA entity와 DDL 방향이 일치한다.
- Files:
-
Task 2.2: 공개/직전 스냅샷 조회를
rank_no asc기준으로 변경- 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 - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt
- Modify:
- RED:
finalScore가 같거나 저장 순서가 섞여 있어도findLatestVisibleSnapshots,findPreviousVisibleSnapshots, 기간 조회가rankNo asc로 반환되는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest - GREEN: native query의
order by crs.final_score desc를order by crs.rank_no asc로 변경한다. - REFACTOR: query service에서 동점 그룹
shuffled()를 제거하고 저장된rankNo를 신뢰한다. - 기대 결과: 공개 조회 순서가 저장 시점 tie-breaker와 동일하다.
- Files:
-
Task 2.3: DDL 문서와 코드 schema 정합성 검증
- Files:
- Modify:
docs/20260710_메인_홈_크리에이터_랭킹_개편/alter-creator-ranking-snapshot-score-detail.sql - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshot.kt
- Modify:
- RED: TDD 예외 사유: 운영 DB DDL은 로컬 단위 테스트로 직접 실패 재현하기 어렵다.
- 대체 검증 방법: entity 컬럼명과 DDL 컬럼명을 수동 대조하고, H2 기반 repository 테스트가 신규 컬럼 저장/조회에 성공하는지 확인한다.
- GREEN: DDL에 기존 데이터 삭제, old semantic 컬럼 drop,
rank_no,score_policy_version,score_detail_json추가, rank 기준 인덱스가 포함되어 있는지 확인한다. - REFACTOR: DDL 주석에 destructive migration 전제와 신규 refresh 필요성을 명확히 유지한다.
- 기대 결과: 운영 반영 SQL과 애플리케이션 entity가 같은 테이블 계약을 가진다.
- Files:
Phase 3: 후보 metric 집계 변경
-
Task 3.1: 캔 매출과 팔로우 증가 후보 집계 변경
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingAggregationPort.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt
- Modify:
- RED: 활성 크리에이터만 포함,
DONATION/LIVE/SPIN_ROULETTElive can 합계,ORDER_CONTENTcontent purchase 합계, 공통 캔 조건,followIncrease음수 0 보정, metric이 모두 0인 후보 제외 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest - GREEN: 기존 좋아요/댓글/채널후원/팬Talk/최종팔로워 raw metric 집계를 제거하고 신규 metric만 반환한다.
- REFACTOR: SQL CTE 이름은 신규 metric 의미와 맞춘다.
- 기대 결과: PRD 후보 조건과 raw metric이 repository 테스트로 고정된다.
- Files:
-
Task 3.2: AI 채팅 개수 집계 추가
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt
- Modify:
- RED:
chat_message중chat_participant.participant_type = 'CHARACTER'메시지만 집계하고, 비활성 message/participant/character/creator와 기간 밖 메시지를 제외하는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest - GREEN: 기존 AI 캐릭터 스냅샷 query의
chat_message,chat_participant,chat_character, creator member 관계를 재사용해aiChatCount를 집계한다. - REFACTOR: AI 채팅 집계 조건이 추천 스냅샷과 갈라지지 않도록 SQL 조건을 나란히 비교한다.
- 기대 결과:
aiChatCount >= 1만으로도 후보가 될 수 있다.
- Files:
-
Task 3.3: 크리에이터 데뷔일 집계 추가
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepositoryTest.kt
- Modify:
- RED: 첫 라이브 시작 시각과 첫 콘텐츠 공개 시각 중 빠른 값을
creatorDebutAt으로 반환하고, 둘 다 없으면 null로 반환하는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest - GREEN: 기존 탐색 상세 화면의 데뷔일 계산 기준과 동일한 source를 aggregation SQL에 반영한다.
- REFACTOR: null debut tie-breaker는 service 정렬에서 nulls last로만 처리하고 SQL에 정렬 책임을 두지 않는다.
- 기대 결과: 최종 tie-breaker 입력값이 후보 집계에서 안정적으로 제공된다.
- Files:
Phase 4: 스냅샷 refresh와 scheduler/job 흐름
-
Task 4.1: refresh 저장 로직을 신규 산식과 20명 제한으로 변경
- Files:
- 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
- Modify:
- RED: 후보 25명 중 최종 tie-breaker 기준 상위 20명만 저장하고, 저장 record에
rankNo1~20,scorePolicyVersion,scoreDetailJson,finalScore가 들어가는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest - GREEN: 기존
takeRankedBoundary동점자 추가 저장 로직을 제거하고 정확히 20명만 저장한다. - REFACTOR: 홈 팔로잉 뉴스 발행 rank도
rankNo를 사용하게 정리한다. - 기대 결과: 스케줄러와 fallback이 같은 refresh 결과를 저장한다.
- Files:
-
Task 4.2: 크리에이터 랭킹 job에
FALLBACKtrigger와 3회 제한 추가- Files:
- 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/CreatorRankingSnapshotJob.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotJobRepository.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepository.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/CreatorRankingSnapshotJobServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotJobRepositoryTest.kt
- Modify:
- RED: 동일 집계 기간
FALLBACKjob이 3개 이상이면 refresh를 실행하지 않고, 3개 미만이면FALLBACKjob을 저장해PROCESSING -> DONE/FAILED로 전이하는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest - GREEN: 콘텐츠 랭킹
AudioRankingSnapshotJobService.refreshLastCompletedWeekByFallback흐름을 크리에이터 랭킹에 맞게 적용한다. - REFACTOR: lock name은 기간과 ranking type이 드러나도록 유지하고 lock 획득 실패는 정상 skip으로 처리한다.
- 기대 결과: fallback 반복 실행 제한과 job 이력이 콘텐츠 랭킹과 같은 패턴을 따른다.
- Files:
-
Task 4.3: scheduler/manual job 흐름 회귀 확인
- Files:
- Verify:
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 - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotSchedulerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotJobServiceTest.kt
- Verify:
- RED: scheduled job은
SCHEDULEDtrigger를 유지하고 fallback 제한 카운트에 포함되지 않는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.scheduler.CreatorRankingSnapshotSchedulerTest - GREEN: scheduler가 기존 실행 시각과 동일하게 job service를 호출하되 내부 refresh 결과는 신규 산식을 사용하게 한다.
- REFACTOR: manual retry/create API가 있다면
FALLBACKtrigger와 충돌하지 않도록 enum 처리 범위를 점검한다. - 기대 결과: 정기 생성, 수동 job, fallback job이 같은 refresh service를 공유한다.
- Files:
Phase 5: 조회 fallback과 공개 응답 유지
-
Task 5.1: cold-start 직접 집계 fallback을 refresh 후 재조회 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
- Modify:
- RED: 최신 공개 스냅샷이 없으면
snapshotJobService.refreshLastCompletedWeekByFallback()을 호출하고, 이후findLatestVisibleSnapshots를 재조회해 응답하는 테스트를 작성한다. 최신 공개 스냅샷이 있으면 fallback을 실행하지 않는 테스트도 함께 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest - GREEN:
isSnapshotTableEmpty()기반 cold-start 직접 집계 응답과aggregateColdStartFallback경로를 제거한다. - REFACTOR: fallback 실패는 로그만 남기고 공개 API는
showRankChange=false,items=[]로 성공 응답하게 유지한다. - 기대 결과: 과거 스냅샷 row 존재 여부와 무관하게 최신 공개 스냅샷 없음 시 콘텐츠 랭킹과 같은 fallback을 시도한다.
- Files:
-
Task 5.2: 조회 순위/순위 변화/차단 마스킹 회귀 고정
- Files:
- 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
- Modify:
- RED: latest/previous snapshot의
rankNo기준으로rank,rankChange,isNew,showRankChange가 계산되고, 차단 크리에이터는 기존처럼 마스킹되는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest - GREEN:
groupBy(finalScore).shuffled()를 제거하고 저장된rankNo또는 repository 정렬 순서를 사용한다. - REFACTOR: API 응답에는
finalScore,scorePolicyVersion,scoreDetailJson을 노출하지 않는다. - 기대 결과: 공개 응답 계약은 유지하고 순위 결정은 저장 스냅샷과 일치한다.
- Files:
-
Task 5.3: controller/facade 공개 스키마 회귀 테스트
- Files:
- Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/ranking/CreatorRankingResponse.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt
- Verify:
- RED: MockMvc 응답 JSON에 기존 공개 필드만 있고
finalScore,rankNo,scorePolicyVersion,scoreDetailJson,fallback이 없는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest - GREEN: 필요 시 테스트 fixture만 신규 내부 record 구조에 맞춰 수정하고 controller/facade 공개 DTO는 변경하지 않는다.
- REFACTOR: API 계층이 ranking persistence model을 import하지 않도록 확인한다.
- 기대 결과: 앱 클라이언트 호환성이 controller 테스트로 고정된다.
- Files:
Phase 6: 통합 검증과 문서 갱신
-
Task 6.1: 핵심 단위/통합 테스트 회귀 실행
- Files:
- Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/** - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt
- Verify:
- RED: TDD 예외 사유: 각 phase에서 실패 테스트를 먼저 작성하므로 이 task는 전체 회귀 실행 task다.
- 대체 검증 방법: 아래 단일/전체 명령을 실행하고 실패가 있으면 해당 phase task로 돌아가 수정한다.
- GREEN:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest
- REFACTOR: 중복 fixture/helper가 과해지면 기존 테스트 스타일 안에서만 정리한다.
- 기대 결과: 신규 산식, persistence, fallback, 공개 응답이 모두 회귀 테스트를 통과한다.
- Files:
-
Task 6.2: 포맷/컴파일/전체 테스트 검증
- Files:
- Verify:
build.gradle.kts - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/** - Verify:
src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/**
- Verify:
- RED: TDD 예외 사유: 빌드/포맷 검증은 구현 완료 후 회귀 확인이다.
- 대체 검증 방법: Gradle 명령 결과를 plan-task 검증 기록에 누적한다.
- GREEN:
./gradlew test./gradlew ktlintCheck- 필요 시
./gradlew compileKotlin
- REFACTOR: ktlint 실패는 formatting만 최소 수정하고, unrelated style 변경은 하지 않는다.
- 기대 결과: 전체 테스트와 포맷 검증이 통과한다.
- Files:
-
Task 6.3: 문서와 운영 반영 절차 최종 정리
- Files:
- Modify:
docs/20260710_메인_홈_크리에이터_랭킹_개편/prd.md - Modify:
docs/20260710_메인_홈_크리에이터_랭킹_개편/plan-task.md - Modify:
docs/20260710_메인_홈_크리에이터_랭킹_개편/alter-creator-ranking-snapshot-score-detail.sql
- Modify:
- RED: TDD 예외 사유: 문서 정합성 작업이다.
- 대체 검증 방법: PRD 결정 사항, DDL, 구현 결과, 테스트 결과가 서로 모순되지 않는지 수동 대조한다.
- GREEN: 각 완료 task를
- [x]로 갱신하고, 실행 명령/결과/검증 이유를 해당 task 아래에 누적한다. - REFACTOR: 구현 중 PRD 범위가 바뀌었으면 먼저 PRD를 갱신한 뒤 계획 문서를 다시 맞춘다.
- 기대 결과: 구현 결과와 운영 DDL, 테스트 근거가 문서에 남는다.
- Files:
전체 검증 명령
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest
./gradlew test
./gradlew ktlintCheck
검증 기록
-
2026-07-10: PRD 기반 구현 계획 문서를 생성했다. 문서 변경 후 명령 유효성 확인을 위해
./gradlew tasks --all을 실행했고BUILD SUCCESSFUL을 확인했다. sandbox에서는 Gradle wrapper lock 파일 접근 제한으로 실패해 승인된 외부 실행으로 재시도했다. -
2026-07-10: Task 5.1에 최신 공개 스냅샷이 있을 때 fallback을 실행하지 않는 회귀 테스트 요구를 추가했다. 문서 변경 후
./gradlew tasks --all을 실행했고BUILD SUCCESSFUL을 확인했다. sandbox에서는 Gradle wrapper lock 파일 접근 제한으로 실패해 승인된 외부 실행으로 재시도했다. -
2026-07-10: creator ranking V2 refactor 구현 후
./gradlew compileKotlin을 실행해 main source 컴파일BUILD SUCCESSFUL을 확인했다. -
2026-07-10: TDD RED 확인으로
./gradlew compileTestKotlin을 실행했고 old semantic snapshot/candidate 필드 제거로 기존 테스트가 컴파일 실패하는 것을 확인했다. 이후 V2 rank policy,rankNo/scorePolicyVersion/scoreDetailJson, fallback refresh-after-requery, FALLBACK job/count 테스트로 갱신했다. -
2026-07-10: targeted GREEN 검증으로
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotJobRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotJobServiceTest --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest ktlintCheck를 실행했고BUILD SUCCESSFUL을 확인했다. -
2026-07-10: reviewer gate에서 aggregation/snapshot/controller 테스트가 약화되었다는 rejection을 받아
DefaultCreatorRankingAggregationRepositoryTest와DefaultCreatorRankingSnapshotRepositoryTest를@DataJpaTest기반으로,CreatorRankingControllerTest를@SpringBootTest+MockMvc기반으로 복구했다. 복구 과정에서 H2 native query의Timestamp매핑과 직전 공개 스냅샷 조회의visible_from_at <= nowUtc누락을 테스트로 확인하고 수정했다. -
2026-07-10: surface 재검증으로
./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest를 실행했고BUILD SUCCESSFUL을 확인했다. -
2026-07-10: 최종 회귀 검증으로
./gradlew test,./gradlew ktlintCheck,GIT_MASTER=1 git diff --check를 실행했고 모두 성공했다. old cold-start fallback 및 old score 필드 잔존 검색도 신규 JSON 직렬화 코드 외 유의미한 잔존이 없음을 확인했다. -
2026-07-10: reviewer gate 재심사에서
UNCONDITIONAL APPROVAL을 받았다. -
2026-07-10: 코드 리뷰 반영으로
CreatorRankingScoreDetail의LocalDateTimeJSON 직렬화를 ISO-8601 문자열로 고정하고,CreatorRankingScorePolicyTest에서creatorDebutAt = "2026-06-01T00:00:00"값을 직접 검증하도록 보강했다. 운영 DDL에는creator_ranking_snapshot_job.trigger_typecomment를SCHEDULED, MANUAL, FALLBACK으로 갱신하는 alter를 추가했다. -
2026-07-10: 리뷰 반영 후 재검증으로
./gradlew test,./gradlew ktlintCheck,git diff --check를 실행했고 모두BUILD SUCCESSFUL또는 성공 종료를 확인했다../gradlew ktlintCheck는 sandbox에서 Gradle wrapper lock 파일 접근 제한으로 실패해 승인된 외부 실행으로 재시도했다.