Files
sodalive-backend-spring-boot/docs/20260621_크리에이터_채널_커뮤니티_탭_API/plan-task.md

51 KiB

크리에이터 채널 커뮤니티 탭 API Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development 또는 superpowers:executing-plans로 task 단위 구현을 진행한다. 각 단계는 체크박스(- [ ])로 진행 상태를 갱신한다.

Goal: 인증 회원이 GET /api/v2/creator-channels/{creatorId}/community로 크리에이터 채널 커뮤니티 탭의 조회 가능한 전체 게시글 개수와 페이징된 게시글 목록을 조회할 수 있게 한다.

Architecture: 공개 API controller/facade/response DTO는 kr.co.vividnext.sodalive.v2.api.creator.channel.community 조립 계층에 둔다. 커뮤니티 게시글 조회 service, page/content masking 정책, tab domain model, port, QueryDSL repository는 kr.co.vividnext.sodalive.v2.creator.channel.community 하위에 두고 v2.api.*에 의존하지 않는다. 홈 API는 홈 repository에 커뮤니티 조회 쿼리를 직접 두지 않고, 분리된 커뮤니티 조회 도메인의 홈 요약 조회 메서드를 호출해 기존 notices, communities 응답 계약을 유지한다.

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/{creatorId}/community
  • 인증 정책: 인증 회원만 조회 가능. 비회원은 기존 Security 흐름과 requireMember 정책으로 거부한다.
  • request:
    • path variable: creatorId
    • query parameter: page, required = false, 기본값 0, page < 0이면 0으로 fallback
    • query parameter: size, required = false, 기본값 20, size < 20이면 20, size > 50이면 50으로 fallback
  • response:
    • communityPostCount: 조회자가 조회 가능한 커뮤니티 게시글 전체 개수
    • communityPosts: 커뮤니티 게시글 목록
    • page: fallback 보정 후 실제 적용된 page index
    • size: fallback 보정 후 실제 적용된 page size
    • hasNext: 다음 page 존재 여부
  • community post item:
    • postId, creatorId, creatorNickname, creatorProfileUrl, createdAtUtc, content, imageUrl, audioUrl, price, isCommentAvailable, existOrdered, likeCount, commentCount, isPinned
  • 공개 게시글 기준: CreatorCommunity.isActive == true, CreatorCommunity.member.id == creatorId, CreatorCommunity.member.isActive == true.
  • 성인 콘텐츠는 조회자의 성인 콘텐츠 노출 정책이 false이면 목록과 count에서 제외한다.
  • 성인 콘텐츠 필터는 구매 여부보다 우선한다. 조회자가 19금 게시글을 구매했거나 작성자여도 성인 콘텐츠 노출 정책이 false이면 제외한다.
  • 목록 정렬:
    • 고정 게시글을 먼저 노출한다.
    • 고정 게시글 사이의 정렬은 fixedAt desc, id desc다.
    • 일반 게시글 사이의 정렬은 createdAt desc, id desc다.
    • 고정 게시글과 일반 게시글은 하나의 목록으로 페이징한다.
  • communityPostCount는 고정 게시글과 일반 게시글을 모두 포함한 전체 개수다.
  • createdAtUtc는 게시글 생성 시각을 UTC 기준 ISO-8601 문자열로 내려준다. 구현 전 재사용 가능한 toUtcIso 확장함수를 검색하고, public 확장함수가 있으면 신규 생성 없이 import해서 사용한다.
  • 문서 작성 시점 확인 결과 toUtcIso는 일부 DTO의 private/internal 확장함수로만 존재하고, 공용 확장 파일인 kr.co.vividnext.sodalive.extensions.LocalDateTimeExtensions.kt에는 없다. 구현 시점에도 public 확장함수가 없으면 이 공용 확장 파일에 fun LocalDateTime.toUtcIso(): String을 추가하고 커뮤니티 DTO에서 import한다.
  • creatorProfileUrlCreatorCommunity.member.profileImageString?.toCdnUrl(cloudFrontHost)로 변환하고, 없으면 기존 홈 API의 기본 프로필 이미지 URL을 내려준다.
  • existOrdered는 조회자가 게시글 작성자이면 true, 조회자가 유효 구매 내역을 가지고 있으면 true, 그 외에는 false로 내려준다.
  • imageUrlCreatorCommunity.imagePath가 있고 이미지 접근 권한이 있을 때만 String?.toCdnUrl(cloudFrontHost)로 변환한다. 경로가 없거나 blank이면 null이다.
  • legacy 커뮤니티 목록은 유료 미구매 게시글의 이미지를 노출했지만, 커뮤니티 탭 API는 유료 미구매 게시글의 imageUrlaudioUrl과 동일하게 null로 내려준다.
  • 이미지는 signed URL을 생성하지 않고 기존 CDN URL 조합 정책만 사용한다.
  • audioUrlCreatorCommunity.audioPath가 있고 접근 권한이 있을 때만 AudioContentCloudFront.generateSignedURL(resourcePath, 1000 * 60 * 30) 결과를 내려준다.
  • 이미지/오디오 접근 권한:
    • 무료 게시글이면 접근 가능
    • 유료 게시글이고 조회자가 게시글 작성자이면 접근 가능
    • 유료 게시글이고 조회자가 CanUsage.PAID_COMMUNITY_POST, isRefund == false 구매 내역을 가지면 접근 가능
    • 그 외에는 imageUrl == null, audioUrl == null
  • 유료 게시글 본문은 기존 홈 API/legacy 커뮤니티 정책과 같은 마스킹을 적용한다.
    • 접근 가능하면 원문
    • 접근 불가이고 길이가 15 code point 초과이면 앞 15 code point + ...
    • 접근 불가이고 길이가 15 code point 이하이면 앞 절반 code point + ...
  • commentCountisCommentAvailable == false이면 0이다.
  • commentCount는 활성 최상위 댓글만 세고, 기존 커뮤니티 댓글의 차단 관계와 비밀 댓글 노출 정책을 적용한다.
  • likeCount는 활성 좋아요 수만 센다.
  • legacy /creator-community 공개 endpoint는 변경하지 않는다.
  • 홈 API 공개 응답 스키마는 변경하지 않는다.

