34 KiB
메인 홈 추천 인기 커뮤니티 스냅샷 수정 Plan/Task
시나리오 계약
- Happy path: 홈 API 호출 시각 기준 KST 전날 대상일
snapshotAt을 계산하고, 대상일 포함 최근 7일 데이터를 UTC half-open 범위로 변환해POPULAR_COMMUNITY점수를 계산한 뒤 점수순 상위 20개 스냅샷을 저장한다. Real surface:RecommendationSnapshotWindowPolicyTest,DefaultHomeRecommendationQueryRepositoryTest,RecommendationSnapshotRefreshServiceTest. - Score: 인기 커뮤니티 점수는
((likeCount * 0.50) + (commentCount * 0.40) + (creatorFollowerCount * 0.10)) * newBoost다.creatorFollowerCount는 스냅샷 생성 시점의 활성 팔로워 총수다. Real surface:RecommendationScorePolicyTest,DefaultHomeRecommendationQueryRepositoryTest. - Exclusion: 공지/고정 게시글(
CreatorCommunity.isFixed/is_fixed = true)과 유료 게시글(price > 0)은 스냅샷 후보와 상세 조회 결과에서 제외한다. Real surface:DefaultHomeRecommendationQueryRepositoryTest,HomeRecommendationQueryServiceTest. - Boost: 신규 부스트는 크리에이터 데뷔일 기준 0
10일20일1.15, 111.10, 21~30일1.05, 31일 이상1.0이다. Real surface:RecommendationScorePolicyTest,DefaultHomeRecommendationQueryRepositoryTest. - Daily freshness: 과거 최신
POPULAR_COMMUNITY스냅샷이 있어도 홈 API 호출 기준 대상일snapshotAt스냅샷이 없으면 fallback refresh를 시도한다. Real surface:HomeRecommendationQueryServiceTest,RecommendationSnapshotFallbackServiceTest. - Fallback: 대상일
POPULAR_COMMUNITY스냅샷이 없으면 lock, double-check, 동일 refresh 로직 재사용, refresh 후 재조회 순서로 fallback을 실행한다. lock 대기는 최대 300ms, 홈 API refresh 완료 대기는 최대 1,500ms다. Real surface: fallback service test,HomeRecommendationQueryServiceTest. - Empty marker:
POPULAR_COMMUNITYrefresh 결과가 0건이면targetId = 0marker를 저장해 정상 refresh 완료 상태를 남기고, 조회 응답에서는 marker를 제외한다. Real surface:RecommendationSnapshotPersistenceAdapterTest,HomeRecommendationQueryServiceTest. - Adjacent regression: 메인 홈 추천 API URL과
popularCommunityPosts응답 필드는 변경하지 않는다. AI 캐릭터, 응원 크리에이터, 최근 데뷔 등 다른 섹션 산식과 공개 스키마는 이번 변경으로 바꾸지 않는다. Real surface: 기존 focused tests,HomeRecommendationControllerTest.
범위와 전제
- 이번 문서는
docs/20260710_메인_홈_추천_인기커뮤니티_스냅샷/prd.md의 구현 계획이다. - 신규 공개 API, 신규 응답 필드, 운영 DDL 추가는 범위에 포함하지 않는다.
- 기존
recommendation_snapshot테이블과RecommendedSectionType.POPULAR_COMMUNITY를 재사용한다. POPULAR_COMMUNITY집계는 현재 구조와 성능 특성을 유지해 DB-side exact scoring을 기본으로 한다. Kotlin 단에는 산식/부스트 근거 테스트용 정책 함수를 둔다.- 최근 7일 기준은 KST 대상일 포함 7일 00:00:00 이상, 대상일 다음날 00:00:00 미만이며, DB 조회에는 UTC half-open window를 사용한다.
- 홈 조회는 대상일
snapshotAt스냅샷을 우선 조회한다. 최신 스냅샷이 대상일보다 과거이면 fallback refresh 대상으로 본다. - fallback orchestration은 기존
RecommendationSnapshotFallbackService에POPULAR_COMMUNITYtarget을 추가하는 최소 변경을 우선한다. - empty marker는 이번 범위에서
POPULAR_COMMUNITY에만 추가한다. 다른 섹션 공통화는 별도 후속 작업으로 둔다.
기존 POPULAR_COMMUNITY 로직 유지/변경 경계
- 유지:
RecommendedSectionType.POPULAR_COMMUNITYenum 값과 code는 변경하지 않는다. - 유지: 홈 인기 커뮤니티 응답 필드는 변경하지 않는다.
- 유지: 스냅샷 후보 저장 수 최대 20개, 홈 첫 화면 반환 수 최대 10개를 유지한다.
- 유지: 상세 조회 시점의 활성 게시글/크리에이터 필터, 성인 필터, 차단 필터, 크리에이터 중복 제거 정책을 유지한다.
- 유지: 댓글 불가 게시글은 댓글 수를 0으로 계산한다.
- 변경: 댓글 가중치를
0.50에서0.40으로 바꾼다. - 변경: 신규 부스트는 기존 크리에이터 공통 부스트
1.5/1.3/1.2가 아니라POPULAR_COMMUNITY전용1.15/1.10/1.05를 사용한다. - 변경: 좋아요/댓글 집계 기간은 UTC half-open 최근 7일 window를 사용한다.
- 변경: 게시글 자체 생성 시각 조건도 대상일
windowEndExclusiveUtc이전으로 맞춘다. - 추가:
POPULAR_COMMUNITY대상일 스냅샷이 없을 때 fallback refresh를 실행한다. - 추가:
POPULAR_COMMUNITYrefresh 결과 0건이면 empty snapshot marker를 저장한다.
실행 명령
- 문서 명령 확인:
./gradlew tasks --all - 산식/부스트/window 단위 테스트:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicyTest - 스냅샷 저장 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.RecommendationSnapshotFallbackServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest - 홈 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
- Verify:
- RED: 문서 작업은 TDD 예외. TDD 예외 사유: 코드 동작 변경 전 요구사항과 구현 순서를 고정하는 작업이다.
- GREEN: PRD의 산식, 최근 7일 KST/UTC half-open 범위, 공지/유료 제외, 대상일
snapshotAt조회, empty marker, fallback timeout/lock 정책을 task로 분해한다. - REFACTOR: 기존 홈 추천 구현 파일과 테스트 파일 기준으로 task별 수정/검증 경로를 맞춘다.
- 기대 결과: 구현 시작 전에 PRD와 plan-task가 같은 디렉터리에 준비된다.
- 파일 경로:
Phase 2: 산식, 부스트, window 정책
-
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
- Modify:
- RED:
COMMUNITY_LIKE_WEIGHT = 0.50,COMMUNITY_COMMENT_WEIGHT = 0.40,COMMUNITY_FOLLOWER_WEIGHT = 0.10을 기대하는 실패 테스트를 작성한다.calculateCommunityScore(...)가 세 입력값과 부스트를 PRD 산식대로 계산하는지도 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest - GREEN: 인기 커뮤니티 점수 상수를 PRD 값으로 변경하고 기존
calculateCommunityScore(...)가 같은 상수를 사용하게 한다. - REFACTOR: 기존 AI/최근 데뷔/응원 크리에이터 상수와 함수 값은 변경하지 않았는지 같은 테스트 안에서 회귀 assertion을 유지한다.
- 계산식 테스트 케이스:
likeCount=0,commentCount=0,followerCount=0,newBoost=1.0이면0.0likeCount=10,commentCount=0,followerCount=0,newBoost=1.0이면5.0likeCount=0,commentCount=10,followerCount=0,newBoost=1.0이면4.0likeCount=0,commentCount=0,followerCount=10,newBoost=1.0이면1.0likeCount=40,commentCount=20,followerCount=100,newBoost=1.0이면38.0likeCount=40,commentCount=20,followerCount=100,newBoost=1.15이면43.7
- 기대 결과: 인기 커뮤니티 점수 산식 근거가 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
- Modify:
- 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:
calculateCommunityCreatorNewBoost(debutAt, now)또는 동등한 전용 함수를 추가하고COMMUNITY_NEW_BOOST_*상수를 사용한다. - REFACTOR: 기존
calculateCreatorNewBoost(...)는 최근 데뷔용 기존 값1.5/1.3/1.2를 유지한다.CHEER_CREATOR전용 부스트와 같은 값이더라도 이름은 섹션별 의도를 드러낸다. - 기대 결과:
POPULAR_COMMUNITY만 낮아진 신규 부스트 값을 사용하고 다른 크리에이터 기반 섹션은 기존 부스트를 유지한다.
- 파일 경로:
-
Task 2.3: 최근 7일 KST window 정책 추가
- 파일 경로:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationSnapshotWindowPolicy.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationSnapshotWindowPolicyTest.kt
- Modify:
- RED:
nowUtc = 2026-07-10T03:00:00이면 KST 기준 대상일이2026-07-09이고, 최근 7일 window가 UTC2026-07-02T15:00:00 <= t < 2026-07-09T15:00:00,snapshotAt = 2026-07-09T14:59:59로 계산되는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicyTest - GREEN:
previousKstSevenDayUtcWindow(nowUtc)또는 의미가 명확한 함수를 추가한다. - REFACTOR: 기존
previousKstDayUtcWindow(...)는 AI 캐릭터/응원 크리에이터 전날 하루 집계에서 계속 사용하므로 변경하지 않는다. - 기대 결과: 인기 커뮤니티만 최근 7일 window를 사용하고, 대상일
snapshotAt계산은 다른 섹션과 일관된다.
- 파일 경로:
Phase 3: DB 스냅샷 query와 상세 조회
-
Task 3.1:
POPULAR_COMMUNITYrefresh 입력을 최근 7일 UTC half-open window로 변경- 파일 경로:
- 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
- Modify:
- RED:
refreshPopularCommunitySnapshots(nowUtc)가 최근 7일 window의windowStartUtc,windowEndExclusiveUtc,snapshotAt을 사용해queryPort.findPopularCommunitySnapshots(...)와snapshotPort.replaceSnapshots(...)를 호출하는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest - GREEN:
POPULAR_COMMUNITY단일 refresh 경로를 분리하고, 기존 일괄 refresh에서 같은 경로를 호출하게 한다. - REFACTOR:
HomeRecommendationQueryPort.findPopularCommunitySnapshots(...)시그니처는windowEndExclusiveUtc의미가 드러나도록 정리한다. - 기대 결과: 스케줄러와 fallback이 같은
POPULAR_COMMUNITY최근 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
- Modify:
- RED: 활성 좋아요/댓글만 최근 7일 half-open window 안에서 distinct id로 집계하고,
creatorFollowerCount는 기간 조건 없이 스냅샷 생성 시점의 활성 팔로워 총수로 집계하는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest - GREEN:
like_stats,comment_stats,follower_statsnative query를 PRD 기준으로 수정한다. 좋아요/댓글은created_at >= :windowStartUtc and created_at < :windowEndExclusiveUtc, 팔로워는is_active = true총수로 계산한다. - REFACTOR: 댓글 불가 게시글은 기존처럼
commentCount = 0으로 계산하는 조건을 유지한다. - 기대 결과: 세 metric의 기간/활성 조건이 서로 다른 의도대로 고정된다.
- 파일 경로:
-
Task 3.3: 공지/고정 및 유료 게시글 제외 조건 고정
- 파일 경로:
- 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
- Modify:
- RED:
is_fixed = true게시글,price > 0게시글, 비활성 게시글, 비활성 크리에이터가 스냅샷 후보에서 제외되는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest - GREEN: 스냅샷 query의 후보 조건을
cc.is_active = true,m.is_active = true,cc.is_fixed = false,cc.price <= 0기준으로 맞춘다. - REFACTOR: 기존
cc.price = 0조건이 도메인상 무료 판정과 다르면price <= 0로 통일한다. - 기대 결과: 공지/유료 게시글이 추천 스냅샷에 저장되지 않는다.
- 파일 경로:
-
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
- Modify:
- RED: DB 스냅샷 query 결과 점수가
RecommendationScorePolicy.calculateCommunityScore(...)와 동일하고, 데뷔일 기준 신규 부스트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
- Modify:
- RED: 좋아요/댓글/팔로워 수가 모두 0인 게시글 제외, 데뷔일이 없는 크리에이터 제외,
created_at >= windowEndExclusiveUtc게시글 제외, 점수 내림차순/randomTieBreaker오름차순/limit 20 동작을 검증하는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest - GREEN: 기존 후보 제외 조건과 정렬/limit을 PRD 기준에 맞게 유지 또는 보정한다.
- REFACTOR: AI 캐릭터와 응원 크리에이터 스냅샷 query가 영향받지 않았는지 focused test로 확인한다.
- 기대 결과:
POPULAR_COMMUNITY스냅샷 저장 후보만 정확히 변경된다.
- 파일 경로:
-
Task 3.6: 인기 커뮤니티 상세 조회에도 공지/유료 제외 적용
- 파일 경로:
- 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
- Modify:
- RED: 스냅샷 저장 이후 게시글이
isFixed = true또는price > 0로 변경되면findPopularCommunityRecommendationDetails(...)결과에서 제외되는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest - GREEN: 상세 조회 where 조건에
creatorCommunity.isFixed.isFalse, 무료 조건을 명시한다. - REFACTOR: 성인 필터, 차단 필터, 좋아요 여부, 구매 여부 계산은 기존 동작을 유지한다.
- 기대 결과: 스냅샷 생성 이후 상태가 바뀐 공지/유료 게시글도 홈 응답에 노출되지 않는다.
- 파일 경로:
Phase 4: refresh 저장과 empty marker
-
Task 4.1:
POPULAR_COMMUNITY빈 결과 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
- Modify:
- RED:
replaceSnapshots(RecommendedSectionType.POPULAR_COMMUNITY, snapshotAt, emptyList())호출 시targetId = 0marker가 저장되고,findSnapshots(POPULAR_COMMUNITY, snapshotAt)는 빈 배열이며,existsSnapshot(POPULAR_COMMUNITY, snapshotAt)는 true인 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshotPersistenceAdapterTest - GREEN: empty marker 적용 대상에
POPULAR_COMMUNITY를 추가한다. - REFACTOR: marker 대상 조건은
supportsEmptySnapshotMarker(...)같은 명시적 함수에 유지하고, 조회 쿼리의target_id <> 0조건은 변경하지 않는다. - 기대 결과: 데이터가 없는 날에도
POPULAR_COMMUNITYrefresh 완료 상태가 저장된다.
- 파일 경로:
-
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
- Modify:
- 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:
AI_CHARACTER,CHEER_CREATORmarker 동작 회귀 테스트도 함께 유지한다. - 기대 결과: 빈 결과 refresh 후 재실행으로 실제 추천 row가 생겨도 marker가 응답/존재 상태를 오염시키지 않는다.
- 파일 경로:
-
Task 4.3: refresh service가
POPULAR_COMMUNITY저장 수와 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
- Modify:
- RED:
refreshPopularCommunitySnapshots(nowUtc)성공 시event=popular_community_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를 사용한다.
- 기대 결과: 운영에서
POPULAR_COMMUNITYrefresh 결과 0건과 실패를 구분할 수 있다.
- 파일 경로:
Phase 5: 대상일 조회와 fallback refresh
-
Task 5.1: 홈 인기 커뮤니티 조회를 대상일
snapshotAt기준으로 변경- 파일 경로:
- 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
- Modify:
- RED: 과거 최신
POPULAR_COMMUNITY스냅샷이 있어도 홈 API 호출 기준 대상일snapshotAt스냅샷이 없으면 과거 스냅샷을 반환하지 않고 fallback을 호출하는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest - GREEN:
RecommendationSnapshotWindowPolicy로 대상일snapshotAt을 계산하고snapshotPort.findSnapshots(POPULAR_COMMUNITY, snapshotAt, ...)를 사용한다. - REFACTOR: 스냅샷 후보 20개 조회, 상세 조회 후 최대 10개 반환, 크리에이터 중복 제거, 차단/성인 필터 전달은 기존 동작을 유지한다.
- 기대 결과: 인기 커뮤니티 홈 조회가 일 단위 최신성을 강제한다.
- 파일 경로:
-
Task 5.2:
POPULAR_COMMUNITYfallback 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
- Modify:
- RED: 대상일
POPULAR_COMMUNITY스냅샷이 없을 때 lock keylock:recommendation-snapshot-refresh:POPULAR_COMMUNITY로 lock을 얻고,refreshPopularCommunitySnapshots(nowUtc)를 호출한 뒤 대상일 스냅샷을 재조회하는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest - GREEN:
POPULAR_COMMUNITYfallback target을 등록하고 lock 대기 300ms, 홈 대기 1,500ms 상수를 PRD 값으로 유지한다. - REFACTOR:
AI_CHARACTER,CHEER_CREATORfallback lock key와 상호 간섭하지 않도록 section별 key를 분리한다. - 기대 결과: 스냅샷 없음 상황에서 홈 API가 스케줄러와 동일한
POPULAR_COMMUNITYrefresh 로직을 재사용한다.
- 파일 경로:
-
Task 5.3: fallback double-check를 대상일
snapshotAt기준으로 검증- 파일 경로:
- 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
- Modify:
- RED: lock 획득 후 대상일
snapshotAt스냅샷이 이미 존재하면 refresh를 실행하지 않고 해당 스냅샷을 반환하는 실패 테스트를 작성한다. 과거 최신 스냅샷만 있는 경우에는 refresh를 실행해야 한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotFallbackServiceTest - GREEN: fallback service의 조회/존재 확인을
findSnapshots(sectionType, snapshotAt, ...),existsSnapshot(sectionType, snapshotAt)기준으로 수행한다. - REFACTOR: AI 캐릭터/응원 크리에이터는 기존 요구사항과 충돌하지 않도록 필요한 경우 대상일 snapshot 조회 방식으로 함께 정리하되, 산식/window는 변경하지 않는다.
- 기대 결과: fallback double-check가 일 단위 최신성 요구와 일치한다.
- 파일 경로:
-
Task 5.4: 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
- Modify:
- 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 처리, existsSnapshot double-check 흐름을 구현한다.
- REFACTOR: fallback service는 점수 계산이나 상세 DTO 조립을 직접 하지 않고 snapshot 조회와 refresh orchestration만 담당한다.
- 기대 결과: 홈 조회 경로에서 refresh 중복, 장시간 대기, 전체 API 실패가 발생하지 않는다.
- 파일 경로:
-
Task 5.5: 스케줄러와 fallback refresh lock key 공유
- 파일 경로:
- 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
- Modify:
- RED: 일괄 scheduler refresh가
POPULAR_COMMUNITY갱신 시lock:recommendation-snapshot-refresh:POPULAR_COMMUNITY를 사용하고, lock miss 시 중복 refresh를 실행하지 않는 실패 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest - GREEN: 기존 AI 캐릭터/응원 크리에이터 section lock 패턴에
POPULAR_COMMUNITY를 추가한다. - REFACTOR: 세 섹션 lock 처리 중복이 커지면 section type과 lock key를 받는 최소 helper로 정리한다.
- 기대 결과: 스케줄러와 홈 fallback이 같은 섹션 lock으로 중복 refresh를 막는다.
- 파일 경로:
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
- Test:
- RED: 홈 통합 조회의
popularCommunityPosts응답이 기존 필드만 유지하고 신규 필드를 추가하지 않는 회귀 테스트를 확인/보강한다. - 실패 확인:
./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
- Modify:
- RED: 구현 task 완료 후 계획 문서에 기록할 focused command 목록을 확정한다.
- 실패 확인: 해당 없음. TDD 예외 사유: 검증 기록 문서화 task다.
- GREEN: 아래 명령을 실행하고 결과를 이 문서 하단 검증 기록에 누적한다.
- REFACTOR: 실패한 명령이 있으면 원인과 재실행 결과를 같은 task 아래에 기록한다.
- 실행 명령:
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicyTest./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
- 기대 결과: 산식/window/집계/marker/fallback/API 회귀가 최소 명령으로 검증된다.
- 파일 경로:
-
Task 6.3: 전체 회귀와 문서 검증
- 파일 경로:
- Modify:
docs/20260710_메인_홈_추천_인기커뮤니티_스냅샷/plan-task.md
- Modify:
- RED: 구현 완료 후 전체 회귀 명령 실행 전에는 검증 기록이 구현 전 상태여야 한다.
- 실패 확인: 해당 없음. TDD 예외 사유: 검증 기록 문서화 task다.
- GREEN:
./gradlew ktlintCheck,./gradlew test,./gradlew tasks --all을 실행하고 결과를 문서 하단 검증 기록에 누적한다. - REFACTOR: PRD와 plan-task가 구현 결과와 어긋나면 먼저 문서를 갱신하고 필요한 focused test를 재실행한다.
- 기대 결과: 포맷, 전체 테스트, 문서 명령 유효성을 모두 확인한다.
- 파일 경로:
검증 기록
-
2026-07-10: 구현 계획 문서 작성. 코드 변경은 아직 수행하지 않았다.
-
2026-07-10: focused regression 통과.
./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicyTest성공../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성공.
-
2026-07-10: 전체 회귀와 문서 명령 검증 통과.
./gradlew ktlintCheck성공../gradlew test성공../gradlew tasks --all성공.
-
2026-07-10: 리뷰 게이트 후 차단 이슈 수정 및 재검증 통과.
- 리뷰 지적:
POPULAR_COMMUNITY0점 후보 제외 누락, 최근 데뷔 크리에이터 query에 커뮤니티 전용 부스트 상수 침투. - 수정: 인기 커뮤니티 후보에 좋아요/댓글/팔로워 중 하나 이상 존재 조건 추가, 최근 데뷔 크리에이터는 기존
NEW_BOOST_*상수로 복원. - 회귀 테스트 추가: 0점 인기 커뮤니티 후보 제외, 최근 데뷔 크리에이터 기존 신규 부스트 유지.
./gradlew ktlintCheck성공../gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest성공../gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationSnapshotWindowPolicyTest --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성공../gradlew cleanTest test성공.
- 리뷰 지적:
-
2026-07-10:
POPULAR_COMMUNITYsnapshot 후보 paid/fixed 제외 직접 통합 테스트 보강.- 추가:
shouldExcludePaidAndFixedPostsFromPopularCommunitySnapshots에서 점수가 있는 유료/고정 게시글도 스냅샷 후보에서 제외됨을 검증. ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest성공../gradlew ktlintCheck성공.
- 추가: