Files

33 KiB

메인 홈 추천 응원 크리에이터 스냅샷 수정 Plan/Task

시나리오 계약

  • Happy path: 인기 커뮤니티와 동일한 최근 7일 UTC half-open 범위로 CHEER_CREATOR 점수를 계산하고, 점수순 상위 16개 스냅샷을 저장한다. Real surface: DefaultHomeRecommendationQueryRepositoryTest, RecommendationSnapshotRefreshServiceTest.
  • Score: 응원 점수는 ((donationAmount * 0.45) + (fanTalkCount * 0.30) + (donationCount * 0.10)) * newBoost다. 후원 금액은 CHANNEL_DONATIONDONATIONuse_can_calculate.can을 그대로 사용하고, 후원 수는 UseCanCalculate.useCan 기준으로 중복 제거한다. Real surface: RecommendationScorePolicyTest, DefaultHomeRecommendationQueryRepositoryTest.
  • Boost: 신규 부스트는 크리에이터 데뷔일 기준 010일 1.15, 1120일 1.10, 21~30일 1.05, 31일 이상 1.0이다. Real surface: RecommendationScorePolicyTest, DefaultHomeRecommendationQueryRepositoryTest.
  • Fallback: 최신 CHEER_CREATOR 스냅샷이 없으면 lock, double-check, 동일 refresh 로직 재사용, refresh 후 재조회 순서로 fallback을 실행한다. lock 대기는 최대 300ms, 홈 API refresh 완료 대기는 최대 1,500ms다. Real surface: fallback service test, HomeRecommendationQueryServiceTest.
  • Empty marker: CHEER_CREATOR refresh 결과가 0건이면 targetId = 0 marker를 저장해 정상 refresh 완료 상태를 남기고, 조회 응답에서는 marker를 제외한다. Real surface: RecommendationSnapshotPersistenceAdapterTest, HomeRecommendationQueryServiceTest.
  • Adjacent regression: 메인 홈 추천 API URL과 CHEER_CREATOR 응답 필드는 변경하지 않는다. AI 캐릭터, 인기 커뮤니티, 최근 데뷔 등 다른 섹션 산식과 공개 스키마는 이번 변경으로 바꾸지 않는다. Real surface: 기존 focused tests, HomeRecommendationControllerTest.

범위와 전제

  • 이번 문서는 docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md의 구현 계획이다.
  • 신규 공개 API, 신규 응답 필드, 운영 DDL 추가는 범위에 포함하지 않는다.
  • 기존 recommendation_snapshot 테이블과 RecommendedSectionType.CHEER_CREATOR를 재사용한다.
  • CHEER_CREATOR 집계는 현재 구조와 성능 특성을 유지해 DB-side exact scoring을 기본으로 한다. Kotlin 단에는 산식/부스트 근거 테스트용 정책 함수를 둔다.
  • 집계 기간은 인기 커뮤니티와 동일하게 KST 전날을 포함한 최근 7일이며, DB 조회에는 UTC half-open window를 사용한다.
  • fallback orchestration은 AI 캐릭터 전용 구현을 그대로 복사하지 않고, 섹션별 lock key와 refresh action을 받을 수 있는 최소 공통 runner를 우선 적용한다.
  • 다른 스냅샷 섹션으로 empty marker를 확장하는 작업은 이번 구현 범위에서 제외한다. 단, CHEER_CREATOR에 적용할 때 이후 공통화가 가능하도록 조건문/상수명을 명확히 둔다.

