Files

74 KiB

채널 후원 메시지 숨김 구현 계획

문서 항목 내용
상태 서버 구현·기능 범위 검증·최종 리뷰 완료
작성일 2026-11-05
요구사항 기준 prd.md
API 기준 prd.md §8
현재 Phase 전체 Phase 완료
현재 활성 Goal 없음
다음 Goal 없음. 운영 DDL/배포·프론트엔드는 별도 작업

목표와 실행 경계

목표: 소유 CREATOR가 채널 후원 항목을 soft-hide하면 모든 사용자에게 V2 채널 홈과 후원 탭에서 보이지 않고, 원본 후원과 재무 영향은 보존된다.

사용자의 "문서대로 진행" 요청으로 PRD DEC-006의 기술안과 아래 계획 실행을 승인했다. 과거 기록의 초안/미착수 표시는 해당 기록 시점의 상태이며 현재 상태는 상단 표를 따른다. 동시에 하나의 미완료 Goal만 실행하며, Phase 중간 결과를 개별 배포하지 않는다. 배포 시 DDL을 선반영하고 숨김 API와 두 조회 필터를 함께 배포한다. 서버 검증과 프론트엔드 연동 완료는 구분하며, 운영 DDL 실행·배포는 별도 승인 대상이다.

현재 상태

Phase 상태 완료 Task 활성/다음 Goal 차단 또는 남은 조건
1 완료 2/2 없음 Gate 및 독립 리뷰 통과
2 완료 2/2 없음 Gate 및 독립 리뷰 통과
3 완료 1/1 및 P3-R1 없음 최종 리뷰 승인, 운영 적용 별도

범위

포함

  • ChannelDonationMessage.isHidden soft-hide 필드 및 운영 반영용 DDL 파일 추가 Task.
  • 소유 CREATOR 한 명의 단일 donation 숨김 use case와 bodyless DELETE API.
  • 홈과 탭 목록의 local hidden filter, 탭 count/pagination 및 홈 최대 8건 refill.
  • 두 V2 응답에 owner-only donationId 추가 및 회귀 assertion 조정.
  • HTTP/repository 통합 테스트로 권한, 데이터 보존, 기존 랭킹/재무 효과 및 legacy API 불변 검증.

제외

  • 운영 DB DDL 실행, 배포 및 공유 인프라 변경. 코드·테스트·DDL 문서 작성은 사용자 승인으로 포함한다.
  • 복원, hard delete, audit metadata/reason/history, bulk hide, 실시간 push.
  • legacy/financial/ranking 전역 query 필터, Hibernate 전역 soft-delete annotation.
  • frontend 파일, 화면/UX 디자인.

기술 초안과 경계

  • 현행 엔티티 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/channelDonation/ChannelDonationMessage.kt는 BaseEntity ID/시간만 상속한다. 권고 변경은 isHidden: Boolean = false와 운영 MySQL DDL is_hidden TINYINT(1) NOT NULL DEFAULT 0 COMMENT '채널 후원 표시 숨김 여부'다. 구현 전 DDL 위치/운영 schema 적용 절차를 확인한다.
  • donation 탭 list/count query의 현재 predicate는 creator 및 KST 월 범위다. isHidden=false를 두 query 모두에 적용한다. 홈 query도 local isHidden=false 필터를 적용하고 기존 8건 한도를 숨김 제외 후 적용한다.
  • 양 query에 공유 도메인 전역 filter를 만들지 않는다. 숨김 predicate는 공개 후원 탭 및 홈 전용이다. 랭킹 port/adapter, 정산, can usage와 explorer.profile.channelDonation legacy query에 추가하지 않는다.
  • visible 행은 기존 isSecret 값과 masking을 보존한다. hidden은 공개/비밀 모두 제외한다.
  • owner-only donationId nullable/omitted JSON 정책을 검증한다. donor memberId는 응답하지 않는다. 기존 E2E에서 전 역할 donation ID 부재를 단정한 부분은 소유자 케이스만 ID 기대값으로 바꾸고 비소유 케이스의 부재 assertion을 유지한다.
  • API 초안은 DELETE /api/v2/creator-channels/{creatorId}/donations/{donationId}, body 없음, ApiResponse.ok data 없음이다. 인증/도메인 거부는 기존 SodaException, i18n, 공통 handler를 재사용하며 HTTP 200 success=false 관례와 충돌하는 generic REST 403/404는 만들지 않는다.
  • actor.role == CREATOR, actor.id == creatorId, donation ID와 creator ID의 동일 소속을 모두 검증한다. 존재하지 않음 또는 다른 채널의 대상도 변화 없이 거부하고 존재 여부 정보 노출은 기존 domain 오류 패턴으로 제한한다.
  • 숨김 재요청은 이미 isHidden=true이면 성공한다. 원본 저장값을 덮어쓰지 않고 can/refund/ranking/settlement/usage 경계를 호출하지 않는다.

검증 명령 정책

  • 미래 구현에서는 각 Task 아래 명시된 실제 테스트 클래스만 --tests로 지정한다. 아래 RED/GREEN 명령은 향후 실행 명령이며 현재 실행하지 않는다.
  • Phase Gate도 관련 donation/home/legacy HTTP 및 repository 테스트 클래스만 --tests로 지정한다. 무필터 테스트를 실행하지 않는다.
  • 프로젝트 전체 테스트는 사용자가 직접 ./gradlew test로 실행한다. check, build, clean build도 자동 실행하지 않는다. 미래 빌드/포맷 확인은 필요한 경우 ./gradlew bootJar, ./gradlew ktlintCheck만 사용한다.
  • 미래 구현 전용 명령 예시는 실행 사실이나 성공 기록이 아니다. 테스트 결과, SQL 운영 적용, H2 통합 근거를 실행 후 실제로 기록한다.

Review Focus

  • 소유자가 아닌 CREATOR, donor, USER, ADMIN 및 anonymous 요청의 권한/상태 변경 없음: Task 1.2 HTTP 통합 검증.
  • 경로의 creator와 실제 donation 소속 불일치 또는 ID 미존재: Task 1.2의 forged ID HTTP 검증.
  • hidden row가 count, offset/page 결과에 남거나 페이지를 덜 채우지 않는지: Task 2.1 repository/HTTP 검증.
  • home hidden 후보가 최신 8건을 줄이지 않고 visible 최대 8건으로 refill하는지: Task 2.2 home repository/HTTP 검증.
  • 기록·랭킹·정산·can 사용 및 legacy 출력 불변: P3-GATE. owner-only ID와 donor ID 비노출: Task 2.1/2.2 통합 검증.

Phase 1: 저장과 소유자 전용 숨김 API

Phase 결과: 소유 CREATOR가 한 donation을 기록 보존형으로 숨기며, 권한 실패/중복 요청도 안전하다.

선행조건: 별도 명시 구현 요청. PRD 제품 결정을 유지하고 §7~§8 기술 초안의 실제 패턴과 DDL 반영 절차를 재확인한다.

Phase 완료 조건: P1-T1, P1-T2, P1-GATE 완료 증거 기록.

Task 1.1: 숨김 저장 모델과 persistence port

  • Task 1.1 완료: 아래 TDD 단계와 완료 증거를 충족한다.

Goal 실행 P1-T1: donation record에 기본 비숨김 상태를 저장하고 포트가 donation을 채널 범위로 숨길 수 있게 한다.

  • 시작 조건: 구현 요청 및 ChannelDonationMessage.kt, BaseEntity.kt, schema/DDL 관례 재확인.
  • 완료 증거: entity 매핑, MySQL DDL 파일, repository integration에서 기본 false/true 저장, 동일 채널 한정 및 원본 보존이 확인됨.
  • 범위 밖: API wiring, 조회 filter 및 금융 동작 변경.

Files:

  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/channelDonation/ChannelDonationMessage.kt
  • Create: docs/20261105_채널후원메시지숨김/channel_donation_message_is_hidden_ddl.sql (작성 완료, 운영 미적용)
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/HideCreatorChannelDonationPort.kt
  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultHideCreatorChannelDonationRepository.kt
  • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultHideCreatorChannelDonationRepositoryTest.kt

Interfaces:

  • Consumes: existing ChannelDonationMessage.id, creator.id, mutable JPA entity and project DB mappings.

  • Produces: entity isHidden: Boolean = false; HideCreatorChannelDonationPort.hideOwnedDonation(creatorId: Long, donationId: Long): Boolean, true when matching donation exists and is already/now hidden, false otherwise. Repository update must condition by both IDs and leave other fields untouched.

  • RED: shouldHideOnlyDonationOwnedByCreatorAndRetainOriginalValues에서 기본 false와 숨김 후 can/message/isSecret/member/creator/id 보존을 검증한다.

  • RED 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultHideCreatorChannelDonationRepositoryTest'를 실행해 새 필드/동작 누락으로 assertion 실패를 확인한다. 컴파일/fixture 오류는 RED 증거가 아니다.

  • GREEN: boolean field, port, 채널+donation 제한 update 및 DDL을 최소 구현한다.

  • GREEN 확인: 같은 focused 명령을 실행해 H2 DB 왕복과 다른 channel ID 무변경이 통과하는지 확인한다.

  • REFACTOR: 이번 Task의 중복만 정리하고 같은 focused test 및 ./gradlew ktlintCheck를 실행해 결과를 기록한다.

