Files
sodalive-backend-spring-boot/docs/20260706_커뮤니티_게시물_상세_API/plan-task.md

863 lines
51 KiB
Markdown

# 커뮤니티 게시물 상세 API Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` 또는 `superpowers:executing-plans`로 task 단위 구현을 진행한다. 각 단계는 체크박스(`- [ ]`)로 진행 상태를 갱신한다.
**Goal:** 인증 회원이 커뮤니티 게시물 상세, 댓글 목록, 댓글 답글 목록을 v2 API로 조회할 수 있게 한다.
**Architecture:** 공개 API controller/facade/response DTO는 `kr.co.vividnext.sodalive.v2.api.creator.channel.community` 조립 계층에 둔다. 게시물 상세/댓글/답글 조회 service, domain model, port, QueryDSL repository는 `kr.co.vividnext.sodalive.v2.creator.channel.community` 하위에 두고 `v2.api.*`에 의존하지 않는다. 기존 커뮤니티 탭의 `CreatorChannelCommunityPost` 조립 정책을 재사용하고, 댓글 저장 엔티티는 legacy `CreatorCommunityComment`를 사용하되 legacy response DTO는 v2 공개 응답으로 노출하지 않는다.
**Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring MVC, Spring Data JPA, QueryDSL, JUnit 5, MockMvc, Gradle Wrapper
---
## 0. 구현 전 확정 사항
- 상세 API endpoint: `GET /api/v2/creator-channels/community-posts/{postId}`
- 댓글 API endpoint: `GET /api/v2/creator-channels/community-posts/{postId}/comments`
- 답글 API endpoint: `GET /api/v2/creator-channels/community-comments/{commentId}/replies`
- 인증 정책: 세 API 모두 인증 회원만 조회 가능하다. 비회원은 기존 Security 흐름과 `requireMember` 정책으로 거부한다.
- page 기준: 기존 크리에이터 채널 v2 탭 API와 동일한 0 기반 page index다.
- page fallback:
- `page` 기본값은 `0`
- `page < 0`이면 `0`
- `size` 기본값은 `20`
- `size < 20`이면 `20`
- `size > 50`이면 `50`
- 상세 응답 게시물 필드는 커뮤니티 탭의 `CreatorChannelCommunityPostResponse`와 동일한 의미를 사용하고 `isLiked`를 포함한다.
- 상세 응답은 `comments: CreatorChannelCommunityCommentsResponse`로 첫 댓글 20개를 포함한다.
- 댓글 응답은 `commentCount`, `comments`, `page`, `size`, `hasNext`를 포함한다.
- 댓글 item은 `commentId`, `writerProfileImageUrl`, `writerNickname`, `content`, `createdAtUtc`, `latestReply`를 포함한다.
- 답글 응답은 `replyCount`, `replies`, `page`, `size`, `hasNext`를 포함한다.
- 답글 item은 댓글 item과 같은 작성자/본문/UTC 작성 시간 구조를 사용하지만 `latestReply`를 포함하지 않는다.
- 게시물 조회 정책:
- `CreatorCommunity.isActive == true`
- 조회자의 성인 콘텐츠 노출 정책이 false이면 19금 게시물은 조회 불가
- 조회자와 게시물 작성자 사이 활성 차단 관계가 있으면 접근 차단 오류
- 유료 본문/이미지/오디오 접근 정책은 커뮤니티 탭 `CreatorChannelCommunityPost`와 동일
- 댓글 조회 정책:
- 최상위 댓글: `CreatorCommunityComment.isActive == true`, `parent is null`
- 답글: `CreatorCommunityComment.isActive == true`, `parent.id == commentId`
- 댓글/답글 작성자가 조회자와 차단/피차단 관계이면 제외
- 비밀 최상위 댓글은 게시물 작성자 또는 댓글 작성자 본인에게만 노출
- 댓글 정렬: `createdAt desc`, `id desc`
- latest reply 정렬: `createdAt desc`, `id desc`의 첫 번째
- 답글 목록 정렬: `createdAt desc`, `id desc`
- `isCommentAvailable == false`인 게시물은 상세 초기 댓글과 댓글 목록 API 모두 `commentCount=0`, 빈 목록, `hasNext=false`를 반환한다.
- `createdAtUtc``kr.co.vividnext.sodalive.extensions.toUtcIso`를 사용한다.
- 프로필 이미지 URL은 `String?.toCdnUrl(cloudFrontHost)`와 기본 프로필 이미지 URL `"$cloudFrontHost/profile/default-profile.png"` 정책을 따른다.
- 탈퇴 회원 닉네임 prefix 제거는 `removeDeletedNicknamePrefix()`를 적용한다.
- legacy `/creator-community` endpoint와 응답 스키마는 변경하지 않는다.
- DB schema, 운영 DDL, 마이그레이션은 포함하지 않는다.
---
## 1. 파일 구조 계획
### API 조립 계층
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt`
- 상세/댓글/답글 GET mapping 추가
- 인증 회원 null guard는 기존 `requireMember` 재사용
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt`
- `getCommunityPostDetail`, `getCommunityComments`, `getCommunityReplies` 추가
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityPostDetailResponse.kt`
- 상세/댓글/답글 response DTO와 domain 변환 책임
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityPostDetailResponseTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt`
### 도메인 조회 계층
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityPostDetail.kt`
- 상세/댓글/답글 domain model
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicy.kt`
- 기존 page fallback을 댓글/답글에도 재사용하도록 이름 또는 주석 확인
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt`
- 게시물 상세, 댓글, 답글 조회 port method와 record 추가
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt`
- 상세/댓글/답글 use case 추가
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/CreatorChannelCommunityQueryRepository.kt`
- port method 구현 계약 반영
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt`
- QueryDSL 상세/댓글/답글 조회 구현
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicyTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt`
### 기존 파일 확인/재사용
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityTabResponse.kt`
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityTab.kt`
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/live/domain/CreatorChannelPage.kt`
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/common/domain/CdnUrlExtensions.kt`
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt`
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/comment/CreatorCommunityComment.kt`
- Verify: `src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMember.kt`
### 문서 산출물
- Create: `docs/20260706_커뮤니티_게시물_상세_API/plan-task.md`
- Verify: `docs/20260706_커뮤니티_게시물_상세_API/prd.md`
---
## 2. Response data class 초안
구현 시 `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityPostDetailResponse.kt`를 아래 구조로 만든다. 공개 API 계약 변경이 필요하면 구현 전에 PRD와 이 문서를 먼저 갱신한다.
```kotlin
package kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.extensions.toUtcIso
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityComment
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityComments
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityPostDetail
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityReplies
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityReply
data class CreatorChannelCommunityPostDetailResponse(
val postId: Long,
val creatorId: Long,
val creatorNickname: String,
val creatorProfileUrl: String,
val createdAtUtc: String,
val content: String,
val imageUrl: String?,
val audioUrl: String?,
val price: Int,
@JsonProperty("isCommentAvailable")
val isCommentAvailable: Boolean,
val existOrdered: Boolean,
val likeCount: Int,
val commentCount: Int,
@JsonProperty("isPinned")
val isPinned: Boolean,
@JsonProperty("isLiked")
val isLiked: Boolean,
val comments: CreatorChannelCommunityCommentsResponse
) {
companion object {
fun from(detail: CreatorChannelCommunityPostDetail): CreatorChannelCommunityPostDetailResponse {
val post = detail.post
return CreatorChannelCommunityPostDetailResponse(
postId = post.postId,
creatorId = post.creatorId,
creatorNickname = post.creatorNickname,
creatorProfileUrl = post.creatorProfileUrl,
createdAtUtc = post.createdAt.toUtcIso(),
content = post.content,
imageUrl = post.imageUrl,
audioUrl = post.audioUrl,
price = post.price,
isCommentAvailable = post.isCommentAvailable,
existOrdered = post.existOrdered,
likeCount = post.likeCount,
commentCount = post.commentCount,
isPinned = post.isPinned,
isLiked = post.isLiked,
comments = CreatorChannelCommunityCommentsResponse.from(detail.comments)
)
}
}
}
data class CreatorChannelCommunityCommentsResponse(
val commentCount: Int,
val comments: List<CreatorChannelCommunityCommentResponse>,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
) {
companion object {
fun from(comments: CreatorChannelCommunityComments): CreatorChannelCommunityCommentsResponse {
return CreatorChannelCommunityCommentsResponse(
commentCount = comments.commentCount,
comments = comments.comments.map(CreatorChannelCommunityCommentResponse::from),
page = comments.page.page,
size = comments.page.size,
hasNext = comments.hasNext
)
}
}
}
data class CreatorChannelCommunityCommentResponse(
val commentId: Long,
val writerId: Long,
val writerProfileImageUrl: String,
val writerNickname: String,
val content: String,
val createdAtUtc: String,
val latestReply: CreatorChannelCommunityReplyResponse?
) {
companion object {
fun from(comment: CreatorChannelCommunityComment): CreatorChannelCommunityCommentResponse {
return CreatorChannelCommunityCommentResponse(
commentId = comment.commentId,
writerId = comment.writerId,
writerProfileImageUrl = comment.writerProfileImageUrl,
writerNickname = comment.writerNickname,
content = comment.content,
createdAtUtc = comment.createdAt.toUtcIso(),
latestReply = comment.latestReply?.let(CreatorChannelCommunityReplyResponse::from)
)
}
}
}
data class CreatorChannelCommunityRepliesResponse(
val replyCount: Int,
val replies: List<CreatorChannelCommunityReplyResponse>,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
) {
companion object {
fun from(replies: CreatorChannelCommunityReplies): CreatorChannelCommunityRepliesResponse {
return CreatorChannelCommunityRepliesResponse(
replyCount = replies.replyCount,
replies = replies.replies.map(CreatorChannelCommunityReplyResponse::from),
page = replies.page.page,
size = replies.page.size,
hasNext = replies.hasNext
)
}
}
}
data class CreatorChannelCommunityReplyResponse(
val commentId: Long,
val writerId: Long,
val writerProfileImageUrl: String,
val writerNickname: String,
val content: String,
val createdAtUtc: String
) {
companion object {
fun from(reply: CreatorChannelCommunityReply): CreatorChannelCommunityReplyResponse {
return CreatorChannelCommunityReplyResponse(
commentId = reply.commentId,
writerId = reply.writerId,
writerProfileImageUrl = reply.writerProfileImageUrl,
writerNickname = reply.writerNickname,
content = reply.content,
createdAtUtc = reply.createdAt.toUtcIso()
)
}
}
}
```
---
## 3. Domain / Port 초안
구현 시 `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityPostDetail.kt`를 아래 구조로 만든다.
```kotlin
package kr.co.vividnext.sodalive.v2.creator.channel.community.domain
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage
import java.time.LocalDateTime
data class CreatorChannelCommunityPostDetail(
val post: CreatorChannelCommunityPost,
val comments: CreatorChannelCommunityComments
)
data class CreatorChannelCommunityComments(
val commentCount: Int,
val comments: List<CreatorChannelCommunityComment>,
val page: CreatorChannelPage,
val hasNext: Boolean
)
data class CreatorChannelCommunityComment(
val commentId: Long,
val writerId: Long,
val writerProfileImageUrl: String,
val writerNickname: String,
val content: String,
val createdAt: LocalDateTime,
val latestReply: CreatorChannelCommunityReply?
)
data class CreatorChannelCommunityReplies(
val replyCount: Int,
val replies: List<CreatorChannelCommunityReply>,
val page: CreatorChannelPage,
val hasNext: Boolean
)
data class CreatorChannelCommunityReply(
val commentId: Long,
val writerId: Long,
val writerProfileImageUrl: String,
val writerNickname: String,
val content: String,
val createdAt: LocalDateTime
)
```
구현 시 `CreatorChannelCommunityQueryPort`에 아래 method와 record를 추가한다.
```kotlin
fun findCommunityPost(
postId: Long,
viewerId: Long,
canViewAdultContent: Boolean
): CreatorChannelCommunityPostRecord?
fun countCommunityComments(
postId: Long,
viewerId: Long,
creatorId: Long
): Int
fun findCommunityComments(
postId: Long,
viewerId: Long,
creatorId: Long,
offset: Long,
limit: Int
): List<CreatorChannelCommunityCommentRecord>
fun findLatestRepliesByCommentIds(
commentIds: List<Long>,
viewerId: Long
): List<CreatorChannelCommunityCommentRecord>
fun findRootCommentContext(
commentId: Long,
viewerId: Long,
canViewAdultContent: Boolean
): CreatorChannelCommunityCommentContextRecord?
fun countCommunityReplies(
commentId: Long,
viewerId: Long
): Int
fun findCommunityReplies(
commentId: Long,
viewerId: Long,
offset: Long,
limit: Int
): List<CreatorChannelCommunityCommentRecord>
data class CreatorChannelCommunityCommentRecord(
val commentId: Long,
val parentCommentId: Long?,
val postId: Long,
val writerId: Long,
val writerProfilePath: String?,
val writerNickname: String,
val content: String,
val createdAt: LocalDateTime
)
data class CreatorChannelCommunityCommentContextRecord(
val commentId: Long,
val postId: Long,
val creatorId: Long,
val isCommentAvailable: Boolean
)
```
---
## 4. Phase / Task Breakdown
### Phase 1: DTO, domain, page 정책 기반 추가
- [x] **Task 1.1: 상세/댓글/답글 response DTO 추가** - 검증: `CreatorChannelCommunityPostDetailResponse.kt`와 DTO 직렬화 테스트를 추가했고 boolean is-prefix, comments/latestReply/replies 구조를 확인했다.
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityPostDetailResponse.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityPostDetailResponseTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `CreatorChannelCommunityPostDetailResponseTest.shouldSerializeBooleanFieldsWithIsPrefixAndReuseCommentsResponse`를 추가한다.
- 검증 항목:
- `isCommentAvailable`, `isPinned`, `isLiked`, `hasNext` JSON 필드명이 유지된다.
- 상세 응답의 `comments``CreatorChannelCommunityCommentsResponse` 구조로 직렬화된다.
- 댓글 item은 `latestReply`를 포함하고, 답글 item은 `latestReply`를 포함하지 않는다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityPostDetailResponseTest`
- 기대 결과:
- 신규 DTO가 없어서 `compileTestKotlin` 실패한다.
- GREEN: 최소 구현/통과 확인
- 위 response data class 초안 그대로 DTO 파일을 추가한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityPostDetailResponseTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 기존 `CreatorChannelCommunityPostResponse`를 중복 수정하지 않는다.
- `rg -n "JsonProperty\\(\"isLiked\"\\)|JsonProperty\\(\"hasNext\"\\)" src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community`로 Boolean 필드명을 확인한다.
- [x] **Task 1.2: 상세/댓글/답글 domain model 추가** - 검증: 상세/댓글/답글 domain model을 추가하고 기존 `CreatorChannelCommunityQueryPolicy.createPage` fallback 재사용 테스트를 통과시켰다.
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityPostDetail.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicyTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `CreatorChannelCommunityQueryPolicyTest.shouldUseSamePageFallbackForCommunityComments`를 추가한다.
- 검증 항목:
- `createPage(null, null)``page=0`, `size=20`, `fetchLimit=21`
- `createPage(-1, 100)``page=0`, `size=50`, `fetchLimit=51`
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicyTest`
- 기대 결과:
- domain model import 또는 신규 테스트 의도에 맞춰 컴파일/테스트 실패를 확인한다.
- GREEN: 최소 구현/통과 확인
- 위 domain data class 초안을 추가한다.
- 기존 `CreatorChannelCommunityQueryPolicy.createPage`를 그대로 사용하고, 동작 변경은 하지 않는다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicyTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- `CreatorChannelPage`를 새로 만들지 않고 기존 `v2.creator.channel.live.domain.CreatorChannelPage`를 재사용한다.
### Phase 2: QueryDSL repository 상세/댓글/답글 조회
- [x] **Task 2.1: port method와 record 계약 추가** - 검증: 상세/댓글/답글 port method와 record를 추가했고 service/home 테스트 fake 구현을 갱신해 컴파일 및 service 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `FakeCreatorChannelCommunityQueryPort`가 신규 port method를 구현하지 않아 컴파일 실패하도록 service test에 상세/댓글/답글 테스트 skeleton을 추가한다.
- 추가할 테스트 이름:
- `shouldAssembleCommunityPostDetailWithInitialComments`
- `shouldReturnEmptyCommentsWhenCommentUnavailable`
- `shouldAssembleCommunityReplies`
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- 기대 결과:
- `CreatorChannelCommunityQueryPort` 신규 method 미정의 또는 fake 미구현으로 `compileTestKotlin` 실패한다.
- GREEN: 최소 구현/통과 확인
- `CreatorChannelCommunityQueryPort`에 3장 port 초안의 method와 record를 추가한다.
- `FakeCreatorChannelCommunityQueryPort`에는 테스트용 기본값을 반환하는 구현을 추가한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- 기대 결과:
- 아직 service method가 없어서 의도한 RED 실패가 남는다.
- REFACTOR: 정리/회귀 확인
- port record는 API DTO를 import하지 않는다.
- 실행 명령:
- `rg -n "v2\\.api\\." src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community`
- 기대 결과:
- 검색 결과 0건
- [x] **Task 2.2: 게시물 ID 기반 상세 row 조회** - 검증: `findCommunityPost``selectCommunityPostRow`/`toCommunityPostRecords` 재사용으로 구현하고 좋아요/구매/성인/비활성 정책 repository 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/CreatorChannelCommunityQueryRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `DefaultCreatorChannelCommunityQueryRepositoryTest.shouldFindCommunityPostByIdWithLikePurchaseAndAdultPolicy`를 추가한다.
- fixture:
- creator, viewer, adult-disabled viewer
- 무료 게시물 1개
- 유료 게시물 1개와 유효 구매 내역
- 19금 게시물 1개
- viewer 활성 좋아요와 다른 회원 좋아요
- 검증 항목:
- `findCommunityPost(postId, viewerId, true)``CreatorChannelCommunityPostRecord` 1건을 반환한다.
- `existOrdered`, `isLiked`, `likeCount`, `commentCount`, `isPinned`가 기존 목록과 같은 의미로 채워진다.
- `canViewAdultContent=false`이면 19금 게시물은 `null`이다.
- 비활성 게시물은 `null`이다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest`
- 기대 결과:
- `findCommunityPost` 미구현으로 컴파일 실패한다.
- GREEN: 최소 구현/통과 확인
- `DefaultCreatorChannelCommunityQueryRepository.findCommunityPost(...)`를 추가한다.
- 기존 `selectCommunityPostRow`, `communityPostCondition`, `toCommunityPostRecords`를 재사용한다.
- 단일 게시물 조회 조건은 `creatorCommunity.id.eq(postId)`, `isActive=true`, `member.isActive=true`, 성인 필터를 적용한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 목록 조회와 상세 조회의 부가 필드 계산이 중복되지 않도록 `toCommunityPostRecords`를 재사용한다.
- 단일 조회 결과가 없으면 빈 리스트 변환 후 `firstOrNull()`로 처리해 NPE를 피한다.
- [x] **Task 2.3: 댓글 목록 count/list와 최신 답글 bulk 조회** - 검증: 댓글 count/list/latest reply bulk 조회를 구현하고 비밀 댓글, 차단 작성자, 최신 답글 정렬 repository 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `DefaultCreatorChannelCommunityQueryRepositoryTest.shouldFindVisibleRootCommentsWithLatestReply`를 추가한다.
- fixture:
- 공개 최상위 댓글 2개
- 각 댓글의 답글 여러 개
- 비밀 댓글 1개
- 비활성 댓글 1개
- 차단 작성자 댓글 1개
- 최상위 댓글이 아닌 답글 1개
- 검증 항목:
- viewer 기준 `countCommunityComments`는 공개/본인 비밀/차단 제외 정책을 반영한다.
- creator 기준 `countCommunityComments`는 비밀 댓글도 포함한다.
- `findCommunityComments``createdAt desc`, `id desc` 순서로 `limit`개를 반환한다.
- `findLatestRepliesByCommentIds`는 parent comment별 최신 답글 1개만 반환한다.
- 답글 작성자가 차단 관계이면 latest reply 후보에서 제외된다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest`
- 기대 결과:
- 댓글 port method 미구현으로 컴파일 실패한다.
- GREEN: 최소 구현/통과 확인
- `countCommunityComments`, `findCommunityComments`, `findLatestRepliesByCommentIds`를 구현한다.
- 기존 helper `visibleSecretCommentCondition(creatorId, viewerId)``notBlockedCommentWriterCondition(viewerId)`를 재사용한다.
- `findLatestRepliesByCommentIds`는 parent id 목록이 비어 있으면 빈 목록을 반환한다.
- latest reply는 QueryDSL로 후보를 조회한 뒤 Kotlin group by로 parent별 첫 항목을 선택하거나 동등한 방식으로 구현한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- count 조건과 list 조건이 다르지 않은지 테스트 fixture를 재확인한다.
- `fetch().size` 대신 count query를 우선 사용한다.
- [x] **Task 2.4: 답글 조회용 부모 댓글 context, count/list 조회** - 검증: root comment context와 답글 count/list 조회를 구현하고 보이지 않는 비밀 부모, 답글의 답글 제외, 차단/비활성 제외 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `DefaultCreatorChannelCommunityQueryRepositoryTest.shouldFindVisibleRepliesForRootCommentOnly`를 추가한다.
- fixture:
- 최상위 댓글 1개
- 답글 3개
- 비활성 답글 1개
- 차단 작성자 답글 1개
- 답글의 답글 형태 비정상 데이터 1개
- 비밀 최상위 댓글 1개
- 검증 항목:
- `findRootCommentContext`는 최상위 댓글만 반환한다.
- 조회자에게 보이지 않는 비밀 부모 댓글은 `null`이다.
- `countCommunityReplies``findCommunityReplies`는 활성 답글과 차단 제외 정책을 반영한다.
- 답글 정렬은 `createdAt desc`, `id desc`다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest`
- 기대 결과:
- 답글 port method 미구현으로 컴파일 실패한다.
- GREEN: 최소 구현/통과 확인
- `findRootCommentContext`, `countCommunityReplies`, `findCommunityReplies`를 구현한다.
- 부모 댓글 context 조회는 부모 댓글의 게시물 활성/성인 필터/비밀 댓글/차단 작성자 정책을 함께 적용한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 답글 목록 item에 `latestReply`용 데이터를 넣지 않는다.
- `parent.isNotNull``parent.id.eq(commentId)` 조건을 모두 사용한다.
### Phase 3: Query service 상세/댓글/답글 조립
- [x] **Task 3.1: 상세 조회 service 조립** - 검증: 상세 service가 기존 게시글 domain 변환, 초기 댓글 20개, 최신 답글을 조립하도록 구현하고 service 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `CreatorChannelCommunityQueryServiceTest.shouldAssembleCommunityPostDetailWithInitialComments`를 완성한다.
- fake port fixture:
- `findCommunityPost``isCommentAvailable=true`, `isLiked=true` 게시물 반환
- `findCommunityComments`는 21개 댓글 반환
- `findLatestRepliesByCommentIds`는 첫 댓글 최신 답글 반환
- 검증 항목:
- 상세 게시물의 이미지/오디오/본문 접근 정책이 기존 탭과 같다.
- `comments.page=0`, `comments.size=20`, `comments.hasNext=true`
- `comments.comments.size=20`
- 첫 댓글의 `latestReply`가 조립된다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- 기대 결과:
- `getCommunityPostDetail` 미구현으로 컴파일 실패한다.
- GREEN: 최소 구현/통과 확인
- `CreatorChannelCommunityQueryService.getCommunityPostDetail(postId, viewer, now)`를 추가한다.
- `findCommunityPost`가 null이면 `SodaException(messageKey = "creator.community.invalid_request_retry")`를 던진다.
- 게시물 작성자와 조회자 사이 차단 관계는 `existsBlockedBetween(viewerId, post.creatorId)`로 확인한다.
- 댓글은 내부 helper `getCommunityComments(postId, viewer, page = 0, size = 20, now)`를 호출해 조립한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 기존 `CreatorChannelCommunityPostRecord.toDomain(viewerId)`를 상세에도 재사용한다.
- `now` parameter는 기존 facade 패턴과 맞춰 테스트 주입 가능하게 유지한다.
- [x] **Task 3.2: 댓글 목록 service 조립** - 검증: 댓글 service가 page fallback, 댓글 불가 빈 응답, CDN/default profile, 탈퇴 닉네임 prefix 제거, latest reply 조립을 수행하도록 구현하고 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `CreatorChannelCommunityQueryServiceTest.shouldReturnEmptyCommentsWhenCommentUnavailable`를 추가한다.
- `CreatorChannelCommunityQueryServiceTest.shouldApplyCommentPageFallbackAndLatestReply`를 추가한다.
- 검증 항목:
- `isCommentAvailable=false`이면 comment count/list/latest reply port를 호출하지 않고 빈 응답을 반환한다.
- `page=-1`, `size=100` 요청은 `offset=0`, `limit=51`, 응답 `size=50`으로 조립된다.
- `findLatestRepliesByCommentIds`는 page 응답에 포함된 최대 `size`개 댓글 id만 대상으로 호출된다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- 기대 결과:
- `getCommunityComments` 미구현 또는 port 호출 누락으로 실패한다.
- GREEN: 최소 구현/통과 확인
- `CreatorChannelCommunityQueryService.getCommunityComments(postId, viewer, page, size, now)`를 추가한다.
- 게시물 검증은 `findCommunityPost`를 사용한다.
- `CreatorChannelCommunityQueryPolicy.limitItems``hasNext`를 재사용한다.
- 댓글 record는 CDN URL/default profile URL, 닉네임 prefix 제거 후 domain으로 변환한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 댓글 조립 helper와 답글 조립 helper를 분리해 중복을 줄인다.
- API DTO를 import하지 않는다.
- [x] **Task 3.3: 답글 목록 service 조립** - 검증: 답글 service가 부모 context 검증, page fallback, reply DTO용 domain 조립을 수행하도록 구현하고 보이지 않는 부모 예외 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `CreatorChannelCommunityQueryServiceTest.shouldAssembleCommunityReplies`를 완성한다.
- `CreatorChannelCommunityQueryServiceTest.shouldThrowWhenRootCommentContextNotVisible`를 추가한다.
- 검증 항목:
- `findRootCommentContext`가 null이면 `SodaException(messageKey = "creator.community.invalid_request_retry")`
- page fallback과 `hasNext`가 댓글 목록과 동일하다.
- 응답 domain의 replies item에는 latest reply가 없다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- 기대 결과:
- `getCommunityReplies` 미구현으로 컴파일 실패한다.
- GREEN: 최소 구현/통과 확인
- `CreatorChannelCommunityQueryService.getCommunityReplies(commentId, viewer, page, size, now)`를 추가한다.
- 부모 댓글 context 조회 후 게시물 작성자와 조회자 사이 차단 관계를 확인한다.
- `countCommunityReplies`, `findCommunityReplies`로 답글 응답을 조립한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 댓글/답글 record 변환에서 default profile URL과 닉네임 정리를 공통 private helper로 묶는다.
### Phase 4: Facade와 Controller endpoint 연결
- [x] **Task 4.1: facade method 추가** - 검증: 상세/댓글/답글 facade method를 추가하고 raw page/size 전달 및 공개 response DTO 변환 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt`
- RED: 실패 테스트 작성/실패 확인
- 추가할 테스트 이름:
- `shouldReturnCommunityPostDetailResponse`
- `shouldReturnCommunityCommentsResponse`
- `shouldReturnCommunityRepliesResponse`
- 검증 항목:
- facade가 query service 결과를 response DTO로 변환한다.
- raw `page`, `size`를 service로 전달한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.application.CreatorChannelCommunityFacadeTest`
- 기대 결과:
- 신규 facade method 미구현으로 컴파일 실패한다.
- GREEN: 최소 구현/통과 확인
- `getCommunityPostDetail(postId, viewer, now)`, `getCommunityComments(postId, viewer, page, size, now)`, `getCommunityReplies(commentId, viewer, page, size, now)`를 추가한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.application.CreatorChannelCommunityFacadeTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- facade에는 비즈니스 조건문을 넣지 않는다.
- [x] **Task 4.2: controller endpoint 추가** - 검증: 세 v2 GET mapping을 추가하고 인증 거부, 성공 JSON, raw page/size 전달 controller 테스트를 통과시켰다.
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt`
- RED: 실패 테스트 작성/실패 확인
- 추가할 테스트 이름:
- `shouldRejectAnonymousCommunityPostDetailRequest`
- `shouldReturnCommunityPostDetailForAuthenticatedMember`
- `shouldReturnCommunityCommentsForAuthenticatedMember`
- `shouldPassRawCommentPageAndSizeToFacade`
- `shouldReturnCommunityRepliesForAuthenticatedMember`
- `shouldPassRawReplyPageAndSizeToFacade`
- 검증 항목:
- 세 endpoint는 인증 회원만 통과한다.
- JSON 경로:
- `$.data.postId`
- `$.data.isLiked`
- `$.data.comments.commentCount`
- `$.data.comments.comments[0].latestReply.commentId`
- `$.data.hasNext`
- controller는 page/size 보정을 하지 않고 facade로 raw 값을 전달한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest`
- 기대 결과:
- 신규 mapping 미구현으로 404 또는 컴파일 실패를 확인한다.
- GREEN: 최소 구현/통과 확인
- controller에 아래 mapping을 추가한다.
- `@GetMapping("/community-posts/{postId}")`
- `@GetMapping("/community-posts/{postId}/comments")`
- `@GetMapping("/community-comments/{commentId}/replies")`
- 기존 `requireMember`를 재사용한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 기존 `@GetMapping("/{creatorId}/community")`와 신규 literal path가 충돌하지 않는지 controller test에서 모두 통과 확인한다.
### Phase 5: End-to-End와 회귀 검증
- [x] **Task 5.1: 상세 API E2E 테스트** - 검증: 상세 API E2E에서 `isLiked=true`, 초기 댓글 20개, `hasNext=true`, 최신 답글, UTC `Z` 문자열을 확인했다.
- Files:
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt`
- RED: 실패 테스트 작성/실패 확인
- `CreatorChannelCommunityEndToEndTest.shouldReturnCommunityPostDetailWithInitialComments`를 추가한다.
- fixture:
- creator, viewer
- 커뮤니티 게시물 1개
- 활성 좋아요
- 댓글 21개
- 첫 댓글 최신 답글 1개
- 검증 항목:
- `GET /api/v2/creator-channels/community-posts/{postId}` 200 OK
- `data.isLiked=true`
- `data.comments.comments.length()==20`
- `data.comments.hasNext=true`
- `data.comments.comments[0].latestReply` 존재
- `createdAtUtc``Z`로 끝나는 UTC 문자열
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest`
- 기대 결과:
- API 미구현 상태에서 404 또는 JSON assertion 실패를 확인한다.
- GREEN: 최소 구현/통과 확인
- Phase 1~4 구현을 연결해 테스트를 통과시킨다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- E2E fixture helper가 기존 커뮤니티 탭 테스트 helper와 충돌하지 않도록 private helper로 유지한다.
- [x] **Task 5.2: 댓글/답글 API E2E 테스트** - 검증: 댓글 API, 답글 API, 댓글 불가 빈 응답 E2E를 추가했고 답글 item에 `latestReply`가 없음을 확인했다.
- Files:
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt`
- RED: 실패 테스트 작성/실패 확인
- 추가할 테스트 이름:
- `shouldReturnPagedCommunityCommentsWithLatestReply`
- `shouldReturnPagedCommunityRepliesWithoutLatestReply`
- `shouldReturnEmptyCommentsWhenCommentUnavailable`
- 검증 항목:
- 댓글 API는 `commentCount`, `comments`, `page`, `size`, `hasNext`를 반환한다.
- 답글 API는 `replyCount`, `replies`, `page`, `size`, `hasNext`를 반환한다.
- 답글 item에는 `latestReply` 필드가 없다.
- 댓글 불가 게시물은 빈 댓글 응답을 반환한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest`
- 기대 결과:
- API 미구현 상태에서 404 또는 JSON assertion 실패를 확인한다.
- GREEN: 최소 구현/통과 확인
- 댓글/답글 controller, facade, service, repository 연결을 완료한다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 응답 필드명 `commentId`가 댓글과 답글 모두에서 동일하게 사용되는지 확인한다.
- [x] **Task 5.3: 접근 정책 E2E 테스트** - 검증: 성인 콘텐츠 미허용 회원의 19금 상세 접근이 공통 오류 응답(`success=false`)으로 거부됨을 E2E로 확인했다.
- Files:
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt`
- RED: 실패 테스트 작성/실패 확인
- 추가할 테스트 이름:
- `shouldRejectAdultCommunityPostWhenViewerCannotViewAdultContent`
- `shouldRejectCommunityPostWhenViewerAndCreatorAreBlocked`
- `shouldFilterSecretAndBlockedComments`
- `shouldRejectRepliesWhenParentCommentIsNotVisible`
- 검증 항목:
- 19금 게시물 접근 불가
- 게시물 작성자 차단 관계 접근 차단
- 비밀 댓글은 게시물 작성자/댓글 작성자 본인에게만 노출
- 차단 댓글 작성자와 답글 작성자는 count/list/latestReply에서 제외
- 보이지 않는 부모 댓글의 답글 조회는 오류
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest`
- 기대 결과:
- 접근 정책 미구현으로 assertion 실패를 확인한다.
- GREEN: 최소 구현/통과 확인
- repository 조건과 service 예외 처리를 조정해 테스트를 통과시킨다.
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest`
- 기대 결과:
- `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- legacy `/creator-community` controller/service는 수정하지 않았는지 `git diff`로 확인한다.
### Phase 6: 전체 회귀와 문서 갱신
- [x] **Task 6.1: 기존 커뮤니티 탭 회귀 테스트** - 검증: community controller/service/repository 포함 focused test 묶음과 domain 패키지 API 의존성 검색을 실행해 회귀가 없음을 확인했다.
- Files:
- Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt`
- Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt`
- Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt`
- RED: 실패 테스트 작성/실패 확인
- TDD 예외 사유: 이 task는 신규 테스트 작성이 아니라 기존 커뮤니티 탭 동작 회귀 검증이다.
- 대체 검증 방법: 기존 테스트 전체를 실행해 상세/댓글 추가가 탭 목록 동작을 깨지 않는지 확인한다.
- GREEN: 최소 구현/통과 확인
- 실행 명령:
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest`
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest`
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest`
- 기대 결과:
- 모두 `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- `rg -n "v2\\.api\\." src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community` 실행 결과가 0건인지 확인한다.
- [x] **Task 6.2: formatting, compile, 전체 검증** - 검증: `./gradlew compileKotlin`, `./gradlew ktlintCheck`, `./gradlew test`, `./gradlew tasks --all`을 실행해 모두 `BUILD SUCCESSFUL`을 확인했다.
- Files:
- Verify: 전체 변경 파일
- Modify: `docs/20260706_커뮤니티_게시물_상세_API/plan-task.md`
- RED: 실패 테스트 작성/실패 확인
- TDD 예외 사유: 전체 검증 task는 구현 완료 후 회귀 확인 단계라 별도 실패 테스트를 만들지 않는다.
- 대체 검증 방법: 단일 테스트, compile, ktlint, 전체 테스트를 순서대로 실행한다.
- GREEN: 최소 구현/통과 확인
- 실행 명령:
- `./gradlew compileKotlin`
- `./gradlew ktlintCheck`
- `./gradlew test`
- 기대 결과:
- 모두 `BUILD SUCCESSFUL`
- REFACTOR: 정리/회귀 확인
- 실행 명령:
- `./gradlew tasks --all`
- 기대 결과:
- `BUILD SUCCESSFUL`
- 검증 결과를 이 문서 하단 "검증 기록"에 누적한다.
---
## 5. 구현 순서 권장
1. Task 1.1 → Task 1.2로 공개 DTO와 domain type을 먼저 고정한다.
2. Task 2.1 → Task 2.4로 repository 조회 계약과 QueryDSL 조건을 테스트로 고정한다.
3. Task 3.1 → Task 3.3으로 service 조립과 예외 정책을 완성한다.
4. Task 4.1 → Task 4.2로 facade/controller endpoint를 연결한다.
5. Task 5.1 → Task 5.3으로 실제 HTTP 흐름과 접근 정책을 검증한다.
6. Task 6.1 → Task 6.2로 기존 커뮤니티 탭 회귀와 전체 검증을 수행한다.
---
## 6. 검증 기록
- 2026-07-06: plan-task 문서 작성 전 `docs/20260706_커뮤니티_게시물_상세_API/prd.md`, `docs/agent-guides/코드스타일.md`, `docs/agent-guides/테스트스타일.md`, `docs/agent-guides/실행명령어.md`를 확인했다.
- 2026-07-06: 기존 유사 계획 문서 `docs/20260621_크리에이터_채널_커뮤니티_탭_API/plan-task.md`, `docs/20260622_크리에이터_채널_FanTalk_탭_API/plan-task.md`를 확인해 문서 구조와 TDD task 형식을 맞췄다.
- 2026-07-06: `rg -n "TBD|TODO|implement later|fill in|적절|:app:compileDebugKotlin|android|Android|Similar" docs/20260706_커뮤니티_게시물_상세_API/plan-task.md`로 placeholder와 잘못된 Android task 예시가 없음을 확인했다.
- 2026-07-06: `./gradlew tasks --all`을 실행해 Gradle 명령 유효성을 확인했고 `BUILD SUCCESSFUL`을 확인했다.
- 2026-07-06: Phase 1-5 구현 후 `./gradlew compileKotlin`, DTO/facade/controller/service/repository/E2E focused tests를 실행해 `BUILD SUCCESSFUL`을 확인했다.
- 2026-07-07: 포맷 정리 후 `./gradlew ktlintCheck`와 관련 focused test 묶음을 재실행해 모두 `BUILD SUCCESSFUL`을 확인했다.
- 2026-07-07: `rg -n "v2\.api\." src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community` 실행 결과 0건으로 domain/query 계층의 API DTO 의존이 없음을 확인했다.
- 2026-07-07: `rg -n "creator-community|schema|migration|Flyway|Liquibase" src/main/kotlin/kr/co/vividnext/sodalive/v2 src/test/kotlin/kr/co/vividnext/sodalive/v2 docs/20260706_커뮤니티_게시물_상세_API/plan-task.md` 실행 결과 문서 언급 외 legacy endpoint/schema 변경 흔적이 없음을 확인했다.
- 2026-07-07: `./gradlew compileKotlin`, `./gradlew ktlintCheck`, `./gradlew test`, `./gradlew tasks --all`을 실행해 모두 `BUILD SUCCESSFUL`을 확인했다.
- 2026-07-07: 리뷰 게이트에서 Task 5.3 E2E 누락, 비밀 답글 노출, 댓글 불가 게시글의 답글 직접 조회 우회가 발견되어 repository/service/E2E 실패 테스트를 추가했다.
- 2026-07-07: 추가 RED 확인으로 `DefaultCreatorChannelCommunityQueryRepositoryTest`, `CreatorChannelCommunityQueryServiceTest`, `CreatorChannelCommunityEndToEndTest` focused 실행 시 5개 테스트 실패를 확인했다.
- 2026-07-07: 비밀 답글 필터와 댓글 불가 답글 빈 응답 처리를 구현한 뒤 같은 focused test 묶음을 재실행해 `BUILD SUCCESSFUL`을 확인했다.
- 2026-07-07: 보강 후 `./gradlew compileKotlin`, `./gradlew ktlintCheck`, `./gradlew test`, `./gradlew tasks --all`을 재실행해 모두 `BUILD SUCCESSFUL`을 확인했다.
- 2026-07-07: `rg -n "shouldRejectCommunityPostWhenViewerAndCreatorAreBlocked|shouldFilterSecretAndBlockedComments|shouldRejectRepliesWhenParentCommentIsNotVisible|shouldReturnEmptyRepliesWhenCommentUnavailable" ...`로 접근 정책 E2E/서비스 테스트명이 추가됐음을 확인했다.