기존 CHEER_CREATOR 로직 유지/변경 경계

  • 유지: RecommendedSectionType.CHEER_CREATOR enum 값과 code는 변경하지 않는다.
  • 유지: 홈 응원 크리에이터 응답 필드인 creatorId, creatorNickname, creatorProfileImage는 변경하지 않는다.
  • 유지: 홈 첫 화면 응답은 최대 8명, 스냅샷 후보 조회는 최대 16개를 사용한다.
  • 유지: 상세 조회 시점의 활성 크리에이터 필터와 차단 필터는 유지한다.
  • 유지: 후원 금액은 use_can_calculate.can 값을 그대로 합산한다.
  • 변경: 점수 가중치는 후원 금액 45%, 팬Talk 수 30%, 후원 수 10%로 바꾼다.
  • 변경: 후원 수는 UseCanCalculate.useCan 기준 distinct count로 계산한다.
  • 변경: 팬Talk 수는 CreatorCheers.isActive == true row 수로 계산한다.
  • 변경: 집계 기간은 인기 커뮤니티와 동일한 최근 7일로 한다.
  • 변경: 신규 부스트는 기존 크리에이터 공통 부스트 1.5/1.3/1.2가 아니라 CHEER_CREATOR 전용 1.15/1.10/1.05를 사용한다.
  • 추가: CHEER_CREATOR 최신 스냅샷이 없을 때 fallback refresh를 실행한다.
  • 추가: CHEER_CREATOR refresh 결과 0건이면 empty snapshot marker를 저장한다.

실행 명령

  • 문서 명령 확인: ./gradlew tasks --all
  • 산식/부스트 단위 테스트: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest
  • 스냅샷 저장 marker 테스트: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest
  • 응원 크리에이터 query 통합 테스트: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
  • refresh/fallback 조회 테스트: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest
  • fallback service 테스트: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest
  • 홈 API 회귀 테스트: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.HomeRecommendationResponseTest
  • 포맷 검증: ./gradlew ktlintCheck
  • 전체 회귀: ./gradlew test

Phase 1: 문서와 기준 고정

  • Task 1.1: PRD 기반 구현 계획 문서 작성
    • 파일 경로:
      • Verify: docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/prd.md
      • Create: docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md
    • RED: 문서 작업은 TDD 예외. TDD 예외 사유: 코드 동작 변경 전 요구사항과 구현 순서를 고정하는 작업이다.
    • GREEN: PRD의 산식, 최근 7일 UTC half-open 범위, 후원 수 distinct 기준, empty marker, fallback timeout/lock 정책을 task로 분해한다.
    • REFACTOR: 기존 홈 추천 구현 파일과 테스트 파일 기준으로 task별 수정/검증 경로를 맞춘다.
    • 기대 결과: 구현 시작 전에 PRD와 plan-task가 같은 디렉터리에 준비된다.

Phase 2: 산식과 부스트 정책

  • Task 2.1: 응원 점수 계산식 계약 테스트 보강

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScoreSpec.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicy.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicyTest.kt
    • RED: CHEER_DONATION_AMOUNT_WEIGHT = 0.45, CHEER_FAN_TALK_WEIGHT = 0.30, CHEER_DONATION_COUNT_WEIGHT = 0.10을 기대하는 실패 테스트를 작성한다. calculateCheerScore(...)가 세 입력값과 부스트를 PRD 산식대로 계산하는지도 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest
    • GREEN: 응원 점수 상수를 PRD 값으로 변경하고 기존 calculateCheerScore(...)가 같은 상수를 사용하게 한다.
    • REFACTOR: 기존 AI/최근 데뷔/인기 커뮤니티 상수와 함수 값은 변경하지 않았는지 같은 테스트 안에서 회귀 assertion을 유지한다.
    • 계산식 테스트 케이스:
      • donationAmount=0, fanTalkCount=0, donationCount=0, newBoost=1.0이면 0.0
      • donationAmount=100, fanTalkCount=0, donationCount=0, newBoost=1.0이면 45.0
      • donationAmount=0, fanTalkCount=10, donationCount=0, newBoost=1.0이면 3.0
      • donationAmount=0, fanTalkCount=0, donationCount=10, newBoost=1.0이면 1.0
      • donationAmount=100, fanTalkCount=10, donationCount=10, newBoost=1.0이면 49.0
      • donationAmount=100, fanTalkCount=10, donationCount=10, newBoost=1.15이면 56.35
    • 기대 결과: 응원 점수 산식 근거가 Kotlin 단위 테스트로 고정된다.
  • Task 2.2: 응원 크리에이터 신규 부스트 경계값 테스트 추가

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScoreSpec.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicy.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicyTest.kt
    • RED: 응원 크리에이터 전용 신규 부스트가 데뷔일 기준 0일/10일 1.15, 11일/20일 1.10, 21일/30일 1.05, 31일 1.0으로 계산되는 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest
    • GREEN: calculateCheerCreatorNewBoost(debutAt, now) 또는 동등한 전용 함수를 추가하고 CHEER_NEW_BOOST_* 상수를 사용한다.
    • REFACTOR: 기존 calculateCreatorNewBoost(...)는 최근 데뷔/인기 커뮤니티용 기존 값 1.5/1.3/1.2를 유지한다.
    • 기대 결과: CHEER_CREATOR만 낮아진 신규 부스트 값을 사용하고 다른 크리에이터 기반 섹션은 기존 부스트를 유지한다.