1. 파일 구조 계획

커뮤니티 탭 신규 API 조립 계층

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityTabResponse.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt

커뮤니티 도메인 조회 계층

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityTab.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicy.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/CreatorChannelCommunityQueryRepository.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicyTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt

홈 API 커뮤니티 조회 분리 대상

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt
  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt
  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt
  • Verify: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.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
  • Modify/Verify: src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt
  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt
  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/CreatorCommunity.kt
  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/comment/CreatorCommunityCommentRepository.kt
  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/creatorCommunity/like/CreatorCommunityLikeRepository.kt
  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt
  • Verify: src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt

문서 산출물

  • Create: docs/20260621_크리에이터_채널_커뮤니티_탭_API/plan-task.md
  • Verify: docs/20260621_크리에이터_채널_커뮤니티_탭_API/prd.md

2. Response data class 초안

구현 시 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityTabResponse.kt에 아래 DTO를 기준으로 추가한다. 필드명은 공개 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.CreatorChannelCommunityPost
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityTab

data class CreatorChannelCommunityTabResponse(
    val communityPostCount: Int,
    val communityPosts: List<CreatorChannelCommunityPostResponse>,
    val page: Int,
    val size: Int,
    @JsonProperty("hasNext")
    val hasNext: Boolean
) {
    companion object {
        fun from(tab: CreatorChannelCommunityTab): CreatorChannelCommunityTabResponse {
            return CreatorChannelCommunityTabResponse(
                communityPostCount = tab.communityPostCount,
                communityPosts = tab.communityPosts.map(CreatorChannelCommunityPostResponse::from),
                page = tab.page.page,
                size = tab.page.size,
                hasNext = tab.hasNext
            )
        }
    }
}

data class CreatorChannelCommunityPostResponse(
    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
) {
    companion object {
        fun from(post: CreatorChannelCommunityPost): CreatorChannelCommunityPostResponse {
            return CreatorChannelCommunityPostResponse(
                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
            )
        }
    }
}

3. Domain / Port 초안

구현 시 아래 형태를 기준으로 추가한다. API DTO가 domain model을 참조하지만 domain/port는 API DTO를 참조하지 않는다.

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 CreatorChannelCommunityTab(
    val communityPostCount: Int,
    val communityPosts: List<CreatorChannelCommunityPost>,
    val page: CreatorChannelPage,
    val hasNext: Boolean
)

data class CreatorChannelCommunityPost(
    val postId: Long,
    val creatorId: Long,
    val creatorNickname: String,
    val creatorProfileUrl: String,
    val imageUrl: String?,
    val audioUrl: String?,
    val content: String,
    val price: Int,
    val createdAt: LocalDateTime,
    val existOrdered: Boolean,
    val isCommentAvailable: Boolean,
    val likeCount: Int,
    val commentCount: Int,
    val isPinned: Boolean
)
package kr.co.vividnext.sodalive.v2.creator.channel.community.port.out

import kr.co.vividnext.sodalive.member.MemberRole
import java.time.LocalDateTime

interface CreatorChannelCommunityQueryPort {
    fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCommunityCreatorRecord?
    fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
    fun countCommunityPosts(
        creatorId: Long,
        viewerId: Long,
        canViewAdultContent: Boolean
    ): Int
    fun findCommunityPosts(
        creatorId: Long,
        viewerId: Long,
        canViewAdultContent: Boolean,
        offset: Long,
        limit: Int
    ): List<CreatorChannelCommunityPostRecord>
    fun findHomeCommunityPosts(
        creatorId: Long,
        viewerId: Long,
        isPinned: Boolean,
        canViewAdultContent: Boolean,
        limit: Int
    ): List<CreatorChannelCommunityPostRecord>
}

data class CreatorChannelCommunityCreatorRecord(
    val creatorId: Long,
    val role: MemberRole,
    val nickname: String
)

data class CreatorChannelCommunityPostRecord(
    val postId: Long,
    val creatorId: Long,
    val creatorNickname: String,
    val creatorProfilePath: String?,
    val imagePath: String?,
    val audioPath: String?,
    val content: String,
    val price: Int,
    val createdAt: LocalDateTime,
    val existOrdered: Boolean,
    val isCommentAvailable: Boolean,
    val likeCount: Int,
    val commentCount: Int,
    val isPinned: Boolean
)

4. 작업 계획

