34 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 == false및CreatorFollowing.updatedAt기준으로 계산한다.
1. 파일 구조 계획
신규 ranking domain/application
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingPeriodPolicy.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScorePolicy.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingScoreSpec.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingSnapshotCandidate.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/domain/CreatorRankingItem.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingAggregationPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingSnapshotPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/port/out/CreatorRankingBlockPort.kt
신규 홈 API 조립 계층
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/CreatorRankingController.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeCreatorRankingFacade.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/ranking/CreatorRankingResponse.kt
신규 scheduler / persistence
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotScheduler.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshot.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingBlockRepository.kt
문서 산출물
- Create:
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
- Create:
- RED: 월요일 KST 기준 지난 주 기간, 월/연도 경계, 서버 timezone UTC와 무관한 기간 산출, KST 2026-06-01 00:00:00
2026-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 변환이 테스트로 고정된다.
- Files:
-
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
- Create:
- 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 정책과 음수 팔로우 증가 반영이 테스트로 고정된다.
- Files:
-
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
- Create:
- 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가 같은 타입을 재사용할 수 있다.
- Files:
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
- Create:
- 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 계층으로 노출하지 않는다.
- 기대 결과: 같은 기간 재생성 시 중복 노출되지 않고 최신/직전 주차를 구분해 조회할 수 있다.
- Files:
-
Task 2.2: 운영 DB 반영용 스냅샷 DDL 문서 작성
- Files:
- Create:
docs/20260608_크리에이터_랭킹/create-ranking-tables.sql - Modify:
docs/20260608_크리에이터_랭킹/plan-task.md
- Create:
- 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을 검토할 수 있다.
- Files:
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
- Create:
- 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으로 분리해 산식과 조회 조건이 섞이지 않도록 한다.
- 기대 결과: 콘텐츠/라이브 카테고리의 원천 지표가 정확히 집계된다.
- Files:
-
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
- Modify:
- RED: 활성 콘텐츠 좋아요 수, 댓글+대댓글 수, 크리에이터 본인 댓글/대댓글 제외, 비활성/삭제 정책 제외를 검증하는 repository 통합 테스트를 작성한다.
- 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest - GREEN:
AudioContentLike,AudioContentComment,AudioContent를 기준으로 크리에이터별 좋아요/댓글 원천 지표를 반환한다. - REFACTOR: 댓글 작성자가 콘텐츠 소유 크리에이터와 같은 경우 제외하는 조건을 테스트 fixture 이름에 드러나게 정리한다.
- 기대 결과: 참여 반응 점수 입력값이 PRD 조건과 일치한다.
- Files:
-
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
- Modify:
- 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 기준으로 집계된다.
- Files:
-
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
- Modify:
- RED: 최종 팔로우 수는 기간 종료 시점 활성 row, 신규 팔로우 수는
createdAt기간 내, 언팔로우 수는isActive=false및updatedAt기간 내, 기간 내 재팔로우는 신규/언팔로우 이벤트로 별도 복원하지 않는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest - GREEN:
CreatorFollowing기준 최종 팔로우 수와 팔로우 증가 수를 반환한다. - REFACTOR: 현재 row만으로 계산하는 정책 한계를 테스트명과 주석 한 줄로 남긴다.
- 기대 결과: 팬 충성도 점수 입력값이 PRD의
createdAt/updatedAt정책과 일치한다.
- Files:
-
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
- Modify:
- RED: 여러 원천 지표를 크리에이터별로 합쳐 후보를 만들고, 비활성/탈퇴 크리에이터와 최종 점수 1점 미만 후보가 제외되는 테스트를 작성한다.
- 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest - GREEN: 원천 지표 aggregate를 크리에이터 id 기준으로 합쳐
CreatorRankingSnapshotCandidate를 반환한다. - REFACTOR: 복잡한 집계가 QueryDSL로 과도해지면 native SQL을 사용하되, 테스트로 H2 호환성을 고정한다.
- 기대 결과: 스냅샷 생성 서비스가 별도 원천 조회를 여러 번 조합하지 않고 후보 목록을 받을 수 있다.
- Files:
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
- Create:
- 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위 동점 전체” 규칙을 만족한다.
- Files:
-
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
- Create:
- 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 기준으로 고정된다.
- Files:
-
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
- Modify:
- 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: 기존
RedissonClientbean을 스케줄러에 주입하고tryLock으로 lock을 획득한 인스턴스만 refresh service를 호출한다. lock 획득 실패는 정상 skip으로 처리한다. - REFACTOR: DB 기반 scheduler lock 테이블은 추가하지 않고, 기존
AudioContentReleaseScheduledTask의 Redisson lock 패턴을 참고하되 스케줄러에는 lock 획득/해제와 service 호출만 둔다. - 기대 결과: 여러 서버 인스턴스에서 같은 cron이 동시에 실행돼도 클러스터 전체에서 한 인스턴스만 주간 랭킹 스냅샷을 생성한다.
- Files:
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
- Create:
- 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에서 완성된다.
- Files:
-
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
- Create:
- 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 수는 유지되고 개인 식별 정보만 가려진다.
- Files:
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
- Create:
- RED:
GET /api/v2/home/rankings/creators가showRankChange,items[].rank,rankChange,isNew,creatorId,nickname,profileImageUrl만 반환하고 날짜와finalScore를 반환하지 않는 controller 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest - GREEN: controller, API Facade, response DTO를 구현하고 Facade가
CreatorRankingQueryService를 호출해 홈 API 응답으로 변환한다. - REFACTOR: URL과 클라이언트 API 표면은
v2.api.home하위에 두고, 랭킹 DTO는v2.api.home.dto.ranking하위에 둔다. 랭킹 계산/조회 본체는v2.ranking에 유지한다. - 기대 결과: 클라이언트 홈 랭킹 탭에서 사용할 공개 API 계약이 테스트로 고정된다.
- Files:
-
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
- Modify:
- RED: 비회원 조회는 기본 랭킹을 반환하고, 인증 회원 조회는 차단 관계 마스킹을 적용하는 controller 테스트를 작성한다.
- 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest - GREEN: 기존 인증 주입 패턴을 확인해 member nullable 흐름을 service에 전달한다.
- REFACTOR: 기존 API 응답 wrapper 관례와 상태 코드를 맞춘다.
- 기대 결과: 인증 여부에 따라 차단 마스킹만 달라지고 endpoint 계약은 동일하다.
- Files:
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
- Modify:
- 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 확인에 필요한 최소 로그가 남는다.
- Files:
-
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
- Verify:
- 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 조립 계층 테스트, 포맷, 전체 회귀 테스트가 통과한다.
- Files:
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 기본 권한에서~/.gradlewrapper lock 파일 접근 권한 문제로 실패했고, 권한 승인 후 재실행해BUILD SUCCESSFUL in 778ms를 확인했다. - 2026-06-08: Phase 1 RED 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicyTest,./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest,./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest실행 시 신규 ranking domain 타입 미정의로compileTestKotlin실패를 확인했다. - 2026-06-08: Phase 1 GREEN 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicyTest와./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingQueryServiceTest는BUILD SUCCESSFUL을 확인했다. 병렬 실행한 period 단일 테스트 1건은 Kotlin/kapt cache 경합으로 실패해 후속 통합 검증에서 재확인한다. - 2026-06-08: Phase 2 RED 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest는 production persistence 추가 전 실행했으나 Kotlin daemon heap 오류로 컴파일 단계에서 중단됐다. 당시 테스트가 참조하는CreatorRankingSnapshotRepository등 production 타입은 미구현 상태였다. - 2026-06-08: Phase 2 GREEN 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest재실행 결과BUILD SUCCESSFUL in 1m 49s를 확인했다. - 2026-06-08: DDL 대체 검증:
rg -n "creator_ranking_snapshot|aggregation_start_at_utc|creator_id|final_score" docs/20260608_크리에이터_랭킹/create-ranking-tables.sql로 테이블명, 기간 컬럼, 크리에이터 id, 최종 점수 컬럼 및 index 문구를 확인했다. - 2026-06-08: Phase 1~2 ranking 범위 회귀 검증:
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'재실행 결과BUILD SUCCESSFUL in 22s를 확인했다. - 2026-06-08: 포맷 검증:
./gradlew ktlintCheck는 최초 신규 테스트 긴 줄로 실패했고, 줄바꿈 수정 후 재실행해BUILD SUCCESSFUL in 10s를 확인했다. - 2026-06-08: 전체 회귀 검증:
./gradlew test실행 결과BUILD SUCCESSFUL in 1m 16s를 확인했다. - 2026-06-08: Phase 3 RED 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest실행 결과DefaultCreatorRankingAggregationRepository미구현으로compileTestKotlin실패를 확인했다. - 2026-06-08: Phase 3 GREEN 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest재실행 결과BUILD SUCCESSFUL in 13s를 확인했다. - 2026-06-08: Phase 3 focused 재검증:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingAggregationRepositoryTest재실행 결과BUILD SUCCESSFUL in 14s를 확인했다. - 2026-06-08: Phase 3 ranking 범위 회귀 검증:
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'실행 결과BUILD SUCCESSFUL in 12s를 확인했다. - 2026-06-08: Phase 3 포맷 검증:
./gradlew ktlintCheck는 최초 신규 테스트 긴 줄로 실패했고, 줄바꿈 수정 후 재실행해BUILD SUCCESSFUL in 5s를 확인했다. - 2026-06-08: Phase 4 RED 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest실행 결과CreatorRankingSnapshotRefreshService,CreatorRankingSnapshotScheduler미구현으로compileTestKotlin실패를 확인했다. - 2026-06-08: Phase 4 GREEN 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest재실행 결과BUILD SUCCESSFUL in 3s를 확인했다. - 2026-06-08: Phase 4 ranking 범위 회귀 검증:
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'실행 결과BUILD SUCCESSFUL in 15s를 확인했다. - 2026-06-08: Phase 4 포맷 검증:
./gradlew ktlintCheck실행 결과BUILD SUCCESSFUL in 22s를 확인했다. - 2026-06-08: Phase 4 전체 회귀 검증:
./gradlew test실행 결과BUILD SUCCESSFUL in 56s를 확인했다. - 2026-06-08: Phase 4 reviewer gate: 스냅샷 생성 서비스/스케줄러/테스트/문서 변경에 대해 strict review를 수행했고
PASS판정을 확인했다. - 2026-06-08: Task 4.3 및 07:30 스케줄 변경 focused 검증:
./gradlew test --tests kr.co.vividnext.sodalive.v2.ranking.application.CreatorRankingSnapshotRefreshServiceTest실행 결과BUILD SUCCESSFUL in 16s를 확인했다. - 2026-06-08: Task 4.3 ranking 범위 회귀 검증:
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.ranking.*'실행 결과BUILD SUCCESSFUL in 18s를 확인했다. - 2026-06-08: Task 4.3 포맷 검증:
./gradlew ktlintCheck실행 결과BUILD SUCCESSFUL in 26s를 확인했다. - 후속 구현 중 각 task 완료 시 실행 명령, 목적, 결과를 이 섹션에 누적한다.