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

34 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 경계(kr.co.vividnext.sodalive.v2.creator.channel)를 유지하되, 라이브 탭 조회 책임은 별도 service/port/repository로 분리한다. 공용 콘텐츠 정렬 enum은 채널 전용 이름을 피하고 kr.co.vividnext.sodalive.v2.common.domain.ContentSort로 둔다. 기존 CreatorChannelAudioContentResponseisOwned, isRented를 추가해 라이브 다시듣기와 다음 범위의 오디오 콘텐츠 조회 API가 같은 응답 DTO를 재사용한다.

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
    • liveReplayContents: 기존 CreatorChannelAudioContentResponse
    • sort: 실제 적용한 ContentSort
    • page: 이번 요청에 적용된 page index
    • size: 이번 요청에 적용된 page size
    • hasNext: 다음 page 존재 여부
  • CreatorChannelAudioContentResponse에는 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, random
    • POPULAR: 구매 매출 합계 desc, releaseDate desc, random
    • OWNED: 조회자 소장 여부 desc, releaseDate desc, random
    • PRICE_HIGH: price desc, releaseDate desc, random
    • PRICE_LOW: price asc, releaseDate desc, random
  • 인기순 매출은 대여/소장 여부와 관계없이 순수하게 결제된 캔 매출인 orders.can 합계를 사용한다. orders.point는 포함하지 않고, orders.is_active = true인 주문만 포함한다. 환불/비활성 주문은 제외한다.
  • page/size validation은 service에서 명시적으로 수행한다. page < 0 또는 size < 1이면 기존 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 확장

  • 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

라이브 탭 신규 application/domain/port/repository/controller

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/in/web/CreatorChannelHomeController.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelLiveQueryService.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelLiveTab.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelPage.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelLiveReplayQueryPolicy.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/dto/CreatorChannelHomeResponse.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/port/out/CreatorChannelLiveQueryPort.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/CreatorChannelLiveQueryRepository.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelLiveReplayQueryPolicyTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelLiveQueryServiceTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/in/web/CreatorChannelHomeControllerTest.kt

문서 산출물

  • Modify: docs/20260617_크리에이터_채널_라이브_API/plan-task.md

2. Response data class 초안

구현 시 src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/dto/CreatorChannelHomeResponse.kt에 아래 DTO를 기준으로 추가/수정한다. 필드명은 공개 API 계약이므로 변경이 필요하면 먼저 PRD와 이 문서를 갱신한다.

package kr.co.vividnext.sodalive.v2.creator.channel.dto

import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.creator.channel.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
)

private fun LocalDateTime.toUtcIso(): String {
    return atOffset(ZoneOffset.UTC).toInstant().toString()
}

위 예시는 새/수정 필드만 보여준다. 기존 CreatorChannelHomeResponse, CreatorChannelCreatorResponse, CreatorChannelLiveResponse 등은 유지한다.


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/domain/CreatorChannelLiveTab.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelPage.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelLiveReplayQueryPolicy.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelLiveReplayQueryPolicyTest.kt
    • RED: page=0,size=20이면 offset 0, fetch limit 21, 응답 items limit 20, hasNext == true 판정이 되는 테스트를 작성한다.
    • RED: page < 0, size < 1이면 정책이 예외를 던지는 테스트를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelLiveReplayQueryPolicyTest
    • GREEN: CreatorChannelPage(page: Int, size: Int)CreatorChannelLiveReplayQueryPolicy를 추가한다. offset = page * size, fetchLimit = size + 1, items = fetched.take(size), hasNext = fetched.size > size를 제공한다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelLiveReplayQueryPolicyTest
    • REFACTOR: page/size 계산은 service나 repository에 중복 구현하지 않는다.
  • Task 2.2: CreatorChannelLiveQueryService 골격 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelLiveQueryService.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/port/out/CreatorChannelLiveQueryPort.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/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.application.CreatorChannelLiveQueryServiceTest
    • GREEN: 기존 CreatorChannelHomeQueryService의 인증 회원 기준 정책을 따라 creator 검증, 차단 검증, adult visibility, effective gender 계산을 구현한다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest
    • REFACTOR: CreatorChannelHomeQueryService와 중복되는 private 변환/검증 코드가 과도해지면 같은 phase 안에서는 복사 유지하고, 추후 별도 공용 validator 추출은 구현 범위 밖으로 둔다.
  • Task 2.3: 라이브 탭 service 응답 조립 완성

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelLiveQueryService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelLiveTab.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelLiveQueryServiceTest.kt
    • RED: fake port가 size + 1개 콘텐츠를 반환하면 service 응답의 liveReplayContents.size == size, hasNext == true, page == 0, size == 20, sort == LATEST인지 검증한다.
    • RED: page 범위에 콘텐츠가 없으면 liveReplayContents는 빈 배열이고 count는 port count 값을 유지하는지 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest
    • GREEN: service에서 policy로 page를 검증하고, count와 size + 1 조회 결과를 조립해 CreatorChannelLiveTab을 반환한다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest
    • REFACTOR: ContentSort 기본값은 controller가 기본값을 넣되, service 테스트에서도 명시 인자를 사용해 정책 위치를 분리한다.

