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, 기본값/fallbackLATEST - 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
- path variable:
- controller는 invalid
sortfallback을 위해sort: String?으로 받고 service/facade 경계에서ContentSort로 보정한다. - response:
audioContentCount: 적용된 필터 기준 오디오 콘텐츠 전체 개수paidAudioContentCount: 적용된 필터 기준price > 0콘텐츠 개수purchasedAudioContentCount: 적용된 필터 기준 유료 콘텐츠 중 조회자가 유효하게 소장하거나 대여 중인 콘텐츠 개수purchasedAudioContentRate:paidAudioContentCount == 0이면0.0, 아니면(purchasedAudioContentCount / paidAudioContentCount) * 100themes: 활성 테마 중 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 1개 이상 있는 테마 목록. 선택한themeId와 무관하게 내려준다.audioContents: 기존CreatorChannelAudioContentResponse와 같은 필드/의미를 가진 item 목록sort: 실제 적용한ContentSortthemeId: 실제 적용한 활성 테마 id, 전체 조회 fallback이면nullpage: fallback 보정 후 실제 적용된 page indexsize: fallback 보정 후 실제 적용된 page sizehasNext: 다음 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 descPOPULAR: 구매 매출 합계 desc,releaseDate desc,audioContent.id descOWNED: 조회자 소장 또는 유효 대여 여부 desc,releaseDate desc,audioContent.id descPRICE_HIGH:price desc,releaseDate desc,audioContent.id descPRICE_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:
CreatorChannelAudioQueryPolicyfallback 정책 추가- 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
- Create:
- 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미존재 컴파일 실패
- Run:
- 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
- Run:
- REFACTOR: 라이브 탭의
CreatorChannelLiveReplayQueryPolicy는 변경하지 않는다. 오디오 탭만 fallback 정책을 가진다. - 회귀 확인:
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest - Expected: 기존 라이브 탭 정책 테스트가 있으면 통과한다. 테스트가 없으면
No tests found가 아닌 컴파일 실패가 없는지 확인한다.
- Run:
- Files:
-
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
- Create:
- RED: service 테스트 파일에
CreatorChannelAudioTab,CreatorChannelAudioTheme,CreatorChannelAudioContent,CreatorChannelAudioQueryPortimport를 추가하고 아직 service가 없어서 컴파일 실패하는 상태를 만든다. - 실패 확인:
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest - Expected:
CreatorChannelAudioQueryService또는 domain/port 미존재 컴파일 실패
- Run:
- 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
- Run:
- REFACTOR:
rg -n "v2\\.api\\.creator\\.channel\\.audio" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio로 domain/port가 API 조립 계층에 의존하지 않는지 확인한다.
- Files:
-
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
- Create:
- 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
- Run:
- 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이 남지 않았는지 확인한다.
- Files:
Phase 2: 오디오 탭 service와 API DTO 변환
-
Task 2.1:
CreatorChannelAudioQueryServiceorchestration 추가- 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
- Create:
- 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미존재 컴파일 실패
- Run:
- 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
- Run:
- 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: 검색 결과 없음
- Run:
- Files:
-
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
- Create:
- 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 미존재 컴파일 실패
- Run:
- 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
- Run:
- 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 유지
- Run:
- Files:
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
- Create:
- 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 미존재 컴파일 실패
- Run:
- 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
- Run:
- 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 컨텍스트 전달을 확인했다.
- Files:
-
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
- Modify:
- 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 미구현 실패
- Run:
- 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
- Run:
- 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를 사용한다.
- Run:
- Files:
-
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
- Modify:
- 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 미구현 실패
- Run:
- 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
- Run:
- REFACTOR: QueryDSL 중복이 커지면 오디오 탭 repository 내부 private helper로만 정리하고, 라이브 탭 repository까지 건드리는 공용화는 이번 범위에서 하지 않는다.
- Files:
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
- Create:
- 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 미존재 컴파일 실패
- Run:
- 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
- Run:
- REFACTOR: 기존
/live,/homemapping과 충돌하지 않는지 확인한다.- Run:
rg -n "@GetMapping\\(\"/\\{creatorId\\}/(home|live|audio)\"\\)" src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel - Expected: home/live/audio 각각 1건
- Run:
- Files:
-
Task 4.2: 오디오 탭 통합 흐름 테스트 추가
- Files:
- Create:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioEndToEndTest.kt
- Create:
- 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 미구현으로 실패
- Run:
- 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
- Run:
- REFACTOR: E2E fixture가 기존 테스트 데이터를 과도하게 공유하지 않는지 확인하고, 불필요한 데이터 생성 helper는 추가하지 않는다.
- Files:
Phase 5: 회귀 검증과 문서 기록
-
Task 5.1: 관련 단위/슬라이스 테스트 회귀 실행
- Files:
- Verify:
docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md
- Verify:
- 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
- Run:
- 검증 기록: 구현 완료 후 실행 명령, 결과, 실패 시 원인과 수정 내역을 이 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.
- Files:
-
Task 5.2: 전체 회귀와 포맷 검증
- Files:
- Verify:
docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md
- Verify:
- 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 검색은 의도하지 않은 결과 없음
- Run:
- 검증 기록: 구현 완료 후 전체 검증 결과를 이 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, 소장/대여 상태 관련 차단 이슈 없음.
- Files:
4. 구현 순서 요약
- 오디오 탭 fallback/page/sort/rate 정책을 테스트로 고정한다.
- domain model과 port 계약을 추가한다.
- service orchestration을 fake port 테스트로 고정한다.
- API DTO와 facade 변환을 고정한다.
- QueryDSL repository를 creator/block/theme/count/list 순서로 구현한다.
- controller 공개 계약을 MockMvc로 고정한다.
- 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->LATESTfallback: 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→ 출력 없음.
- RED:
-
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 변수만 확인.
- RED:
-
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<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.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→ 출력 없음.
- Task 2.1 RED:
-
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→ 출력 없음.
- Task 4.1 RED:
-
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.
- 요구사항: 오디오 탭