|
|
|
@@ -20,12 +20,12 @@
|
|
|
|
- 기본 노출 수:
|
|
|
|
- 기본 노출 수:
|
|
|
|
- `banners`: 메인 홈 추천 배너와 동일
|
|
|
|
- `banners`: 메인 홈 추천 배너와 동일
|
|
|
|
- `originalSeries`: 최신순 12개
|
|
|
|
- `originalSeries`: 최신순 12개
|
|
|
|
- `latestAudios`: 최신순 12개
|
|
|
|
- `latestAudios`: 최신순 15개
|
|
|
|
- `newAndHotAudios`: 최대 12개
|
|
|
|
- `newAndHotAudios`: 최대 12개
|
|
|
|
- `freeAudios`: 최대 10개 랜덤
|
|
|
|
- `freeAudios`: 최대 15개 랜덤
|
|
|
|
- `pointAudios`: 최대 10개 랜덤
|
|
|
|
- `pointAudios`: 최대 15개 랜덤
|
|
|
|
- `mostCommentedAudios`: 최대 5개
|
|
|
|
- `mostCommentedAudios`: 최대 5개
|
|
|
|
- `recommendedAudios`: 최대 10개
|
|
|
|
- `recommendedAudios`: 최대 20개
|
|
|
|
- 공개 오디오 공통 조건: `AudioContent.isActive == true`, `AudioContent.duration != null`, `AudioContent.releaseDate != null`, `AudioContent.releaseDate <= now`, 크리에이터 회원 활성.
|
|
|
|
- 공개 오디오 공통 조건: `AudioContent.isActive == true`, `AudioContent.duration != null`, `AudioContent.releaseDate != null`, `AudioContent.releaseDate <= now`, 크리에이터 회원 활성.
|
|
|
|
- 비회원과 19금 노출 불가 회원은 성인 콘텐츠를 제외하고 `SAFE` 스냅샷을 조회한다.
|
|
|
|
- 비회원과 19금 노출 불가 회원은 성인 콘텐츠를 제외하고 `SAFE` 스냅샷을 조회한다.
|
|
|
|
- 19금 노출 가능 회원은 성인/비성인 콘텐츠를 모두 포함하는 `ALL` 스냅샷을 조회한다.
|
|
|
|
- 19금 노출 가능 회원은 성인/비성인 콘텐츠를 모두 포함하는 `ALL` 스냅샷을 조회한다.
|
|
|
|
@@ -346,7 +346,7 @@ interface AudioRecommendationQueryPort {
|
|
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/AudioRecommendationQueryRepository.kt`
|
|
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/AudioRecommendationQueryRepository.kt`
|
|
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`
|
|
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`
|
|
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt`
|
|
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt`
|
|
|
|
- RED: 배너는 기존 홈 추천 배너와 동일 필드/활성/차단 정책을 적용하고, 오리지널 시리즈는 `isOriginal = true` 최신순 12개, 최신 오디오는 `releaseDate desc`, `audioContentId desc` 12개를 반환하는 repository 테스트를 작성한다.
|
|
|
|
- RED: 배너는 기존 홈 추천 배너와 동일 필드/활성/차단 정책을 적용하고, 오리지널 시리즈는 `isOriginal = true` 최신순 12개, 최신 오디오는 `releaseDate desc`, `audioContentId desc` 15개를 반환하는 repository 테스트를 작성한다.
|
|
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest`
|
|
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest`
|
|
|
|
- GREEN: QueryDSL로 `findBanners`, `findOriginalSeries`, `findLatestAudios`를 구현한다. 이미지 경로는 `toCdnUrl(cloudFrontHost)`를 사용한다.
|
|
|
|
- GREEN: QueryDSL로 `findBanners`, `findOriginalSeries`, `findLatestAudios`를 구현한다. 이미지 경로는 `toCdnUrl(cloudFrontHost)`를 사용한다.
|
|
|
|
- REFACTOR: 공개 오디오 조건, 성인 콘텐츠 조건, 차단 관계 조건을 private 조건 함수로 분리한다.
|
|
|
|
- REFACTOR: 공개 오디오 조건, 성인 콘텐츠 조건, 차단 관계 조건을 private 조건 함수로 분리한다.
|
|
|
|
@@ -356,7 +356,7 @@ interface AudioRecommendationQueryPort {
|
|
|
|
- Files:
|
|
|
|
- Files:
|
|
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`
|
|
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`
|
|
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt`
|
|
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt`
|
|
|
|
- RED: 무료 오디오는 `price = 0` 공개 오디오 중 최대 10개, 포인트 오디오는 `isPointAvailable = true` 공개 오디오 중 최대 10개를 반환하고 두 섹션 간 중복을 제거하지 않는 테스트를 작성한다.
|
|
|
|
- RED: 무료 오디오는 `price = 0` 공개 오디오 중 최대 15개, 포인트 오디오는 `isPointAvailable = true` 공개 오디오 중 최대 15개를 반환하고 두 섹션 간 중복을 제거하지 않는 테스트를 작성한다.
|
|
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest`
|
|
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest`
|
|
|
|
- GREEN: `findFreeAudios`, `findPointAudios`를 구현하고 DB 랜덤 정렬은 기존 repository 관례에 맞춰 `Expressions.numberTemplate(Double::class.java, "function('rand')")` 또는 동일 프로젝트에서 쓰는 랜덤 정렬 방식을 사용한다.
|
|
|
|
- GREEN: `findFreeAudios`, `findPointAudios`를 구현하고 DB 랜덤 정렬은 기존 repository 관례에 맞춰 `Expressions.numberTemplate(Double::class.java, "function('rand')")` 또는 동일 프로젝트에서 쓰는 랜덤 정렬 방식을 사용한다.
|
|
|
|
- REFACTOR: 무료/포인트 조회가 같은 공통 projection 함수를 사용하게 정리한다.
|
|
|
|
- REFACTOR: 무료/포인트 조회가 같은 공통 projection 함수를 사용하게 정리한다.
|
|
|
|
@@ -399,11 +399,11 @@ interface AudioRecommendationQueryPort {
|
|
|
|
- Files:
|
|
|
|
- Files:
|
|
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`
|
|
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`
|
|
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt`
|
|
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt`
|
|
|
|
- RED: 상세 조회수 45%, 좋아요 25%, 댓글 수 20%, 최신성 10% 점수를 계산하고 `SAFE/ALL` visibility별 최대 10개 후보를 반환하는 테스트를 작성한다.
|
|
|
|
- RED: 상세 조회수 45%, 좋아요 25%, 댓글 수 20%, 최신성 10% 점수를 계산하고 `SAFE/ALL` visibility별 최대 20개 후보를 반환하는 테스트를 작성한다.
|
|
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest`
|
|
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest`
|
|
|
|
- GREEN: `findRecommendedAudioSnapshots(...)`를 구현한다. 상세 조회수는 `creator_content_view_history` count를 사용하고 `AudioContent.playCount`를 사용하지 않는다.
|
|
|
|
- GREEN: `findRecommendedAudioSnapshots(...)`를 구현한다. 상세 조회수는 `creator_content_view_history` count를 사용하고 `AudioContent.playCount`를 사용하지 않는다.
|
|
|
|
- REFACTOR: New & Hot과 공유 가능한 조회수/좋아요/댓글 aggregate CTE를 private SQL fragment 또는 QueryDSL helper로 정리한다.
|
|
|
|
- REFACTOR: New & Hot과 공유 가능한 조회수/좋아요/댓글 aggregate CTE를 private SQL fragment 또는 QueryDSL helper로 정리한다.
|
|
|
|
- 기대 결과: `RECOMMENDED_AUDIO_SAFE/ALL`에 저장할 top 10 후보가 정확한 점수순으로 산출된다.
|
|
|
|
- 기대 결과: `RECOMMENDED_AUDIO_SAFE/ALL`에 저장할 top 20 후보가 정확한 점수순으로 산출된다.
|
|
|
|
|
|
|
|
|
|
|
|
- [x] **Task 4.4: 스냅샷 refresh service와 lazy 보강 구현**
|
|
|
|
- [x] **Task 4.4: 스냅샷 refresh service와 lazy 보강 구현**
|
|
|
|
- Files:
|
|
|
|
- Files:
|
|
|
|
@@ -680,3 +680,5 @@ interface AudioRecommendationQueryPort {
|
|
|
|
- 2026-06-23 리뷰 보정 검증: repository focused test는 병렬 Gradle 실행 중 `kaptGenerateStubsTestKotlin` 출력 디렉터리 충돌로 1회 실패해 단독 재실행했다. H2 `MODE=MySQL`의 `TIMESTAMPDIFF` 경계 동작이 운영 MySQL 공식 기준과 달라 신규 repository 경계 테스트는 제거하고 Kotlin 정책 테스트로 24시간 경계를 고정했다. 최종 `./gradlew test --tests kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest`: `BUILD SUCCESSFUL`.
|
|
|
|
- 2026-06-23 리뷰 보정 검증: repository focused test는 병렬 Gradle 실행 중 `kaptGenerateStubsTestKotlin` 출력 디렉터리 충돌로 1회 실패해 단독 재실행했다. H2 `MODE=MySQL`의 `TIMESTAMPDIFF` 경계 동작이 운영 MySQL 공식 기준과 달라 신규 repository 경계 테스트는 제거하고 Kotlin 정책 테스트로 24시간 경계를 고정했다. 최종 `./gradlew test --tests kr.co.vividnext.sodalive.v2.audio.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest`: `BUILD SUCCESSFUL`.
|
|
|
|
- 2026-06-23 리뷰 보정 검증: `./gradlew ktlintCheck`: `BUILD SUCCESSFUL`, `git diff --check`: 출력 없음.
|
|
|
|
- 2026-06-23 리뷰 보정 검증: `./gradlew ktlintCheck`: `BUILD SUCCESSFUL`, `git diff --check`: 출력 없음.
|
|
|
|
- 2026-06-25 후속 보정: `DefaultAudioRecommendationQueryRepositoryTest.shouldFindNewAndHotSnapshotsWithVisibility`의 score 비교 실패 원인은 repository native SQL의 `timestampdiff(day, c.release_date, :snapshotAt)` 최신성 계산이 DB 날짜 경계 기준에 의존해 `AudioRecommendationScorePolicy`의 24시간 경과 기준 `ChronoUnit.DAYS` 계산과 어긋날 수 있는 점으로 확인했다. `DefaultAudioRecommendationQueryRepository`의 New & Hot/추천 오디오 공개일 최신성 계산을 `floor(timestampdiff(hour, c.release_date, :snapshotAt) / 24)`로 변경해 Kotlin 정책과 일치시켰고, `SAFE` 성인 콘텐츠 제외 조건은 기존 `(:includeAdult = true or c.is_adult = false)` 구현이 올바른 것으로 확인했다. 검증은 `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest.shouldFindNewAndHotSnapshotsWithVisibility'`, `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationScorePolicyTest'`, `./gradlew ktlintCheck` 모두 `BUILD SUCCESSFUL`로 완료했다.
|
|
|
|
- 2026-06-25 후속 보정: `DefaultAudioRecommendationQueryRepositoryTest.shouldFindNewAndHotSnapshotsWithVisibility`의 score 비교 실패 원인은 repository native SQL의 `timestampdiff(day, c.release_date, :snapshotAt)` 최신성 계산이 DB 날짜 경계 기준에 의존해 `AudioRecommendationScorePolicy`의 24시간 경과 기준 `ChronoUnit.DAYS` 계산과 어긋날 수 있는 점으로 확인했다. `DefaultAudioRecommendationQueryRepository`의 New & Hot/추천 오디오 공개일 최신성 계산을 `floor(timestampdiff(hour, c.release_date, :snapshotAt) / 24)`로 변경해 Kotlin 정책과 일치시켰고, `SAFE` 성인 콘텐츠 제외 조건은 기존 `(:includeAdult = true or c.is_adult = false)` 구현이 올바른 것으로 확인했다. 검증은 `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.content.recommendation.adapter.out.persistence.DefaultAudioRecommendationQueryRepositoryTest.shouldFindNewAndHotSnapshotsWithVisibility'`, `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationScorePolicyTest'`, `./gradlew ktlintCheck` 모두 `BUILD SUCCESSFUL`로 완료했다.
|
|
|
|
|
|
|
|
- 2026-07-12 후속 정책 변경: 콘텐츠 추천 탭의 `latestAudios`, `freeAudios`, `pointAudios` 기본 노출 수를 각각 15개로 확정했다. PRD/plan-task의 섹션별 기본 노출 수와 관련 task 설명을 15개 정책으로 갱신했고, service 테스트는 `AudioRecommendationQueryService` 상수 기준으로 limit 전달을 검증하도록 정리했다. 검증은 `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest`, `./gradlew --no-daemon ktlintCheck`, `./gradlew --no-daemon tasks --all` 모두 `BUILD SUCCESSFUL`로 완료했다. 단, `tasks --all`은 일반 sandbox에서 `~/.gradle` wrapper lock 파일 접근 제한으로 실패해 승인 후 재실행했다.
|
|
|
|
|
|
|
|
- 2026-07-12 후속 정책 변경: 콘텐츠 추천 탭의 `recommendedAudios`와 `RECOMMENDED_AUDIO_SAFE/ALL` 스냅샷 후보 저장 수를 최대 20개로 확정했다. PRD의 섹션별 기본 노출 수와 추천 오디오 요구사항, plan-task의 개요와 Task 4.3 후보 산정 설명을 20개 정책으로 갱신했다. 해당 개수는 정책값으로 이후 변경될 수 있으므로 테스트는 `assertEquals(20, RECOMMENDED_AUDIO_LIMIT)`처럼 숫자 자체를 고정하지 않고, 기존 service/refresh 테스트에서 `RECOMMENDED_AUDIO_LIMIT`가 snapshot 조회와 후보 산정 port 호출에 전달되는 흐름을 검증하는 방식으로 유지했다. 검증은 이전 개수 정책 문구 잔존 검색 결과 없음, `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationSnapshotRefreshServiceTest` `BUILD SUCCESSFUL`, `git diff --check` 출력 없음, `./gradlew --no-daemon tasks --all` `BUILD SUCCESSFUL`로 완료했다. 단, `tasks --all`은 일반 sandbox에서 `~/.gradle` wrapper lock 파일 접근 제한으로 실패해 승인 후 재실행했다.
|
|
|
|
|