Phase 3: 최근 7일 집계 window와 DB 스냅샷 query

  • Task 3.1: CHEER_CREATOR refresh window를 인기 커뮤니티와 동일한 최근 7일로 변경

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: refreshCheerCreatorSnapshots(nowUtc)RecommendationSnapshotWindowPolicy.previousKstSevenDayUtcWindow(nowUtc)로 얻은 windowStartUtc, windowEndExclusiveUtc, snapshotAt을 사용하도록 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: CHEER_CREATOR 단일 refresh 경로를 분리하고, 기존 일괄 refresh에서 인기 커뮤니티와 같은 최근 7일 UTC half-open window를 넘긴다.
    • REFACTOR: POPULAR_COMMUNITY의 기존 7일 window와 동일한 window를 사용한다. HomeRecommendationQueryPort.findCheerCreatorSnapshots(...) 시그니처는 windowEndExclusiveUtc 의미가 드러나도록 정리한다.
    • 기대 결과: 스케줄러와 fallback이 같은 CHEER_CREATOR 최근 7일 refresh 경로를 호출할 수 있다.
  • Task 3.2: 채널 후원과 일반 후원 금액/후원 수 집계 기준 변경

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: 같은 UseCanCalculate.useCan을 참조하는 여러 row가 있을 때 donationAmountuse_can_calculate.can 값을 그대로 합산하고, donationCount는 distinct use_can 기준 1건으로 계산되는 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: donation stats query에서 금액은 기존 sum(ucc.can)을 유지하고, 후원 수는 count(distinct ucc.use_can_id) 또는 엔티티 매핑에 맞는 동일 의미 컬럼으로 변경한다.
    • REFACTOR: CanUsage.CHANNEL_DONATIONCanUsage.DONATION을 포함하고, status = RECEIVED, is_refund = false 등 기존 제외 조건은 유지한다.
    • 기대 결과: 후원 이벤트 단위 중복 제거가 점수의 후원 수 항목에만 적용된다.
  • Task 3.3: 팬Talk 수와 half-open 시간 조건 적용

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: CreatorCheers.isActive == true row만 집계하고, created_at >= windowStartUtc and created_at < windowEndExclusiveUtc 조건으로 집계 경계를 검증하는 실패 테스트를 작성한다. windowEndExclusiveUtc와 같은 시각의 row는 제외되어야 한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: creator_cheers 집계 조건을 active row와 half-open time range로 맞춘다.
    • REFACTOR: 후원 집계 조건도 같은 half-open range를 사용해 <= :snapshotAt 방식이 남지 않게 정리한다.
    • 기대 결과: 최근 7일 KST 경계가 후원과 팬Talk 집계에 동일하게 적용된다.
  • Task 3.4: 응원 크리에이터 DB-side 점수와 부스트 적용

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: DB 스냅샷 query 결과 점수가 RecommendationScorePolicy.calculateCheerScore(...)와 동일하고, 데뷔일 기준 신규 부스트 1.15/1.10/1.05/1.0 경계값을 반영하는 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: native SQL score expression과 boost case expression을 RecommendationScoreSpec의 응원 전용 상수로 변경한다.
    • REFACTOR: creator_debut 계산은 콘텐츠 첫 공개일과 라이브 첫 진행일 중 빠른 날짜라는 기존 정의를 유지한다.
    • 기대 결과: DB-side exact scoring과 Kotlin 정책 테스트의 산식 값이 일치한다.
  • Task 3.5: 후보 제외, 정렬, limit 회귀 테스트 보강

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: 후원/팬Talk가 모두 0인 크리에이터 제외, 데뷔일이 없는 크리에이터 제외, 비활성 크리에이터 제외, 점수 내림차순/randomTieBreaker 오름차순/limit 16 동작을 검증하는 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: 기존 후보 제외 조건과 정렬/limit을 PRD 기준에 맞게 유지 또는 보정한다.
    • REFACTOR: AI 캐릭터와 인기 커뮤니티 스냅샷 query가 영향받지 않았는지 focused test로 확인한다.
    • 기대 결과: CHEER_CREATOR 스냅샷 저장 후보만 정확히 변경된다.