Phase 1: 커뮤니티 도메인 계약과 순수 정책 추가

  • Task 1.1: CreatorChannelCommunityQueryPolicy와 domain/port 계약 테스트 작성
    • Files:
      • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicyTest.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityQueryPolicy.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorChannelCommunityTab.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt
    • RED: 아래 케이스를 테스트로 먼저 작성한다.
      • page = null, size = null이면 page=0, size=20, offset=0, fetchLimit=21이다.
      • page = -1, size = 10이면 page=0, size=20, fetchLimit=21이다.
      • page = 2, size = 100이면 page=2, size=50, offset=100, fetchLimit=51이다.
      • limitItemssize만큼만 남기고 hasNextfetched.size > size로 계산한다.
      • 유료 본문 마스킹은 15 code point 초과면 앞 15자 + ..., 15자 이하면 앞 절반 + ...로 계산한다.
      • 무료 게시글, 작성자 본인, 구매자는 유료 본문 원문을 볼 수 있다.
      • domain model과 port record가 Phase 1 계약 필드를 유지한다.
    • RED 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicyTest"
      • 기대 결과: CreatorChannelCommunityQueryPolicy, domain, port 미구현으로 컴파일 실패 또는 테스트 실패
    • GREEN: CreatorChannelPage를 재사용해 page 정책을 만들고, maskPaidContent(content, price, isCreatorSelf, existOrdered) 순수 함수를 추가한다.
    • GREEN 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicyTest"
      • 기대 결과: BUILD SUCCESSFUL
    • REFACTOR: 정책 클래스에는 DB, Spring MVC, API DTO 의존성을 넣지 않는다.
    • 검증 기록:
      • RED: ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicyTest" 실행 결과 CreatorChannelCommunityQueryPolicy, domain, port 미구현 심볼로 compileTestKotlin 실패를 확인했다.
      • GREEN: 같은 명령 재실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 범위: Phase 1의 순수 정책, domain model, port 계약, 계약 테스트만 추가했고 DB/Spring MVC/API DTO 의존성은 넣지 않았다.

Phase 2: QueryDSL repository 분리와 조회 정책 구현

  • Task 2.1: 커뮤니티 repository의 creator/차단/count/list 조회 테스트 작성
    • Files:
      • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepositoryTest.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/CreatorChannelCommunityQueryRepository.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt
    • RED: @DataJpaTest(properties = ["spring.cache.type=none", "spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE"]), @Import(QueryDslConfig::class) 패턴으로 아래 케이스를 작성한다.
      • 활성 creator는 findCreator로 조회되고 비활성 creator는 null이다.
      • viewer와 creator 사이 양방향 활성 차단 관계는 existsBlockedBetween에서 true다.
      • countCommunityPosts는 creator의 활성 게시글만 세고 다른 creator, 비활성 게시글은 제외한다.
      • canViewAdultContent=false이면 19금 게시글은 count와 list에서 제외된다.
      • canViewAdultContent=false이고 viewer가 19금 게시글을 구매했어도 count와 list에서 제외된다.
      • list는 고정 게시글을 먼저 반환하고, 고정 게시글은 fixedAt desc, 일반 게시글은 createdAt desc 순서를 따른다.
      • offset, limit으로 하나의 통합 목록을 페이징한다.
      • likeCount는 활성 좋아요만 센다.
      • isCommentAvailable=false인 게시글의 commentCount0이다.
      • commentCount는 활성 최상위 댓글만 세고, 비밀 댓글은 작성자 본인 또는 콘텐츠 creator에게만 포함한다.
      • 차단 관계에 걸린 댓글 작성자의 댓글은 commentCount에서 제외된다.
      • 유효 구매 내역은 CanUsage.PAID_COMMUNITY_POST, UseCan.member.id == viewerId, UseCan.communityPost.id == postId, UseCan.isRefund == false다.
      • 같은 게시글에 구매 내역이 중복되어도 list item은 중복되지 않는다.
      • 조회자가 게시글 작성자이면 구매 내역이 없어도 existOrdered == true다.
    • RED 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest"
      • 기대 결과: repository 미구현으로 컴파일 실패 또는 테스트 실패
    • GREEN: 기존 DefaultCreatorChannelHomeQueryRepository.findCommunityPosts, orderedCommunityPostIds, communityLikeCounts, communityCommentCounts, 차단 sub query, adult condition을 커뮤니티 repository로 옮기되 탭용 통합 정렬과 count를 추가한다.
    • GREEN 구현 기준:
      • tab list where는 isActive == true, member.id == creatorId, member.isActive == true, adult condition을 먼저 적용한다.
      • 구매 내역 exists/join은 접근 권한 계산에만 사용하고 adult condition을 우회하지 않는다.
      • 정렬은 isFixed desc, fixedAt desc nullsLast, createdAt desc, id desc를 사용한다.
      • home summary 조회는 findHomeCommunityPosts(creatorId, viewerId, isPinned, canViewAdultContent, limit)로 제공하고, 기존 홈과 동일하게 고정글/일반글을 분리 조회한다.
    • GREEN 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorChannelCommunityQueryRepositoryTest"
      • 기대 결과: BUILD SUCCESSFUL
    • REFACTOR: v2.creator.channel.community.adapter.out.persistencev2.api.*를 import하지 않는다.
    • 검증 기록:
      • RED: focused test 실행 결과 DefaultCreatorChannelCommunityQueryRepository 미구현으로 compileTestKotlin 실패를 확인했다.
      • GREEN: repository 구현 추가 후 focused test 재실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 계약 보정: block fixture와 구현을 양방향 활성 차단 정책에 맞춘 뒤 focused test 재실행 결과 BUILD SUCCESSFUL을 확인했다.
      • Review follow-up RED: raw createdAt, 같은 fixedAt 고정글 id desc, 홈 구매자 비활성 게시글 비노출 테스트 추가 후 focused test 2건 실패를 확인했다.
      • Review follow-up GREEN: repository 보정 후 focused test 재실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 전체 테스트: ./gradlew --no-daemon test 실행 결과 BUILD SUCCESSFUL을 확인했다.
      • ktlint: ./gradlew --no-daemon ktlintCheckDefaultCreatorChannelCommunityQueryRepository.kt 1개 줄에서 처음 실패했고, formatting 후 재실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 범위: Phase 2 repository/test 파일만 추가했고 service/API/home refactor는 건드리지 않았다.