P1-T1 실제 실행 기록: 2026-11-05 (상위 리뷰 전, Task 최종 완료 미체크)

  • 범위: 이 Task의 entity/port/repository/test/DDL과 이 기록만 변경한다. P1-T2, 조회 filter, API 및 금융 경계는 시작하지 않는다.
  • 테스트 작성 후 컴파일 가능한 최소 bootstrap으로 isHidden=false 매핑, port와 항상 false를 반환하는 repository stub을 추가했다. 숨김 동작 구현 전 assertion RED를 확인했다.
  • RED 명령: Java 17(liberica-17.0.15+10)에서 ./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultHideCreatorChannelDonationRepositoryTest'. 최초 120초 실행은 compileKotlin 도중 도구 timeout으로 종료되어 RED 근거로 사용하지 않았다. 동일 명령을 600초 제한으로 재실행해 exit 1, 6건 중 assertion 실패 3건, errors/skip 0을 확인했다.
  • RED 실패: shouldHideOnlyDonationOwnedByCreatorAndRetainOriginalValues(false/true)의 52행과 shouldSucceedWhenHidingAgainWithoutChangingOriginalValues의 108행에서 expected: <true> but was: <false>. 컴파일 및 H2 fixture 생성은 성공했다.
  • MySQL 멱등성 근거: Connector/J의 useAffectedRows 설명은 CLIENT_FOUND_ROWS에 따라 matched/affected count가 달라짐을 명시한다. update는 두 ID 및 isHidden=false로 제한하고 true만 설정한다. 따라서 최초 변경은 양 설정 모두 1건이고 반복 숨김은 양 설정 모두 0건이며, 같은 두 ID의 존재 확인으로 true를 반환한다. H2에서도 반복 요청이 0건 update 경로를 통과하므로 H2의 무변경 update count를 MySQL 근거로 사용하지 않는다.
  • 트랜잭션: repository의 쓰기 메서드에 @Transactional을 두어 독립 포트 호출도 update/존재 조회를 한 트랜잭션에서 수행한다. 후속 service의 트랜잭션이 있으면 참여하며 service/API 구현은 이 Task에 추가하지 않는다.
  • 임시 경로: /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/channel-hide.aeTJdH/p1-t1.SxYFSF. 각 shell 실행에 이 경로의 TMPDIR, NODE_COMPILE_CACHE, BUN_RUNTIME_TRANSPILER_CACHE_PATH를 지정한다. 운영 SQL은 실행하지 않는다.
  • GREEN 명령: 위 RED와 동일한 focused 명령을 구현 후 실행해 exit 0, 신규 6건 통과(failures/errors/skip 0)를 확인했다. compileKotlin, compileJava, compileTestKotlin도 성공했다. 실제 DB는 캐시를 끈 @DataJpaTest의 H2이며 QueryDslConfig와 실제 repository bean을 import했다. mock은 사용하지 않았다.
  • DB 동작 증거: 신규 기본 false, 공개/비밀 각각 선택한 행만 true, 같은 채널의 다른 행 및 다른 채널 행은 false, wrong creator/missing ID는 false 반환, 반복 숨김 true 반환을 확인했다. 매번 flush/clear/find로 DB에서 다시 읽어 id/can/additionalMessage/isSecret/member/creator/createdAt을 고정 기대값과 정확히 비교했다. 선택 항목 테스트에서 3행, 나머지 거부/반복 테스트에서 1행이 유지됐다. 생성 시각은 기존 DDL의 초 정밀도에 맞는 고정값 2026-11-05T00:00:00을 사용했고 비교값 절삭이나 허용오차는 사용하지 않았다.
  • SQL 관찰: 테스트 XML의 Hibernate 출력에서 hide는 UPDATE channel_donation_message SET is_hidden=? WHERE id=? AND creator_id=? AND is_hidden=?만 실행하고, 반복 숨김과 거부는 같은 두 ID로 SELECT 1을 수행했다. hide SQL에 원본 필드 update, DELETE 또는 전역 filter는 없었다. fixture 준비의 createdAt update와 hide update를 구분했다.
  • 회귀/포맷 명령: ./gradlew --no-daemon -Pkotlin.compiler.execution.strategy=in-process test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultHideCreatorChannelDonationRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest' ktlintCheck 실행, exit 0. XML 기준 신규 6건 + 기존 query 14건 = 20건, failures/errors/skip 모두 0. ktlintMainSourceSetCheck, ktlintTestSourceSetCheck, Kotlin script 검사 및 ktlintCheck 성공. 추가 리팩터링은 필요하지 않아 범위를 확장하지 않았다.
  • 검증 한계: MySQL 서버에서 DDL 및 repository를 실행하지 않았고 운영 SQL은 미적용이다. MySQL matched/affected 차이는 위 Connector/J 근거와 조건부 update로 처리했다. lsp_diagnostics 도구가 없어 컴파일/ktlint로 대체했으며 LSP 성공을 주장하지 않는다. 기존 무관 코드의 !!/unchecked-cast/unused-parameter 컴파일 경고와 Gradle 9 비호환 deprecation 안내는 수정하지 않았다.
  • 중단 상태: P1-T1 실행 substep만 체크하고 Task 1.1 최종 완료는 상위 리뷰 대기로 남긴다. API/조회 filter는 미구현이므로 실제 화면 숨김 완료로 보고하지 않는다. 전체 테스트·check/build/clean build·bootJar·리뷰어·커밋·stage·push는 실행하지 않았다. 전체 테스트는 사용자가 ./gradlew test로 별도 실행한다.
  • 변경 범위/공백: read-only git status/diff에서 P1-T1 지정 파일과 이 계획의 실행 기록만 변경했고, 원래 untracked인 PRD는 수정하지 않았다. tracked diff 및 신규 5개 파일(entity 외 port/repository/test/DDL/계획)의 git diff --no-index --check /dev/null <file>를 검사해 모두 exit 0·출력 없음이었다. 프로세스 종료와 임시 루트의 열린 파일 없음, 빈 디렉터리임을 확인했으며 기존 상위 Gradle daemon은 종료하지 않는다.

Task 1.2: use case와 API 권한 경계

선행 검증: P1-T1 독립 리뷰에서 명세/품질 PASS. 상위에서 신규 repository 테스트를 재실행해 6건, failures/errors/skip 0, BUILD SUCCESSFUL을 확인했다. 저장 단계만 완료이며 조회 숨김은 아직 미구현이다.

  • Task 1.2 완료: 아래 TDD 단계와 완료 증거를 충족한다.

Goal 실행 P1-T2: 소유 CREATOR 전용 bodyless DELETE endpoint가 대상 항목을 멱등 숨김 처리하고 도메인 오류를 기존 envelope로 반환한다.

  • 시작 조건: P1-T1 완료. 현재 CreatorChannelDonationController, 기존 authentication/member role 및 SodaExceptionHandler의 실제 오류 패턴을 재확인한다.
  • 완료 증거: 서비스/HTTP 테스트가 성공, role 및 owner/channel mismatch 거부, hidden duplicate 성공, response data 없음, 금융 효과 불변을 확인함.
  • 범위 밖: 조회 filter, DTO ID 및 frontend 연동.

Files:

  • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/HideCreatorChannelDonationService.kt
  • Create: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/HideCreatorChannelDonationServiceTest.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt
  • Modify: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt
  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt
  • Modify: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt
  • Fixture: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt (생성자 주입 인자 변경만 반영)

Interfaces:

  • Consumes: HideCreatorChannelDonationPort.hideOwnedDonation(creatorId, donationId) from P1-T1; authenticated Member; existing ApiResponse/SodaException handler.

  • Produces: HideCreatorChannelDonationService.hide(creatorId: Long, donationId: Long, actor: Member): Unit; DELETE /api/v2/creator-channels/{creatorId}/donations/{donationId} bodyless, success ApiResponse.ok with no data.

  • RED: shouldAllowOnlyOwningCreatorToHideDonationThroughHttp와 역할별 denial, forged creator/donation ID, not-found, repeated hide 통합 cases를 작성한다. 승인된 역할만 성공하며 실패는 모든 원본/금융 fixture를 유지해야 한다.

  • RED 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest'를 실행해 endpoint 미구현 또는 정책 assertion 실패를 확인한다.

  • GREEN: service/facade/controller를 구현하고 기존 error key/handler로 role, owner, 같은 channel 소속을 검증한다. 같은 소유자의 중복 요청은 성공 처리하되 금융 변경은 하지 않는다.

  • GREEN 확인: 같은 focused 명령으로 소유 성공, donor/other creator/user/admin 거부, forged ID 거부, 중복 성공, HTTP-200 domain error envelope와 금융/원본 불변을 확인한다.

  • REFACTOR: 이번 Task의 중복만 정리하고 focused controller/E2E 테스트 및 ./gradlew ktlintCheck 결과를 기록한다.

P1-T2 실제 실행 기록: 2026-11-05 (상위 리뷰 대기, Task 최종 완료 미체크)

  • 승인 범위: P1-T2 및 P1-GATE까지만 실행한다. P1-T1 entity/port/repository/DDL은 보존하며 P2 조회 filter/응답 ID는 시작하지 않는다. 기존 Facade 직접 생성 fixture도 주입 변경에 맞춘다.
  • 경계 판단: 인증은 기존 SecurityConfig/TokenProvider/requireMember를 그대로 사용한다. 외부 입력인 두 경로 ID는 service의 CREATOR role/actor ID 확인 후 기존 채널 범위 port에 전달한다. role/owner 거부는 common.error.access_denied, 대상 소속 불일치/미존재는 같은 common.error.invalid_request로 처리해 존재 여부를 구별하지 않는다. 기존 read-only Facade의 hide 진입점은 명시적인 쓰기 트랜잭션으로 재정의한다.
  • 임시 루트: 부모 경로를 ls -ld로 확인하고 mktemp -d로 /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/channel-hide.aeTJdH/p1-t2.jkumZp를 만들었다. shell 실행마다 해당 루트의 TMPDIR, NODE_COMPILE_CACHE, BUN_RUNTIME_TRANSPILER_CACHE_PATH를 지정한다. Java는 17.0.15다.
  • RED 명령: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest', 도구 timeout 600000ms. 정상 컴파일 후 exit 1, 30건 중 failures 8, errors/skip 0, 3분 11초. 신규 인증 DELETE 8건은 endpoint 미구현으로 183행에서 expected: <200 OK> but was: <404 NOT_FOUND> assertion 실패했다. 기존 21건 및 신규 anonymous 401 검증 1건은 통과했다.
  • RED 실제 HTTP: RANDOM_PORT의 Tomcat 50634에서 실제 JWT와 TestRestTemplate로 body 없는 DELETE를 전송했다. 예: /api/v2/creator-channels/90/donations/104 소유 CREATOR는 404 및 status:404,error:Not Found, /api/v2/creator-channels/131/donations/162 anonymous는 401 및 status:401,error:Unauthorized를 반환했다. JWT는 출력하지 않았다. @DirtiesContext의 Tomcat 종료 로그를 확인했다.
  • 구현 후 첫 동일 E2E 실행은 exit 1, 30건 중 failures 8, errors/skip 0, 4분 35초였다. 실제 owner 응답은 이미 200 {"success":true,"message":null,"data":null,"errorProperty":null}이고 domain 거부도 200 success=false였지만 신규 테스트가 data 키 자체의 생략을 잘못 가정했다. ApiResponse.kt의 nullable 필드와 기존 ApiResponse.ok<Unit>() 사용을 확인해 공통 envelope를 변경하지 않고 신규 assertion을 data의 정확한 JSON null 검증으로 정정했다. 이는 payload가 없다는 계약이며 {}/Unit 객체는 허용하지 않는다. 기존 테스트는 완화하지 않았다.
  • GREEN 및 포맷 1차 명령: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.application.HideCreatorChannelDonationServiceTest' --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' ktlintCheck. XML 기준 E2E 30 + 실제 DB service 9 + controller 4 + facade fixture 회귀 2 = 45건, failures/errors/skip 모두 0. 명령 전체는 신규 service test의 listOf 인자 줄바꿈 ktlint 위반 5건으로 exit 1(1분 21초)이며, 해당 신규 코드 포맷만 수정하고 Gate에서 다시 확인한다.
  • 최종 GREEN: 아래 P1-GATE 명령에서 E2E 30건, service 9건, controller 4건, facade fixture 2건이 다시 통과했다. GET 동작/응답 ID 부재를 검증하던 기존 테스트 21건은 그대로 유지했다. HideCreatorChannelDonationServiceTest는 실제 Spring bean/기존 repository/H2를 사용하고 외부 mock 없이 검증한다. USER/ADMIN은 자기 ID와 경로가 같아도 거부하며, 비소유 USER/ADMIN/CREATOR는 대상 미존재 여부보다 먼저 access_denied를 반환한다.
  • 실제 socket HTTP 최종 증거: 기존 E2E의 RANDOM_PORT Tomcat 50765, 실제 TokenProvider 발급 JWT, 기존 운영 인증 체인을 사용했다. body 없는 DELETE 총 13요청을 실행했고 JWT는 로그에 출력하지 않았다. 공개 /api/v2/creator-channels/90/donations/104, 비밀 /api/v2/creator-channels/95/donations/109 및 중복 /api/v2/creator-channels/1/donations/2 2회는 200 {"success":true,"message":null,"data":null,"errorProperty":null}였다. donor/outsider/other CREATOR/ADMIN 및 경로 소유권 위조는 200 success=false,message=권한이 없습니다.,data=null; 다른 채널 /api/v2/creator-channels/106/donations/145와 미존재 /api/v2/creator-channels/106/donations/9223372036854775807는 동일한 200 success=false,message=잘못된 요청입니다.,data=null이었다. 이미 숨긴 항목의 donor 요청도 거부됐다. anonymous /api/v2/creator-channels/131/donations/162는 실제 인증 체인에서 401 및 status:401,error:Unauthorized였다.
  • DB 보존 증거: 각 HTTP fixture는 해당 채널 3행과 다른 채널 1행, 실제 UseCan 1행(can=450,rewardCan=50,isRefund=false) 및 UseCanCalculate 1행(can=500,status=RECEIVED)을 생성해 commit했다. JDBC로 전체 컬럼을 요청 전후 다시 읽어 donation의 is_hidden만 제외한 원본/연결/시각과 재무 두 테이블의 모든 컬럼·행 수가 정확히 동일함을 확인했다. 별도 transaction/clear/find로 선택 ID만 hidden=true이고 나머지는 false인 것도 확인했다. 거부는 전 행 false, 중복은 첫 요청 이후 같은 상태였다. 고정 donation 생성 시각은 2026-11-05T00:00:00이며 비교값 절삭/허용오차를 사용하지 않았다.
  • 정리 영수증: 각 socket 테스트의 발급 token은 해당 member ID만 @AfterEach에서 제거했다. @DirtiesContext로 50765 Tomcat 및 E2E/service Hikari 종료 로그를 확인했다. 이번 실행들의 Test worker PID 73880,75780,76195,76844를 ps로 조회해 잔존 프로세스가 없었고 lsof +D는 열린 파일 없이 종료했다. 임시 루트는 0 entries였으며 symlink 해석 후 부모 allowlist, 현재 UID 소유권과 빈 상태를 확인해 rmdir로 p1-t2.jkumZp만 삭제했다. shared Gradle/Kotlin daemon은 종료하지 않았다.
  • 한계/중단: LSP 도구가 없어 실제 Kotlin/Java 컴파일 및 ktlint로 대체했다. MySQL/운영 DDL 적용, 전체 테스트, check/build/clean build/bootJar, dependency audit, reviewer/subagent, commit/stage/push는 수행하지 않았다. 전체 테스트는 사용자 실행 명령 ./gradlew test다. P2 filter/ID는 미구현이므로 두 V2 목록은 아직 hidden 항목을 제외하지 않는다. P3의 실제 legacy/ranking/정산 조회 불변 최종 Gate는 수행하지 않았으며 이번 증거는 원본·재무 record 보존까지다. Task 1.2와 Phase 최종 완료 판정은 상위 리뷰에 남기고 여기서 중단한다.

Phase 1 Gate

Goal 실행 P1-GATE: 숨김 저장 및 owner-only API 권한/불변 조건을 확인한다.

  • 시작 조건: P1-T1, P1-T2 완료.
  • 완료 증거: 두 focused 테스트 통과, entity/repository 데이터 보존 및 E2E 권한/멱등성 증거 기록.
  • 범위 밖: 전체 suite, 조회 필터 및 legacy query 수정.
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultHideCreatorChannelDonationRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.application.HideCreatorChannelDonationServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationControllerTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest'
./gradlew ktlintCheck

Expected: 구현 후 각 명령 exit 0, 실패/skip 0. DB 원본 및 can/정산 관련 fixture 불변. 현재 실행하지 않음.

P1-GATE 실제 실행 기록: 2026-11-05 (실행 통과, 최종 완료는 상위 리뷰 대기)

  • 위 Gate의 4개 클래스와 주입 변경 직접 영향 범위인 CreatorChannelDonationFacadeTest를 함께 focused 실행했다.
  • 같은 invocation에서 ktlintCheck를 실행해 main/test/Kotlin script 검사 성공을 확인했다.
  • Phase 1 최종 완료 판정: 독립 리뷰 명세/품질 PASS. 상위에서 실제 HTTP를 포함한 E2E 30건을 재실행해 failures/errors/skip 0, BUILD SUCCESSFUL(26초)을 확인했다. 로그에서 Tomcat·DB 풀 종료도 확인했다.