Phase 4: refresh 저장과 empty marker

  • Task 4.1: CHEER_CREATOR 빈 결과 marker 저장 정책 추가

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapter.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapterTest.kt
    • RED: replaceSnapshots(RecommendedSectionType.CHEER_CREATOR, snapshotAt, emptyList()) 호출 시 targetId = 0 marker가 저장되고, findLatestSnapshots(CHEER_CREATOR)는 빈 배열이며, existsLatestSnapshot(CHEER_CREATOR)는 true인 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest
    • GREEN: empty marker 적용 대상을 AI_CHARACTERCHEER_CREATOR로 제한하는 명시적 상수/함수를 추가한다.
    • REFACTOR: POPULAR_COMMUNITY 등 다른 섹션으로 marker 정책을 확장하지 않는다.
    • 기대 결과: 데이터가 없는 날에도 CHEER_CREATOR refresh 완료 상태가 저장된다.
  • Task 4.2: 실제 row 재실행 시 marker 대체 보장

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapter.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotPersistenceAdapterTest.kt
    • RED: 같은 sectionType, snapshotAt에 marker가 있는 상태에서 실제 스냅샷 row로 replaceSnapshots(...)를 호출하면 marker가 제거되고 실제 row만 남는 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest
    • GREEN: 기존 deleteBySectionTypeAndSnapshotAt 후 저장 흐름이 marker 대체를 보장하는지 확인하고, 부족하면 해당 경로만 보정한다.
    • REFACTOR: 조회 쿼리의 target_id <> 0 조건은 유지한다.
    • 기대 결과: 빈 결과 refresh 후 재실행으로 실제 추천 row가 생겨도 marker가 응답/존재 상태를 오염시키지 않는다.
  • Task 4.3: refresh service가 CHEER_CREATOR 저장 수와 marker 상태를 로그로 남김

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: refreshCheerCreatorSnapshots(nowUtc) 성공 시 event=cheer_creator_recommendation_snapshot_refresh_success, savedCount, windowStartUtc, windowEndExclusiveUtc, snapshotAt이 로그에 남는 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: 기존 구조화 로그 관례에 맞춰 section refresh 성공/실패 로그를 추가한다.
    • REFACTOR: 전체 일괄 refresh 성공 로그는 유지하되, 섹션별 로그와 중복되어도 검색 가능한 event key를 사용한다.
    • 기대 결과: 운영에서 CHEER_CREATOR refresh 결과 0건과 실패를 구분할 수 있다.

