# 크리에이터 채널 비밀 후원 익명 조회 구현 계획 | 문서 항목 | 내용 | |---|---| | 상태 | Phase 1 및 `P2-T1`·`P2-GATE` 완료. 전체 테스트는 사용자 실행 | | 작성일 | 2026-11-04 | | 요구사항 기준 | [prd.md](prd.md) | | API 기준 | [prd.md §8](prd.md#8-api-계약) | | 현재 Phase | Phase 2: 응답 isSecret UI 표시 의미 변경 | | 현재 활성 Goal | 없음 | | 다음 Goal | 없음 | > 실행 준비 기준: goal별로 한 번에 하나씩 실행하며, 각 Task의 명령과 체크박스는 실행 후에만 완료 처리한다. 구현 방법은 본 계획의 TDD 순서를 따른다. **실행 방식:** 사용자 지시에 따라 worktree 없이 현재 `test` 브랜치에서 Goal을 순차 실행한다. 커밋·푸시·배포는 하지 않는다. **Architecture:** persistence record는 실제 `isSecret`을 유지한다. 서비스의 기존 `toDomain` mapper가 조회자별 `isMasked`로 표시 필드와 응답 domain의 `isSecret`을 정한다. DTO 필드명·타입과 기존 이미지 URL 조합은 바꾸지 않는다. **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`은 조회자별 마스킹 Boolean, DB/record는 원래 비밀 Boolean이다. 양 endpoint 역할별 JSON과 저장값 불변을 검증하고 랭킹은 유지한다. ## 목표 크리에이터 채널 홈과 후원 탭에서 현재 KST 월 비밀 후원을 유지하며, 크리에이터와 후원자 외의 사용자에게는 식별 정보 대신 익명 필드를 반환한다. ## 현재 상태 | Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | |---:|---|---:|---|---| | 1 | 범위 한정 완료 | `2/2` | 없음 | 전체 suite는 600000ms timeout으로 결과 미확정. 전체 suite 통과로 간주하지 않음 | | 2 | 완료 | `1/1` | 없음 | 관련 196개 회귀 및 최종 HTTP 55개 통과. 전체 테스트는 사용자 실행 | - 구현 단계의 실행 체크박스는 실제 테스트 및 검토 증거가 확보된 뒤에만 완료 처리한다. - 미완료 Goal은 동시에 하나만 실행한다. - 파일명과 URL 구성 계약 및 설정된 CDN 이미지 접근은 확인했다. 전체 프로젝트 suite는 timeout으로 미완료 상태이며 범위 한정 Gate 성공과 분리한다. ## 범위 ### 포함 - 홈 및 후원 탭의 현재 KST 월 비밀 후원 조회, DB 비밀 설정 보존, 조회자별 UI flag `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 필드를 추가하지 않는다. 내부 `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 판정을 수행한다. 새 공유 추상화는 만들지 않는다. - 신규 로직은 먼저 요구 동작을 검증하는 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의 새 회귀 수정 체크박스와 증거를 계획에 추가한 뒤 수정한다. 성공을 추정하지 않는다. ### 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` 요청으로 기존 변경의 커밋을 진행한다. 이전 구현 단계의 커밋 미실행 기록은 당시 상태로 보존한다. - 실행: `./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 결과는 미확정이며 통과로 주장하지 않는다. |