Phase 3: 커뮤니티 조회 service 구현

  • Task 3.1: CreatorChannelCommunityQueryService 단위 테스트 작성
    • Files:
      • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt
    • RED: fake CreatorChannelCommunityQueryPort, mock MemberContentPreferenceService, mock AudioContentCloudFront, LangContext, SodaMessageSource를 사용해 아래 케이스를 작성한다.
      • 요청 page/size fallback 결과를 port의 offset, limit에 전달하고 hasNext와 응답 목록 size를 조립한다.
      • creator가 없으면 member.validation.user_not_found 예외를 던진다.
      • creator role이 CREATOR가 아니면 member.validation.creator_not_found 예외를 던진다.
      • 차단 관계가 있으면 기존 explorer.creator.blocked_access 메시지 예외를 던진다.
      • MemberContentPreferenceServiceisAdultVisibleByPolicy 결과를 port의 canViewAdultContent로 전달한다.
      • 이미지 path는 toCdnUrl(cloudFrontHost)로 변환하고 blank path는 null이다.
      • 작성자 프로필 path가 없으면 기존 홈 API의 기본 프로필 URL 정책을 적용한다.
      • 조회자가 게시글 작성자이면 구매 내역이 없어도 existOrdered == true로 조립한다.
      • 무료 이미지, 구매한 유료 이미지, 작성자 본인 유료 이미지는 CDN URL을 사용하고 signed URL을 생성하지 않는다.
      • 미구매 유료 이미지는 imageUrl == null이다.
      • 무료 오디오, 구매한 유료 오디오, 작성자 본인 유료 오디오는 AudioContentCloudFront.generateSignedURL(audioPath, 1000 * 60 * 30) 결과를 사용한다.
      • 미구매 유료 오디오는 signed URL을 생성하지 않고 audioUrl == null이다.
      • 유료 미구매 본문은 policy의 마스킹 결과를 사용한다.
      • findHomeCommunityPosts는 탭 전체 검증 없이 받은 viewerId, canViewAdultContent, isPinned, limit로 홈 요약 목록을 조립한다.
    • RED 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest"
      • 기대 결과: service 미구현으로 컴파일 실패 또는 테스트 실패
    • GREEN: getCommunityTab(creatorId, viewer, page, size, now)findHomeCommunityPosts(creatorId, viewerId, isPinned, canViewAdultContent, limit)를 구현한다.
    • GREEN 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest"
      • 기대 결과: BUILD SUCCESSFUL
    • REFACTOR: signed URL 생성은 접근 가능한 오디오 path가 있을 때만 호출하고, 이미지는 signed URL 생성 대상에서 제외한다. service는 API DTO를 반환하지 않는다.
    • 검증 기록:
      • RED: ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryServiceTest" 실행 결과 CreatorChannelCommunityQueryService 미구현 심볼로 compileTestKotlin 실패를 확인했다.
      • GREEN: service 구현 추가 후 같은 focused test 실행 중 Phase 1 마스킹 정책 기대값(15 code point 이하이면 앞 절반 + ...)과 테스트 기대값 불일치 1건을 확인했고, 테스트 기대값을 정책에 맞춘 뒤 재실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 범위: Phase 3 service/test 파일만 추가했고 API DTO/controller/facade와 홈 API 연결은 건드리지 않았다.