실제 명령:

./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultHideCreatorChannelDonationRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.application.HideCreatorChannelDonationServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationControllerTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacadeTest' ktlintCheck
  • 결과: exit 0, BUILD SUCCESSFUL in 1m 9s. XML 기준 repository 6 + service 9 + controller 4 + E2E 30 = Gate 49건, 추가 facade 2건으로 총 51건이며 failures/errors/skipped는 전부 0이다. 앞선 실행에서 변경된 production Kotlin/Java와 test Kotlin 컴파일이 성공했고 최종 실행의 test Kotlin 컴파일도 성공했다. 기존 무관 컴파일 경고와 Gradle deprecation 안내는 범위 밖으로 유지한다.
  • 증거 경로: build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest.xml의 HTTP EVIDENCE 13행과 각 클래스 XML의 testsuite 집계를 직접 확인했다. 실제 HTTP/DB/정리 내용은 바로 위 P1-T2 기록에 누적했다. 전체 테스트 통과나 화면 숨김 완료를 뜻하지 않는다.

Phase 2: V2 조회 필터, count 및 owner-only ID

Phase 결과: 홈/탭은 hidden item을 모든 사용자에게 제외하고 count/page/refill 및 ID 공개 범위를 올바르게 유지한다.

선행조건: P1-GATE 완료. P1-T2 성공 이후 API 조회 필드 노출과 기존 JSON assertion을 갱신한다.

Phase 완료 조건: P2-T1, P2-T2, P2-GATE 완료 증거 기록.

Task 2.1: donation 탭 filter, count, pagination, ID

  • Task 2.1 완료: 아래 TDD 단계와 완료 증거를 충족한다.

Goal 실행 P2-T1: donation 탭은 hidden 제외 count/list를 동일 조건으로 제공하고 owner에게만 donationId를 제공한다.

  • 시작 조건: Phase 1 Gate 완료. 현재 query/service/domain/response 및 기존 E2E ID 부재 assertion 확인.
  • 완료 증거: repository와 HTTP 통합에서 역할별 hidden 제외, count/page/hasNext, visible 공개/비밀 masking 유지, owner ID 및 non-owner 부재가 검증됨.
  • 범위 밖: 홈 목록 filter 및 legacy/ranking query 변경.

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/application/CreatorChannelDonationQueryServiceTest.kt
  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt
  • Fixture/contract: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt
  • Fixture/contract: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt
  • Fixture/contract: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt

Interfaces:

  • Consumes: ChannelDonationMessage.isHidden; actor/owner context available to V2 response assembly. Existing countChannelDonations and findChannelDonations page API.

  • Produces: donation tab item response donationId: Long? serialized only for owning CREATOR; count/list share hidden predicate before pagination. No donor member ID.

  • RED: shouldExcludeHiddenRowsFromDonationTabCountAndPagesForEveryViewer 및 shouldExposeDonationIdOnlyToOwningCreator를 추가한다. mixed 25 rows, hidden within first page, all-hidden empty, and owner/non-owner JSON boundary cases를 포함한다.

  • RED 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest'를 실행해 hidden filter 및 owner ID 부재/노출 assertion 실패를 확인한다.

  • GREEN: count와 목록에 동일 local predicate를 적용하고 pagination 전에 제외한다. response mapping에서 소유 CREATOR에만 persisted ID를 제공한다.

  • GREEN 확인: 같은 focused 명령을 실행해 viewer 전원 hidden 제외, donationCount, page/hasNext, 공개/비밀 masking 및 ID JSON을 검증한다.

  • REFACTOR: 현재 ID 없음만 가정하던 owner assertion은 owner-only ID 기대값으로 수정하고 non-owner 부재 assertion은 보존한다. focused donation query/service/E2E 및 ./gradlew ktlintCheck 결과 기록.

P2-T1 실행 증거: 2026-11-05 (최종 Task 판정은 상위 리뷰 대기)

  • RED: 위 두 클래스 명령, 정상 컴파일 후 48건 중 assertion 실패 7건, errors/skip 0, BUILD FAILED in 36s. repository는 hidden 혼합 count 22 != 25, all-hidden 0 != 2; 실제 socket GET은 count 22 != 28, owner ID [3,2,1] != [null,null,null], 기존 언어별 owner JSON은 donationId 누락으로 실패했다. 최초 fixture의 숨김 플래그 변경이 managed createdAt을 덮어쓰는 문제를 바로잡아 persist 전에 플래그를 설정하고 RED를 재확인했다. 중간 생성자 컴파일 오류는 RED 근거에서 제외했다.
  • GREEN: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryServiceTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicyTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.*' ktlintCheck, exit 0, BUILD SUCCESSFUL in 1m. repository 16 + service 20 + policy 5 + E2E 32 + controller 4 + facade 2 = 79건, failures/errors/skip 0; Kotlin/Java/test 컴파일 및 ktlint 통과.
  • 검증: 공개·비밀 25행 중 최신 3행 숨김 후 22행을 20+2로 조회, HTTP 두 건 DELETE 후 정확히 20행/hasNext=false, 전부 숨김 후 count=0/empty/false를 owner/donor/outsider/other CREATOR/ADMIN 모두 확인했다. record ID는 실제 저장 행의 ID와 비교하며 서비스는 같은 ID의 USER/ADMIN에도 ID를 주지 않는다. 기존 월 경계·정렬·다국어 masking assertion과 non-owner 필드 부재를 유지했다.
  • 기존 RANDOM_PORT/TestRestTemplate/JWT helper를 GET에도 재사용했다. 토큰을 출력하지 않고 기존 token 삭제와 DirtiesContext 종료를 유지했다. 홈/P3/legacy/재무 query와 P1 production은 수정하지 않았다. LSP 도구가 없어 컴파일/ktlint로 검증한다.

Task 2.2: 홈 filter, refill, owner-only ID

  • Task 2.2 완료: 아래 TDD 단계와 완료 증거를 충족한다.

Goal 실행 P2-T2: 채널 홈은 hidden donation을 제외한 최신 visible 최대 8건을 제공하고 소유 CREATOR에게만 donationId를 제공한다.

  • 시작 조건: P2-T1 완료. 홈 query/service/domain/response와 현재 KST 월/최대 8개 테스트 확인.
  • 완료 증거: repository 및 HTTP 통합으로 hidden 전체 제외, hidden 섞인 최신 8개 refill, owner ID 범위 및 visible isSecret 표시 유지.
  • 범위 밖: donation tab count, home 새 count 필드, legacy API 변경.

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
  • 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/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt
  • Fixture/contract: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt
  • Fixture/contract: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt
  • Fixture/contract: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt

Interfaces:

  • Consumes: ChannelDonationMessage.isHidden; existing home findChannelDonations(creatorId, viewerId, now, limit, lang) and owner context in response assembly.

  • Produces: channelDonations[] hidden-excluded, maximum 8 visible rows after filter; donationId: Long? only for owning CREATOR, no donor member ID.

  • RED: shouldRefillHomeWithLatestEightVisibleDonationsAfterHiddenRowsAreExcluded 및 shouldExposeHomeDonationIdOnlyToOwningCreator를 작성한다. hidden newest candidates, tied timestamps, public/secret visible rows, all-hidden empty를 포함한다.

  • RED 확인: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest'로 숨김 누락 또는 owner ID assertion 실패를 확인한다.

  • GREEN: home query의 channel/month 조건과 함께 isHidden=false를 적용하고 기존 정렬 뒤 최대 8개를 조회한다. 소유자 응답에만 ID를 전달한다.

  • GREEN 확인: 같은 focused 명령으로 모든 viewer의 hidden 부재, visible 최대 8개 refill, 기존 월 경계/정렬/secret masking 및 ID 응답 범위를 검증한다.

  • REFACTOR: owner-only ID 회귀를 유지하며 현재 donation ID 부재만 강제하던 assertion을 owner/non-owner 구분 검증으로 갱신한다. focused home tests 및 ./gradlew ktlintCheck 결과 기록.

