Files
sodalive-backend-spring-boot/docs/20260529_메인_홈_추천_API/plan-task.md

100 KiB

메인 홈 추천 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.recommendation에 둔다. v2.api.homev2.recommendation의 application use case만 호출하며, v2.recommendation는 API DTO에 의존하지 않는다.

Tech Stack: Kotlin, Spring Boot 2.7.14, Java 17, Spring Data JPA, QueryDSL, native SQL, 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
  • 추천 크리에이터 동시 팔로우: 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")로 등록한다.
  • 다중 서버 인스턴스에서 스냅샷 일 배치가 중복 실행되지 않도록 기존 Redisson 기반 분산 lock을 사용한다.
  • 추천 스냅샷 lock key는 lock:recommendation-snapshot-refresh로 고정하고, lock 획득 실패 인스턴스는 정상 skip한다.
  • 저장소에는 DB migration 디렉터리가 없으므로 신규 스냅샷/조회 이력 엔티티 추가 시 운영 DB DDL 반영은 배포 절차에서 별도 수행한다. 코드 구현 task에는 JPA 엔티티/리포지토리와 통합 테스트를 포함하고, Phase 7 완료 후 신규 엔티티 테이블 생성 SQL을 문서 산출물로 작성한다.
  • 조회 구현은 JPA/QueryDSL 우선, native SQL 제한 사용의 하이브리드 전략으로 진행한다. 단순 조회/상세 조립/대상 활성 조건은 JPA 또는 QueryDSL로 표현하고, CTE/window function/union all/DB-side exact scoring처럼 SQL 고급 기능이 필요한 추천 산정에만 native SQL을 사용한다. native SQL 사용 시에는 H2 MySQL mode와 Kotlin 정책 산식 parity를 포함한 repository 통합 테스트를 반드시 둔다.
  • 이번 범위에서는 기존 홈/콘텐츠 홈/라이브/AI 캐릭터 API의 공개 스키마를 변경하지 않고, 앱 다국어 문구 번역, ML 개인화, A/B 테스트 플랫폼, 관리자 화면, 추천 결과 수동 편집 기능은 구현하지 않는다. 응답 enum은 앱 다국어 처리를 위해 안정적인 영문 code로 유지한다.

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/recommendation/HomeRecommendationResponse.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/HomeRecommendationPageResponse.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/FollowRecommendedCreatorsRequest.kt

신규 추천 기능 계층

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicy.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/CreatorDebutPolicy.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendedActivityType.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendedSectionType.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/CreatorContentViewHistoryService.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendedCreatorFollowService.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/CreatorContentViewHistoryPort.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/RecommendationSnapshotPort.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/HomeRecommendationQueryRepository.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/CreatorContentViewHistory.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/CreatorContentViewHistoryRepository.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshot.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotRepository.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/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/recommendation/domain/RecommendationScorePolicyTest.kt
  • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/CreatorDebutPolicyTest.kt
  • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
  • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/CreatorContentViewHistoryServiceTest.kt
  • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendedCreatorFollowServiceTest.kt
  • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
  • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt

Phase 1: 도메인 정책과 공통 모델

  • Task 1.1: 추천 점수/신규 부스트 정책 작성

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicy.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicyTest.kt
    • RED: shouldApplyCreatorNewBoostByDebutDays, shouldApplyAiCharacterNewBoostByCreatedDays, shouldCalculateDebutCreatorScore, shouldCalculateAiChatScore, shouldCalculateCheerScore, shouldCalculateCommunityScore, shouldCalculateFirstAudioRecencyScore 테스트를 먼저 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.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) 범위 안에 들어간다.
  • Task 1.2: 데뷔일/신규 크리에이터 판정 정책 작성

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/CreatorDebutPolicy.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/CreatorDebutPolicyTest.kt
    • RED: 첫 공개 콘텐츠 일시와 첫 라이브 일시 중 빠른 값을 데뷔일로 선택하는 테스트, 데뷔 후 30일 이내만 true인 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.CreatorDebutPolicyTest
    • GREEN: resolveDebutAt(firstContentPublishedAt, firstLiveAt)isNewCreator(debutAt, now)를 구현한다.
    • REFACTOR: 기존 ExplorerService.getCreatorDetaildebutDateTime 계산과 비교해 의미가 어긋나지 않는지 확인한다.
    • 기대 결과: 콘텐츠만 있는 경우, 라이브만 있는 경우, 둘 다 있는 경우, 둘 다 없는 경우가 모두 명확히 검증된다.
  • Task 1.3: 섹션/활동 enum과 내부 응답 모델 작성

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendedActivityType.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendedSectionType.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
    • RED: LIVE_REPLAY 테마 콘텐츠가 AUDIO가 아니라 LIVE_REPLAY로 분류되는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest
    • GREEN: 내부 모델에 LIVE, AUDIO, COMMUNITY, LIVE_REPLAY enum을 추가하고 활동 분류 함수를 구현한다.
    • REFACTOR: enum 값은 앱 다국어 처리를 위해 영문 code와 동일하게 유지한다.
    • 기대 결과: 활동 타입 응답 문자열이 PRD의 enum 후보와 일치한다.

