Files
sodalive-backend-spring-boot/docs/20260617_크리에이터_채널_라이브_API/plan-task.md

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
  • response:
    • liveReplayContentCount: 같은 필터를 적용한 라이브 다시듣기 콘텐츠 전체 개수
    • currentLive: 기존 CreatorChannelLiveResponse와 같은 필드/의미를 가진 라이브 탭 API 응답 DTO
    • liveReplayContents: 기존 CreatorChannelAudioContentResponse와 같은 필드/의미에 isOwned, isRented를 포함한 라이브 탭 API 응답 DTO
    • sort: 실제 적용한 ContentSort
    • page: 이번 요청에 적용된 page index
    • size: 이번 요청에 적용된 page size
    • hasNext: 다음 page 존재 여부
  • 라이브 탭 API 응답의 오디오 콘텐츠 item에는 isOwned, isRented를 포함한다.
  • isOwned/isRented 판정은 주문 row를 각각 확인한다. 유효한 KEEP 주문이 있으면 isOwned == true, 유효한 RENTAL 주문이 있으면 isRented == true다.
  • isOwned == trueisRented == 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 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인 주문만 포함한다. 환불/비활성 주문은 제외한다.
  • 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: 공용 ContentSort enum 추가

    • 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
    • RED: ContentSortTest를 먼저 추가해 LATEST, POPULAR, OWNED, PRICE_HIGH, PRICE_LOW 값이 존재하는지 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.common.domain.ContentSortTest
    • GREEN: ContentSort enum을 추가한다.
    • 통과 확인: ./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 단계에서 ContentSort enum 추가 후 ./gradlew test --tests kr.co.vividnext.sodalive.v2.common.domain.ContentSortTest 성공을 확인했다.
  • 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
    • RED: controller 테스트에서 latestAudioContent.isOwned, latestAudioContent.isRented, audioContents[0].isOwned, audioContents[0].isRented JSON 필드를 기대하도록 추가한다.
    • RED: service 테스트에서 CreatorChannelAudioContentRecordCreatorChannelAudioContent 변환 시 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 성공을 확인했다.
  • 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
    • 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, findAudioContentsviewerId를 전달하고, 조회된 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 성공을 확인했다.

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
    • RED: page=0,size=20이면 offset 0, fetch limit 21, 응답 items limit 20, 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 이하로 검증해 fetchLimit overflow를 방지한다.
    • 통과 확인: ./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_VALUEcommon.error.invalid_request를 던지고 size = 50fetchLimit이 51인지 검증했다.
  • 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
    • 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 단계에서 CreatorChannelLiveQueryServiceCreatorChannelLiveQueryPort 미존재 컴파일 실패를 확인했다. GREEN 단계에서 홈 API 서비스의 creator 검증, 차단 검증, adult visibility, effective gender 전달 패턴을 반영하고 ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest 성공을 확인했다.
  • 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
    • 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로 확인했다. 추가 리뷰 반영으로 invalid page/size 요청이 ObjectProvider.getObject()보다 먼저 common.error.invalid_request로 중단되는지 검증했다.

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
    • 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로 검증했다.
  • 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
    • RED: offset=20, limit=21 인자를 전달하면 21개까지만 조회하고, 앞 page 콘텐츠가 제외되는지 검증한다.
    • RED: LATEST 정렬에서 공개일 최신순, 같은 공개일이면 높은 가격순이 먼저인지 검증한다.
    • RED: 다시듣기보다 오래된 다른 테마 공개 오디오 콘텐츠가 있으면 다시듣기 목록 item의 isFirstContentfalse인지 검증한다.
    • 실패 확인: ./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를 QueryDSL orderBy/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 성공을 확인했다.
  • 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
    • 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로 확인했다.
  • 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
    • RED: OWNED 정렬에서 조회자가 KEEP 주문한 콘텐츠가 먼저 나오고, 나머지는 공개일 최신순으로 정렬되는지 검증한다.
    • RED: 유효한 RENTAL 주문만 있는 콘텐츠는 isRented == true, isOwned == false인지 검증한다.
    • RED: KEEP과 유효한 RENTAL이 모두 있으면 isOwned == true, isRented == true인지 검증한다.
    • RED: 만료된 RENTALisRented == 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로 확인했다.
  • 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
    • 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에 복사해 성인 노출, 성별 제한, 크리에이터 입장 제한, 진행 중 라이브 정렬 정책을 맞췄다. shouldFindCurrentLiveWithHomePolicyshouldFindCreatorAndBlockedRelationship으로 current live/creator/block port 계약을 확인했다.

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
    • 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로 거부됨을 확인했다.
  • 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
    • 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 회귀를 함께 확인했다.

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
    • RED: 기존 홈 API의 latestAudioContentaudioContents에 새 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에서 latestAudioContentKEEP 주문과 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 성공을 확인했다.
  • 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
    • 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 성공을 확인했다.
  • 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
    • 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/isRented JSON으로 내려온다.
      • 이미지 경로는 실제 facade/service mapping을 거쳐 CDN URL로 내려온다.
    • 실행 확인: ./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를 추가했고, 동일 명령 재실행 성공을 확인했다.
  • Task 5.4: 전체 회귀 검증과 문서 검증 기록 추가

    • Files:
      • Modify: docs/20260617_크리에이터_채널_라이브_API/plan-task.md
    • 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 ktlintCheck
      • git diff --check

Phase 6: 다음 범위 홈 API 구조 정렬 인계

  • Task 6.1: 크리에이터 채널 홈 API 리팩토링 후속 프롬프트 보존
    • Files:
      • Modify: docs/20260617_크리에이터_채널_라이브_API/plan-task.md
    • TDD 예외 사유: 다음 범위 작업을 위한 인계 프롬프트 작성 task로 production/test 코드 변경이 없다.
    • 대체 검증 방법:
      • 문서 내 프롬프트가 이번 라이브 탭 구현을 다시 수정하라고 지시하지 않는지 확인한다.
      • 프롬프트가 기존 홈 API endpoint와 공개 응답 계약 보존, 테스트 선행, 패키지 의존 방향을 명시하는지 확인한다.
    • 후속 작업용 GPT-5.5 프롬프트:
너는 /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. 구현 순서 요약

  1. ContentSort 공용 enum을 먼저 추가한다.
  2. 기존 완료 범위인 CreatorChannelAudioContentResponse와 domain/record의 isOwned, isRented 확장 상태를 유지한다.
  3. 라이브 탭 page 정책과 service 골격을 v2.creator.channel.live 하위에 만든다.
  4. 라이브 다시듣기 count/list repository를 v2.creator.channel.live 하위에 구현한다.
  5. controller/facade/응답 DTO를 v2.api.creator.channel.live 하위에 연결한다.
  6. 기존 홈 API 회귀와 라이브 탭 통합 시나리오를 검증한다.
  7. 다음 범위에서 홈 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 검증: CreatorChannelLiveReplayQueryPolicyTestCreatorChannelLiveQueryServiceTest를 먼저 추가해 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의 isFirstContentfalse인지 확인하는 회귀 테스트를 추가했다. 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 통과를 확인했다. invalid page/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/isRentedaudioContents.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가 모두 성공했다.