P2-T2 실행 증거: 2026-11-05 (최종 Task 판정은 상위 리뷰 대기)

  • RED: 위 두 클래스에 실제 socket home GET을 추가한 CreatorChannelDonationEndToEndTest까지 focused 실행. 정상 컴파일 후 93건 중 assertion 실패 8건, errors/skip 0, BUILD FAILED in 51s. repository는 visible 기대 [9,8,7,6,5,4,3,2] 대신 hidden 포함 [12,11,10,9,8,7,6,5], all-hidden empty 실패; socket home은 hidden 원본 포함 [300,200,100,12,11,10,9,8], owner ID [179,178,177] 대신 null 세 건. 기존 다국어 owner field-set assertion 3건도 ID 누락으로 실패했다.
  • GREEN: ./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.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest' ktlintCheck, exit 0, BUILD SUCCESSFUL in 1m 31s. home repository 40 + service 26 + policy 5 + E2E 18 + controller 3 + facade 2 + socket donation E2E 35 = 129건, failures/errors/skip 0; 컴파일 및 ktlint 통과.
  • 검증: 최신 숨김 공개·비밀 후보 3건을 제외하고 동시각 ID 내림차순 visible 8건을 모든 5역할에 보충했다. owner가 홈에서 얻은 실제 ID로 공개/비밀 각각 DELETE한 뒤 홈 8건 refill 및 탭 count=8/hasNext=false, 두 JSON item 배열의 완전 일치를 검증했다. 전부 숨김 후 모든 역할의 홈은 빈 배열이다. 3행 별도 fixture에서도 양 조회면 persisted ID가 정확히 같고 선택한 비밀 행을 숨긴 뒤 모든 역할에서 두 목록이 일치한다.
  • 새 월 의존 fixture는 현재 UTC 시각으로 계산한 KST 월 범위를 사용한다. 기존 P1 고정 fixture/원본 snapshot은 유지했다. 홈 isViewerCreator의 role-only live 접근 정책은 변경하거나 소유권에 재사용하지 않았다. 양 도메인에 소유자 ID만 전달하며 API property NON_NULL로 non-owner 키 자체를 생략한다. home E2E에도 DirtiesContext 종료를 명시했다.

Phase 2 Gate

Goal 실행 P2-GATE: 양 V2 조회 표면에서 숨김, page/count/refill과 owner ID를 통합 확인한다.

  • 시작 조건: P2-T1, P2-T2 완료.
  • 완료 증거: 아래 focused test, ktlintCheck, bootJar 결과. HTTP integration에서 creator/donor/USER/other CREATOR/ADMIN별 숨김 부재 확인.
  • 범위 밖: 전체 test suite 및 frontend UI 구현.
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest'
./gradlew ktlintCheck
./gradlew bootJar

Expected: 구현 후 지정 focused tests, lint, jar 성공. hidden 공개/비밀 item은 어떤 role에도 없고 count/page/home fill 모두 visible 기준이다. donor ID는 추가되지 않는다. 현재 실행하지 않음.

P2-GATE 실행 증거: 2026-11-05 (실행 통과·상위 리뷰 대기)

  • 지정 Gate 4개 클래스 109건 및 직접 영향받는 service/policy/controller/facade 67건, 합계 176건 통과. failures/errors/skipped 모두 0.
  • 같은 invocation에서 ktlintCheck, bootJar 성공, exit 0, BUILD SUCCESSFUL in 1m 14s. bootJar는 실제 생성했고 lint는 직전 GREEN 결과와 동일 입력의 UP-TO-DATE 판정이다. 마지막 fixture UTC 조정 후 test Kotlin 재컴파일도 성공했다.
  • 기존 socket E2E에서 두 V2 GET → owner ID로 DELETE → 두 GET을 실제 인증 체인으로 확인했다. 모든 역할의 hidden 부재·count/page/refill·non-owner ID 키 생략·donor memberId 부재를 검증했다.
  • Phase 2 Task 및 Gate 최종 승인: 독립 리뷰에서 명세/품질 PASS. 상위가 홈·후원 E2E 53건을 재실행해 실패/오류/skip 0과 BUILD SUCCESSFUL(40초), 실제 HTTP 및 종료 로그를 확인했다.

실제 최종 명령 (Java 17, timeout 600000ms):

./gradlew test \
  --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultCreatorChannelDonationQueryRepositoryTest' \
  --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest' \
  --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.adapter.out.persistence.DefaultCreatorChannelHomeQueryRepositoryTest' \
  --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest' \
  --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryServiceTest' \
  --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicyTest' \
  --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.creator.channel.home.application.CreatorChannelHomeQueryServiceTest' \
  --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelHomeQueryPolicyTest' \
  --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' \
  ktlintCheck bootJar

실제 HTTP 응답 증거는 build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest.xml의 HTTP EVIDENCE에 있다. Tomcat port는 51039, worker PID는 84091이다. 아래는 실제 응답의 관련 필드만 발췌한 것이며 token/header는 기록하지 않았다.

요청 HTTP 실제 응답 관련 필드
owner GET /api/v2/creator-channels/120/home 및 /donations 200 양 목록 donationId=[179,178,177]; 탭 donationCount=3
owner DELETE /api/v2/creator-channels/120/donations/178 200 {"success":true,"message":null,"data":null,"errorProperty":null}
같은 두 GET 재요청 200 양 목록 donationId=[179,177], can=[300,100]; 탭 donationCount=2,hasNext=false
owner 홈 GET → 공개 ID 12 DELETE → 두 GET (creatorId=1) 200 홈 can=[9,8,7,6,5,4,3,2] → [9,7,6,5,4,3,2,1]; 두 배열 동일, 탭 count=8
owner 홈 GET → 비밀 ID 29 DELETE → 두 GET (creatorId=6) 200 홈 can=[9,8,7,6,5,4,3,2] → [8,7,6,5,4,3,2,1]; 두 배열 동일, 탭 count=8
  • donor/outsider/other CREATOR/ADMIN도 같은 hidden 제외 목록을 받으며 non-owner JSON에는 donationId 키가 없었다. 채널 소유자와 같은 ID의 USER/ADMIN은 service matrix로도 ID 부재를 확인했다. 신규 로그와 최종 XML에 JWT 패턴 검색 결과 0건이며, 토큰은 기존 AfterEach에서 발급 member ID별 삭제했다.
  • 변경 검토: production diff는 두 query의 local predicate/ID projection, 두 port record/domain/service/DTO뿐이다. P1 저장·숨김 API, 공통 직렬화, 랭킹/정산/legacy query와 의존성·설정은 변경하지 않았다. git diff --check 통과. PRD는 수정하지 않았다.
  • 정리 영수증: 부모 경로를 먼저 확인하고 생성한 전용 루트 channel-hide.aeTJdH/p2.1mgEdJ만 사용했다. Tomcat 51039와 두 E2E Hikari 종료 로그를 확인했고 PID 84091·Gradle Test Executor·redis-server 및 해당 port listener 잔존 없음, lsof +D 열린 파일 없음이었다. UID 501 소유권·canonical path·빈 상태를 확인해 자체 로그 5개와 전용 루트만 삭제했다. 부모와 기존 p1-t1.SxYFSF, shared Gradle/Kotlin daemon PID 53466/73431은 보존했다. 최종 XML/HTML 테스트 결과는 build/에 남겼다.
  • 검증 한계: LSP 도구가 없어 Kotlin/Java 컴파일 및 ktlint로 대체했다. 전체 테스트·check/build/clean build, P3 settlement/legacy 회귀, MySQL/운영 DDL 적용, subagent/reviewer, commit/stage/push는 수행하지 않았다. 전체 테스트는 사용자가 ./gradlew test로 실행한다. 기존 deprecation/unchecked-cast 경고는 범위 밖이다.

Phase 2의 정확한 변경 파일 (22개, 기존 P1 변경은 별도 보존):

src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/domain/CreatorChannelHome.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt
src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/dto/CreatorChannelHomeResponse.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepositoryTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepositoryTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt
src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt
docs/20261105_채널후원메시지숨김/plan-task.md

Phase 3: 보존, legacy 및 최종 회귀 증거

Phase 결과: 숨김이 표시 조정에 한정되고 원본/재무/legacy 동작을 바꾸지 않았다는 증거가 남는다.

선행조건: P2-GATE 완료.

Phase 완료 조건: P3-GATE 증거 기록.

Task 3.1: 재무 불변과 legacy 경계 Gate

  • Task 3.1 완료: 아래 회귀 검증과 완료 증거를 충족했다. 최종 리뷰의 P3-R1 보정 및 재리뷰도 통과했다.

Goal 실행 P3-GATE: 실제 persistence/HTTP 증거로 DB 보존, 랭킹/정산/can 사용 불변 및 legacy 목록의 기존 출력 유지를 확인한다.

  • 시작 조건: Phase 2 완료. 기존 legacy 채널 후원 query/controller 및 랭킹 adapter의 실제 경로를 구현 전 다시 확인한다.
  • 완료 증거: 통합 테스트에서 숨김 전후 원본 행과 금융 record 동일, 랭킹 동일, legacy 결과에서 같은 donation 표시, 문서에 global hide가 아님을 확인.
  • 범위 밖: legacy API를 숨김 처리하거나 재무 query를 변경하는 것.
  • TDD 예외 사유: Gate 자체는 별도 production behavior를 추가하지 않고 앞선 integration test의 회귀 범위를 판정한다. 필요한 실패 assertion은 소유 Task에 추가 후 재검증한다.
  • 대체 검증 방법: repository/HTTP integration test, DB flush/clear/reload, legacy endpoint response 대조 및 focused lint/build를 기록한다.

