docs(ranking): 크리에이터 랭킹 계획을 작성한다
This commit is contained in:
310
docs/20260608_크리에이터_랭킹/plan-task.md
Normal file
310
docs/20260608_크리에이터_랭킹/plan-task.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# 크리에이터 랭킹 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 == 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 / 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: 기간/점수 도메인 정책
|
||||
|
||||
- [x] **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:00~2026-06-08 00:00:00이 UTC 2026-05-31 15:00:00~2026-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 변환이 테스트로 고정된다.
|
||||
|
||||
- [x] **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 정책과 음수 팔로우 증가 반영이 테스트로 고정된다.
|
||||
|
||||
- [x] **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
|
||||
|
||||
- [x] **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 계층으로 노출하지 않는다.
|
||||
- 기대 결과: 같은 기간 재생성 시 중복 노출되지 않고 최신/직전 주차를 구분해 조회할 수 있다.
|
||||
|
||||
- [x] **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=false` 및 `updatedAt` 기간 내, 기간 내 재팔로우는 신규/언팔로우 이벤트로 별도 복원하지 않는 테스트를 작성한다.
|
||||
- 실패 확인: `./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/creators`가 `showRankChange`, `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.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`를 확인했다.
|
||||
- 후속 구현 중 각 task 완료 시 실행 명령, 목적, 결과를 이 섹션에 누적한다.
|
||||
Reference in New Issue
Block a user