Phase 4: 홈 API 커뮤니티 조회 로직을 새 도메인으로 연결

  • Task 4.1: 홈 service 회귀 테스트를 새 커뮤니티 service 의존으로 갱신

    • Files:
      • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/domain/CreatorChannelHome.kt
    • RED: 기존 홈 service 테스트에서 home query port의 findCommunityPosts stub 대신 CreatorChannelCommunityQueryService.findHomeCommunityPosts 결과를 사용하도록 테스트를 먼저 바꾼다.
      • noticesisPinned=true, limit=3으로 조회한다.
      • communitiesisPinned=false, limit=3으로 조회한다.
      • 홈 응답의 커뮤니티 필드명은 유지하되, 커뮤니티 도메인 정책에 맞춰 유료 미구매 게시글의 imageUrl/audioUrlnull이고 dateUtc는 게시글 작성 시각(createdAt) 기준이다.
    • RED 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest"
      • 기대 결과: service 생성자/호출부 미구현으로 컴파일 실패 또는 테스트 실패
    • GREEN: CreatorChannelHomeQueryServiceCreatorChannelCommunityQueryService를 주입하고, 기존 queryPort.findCommunityPosts 호출 2곳을 새 community service 호출로 교체한다.
    • GREEN 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest"
      • 기대 결과: BUILD SUCCESSFUL
    • REFACTOR: 홈 domain의 기존 CreatorChannelCommunityPost data class를 제거하고, 홈의 notices, communities 타입은 kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityPost를 사용한다. 홈 API response DTO 변환 결과가 바뀌지 않는지 테스트로 확인한다.
    • 검증 기록:
      • RED: ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest" 실행 결과 communityQueryService 생성자 파라미터 미구현, 홈/커뮤니티 post domain 타입 불일치, 기존 home port findCommunityPosts 잔존으로 compileTestKotlin 실패를 확인했다.
      • GREEN: 홈 service가 CreatorChannelCommunityQueryService.findHomeCommunityPostsisPinned=true/false, limit=3으로 호출하도록 교체하고, 홈 domain/DTO/test fixture를 커뮤니티 domain post 기준으로 보정한 뒤 같은 focused test BUILD SUCCESSFUL을 확인했다.
      • 홈 API 회귀: 유료 미구매 홈 커뮤니티 게시글의 imageUrl/audioUrl == null, 고정글 dateUtc == createdAt 응답을 CreatorChannelHomeControllerTest, CreatorChannelHomeFacadeTest에 고정했고, 포함 회귀 focused test 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • Task 4.2: 홈 port/repository에서 커뮤니티 조회 책임 제거

    • Files:
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt
      • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt
      • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt
    • RED: 홈 repository 테스트에서 커뮤니티 게시글 조회 전용 테스트가 있으면 동일한 케이스가 DefaultCreatorChannelCommunityQueryRepositoryTest로 이동되어야 함을 먼저 확인한다.
    • RED 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest"
      • 기대 결과: 기존 home port method 제거 전에는 테스트/컴파일이 아직 기존 구조를 기대해 실패할 수 있다.
    • GREEN:
      • CreatorChannelHomeQueryPort.findCommunityPostsCreatorChannelCommunityPostRecord를 제거한다.
      • DefaultCreatorChannelHomeQueryRepository.findCommunityPosts, orderedCommunityPostIds, communityLikeCounts, communityCommentCounts, 커뮤니티 전용 차단 sub query, canAccessPaidCommunityContent, maskPaidCommunityContent, adultCommunityCondition, fixedNoticeCondition, visibleCommunityPostCondition 중 홈 repository에서 더 이상 쓰지 않는 커뮤니티 전용 helper를 제거한다.
      • 같은 로직은 DefaultCreatorChannelCommunityQueryRepository에만 남긴다.
    • GREEN 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest"
      • 기대 결과: BUILD SUCCESSFUL
    • REFACTOR: 홈 repository에서 creatorCommunity, creatorCommunityLike, creatorCommunityComment, useCan import가 더 이상 필요 없으면 제거한다. 다른 홈 조회에서 쓰는 import는 유지한다.
    • 검증 기록:
      • GREEN: CreatorChannelHomeQueryPort.findCommunityPosts, home 전용 CreatorChannelCommunityPostRecord, DefaultCreatorChannelHomeQueryRepository.findCommunityPosts와 커뮤니티 전용 helper/import 및 home repository의 직접 커뮤니티 조회 테스트를 제거했다.
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest" 실행 결과 BUILD SUCCESSFUL을 확인했다.
      • rg -n "CreatorChannelHomeQueryPort\.findCommunityPosts|CreatorChannelCommunityPostRecord|findCommunityPosts\(" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence 결과 home port/repository의 커뮤니티 조회 책임 잔존 0건을 확인했다.

