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

36 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을 사용하고, 클라이언트 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는 최신 완료 주차 스냅샷만 읽어 응답을 조립한다.

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
  • 집계 기간: 조회/스냅샷 생성 시점 기준 KST 지난 주 월요일 00:00:00 이상, 이번 주 월요일 00:00:00 미만
  • DB 조회 기간: KST 집계 기간을 UTC 기준 LocalDateTime 또는 프로젝트 표준 시간 타입으로 변환한 기간
  • 스냅샷 생성 스케줄 후보: 매주 월요일 KST 07:30, @Scheduled(cron = "0 30 7 * * MON", zone = "Asia/Seoul")
  • 다중 서버 인스턴스에서 스냅샷 스케줄러가 중복 실행되지 않도록 기존 Redisson 기반 분산 lock을 사용한다.
  • 랭킹 스냅샷 lock key는 lock:creator-ranking-snapshot-refresh로 고정하고, lock 획득 실패 인스턴스는 정상 skip한다.
  • 조회 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 조립 계층

  • 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: 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

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: 매주 월요일 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 기준으로 고정된다.
  • 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: 조회 서비스, 순위 변화, 차단 마스킹

  • 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에서 완성된다.
  • 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

  • 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/creatorsshowRankChange, 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 계약이 테스트로 고정된다.
  • 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: 관측/문서/회귀 검증

  • 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/**
      • 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 조립 계층 테스트, 포맷, 전체 회귀 테스트가 통과한다.

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, 응답 스키마, 인증/비인증 연결을 검증한다.
  • Feature H: Task 2.1, Task 2.2, Task 4.1, Task 4.2, Task 4.3에서 주간 스냅샷 저장, 스케줄, 클러스터 단일 실행 lock을 검증한다.
  • Feature I: Phase 5의 ranking 기능 본체는 v2.ranking 패키지 경계를 유지하고, Phase 6의 클라이언트 API 표면은 v2.api.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 판정을 확인했다.
  • 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를 확인했다.
  • 후속 구현 중 각 task 완료 시 실행 명령, 목적, 결과를 이 섹션에 누적한다.