Files

51 KiB

커뮤니티 게시물 상세 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를 반환한다.
  • createdAtUtckr.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와 이 문서를 먼저 갱신한다.

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,
    @JsonProperty("isSecret")
    val isSecret: Boolean,
    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,
                isSecret = comment.isSecret,
                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를 아래 구조로 만든다.

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 isSecret: Boolean,
    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를 추가한다.

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 isSecret: Boolean,
    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 정책 기반 추가

  • 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 필드명이 유지된다.
        • 상세 응답의 commentsCreatorChannelCommunityCommentsResponse 구조로 직렬화된다.
        • 댓글 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 필드명을 확인한다.
  • 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 상세/댓글/답글 조회

  • 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건
  • Task 2.2: 게시물 ID 기반 상세 row 조회 - 검증: findCommunityPostselectCommunityPostRow/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를 피한다.
  • 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는 비밀 댓글도 포함한다.
        • findCommunityCommentscreatedAt 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를 우선 사용한다.
  • 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이다.
        • countCommunityRepliesfindCommunityReplies는 활성 답글과 차단 제외 정책을 반영한다.
        • 답글 정렬은 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.isNotNullparent.id.eq(commentId) 조건을 모두 사용한다.

Phase 3: Query service 상세/댓글/답글 조립

  • 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:
        • findCommunityPostisCommentAvailable=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 패턴과 맞춰 테스트 주입 가능하게 유지한다.
  • 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.limitItemshasNext를 재사용한다.
      • 댓글 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하지 않는다.
  • 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 연결

  • 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에는 비즈니스 조건문을 넣지 않는다.
  • 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와 회귀 검증

  • 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 존재
        • createdAtUtcZ로 끝나는 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로 유지한다.
  • 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가 댓글과 답글 모두에서 동일하게 사용되는지 확인한다.
  • 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: 전체 회귀와 문서 갱신

  • 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건인지 확인한다.
  • 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/서비스 테스트명이 추가됐음을 확인했다.