Phase 2: 스냅샷 엔티티와 일 1회 집계 작업

  • Task 2.1: 추천 스냅샷 엔티티/리포지토리 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/RecommendationSnapshotPort.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshot.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/RecommendationSnapshotRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: 섹션 타입, 대상 id, 점수, 기준 시각, 랜덤 tie-breaker를 저장하고 기준 시각별 최신 스냅샷만 읽는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: RecommendationSnapshot JPA 엔티티와 findTop..., deleteBySectionTypeAndSnapshotAt 계열 리포지토리 메서드를 구현하고, application service가 의존할 RecommendationSnapshotPort를 둔다.
    • REFACTOR: 스냅샷 조회가 없으면 빈 배열을 반환하도록 service 경계에서 처리한다.
    • 기대 결과: 스냅샷 없음이 예외가 아니라 빈 결과로 검증된다.
  • Task 2.2: 스냅샷 갱신 서비스 작성

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Create: 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/HomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: AI 캐릭터, 최근 응원, 인기 커뮤니티 점수를 전날 23:59:59 기준으로 생성하는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: 최근 7일 집계와 신규 부스트를 적용해 AI_CHARACTER, CHEER_CREATOR, POPULAR_COMMUNITY 스냅샷을 저장한다. AI 캐릭터의 followIncrease는 팔로우 대상/관계 정의가 확정되지 않아 이번 스프린트에서 제외하고 0으로 집계한다.
    • REFACTOR: 무거운 QueryDSL 집계는 repository에 두고 점수 산식은 RecommendationScorePolicy만 사용한다.
    • 기대 결과: 동일 점수 항목은 randomTieBreaker가 저장되어 조회 시 랜덤 tie-breaker 정렬에 사용할 수 있다.
  • Task 2.3: 매일 06:00 KST 스케줄러 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/scheduler/RecommendationSnapshotScheduler.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: KST 매일 06:00:00 cron과 Asia/Seoul zone이 선언되는지 reflection 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: 스케줄러가 KST 06:00:00 cron으로 RecommendationSnapshotRefreshService.refreshDailySnapshots()를 호출하도록 구현한다.
    • REFACTOR: 스케줄러에는 집계 로직을 두지 않고 호출만 남긴다.
    • 기대 결과: KST 매일 06:00에 전날 23:59:59 KST 기준 집계가 실행되는 계약이 테스트로 고정된다.
  • Task 2.3.1: 일 스냅샷 스케줄러 Redisson lock 적용

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/scheduler/RecommendationSnapshotScheduler.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: Redisson lock 획득 성공 시 RecommendationSnapshotRefreshService.refreshDailySnapshots()를 1회 호출하고, 획득 실패 시 호출하지 않는 테스트를 작성한다. lock key가 lock:recommendation-snapshot-refresh인지도 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: 기존 RedissonClient bean을 스케줄러에 주입하고 tryLock으로 lock을 획득한 인스턴스만 refresh service를 호출한다. lock 획득 실패는 정상 skip으로 처리한다.
    • REFACTOR: DB 기반 scheduler lock 테이블은 추가하지 않고, 기존 AudioContentReleaseScheduledTask의 Redisson lock 패턴을 참고하되 스케줄러에는 lock 획득/해제와 service 호출만 둔다.
    • 기대 결과: 여러 서버 인스턴스에서 같은 cron이 동시에 실행돼도 클러스터 전체에서 한 인스턴스만 추천 스냅샷을 갱신한다.
  • Task 2.4: Phase 2 스냅샷 집계 통합 검증과 경계 보강

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/scheduler/RecommendationSnapshotScheduler.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/RecommendationSnapshotPort.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: QueryDSL 집계 통합 테스트를 추가해 AI 캐릭터 최근 채팅 수/활성 사용자 수, 최근 응원 CHANNEL_DONATION 후원 금액/후원 수와 팬 Talk 수, 인기 커뮤니티 좋아요/댓글/팔로워 수가 Phase 2 요구와 일치하는지 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.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 경계 정리가 문서/테스트/구현에 함께 반영된다.
  • Task 2.5: 크리에이터 신규 부스트 실제 데뷔일 적용

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: 최근 응원/인기 커뮤니티 신규 부스트가 단순 Member.createdAt이 아니라 실제 데뷔일을 사용하도록 실패 테스트를 추가한다. 실제 데뷔일은 첫 공개 콘텐츠 일시와 첫 라이브 일시 중 빠른 값이며, 둘 다 없는 경우는 스냅샷 후보에서 제외되는 실패 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: 최근 응원/인기 커뮤니티 후보 DTO가 실제 데뷔일을 담도록 QueryDSL 집계를 수정하고, service는 신규 부스트 계산 시 해당 데뷔일만 사용한다.
    • REFACTOR: 데뷔일 의미는 CreatorDebutPolicy.resolveDebutAt(...)과 일치하도록 중복 계산을 최소화한다.
    • 기대 결과: 최근 응원/인기 커뮤니티 신규 부스트가 Member.createdAt이 아니라 실제 데뷔일 기준으로 계산된다.
  • Task 2.6: AI 캐릭터 최근 채팅 수를 AI 발화 수로 고정

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: AI 캐릭터 최근 채팅 수가 최근 7일 안에 해당 AI 캐릭터가 발화한 채팅 메시지 수만 세도록 실패 테스트를 추가한다. 사용자 메시지, 다른 캐릭터 메시지, 비활성 메시지, 기간 밖 메시지는 제외되는지 fixture로 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: QueryDSL where/join 조건을 보강해 recentChatCount가 AI 발화 메시지 수만 반환하도록 구현한다.
    • REFACTOR: 테스트 이름과 후보 DTO 필드 설명이 PRD의 "AI가 발화한 채팅 수" 의미를 드러내도록 정리한다.
    • 기대 결과: AI 캐릭터 추천 점수의 최근 발생한 AI 채팅 수 입력값이 AI 발화 수로 고정된다.
  • Task 2.7: AI 캐릭터 채팅 활성 사용자 수를 중복 없는 채팅 사용자 수로 고정

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: 최근 활성 사용자 수가 최근 7일 안에 해당 AI 캐릭터와 1회 이상 채팅한 중복 없는 사용자 수로 계산되도록 실패 테스트를 추가한다. 같은 사용자의 다중 메시지는 1명으로 세고, 다른 캐릭터와만 채팅한 사용자는 제외한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: QueryDSL 집계가 캐릭터별 distinct 사용자 수를 반환하도록 구현한다.
    • REFACTOR: 활성 사용자 수 집계는 Task 2.6의 AI 발화 수 집계와 의미가 섞이지 않도록 별도 테스트 케이스로 유지한다.
    • 기대 결과: AI 캐릭터 추천 점수의 최근 활성 사용자 수 입력값이 중복 없는 채팅 사용자 수로 고정된다.
  • Task 2.8: 스냅샷 최종 저장 수를 점수순으로 제한

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: 스냅샷 저장 결과가 최종 점수 내림차순과 randomTieBreaker 기준으로 AI 캐릭터 최대 20개, 최근 응원이 많은 크리에이터 최대 16개, 인기 커뮤니티 최대 20개만 저장되는 실패 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: repository 조회에서 최종 점수와 randomTieBreaker를 계산하고, 점수 정렬 이후 동점자 랜덤 노출 여지를 위한 섹션별 최종 저장 수를 적용한다. service는 기준 시각 계산과 snapshot replace만 담당한다.
    • REFACTOR: GENRE_CREATOR는 Phase 2 스냅샷 갱신 대상이 아니라 Task 4.2의 조회 이력 기반 추천임을 문서/테스트 경계로 유지한다.
    • 기대 결과: application/service가 전체 후보를 메모리로 불러와 점수를 계산하지 않고, DB에서 정확한 최종 top 후보를 동점자 랜덤 정렬까지 반영해 반환하고 저장한다.
  • Task 2.9: DB-side exact scoring으로 스냅샷 후보 산정 전환

    • Files:
      • Create: 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
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/HomeRecommendationQueryPort.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendationScorePolicyTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: RecommendationScoreSpec 공유 산식과 DB-scored snapshot 조회 계약이 없으면 컴파일/테스트가 실패하도록 테스트를 작성한다. native SQL을 사용하는 쿼리는 Kotlin RecommendationScorePolicy 기대값과 DB score를 비교하고, 부스트 경계일, null aggregate, 비활성/제외 row, score desc, randomTieBreaker asc 정렬, 최종 점수 계산 이후 limit 적용, H2 MySQL mode parameter binding 호환성을 함께 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.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 RecommendationScorePolicyRecommendationScoreSpec의 가중치/부스트 구간 상수를 공유하도록 정리하고, 최근 응원/인기 커뮤니티 집계는 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/recommendation/port/out/HomeRecommendationQueryPort.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/HomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • 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
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: 라이브 최신순 20개, 활성 배너 orders 정렬 최대 20개, 동일 orders 배너의 랜덤 tie-breaker 정렬, 크리에이터당 최신 활동 1개만 반환하는 테스트를 작성한다. 라이브 노출 정보는 크리에이터 닉네임/프로필 이미지/라이브 번호를 포함하고, 활동 크리에이터 노출 정보는 크리에이터 프로필 이미지/닉네임/활동 타입/UTC 활동 시간/이동 대상 id를 포함하며 라이브 활동의 이동 대상 id는 nullable임을 검증한다. 배너는 비활성 이벤트 대상 EVENT, 비활성 크리에이터 대상 CREATOR, 비활성 시리즈 대상 SERIES, 비활성 시리즈 소유 회원 대상 SERIES가 제외되고, LINK 배너는 별도 대상 엔티티 검증 없이 배너 자체 활성 상태만으로 노출되는 repository 테스트를 함께 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: LiveRoom, AudioContentBanner, AudioContent, CreatorCommunity 기반 QueryDSL 조회를 구현한다. application service는 HomeRecommendationQueryPort에만 의존하고 persistence 구현체가 port를 구현한다. 배너는 기존 콘텐츠 홈 배너의 앱 이동 필드를 유지하고, 동일 orders 값은 후보군 축소 후 랜덤화하거나 랜덤 tie-breaker를 적용한다. 배너 대상 활성 조건은 service 후처리가 아니라 repository 조회 조건으로 고정한다. 활동 타입 enum 값은 LIVE, AUDIO, COMMUNITY, LIVE_REPLAY 영문 code 그대로 유지한다. 최근 활동 COMMUNITY의 이동 대상 id는 커뮤니티 게시글 id가 아니라 해당 게시글 작성자 크리에이터 id를 사용한다.
    • REFACTOR: 차단 관계, 비활성 회원, 비활성 콘텐츠/배너 제외 조건을 공통 private 조건 함수로 정리한다. 단순 조회와 대상 활성 조건은 QueryDSL/JPA 우선으로 표현하고, native SQL은 SQL 고급 기능이 필요한 쿼리에만 남긴다.
    • 기대 결과: 특정 섹션 데이터가 부족해도 service가 가능한 개수만 반환한다.
  • Task 3.2: 최근 데뷔/첫 오디오 콘텐츠 조회 구현

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/HomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
    • RED: 데뷔 후 30일 이내 추천 점수순, 최근 데뷔 크리에이터 노출 정보의 프로필 이미지/닉네임, 첫 오디오 콘텐츠 3번째 이내 활성 콘텐츠만 인정, 최신성 점수 구간별 정렬, 예약 공개 콘텐츠 제외 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.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/recommendation/application/HomeRecommendationQueryService.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/HomeRecommendationQueryServiceTest.kt
    • RED: 스냅샷 기준 AI 캐릭터 10개, AI 캐릭터 응답의 캐릭터 이름/소개/전체 채팅 수/오리지널 작품명 조건, 최근 응원 8명과 크리에이터 프로필 이미지/닉네임, 인기 커뮤니티 10개와 크리에이터 프로필 이미지/닉네임/UTC 시간/좋아요 수/댓글 수/커뮤니티 내용, 스냅샷 없음 빈 배열 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest
    • GREEN: RecommendationSnapshotRepository에서 최신 스냅샷을 읽고 대상 엔티티 상세 정보를 조립한다. AI 캐릭터 작품명은 오리지널 작품 캐릭터인 경우에만 채우고, 인기 커뮤니티는 스냅샷에 저장된 점수/랜덤 tie-breaker 순서를 유지한다.
    • REFACTOR: 비활성/노출 제한 캐릭터, 커뮤니티 비공개/유료/핀/성인 조건을 repository 조건으로 고정한다.
    • 기대 결과: AI 캐릭터 노출 필드가 PRD와 일치하고, 인기 커뮤니티는 크리에이터당 1개만 반환하며 동일 점수는 스냅샷 생성 시 저장한 랜덤 tie-breaker 기준으로 노출된다.