Phase 5: 커뮤니티 탭 API 조립 계층 추가

  • Task 5.1: response DTO와 facade 테스트 작성

    • Files:
      • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt
      • Modify/Verify: src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/dto/CreatorChannelCommunityTabResponse.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt
    • RED: 아래 케이스를 테스트로 먼저 작성한다.
      • facade는 CreatorChannelCommunityQueryService.getCommunityTab 결과를 CreatorChannelCommunityTabResponse로 변환한다.
      • createdAtUtc는 UTC ISO-8601 문자열이다.
      • createdAtUtc 변환은 재사용 가능한 toUtcIso 확장함수가 있으면 기존 확장함수를 import해서 사용하고, 없으면 LocalDateTimeExtensions.kt에 공용 확장함수를 추가해 사용한다.
      • creatorProfileUrl, existOrdered가 응답에 포함된다.
      • imageUrl == null, audioUrl == null이 그대로 응답된다.
      • @JsonPropertyisCommentAvailable, isPinned, hasNext 필드명이 유지된다.
    • RED 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.application.CreatorChannelCommunityFacadeTest"
      • 기대 결과: DTO/facade 미구현으로 컴파일 실패 또는 테스트 실패
    • GREEN: PRD와 이 문서의 response data class 초안을 기준으로 DTO와 facade를 구현한다.
    • GREEN 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.application.CreatorChannelCommunityFacadeTest"
      • 기대 결과: BUILD SUCCESSFUL
    • REFACTOR: DTO는 공개 API 필드 변환만 담당하고, 구매/성인/정렬 정책을 포함하지 않는다.
    • 검증 기록:
      • RED: ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.application.CreatorChannelCommunityFacadeTest" 실행 결과 CreatorChannelCommunityTabResponse, CreatorChannelCommunityFacade 미구현 심볼로 compileTestKotlin 실패를 확인했다.
      • GREEN: DTO/facade와 공용 LocalDateTime.toUtcIso() 확장함수를 추가한 뒤 같은 focused test 재실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 범위: 공개 API 응답 DTO 변환과 facade 위임만 추가했고 구매/성인/정렬 정책은 DTO에 넣지 않았다.
  • Task 5.2: controller 테스트와 endpoint 구현

    • Files:
      • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt
      • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt
    • RED: @WebMvcTest(CreatorChannelCommunityController::class)와 기존 시리즈/오디오 controller test의 TestSecurityConfig 패턴으로 아래 케이스를 작성한다.
      • 비회원 요청은 401 Unauthorized다.
      • 인증 회원 요청은 GET /api/v2/creator-channels/{creatorId}/community를 호출하고 creatorId, page, size, viewer를 facade에 전달한다.
      • page=-1, size=100 같은 값은 controller에서 거부하지 않고 facade로 전달한다.
      • 성공 응답은 success=true, data.communityPostCount, data.communityPosts[0].postId, data.communityPosts[0].creatorProfileUrl, data.communityPosts[0].existOrdered, data.communityPosts[0].isCommentAvailable, data.communityPosts[0].isPinned, data.page, data.size, data.hasNext를 포함한다.
    • RED 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest"
      • 기대 결과: controller 미구현으로 컴파일 실패 또는 테스트 실패
    • GREEN: @RestController, @RequestMapping("/api/v2/creator-channels"), @GetMapping("/{creatorId}/community"), @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") 패턴으로 구현한다.
    • GREEN 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest"
      • 기대 결과: BUILD SUCCESSFUL
    • REFACTOR: 인증 null guard는 기존 탭 controller와 같은 requireMember private 함수로 둔다.
    • 검증 기록:
      • RED: ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest" 실행 결과 CreatorChannelCommunityController 미구현 심볼로 compileTestKotlin 실패를 확인했다.
      • GREEN: GET /api/v2/creator-channels/{creatorId}/community controller 구현 후 같은 focused test 재실행 결과 BUILD SUCCESSFUL을 확인했다.
      • Phase 5 focused 회귀: facade/controller focused tests 동시 실행 결과 BUILD SUCCESSFUL을 확인했다.
      • ktlint: ./gradlew --no-daemon ktlintCheck 실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 의존 방향: rg -n "kr\.co\.vividnext\.sodalive\.v2\.api\." src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community 실행 결과 출력 없음으로 domain/query 계층의 API 의존 0건을 확인했다.
      • 코드 리뷰 및 fresh 검증: controller는 기존 v2 탭 API와 같은 인증/requireMember 패턴으로 facade에 creatorId, viewer, raw page, raw size만 전달하고, facade/DTO는 query service 결과를 공개 응답 DTO로 변환만 하는 것을 확인했다. LocalDateTime.toUtcIso() 공용 확장함수는 기존 v2 DTO private 확장함수와 동일한 UTC offset 직렬화 방식임을 확인했다. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.*", ./gradlew --no-daemon ktlintCheck, ./gradlew --no-daemon test, git diff --check 모두 BUILD SUCCESSFUL 또는 출력 없음으로 통과했고, 커뮤니티 domain/query 계층의 v2.api.* import 검색도 출력 없음으로 확인했다.

Phase 6: E2E와 회귀 검증

  • Task 6.1: 커뮤니티 탭 end-to-end 테스트 작성

    • Files:
      • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt
    • RED: @SpringBootTest, @AutoConfigureMockMvc, @ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]), TransactionTemplate 패턴으로 아래 케이스를 작성한다.
      • controller-service-repository를 거쳐 전체 응답 필드를 반환한다.
      • 고정 게시글이 일반 게시글보다 먼저 반환된다.
      • page=-1, size=10 요청은 page=0, size=20으로 fallback된다.
      • 성인 콘텐츠 노출 정책이 false인 조회자는 19금 게시글을 count와 list에서 받지 않는다.
      • 성인 콘텐츠 노출 정책이 false인 조회자가 19금 게시글을 구매했더라도 count와 list에서 받지 않는다.
      • 구매한 유료 게시글의 imageUrl은 CDN URL이고 signed URL이 아니며, 미구매 유료 게시글의 imageUrlnull이다.
      • 구매한 유료 게시글의 audioUrl은 signed URL 형태이고, 미구매 유료 게시글의 audioUrlnull이다.
      • 이미지가 없는 게시글의 imageUrlnull이다.
    • RED 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest"
      • 기대 결과: API 미구현 또는 fixture 미연결로 실패
    • GREEN: 필요한 fixture helper를 테스트 내부에 추가하고, @MockBean AudioContentCloudFront로 signed URL 결과를 https://signed.test/community-audio처럼 고정한다. E2E 테스트에서 실제 CloudFront private key 파일을 요구하지 않게 한다.
    • GREEN 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest"
      • 기대 결과: BUILD SUCCESSFUL
    • REFACTOR: E2E fixture는 테스트 내부 helper로 유지하고 운영 코드에 테스트 전용 분기를 넣지 않는다.
    • 검증 기록:
      • RED/GREEN: CreatorChannelCommunityEndToEndTest를 추가한 뒤 ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest" 실행 결과 BUILD SUCCESSFUL을 확인했다. 별도 production 수정 없이 즉시 GREEN이었으며, Phase 1-5 구현이 이미 endpoint 동작을 충족했기 때문으로 확인했다.
      • 범위: @SpringBootTest, @AutoConfigureMockMvc, EmbeddedRedisInitializer, TransactionTemplate, @MockBean AudioContentCloudFront 패턴으로 controller-service-repository 실제 경로를 검증했다. 고정글 우선 정렬, page=-1/size=10 fallback, 성인 콘텐츠 비노출, 구매/미구매 유료 이미지·오디오 접근, 이미지 없는 게시글 imageUrl == null을 E2E 응답으로 고정했고 운영 코드는 변경하지 않았다. 리뷰 후 기존 v2 E2E와 같은 shared H2 datasource를 사용하도록 보정하고, 미구매 오디오 signed URL 생성이 발생하면 실패하도록 AudioContentCloudFront interaction 검증을 추가한 뒤 focused E2E와 ktlint를 재실행해 BUILD SUCCESSFUL을 확인했다.
  • Task 6.2: 홈 API 회귀와 의존 방향 검증

    • Files:
      • Verify: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt
      • Verify: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt
      • Verify: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt
      • Verify: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt
    • RED: 홈 API 응답 스키마가 변경되지 않아야 하므로 기존 테스트가 실패하면 변경 원인을 확인한다.
    • 검증 실행:
      • ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.home.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.*"
      • 기대 결과: BUILD SUCCESSFUL
    • 의존 방향 검색:
      • rg -n "kr\\.co\\.vividnext\\.sodalive\\.v2\\.api\\." src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community
      • 기대 결과: 검색 결과 0건
    • REFACTOR: 홈 API response DTO의 필드명, dateUtc, existOrdered, likeCount, commentCount 의미가 바뀌지 않도록 API DTO 변경을 피한다.
    • 검증 기록:
      • 홈 회귀: ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.home.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.*" 실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 의존 방향: rg -n "kr\.co\.vividnext\.sodalive\.v2\.api\." src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community 실행 결과 출력 없음으로 community domain/query 계층의 API 의존 0건을 확인했다.
      • 코드 리뷰 및 fresh 검증: 신규 E2E가 Phase 6 범위인 controller-service-repository 실제 경로, page/size fallback, 고정글 우선 정렬, 성인 콘텐츠 비노출, 구매/미구매 유료 미디어 접근, 홈 API 회귀, 의존 방향을 검증하는지 확인했고 추가 코드 이슈는 발견하지 않았다. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest", ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.home.*" --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.*", ./gradlew --no-daemon ktlintCheck, ./gradlew --no-daemon test는 모두 BUILD SUCCESSFUL; git diff --check는 출력 없음, 커뮤니티 domain/query 계층의 v2.api.* import 검색도 출력 없음으로 확인했다.