Files:

  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt

  • Verify: src/test/kotlin/kr/co/vividnext/sodalive/explorer/profile/channelDonation/ChannelDonationControllerTest.kt (기존 mock 기반 controller 회귀이며 DB 보존/조회 증거를 대체하지 않음)

  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/legacy/LegacyCreatorChannelDonationRankingAdapterTest.kt

  • 실제 persistence를 사용하는 CreatorChannelDonationEndToEndTest에서 숨김 API 성공 후 legacy GET을 호출해 동일 행이 유지되는지 확인한다. mock 응답만으로 판정하지 않는다.

  • entity를 DB에서 재조회해 ID/can/message/isSecret/createdAt/member/creator 값이 숨김 전후 동일한지 확인한다.

  • UseCan, UseCanCalculate 및 실제 ranking/정산 조회 결과가 숨김 전후 바뀌지 않는지 확인한다. mock 위임 테스트만으로 불변을 주장하지 않는다.

  • 관련 donation/home/legacy 클래스만 --tests로 실행하고 ./gradlew ktlintCheck, ./gradlew bootJar 결과를 기록한다.

./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest' --tests 'kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationControllerTest' --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.legacy.LegacyCreatorChannelDonationRankingAdapterTest'

Expected: 지정 테스트 exit 0, legacy 실제 조회와 재무 원본/집계 결과가 숨김 전후 동일. 현재 실행하지 않음.

P3-GATE 실행 증거: 2026-11-05 (실행 통과, Task 최종 승인은 상위 전체 변경 리뷰 대기)

  • 변경은 위 E2E 파일과 이 기록뿐이며 production/PRD/P1·P2 기존 테스트는 보존했다. shouldPreserveLegacyRankingAndSettlementAfterHidingDonationThroughHttp(false/true) 2건을 추가하고 기존 HTTP/JWT·snapshot·hidden 재조회 helper를 재사용했다. Gate의 승인된 TDD 예외에 따라 production을 인위적으로 되돌리지 않았다.
  • 금융 기여 연결: 채널별 메시지는 선택할 500캔 한 건뿐이고, 동일 donor/creator·비밀 여부·시각의 UseCan(can=450,rewardCan=50,isRefund=false,CHANNEL_DONATION) 및 UseCanCalculate(can=500,status=RECEIVED) 한 건을 commit했다. 별도 채널의 400캔 행도 보존한다. DB flush/clear/reload로 금액·연결·상태를 대조했으며 P1의 독립 500캔 fixture를 보존 증거로 재사용하지 않았다.
  • 월/격리: 현재 UTC로 구한 KST 월 시작에 2시간을 더한 초 정밀도 입력을 사용했다. 공개 2026-10-31T17:00:00Z, 비밀 2026-10-31T17:00:01Z는 KST 11월이다. 날짜 설정 후 baseline을 읽었다. 정산은 실제 주입된 AdminChannelDonationCalculateQueryRepository.getChannelDonationByCreatorTotal 및 getChannelDonationByCreator를 동일 초의 inclusive 범위로 전후 호출해 다른 fixture와 격리했다. 날짜 포맷 함수/alias/mock은 사용하지 않았다.
  • 신규 2건 우선 명령: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest.shouldPreserveLegacyRankingAndSettlementAfterHidingDonationThroughHttp' ktlintCheck. exit 0, BUILD SUCCESSFUL in 31s, tests=2, failures/errors/skipped=0. test Kotlin 컴파일과 변경 test source ktlint 검사를 실제 실행했다.

최종 Gate 명령 (Java 17.0.15, timeout 600000ms, 표준 wrapper, 강제 재실행 없음):

./gradlew test \
  --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest' \
  --tests 'kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationControllerTest' \
  --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.legacy.LegacyCreatorChannelDonationRankingAdapterTest' \
  --tests 'kr.co.vividnext.sodalive.admin.calculate.channelDonation.AdminChannelDonationCalculateQueryRepositoryTest' \
  ktlintCheck bootJar
  • 결과: exit 0, BUILD SUCCESSFUL in 33s. 지정 Gate는 E2E 37 + legacy controller 2 + ranking adapter 2 = 41건, 직접 호출한 정산 repository 회귀 2건을 더해 총 43건, failures/errors/skipped 모두 0. 기존 E2E 35건도 전부 유지·통과했다. ktlintCheck와 bootJar는 동일 입력의 UP-TO-DATE 성공이며 기존 build/libs/sodalive-0.0.1-SNAPSHOT.jar(141 MiB)를 확인했다. 새 production 재컴파일/새 jar 생성으로 표현하지 않는다.
  • 실제 socket 증거: Tomcat 51284, worker PID 88424, 실제 JWT 인증 체인의 GET → owner 응답 ID로 bodyless DELETE → GET 순서다. 공개 creatorId=125,donationId=181, 비밀 creatorId=130,donationId=183 모두 DELETE 200 {"success":true,"message":null,"data":null,"errorProperty":null}. 양 경우 탭 donationCount=1→0,donations=[]/hasNext=false, 홈 channelDonations 1건→빈 배열을 확인했다.
  • legacy 증거: owner GET /explorer/profile/channel-donation?creatorId=125&page=0&size=20 및 creatorId=130의 전체 JSON envelope가 전후 정확히 동일했다. totalCount=1, 같은 ID 181/183·500캔·공개/비밀 플래그·원문/생성 시각이 남는다. 이 기능은 두 V2 표시 목록만 숨기며 global hide가 아니다.
  • 랭킹 증거: 탭 owner 응답 전체 rankings가 전후 동일하고 donor 126/131의 donationCan=500→500이었다. DELETE 후 해당 fixture의 정확한 Redis ranking key만 제거하고 제거 성공을 확인해, stale cache가 아닌 실제 legacy query 재조회로 검증했다.
  • 정산 증거: 공개·비밀 모두 실제 total/count 및 creator별 query 결과 전체와 toResponseItem() 전체가 전후 동일했다. 각각 count=1,totalCan=500,krw=50000,fee=3300,settlementAmount=39695,withholdingTax=1310,depositAmount=38385. 0건/0원 불변을 통과 근거로 쓰지 않았다.
  • DB 증거: JDBC 전체 컬럼 snapshot에서 donation 2행(자기 채널 1+다른 채널 1)은 is_hidden 외 모든 값, UseCan 1행과 UseCanCalculate 1행은 모든 컬럼·행 수가 동일했다. 별도 transaction/clear/find로 선택 ID만 hidden=true임을 확인했다. 행 삭제나 비교 정밀도 완화는 없다.
  • 증거 파일: build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest.xml의 HTTP EVIDENCE, SETTLEMENT EVIDENCE, testsuite 집계 및 나머지 3개 클래스 XML. JWT 패턴 검색 결과는 0건이며 토큰은 기존 AfterEach에서 발급 member ID별 삭제했다.
  • 정리: 확인한 부모 아래 mktemp -d로 생성한 channel-hide.aeTJdH/p3.OBzvWM의 TMPDIR/NODE_COMPILE_CACHE/BUN_RUNTIME_TRANSPILER_CACHE_PATH를 모든 shell에 지정했다. 두 실행의 Tomcat 51262/51284 및 Hikari 종료 로그, worker PID 88133/88424·Test Executor·redis-server·port listener 잔존 없음, lsof +D 열린 파일 없음과 0 entries를 확인했다. canonical 부모 allowlist·현재 UID 소유권·symlink 아님을 검사한 뒤 rmdir로 해당 빈 루트만 삭제했다. shared Gradle/Kotlin daemon PID 53466/73431과 부모/다른 Task 임시 경로는 보존했다.
  • 한계/인계: LSP 도구가 없어 test Kotlin 컴파일·ktlint로 대체했다. MySQL/운영 DDL, 전체 테스트·check/build/clean build, subagent/reviewer, commit/stage/push는 미실행이다. 기존 Gradle deprecation 안내는 유지했다. 전체 테스트의 사용자 명령은 ./gradlew test이며, 상위의 broad scoped donation/home 회귀와 최종 전체 변경 리뷰·Task 승인에 넘긴다.

Phase Gate와 전체 테스트 정책