Phase 4: 콘텐츠 조회 이력 기록

  • Task 4.1: 콘텐츠 조회 이력 엔티티/서비스 작성

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/port/out/CreatorContentViewHistoryPort.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/CreatorContentViewHistory.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/CreatorContentViewHistoryRepository.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/CreatorContentViewHistoryService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/CreatorContentViewHistoryServiceTest.kt
    • RED: 인증 회원의 콘텐츠 상세 진입 시 memberId/contentId/genreId/viewedAt이 저장되는 테스트와 비회원은 저장하지 않는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryServiceTest
    • GREEN: 이력 저장 service와 repository를 구현한다. application service는 CreatorContentViewHistoryPort에만 의존하고 persistence 구현체가 port를 구현한다.
    • REFACTOR: 동일 회원/콘텐츠의 연속 중복 저장 허용 여부는 추천 이력으로 보존하며, 집계 시 distinct 장르 기준으로 처리한다.
    • 기대 결과: 조회 이력 저장 실패가 콘텐츠 상세 조회 자체를 실패시키지 않도록 호출부에서 예외 전파 범위를 제한한다.
  • Task 4.2: 장르 기반 크리에이터 추천 조회 구현

    • 선행 조건: Task 4.1의 CreatorContentViewHistory 엔티티/리포지토리/저장 service가 준비되어 있어야 한다.
    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.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/HomeRecommendationQueryServiceTest.kt
    • RED: 조회 이력 콘텐츠의 content_theme 기준 랜덤 5개, 부족분 랜덤 보충, 테마별 8명, 한 응답의 5개 테마 안에서 크리에이터 중복 제거, 서로 다른 조회 시점에서는 같은 크리에이터 재노출 허용, 팔로우 크리에이터 제외, 활성 크리에이터/활성 콘텐츠가 없어 빈 그룹이 되는 테마 제외, 크리에이터 프로필 이미지/닉네임/id 노출 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest
    • GREEN: CreatorContentViewHistory.contentIdcontent.theme_id 매핑을 기반으로 후보 테마/크리에이터를 조회한다. 기존 응답 필드명은 공개 스키마 호환을 위해 genreId, genreName을 유지하되 값은 content_theme.id, content_theme.theme을 담는다.
    • REFACTOR: 성인 콘텐츠 테마는 MemberContentPreference.isAdultContentVisible == true 회원에게만 포함되도록 조건을 공통화한다.
    • 기대 결과: 비회원 또는 조회 이력 없는 회원도 조회 가능한 테마 중 랜덤 5개를 받고, 활성 크리에이터/활성 콘텐츠가 없는 빈 그룹은 제외한 뒤 다른 테마로 보충된다.
  • Task 4.3: 기존 콘텐츠 상세 조회 흐름에 이력 기록 연결

    • 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 필드가 바뀌지 않는다.
  • Task 4.4: 장르 기반 크리에이터 추천 본인 제외 보정

    • Files:
      • Modify: docs/20260529_메인_홈_추천_API/prd.md
      • Modify: docs/20260529_메인_홈_추천_API/plan-task.md
      • 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
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
    • RED: 조회자가 크리에이터인 경우 본인만 있는 장르는 제외하고, 8명 중 본인이 포함된 장르는 본인을 제외한 뒤 대체 크리에이터가 있으면 8명을 채우며, 대체 크리에이터가 없거나 장르 전체가 8명 미만이면 조회 가능한 크리에이터만 응답하는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest.shouldExcludeRequesterOnlyGenreFromGenreCreatorRecommendations --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest.shouldBackfillCreatorAfterExcludingRequesterFromGenreCreatorRecommendations --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest.shouldReturnAvailableCreatorsAfterExcludingRequesterFromGenreCreatorRecommendations
    • GREEN: 장르 후보 eligibility, fallback 후보 count, 실제 장르별 크리에이터 조회 SQL에서 memberId가 있는 경우 조회자 본인 크리에이터를 제외한다.
    • REFACTOR: 공개 API 응답 스키마와 service의 장르별 중복 제거/보충 정책은 유지하고, repository 후보 산정과 응답 크리에이터 목록이 같은 eligibility 기준을 쓰는지 회귀 테스트로 확인한다.
    • 기대 결과: 본인만 있는 장르는 응답하지 않고, 본인을 제외한 추천 가능 크리에이터가 있으면 최대 8명까지 응답하며, 8명 미만이면 가능한 만큼만 응답한다.