Phase 3: 라이브 다시듣기 persistence adapter

  • Task 3.1: 라이브 탭 repository 골격과 count 조회 추가

    • Files:
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/CreatorChannelLiveQueryRepository.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
    • RED: fixture로 다시듣기 테마 콘텐츠 2개, 다른 테마 콘텐츠 1개, 예약 공개 콘텐츠 1개, 비활성 콘텐츠 1개를 만들고 count가 공개 다시듣기 콘텐츠만 세는지 검증한다.
    • RED: 성인 노출 불가이면 성인 다시듣기 콘텐츠가 count에서 제외되는지 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
    • REFACTOR: "다시듣기" 문자열은 repository companion object의 LIVE_REPLAY_THEME 상수로 둔다.
  • Task 3.2: 라이브 다시듣기 기본 목록과 페이징 조회 추가

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
    • RED: offset=20, limit=21 인자를 전달하면 21개까지만 조회하고, 앞 page 콘텐츠가 제외되는지 검증한다.
    • RED: LATEST 정렬에서 공개일 최신순, 같은 공개일이면 높은 가격순이 먼저인지 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
    • REFACTOR: content row 조회, first content id 조회, series summary 조회, order status 조회를 작은 private 함수로 분리한다.
  • Task 3.3: POPULAR 정렬 구현

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
    • RED: 대여/소장 여부와 관계없이 orders.can 합계가 큰 콘텐츠가 먼저 나오고, 같은 매출이면 공개일 최신순이 먼저 나오는 테스트를 작성한다.
    • RED: orders.isActive == false 주문과 orders.point 값은 매출 합계에서 제외되는지 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
    • GREEN: orders left join 또는 집계 subquery로 콘텐츠별 활성 주문의 can 합계를 계산하고 POPULAR 정렬에 반영한다. point는 더하지 않는다. 매출이 없으면 0으로 처리한다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
    • REFACTOR: 인기순 집계가 기본 목록 count를 중복시키지 않도록 count query와 list query를 분리 유지한다.
  • Task 3.4: OWNED 정렬과 소장/대여 응답 상태 구현

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
    • GREEN: 조회된 content id 목록 기준으로 주문 상태를 bulk 조회해 record에 채운다. OWNED 정렬은 KEEP 존재 여부를 1차 기준으로 삼는다. 응답의 isOwned, isRented는 각각의 주문 존재 여부를 그대로 반영하고 서로 배타적으로 보정하지 않는다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
    • REFACTOR: 응답 상태 판정과 OWNED 정렬 판정의 의미가 어긋나지 않도록 동일한 유효 주문 조건을 사용한다.
  • Task 3.5: 현재 라이브 조회 위임 구현

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
    • RED: 현재 진행 중인 라이브 조회가 기존 홈 API와 같은 정책으로 성인 노출, 성별 제한, 크리에이터 입장 제한을 반영하는지 대표 케이스를 작성한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
    • GREEN: DefaultCreatorChannelLiveQueryRepository.findCurrentLive는 기존 DefaultCreatorChannelHomeQueryRepository.findCurrentLive와 같은 조건을 사용한다. 코드 중복이 과하면 private helper를 복사하되, 동작 보존을 우선한다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
    • REFACTOR: 이후 공용 live query helper 추출은 이번 구현 범위에서 제외한다.