회귀 수정 P3-R1: MySQL 자동 수정 시각 보존

  • Task P3-R1 완료: 숨김 UPDATE가 기존 updated_at을 명시적으로 유지함을 회귀 검사하고 최종 재리뷰를 통과했다.
  • 목표: MySQL의 자동 갱신 시각 설정에서도 숨김 상태 외 원본 컬럼을 바꾸지 않는다.
  • 시작 조건: 최종 Oracle 리뷰에서 기존 DDL의 updated_at ... ON UPDATE CURRENT_TIMESTAMP와 현재 UPDATE의 시각 자기 대입 누락을 확인했다.
  • Files: Modify src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultHideCreatorChannelDonationRepository.kt; Test src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultHideCreatorChannelDonationRepositoryTest.kt.
  • 범위 밖: 기존 테이블 DDL 수정, 시각 초기화, 공통 시간 변환 변경, snapshot assertion 완화.
  • RED: 실제 생성 UPDATE SQL에 updated_at=updated_at 보존식이 없는 실패를 확인한다.
  • GREEN: QueryDSL UPDATE에 기존 updatedAt 자기 대입만 추가한다.
  • GREEN 확인: repository 회귀와 실제 HTTP 원본 snapshot 검사 통과, 생성 SQL 보존식 확인.
  • REFACTOR 검증: 추가 리팩터링 없이 이번 변경 범위의 lint/컴파일 및 bootJar 결과를 기록한다.
  • 최종 재리뷰: Oracle가 P3-R1을 ADDRESSED로 판정하고 전체 변경을 승인했다. 새로운 명세·품질·보안 차단 사항은 없다.
  • 검증 명령: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultHideCreatorChannelDonationRepositoryTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest' ktlintCheck bootJar.
  • 검증 한계: H2의 Hibernate 생성 스키마에는 기존 MySQL 자동 시각 설정이 없으므로 기존 green만으로 MySQL 원본 시각 보존을 보장하지 않는다. 실제 MySQL 실행 여부는 결과에 별도로 기록한다.

P3-R1 실행 증거: 2026-11-05 (RED/GREEN·검증 완료, 최종 재리뷰 대기)

  • 기존 DDL docs/plan-task/20260223_channel_donation_message_ddl.sql의 updated_at TIMESTAMP ... ON UPDATE CURRENT_TIMESTAMP를 확인했다. production 변경은 .set(channelDonationMessage.updatedAt, channelDonationMessage.updatedAt) 한 줄뿐이다. 기존 HTTP 전체 컬럼 snapshot·다른 행·멱등성 검사는 변경하지 않았다.
  • 신규 shouldPreserveUpdatedAtWithSelfAssignmentInGeneratedHideSql은 테스트 내부 TestConfiguration/HibernatePropertiesCustomizer/StatementInspector로 실제 repository가 실행하는 SQL을 수집한다. fixture flush/clear/reload 후 수집값을 비우고 숨김 UPDATE의 자기 대입을 검사하며, DB에서 다시 읽은 updatedAt도 정확히 비교한다. 소스 문자열·mock 검증 또는 H2의 MySQL 자동 갱신 모방은 없다.
  • RED 명령: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultHideCreatorChannelDonationRepositoryTest.shouldPreserveUpdatedAtWithSelfAssignmentInGeneratedHideSql'. Java 17.0.15, timeout 600000ms, exit 1, BUILD FAILED in 14s, tests=1/failures=1/errors=0/skipped=0. 정상 컴파일 후 60행의 실제 SQL assertion이 update channel_donation_message set is_hidden=? where id=? and creator_id=? and is_hidden=? ==> expected: <true> but was: <false>로 실패했다.
  • GREEN: 위 두 클래스 지정 검증 명령을 그대로 실행했다(timeout 600000ms, 표준 daemon·캐시 사용, 강제 재실행 없음). exit 0, BUILD SUCCESSFUL in 49s, repository 7 + HTTP E2E 37 = 44건, failures/errors/skipped 모두 0. production/test Kotlin 컴파일, main/test ktlint 검사 및 bootJar가 실제 실행됐고 Kotlin script 검사는 UP-TO-DATE 성공이다. 새 jar build/libs/sodalive-0.0.1-SNAPSHOT.jar는 141 MiB다.
  • 실제 GREEN SQL(공백만 정리): update channel_donation_message set is_hidden=?, updated_at=updated_at where id=? and creator_id=? and is_hidden=?. 증거는 build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence.DefaultHideCreatorChannelDonationRepositoryTest.xml의 신규 testcase 및 Hibernate 출력이다.
  • 실제 HTTP: Tomcat 51419에서 JWT 인증 DELETE 성공·중복 성공, 권한/ID 거부 및 anonymous 401을 확인했다. 공개/비밀 /api/v2/creator-channels/125/donations/181, /api/v2/creator-channels/130/donations/183은 모두 200 success=true,data=null이며 원본/재무 전체 컬럼 snapshot, legacy 전체 JSON, 랭킹 500캔 및 정산 39,695원·입금 38,385원 전후 동일 검사가 통과했다. 증거는 같은 디렉터리의 TEST-kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest.xml이다.
  • 정리: 부모를 확인한 뒤 만든 전용 루트 /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/channel-hide.aeTJdH/p3-r1.tm8TPt에 shell별 TMPDIR/NODE_COMPILE_CACHE/BUN_RUNTIME_TRANSPILER_CACHE_PATH를 지정했다. worker PID 91431/91649 종료, Tomcat·Hikari 종료 로그, port 51419 listener·Test Executor·redis-server 잔존 없음, lsof +D 열린 파일 없음과 0 entries를 확인했다. canonical 부모·현재 UID·symlink 아님을 확인하고 rmdir로 전용 루트만 삭제했다. shared Gradle/Kotlin daemon PID 53466/73431, 부모와 다른 작업 경로는 보존했다.
  • 한계: 실제 MySQL/운영 DDL, 전체 테스트, subagent/reviewer, commit/stage/push는 실행하지 않았다. LSP 도구는 미제공이며 Kotlin 컴파일·ktlint 결과만 보고한다. 기존 Gradle deprecation 안내는 유지했다. 전체 테스트의 사용자 실행 명령은 ./gradlew test다. Task P3-R1 및 최종 재리뷰 체크는 상위 판정까지 미완료로 남긴다.

모든 Phase Gate는 구현 경로를 직접 검증하는 focused feature test만 실행한다. 무필터 test 또는 전체 suite를 포함하는 check/build/clean build를 실행하지 않는다. 전체 테스트는 사용자 실행 항목이며 명령은 ./gradlew test다. Phase Gate의 성공은 전체 suite 성공을 뜻하지 않는다.

프론트엔드 후속 범위

프론트엔드 작업은 이 backend 저장소 범위 밖이다. 별도 작업에서 소유 CREATOR에게만 전달되는 donationId를 사용해 숨김 API를 호출하고, 성공 뒤 홈과 탭 데이터를 다시 조회해야 한다. 프론트엔드 파일 경로, UI 디자인, 추가 확인 dialog/interaction을 이 계획에서 정하지 않는다.

실행 순서와 의존성

순서 Goal 선행조건 병행 가능 차단 시 다음 행동
1 P1-T1 별도 명시 요청 아니요 DDL/schema 근거 확인
2 P1-T2 P1-T1 아니요 기존 auth/error 관례 재확인
3 P1-GATE P1-T1, P1-T2 아니요 실패 소유 Task에 회귀 추가
4 P2-T1 P1-GATE 아니요 count/list 동일 predicate 확인
5 P2-T2 P2-T1 아니요 홈 fill 및 owner ID 검증 보강
6 P2-GATE P2-T1, P2-T2 아니요 실패 소유 Task에 회귀 추가
7 P3-GATE P2-GATE 아니요 legacy/financial 경계 확인
P1-T1 -> P1-T2 -> P1-GATE -> P2-T1 -> P2-T2 -> P2-GATE -> P3-GATE

변경 금지와 중단 조건

  • 별도 구현 요청 전에는 이 계획의 체크박스를 시작하거나 코드를 수정하지 않는다.
  • soft-hide 행과 원본 값을 보존한다. delete SQL, 필드 초기화, 숨김 metadata/history를 추가하지 않는다.
  • isHidden 조건을 global entity filter, legacy list, ranking, settlement 또는 can use query에 적용하지 않는다.
  • 권한을 UI에만 맡기지 않는다. 인증, CREATOR role, channel ownership, donation-channel match를 서버에서 검증한다.
  • 상태 오류를 generic HTTP 403/404로 바꾸지 않고 기존 SodaException/handler 응답 관례를 확인한다.
  • 사용자 결정과 기술 제안을 합치지 않는다. 구현 단계에서 제안 변경이 필요하면 구현 전에 PRD/plan Decision Log를 먼저 갱신한다.

Progress 및 문서 검증 기록