Phase 5: fallback refresh

  • Task 5.1: 섹션 스냅샷 fallback 공통 runner 도입

    • 파일 경로:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/AiCharacterSnapshotFallbackPort.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackServiceTest.kt
    • RED: 공통 runner가 section type, lock key, refresh action, offset/limit을 받아 double-check 조회 후 refresh를 실행하는 실패 테스트를 작성한다. 기존 AI 캐릭터 fallback 테스트도 통과해야 한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest
    • GREEN: AI 캐릭터 전용 lock/timeout/single-flight 흐름을 공통 runner로 이동하거나, AI 서비스가 공통 runner를 위임 호출하도록 최소 변경한다.
    • REFACTOR: Redisson RLock 획득/해제는 refresh worker thread 안에서 수행한다. 테스트에서는 deterministic executor를 사용해 sleep 기반 테스트를 피한다.
    • 기대 결과: CHEER_CREATOR fallback을 추가할 때 lock/double-check/timeout 구현을 중복 작성하지 않는다.
  • Task 5.2: CHEER_CREATOR fallback target 추가

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackServiceTest.kt
    • RED: 최신 CHEER_CREATOR 스냅샷이 없을 때 lock key lock:recommendation-snapshot-refresh:CHEER_CREATOR로 lock을 얻고, refreshCheerCreatorSnapshots(nowUtc)를 호출한 뒤 저장된 스냅샷을 재조회하는 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest
    • GREEN: CHEER_CREATOR fallback target을 등록하고 lock 대기 300ms, 홈 대기 1,500ms 상수를 PRD 값으로 유지한다.
    • REFACTOR: AI_CHARACTER fallback lock key와 상호 간섭하지 않도록 section별 key를 분리한다.
    • 기대 결과: 스냅샷 없음 상황에서 홈 API가 스케줄러와 동일한 CHEER_CREATOR refresh 로직을 재사용한다.
  • Task 5.3: lock miss, timeout, 실패, empty marker fallback 케이스 검증

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotFallbackServiceTest.kt
    • RED: lock 획득 실패 시 중복 refresh를 시작하지 않는 테스트, 홈 대기 1,500ms timeout 시 빈 배열을 반환하되 background refresh를 cancel하지 않는 테스트, refresh 실패 시 빈 배열과 warn log를 남기는 테스트, marker가 있으면 fallback을 반복하지 않는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest
    • GREEN: 공통 runner의 future/single-flight 상태, 예외 처리, timeout 처리, existsLatestSnapshot double-check 흐름을 구현한다.
    • REFACTOR: fallback service는 점수 계산이나 상세 DTO 조립을 직접 하지 않고 snapshot 조회와 refresh orchestration만 담당한다.
    • 기대 결과: 홈 조회 경로에서 refresh 중복, 장시간 대기, 전체 API 실패가 발생하지 않는다.
  • Task 5.4: 홈 응원 크리에이터 조회에 fallback 연결

    • 파일 경로:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
    • RED: findCheerCreatorRecommendations(...)가 최신 스냅샷 없음이면 CHEER_CREATOR fallback을 호출하고, fallback 결과 스냅샷 순서대로 상세를 조립하는 실패 테스트를 작성한다. marker가 있어 existsLatestSnapshot(CHEER_CREATOR) == true이면 fallback을 호출하지 않는 테스트도 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest
    • GREEN: 기존 findAiCharacterSnapshotsWithFallback(...)와 같은 패턴으로 CHEER_CREATOR 스냅샷 조회에 fallback을 연결한다.
    • REFACTOR: 스냅샷 후보 16개 조회, 상세 조회 후 최대 8개 반환, 차단 필터 전달은 기존 동작을 유지한다.
    • 기대 결과: 홈 통합 조회의 최근 응원이 많은 크리에이터 섹션이 스냅샷 없음 상황을 자체 복구할 수 있다.

