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
- path variable:
- response:
communityPostCount: 조회자가 조회 가능한 커뮤니티 게시글 전체 개수communityPosts: 커뮤니티 게시글 목록page: fallback 보정 후 실제 적용된 page indexsize: fallback 보정 후 실제 적용된 page sizehasNext: 다음 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한다. creatorProfileUrl은CreatorCommunity.member.profileImage를String?.toCdnUrl(cloudFrontHost)로 변환하고, 없으면 기존 홈 API의 기본 프로필 이미지 URL을 내려준다.existOrdered는 조회자가 게시글 작성자이면true, 조회자가 유효 구매 내역을 가지고 있으면true, 그 외에는false로 내려준다.imageUrl은CreatorCommunity.imagePath가 있고 이미지 접근 권한이 있을 때만String?.toCdnUrl(cloudFrontHost)로 변환한다. 경로가 없거나 blank이면null이다.- legacy 커뮤니티 목록은 유료 미구매 게시글의 이미지를 노출했지만, 커뮤니티 탭 API는 유료 미구매 게시글의
imageUrl도audioUrl과 동일하게null로 내려준다. - 이미지는 signed URL을 생성하지 않고 기존 CDN URL 조합 정책만 사용한다.
audioUrl은CreatorCommunity.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 +
...
commentCount는isCommentAvailable == 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
- Create:
- 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이다.limitItems는size만큼만 남기고hasNext는fetched.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 의존성은 넣지 않았다.
- RED:
- Files:
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
- Create:
- 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인 게시글의commentCount는0이다.commentCount는 활성 최상위 댓글만 세고, 비밀 댓글은 작성자 본인 또는 콘텐츠 creator에게만 포함한다.- 차단 관계에 걸린 댓글 작성자의 댓글은
commentCount에서 제외된다. - 유효 구매 내역은
CanUsage.PAID_COMMUNITY_POST,UseCan.member.id == viewerId,UseCan.communityPost.id == postId,UseCan.isRefund == false다. - 같은 게시글에 구매 내역이 중복되어도 list item은 중복되지 않는다.
- 조회자가 게시글 작성자이면 구매 내역이 없어도
existOrdered == true다.
- 활성 creator는
- 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)로 제공하고, 기존 홈과 동일하게 고정글/일반글을 분리 조회한다.
- tab list where는
- 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.persistence는v2.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 ktlintCheck는DefaultCreatorChannelCommunityQueryRepository.kt1개 줄에서 처음 실패했고, formatting 후 재실행 결과BUILD SUCCESSFUL을 확인했다. - 범위: Phase 2 repository/test 파일만 추가했고 service/API/home refactor는 건드리지 않았다.
- RED: focused test 실행 결과
- Files:
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
- Create:
- RED: fake
CreatorChannelCommunityQueryPort, mockMemberContentPreferenceService, mockAudioContentCloudFront,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메시지 예외를 던진다. MemberContentPreferenceService와isAdultVisibleByPolicy결과를 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로 홈 요약 목록을 조립한다.
- 요청 page/size fallback 결과를 port의
- 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 연결은 건드리지 않았다.
- RED:
- Files:
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
- Modify:
- RED: 기존 홈 service 테스트에서 home query port의
findCommunityPostsstub 대신CreatorChannelCommunityQueryService.findHomeCommunityPosts결과를 사용하도록 테스트를 먼저 바꾼다.notices는isPinned=true,limit=3으로 조회한다.communities는isPinned=false,limit=3으로 조회한다.- 홈 응답의 커뮤니티 필드명은 유지하되, 커뮤니티 도메인 정책에 맞춰 유료 미구매 게시글의
imageUrl/audioUrl은null이고dateUtc는 게시글 작성 시각(createdAt) 기준이다.
- RED 실행:
./gradlew --no-daemon test --tests "kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest"- 기대 결과: service 생성자/호출부 미구현으로 컴파일 실패 또는 테스트 실패
- GREEN:
CreatorChannelHomeQueryService에CreatorChannelCommunityQueryService를 주입하고, 기존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의 기존
CreatorChannelCommunityPostdata 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 portfindCommunityPosts잔존으로compileTestKotlin실패를 확인했다. - GREEN: 홈 service가
CreatorChannelCommunityQueryService.findHomeCommunityPosts를isPinned=true/false,limit=3으로 호출하도록 교체하고, 홈 domain/DTO/test fixture를 커뮤니티 domain post 기준으로 보정한 뒤 같은 focused testBUILD SUCCESSFUL을 확인했다. - 홈 API 회귀: 유료 미구매 홈 커뮤니티 게시글의
imageUrl/audioUrl == null, 고정글dateUtc == createdAt응답을CreatorChannelHomeControllerTest,CreatorChannelHomeFacadeTest에 고정했고, 포함 회귀 focused test 실행 결과BUILD SUCCESSFUL을 확인했다.
- RED:
- Files:
-
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
- Modify:
- 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.findCommunityPosts와CreatorChannelCommunityPostRecord를 제거한다.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,useCanimport가 더 이상 필요 없으면 제거한다. 다른 홈 조회에서 쓰는 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건을 확인했다.
- GREEN:
- Files:
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
- Create:
- RED: 아래 케이스를 테스트로 먼저 작성한다.
- facade는
CreatorChannelCommunityQueryService.getCommunityTab결과를CreatorChannelCommunityTabResponse로 변환한다. createdAtUtc는 UTC ISO-8601 문자열이다.createdAtUtc변환은 재사용 가능한toUtcIso확장함수가 있으면 기존 확장함수를 import해서 사용하고, 없으면LocalDateTimeExtensions.kt에 공용 확장함수를 추가해 사용한다.creatorProfileUrl,existOrdered가 응답에 포함된다.imageUrl == null,audioUrl == null이 그대로 응답된다.@JsonProperty로isCommentAvailable,isPinned,hasNext필드명이 유지된다.
- facade는
- 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에 넣지 않았다.
- RED:
- Files:
-
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
- Create:
- 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와 같은
requireMemberprivate 함수로 둔다. - 검증 기록:
- 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}/communitycontroller 구현 후 같은 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, rawpage, rawsize만 전달하고, 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 검색도 출력 없음으로 확인했다.
- RED:
- Files:
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
- Create:
- 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이 아니며, 미구매 유료 게시글의imageUrl은null이다. - 구매한 유료 게시글의
audioUrl은 signed URL 형태이고, 미구매 유료 게시글의audioUrl은null이다. - 이미지가 없는 게시글의
imageUrl은null이다.
- 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=10fallback, 성인 콘텐츠 비노출, 구매/미구매 유료 이미지·오디오 접근, 이미지 없는 게시글imageUrl == null을 E2E 응답으로 고정했고 운영 코드는 변경하지 않았다. 리뷰 후 기존 v2 E2E와 같은 shared H2 datasource를 사용하도록 보정하고, 미구매 오디오 signed URL 생성이 발생하면 실패하도록AudioContentCloudFrontinteraction 검증을 추가한 뒤 focused E2E와 ktlint를 재실행해BUILD SUCCESSFUL을 확인했다.
- RED/GREEN:
- Files:
-
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
- Verify:
- 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 검색도 출력 없음으로 확인했다.
- 홈 회귀:
- Files:
Phase 7: 전체 검증과 문서 갱신
- Task 7.1: 전체 테스트와 ktlint 검증
- Files:
- Verify:
docs/20260621_크리에이터_채널_커뮤니티_탭_API/plan-task.md
- Verify:
- 검증 실행:
./gradlew --no-daemon test- 기대 결과:
BUILD SUCCESSFUL ./gradlew --no-daemon ktlintCheck- 기대 결과:
BUILD SUCCESSFUL
- 문서 검증:
- 각 완료 task의 체크박스를
- [x]로 갱신한다. - 각 task 아래에 무엇을, 왜, 어떻게 검증했는지, 실행 명령과 결과를 한국어로 누적 기록한다.
- 전체 검증 결과는 아래
전체 검증 기록섹션에 누적한다.
- 각 완료 task의 체크박스를
- REFACTOR: 검증 실패가 구현 범위 변경을 요구하면 먼저 이 문서의 task를 갱신한 뒤 코드를 수정한다.
- 검증 기록:
- 전체 테스트:
./gradlew --no-daemon test실행 결과BUILD SUCCESSFUL을 확인했다. - ktlint:
./gradlew --no-daemon ktlintCheck실행 결과BUILD SUCCESSFUL을 확인했다. - 범위: Phase 7은 검증과 문서 갱신만 수행했고 production/test code는 변경하지 않았다.
- 전체 테스트:
- Files:
5. 구현 순서 요약
- Phase 1에서 순수 정책과 domain/port 계약을 먼저 고정한다.
- Phase 2에서 QueryDSL repository를 새 커뮤니티 도메인으로 분리한다.
- Phase 3에서 service가 인증/성인/차단/CDN URL/오디오 signed URL/마스킹 정책을 조립하게 한다.
- Phase 4에서 홈 API의 커뮤니티 조회를 새 도메인으로 연결하고 홈 repository의 커뮤니티 쿼리를 제거한다.
- Phase 5에서 공개 API DTO/facade/controller를 추가한다.
- Phase 6에서 커뮤니티 탭 E2E와 홈 API 회귀를 확인한다.
- 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은~/.gradlelock 파일 접근 제한으로 실패했고, 승인 실행으로 재시도한./gradlew tasks --all은BUILD 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에서 rawcreatedAt, 같은fixedAt고정글id desc, 홈 구매자 비활성 게시글 비노출 테스트 추가 후 2건 실패를 확인했고 repository 보정 후 focused testBUILD SUCCESSFUL확인../gradlew --no-daemon test는BUILD 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 portfindCommunityPosts잔존으로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 --check와rg -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는 변경하지 않았다.