diff --git a/docs/20260612_크리에이터_채널_홈_API/plan-task.md b/docs/20260612_크리에이터_채널_홈_API/plan-task.md index 792f1e57..19350d75 100644 --- a/docs/20260612_크리에이터_채널_홈_API/plan-task.md +++ b/docs/20260612_크리에이터_채널_홈_API/plan-task.md @@ -19,8 +19,9 @@ - 스케줄 타입: `LIVE`, `AUDIO`만 사용한다. 오디오 콘텐츠가 `다시보기` 카테고리여도 `AUDIO`로 내려준다. - 스케줄 정렬/개수: 현재 시각 이후 예약 중 오늘 날짜와 가장 근접한 3개, 예약 시각 오름차순, 같은 예약 시각이면 라이브 먼저 표시한다. - 스케줄 성인 노출 정책: repository query에서 조회자의 성인 노출 정책을 먼저 반영하고, service 최종 조합에서도 내부 스케줄 후보의 `isAdult`로 한 번 더 보정한다. 공개 스케줄 응답에는 `isAdult`를 노출하지 않는다. +- 현재 라이브와 예약 라이브 스케줄은 기존 라이브 목록과 동일하게 성별 제한(`LiveRoom.genderRestriction`)과 크리에이터 입장 제한(`LiveRoom.isAvailableJoinCreator`)을 반영한다. application service는 조회자의 `Auth.gender`가 있으면 이를 우선하고, 없으면 `Member.gender`를 사용하는 `effectiveViewerGender`를 산출해 query port에 넘긴다. - 신규 오디오 콘텐츠와 오디오 목록은 중복 노출하지 않는다. `latestAudioContent`로 내려간 가장 최신 콘텐츠를 오디오 목록에서 제외한다. -- 채널 후원 홈 섹션은 기존 채널 후원 목록과 동일하게 이번 달 기준 최신순 8개를 내려준다. +- 채널 후원 홈 섹션은 기존 채널 후원 목록과 동일하게 이번 달 기준 최신순 8개를 내려준다. 응답 메시지는 기본 문구를 조합하지 않고 후원자가 입력한 추가 메시지만 내려준다. - 오리지널 시리즈 여부는 `Series.isOriginal == true`로 판단한다. - 화보와 상단 탭별 전체보기 API는 이번 범위에서 제외한다. @@ -85,6 +86,7 @@ data class CreatorChannelHomeResponse( data class CreatorChannelCreatorResponse( val creatorId: Long, + val characterId: Long?, val nickname: String, val profileImageUrl: String, val followerCount: Int, @@ -126,13 +128,9 @@ data class CreatorChannelAudioContentResponse( ) data class CreatorChannelDonationResponse( - val donationId: Long, - val memberId: Long, val nickname: String, val profileImageUrl: String, val can: Int, - @JsonProperty("isSecret") - val isSecret: Boolean, val message: String, val createdAtUtc: String ) @@ -148,14 +146,9 @@ data class CreatorChannelSeriesResponse( val seriesId: Long, val title: String, val coverImageUrl: String, - val publishedDaysOfWeek: String, - @JsonProperty("isComplete") - val isComplete: Boolean, val numberOfContent: Int, @JsonProperty("isNew") val isNew: Boolean, - @JsonProperty("isPopular") - val isPopular: Boolean, @JsonProperty("isOriginal") val isOriginal: Boolean ) @@ -274,7 +267,7 @@ data class CreatorChannelSnsResponse( ### Phase 3: 조회 port와 persistence adapter -- [ ] **Task 3.1: 조회 port와 record 타입 정의** +- [x] **Task 3.1: 조회 port와 record 타입 정의** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/port/out/CreatorChannelHomeQueryPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt` @@ -286,7 +279,7 @@ data class CreatorChannelSnsResponse( - REFACTOR: record 타입은 JPA entity를 노출하지 않는 data class로 둔다. - 기대 결과: application service가 의존할 조회 인터페이스가 고정된다. -- [ ] **Task 3.2: 크리에이터 기본 정보/차단/팔로우/AI 채팅/DM 조회 구현** +- [x] **Task 3.2: 크리에이터 기본 정보/차단/팔로우/AI 채팅/DM 조회 구현** - Files: - 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` @@ -302,48 +295,53 @@ data class CreatorChannelSnsResponse( - REFACTOR: 프로필 이미지 URL 조합은 application/DTO에서 cloudFrontHost로 처리할지 repository에서 처리할지 한 곳으로 고정한다. 기존 v2 홈 DTO 관례처럼 path record와 URL 변환 함수를 분리하는 방식을 우선한다. - 기대 결과: 기본 정보와 접근 차단 판단이 기존 정책과 맞는다. -- [ ] **Task 3.3: 현재 라이브와 예약 스케줄 조회 구현** +- [x] **Task 3.3: 현재 라이브와 예약 스케줄 조회 구현** - Files: - 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 통합 테스트를 작성한다. - 현재 라이브는 `channelName`이 있고 활성 상태이며 크리에이터가 진행 중인 라이브만 반환한다. - 예약 라이브는 `beginDateTime > now`, 활성 상태인 row만 스케줄 후보로 반환한다. - - 예약 오디오는 `releaseDate > now`인 콘텐츠만 스케줄 후보로 반환한다. + - 예약 오디오는 `duration != null`, `releaseDate != null`, `releaseDate > now`이면 `isActive` 상태와 관계없이 스케줄 후보로 반환한다. - 다시듣기 테마 예약 오디오도 스케줄 타입은 `AUDIO`다. - 같은 예약 시각이면 라이브가 오디오보다 먼저 온다. - 성인 라이브/오디오는 조회자의 성인 노출 정책이 false이면 제외된다. + - 현재 라이브와 예약 라이브 스케줄은 query port의 `effectiveViewerGender`, `viewerId`, `isViewerCreator` 입력으로 기존 라이브 목록의 성별 제한과 크리에이터 입장 제한을 반영한다. - service 최종 보정을 위해 스케줄 후보 record에는 `isAdult`가 포함된다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` - - GREEN: `LiveRoom`, `AudioContent`, `AudioContentTheme` 조회를 구현하고 `CreatorActivityType.LIVE`/`AUDIO`와 `isAdult`를 record에 담는다. + - GREEN: `LiveRoom`, `AudioContent`, `AudioContentTheme` 조회를 구현하고 `CreatorActivityType.LIVE`/`AUDIO`와 `isAdult`를 record에 담는다. 예약 오디오는 `isActive`가 아니라 `duration`과 미래 `releaseDate`로 판정한다. - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` - REFACTOR: 최종 3개 제한은 repository query와 `CreatorChannelHomeQueryPolicy.limitSchedules` 양쪽 중복 방어를 허용하되, service에서 최종 보정한다. - 기대 결과: 스케줄 섹션이 PRD의 타입/정렬/개수 정책을 만족한다. -- [ ] **Task 3.4: 최신 오디오와 오디오 목록 조회 구현** +- [x] **Task 3.4: 최신 오디오와 오디오 목록 조회 구현** - Files: - 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 통합 테스트를 작성한다. - `latestAudioContent`는 예약 공개 전 콘텐츠를 제외하고 공개 시각 최신순 1개를 반환한다. - 오디오 목록은 `latestAudioContent`를 제외하고 최대 9개를 최신순으로 반환한다. + - `releaseDate == null`인 오디오는 최신/목록/첫 콘텐츠 판정에서 제외한다. - `isPointAvailable`, duration, cover image, price가 record에 포함된다. - 공개 순서상 첫 콘텐츠만 `isFirstContent=true`다. - 시리즈 콘텐츠이면 시리즈 이름과 `Series.isOriginal`이 포함된다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` - GREEN: `AudioContent`, `SeriesContent`, `Series` 기반 조회를 구현한다. - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` - - REFACTOR: 예약 공개 여부 조건은 `releaseDate == null || releaseDate <= now`처럼 기존 콘텐츠 목록 정책과 어긋나지 않도록 작성한다. + - REFACTOR: 공개 오디오 조회 조건은 `releaseDate != null && releaseDate <= now`로 작성한다. `releaseDate == null`은 삭제/미공개 데이터로 보고 제외한다. - 기대 결과: 신규 오디오 영역과 오디오 목록이 중복 없이 구성된다. -- [ ] **Task 3.5: 채널 후원, 공지, 커뮤니티, 팬 Talk 조회 구현** +- [x] **Task 3.5: 채널 후원, 공지, 커뮤니티, 팬 Talk 조회 구현** - Files: - 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 통합 테스트를 작성한다. - 채널 후원은 KST 기준 이번 달 범위의 최신순 8개만 반환한다. - - 공지는 `CreatorCommunity.isFixed == true`, 최대 3개, 고정 시각 최신순으로 반환한다. + - 채널 후원 응답에는 후원자 닉네임, 프로필 이미지, 후원 can, 후원자가 입력한 추가 메시지, UTC 생성 시각만 포함한다. + - 채널 후원 `message`는 기본 문구(`"%s캔을 비밀후원하셨습니다."`, `"%s캔을 후원하셨습니다."`)를 조합하지 않고 `additionalMessage`만 반환한다. + - 공지는 `CreatorCommunity.isFixed == true`, `fixedAt != null`인 데이터로 보고 최대 3개, 고정 시각 최신순으로 반환한다. - 커뮤니티는 `isFixed == false`, 최대 3개, 작성 시각 최신순으로 반환한다. + - 성인 커뮤니티 글은 구매 여부와 무관하게 조회자의 성인 콘텐츠 노출 정책이 false이면 제외한다. - 공지와 커뮤니티의 홈 응답 게시글 요약 필드는 기존 커뮤니티 전체보기 응답과 같은 의미로 계산한다. - 팬 Talk는 `CreatorCheers.parent == null`, `isActive == true`인 최신 1개와 전체 개수를 반환한다. - 차단 관계가 있는 팬 Talk 작성자는 기존 팬 Talk 목록 정책과 동일하게 제외한다. @@ -353,15 +351,16 @@ data class CreatorChannelSnsResponse( - REFACTOR: 커뮤니티 유료 이미지/오디오 구매 여부(`existOrdered`)는 인증 회원 기준으로 기존 community query 의미와 동일하게 계산한다. - 기대 결과: 홈 후원/공지/커뮤니티/팬 Talk 섹션이 기존 전체보기 의미와 맞게 내려간다. -- [ ] **Task 3.6: 시리즈, 소개, 활동, SNS 조회 구현** +- [x] **Task 3.6: 시리즈, 소개, 활동, SNS 조회 구현** - Files: - 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 통합 테스트를 작성한다. - 시리즈는 최대 8개, 시리즈에 속한 공개 콘텐츠 최신 공개 시각 내림차순으로 반환한다. - - 시리즈 응답 record에는 id, 제목, 커버 이미지, 연재 요일, 완결 여부, 콘텐츠 개수, 신규/인기 표시 정보가 포함된다. + - 시리즈 응답 record에는 id, 제목, 커버 이미지, 콘텐츠 개수, 신규 표시, 오리지널 시리즈 여부가 포함된다. - 소개는 `Member.introduce`를 반환한다. - - 데뷔일은 첫 라이브 시작 시각과 첫 공개 오디오 공개 시각 중 빠른 값이다. + - 데뷔일은 첫 라이브 시작 시각과 오디오 데뷔 후보 시각 중 빠른 값이다. + - 오디오 데뷔 후보는 업로드 순서 기준 첫 3개만 보고, 1~2번째 삭제 오디오는 건너뛰며, 3번째 삭제 오디오는 4번째로 넘어가지 않고 해당 `createdAt`을 후보로 쓴다. - 업로드 오디오 콘텐츠 개수는 예약 업로드를 제외한다. - 라이브 진행 횟수/누적 시간/누적 참여자는 기존 `ExplorerQueryRepository` 의미와 맞는다. - SNS는 `instagramUrl`, `fancimmUrl`, `xUrl`, `youtubeUrl`, `websiteUrl`을 기존 상세 API 의미로 반환한다. @@ -371,6 +370,151 @@ data class CreatorChannelSnsResponse( - REFACTOR: 기존 `ExplorerService.getCreatorDetail`과 의미가 같은 계산은 테스트명에 근거를 남기고, 구버전 service를 직접 호출하지 않는다. - 기대 결과: 활동/SNS/시리즈가 구버전 상세 의미와 신규 홈 요구를 함께 만족한다. +- [x] **Task 3.7: `findCreator` 기본 정보 조회를 count/exists 중심으로 개선** + - Files: + - 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: 기존 `shouldFindCreatorProfileWithRelationshipFlags`, `shouldNotExposeNotifyForInactiveFollowing` 테스트를 유지하고, 활성 팔로워가 여러 명이어도 `followerCount` 결과가 정확한 회귀 테스트를 보강한다. + - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - GREEN: `findCreator`에서 팔로워 수는 `select(id).fetch().size` 대신 DB `count()`로 계산하고, AI 채팅 가능 여부는 필요한 id만 조회하는 `exists` 성격의 쿼리로 유지한다. creator 기본 정보처럼 record 생성자 인자로 바로 매핑할 수 있는 조회는 필요한 컬럼을 `Tuple`로 가져와 재조립하지 않고 QueryDSL `Projections.constructor`로 record를 생성한다. + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - REFACTOR: `CreatorChannelCreatorRecord` 조립을 위해 불필요한 `Member` entity 전체를 로딩하지 않는지 코드로 확인한다. QueryDSL `@QueryProjection`은 port record가 QueryDSL에 의존하게 되므로 사용하지 않는다. + - 기대 결과: `findCreator`가 기존 응답 의미를 유지하면서 대량 follower row를 애플리케이션 메모리로 가져오지 않는다. + +- [x] **Task 3.8: 단건/단순 목록 조회를 필요한 컬럼 projection으로 개선** + - Files: + - 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: `findCurrentLive`, `findLatestAudioContent`, `findAudioContents`, `findChannelDonations`, `findSns`의 기존 repository 테스트를 유지해 projection 변경 전후 응답 값이 같음을 고정한다. 단, 채널 후원 공개 응답 계약 변경은 `Task 3.15`를 우선한다. + - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - GREEN: 위 메서드들이 `selectFrom(entity).fetch().map { ... }`로 모든 컬럼을 가져오지 않도록 필요한 컬럼만 조회한다. record 생성자와 조회 컬럼이 1:1로 대응되는 부분은 `Tuple` 조회 후 수동 재조립하지 않고 QueryDSL `Projections.constructor`로 바로 record를 생성한다. `findAudioContents`는 목록 row마다 series/first content 조회가 반복되지 않도록 시리즈 정보와 첫 콘텐츠 id를 bulk 또는 별도 1회 쿼리로 계산한다. + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - REFACTOR: port record에는 QueryDSL annotation을 붙이지 않고, QueryDSL 의존은 persistence adapter 내부에만 둔다. 계산/병합/그룹핑 때문에 constructor projection으로 표현하기 어려운 부분에만 최소 범위로 `Tuple` 또는 별도 bulk map 조립을 허용한다. + - 기대 결과: 현재 라이브, 오디오, 후원, SNS 조회가 필요한 컬럼만 읽고 projection 변경 자체로는 응답 값이 바뀌지 않는다. 채널 후원 공개 응답 계약 변경은 `Task 3.15`에서 별도로 반영한다. + +- [x] **Task 3.9: `findSchedules` 스케줄 후보 조회를 projection으로 개선** + - Files: + - 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: 기존 스케줄 테스트에 live/audio 후보가 여러 개 있을 때 예약 시각 오름차순, 같은 시각 live 우선, `limit` 적용 결과가 유지되는 케이스를 보강한다. + - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - GREEN: 예약 라이브와 예약 오디오 조회에서 entity 전체를 가져오지 않고 `scheduledAt`, `title`, `targetId`, `isAdult` 등 record 구성에 필요한 컬럼만 조회한다. live/audio 후보 각각이 record 생성자 인자로 바로 매핑되는 경우 QueryDSL `Projections.constructor`를 사용하고, 후보 병합 이후 정렬과 `limit`는 기존 정책과 동일하게 유지한다. + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - REFACTOR: 스케줄 공개 응답에는 `isAdult`가 노출되지 않고 내부 record에만 남는 구조를 유지한다. + - 기대 결과: 스케줄 후보 조회가 불필요한 `LiveRoom`, `AudioContent` 전체 컬럼을 읽지 않는다. + +- [x] **Task 3.10: `findCommunityPosts` 게시글 요약 조회의 반복 쿼리와 전체 컬럼 조회 개선** + - Files: + - 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: 공지/커뮤니티 테스트에 여러 게시글을 넣고 `existOrdered`, `likeCount`, `commentCount`, 성인 필터, `fixedAt != null` 공지 조건이 기존 의미대로 유지되는지 검증한다. + - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - GREEN: 게시글 본문 조회는 필요한 컬럼만 조회하되, 본문 record로 바로 매핑 가능한 부분은 `Tuple`로 받은 뒤 재조립하지 않고 QueryDSL `Projections.constructor`를 사용한다. 게시글별 `existsCommunityOrder`, `countCommunityLikes`, `countCommunityComments` 반복 호출은 subquery 또는 게시글 id 목록 기반 bulk 조회로 줄인다. + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - REFACTOR: 기존 커뮤니티 전체보기의 구매 여부, 댓글 수, 댓글 불가 게시글 `commentCount = 0` 의미가 깨지지 않도록 테스트명을 근거로 남긴다. + - 기대 결과: 홈 공지/커뮤니티 최대 3개 조회가 게시글 수에 비례해 불필요한 추가 쿼리를 만들지 않는다. + +- [x] **Task 3.11: `findSeries` 시리즈 조회의 전체 entity 로딩과 시리즈별 콘텐츠 조회 개선** + - Files: + - 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: 시리즈 테스트에 여러 시리즈와 콘텐츠를 넣고 최신 공개 시각 정렬, 콘텐츠 개수, 신규 표시, 오리지널 시리즈 여부, 성인/콘텐츠 타입/차단 정책, `releaseDate == null` 콘텐츠 제외가 유지되는지 검증한다. + - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - GREEN: `findSeries`가 `selectFrom(series).fetch()` 뒤 시리즈마다 `publishedSeriesContents`, `hasNewSeriesContent`를 호출하지 않도록 `SeriesContent`/`AudioContent` join, group by, aggregate 기반 조회로 record를 만든다. aggregate 결과가 record 생성자 인자로 바로 대응되는 부분은 QueryDSL `Projections.constructor`를 사용하고, 후처리 집계가 필요한 경우에만 최소 범위로 bulk map 조립을 허용한다. + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - REFACTOR: 홈 시리즈 공개 응답에 `publishedDaysOfWeek`, `isComplete`, `isPopular`가 다시 포함되지 않도록 DTO와 mapper를 확인한다. + - 기대 결과: 시리즈 최대 8개 조회가 시리즈 수만큼 콘텐츠 조회를 반복하지 않고 기존 시리즈 목록 의미를 유지한다. + +- [x] **Task 3.12: `findFanTalkSummary` 전체 row fetch 제거** + - Files: + - 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: 팬 Talk 테스트에 활성 최상위 글 여러 개와 비활성/답글/차단 작성자 글을 넣고 `totalCount`와 최신 1건이 기존 정책대로 계산되는지 검증한다. + - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - GREEN: `findFanTalkSummary`에서 조건에 맞는 전체 `CreatorCheers` row를 `fetch()`하지 않고, `totalCount`는 DB `count()`로 계산하며 `latestFanTalk`는 필요한 컬럼만 `orderBy(...).limit(1)`로 조회한다. `latestFanTalk`처럼 생성자 인자로 바로 매핑 가능한 record는 QueryDSL `Projections.constructor`로 생성한다. + - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - REFACTOR: 차단 필터는 기존 팬 Talk 목록 정책과 동일하게 조회자 기준 양방향 차단만 반영한다. + - 기대 결과: 팬 Talk가 많은 크리에이터도 홈 조회에서 전체 팬 Talk row를 애플리케이션 메모리로 가져오지 않는다. + +- [x] **Task 3.13: 크리에이터 기본 응답에 `characterId` 추가** + - 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/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt` + - 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` + - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.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/in/web/CreatorChannelHomeControllerTest.kt` + - RED: 활성 `ChatCharacter`가 있는 크리에이터는 `creator.characterId`가 해당 캐릭터 ID로 내려가고, 활성 캐릭터가 없으면 `null`로 내려가는 repository/service/controller 응답 계약 테스트를 먼저 추가한다. + - 실패 확인: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest` + - GREEN: creator record/domain/response DTO에 nullable `characterId`를 추가하고, `findCreator`가 활성 `ChatCharacter` ID를 projection으로 함께 조회하도록 구현한다. + - 통과 확인: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest` + - REFACTOR: `isAiChatAvailable`은 `characterId != null` 기준과 의미가 어긋나지 않도록 한 곳에서 계산한다. + - 기대 결과: 클라이언트가 AI 채팅 진입에 필요한 `characterId`를 홈 응답에서 바로 사용할 수 있다. + +- [x] **Task 3.14: 시리즈 응답에서 연재 요일/완결/인기 필드 제거** + - 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/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt` + - 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` + - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.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/in/web/CreatorChannelHomeControllerTest.kt` + - RED: 시리즈 JSON 응답에 `publishedDaysOfWeek`, `isComplete`, `isPopular`가 없고, `seriesId`, `title`, `coverImageUrl`, `numberOfContent`, `isNew`, `isOriginal`만 필요한 계약으로 내려가는 테스트를 먼저 추가한다. + - 실패 확인: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest` + - GREEN: series record/domain/response DTO와 mapper에서 `publishedDaysOfWeek`, `isComplete`, `isPopular`를 제거하고, repository projection도 더 이상 해당 응답 필드 조립을 위해 조회하지 않도록 정리한다. + - 통과 확인: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest` + - REFACTOR: `rg -n "publishedDaysOfWeek|isComplete|isPopular" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel`로 신규 홈 API 경계에 제거 대상 필드가 남지 않았는지 확인한다. + - 기대 결과: 홈 시리즈 응답이 클라이언트 요청 계약에 맞게 축소된다. + +- [x] **Task 3.15: 채널 후원 응답에서 기본 후원 문구와 내부 식별 필드 제거** + - 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/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt` + - 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` + - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.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/in/web/CreatorChannelHomeControllerTest.kt` + - RED: 채널 후원 JSON 응답에 `donationId`, `memberId`, `isSecret`이 없고, `nickname`, `profileImageUrl`, `can`, `message`, `createdAtUtc`만 내려가는 테스트를 먼저 추가한다. repository 테스트에서는 `message`가 기본 문구 조합 없이 `ChannelDonationMessage.additionalMessage` 값만 반환되고, 추가 메시지가 없으면 빈 문자열이 되는지 검증한다. 비밀 후원은 기존 정책처럼 후원자 본인과 받은 크리에이터만 조회 가능하고, 제3자는 같은 달 비밀 후원을 조회할 수 없는 negative case를 추가한다. + - 실패 확인: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest` + - GREEN: donation record/domain/response DTO와 mapper에서 `donationId`, `memberId`, `isSecret` 공개 응답 필드를 제거하고, repository는 `additionalMessage.orEmpty()`를 `message`로 매핑한다. 비밀 후원 노출 여부와 이번 달 최신순 8개 조회 정책은 유지하되, `isSecret`은 repository 조회 조건에만 사용하고 공개 응답에는 포함하지 않는다. record 생성자 인자로 바로 매핑되는 조회는 QueryDSL `Projections.constructor`를 사용한다. + - 통과 확인: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.in.web.CreatorChannelHomeControllerTest` + - REFACTOR: `SodaMessageSource`/`LangContext`가 채널 후원 메시지 조합만을 위해 주입되어 있다면 제거한다. `rg -n "donationId|memberId|isSecret|비밀후원|후원하셨습니다|SodaMessageSource|LangContext" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel`로 신규 홈 API 경계에 제거 대상 응답 필드나 기본 문구 조합이 남지 않았는지 확인한다. + - 기대 결과: 홈 채널 후원 섹션이 변경된 클라이언트 계약에 맞게 추가 메시지 중심의 최소 응답만 내려준다. + +- [x] **Task 3.16: 구매한 삭제 유료 커뮤니티 게시글 조회 정책 보정** + - Files: + - 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: 유료 커뮤니티 게시글을 구매한 조회자는 크리에이터가 이후 삭제해 `CreatorCommunity.isActive == false`가 되어도 해당 게시글을 조회하고, 비구매자는 조회하지 못하는 repository 테스트를 먼저 추가한다. + - 실패 확인: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon` + - GREEN: `findCommunityPosts`의 구현 방식은 게시글 후보 조회 후 구매 여부/좋아요 수/댓글 수를 bulk 조회하는 기존 구조를 유지한다. 게시글 후보 조건만 기존 커뮤니티 전체보기 의미에 맞춰 `CreatorCommunity.isActive == true` 또는 인증 조회자가 환불되지 않은 `PAID_COMMUNITY_POST` 구매 이력이 있는 경우로 보정한다. + - 통과 확인: + - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon` + - REFACTOR: 좋아요/댓글/구매 여부 조회를 `leftJoin` 하나로 합치지 않고, 현재의 id 목록 기반 bulk 조회 구조를 유지한다. + - 기대 결과: 구매자는 삭제된 유료 게시글도 기존 전체보기 의미와 동일하게 접근할 수 있고, 비구매자는 삭제된 게시글을 조회하지 못한다. + --- ### Phase 4: application service 조립 @@ -381,8 +525,9 @@ data class CreatorChannelSnsResponse( - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/domain/CreatorChannelHomeQueryPolicy.kt` - Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/application/CreatorChannelHomeQueryServiceTest.kt` - RED: fake port를 사용해 모든 섹션 record를 넣고, service가 `CreatorChannelHome`으로 전체 섹션을 조립하는 테스트를 작성한다. `latestAudioContent`와 오디오 목록 중복 제거, 스케줄 최대 3개 제한, 같은 시각 라이브 우선 정렬, 성인 스케줄 최종 제외도 service 테스트에서 검증한다. + - RED: 조회자에게 `Auth.gender`가 있으면 `Member.gender`보다 `Auth.gender`를 우선해 `effectiveViewerGender`를 산출하고, `viewerId`, `isViewerCreator`, `effectiveViewerGender`가 `findCurrentLive`와 `findSchedules`에 전달되는지 fake port로 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` - - GREEN: service에서 creator 검증, 성인 노출 정책 입력, port 호출, policy 적용, URL 변환에 필요한 host 전달을 구현한다. + - GREEN: service에서 creator 검증, 성인 노출 정책 입력, 조회자 라이브 정책 컨텍스트(`viewerId`, `isViewerCreator`, `effectiveViewerGender`) 산출, port 호출, policy 적용, URL 변환에 필요한 host 전달을 구현한다. - 통과 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` - REFACTOR: service는 트랜잭션 경계 `@Transactional(readOnly = true)`를 갖고, persistence adapter의 세부 query에 의존하지 않도록 port만 사용한다. - 기대 결과: controller가 단일 service 호출만으로 홈 응답을 받을 수 있다. @@ -515,3 +660,36 @@ data class CreatorChannelSnsResponse( - 2026-06-12: Phase 2 리뷰 보정 RED 확인 - 오디오 콘텐츠 `isAdult`와 스케줄 현재시각 필터 테스트 추가 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest` 실행 시 `Unresolved reference: isAdult`, `Too many arguments for limitSchedules` 컴파일 오류를 확인했다. - 2026-06-12: Phase 2 리뷰 보정 GREEN 확인 - `CreatorChannelAudioContent`/`CreatorChannelAudioContentResponse`에 `isAdult`를 추가하고 `CreatorChannelHomeQueryPolicy.limitSchedules(schedules, now)`가 `scheduledAt > now`만 남기도록 수정한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest` 통과. - 2026-06-12: 스케줄 성인 노출 정책 보강 - PRD와 plan-task에 repository query 1차 필터 + service 최종 보정 방식을 명시하고, 내부 `CreatorChannelSchedule.isAdult`와 `CreatorChannelHomeQueryPolicy.limitSchedules(schedules, now, canViewAdultContent)`를 반영했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest`, `./gradlew ktlintCheck` 통과. +- 2026-06-12: Phase 3 RED/GREEN 확인 - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` 실행 시 `Unresolved reference: DefaultCreatorChannelHomeQueryRepository` 컴파일 오류를 확인한 뒤 조회 port/record와 `DefaultCreatorChannelHomeQueryRepository`를 구현해 통과. 추가로 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest`, `./gradlew ktlintCheck` 통과. +- 2026-06-12: Phase 3 예약 라이브 조건 보강 - 기존 `LiveRoomRepository.getLiveRoomListReservationWithoutDate` 의미에 맞춰 예약 라이브 스케줄은 `channelName`이 비어 있는 row만 조회하도록 테스트를 보강했다. 보강 직후 `DefaultCreatorChannelHomeQueryRepositoryTest`가 스케줄 assertion에서 실패하는 것을 확인했고, `findSchedules` live 조건을 `channelName is null or empty`로 수정한 뒤 `./gradlew clean test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest`, `./gradlew ktlintCheck` 통과. +- 2026-06-12: Phase 3 리뷰 보정 RED/GREEN 확인 - 비활성 팔로우 알림, `releaseDate == null` 공개 오디오, KST 월 경계/크리에이터 비밀 후원 열람, 팬 Talk 작성자→조회자 차단, 미래 라이브 데뷔일 제외 테스트를 추가한 뒤 `DefaultCreatorChannelHomeQueryRepositoryTest`에서 5개 실패를 확인했다. 조회 조건을 기존 repository/service 의미에 맞게 보정한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest`, `./gradlew ktlintCheck` 통과. +- 2026-06-12: Phase 3 Task 3.7~3.12 RED 확인 - `findCreator` 다중 활성 팔로워 count 회귀 테스트와 projection/bulk 구조 회귀 테스트를 추가한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` 실행 시 `shouldUseProjectionAndBulkQueriesForPhaseThreeOptimizedMethods`가 기존 `selectFrom(...)`, `fetch().size`, per-row helper 사용을 잡아 실패하는 것을 확인했다. +- 2026-06-12: Phase 3 Task 3.7~3.12 GREEN 확인 - `findCreator`, `findCurrentLive`, `findLatestAudioContent`, `findAudioContents`, `findChannelDonations`, `findSns`, `findSchedules`, `findCommunityPosts`, `findSeries`, `findFanTalkSummary`를 필요한 컬럼 projection, DB count, id 목록 기반 bulk 조회로 개선하고 `findActivity`의 id fetch count도 DB count로 보정했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` 통과. +- 2026-06-12: Phase 3 Task 3.7~3.12 회귀/정리 확인 - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` 통과, `./gradlew ktlintCheck` 통과. +- 2026-06-12: Phase 3 Task 3.9/3.11 리뷰 보정 확인 - 스케줄 후보 병합 후 `limit` 적용 테스트와 `select(series)` entity fetch 금지 테스트를 추가해 RED를 확인했고, `findSeries`를 tuple projection과 `publishedDaysOfWeek` id 목록 기반 bulk join으로 변경했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest`, `./gradlew ktlintCheck` 통과. `rg -n "\.select\(series\)|selectFrom\(series\)|publishedSeriesContents\(|hasNewSeriesContent\(" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt` 결과 없음. +- 2026-06-12: Phase 3 추가 리뷰 보정 RED/GREEN 확인 - 커뮤니티 성인 필터/작성자 본인 구매 처리, 시리즈 성인·콘텐츠 타입·차단·신규 표시 정책, `releaseDate == null` 오디오의 `createdAt` 정렬 fallback, 팬 Talk 조회자 기준 차단 정책 테스트를 추가한 뒤 port/repository signature와 query 조건을 보정했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest`, `./gradlew ktlintCheck` 통과. +- 2026-06-12: Phase 3 reviewer follow-up 보정 - 리뷰에서 지적된 보이는 첫 오디오 판정의 성인 정책 반영 누락과 시리즈 콘텐츠 후보의 `duration is not null` 조건 누락 가능성을 테스트로 고정했다. 보강 직후 `DefaultCreatorChannelHomeQueryRepositoryTest`에서 2개 실패를 확인했고, `firstAudioContentId`에 `canViewAdultContent`를 반영하고 시리즈 콘텐츠/신규 판정에 `duration.isNotNull`을 추가한 뒤 `./gradlew clean test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest`, `./gradlew ktlintCheck` 통과. +- 2026-06-12: Phase 3 리뷰 결과 보정 RED/GREEN 확인 - 커뮤니티 댓글 수의 기존 목록 의미(parent null/양방향 차단/비밀 댓글 권한), 채널 후원 메시지 기본 문구+캔 포맷+추가 메시지 조합, 단독 오디오 duration null 제외 정책을 repository 테스트로 추가했다. 보강 직후 `DefaultCreatorChannelHomeQueryRepositoryTest`에서 3개 실패(`commentCount 5 -> 1`, null duration 최신 오디오 선택, 후원 메시지 additionalMessage 단독 반환)를 확인했고, query/메시지 조합을 보정한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` 통과. +- 2026-06-12: Phase 3 리뷰어 게이트 보정 - Context mining 리뷰에서 지적된 `isCommentAvailable == false` 게시글 댓글 수와 채널 후원 메시지 다국어 메시지 소스 의미를 추가 테스트로 고정했다. 보강 직후 constructor mismatch RED를 확인했고, repository에 `SodaMessageSource`/`LangContext` 기반 메시지 조합과 댓글 불가 게시글 `commentCount = 0` 처리를 반영한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest` 통과. +- 2026-06-12: Phase 3 문서 정합성 보정 - 성인 커뮤니티 글은 구매 여부와 무관하게 조회자의 성인 콘텐츠 노출 정책이 우선한다는 점과 `isFixed == true` 게시글은 `fixedAt != null`인 데이터로 본다는 전제를 PRD/plan-task에 명시했다. +- 2026-06-12: Phase 3 리뷰 결과 보정 - `isFixed == true`이지만 `fixedAt == null`인 공지 fixture와 팬 Talk 활성 최상위 글 전체 개수/최신 1건 테스트를 추가했다. 보강 직후 `DefaultCreatorChannelHomeQueryRepositoryTest`에서 공지 assertion 실패를 확인했고, 공지 조회에 `fixedAt.isNotNull` 조건을 추가하고 팬 Talk 요약을 `count` 쿼리와 최신 `limit(1).fetchFirst()` 쿼리로 분리한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest`, `./gradlew ktlintCheck` 통과. +- 2026-06-12: Phase 3 조회 효율 개선 Task 문서화 - `findCreator`의 count/exists 중심 개선과 entity 전체 컬럼 조회 후 record mapping을 줄이는 작업을 `Task 3.7`~`Task 3.12`로 분리해 plan-task에 추가했다. QueryDSL `@QueryProjection`은 사용하지 않고, persistence adapter 내부 projection/tuple/bulk/count 쿼리로 개선하는 방향을 명시했다. +- 2026-06-12: Phase 3 조회 효율 개선 Task 문서화 검증 - 문서 변경 후 `./gradlew tasks --all`을 실행해 Gradle 명령 유효성을 확인했다. sandbox 권한에서는 `~/.gradle` lock 파일 접근 제한으로 실패했고, 권한 승격 후 `BUILD SUCCESSFUL`을 확인했다. +- 2026-06-12: 응답 계약 변경 문서화 - PRD와 plan-task에 `creator.characterId` 추가, 시리즈 응답의 `publishedDaysOfWeek`/`isComplete`/`isPopular` 제거를 반영했다. Phase 3의 미완료 항목 `Task 3.7`~`Task 3.12` 체크를 해제하고, 변경 반영 구현 항목을 `Task 3.13`, `Task 3.14`로 추가했다. +- 2026-06-12: 응답 계약 변경 문서 검증 - `./gradlew tasks --all`을 실행해 Gradle 명령 유효성을 확인했다. sandbox 권한에서는 `~/.gradle` lock 파일 접근 제한으로 실패했고, 권한 승격 후 `BUILD SUCCESSFUL`을 확인했다. +- 2026-06-12: Phase 3 projection 구현 방향 문서화 - `Task 3.7`~`Task 3.12`에서 record 생성자 인자로 바로 매핑 가능한 조회는 `Tuple` 조회 후 수동 재조립하지 않고 QueryDSL `Projections.constructor`를 사용하도록 task 문구를 보정했다. +- 2026-06-12: Phase 3 projection 구현 방향 문서 검증 - `./gradlew tasks --all`을 실행해 Gradle 명령 유효성을 확인했다. sandbox 권한에서는 `~/.gradle` lock 파일 접근 제한으로 실패했고, 권한 승격 후 `BUILD SUCCESSFUL`을 확인했다. +- 2026-06-13: Phase 3 Task 3.13/3.14 RED 확인 - `characterId`와 시리즈 응답 축소 계약 테스트를 먼저 추가한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest` 실행 시 `Unresolved reference: characterId`, `No value passed for parameter 'publishedDaysOfWeek'/'isComplete'/'isPopular'` 컴파일 오류를 확인했다. +- 2026-06-13: Phase 3 Task 3.13/3.14 GREEN 확인 - creator record/domain/response에 nullable `characterId`를 추가하고 `findCreator`가 활성 `ChatCharacter` id를 조회하도록 보정했다. series record/domain/response/repository projection에서는 `publishedDaysOfWeek`, `isComplete`, `isPopular`를 제거했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --no-daemon`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon` 통과. `rg -n "publishedDaysOfWeek|SeriesPublishedDaysOfWeek|SeriesState|isComplete|isPopular" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel` 결과 없음. +- 2026-06-13: Phase 3 보안 리뷰 보정 RED/GREEN 확인 - 리뷰어 게이트에서 유료 커뮤니티 게시글 비구매자에게 원문 `content`와 `audioPath`가 노출될 수 있다는 차단 이슈를 확인했다. 비구매자/구매자/작성자 유료 커뮤니티 접근 테스트를 추가한 뒤 `DefaultCreatorChannelHomeQueryRepositoryTest`에서 `shouldMaskPaidCommunityContentAndAudioForNonBuyer` 실패를 확인했고, 기존 커뮤니티 목록과 동일하게 유료/미구매/비작성자 본문 축약 및 오디오 숨김을 반영한 뒤 `./gradlew clean test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon` 통과. +- 2026-06-13: Phase 3 Task 3.7~3.12 constructor projection 조건 RED/GREEN 확인 - 직접 record 생성자 인자로 매핑 가능한 조회가 `Projections.constructor`를 사용하도록 source guardrail을 추가한 뒤 `shouldUseProjectionAndBulkQueriesForPhaseThreeOptimizedMethods` 실패를 확인했다. `findCurrentLive`, `findSchedules` live/audio 후보, `findFanTalkSummary` 최신 글, `findSns`를 QueryDSL `Projections.constructor`로 변경하고 후처리/마스킹/집계가 필요한 creator/audio/donation/community/series/activity 조회는 수동 조립을 유지했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --no-daemon`, `./gradlew ktlintCheck --no-daemon` 통과. `rg -n "publishedDaysOfWeek|SeriesPublishedDaysOfWeek|SeriesState|isComplete|isPopular" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel` 결과 없음. +- 2026-06-13: Phase 3 오디오 공개 조건 문서/구현 정합성 보정 - 공개 또는 예약 공개 오디오는 `releaseDate != null`이고, `releaseDate == null`은 삭제/미공개 데이터로 조회에서 제외한다는 전제를 PRD/plan-task에 반영했다. `releaseDate == null` 오디오가 최신/목록/첫 콘텐츠/시리즈 공개 콘텐츠 집계에서 제외되는 테스트를 추가했고, 보강 직후 `DefaultCreatorChannelHomeQueryRepositoryTest`에서 3개 실패를 확인했다. `findAudioContentRows`, `firstAudioContentId`, `seriesContentStats`의 공개 조건을 `releaseDate != null && releaseDate <= now`로 보정한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon` 통과. +- 2026-06-13: Phase 3 데뷔일 오디오 후보 정책 보정 - 오디오 데뷔 후보는 업로드 순서 기준 첫 3개만 보며, 1~2번째 삭제 오디오는 건너뛰고 3번째 삭제 오디오는 4번째로 넘어가지 않고 해당 `createdAt`을 후보로 쓰는 정책을 PRD/plan-task에 반영했다. 3번째 삭제 시 4번째 공개 오디오로 넘어가던 RED를 `DefaultCreatorChannelHomeQueryRepositoryTest`에서 확인했고, `findActivity`가 `firstAudioDebutAt`으로 오디오 후보를 계산하도록 보정했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon`, `./gradlew ktlintCheck --no-daemon` 통과. +- 2026-06-13: Phase 3 P1/P2 리뷰 보정 RED/GREEN 확인 - 예약 오디오는 `duration != null && releaseDate != null && releaseDate > now`이면 `isActive`와 관계없이 스케줄 후보라는 정책과, 시리즈 신규 표시가 기존 목록처럼 7일 전/현재 시각 경계를 포함한다는 정책을 repository 테스트로 추가했다. 보강 직후 `DefaultCreatorChannelHomeQueryRepositoryTest`에서 2개 실패를 확인했고, `findSchedules` 예약 오디오 조건과 `newSeriesIds`의 `between(now.minusDays(7), now)` 조건을 보정했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon`, `./gradlew ktlintCheck --no-daemon` 통과. +- 2026-06-13: 채널 후원 응답 계약 변경 문서화 - 채널 후원 홈 응답에서 기본 후원 문구 조합과 공개 응답의 `donationId`/`memberId`/`isSecret`을 제거하고, 후원자 닉네임/프로필 이미지/후원 can/추가 메시지/UTC 생성 시각만 내려주는 요구사항을 PRD와 plan-task에 반영했다. Phase 3에 `Task 3.15`를 추가해 repository/service/controller 테스트와 구현 범위를 문서화했다. `git diff --check`, `./gradlew tasks --all --no-daemon` 통과. +- 2026-06-13: 채널 후원 비밀 후원 정책/Projection 구현 기준 문서화 - `Task 3.15`에 비밀 후원은 후원자 본인과 받은 크리에이터만 조회 가능하고 제3자는 조회할 수 없는 negative case를 추가했다. `isSecret`은 repository 조회 조건에만 사용하고 공개 응답에는 포함하지 않는다고 명시했으며, record 생성자 인자로 바로 매핑되는 조회는 QueryDSL `Projections.constructor`를 사용하도록 구현 기준을 보강했다. `git diff --check`, `./gradlew tasks --all --no-daemon` 통과. +- 2026-06-13: Phase 3 Task 3.16 RED/GREEN 확인 - 구매한 유료 커뮤니티 게시글은 크리에이터가 이후 삭제해 `CreatorCommunity.isActive == false`가 되어도 구매자에게 조회되고, 비구매자에게는 조회되지 않는 요구사항을 PRD/plan-task에 반영했다. `shouldExposeDeletedPaidCommunityContentToBuyer` 테스트 추가 직후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon`에서 해당 테스트 실패를 확인했고, `findCommunityPosts` 게시글 후보 조건을 `isActive == true` 또는 환불되지 않은 `PAID_COMMUNITY_POST` 구매 이력 존재로 보정했다. 구현 방식은 게시글 후보 조회 후 구매 여부/좋아요 수/댓글 수를 id 목록 기반 bulk 조회하는 기존 구조를 유지했다. 보정 후 같은 repository 테스트 통과. +- 2026-06-13: Phase 3 Task 3.16 정리 검증 - `git diff --check`, `./gradlew ktlintCheck --no-daemon`, `./gradlew tasks --all --no-daemon` 통과. +- 2026-06-13: Phase 3 리뷰 반영 RED/GREEN 확인 - `LiveRoomStatus` 자체가 아니라 라이브 홈 조회의 노출 정책만 보정하기 위해 현재/예약 라이브에 조회자 성별 제한과 크리에이터 입장 제한 테스트를 추가하고, 팬 Talk 작성자 닉네임의 `deleted_` prefix 제거 테스트를 추가했다. 보강 직후 `DefaultCreatorChannelHomeQueryRepositoryTest`가 `viewerId`/`isViewerCreator`/`effectiveViewerGender` 미정의 컴파일 오류로 실패하는 것을 확인했고, `findCurrentLive`/`findSchedules`에 조회자 라이브 정책 입력을 추가해 라이브 후보만 필터링하고 `findFanTalkSummary` 최신 글 닉네임에 `removeDeletedNicknamePrefix()`를 적용했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon` 통과. +- 2026-06-13: Phase 3 리뷰 반영 정리 검증 - `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon`, `git diff --check`, `./gradlew ktlintCheck --no-daemon` 통과. +- 2026-06-13: Phase 3 P2/P3 리뷰 반영 RED/GREEN 확인 - query port 계약을 기존 raw 조회자 성별 파라미터명이 아니라 기존 라이브 목록 의미와 같은 `effectiveViewerGender`로 명확히 바꾸기 위해 repository 테스트 호출부를 먼저 변경했고, `DefaultCreatorChannelHomeQueryRepositoryTest`에서 `Cannot find a parameter with this name: effectiveViewerGender` 컴파일 실패를 확인했다. 이후 `CreatorChannelHomeQueryPort`와 `DefaultCreatorChannelHomeQueryRepository`의 현재/예약 라이브 조회 계약을 `effectiveViewerGender`로 변경했다. PRD와 plan-task에는 현재/예약 라이브의 성별 제한·크리에이터 입장 제한 정책, service의 `Auth.gender` 우선 effective gender 산출, Phase 4 service fake port 테스트 요구사항을 명시했다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.application.CreatorChannelHomeQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest --no-daemon`, `git diff --check`, `./gradlew ktlintCheck --no-daemon` 통과. `rg -n "viewer""Gender" docs/20260612_크리에이터_채널_홈_API src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel` 결과 없음. diff --git a/docs/20260612_크리에이터_채널_홈_API/prd.md b/docs/20260612_크리에이터_채널_홈_API/prd.md index ebfa5c8a..2c5f3798 100644 --- a/docs/20260612_크리에이터_채널_홈_API/prd.md +++ b/docs/20260612_크리에이터_채널_홈_API/prd.md @@ -80,6 +80,7 @@ #### Requirements - 크리에이터 기본 정보에는 다음 값을 포함한다. - `creatorId` + - `characterId` - `nickname` - `profileImageUrl` - `followerCount` @@ -88,6 +89,7 @@ - `isFollow` - `isNotify` - `followerCount`는 활성 팔로우 수 기준으로 계산한다. +- `characterId`는 해당 `Member`와 연결된 활성 `ChatCharacter` ID를 내려주고, 활성 캐릭터가 없으면 `null`로 내려준다. - `isAiChatAvailable`은 해당 `Member`와 연결된 활성 `ChatCharacter`가 있는지로 판단한다. 구현 후보는 `ChatCharacterRepository.existsByCreatorMemberId(creatorId)`를 기준으로 한다. - `isDmAvailable`은 `member.memberKind != MemberKind.AI_CHARACTER`이면 `true`, `AI_CHARACTER`이면 `false`로 판단한다. - `isFollow`, `isNotify`는 인증 회원의 기존 `CreatorFollowing` 상태를 기준으로 내려준다. @@ -103,6 +105,9 @@ - 현재 진행 중인 라이브는 기존 라이브 도메인의 `LiveRoomStatus.NOW` 의미와 동일하게 판단한다. - 응답에는 라이브 ID, 제목, 커버 이미지, 시작 시각 UTC, 유료 여부 또는 가격, 성인 여부, 예약 여부가 아닌 현재 라이브 여부를 포함한다. - 조회자의 성인 콘텐츠 노출 정책과 차단 정책을 반영한다. +- 현재 라이브 노출은 기존 라이브 목록 정책과 동일하게 성별 제한(`LiveRoom.genderRestriction`)과 크리에이터 입장 제한(`LiveRoom.isAvailableJoinCreator`)을 반영한다. +- 성별 제한 판단에 사용하는 조회자 성별은 기존 라이브 목록과 동일하게 `Auth.gender`가 있으면 이를 우선하고, 없으면 `Member.gender`를 사용하는 effective gender다. +- `LiveRoomStatus`는 라이브가 현재 진행 중인지/예약인지 구분하는 기준일 뿐, 라이브 노출 정책 자체로 사용하지 않는다. #### Edge Cases - 현재 진행 중인 라이브가 없으면 `currentLive`는 `null`로 내려준다. @@ -112,6 +117,7 @@ #### Requirements - Figma 홈 상단에 노출되는 신규 오디오 콘텐츠 영역에 사용할 최신 공개 오디오 콘텐츠를 내려준다. - 예약 공개 전 콘텐츠는 신규 오디오 콘텐츠로 노출하지 않는다. +- 공개 또는 예약 공개 오디오 콘텐츠는 항상 `releaseDate != null`인 데이터로 본다. `releaseDate == null`인 오디오 콘텐츠는 삭제/미공개 데이터로 보고 홈 오디오 조회에서 제외한다. - 응답 필드는 홈 오디오 콘텐츠 카드와 동일하게 콘텐츠 ID, 제목, duration, 커버 이미지, 가격, 포인트 사용 가능 여부, 성인 여부를 포함한다. - 정렬은 공개 시각 최신순이다. @@ -122,13 +128,14 @@ #### Requirements - 채널 후원은 최신순 최대 8개를 내려준다. -- 기존 `ChannelDonationService.getChannelDonationList` 응답 필드 의미를 유지한다. - 조회 범위는 기존 채널 후원 목록과 동일하게 이번 달 기준으로 한다. -- 응답에는 후원 ID, 회원 ID, 닉네임, 프로필 이미지, 후원 can, 비밀 후원 여부, 메시지, 생성 시각 UTC를 포함한다. -- 비밀 후원 표시 정책은 기존 채널 후원 목록 정책을 따른다. +- 응답에는 후원자 닉네임, 후원자 프로필 이미지, 후원한 can 수, 후원 시 추가한 메시지, 생성 시각 UTC를 포함한다. +- `message`는 기본 문구(`"%s캔을 비밀후원하셨습니다."`, `"%s캔을 후원하셨습니다."`)를 조합하지 않고, 후원자가 입력한 추가 메시지(`ChannelDonationMessage.additionalMessage`)만 내려준다. +- 비밀 후원 노출/숨김 정책은 기존 채널 후원 목록 정책을 따른다. #### Edge Cases - 후원이 없으면 빈 배열을 내려준다. +- 후원자가 추가 메시지를 입력하지 않았으면 `message`는 빈 문자열로 내려준다. ### Feature F. 공지 @@ -136,12 +143,14 @@ - 커뮤니티 게시글 중 `isFixed == true`인 글을 홈의 공지 섹션으로 처리한다. - 응답에는 게시글 ID, 크리에이터 ID, 크리에이터 닉네임, 크리에이터 프로필 이미지, 이미지 URL, 오디오 URL, 본문, 가격, 작성 시각 UTC, 구매 여부, 좋아요 수, 댓글 수를 포함한다. - 홈 응답에 포함하는 게시글 요약 필드는 기존 크리에이터 커뮤니티 전체보기 게시글 리스트와 같은 의미를 유지한다. -- 정렬은 고정 시각 최신순을 우선하고, 고정 시각이 없으면 작성 시각 최신순으로 한다. +- 유료 공지는 조회자가 이미 구매했다면 크리에이터가 이후 삭제해 `isActive == false`가 되어도 기존 커뮤니티 전체보기 의미와 동일하게 구매자에게 조회된다. +- `isFixed == true`인 게시글은 항상 `fixedAt != null`인 데이터로 본다. +- 정렬은 고정 시각 최신순으로 한다. - 공지 최대 노출 개수는 기존 고정 글 제한 정책에 맞춰 최대 3개로 한다. #### Edge Cases - 고정 게시글이 없으면 빈 배열을 내려준다. -- 성인 커뮤니티 글은 조회자의 성인 콘텐츠 노출 정책을 따른다. +- 성인 커뮤니티 글은 조회자의 성인 콘텐츠 노출 정책을 따른다. 조회자가 해당 글을 구매했더라도 성인 콘텐츠 노출 정책이 false이면 노출하지 않는다. ### Feature G. 스케줄 @@ -149,7 +158,7 @@ - 예약 라이브와 예약 업로드 오디오 콘텐츠를 하나의 스케줄 배열로 내려준다. - 스케줄은 오늘 날짜와 가장 근접한 예약 항목 최대 3개를 내려준다. - 예약 라이브는 `LiveRoomStatus.RESERVATION` 의미와 동일하게, `LiveRoom.beginDateTime > now`이고 활성 상태인 라이브를 대상으로 한다. -- 예약 업로드 오디오 콘텐츠는 `AudioContent.releaseDate > now`인 활성 또는 예약 상태 콘텐츠를 대상으로 한다. +- 예약 업로드 오디오 콘텐츠는 `AudioContent.duration != null`, `AudioContent.releaseDate != null`, `AudioContent.releaseDate > now`인 콘텐츠를 대상으로 한다. `AudioContent.isActive` 값은 예약 스케줄 후보 판정에 사용하지 않는다. - 응답에는 예약 날짜/시간 UTC, 제목, 타입, 대상 ID를 포함한다. - 타입 값은 기존 추천 페이지의 `RecommendedActivityType` 코드 체계를 사용한다. - 구현 시 `RecommendedActivityType`은 `CreatorActivityType`으로 이름을 변경하고 공용 패키지로 이동한다. @@ -160,6 +169,8 @@ - 정렬은 예약 날짜/시간 오름차순이다. 같은 예약 시간이면 라이브를 오디오보다 먼저 표시한다. - 성인 예약 라이브/오디오는 조회자의 성인 노출 정책이 false이면 노출하지 않는다. - 성인 노출 정책은 DB 조회 조건에 먼저 반영하고, 라이브/오디오 스케줄 후보를 service에서 합친 뒤에도 최종 응답 전 한 번 더 보정한다. +- 예약 라이브 스케줄은 기존 예약 라이브 목록 정책과 동일하게 성별 제한(`LiveRoom.genderRestriction`)과 크리에이터 입장 제한(`LiveRoom.isAvailableJoinCreator`)을 반영한다. +- 예약 라이브 성별 제한 판단에 사용하는 조회자 성별은 기존 라이브 목록과 동일하게 `Auth.gender`가 있으면 이를 우선하고, 없으면 `Member.gender`를 사용하는 effective gender다. - service 최종 보정에 필요한 성인 여부는 내부 스케줄 후보 record/domain model에만 포함하고, 공개 스케줄 응답 필드에는 포함하지 않는다. #### Edge Cases @@ -171,6 +182,7 @@ - 최근 업로드된 오디오 콘텐츠를 최대 9개 내려준다. - 신규 오디오 콘텐츠 영역과 오디오 목록 영역의 첫 번째 항목이 겹치지 않도록, 오디오 목록에서는 Feature D의 `latestAudioContent`로 내려간 가장 최신 콘텐츠를 제외한다. - 예약 업로드 전 콘텐츠는 포함하지 않는다. +- `releaseDate == null`인 오디오 콘텐츠는 목록, 최신 콘텐츠, 첫 콘텐츠 판정에서 제외한다. - 응답에는 다음 값을 포함한다. - 오디오 콘텐츠 ID - 제목 @@ -195,7 +207,9 @@ #### Requirements - 시리즈는 최대 8개를 내려준다. - 정렬은 각 시리즈에 속한 공개 콘텐츠의 최신 공개 시각 내림차순이다. -- 응답에는 기존 시리즈 카드 구성에 필요한 시리즈 ID, 제목, 커버 이미지, 연재 요일, 완결 여부, 콘텐츠 개수, 신규/인기 표시 정보를 포함한다. +- 응답에는 기존 시리즈 카드 구성에 필요한 시리즈 ID, 제목, 커버 이미지, 콘텐츠 개수, 신규 표시, 오리지널 시리즈 여부를 포함한다. +- 시리즈 응답에는 `publishedDaysOfWeek`, `isComplete`, `isPopular`를 포함하지 않는다. +- 시리즈의 공개 콘텐츠 집계와 정렬에서도 `releaseDate == null`인 오디오 콘텐츠는 제외한다. - 성인 콘텐츠 노출 정책과 조회자 콘텐츠 타입 선호 정책은 기존 `ContentSeriesService.getSeriesList` 정책을 따른다. #### Edge Cases @@ -209,9 +223,11 @@ - 최대 3개를 최신순으로 내려준다. - 응답에는 게시글 ID, 크리에이터 ID, 크리에이터 닉네임, 크리에이터 프로필 이미지, 이미지 URL, 오디오 URL, 본문, 가격, 작성 시각 UTC, 구매 여부, 좋아요 수, 댓글 수를 포함한다. - 홈 응답에 포함하는 게시글 요약 필드는 기존 크리에이터 커뮤니티 전체보기 게시글 리스트와 같은 의미를 유지한다. +- 유료 커뮤니티 게시글은 조회자가 이미 구매했다면 크리에이터가 이후 삭제해 `isActive == false`가 되어도 기존 커뮤니티 전체보기 의미와 동일하게 구매자에게 조회된다. #### Edge Cases - 고정 공지는 커뮤니티 섹션에 중복 노출하지 않는다. +- 성인 커뮤니티 글은 조회자의 성인 콘텐츠 노출 정책을 따른다. 조회자가 해당 글을 구매했더라도 성인 콘텐츠 노출 정책이 false이면 노출하지 않는다. - 커뮤니티 게시글이 없으면 빈 배열을 내려준다. ### Feature K. 팬 Talk @@ -245,7 +261,11 @@ - 라이브 누적 참여자 - 업로드한 오디오 콘텐츠 개수 - 시리즈 개수 -- 데뷔일은 첫 라이브 시작 시각과 첫 공개 오디오 콘텐츠 공개 시각 중 빠른 값으로 계산한다. +- 데뷔일은 첫 라이브 시작 시각과 첫 오디오 데뷔 후보 시각 중 빠른 값으로 계산한다. +- 오디오 데뷔 후보는 오디오 업로드 순서(`createdAt`, 동일 시각이면 `id`) 기준 첫 3개만 본다. +- 첫 번째 또는 두 번째 오디오가 삭제되어 `releaseDate == null`이면 다음 업로드 오디오의 공개 시각을 후보로 본다. +- 세 번째 오디오가 삭제되어 `releaseDate == null`이면 네 번째 오디오로 넘어가지 않고 세 번째 오디오의 `createdAt`을 후보로 본다. +- 세 번째 오디오까지 공개 후보가 없고 세 번째 삭제 오디오도 없으면 오디오 데뷔 후보는 없다. - 라이브 진행 횟수, 라이브 누적 진행 시간, 라이브 누적 참여자는 기존 `ExplorerQueryRepository.getLiveCount`, `getLiveTime`, `getLiveContributorCount`의 의미를 기준으로 한다. - 업로드한 오디오 콘텐츠 개수는 공개된 오디오 콘텐츠만 포함하고 예약 업로드는 반영하지 않는다. - 시리즈 개수는 크리에이터의 활성 시리즈 개수를 기준으로 한다.