33 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}/donations로 크리에이터 채널 후원 탭의 전체 채널 후원 개수, 후원 순위 Top 8, 페이징된 채널 후원 목록을 조회할 수 있게 한다.
Architecture: 공개 API controller/facade/response DTO는 kr.co.vividnext.sodalive.v2.api.creator.channel.donation 조립 계층에 둔다. 후원 탭 조회 service, page/month 정책, tab domain model, port, QueryDSL repository는 kr.co.vividnext.sodalive.v2.creator.channel.donation 하위에 두고 v2.api.*에 의존하지 않는다. 채널 후원 목록은 기존 ChannelDonationMessage와 홈 API 후원 섹션 조건을 따르고, 후원 순위는 legacy CreatorDonationRankingService를 통해 CreatorDonationRankingQueryRepository.getMemberDonationRanking 결과와 동일하게 재사용한다.
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}/donations - 인증 정책: 인증 회원만 조회 가능. 비회원은 기존 Security 흐름과
requireMember정책으로 거부한다. - request:
- path variable:
creatorId - query parameter:
page,required = false, 기본값0,page < 0이면0으로 보정 - query parameter:
size,required = false, 기본값20,size < 20이면20,size > 50이면50으로 보정
- path variable:
- response:
donationCount: 조회자가 조회 가능한 현재 KST 월 범위의 전체 채널 후원 개수rankings: 후원 순위 Top 8 목록donations: 채널 후원 목록page: 보정 후 실제 적용된 page indexsize: 보정 후 실제 적용된 page sizehasNext: 다음 page 존재 여부
- channel donation item:
nickname,profileImageUrl,can,message,createdAtUtc
- ranking item:
userId,nickname,profileImage,donationCan
- 채널 후원 목록 기준:
- 저장 엔티티는
kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationMessage를 사용한다. - 기간은 홈 후원 섹션과 동일하게 현재 KST 월 시작 이상, 다음 달 KST 월 시작 미만을 UTC
LocalDateTime으로 변환해 사용한다. - 정렬은
createdAt desc,id desc를 따른다. hasNext는size + 1개 조회 또는 동등한 방식으로 판단하고, 응답 목록에는 최대size개만 내려준다.
- 저장 엔티티는
- 비공개 후원 노출:
- 조회자가 크리에이터 본인이면 해당 크리에이터의 비공개 후원까지 목록과 개수에 포함한다.
- 조회자가 크리에이터 본인이 아니면 공개 후원과 조회자 본인의 비공개 후원만 목록과 개수에 포함한다.
- 후원 순위 기준:
CreatorDonationRankingService.getMemberDonationRanking(...)를 통해 legacyCreatorDonationRankingQueryRepository.getMemberDonationRanking결과를 재사용한다.- Top 8 조회는
offset = 0,limit = 8을 사용한다. - 기간은 크리에이터의
donationRankingPeriod를 따르고, 값이 없으면DonationRankingPeriod.CUMULATIVE를 사용한다. - 조회자가 크리에이터 본인이거나 크리에이터의
isVisibleDonationRank가true이면rankings를 내려준다. - 조회자가 크리에이터 본인이 아니고 크리에이터의
isVisibleDonationRank가false이면rankings는 빈 배열이다. donationCan은 기존 프로필 정책과 동일하게 크리에이터 본인 조회 시 실제 값을 내려주고, 일반 회원 조회 시0으로 내려준다.
- creator 검증:
- 조회 대상 회원이 없으면
member.validation.user_not_found - 조회 대상 회원이 크리에이터가 아니면
member.validation.creator_not_found - 조회자와 크리에이터 사이에 차단 관계가 있으면 기존 크리에이터 채널 접근 차단 오류
- 조회 대상 회원이 없으면
createdAtUtc는ChannelDonationMessage.createdAt을kr.co.vividnext.sodalive.extensions.toUtcIso로 변환한다.- 프로필 이미지 URL은
String?.toCdnUrl(cloudFrontHost)를 사용하고, 없으면 기존 홈 API와 같은"$cloudFrontHost/profile/default-profile.png"를 내려준다. - 후원자 닉네임은
removeDeletedNicknamePrefix()를 적용한다. - 후원 메시지는 홈 API와 동일하게
additionalMessage가 없으면 빈 문자열로 내려준다. 레거시ChannelDonationService.buildMessage기본 문구 조합은 사용하지 않는다. - legacy
/explorer/profile/channel-donation공개 endpoint와 응답 스키마는 변경하지 않는다. - 크리에이터 채널 홈 API의
channelDonations공개 응답 의미는 변경하지 않는다. - DB schema, 운영 DDL, 마이그레이션은 포함하지 않는다.
1. 파일 구조 계획
후원 탭 신규 API 조립 계층
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt
후원 도메인 조회 계층
- Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicy.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationRankingPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/CreatorChannelDonationQueryRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/legacy/LegacyCreatorChannelDonationRankingAdapter.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepositoryTest.kt - Test:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/legacy/LegacyCreatorChannelDonationRankingAdapterTest.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/channelDonation/ChannelDonationMessage.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingService.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingQueryRepository.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkController.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt
문서 산출물
- Create:
docs/20260622_크리에이터_채널_후원_탭_API/plan-task.md - Verify:
docs/20260622_크리에이터_채널_후원_탭_API/prd.md
2. Response data class 초안
구현 시 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.kt에 아래 DTO를 기준으로 추가한다. 필드명은 공개 API 계약이므로 변경이 필요하면 먼저 PRD와 이 문서를 갱신한다.
package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.extensions.toUtcIso
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonation
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationRanking
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationTab
data class CreatorChannelDonationTabResponse(
val donationCount: Int,
val rankings: List<MemberDonationRankingResponse>,
val donations: List<CreatorChannelDonationResponse>,
val page: Int,
val size: Int,
@JsonProperty("hasNext")
val hasNext: Boolean
) {
companion object {
fun from(tab: CreatorChannelDonationTab): CreatorChannelDonationTabResponse {
return CreatorChannelDonationTabResponse(
donationCount = tab.donationCount,
rankings = tab.rankings.map(MemberDonationRankingResponse::from),
donations = tab.donations.map(CreatorChannelDonationResponse::from),
page = tab.page.page,
size = tab.page.size,
hasNext = tab.hasNext
)
}
}
}
data class MemberDonationRankingResponse(
@JsonProperty("userId") val userId: Long,
@JsonProperty("nickname") val nickname: String,
@JsonProperty("profileImage") val profileImage: String,
@JsonProperty("donationCan") val donationCan: Int
) {
companion object {
fun from(ranking: CreatorChannelDonationRanking): MemberDonationRankingResponse {
return MemberDonationRankingResponse(
userId = ranking.userId,
nickname = ranking.nickname,
profileImage = ranking.profileImage,
donationCan = ranking.donationCan
)
}
}
}
data class CreatorChannelDonationResponse(
val nickname: String,
val profileImageUrl: String,
val can: Int,
val message: String,
val createdAtUtc: String
) {
companion object {
fun from(donation: CreatorChannelDonation): CreatorChannelDonationResponse {
return CreatorChannelDonationResponse(
nickname = donation.nickname,
profileImageUrl = donation.profileImageUrl,
can = donation.can,
message = donation.message,
createdAtUtc = donation.createdAt.toUtcIso()
)
}
}
}
3. Domain / Port 초안
구현 시 아래 형태를 기준으로 추가한다. API DTO가 domain model을 참조하지만 domain/port는 API DTO를 참조하지 않는다.
package kr.co.vividnext.sodalive.v2.creator.channel.donation.domain
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage
import java.time.LocalDateTime
data class CreatorChannelDonationTab(
val donationCount: Int,
val rankings: List<CreatorChannelDonationRanking>,
val donations: List<CreatorChannelDonation>,
val page: CreatorChannelPage,
val hasNext: Boolean
)
data class CreatorChannelDonationRanking(
val userId: Long,
val nickname: String,
val profileImage: String,
val donationCan: Int
)
data class CreatorChannelDonation(
val nickname: String,
val profileImageUrl: String,
val can: Int,
val message: String,
val createdAt: LocalDateTime
)
package kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
import kr.co.vividnext.sodalive.member.MemberRole
import java.time.LocalDateTime
interface CreatorChannelDonationQueryPort {
fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelDonationCreatorRecord?
fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
fun countChannelDonations(
creatorId: Long,
viewerId: Long,
now: LocalDateTime
): Int
fun findChannelDonations(
creatorId: Long,
viewerId: Long,
now: LocalDateTime,
offset: Long,
limit: Int
): List<CreatorChannelDonationRecord>
}
interface CreatorChannelDonationRankingPort {
fun findTopRankings(
creatorId: Long,
period: DonationRankingPeriod,
withDonationCan: Boolean
): List<CreatorChannelDonationRankingRecord>
}
data class CreatorChannelDonationCreatorRecord(
val creatorId: Long,
val role: MemberRole,
val nickname: String,
val isVisibleDonationRank: Boolean,
val donationRankingPeriod: DonationRankingPeriod?
)
data class CreatorChannelDonationRecord(
val nickname: String,
val profileImagePath: String?,
val can: Int,
val message: String,
val createdAt: LocalDateTime
)
data class CreatorChannelDonationRankingRecord(
val userId: Long,
val nickname: String,
val profileImage: String,
val donationCan: Int
)
4. 구현 Tasks
Phase 1: 공개 계약과 순수 정책 추가
-
Task 1.1: 후원 탭 domain model, port, page/month 정책 추가
- 파일:
- Create:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicy.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationRankingPort.kt
- Create:
- RED:
CreatorChannelDonationQueryPolicyTest를 먼저 작성한다.- null page/size가
0/20, fetchLimit21로 보정되는지 검증한다. page = -1,size = 10이0/20으로 보정되는지 검증한다.page = 2,size = 100이2/50, offset100, fetchLimit51로 보정되는지 검증한다.- fetched 21개에서 응답 item 20개와
hasNext = true가 계산되는지 검증한다. now = 2026-06-22T03:00:00기준 KST 월 범위가2026-05-31T15:00:00이상,2026-06-30T15:00:00미만 UTC로 계산되는지 검증한다.- domain/port record가 PRD 필드를 보존하는지 생성 테스트로 검증한다.
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicyTest - Expected: 신규 클래스가 없어 컴파일 실패한다.
- null page/size가
- GREEN: domain model, port,
CreatorChannelDonationQueryPolicy를 최소 구현한다.createPage(page, size)는 기존 FanTalk 정책과 같은 보정값을 사용한다.limitItems(fetched, page)는fetched.take(page.size)를 반환한다.hasNext(fetched, page)는fetched.size > page.size를 반환한다.currentKstMonthRange(now)는 홈 후원 섹션과 동일한 KST 월 범위 UTC 변환을 반환한다.- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicyTest - Expected:
BUILD SUCCESSFUL
- REFACTOR: 중복 상수와 월 범위 계산을 읽기 쉽게 정리하되 기존
CreatorChannelPage를 재사용한다.- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicyTest - Expected:
BUILD SUCCESSFUL
- Run:
- 파일:
-
Task 1.2: response DTO와 facade 매핑 추가
- 파일:
- Create:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/extensions/LocalDateTimeExtensions.kt
- Create:
- RED:
CreatorChannelDonationFacadeTest를 먼저 작성한다.CreatorChannelDonationTabResponse.from(...)이donationCount,rankings,donations,page,size,hasNext를 공개 필드로 매핑하는지 검증한다.rankings[0]의 JSON 필드가userId,nickname,profileImage,donationCan인지 검증한다.donations[0]의 JSON 필드가nickname,profileImageUrl,can,message,createdAtUtc인지 검증한다.hasNext가 JSON에서hasNext로 직렬화되는지 검증한다.- facade가 query service 결과를
CreatorChannelDonationTabResponse로 변환하는지 검증한다. - Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacadeTest - Expected: DTO/facade가 없어 컴파일 실패한다.
- GREEN: DTO와 facade를 최소 구현한다.
CreatorChannelDonationFacade.getDonationTab(creatorId, viewer, page, size, now)는 query service를 호출하고CreatorChannelDonationTabResponse.from(...)을 반환한다.- DTO의
createdAtUtc변환은 기존toUtcIso를 사용한다. - Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacadeTest - Expected:
BUILD SUCCESSFUL
- REFACTOR: DTO가 도메인 model만 import하고 persistence/legacy 타입을 import하지 않는지 확인한다.
- Run:
rg -n "adapter\\.out|explorer\\.profile" src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation - Expected: 검색 결과 0건
- Run:
- 파일:
-
Task 1.3: controller와 인증/API 계약 추가
- 파일:
- Create:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt
- Create:
- RED:
CreatorChannelDonationControllerTest를 먼저 작성한다.- 비회원 요청
GET /api/v2/creator-channels/1/donations는 401 또는 기존 테스트 보안 설정 기준 인증 실패로 거부되는지 검증한다. - 인증 회원 요청은
page,size,creatorId,viewer를 facade에 전달하는지 검증한다. - 성공 응답 JSON에
data.donationCount,data.rankings[0].userId,data.donations[0].createdAtUtc,data.page,data.size,data.hasNext가 포함되는지 검증한다. - Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationControllerTest - Expected: controller가 없어 컴파일 실패한다.
- 비회원 요청
- GREEN: controller를 최소 구현한다.
@RestController,@RequestMapping("/api/v2/creator-channels"),@GetMapping("/{creatorId}/donations")를 사용한다.@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member")로 회원을 받고, null이면SodaException(messageKey = "common.error.bad_credentials")를 던진다.- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationControllerTest - Expected:
BUILD SUCCESSFUL
- REFACTOR: 기존 FanTalk/커뮤니티 controller와 request mapping 스타일이 같은지 확인한다.
- Run:
rg -n "class CreatorChannelDonationController|/\\{creatorId\\}/donations" src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation - Expected: controller class와 endpoint mapping 각 1건 확인
- Run:
- 파일:
Phase 2: 도메인 조회 서비스와 legacy ranking 재사용 추가
-
Task 2.1: 후원 탭 query service 추가
- 파일:
- Create:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt
- Create:
- RED: fake
CreatorChannelDonationQueryPort, fakeCreatorChannelDonationRankingPort를 사용해 query service 테스트를 먼저 작성한다.- creator가 없으면
member.validation.user_not_found예외를 던지는지 검증한다. - creator role이
CREATOR가 아니면member.validation.creator_not_found예외를 던지는지 검증한다. - 조회자와 크리에이터 사이 차단 관계가 있으면 기존 차단 메시지 예외를 던지는지 검증한다.
page = -1,size = 10요청이offset = 0,limit = 21로 port에 전달되고 응답은 size 20으로 잘리는지 검증한다.- 조회자 본인이 크리에이터이면 ranking port에
withDonationCan = true가 전달되는지 검증한다. - 조회자 본인이 아니고
isVisibleDonationRank = true이면 ranking port에withDonationCan = false가 전달되고rankings가 반환되는지 검증한다. - 조회자 본인이 아니고
isVisibleDonationRank = false이면 ranking port를 호출하지 않고rankings가 빈 배열인지 검증한다. - donation 작성자 닉네임의 삭제 prefix 제거, profileImagePath CDN 변환, 기본 프로필 이미지 fallback, null message의 빈 문자열 변환을 검증한다.
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryServiceTest - Expected: query service가 없어 컴파일 실패한다.
- creator가 없으면
- GREEN: query service를 최소 구현한다.
ObjectProvider<CreatorChannelDonationQueryPort>패턴을 사용해 기존 FanTalk query service와 같은 순환 의존 회피 스타일을 따른다.CreatorChannelDonationRankingPort는 생성자 주입한다.DonationRankingPeriod는 creator record 값이 null이면DonationRankingPeriod.CUMULATIVE로 보정한다.findChannelDonations(...)결과는limitItems적용 후 domain으로 변환한다.hasNext는 fetch 결과 크기로 계산한다.- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryServiceTest - Expected:
BUILD SUCCESSFUL
- REFACTOR: query service가 API DTO를 import하지 않는지 확인한다.
- Run:
rg -n "v2\\.api" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation - Expected: 검색 결과 0건
- Run:
- 파일:
-
Task 2.2: 채널 후원 QueryDSL repository 추가
- 파일:
- Create:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepositoryTest.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/CreatorChannelDonationQueryRepository.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt
- Create:
- RED:
@DataJpaTest로 repository 테스트를 먼저 작성한다.- 활성 creator는 role, nickname,
isVisibleDonationRank,donationRankingPeriod를 조회하고 비활성 회원은 조회하지 않는지 검증한다. - 조회자와 크리에이터 사이 양방향 활성 차단만 차단 상태로 조회하는지 검증한다.
- 현재 KST 월 범위의 채널 후원만 count/list에 포함되는지 검증한다.
- 크리에이터 본인은 비공개 후원까지 count/list에 포함되는지 검증한다.
- 일반 조회자는 공개 후원과 본인의 비공개 후원만 count/list에 포함되는지 검증한다.
- 목록 정렬이
createdAt desc,id desc인지 검증한다. offset,limit이 적용되는지 검증한다.- projection이
selectFrom(channelDonationMessage)가 아니라 필요한 컬럼 projection을 사용하는지 소스 문자열 또는 동작 테스트로 확인한다. - Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest - Expected: repository가 없어 컴파일 실패한다.
- 활성 creator는 role, nickname,
- GREEN: QueryDSL repository를 최소 구현한다.
findCreator(...)는 활성 회원만 조회하고 role이 USER인 회원도 record로 반환해 service에서creator_not_found를 판단하게 한다.existsBlockedBetween(...)은 기존 홈/FanTalk repository의 차단 조건과 동일하게 구현한다.countChannelDonations(...)와findChannelDonations(...)는CreatorChannelDonationQueryPolicy.currentKstMonthRange(now)결과와 같은 월 범위 조건을 적용한다.donationVisibilityCondition(creatorId, viewerId)는 홈 API의 조건과 동일하게 구현한다.- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest - Expected:
BUILD SUCCESSFUL
- REFACTOR: 홈 repository의 기존
findChannelDonations공개 동작이 변경되지 않았는지 관련 테스트를 실행한다.- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest - Expected:
BUILD SUCCESSFUL
- Run:
- 파일:
-
Task 2.3: legacy 후원 랭킹 adapter 추가
- 파일:
- Create:
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/legacy/LegacyCreatorChannelDonationRankingAdapterTest.kt - Create:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/legacy/LegacyCreatorChannelDonationRankingAdapter.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingService.kt
- Create:
- RED: mock
CreatorDonationRankingService를 사용해 adapter 테스트를 먼저 작성한다.findTopRankings(creatorId = 1, period = CUMULATIVE, withDonationCan = false)호출 시 legacy service에offset = 0,limit = 8,withDonationCan = false, 같은 period가 전달되는지 검증한다.- legacy
MemberDonationRankingResponse결과가CreatorChannelDonationRankingRecord로 필드 손실 없이 변환되는지 검증한다. - Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.legacy.LegacyCreatorChannelDonationRankingAdapterTest - Expected: adapter가 없어 컴파일 실패한다.
- GREEN:
CreatorChannelDonationRankingPort구현체를 최소 구현한다.CreatorDonationRankingService.getMemberDonationRanking(creatorId, offset = 0, limit = 8, withDonationCan, period)를 호출한다.- 반환값의
userId,nickname,profileImage,donationCan을 record로 복사한다. - Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.legacy.LegacyCreatorChannelDonationRankingAdapterTest - Expected:
BUILD SUCCESSFUL
- REFACTOR: 랭킹 산식이나 기간 계산을 V2 코드에 중복 구현하지 않았는지 확인한다.
- Run:
rg -n "previousOrSame|SPIN_ROULETTE|CanUsage\\.DONATION|creator_donation_ranking" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation - Expected: 검색 결과 0건
- Run:
- 파일:
Phase 3: 통합 검증과 회귀 확인
-
Task 3.1: 후원 탭 End-to-End 테스트 추가
- 파일:
- Create:
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/legacy/LegacyCreatorChannelDonationRankingAdapter.kt
- Create:
- RED:
@SpringBootTest+MockMvc통합 테스트를 먼저 작성한다.- controller-service-repository를 거쳐 후원 탭 API가
donationCount,donations,page,size,hasNext를 반환하는지 검증한다. page범위 밖 요청은 빈donations, 유지된donationCount,hasNext = false를 반환하는지 검증한다.page = -1,size = 100요청은 응답의page = 0,size = 50으로 보정되는지 검증한다.- 일반 조회자에게 크리에이터의 비공개 후원은 숨기고 조회자 본인의 비공개 후원은 노출하는지 검증한다.
- 크리에이터 본인 조회 시 비공개 후원과
donationCan값이 포함된 ranking이 내려오는지 검증한다. - Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest - Expected: 통합 wiring 또는 신규 API가 없어 실패한다.
- controller-service-repository를 거쳐 후원 탭 API가
- GREEN: 누락된 Spring bean wiring, package scan, constructor 주입 문제를 최소 수정한다.
- 신규 repository/adapter/service/controller가 component scan 대상 package에 들어가야 한다.
- 테스트 데이터는
ChannelDonationMessage,UseCan,UseCanCalculate등 기존 엔티티 저장 방식에 맞춰 생성한다. - Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest - Expected:
BUILD SUCCESSFUL
- REFACTOR: End-to-End 테스트 fixture helper 중복을 줄이되 테스트 의도를 흐리지 않는 범위에서만 정리한다.
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest - Expected:
BUILD SUCCESSFUL
- Run:
- 파일:
-
Task 3.2: 관련 테스트와 아키텍처 의존 방향 검증
- 파일:
- Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation - Verify:
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation - Verify:
docs/20260622_크리에이터_채널_후원_탭_API/prd.md - Verify:
docs/20260622_크리에이터_채널_후원_탭_API/plan-task.md
- Verify:
- RED: 이 task는 신규 실패 테스트 작성 대상이 아니라 구현 완료 후 회귀/아키텍처 검증 task다.
- TDD 예외 사유: 개별 동작 실패 테스트는 Task 1.1부터 Task 3.1까지 작성한다. 이 task는 전체 검증과 문서 상태 확인만 담당한다.
- 대체 검증 방법: 관련 단일 테스트 묶음, import 검색, ktlint를 실행한다.
- GREEN: 관련 테스트를 묶어서 실행한다.
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicyTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacadeTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationControllerTest --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.legacy.LegacyCreatorChannelDonationRankingAdapterTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest - Expected:
BUILD SUCCESSFUL
- Run:
- REFACTOR: 의존 방향과 포맷을 검증한다.
- Run:
rg -n "v2\\.api" src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation - Expected: 검색 결과 0건
- Run:
rg -n "class CreatorChannelDonationController|/\\{creatorId\\}/donations" src/main/kotlin/kr/co/vividnext/sodalive/v2 - Expected: 후원 탭 controller와 endpoint mapping 각 1건 확인
- Run:
./gradlew ktlintCheck - Expected:
BUILD SUCCESSFUL
- Run:
- 파일:
5. 구현 순서
- Phase 1에서 공개 계약, domain/port, page/month 정책, facade/controller를 먼저 고정한다.
- Phase 2에서 query service, QueryDSL repository, legacy ranking adapter를 TDD로 추가한다.
- Phase 3에서 End-to-End 테스트와 아키텍처/포맷 검증을 수행한다.
- 각 task 완료 즉시 해당 체크박스를
- [x]로 변경하고, 실행한 명령과 결과를 task 아래에 한국어로 누적 기록한다.
6. 전체 검증 기록
- 아직 구현 전이므로 검증 기록 없음.