Phase 6: API 회귀와 최종 검증

  • Task 6.1: 홈 API 응답 스키마 회귀 검증

    • 파일 경로:
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationResponseTest.kt
    • RED: 홈 통합 조회의 CHEER_CREATOR 응답이 기존 creatorId, creatorNickname, creatorProfileImage 필드만 유지하고 신규 필드를 추가하지 않는 회귀 테스트를 확인/보강한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.HomeRecommendationResponseTest
    • GREEN: DTO/Controller 변경 없이 application service 결과가 기존 response로 매핑되게 한다.
    • REFACTOR: 공개 API URL과 JSON field name 변경이 없음을 assertion으로 유지한다.
    • 기대 결과: 클라이언트 공개 스키마는 변경되지 않는다.
  • Task 6.2: focused regression 실행

    • 파일 경로:
      • Modify: docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md
    • RED: 구현 task 완료 후 계획 문서에 기록할 focused command 목록을 확정한다.
    • 실패 확인: 해당 없음. TDD 예외 사유: 검증 기록 문서화 task다.
    • GREEN: 아래 명령을 실행하고 결과를 이 문서 하단 검증 기록에 누적한다.
    • REFACTOR: 실패한 명령이 있으면 원인과 재실행 결과를 같은 task 아래에 기록한다.
    • 실행 명령:
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.HomeRecommendationResponseTest
    • 기대 결과: 산식/집계/marker/fallback/API 회귀가 최소 명령으로 검증된다.
  • Task 6.3: 전체 회귀와 문서 검증

    • 파일 경로:
      • Modify: docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md
    • RED: 구현 완료 후 전체 회귀 명령 실행 전에는 검증 기록이 구현 전 상태여야 한다.
    • 실패 확인: 해당 없음. TDD 예외 사유: 검증 기록 문서화 task다.
    • GREEN: ./gradlew ktlintCheck, ./gradlew test, ./gradlew tasks --all, git diff --check를 실행하고 결과를 검증 기록에 남긴다.
    • REFACTOR: 문서와 코드의 산식/timeout/window 값이 다르면 구현 또는 문서를 수정한 뒤 재검증한다.
    • 기대 결과: 전체 테스트, 포맷, 문서 명령 유효성, diff 공백 검사가 모두 통과한다.

Coverage Check

  • Feature A: Task 2.1, Task 3.2, Task 3.4에서 후원 금액 45%, 팬Talk 30%, 후원 수 10%, 후원 수 distinct 기준을 검증한다.
  • Feature B: Task 3.1, Task 3.3에서 최근 7일 KST 범위를 UTC half-open 조회 범위로 변환하고 windowEndExclusiveUtc 경계를 검증한다.
  • Feature C: Task 2.2, Task 3.4에서 데뷔일 기준 응원 전용 신규 부스트와 경계값을 검증한다.
  • Feature D: Task 3.5, Task 5.4, Task 6.1에서 최신 CHEER_CREATOR 스냅샷 순서, 후보 16개/응답 8개, 기존 응답 스키마 유지를 검증한다.
  • Feature E: Task 5.1, Task 5.2, Task 5.3, Task 5.4에서 fallback refresh 재사용, double-check, 300ms lock 대기, 1,500ms 홈 API 대기, timeout 후 background 완료, 중복 refresh 방지를 검증한다.
  • Feature F: Task 4.1, Task 4.2, Task 5.3에서 CHEER_CREATOR empty marker 저장, 조회 제외, 존재 여부 true, marker 기반 fallback 반복 방지를 검증한다.
  • Non-Goals: Task 4.1, Task 6.1, Task 6.3에서 다른 스냅샷 섹션 marker 확장 없음, 공개 API URL/응답 필드 변경 없음, 신규 DDL 없음, 관리자/ML/A-B 제외를 확인한다.

