Files
sodalive-backend-spring-boot/docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md

52 KiB

크리에이터 채널 오디오 탭 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와 이 문서를 갱신한다.

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<CreatorChannelAudioThemeResponse>,
    val audioContents: List<CreatorChannelAudioContentResponse>,
    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를 참조하지 않는다.

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<CreatorChannelAudioTheme>,
    val audioContents: List<CreatorChannelAudioContent>,
    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
)
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<CreatorChannelAudioThemeRecord>
    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<CreatorChannelAudioContentRecord>
}

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 계약

  • 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가 아닌 컴파일 실패가 없는지 확인한다.
  • 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 조립 계층에 의존하지 않는지 확인한다.
  • 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를 공통 CreatorChannelAudioContentCreatorChannelAudioContentResponse 기준으로 변경하고 기존 타입 미존재/필드 불일치 컴파일 실패를 확인한다.
    • 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 변환

  • 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: 검색 결과 없음
  • 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 구현

  • 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로 구현한다. findAudioThemesaudioContentCondition(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에서 실패함을 확인했다.
      • 무엇: findAudioThemescreatorId, 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 컨텍스트 전달을 확인했다.
  • 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를 사용한다.
  • 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 테스트를 추가한다.
      • findAudioContentssize + 1개 조회가 가능하도록 전달받은 limit을 그대로 사용한다.
      • LATEST, PRICE_HIGH, PRICE_LOW 정렬이 PRD 기준으로 동작한다.
      • POPULARorders.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 계약

  • 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건
  • 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: 회귀 검증과 문서 기록

  • 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.DefaultCreatorChannelAudioQueryRepositoryTestBUILD SUCCESSFUL.
  • 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 testBUILD SUCCESSFUL.
    • 2026-06-19 실행: ./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheckBUILD 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.CreatorChannelAudioQueryPolicyTestBUILD SUCCESSFUL.
    • GREEN: ./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTestBUILD SUCCESSFUL.
    • 회귀: ./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTestBUILD 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.CreatorChannelAudioQueryServiceTestBUILD 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.CreatorChannelLiveControllerTestBUILD 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 ktlintCheckBUILD 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.CreatorChannelAudioQueryServiceTestCreatorChannelAudioQueryService 미존재 컴파일 실패 확인.
    • 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.CreatorChannelAudioFacadeTestCreatorChannelAudioFacade 미존재 컴파일 실패 확인.
    • 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.CreatorChannelAudioFacadeTestBUILD SUCCESSFUL.
    • 리뷰 보강: Phase 3 port 구현 전 Spring bean 생성 실패를 피하기 위해 live 탭과 동일하게 ObjectProvider<CreatorChannelAudioQueryPort> 주입으로 조정했다.
    • 회귀: ./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.CreatorChannelAudioFacadeTestBUILD SUCCESSFUL.
    • 포맷: ./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheckBUILD 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.DefaultCreatorChannelAudioQueryRepositoryTestBUILD 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.CreatorChannelAudioFacadeTestBUILD SUCCESSFUL.

    • 포맷: ./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheckBUILD 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.DefaultCreatorChannelAudioQueryRepositoryTestBUILD 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.CreatorChannelAudioFacadeTestBUILD SUCCESSFUL.

    • 보강 포맷: ./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheckBUILD 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.CreatorChannelAudioControllerTestCreatorChannelAudioController 미존재 컴파일 실패 확인.
    • 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.CreatorChannelAudioEndToEndTestBUILD 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.CreatorChannelAudioEndToEndTestBUILD 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.CreatorChannelAudioEndToEndTestBUILD SUCCESSFUL.
    • 보강 포맷: ./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheckBUILD SUCCESSFUL.
    • 보강 전체 회귀: ./gradlew --no-daemon --max-workers=1 -Dkotlin.compiler.execution.strategy=in-process testBUILD 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.DefaultCreatorChannelAudioQueryRepositoryTestBUILD SUCCESSFUL.
    • 전체 회귀: ./gradlew --no-daemon --max-workers=1 -Dkotlin.compiler.execution.strategy=in-process testBUILD SUCCESSFUL.
    • 포맷: ./gradlew --no-daemon -Dkotlin.compiler.execution.strategy=in-process ktlintCheckBUILD 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.CreatorChannelAudioQueryServiceTestBUILD SUCCESSFUL.
    • 문서 명령 확인: ./gradlew tasks --allBUILD SUCCESSFUL.
    • 포맷: ./gradlew ktlintCheckBUILD SUCCESSFUL.