docs(channel-donation): 익명 표시 응답 계약과 검증 결과를 기록한다

This commit is contained in:
2026-11-05 18:36:05 +09:00
parent 454f20751d
commit 64b52db208
2 changed files with 94 additions and 16 deletions
@@ -2,19 +2,19 @@
| 문서 항목 | 내용 |
|---|---|
| 상태 | `P1-T1`, `P1-T2`, 범위 한정 `P1-GATE` 완료. 전체 suite는 timeout으로 미검증 |
| 상태 | Phase 1 및 `P2-T1`·`P2-GATE` 완료. 전체 테스트는 사용자 실행 |
| 작성일 | 2026-11-04 |
| 요구사항 기준 | [prd.md](prd.md) |
| API 기준 | [prd.md §8](prd.md#8-api-계약) |
| 현재 Phase | Phase 1: 두 후원 조회 경로 완성 |
| 현재 Phase | Phase 2: 응답 isSecret UI 표시 의미 변경 |
| 현재 활성 Goal | 없음 |
| 다음 Goal | 없음. 전체 suite 완료 여부는 별도 미해결 검증 한계 |
| 다음 Goal | 없음 |
> 실행 준비 기준: 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 하나에서 함께 완성한다.
**Architecture:** persistence record는 실제 `isSecret`을 유지한다. 서비스의 기존 `toDomain` mapper가 조회자별 `isMasked`로 표시 필드와 응답 domain의 `isSecret`을 정한다. DTO 필드명·타입과 기존 이미지 URL 조합은 바꾸지 않는다.
**Spec:** [prd.md](prd.md), 특히 §7~§10.
@@ -24,7 +24,7 @@
- 비밀 행은 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.
- 응답 `isSecret`은 조회자별 마스킹 Boolean, DB/record는 원래 비밀 Boolean이다. 양 endpoint 역할별 JSON과 저장값 불변을 검증하고 랭킹은 유지한다.
## 목표
@@ -35,6 +35,7 @@
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 범위 한정 완료 | `2/2` | 없음 | 전체 suite는 600000ms timeout으로 결과 미확정. 전체 suite 통과로 간주하지 않음 |
| 2 | 완료 | `1/1` | 없음 | 관련 196개 회귀 및 최종 HTTP 55개 통과. 전체 테스트는 사용자 실행 |
- 구현 단계의 실행 체크박스는 실제 테스트 및 검토 증거가 확보된 뒤에만 완료 처리한다.
- 미완료 Goal은 동시에 하나만 실행한다.
@@ -44,7 +45,7 @@
### 포함
- 홈 및 후원 탭의 현재 KST 월 비밀 후원 조회, persisted `isSecret`, 서비스 계층 마스킹.
- 홈 및 후원 탭의 현재 KST 월 비밀 후원 조회, DB 비밀 설정 보존, 조회자별 UI flag `isSecret` 및 서비스 계층 마스킹.
- 양쪽 DTO 직렬화와 repository, service, controller, facade 및 HTTP MockMvc E2E 검증.
- 기존 접근 제어, 정렬, count/page, 랭킹, 날짜·금액 계약 보존.
@@ -58,7 +59,7 @@
## 기술적 제약
- Kotlin, Spring Boot 2.7.14, Java 17, Gradle Wrapper, QueryDSL 및 기존 `SodaMessageSource`/CDN URL 변환을 사용한다.
- 공개 API 목록 DTO에 donor `memberId`나 donation ID를 추가하지 않는다. 내부 record/domain의 기존 `memberId`는 권한 판정에 유지한다.
- 이번 변경에서 공개 API 필드를 추가하지 않는다. 내부 `memberId`, 현재 채널 주인 전용 `donationId` 및 숨김 정책은 유지한다.
- persistence에서 비밀 row를 유지하되 서비스가 response domain을 만들기 전 익명화한다. 두 계층 모두 수정과 테스트를 하나의 end-to-end Task로 수행한다.
- 각 Task의 `isSecret` 추가는 해당 탭의 port record, domain model, QueryDSL constructor projection 순서와 해당 테스트 fixture constructor를 함께 갱신한다.
- 로컬 변환 함수 `toDomain`이 이미 있으므로 기존 함수가 있는 service 안에서 viewer/creator/lang 판정을 수행한다. 새 공유 추상화는 만들지 않는다.
@@ -232,8 +233,73 @@
- EN/JA는 사용자 승인 문구가 아닌 작성자 기술 초안이다. implementation review에서 승인/수정 확인을 기록하며 사용자 확정 문구로 간주하지 않는다.
- 회귀가 발견되면 해당 Task의 새 회귀 수정 체크박스와 증거를 계획에 추가한 뒤 수정한다. 성공을 추정하지 않는다.
### Phase 2: 응답 isSecret을 익명 UI 표시 여부로 변경
**결과:** 두 탭에서 익명 표시가 필요한 타인의 비밀 후원만 `isSecret=true`를 반환한다. Phase 1 완료 기록은 당시 저장값 전달 계약의 이력으로 보존하며 현재 의미는 이 Phase가 우선한다.
- [x] **Task 2.1: 조회자별 UI flag 전달**
**Goal `P2-T1`:** 기존 마스킹 판정과 응답 flag를 일치시키되 DB 비밀 설정과 나머지 응답·권한 정책을 유지한다.
- 시작 조건: PRD `DEC-008` 승인. 현재 `test` 브랜치에서 진행, worktree·commit·push 없음.
- 완료 증거: 기존 HTTP 역할/언어 매트릭스 RED→GREEN, 두 기능 회귀 성공, DB 비밀값 불변과 owner 전용 donationId/숨김 회귀 통과.
- 범위 밖: DB/record/쿼리 수정, 추가 응답 필드, 숨김·랭킹·인증·시각 정책 변경.
**Files:**
- 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/donation/application/CreatorChannelDonationQueryService.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/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.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/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt`
**Interface:** 로컬 mapper에서 응답 domain 생성 인자 `isSecret = isMasked`를 사용한다. 입력 record의 실제 비밀값·기존 mapper signature 및 DTO signature는 유지한다.
- [x] **RED:** 기존 두 HTTP E2E 및 service 테스트에서 내 채널/본인 비밀 후원의 flag 기대값을 false로, 타인 비밀은 true로 변경했다. 공개·타인 CREATOR·언어·내용·이미지·금액·시각·donationId assertion은 유지했다.
- [x] **RED 확인:** 두 E2E 메서드 실행은 컴파일 성공 후 `:test` 단계에서 600000ms timeout으로 assertion 결과를 얻지 못했다. 두 service의 역할별 메서드만 실행해 22개 중 8개에서 expected false / actual true 실패를 확인했다. HTTP 검증은 환경 지연 원인을 조사한 뒤 별도로 완료한다.
- [x] **GREEN:** 두 서비스의 반환 `isSecret` 값만 `isMasked`로 바꿨다. 테스트는 fixture/id/can으로 원본 공개·비밀 row를 선택하며 소유자의 응답 flag로 원본 비밀 여부를 추정하지 않는다.
- [x] **GREEN 확인:** 두 service 46개, 관련 패키지 196개 및 최종 HTTP E2E 55개가 통과했다. 조회 후 별도 트랜잭션 clear/재조회로 DB 비밀값 보존을 확인했다.
- [x] **REFACTOR:** 추가 추상화 없이 기능별 회귀·lint 성공과 독립 검토 승인을 기록했다. repository record의 실제 비밀값 assertion은 변경하지 않았다.
**Goal `P2-GATE`:** 두 기능의 새 flag 계약 및 직접 영향 회귀를 판정한다.
- 시작 조건: `P2-T1` 구현 및 검증 완료.
- 완료 증거: 아래 명령 성공과 독립 검토. 전체 테스트는 사용자가 `./gradlew test`로 실행한다.
- TDD 예외 사유: 검증 전용 Goal이며 제품 코드를 추가하지 않는다.
- 대체 검증: 실제 SpringBootTest/H2/MockMvc 요청 결과와 DB 재조회 assertion, 코드 리뷰 대조.
```bash
./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
```
실행 순서: `P2-T1` → `P2-GATE`. 이번 변경에서는 전체 테스트를 자동 실행하지 않는다.
- [x] `P2-GATE`: 관련 기능 회귀, 실제 HTTP/MockMvc 응답, DB 불변, ktlint 및 독립 검토 통과.
## Progress
### 응답 isSecret 의미 변경 커밋 전 재검증: 2026-11-05
- 사용자 `/commit-with-validation` 요청에 따라 기존 구현 변경을 커밋한다. 구현 단계의 커밋 미실행 기록은 당시 이력으로 보존한다.
- 실행 명령: `./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests '*CreatorChannelHomeQueryServiceTest' --tests '*CreatorChannelDonationQueryServiceTest' --tests '*CreatorChannelHomeEndToEndTest' --tests '*CreatorChannelDonationEndToEndTest' ktlintCheck --console=plain`.
- 결과: exit 0, `BUILD SUCCESSFUL`, 2분 27초. 네 클래스 101개 테스트, 실패·오류·skip 0. `ktlintCheck`는 `UP-TO-DATE`로 통과했다.
- `git diff --check` 통과. 전체 테스트는 실행하지 않았으며 사용자 실행 명령은 `./gradlew test`다.
### P2-GATE 완료 증거
- 관련 service 두 클래스 46개 및 `ktlintCheck` 성공. 이어 네 home/donation 패키지를 위 명령으로 검증했다 (`--tests` 패턴 앞에 `*` 사용, `--info --console=plain` 추가). 15개 클래스·196개 테스트, 실패·오류·skip 0, exit 0, 1분 32초. 기존 숨김·owner donationId·정산·랭킹 회귀 포함.
- 독립 정적 리뷰 APPROVE: production 변경은 두 반환 할당뿐이며, 저장값과 익명 내용·기존 권한/필드 구조 보존을 확인했다.
- 부모 최종 실행: `./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests '*CreatorChannelHomeEndToEndTest' --tests '*CreatorChannelDonationEndToEndTest' ktlintCheck bootJar --console=plain`. exit 0, 1분 45초. home 18 + donation 37 = 55개, 실패·오류·skip 0. `bootJar` 성공, lint 직전 성공 결과 재사용.
- Surface: RANDOM_PORT/TestRestTemplate/실제 JWT 요청과 MockMvc 역할·언어 매트릭스를 구분해 검증했다. 동일 비밀행의 owner/donor GET은 원문과 false, outsider GET은 익명과 true다. 기존 owner-only donationId, 숨김 후 최신 목록 보충 및 DB 원본 불변 assertion 통과.
- 초기 HTTP timeout은 재현되지 않았다. 진단 실행의 donation/home Spring 시작은 각각 약 25.7초/11.7초였으며, 소유 Test Executor 스레드 덤프는 repository 초기화 중 RUNNABLE 상태였고 이후 정상 종료했다. 과거 timeout 원인은 미확정이며 설정을 변경하지 않았다.
- 범위 밖 관찰: H2의 background scheduler에서 `FOR UPDATE SKIP LOCKED` 문법 오류 로그가 있었으나 관련 테스트 실패는 없었다. 원인을 이번 변경이나 timeout으로 단정하거나 수정하지 않았다.
- Kotlin LSP 미설치/도구 부재로 LSP 통과 주장은 하지 않는다. 관련 Kotlin 컴파일과 lint로 검증했다. 전체 테스트·운영 데이터/CDN 재검증·커밋·푸시는 수행하지 않았다. 전체 테스트 사용자 명령: `./gradlew test`.
### P2-T1 RED 및 최소 변경
- HTTP RED 명령: `./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests '*CreatorChannelHomeEndToEndTest.shouldMaskOnlyOtherMembersSecretDonationsThroughHttp*' --tests '*CreatorChannelDonationEndToEndTest.shouldMaskOnlyOtherMembersSecretDonationsThroughHttp*'`. 컴파일 성공 후 `:test`에서 10분 timeout; RED assertion 증거로 간주하지 않는다.
- 대체 RED 명령: `./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests '*CreatorChannelHomeQueryServiceTest.shouldMaskSecretDonationOnlyForOtherViewers*' --tests '*CreatorChannelDonationQueryServiceTest.shouldMaskSecretDonationOnlyForOtherViewers*'`. 55초, 22개 중 8개 expected false / actual true 실패. 본인 및 채널 주인의 저장된 비밀 후원에서 기대와 다름을 확인했다.
- 그 뒤 production은 두 서비스의 `isSecret = isSecret`을 `isSecret = isMasked`로 바꾼 2줄만 수정했다. repository/record/DB 및 기존 donationId 판정은 변경하지 않았다.
- 전체 테스트는 실행하지 않는다. 기존 기능별 HTTP/서비스 테스트와 직접 영향 package 회귀만 실행하며, 전체 검증은 사용자 명령 `./gradlew test`로 분리한다.
### 커밋 전 재검증: 2026-11-04
- 사용자 `/commit-with-validation` 요청으로 기존 변경의 커밋을 진행한다. 이전 구현 단계의 커밋 미실행 기록은 당시 상태로 보존한다.