docs(gift): 전달불가 선물의 개인정보 파기 정책을 정의한다

This commit is contained in:
2026-11-01 14:33:29 +09:00
parent 610a8edce4
commit 1a544ddd45
@@ -4,9 +4,9 @@
| 항목 | 내용 | | 항목 | 내용 |
|---|---| |---|---|
| 문서 상태 | Phase 16 및 수령확인 시각 정밀도 회귀 수정·검증 완료 | | 문서 상태 | Phase 17 통합 개인정보 정리 구현·검증 완료 |
| 작성일 | 2026-09-29 | | 작성일 | 2026-09-29 |
| 최종 수정일 | 2026-10-26 | | 최종 수정일 | 2026-11-01 |
| 대상 제품 | 크리에이터 선물하기 | | 대상 제품 | 크리에이터 선물하기 |
| 작성자·결정권자 | 사용자 | | 작성자·결정권자 | 사용자 |
| 관련 API Contract | 별도 문서 없음. 이 문서의 `8. API 계약`을 기준으로 사용 | | 관련 API Contract | 별도 문서 없음. 이 문서의 `8. API 계약`을 기준으로 사용 |
@@ -258,9 +258,9 @@ INSPECTION_COMPLETED -> UNDELIVERABLE
| `GIFT-015` | 확정 | 관리자는 전체 선물함 목록과 상세를 조회하고 현재 상태에서 가능한 운영 액션을 확인할 수 있다 | 목록은 상태/신청번호/닉네임 검색과 페이징을 지원하고, 상세는 발송인/수취인 개인정보와 상품/배송/상태 정보를 내려준다 | `P10` | | `GIFT-015` | 확정 | 관리자는 전체 선물함 목록과 상세를 조회하고 현재 상태에서 가능한 운영 액션을 확인할 수 있다 | 목록은 상태/신청번호/닉네임 검색과 페이징을 지원하고, 상세는 발송인/수취인 개인정보와 상품/배송/상태 정보를 내려준다 | `P10` |
| `GIFT-016` | 확정 | 관리자는 운송장 등록 전 접수된 선물을 취소할 수 있다 | 관리자만 호출할 수 있고 `RECEIVED`에서만 `CANCELED`로 전이하며 `GiftDelivery.canceledAt`을 저장하고 발신자에게 사용 캔 전액을 리워드캔으로 환불한다 | `P14-T2` | | `GIFT-016` | 확정 | 관리자는 운송장 등록 전 접수된 선물을 취소할 수 있다 | 관리자만 호출할 수 있고 `RECEIVED`에서만 `CANCELED`로 전이하며 `GiftDelivery.canceledAt`을 저장하고 발신자에게 사용 캔 전액을 리워드캔으로 환불한다 | `P14-T2` |
| `GIFT-017` | 확정 | 관리자 선물함 목록과 상세는 현재 가능한 액션으로 취소 가능 여부를 함께 제공한다 | `RECEIVED`에서만 `availableActions=["CANCEL"]`; 다른 상태의 기존 액션은 유지한다 | `P14-T3` | | `GIFT-017` | 확정 | 관리자 선물함 목록과 상세는 현재 가능한 액션으로 취소 가능 여부를 함께 제공한다 | `RECEIVED`에서만 `availableActions=["CANCEL"]`; 다른 상태의 기존 액션은 유지한다 | `P14-T3` |
| `GIFT-018` | 확정 | 전달 완료 후 7일이 지난 선물의 배송 개인정보를 지운다 | 실행 시 기존 업무 시각 관례의 `now`에 대해 `status=DELIVERED`, `deliveredAt != null`, `deliveredAt <= now.minusDays(7)`인 과거 누적 건과 이후 조건을 충족하는 모든 건의 `GiftDelivery` 이름/연락처/우편번호/주소/상세주소, 택배사명/운송장번호 12개 필드를 null 처리한다. 식별자·상태·회원 닉네임·결제·동의·리뷰와 업무 lifecycle/deadline/reminder 시각은 보존하며 재실행은 추가 변경 없이 끝난다 | `P15-T2`, `P15-T3` | | `GIFT-018` | 확정 | 전달 완료 또는 전달불가 후 7일이 지난 선물의 배송 개인정보를 지운다 | 기존 업무 시각 관례의 `now`에서 `cutoff=now.minusDays(7)`로 판단한다. `DELIVERED`와 non-null `deliveredAt <= cutoff` 또는 `UNDELIVERABLE`과 non-null `undeliverableAt <= cutoff`가 짝을 이루는 과거 누적 및 이후 적격 건의 기존 배송 개인정보 12개만 null 처리한다. 다른 시각 fallback 없이 `CANCELED`와 비대상 상태는 제외하며 식별자·상태·회원·결제·동의·리뷰·업무 시각·전달불가 사유는 보존한다. 재실행은 추가 변경 0건이다 | `P15-T2`, `P15-T3`, `P17-T1`, `P17-GATE` |
| `GIFT-019` | 확정 | 배송 개인정보를 nullable persistence로 저장한다 | `GiftDelivery`의 sender 다섯 Kotlin property는 모두 `var String?`; 현행 NOT NULL sender DB 열 네 개만 nullable로 완화한다. `sender_address_detail`은 이미 nullable이며 ALTER하지 않는다. 기존 CREATE 정의는 유지하고 `gift-schema.sql` 끝에 추가된 ALTER가 fresh install 최종 schema에도 적용된다 | `P15-T1`, `P15-T2` | | `GIFT-019` | 확정 | 배송 개인정보를 nullable persistence로 저장한다 | `GiftDelivery`의 sender 다섯 Kotlin property는 모두 `var String?`; 현행 NOT NULL sender DB 열 네 개만 nullable로 완화한다. `sender_address_detail`은 이미 nullable이며 ALTER하지 않는다. 기존 CREATE 정의는 유지하고 `gift-schema.sql` 끝에 추가된 ALTER가 fresh install 최종 schema에도 적용된다 | `P15-T1`, `P15-T2` |
| `GIFT-020` | 확정 | 개인정보 null 상태에서도 기존 사용자/관리자 API 계약을 유지한다 | 기존 request 필수 타입/validation, response schema/바깥 객체/visibility와 flags/deadlines 계산 규칙을 유지한다. 데이터 없는 non-null 문자열은 `""`; 주소 전체가 null이면 주소 문자열은 정확히 `""`. nullable 바깥 객체와 tracking/admin courier/tracking은 기존 null 정책 유지 | `P15-T2`, `P15-GATE` | | `GIFT-020` | 확정 | 개인정보 null 상태에서도 기존 사용자/관리자 API 계약을 유지한다 | 기존 request 필수 타입/validation, response schema/바깥 객체/visibility와 flags/deadlines 계산 규칙을 유지한다. 데이터 없는 non-null 문자열은 `""`; 주소 전체가 null이면 주소 문자열은 정확히 `""`. nullable 바깥 객체와 tracking/admin courier/tracking은 기존 null 정책 유지 | `P15-T2`, `P15-GATE`, `P17-T1`, `P17-GATE` |
| `GIFT-021` | 확정 | 배송지 기한 만료와 deadline reminder 업무 메서드를 시간별 스케줄로 등록한다 | 기존 매시 00분 자동취소 등록은 유지한다. Asia/Seoul 기준 매시 10분은 기존 recipient expiry, 20분은 신규 erasure, 40분은 기존 reminders를 각자 독립 `@Scheduled`/lock/transaction wrapper로 위임한다. 기존 업무 로직은 바꾸지 않고 작업 간 비중첩을 보장하지 않는다 | `P15-T3`, `P15-T4` | | `GIFT-021` | 확정 | 배송지 기한 만료와 deadline reminder 업무 메서드를 시간별 스케줄로 등록한다 | 기존 매시 00분 자동취소 등록은 유지한다. Asia/Seoul 기준 매시 10분은 기존 recipient expiry, 20분은 신규 erasure, 40분은 기존 reminders를 각자 독립 `@Scheduled`/lock/transaction wrapper로 위임한다. 기존 업무 로직은 바꾸지 않고 작업 간 비중첩을 보장하지 않는다 | `P15-T3`, `P15-T4` |
## 8. API 계약 ## 8. API 계약
@@ -932,19 +932,21 @@ Response `data`는 기존 `AdminGiftOperationStatusResponse`를 사용한다.
스케줄러는 중복 실행되어도 같은 선물에 중복 환불 또는 중복 푸시가 발생하지 않아야 한다. 스케줄러는 중복 실행되어도 같은 선물에 중복 환불 또는 중복 푸시가 발생하지 않아야 한다.
배송지 입력 기한과 현재 시각이 같으면 아직 기한 초과가 아니며, `RECEIVED`, `DELIVERED`, `UNDELIVERABLE`, `CANCELED` 상태는 배송지 미입력 전달불가 대상에서 제외한다. 배송지 입력 기한과 현재 시각이 같으면 아직 기한 초과가 아니며, `RECEIVED`, `DELIVERED`, `UNDELIVERABLE`, `CANCELED` 상태는 배송지 미입력 전달불가 대상에서 제외한다.
### 8.16 전달 완료 배송 개인정보 정리 ### 8.16 통합 배송 개인정보 정리
- 매 실행 시각 `now` 기준 `Gift.status=DELIVERED`, `GiftDelivery.deliveredAt != null`, `deliveredAt <= now.minusDays(7)`인 모든 건을 처리한다. 7일 경계와 그 이전의 과거 누적 건을 포함한다. - 매 실행 시각 `now`에서 `cutoff=now.minusDays(7)`로 판단한다. `(Gift.status=DELIVERED AND deliveredAt != null AND deliveredAt <= cutoff) OR (Gift.status=UNDELIVERABLE AND undeliverableAt != null AND undeliverableAt <= cutoff)`인 모든 건을 처리한다. 두 상태 모두 정확한 7일 경계와 과거 누적 건을 포함한다.
- 각 상태와 짝인 시각만 사용한다. 짝인 시각이 null이거나 아직 7일 미경과면 다른 시각이 오래됐어도 처리하지 않으며 `COALESCE` fallback을 두지 않는다. `CANCELED`를 비롯한 다른 상태는 제외한다.
- `GiftDelivery`의 다음 12개 값만 null로 지운다: `senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail`, `recipientName`, `recipientPhoneNumber`, `recipientZipCode`, `recipientAddress`, `recipientAddressDetail`, `courierCompanyName`, `trackingNumber`. - `GiftDelivery`의 다음 12개 값만 null로 지운다: `senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail`, `recipientName`, `recipientPhoneNumber`, `recipientZipCode`, `recipientAddress`, `recipientAddressDetail`, `courierCompanyName`, `trackingNumber`.
- 기존 입력 request의 필수/non-null 계약과 검증은 유지한다. 공개 DTO 스키마와 바깥 nullable 객체는 유지하고, 기존 non-null 응답 문자열에서 데이터가 없으면 `""`로 표현한다. 주소는 완전 삭제 상태에서 정확히 빈 문자열이며 `"() "`, `"(null) null"`, 문자열 `"null"`을 만들지 않는다. - 기존 입력 request의 필수/non-null 계약과 검증은 유지한다. 공개 DTO 스키마와 바깥 nullable 객체는 유지하고, 기존 non-null 응답 문자열에서 데이터가 없으면 `""`로 표현한다. 주소는 완전 삭제 상태에서 정확히 빈 문자열이며 `"() "`, `"(null) null"`, 문자열 `"null"`을 만들지 않는다.
- `SENT`의 `senderInfo` 객체는 유지하고 각 문자열은 빈 문자열로 응답한다. `RECEIVED`의 `senderInfo`는 계속 null이다. 모든 받는 주소 값이 null이면 `recipientAddress`는 null이다. 사용자 tracking과 관리자 courier/tracking nullable 값은 null이다. - `SENT`의 `senderInfo` 객체는 유지하고 각 문자열은 빈 문자열로 응답한다. `RECEIVED`의 `senderInfo`는 계속 null이다. 모든 받는 주소 값이 null이면 `recipientAddress`는 null이다. 사용자 tracking과 관리자 courier/tracking nullable 값은 null이다.
- 기존 `recipientAddressRegistered`는 실제 필드 완비 여부를 따르므로 삭제 뒤 false다. 별도 등록 이력을 만들지 않는다. `DELIVERED` 상태, 가시성, 기존 derived flags/deadlines, 관리자 액션(빈 목록)을 유지해 재입력/운영 액션을 요구하지 않는다. - 기존 `recipientAddressRegistered`는 실제 필드 완비 여부를 따르므로 삭제 뒤 false다. 별도 등록 이력을 만들지 않는다. `DELIVERED`/`UNDELIVERABLE` 상태, 가시성, 기존 derived flags/deadlines, 관리자 액션(빈 목록)을 유지해 재입력/운영 액션을 요구하지 않는다.
- 발신/수신 배송 개인정보만 지운다. Member nickname/profile, Gift 및 GiftDelivery 식별자·status, 결제, 동의 여부/시각, 리뷰, `createdAt`, 업무 lifecycle/deadline/reminder 시각은 보존한다. `updatedAt`은 기존 `BaseEntity @PreUpdate` 동작을 따른다. - 발신/수신 배송 개인정보만 지운다. Member nickname/profile, Gift 및 GiftDelivery 식별자·status, 결제, 동의 여부/시각, 리뷰, `createdAt`, 업무 lifecycle/deadline/reminder 시각 및 `undeliverableReason`은 보존한다. `updatedAt`은 기존 `BaseEntity @PreUpdate` 동작을 따른다.
- 비대상 상태, null `deliveredAt`, 아직 7일이 안 된 건은 유지한다. 부분 삭제된 적격 건은 나머지를 지우며 완전 null 건의 재실행 변경 수는 0이다. - 비대상 상태, 상태와 짝인 null 시각, 아직 7일이 안 된 건은 유지한다. 부분 삭제된 적격 건은 나머지를 지우며 완전 null 건의 재실행 변경 수는 0이다.
- 이 작업은 푸시/환불/상태 전이를 추가하지 않는다. 앱 서버 내 DB 배송정보 정리이며 백업·로그·캐시의 삭제를 뜻하지 않는다. - 이 작업은 푸시/환불/상태 전이를 추가하지 않는다. 앱 서버 내 DB 배송정보 정리이며 백업·로그·캐시의 삭제를 뜻하지 않는다.
- 기존 자동취소만 이미 `:00`에 예약되어 있다. 다음 3개 업무 메서드를 별도 Asia/Seoul 시간별 wrapper로 등록한다. 배송지 만료 `0 10 * * * *`, 개인정보 정리 `0 20 * * * *`, 기한 안내 `0 40 * * * *`. 업무 로직은 기존 메서드 또는 새 정리 메서드로 위임한다. - 기존 자동취소만 이미 `:00`에 예약되어 있다. 다음 3개 업무 메서드를 별도 Asia/Seoul 시간별 wrapper로 등록한다. 배송지 만료 `0 10 * * * *`, 개인정보 정리 `0 20 * * * *`, 기한 안내 `0 40 * * * *`. 업무 로직은 기존 메서드 또는 새 정리 메서드로 위임한다.
- 세 신규 wrapper는 `tryLock(0, -1, SECONDS)`, `@Transactional`과 서로 다른 lock key를 사용한다. 획득한 lock은 `finally`에서 해제를 예약하고 DB commit/rollback 이후 `afterCompletion`에서 소유권 확인 후 해제한다. transaction 없는 직접 호출에서는 업무 종료 후 해제한다. 기존 `:00` 취소 wrapper 및 key는 변경하지 않는다. 다른 작업 간 비중첩은 보장하지 않는다. - 세 신규 wrapper는 `tryLock(0, -1, SECONDS)`, `@Transactional`과 서로 다른 lock key를 사용한다. 획득한 lock은 `finally`에서 해제를 예약하고 DB commit/rollback 이후 `afterCompletion`에서 소유권 확인 후 해제한다. transaction 없는 직접 호출에서는 업무 종료 후 해제한다. 기존 `:00` 취소 wrapper 및 key는 변경하지 않는다. 다른 작업 간 비중첩은 보장하지 않는다.
- `LocalDateTime`에 KST를 자동 변환하지 않는다. 기존 작업의 clock convention을 따르고 고정 `now`를 전달해 판단한다. - `LocalDateTime`에 KST를 자동 변환하지 않는다. 기존 작업의 clock convention을 따르고 고정 `now`를 전달해 판단한다.
- Phase 17은 기존 Asia/Seoul `0 20 * * * *` 작업 하나를 확장한다. 내부 이름은 `runGiftPersonalDataErasure()`, `eraseGiftPersonalData(now: LocalDateTime): Int`, `findGiftsWithPersonalDataRetentionExpired(cutoff: LocalDateTime): List<Gift>`로 통합한다. 상수 symbol은 `GIFT_PERSONAL_DATA_ERASURE_LOCK_KEY`로 바꾸되 Redis literal `lock:gift-delivered-personal-data-erasure`와 transaction/unlock 처리는 유지한다. 새 스케줄·DDL·API 계약은 추가하지 않는다.
## 9. 푸시 알림 정책 ## 9. 푸시 알림 정책
@@ -1070,11 +1072,12 @@ Response `data`는 기존 `AdminGiftOperationStatusResponse`를 사용한다.
- 내부 운영 화면에서는 필요한 권한을 가진 사용자에게만 개인정보를 노출한다. - 내부 운영 화면에서는 필요한 권한을 가진 사용자에게만 개인정보를 노출한다.
- 약관 동의 여부와 동의시각은 선물 신청 건 감사 근거로 보존한다. - 약관 동의 여부와 동의시각은 선물 신청 건 감사 근거로 보존한다.
### 11.1 전달 완료 배송 개인정보 수명주기 ### 11.1 통합 배송 개인정보 수명주기
- 정리 범위는 선물 도메인 DB의 지정된 `GiftDelivery` 열 12개다. 배송지 입력 당시 사용한 요청 필드와 검증 규칙은 그대로 필수다. - 정리 범위는 선물 도메인 DB의 지정된 `GiftDelivery` 열 12개다. 배송지 입력 당시 사용한 요청 필드와 검증 규칙은 그대로 필수다.
- 보존기간은 `DELIVERED`의 `deliveredAt`, `UNDELIVERABLE`의 `undeliverableAt` 각각으로부터 7일이다. 정확한 경계를 포함하며 짝인 시각이 null인 이력은 다른 시각으로 대체하지 않는다. `CANCELED`는 이번 확장의 대상이 아니다.
- 이 정리는 회원 계정, `Member` nickname/profile, Gift 신청/상태/ID, 캔 결제 내역, 약관 동의 snapshot, 리뷰를 삭제하지 않는다. - 이 정리는 회원 계정, `Member` nickname/profile, Gift 신청/상태/ID, 캔 결제 내역, 약관 동의 snapshot, 리뷰를 삭제하지 않는다.
- `GiftDelivery`의 업무 시각 및 기한은 운영 이력으로 보존한다. `createdAt`은 유지하고 `updatedAt`은 공통 entity auditing 규칙대로 갱신될 수 있다. - `GiftDelivery`의 업무 시각 및 기한과 `undeliverableReason`은 운영 이력으로 보존한다. `createdAt`은 유지하고 `updatedAt`은 공통 entity auditing 규칙대로 갱신될 수 있다.
- 이 정책은 백업, 애플리케이션/인프라 로그, 외부 관측 시스템, 캐시의 삭제나 보존기간을 정의하지 않는다. 전체 개인정보 삭제 완료로 설명하지 않는다. - 이 정책은 백업, 애플리케이션/인프라 로그, 외부 관측 시스템, 캐시의 삭제나 보존기간을 정의하지 않는다. 전체 개인정보 삭제 완료로 설명하지 않는다.
- 정리 작업 자체는 상태 전이·환불·알림을 일으키지 않는다. 기존 업무 정책이 정리 task를 통해 달라지지 않도록 한다. - 정리 작업 자체는 상태 전이·환불·알림을 일으키지 않는다. 기존 업무 정책이 정리 task를 통해 달라지지 않도록 한다.
- `GiftDelivery`의 sender 다섯 Kotlin property를 nullable로 선언한다. `sender_address_detail`은 이미 nullable이다. 현재 NOT NULL인 `sender_name`, `sender_phone_number`, `sender_zip_code`, `sender_address`만 schema에서 nullable로 바꾼다. - `GiftDelivery`의 sender 다섯 Kotlin property를 nullable로 선언한다. `sender_address_detail`은 이미 nullable이다. 현재 NOT NULL인 `sender_name`, `sender_phone_number`, `sender_zip_code`, `sender_address`만 schema에서 nullable로 바꾼다.
@@ -1098,11 +1101,11 @@ Response `data`는 기존 `AdminGiftOperationStatusResponse`를 사용한다.
- 관리자 목록과 상세 모두 `RECEIVED`에서만 `CANCEL` 액션을 제공하며 기존 `COMPLETE_DELIVERY` 조건부 액션을 포함한 나머지 상태 매핑은 유지한다. - 관리자 목록과 상세 모두 `RECEIVED`에서만 `CANCEL` 액션을 제공하며 기존 `COMPLETE_DELIVERY` 조건부 액션을 포함한 나머지 상태 매핑은 유지한다.
- 카테고리 `classificationNumber`, `receiptCode`, `representativeItem`의 관리자 등록/수정/조회와 사용자 폼 옵션의 `분류명/대표품목` 조합을 테스트한다. - 카테고리 `classificationNumber`, `receiptCode`, `representativeItem`의 관리자 등록/수정/조회와 사용자 폼 옵션의 `분류명/대표품목` 조합을 테스트한다.
- 신청번호는 카테고리+날짜별 sequence, 4자리 padding, `9999` 초과 자리수 증가, 병렬 unique를 테스트한다. - 신청번호는 카테고리+날짜별 sequence, 4자리 padding, `9999` 초과 자리수 증가, 병렬 unique를 테스트한다.
- 개인정보 정리는 7일 경계 포함/1초 미경과 제외, 더 오래된 이력 포함, null 완료시각 및 비전달완료 상태 제외, 부분 삭제/완전 삭제 재실행 0건을 실제 JPA 저장·flush/clear/reload로 검증한다. 보호할 결제/동의/리뷰/회원/업무 시각도 재조회해 불변임을 확인한다. - 개인정보 정리는 `DELIVERED`/`UNDELIVERABLE` 각각의 7일 경계 포함/1초 미경과 제외, 오래된 이력 포함, 짝인 null 시각 제외, 다른 시각이 오래돼도 짝인 null/최근 시각을 대체하지 않음, `CANCELED`와 다른 비대상 상태 보존, 부분 삭제/완전 삭제 재실행 0건을 실제 JPA 저장·flush/clear/reload로 검증한다. 결제/동의/리뷰/회원/업무 시각과 `undeliverableReason`도 재조회해 불변임을 확인한다.
- `GiftDelivery` 매핑은 sender 다섯 property 모두 null 저장을 허용해야 한다. DDL review는 네 nonnull sender columns만 변경하고 기존 `gift-schema.sql` trailing ALTER를 확인한다. - `GiftDelivery` 매핑은 sender 다섯 property 모두 null 저장을 허용해야 한다. DDL review는 네 nonnull sender columns만 변경하고 기존 `gift-schema.sql` trailing ALTER를 확인한다.
- 사용자 및 관리자 query/controller는 삭제 후 기존 DTO 형태, 빈 non-null 문자열의 정확한 주소 형식, nullable 필드, nickname, `DELIVERED`의 액션/CTA 미노출을 검증한다. - 사용자 및 관리자 query/controller는 두 대상 상태의 삭제 후 기존 DTO 형태, 빈 non-null 문자열의 정확한 주소 형식, nullable 필드, nickname, 액션/CTA 미노출 및 전달불가 시각/사유 보존을 검증한다. 기존 실제 JWT HTTP의 인증/권한과 완전 일치 assertion을 유지한다.
- 실제 `GiftRepository` query를 기존 `@DataJpaTest`의 embedded H2에서 실행한다. 실제 datasource replacement를 확인해 MySQL-mode/운영 MySQL 검증과 구분한다. 고정 `now`에서 cutoff 동일 시각은 포함하고 cutoff보다 1초 늦은 시각은 제외한다. - 실제 `GiftRepository` query를 기존 `@DataJpaTest`의 embedded H2에서 실행한다. 실제 datasource replacement를 확인해 MySQL-mode/운영 MySQL 검증과 구분한다. 고정 `now`에서 cutoff 동일 시각은 포함하고 cutoff보다 1초 늦은 시각은 제외한다.
- 오래된 과거 이력, null `deliveredAt`, 다른 Gift status도 각 fixture에서 독립 확인한다. 미래에 cutoff를 지나는 데이터도 매번 조회되어야 하며 첫 정리에서만 처리되는 신규 데이터 제한을 두지 않는다. - 오래된 과거 이력, null `deliveredAt`/`undeliverableAt`, 다른 Gift status도 각 fixture에서 독립 확인한다. 미래에 cutoff를 지나는 데이터도 매번 조회되어야 하며 첫 정리에서만 처리되는 신규 데이터 제한을 두지 않는다.
- 정리 전후 DB를 flush, clear, reload해 null 저장이 실제 반영됐는지 확인한다. 재실행 후 반환 변경 수가 0인지 확인한다. - 정리 전후 DB를 flush, clear, reload해 null 저장이 실제 반영됐는지 확인한다. 재실행 후 반환 변경 수가 0인지 확인한다.
- 매핑 test는 12개 필드를 모두 null로 저장할 수 있음을 확인하되 지정 외 상태 시각 필드는 그대로 남는지 검증한다. - 매핑 test는 12개 필드를 모두 null로 저장할 수 있음을 확인하되 지정 외 상태 시각 필드는 그대로 남는지 검증한다.
- controller 테스트는 기존 JSON object shape를 비교하고 사용자 요청 DTO 필수 validation이 유지되는지 확인한다. - controller 테스트는 기존 JSON object shape를 비교하고 사용자 요청 DTO 필수 validation이 유지되는지 확인한다.
@@ -1123,6 +1126,7 @@ Response `data`는 기존 `AdminGiftOperationStatusResponse`를 사용한다.
- [ ] 카테고리 추가 필드와 새 신청번호 정책이 관리자 API, 사용자 폼 옵션, 선물 신청에 반영된다. - [ ] 카테고리 추가 필드와 새 신청번호 정책이 관리자 API, 사용자 폼 옵션, 선물 신청에 반영된다.
- [x] 관리자 취소는 `RECEIVED`에서만 전액 환불과 함께 성공하고, 목록/상세의 `CANCEL` 액션은 상태에 맞게 제공된다. - [x] 관리자 취소는 `RECEIVED`에서만 전액 환불과 함께 성공하고, 목록/상세의 `CANCEL` 액션은 상태에 맞게 제공된다.
- [x] 7일 이상 지난 전달 완료 건의 지정된 배송 개인정보만 DB에서 지워지고 기존 업무 데이터와 API 계약은 유지된다. 격리 DB 및 실제 HTTP로 검증했다. (`GIFT-018`, `P15-GATE`) - [x] 7일 이상 지난 전달 완료 건의 지정된 배송 개인정보만 DB에서 지워지고 기존 업무 데이터와 API 계약은 유지된다. 격리 DB 및 실제 HTTP로 검증했다. (`GIFT-018`, `P15-GATE`)
- [x] 전달불가 후 7일인 건도 같은 작업에서 정리하며 상태별 짝인 시각·12개 필드·보존 계약과 사용자/관리자 응답을 검증했다. (`GIFT-018`, `DEC-023`, `P17-T1`, `P17-GATE`)
- [x] 다섯 sender property nullable mapping 및 네 DB 열의 ALTER 문서가 schema 요구를 충족한다. 운영 적용은 별도다. (`GIFT-019`, `P15-T1`, `P15-T2`) - [x] 다섯 sender property nullable mapping 및 네 DB 열의 ALTER 문서가 schema 요구를 충족한다. 운영 적용은 별도다. (`GIFT-019`, `P15-T1`, `P15-T2`)
- [x] 삭제 후 빈 문자열/null 표현으로 기존 API schema와 사용자 입력 검증을 유지한다. (`GIFT-020`, `P15-T2`, `P15-GATE`) - [x] 삭제 후 빈 문자열/null 표현으로 기존 API schema와 사용자 입력 검증을 유지한다. (`GIFT-020`, `P15-T2`, `P15-GATE`)
- [x] 만료/정리/reminder 업무 메서드가 각각 Asia/Seoul `:10`/`:20`/`:40` schedule wrapper로 등록된다. 실제 Spring 등록을 확인했다. (`GIFT-021`, `P15-T3`, `P15-T4`) - [x] 만료/정리/reminder 업무 메서드가 각각 Asia/Seoul `:10`/`:20`/`:40` schedule wrapper로 등록된다. 실제 Spring 등록을 확인했다. (`GIFT-021`, `P15-T3`, `P15-T4`)
@@ -1145,9 +1149,9 @@ Phase 15 서버 구현 완료 판정에는 `P15-T2`~`P15-T4` focused test와 격
| `GIFT-011` | 6 | 푸시 이벤트 테스트 | 딥링크와 수신자 확인 | | `GIFT-011` | 6 | 푸시 이벤트 테스트 | 딥링크와 수신자 확인 |
| `GIFT-014` | 8 | 선물 사용자/관리자 controller 날짜 응답 테스트 | UTC ISO 문자열 확인 | | `GIFT-014` | 8 | 선물 사용자/관리자 controller 날짜 응답 테스트 | UTC ISO 문자열 확인 |
| `GIFT-016`, `GIFT-017` | 14 | 관리자 취소 service/controller 및 query service 테스트 | `RECEIVED` 액션과 취소/환불 결과 확인 | | `GIFT-016`, `GIFT-017` | 14 | 관리자 취소 service/controller 및 query service 테스트 | `RECEIVED` 액션과 취소/환불 결과 확인 |
| `GIFT-018` | 15 | repository predicate 및 erasure method test | 격리 fixture 이후 삭제 필드와 보존 데이터 재조회 | | `GIFT-018` | 15, 17 | 두 상태별 repository predicate 및 통합 erasure method test | 격리 fixture 이후 삭제 필드와 보존 데이터 재조회 |
| `GIFT-019` | 15 | GiftEntityMappingTest, SQL section review | no production DB execution in Gate | | `GIFT-019` | 15 | GiftEntityMappingTest, SQL section review | no production DB execution in Gate |
| `GIFT-020` | 15 | query/controller contract regression | fixture erase 후 실제 socket HTTP SENT/RECEIVED/admin 조회 | | `GIFT-020` | 15, 17 | 두 상태의 query/controller contract regression | fixture erase 후 실제 socket HTTP SENT/RECEIVED/admin 조회 |
| `GIFT-021` | 15 | scheduler reflection/lock/delegation tests | schedule/zone/lock key 대조 | | `GIFT-021` | 15 | scheduler reflection/lock/delegation tests | schedule/zone/lock key 대조 |
`GIFT-019` rollout은 운영 schema 확인, DDL review 및 별도 실행 승인, nullable schema와 호환 reader 준비를 거친다. Gate는 운영 DB에서 SQL 또는 정리 작업을 실행하지 않는다. `GIFT-019` rollout은 운영 schema 확인, DDL review 및 별도 실행 승인, nullable schema와 호환 reader 준비를 거친다. Gate는 운영 DB에서 SQL 또는 정리 작업을 실행하지 않는다.
@@ -1178,9 +1182,14 @@ Phase 15 서버 구현 완료 판정에는 `P15-T2`~`P15-T4` focused test와 격
| 2026-10-23 | `DEC-020` | 확정 | sender 다섯 Kotlin property 중 `senderAddressDetail` 포함 다섯 모두 nullable, DB상 현재 NOT NULL인 sender 열 네 개만 변경한다 | entity 필드와 DDL nullable 경계의 구별 | `GIFT-019`, `P15-T2` | | 2026-10-23 | `DEC-020` | 확정 | sender 다섯 Kotlin property 중 `senderAddressDetail` 포함 다섯 모두 nullable, DB상 현재 NOT NULL인 sender 열 네 개만 변경한다 | entity 필드와 DDL nullable 경계의 구별 | `GIFT-019`, `P15-T2` |
| 2026-10-23 | `DEC-021` | 확정 | 신규 시간별 scheduler wrapper는 기존 만료/안내 업무 메서드에 위임하며, 기존 예약은 자동취소 `:00`뿐이다. 신규 등록은 `:10`, `:20`, `:40`이다 | 현재 scheduler 코드와 승인 일정 | `GIFT-021`, `P15-T3`~`P15-T4` | | 2026-10-23 | `DEC-021` | 확정 | 신규 시간별 scheduler wrapper는 기존 만료/안내 업무 메서드에 위임하며, 기존 예약은 자동취소 `:00`뿐이다. 신규 등록은 `:10`, `:20`, `:40`이다 | 현재 scheduler 코드와 승인 일정 | `GIFT-021`, `P15-T3`~`P15-T4` |
| 2026-10-26 | `DEC-022` | 확정 | 기존 PRD의 수령확인·배송지 만료 발신자 알림 호출 누락 두 곳을 보완하고 관리자 가격 변경/카테고리 비활성화 후 기존 신청 스냅샷 보존 통합 테스트를 추가한다 | 사용자 후속 작업 승인 | `GIFT-008`, `GIFT-011`, `GIFT-013`, `P16-T1`~`P16-GATE` | | 2026-10-26 | `DEC-022` | 확정 | 기존 PRD의 수령확인·배송지 만료 발신자 알림 호출 누락 두 곳을 보완하고 관리자 가격 변경/카테고리 비활성화 후 기존 신청 스냅샷 보존 통합 테스트를 추가한다 | 사용자 후속 작업 승인 | `GIFT-008`, `GIFT-011`, `GIFT-013`, `P16-T1`~`P16-GATE` |
| 2026-11-01 | `DEC-023` | 확정 | 기존 :20 파기를 `DELIVERED`+non-null `deliveredAt <= now.minusDays(7)` 또는 `UNDELIVERABLE`+non-null `undeliverableAt <= cutoff`로 확장한다. `COALESCE`/다른 시각 fallback 및 `CANCELED`는 제외한다. 기존 12개 필드와 API/업무 데이터/전달불가 사유, Redis literal·transaction/unlock을 보존하고 내부 명칭만 통합한다 | 사용자 승인된 통합 개인정보 파기 정책. `DEC-017`의 당시 전달완료 구현 기록은 보존한다 | `GIFT-018`, `GIFT-020`, `8.16`, `11.1`, `P17-T1`, `P17-GATE` |
## 문서 상태 안내 ## 문서 상태 안내
Phase 14의 당시 전체 테스트 제외 및 검증 이력은 `plan-task.md`의 P14-GATE 기록을 보존한다. Phase 15는 후속 실행 승인에 따라 서버 구현과 최종 전체 테스트 2,396개·lint·build·실소켓 QA·리뷰를 완료했다. 운영 MySQL DDL 실행 및 배포는 별도 승인/조건이며 이번 완료 범위에 포함하지 않는다. Phase 14의 당시 전체 테스트 제외 및 검증 이력은 `plan-task.md`의 P14-GATE 기록을 보존한다. Phase 15는 후속 실행 승인에 따라 서버 구현과 최종 전체 테스트 2,396개·lint·build·실소켓 QA·리뷰를 완료했다. 운영 MySQL DDL 실행 및 배포는 별도 승인/조건이며 이번 완료 범위에 포함하지 않는다.
Phase 16은 2026-10-26 후속 승인 범위의 서버 구현·검증을 완료했다. 최종 전체 테스트 2,400개·lint·build 통과 및 실제 localhost HTTP/DB 시나리오와 독립 리뷰 승인을 확인했다. Phase15의 과거 기록과 달리 이번 두 성공 기준은 새 검증 근거로 완료 표시했으며, 나머지 과거 성공 기준은 일괄 체크하지 않았다. Phase 16은 2026-10-26 후속 승인 범위의 서버 구현·검증을 완료했다. 최종 전체 테스트 2,400개·lint·build 통과 및 실제 localhost HTTP/DB 시나리오와 독립 리뷰 승인을 확인했다. Phase15의 과거 기록과 달리 이번 두 성공 기준은 새 검증 근거로 완료 표시했으며, 나머지 과거 성공 기준은 일괄 체크하지 않았다.
Phase 17은 2026-11-01 기존 문서를 먼저 갱신한 후 UNDELIVERABLE 미처리 assertion RED와 통합 구현 GREEN을 확인했다. 관련 5개 클래스 165개와 lint가 통과했으며 두 상태의 실제 JWT HTTP 파기 전후 검증을 포함한다. 전체 suite·독립 review·최종 QA는 parent의 `P17-GATE` 대기이며 이번 focused 결과로 Gate나 운영 반영 완료를 주장하지 않는다.
Phase 17 최종 판정: 위 focused 인계 이후 전체 2,904개 테스트(실패·오류·skip 0), build·ktlintCheck와 단일 독립 리뷰 APPROVE를 확인해 `P17-GATE`를 완료했다. lead가 별도로 실행한 실제 JWT HTTP/DB 두 상태 검증과 scheduler 59개도 통과했다. 운영 MySQL·배포·실제 Redis 다중 인스턴스는 미검증이며 커밋·푸시는 하지 않았다.