docs(home): 메인 홈 추천 스냅샷 요구사항을 보강한다

This commit is contained in:
2026-05-31 00:56:45 +09:00
parent 029408039d
commit 602063863a
2 changed files with 132 additions and 29 deletions

View File

@@ -24,6 +24,7 @@
- 장르의 크리에이터와 최근 응원이 많은 크리에이터는 동일한 id 리스트 검증/팔로우 저장 로직을 사용한다.
- 페이징 방식: 기존 Spring `Pageable`을 우선 사용하고 응답에는 `items`, `page`, `size`, `hasNext`를 포함한다.
- 시간 응답: `LocalDateTime` 저장값을 기존 관례처럼 KST 기준 저장값으로 보고 UTC ISO 문자열(`...Z`)로 변환한다.
- 스냅샷 일 배치는 KST 매일 06:00:00에 실행하고, 스냅샷 기준 시각은 전날 23:59:59 KST 의미를 코드에서 명확히 계산한다. 스케줄러는 `@Scheduled(cron = "0 0 6 * * *", zone = "Asia/Seoul")`로 등록한다.
- 저장소에는 DB migration 디렉터리가 없으므로 신규 스냅샷/조회 이력 엔티티 추가 시 운영 DB DDL 반영은 배포 절차에서 별도 수행한다. 코드 구현 task에는 JPA 엔티티/리포지토리와 통합 테스트를 포함한다.
---
@@ -51,7 +52,7 @@
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/CreatorContentViewHistoryPort.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/RecommendationSnapshotPort.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepository.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepositoryImpl.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistory.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryRepository.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/RecommendationSnapshot.kt`
@@ -111,7 +112,7 @@
### Phase 2: 스냅샷 엔티티와 일 1회 집계 작업
- [ ] **Task 2.1: 추천 스냅샷 엔티티/리포지토리 추가**
- [x] **Task 2.1: 추천 스냅샷 엔티티/리포지토리 추가**
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/RecommendationSnapshotPort.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/RecommendationSnapshot.kt`
@@ -123,35 +124,112 @@
- REFACTOR: 스냅샷 조회가 없으면 빈 배열을 반환하도록 service 경계에서 처리한다.
- 기대 결과: 스냅샷 없음이 예외가 아니라 빈 결과로 검증된다.
- [ ] **Task 2.2: 스냅샷 갱신 서비스 작성**
- [x] **Task 2.2: 스냅샷 갱신 서비스 작성**
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshService.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepositoryImpl.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
- RED: AI 캐릭터, 최근 응원, 인기 커뮤니티 점수를 전날 23:59:59 기준으로 생성하는 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`
- GREEN: 최근 7일 집계와 신규 부스트를 적용해 `AI_CHARACTER`, `CHEER_CREATOR`, `POPULAR_COMMUNITY` 스냅샷을 저장한다.
- GREEN: 최근 7일 집계와 신규 부스트를 적용해 `AI_CHARACTER`, `CHEER_CREATOR`, `POPULAR_COMMUNITY` 스냅샷을 저장한다. AI 캐릭터의 `followIncrease`는 팔로우 대상/관계 정의가 확정되지 않아 이번 스프린트에서 제외하고 0으로 집계한다.
- REFACTOR: 무거운 QueryDSL 집계는 repository에 두고 점수 산식은 `RecommendationScorePolicy`만 사용한다.
- 기대 결과: 동일 점수 항목은 `randomTieBreaker`가 저장되어 조회 시 랜덤 tie-breaker 정렬에 사용할 수 있다.
- [ ] **Task 2.3: 매일 00:00 스케줄러 추가**
- [x] **Task 2.3: 매일 06:00 KST 스케줄러 추가**
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/scheduler/RecommendationSnapshotScheduler.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
- RED: 스케줄러 메서드에 `@Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul")`이 선언되는지 reflection 테스트를 작성한다.
- RED: KST 매일 06:00:00 cron과 `Asia/Seoul` zone이 선언되는지 reflection 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`
- GREEN: 스케줄러가 `RecommendationSnapshotRefreshService.refreshDailySnapshots()`를 호출하도록 구현한다.
- GREEN: 스케줄러가 KST 06:00:00 cron으로 `RecommendationSnapshotRefreshService.refreshDailySnapshots()`를 호출하도록 구현한다.
- REFACTOR: 스케줄러에는 집계 로직을 두지 않고 호출만 남긴다.
- 기대 결과: 매일 00:00에 전날 23:59:59 기준 집계가 실행되는 계약이 테스트로 고정된다.
- 기대 결과: KST 매일 06:00에 전날 23:59:59 KST 기준 집계가 실행되는 계약이 테스트로 고정된다.
- [x] **Task 2.4: Phase 2 스냅샷 집계 통합 검증과 경계 보강**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/scheduler/RecommendationSnapshotScheduler.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/RecommendationSnapshotPort.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
- RED: QueryDSL 집계 통합 테스트를 추가해 AI 캐릭터 최근 채팅 수/활성 사용자 수, 최근 응원 `CHANNEL_DONATION` 후원 금액/후원 수와 팬 Talk 수, 인기 커뮤니티 좋아요/댓글/팔로워 수가 Phase 2 요구와 일치하는지 검증한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`
- GREEN: 스케줄러 cron을 KST 06:00:00 `Asia/Seoul` zone으로 수정하고, 최근 응원 후원 금액/후원 수는 `CanUsage.CHANNEL_DONATION`만 집계한다.
- REFACTOR: `RecommendationSnapshotPort`가 persistence entity를 직접 노출하지 않도록 application/domain 경계 DTO 또는 모델을 도입해 `port.out` 의존 경계를 정리한다.
- 기대 결과: Phase 2 집계 의미가 DB 기반 테스트로 고정되고, 스케줄러 timezone 계약과 `port.out` 경계 정리가 문서/테스트/구현에 함께 반영된다.
- [x] **Task 2.5: 크리에이터 신규 부스트 실제 데뷔일 적용**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
- RED: 최근 응원/인기 커뮤니티 신규 부스트가 단순 `Member.createdAt`이 아니라 실제 데뷔일을 사용하도록 실패 테스트를 추가한다. 실제 데뷔일은 첫 공개 콘텐츠 일시와 첫 라이브 일시 중 빠른 값이며, 둘 다 없는 경우는 스냅샷 후보에서 제외되는 실패 테스트를 추가한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`
- GREEN: 최근 응원/인기 커뮤니티 후보 DTO가 실제 데뷔일을 담도록 QueryDSL 집계를 수정하고, service는 신규 부스트 계산 시 해당 데뷔일만 사용한다.
- REFACTOR: 데뷔일 의미는 `CreatorDebutPolicy.resolveDebutAt(...)`과 일치하도록 중복 계산을 최소화한다.
- 기대 결과: 최근 응원/인기 커뮤니티 신규 부스트가 `Member.createdAt`이 아니라 실제 데뷔일 기준으로 계산된다.
- [x] **Task 2.6: AI 캐릭터 최근 채팅 수를 AI 발화 수로 고정**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`
- RED: AI 캐릭터 최근 채팅 수가 최근 7일 안에 해당 AI 캐릭터가 발화한 채팅 메시지 수만 세도록 실패 테스트를 추가한다. 사용자 메시지, 다른 캐릭터 메시지, 비활성 메시지, 기간 밖 메시지는 제외되는지 fixture로 검증한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest`
- GREEN: QueryDSL where/join 조건을 보강해 `recentChatCount`가 AI 발화 메시지 수만 반환하도록 구현한다.
- REFACTOR: 테스트 이름과 후보 DTO 필드 설명이 PRD의 "AI가 발화한 채팅 수" 의미를 드러내도록 정리한다.
- 기대 결과: AI 캐릭터 추천 점수의 `최근 발생한 AI 채팅 수` 입력값이 AI 발화 수로 고정된다.
- [x] **Task 2.7: AI 캐릭터 채팅 활성 사용자 수를 중복 없는 채팅 사용자 수로 고정**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`
- RED: 최근 활성 사용자 수가 최근 7일 안에 해당 AI 캐릭터와 1회 이상 채팅한 중복 없는 사용자 수로 계산되도록 실패 테스트를 추가한다. 같은 사용자의 다중 메시지는 1명으로 세고, 다른 캐릭터와만 채팅한 사용자는 제외한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest`
- GREEN: QueryDSL 집계가 캐릭터별 distinct 사용자 수를 반환하도록 구현한다.
- REFACTOR: 활성 사용자 수 집계는 Task 2.6의 AI 발화 수 집계와 의미가 섞이지 않도록 별도 테스트 케이스로 유지한다.
- 기대 결과: AI 캐릭터 추천 점수의 `최근 활성 사용자 수` 입력값이 중복 없는 채팅 사용자 수로 고정된다.
- [x] **Task 2.8: 스냅샷 최종 저장 수를 점수순으로 제한**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`
- RED: 스냅샷 저장 결과가 최종 점수 내림차순과 `randomTieBreaker` 기준으로 AI 캐릭터 최대 20개, 최근 응원이 많은 크리에이터 최대 16개, 인기 커뮤니티 최대 20개만 저장되는 실패 테스트를 추가한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest`
- GREEN: repository 조회에서 최종 점수와 `randomTieBreaker`를 계산하고, 점수 정렬 이후 동점자 랜덤 노출 여지를 위한 섹션별 최종 저장 수를 적용한다. service는 기준 시각 계산과 snapshot replace만 담당한다.
- REFACTOR: `GENRE_CREATOR`는 Phase 2 스냅샷 갱신 대상이 아니라 Task 3.4의 조회 이력 기반 추천임을 문서/테스트 경계로 유지한다.
- 기대 결과: application/service가 전체 후보를 메모리로 불러와 점수를 계산하지 않고, DB에서 정확한 최종 top 후보를 동점자 랜덤 정렬까지 반영해 반환하고 저장한다.
- [x] **Task 2.9: DB-side exact scoring으로 스냅샷 후보 산정 전환**
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScoreSpec.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicy.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicyTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
- RED: `RecommendationScoreSpec` 공유 산식과 DB-scored snapshot 조회 계약이 없으면 컴파일/테스트가 실패하도록 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`
- GREEN: DB 조회에서 모든 적격 후보의 최종 score와 `randomTieBreaker`를 계산한 뒤 `score desc, randomTieBreaker asc` 정렬과 섹션별 최종 limit을 적용한다. service는 기준 시각 계산과 snapshot replace만 담당하고 Kotlin-side score 재계산과 service-side limit을 제거한다.
- REFACTOR: DB score expression과 Kotlin `RecommendationScorePolicy``RecommendationScoreSpec`의 가중치/부스트 구간 상수를 공유하도록 정리하고, 최근 응원/인기 커뮤니티 집계는 aggregate CTE 기반으로 중복 계산을 줄인다.
- 기대 결과: candidate pre-limit 없이 DB에서 정확한 최종 top 후보를 산정하고, 20/16/20 저장 상한은 최종 점수 계산과 동점 랜덤 정렬 이후 적용되는 저장 limit으로만 유지된다.
### Phase 3: 추천 조회 repository와 application service
- [ ] **Task 3.1: 라이브/배너/활동 크리에이터 조회 구현**
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepository.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepositoryImpl.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/HomeRecommendationQueryPort.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt`
- RED: 라이브 최신순 20개, 활성 배너 orders 정렬 최대 20개, 동일 `orders` 배너의 랜덤 tie-breaker 정렬, 크리에이터당 최신 활동 1개만 반환하는 테스트를 작성한다.
@@ -163,7 +241,7 @@
- [ ] **Task 3.2: 최근 데뷔/첫 오디오 콘텐츠 조회 구현**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepositoryImpl.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt`
- RED: 데뷔 후 30일 이내 추천 점수순, 첫 오디오 콘텐츠 3번째 이내 활성 콘텐츠만 인정, 최신성 점수 구간별 정렬, 예약 공개 콘텐츠 제외 테스트를 작성한다.
@@ -175,18 +253,18 @@
- [ ] **Task 3.3: AI 캐릭터/응원/인기 커뮤니티 스냅샷 조회 구현**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepositoryImpl.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt`
- RED: 스냅샷 기준 AI 캐릭터 10개, AI 캐릭터 응답의 캐릭터 이름/소개/전체 채팅 수/오리지널 작품명 조건, 최근 응원 8명, 인기 커뮤니티 10개, 댓글 불가 게시글의 댓글 수 0점 계산, 스냅샷 없음 빈 배열 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryServiceTest`
- GREEN: `RecommendationSnapshotRepository`에서 최신 스냅샷을 읽고 대상 엔티티 상세 정보를 조립한다. AI 캐릭터 작품명은 오리지널 작품 캐릭터인 경우에만 채우고, 인기 커뮤니티 점수 계산 시 댓글 불가 게시글은 댓글 수 0으로 포함한다.
- REFACTOR: 비활성/노출 제한 캐릭터, 커뮤니티 비공개/유료/핀/성인 조건을 repository 조건으로 고정한다.
- 기대 결과: AI 캐릭터 노출 필드가 PRD와 일치하고, 인기 커뮤니티는 크리에이터당 1개만 반환하며 동일 점수는 최신 게시글 우선이다.
- 기대 결과: AI 캐릭터 노출 필드가 PRD와 일치하고, 인기 커뮤니티는 크리에이터당 1개만 반환하며 동일 점수는 스냅샷 생성 시 저장한 랜덤 tie-breaker 기준으로 노출된다.
- [ ] **Task 3.4: 장르 기반 크리에이터 추천 조회 구현**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/HomeRecommendationQueryRepositoryImpl.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt`
- RED: 조회 이력 장르 랜덤 5개, 부족분 랜덤 보충, 장르별 8명, 한 응답 내 크리에이터 중복 제거, 팔로우 크리에이터 제외 테스트를 작성한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryServiceTest`
@@ -325,13 +403,13 @@
- Feature D: Task 1.3, Task 3.1에서 활동 타입, 최신 활동 1개, UTC 시간, 이동 대상 id nullable을 검증한다.
- Feature E: Task 1.1, Task 1.2, Task 3.2, Task 6.3에서 데뷔일/점수/전체보기를 검증한다.
- Feature F: Task 1.1, Task 3.2, Task 6.3에서 첫 오디오 콘텐츠 판정, 최신성 점수 구간, 예약 공개 제외를 검증한다.
- Feature G: Task 1.1, Task 2.2, Task 3.3, Task 6.3에서 AI 캐릭터 점수, 캐릭터 생성일 기준 신규 부스트, 스냅샷, 응답 필드, 오리지널 작품명 조건, 전체보기를 검증한다.
- Feature G: Task 1.1, Task 2.2, Task 2.6, Task 2.7, Task 2.8, Task 3.3, Task 6.3에서 AI 캐릭터 점수, 캐릭터 생성일 기준 신규 부스트, 스냅샷, AI 채팅 집계 범위, 응답 필드, 오리지널 작품명 조건, 전체보기를 검증한다.
- Feature H: Task 3.4, Task 4.1, Task 4.2에서 장르 조회 이력과 장르별 크리에이터 추천을 검증한다.
- Feature I: Task 5.1, Task 5.2에서 장르의 크리에이터와 최근 응원이 많은 크리에이터가 공통 동시 팔로우 use case를 재사용하는지 검증한다.
- Feature J: Task 1.1, Task 2.2, Task 3.3, Task 5.1, Task 5.2에서 최근 응원 점수/스냅샷 조회 해당 섹션의 동시 팔로우를 검증한다.
- Feature K: Task 1.1, Task 2.2, Task 3.3, Task 6.3, Task 7.1에서 인기 커뮤니티 점수/조건/댓글 불가 게시글 댓글 수 0점 계산/전체보기를 검증한다.
- Feature J: Task 1.1, Task 2.2, Task 2.4, Task 2.5, Task 2.8, Task 3.3, Task 5.1, Task 5.2에서 최근 응원 점수/스냅샷 조회, `CHANNEL_DONATION` 기준 후원 금액/후원 수, 데뷔일 기준 신규 부스트, 해당 섹션의 동시 팔로우를 검증한다.
- Feature K: Task 1.1, Task 2.2, Task 2.5, Task 2.8, Task 3.3, Task 6.3, Task 7.1에서 인기 커뮤니티 점수/조건/댓글 불가 게시글 댓글 수 0점 계산, 데뷔일 기준 신규 부스트, 전체보기를 검증한다.
- Metrics: Task 7.2에서 PRD Metrics 항목의 로그 또는 metric 기록 지점을 검증한다.
- Technical Constraints: Phase 1~7에서 `v2.api.home`/`v2.recommend` 패키지 경계, `port.out` 의존 방향, 신규 v2 endpoint 분리, 기존 공개 스키마 유지 조건을 검증한다.
- Technical Constraints: Phase 1~7에서 `v2.api.home`/`v2.recommend` 패키지 경계, `port.out` 의존 방향, 신규 v2 endpoint 분리, 기존 공개 스키마 유지 조건을 검증한다. `RecommendationSnapshotPort`의 persistence entity 노출 정리는 Task 2.4에서 검증한다.
---
@@ -350,3 +428,17 @@
- 2026-05-30: Phase 1 Task 1.2 RED/GREEN을 진행했다. `CreatorDebutPolicyTest`는 구현 전 `Unresolved reference: CreatorDebutPolicy`로 실패했고, 구현 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.domain.CreatorDebutPolicyTest``BUILD SUCCESSFUL`로 통과했다.
- 2026-05-30: Phase 1 Task 1.3 RED/GREEN을 진행했다. `HomeRecommendationQueryServiceTest`는 구현 전 `RecommendedActivityType`, `RecommendedSectionType`, `HomeRecommendationQueryService` 미구현으로 실패했고, 구현 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryServiceTest``BUILD SUCCESSFUL`로 통과했다.
- 2026-05-30: Phase 1 최종 검증으로 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommend.*'`, `./gradlew ktlintCheck`, `./gradlew test`를 실행했고 모두 `BUILD SUCCESSFUL`로 통과했다. Kotlin LSP는 이 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행하지 못했고, Gradle 컴파일/테스트/ktlint로 대체 확인했다.
- 2026-05-30: Phase 2 Task 2.1~2.3 RED/GREEN을 진행했다. `RecommendationSnapshotRefreshServiceTest`는 구현 전 `RecommendationSnapshot`, `RecommendationSnapshotPort`, `HomeRecommendationQueryPort`, `RecommendationSnapshotRefreshService`, `RecommendationSnapshotScheduler` 미구현으로 실패했고, 구현 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest``BUILD SUCCESSFUL`로 통과했다.
- 2026-05-30: Phase 2 검증으로 `./gradlew clean test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommend.*'`, `./gradlew ktlintCheck`, `./gradlew test`를 순차 실행했고 모두 `BUILD SUCCESSFUL`로 통과했다. 병렬 Gradle 실행 중에는 KAPT 임시 stub 파일 경합이 발생해 이후 검증은 순차 실행으로 고정했다. Kotlin LSP는 이 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행하지 못했고, Gradle 컴파일/테스트/ktlint로 대체 확인했다.
- 2026-05-30: 기본 구현체 명명 규칙을 접미사 `Impl` 대신 접두사 `Default`로 변경했다. `HomeRecommendationQueryRepositoryImpl``DefaultHomeRecommendationQueryRepository`로 바꿨고, PRD와 구현 계획에 AI 캐릭터 `followIncrease`는 팔로우 대상/관계 정의 확정 전까지 이번 스프린트 산식과 집계에서 제외한다고 기록했다.
- 2026-05-30: 구현 전 문서 보강으로 기본 구현체 명명 규칙을 `docs/agent-guides/코드스타일.md`에 반영하고, 당시 스냅샷 일 배치 기준을 PRD/Task 2.3~2.4에 기록했다. 이후 Phase 2 권고 보강에서 스케줄은 KST 06:00 `Asia/Seoul` zone으로 변경했다. QueryDSL 집계 통합 테스트, `RecommendationSnapshotPort` 경계 정리, 최근 응원 `CHANNEL_DONATION` 기준 후원 금액/후원 수 검증은 Task 2.4로 추가했다.
- 2026-05-30: Phase 2 Task 2.4 RED/GREEN을 진행했다. RED에서 `RecommendationSnapshotRefreshServiceTest`는 기존 KST cron/JVM timezone 기준 계산으로 실패했고, `DefaultHomeRecommendationQueryRepositoryTest`는 최근 응원 후원 금액이 `CHANNEL_DONATION` 외 사용처까지 포함되어 `expected: <150> but was: <3150>`으로 실패했다. GREEN 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest`, `./gradlew ktlintCheck`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommend.*'`를 순차 실행했고 모두 `BUILD SUCCESSFUL`로 통과했다.
- 2026-05-30: Phase 2 재점검을 진행했다. `RecommendationSnapshotRefreshServiceTest``DefaultHomeRecommendationQueryRepositoryTest`는 각각 재실행 시 `BUILD SUCCESSFUL`로 통과했지만, 최근 응원/인기 커뮤니티 신규 부스트가 실제 데뷔일이 아니라 `Member.createdAt`에 의존하는 점, AI 캐릭터 최근 채팅 수의 participant 범위가 명확히 고정되지 않은 점, 스냅샷 후보 전체 저장은 과도한 데이터 저장으로 이어질 수 있다는 점을 확인했다. 해당 보완사항은 Task 2.5~2.8과 Coverage Check에 나누어 반영했고, 실제 데뷔일이 없는 크리에이터는 Task 2.5에서 스냅샷 후보 제외로 확정하고 테스트로 검증했다.
- 2026-05-30: Phase 2 Task 2.5~2.8 보강을 진행했다. `DefaultHomeRecommendationQueryRepositoryTest``RecommendationSnapshotRefreshServiceTest`에 실제 데뷔일 기준 후보, AI 발화 수, 중복 없는 활성 사용자 수, 섹션별 스냅샷 저장 상한(20/16/20) 검증을 추가했다. 첫 실행은 `AudioContent.theme` fixture 누락과 QueryDSL alias 문제로 실패했고, 보정 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest``BUILD SUCCESSFUL`로 통과했다.
- 2026-05-30: Phase 2 Task 2.5~2.8 최종 검증으로 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommend.*'`, `./gradlew ktlintCheck`, `./gradlew test`를 순차 실행했고 모두 `BUILD SUCCESSFUL`로 통과했다. Kotlin LSP는 이 환경에 `kotlin-lsp`가 설치되어 있지 않아 실행하지 못했고, Gradle 컴파일/테스트/ktlint로 대체 확인했다.
- 2026-05-30: Phase 2 권고 보강으로 스냅샷 스케줄을 KST 06:00 `Asia/Seoul` zone으로 변경했다. 최종 점수 계산 전 후보 사전 제한은 정확한 top 후보를 누락할 수 있어 적용하지 않는다. AI 20개, 최근 응원 16개, 인기 커뮤니티 20개 저장 상한은 최종 점수와 동점 랜덤 정렬 이후 repository에서 적용하는 최종 limit으로 유지한다.
- 2026-05-30: 사용자 피드백에 따라 service가 전체 후보를 모두 불러와 점수를 계산하는 구조를 DB-side exact scoring으로 전환하기로 확정했다. PRD와 Task 2.9에 `RecommendationScoreSpec` 공유 산식, DB 최종 점수 계산 후 정렬/limit, candidate pre-limit 금지, service scoring 제거 요구사항을 반영했다. 기존 20/16/20 저장 상한은 동점자 랜덤 노출 여지를 위한 최종 저장 limit으로 유지하되, 최종 점수 계산 전 후보 제한 의미로는 사용하지 않도록 명확히 했다.
- 2026-05-31: Phase 2 Task 2.9 RED/GREEN을 진행했다. RED에서 `RecommendationScoreSpec`과 DB-scored snapshot 조회 계약 미구현으로 `RecommendationScorePolicyTest`, `DefaultHomeRecommendationQueryRepositoryTest`, `RecommendationSnapshotRefreshServiceTest` 컴파일이 실패했다. GREEN에서 `RecommendationScoreSpec`을 추가하고, AI/최근 응원/인기 커뮤니티 스냅샷 조회가 DB에서 최종 score와 `randomTieBreaker`를 계산한 뒤 `score desc, randomTieBreaker asc` 정렬과 최종 limit을 적용하도록 변경했다. `RecommendationSnapshotRefreshService`에서는 Kotlin-side score 재계산과 service-side limit을 제거했다.
- 2026-05-31: Phase 2 Task 2.9 검증으로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommend.*'`, `./gradlew ktlintCheck`, `./gradlew test`를 순차 실행했고 모두 `BUILD SUCCESSFUL`로 통과했다.
- 2026-05-31: Phase 2 Task 2.9 리뷰 후속으로 `RecommendationScoreSpec`에 신규 부스트 일수 상수를 추가하고, `RecommendationScorePolicy`와 native SQL boost window가 같은 상수를 쓰도록 정리했다. 최근 응원/인기 커뮤니티 native SQL은 후보 행마다 donation/comment/follower/debut 집계를 반복하지 않도록 aggregate CTE 기반으로 변경했고, 데뷔일은 콘텐츠 공개일과 라이브 시작일을 `union all`한 이벤트 집계에서 `min(debut_at)`으로 계산해 DB-side exact scoring 의미를 유지했다. PRD/plan-task의 동일 점수 정렬 문구는 스냅샷 저장 `randomTieBreaker` 기준으로 맞췄다.
- 2026-05-31: 리뷰 후속 검증으로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommend.*'`, `./gradlew ktlintCheck`, `./gradlew test`를 순차 실행했고 모두 `BUILD SUCCESSFUL`로 통과했다. 중간에 H2 native SQL 타입 추론 문제와 ktlint line length 오류가 있었고, 데뷔일 CTE를 `union all` 이벤트 집계로 단순화하고 긴 `setParameter` 호출을 줄바꿈해 해결했다.