58 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}/live로 현재 진행 중인 라이브와 라이브 다시듣기 콘텐츠를 페이징/정렬 조회할 수 있게 한다.
Architecture: 라이브 탭 공개 API는 기존 크리에이터 채널 홈 API 경계를 확장하지 않고 kr.co.vividnext.sodalive.v2.api.creator.channel.live 조립 계층에 둔다. Controller와 Facade, API 응답 DTO는 이 계층에서 관리하고, 라이브/콘텐츠/시리즈/주문 상태처럼 재사용 가능한 조회 책임은 API 패키지 밖의 도메인 패키지에서 제공한다. 공용 콘텐츠 정렬 enum은 채널 전용 이름을 피하고 kr.co.vividnext.sodalive.v2.common.domain.ContentSort로 둔다. 기존 홈 API는 이번 구현 중 구조 이동하지 않고, 마지막 Phase에 다음 범위 작업용 리팩토링 프롬프트만 남긴다.
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}/live - 인증 정책: 인증 회원만 조회 가능. 비회원은 기존 Security 흐름과
requireMember정책으로 거부한다. - request:
- path variable:
creatorId - query parameter:
sort, 기본값LATEST - query parameter:
page, 기본값0, 0부터 시작 - query parameter:
size, 기본값20
- path variable:
- response:
liveReplayContentCount: 같은 필터를 적용한 라이브 다시듣기 콘텐츠 전체 개수currentLive: 기존CreatorChannelLiveResponse와 같은 필드/의미를 가진 라이브 탭 API 응답 DTOliveReplayContents: 기존CreatorChannelAudioContentResponse와 같은 필드/의미에isOwned,isRented를 포함한 라이브 탭 API 응답 DTOsort: 실제 적용한ContentSortpage: 이번 요청에 적용된 page indexsize: 이번 요청에 적용된 page sizehasNext: 다음 page 존재 여부
- 라이브 탭 API 응답의 오디오 콘텐츠 item에는
isOwned,isRented를 포함한다. isOwned/isRented판정은 주문 row를 각각 확인한다. 유효한KEEP주문이 있으면isOwned == true, 유효한RENTAL주문이 있으면isRented == true다.isOwned == true와isRented == true가 동시에 발생할 가능성은 없지만, 데이터상 동시에 유효한 소장/대여 주문이 있으면 둘 다true로 내려준다.- 라이브 다시듣기 콘텐츠 기준:
AudioContentTheme.theme == "다시듣기"이고AudioContentTheme.isActive == true인 공개 오디오 콘텐츠. - 공개 콘텐츠 기준:
AudioContent.isActive == true,AudioContent.duration != null,AudioContent.releaseDate != null,AudioContent.releaseDate <= now. - 성인 콘텐츠는 조회자의 성인 콘텐츠 노출 정책이 false이면 목록과 count에서 제외한다.
- 현재 라이브 노출은 기존 홈 API의
findCurrentLive정책을 재사용한다. - 정렬:
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인 주문만 포함한다. 환불/비활성 주문은 제외한다. - page/size validation은 service에서 명시적으로 수행한다.
page < 0,size < 20,size > 50이면 기존common.error.invalid_request계열 오류를 사용한다.
1. 파일 구조 계획
공용 정렬 enum
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/common/domain/ContentSort.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/common/domain/ContentSortTest.kt
기존 크리에이터 채널 DTO/domain 확장
이미 완료된 선행 범위다. 미완료 라이브 탭 구현은 아래
v2.api.creator.channel.live조립 계층과v2.creator.channel.live도메인 조회 계층을 따른다.
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelHome.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/dto/CreatorChannelHomeResponse.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/port/out/CreatorChannelHomeQueryPort.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelHomeQueryService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/in/web/CreatorChannelHomeControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelHomeQueryServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt
라이브 탭 신규 API 조립 계층
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveController.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacade.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/dto/CreatorChannelLiveTabResponse.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacadeTest.kt
라이브 탭 도메인 조회 계층
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelLiveTab.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelPage.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelLiveReplayQueryPolicy.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/port/out/CreatorChannelLiveQueryPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/CreatorChannelLiveQueryRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelLiveReplayQueryPolicyTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
문서 산출물
- Modify:
docs/20260617_크리에이터_채널_라이브_API/plan-task.md
2. Response data class 초안
구현 시 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/dto/CreatorChannelLiveTabResponse.kt에 아래 DTO를 기준으로 추가한다. 필드명은 공개 API 계약이므로 변경이 필요하면 먼저 PRD와 이 문서를 갱신한다.
package kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelAudioContent
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLive
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveTab
import java.time.LocalDateTime
import java.time.ZoneOffset
data class CreatorChannelLiveTabResponse(
val liveReplayContentCount: Int,
val currentLive: CreatorChannelLiveResponse?,
val liveReplayContents: List<CreatorChannelAudioContentResponse>,
val sort: ContentSort,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
) {
companion object {
fun from(tab: CreatorChannelLiveTab): CreatorChannelLiveTabResponse {
return CreatorChannelLiveTabResponse(
liveReplayContentCount = tab.liveReplayContentCount,
currentLive = tab.currentLive?.let(CreatorChannelLiveResponse::from),
liveReplayContents = tab.liveReplayContents.map(CreatorChannelAudioContentResponse::from),
sort = tab.sort,
page = tab.page.page,
size = tab.page.size,
hasNext = tab.hasNext
)
}
}
}
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
)
}
}
}
data class CreatorChannelLiveResponse(
val liveId: Long,
val title: String,
val coverImageUrl: String?,
val beginDateTimeUtc: String,
val price: Int,
@JsonProperty("isAdult")
val isAdult: Boolean
) {
companion object {
fun from(live: CreatorChannelLive): CreatorChannelLiveResponse {
return CreatorChannelLiveResponse(
liveId = live.liveId,
title = live.title,
coverImageUrl = live.coverImageUrl,
beginDateTimeUtc = live.beginDateTime.toUtcIso(),
price = live.price,
isAdult = live.isAdult
)
}
}
}
private fun LocalDateTime.toUtcIso(): String {
return atOffset(ZoneOffset.UTC).toInstant().toString()
}
위 예시는 라이브 탭 공개 API 응답 DTO 기준이다. 기존
CreatorChannelHomeResponse파일은 이번 라이브 탭 구조 정렬 작업에서 이동하지 않는다.
Phase 1: 공용 정렬 enum과 기존 오디오 응답 확장
-
Task 1.1: 공용
ContentSortenum 추가- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/common/domain/ContentSort.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/common/domain/ContentSortTest.kt
- Create:
- RED:
ContentSortTest를 먼저 추가해LATEST,POPULAR,OWNED,PRICE_HIGH,PRICE_LOW값이 존재하는지 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.common.domain.ContentSortTest - GREEN:
ContentSortenum을 추가한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.common.domain.ContentSortTest - REFACTOR: enum 이름에 크리에이터 채널 전용 의미가 남아 있지 않은지
rg -n "CreatorChannel.*Sort|Live.*Sort" src/main/kotlin/kr/co/vividnext/sodalive/v2로 확인한다. - 검증 기록(2026-06-17): RED 단계에서
ContentSort미존재 컴파일 실패를 확인했다. GREEN 단계에서ContentSortenum 추가 후./gradlew test --tests kr.co.vividnext.sodalive.v2.common.domain.ContentSortTest성공을 확인했다.
- Files:
-
Task 1.2:
CreatorChannelAudioContentResponse에 소장/대여 필드 추가- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelHome.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/dto/CreatorChannelHomeResponse.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/port/out/CreatorChannelHomeQueryPort.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelHomeQueryService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/in/web/CreatorChannelHomeControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelHomeQueryServiceTest.kt
- Modify:
- RED: controller 테스트에서
latestAudioContent.isOwned,latestAudioContent.isRented,audioContents[0].isOwned,audioContents[0].isRentedJSON 필드를 기대하도록 추가한다. - RED: service 테스트에서
CreatorChannelAudioContentRecord→CreatorChannelAudioContent변환 시isOwned,isRented가 유지되는지 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest - GREEN: domain model, record, response DTO, service 변환에
isOwned,isRented를 추가한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest - REFACTOR: 기존 홈 API 응답에 새 boolean 필드가 항상 존재하도록 null 불가능
Boolean으로 유지한다. - 검증 기록(2026-06-17): RED 단계에서
isOwned/isRented미존재 컴파일 실패를 확인했다. GREEN 단계에서 domain/record/response/service mapper를 확장하고./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest성공을 확인했다.
- Files:
-
Task 1.3: 기존 홈 오디오 조회에 주문 상태 bulk 판정 추가
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/port/out/CreatorChannelHomeQueryPort.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelHomeQueryService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt
- Modify:
- RED: repository 테스트에 조회자가
KEEP주문한 콘텐츠와 유효한RENTAL주문한 콘텐츠를 넣고,findLatestAudioContent,findAudioContents결과의isOwned/isRented가 각각 맞는지 검증한다. - RED: 같은 콘텐츠에
KEEP과 유효한RENTAL이 함께 있으면isOwned == true,isRented == true를 기대한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest - GREEN:
findLatestAudioContent,findAudioContents에viewerId를 전달하고, 조회된 content id 묶음으로 주문 상태를 bulk 조회해CreatorChannelAudioContentRecord에 채운다. 유효 대여 조건은 기존 주문 정책과 같이order.isActive == true,order.type == RENTAL,order.endDate > now를 사용한다. 소장 조건은order.isActive == true,order.type == KEEP이다. 소장/대여 상태는 서로 배타적으로 보정하지 않는다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest - REFACTOR: 콘텐츠마다
OrderRepository.isExistOrderedAndOrderType를 반복 호출하지 않고 content id 목록 기반 bulk 조회를 유지한다. - 검증 기록(2026-06-17): RED 단계에서 repository method signature와
isOwned/isRented미존재 컴파일 실패를 확인했다. GREEN 단계에서 content id 목록 기반 bulk 주문 상태 조회를 추가하고./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest성공을 확인했다.
- Files:
Phase 2: 라이브 탭 domain/application 정책
-
Task 2.1: 라이브 탭 domain model과 page 정책 추가
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelLiveTab.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelPage.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelLiveReplayQueryPolicy.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelLiveReplayQueryPolicyTest.kt
- Create:
- RED:
page=0,size=20이면 offset0, fetch limit21, 응답 items limit20,hasNext == true판정이 되는 테스트를 작성한다. - RED:
page < 0,size < 20,size > 50이면 정책이 예외를 던지는 테스트를 작성한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest - GREEN:
CreatorChannelPage(page: Int, size: Int)와CreatorChannelLiveReplayQueryPolicy를 추가한다.offset = page * size,fetchLimit = size + 1,items = fetched.take(size),hasNext = fetched.size > size를 제공한다.size는 20 이상 50 이하로 검증해fetchLimitoverflow를 방지한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest - REFACTOR: page/size 계산은 service나 repository에 중복 구현하지 않는다.
- 검증 기록(2026-06-17): RED 단계에서
CreatorChannelLiveReplayQueryPolicy미존재 컴파일 실패를 확인했다. GREEN 단계에서CreatorChannelPage,CreatorChannelLiveTab,CreatorChannelLiveReplayQueryPolicy를 추가하고./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest성공을 확인했다. 추가 리뷰 반영으로size < 20,size > 50,size = Int.MAX_VALUE가common.error.invalid_request를 던지고size = 50의fetchLimit이 51인지 검증했다.
- Files:
-
Task 2.2:
CreatorChannelLiveQueryService골격 추가- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/port/out/CreatorChannelLiveQueryPort.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt
- Create:
- RED: service 테스트에서
getLiveTab(creatorId, viewer, sort = ContentSort.LATEST, page = 0, size = 20)호출 시 port의findCreator,existsBlockedBetween,findCurrentLive,countLiveReplayAudioContents,findLiveReplayAudioContents가 필요한 인자로 호출되는지 fake port로 검증한다. - RED: 조회 대상이 없으면
member.validation.user_not_found, 크리에이터가 아니면member.validation.creator_not_found, 차단 관계이면 기존 차단 메시지 예외를 기대한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest - GREEN: 기존
CreatorChannelHomeQueryService의 인증 회원 기준 정책을 따라 creator 검증, 차단 검증, adult visibility, effective gender 계산을 구현한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest - REFACTOR:
CreatorChannelHomeQueryService와 중복되는 private 변환/검증 코드가 과도해지면 같은 phase 안에서는 복사 유지하고, 추후 별도 공용 validator 추출은 구현 범위 밖으로 둔다. - 검증 기록(2026-06-17): RED 단계에서
CreatorChannelLiveQueryService와CreatorChannelLiveQueryPort미존재 컴파일 실패를 확인했다. GREEN 단계에서 홈 API 서비스의 creator 검증, 차단 검증, adult visibility, effective gender 전달 패턴을 반영하고./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest성공을 확인했다.
- Files:
-
Task 2.3: 라이브 탭 service 응답 조립 완성
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt - Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelLiveTab.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt
- Modify:
- RED: fake port가
size + 1개 콘텐츠를 반환하면 service 응답의liveReplayContents.size == size,hasNext == true,page == 0,size == 20,sort == LATEST인지 검증한다. - RED: invalid
page/size요청은 port bean 조회 전에common.error.invalid_request를 던지는지 검증한다. - RED:
page범위에 콘텐츠가 없으면liveReplayContents는 빈 배열이고 count는 port count 값을 유지하는지 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest - GREEN: service에서 policy로 page/size를 먼저 검증하고, 검증 후 port를 조회해 count와
size + 1조회 결과를 조립해CreatorChannelLiveTab을 반환한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest - REFACTOR:
ContentSort기본값은 controller가 기본값을 넣되, service 테스트에서도 명시 인자를 사용해 정책 위치를 분리한다. - 검증 기록(2026-06-17): RED 단계에서 service 조립 대상 domain/port/service 미존재 컴파일 실패를 확인했다. GREEN 단계에서 count, 현재 라이브,
size + 1다시듣기 목록을CreatorChannelLiveTab으로 조립하고hasNext, page, sort, 소장/대여 상태 보존을./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest로 확인했다. 추가 리뷰 반영으로 invalidpage/size요청이ObjectProvider.getObject()보다 먼저common.error.invalid_request로 중단되는지 검증했다.
- Files:
Phase 3: 라이브 다시듣기 persistence adapter
-
Task 3.1: 라이브 탭 repository 골격과 count 조회 추가
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/CreatorChannelLiveQueryRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
- Create:
- RED: fixture로
다시듣기테마 콘텐츠 2개, 다른 테마 콘텐츠 1개, 예약 공개 콘텐츠 1개, 비활성 콘텐츠 1개를 만들고 count가 공개다시듣기콘텐츠만 세는지 검증한다. - RED: 성인 노출 불가이면 성인
다시듣기콘텐츠가 count에서 제외되는지 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - GREEN:
countLiveReplayAudioContents(creatorId, now, canViewAdultContent)를 구현한다. 조건은 creator, active member, active content, active theme,theme == "다시듣기",duration != null,releaseDate != null,releaseDate <= now, adult filter다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - REFACTOR:
"다시듣기"문자열은 repository companion object의LIVE_REPLAY_THEME상수로 둔다. - 검증 기록(2026-06-17): RED 단계에서
DefaultCreatorChannelLiveQueryRepository미존재 컴파일 실패를 확인했다. GREEN 단계에서 live query repository interface/default 구현체와countLiveReplayAudioContents를 추가하고, 공개다시듣기콘텐츠/성인 노출 정책 count를DefaultCreatorChannelLiveQueryRepositoryTest.shouldCountPublicLiveReplayAudioContentsOnly로 검증했다.
- Files:
-
Task 3.2: 라이브 다시듣기 기본 목록과 페이징 조회 추가
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
- Modify:
- RED:
offset=20, limit=21인자를 전달하면 21개까지만 조회하고, 앞 page 콘텐츠가 제외되는지 검증한다. - RED:
LATEST정렬에서 공개일 최신순, 같은 공개일이면 높은 가격순이 먼저인지 검증한다. - RED:
다시듣기보다 오래된 다른 테마 공개 오디오 콘텐츠가 있으면다시듣기목록 item의isFirstContent가false인지 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - GREEN:
findLiveReplayAudioContents(creatorId, viewerId, now, canViewAdultContent, sort, offset, limit)를 구현한다. 우선LATEST,PRICE_HIGH,PRICE_LOW정렬을 QueryDSL order specifier로 처리한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - REFACTOR: content row 조회, first content id 조회, series summary 조회, order status 조회를 작은 private 함수로 분리한다.
- 검증 기록(2026-06-17):
findLiveReplayAudioContents를 QueryDSLorderBy/offset/limit기반으로 구현하고,LATEST의 공개일/가격 정렬, page offset/limit 적용, first content 및 series summary mapping을shouldFindLiveReplayAudioContentsWithPaginationAndLatestSort로 확인했다.PRICE_HIGH,PRICE_LOW정렬은shouldSortLiveReplayAudioContentsByPrice로 확인했다. - 보완 검증 기록(2026-06-17):
isFirstContent는다시듣기테마 안의 첫 콘텐츠가 아니라 크리에이터의 전체 공개 오디오 콘텐츠 중 첫 콘텐츠 기준이어야 하므로shouldMarkFirstContentByAllPublicAudioContentsNotLiveReplayTheme를 추가했다. RED 단계에서 기존 구현이isFirstContent == true를 반환해 실패하는 것을 확인했고, GREEN 단계에서 first content id 조회 조건에서다시듣기테마 필터를 제거해 기존 홈 API와 같은 전체 공개 오디오 기준으로 보정했다. 이후./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest,./gradlew ktlintCheck,git diff --check성공을 확인했다.
- Files:
-
Task 3.3:
POPULAR정렬 구현- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
- Modify:
- RED: 대여/소장 여부와 관계없이
orders.can합계가 큰 콘텐츠가 먼저 나오고, 같은 매출이면 공개일 최신순이 먼저 나오는 테스트를 작성한다. - RED:
orders.isActive == false주문과orders.point값은 매출 합계에서 제외되는지 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - GREEN: orders left join 또는 집계 subquery로 콘텐츠별 활성 주문의
can합계를 계산하고POPULAR정렬에 반영한다.point는 더하지 않는다. 매출이 없으면 0으로 처리한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - REFACTOR: 인기순 집계가 기본 목록 count를 중복시키지 않도록 count query와 list query를 분리 유지한다.
- 검증 기록(2026-06-17):
POPULAR정렬은 활성 주문의orders.can합계를 left join/group by로 계산하도록 구현했다.orders.point와 비활성 주문이 정렬에 반영되지 않는지shouldSortLiveReplayAudioContentsByPopularCanRevenue로 확인했다.
- Files:
-
Task 3.4:
OWNED정렬과 소장/대여 응답 상태 구현- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
- Modify:
- RED:
OWNED정렬에서 조회자가KEEP주문한 콘텐츠가 먼저 나오고, 나머지는 공개일 최신순으로 정렬되는지 검증한다. - RED: 유효한
RENTAL주문만 있는 콘텐츠는isRented == true,isOwned == false인지 검증한다. - RED:
KEEP과 유효한RENTAL이 모두 있으면isOwned == true,isRented == true인지 검증한다. - RED: 만료된
RENTAL은isRented == false인지 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - GREEN: 조회된 content id 목록 기준으로 주문 상태를 bulk 조회해 record에 채운다.
OWNED정렬은KEEP존재 여부를 1차 기준으로 삼는다. 응답의isOwned,isRented는 각각의 주문 존재 여부를 그대로 반영하고 서로 배타적으로 보정하지 않는다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - REFACTOR: 응답 상태 판정과
OWNED정렬 판정의 의미가 어긋나지 않도록 동일한 유효 주문 조건을 사용한다. - 검증 기록(2026-06-17):
OWNED정렬은 조회자의 활성KEEP주문 존재 여부를 QueryDSL group by 정렬 기준으로 삼고, 응답의isOwned/isRented는 조회된 content id 목록 기준 bulk 조회로 채우도록 구현했다. 유효 대여, 만료 대여, 소장+대여 동시 존재를shouldSortLiveReplayAudioContentsByOwnedAndReturnOrderStates로 확인했다.
- Files:
-
Task 3.5: 현재 라이브 조회 위임 구현
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
- Modify:
- RED: 현재 진행 중인 라이브 조회가 기존 홈 API와 같은 정책으로 성인 노출, 성별 제한, 크리에이터 입장 제한을 반영하는지 대표 케이스를 작성한다.
- 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - GREEN:
DefaultCreatorChannelLiveQueryRepository.findCurrentLive는 기존DefaultCreatorChannelHomeQueryRepository.findCurrentLive와 같은 조건을 사용한다. 코드 중복이 과하면 private helper를 복사하되, 동작 보존을 우선한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest - REFACTOR: 이후 공용 live query helper 추출은 이번 구현 범위에서 제외한다.
- 검증 기록(2026-06-17): 기존 홈 API의 현재 라이브 조건을 live tab repository에 복사해 성인 노출, 성별 제한, 크리에이터 입장 제한, 진행 중 라이브 정렬 정책을 맞췄다.
shouldFindCurrentLiveWithHomePolicy와shouldFindCreatorAndBlockedRelationship으로 current live/creator/block port 계약을 확인했다.
- Files:
Phase 4: Controller와 공개 응답
-
Task 4.1: 라이브 탭 controller endpoint 추가
- Files:
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveController.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacade.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/dto/CreatorChannelLiveTabResponse.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/application/CreatorChannelLiveFacadeTest.kt
- Create:
- RED:
GET /api/v2/creator-channels/1/live가 인증 회원,creatorId, 기본sort=LATEST, 기본page=0, 기본size=20을 service에 전달하는 MockMvc 테스트를 작성한다. - RED: 응답 JSON에
liveReplayContentCount,currentLive,liveReplayContents,sort,page,size,hasNext,liveReplayContents[0].isOwned,liveReplayContents[0].isRented가 존재하는지 검증한다. - RED: anonymous 요청은 기존 홈 API와 같이 unauthorized가 되는지 검증한다.
- 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacadeTest - GREEN:
CreatorChannelLiveController에@GetMapping("/{creatorId}/live")를 추가하고CreatorChannelLiveFacade를 주입한다. query parameter는@RequestParam(defaultValue = "LATEST") sort: ContentSort,@RequestParam(defaultValue = "0") page: Int,@RequestParam(defaultValue = "20") size: Int로 받는다. Facade는CreatorChannelLiveQueryService결과를 공개 API DTO로 변환한다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacadeTest - REFACTOR: 기존
CreatorChannelHomeController에는 라이브 endpoint를 추가하지 않는다. - 검증 기록(2026-06-17): RED 단계에서
CreatorChannelLiveController,CreatorChannelLiveFacade, 라이브 탭 공개 DTO 미존재 컴파일 실패를 확인했다. GREEN 단계에서v2.api.creator.channel.live하위 controller/facade/DTO를 추가하고, 인증 회원 기본 요청이sort=LATEST,page=0,size=20을 facade에 전달하며liveReplayContentCount,currentLive,liveReplayContents,sort,page,size,hasNext,isOwned,isRented응답 필드를 반환하는지./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacadeTest로 확인했다. 비회원 요청은 기존 홈 API와 같은 테스트 보안 설정에서 401로 거부됨을 확인했다.
- Files:
-
Task 4.2: 잘못된 page/size validation 표면 확인
- Files:
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt
- Modify:
- RED:
page=-1또는size=0요청이 기존SodaExceptionHandler오류 표면인 HTTP 200 +success=false로 처리되는지 controller/service 테스트를 추가한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest - GREEN: service에서
CreatorChannelLiveReplayQueryPolicy.validatePage(page, size)를 호출하고 invalid request 예외를 던진다. - 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest - REFACTOR: Spring enum binding 실패(
sort=UNKNOWN)는 별도 custom handling을 추가하지 않고 기존 MVC 오류 흐름을 사용한다. - 검증 기록(2026-06-17): controller 테스트에
page=-1,size=0요청 표면을 추가하고, 기존SodaExceptionHandler흐름에 맞춰 HTTP 200 +success=false응답으로 확인했다. service invalid 요청은 Phase 2에서 port 조회 전common.error.invalid_request로 중단되도록 구현되어 있어./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest로 controller 표면과 service validation 회귀를 함께 확인했다.
- Files:
Phase 5: 회귀 및 문서 동기화
-
Task 5.1: 기존 홈 API 회귀 테스트 보강
- Files:
- Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/in/web/CreatorChannelHomeControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelHomeQueryServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt
- Test:
- RED: 기존 홈 API의
latestAudioContent와audioContents에 새isOwned,isRented필드가 내려오는지 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest - GREEN: Phase 1 구현이 빠뜨린 변환/fixture를 보정한다.
- 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest - REFACTOR: test fixture의
CreatorChannelAudioContent생성부가 반복되면 테스트 내부 helper만 추가하고 production abstraction은 만들지 않는다. - 검증 기록(2026-06-17): 기존 controller/service 테스트의
isOwned/isRented응답/변환 회귀에 더해, 홈 repository 통합 fixture에서latestAudioContent의KEEP주문과audioContents의 유효RENTAL주문 상태를 함께 검증하도록 보강했다../gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest성공을 확인했다.
- Files:
-
Task 5.2: 라이브 탭 통합 시나리오 검증
- Files:
- Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt
- Test:
- RED: 실제 fixture 기반으로 현재 라이브 1개, 라이브 다시듣기 21개, 소장/대여/미구매 콘텐츠를 넣고
page=0,size=20,sort=LATEST응답 표면을 검증한다. - 실패 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest - GREEN: 누락된 mapping, count, hasNext, sort 응답을 보정한다.
- 통과 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest - REFACTOR: 통합 테스트는 하나의 대표 시나리오만 유지하고 세부 정렬/상태 케이스는 repository 단위 테스트로 둔다.
- 검증 기록(2026-06-17): 기존 repository 테스트의 21개 조회/pagination/current live/order state 검증에 더해, controller 응답 표면에서
liveReplayContentCount=21,liveReplayContents.length()==20,hasNext=true,sort=LATEST,page=0,size=20, 소장/대여/미구매 상태가 JSON으로 내려오는 대표 시나리오를 추가했다../gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest성공을 확인했다.
- Files:
-
Task 5.3: 라이브 탭 end-to-end 통합 테스트 추가
- Files:
- Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveEndToEndTest.kt
- Test:
- TDD 예외 사유: production 동작 변경 없이 기존 구현의 controller-service-repository-DB-JSON 연결을 고정하는 회귀 테스트 추가 task다.
- RED:
@SpringBootTest + MockMvc기반으로 인증 회원이GET /api/v2/creator-channels/{creatorId}/live?page=0&size=20&sort=LATEST를 호출하는 실제 end-to-end 테스트를 추가한다. - 검증 대상:
- 현재 라이브 1개가
currentLive로 내려온다. - 공개
다시듣기콘텐츠 21개 중 응답 목록은 20개만 내려온다. liveReplayContentCount=21,hasNext=true,sort=LATEST,page=0,size=20이 내려온다.- 조회자의
KEEP, 유효RENTAL, 미구매 콘텐츠 상태가isOwned/isRentedJSON으로 내려온다. - 이미지 경로는 실제 facade/service mapping을 거쳐 CDN URL로 내려온다.
- 현재 라이브 1개가
- 실행 확인:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveEndToEndTest - GREEN: production 변경 없이 기존 구현이 통과하면 회귀 테스트로 유지한다. 실패하면 실패 원인이 테스트 fixture인지 실제 연결 결함인지 구분해 최소 수정한다.
- REFACTOR: fixture helper는 테스트 파일 내부에만 둔다. 기존 mock 기반 controller 테스트와 repository 세부 테스트는 유지한다.
- 검증 기록(2026-06-17):
CreatorChannelLiveEndToEndTest를 추가해 실제 Spring context에서Controller -> Facade -> Service -> Repository -> DB -> Response JSON흐름을 검증했다. 테스트 fixture는 커밋된 DB 상태를 MockMvc 요청에서 조회하도록TransactionTemplate으로 생성했다../gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveEndToEndTest성공을 확인했다. 최초 성공 실행에서 H2 shutdown 경고가 있어 테스트 전용 datasource URL에DB_CLOSE_ON_EXIT=FALSE를 추가했고, 동일 명령 재실행 성공을 확인했다.
- Files:
-
Task 5.4: 전체 회귀 검증과 문서 검증 기록 추가
- Files:
- Modify:
docs/20260617_크리에이터_채널_라이브_API/plan-task.md
- Modify:
- TDD 예외 사유: 문서 검증 기록 갱신 task로 production/test 코드 변경이 없다.
- 대체 검증 방법: 아래 명령 실행 결과를 이 task 아래와 문서 하단 검증 기록에 누적한다.
- 실행 명령:
./gradlew test --tests kr.co.vividnext.sodalive.v2.common.domain.ContentSortTest./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveEndToEndTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacadeTest./gradlew ktlintCheck
- 기대 결과: 모든 명령이 성공한다.
- REFACTOR: 실패가 발생하면 실패 task로 돌아가 문서의 체크박스를 완료 처리하지 않는다.
- 검증 기록(2026-06-17): Phase 5 최종 회귀로 아래 명령이 모두 성공함을 확인했다.
./gradlew test --tests kr.co.vividnext.sodalive.v2.common.domain.ContentSortTest./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveEndToEndTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacadeTest./gradlew ktlintCheckgit diff --check
- Files:
Phase 6: 다음 범위 홈 API 구조 정렬 인계
- Task 6.1: 크리에이터 채널 홈 API 리팩토링 후속 프롬프트 보존
- Files:
- Modify:
docs/20260617_크리에이터_채널_라이브_API/plan-task.md
- Modify:
- TDD 예외 사유: 다음 범위 작업을 위한 인계 프롬프트 작성 task로 production/test 코드 변경이 없다.
- 대체 검증 방법:
- 문서 내 프롬프트가 이번 라이브 탭 구현을 다시 수정하라고 지시하지 않는지 확인한다.
- 프롬프트가 기존 홈 API endpoint와 공개 응답 계약 보존, 테스트 선행, 패키지 의존 방향을 명시하는지 확인한다.
- 후속 작업용 GPT-5.5 프롬프트:
- Files:
너는 /Users/klaus/Develop/sodalive/Server/sodalive 저장소에서 작업하는 GPT-5.5 기반 코딩 에이전트다.
목표:
기존 크리에이터 채널 홈 API 구현을 현재 v2 공개 API 설계와 맞게 `v2.api.*` 조립 계층 + API 패키지 밖 도메인 패키지 구조로 정렬한다.
반드시 지킬 규칙:
- 사용자와 저장소의 AGENTS.md, `docs/agent-guides/코드스타일.md`, `docs/agent-guides/테스트스타일.md`, `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`를 먼저 읽고 따른다.
- 구현 전 기존 PRD/plan-task 문서를 확인하고, 이 작업이 새 범위라면 `docs/[날짜]_크리에이터_채널_홈_API_구조정렬/prd.md`, `docs/[날짜]_크리에이터_채널_홈_API_구조정렬/plan-task.md`를 작성한다.
- 기존 공개 endpoint `GET /api/v2/creator-channels/{creatorId}/home`과 응답 필드명/의미를 변경하지 않는다.
- 리팩토링 목적은 파일 위치와 책임 경계 정렬이다. 기능 추가, 응답 스키마 확장, 불필요한 공용화는 하지 않는다.
- 클라이언트 공개 API controller/facade/response DTO는 `kr.co.vividnext.sodalive.v2.api.creator.channel.home` 하위로 이동한다.
- 재사용 가능한 조회/정책/port/repository는 `kr.co.vividnext.sodalive.v2.creator.channel.home` 또는 더 적합한 도메인 패키지 하위에 둔다. 도메인 패키지는 `kr.co.vividnext.sodalive.v2.api.*`에 의존하지 않는다.
- 의존 방향은 항상 `v2.api.creator.channel.home -> 도메인 패키지`로 유지한다.
- 기존 `v2.creator.channel.adapter.in.web.CreatorChannelHomeController`를 새 API 조립 계층으로 옮길 때 Spring mapping 충돌이 생기지 않도록 기존 controller 제거/이동 범위를 명확히 한다.
- 테스트는 먼저 실패하도록 작성하거나 이동한 뒤 실패를 확인하고, 최소 구현으로 통과시킨다.
- 기존 홈 API 회귀 테스트를 유지한다. 최소 검증 대상은 controller, facade 또는 service, repository 단위 테스트와 `./gradlew ktlintCheck`다.
- 이번 라이브 탭 API 구현(`v2.api.creator.channel.live`, `v2.creator.channel.live`)은 리팩토링 대상이 아니다. 필요한 경우 import 관계 확인만 하고 동작 변경은 하지 않는다.
권장 진행 순서:
1. 기존 홈 API 파일과 테스트를 모두 찾고 현재 public contract를 문서화한다.
2. 새 PRD에 “동작 보존 리팩토링” 범위와 non-goal을 명시한다.
3. plan-task에 TDD 기준으로 파일 이동, controller/facade 분리, domain package 정렬, 회귀 검증 task를 작성한다.
4. Controller/DTO를 `v2.api.creator.channel.home`으로 이동하고, 기존 service/domain/port/repository는 API 패키지 밖에 유지하거나 `v2.creator.channel.home` 하위로 정렬한다.
5. `rg -n "kr\\.co\\.vividnext\\.sodalive\\.v2\\.api" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator src/main/kotlin/kr/co/vividnext/sodalive/v2/live src/main/kotlin/kr/co/vividnext/sodalive/v2/content src/main/kotlin/kr/co/vividnext/sodalive/v2/series`로 도메인 패키지가 API 패키지를 import하지 않는지 확인한다.
6. `GET /api/v2/creator-channels/{creatorId}/home` 회귀 테스트와 관련 단위 테스트를 실행하고, 검증 결과를 plan-task에 기록한다.
성공 기준:
- 홈 API endpoint와 응답 계약이 유지된다.
- 홈 API 공개 조립 계층은 `v2.api.creator.channel.home`에 있다.
- 도메인 패키지는 `v2.api.*`에 의존하지 않는다.
- 관련 테스트와 ktlint 검증 결과가 plan-task에 기록되어 있다.
3. 구현 순서 요약
ContentSort공용 enum을 먼저 추가한다.- 기존 완료 범위인
CreatorChannelAudioContentResponse와 domain/record의isOwned,isRented확장 상태를 유지한다. - 라이브 탭 page 정책과 service 골격을
v2.creator.channel.live하위에 만든다. - 라이브 다시듣기 count/list repository를
v2.creator.channel.live하위에 구현한다. - controller/facade/응답 DTO를
v2.api.creator.channel.live하위에 연결한다. - 기존 홈 API 회귀와 라이브 탭 통합 시나리오를 검증한다.
- 다음 범위에서 홈 API 구조 정렬을 진행할 수 있도록 Phase 6 프롬프트를 보존한다.
4. 검증 기록
- 아직 구현 전 계획 문서 작성 단계다. 구현 task 완료 후 각 task 아래에 무엇을, 왜, 어떻게 검증했는지 실행 명령과 결과를 누적 기록한다.
- 2026-06-17 문서 갱신 검증: 라이브 탭 신규 구현을
v2.api.creator.channel.live조립 계층과v2.creator.channel.live도메인 조회 계층으로 진행하도록 PRD/plan-task를 갱신했다. 기존 홈 API 구조 정렬은 Phase 6 후속 프롬프트로 분리했다.git diff --check로 whitespace 오류가 없음을 확인했고,./gradlew tasks --all로 Gradle 명령 유효성 확인에 성공했다. - 2026-06-17 Phase 2 검증:
CreatorChannelLiveReplayQueryPolicyTest와CreatorChannelLiveQueryServiceTest를 먼저 추가해 RED 단계에서 Phase 2 production 클래스 미존재 컴파일 실패를 확인했다. GREEN 단계에서 라이브 탭 domain/page 정책, port, service 조립을 추가하고./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest,./gradlew ktlintCheck,git diff --check성공을 확인했다. Phase 3 port 구현 전 Spring context가 깨지지 않도록 service는ObjectProvider<CreatorChannelLiveQueryPort>로 port를 지연 조회하게 했고,./gradlew test --tests kr.co.vividnext.sodalive.support.SpringBootIntegrationSampleTest성공으로 대표 context 기동을 확인했다. 추가 리뷰 반영으로page >= 0,20 <= size <= 50검증과 invalid 요청 시 port 조회 전 중단을 보강했고, 동일 targeted 테스트와ktlintCheck,git diff --check성공을 확인했다. 전체./gradlew test는 실행 중 기존 SpringBoot 통합 테스트들의 bean/OOM 계열 실패와 timeout이 발생해 완료하지 못했으며, Phase 2 직접 변경 범위는 위 targeted/context 검증으로 확인했다. - 2026-06-17 Phase 3 검증:
DefaultCreatorChannelLiveQueryRepositoryTest를 먼저 추가해 RED 단계에서DefaultCreatorChannelLiveQueryRepository미존재 컴파일 실패를 확인했다. GREEN 단계에서 live tab repository interface/default 구현체를 추가하고 count/list/pagination/sort/order state/current live policy를 구현했다../gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest,./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest --tests kr.co.vividnext.sodalive.support.SpringBootIntegrationSampleTest,./gradlew ktlintCheck,git diff --check성공을 확인했다. 리뷰 게이트에서 Phase 3 persistence adapter 범위와 시나리오 검증에 대한 unconditional approval을 받았다. - 2026-06-17 Phase 3 리뷰 보완 검증:
isFirstContent의미를 PRD에 명시하고, 라이브 다시듣기 repository 테스트에 다른 테마의 더 오래된 공개 오디오가 있을 때다시듣기item의isFirstContent가false인지 확인하는 회귀 테스트를 추가했다. RED 단계에서 기존 구현이 실패하는 것을 확인했고, first content id 조회를 기존 홈 API와 같은 전체 공개 오디오 기준으로 수정했다. 검증은./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest,./gradlew ktlintCheck,./gradlew tasks --all,git diff --check로 수행했고 모두 성공했다. 검증 중 Gradle 테스트 2개를 병렬 실행했을 때 한 세션에서 QueryDSL generated source compile race로compileJava가 실패했으나, 단일 세션으로 재실행한 repository 전체 테스트는 성공했다. - 2026-06-17 Phase 4 검증: 라이브 탭 공개 API 조립 계층을
v2.api.creator.channel.live하위에 추가했다. RED 단계에서 controller/facade/DTO 미존재 컴파일 실패를 확인했고, GREEN 단계에서CreatorChannelLiveControllerTest,CreatorChannelLiveFacadeTest통과를 확인했다. invalidpage/size요청은 기존 오류 응답 표면인 HTTP 200 +success=false로 확인했고,CreatorChannelLiveQueryServiceTest와 함께 service validation 회귀를 확인했다. 검증은./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorChannelLiveFacadeTest,./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest,./gradlew ktlintCheck,git diff --check로 수행했고 모두 성공했다. - 2026-06-17 Phase 5 검증: 기존 홈 API 회귀와 라이브 탭 대표 응답 표면을 보강했다. 홈 repository 통합 fixture는
latestAudioContent.isOwned/isRented와audioContents.isOwned/isRented를 주문 상태 기반으로 검증하고, 라이브 탭 controller는 현재 라이브 1개, 다시듣기 20개 응답, 전체 count 21,hasNext=true, 소장/대여/미구매 상태를 확인한다. 추가로CreatorChannelLiveEndToEndTest를 만들어 실제 Spring context에서Controller -> Facade -> Service -> Repository -> DB -> Response JSON흐름을 검증했다. Phase 5 지정 테스트와./gradlew ktlintCheck,git diff --check가 모두 성공했다.