Phase 7: 전체 검증과 문서 갱신

  • Task 7.1: 전체 테스트와 ktlint 검증
    • Files:
      • Verify: docs/20260621_크리에이터_채널_커뮤니티_탭_API/plan-task.md
    • 검증 실행:
      • ./gradlew --no-daemon test
      • 기대 결과: BUILD SUCCESSFUL
      • ./gradlew --no-daemon ktlintCheck
      • 기대 결과: BUILD SUCCESSFUL
    • 문서 검증:
      • 각 완료 task의 체크박스를 - [x]로 갱신한다.
      • 각 task 아래에 무엇을, 왜, 어떻게 검증했는지, 실행 명령과 결과를 한국어로 누적 기록한다.
      • 전체 검증 결과는 아래 전체 검증 기록 섹션에 누적한다.
    • REFACTOR: 검증 실패가 구현 범위 변경을 요구하면 먼저 이 문서의 task를 갱신한 뒤 코드를 수정한다.
    • 검증 기록:
      • 전체 테스트: ./gradlew --no-daemon test 실행 결과 BUILD SUCCESSFUL을 확인했다.
      • ktlint: ./gradlew --no-daemon ktlintCheck 실행 결과 BUILD SUCCESSFUL을 확인했다.
      • 범위: Phase 7은 검증과 문서 갱신만 수행했고 production/test code는 변경하지 않았다.

5. 구현 순서 요약

  1. Phase 1에서 순수 정책과 domain/port 계약을 먼저 고정한다.
  2. Phase 2에서 QueryDSL repository를 새 커뮤니티 도메인으로 분리한다.
  3. Phase 3에서 service가 인증/성인/차단/CDN URL/오디오 signed URL/마스킹 정책을 조립하게 한다.
  4. Phase 4에서 홈 API의 커뮤니티 조회를 새 도메인으로 연결하고 홈 repository의 커뮤니티 쿼리를 제거한다.
  5. Phase 5에서 공개 API DTO/facade/controller를 추가한다.
  6. Phase 6에서 커뮤니티 탭 E2E와 홈 API 회귀를 확인한다.
  7. Phase 7에서 전체 테스트, ktlint, 의존 방향 검색 결과를 누적 기록한다.

