From 2ea030e0d6c9d4ab868c1ed98519854a7513cec9 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 17 Jun 2026 16:37:35 +0900 Subject: [PATCH] =?UTF-8?q?docs(creator):=20=EC=B1=84=EB=84=90=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=20API=20=EA=B5=AC=EC=A1=B0=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=EC=9D=84=20=EA=B0=B1=EC=8B=A0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 270 ++++++++++++------ .../prd.md | 19 +- 2 files changed, 200 insertions(+), 89 deletions(-) diff --git a/docs/20260617_크리에이터_채널_라이브_API/plan-task.md b/docs/20260617_크리에이터_채널_라이브_API/plan-task.md index 4893ce48..c8d923fb 100644 --- a/docs/20260617_크리에이터_채널_라이브_API/plan-task.md +++ b/docs/20260617_크리에이터_채널_라이브_API/plan-task.md @@ -4,7 +4,7 @@ **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`로 둔다. 기존 `CreatorChannelAudioContentResponse`에 `isOwned`, `isRented`를 추가해 라이브 다시듣기와 다음 범위의 오디오 콘텐츠 조회 API가 같은 응답 DTO를 재사용한다. +**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 @@ -21,13 +21,13 @@ - query parameter: `size`, 기본값 `20` - response: - `liveReplayContentCount`: 같은 필터를 적용한 라이브 다시듣기 콘텐츠 전체 개수 - - `currentLive`: 기존 `CreatorChannelLiveResponse` - - `liveReplayContents`: 기존 `CreatorChannelAudioContentResponse` + - `currentLive`: 기존 `CreatorChannelLiveResponse`와 같은 필드/의미를 가진 라이브 탭 API 응답 DTO + - `liveReplayContents`: 기존 `CreatorChannelAudioContentResponse`와 같은 필드/의미에 `isOwned`, `isRented`를 포함한 라이브 탭 API 응답 DTO - `sort`: 실제 적용한 `ContentSort` - `page`: 이번 요청에 적용된 page index - `size`: 이번 요청에 적용된 page size - `hasNext`: 다음 page 존재 여부 -- `CreatorChannelAudioContentResponse`에는 `isOwned`, `isRented`를 추가한다. +- 라이브 탭 API 응답의 오디오 콘텐츠 item에는 `isOwned`, `isRented`를 포함한다. - `isOwned`/`isRented` 판정은 주문 row를 각각 확인한다. 유효한 `KEEP` 주문이 있으면 `isOwned == true`, 유효한 `RENTAL` 주문이 있으면 `isRented == true`다. - `isOwned == true`와 `isRented == true`가 동시에 발생할 가능성은 없지만, 데이터상 동시에 유효한 소장/대여 주문이 있으면 둘 다 `true`로 내려준다. - 라이브 다시듣기 콘텐츠 기준: `AudioContentTheme.theme == "다시듣기"`이고 `AudioContentTheme.isActive == true`인 공개 오디오 콘텐츠. @@ -52,6 +52,8 @@ - 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` @@ -61,20 +63,24 @@ - 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` +### 라이브 탭 신규 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` @@ -83,14 +89,16 @@ ## 2. Response data class 초안 -구현 시 `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/dto/CreatorChannelHomeResponse.kt`에 아래 DTO를 기준으로 추가/수정한다. 필드명은 공개 API 계약이므로 변경이 필요하면 먼저 PRD와 이 문서를 갱신한다. +구현 시 `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/live/dto/CreatorChannelLiveTabResponse.kt`에 아래 DTO를 기준으로 추가한다. 필드명은 공개 API 계약이므로 변경이 필요하면 먼저 PRD와 이 문서를 갱신한다. ```kotlin -package kr.co.vividnext.sodalive.v2.creator.channel.dto +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.domain.CreatorChannelLiveTab +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 @@ -138,14 +146,56 @@ data class CreatorChannelAudioContentResponse( 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() } ``` -> 위 예시는 새/수정 필드만 보여준다. 기존 `CreatorChannelHomeResponse`, `CreatorChannelCreatorResponse`, `CreatorChannelLiveResponse` 등은 유지한다. +> 위 예시는 라이브 탭 공개 API 응답 DTO 기준이다. 기존 `CreatorChannelHomeResponse` 파일은 이번 라이브 탭 구조 정렬 작업에서 이동하지 않는다. --- @@ -198,39 +248,39 @@ private fun LocalDateTime.toUtcIso(): String { - [ ] **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` + - 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 < 1`이면 정책이 예외를 던지는 테스트를 작성한다. - - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelLiveReplayQueryPolicyTest` + - 실패 확인: `./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`를 제공한다. - - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelLiveReplayQueryPolicyTest` + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.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` + - 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.application.CreatorChannelLiveQueryServiceTest` + - 실패 확인: `./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.application.CreatorChannelLiveQueryServiceTest` + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.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` + - 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: `page` 범위에 콘텐츠가 없으면 `liveReplayContents`는 빈 배열이고 count는 port count 값을 유지하는지 검증한다. - - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest` + - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest` - GREEN: service에서 policy로 page를 검증하고, count와 `size + 1` 조회 결과를 조립해 `CreatorChannelLiveTab`을 반환한다. - - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest` + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.application.CreatorChannelLiveQueryServiceTest` - REFACTOR: `ContentSort` 기본값은 controller가 기본값을 넣되, service 테스트에서도 명시 인자를 사용해 정책 위치를 분리한다. --- @@ -239,59 +289,59 @@ private fun LocalDateTime.toUtcIso(): String { - [ ] **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` + - 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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 실패 확인: `./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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.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` + - 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` 정렬에서 공개일 최신순, 같은 공개일이면 높은 가격순이 먼저인지 검증한다. - - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 실패 확인: `./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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 통과 확인: `./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 함수로 분리한다. - [ ] **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` + - 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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 실패 확인: `./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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.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` + - 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: 만료된 `RENTAL`은 `isRented == false`인지 검증한다. - - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 실패 확인: `./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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.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` + - 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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 실패 확인: `./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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.live.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest` - REFACTOR: 이후 공용 live query helper 추출은 이번 구현 범위에서 제외한다. --- @@ -300,26 +350,28 @@ private fun LocalDateTime.toUtcIso(): String { - [ ] **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` + - 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.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`로 분리할지 별도 작업으로 판단한다. + - 실패 확인: `./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를 추가하지 않는다. - [ ] **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` + - 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` 요청이 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` + - 실패 확인: `./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.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelLiveQueryServiceTest` + - 통과 확인: `./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 오류 흐름을 사용한다. --- @@ -339,12 +391,12 @@ private fun LocalDateTime.toUtcIso(): String { - [ ] **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` + - 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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest` + - 실패 확인: `./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.adapter.out.persistence.DefaultCreatorChannelLiveQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest` + - 통과 확인: `./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 단위 테스트로 둔다. - [ ] **Task 5.3: 전체 회귀 검증과 문서 검증 기록 추가** @@ -354,27 +406,77 @@ private fun LocalDateTime.toUtcIso(): String { - 대체 검증 방법: 아래 명령 실행 결과를 이 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 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.application.CreatorChannelLiveFacadeTest` - `./gradlew ktlintCheck` - 기대 결과: 모든 명령이 성공한다. - REFACTOR: 실패가 발생하면 실패 task로 돌아가 문서의 체크박스를 완료 처리하지 않는다. --- +### Phase 6: 다음 범위 홈 API 구조 정렬 인계 + +- [ ] **Task 6.1: 크리에이터 채널 홈 API 리팩토링 후속 프롬프트 보존** + - Files: + - Modify: `docs/20260617_크리에이터_채널_라이브_API/plan-task.md` + - TDD 예외 사유: 다음 범위 작업을 위한 인계 프롬프트 작성 task로 production/test 코드 변경이 없다. + - 대체 검증 방법: + - 문서 내 프롬프트가 이번 라이브 탭 구현을 다시 수정하라고 지시하지 않는지 확인한다. + - 프롬프트가 기존 홈 API endpoint와 공개 응답 계약 보존, 테스트 선행, 패키지 의존 방향을 명시하는지 확인한다. + - 후속 작업용 GPT-5.5 프롬프트: + +```text +너는 /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`를 추가해 홈 API 컴파일/테스트를 먼저 복구한다. -3. 라이브 탭 page 정책과 service 골격을 만든다. -4. 라이브 다시듣기 count/list repository를 구현한다. -5. controller endpoint와 응답 DTO를 연결한다. +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 명령 유효성 확인에 성공했다. diff --git a/docs/20260617_크리에이터_채널_라이브_API/prd.md b/docs/20260617_크리에이터_채널_라이브_API/prd.md index d3c7b7eb..55d2a10f 100644 --- a/docs/20260617_크리에이터_채널_라이브_API/prd.md +++ b/docs/20260617_크리에이터_채널_라이브_API/prd.md @@ -15,6 +15,8 @@ ## 3. Goals - 크리에이터 채널 라이브 탭 조회 API를 제공한다. +- 클라이언트에서 호출하는 공개 API는 `kr.co.vividnext.sodalive.v2.api.creator.channel.live` 하위 조립 계층에 둔다. +- 라이브, 다시듣기 콘텐츠, 시리즈/소장 상태처럼 재사용 가능한 조회 책임은 API 패키지 밖의 도메인 패키지에 둔다. - 요청은 `creatorId`와 정렬 순서를 받는다. - 정렬 순서를 보내지 않으면 최신순을 기본값으로 사용한다. - 응답에는 `다시듣기` 콘텐츠 개수, 현재 진행 중인 라이브, `다시듣기` 콘텐츠 목록, 실제 적용된 정렬 순서를 포함한다. @@ -27,7 +29,8 @@ ## 4. Non-Goals - 이번 범위는 크리에이터 채널 `라이브` 탭 조회 API만 포함한다. -- 기존 크리에이터 채널 홈 API 응답 스키마는 변경하지 않는다. +- 기존 크리에이터 채널 홈 API endpoint와 기존 응답 필드의 의미는 변경하지 않는다. +- 기존 크리에이터 채널 홈 API를 `v2.api.*` 조립 계층 + 도메인 패키지 구조로 옮기는 리팩토링은 이번 범위에서 구현하지 않는다. - 라이브 생성, 예약, 입장, 종료 API는 포함하지 않는다. - 오디오 콘텐츠 구매, 소장, 대여, 결제 API는 포함하지 않는다. - `다시듣기` 테마명 관리 화면 또는 테마 마이그레이션은 포함하지 않는다. @@ -57,7 +60,10 @@ #### Requirements - 신규 API는 크리에이터 채널 전용 v2 API로 작성한다. -- 신규 코드 위치는 기존 크리에이터 채널 홈 API와 같은 `kr.co.vividnext.sodalive.v2.creator.channel` 하위 경계를 기본 후보로 한다. +- 클라이언트 공개 API controller/facade/response DTO는 `kr.co.vividnext.sodalive.v2.api.creator.channel.live` 하위에 작성한다. +- API 조립 계층은 필요한 도메인 조회 서비스를 호출해 라이브 탭 응답을 조립한다. +- API 조립 계층이 호출하는 도메인 조회 코드는 `kr.co.vividnext.sodalive.v2.live`, `kr.co.vividnext.sodalive.v2.content`, `kr.co.vividnext.sodalive.v2.series` 또는 채널 문맥이 필요한 경우 `kr.co.vividnext.sodalive.v2.creator.channel.live` 하위에 둔다. +- 도메인 패키지는 `kr.co.vividnext.sodalive.v2.api.*` 패키지에 의존하지 않는다. - API endpoint는 `GET /api/v2/creator-channels/{creatorId}/live`를 기본안으로 한다. - `creatorId`는 path variable로 받는다. - 정렬 순서는 query parameter로 받는다. @@ -238,9 +244,10 @@ enum class ContentSort { - 빌드 도구는 Gradle Wrapper(`./gradlew`)를 사용한다. - Kotlin + Spring Boot 2.7.14 기존 스타일을 따른다. - 신규 공개 API 스키마는 구현 전에 PRD와 구현 계획/TASK 문서에 명시한다. -- 기존 `CreatorChannelLiveResponse`, `CreatorChannelAudioContentResponse` DTO를 재사용한다. -- `CreatorChannelAudioContentResponse`에는 `isOwned`, `isRented`를 추가하고, 라이브 다시듣기와 다음 범위의 오디오 콘텐츠 조회 API가 같은 응답 DTO를 사용하도록 한다. -- 기존 크리에이터 채널 홈 API의 패키지, 인증, 예외, 성인 콘텐츠 노출, 차단 관계 정책을 우선 재사용한다. +- 기존 `CreatorChannelLiveResponse`, `CreatorChannelAudioContentResponse`와 필드/의미가 어긋나지 않도록 라이브 탭 API 응답 DTO를 작성한다. +- 라이브 탭 API의 `CreatorChannelAudioContentResponse`에는 `isOwned`, `isRented`를 포함하고, 다음 범위의 오디오 콘텐츠 조회 API에서도 같은 의미를 재사용할 수 있게 한다. +- 기존 크리에이터 채널 홈 API의 인증, 예외, 성인 콘텐츠 노출, 차단 관계 정책은 재사용하되, 신규 공개 API 파일 위치는 `v2.api.*` 조립 계층을 따른다. +- 기존 크리에이터 채널 홈 API가 `v2.creator.channel.adapter.in.web`에 위치한 것은 현재 구조의 예외로 보고, 이번 라이브 탭 구현에서는 같은 예외를 확장하지 않는다. - 페이징 응답은 기존 v2 홈 추천 페이지 응답과 같은 `page`, `size`, `hasNext` 패턴을 따른다. - `다시듣기` 테마명은 기존 코드의 문자열 상수와 중복되지 않도록 구현 시 공용 상수 또는 정책 객체로 관리하는 방안을 검토한다. - `ContentSort`는 API binding, service 정책, 테스트에서 같은 타입을 사용한다. @@ -258,4 +265,6 @@ enum class ContentSort { ## 10. Resolved Decisions - 인기순 매출 기준은 대여/소장 여부와 관계없이 순수하게 결제된 캔 매출만 사용한다. +- 라이브 탭 신규 API는 기존 크리에이터 채널 홈 API 위치를 따라가지 않고, `v2.api.creator.channel.live` 공개 API 조립 계층으로 작성한다. +- 기존 크리에이터 채널 홈 API의 패키지 구조 정렬은 이번 라이브 탭 구현과 분리해 다음 범위에서 별도 리팩토링한다. - `isOwned == true`와 `isRented == true`가 동시에 발생할 가능성은 없지만, 만약 그런 상황이 발생하면 둘 다 `true`로 내려준다.