diff --git a/docs/20260929_크리에이터_선물하기/plan-task.md b/docs/20260929_크리에이터_선물하기/plan-task.md index c07e9704..c6c44d7a 100644 --- a/docs/20260929_크리에이터_선물하기/plan-task.md +++ b/docs/20260929_크리에이터_선물하기/plan-task.md @@ -4,12 +4,12 @@ | 문서 항목 | 내용 | |---|---| -| 상태 | Phase 16 및 P16-R1 시각 정밀도 회귀 수정·검증 완료 | +| 상태 | Phase 17 통합 개인정보 정리 구현·Gate 완료 | | 작성일 | 2026-09-29 | | 요구사항 기준 | `docs/20260929_크리에이터_선물하기/prd.md` | | API 기준 | `docs/20260929_크리에이터_선물하기/prd.md`의 `8. API 계약` | -| 현재 Phase | Phase 16 누락 푸시와 가격 스냅샷 검증 | -| 현재 활성 Goal | 없음. `P16-R1` 완료, CI 재실행·배포는 별도 | +| 현재 Phase | Phase 17 통합 배송 개인정보 파기 확장 | +| 현재 활성 Goal | 없음. `P17-T1`, `P17-GATE` 완료 | ## 목표 @@ -44,6 +44,7 @@ | 14 | 완료 | `4/4` | 완료 | 전체 테스트 제외, 운영 MySQL/배포/별도 프론트엔드 검증 범위 밖 | | 15 | 서버 구현·Gate 완료 | `4/4` Task, `P15-R1` 완료 | 없음 | 운영 schema/reader 배포 확인은 별도 | | 16 | 기존 구현·회귀 수정 완료 | `2/2` Task, `P16-R1` 완료 | 없음 | 최신 전체2403개 통과, CI 재실행·배포 별도 | +| 17 | 구현·Gate 완료 | `1/1` | 없음 | 전체 2,904개 통과, 운영 DB·배포는 별도 | - 동시에 하나의 미완료 goal만 운용한다. - 구현 중 요구사항이 바뀌면 이 문서의 체크박스와 범위를 먼저 갱신한 뒤 코드를 수정한다. @@ -2079,3 +2080,73 @@ P15-T1 문서 검증 → 별도 구현 승인 → P15-T2 → P15-T3 → P15-T4 - 검증 명령: `./gradlew --no-daemon test --rerun --tests '*GiftCommandServiceTest' --tests '*AdminGiftControllerTest' --tests '*GiftAdminServiceTest' ktlintCheck`. - 결과: exit 0, `BUILD SUCCESSFUL in 1m 33s`. 최신 JUnit XML 3개에서 137개 테스트, 실패·오류·skip 0을 확인했다. 테스트는 재실행됐으며 컴파일과 lint는 동일 입력의 UP-TO-DATE 결과다. `git diff --check`도 통과했다. - 범위: 수령확인 시각 정밀도 수정의 커밋 검증으로 관련 서비스·HTTP·DB 왕복 회귀를 재실행했다. 전체 테스트와 패키징 빌드는 반복하지 않았으며 위 2,403개 결과는 이전 검증 기록이다. 운영 MySQL·Jenkins 재실행·배포는 미검증이다. + +### Phase 17: 통합 배송 개인정보 파기 확장 + +**승인 기준:** 2026-11-01 사용자 승인, PRD `GIFT-018`, `8.16`, `11.1`, `DEC-023`. Phase 15/16의 완료 체크박스·당시 symbol·검증 이력은 보존한다. 새 계획/스케줄/DDL/API/의존성 없이 기존 :20 작업만 확장한다. + +#### Task 17.1 상태별 보존기간과 통합 파기 회귀 구현 + +- [x] **Task 17.1 (`P17-T1`):** 전달완료·전달불가의 짝인 종료 시각으로부터 7일 지난 배송 개인정보를 기존 작업에서 동일하게 정리한다. +- **시작 조건:** 승인 정책과 기존 문서 갱신을 production 수정 전에 완료한다. 기존 `GiftSchedulerTest` 52/52 통과는 parent가 확인한 baseline이며 이번 실행의 RED/GREEN과 구분한다. +- **완료 증거:** 아래 TDD 체크박스, 실제 repository/H2 왕복, 관련 query/controller의 기존 assertion을 보존한 focused 통과와 실행 기록. +- **범위 밖:** 전체 suite·독립 review·최종 QA/Gate 판정(parent 소유), 새 scheduler·DDL·API·dependency·전역 설정, 취소·상태 전이·결제·푸시 수정, 운영 DB·배포·커밋. + +**Files:** + +- Modify: `docs/20260929_크리에이터_선물하기/prd.md`, `docs/20260929_크리에이터_선물하기/plan-task.md`. +- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt`. +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt`. +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminQueryServiceTest.kt`. +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`. + +**Interfaces와 불변조건:** + +- `(DELIVERED AND deliveredAt IS NOT NULL AND deliveredAt <= cutoff) OR (UNDELIVERABLE AND undeliverableAt IS NOT NULL AND undeliverableAt <= cutoff)`, `cutoff=now.minusDays(7)`. 상태와 다른 시각 fallback/`COALESCE` 없음, `CANCELED` 제외. +- `runGiftPersonalDataErasure()`, `eraseGiftPersonalData(now: LocalDateTime): Int`, `findGiftsWithPersonalDataRetentionExpired(cutoff: LocalDateTime): List`로 모든 실제 호출자를 함께 rename한다. +- 상수 `GIFT_PERSONAL_DATA_ERASURE_LOCK_KEY`의 Redis literal은 계속 `lock:gift-delivered-personal-data-erasure`. Asia/Seoul `0 20 * * * *` 한 작업과 transaction/unlock은 불변이다. +- 기존 12개 필드만 null 처리한다. `undeliverableAt`, `undeliverableReason` 및 다른 업무·결제·회원·동의·리뷰는 보존하며 파기 자체의 push/refund는 0회다. + +- [x] **RED:** 기존 production 이름을 유지한 채 UNDELIVERABLE의 정확한 7일 경계 파기 테스트를 추가한다. +- [x] **RED 확인:** `./gradlew --no-daemon --console=plain test --tests '*GiftSchedulerTest.shouldEraseGiftWhenMatchingTimestampEqualsSevenDayCutoff'`로 미구현 assertion 실패를 확인했다. compilation/fixture 오류는 RED 증거가 아니다. +- [x] **GREEN:** 최소 JPQL 조건 확장과 내부 symbol/caller rename만 적용했다. 두 상태의 경계·과거 이력·null/다른 시각 배제·unsupported 상태·부분 파기·재실행·보존 검증을 기존 테스트 parameterization으로 보강했다. +- [x] **GREEN 확인:** 같은 테스트 2개 및 `GiftSchedulerTest` 전체 59개를 재실행해 성공을 확인했다. +- [x] **REFACTOR:** 이번 변경의 assertion을 재사용하며 query/controller의 두 종료 상태와 기존 실제 JWT HTTP 파기 전후 검증을 확장했다. `./gradlew --no-daemon --console=plain test --tests '*GiftSchedulerTest' --tests '*GiftQueryServiceTest' --tests '*GiftAdminQueryServiceTest' --tests '*GiftControllerTest' --tests '*AdminGiftControllerTest' ktlintCheck`가 통과했다. LSP unavailable이므로 Kotlin compilation으로 타입을 검증했다. + +#### Phase 17 Gate + +- [x] **`P17-GATE`:** 통합 파기 정책과 전체 회귀·독립 review·최종 QA를 parent가 판정했다. +- **시작 조건:** `P17-T1` 완료와 focused 증거 인계. +- **완료 증거:** parent가 전체 회귀 및 독립 review/최종 QA 결과를 별도 누적하고 성공 기준을 판정한다. 구현 담당의 focused 통과만으로 Gate를 체크하지 않는다. +- **범위 밖:** 테스트 완화/삭제, 비관련 수정, 운영 DB·배포·커밋. +- **검증:** 기존 :20 등록/lock literal/transaction-unlock 불변, 상태별 predicate와 지정 외 데이터 보존, 기존 HTTP schema·권한·종료 액션을 대조한다. 전체 suite는 구현 담당이 실행하지 않는다. + +#### Phase 17 Progress — 2026-11-01 + +- 구현 전: 저장소 규칙과 sample, 기존 PRD/plan 및 모든 erasure symbol 호출자를 확인했다. PRD 요구사항/수용 기준/추적표에 확장 정책과 `DEC-023`을 추가하고 기존 완료 이력은 보존했다. +- 문서 작성 시점: `P17-T1` RED 대기. 독립 작업 디렉터리는 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/gift-erasure-implementation.USRpP1`이며 parent가 인계 후 정리한다. 공유 cache/parent ledger는 변경하지 않는다. +- RED: 위 focused 명령 `BUILD FAILED in 39s`, 2개 중 DELIVERED 통과·UNDELIVERABLE 1개 assertion 실패, 오류/skip 0. `GiftSchedulerTest.kt:623`의 기존 `eraseDeliveredGiftPersonalData(ERASURE_NOW)` 반환값이 `expected: <1> but was: <0>`였다. production 소스는 이 확인까지 변경하지 않았다. 로그는 작업 디렉터리 `red.log`, assertion 원본은 당시 JUnit XML로 확인했다. +- 단독 GREEN: RED와 같은 focused 명령 `BUILD SUCCESSFUL in 57s`, 두 상태 2개 실패·오류·skip 0. 로그 `green-boundary.log`, RED assertion 기록 `red-evidence.md`를 보존한다. +- 중간 영향 회귀: 위 5개 클래스 명령의 첫 실행은 165개 중 2개 fixture 오류로 실패했다. `GiftControllerTest` parameterization에서 신청번호가 `varchar(20)`을 넘은 것이 원인이며 짧고 고유한 `ERASE-$giftStatus`로 fixture만 고쳤다. 업무 RED와 구분하며 production 스키마/validation은 바꾸지 않았다. 로그 `green-focused.log`. +- 최종 GREEN/REFACTOR: 위 5개 클래스 + `ktlintCheck` 명령 `BUILD SUCCESSFUL in 1m 30s`, exit 0. 최신 XML 5개에서 scheduler 59 + 사용자 query 21 + 관리자 query 9 + 사용자 controller 16 + 관리자 controller 60 = 165개, 실패·오류·skip 0. Kotlin main/test compilation과 lint 통과. 로그 `green-focused-final.log`. +- 실제 HTTP: `AdminGiftControllerTest.shouldEraseGiftAndPreserveRealJwtHttpContracts`의 두 상태 모두 RANDOM_PORT Tomcat/실제 JWT로 파기 전후 SENT/RECEIVED/admin 상세·목록 200, 익명 401·타인 거부, 빈 문자열/null/닉네임/빈 액션/CTA 미노출과 UTC 종료 시각을 확인했다. DB 재조회로 12개 null, 최초 1건·재실행 0건, 결제·동의·리뷰·이력 및 전달불가 사유 불변을 확인했다. `build/test-results/test/TEST-kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest.xml`의 `P17_HTTP`/`P17_DB`와 `build/reports/tests/test/index.html`에 증거가 있다. +- 검증 경계: scheduler/query `@DataJpaTest`가 실제 datasource를 기본 embedded H2로 대체함을 로그에서 확인했다. 실제 운영 MySQL 또는 MySQL-mode 검증으로 부르지 않는다. HTTP 테스트의 H2 MySQL-mode와도 구분한다. 기존 비관련 scheduler의 H2 `SKIP LOCKED`·JSON/Netty/Security·Gradle 경고는 범위 밖으로 유지했다. +- 현재 상태: `P17-T1` 완료, `P17-GATE` 대기. 전체 suite·별도 packaging build·독립 review·최종 QA는 parent 소유이므로 실행하지 않았다. 운영 DB/배포/실제 FCM/Redis 다중 인스턴스/커밋·푸시는 수행하지 않았다. LSP 부재는 컴파일로 대체했다. +- 문서/범위 확인: 최종 `git diff --check` 통과. 변경은 위 문서 2개·production 2개·관련 test 5개뿐이며 `src`의 old erasure symbol은 0건이다. 기존 완료 이력과 Redis literal·정리 본문·transaction/unlock의 보존을 diff로 대조했다. test worker/Redis 잔류는 없고 작업 전부터 실행된 GradleDaemon PID 6061(12:11 시작)은 보존했다. + +#### Phase 17 최종 Gate — 2026-11-01 + +- 직접 QA: lead가 `./gradlew --no-daemon --console=plain test --tests '*AdminGiftControllerTest.shouldEraseGiftAndPreserveRealJwtHttpContracts' --tests '*GiftSchedulerTest' ktlintCheck` 실행, exit 0, `BUILD SUCCESSFUL in 50s`. scheduler 59개 + 실제 JWT HTTP 2개 = 61개 실패·오류·skip 0. XML의 `P17_HTTP` 4행/`P17_DB` 2행으로 두 상태 모두 삭제 전후 HTTP 200, 개인정보 12개 null, 최초 1건·재실행 0건 및 업무 데이터 보존을 확인했다. +- 전체 검증: `./gradlew --no-daemon --console=plain test --rerun ktlintCheck build --info`를 필터 없이 1회 실행했다. exit 0, `BUILD SUCCESSFUL in 10m 9s`, 최신 JUnit XML 335개에서 2,904개 테스트 실패·오류·skip 0. lead도 별도 XML parser로 합계를 재확인했다. `:test`는 실제 재실행됐고 컴파일·lint task는 동일 입력의 UP-TO-DATE 결과다. clean 재컴파일로 표현하지 않는다. +- 빌드: `build/libs/sodalive-0.0.1-SNAPSHOT.jar` 생성(148,263,130 bytes), 변경한 scheduler/repository class 포함 확인. SHA-256 `b4d54f9943004f90d77bb4a26e84417a16a17b69f9af85fa41f8c5678fa03dda`. 별도 JAR 기동은 하지 않았으며 실제 HTTP는 RANDOM_PORT Spring Boot로 검증했다. +- 리뷰: 단일 독립 reviewer가 전체 diff와 연결된 query/DTO/controller/인증/종료 상태·취소 경로를 읽고 APPROVE, 확정 결함·차단 사항 없음. 기록: `reviews/phase17-personal-data-erasure-review.md`. 운영 코드 추가 수정은 없었다. +- 증거: `build/reports/tests/test/index.html`, `build/test-results/test/TEST-*.xml`; 관리자 XML의 `P17_HTTP`/`P17_DB`는 전체 실행에서도 두 상태 모두 통과했다. 기존 자동취소 테스트와 예약 등록 검사도 전체 회귀에 포함된다. +- 한계: LSP 도구 없음으로 기존 Kotlin compile 및 ktlint 검증을 사용했다. H2 기반 검증이며 운영 MySQL/기존 nullable ALTER 적용, 실 Redis 다중 인스턴스, FCM 기기 수신·배포는 검증하지 않았다. 이미 7일 지난 과거 전달불가 건은 배포 후 적격 실행에서 삭제된다. 혼합 버전 중 구버전이 락을 먼저 얻으면 해당 회차는 전달완료만 처리할 수 있다. +- 자원 확인: 전체 실행의 wrapper 23146·daemon 23158·test worker 23261·Redis 23433 종료 확인. 기존 사용자 GradleDaemon 6061은 보존한다. 소유 임시 root 정리 결과는 아래에 누적한다. 커밋·스테이징·푸시·운영 데이터 조작 없음. +- 정리 완료: 최종 프로세스 재조회에서 기존 daemon 6061만 남았다. 소유 root `gift-erasure.Q703u2`, `gift-erasure-implementation.USRpP1`, `gift-erasure-verification.cDMqxU`, `gift-erasure-review.IWO3vO`의 소유 산출물과 비심볼릭링크 여부를 확인하고 삭제 후 네 경로 부재를 확인했다. 최초 정리 명령은 zsh 예약 변수 `path` 때문에 `rm`을 찾지 못해 삭제하지 않았으며, 변수명을 수정한 재실행으로 정상 정리했다. 공유 cache와 최신 build/JUnit/HTTP 산출물은 보존했다. 최종 문서 공백 검사와 신규 리뷰 기록 확인 후 종료한다. + +#### Phase 17 커밋 전 검증 — 2026-11-01 + +- 실행: `./gradlew --no-daemon --console=plain test --rerun --tests '*GiftSchedulerTest' --tests '*GiftQueryServiceTest' --tests '*GiftAdminQueryServiceTest' --tests '*GiftControllerTest' --tests '*AdminGiftControllerTest' ktlintCheck`. +- 결과: exit 0, `BUILD SUCCESSFUL in 1m 24s`. 최신 JUnit XML 5개에서 165개 테스트, 실패·오류·skip 0을 확인했다. 테스트는 실제 재실행됐으며 컴파일·lint는 동일 입력의 UP-TO-DATE 결과다. `git diff --check`와 `src`의 이전 개인정보 정리 symbol 잔존 0건도 확인했다. +- 범위: 선물 개인정보 정리 변경의 직접 영향 범위만 재검증했다. 전체 suite·패키징 빌드는 위 최종 Gate 이후 코드 추가 변경이 없어 반복하지 않았다. 2,904개 전체 통과는 이전 실행 기록이며 이번 실행의 최신 XML/HTML은 165개 focused 결과다. 운영 MySQL·실제 Redis 다중 인스턴스·배포는 미검증이다. diff --git a/docs/20260929_크리에이터_선물하기/reviews/phase17-personal-data-erasure-review.md b/docs/20260929_크리에이터_선물하기/reviews/phase17-personal-data-erasure-review.md new file mode 100644 index 00000000..06b6d902 --- /dev/null +++ b/docs/20260929_크리에이터_선물하기/reviews/phase17-personal-data-erasure-review.md @@ -0,0 +1,32 @@ +# Phase 17 통합 배송 개인정보 정리 리뷰 + +## 리뷰 정보 + +- 일자: 2026-11-01 +- 대상: 승인된 Phase 17 working tree. 문서 2개, 운영 코드 2개, 테스트 5개. +- 기준: PRD `GIFT-018`, `GIFT-020`, `DEC-023`, 계획 `P17-T1`/`P17-GATE`. +- 방법: 구현 담당과 별개의 단일 read-only reviewer가 전체 diff와 연결된 repository, entity, query, DTO, controller, 인증 및 상태 전이 경로를 검토했다. +- 판정: **APPROVE. 확정 발견 사항 및 수정 차단 사항 없음.** + +## 기준별 판정 + +| 기준 | 결과 | +|---|---| +| 상태와 기준 시각 pairing | DELIVERED/deliveredAt 및 UNDELIVERABLE/undeliverableAt의 non-null 시각만 7일 inclusive cutoff로 판정. fallback·CANCELED 제외 | +| 삭제·보존 범위 | 기존 12개 필드 null 처리 본문 유지. 상태·전달불가 사유·업무 시각·회원·결제·동의·리뷰 보존 | +| 예약·잠금 | 매시 :20 Asia/Seoul, Redis literal, transaction/unlock 불변. 자동취소는 미변경 | +| 경계·반복 | 정확한 경계·과거 이력 포함, 1초 미경과·짝인 null 시각·비대상 상태 제외. 부분 삭제·재실행 0건 검증 | +| 조회 회귀 | 두 상태의 실제 JWT HTTP 사용자/관리자 상세·목록, 빈 문자열/null, 권한·액션·flags·시각·사유 보존 | +| 변경 최소성 | 새 스케줄·DDL·API·dependency 없음. 기존 assertion을 재사용한 parameterization, 내부 이름과 호출자 일치 | + +## 검증 근거 + +- reviewer는 RED의 UNDELIVERABLE `expected 1 / actual 0`, 구현 담당의 focused 165개 통과 기록을 대조했다. +- lead 재실행의 scheduler 59개와 실제 HTTP 2개 XML을 reviewer가 직접 확인했다. 실패·오류·skip 0, 두 상태의 DB `erased=1`, `repeated=0`, `persistedNulls=12`와 보존 assertion 통과. +- 리뷰 당시 전체 실행은 진행 중이었다. 이후 lead가 별도 verifier 결과 및 XML 합계를 확인했다: 전체 2,904개, 실패·오류·skip 0, build·ktlintCheck 통과. 상세 명령과 결과는 기존 `plan-task.md`의 Phase 17 최종 Gate에 기록했다. + +## 운영 경계 + +- 기존 nullable schema/reader 호환이 선행조건이다. 배포 후 오래된 전달불가 개인정보도 비가역적으로 삭제된다. 혼합 버전에서 구버전이 같은 락을 획득한 회차는 전달완료만 처리할 수 있다. +- 기존 전건 조회·건별 delivery 조회·단일 트랜잭션 구조는 유지된다. 운영 누적 건수/실행계획 정보 없이 부하 안전성을 단정하지 않으며 이번 범위에서 batching/index를 추가하지 않는다. +- 운영 MySQL·실제 Redis 다중 인스턴스·배포·FCM·백업/로그/캐시 삭제는 미검증 또는 범위 밖이다. LSP는 제공되지 않아 Kotlin compile/ktlint로 대체했다.