Files
sodalive-backend-spring-boot/docs/20260710_메인_홈_크리에이터_랭킹_개편/plan-task.md

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 >= startInclusiveUtc and created_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_versionscore_detail_json schema 변경으로 처리한다.

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
    • 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로 고정된다.
  • 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
    • 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에 남는다.
  • 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
    • 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 정책 테스트가 최종 저장 순서를 고정한다.

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
    • 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 방향이 일치한다.
  • 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
    • 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 descorder by crs.rank_no asc로 변경한다.
    • REFACTOR: query service에서 동점 그룹 shuffled()를 제거하고 저장된 rankNo를 신뢰한다.
    • 기대 결과: 공개 조회 순서가 저장 시점 tie-breaker와 동일하다.
  • 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
    • 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가 같은 테이블 계약을 가진다.

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
    • RED: 활성 크리에이터만 포함, DONATION/LIVE/SPIN_ROULETTE live can 합계, ORDER_CONTENT content 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 테스트로 고정된다.
  • 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
    • RED: chat_messagechat_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만으로도 후보가 될 수 있다.
  • 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
    • 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 입력값이 후보 집계에서 안정적으로 제공된다.

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
    • RED: 후보 25명 중 최종 tie-breaker 기준 상위 20명만 저장하고, 저장 record에 rankNo 1~20, scorePolicyVersion, scoreDetailJson, finalScore가 들어가는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest
    • GREEN: 기존 takeRankedBoundary 동점자 추가 저장 로직을 제거하고 정확히 20명만 저장한다.
    • REFACTOR: 홈 팔로잉 뉴스 발행 rank도 rankNo를 사용하게 정리한다.
    • 기대 결과: 스케줄러와 fallback이 같은 refresh 결과를 저장한다.
  • Task 4.2: 크리에이터 랭킹 job에 FALLBACK trigger와 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
    • RED: 동일 집계 기간 FALLBACK job이 3개 이상이면 refresh를 실행하지 않고, 3개 미만이면 FALLBACK job을 저장해 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 이력이 콘텐츠 랭킹과 같은 패턴을 따른다.
  • 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
    • RED: scheduled job은 SCHEDULED trigger를 유지하고 fallback 제한 카운트에 포함되지 않는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.scheduler.CreatorRankingSnapshotSchedulerTest
    • GREEN: scheduler가 기존 실행 시각과 동일하게 job service를 호출하되 내부 refresh 결과는 신규 산식을 사용하게 한다.
    • REFACTOR: manual retry/create API가 있다면 FALLBACK trigger와 충돌하지 않도록 enum 처리 범위를 점검한다.
    • 기대 결과: 정기 생성, 수동 job, fallback job이 같은 refresh service를 공유한다.

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
    • 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을 시도한다.
  • 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
    • 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을 노출하지 않는다.
    • 기대 결과: 공개 응답 계약은 유지하고 순위 결정은 저장 스냅샷과 일치한다.
  • 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
    • 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 테스트로 고정된다.

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
    • 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, 공개 응답이 모두 회귀 테스트를 통과한다.
  • 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/**
    • RED: TDD 예외 사유: 빌드/포맷 검증은 구현 완료 후 회귀 확인이다.
    • 대체 검증 방법: Gradle 명령 결과를 plan-task 검증 기록에 누적한다.
    • GREEN:
      • ./gradlew test
      • ./gradlew ktlintCheck
      • 필요 시 ./gradlew compileKotlin
    • REFACTOR: ktlint 실패는 formatting만 최소 수정하고, unrelated style 변경은 하지 않는다.
    • 기대 결과: 전체 테스트와 포맷 검증이 통과한다.
  • 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
    • RED: TDD 예외 사유: 문서 정합성 작업이다.
    • 대체 검증 방법: PRD 결정 사항, DDL, 구현 결과, 테스트 결과가 서로 모순되지 않는지 수동 대조한다.
    • GREEN: 각 완료 task를 - [x]로 갱신하고, 실행 명령/결과/검증 이유를 해당 task 아래에 누적한다.
    • REFACTOR: 구현 중 PRD 범위가 바뀌었으면 먼저 PRD를 갱신한 뒤 계획 문서를 다시 맞춘다.
    • 기대 결과: 구현 결과와 운영 DDL, 테스트 근거가 문서에 남는다.

전체 검증 명령

./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을 받아 DefaultCreatorRankingAggregationRepositoryTestDefaultCreatorRankingSnapshotRepositoryTest@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: 코드 리뷰 반영으로 CreatorRankingScoreDetailLocalDateTime JSON 직렬화를 ISO-8601 문자열로 고정하고, CreatorRankingScorePolicyTest에서 creatorDebutAt = "2026-06-01T00:00:00" 값을 직접 검증하도록 보강했다. 운영 DDL에는 creator_ranking_snapshot_job.trigger_type comment를 SCHEDULED, MANUAL, FALLBACK으로 갱신하는 alter를 추가했다.

  • 2026-07-10: 리뷰 반영 후 재검증으로 ./gradlew test, ./gradlew ktlintCheck, git diff --check를 실행했고 모두 BUILD SUCCESSFUL 또는 성공 종료를 확인했다. ./gradlew ktlintCheck는 sandbox에서 Gradle wrapper lock 파일 접근 제한으로 실패해 승인된 외부 실행으로 재시도했다.