# 크리에이터 채널 오디오 탭 API Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`로 task 단위 구현을 진행한다. 각 단계는 체크박스(`- [ ]`)로 진행 상태를 갱신한다. **Goal:** 인증 회원이 `GET /api/v2/creator-channels/{creatorId}/audio`로 크리에이터 채널 오디오 탭의 테마 목록, 콘텐츠 개수, 소장률, 오디오 콘텐츠 목록을 조회할 수 있게 한다. **Architecture:** 공개 API controller/facade/response DTO는 `kr.co.vividnext.sodalive.v2.api.creator.channel.audio` 조립 계층에 둔다. 오디오 탭 조회 service, 순수 fallback/page 정책, tab domain model, port, QueryDSL repository는 `kr.co.vividnext.sodalive.v2.creator.channel.audio` 하위에 두고 `v2.api.*`에 의존하지 않는다. 크리에이터 채널 오디오 콘텐츠 item domain/response는 홈/라이브/오디오 탭에서 동일하게 쓰도록 채널 공통 패키지에 둔다. 라이브 탭에서 만든 `ContentSort`와 오디오 콘텐츠 응답 의미는 재사용하되, `sort/page/size/themeId` fallback 정책은 오디오 탭 전용 정책으로 명시한다. **Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring MVC, Spring Data JPA, QueryDSL, JUnit 5, MockMvc, Gradle Wrapper --- ## 0. 구현 전 확정 사항 - API endpoint: `GET /api/v2/creator-channels/{creatorId}/audio` - 인증 정책: 인증 회원만 조회 가능. 비회원은 기존 Security 흐름과 `requireMember` 정책으로 거부한다. - request: - path variable: `creatorId` - query parameter: `sort`, `required = false`, 기본값/fallback `LATEST` - query parameter: `themeId`, `required = false`, 없거나 비활성/미존재이면 전체 활성 테마 조회 - query parameter: `page`, `required = false`, 기본값 `0`, `page < 0`이면 `0`으로 fallback - query parameter: `size`, `required = false`, 기본값 `20`, `size < 20`이면 `20`, `size > 50`이면 `50`으로 fallback - controller는 invalid `sort` fallback을 위해 `sort: String?`으로 받고 service/facade 경계에서 `ContentSort`로 보정한다. - response: - `audioContentCount`: 적용된 필터 기준 오디오 콘텐츠 전체 개수 - `paidAudioContentCount`: 적용된 필터 기준 `price > 0` 콘텐츠 개수 - `purchasedAudioContentCount`: 적용된 필터 기준 유료 콘텐츠 중 조회자가 유효하게 소장하거나 대여 중인 콘텐츠 개수 - `purchasedAudioContentRate`: `paidAudioContentCount == 0`이면 `0.0`, 아니면 `(purchasedAudioContentCount / paidAudioContentCount) * 100` - `themes`: 활성 테마 중 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 1개 이상 있는 테마 목록. 선택한 `themeId`와 무관하게 내려준다. - `audioContents`: 기존 `CreatorChannelAudioContentResponse`와 같은 필드/의미를 가진 item 목록 - `sort`: 실제 적용한 `ContentSort` - `themeId`: 실제 적용한 활성 테마 id, 전체 조회 fallback이면 `null` - `page`: fallback 보정 후 실제 적용된 page index - `size`: fallback 보정 후 실제 적용된 page size - `hasNext`: 다음 page 존재 여부 - 공개 콘텐츠 기준: `AudioContent.isActive == true`, `AudioContent.duration != null`, `AudioContent.releaseDate != null`, `AudioContent.releaseDate <= now`. - 성인 콘텐츠는 조회자의 성인 콘텐츠 노출 정책이 false이면 목록과 count에서 제외한다. - 테마명은 `LangContext.lang.code` 기준으로 `ContentThemeTranslation`을 우선하고, 없거나 빈 문자열이면 `AudioContentTheme.theme` 원문으로 fallback한다. - 테마 목록 필터링은 콘텐츠 목록/count와 같은 공개 조건, 예약 공개 제외, 성인 콘텐츠 노출 정책을 적용한다. - 시리즈명은 `LangContext.lang.code` 기준으로 `SeriesTranslation`을 우선하고, 없거나 빈 문자열이면 `Series.title` 원문으로 fallback한다. - `isFirstContent`는 선택 테마 안의 첫 콘텐츠가 아니라 크리에이터의 전체 공개 오디오 콘텐츠 중 첫 콘텐츠인지로 판단한다. - 정렬: - `LATEST`: `releaseDate desc`, `price desc`, `audioContent.id desc` - `POPULAR`: 구매 매출 합계 desc, `releaseDate desc`, `audioContent.id desc` - `OWNED`: 조회자 소장 또는 유효 대여 여부 desc, `releaseDate desc`, `audioContent.id desc` - `PRICE_HIGH`: `price desc`, `releaseDate desc`, `audioContent.id desc` - `PRICE_LOW`: `price asc`, `releaseDate desc`, `audioContent.id desc` - 인기순 매출은 대여/소장 여부와 관계없이 순수하게 결제된 캔 매출인 `orders.can` 합계를 사용한다. `orders.point`는 포함하지 않고, `orders.is_active = true`인 주문만 포함한다. --- ## 1. 파일 구조 계획 ### 오디오 탭 신규 API 조립 계층 - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/dto/CreatorChannelAudioTabResponse.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioControllerTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt` ### 오디오 탭 도메인 조회 계층 - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioTab.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/port/out/CreatorChannelAudioQueryPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/CreatorChannelAudioQueryRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicyTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt` ### 크리에이터 채널 공통 오디오 콘텐츠 item - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/common/domain/CreatorChannelAudioContent.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/common/dto/CreatorChannelAudioContentResponse.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelLiveTab.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/dto/CreatorChannelLiveTabResponse.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/domain/CreatorChannelHome.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/dto/CreatorChannelHomeResponse.kt` ### 기존 파일 확인/재사용 - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/common/domain/ContentSort.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/dto/CreatorChannelLiveTabResponse.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/content/theme/translation/ContentThemeTranslationRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/content/series/translation/SeriesTranslationRepository.kt` ### 문서 산출물 - Create: `docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md` - Verify: `docs/20260619_크리에이터_채널_오디오_탭_API/prd.md` --- ## 2. Response data class 초안 구현 시 `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/dto/CreatorChannelAudioTabResponse.kt`에 아래 DTO를 기준으로 추가한다. 필드명은 공개 API 계약이므로 변경이 필요하면 먼저 PRD와 이 문서를 갱신한다. ```kotlin package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto import com.fasterxml.jackson.annotation.JsonProperty import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioContent import kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioTab import kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioTheme data class CreatorChannelAudioTabResponse( val audioContentCount: Int, val paidAudioContentCount: Int, val purchasedAudioContentCount: Int, val purchasedAudioContentRate: Double, val themes: List, val audioContents: List, val sort: ContentSort, val themeId: Long?, val page: Int, val size: Int, @JsonProperty("hasNext") val hasNext: Boolean ) { companion object { fun from(tab: CreatorChannelAudioTab): CreatorChannelAudioTabResponse { return CreatorChannelAudioTabResponse( audioContentCount = tab.audioContentCount, paidAudioContentCount = tab.paidAudioContentCount, purchasedAudioContentCount = tab.purchasedAudioContentCount, purchasedAudioContentRate = tab.purchasedAudioContentRate, themes = tab.themes.map(CreatorChannelAudioThemeResponse::from), audioContents = tab.audioContents.map(CreatorChannelAudioContentResponse::from), sort = tab.sort, themeId = tab.themeId, page = tab.page.page, size = tab.page.size, hasNext = tab.hasNext ) } } } data class CreatorChannelAudioThemeResponse( val themeId: Long, val themeName: String ) { companion object { fun from(theme: CreatorChannelAudioTheme): CreatorChannelAudioThemeResponse { return CreatorChannelAudioThemeResponse( themeId = theme.themeId, themeName = theme.themeName ) } } } data class CreatorChannelAudioContentResponse( val audioContentId: Long, val title: String, val duration: String?, val imageUrl: String?, val price: Int, @JsonProperty("isAdult") val isAdult: Boolean, @JsonProperty("isPointAvailable") val isPointAvailable: Boolean, @JsonProperty("isFirstContent") val isFirstContent: Boolean, val seriesName: String?, @JsonProperty("isOriginalSeries") val isOriginalSeries: Boolean?, @JsonProperty("isOwned") val isOwned: Boolean, @JsonProperty("isRented") val isRented: Boolean ) { companion object { fun from(content: CreatorChannelAudioContent): CreatorChannelAudioContentResponse { return CreatorChannelAudioContentResponse( audioContentId = content.audioContentId, title = content.title, duration = content.duration, imageUrl = content.imageUrl, price = content.price, isAdult = content.isAdult, isPointAvailable = content.isPointAvailable, isFirstContent = content.isFirstContent, seriesName = content.seriesName, isOriginalSeries = content.isOriginalSeries, isOwned = content.isOwned, isRented = content.isRented ) } } } ``` --- ## 3. Domain / Port 초안 구현 시 아래 형태를 기준으로 추가한다. API DTO가 domain model을 참조하지만 domain/port는 API DTO를 참조하지 않는다. ```kotlin package kr.co.vividnext.sodalive.v2.creator.channel.audio.domain import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage data class CreatorChannelAudioTab( val audioContentCount: Int, val paidAudioContentCount: Int, val purchasedAudioContentCount: Int, val purchasedAudioContentRate: Double, val themes: List, val audioContents: List, val sort: ContentSort, val themeId: Long?, val page: CreatorChannelPage, val hasNext: Boolean ) data class CreatorChannelAudioTheme( val themeId: Long, val themeName: String ) data class CreatorChannelAudioContent( val audioContentId: Long, val title: String, val duration: String?, val imageUrl: String?, val price: Int, val isAdult: Boolean, val isPointAvailable: Boolean, val isFirstContent: Boolean, val seriesName: String?, val isOriginalSeries: Boolean?, val isOwned: Boolean, val isRented: Boolean ) ``` ```kotlin package kr.co.vividnext.sodalive.v2.creator.channel.audio.port.out import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.v2.common.domain.ContentSort import java.time.LocalDateTime interface CreatorChannelAudioQueryPort { fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelAudioCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean fun findActiveThemeId(themeId: Long): Long? fun findAudioThemes( creatorId: Long, now: LocalDateTime, canViewAdultContent: Boolean, locale: String ): List fun countAudioContents(creatorId: Long, themeId: Long?, now: LocalDateTime, canViewAdultContent: Boolean): Int fun countPaidAudioContents(creatorId: Long, themeId: Long?, now: LocalDateTime, canViewAdultContent: Boolean): Int fun countPurchasedAudioContents( creatorId: Long, viewerId: Long, themeId: Long?, now: LocalDateTime, canViewAdultContent: Boolean ): Int fun findAudioContents( creatorId: Long, viewerId: Long, themeId: Long?, now: LocalDateTime, canViewAdultContent: Boolean, sort: ContentSort, locale: String, offset: Long, limit: Int ): List } data class CreatorChannelAudioCreatorRecord( val creatorId: Long, val role: MemberRole, val nickname: String ) data class CreatorChannelAudioThemeRecord( val themeId: Long, val themeName: String ) data class CreatorChannelAudioContentRecord( val audioContentId: Long, val title: String, val duration: String?, val imagePath: String?, val price: Int, val isAdult: Boolean, val isPointAvailable: Boolean, val isFirstContent: Boolean, val seriesName: String?, val isOriginalSeries: Boolean?, val isOwned: Boolean, val isRented: Boolean ) ``` --- ### Phase 1: 오디오 탭 정책과 domain 계약 - [x] **Task 1.1: `CreatorChannelAudioQueryPolicy` fallback 정책 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicy.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioQueryPolicyTest.kt` - RED: `createPage(-1, 10)`이 `page=0`, `size=20`을 반환하고, `createPage(2, 100)`이 `page=2`, `size=50`을 반환하며, `resolveSort(null)`과 `resolveSort("UNKNOWN")`이 `ContentSort.LATEST`를 반환하는 테스트를 작성한다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest` - Expected: `CreatorChannelAudioQueryPolicy` 미존재 컴파일 실패 - GREEN: `resolveSort(sort: String?): ContentSort`, `createPage(page: Int?, size: Int?): CreatorChannelPage`, `limitItems`, `hasNext`, `purchaseRate`를 구현한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: 라이브 탭의 `CreatorChannelLiveReplayQueryPolicy`는 변경하지 않는다. 오디오 탭만 fallback 정책을 가진다. - 회귀 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest` - Expected: 기존 라이브 탭 정책 테스트가 있으면 통과한다. 테스트가 없으면 `No tests found`가 아닌 컴파일 실패가 없는지 확인한다. - [x] **Task 1.2: 오디오 탭 domain model과 port 계약 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/domain/CreatorChannelAudioTab.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/port/out/CreatorChannelAudioQueryPort.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt` - RED: service 테스트 파일에 `CreatorChannelAudioTab`, `CreatorChannelAudioTheme`, `CreatorChannelAudioContent`, `CreatorChannelAudioQueryPort` import를 추가하고 아직 service가 없어서 컴파일 실패하는 상태를 만든다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` - Expected: `CreatorChannelAudioQueryService` 또는 domain/port 미존재 컴파일 실패 - GREEN: 위 "Domain / Port 초안"의 타입을 추가한다. `CreatorChannelPage`는 기존 `kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage`를 재사용한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: `rg -n "v2\\.api\\.creator\\.channel\\.audio" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio`로 domain/port가 API 조립 계층에 의존하지 않는지 확인한다. - [x] **Task 1.3: 크리에이터 채널 오디오 콘텐츠 item 공통화** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/common/domain/CreatorChannelAudioContent.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/common/dto/CreatorChannelAudioContentResponse.kt` - Modify: live/home/audio domain과 DTO import - RED: live/home/audio 테스트 import를 공통 `CreatorChannelAudioContent`와 `CreatorChannelAudioContentResponse` 기준으로 변경하고 기존 타입 미존재/필드 불일치 컴파일 실패를 확인한다. - GREEN: 기존 live/home/audio의 중복 `CreatorChannelAudioContent`와 중복 Response를 제거하고 공통 타입을 사용한다. 실질 사용처가 없는 `publishedAt`은 공통 domain과 live/home mapping에서 제거한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest` - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest` - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` - REFACTOR: `rg -n "data class CreatorChannelAudioContent|data class CreatorChannelAudioContentResponse|publishedAt =|v2\\.creator\\.channel\\.(live|home|audio)\\.domain\\.CreatorChannelAudioContent|v2\\.api\\.creator\\.channel\\.(live|home)\\.dto\\.CreatorChannelAudioContentResponse" src/main/kotlin src/test/kotlin`로 중복 타입, 기존 패키지 import, 불필요한 domain field mapping이 남지 않았는지 확인한다. ### Phase 2: 오디오 탭 service와 API DTO 변환 - [x] **Task 2.1: `CreatorChannelAudioQueryService` orchestration 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryService.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt` - RED: fake port 기반 service 테스트를 작성한다. - `getAudioTab(creatorId=1, viewer, sort="UNKNOWN", themeId=999, page=-1, size=100)` 호출 시 실제 `sort=LATEST`, `themeId=null`, `page=0`, `size=50`, `offset=0`, `limit=51`이 port에 전달되어야 한다. - `paidAudioContentCount=4`, `purchasedAudioContentCount=3`이면 `purchasedAudioContentRate=75.0`이어야 한다. - `paidAudioContentCount=0`이면 `purchasedAudioContentRate=0.0`이어야 한다. - `creator`가 없으면 `member.validation.user_not_found`, role이 `CREATOR`가 아니면 `member.validation.creator_not_found`, 차단 관계면 기존 차단 메시지 예외를 던져야 한다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` - Expected: `CreatorChannelAudioQueryService` 미존재 컴파일 실패 - GREEN: 라이브 탭 service의 인증/차단/성인 콘텐츠 정책을 참고해 최소 구현한다. `LangContext.lang.code`를 theme/series 번역 조회 locale로 전달하고, `String?.toCdnUrl()`은 라이브 탭 service와 같은 규칙으로 구현한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: service가 QueryDSL/Q타입을 직접 import하지 않는지 확인한다. - Run: `rg -n "Q[A-Z]|queryFactory|javax\\.persistence" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application` - Expected: 검색 결과 없음 - [x] **Task 2.2: 오디오 탭 API response DTO와 facade 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/dto/CreatorChannelAudioTabResponse.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt` - RED: facade가 service 결과를 `CreatorChannelAudioTabResponse`로 변환하고 `isOwned`, `isRented`, `hasNext`의 JSON property 의미를 보존하는 테스트를 작성한다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest` - Expected: facade/DTO 미존재 컴파일 실패 - GREEN: 위 "Response data class 초안"에 맞춰 DTO를 추가하고 facade에서 `CreatorChannelAudioQueryService.getAudioTab(creatorId, viewer, sort, themeId, page, size, now)` 결과를 `CreatorChannelAudioTabResponse.from(tab)`으로 변환한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: 기존 라이브 탭 DTO를 이동하거나 수정하지 않는다. - Run: `rg -n "package kr\\.co\\.vividnext\\.sodalive\\.v2\\.api\\.creator\\.channel\\.live\\.dto" src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/dto/CreatorChannelLiveTabResponse.kt` - Expected: 기존 라이브 탭 DTO package 유지 ### Phase 3: QueryDSL repository 구현 - [x] **Task 3.1: repository skeleton과 creator/block/theme 조회 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/CreatorChannelAudioQueryRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt` - RED: `@DataJpaTest(properties = ["spring.cache.type=none"])` 기반으로 `findCreator`, `existsBlockedBetween`, `findActiveThemeId`, `findAudioThemes(creatorId, now, canViewAdultContent, locale="en")` 테스트를 작성한다. `ContentThemeTranslation`이 있으면 번역명, 없으면 원문명을 반환해야 하며, 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 없는 테마는 제외해야 한다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: repository 미존재 컴파일 실패 - GREEN: 라이브 탭 repository의 `findCreator`, `existsBlockedBetween`을 오디오 패키지로 필요한 만큼 복사하고, `findActiveThemeId`, `findAudioThemes`를 QueryDSL로 구현한다. `findAudioThemes`는 `audioContentCondition(creatorId, themeId = null, now, canViewAdultContent)`를 공유해 콘텐츠 목록/count와 같은 공개 조건을 적용한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: `ContentThemeTranslation.theme`이 blank인 경우 원문 fallback을 repository 또는 domain mapping 중 한 곳에서만 처리한다. - 후속 수정 검증 기록: - 무엇: 테마 목록에서 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 없는 테마를 제외하는 RED 테스트를 추가했다. - 왜: 오디오 탭에서 선택 가능한 테마가 실제 콘텐츠가 없는 빈 필터로 노출되지 않아야 한다. - 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest`를 실행했다. - 결과: 현재 구현은 활성 테마 전체를 반환해 `DefaultCreatorChannelAudioQueryRepositoryTest.kt:71`, `DefaultCreatorChannelAudioQueryRepositoryTest.kt:96`에서 실패함을 확인했다. - 무엇: `findAudioThemes`가 `creatorId`, `now`, `canViewAdultContent`, `locale`를 받아 콘텐츠 목록/count와 같은 공개 조건으로 테마를 조회하도록 수정했다. - 왜: 조회 가능한 아이템이 없는 테마와 성인 노출 정책상 볼 수 없는 테마를 응답에서 제외해야 한다. - 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest`를 실행했다. - 결과: `BUILD SUCCESSFUL`로 repository 필터링과 service 컨텍스트 전달을 확인했다. - [x] **Task 3.2: 오디오 콘텐츠 count와 소장률 count 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt` - RED: 아래 조건을 검증하는 repository 테스트를 추가한다. - `countAudioContents`는 공개/활성/예약 공개/성인 콘텐츠 정책과 활성 `themeId` 필터를 적용한다. - `countPaidAudioContents`는 같은 필터에서 `price > 0`만 계산한다. - `countPurchasedAudioContents`는 유료 콘텐츠 중 `OrderType.KEEP` 또는 유효한 `OrderType.RENTAL` 주문을 가진 콘텐츠만 계산한다. - 무료 콘텐츠는 구매 count와 소장률 count에서 제외한다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: 신규 count method 미구현 실패 - GREEN: 공통 `audioContentCondition(creatorId, themeId, now, canViewAdultContent)` private helper를 만들고 count query들이 같은 조건을 공유하게 구현한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: 목록 query와 count query의 조건이 어긋나지 않도록 helper 사용 여부를 확인한다. - Run: `rg -n "audioContentCondition|countAudioContents|countPaidAudioContents|countPurchasedAudioContents" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt` - Expected: 세 count method가 공통 조건 helper를 사용한다. - [x] **Task 3.3: 오디오 콘텐츠 목록, 정렬, 시리즈 번역, 소장/대여 상태 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt` - RED: 아래 조건을 검증하는 repository 테스트를 추가한다. - `findAudioContents`는 `size + 1`개 조회가 가능하도록 전달받은 `limit`을 그대로 사용한다. - `LATEST`, `PRICE_HIGH`, `PRICE_LOW` 정렬이 PRD 기준으로 동작한다. - `POPULAR`은 `orders.can` 합계 기준으로 정렬하고 비활성 주문은 제외한다. - `OWNED`는 소장 또는 유효 대여 중인 콘텐츠를 먼저 노출한다. - 시리즈에 속한 콘텐츠는 `SeriesTranslation(locale)`이 있으면 번역명을, 없으면 원문명을 `seriesName`으로 반환한다. - `isFirstContent`는 테마 필터와 무관하게 크리에이터의 전체 공개 오디오 콘텐츠 중 첫 콘텐츠 기준이다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: 목록/정렬 method 미구현 실패 - GREEN: 라이브 탭 repository의 `findLiveReplayAudioRows`, `audioSeriesByContentIds`, `orderStatesByContentIds`, `firstAudioContentId` 구조를 오디오 탭 범위에 맞춰 구현한다. `themeId == null`이면 전체 활성 테마를 대상으로 한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: QueryDSL 중복이 커지면 오디오 탭 repository 내부 private helper로만 정리하고, 라이브 탭 repository까지 건드리는 공용화는 이번 범위에서 하지 않는다. ### Phase 4: Controller와 공개 API 계약 - [x] **Task 4.1: `CreatorChannelAudioController` 추가** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioController.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioControllerTest.kt` - RED: MockMvc 테스트를 작성한다. - 비회원 `GET /api/v2/creator-channels/1/audio`는 401을 반환한다. - 인증 회원 기본 요청은 facade에 `sort=null`, `themeId=null`, `page=null`, `size=null`을 전달하고 성공 응답을 반환한다. - `sort=INVALID&page=-1&size=100&themeId=999` 요청은 controller에서 400을 내지 않고 facade까지 원 요청값을 전달한다. - 응답 JSON에는 `audioContentCount`, `paidAudioContentCount`, `purchasedAudioContentCount`, `purchasedAudioContentRate`, `themes`, `audioContents`, `sort`, `themeId`, `page`, `size`, `hasNext`, `audioContents[0].isOwned`, `audioContents[0].isRented`가 있어야 한다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioControllerTest` - Expected: controller 미존재 컴파일 실패 - GREEN: `@RequestMapping("/api/v2/creator-channels")`, `@GetMapping("/{creatorId}/audio")` controller를 추가한다. query parameter는 `@RequestParam(required = false) sort: String?`, `themeId: Long?`, `page: Int?`, `size: Int?`로 받는다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioControllerTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: 기존 `/live`, `/home` mapping과 충돌하지 않는지 확인한다. - Run: `rg -n "@GetMapping\\(\"/\\{creatorId\\}/(home|live|audio)\"\\)" src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel` - Expected: home/live/audio 각각 1건 - [x] **Task 4.2: 오디오 탭 통합 흐름 테스트 추가** - Files: - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioEndToEndTest.kt` - RED: `@SpringBootTest + MockMvc` 기반으로 인증 회원이 `GET /api/v2/creator-channels/{creatorId}/audio?sort=INVALID&page=-1&size=100&themeId=999`를 호출했을 때 200 성공과 fallback 적용 응답(`sort=LATEST`, `themeId=null`, `page=0`, `size=50`)을 받는 테스트를 작성한다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioEndToEndTest` - Expected: endpoint 또는 fixture 미구현으로 실패 - GREEN: 필요한 최소 fixture만 추가하고 controller, facade, service, repository wiring이 동작하도록 구현을 보완한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioEndToEndTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: E2E fixture가 기존 테스트 데이터를 과도하게 공유하지 않는지 확인하고, 불필요한 데이터 생성 helper는 추가하지 않는다. ### Phase 5: 회귀 검증과 문서 기록 - [x] **Task 5.1: 관련 단위/슬라이스 테스트 회귀 실행** - Files: - Verify: `docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md` - TDD 예외 사유: 코드 구현이 아니라 구현 완료 후 검증 기록을 누적하는 문서 작업이다. - 대체 검증 방법: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest` - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest` - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioControllerTest` - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: 모두 `BUILD SUCCESSFUL` - 검증 기록: 구현 완료 후 실행 명령, 결과, 실패 시 원인과 수정 내역을 이 task 아래에 누적한다. - 2026-06-19 실행: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` → `BUILD SUCCESSFUL`. - [x] **Task 5.2: 전체 회귀와 포맷 검증** - Files: - Verify: `docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md` - TDD 예외 사유: 전체 회귀/포맷 검증 기록 task다. - 대체 검증 방법: - Run: `./gradlew test` - Run: `./gradlew ktlintCheck` - Run: `git diff --check` - Run: `rg -n "미완성 표시|후속 처리 표시" docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio` - Expected: Gradle 명령은 `BUILD SUCCESSFUL`, `git diff --check`는 출력 없음, placeholder 검색은 의도하지 않은 결과 없음 - 검증 기록: 구현 완료 후 전체 검증 결과를 이 task 아래와 문서 하단의 검증 기록에 누적한다. - 2026-06-19 실행: `./gradlew --no-daemon --max-workers=1 -Dkotlin.compiler.execution.strategy=in-process test` → `BUILD SUCCESSFUL`. - 2026-06-19 실행: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheck` → `BUILD SUCCESSFUL`. - 2026-06-19 실행: `git diff --check` → 출력 없음. - 2026-06-19 실행: `rg -n "미완성 표시|후속 처리 표시" docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio` → 계획 문서의 검증 명령/기록 자체만 매칭되어 의도하지 않은 placeholder 없음. - 2026-06-19 코드 리뷰: controller/facade/service/repository/test 경로를 PRD 요구사항 기준으로 검토했고, 공개 조건, fallback, count, 정렬, 시리즈/테마 번역 fallback, 소장/대여 상태 관련 차단 이슈 없음. --- ## 4. 구현 순서 요약 1. 오디오 탭 fallback/page/sort/rate 정책을 테스트로 고정한다. 2. domain model과 port 계약을 추가한다. 3. service orchestration을 fake port 테스트로 고정한다. 4. API DTO와 facade 변환을 고정한다. 5. QueryDSL repository를 creator/block/theme/count/list 순서로 구현한다. 6. controller 공개 계약을 MockMvc로 고정한다. 7. E2E 테스트와 전체 회귀 검증을 실행하고 결과를 이 문서에 누적한다. --- ## 5. PRD 요구사항 추적 - API endpoint와 공개 API 패키지: Phase 4 Task 4.1 - 재사용 가능한 조회 책임을 API 밖 도메인 패키지에 배치: Phase 1, Phase 2, Phase 3 - `creatorId`, `sort`, `themeId`, `page`, `size` 요청 처리: Phase 1 Task 1.1, Phase 4 Task 4.1 - invalid `sort` -> `LATEST` fallback: Phase 1 Task 1.1, Phase 4 Task 4.1, Phase 4 Task 4.2 - page/size fallback: Phase 1 Task 1.1, Phase 2 Task 2.1, Phase 4 Task 4.2 - 비활성/미존재 `themeId` 전체 조회 fallback: Phase 2 Task 2.1, Phase 3 Task 3.1, Phase 4 Task 4.2 - 테마 다국어 목록: Phase 3 Task 3.1 - 오디오/유료/구매 count와 퍼센트 소장률: Phase 2 Task 2.1, Phase 3 Task 3.2 - 오디오 콘텐츠 목록과 `CreatorChannelAudioContentResponse` 의미 보존: Phase 2 Task 2.2, Phase 3 Task 3.3 - 시리즈 이름 다국어 표시: Phase 3 Task 3.3 - 정렬 정책: Phase 3 Task 3.3 - 기존 API endpoint/응답 의미 보존: Phase 4 Task 4.1, Phase 5 Task 5.2 --- ## 6. 검증 기록 - 2026-06-19: plan-task 문서 작성 단계. 구현 코드는 아직 변경하지 않았다. - 2026-06-19: Phase 1 완료. - RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` 실행 시 `CreatorChannelAudioQueryPolicy`, `CreatorChannelAudioTab`, `CreatorChannelAudioQueryPort` 미존재 컴파일 실패 확인. - GREEN: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest` → `BUILD SUCCESSFUL`. - GREEN: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` → `BUILD SUCCESSFUL`. - 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest` → `BUILD SUCCESSFUL`. - 의존성 확인: `rg -n "v2\\.api\\.creator\\.channel\\.audio" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio` → 출력 없음. - 2026-06-19: Phase 1 보강 범위 추가. - 크리에이터 채널 오디오 콘텐츠 item은 홈/라이브/오디오 탭에서 공통 domain/response를 사용한다. - live/home domain model의 `publishedAt`은 공개 응답에 사용하지 않고 오디오 item 공통 계약에도 필요하지 않아 제거 대상으로 확정했다. - 2026-06-19: Task 1.3 완료. - RED: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` → 공통 `CreatorChannelAudioContent`, `CreatorChannelAudioContentResponse` 미존재와 `publishedAt` 필드 불일치 컴파일 실패 확인. - GREEN: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` → `BUILD SUCCESSFUL`. - 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacadeTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest` → `BUILD SUCCESSFUL`. - 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.home.application.CreatorChannelHomeFacadeTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelHomeQueryPolicyTest` → 단독 재실행 시 `BUILD SUCCESSFUL`. - 참고: live/home 회귀 테스트를 동시에 실행했을 때 home 테스트 결과 XML 파일 쓰기 실패가 1회 발생했다. 단독 재실행에서 통과해 Gradle 병렬 실행 중 `build/test-results/test` 쓰기 충돌로 판단했다. - 포맷: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheck` → `BUILD SUCCESSFUL`. - 공백: `git diff --check` → 출력 없음. - 중복 확인: `rg -n "data class CreatorChannelAudioContent|data class CreatorChannelAudioContentResponse|publishedAt =|v2\\.creator\\.channel\\.(live|home|audio)\\.domain\\.CreatorChannelAudioContent|v2\\.api\\.creator\\.channel\\.(live|home)\\.dto\\.CreatorChannelAudioContentResponse" src/main/kotlin src/test/kotlin` → 공통 domain/response 1건씩, 각 탭 port record, 홈 시리즈 집계 local 변수만 확인. - 2026-06-19: Phase 2 완료. - Task 2.1 RED: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` → `CreatorChannelAudioQueryService` 미존재 컴파일 실패 확인. - Task 2.2 RED: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest` → `CreatorChannelAudioFacade` 미존재 컴파일 실패 확인. - GREEN: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest` → `BUILD SUCCESSFUL`. - 리뷰 보강: Phase 3 port 구현 전 Spring bean 생성 실패를 피하기 위해 live 탭과 동일하게 `ObjectProvider` 주입으로 조정했다. - 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest` → `BUILD SUCCESSFUL`. - 포맷: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheck` → `BUILD SUCCESSFUL`. - 의존성 확인: `rg -n "Q[A-Z]|queryFactory|javax\\.persistence" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/application` → 출력 없음. - 공백: `git diff --check` → 출력 없음. - 2026-06-19: Phase 3 완료. - Task 3.1~3.3 RED: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` → 테스트 미존재/구현 전 실패 확인. - GREEN: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` → `BUILD SUCCESSFUL`. - 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest` → `BUILD SUCCESSFUL`. - 포맷: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheck` → `BUILD SUCCESSFUL`. - 공백: `git diff --check` → 출력 없음. - 참고: 검증 중 Gradle 명령을 병렬 실행했을 때 QueryDSL generated source 참조 오류가 1회 발생했다. 단독 순차 재실행에서 컴파일과 테스트가 통과해 병렬 Gradle 실행 중 generated source 작업 충돌로 판단했다. - 리뷰 보강: `OWNED` 정렬이 주문 수가 아니라 소장 또는 유효 대여 여부 boolean 기준이 되도록 `CaseBuilder` 정렬로 수정했다. - 보강 RED: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest.shouldSortAudioContentsByOwnedAndReturnOrderStatesWithSeriesFallback` → 중복 주문 콘텐츠가 더 최신 소장 콘텐츠보다 앞서는 assertion 실패 확인. - 보강 GREEN: 같은 테스트 재실행 → `BUILD SUCCESSFUL`. - 보강 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` → `BUILD SUCCESSFUL`. - 보강 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest` → `BUILD SUCCESSFUL`. - 보강 포맷: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheck` → `BUILD SUCCESSFUL`. - 보강 공백: `git diff --check` → 출력 없음. - 2026-06-19: Phase 4 완료. - Task 4.1 RED: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioControllerTest` → `CreatorChannelAudioController` 미존재 컴파일 실패 확인. - Task 4.1 GREEN: 같은 테스트 재실행 → `BUILD SUCCESSFUL`. - Task 4.2: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioEndToEndTest` → `BUILD SUCCESSFUL`. - 보강: 전체 suite 실행 중 SpringBootTest context 추가로 heap 사용량이 증가해 `OutOfMemoryError`가 발생했다. 오디오 E2E가 기존 라이브 E2E와 Spring TestContext cache를 재사용하도록 datasource property를 동일하게 맞추고, 공유 DB에서 theme 정렬에 의존하지 않도록 assertion을 조정했다. - 보강 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveEndToEndTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioEndToEndTest` → `BUILD SUCCESSFUL`. - 보강 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioEndToEndTest` → `BUILD SUCCESSFUL`. - 보강 포맷: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheck` → `BUILD SUCCESSFUL`. - 보강 전체 회귀: `./gradlew --no-daemon --max-workers=1 -Dkotlin.compiler.execution.strategy=in-process test` → `BUILD SUCCESSFUL`. - 보강 매핑 확인: `rg -n "@GetMapping\\(\\\"/\\{creatorId\\}/(home|live|audio)\\\"\\)" src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel` → home/live/audio 각각 1건. - 보강 공백: `git diff --check` → 출력 없음. - 2026-06-19: Phase 5 완료. - 대상 회귀: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.domain.CreatorChannelAudioQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application.CreatorChannelAudioFacadeTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` → `BUILD SUCCESSFUL`. - 전체 회귀: `./gradlew --no-daemon --max-workers=1 -Dkotlin.compiler.execution.strategy=in-process test` → `BUILD SUCCESSFUL`. - 포맷: `./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheck` → `BUILD SUCCESSFUL`. - 공백: `git diff --check` → 출력 없음. - placeholder 확인: `rg -n "미완성 표시|후속 처리 표시" docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio` → 계획 문서의 검증 명령/기록 자체만 매칭되어 의도하지 않은 placeholder 없음. - 코드 리뷰: controller/facade/service/repository/test 경로를 PRD 요구사항 기준으로 검토했고, 공개 조건, fallback, count, 정렬, 시리즈/테마 번역 fallback, 소장/대여 상태 관련 차단 이슈 없음. - 2026-06-19: 후속 수정 완료. - 요구사항: 오디오 탭 `themes` 응답에서 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 없는 테마를 제외한다. - RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` → 활성 테마 전체를 반환해 신규 assertion 실패 확인. - GREEN: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` → `BUILD SUCCESSFUL`. - 문서 명령 확인: `./gradlew tasks --all` → `BUILD SUCCESSFUL`. - 포맷: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL`.