Phase 5: 추천 크리에이터 동시 팔로우

  • Task 5.1: 팔로우 use case 작성

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendedCreatorFollowService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendedCreatorFollowServiceTest.kt
    • RED: mock 없이 실제 Spring/JPA 흐름으로 신규 팔로우 id와 이미 팔로우/본인 id 등 제외 id를 구분하는 테스트, 존재하지 않는 id/크리에이터가 아닌 id 포함 시 전체 실패 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendedCreatorFollowServiceTest
    • GREEN: MemberRepository, CreatorFollowingRepository를 사용해 전체 입력을 검증하고, 이미 팔로우 중인 id와 본인 id는 서버 내부에서 제외하며 신규 팔로우만 저장한다. 과거 언팔로우로 비활성화된 팔로우 이력은 신규 row를 만들지 않고 다시 활성화한다.
    • REFACTOR: 섹션별 분기 없이 팔로우 처리 로직은 followCreators(member, creatorIds) 하나로 유지한다.
    • 기대 결과: 존재하지 않는 id 또는 크리에이터가 아닌 id가 하나라도 있으면 신규 저장이 발생하지 않고, 이미 팔로우 중인 id와 본인 id는 실패가 아니라 서버 내부 제외 대상으로 처리된다. 동일 회원과 동일 크리에이터의 팔로우 row는 중복 저장되지 않는다.
  • Task 5.2: 팔로우 API DTO/Controller 연결

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/FollowRecommendedCreatorsRequest.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: mock 없이 @SpringBootTest와 실제 repository를 사용해 비로그인 요청은 Spring Security에서 거부되고, 로그인 요청은 creatorIds를 service에 전달해 신규 팔로우만 저장하며 결과를 ApiResponse.ok로 반환하는 controller 테스트를 작성한다. creatorIds null/empty/50개 초과 요청은 실패하고 신규 저장하지 않는 테스트를 포함한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest
    • GREEN: POST /api/v2/home/recommendations/creators/follow를 구현한다.
    • REFACTOR: request id 리스트가 null/empty이거나 50개를 초과하면 SodaException으로 거부한다.
    • 기대 결과: 클라이언트 응답은 성공/실패 여부만 제공하고, 신규 팔로우 id와 제외 id 목록은 공개 응답에 포함하지 않는다.
  • Task 5.3: 기존 팔로우 테이블 유니크 제약 운영 반영 문서화

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowing.kt
      • Create: docs/20260529_메인_홈_추천_API/alter-existing-tables.sql
    • TDD 예외 사유: 운영 DB 반영 SQL 문서 산출물 작성 task라 제품 코드 테스트를 새로 작성하지 않는다.
    • 대체 검증 방법:
      • rg -n "uk_creator_following_member_creator|creator_following|duplicate_count|ALTER TABLE|alter table" docs/20260529_메인_홈_추천_API/alter-existing-tables.sql src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowing.kt
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendedCreatorFollowServiceTest --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest
    • GREEN: 동일 회원과 동일 크리에이터의 팔로우 row를 중복 저장하지 않도록 creator_following(member_id, creator_id) 유니크 제약을 JPA entity에 명시하고, 운영 DB 반영 전 중복 데이터 점검/정리 및 ALTER TABLE 절차를 문서화한다.
    • 기대 결과: 테스트 H2 schema와 운영 DB 반영 절차가 같은 유니크 제약명 uk_creator_following_member_creator를 사용하며, 기존 중복 row가 있어도 배포 전 정리 절차를 검토할 수 있다.

Phase 6: 홈 통합/전체보기 API

  • Task 6.1: 홈 통합 응답 DTO와 facade 작성

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/dto/recommendation/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/recommendation/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 4개를 controller에 추가하고 HomeRecommendationPageResponse로 반환한다.
    • REFACTOR: size 기본값은 홈 기본 노출 수와 분리해 20으로 두고 최대값은 50으로 제한한다.
    • 기대 결과: 커뮤니티를 제외한 전체보기 API가 같은 페이징 응답 형식을 사용한다.
  • Task 6.4: Phase 6 리뷰 보완과 인증/성인 노출 경계 수정

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.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 캐릭터 전체보기는 비회원 요청을 거부하는 테스트, 음수 page가 런타임 예외를 만들지 않는 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest
    • GREEN: Security permitAll은 통합 조회 GET만 유지하고 전체보기 GET은 인증 대상이 되도록 정리한다. controller는 전체보기 요청에서 member == null이면 SodaException(common.error.bad_credentials)로 거부하고, page < 0은 0으로 보정한다. 성인 노출 여부는 단순 member.auth != null 대신 MemberContentPreferenceService.initializeDefaultPreference(member).isAdultContentVisible와 기존 isAdultVisibleByPolicy(...)를 사용한다.
    • REFACTOR: 공개 응답 스키마와 기존 follow API 동작은 변경하지 않는다.
    • 기대 결과: 홈 통합 API는 비회원 조회 가능, 세부 전체보기 API는 회원만 조회 가능하며 성인 노출 정책과 page 경계가 기존 프로젝트 관례와 일치한다.
  • Task 6.5: 섹션 전체보기 성인 노출 정책 전파 보완

    • Files:
      • 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: 남은 섹션 전체보기 요청에서 controller가 인증 회원을 facade에 전달하고, 홈 통합 조회 응답도 같은 회원 성인 노출 정책을 사용하는 실패 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest
    • GREEN: 라이브/최근 데뷔/첫 오디오/AI 캐릭터 전체보기 controller가 인증 회원을 facade에 전달하고, facade는 홈 통합 조회와 전체보기에서 같은 MemberContentPreferenceService.initializeDefaultPreference(member).isAdultContentVisibleisAdultVisibleByPolicy(...) 기준으로 회원별 성인 노출 여부를 계산한다.
    • REFACTOR: 성인 노출 계산이 홈 통합 조회와 전체보기에서 서로 다른 의미로 분기되지 않도록 facade 내부 private 함수로만 정리한다.
    • 기대 결과: 홈 통합 조회와 남은 섹션 전체보기 모두 동일한 회원 성인 노출 정책을 사용하며, 커뮤니티 전체보기 구현 없이 회원 설정 기반 노출 여부가 일관되게 적용된다.
  • Task 6.6: 전체보기 DB 레벨 페이징과 실제 데이터 페이징 테스트 보강

    • Files:
      • 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/HomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt
    • RED: facade 메모리 drop/take 방식으로는 실제 DB 데이터에서 page, size, hasNext가 정확히 보장되지 않는 실패 테스트를 추가하고, 라이브/최근 데뷔/첫 오디오/AI 캐릭터 전체보기의 실제 데이터 페이징 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest
    • GREEN: 전체보기 조회 port/repository/service가 Spring Pageable과 동일한 의미의 page, size, offset, limit + 1 조회를 DB 레벨에서 적용하도록 변경하고, facade는 repository 결과를 재페이징하지 않고 items, page, size, hasNext 응답 조립만 담당한다.
    • REFACTOR: 홈 통합 조회의 고정 노출 수 조회와 전체보기 페이징 조회를 분리해, 전체보기 때문에 홈 통합 조회 쿼리 의미가 바뀌지 않도록 유지한다.
    • 기대 결과: 전체보기 API는 facade 메모리 페이징이 아니라 DB 레벨 페이징을 사용하고, 실제 데이터 기반 테스트로 각 섹션의 items, page, size, hasNext 계산이 검증된다.
  • Task 6.7: 커뮤니티 전체보기 endpoint와 연결 로직 제거

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.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: /api/v2/home/recommendations/communities 전체보기 endpoint와 HomeRecommendationController.getCommunities 연결이 더 이상 존재하지 않아야 하는 실패 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest
    • GREEN: 커뮤니티 전체보기 controller method, facade section full-view 연결, Security matcher를 제거하고 홈 통합 조회의 인기 커뮤니티 기본 노출은 유지한다.
    • REFACTOR: 커뮤니티 전체보기가 필요하다는 전제의 테스트명/fixture만 제거하고, 홈 통합 조회의 인기 커뮤니티 응답 검증은 유지한다.
    • 기대 결과: 커뮤니티 전체보기 API는 Phase 6 공개 endpoint에서 제외되고, 연결 로직 제거 후에도 홈 통합 조회의 인기 커뮤니티 섹션은 기존처럼 동작한다.
  • Task 6.8: Phase 6 보완 task 경계와 상태 확인

    • Files:
      • Modify: docs/20260529_메인_홈_추천_API/plan-task.md
    • RED: Task 6.5~6.8이 Phase 6 보완 범위이고 Phase 6 보완 범위 밖 구현 항목이 섞이지 않았는지 문서 diff로 확인한다.
    • 실패 확인: rg -n "Task 6\.[5-8]" docs/20260529_메인_홈_추천_API/plan-task.md
    • GREEN: Task 6.5~6.8을 성인 노출 정책 전파, DB 레벨 페이징, 커뮤니티 전체보기 제거, Phase 6 보완 task 상태 확인 범위로만 유지한다.
    • REFACTOR: Phase 6 보완 task 제목과 기대 결과가 서로 겹치거나 구현 범위를 넓히지 않도록 문구만 정리한다.
    • 기대 결과: Task 6.5~6.8이 모두 완료 상태로 유지되고, Phase 6에서 처리한 후속 작업 범위와 상태가 명확하다.