6. 전체 검증 기록

  • 구현 전 문서 작성 단계에서는 코드 검증을 수행하지 않는다. 구현 단계에서 각 task 완료 즉시 실행 명령과 결과를 이 섹션에 누적한다.
  • 2026-06-21: 문서 작성 검증 - placeholder와 모호한 문구 검색 결과 0건.
  • 2026-06-21: 문서 변경 whitespace 검증 - git diff --check 실행 결과 출력 없음, exit code 0.
  • 2026-06-21: 문서 유지보수 규칙 검증 - sandbox 내부 ./gradlew tasks --all~/.gradle lock 파일 접근 제한으로 실패했고, 승인 실행으로 재시도한 ./gradlew tasks --allBUILD SUCCESSFUL 확인.
  • 2026-06-21: Phase 1 Task 1.1 검증 - RED focused test는 미구현 심볼로 compileTestKotlin 실패, GREEN focused test는 BUILD SUCCESSFUL 확인.
  • 2026-06-21: Phase 2 Task 2.1 검증 - focused repository test는 RED에서 미구현 repository로 compileTestKotlin 실패, GREEN과 양방향 활성 차단 계약 보정 후 BUILD SUCCESSFUL 확인. Review follow-up에서 raw createdAt, 같은 fixedAt 고정글 id desc, 홈 구매자 비활성 게시글 비노출 테스트 추가 후 2건 실패를 확인했고 repository 보정 후 focused test BUILD SUCCESSFUL 확인. ./gradlew --no-daemon testBUILD SUCCESSFUL 확인. ./gradlew --no-daemon ktlintCheck는 formatting 후 BUILD SUCCESSFUL, rg -n "kr\.co\.vividnext\.sodalive\.v2\.api\." src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community는 출력 없음 확인.
  • 2026-06-21: Phase 3 Task 3.1 검증 - RED focused service test는 CreatorChannelCommunityQueryService 미구현 심볼로 compileTestKotlin 실패, GREEN focused service test는 BUILD SUCCESSFUL 확인. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.community.*", ./gradlew --no-daemon test, ./gradlew --no-daemon ktlintCheck 모두 BUILD SUCCESSFUL 확인.
  • 2026-06-21: Phase 4 Task 4.1 검증 - RED focused home service test는 communityQueryService 생성자 파라미터 미구현, 홈/커뮤니티 post domain 타입 불일치, 기존 home port findCommunityPosts 잔존으로 compileTestKotlin 실패 확인. GREEN focused home service test는 BUILD SUCCESSFUL 확인. 리뷰 후 홈 커뮤니티 정책을 유료 미구매 imageUrl/audioUrl == null, dateUtc == createdAt으로 명시하고 테스트에 고정했다.
  • 2026-06-21: Phase 4 Task 4.2 검증 - focused home repository test는 BUILD SUCCESSFUL 확인. 홈/커뮤니티 회귀 focused test(CreatorChannelHomeControllerTest, CreatorChannelHomeFacadeTest, CreatorChannelCommunityQueryServiceTest, DefaultCreatorChannelCommunityQueryRepositoryTest)는 BUILD SUCCESSFUL 확인. ./gradlew --no-daemon ktlintCheck는 import 정렬로 1회 실패 후 ./gradlew --no-daemon ktlintFormat 적용 및 재실행 결과 BUILD SUCCESSFUL 확인. ./gradlew --no-daemon test 전체 테스트 BUILD SUCCESSFUL 확인.
  • 2026-06-21: Phase 5 Task 5.1 검증 - RED focused facade test는 CreatorChannelCommunityTabResponse, CreatorChannelCommunityFacade 미구현 심볼로 compileTestKotlin 실패 확인. GREEN focused facade test는 BUILD SUCCESSFUL 확인.
  • 2026-06-21: Phase 5 Task 5.2 검증 - RED focused controller test는 CreatorChannelCommunityController 미구현 심볼로 compileTestKotlin 실패 확인. GREEN focused controller test는 BUILD SUCCESSFUL 확인. Phase 5 facade/controller focused tests 동시 실행, ./gradlew --no-daemon ktlintCheck, ./gradlew --no-daemon test는 모두 BUILD SUCCESSFUL 확인. 커뮤니티 domain/query 계층의 v2.api.* import 검색은 출력 없음 확인.
  • 2026-06-21: Phase 5 코드 리뷰 및 fresh 검증 - controller/facade/DTO 구현이 Phase 5 범위인 인증 사용자 전달, raw page/size 전달, query service 위임, 공개 응답 변환에 머무는지 확인했고 추가 코드 이슈는 발견하지 않았다. ./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.api.creator.channel.community.*", ./gradlew --no-daemon ktlintCheck, ./gradlew --no-daemon test는 모두 BUILD SUCCESSFUL; git diff --checkrg -n "kr\.co\.vividnext\.sodalive\.v2\.api\." src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community는 출력 없음 확인.
  • 2026-06-22: Phase 6 Task 6.1/6.2 검증 - 커뮤니티 탭 E2E focused test, 홈 API 회귀 focused test, ./gradlew --no-daemon ktlintCheck, ./gradlew --no-daemon test는 모두 BUILD SUCCESSFUL 확인. git diff --check와 커뮤니티 domain/query 계층의 v2.api.* import 검색은 출력 없음 확인. 리뷰 후 shared H2 datasource와 오디오 signed URL interaction 검증을 보정했고, focused E2E와 ktlint 재실행 결과 BUILD SUCCESSFUL을 확인했다.
  • 2026-06-22: Phase 6 코드 리뷰 및 fresh 검증 - 신규 E2E와 Phase 6 문서 기록을 재검토했고 추가 코드 이슈는 발견하지 않았다. focused 커뮤니티 E2E, 홈 API 회귀, ktlintCheck, 전체 테스트는 모두 BUILD SUCCESSFUL; git diff --check와 community domain/query 계층의 API 의존 검색은 출력 없음으로 확인했다.
  • 2026-06-22: Phase 7 Task 7.1 검증 - ./gradlew --no-daemon test 실행 결과 BUILD SUCCESSFUL, ./gradlew --no-daemon ktlintCheck 실행 결과 BUILD SUCCESSFUL을 확인했다. Phase 7은 전체 검증과 문서 갱신만 수행했고 production/test code는 변경하지 않았다.