Files
sodalive-backend-spring-boot/docs/20260608_크리에이터_랭킹/plan-task.md

31 KiB

크리에이터 랭킹 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 기준 지난 주 크리에이터 랭킹 상위 20명을 조회한다.

Architecture: 공개 endpoint는 home 하위 URL을 사용하지만 구현 코드는 추천 기능과 분리된 kr.co.vividnext.sodalive.v2.ranking 하위에 둔다. 주간 스냅샷 생성 작업이 KST 기간을 UTC DB 조회 조건으로 변환해 원천 데이터를 집계하고, 조회 API는 최신 완료 주차 스냅샷만 읽어 응답을 조립한다.

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
  • 집계 기간: 조회/스냅샷 생성 시점 기준 KST 지난 주 월요일 00:00:00 이상, 이번 주 월요일 00:00:00 미만
  • DB 조회 기간: KST 집계 기간을 UTC 기준 LocalDateTime 또는 프로젝트 표준 시간 타입으로 변환한 기간
  • 스냅샷 생성 스케줄 후보: 매주 월요일 KST 06:00, @Scheduled(cron = "0 0 6 * * MON", zone = "Asia/Seoul")
  • 조회 API는 원천 데이터 실시간 계산 fallback을 두지 않는다.
  • 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 == falseCreatorFollowing.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 / scheduler / persistence

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/in/web/CreatorRankingController.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/in/web/CreatorRankingResponse.kt
  • 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: 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/ranking/adapter/in/web/CreatorRankingControllerTest.kt

Phase 1: 기간/점수 도메인 정책

  • 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:002026-06-08 00:00:00이 UTC 2026-05-31 15:00:002026-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 변환이 테스트로 고정된다.
  • 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 정책과 음수 팔로우 증가 반영이 테스트로 고정된다.
  • 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

  • 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 계층으로 노출하지 않는다.
    • 기대 결과: 같은 기간 재생성 시 중복 노출되지 않고 최신/직전 주차를 구분해 조회할 수 있다.
  • 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

  • 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으로 분리해 산식과 조회 조건이 섞이지 않도록 한다.
    • 기대 결과: 콘텐츠/라이브 카테고리의 원천 지표가 정확히 집계된다.
  • 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 조건과 일치한다.
  • 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 기준으로 집계된다.
  • 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=falseupdatedAt 기간 내, 기간 내 재팔로우는 신규/언팔로우 이벤트로 별도 복원하지 않는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest
    • GREEN: CreatorFollowing 기준 최종 팔로우 수와 팔로우 증가 수를 반환한다.
    • REFACTOR: 현재 row만으로 계산하는 정책 한계를 테스트명과 주석 한 줄로 남긴다.
    • 기대 결과: 팬 충성도 점수 입력값이 PRD의 createdAt/updatedAt 정책과 일치한다.
  • 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: 스냅샷 생성 서비스와 스케줄러

  • 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위 동점 전체” 규칙을 만족한다.
  • Task 4.2: 매주 월요일 06:00 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 0 6 * * MON", zone = "Asia/Seoul")가 선언되어 있는지 reflection 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest
    • GREEN: 스케줄러가 CreatorRankingSnapshotRefreshService.refreshLastCompletedWeek()를 호출하도록 구현한다.
    • REFACTOR: 스케줄러에는 기간/점수/DB 로직을 두지 않는다.
    • 기대 결과: 주간 스냅샷 생성 트리거가 KST 기준으로 고정된다.

Phase 5: 조회 서비스, 순위 변화, 차단 마스킹

  • 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 응답에 필요한 showRankChange와 item 목록이 application service에서 완성된다.
  • 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와 DTO

  • Task 6.1: 랭킹 조회 DTO와 Controller 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/in/web/CreatorRankingResponse.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/in/web/CreatorRankingController.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/in/web/CreatorRankingControllerTest.kt
    • RED: GET /api/v2/home/rankings/creatorsshowRankChange, items[].rank, rankChange, isNew, creatorId, nickname, profileImageUrl만 반환하고 날짜와 finalScore를 반환하지 않는 controller 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.in.web.CreatorRankingControllerTest
    • GREEN: controller와 response DTO를 구현하고 CreatorRankingQueryService를 호출한다.
    • REFACTOR: URL은 home 하위지만 코드 패키지는 v2.ranking에 유지한다.
    • 기대 결과: 클라이언트 홈 랭킹 탭에서 사용할 공개 API 계약이 테스트로 고정된다.
  • Task 6.2: 인증/비인증 조회와 차단 마스킹 연결

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/in/web/CreatorRankingController.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/in/web/CreatorRankingControllerTest.kt
    • RED: 비회원 조회는 기본 랭킹을 반환하고, 인증 회원 조회는 차단 관계 마스킹을 적용하는 controller 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.in.web.CreatorRankingControllerTest
    • GREEN: 기존 인증 주입 패턴을 확인해 member nullable 흐름을 service에 전달한다.
    • REFACTOR: 기존 API 응답 wrapper 관례와 상태 코드를 맞춘다.
    • 기대 결과: 인증 여부에 따라 차단 마스킹만 달라지고 endpoint 계약은 동일하다.

Phase 7: 관측/문서/회귀 검증

  • 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 확인에 필요한 최소 로그가 남는다.
  • 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/**
      • Modify: docs/20260608_크리에이터_랭킹/plan-task.md
    • RED: 테스트 작성 예외. TDD 예외 사유: 구현 완료 후 회귀 검증 task다.
    • 대체 검증 방법:
      • ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'
      • ./gradlew ktlintCheck
      • ./gradlew test
    • GREEN: 실패하는 테스트가 있으면 해당 phase task로 돌아가 수정하고, 모든 명령을 통과시킨다.
    • REFACTOR: plan-task 하단 검증 기록에 실행 명령, 목적, 결과를 누적한다.
    • 기대 결과: ranking 기능 단위 테스트, 포맷, 전체 회귀 테스트가 통과한다.

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, Task 6.1, Task 6.2에서 API endpoint, 응답 스키마, 순위 변화, 신규 진입, 차단 마스킹을 검증한다.
  • Feature H: Task 2.1, Task 2.2, Task 4.1, Task 4.2에서 주간 스냅샷 저장과 스케줄을 검증한다.
  • Feature I: 모든 task에서 v2.ranking 패키지 경계를 유지하고, Task 6.1에서 endpoint만 home 하위로 둔다.

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.CreatorRankingQueryServiceTestBUILD 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 판정을 확인했다.
  • 후속 구현 중 각 task 완료 시 실행 명령, 목적, 결과를 이 섹션에 누적한다.