Phase 4: Controller와 공개 응답

  • Task 4.1: 라이브 탭 controller endpoint 추가

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/in/web/CreatorChannelHomeController.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/dto/CreatorChannelHomeResponse.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/in/web/CreatorChannelHomeControllerTest.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.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest
    • GREEN: 같은 controller에 @GetMapping("/{creatorId}/live")를 추가하고 CreatorChannelLiveQueryService를 주입한다. query parameter는 @RequestParam(defaultValue = "LATEST") sort: ContentSort, @RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "20") size: Int로 받는다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest
    • REFACTOR: controller 이름은 기존 CreatorChannelHomeController를 유지하되, 추후 채널 탭 API가 늘면 CreatorChannelController로 분리할지 별도 작업으로 판단한다.
  • Task 4.2: 잘못된 page/size validation 표면 확인

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelLiveQueryService.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/CreatorChannelLiveQueryServiceTest.kt
    • RED: page=-1 또는 size=0 요청이 400 계열 오류로 처리되는지 controller/service 테스트를 추가한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest
    • GREEN: service에서 CreatorChannelLiveReplayQueryPolicy.validatePage(page, size)를 호출하고 invalid request 예외를 던진다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest
    • REFACTOR: Spring enum binding 실패(sort=UNKNOWN)는 별도 custom handling을 추가하지 않고 기존 MVC 오류 흐름을 사용한다.

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은 만들지 않는다.
  • Task 5.2: 라이브 탭 통합 시나리오 검증

    • Files:
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepositoryTest.kt
      • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/in/web/CreatorChannelHomeControllerTest.kt
    • RED: 실제 fixture 기반으로 현재 라이브 1개, 라이브 다시듣기 21개, 소장/대여/미구매 콘텐츠를 넣고 page=0,size=20,sort=LATEST 응답 표면을 검증한다.
    • 실패 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest
    • GREEN: 누락된 mapping, count, hasNext, sort 응답을 보정한다.
    • 통과 확인: ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest
    • REFACTOR: 통합 테스트는 하나의 대표 시나리오만 유지하고 세부 정렬/상태 케이스는 repository 단위 테스트로 둔다.
  • Task 5.3: 전체 회귀 검증과 문서 검증 기록 추가

    • 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.domain.CreatorChannelLiveReplayQueryPolicyTest
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest
      • ./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest
      • ./gradlew ktlintCheck
    • 기대 결과: 모든 명령이 성공한다.
    • REFACTOR: 실패가 발생하면 실패 task로 돌아가 문서의 체크박스를 완료 처리하지 않는다.

3. 구현 순서 요약

  1. ContentSort 공용 enum을 먼저 추가한다.
  2. 기존 CreatorChannelAudioContentResponse와 domain/record에 isOwned, isRented를 추가해 홈 API 컴파일/테스트를 먼저 복구한다.
  3. 라이브 탭 page 정책과 service 골격을 만든다.
  4. 라이브 다시듣기 count/list repository를 구현한다.
  5. controller endpoint와 응답 DTO를 연결한다.
  6. 기존 홈 API 회귀와 라이브 탭 통합 시나리오를 검증한다.

4. 검증 기록

  • 아직 구현 전 계획 문서 작성 단계다. 구현 task 완료 후 각 task 아래에 무엇을, 왜, 어떻게 검증했는지 실행 명령과 결과를 누적 기록한다.