445 lines
55 KiB
Markdown
445 lines
55 KiB
Markdown
# 메인 홈 추천 API Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`로 task 단위 구현을 진행한다. 각 단계는 체크박스(`- [ ]`)로 진행 상태를 갱신한다.
|
|
|
|
**Goal:** `/api/v2/home/recommendations` 하위에 메인 홈 추천 통합 조회, 섹션별 전체보기, 콘텐츠 조회 이력 기록, 추천 크리에이터 동시 팔로우 API를 제공한다.
|
|
|
|
**Architecture:** 공개 API 조립은 `kr.co.vividnext.sodalive.v2.api.home`에 두고, 추천 정책/점수/스냅샷/조회 이력/팔로우 기능은 `kr.co.vividnext.sodalive.v2.recommend`에 둔다. `v2.api.home`은 `v2.recommend`의 application use case만 호출하며, `v2.recommend`는 API DTO에 의존하지 않는다.
|
|
|
|
**Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring Data JPA, QueryDSL, JUnit 5, Gradle Wrapper
|
|
|
|
---
|
|
|
|
## 0. 구현 전 확정 사항
|
|
|
|
- 통합 조회: `GET /api/v2/home/recommendations`
|
|
- 전체보기 조회:
|
|
- `GET /api/v2/home/recommendations/lives`
|
|
- `GET /api/v2/home/recommendations/debut-creators`
|
|
- `GET /api/v2/home/recommendations/first-audio-contents`
|
|
- `GET /api/v2/home/recommendations/ai-characters`
|
|
- `GET /api/v2/home/recommendations/communities`
|
|
- 추천 크리에이터 동시 팔로우: `POST /api/v2/home/recommendations/creators/follow`
|
|
- 요청에는 `creatorIds`만 포함한다.
|
|
- 장르의 크리에이터와 최근 응원이 많은 크리에이터는 동일한 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 엔티티/리포지토리와 통합 테스트를 포함한다.
|
|
|
|
---
|
|
|
|
## 1. 파일 구조 계획
|
|
|
|
### 신규 API 조립 계층
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/HomeRecommendationController.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/HomeRecommendationResponse.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/HomeRecommendationPageResponse.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/FollowRecommendedCreatorsRequest.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/FollowRecommendedCreatorsResponse.kt`
|
|
|
|
### 신규 추천 기능 계층
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicy.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/CreatorDebutPolicy.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendedActivityType.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendedSectionType.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryService.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryService.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendedCreatorFollowService.kt`
|
|
- 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`
|
|
- 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/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`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/RecommendationSnapshotRepository.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/scheduler/RecommendationSnapshotScheduler.kt`
|
|
|
|
### 기존 코드 연결
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
|
|
- 콘텐츠 상세 조회 성공 시 `CreatorContentViewHistoryService.recordView(...)`를 호출한다.
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt`
|
|
- 기존 공개 스키마는 유지하고 인증 회원 정보를 서비스로 전달하는 기존 흐름만 활용한다.
|
|
|
|
### 테스트
|
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicyTest.kt`
|
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/CreatorDebutPolicyTest.kt`
|
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt`
|
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryServiceTest.kt`
|
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendedCreatorFollowServiceTest.kt`
|
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
|
|
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`
|
|
|
|
---
|
|
|
|
### Phase 1: 도메인 정책과 공통 모델
|
|
|
|
- [x] **Task 1.1: 추천 점수/신규 부스트 정책 작성**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicy.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendationScorePolicyTest.kt`
|
|
- RED: `shouldApplyCreatorNewBoostByDebutDays`, `shouldApplyAiCharacterNewBoostByCreatedDays`, `shouldCalculateDebutCreatorScore`, `shouldCalculateAiChatScore`, `shouldCalculateCheerScore`, `shouldCalculateCommunityScore`, `shouldCalculateFirstAudioRecencyScore` 테스트를 먼저 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScorePolicyTest`
|
|
- GREEN: PRD 산식과 부스트 값을 그대로 구현한다. AI 캐릭터 신규 부스트는 캐릭터 생성일 기준 10일 이내 1.5, 20일 이내 1.3, 30일 이내 1.2, 그 외 1을 적용한다. 첫 오디오 최신성 점수는 `release_date` 기준 3일 이내 100, 7일 이내 80, 14일 이내 60, 21일 이내 40, 30일 이내 20을 적용한다.
|
|
- REFACTOR: 산식별 public 함수명과 파라미터가 PRD 용어를 반영하는지 정리한다.
|
|
- 기대 결과: 모든 산식/부스트/최신성 점수 테스트가 PASS이고 소수 계산 오차는 `assertEquals(expected, actual, 0.0001)` 범위 안에 들어간다.
|
|
|
|
- [x] **Task 1.2: 데뷔일/신규 크리에이터 판정 정책 작성**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/CreatorDebutPolicy.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/CreatorDebutPolicyTest.kt`
|
|
- RED: 첫 공개 콘텐츠 일시와 첫 라이브 일시 중 빠른 값을 데뷔일로 선택하는 테스트, 데뷔 후 30일 이내만 true인 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.domain.CreatorDebutPolicyTest`
|
|
- GREEN: `resolveDebutAt(firstContentPublishedAt, firstLiveAt)`와 `isNewCreator(debutAt, now)`를 구현한다.
|
|
- REFACTOR: 기존 `ExplorerService.getCreatorDetail`의 `debutDateTime` 계산과 비교해 의미가 어긋나지 않는지 확인한다.
|
|
- 기대 결과: 콘텐츠만 있는 경우, 라이브만 있는 경우, 둘 다 있는 경우, 둘 다 없는 경우가 모두 명확히 검증된다.
|
|
|
|
- [x] **Task 1.3: 섹션/활동 enum과 내부 응답 모델 작성**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendedActivityType.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/domain/RecommendedSectionType.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: `LIVE_REPLAY` 테마 콘텐츠가 `AUDIO`가 아니라 `LIVE_REPLAY`로 분류되는 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryServiceTest`
|
|
- GREEN: 내부 모델에 `LIVE`, `AUDIO`, `COMMUNITY`, `LIVE_REPLAY` enum을 추가하고 활동 분류 함수를 구현한다.
|
|
- REFACTOR: enum 값은 앱 다국어 처리를 위해 영문 code와 동일하게 유지한다.
|
|
- 기대 결과: 활동 타입 응답 문자열이 PRD의 enum 후보와 일치한다.
|
|
|
|
### Phase 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`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/RecommendationSnapshotRepository.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
|
|
- RED: 섹션 타입, 대상 id, 점수, 기준 시각, 랜덤 tie-breaker를 저장하고 기준 시각별 최신 스냅샷만 읽는 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`
|
|
- GREEN: `RecommendationSnapshot` JPA 엔티티와 `findTop...`, `deleteBySectionTypeAndSnapshotAt` 계열 리포지토리 메서드를 구현하고, application service가 의존할 `RecommendationSnapshotPort`를 둔다.
|
|
- REFACTOR: 스냅샷 조회가 없으면 빈 배열을 반환하도록 service 경계에서 처리한다.
|
|
- 기대 결과: 스냅샷 없음이 예외가 아니라 빈 결과로 검증된다.
|
|
|
|
- [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/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` 스냅샷을 저장한다. AI 캐릭터의 `followIncrease`는 팔로우 대상/관계 정의가 확정되지 않아 이번 스프린트에서 제외하고 0으로 집계한다.
|
|
- REFACTOR: 무거운 QueryDSL 집계는 repository에 두고 점수 산식은 `RecommendationScorePolicy`만 사용한다.
|
|
- 기대 결과: 동일 점수 항목은 `randomTieBreaker`가 저장되어 조회 시 랜덤 tie-breaker 정렬에 사용할 수 있다.
|
|
|
|
- [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: KST 매일 06:00:00 cron과 `Asia/Seoul` zone이 선언되는지 reflection 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`
|
|
- GREEN: 스케줄러가 KST 06:00:00 cron으로 `RecommendationSnapshotRefreshService.refreshDailySnapshots()`를 호출하도록 구현한다.
|
|
- REFACTOR: 스케줄러에는 집계 로직을 두지 않고 호출만 남긴다.
|
|
- 기대 결과: 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:
|
|
- 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개만 반환하는 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryServiceTest`
|
|
- GREEN: `LiveRoom`, `AudioContentBanner`, `AudioContent`, `CreatorCommunity` 기반 QueryDSL 조회를 구현한다. application service는 `HomeRecommendationQueryPort`에만 의존하고 persistence 구현체가 port를 구현한다. 배너는 기존 콘텐츠 홈 배너의 앱 이동 필드를 유지하고, 동일 `orders` 값은 후보군 축소 후 랜덤화하거나 랜덤 tie-breaker를 적용한다.
|
|
- REFACTOR: 차단 관계, 비활성 회원, 비활성 콘텐츠/배너 제외 조건을 공통 private 조건 함수로 정리한다.
|
|
- 기대 결과: 특정 섹션 데이터가 부족해도 service가 가능한 개수만 반환한다.
|
|
|
|
- [ ] **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/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번째 이내 활성 콘텐츠만 인정, 최신성 점수 구간별 정렬, 예약 공개 콘텐츠 제외 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryServiceTest`
|
|
- GREEN: 데뷔일 계산, 최근 7일/30일 집계, `release_date` 기준 최신성 점수, 동점 랜덤 정렬을 구현한다.
|
|
- REFACTOR: 데뷔일 계산은 `CreatorDebutPolicy`, 산식은 `RecommendationScorePolicy`만 호출하도록 중복 제거한다.
|
|
- 기대 결과: 앞선 비활성 콘텐츠가 3개 이상이면 이후 활성 콘텐츠가 제외된다.
|
|
|
|
- [ ] **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/DefaultHomeRecommendationQueryRepository.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt`
|
|
- RED: 스냅샷 기준 AI 캐릭터 10개, AI 캐릭터 응답의 캐릭터 이름/소개/전체 채팅 수/오리지널 작품명 조건, 최근 응원 8명, 인기 커뮤니티 10개, 스냅샷 없음 빈 배열 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryServiceTest`
|
|
- GREEN: `RecommendationSnapshotRepository`에서 최신 스냅샷을 읽고 대상 엔티티 상세 정보를 조립한다. AI 캐릭터 작품명은 오리지널 작품 캐릭터인 경우에만 채우고, 인기 커뮤니티는 스냅샷에 저장된 점수/랜덤 tie-breaker 순서를 유지한다.
|
|
- REFACTOR: 비활성/노출 제한 캐릭터, 커뮤니티 비공개/유료/핀/성인 조건을 repository 조건으로 고정한다.
|
|
- 기대 결과: 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/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`
|
|
- GREEN: `CreatorContentViewHistory`와 콘텐츠 장르 매핑을 기반으로 후보 장르/크리에이터를 조회한다.
|
|
- REFACTOR: 성인 장르는 `MemberContentPreference.isAdultContentVisible == true` 회원에게만 포함되도록 조건을 공통화한다.
|
|
- 기대 결과: 비회원 또는 조회 이력 없는 회원도 조회 가능한 장르 중 랜덤 5개를 받는다.
|
|
|
|
### Phase 4: 콘텐츠 조회 이력 기록
|
|
|
|
- [ ] **Task 4.1: 콘텐츠 조회 이력 엔티티/서비스 작성**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/port/out/CreatorContentViewHistoryPort.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/application/CreatorContentViewHistoryService.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryServiceTest.kt`
|
|
- RED: 인증 회원의 콘텐츠 상세 진입 시 memberId/contentId/genreId/viewedAt이 저장되는 테스트와 비회원은 저장하지 않는 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.CreatorContentViewHistoryServiceTest`
|
|
- GREEN: 이력 저장 service와 repository를 구현한다. application service는 `CreatorContentViewHistoryPort`에만 의존하고 persistence 구현체가 port를 구현한다.
|
|
- REFACTOR: 동일 회원/콘텐츠의 연속 중복 저장 허용 여부는 추천 이력으로 보존하며, 집계 시 distinct 장르 기준으로 처리한다.
|
|
- 기대 결과: 조회 이력 저장 실패가 콘텐츠 상세 조회 자체를 실패시키지 않도록 호출부에서 예외 전파 범위를 제한한다.
|
|
|
|
- [ ] **Task 4.2: 기존 콘텐츠 상세 조회 흐름에 이력 기록 연결**
|
|
- Files:
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt`
|
|
- RED: `getDetail` 성공 시 `CreatorContentViewHistoryService.recordView(...)`가 호출되는 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest`
|
|
- GREEN: `AudioContentService` 생성자에 optional하지 않은 신규 service 의존성을 추가하고 상세 조회 성공 지점에서 기록한다.
|
|
- REFACTOR: 기존 `GetAudioContentDetailResponse` 스키마와 Controller URL/응답은 변경하지 않는다.
|
|
- 기대 결과: 기존 상세 조회 테스트가 모두 통과하고 응답 JSON 필드가 바뀌지 않는다.
|
|
|
|
### Phase 5: 추천 크리에이터 동시 팔로우
|
|
|
|
- [ ] **Task 5.1: 팔로우 use case 작성**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendedCreatorFollowService.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendedCreatorFollowServiceTest.kt`
|
|
- RED: 신규 팔로우 id와 이미 팔로우/제외 id를 구분하는 테스트, 존재하지 않는 id/크리에이터가 아닌 id/본인 id 포함 시 전체 실패 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendedCreatorFollowServiceTest`
|
|
- GREEN: `MemberRepository`, `CreatorFollowingRepository`를 사용해 전체 입력 검증 후 신규 팔로우만 저장한다.
|
|
- REFACTOR: 섹션별 분기 없이 팔로우 처리 로직은 `followCreators(member, creatorIds)` 하나로 유지한다.
|
|
- 기대 결과: 유효하지 않은 id가 하나라도 있으면 신규 저장이 발생하지 않는다.
|
|
|
|
- [ ] **Task 5.2: 팔로우 API DTO/Controller 연결**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/FollowRecommendedCreatorsRequest.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/FollowRecommendedCreatorsResponse.kt`
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/HomeRecommendationController.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`
|
|
- RED: 비로그인 요청은 `common.error.bad_credentials`, 로그인 요청은 `creatorIds`를 service에 전달하고 결과를 `ApiResponse.ok`로 반환하는 controller 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest`
|
|
- GREEN: `POST /api/v2/home/recommendations/creators/follow`를 구현한다.
|
|
- REFACTOR: request id 리스트가 비어 있으면 `SodaException`으로 거부한다.
|
|
- 기대 결과: 응답에 `followedCreatorIds`, `skippedCreatorIds`가 포함된다.
|
|
|
|
### Phase 6: 홈 통합/전체보기 API
|
|
|
|
- [ ] **Task 6.1: 홈 통합 응답 DTO와 facade 작성**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/HomeRecommendationResponse.kt`
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`
|
|
- RED: 통합 조회가 섹션별 기본 limit(20/20/10/10/10/10/5x8/8/10)을 service에 전달하고, 인증 회원의 팔로우 제외/콘텐츠 조회 이력/본인인증 여부를 service 조건으로 전달하는 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest`
|
|
- GREEN: facade가 `HomeRecommendationQueryService` 결과를 API DTO로 변환한다. 인증 회원이면 팔로우 제외, 콘텐츠 조회 이력, 본인인증 여부 조건을 조회 context에 포함하고 비회원이면 회원 의존 조건을 제외한다.
|
|
- REFACTOR: API DTO에는 앱 이동 대상 id가 없는 라이브 활동의 target id를 nullable로 둔다.
|
|
- 기대 결과: 특정 섹션이 빈 배열이어도 통합 조회는 성공 응답이다.
|
|
|
|
- [ ] **Task 6.2: 홈 통합 Controller 작성**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/HomeRecommendationController.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`
|
|
- RED: `GET /api/v2/home/recommendations`가 인증 회원/비회원 모두 호출 가능하고 `ApiResponse.ok`를 반환하는 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest`
|
|
- GREEN: `@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?` 패턴으로 구현한다.
|
|
- REFACTOR: controller에는 인증 null 허용과 request parameter 전달 외 로직을 두지 않는다.
|
|
- 기대 결과: 비회원은 회원 의존 조건 없이 기본 추천을 받는다.
|
|
|
|
- [ ] **Task 6.3: 섹션별 전체보기 API 작성**
|
|
- Files:
|
|
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/HomeRecommendationPageResponse.kt`
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/adapter/in/web/HomeRecommendationController.kt`
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`
|
|
- RED: 라이브/최근 데뷔/첫 오디오/AI 캐릭터/인기 커뮤니티 전체보기 endpoint가 `page`, `size`를 전달하는 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest`
|
|
- GREEN: 확정 URL 5개를 controller에 추가하고 `HomeRecommendationPageResponse`로 반환한다.
|
|
- REFACTOR: size 기본값은 홈 기본 노출 수와 분리해 `20`으로 두고 최대값은 `50`으로 제한한다.
|
|
- 기대 결과: 모든 전체보기 API가 같은 페이징 응답 형식을 사용한다.
|
|
|
|
### Phase 7: 통합 검증과 문서 갱신
|
|
|
|
- [ ] **Task 7.1: repository 조건 회귀 테스트 보강**
|
|
- Files:
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/HomeRecommendationQueryServiceTest.kt`
|
|
- RED: 차단 관계 양방향 제외, 커뮤니티 성인 노출 조건, 본인인증 여부 조건이 필요한 추천 필터, 팔로우 크리에이터 제외, 비활성 회원 제외 테스트를 추가한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryServiceTest`
|
|
- GREEN: 누락 조건을 QueryDSL where 조건 또는 service 필터에 추가한다.
|
|
- REFACTOR: 같은 차단/성인 조건이 여러 쿼리에 반복되면 repository private 함수로만 정리한다.
|
|
- 기대 결과: PRD Edge Case와 회원 조건이 테스트 이름으로 추적된다.
|
|
|
|
- [ ] **Task 7.2: 운영 지표 기록 지점 확인**
|
|
- Files:
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt`
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryService.kt`
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendedCreatorFollowService.kt`
|
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshService.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryServiceTest.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendedCreatorFollowServiceTest.kt`
|
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/RecommendationSnapshotRefreshServiceTest.kt`
|
|
- RED: 메인 홈 API 성공/실패, 섹션별 빈 응답 여부, 전체보기 조회, 동시 팔로우 요청/성공 수, 콘텐츠 조회 이력 기록 성공/실패, 일 배치 집계 성공/실패와 소요 시간을 관측할 수 있는 로그 또는 metric 호출 테스트를 작성한다.
|
|
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.recommend.application.CreatorContentViewHistoryServiceTest --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendedCreatorFollowServiceTest --tests kr.co.vividnext.sodalive.v2.recommend.application.RecommendationSnapshotRefreshServiceTest`
|
|
- GREEN: 프로젝트에 이미 사용하는 metric 클라이언트가 있으면 해당 클라이언트를 사용하고, 없으면 구조화 로그로 PRD Metrics 항목을 관측 가능하게 남긴다.
|
|
- REFACTOR: 지표 기록 때문에 공개 응답 스키마나 비즈니스 분기가 바뀌지 않도록 application 경계에서만 정리한다.
|
|
- 기대 결과: PRD Metrics 항목이 구현 코드의 로그/metric 지점으로 추적되고 테스트에서 호출 여부를 확인할 수 있다.
|
|
|
|
- [ ] **Task 7.3: 전체 테스트/린트 검증**
|
|
- Files:
|
|
- Modify: `docs/20260529_메인_홈_추천_API/plan-task.md`
|
|
- TDD 예외 사유: 검증 명령 실행과 문서 기록 task라 제품 코드 테스트를 새로 작성하지 않는다.
|
|
- 대체 검증 방법:
|
|
- `./gradlew test`
|
|
- `./gradlew ktlintCheck`
|
|
- `./gradlew tasks --all`
|
|
- 기대 결과: 세 명령이 모두 성공하고, 이 문서 하단 검증 기록에 실행 일시/명령/결과를 누적한다.
|
|
|
|
---
|
|
|
|
## PRD Coverage Check
|
|
|
|
- Feature A: Phase 3, Phase 6, Phase 7에서 통합 조회, limit, 인증/비회원, 팔로우 제외, 콘텐츠 조회 이력, 본인인증 여부, 차단 필터, 스냅샷 빈 배열 처리를 검증한다.
|
|
- Feature B: Task 3.1, Task 6.3에서 라이브 최신순/전체보기/비활성 회원 제외를 검증한다.
|
|
- Feature C: Task 3.1에서 기존 콘텐츠 홈 배너 재활용, orders 정렬, 동일 orders 랜덤 정렬, 활성 배너/콘텐츠 조건, 앱 이동 필드 유지를 검증한다.
|
|
- 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 2.6, Task 2.7, Task 2.8, Task 2.9, Task 3.3, Task 6.3에서 AI 캐릭터 점수, 캐릭터 생성일 기준 신규 부스트, 스냅샷, AI 채팅 집계 범위, DB-side exact scoring, 응답 필드, 오리지널 작품명 조건, 전체보기를 검증한다.
|
|
- 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 2.4, Task 2.5, Task 2.8, Task 2.9, Task 3.3, Task 5.1, Task 5.2에서 최근 응원 점수/스냅샷 조회, `CHANNEL_DONATION` 기준 후원 금액/후원 수, 데뷔일 기준 신규 부스트, DB-side exact scoring, 해당 섹션의 동시 팔로우를 검증한다.
|
|
- Feature K: Task 1.1, Task 2.2, Task 2.5, Task 2.8, Task 2.9, Task 3.3, Task 6.3, Task 7.1에서 인기 커뮤니티 점수/조건/댓글 불가 게시글 댓글 수 0점 계산, 데뷔일 기준 신규 부스트, DB-side exact scoring, 전체보기를 검증한다.
|
|
- Metrics: Task 7.2에서 PRD Metrics 항목의 로그 또는 metric 기록 지점을 검증한다.
|
|
- Technical Constraints: Phase 1~7에서 `v2.api.home`/`v2.recommend` 패키지 경계, `port.out` 의존 방향, 신규 v2 endpoint 분리, 기존 공개 스키마 유지 조건을 검증한다. `RecommendationSnapshotPort`의 persistence entity 노출 정리는 Task 2.4에서, 점수 기반 스냅샷의 `RecommendationScoreSpec` 공유 산식과 candidate pre-limit 금지는 Task 2.9에서 검증한다.
|
|
|
|
---
|
|
|
|
## Verification Log
|
|
|
|
- 2026-05-30: plan-task 문서 작성 전 `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`, `docs/agent-guides/코드스타일.md`, `docs/agent-guides/테스트스타일.md`, `docs/agent-guides/실행명령어.md`, `docs/20260529_메인_홈_추천_API/prd.md`를 확인했다.
|
|
- 2026-05-30: 기존 v2 패키지 구조, 테스트 스타일, QueryDSL/스케줄러 사용 패턴, 관련 엔티티/리포지토리 후보를 `find`, `rg`, `sed`로 확인해 계획의 파일 경로와 검증 명령에 반영했다.
|
|
- 2026-05-30: `./gradlew tasks --all`을 실행했다. sandbox 기본 권한에서는 `/Users/klaus/.gradle/.../gradle-8.1.1-bin.zip.lck` 생성 권한 문제로 실패했고, 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 13s`를 확인했다.
|
|
- 2026-05-30: 사용자 피드백에 따라 PRD의 Feature I가 특정 섹션 한정이 아니라 공통 "여러 크리에이터 동시 팔로우" 요구사항임을 확인했다. 장르의 크리에이터와 최근 응원이 많은 크리에이터가 동일한 팔로우 로직을 쓰도록 endpoint를 `POST /api/v2/home/recommendations/creators/follow`로 일반화했다.
|
|
- 2026-05-30: 동시 팔로우 범위 수정 후 `rg`로 장르 전용 명칭(`GenreCreator`, `genre-creators`, `FollowGenre`)과 placeholder 문구가 남지 않았음을 확인했다. `./gradlew tasks --all`은 sandbox 기본 권한에서 동일한 `.gradle` lock 파일 권한 문제로 실패했고, 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 752ms`를 확인했다.
|
|
- 2026-05-30: `sourceSection`은 PRD 필수 요구사항이 아니므로 제거했다. 동시 팔로우 요청은 `creatorIds`만 받도록 단순화하고, 장르의 크리에이터/최근 응원이 많은 크리에이터 화면은 같은 API를 호출하는 것으로 정리했다.
|
|
- 2026-05-30: `sourceSection` 제거 후 `./gradlew tasks --all`을 실행했다. sandbox 기본 권한에서는 동일한 `.gradle` lock 파일 권한 문제로 실패했고, 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 718ms`를 확인했다.
|
|
- 2026-05-30: PRD와 plan-task를 대조해 본인인증 조건, 동일 orders 배너 랜덤 정렬, AI 캐릭터 응답 필드/캐릭터 생성일 기준 부스트, 첫 오디오 최신성 점수 구간, 댓글 불가 커뮤니티 점수 계산, Metrics 관측 지점, `port.out` 의존 경계 보강이 필요함을 확인하고 관련 task와 Coverage Check에 반영했다.
|
|
- 2026-05-30: 문서 보강 후 `./gradlew tasks --all`을 실행했다. sandbox 기본 권한에서는 동일한 `.gradle` lock 파일 권한 문제로 실패했고, 권한 승인 후 재실행해 `BUILD SUCCESSFUL in 789ms`를 확인했다.
|
|
- 2026-05-30: Phase 1 Task 1.1 RED/GREEN을 진행했다. `RecommendationScorePolicyTest`는 구현 전 `Unresolved reference: RecommendationScorePolicy`로 실패했고, 구현 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.recommend.domain.RecommendationScorePolicyTest`가 `BUILD SUCCESSFUL`로 통과했다.
|
|
- 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` 호출을 줄바꿈해 해결했다.
|