전체 검증 기록

  • 2026-07-10: PRD 기반으로 plan-task.md를 생성했다. 구현 전 계획 문서 작성 작업이므로 코드 테스트는 아직 실행하지 않았고, 문서 형식/명령 유효성 검증을 진행한다.
  • 2026-07-10: 문서 검증으로 git diff --check -- docs/20260710_메인_홈_추천_응원크리에이터_스냅샷/plan-task.md를 실행해 통과했다. ./gradlew tasks --all은 일반 sandbox에서 ~/.gradle wrapper lock 파일 접근 제한으로 실패했고, 권한 상승 재실행 결과 BUILD SUCCESSFUL로 통과했다.
  • 2026-07-10: 구현 RED 확인으로 RecommendationScorePolicyTestCHEER_NEW_BOOST_*calculateCheerCreatorNewBoost(...) 미구현 컴파일 실패를 확인했고, DefaultHomeRecommendationQueryRepositoryTest는 half-open/distinct 집계 기대값 불일치 실패를 확인했다. RecommendationSnapshotPersistenceAdapterTestCHEER_CREATOR empty marker 미지원 실패를 확인했다.
  • 2026-07-10: focused regression으로 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.api.home.dto.recommendation.HomeRecommendationResponseTest를 실행해 BUILD SUCCESSFUL로 통과했다.
  • 2026-07-10: 최종 검증으로 ./gradlew ktlintCheck, ./gradlew test, ./gradlew tasks --all, git diff --check를 실행했다. ./gradlew test는 최초 300초 timeout 후 600초 timeout으로 재실행해 BUILD SUCCESSFUL로 통과했고, 나머지 명령도 BUILD SUCCESSFUL 또는 출력 없음으로 통과했다.
  • 2026-07-10: 리뷰 보강으로 CHEER_CREATOR 스케줄러 refresh도 section lock을 사용하도록 수정하고, 공통 fallback runner의 기본 worker를 2개 thread로 변경해 AI/응원 섹션 간 fallback 대기 간섭을 줄였다. RecommendationSnapshotFallbackServiceTest에 lock miss, refresh 실패, timeout 후 worker 지속, AI block 중 CHEER fallback 독립 실행 테스트를 추가해 Task 5.3 커버리지와 완료 표시를 맞췄다. 보강 RED 확인 후 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest 재실행 결과 BUILD SUCCESSFUL로 통과했다.
  • 2026-07-10: 리뷰 보강 후 순차 검증으로 ./gradlew ktlintCheck, ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest, focused recommendation/API regression 묶음, git diff --check를 실행했다. 모두 BUILD SUCCESSFUL 또는 출력 없음으로 통과했다.
  • 2026-07-10: 추가 리뷰 보강으로 CHEER_CREATOR 홈/fallback 조회를 expected snapshotAt 기준으로 변경해 stale latest snapshot이 fallback을 막지 않게 했다. RecommendationSnapshotPort.findSnapshots(...), existsSnapshot(...)와 adapter/repository exact snapshot 조회 테스트를 추가했고, DefaultHomeRecommendationQueryRepositoryTestfindCheerCreatorSnapshots(...) 호출부는 두 번째 인자를 windowEndExclusive로 정리했다. 비활성 legacy AiCharacterSnapshotFallbackServiceTest는 삭제하고 AI 핵심 fallback coverage를 RecommendationSnapshotFallbackServiceTest로 이관했다. focused regression, ./gradlew test, ./gradlew ktlintCheck, git diff --check를 재실행해 BUILD SUCCESSFUL 또는 출력 없음으로 통과했다.
  • 2026-07-10: review-work에서 발견한 blocking 이슈를 수정했다. HomeRecommendationQueryService가 expected snapshotAt 계산에 사용한 같은 nowUtcrefreshCheerCreatorIfMissing(...)에 전달하도록 변경했고, RecommendationSnapshotFallbackServiceTest의 기본 현재 시각 의존 호출을 고정 nowUtc로 바꿨다. HomeRecommendationQueryServiceTest는 fallback에 전달된 nowUtc를 검증한다. 재검증으로 focused recommendation/API regression, ./gradlew ktlintCheck, ./gradlew test, git diff --check를 실행했고 모두 BUILD SUCCESSFUL 또는 출력 없음으로 통과했다.