Phase 7: 통합 검증과 문서 갱신

  • Task 7.1: repository 조건 회귀 테스트 보강

    • Files:
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
    • RED: 차단 관계 양방향 제외, 커뮤니티 성인 노출 조건, 본인인증 여부 조건이 필요한 추천 필터, 팔로우 크리에이터 제외, 비활성 회원 제외 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest
    • GREEN: 누락 조건을 QueryDSL where 조건 또는 service 필터에 추가한다.
    • REFACTOR: 같은 차단/성인 조건이 여러 쿼리에 반복되면 repository private 함수로만 정리한다.
    • 기대 결과: PRD Edge Case와 회원 조건이 테스트 이름으로 추적된다.
  • Task 7.2: 운영 지표 기록 지점 확인

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/CreatorContentViewHistoryService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendedCreatorFollowService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshService.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/CreatorContentViewHistoryServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendedCreatorFollowServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: 메인 홈 API 성공/실패와 응답 시간, 섹션별 빈 응답 여부, 전체보기 조회 수, 추천 섹션별 클릭률 기록 지점, 동시 팔로우 요청/성공 수, 콘텐츠 조회 이력 기록 성공/실패, 콘텐츠 상세 조회 흐름에서 CreatorContentViewHistoryService.recordView(...) 실패가 runCatching으로 삼켜지더라도 구조화 로그 또는 metric으로 관측되는지, 일 배치 집계 성공/실패와 소요 시간을 관측할 수 있는 로그 또는 metric 호출 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendedCreatorFollowServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: 프로젝트에 이미 사용하는 metric 클라이언트가 있으면 해당 클라이언트를 사용하고, 없으면 구조화 로그로 PRD Metrics 항목을 관측 가능하게 남긴다. 콘텐츠 조회 이력 저장 실패는 상세 조회 응답 실패로 전파하지 않되, 실패 원인과 memberId, contentId를 추적 가능한 형태로 남긴다.
    • 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
    • 기대 결과: 세 명령이 모두 성공하고, 이 문서 하단 검증 기록에 실행 일시/명령/결과를 누적한다.
  • Task 7.4: 신규 엔티티 테이블 생성 SQL 문서화

    • Files:
      • Create: docs/20260529_메인_홈_추천_API/create-new-entity-tables.sql
      • Modify: docs/20260529_메인_홈_추천_API/plan-task.md
    • TDD 예외 사유: 운영 DB 반영용 SQL 문서 산출물 작성 task라 제품 코드 테스트를 새로 작성하지 않는다.
    • 대체 검증 방법:
      • rg -n "CREATE TABLE|recommendation_snapshot|creator_content_view_history" docs/20260529_메인_홈_추천_API/create-new-entity-tables.sql
      • ./gradlew tasks --all
    • 작성 기준: Phase 7까지 완료된 최종 JPA 엔티티 필드/인덱스/nullable 조건을 기준으로 RecommendationSnapshot, CreatorContentViewHistory 등 이번 작업에서 신규 생성된 엔티티의 운영 DB 테이블 생성 SQL을 작성한다.
    • REFACTOR: SQL 문서에는 이번 작업에서 새로 추가된 테이블만 포함한다. 기존 테이블 변경이나 데이터 마이그레이션은 별도 배포 절차 항목으로 분리하며, Phase 5의 creator_following 유니크 제약은 docs/20260529_메인_홈_추천_API/alter-existing-tables.sql에 기록한다.
    • 기대 결과: Phase 7 완료 시점의 최종 엔티티 구조와 일치하는 신규 테이블 생성 SQL이 문서로 남아 운영 DB 반영 범위를 검토할 수 있다.
  • Task 7.5: 공통 차단 필터 전체 추천 섹션 적용 보완

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.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/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
    • RED: 라이브, 최근 활동, 최근 데뷔, 첫 오디오, 최근 응원 상세, 인기 커뮤니티 상세가 회원과 크리에이터의 양방향 활성 차단 관계를 제외하는 테스트를 추가한다. 커뮤니티 성인 노출, 본인인증 기반 성인 노출 조건, 팔로우 제외, 비활성 회원 제외 회귀 테스트 이름을 명시적으로 유지한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest
    • GREEN: facade/service/port/repository에 memberId 조회 컨텍스트를 전파하고, QueryDSL/native SQL 조회에 양방향 block_member 제외 조건을 적용한다.
    • 기대 결과: 장르 추천뿐 아니라 요청된 모든 홈 추천 섹션에서 내가 차단했거나 나를 차단한 크리에이터의 데이터가 제외된다.
  • Task 7.6: 운영 성공 로그 after-commit 기록 보완

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/CreatorContentViewHistoryService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendedCreatorFollowService.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/CreatorContentViewHistoryServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendedCreatorFollowServiceTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/RecommendationSnapshotRefreshServiceTest.kt
    • RED: 조회 이력 저장, 추천 크리에이터 동시 팔로우, 일 스냅샷 갱신 성공 로그가 트랜잭션 커밋 전에는 기록되지 않고 커밋 후 기록되는 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendedCreatorFollowServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest
    • GREEN: 성공 로그는 TransactionSynchronizationManagerafterCommit으로 등록하고, 트랜잭션 동기화가 없는 단위 실행에서는 기존처럼 즉시 기록한다. 실패 로그와 skip 로그는 기존 동작을 유지한다.
    • 기대 결과: 트랜잭션이 커밋되기 전 성공 로그가 먼저 남아 운영 지표를 오염시키지 않는다.
  • Task 7.7: 홈 배너 차단 필터 누락 보완

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryService.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/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
    • RED: 홈 배너 CREATOR 대상 크리에이터와 SERIES 대상 시리즈 소유자가 회원과 양방향 활성 차단 관계인 경우 제외되는 테스트를 추가한다. EVENTLINK 배너는 기존 활성 조건 기준으로 유지한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest.shouldExcludeBidirectionalBlockedCreatorsFromHomeBanners
    • GREEN: 홈 통합 조회에서 배너 조회에도 memberId를 전달하고, 배너 조회 포트/서비스/repository가 CREATORbannerCreator.id, SERIESseriesOwner.id 기준으로 양방향 block_member 제외 조건을 적용한다.
    • 기대 결과: 홈 배너 섹션에서도 차단 관계 크리에이터 또는 시리즈 소유자의 추천 데이터가 노출되지 않는다.

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과 Task 7.7에서 기존 콘텐츠 홈 배너 재활용, orders 정렬, 동일 orders 랜덤 정렬, 활성 배너/콘텐츠 조건, EVENT/CREATOR/SERIES 대상 비활성 제외, CREATOR/SERIES 대상 양방향 차단 제외, LINK 배너의 자체 활성 상태 기준 노출, 앱 이동 필드 유지를 검증한다.
  • Feature D: Task 1.3, Task 3.1에서 활동 타입 영문 enum, 최신 활동 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 4.1, Task 4.2, Task 4.3, Task 4.4에서 장르 조회 이력, 조회 이력 없을 때 랜덤 장르, 부족분 랜덤 보충, 한 응답 내 크리에이터 중복 제거, 조회 시점별 재노출 허용, 팔로우 제외, 조회자 본인 크리에이터 제외, 성인 장르 조건, 크리에이터 프로필 이미지/닉네임/id 노출을 검증한다.
  • Feature I: Task 5.1, Task 5.2에서 장르의 크리에이터와 최근 응원이 많은 크리에이터가 공통 동시 팔로우 use case를 재사용하고, 이미 팔로우 중인 id와 본인 id는 서버 내부에서 제외하며, 비활성 팔로우 이력은 재활성화하고, 존재하지 않는 id/크리에이터가 아닌 id는 전체 실패로 처리하는지 검증한다.
  • Feature J: Task 1.1, Task 2.2, Task 2.3.1, Task 2.4, Task 2.5, Task 2.8, Task 2.9, Task 3.3, Task 5.1, Task 5.2에서 최근 응원 점수/스냅샷 조회, 스냅샷 일 배치 클러스터 단일 실행, 8명 limit, 크리에이터 프로필 이미지/닉네임 노출, CHANNEL_DONATION 기준 후원 금액/후원 수, 팬 Talk 수, 최근 7일 집계, 데뷔일 기준 신규 부스트, 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 7.1에서 인기 커뮤니티 점수/조건/홈 통합 응답 노출 필드(크리에이터 프로필 이미지, 닉네임, UTC 시간, 좋아요 수, 댓글 수, 내용)/댓글 불가 게시글 댓글 수 0점 계산, 데뷔일 기준 신규 부스트, 최근 7일 집계, DB-side exact scoring을 검증한다.
  • Metrics: Task 7.2에서 메인 홈 API 성공률/응답 시간, 섹션별 빈 응답 비율, 전체보기 API 조회 수, 추천 섹션별 클릭률, 동시 팔로우 요청/성공 수, 콘텐츠 조회 이력 기록 성공률, 일 배치 집계 성공/실패 수와 스냅샷 생성 소요 시간의 로그 또는 metric 기록 지점을 검증한다.
  • Technical Constraints/Non-Goals: Phase 1~7에서 v2.api.home/v2.recommendation 패키지 경계, port.out 의존 방향, 신규 v2 endpoint 분리, 기존 공개 스키마 유지, 서버 다국어 번역/ML 개인화/A-B 테스트/관리자 화면/수동 편집 제외 조건을 검증한다. 응답 enum 영문 code 안정성은 Task 1.3과 Task 3.1에서, RecommendationSnapshotPort의 persistence entity 노출 정리는 Task 2.4에서, 점수 기반 스냅샷의 RecommendationScoreSpec 공유 산식과 candidate pre-limit 금지는 Task 2.9에서, JPA/QueryDSL 우선 및 native SQL 제한 사용 전략은 Task 2.9와 Task 3.1에서, 신규 엔티티 테이블 생성 SQL 문서화는 Task 7.4에서 검증한다.

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.recommendation.domain.RecommendationScorePolicyTestBUILD SUCCESSFUL로 통과했다.
  • 2026-05-30: Phase 1 Task 1.2 RED/GREEN을 진행했다. CreatorDebutPolicyTest는 구현 전 Unresolved reference: CreatorDebutPolicy로 실패했고, 구현 후 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.domain.CreatorDebutPolicyTestBUILD SUCCESSFUL로 통과했다.
  • 2026-05-30: Phase 1 Task 1.3 RED/GREEN을 진행했다. HomeRecommendationQueryServiceTest는 구현 전 RecommendedActivityType, RecommendedSectionType, HomeRecommendationQueryService 미구현으로 실패했고, 구현 후 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTestBUILD SUCCESSFUL로 통과했다.
  • 2026-05-30: Phase 1 최종 검증으로 ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*', ./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.recommendation.application.RecommendationSnapshotRefreshServiceTestBUILD SUCCESSFUL로 통과했다.
  • 2026-05-30: Phase 2 검증으로 ./gradlew clean test --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest, ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*', ./gradlew ktlintCheck, ./gradlew test를 순차 실행했고 모두 BUILD SUCCESSFUL로 통과했다. 병렬 Gradle 실행 중에는 KAPT 임시 stub 파일 경합이 발생해 이후 검증은 순차 실행으로 고정했다. Kotlin LSP는 이 환경에 kotlin-lsp가 설치되어 있지 않아 실행하지 못했고, Gradle 컴파일/테스트/ktlint로 대체 확인했다.
  • 2026-05-30: 기본 구현체 명명 규칙을 접미사 Impl 대신 접두사 Default로 변경했다. HomeRecommendationQueryRepositoryImplDefaultHomeRecommendationQueryRepository로 바꿨고, 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.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest, ./gradlew ktlintCheck, ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*'를 순차 실행했고 모두 BUILD SUCCESSFUL로 통과했다.
  • 2026-05-30: Phase 2 재점검을 진행했다. RecommendationSnapshotRefreshServiceTestDefaultHomeRecommendationQueryRepositoryTest는 각각 재실행 시 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 보강을 진행했다. DefaultHomeRecommendationQueryRepositoryTestRecommendationSnapshotRefreshServiceTest에 실제 데뷔일 기준 후보, AI 발화 수, 중복 없는 활성 사용자 수, 섹션별 스냅샷 저장 상한(20/16/20) 검증을 추가했다. 첫 실행은 AudioContent.theme fixture 누락과 QueryDSL alias 문제로 실패했고, 보정 후 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTestBUILD SUCCESSFUL로 통과했다.
  • 2026-05-30: Phase 2 Task 2.5~2.8 최종 검증으로 ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*', ./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.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest, ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*', ./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.recommendation.domain.RecommendationScorePolicyTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest, ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*', ./gradlew ktlintCheck, ./gradlew test를 순차 실행했고 모두 BUILD SUCCESSFUL로 통과했다. 중간에 H2 native SQL 타입 추론 문제와 ktlint line length 오류가 있었고, 데뷔일 CTE를 union all 이벤트 집계로 단순화하고 긴 setParameter 호출을 줄바꿈해 해결했다.
  • 2026-05-31: Phase 3 Task 3.1 RED/GREEN을 진행했다. RED에서 HomeRecommendationQueryServiceTestDefaultHomeRecommendationQueryRepositoryTest는 라이브/배너/최근 활동 크리에이터 조회 record, port 메서드, service 메서드, repository 쿼리 미구현으로 컴파일 실패했다. GREEN에서 라이브 최신순 20개, 활성 배너 orders 정렬/동일 orders 랜덤 tie-breaker/최대 20개, 크리에이터당 최신 활동 1개와 LIVE/AUDIO/COMMUNITY/LIVE_REPLAY 분류를 구현했고, ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTestBUILD SUCCESSFUL로 통과했다.
  • 2026-05-31: Phase 3 Task 3.1 추가 검증으로 ./gradlew ktlintCheck./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*'를 실행했고 모두 BUILD SUCCESSFUL로 통과했다. 중간에 테스트 코드 line length ktlint 오류가 있었고, 긴 fixture 호출을 줄바꿈해 해결했다.
  • 2026-05-31: Phase 3 Task 3.2 RED/GREEN을 진행했다. RED에서 HomeRecommendationQueryServiceTestDefaultHomeRecommendationQueryRepositoryTest는 최근 데뷔 크리에이터/첫 오디오 콘텐츠 record, port 메서드, service 메서드, repository 쿼리 미구현으로 컴파일 실패했다. GREEN에서 실제 데뷔일 30일 이내 최근 데뷔 크리에이터 점수/랜덤 tie-breaker 정렬, 첫 3개 업로드 이내 활성 공개 오디오 콘텐츠 판정, 비활성 선행 콘텐츠 경계, 예약 공개 제외, release_date 최신성 점수 정렬을 구현했고, ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTestBUILD SUCCESSFUL로 통과했다.
  • 2026-05-31: Phase 3 Task 3.3 RED/GREEN을 진행했다. RED에서 HomeRecommendationQueryServiceTestDefaultHomeRecommendationQueryRepositoryTest는 AI 캐릭터/최근 응원/인기 커뮤니티 상세 record, port 메서드, service 메서드, repository 상세 조회 미구현으로 compileTestKotlin이 실패했다. GREEN에서 RecommendationSnapshotPort.findLatestSnapshots(sectionType) 기반 최신 스냅샷 조회, AI 캐릭터 10개/최근 응원 8명/인기 커뮤니티 10개 limit, 스냅샷 순서 보존, 누락 상세 필터링, 인기 커뮤니티 크리에이터 중복 제거, 커뮤니티 비노출 조건 필터링, AI 캐릭터 원작명 조건부 응답과 전체 AI 발화 수 조립을 구현했다. ./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest./gradlew ktlintCheckBUILD SUCCESSFUL로 통과했다.
  • 2026-05-31: Phase 3에는 CreatorContentViewHistory 엔티티/리포지토리/저장 서비스가 아직 구현되어 있지 않아 장르 기반 크리에이터 추천 조회를 포함하지 않았다. 해당 산출물은 Phase 4 Task 4.1 범위이므로, 장르 기반 크리에이터 추천 조회는 조회 이력 저장 모델이 준비된 뒤 Phase 4 Task 4.2에서 별도 RED/GREEN으로 진행한다.
  • 2026-05-31: Phase 3 리뷰 후속으로 인기 커뮤니티 추천이 상위 10개 스냅샷을 먼저 자른 뒤 크리에이터 중복을 제거해 10개 미만으로 내려갈 수 있는 문제를 보완했다. RED에서 상위 스냅샷 중복 제거 후 뒤 후보로 기본 10개를 채우는 테스트가 실패했고, GREEN에서 인기 커뮤니티만 최신 스냅샷 후보 20개를 읽은 뒤 상세 조립/크리에이터 중복 제거 후 최종 10개를 반환하도록 수정했다. 후속 검증으로 ./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest, ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*', ./gradlew ktlintCheck, ./gradlew test를 순차 실행했고 모두 BUILD SUCCESSFUL로 통과했다.
  • 2026-05-31: 사용자 피드백에 따라 신규 엔티티 테이블 생성 SQL은 Phase 7 완료 후 최종 엔티티 구조 기준으로 작성하도록 계획을 보강했다. RecommendationSnapshot, CreatorContentViewHistory 등 이번 작업에서 새로 생성된 엔티티의 운영 DB DDL을 docs/20260529_메인_홈_추천_API/create-new-entity-tables.sql 산출물로 남기는 Task 7.4를 추가했다.
  • 2026-05-31: PRD와 plan-task를 재대조해 큰 기능 흐름은 반영되어 있었으나 일부 PRD 세부 항목의 task 추적성이 약한 점을 확인했다. 라이브/활동/최근 데뷔/최근 응원/인기 커뮤니티/장르 크리에이터 노출 필드, LINK 배너 자체 활성 상태 기준, 활동 enum 영문 code 안정성, 한 응답 내 장르 크리에이터 중복 제거와 조회 시점별 재노출 허용, 추천 섹션별 클릭률 metric, Non-Goals 범위를 관련 task와 Coverage Check에 보강했다.
  • 2026-05-31: Phase 2/3 재점검 후속으로 배너 대상 활성 조건과 스냅샷 데뷔일 계산의 빈 channel_name 라이브 제외 누락을 확인했다. RED에서 DefaultHomeRecommendationQueryRepositoryTest에 회귀 테스트를 추가했고 shouldExcludeHomeBannersWithInactiveTargetsExceptLink, shouldExcludeBlankChannelNameLiveFromSnapshotDebutAt가 실패했다. GREEN에서 findHomeBannersEVENT/CREATOR/SERIES 대상 활성 조건을 추가하고, 최근 응원/인기 커뮤니티 데뷔일 CTE에 lr.channel_name <> '' 조건을 추가했다. 리뷰 중 PRD의 인기 커뮤니티 성인 노출 조건은 항상 제외가 아니라 MemberContentPreference.isAdultContentVisible == true 회원에게 노출 허용임을 재확인해, 스냅샷 산정은 성인 게시글도 후보로 유지하고 상세 조회에서 includeAdultCommunities로 필터링하도록 수정했다. 추가 코드 리뷰에서 기존 홈 배너가 tabId = 1일 때 tab is null만 조회하는 계약을 확인해, findHomeBannerscb.tab_id is null 조건과 탭 전용 배너 제외 회귀 테스트를 보강했다. 후속 검증으로 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest, ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest, ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*', ./gradlew ktlintCheck가 모두 BUILD SUCCESSFUL로 통과했다. 병렬 Gradle test 실행 중 XML 결과 파일 쓰기 충돌로 한 번 실패했으나, 동일 명령 단독 재실행 시 성공해 테스트 assertion 실패가 아님을 확인했다.
  • 2026-05-31: 사용자 피드백에 따라 여러 크리에이터 동시 팔로우에서 본인 크리에이터 id는 전체 실패 조건에서 제외하고, 이미 팔로우 중인 id와 동일하게 처리 제외 대상으로 보도록 PRD와 plan-task를 수정했다.
  • 2026-06-01: 사용자 피드백에 따라 동시 팔로우 공개 응답은 성공/실패 여부만 제공하도록 단순화했다. 이미 팔로우 중인 id와 본인 id는 실패 사유로 보지 않고 서버 내부에서 제외하며, 테스트는 mock 없이 실제 Spring/JPA 흐름으로 검증하도록 조정한다.
  • 2026-05-31: Phase 4 구현 중 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryServiceTest, ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest, ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest, ./gradlew test --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest를 실행해 모두 BUILD SUCCESSFUL을 확인했다.
  • 2026-06-01: Phase 6 Task 6.1~6.3을 진행했다. HomeRecommendationResponse/HomeRecommendationPageResponse API DTO와 HomeRecommendationFacade(섹션별 기본 limit 20/20/10/10/10/10/5x8/8/10 전달, 회원의 성인 노출 여부=member.auth != nullmemberId를 장르/커뮤니티 조회 조건으로 전달, KST→UTC ISO 변환, cloud-front host 이미지 URL 조립)를 추가했다. HomeRecommendationController에 통합 조회 GET /api/v2/home/recommendations와 전체보기 5개(/lives, /debut-creators, /first-audio-contents, /ai-characters, /communities)를 추가했고 size 기본값 20/최대 50으로 정규화했다. SecurityConfig에 해당 GET endpoint 6개 permitAll을 추가해 비회원 접근을 허용했다. HomeRecommendationControllerTest에 통합 조회(비회원/회원), 페이징 응답 형식, size 상한 테스트를 추가했고 ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest가 12/12 통과했다. ktlint는 이 환경 셸 PATH에 Java가 없어 직접 실행하지 못했고 IDE 인스펙션으로 신규 파일 무경고를 확인했다(컨트롤러의 @AuthenticationPrincipal SpEL 문자열 경고는 기존 팔로우 endpoint와 동일한 false positive).
  • 2026-06-01: Phase 6 Task 6.4 리뷰 보완을 진행했다. RED에서 HomeRecommendationControllerTest에 세부 전체보기 비회원 거부와 음수 page 보정 테스트를 추가했고 기존 구현은 shouldRejectAnonymousSectionPages, shouldNormalizeNegativePageToZero 2건 실패로 확인했다. GREEN에서 SecurityConfig는 통합 조회 GET /api/v2/home/recommendationspermitAll로 유지하고 전체보기 5개는 회원 인증 대상으로 변경했다. HomeRecommendationController는 전체보기 요청에서 인증 회원을 요구하고 page < 0을 0으로 보정하며, HomeRecommendationFacade는 성인 노출 여부를 MemberContentPreferenceService.initializeDefaultPreference(member).isAdultContentVisible와 기존 isAdultVisibleByPolicy(...)로 계산하도록 수정했다. 검증으로 ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest를 실행해 BUILD SUCCESSFUL을 확인했다.
  • 2026-06-01: Phase 7 Task 7.1 RED/GREEN을 진행했다. RED에서 DefaultHomeRecommendationQueryRepositoryTest.shouldExcludeBidirectionalBlockedCreatorsFromGenreCreatorRecommendations가 양방향 차단 크리에이터를 제외하지 못해 실패했고, GREEN에서 장르 추천 테마 후보/크리에이터 조회 native SQL에 block_member 양방향 활성 차단 제외 조건을 추가했다. 검증으로 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest.shouldExcludeBidirectionalBlockedCreatorsFromGenreCreatorRecommendationsBUILD SUCCESSFUL로 통과했다.
  • 2026-06-01: Phase 7 Task 7.2 RED/GREEN을 진행했다. RED에서 홈 통합/전체보기 성공, 콘텐츠 조회 이력 저장/스킵, 추천 크리에이터 동시 팔로우 성공/실패, 일 스냅샷 갱신 성공, 콘텐츠 상세 조회 이력 기록 실패 관측 로그 테스트 9건이 로그 이벤트 키 미존재로 실패했다. GREEN에서 기존 프로젝트 관례대로 신규 metric dependency 없이 LoggerFactory 구조화 로그를 추가했고, 콘텐츠 상세 조회의 CreatorContentViewHistoryService.recordView(...) 실패는 응답 실패로 전파하지 않고 memberId, contentId, 원인을 로그로 남기도록 했다. 검증으로 Phase 7 대상 테스트 묶음 ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendedCreatorFollowServiceTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.RecommendationSnapshotRefreshServiceTest --tests kr.co.vividnext.sodalive.content.AudioContentServiceTestBUILD SUCCESSFUL로 통과했다. 추천 섹션별 클릭률은 별도 클릭 ingress가 없어 이번 범위에서는 응답/impression 관측 로그만 추가했다.
  • 2026-06-01: Phase 7 리뷰 지적에 따라 홈 통합 조회와 라이브 전체보기 조회 실패 로그 테스트를 추가하고, HomeRecommendationFacade에서 실패 시 home_recommendations_query_failure, home_recommendations_page_query_failure 로그를 남긴 뒤 예외를 재전파하도록 보강했다. 검증으로 ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldLogHomeRecommendationFailure --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldLogHomeRecommendationPageFailureBUILD SUCCESSFUL로 통과했다.
  • 2026-06-01: Phase 7 재리뷰 지적에 따라 최근 데뷔/첫 오디오/AI 캐릭터 전체보기 실패도 home_recommendations_page_query_failure 로그를 남긴 뒤 예외를 재전파하도록 보강했다. 검증으로 ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldLogOtherHomeRecommendationPageFailuresBUILD SUCCESSFUL로 통과했고, 이후 ./gradlew ktlintCheckBUILD SUCCESSFUL in 16s, ./gradlew testBUILD SUCCESSFUL in 54s로 통과했다.
  • 2026-06-01: Phase 7 Task 7.4로 신규 엔티티 테이블 생성 SQL docs/20260529_메인_홈_추천_API/create-new-entity-tables.sql을 작성했다. 최종 JPA 엔티티 기준으로 recommendation_snapshot, creator_content_view_history 두 신규 테이블만 포함했고, 기존 테이블 변경은 alter-existing-tables.sql 범위로 유지했다. 검증으로 rg -n "CREATE TABLE|create table|recommendation_snapshot|creator_content_view_history" docs/20260529_메인_홈_추천_API/create-new-entity-tables.sql./gradlew tasks --all이 모두 성공했다.
  • 2026-06-01: Phase 7 Task 7.3 전체 검증을 순차 실행했다. ./gradlew ktlintCheck는 처음에 신규 로그 호출의 긴 라인과 리뷰 보완 후 테스트 import 순서로 실패했고 줄바꿈/import 정리 후 통과했다. 최종 재리뷰 보완 후 ./gradlew ktlintCheckBUILD SUCCESSFUL in 16s, ./gradlew testBUILD SUCCESSFUL in 54s로 통과했고, ./gradlew tasks --all은 앞선 Task 7.4 검증에서 BUILD SUCCESSFUL in 1s로 통과했다.
  • 2026-06-01: Phase 7 Task 7.5~7.6 보완을 진행했다. 라이브/최근 활동/최근 데뷔/첫 오디오/최근 응원/인기 커뮤니티에 회원 memberId 조회 컨텍스트를 전달하고 양방향 활성 차단 관계를 제외하도록 수정했다. 커뮤니티 성인 노출, 본인인증 기반 성인 노출 조건, 팔로우 제외, 비활성 회원 제외 회귀 테스트 이름을 명시적으로 유지하고, 조회 이력/동시 팔로우/스냅샷 성공 로그는 트랜잭션 커밋 후 기록되도록 보완했다. 검증으로 Phase 7 대상 테스트 묶음, ./gradlew ktlintCheck, ./gradlew test가 모두 BUILD SUCCESSFUL로 통과했다.
  • 2026-06-01: 리뷰 게이트 Context Mining에서 홈 배너 CREATOR/SERIES 대상의 차단 필터 누락을 발견해 Phase 7 Task 7.7로 보완했다. 홈 통합 조회의 배너 조회에도 memberId를 전달하고, 배너 repository 조회에서 CREATOR는 대상 크리에이터, SERIES는 시리즈 소유자 기준 양방향 활성 block_member 제외 조건을 적용했다. 회귀 테스트 DefaultHomeRecommendationQueryRepositoryTest.shouldExcludeBidirectionalBlockedCreatorsFromHomeBannersHomeRecommendationQueryServiceTest를 추가/보강했고, ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest.shouldExcludeBidirectionalBlockedCreatorsFromHomeBanners --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTestBUILD SUCCESSFUL로 통과했다.
  • 2026-06-06: 사용자 피드백에 따라 장르 기반 크리에이터 추천에서 조회자가 크리에이터인 경우 본인을 제외하도록 PRD와 Task 4.4를 보강했다. RED에서 DefaultHomeRecommendationQueryRepositoryTest.shouldExcludeRequesterOnlyGenreFromGenreCreatorRecommendations, shouldBackfillCreatorAfterExcludingRequesterFromGenreCreatorRecommendations, shouldReturnAvailableCreatorsAfterExcludingRequesterFromGenreCreatorRecommendations 3건이 실패했고, GREEN에서 장르 후보 eligibility, fallback 후보 count, 실제 크리에이터 조회 native SQL에 (:memberId is null or m.id <> :memberId) 조건을 추가했다. service 경계에는 HomeRecommendationQueryServiceTest.shouldReturnAvailableCreatorsWhenGenreCreatorCountIsUnderLimit를 추가해 8명 미만이면 가능한 만큼 응답하는 정책을 고정했다. 검증으로 신규 RED 테스트 재실행, service 단일 테스트, ./gradlew test --tests kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.DefaultHomeRecommendationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryServiceTest, ./gradlew ktlintCheck, ./gradlew tasks --all, ./gradlew test가 모두 BUILD SUCCESSFUL로 통과했다.
  • 2026-06-08: 홈 추천 API DTO 패키지 경계를 정리했다. 기존 HomeRecommendationResponse, HomeRecommendationPageResponse, FollowRecommendedCreatorsRequest 3개 DTO를 kr.co.vividnext.sodalive.v2.api.home.dto.recommendation 하위로 이동하고, Controller/Facade 및 DTO 테스트 import를 갱신했다. 기존 추천 API DTO 이동은 홈 추천 API 문서 범위에만 기록하며, 크리에이터 랭킹 문서는 변경하지 않았다. 검증으로 후속 focused test와 compile/test를 실행한다.
  • 2026-06-08: 홈 추천 기능 본체 패키지를 단수 동사형 recommend에서 명사형 recommendation 기준인 kr.co.vividnext.sodalive.v2.recommendation으로 변경했다. src/main/src/test 디렉터리, Kotlin package/import, 문서의 파일 경로와 Gradle --tests 필터를 새 패키지명으로 맞췄다. /api/v2/home/recommendations, v2.api.home, v2.api.home.dto.recommendation, 클래스명과 API 스키마는 변경하지 않았다. 검증으로 stale reference 검색, ktlintCheck, 추천 패키지 테스트, 홈 API 테스트, 전체 테스트를 실행한다.