최종 완료 판정: 2026-11-05

  • P3-R1 수정 후 상위에서 아래 「최종 기능 범위 재검증」과 동일한 명령을 다시 실행했다. BUILD SUCCESSFUL in 1m 7s, XML 19개 클래스 총 208건, failures/errors/skipped 모두 0이다. repository 회귀 1건이 추가됐으며 기존 207건도 통과했다.
  • ktlintCheck·bootJar는 수정 후 생성 산출물의 UP-TO-DATE 성공이다. 직전 P3-R1 실행에서는 production/test 컴파일·lint·jar 생성이 실제 수행됐다. 전체 suite를 실행한 것은 아니다.
  • 최초 최종 리뷰에서 MySQL 자동 updated_at 갱신 위험을 발견했다. 보존식 1줄과 실제 생성 SQL RED/GREEN을 추가했고, scoped 재리뷰에서 ADDRESSED / 전체 변경 승인을 받았다. 실제 MySQL 실행 한계는 유지한다.
  • 최종 테스트 worker PID 92754 잔존 없음과 임시 루트 열린 파일 없음을 확인했다. 테스트/HTTP/SQL 증거는 build/test-results/test/, 빌드 산출물은 build/libs/sodalive-0.0.1-SNAPSHOT.jar에 남긴다.
  • 최종 정리 영수증: 소유권·symlink 아님·빈 상태를 확인해 남은 p1-t1.SxYFSF와 부모 channel-hide.aeTJdH를 rmdir로 제거했다. 임시 notepad도 제거했고 셸에서 세 경로 부재를 확인했다(exit 0). 작업용 서버/worker는 종료됐으며 공유 캐시·기존 daemon은 건드리지 않았다. 최종 tracked 및 신규 파일 공백 검사도 exit 0이었다.
  • 현재 변경은 서버 소스·관련 테스트·본 기능 문서/DDL에 한정한다. 커밋·push·운영 SQL·배포는 하지 않았다. 전체 테스트 사용자 명령은 ./gradlew test다.
  • 프론트엔드 연동은 범위 밖이며 서버 구현 완료와 화면 클라이언트 반영 완료를 구분한다. 기존 프로필 API에서는 숨긴 행이 계속 보일 수 있다.

최종 기능 범위 재검증: 2026-11-05

  • 상위 세션에서 아래 명령을 직접 실행했다. exit 0, BUILD SUCCESSFUL in 1m 5s, XML 19개 클래스 합계 207건, failures/errors/skipped 모두 0이다. 이는 변경 기능과 직접 영향 회귀이며 전체 suite가 아니다.
  • ktlintCheck 및 bootJar는 동일 입력의 UP-TO-DATE 성공이다. production jar는 Phase 2에서 생성했고 이후 production 변경은 없다.
  • 실제 socket HTTP에서 공개/비밀 500캔 각각 DELETE 성공, V2 목록/건수 감소, legacy 전체 JSON 유지, cache 제거 후 랭킹 500캔 유지, 실제 정산 39,695원·입금액 38,385원 유지 로그를 다시 확인했다.
  • 최종 worker PID 89370의 Tomcat·Hikari 종료 로그와 ps -p 89370 잔존 없음, git diff --check exit 0을 확인했다. 전체 변경 Oracle 리뷰는 별도로 대기한다.
./gradlew test \
  --tests 'kr.co.vividnext.sodalive.v2.creator.channel.donation.*' \
  --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.*' \
  --tests 'kr.co.vividnext.sodalive.v2.creator.channel.home.*' \
  --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.*' \
  --tests 'kr.co.vividnext.sodalive.explorer.profile.channelDonation.*' \
  --tests 'kr.co.vividnext.sodalive.admin.calculate.channelDonation.AdminChannelDonationCalculateQueryRepositoryTest' \
  ktlintCheck bootJar --console=plain
  • 전체 테스트 ./gradlew test는 사용자 실행 항목이다. 운영 MySQL/DDL 실행·배포·프론트엔드 연동은 수행하지 않았다. lsp_diagnostics 미제공으로 Kotlin 컴파일/ktlint를 사용했으며 LSP 검증을 주장하지 않는다.

구현 착수: 2026-11-05

  • 사용자 "문서대로 진행" 승인으로 기술 초안과 미래 구현 계획의 실행을 시작한다. 문서 전용 제한/미요청 문구는 과거 작성 시점 기록이며, 현재 제품 코드·테스트·DDL 문서 변경을 허용한다. 실제 운영 DDL 실행과 배포는 하지 않는다.
  • 현재 test 브랜치에서 작업한다. 기존 source 변경이 없고 별도 worktree를 자동 생성하지 않는다. 검증은 프로젝트 기준의 focused test, HTTP 경로, lint 및 bootJar를 사용한다.
  • 제공된 도구에 lsp_diagnostics가 없어 Kotlin 컴파일·ktlint로 대체하며 LSP 검증 성공을 주장하지 않는다.

문서 작성: 2026-11-05

  • 상태: PRD와 미래 계획 작성 완료. 현재 Goal 없음, 구현/테스트 미착수.
  • 무엇을: 사용자 인터뷰 결정과 현행 donation/home V2 query, DTO, controller, tests를 근거로 지정 문서 두 개를 작성한다.
  • 어떻게: 코드 스타일/테스트/작업 절차/문서 유지보수/실행 명령어 지침, 두 sample 문서, 기존 비밀 후원 PRD/계획, ChannelDonationMessage, donation/home repository, response DTO 및 donation HTTP E2E test를 읽었다.
  • 문서 전용이므로 Gradle, API 호출, SQL 실행 및 runtime/test/build 검증은 하지 않았다. 향후 계획의 명령은 실행 결과가 아니다.
  • 최초 초안에서는 콘텐츠 일관성 및 공백 검사가 검증 대기였으며, 후속 결과는 아래 문서 검증 기록에 누적한다.
  • 남은 조건: 사용자의 별도 명시 구현 요청. frontend 작업은 backend와 별도 범위.

문서 검증: 2026-11-05

  • 상태: 문서 작성·검토 완료. 제품 구현은 미착수이며 활성 구현 Goal은 없다.
  • 무엇을/왜: 인터뷰 결정, PRD 요구사항, Goal·파일·검증 명령의 연결을 대조하고 기술 초안이 사용자 확정사항으로 오인되지 않는지 확인했다.
  • 내용 검증: 두 문서 전체 읽기, 실제 source/test 경로 대조, 요구사항 추적표와 상대 링크 확인. 날짜는 date '+%Y-%m-%d %Z'의 2026-11-05 KST에 맞췄다.
  • 독립 검토: 최초 검토 에이전트는 지원되지 않는 모델로 시작 실패했다. 다른 실행 경로의 검토자는 읽기 전용 builder check 후 중요한 미해결 질문·모순·실행 차단 결함이 없다고 판정했다.
  • 공백 검사: 두 문서 각각 GIT_MASTER=1 git diff --no-index --check /dev/null "docs/20261105_채널후원메시지숨김/prd.md", GIT_MASTER=1 git diff --no-index --check /dev/null "docs/20261105_채널후원메시지숨김/plan-task.md" 실행, 각각 exit 0·출력 없음. untracked 문서도 검사 대상에 포함했다.
  • 변경 범위: GIT_MASTER=1 git status --short --untracked-files=all로 이 작업의 신규 문서 두 개만 확인했다. 제품 코드·테스트·설정·SQL은 변경하지 않았다.
  • 미실행: 문서 전용 변경이므로 테스트·빌드·Gradle·API·SQL 실행과 런타임/운영 DB 검증은 생략했다. 실제 기능이 동작한다는 완료 주장은 하지 않는다.
  • 다음 조건: 기술안 검토 및 별도의 구현 요청. 전체 테스트 명령 ./gradlew test는 향후 구현 후 사용자가 직접 실행한다.

Decision Log

날짜 ID 상태 결정 근거 영향
2026-11-05 PLAN-DEC-001 사용자 확정 후원 항목 하나 soft-hide, 원본/재무 보존, 두 V2 목록만 숨김. 사용자 인터뷰 CDH-001~CDH-008
2026-11-05 PLAN-DEC-002 작성자 기술 초안 단일 boolean, 로컬 query filter, owner-only ID, bodyless DELETE API, 멱등 재요청. 현행 패키지 및 query 경계 CDH-009, P1-T1~P2-T2

최종 문서 전용 확인

  • 두 문서 간 ID/범위/기술 제안 정합성 및 trailing whitespace를 확인했다.
  • 변경 파일이 이 디렉터리의 prd.md와 plan-task.md 두 개뿐인지 확인했다.
  • 문서 전용 변경이므로 프로젝트 지침에 따라 Gradle 테스트/빌드는 실행하지 않는다. 런타임 동작은 미검증이다.

커밋 전 검증: 2026-11-05

  • 사용자 /commit-with-validation 요청으로 변경 파일과 전역 커밋 절차 및 저장소 메시지 규칙을 확인했다.
  • Java 17.0.15에서 위 「최종 기능 범위 재검증」의 명령을 동일하게 실행했다. BUILD SUCCESSFUL in 2s, exit 0이며 test·ktlintCheck·bootJar를 포함한 19개 task 모두 UP-TO-DATE였다. 테스트를 새로 실행하거나 jar를 새로 생성한 결과로 표현하지 않는다.
  • 현재 XML 19개 클래스의 208건, failures/errors/skipped 모두 0을 확인했다. git diff --check도 통과했다.
  • 전체 테스트와 운영 MySQL/DDL 적용은 수행하지 않았다. 전체 테스트 사용자 명령은 ./gradlew test이며, 배포 전 DDL 선반영 조건은 유지한다.