docs(channel-donation): 비밀 후원 익명 조회 계약과 검증 결과를 기록한다
This commit is contained in:
@@ -0,0 +1,324 @@
|
|||||||
|
# 크리에이터 채널 비밀 후원 익명 조회 구현 계획
|
||||||
|
|
||||||
|
| 문서 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 상태 | `P1-T1`, `P1-T2`, 범위 한정 `P1-GATE` 완료. 전체 suite는 timeout으로 미검증 |
|
||||||
|
| 작성일 | 2026-11-04 |
|
||||||
|
| 요구사항 기준 | [prd.md](prd.md) |
|
||||||
|
| API 기준 | [prd.md §8](prd.md#8-api-계약) |
|
||||||
|
| 현재 Phase | Phase 1: 두 후원 조회 경로 완성 |
|
||||||
|
| 현재 활성 Goal | 없음 |
|
||||||
|
| 다음 Goal | 없음. 전체 suite 완료 여부는 별도 미해결 검증 한계 |
|
||||||
|
|
||||||
|
> 실행 준비 기준: goal별로 한 번에 하나씩 실행하며, 각 Task의 명령과 체크박스는 실행 후에만 완료 처리한다. 구현 방법은 본 계획의 TDD 순서를 따른다.
|
||||||
|
|
||||||
|
**실행 방식:** 사용자 지시에 따라 worktree 없이 현재 `test` 브랜치에서 Goal을 순차 실행한다. 커밋·푸시·배포는 하지 않는다.
|
||||||
|
|
||||||
|
**Architecture:** 각 endpoint의 persistence record부터 domain과 DTO까지 persisted `isSecret`을 전달하고, 기존 local `toDomain` mapper에서 viewer/creator/donor 권한으로 표시 필드를 정한다. 익명 이미지 URL은 기존 default-profile과 같은 직접 host/path 조합으로 만들며 각 endpoint의 filter 제거와 masking은 end-to-end Task 하나에서 함께 완성한다.
|
||||||
|
|
||||||
|
**Spec:** [prd.md](prd.md), 특히 §7~§10.
|
||||||
|
|
||||||
|
**Review Focus:**
|
||||||
|
|
||||||
|
- 공개 행은 다른 인증 viewer에게도 원본 표시: `P1-T1`, `P1-T2` 공개 후원 E2E assertion.
|
||||||
|
- 비밀 행은 USER 및 본인이 아닌 CREATOR role도 익명: 양 Task의 role별 HTTP E2E.
|
||||||
|
- null/empty 메시지, localized nickname, 마스킹 이미지 URL과 원본/default-profile 회귀: 양 Task의 mapper/service 검증.
|
||||||
|
- 월 경계·정렬·limit/page의 비밀 행 포함: repository와 HTTP E2E의 경계·초과 건수 검증.
|
||||||
|
- `isSecret`은 모든 role에서 persisted Boolean이며 랭킹은 그대로: 양 endpoint JSON과 기존 ranking assertion.
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
크리에이터 채널 홈과 후원 탭에서 현재 KST 월 비밀 후원을 유지하며, 크리에이터와 후원자 외의 사용자에게는 식별 정보 대신 익명 필드를 반환한다.
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||||||
|
|---:|---|---:|---|---|
|
||||||
|
| 1 | 범위 한정 완료 | `2/2` | 없음 | 전체 suite는 600000ms timeout으로 결과 미확정. 전체 suite 통과로 간주하지 않음 |
|
||||||
|
|
||||||
|
- 구현 단계의 실행 체크박스는 실제 테스트 및 검토 증거가 확보된 뒤에만 완료 처리한다.
|
||||||
|
- 미완료 Goal은 동시에 하나만 실행한다.
|
||||||
|
- 파일명과 URL 구성 계약 및 설정된 CDN 이미지 접근은 확인했다. 전체 프로젝트 suite는 timeout으로 미완료 상태이며 범위 한정 Gate 성공과 분리한다.
|
||||||
|
|
||||||
|
## 범위
|
||||||
|
|
||||||
|
### 포함
|
||||||
|
|
||||||
|
- 홈 및 후원 탭의 현재 KST 월 비밀 후원 조회, persisted `isSecret`, 서비스 계층 마스킹.
|
||||||
|
- 양쪽 DTO 직렬화와 repository, service, controller, facade 및 HTTP MockMvc E2E 검증.
|
||||||
|
- 기존 접근 제어, 정렬, count/page, 랭킹, 날짜·금액 계약 보존.
|
||||||
|
|
||||||
|
### 제외
|
||||||
|
|
||||||
|
- 후원 생성, 수정, 취소, DB/DDL, 공개 후원 표시 변경.
|
||||||
|
- 랭킹 변경 또는 전체 익명성 보장.
|
||||||
|
- 신규 dependency, 공통 masking abstraction, port signature 변경(실제 필요가 발견된 경우 별도 결정 전까지 금지).
|
||||||
|
- 새 config, fallback, 공통 helper architecture 또는 외부 번역 서비스.
|
||||||
|
|
||||||
|
## 기술적 제약
|
||||||
|
|
||||||
|
- Kotlin, Spring Boot 2.7.14, Java 17, Gradle Wrapper, QueryDSL 및 기존 `SodaMessageSource`/CDN URL 변환을 사용한다.
|
||||||
|
- 공개 API 목록 DTO에 donor `memberId`나 donation ID를 추가하지 않는다. 내부 record/domain의 기존 `memberId`는 권한 판정에 유지한다.
|
||||||
|
- persistence에서 비밀 row를 유지하되 서비스가 response domain을 만들기 전 익명화한다. 두 계층 모두 수정과 테스트를 하나의 end-to-end Task로 수행한다.
|
||||||
|
- 각 Task의 `isSecret` 추가는 해당 탭의 port record, domain model, QueryDSL constructor projection 순서와 해당 테스트 fixture constructor를 함께 갱신한다.
|
||||||
|
- 로컬 변환 함수 `toDomain`이 이미 있으므로 기존 함수가 있는 service 안에서 viewer/creator/lang 판정을 수행한다. 새 공유 추상화는 만들지 않는다.
|
||||||
|
- 신규 로직은 먼저 요구 동작을 검증하는 assertion 실패를 관찰한 뒤 구현한다. 필드 추가로 발생한 컴파일 오류는 의도한 RED 증거가 아니다.
|
||||||
|
- focused test와 직접 영향 package 회귀를 우선한다. 전체 테스트·빌드는 이 계획의 미래 Gate에서만 실행하며, 실제 수행한 결과만 Progress에 기록한다.
|
||||||
|
- 마스킹 대상 비밀 후원의 이미지 URL은 기존 `defaultProfileImageUrl()`처럼 `cloud.aws.cloud-front.host`에 `/profile/anonymous-profile.png`를 직접 붙여 만든다. 임의 사용자 제공 경로에 `toCdnUrl`을 적용하지 않는다. 테스트 host `https://cdn.test`의 정확한 기대 URL은 `https://cdn.test/profile/anonymous-profile.png`다.
|
||||||
|
- 익명 URL은 마스킹 대상 비밀 donation item에만 적용한다. 공개 및 creator/donor 본인의 원본 profile image, 기존 `default-profile.png` fallback은 그대로 유지한다. 테스트용 host/URL은 실제 운영 자산 접근 검증을 대신하지 않는다.
|
||||||
|
|
||||||
|
### Phase 1: 홈과 후원 탭 응답에서 비밀 후원 마스킹
|
||||||
|
|
||||||
|
**Phase 결과:** 두 API가 비밀 후원을 count/list에 포함하고 권한별 원본 또는 익명 표시를 반환한다.
|
||||||
|
|
||||||
|
**선행조건:** PRD `SDA-001`~`SDA-007` 및 §8 계약 확인. 파일명과 URL 구성 계약이 확정되어 있어 이미지 제공 대기 없이 RED와 GREEN을 진행할 수 있다. 운영 CDN 접근 가능 여부는 최종 Gate에서 검증한다.
|
||||||
|
|
||||||
|
**Phase 완료 조건:** `P1-T1`, `P1-T2` 및 `P1-GATE`의 증거 충족. 운영 CDN에서 이미지에 접근할 수 없으면 Gate는 blocked 상태다.
|
||||||
|
|
||||||
|
### 구현 항목
|
||||||
|
|
||||||
|
- [x] **Task 1.1: 홈 후원 목록 조회와 익명화**
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T1`:** 홈 API는 현재 KST 월 비밀 후원을 포함하고, 그 비밀 행만 크리에이터·해당 후원자는 원본, 다른 조회자는 익명 표시를 받으며 공개 행은 기존 원본 표시를 유지한다.
|
||||||
|
|
||||||
|
- **시작 조건:** 없음. PRD `SDA-001`, `SDA-003`~`SDA-007`과 §8.1~§8.3 확인. 이미지 URL 계약은 확정되어 있다.
|
||||||
|
- **완료 증거:** 홈 DTO `isSecret`/응답 계약 및 서비스 마스킹 테스트, repository query 행 검증, 실제 HTTP E2E와 정확한 이미지 URL 및 원본/default-profile 회귀 검증 기록.
|
||||||
|
- **범위 밖:** 후원 탭 변경, 랭킹 변경, 실제 이미지 자산 생성.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.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/domain/CreatorChannelHome.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/api/creator/channel/home/dto/CreatorChannelHomeResponse.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/domain/CreatorChannelHomeQueryPolicyTest.kt` (관련 동작이 있으면 수정)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: 기존 `CreatorChannelHomeQueryPort.findChannelDonations(creatorId, viewerId, now, limit, lang)`, `SodaMessageSource.getMessage(key, lang)` 및 `cloud.aws.cloud-front.host`.
|
||||||
|
- Produces: `CreatorChannelDonationRecord.isSecret: Boolean`, home domain donation의 `isSecret: Boolean`, response JSON `channelDonations[].isSecret`.
|
||||||
|
- Port method signature를 유지한다. record의 새 `isSecret`을 QueryDSL projection과 모든 constructor fixture에 추가한다. Service mapper signature는 `CreatorChannelDonationRecord.toDomain(creatorId: Long, viewerId: Long, lang: Lang): CreatorChannelDonation`으로 둔다.
|
||||||
|
|
||||||
|
- [x] **RED:** E2E에서 `isSecret` 누락 assertion 실패를 production 변경 전에 확인하고 viewer role 및 공개/비밀 행 cases를 추가했다.
|
||||||
|
- [x] **RED 확인:** JSON path `$.data.channelDonations[0].isSecret` 누락 assertion 실패 확인.
|
||||||
|
- [x] **RED 추가 검증:** role, 현지화, 원본/default/익명 이미지 URL 및 Boolean 응답 검증 cases 작성.
|
||||||
|
- [x] **GREEN:** repository filter 제거, persisted `isSecret` 전달 및 서비스 마스킹을 구현했다. 공개/본인 원본과 기존 default-profile 동작을 보존했다.
|
||||||
|
- [x] **GREEN 확인:** 홈 package 89 tests 통과. 이후 홈 E2E 18 + service 23 및 `ktlintCheck` 통과.
|
||||||
|
- [x] **REFACTOR:** KST 월 경계 회귀를 2026년 12월 고정 검증으로 보강했고, 테스트 lint 위반을 수정했다. 역할/언어/월 경계/최신 8건 및 민감 필드 경계를 검증했다.
|
||||||
|
|
||||||
|
**Task 1.1 검증 명령:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.in.web.CreatorChannelHomeControllerTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.application.CreatorChannelHomeFacadeTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 각 명령 exit code 0. response의 **마스킹 대상 비밀 donation item만** 원본 nickname/message/profile path가 없어야 한다. 공개/본인 후원과 ranking identity는 response에 기존대로 남는다. 구현 시 실행할 명령이며 현재 검증 결과가 아니다.
|
||||||
|
|
||||||
|
**Task 1.1 Progress:** 완료. 세부 RED/GREEN 및 2026년 12월 월 경계 수정 증거는 Progress의 `P1-T1 구현 및 검토`와 후속 수정 기록을 참조한다.
|
||||||
|
|
||||||
|
- [x] **Task 1.2: 후원 탭 목록과 count 익명화**
|
||||||
|
|
||||||
|
**Goal 실행 `P1-T2`:** 후원 탭은 현재 월 비밀 후원을 목록·count에 포함하고, 비밀 row만 권한별로 마스킹하며 공개 row의 기존 원본 표시를 유지한다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1` 완료. 별도 domain mapper를 사용하되 `P1-T1`에서 등록한 i18n key를 재사용한다.
|
||||||
|
- **완료 증거:** donation DTO의 `isSecret`, query count/list, service masking, E2E의 viewer 역할별 응답과 pagination 회귀 기록.
|
||||||
|
- **범위 밖:** 기존 랭킹 쿼리, 랭킹 결과 익명화 또는 랭킹 합계 변경.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt`
|
||||||
|
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.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/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/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt`
|
||||||
|
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: 기존 `CreatorChannelDonationQueryPort.countChannelDonations/findChannelDonations`, `SodaMessageSource.getMessage(key, lang)`, 그리고 `P1-T1`이 추가하는 `explorer.channel_donation.anonymous.nickname`, `explorer.channel_donation.anonymous.message`.
|
||||||
|
- Produces: `CreatorChannelDonationRecord.isSecret: Boolean`, donation domain item의 `isSecret: Boolean`, `donations[].isSecret` JSON 필드.
|
||||||
|
- Port method signature를 유지한다. record constructor projection와 모든 test fixture를 갱신한다. mapper signature는 `CreatorChannelDonationRecord.toDomain(creatorId: Long, viewerId: Long, lang: Lang): CreatorChannelDonation`이다.
|
||||||
|
|
||||||
|
- [x] **RED:** E2E의 `isSecret` JSON assertion을 추가하고 production 변경 전 field 누락 실패를 확인했다. USER/별도 CREATOR role, 공개·본인·타인 비밀 및 out-of-range page cases를 추가했다.
|
||||||
|
- [x] **RED 확인:** `$.data.donations[0].isSecret` 누락 assertion 실패 확인.
|
||||||
|
- [x] **RED 추가 검증:** donor/creator 원본, 기본·익명 이미지, 타 역할 마스킹을 위한 검증 cases 작성.
|
||||||
|
- [x] **GREEN:** secret filter만 제거하고 count/list, paging 및 KST 계약을 유지했다. persisted `isSecret`과 권한별 마스킹을 구현했으며 ranking은 변경하지 않았다.
|
||||||
|
- [x] **GREEN 확인:** donation package 63 tests 및 `ktlintCheck` 통과.
|
||||||
|
- [x] **REFACTOR:** mixed 25행 pagination, 전체 count, 월 경계, 역할/언어/메시지 및 기존 ranking 동작을 검증했다.
|
||||||
|
|
||||||
|
**Task 1.2 검증 명령:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationControllerTest' --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.CreatorChannelDonationEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicyTest'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected:** 구현 시 각 명령 exit code 0. 타인 비밀 항목만 익명화되고 공개/본인 항목 및 랭킹은 유지된다. 100/200/300 fixture의 count는 3, 최신순은 300/200/100이다. 같은 3건을 조회할 때 모든 viewer의 count는 같고, 범위 밖 page는 빈 목록과 count 3을 반환한다. can/createdAtUtc는 모든 역할에서 저장값과 정확히 일치한다.
|
||||||
|
|
||||||
|
**Task 1.2 Progress:** 완료. 상세 결과는 Progress의 `P1-T2 구현 및 검토`와 통합 Gate 기록을 참조한다.
|
||||||
|
|
||||||
|
### Phase 1 Gate
|
||||||
|
|
||||||
|
**Goal 실행 `P1-GATE`:** 두 조회 endpoint의 계약, 권한 마스킹 및 직접 영향 회귀를 범위 한정으로 검증한다. 전체 프로젝트 suite는 timeout으로 미완료이며 성공으로 간주하지 않는다.
|
||||||
|
|
||||||
|
- **시작 조건:** `P1-T1`, `P1-T2` 구현 완료. 파일명과 URL 계약은 확정되어 있으며 실제 운영 CDN 이미지 접근 가능 여부를 검증한다.
|
||||||
|
- **완료 증거:** focused 패키지 테스트, `ktlintCheck`, `bootJar`, SpringBootTest MockMvc 경로 및 설정된 CDN의 실제 이미지 응답 결과를 Progress에 기록한다. 전체 suite는 별도 제한 사항이다.
|
||||||
|
- **범위 밖:** 랭킹 수정, 테스트 삭제·완화, 전체 테스트를 무조건 실행하는 것.
|
||||||
|
- **TDD 예외 사유:** Gate는 제품 로직을 바꾸지 않고 기존 자동 테스트와 제공된 자산 URL을 검증한다.
|
||||||
|
- **대체 검증 방법:** focused/package test, MockMvc 응답 field와 설정된 이미지 URL 접근 결과를 대조해 기록한다. MockMvc는 live socket/server 검증이 아니다. 코드 수정이 필요하면 owning Task로 되돌린다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.*' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.*'
|
||||||
|
./gradlew ktlintCheck
|
||||||
|
./gradlew bootJar
|
||||||
|
```
|
||||||
|
|
||||||
|
위 명령은 구현 후 실행할 미래 Gate 검증이며 `bootJar`는 전체 test suite를 암묵 실행하지 않는다. 전체 회귀는 광범위 공통 코드/인증/보안 변경, 여러 domain 변경, release/final 전체 상태 증거 요구, targeted tests만으로 영향 범위를 판단할 수 없는 실패 또는 사용자 명시 요청 때만 별도로 실행한다. 기존 message group에 두 i18n key를 추가한 사실만으로 전체 회귀를 요구하지 않는다. 생략 시 근거와 focused/package 회귀 결과를 기록한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gate Expected:** E2E와 해당 패키지 테스트·lint·`bootJar`의 실제 성공 결과를 확인한다. 정확한 URL `"$cloudFrontHost/profile/anonymous-profile.png"`가 마스킹 대상 donation item의 `profileImageUrl`이 되고 CDN에서 실제 접근 가능하면 범위 한정 Gate를 완료한다. 전체 suite 결과는 별도로 기록한다.
|
||||||
|
|
||||||
|
**수동 검증:**
|
||||||
|
|
||||||
|
- [x] SpringBootTest/H2/MockMvc 요청으로 creator, donor, USER, 별도 CREATOR role을 검증했다. 별도 live server HTTP 요청은 실행하지 않았다.
|
||||||
|
- [x] 타인 응답에서 마스킹 대상 secret item의 원문 부재, public/own donation 보존, 목록 DTO의 신규 ID 부재를 검증했다.
|
||||||
|
- [x] 설정된 CDN URL의 접근 결과는 HTTP 200, `image/png`, 19,331 bytes다. 업로드 작업은 하지 않았다.
|
||||||
|
- [x] 랭킹의 기존 식별 정보 및 secret contribution 집계와 전역 익명성이 아니라는 제한을 유지했다.
|
||||||
|
|
||||||
|
## 실행 순서와 의존성
|
||||||
|
|
||||||
|
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||||||
|
|---:|---|---|---|---|
|
||||||
|
| 1 | `P1-T1` | 없음 | 아니요 | RED/GREEN 테스트의 실패 위치 판정 |
|
||||||
|
| 2 | `P1-T2` | `P1-T1` 완료 | 아니요 | donation 변경과 직접 테스트에서 실패 위치 판정 |
|
||||||
|
| 3 | `P1-GATE` | `P1-T1`, `P1-T2` 완료 | 아니요 | 운영 CDN 접근 실패 시 blocker 유지 |
|
||||||
|
|
||||||
|
```text
|
||||||
|
P1-T1 → P1-T2 → P1-GATE 범위 한정 완료
|
||||||
|
```
|
||||||
|
|
||||||
|
## 변경 금지 항목
|
||||||
|
|
||||||
|
- Query의 secret filtering을 제거하는 시점에 service masking과 E2E 증거를 함께 완성한다. raw 비밀 필드를 공개하는 중간 배포를 하지 않는다.
|
||||||
|
- 월 경계, 홈 limit 8, donation default/page size, 최신순 tie-breaker, 인증·block·creator validation을 바꾸지 않는다.
|
||||||
|
- 기존 랭킹 query/DTO를 수정하지 않는다.
|
||||||
|
- 익명 이미지 URL은 `"$cloudFrontHost/profile/anonymous-profile.png"`로 확정되었고 설정된 CDN에서 접근 확인했다. 업로드는 수행하지 않았다. 공개/본인 profile 및 기존 default-profile fallback은 변경하지 않는다.
|
||||||
|
- 신규 memberId/donationId를 노출하거나 테스트를 삭제·skip·완화하지 않는다.
|
||||||
|
- [`docs/20260622_크리에이터_채널_후원_탭_API/prd.md`](../20260622_크리에이터_채널_후원_탭_API/prd.md)는 편집하지 않는다.
|
||||||
|
|
||||||
|
## 의사결정 및 중단 규칙
|
||||||
|
|
||||||
|
- `P1-T1`과 `P1-T2`는 확정된 파일명·URL 계약으로 구현 완료했다. 설정된 CDN 접근 검증을 마쳤다. 전체 suite timeout은 범위 한정 Gate의 성공과 구분한다.
|
||||||
|
- EN/JA는 사용자 승인 문구가 아닌 작성자 기술 초안이다. implementation review에서 승인/수정 확인을 기록하며 사용자 확정 문구로 간주하지 않는다.
|
||||||
|
- 회귀가 발견되면 해당 Task의 새 회귀 수정 체크박스와 증거를 계획에 추가한 뒤 수정한다. 성공을 추정하지 않는다.
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
### 커밋 전 재검증: 2026-11-04
|
||||||
|
|
||||||
|
- 사용자 `/commit-with-validation` 요청으로 기존 변경의 커밋을 진행한다. 이전 구현 단계의 커밋 미실행 기록은 당시 상태로 보존한다.
|
||||||
|
- 실행: `./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.*' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.*' ktlintCheck`
|
||||||
|
- 결과: exit 0, `BUILD SUCCESSFUL` 1분 56초. XML 13개에서 home 89개와 donation 63개, 총 152개 테스트의 실패·오류·skip 0을 확인했다. lint는 `UP-TO-DATE`이며 `git diff --check`도 통과했다.
|
||||||
|
- 이번 검증에서 전체 테스트, `bootJar`, live server 및 CDN 접근은 재실행하지 않았다. 기존 전체 suite timeout 이력은 유지하며, 최신 실행 지침에 따라 전체 테스트는 사용자가 `./gradlew test`로 실행한다.
|
||||||
|
|
||||||
|
### 현재 완료 상태: 2026-11-04
|
||||||
|
|
||||||
|
- 상태: `P1-T1`, `P1-T2` 구현 및 독립 리뷰 승인 완료. `P1-GATE`는 아래 범위 한정 검증 완료. 전체 프로젝트 suite는 timeout으로 결과 미확정이며 전체 suite 통과 또는 전체 release 검증 완료로 표시하지 않는다.
|
||||||
|
- 통합 검증: `./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.*' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.*' ktlintCheck bootJar` exit 0, `BUILD SUCCESSFUL` 49s. 총 152 tests (home 89, donation 63), failures/errors/skipped 0. 출력 artifact: `build/libs/sodalive-0.0.1-SNAPSHOT.jar`.
|
||||||
|
- 이미지 접근: `CLOUD_FRONT_HOST/profile/anonymous-profile.png` 요청은 host를 출력하거나 기록하지 않고 HTTP 200, `image/png`, 19,331 bytes를 반환했다. 자산 업로드는 하지 않았다.
|
||||||
|
- Surface 검증은 SpringBootTest/H2/MockMvc의 실제 endpoint 요청 경로다. 별도 live server/curl 요청은 하지 않았다.
|
||||||
|
- 전체 프로젝트 `./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test`는 `:test`에서 600000ms tool timeout에 도달했다. 최종 결과가 없고 실패 assertion도 출력되지 않았으므로 성공/실패 어느 쪽으로도 판정하지 않는다. 이후 Gradle/Test/Redis 프로세스가 남지 않았음을 확인했다.
|
||||||
|
- 독립 reviewer는 P1-T2와 전체 feature를 승인했으며 blocker가 없었다. 홈의 2026년 12월 월 경계 결함 수정은 T1 Progress 기록에 보존되어 있다.
|
||||||
|
- Kotlin LSP 진단은 도구 부재로 수행할 수 없었다. home은 11개 변경 파일, donation은 executor에서 진단 도구를 사용할 수 없었다. 컴파일·lint 성공은 LSP 진단 성공으로 간주하지 않는다.
|
||||||
|
- EN/JA 번역은 기존 문서 결정대로 기술 초안이며 제품 승인 문구로 간주하지 않는다. 커밋, worktree, push, 배포, live application server 실행은 하지 않았다.
|
||||||
|
|
||||||
|
### 이전 실행 기록
|
||||||
|
|
||||||
|
### 문서 작성: 2026-11-04
|
||||||
|
|
||||||
|
- 상태: 계획 작성 완료, 구현 미착수
|
||||||
|
- 무엇을: 요구사항과 구현 경로를 확인하고 PRD 및 goal 실행 계획을 작성했다.
|
||||||
|
- 왜: 구현 전 요구사항과 테스트 가능한 완료 기준을 고정하기 위해서다.
|
||||||
|
- 어떻게:
|
||||||
|
- 샘플 `docs/sample/sample-prd.md`, `docs/sample/sample-plan-task.md` 및 `docs/agent-guides/문서유지보수.md`/`작업절차.md`를 읽었다. 과거 기준 문서는 [`docs/20260622_크리에이터_채널_후원_탭_API/prd.md`](../20260622_크리에이터_채널_후원_탭_API/prd.md)다.
|
||||||
|
- Kotlin source 및 두 기존 `SpringBootTest` MockMvc E2E 파일을 읽고 현재 persistence filter, service mapper, DTO 필드, test fixture 구조를 확인했다.
|
||||||
|
- 문서 전용 작업이므로 Gradle, 테스트, 빌드, 서버 요청은 실행하지 않았다.
|
||||||
|
- 남은 항목: 실제 코드 변경 및 모든 구현 체크박스, 외부 의존 `EXT-001` 이미지 제공/검증.
|
||||||
|
- 다음 행동: `P1-T1`의 첫 RED 테스트를 작성한다. 구현 착수 시 실제 자산 제공 여부를 다시 확인한다.
|
||||||
|
|
||||||
|
### 문서 정합성 검증: 2026-11-04
|
||||||
|
|
||||||
|
- 두 문서를 직접 읽고 인터뷰 결정 및 현재 source/test 구조와 대조했다. 읽기 전용 독립 검토 결과는 APPROVE이며 차단 지적은 없었다.
|
||||||
|
- Node 정적 검사로 문서의 Kotlin 경로 23개와 로컬 링크 8개의 존재, trailing whitespace 없음, 끝 개행, 구현 체크박스 미완료 상태를 확인했다.
|
||||||
|
- 두 Markdown 파일에 `lsp_diagnostics`를 시도했으나 `.md` 서버 미설정으로 진단할 수 없었다. 문서 간 대조와 정적 검사를 대체 근거로 사용했다.
|
||||||
|
- 문서 전용 변경이므로 프로젝트 문서 유지보수 규칙에 따라 Gradle 테스트·빌드·API 실행 검증은 생략했다. 위 미래 구현 검증 명령의 성공을 의미하지 않는다.
|
||||||
|
|
||||||
|
### 이미지 계약 정정: 2026-11-04
|
||||||
|
|
||||||
|
- 기존 Progress 및 `ISSUE-001` 기록은 작성 당시의 filename/path 미제공 상태를 나타내는 역사 기록이다. 이후 사용자 입력으로 filename/path 계약이 제공되어 해당 blocker 상태는 현재 유효하지 않다.
|
||||||
|
- 파일명은 `anonymous-profile.png`이며 URL은 기존 `defaultProfileImageUrl()`과 동일한 직접 host/path 조합인 `"$cloudFrontHost/profile/anonymous-profile.png"`로 확정했다. 테스트 host `https://cdn.test`의 기대 URL은 `https://cdn.test/profile/anonymous-profile.png`다.
|
||||||
|
- 이 계약은 마스킹 대상 비밀 donation item에만 적용한다. 공개 및 본인 원본 profile, 기존 default-profile fallback은 그대로 둔다. 운영 파일 업로드와 실제 CDN 접근 가능성은 검증하지 않았으며 `P1-GATE`에 남는다.
|
||||||
|
- 실행 체크박스는 모두 미완료이며 테스트, 빌드, API 요청, CDN 접근 검증은 수행하지 않았다.
|
||||||
|
|
||||||
|
### P1-T1 구현 및 검토: 2026-11-04
|
||||||
|
|
||||||
|
- 완료: 홈 repository의 비밀행 제외 제거, persisted `isSecret` 전달, ID 기반 마스킹, KO/EN/JA 문구와 익명 프로필 URL을 구현했다.
|
||||||
|
- RED: 기존 E2E에서 `No value at JSON path "$.data.channelDonations[0].isSecret"` assertion 실패를 먼저 확인했다. 확장 테스트에서도 기존 필터의 비밀행 누락을 확인했다.
|
||||||
|
- GREEN: `./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.*'` 성공. 89개, 실패·오류·skip 0 (E2E 18, repository 38, service 23, policy 5, controller 3, facade 2).
|
||||||
|
- 독립 확인: 부모 세션에서 홈 E2E를 같은 옵션으로 재실행하여 성공했다. 변경 파일 11개를 읽고 검토했다.
|
||||||
|
- 리뷰 수정: 테스트의 UTC 월초 `plusMonths` 계산이 12월에 잘못되는 문제를 고정 assertion으로 재현하고 KST 기준 월 이동으로 수정했다. 추가된 테스트 2파일의 ktlint 포맷 위반도 수정했다.
|
||||||
|
- 수정 후 `test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.application.CreatorChannelHomeQueryServiceTest' ktlintCheck` (위 공통 Gradle 옵션) 성공. E2E 18 + service 23, 실패·skip 0. 새 읽기 전용 검토에서 이전 P2 지적 ADDRESSED 및 새 결함 없음 확인.
|
||||||
|
- Kotlin LSP 미설치로 11개 파일 진단 불가. Gradle Kotlin 컴파일 및 ktlint 결과를 기록하며 LSP 통과로 간주하지 않는다.
|
||||||
|
- 실제 API surface는 SpringBootTest/H2/MockMvc 전체 요청 경로로 확인했다. 운영 CDN 접근은 아직 미검증이며 Gate에서 별도로 판정한다.
|
||||||
|
|
||||||
|
### P1-GATE 이미지 사전 검증: 2026-11-04
|
||||||
|
|
||||||
|
- 환경의 `CLOUD_FRONT_HOST`가 설정되어 있음을 확인하고, host 값을 출력하지 않은 채 `curl --silent --show-error --max-time 20 --output /dev/null --write-out 'HTTP_STATUS=%{http_code}\nCONTENT_TYPE=%{content_type}\nSIZE_DOWNLOAD=%{size_download}\n' "$CLOUD_FRONT_HOST/profile/anonymous-profile.png"`를 실행했다.
|
||||||
|
- 결과: HTTP 200, Content-Type `image/png`, 다운로드 크기 19,331바이트. 설정된 CDN의 실제 익명 이미지 접근 가능성을 확인했다. 자산 업로드·수정은 하지 않았다.
|
||||||
|
- 이미지 접근 조건만 충족했으며, `P1-T2`와 통합 회귀·리뷰가 끝나기 전 전체 Gate는 완료하지 않는다.
|
||||||
|
|
||||||
|
### P1-T2 구현 및 통합 Gate: 2026-11-04
|
||||||
|
|
||||||
|
- T2 RED: production 변경 전 MockMvc E2E 5개 중 1개가 `$.data.donations[0].isSecret` 누락 assertion으로 실패했다. T2 GREEN: donation 패키지 63 tests와 `ktlintCheck` 통과. 역할별 마스킹, count/page, KST 경계, KO/EN/JA 및 legacy ranking 회귀를 검증했다. E2E 21, repository 14, service 17, policy 5, controller 2, facade 2, legacy ranking adapter 2개가 통과했다. 결과 XML과 HTML은 `build/test-results/test/`, `build/reports/tests/test/`에 있다.
|
||||||
|
- 통합 명령은 위 현재 완료 상태에 기록한 home/donation 4 package 패턴, `ktlintCheck`, `bootJar`를 함께 실행했으며 152/152 tests, lint 및 jar 성공으로 끝났다. jar 경로는 `build/libs/sodalive-0.0.1-SNAPSHOT.jar`다.
|
||||||
|
- CDN 이미지 확인은 HTTP 200, `image/png`, 19,331 bytes였다. host는 로그에 기록하지 않았다. 자산 업로드는 하지 않았다.
|
||||||
|
- 독립 reviewer는 P1-T2와 전체 feature를 승인했고 blocker가 없었다.
|
||||||
|
- 전체 프로젝트 suite 추가 실행은 `:test` 단계에서 600000ms timeout에 도달해 최종 결과를 얻지 못했다. 실패 assertion 출력은 없었지만 suite 통과로 간주하지 않는다. 프로세스 확인에서 남은 Gradle/Test/Redis 프로세스는 없었다.
|
||||||
|
- LSP 진단은 home 11개 파일 및 donation에서 사용 가능한 도구가 없어 완료되지 않았다. Kotlin 컴파일과 ktlint 결과만 검증 근거다. 실제 live server 요청은 없으며 surface QA는 MockMvc 경로로 한정한다.
|
||||||
|
- EN/JA는 기존 기술 초안 상태를 유지하며 제품 승인으로 변경하지 않는다. commit/worktree/push/deploy는 없었다.
|
||||||
|
|
||||||
|
## Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-11-04 | `DEC-001` | 확정 | 홈 및 후원 탭을 각각 저장소·service·DTO·E2E까지 완료하는 bounded Task로 나눈다. | 서로 다른 domain port와 DTO, 독립 E2E가 존재 | `P1-T1`, `P1-T2` |
|
||||||
|
| 2026-11-04 | `DEC-002` | 확정 | 기존 local mapper와 CDN 변환을 사용하고 공용 masking abstraction 및 port signature 변경을 만들지 않는다. | 현재 service 구현 방식과 최소 변경 | `P1-T1`, `P1-T2` |
|
||||||
|
| 2026-11-04 | `DEC-003` | 확정 (과거 기록, `DEC-004`로 정정) | 익명 이미지 자산은 P0 외부 의존이며 경로 검증 전 Gate를 통과시키지 않는다. | 사용자 제공 자산 필요 | `EXT-001`, `P1-GATE` |
|
||||||
|
| 2026-11-04 | `DEC-004` | 확정 | 사용자가 제공한 파일명은 `anonymous-profile.png`; 기존 default-profile 방식대로 `cloud.aws.cloud-front.host`에 `/profile/anonymous-profile.png`를 직접 붙인다. URL 계약은 확정됐지만 운영 CDN 접근은 미검증이다. | 사용자 입력과 두 query service의 기존 defaultProfileImageUrl 근거 | `P1-T1`, `P1-T2`, `P1-GATE`, `ISSUE-001` |
|
||||||
|
| 2026-11-04 | `DEC-005` | 확정 | 익명 이미지는 마스킹 대상 비밀 donation item에만 쓰고 공개/본인 원본 profile과 기존 default-profile fallback은 보존한다. | 기존 표시 정책 유지 | `P1-T1`, `P1-T2` |
|
||||||
|
|
||||||
|
## 발견된 문제
|
||||||
|
|
||||||
|
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `ISSUE-001` | Blocker | 해결 | 파일명·URL 계약 및 설정된 CDN 접근 검증을 완료했다. 별도의 전체 suite timeout은 제품 버그로 분류하지 않고 현재 검증 한계로 기록한다. | `P1-GATE` | 추가 조치 없음. 전체 suite 결과는 미확정이며 통과로 주장하지 않는다. |
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
# 크리에이터 채널 비밀 후원 익명 조회 PRD
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 문서 상태 | 구현 및 기능 범위 Gate 완료, 전체 테스트 suite 검증은 timeout으로 미완료 |
|
||||||
|
| 작성일 | 2026-11-04 |
|
||||||
|
| 최종 수정일 | 2026-11-04 |
|
||||||
|
| 대상 제품 | 크리에이터 채널 비밀 후원 익명 조회 |
|
||||||
|
| 작성자·결정권자 | 제품 요청 기준 |
|
||||||
|
| 관련 API Contract | 이 문서 §8 |
|
||||||
|
| 관련 구현 계획 | [plan-task.md](plan-task.md) |
|
||||||
|
| 관련 review | 없음 |
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
|
||||||
|
크리에이터 채널 홈과 후원 탭에서 비밀 후원 행은 그대로 보이되, 크리에이터와 후원자 본인 외의 조회자에게 후원자의 식별 정보와 메시지를 숨긴다. 조회 응답은 비밀 여부를 모든 사용자에게 표시해 클라이언트가 비밀 후원을 식별할 수 있게 한다. 마스킹 대상 비밀 후원 이미지 URL은 기존 `default-profile`과 같은 방식으로 `cloud.aws.cloud-front.host`에 `/profile/anonymous-profile.png`를 붙여 구성한다. 구현 및 범위 한정 검증을 마쳤고, 전체 테스트 suite는 실행 제한 시간 내 완료하지 못했다.
|
||||||
|
|
||||||
|
## 2. Problem Statement
|
||||||
|
|
||||||
|
현재 두 조회 경로는 비밀 후원을 일반 조회자에게 결과에서 제거한다. 그 결과 후원 건수와 목록의 의미가 조회자에 따라 달라지고, 사용자는 비밀 후원이 있었는지 확인할 수 없다. 해결 후에는 건수와 행은 유지하면서 권한에 맞게 표시 필드만 가려야 한다.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
### 3.1 제품 목표
|
||||||
|
|
||||||
|
- 홈의 `channelDonations`와 후원 탭 `donations`에 현재 KST 월의 비밀 후원을 포함한다.
|
||||||
|
- 비밀 후원에 한해 크리에이터 본인 또는 해당 후원자 본인에게 원래 닉네임, 메시지, 프로필 이미지를 보여 준다. 공개 후원은 기존처럼 모든 인증 조회자에게 원본을 보여 준다.
|
||||||
|
- 비밀 후원의 나머지 조회자에게 확정된 익명 표시 문구와 `"$cloudFrontHost/profile/anonymous-profile.png"` 이미지를 보여 준다. 공개 후원과 크리에이터·후원자 본인의 프로필 이미지 및 기존 default-profile fallback은 변경하지 않는다.
|
||||||
|
- 두 응답의 모든 후원 항목에 저장된 비밀 여부를 나타내는 `isSecret`을 제공한다.
|
||||||
|
|
||||||
|
### 3.2 UX 목표
|
||||||
|
|
||||||
|
- 익명화된 후원도 목록의 건수와 정렬을 유지해 숨겨진 행으로 오해되지 않게 한다.
|
||||||
|
- 메시지가 null 또는 빈 문자열이어도 비공개 문구를 표시한다.
|
||||||
|
|
||||||
|
## 4. Non-Goals
|
||||||
|
|
||||||
|
- 후원 작성, 수정, 취소, DB 스키마 또는 DDL 변경.
|
||||||
|
- 공개 후원의 표시 변경.
|
||||||
|
- 랭킹의 노출·집계·익명성 변경. 기존 랭킹 식별 정보와 비밀 후원 집계는 유지한다.
|
||||||
|
- 플랫폼 전체에서 후원자를 알아볼 수 없게 하는 익명성 보장. 이 변경은 두 목록의 특정 필드만 가린다.
|
||||||
|
- 인증, 차단, 크리에이터 검증, API route, pagination 정책 변경.
|
||||||
|
- 기존 `20260622` 문서 수정. 본 문서는 목록의 비밀 후원 노출, 건수 및 `isSecret` 표시 범위에 한해 후속 기준이며 그 외 역사적 결정은 대체하지 않는다.
|
||||||
|
|
||||||
|
## 5. Target Users and Permissions
|
||||||
|
|
||||||
|
| 사용자 | 조회 결과 |
|
||||||
|
|---|---|
|
||||||
|
| 채널 크리에이터 (`viewerId == creatorId`) | 공개 및 비밀 후원 모두 원본 필드 |
|
||||||
|
| 후원자 본인 (`viewerId == donor.memberId`) | 자신의 공개 및 비밀 후원 원본 필드 |
|
||||||
|
| 그 외 인증 조회자 | 공개 후원은 원본 필드, 비밀 후원은 행을 유지하고 닉네임·메시지·프로필 이미지를 익명 표시 |
|
||||||
|
|
||||||
|
- 기존 인증 및 크리에이터 조회 권한을 그대로 적용한다.
|
||||||
|
- 기존 차단 확인과 크리에이터 역할 검증을 변경하지 않는다.
|
||||||
|
- 비밀 후원 목록 행에서 새 `memberId` 또는 donation 식별자를 응답에 노출하지 않는다.
|
||||||
|
|
||||||
|
## 6. 핵심 사용자 흐름
|
||||||
|
|
||||||
|
1. 인증 사용자가 크리에이터 채널 홈 또는 후원 탭을 조회한다.
|
||||||
|
2. 서버는 기존 권한·차단·크리에이터 검증을 수행하고 현재 KST 월 후원 목록을 가져온다.
|
||||||
|
3. 비밀 후원은 크리에이터와 해당 후원자만 원본 필드를 받고, 그 외 조회자는 익명화된 값을 받는다. 공개 후원은 기존대로 인증 조회자에게 원본 필드를 제공한다.
|
||||||
|
4. 모든 항목은 `isSecret`으로 원래 비밀 상태를 확인할 수 있고 기존 정렬·건수·pagination을 따른다.
|
||||||
|
|
||||||
|
## 7. 기능 요구사항
|
||||||
|
|
||||||
|
요구사항과 파일명·URL 구성 계약은 확정됐고 구현됐다. 설정된 CDN에서 익명 이미지 URL의 실제 접근도 확인됐다.
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `SDA-001` | 확정 | 홈 `channelDonations`에 현재 KST 월의 공개·비밀 후원을 포함한다. | 현재 월 경계 포함, 이전·다음 월 제외, 전체 후보에서 최신 최대 8건, `createdAt`/id 내림차순. 홈 count 필드는 추가하지 않는다. | §8.1~§8.2, `P1-T1` |
|
||||||
|
| `SDA-002` | 확정 | 후원 탭 `donations`와 `donationCount`에 현재 KST 월의 공개·비밀 후원을 포함한다. | count가 공개/비밀 전체와 일치하고 기존 페이지 경계를 유지 | §8.1~§8.2, `P1-T2` |
|
||||||
|
| `SDA-003` | 확정 | 비밀 후원은 본인 또는 크리에이터에게 원본 필드를 반환하고, 공개 후원은 기존처럼 모든 인증 조회자에게 원본을 반환한다. | 비밀의 경우 `viewerId == creatorId` 또는 `viewerId == donor.memberId`; 공개 행은 인증 viewer 모두 nickname/message/profile 원본 | §8.3, `P1-T1`, `P1-T2` |
|
||||||
|
| `SDA-004` | 확정 | 비밀 후원의 나머지 조회자에게 해당 행 필드만 익명화한다. | 닉네임 `익명`, 메시지 `크리에이터에게만 보여요`, 프로필 URL은 `"$cloudFrontHost/profile/anonymous-profile.png"` | §8.3, `P1-T1`, `P1-T2`, `EXT-001` |
|
||||||
|
| `SDA-005` | 확정 | 각 후원 항목에 저장된 비밀 여부를 `isSecret`으로 반환한다. | 두 목록, 모든 조회자, 공개·비밀 각각 Boolean이 persisted 값을 반영 | §8.1~§8.2, `P1-T1`, `P1-T2` |
|
||||||
|
| `SDA-006` | 확정 | 익명 닉네임과 메시지 문구를 요청 언어에 맞춰 반환한다. | `Lang.KO/EN/JA`와 기존 `SodaMessageSource` 사용, 두 key 모두 언어별 값, KO 문구 정확 일치 | §8.3, `P1-T1`, `P1-T2` |
|
||||||
|
| `SDA-007` | 완료 | 익명 프로필 이미지 파일명은 `anonymous-profile.png`; URL은 `"$cloudFrontHost/profile/anonymous-profile.png"`로 구성한다. | 설정된 CDN에서 HTTP 200, `image/png`, 19,331 bytes를 확인했다. | `EXT-001`, `P1-GATE` |
|
||||||
|
|
||||||
|
## 8. API 계약
|
||||||
|
|
||||||
|
### 8.1 공통 동작
|
||||||
|
|
||||||
|
- 기존 인증, block 검사, creator 검증과 응답 envelope를 유지한다.
|
||||||
|
- 현재 KST 월 시작은 inclusive, 다음 KST 월 시작은 exclusive이며 저장·비교 시간 변환 규칙은 기존 구현을 유지한다.
|
||||||
|
- 최신순은 `createdAt DESC, donation.id DESC`다.
|
||||||
|
- 비밀 여부는 마스킹 여부와 별개의 persisted 상태다. `isSecret`은 숨김 처리한 행에서도 `true`다.
|
||||||
|
- `can`과 `createdAtUtc`는 실제 값 그대로 반환한다.
|
||||||
|
- 새 donor `memberId`나 donation ID는 목록 DTO에 추가하지 않는다.
|
||||||
|
|
||||||
|
### 8.2 Endpoint 계약
|
||||||
|
|
||||||
|
| Method 및 경로 | 응답 필드 | 목록 및 page 규칙 |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET /api/v2/creator-channels/{creatorId}/home` | `data.channelDonations[]`에 `nickname`, `profileImageUrl`, `can`, `message`, `createdAtUtc`, `isSecret` | 현재 월 전체 대상 중 최신 최대 8건 |
|
||||||
|
| `GET /api/v2/creator-channels/{creatorId}/donations` | `data.donations[]`에 같은 후원 필드와 `isSecret`, `data.donationCount` | 기본 page `0`, 기본 size `20`, size `20..50`, hasNext 기존 규칙 |
|
||||||
|
|
||||||
|
### 8.3 익명화 및 현지화
|
||||||
|
|
||||||
|
마스킹 대상 비밀 후원의 `profileImageUrl`은 `cloud.aws.cloud-front.host`를 사용하는 기존 `defaultProfileImageUrl()`과 동일한 직접 문자열 조합으로 `"$cloudFrontHost/profile/anonymous-profile.png"`를 만든다. 이는 임의 사용자 제공 경로에 `toCdnUrl`을 적용하는 계약이 아니다. 기존 구현 근거는 `CreatorChannelHomeQueryService.kt:258`과 `CreatorChannelDonationQueryService.kt:114`의 `defaultProfileImageUrl(): String = "$cloudFrontHost/profile/default-profile.png"`이다. `https://cdn.test`를 host로 쓰는 테스트 기대 URL은 `https://cdn.test/profile/anonymous-profile.png`다.
|
||||||
|
|
||||||
|
이 익명 URL은 **마스킹 대상 비밀 후원 행에만** 사용한다. 공개 후원과 크리에이터·해당 후원자 본인의 원본 프로필 이미지 및 기존 `default-profile.png` fallback은 그대로 유지한다. 설정된 CDN에서 해당 경로의 이미지 접근을 확인했다. 이 검증은 업로드 주체나 별도 운영 배포 상태를 확인한 것은 아니다.
|
||||||
|
|
||||||
|
기존 `SodaMessageSource`에서 다음 key를 `Lang.KO`, `Lang.EN`, `Lang.JA`별로 제공한다.
|
||||||
|
|
||||||
|
| Key | KO | EN 기술 초안 | JA 기술 초안 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `explorer.channel_donation.anonymous.nickname` | `익명` | `Anonymous` | `匿名` |
|
||||||
|
| `explorer.channel_donation.anonymous.message` | `크리에이터에게만 보여요` | `Only visible to the creator` | `クリエイターにのみ表示されます` |
|
||||||
|
|
||||||
|
EN/JA 문구는 사용자가 명시 승인한 문구가 아니며 작성자의 기술 초안이다. 구현 review에서 제품 문구 확인 대상으로 고정하고 `SodaMessageSource` 외부 번역 서비스를 도입하지 않는다. null 또는 빈 원본 메시지에도 익명 조회자는 위 문구를 받는다.
|
||||||
|
|
||||||
|
### 8.4 랭킹 제한
|
||||||
|
|
||||||
|
랭킹 query·응답·가시성·합계를 변경하지 않는다. 랭킹의 `userId`, 닉네임, 프로필은 기존 규칙대로 노출될 수 있고 집계에는 비밀 후원 기여분이 포함된다. 따라서 이 기능만으로 전체 서비스 차원의 후원자 익명성이 보장되지 않는다.
|
||||||
|
|
||||||
|
## 9. 외부 의존
|
||||||
|
|
||||||
|
| ID | 우선순위 | 제공 필요 계약 | 담당 주체 | 구현 영향 | 재개 조건 |
|
||||||
|
|---|---:|---|---|---|---|
|
||||||
|
| `EXT-001` | 완료 | 파일명 `anonymous-profile.png`; URL 계약 `"$cloudFrontHost/profile/anonymous-profile.png"` (`cloud.aws.cloud-front.host` 사용) | 사용자/제품 자산 제공자 | 익명 조회자의 이미지 필드. 설정된 CDN에서 접근 확인 | HTTP 200, `image/png`, 19,331 bytes 확인. 업로드 작업은 수행하지 않음 |
|
||||||
|
|
||||||
|
테스트 host `https://cdn.test`의 정확한 기대 URL은 `https://cdn.test/profile/anonymous-profile.png`다. 신규 config, fallback, 공통 helper 또는 임의 사용자 경로용 `toCdnUrl`은 추가하지 않는다. 설정된 CDN 접근 검증은 통과했다. 저장소 필터를 없애 raw secret 필드를 반환하는 코드를 먼저 배포해서는 안 된다.
|
||||||
|
|
||||||
|
## 10. 보안과 데이터 취급
|
||||||
|
|
||||||
|
- 익명화는 응답 DTO 이전 서버 서비스 변환에서 수행한다. 익명 뷰어에게 raw 비밀 필드를 보내지 않는다.
|
||||||
|
- 저장소가 비밀 행을 조회하도록 바뀌어도 원본 값이 API 출력·로그·오류에 노출되지 않게 서비스 경계를 검증한다.
|
||||||
|
- 이 변경으로 donor ID를 새로 반환하지 않는다.
|
||||||
|
- 랭킹의 기존 식별 정보가 목록 마스킹을 우회해 보일 수 있음을 문서화하며 전역 익명성을 약속하지 않는다.
|
||||||
|
|
||||||
|
## 11. 성공 기준과 추적
|
||||||
|
|
||||||
|
### 11.1 수용 기준
|
||||||
|
|
||||||
|
- [x] 홈 `channelDonations`는 전체 현재 월 공개·비밀 후보 중 최신 8건을 유지하며 새 home count 필드를 추가하지 않는다 (`SDA-001`).
|
||||||
|
- [x] 후원 탭 공개/비밀 모두 건수·페이지·hasNext에 포함된다 (`SDA-002`).
|
||||||
|
- [x] 비밀 후원은 크리에이터와 해당 후원자 본인만 원본을 보고 다른 사용자는 익명 문구와 이미지 자산을 본다. 공개 후원은 모두 원본을 본다 (`SDA-003`, `SDA-004`).
|
||||||
|
- [x] `isSecret`은 모든 응답 항목에 Boolean으로 직렬화되고 원래 상태를 나타낸다 (`SDA-005`).
|
||||||
|
- [x] `can`, `createdAtUtc`, 기존 인증·차단·크리에이터 오류 및 랭킹 동작이 유지된다.
|
||||||
|
- [x] URL 계약과 설정된 CDN 접근을 검증했다. HTTP 200, `image/png`, 19,331 bytes (`SDA-007`).
|
||||||
|
- [x] HTTP MockMvc E2E로 홈과 후원 탭의 익명/본인/크리에이터 응답을 검증했다. 별도 live application server 요청은 수행하지 않았다.
|
||||||
|
|
||||||
|
범위 한정 feature gate는 package tests, `ktlintCheck`, `bootJar`, CDN 접근 검증으로 완료했다. 전체 프로젝트 test suite는 제한 시간 timeout으로 완료되지 않았으며 전체 suite 통과를 뜻하지 않는다.
|
||||||
|
|
||||||
|
### 11.2 추적표
|
||||||
|
|
||||||
|
| 요구사항 | 구현 계획 | 자동 검증 |
|
||||||
|
|---|---|---|
|
||||||
|
| `SDA-001`, `SDA-003`, `SDA-005`, `SDA-006` | `P1-T1` | 홈 repository/service/controller 및 `CreatorChannelHomeEndToEndTest` |
|
||||||
|
| `SDA-002`~`SDA-006` | `P1-T2` | donation repository/service/controller 및 `CreatorChannelDonationEndToEndTest` |
|
||||||
|
| `SDA-007` | `P1-T1`, `P1-T2`, `P1-GATE` | 두 Task는 확정 URL 계약과 기존 이미지 회귀를 검증하고, Gate는 운영 CDN 접근 가능 여부를 검증 |
|
||||||
|
|
||||||
|
## 12. 과거 문서와의 관계
|
||||||
|
|
||||||
|
[`docs/20260622_크리에이터_채널_후원_탭_API/prd.md`](../20260622_크리에이터_채널_후원_탭_API/prd.md)의 문서는 작성 당시의 역사적 기록으로 남긴다. 본 PRD는 홈 및 후원 탭에서 비밀 후원을 제거하던 기존 visibility 동작, 그에 따른 목록 건수, 응답 `isSecret` 부재에 대해서만 후속 기준을 정한다. 랭킹을 비롯한 본 문서에 명시하지 않은 기존 결정은 바꾸지 않는다.
|
||||||
|
|
||||||
|
## 13. Decision Log
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 2026-11-04 | `DEC-001` | 확정 | 두 목록은 전체 현재 월 비밀 후원을 포함하고 표시 필드만 권한별 마스킹한다. | 요청에서 확정된 제품 동작 | `SDA-001`~`SDA-006`, §8 |
|
||||||
|
| 2026-11-04 | `DEC-002` | 확정 | `isSecret`은 persisted 비밀 상태를 보여 주며 모든 사용자에게 제공한다. | 요청에서 확정된 API 동작 | `SDA-005` |
|
||||||
|
| 2026-11-04 | `DEC-003` | 확정 | 랭킹은 변경하지 않고 전역 익명성을 보장하지 않는다. | 요청에서 확정된 범위 | §8.4 |
|
||||||
|
| 2026-11-04 | `DEC-004` | 외부 의존 (과거 기록, `DEC-006`으로 정정) | 익명 프로필 이미지는 자산 경로를 받기 전까지 운영 구현 및 완료를 차단한다. | 실제 이미지 제공 필요 | `EXT-001`, `P1-GATE` |
|
||||||
|
| 2026-11-04 | `DEC-005` | 기술 초안 | EN/JA 문구는 작성자 초안으로 구현 review 때 제품 확인한다. | 한국어만 정확 문구 승인됨 | `SDA-006` |
|
||||||
|
| 2026-11-04 | `DEC-006` | 확정 | 사용자가 제공한 익명 이미지 파일명은 `anonymous-profile.png`; 기존 `defaultProfileImageUrl()`과 동일하게 `cloud.aws.cloud-front.host`를 사용해 `"$cloudFrontHost/profile/anonymous-profile.png"`로 직접 구성한다. | 파일명 및 default-profile 구성 방식 사용자 확정. 운영 CDN 접근성은 별도 미검증 | `SDA-007`, `EXT-001`, §8.3 |
|
||||||
|
| 2026-11-04 | `DEC-007` | 확정 | 익명 이미지는 마스킹 대상 비밀 후원 행에만 적용하고 공개/본인 원본 이미지와 기존 default-profile fallback은 보존한다. | 기존 표시 정책 유지 | `SDA-003`, `SDA-004`, §8.3 |
|
||||||
|
|
||||||
|
## 14. 변경 관리
|
||||||
|
|
||||||
|
요구사항 변경은 Decision Log, 이 PRD, 이 문서 안 API 계약, [plan-task.md](plan-task.md) 순서로 반영한다. 구현 시작 전에는 외부 의존 경계를 다시 확인하고, 진행 체크박스 및 검증 결과를 실제 증거에 따라 누적한다.
|
||||||
Reference in New Issue
Block a user