From 92fe6caf172695cf68697ce1d244c5790b87ccd9 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 19 Jun 2026 21:45:22 +0900 Subject: [PATCH] =?UTF-8?q?docs(creator-channel):=20=EC=98=A4=EB=94=94?= =?UTF-8?q?=EC=98=A4=20=ED=83=AD=20=ED=85=8C=EB=A7=88=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EA=B8=B0=EC=A4=80=EC=9D=84=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan-task.md | 29 ++++++++++++++++--- .../prd.md | 4 ++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md b/docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md index 55573bba..7a5d9918 100644 --- a/docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md +++ b/docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md @@ -26,7 +26,7 @@ - `paidAudioContentCount`: 적용된 필터 기준 `price > 0` 콘텐츠 개수 - `purchasedAudioContentCount`: 적용된 필터 기준 유료 콘텐츠 중 조회자가 유효하게 소장하거나 대여 중인 콘텐츠 개수 - `purchasedAudioContentRate`: `paidAudioContentCount == 0`이면 `0.0`, 아니면 `(purchasedAudioContentCount / paidAudioContentCount) * 100` - - `themes`: 활성 테마 전체 목록. 선택한 `themeId`와 무관하게 내려준다. + - `themes`: 활성 테마 중 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 1개 이상 있는 테마 목록. 선택한 `themeId`와 무관하게 내려준다. - `audioContents`: 기존 `CreatorChannelAudioContentResponse`와 같은 필드/의미를 가진 item 목록 - `sort`: 실제 적용한 `ContentSort` - `themeId`: 실제 적용한 활성 테마 id, 전체 조회 fallback이면 `null` @@ -36,6 +36,7 @@ - 공개 콘텐츠 기준: `AudioContent.isActive == true`, `AudioContent.duration != null`, `AudioContent.releaseDate != null`, `AudioContent.releaseDate <= now`. - 성인 콘텐츠는 조회자의 성인 콘텐츠 노출 정책이 false이면 목록과 count에서 제외한다. - 테마명은 `LangContext.lang.code` 기준으로 `ContentThemeTranslation`을 우선하고, 없거나 빈 문자열이면 `AudioContentTheme.theme` 원문으로 fallback한다. +- 테마 목록 필터링은 콘텐츠 목록/count와 같은 공개 조건, 예약 공개 제외, 성인 콘텐츠 노출 정책을 적용한다. - 시리즈명은 `LangContext.lang.code` 기준으로 `SeriesTranslation`을 우선하고, 없거나 빈 문자열이면 `Series.title` 원문으로 fallback한다. - `isFirstContent`는 선택 테마 안의 첫 콘텐츠가 아니라 크리에이터의 전체 공개 오디오 콘텐츠 중 첫 콘텐츠인지로 판단한다. - 정렬: @@ -247,7 +248,12 @@ interface CreatorChannelAudioQueryPort { fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelAudioCreatorRecord? fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean fun findActiveThemeId(themeId: Long): Long? - fun findAudioThemes(locale: String): List + fun findAudioThemes( + creatorId: Long, + now: LocalDateTime, + canViewAdultContent: Boolean, + locale: String + ): List fun countAudioContents(creatorId: Long, themeId: Long?, now: LocalDateTime, canViewAdultContent: Boolean): Int fun countPaidAudioContents(creatorId: Long, themeId: Long?, now: LocalDateTime, canViewAdultContent: Boolean): Int fun countPurchasedAudioContents( @@ -392,15 +398,24 @@ data class CreatorChannelAudioContentRecord( - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/CreatorChannelAudioQueryRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepositoryTest.kt` - - RED: `@DataJpaTest(properties = ["spring.cache.type=none"])` 기반으로 `findCreator`, `existsBlockedBetween`, `findActiveThemeId`, `findAudioThemes(locale="en")` 테스트를 작성한다. `ContentThemeTranslation`이 있으면 번역명, 없으면 원문명을 반환해야 한다. + - RED: `@DataJpaTest(properties = ["spring.cache.type=none"])` 기반으로 `findCreator`, `existsBlockedBetween`, `findActiveThemeId`, `findAudioThemes(creatorId, now, canViewAdultContent, locale="en")` 테스트를 작성한다. `ContentThemeTranslation`이 있으면 번역명, 없으면 원문명을 반환해야 하며, 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 없는 테마는 제외해야 한다. - 실패 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: repository 미존재 컴파일 실패 - - GREEN: 라이브 탭 repository의 `findCreator`, `existsBlockedBetween`을 오디오 패키지로 필요한 만큼 복사하고, `findActiveThemeId`, `findAudioThemes`를 QueryDSL로 구현한다. + - GREEN: 라이브 탭 repository의 `findCreator`, `existsBlockedBetween`을 오디오 패키지로 필요한 만큼 복사하고, `findActiveThemeId`, `findAudioThemes`를 QueryDSL로 구현한다. `findAudioThemes`는 `audioContentCondition(creatorId, themeId = null, now, canViewAdultContent)`를 공유해 콘텐츠 목록/count와 같은 공개 조건을 적용한다. - 통과 확인: - Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` - Expected: `BUILD SUCCESSFUL` - REFACTOR: `ContentThemeTranslation.theme`이 blank인 경우 원문 fallback을 repository 또는 domain mapping 중 한 곳에서만 처리한다. + - 후속 수정 검증 기록: + - 무엇: 테마 목록에서 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 없는 테마를 제외하는 RED 테스트를 추가했다. + - 왜: 오디오 탭에서 선택 가능한 테마가 실제 콘텐츠가 없는 빈 필터로 노출되지 않아야 한다. + - 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest`를 실행했다. + - 결과: 현재 구현은 활성 테마 전체를 반환해 `DefaultCreatorChannelAudioQueryRepositoryTest.kt:71`, `DefaultCreatorChannelAudioQueryRepositoryTest.kt:96`에서 실패함을 확인했다. + - 무엇: `findAudioThemes`가 `creatorId`, `now`, `canViewAdultContent`, `locale`를 받아 콘텐츠 목록/count와 같은 공개 조건으로 테마를 조회하도록 수정했다. + - 왜: 조회 가능한 아이템이 없는 테마와 성인 노출 정책상 볼 수 없는 테마를 응답에서 제외해야 한다. + - 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest`를 실행했다. + - 결과: `BUILD SUCCESSFUL`로 repository 필터링과 service 컨텍스트 전달을 확인했다. - [x] **Task 3.2: 오디오 콘텐츠 count와 소장률 count 구현** - Files: @@ -605,3 +620,9 @@ data class CreatorChannelAudioContentRecord( - 공백: `git diff --check` → 출력 없음. - placeholder 확인: `rg -n "미완성 표시|후속 처리 표시" docs/20260619_크리에이터_채널_오디오_탭_API/plan-task.md src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/audio src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio` → 계획 문서의 검증 명령/기록 자체만 매칭되어 의도하지 않은 placeholder 없음. - 코드 리뷰: controller/facade/service/repository/test 경로를 PRD 요구사항 기준으로 검토했고, 공개 조건, fallback, count, 정렬, 시리즈/테마 번역 fallback, 소장/대여 상태 관련 차단 이슈 없음. +- 2026-06-19: 후속 수정 완료. + - 요구사항: 오디오 탭 `themes` 응답에서 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 없는 테마를 제외한다. + - RED: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest` → 활성 테마 전체를 반환해 신규 assertion 실패 확인. + - GREEN: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.adapter.out.persistence.DefaultCreatorChannelAudioQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.audio.application.CreatorChannelAudioQueryServiceTest` → `BUILD SUCCESSFUL`. + - 문서 명령 확인: `./gradlew tasks --all` → `BUILD SUCCESSFUL`. + - 포맷: `./gradlew ktlintCheck` → `BUILD SUCCESSFUL`. diff --git a/docs/20260619_크리에이터_채널_오디오_탭_API/prd.md b/docs/20260619_크리에이터_채널_오디오_탭_API/prd.md index 46fff135..a7b67bf5 100644 --- a/docs/20260619_크리에이터_채널_오디오_탭_API/prd.md +++ b/docs/20260619_크리에이터_채널_오디오_탭_API/prd.md @@ -177,10 +177,12 @@ enum class ContentSort { - `ko`는 `AudioContentTheme.theme` 원문을 기본으로 사용한다. - `en`, `ja`는 `ContentThemeTranslation.locale`에 해당하는 번역값이 있으면 해당 `theme`을 사용한다. - 요청 언어의 번역값이 없으면 `AudioContentTheme.theme` 원문을 fallback으로 사용한다. -- 테마 목록은 선택한 `themeId`와 무관하게 활성 테마 전체를 내려준다. +- 테마 목록은 선택한 `themeId`와 무관하게 활성 테마 중 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 1개 이상 있는 테마만 내려준다. #### Edge Cases - 활성 테마가 없으면 `themes`는 빈 배열로 내려준다. +- 활성 테마가 있어도 해당 크리에이터의 조회 가능한 공개 오디오 콘텐츠가 없는 테마는 `themes`에서 제외한다. +- 조회자의 성인 콘텐츠 노출 정책이 false이고 특정 테마의 조회 가능한 콘텐츠가 성인 콘텐츠뿐이면 해당 테마는 `themes`에서 제외한다. - 번역 데이터는 있지만 빈 문자열이면 원문 테마명을 fallback으로 사용한다. ### Feature D. 오디오 콘텐츠 목록과 개수