Files

2153 lines
204 KiB
Markdown

# 크리에이터 선물하기 구현 계획
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
| 문서 항목 | 내용 |
|---|---|
| 상태 | Phase 17 통합 개인정보 정리 구현·Gate 완료 |
| 작성일 | 2026-09-29 |
| 요구사항 기준 | `docs/20260929_크리에이터_선물하기/prd.md` |
| API 기준 | `docs/20260929_크리에이터_선물하기/prd.md`의 `8. API 계약` |
| 현재 Phase | Phase 17 통합 배송 개인정보 파기 확장 |
| 현재 활성 Goal | 없음. `P17-T1`, `P17-GATE` 완료 |
## 목표
팬이 캔으로 크리에이터에게 실물 선물을 신청하고, 운송장 등록부터 크리에이터 배송지 입력, 운영 검수, 전달 완료/불가, 리뷰와 푸시까지 서버에서 추적한다. Phase 14는 `RECEIVED` 선물의 ADMIN 취소·환불과 목록/상세 액션을 제공한다.
## 아키텍처
신규 도메인은 `kr.co.vividnext.sodalive.v2.gift` 아래에 배치하고, v2 신규 도메인 관례대로 `domain`, `application`, `port`, `adapter` 패키지를 사용한다. JPA 엔티티와 repository는 `adapter.out.persistence`에 두고, 순수 enum/정책은 `domain`, 상태 전이와 캔 결제/환불 및 푸시 조합은 `application`에 둔다. 사용자 API는 `kr.co.vividnext.sodalive.v2.api.gift`, 관리자 API는 `kr.co.vividnext.sodalive.v2.api.admin.gift` 아래에 두되 controller는 v2 web adapter 관례에 맞춰 `adapter.in.web`, DTO는 `dto` 하위에 둔다. 공개 API DTO는 API 패키지에 두되 도메인/application service가 API DTO를 import하지 않도록 한다.
## 기술 스택
- Kotlin + Java 17
- Spring Boot 2.7.14
- Spring MVC, Spring Security, Spring Data JPA, QueryDSL
- Gradle Wrapper, ktlint
- 테스트: 기존 JUnit/Spring Boot Test/MockMvc 패턴
## 현재 상태
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1 | 완료 | `4/4` | 완료 | 없음 |
| 2 | 완료 | `4/4` | 완료 | 없음 |
| 3 | 완료 | `4/4` | 완료 | 없음 |
| 4 | 완료 | `3/3` | 완료 | 없음 |
| 5 | 완료 | `3/3` | 완료 | 없음 |
| 6 | 완료 | `2/2` | 완료 | 없음 |
| 7 | 완료 | `3/3` | 완료 | 없음 |
| 8 | 완료 | `1/1` | 완료 | 없음 |
| 9 | 완료 | `3/3` | 완료 | 없음 |
| 10 | 완료 | `3/3` | 완료 | 관리자 선물함 목록/상세 API 추가 |
| 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만 운용한다.
- 구현 중 요구사항이 바뀌면 이 문서의 체크박스와 범위를 먼저 갱신한 뒤 코드를 수정한다.
- 완료된 Task와 Progress 기록은 삭제하거나 덮어쓰지 않는다.
## 범위
### 포함
- 선물 신청, 목록, 상세, 운송장 등록, 취소, 크리에이터 배송지 입력, 크리에이터 수령확인, 리뷰 작성 사용자 API
- 관리자 카테고리 API, 관리자 사이즈 가격 API, 운영 상태변경 API
- Phase 14 관리자 신청 취소·환불과 목록/상세 `CANCEL` 액션, 별도 관리자 기능 추가 프롬프트.
- 운송장 미등록 자동취소, 배송지 미입력 전달불가, 기한 24시간 전 안내 스케줄러
- 캔 사용내역 `선물하기` 추가와 선물 신청 내부 연결
- 팬/크리에이터 대상 푸시 이벤트와 단일 딥링크 `GIFT_DETAIL` + `applicationNo`
- MySQL 운영 반영용 DDL 문서
### 제외
- 약관 본문 제공 API
- 외부 택배 조회, 송장 유효성 실시간 조회, 배송 추적 자동화
- 외부 PG 에스크로, 커머스 플랫폼, provider별 택배사 코드 매핑
- 선물 이미지, 첨부파일, 메시지 카드
- 카테고리 hard delete
- 선물 사이즈 정의 추가/삭제
- 캔 사용내역 화면에서 선물 상세로 이동하는 사용자 기능
## 파일 책임 지도
### 문서와 DDL
- Create: `docs/20260929_크리에이터_선물하기/gift-schema.sql` — MySQL 운영 반영용 신규 테이블/인덱스 DDL.
- Modify: `docs/20260929_크리에이터_선물하기/plan-task.md` — 각 Task 실행 후 체크박스와 Progress 기록 누적.
### 도메인 모델과 repository
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/domain/GiftStatus.kt` — 선물 상태 enum.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/domain/GiftSize.kt` — 선물 사이즈 enum.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/Gift.kt` — 선물 신청 엔티티.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDelivery.kt` — 발신자/수신자 배송 정보와 상태 시각 엔티티.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftCategory.kt` — 관리자 카테고리 엔티티.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftSizePrice.kt` — 사이즈별 기본/판매 금액 설정 엔티티.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftReview.kt` — 선물 리뷰 엔티티.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftApplicationNoSequence.kt` — 날짜별 신청번호 채번 엔티티.
- Create: repository files under `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/`.
### 도메인 service
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftApplicationNoGenerator.kt` — `G{yyyyMMdd}{dailySequence6}` 발급과 동시성 재시도.
- Implemented in `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt` and `GiftCommandService.kt` — 사이즈 가격 조회와 신청 시점 최종 결제 금액 스냅샷.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt` — 신청, 운송장 등록, 취소, 배송지 입력, 수령확인, 리뷰 작성.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt` — 폼 옵션, 목록, 상세 조회.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminService.kt` — 관리자 카테고리/가격 설정과 운영 상태변경.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt` — 자동취소, 배송지 미입력 전달불가, 24시간 전 안내 작업.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftPushService.kt` — 상태별 팬/크리에이터 푸시 이벤트 발행.
- Reuse: `kr.co.vividnext.sodalive.common.SodaException` — 선물 도메인 오류는 기존 공통 예외를 사용한다.
### 사용자 API
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt` — 사용자 API endpoint.
- Create: user request DTO files under `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/` — `GiftApplicationRequest.kt`, `GiftTrackingRegistrationRequest.kt`, `GiftRecipientAddressRegistrationRequest.kt`, `GiftReviewRequest.kt`.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/GiftResponse.kt` — 사용자 API response DTO.
### 관리자 API
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftController.kt` — 관리자 카테고리/가격/운영 상태변경 endpoint.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftRequest.kt` — 관리자 request DTO.
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftResponse.kt` — 관리자 response DTO.
### 기존 파일 변경
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt` — `선물하기` 용도 추가.
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt` — 기존 `spendCan`/`refund` 재사용 가능 여부 확인 후 필요한 최소 연결 메서드 추가.
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt` — `GIFT_DETAIL` 딥링크와 선물 푸시 이벤트 타입 추가.
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationCategory.kt` — 선물 알림 카테고리 필요 시 `GIFT` 추가.
- Reuse: `FcmEvent.title`/`message` 기본 문구 — 선물 푸시는 별도 다국어 키 추가 없이 기존 FCM 기본 문구 경로를 사용한다.
### 테스트
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftApplicationNoGeneratorTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandServiceTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftPushServiceTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`
## Task TDD 작성 규칙
모든 구현 Task는 아래 순서를 지킨다.
- [ ] **RED:** 가장 작은 실패 test를 작성한다.
- [ ] **RED 확인:** focused test를 실행해 요구 동작이 없어서 발생한 의도한 assertion 실패를 확인한다.
- [ ] **GREEN:** RED를 통과시키는 최소 구현을 작성한다.
- [ ] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다.
- [ ] **REFACTOR:** 새 동작을 바꾸지 않는 범위에서 이번 Task가 만든 중복만 정리하고, focused test·직접 영향 회귀·lint를 다시 실행해 실제 결과를 Progress에 기록한다.
## Phase 1: 데이터 기반과 관리자 설정 API
**Phase 결과:** 선물 도메인 테이블, 상태/사이즈 enum, 신청번호 채번, 관리자 카테고리/사이즈 가격 설정, 팬용 폼 옵션 조회가 준비된다.
**선행조건:** 없음.
**Phase 완료 조건:** `P1-T1`~`P1-T4`와 `P1-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 1.1 DDL과 도메인 엔티티 생성
**Goal 실행 `P1-T1`:** PRD의 데이터 모델을 MySQL DDL과 JPA 엔티티로 만든다.
- **시작 조건:** PRD `10. 데이터 모델 요구사항` 확인.
- **완료 증거:** DDL 문서, 엔티티/repository 파일, 엔티티 매핑 focused test 통과.
- **범위 밖:** 사용자 API, 결제, 푸시, 스케줄러.
**Files:**
- Create: `docs/20260929_크리에이터_선물하기/gift-schema.sql`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/domain/GiftStatus.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/domain/GiftSize.kt`
- Create: entity and repository files under `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftEntityMappingTest.kt`
**Interfaces:**
- Produces: `GiftStatus`, `GiftSize`, `Gift`, `GiftDelivery`, `GiftCategory`, `GiftSizePrice`, `GiftReview`.
- [x] **RED:** 엔티티 저장/조회, `applicationNo` unique, `GiftReview.giftId` unique, 카테고리 `categoryCode` unique를 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftEntityMappingTest"`를 실행해 신규 엔티티 미존재 또는 매핑 미구현 실패를 확인한다.
- [x] **GREEN:** 엔티티, repository, DDL을 최소 구현한다. DDL의 모든 컬럼과 테이블에는 MySQL `COMMENT`를 추가하고 `created_at`, `updated_at`은 문서 규칙을 따른다.
- [x] **GREEN 확인:** 같은 focused test가 통과하는지 확인한다.
- [x] **REFACTOR:** 엔티티별 책임을 유지하고, `./gradlew ktlintCheck`와 focused test 결과를 Progress에 기록한다.
#### Task 1.2 신청번호 채번 구현
**Goal 실행 `P1-T2`:** `G{yyyyMMdd}{dailySequence6}` 신청번호를 동시 신청에서도 중복 없이 발급한다.
- **시작 조건:** `P1-T1` 완료.
- **완료 증거:** 채번 단위/동시성 test 통과.
- **범위 밖:** 선물 신청 command 전체.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftApplicationNoGenerator.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftApplicationNoSequenceRepository.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftApplicationNoGeneratorTest.kt`
**Interfaces:**
- Produces: `fun generate(now: LocalDateTime = LocalDateTime.now()): String`.
- [x] **RED:** 같은 날짜 연속 발급이 `G20260929000001`, `G20260929000002`가 되고 병렬 발급 결과가 모두 unique인 실패 test를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationNoGeneratorTest"`로 실패를 확인한다.
- [x] **GREEN:** DB unique 제약, 원자적 채번 또는 충돌 재시도 중 가장 단순한 방식으로 구현한다.
- [x] **GREEN 확인:** 같은 focused test 통과를 확인한다.
- [x] **REFACTOR:** 재시도 횟수와 실패 오류를 명확히 하고 focused test·`ktlintCheck` 결과를 기록한다.
#### Task 1.3 관리자 카테고리와 사이즈 가격 API 구현
**Goal 실행 `P1-T3`:** 관리자가 카테고리와 사이즈별 `basePriceCan`/`salePriceCan`을 조회·수정할 수 있게 한다.
- **시작 조건:** `P1-T1` 완료.
- **완료 증거:** 관리자 service/controller test 통과.
- **범위 밖:** 사용자 선물 신청, 폼 옵션 조회.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminService.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftController.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftRequest.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftResponse.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`
**Interfaces:**
- Produces: `GET/POST/PUT/DELETE /api/v2/admin/gift-categories`, `GET/PUT /api/v2/admin/gift-size-prices`.
- [x] **RED:** 카테고리 등록/수정/논리삭제, 비활성 전환, `salePriceCan > basePriceCan` 거부 test를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.AdminGiftControllerTest"`로 실패를 확인한다.
- [x] **GREEN:** 관리자 service와 controller를 최소 구현한다. 삭제는 `isActive=false`만 수행한다.
- [x] **GREEN 확인:** 같은 focused test 통과를 확인한다.
- [x] **REFACTOR:** 관리자 DTO와 도메인 모델 import 방향을 점검하고 `ktlintCheck` 결과를 기록한다.
#### Task 1.4 팬용 선물 신청 폼 옵션 조회 구현
**Goal 실행 `P1-T4`:** 팬이 선물 신청 페이지에서 활성 사이즈 가격과 활성 카테고리를 한 번에 조회한다.
- **시작 조건:** `P1-T1`, `P1-T3` 완료.
- **완료 증거:** `GET /api/v2/gifts/form-options` contract test 통과.
- **범위 밖:** 선물 신청 mutation.
**Files:**
- Implemented in: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/GiftResponse.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt`
**Interfaces:**
- Produces: `GET /api/v2/gifts/form-options` with `sizes`, `categories`.
- [x] **RED:** 비활성 카테고리 제외, `categoryCode` 미노출, `basePriceCan`/`salePriceCan` 응답을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"`로 실패를 확인한다.
- [x] **GREEN:** 폼 옵션 조회 service/controller/response를 최소 구현한다.
- [x] **GREEN 확인:** 같은 focused test 통과를 확인한다.
- [x] **REFACTOR:** page/DTO 책임 경계를 정리하고 focused test·`ktlintCheck` 결과를 기록한다.
### Phase 1 Gate
**Goal 실행 `P1-GATE`:** 데이터 기반과 관리자 설정/폼 옵션 계약을 최종 판정한다.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"
./gradlew ktlintCheck
./gradlew tasks --all
```
**Expected:** 모든 명령 exit 0. PRD `GIFT-001`, `GIFT-012`, `GIFT-013`이 구현 또는 명시적 후속 범위로 추적된다.
**결과:** 완료. 2026-09-29 기준 Gate 명령 3개 모두 `BUILD SUCCESSFUL`.
## Phase 2: 팬 선물 신청, 결제, 취소, 운송장 등록
**Phase 결과:** 팬이 선물을 신청해 캔을 결제하고, 운송장 등록 전 취소/환불하거나 운송장을 등록할 수 있다.
**선행조건:** `P1-GATE` 완료.
**Phase 완료 조건:** `P2-T1`~`P2-T4`와 `P2-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 2.1 캔 사용내역 `선물하기` 연결
**Goal 실행 `P2-T1`:** 선물 신청에서 사용할 캔 차감/환불 연결점을 만든다.
- **시작 조건:** `P1-GATE` 완료, 기존 `CanPaymentService`와 `CanUsage` 확인.
- **완료 증거:** 캔 차감/환불 focused test 통과.
- **범위 밖:** 선물 신청 API 전체.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`
**Interfaces:**
- Produces: 선물 신청에서 호출할 캔 차감/환불 contract. 기존 `spendCan`/`refund` 재사용을 우선한다.
- [x] **RED:** `CanUsage.GIFT` 또는 동등한 `선물하기` 사용내역이 생성되고 환불 시 1회만 환불되는 실패 test를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest"`로 실패를 확인한다.
- [x] **GREEN:** 기존 결제 service를 최대한 재사용해 최소 연결만 추가한다.
- [x] **GREEN 확인:** 같은 focused test 통과를 확인한다.
- [x] **REFACTOR:** 기존 결제 호출부 회귀를 확인하고 focused test·`./gradlew test --tests "kr.co.vividnext.sodalive.can.*"` 결과를 기록한다.
#### Task 2.2 선물 보내기 등록 구현
**Goal 실행 `P2-T2`:** 팬이 약관/파손면책/발신자 정보를 입력해 선물을 신청하고 캔을 즉시 차감한다.
- **시작 조건:** `P2-T1` 완료.
- **완료 증거:** service/controller 신청 test 통과.
- **범위 밖:** 운송장 등록, 목록/상세 조회.
**Files:**
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt`
- Reuse: `kr.co.vividnext.sodalive.common.SodaException`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/GiftApplicationRequest.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/GiftControllerTest.kt`
**Interfaces:**
- Produces: `POST /api/v2/gifts`.
- [x] **RED:** 필수 발신자 정보, 팬 약관 2개, 조건부 `damageWaiverAgreed`, 비활성 카테고리, 최종 결제 금액 스냅샷, 고유 `applicationNo`, 캔 차감을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"`로 실패를 확인한다.
- [x] **GREEN:** 신청 API와 service를 최소 구현한다. `trackingDeadlineAt`은 신청 시각 + 3일로 저장한다.
- [x] **GREEN 확인:** 같은 focused test 통과를 확인한다.
- [x] **REFACTOR:** validation 중복만 정리하고 focused test·`ktlintCheck` 결과를 기록한다.
#### Task 2.3 선물 보내기 취소 구현
**Goal 실행 `P2-T3`:** 팬이 운송장 등록 전 선물 신청을 취소하고 전액 환불받을 수 있게 한다.
- **시작 조건:** `P2-T2` 완료.
- **완료 증거:** 취소/환불 focused test 통과.
- **범위 밖:** 자동취소 스케줄러.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/GiftControllerTest.kt`
**Interfaces:**
- Produces: `POST /api/v2/gifts/{applicationNo}/cancel`.
- [x] **RED:** 보낸 팬만 취소 가능, `RECEIVED`에서만 가능, `TRACKING_REGISTERED` 이후 실패, 환불 1회만 발생하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test 명령으로 실패를 확인한다.
- [x] **GREEN:** 취소 상태 전이와 환불을 최소 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 상태 전이 helper가 필요하면 이 Task 범위 안에서만 추출하고 회귀 결과를 기록한다.
#### Task 2.4 운송장 등록 구현
**Goal 실행 `P2-T4`:** 팬이 택배사 한글 표시명과 운송장 번호를 등록해 상태를 `TRACKING_REGISTERED`로 바꾼다.
- **시작 조건:** `P2-T2` 완료.
- **완료 증거:** 운송장 등록 service/controller test 통과.
- **범위 밖:** 크리에이터 배송지 입력, 푸시 실제 발송 검증.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftController.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/GiftControllerTest.kt`
**Interfaces:**
- Produces: `POST /api/v2/gifts/{applicationNo}/tracking` with `courierCompanyName`, `trackingNumber`.
- [x] **RED:** 보낸 팬만 등록 가능, `RECEIVED`에서만 가능, `courierCompanyName` 필수/50자 이하, 크리에이터 배송지 입력 기한 생성, 상태 전이를 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test 명령으로 실패를 확인한다.
- [x] **GREEN:** 운송장 등록과 `recipientAddressDeadlineAt = 등록 시각 + 7일` 저장을 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 받은 선물 노출 조건과 충돌하지 않도록 상태 predicate 이름을 정리하고 회귀 결과를 기록한다.
### Phase 2 Gate
**Goal 실행 `P2-GATE`:** 팬 신청/결제/취소/운송장 등록 흐름을 판정한다.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"
./gradlew ktlintCheck
```
**Expected:** `GIFT-002`~`GIFT-005`의 핵심 mutation이 통과하고, 운송장 등록 후 받은 선물 조회 가능 상태가 저장된다.
## Phase 3: 선물함 조회와 크리에이터 배송지 입력
**Phase 결과:** 팬/크리에이터가 선물함 목록과 상세를 조회하고, 크리에이터가 배송지와 수령 약관을 등록할 수 있다.
**선행조건:** `P2-GATE` 완료.
### 구현 항목
#### Task 3.1 선물함 목록 조회 구현
**Goal 실행 `P3-T1`:** `type=ALL|SENT|RECEIVED` 목록과 `direction` 판정을 구현한다.
- **Files:** Modify `GiftQueryService.kt`, `GiftController.kt`, `GiftResponse.kt`; Test `GiftQueryServiceTest.kt`, `GiftControllerTest.kt`.
- [x] **RED:** 보낸 선물은 신청 직후 노출, 받은 선물은 `TRACKING_REGISTERED` 이상부터 노출, `direction`, pagination 보정을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"` 실패를 확인한다.
- [x] **GREEN:** 목록 query와 response를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** QueryDSL 조건을 중복 없이 정리하고 회귀 결과를 기록한다.
#### Task 3.2 선물 상세 조회 구현
**Goal 실행 `P3-T2`:** 단일 상세 API가 로그인 회원 기준으로 보낸/받은 선물을 판정한다.
- **Files:** Modify `GiftQueryService.kt`, `GiftController.kt`, `GiftResponse.kt`; Test `GiftQueryServiceTest.kt`, `GiftControllerTest.kt`.
- [x] **RED:** sender는 `SENT`, recipient는 노출 조건 만족 시 `RECEIVED`, 무관한 회원은 권한 오류 또는 미존재 오류를 반환하는 실패 test를 작성한다.
- [x] **RED:** `direction=SENT` 상세 응답이 `giftInfo.recipientCreatorNickname`, `giftInfo.sizeName`, `giftInfo.categoryName`, `giftInfo.applicationNo`, `giftInfo.paidCan`, `giftInfo.tracking`, `senderInfo.name`, `senderInfo.phoneNumber`, `senderInfo.address`, `mailbox`를 포함하는 실패 test를 작성한다.
- [x] **RED:** `direction=SENT` 상세 응답에서 `giftInfo.senderNickname`, `giftInfo.shippingRequestedAt`, `recipientAddress`가 `null`이고 크리에이터가 입력한 이름/휴대폰 번호/주소가 노출되지 않는 실패 test를 작성한다.
- [x] **RED:** `direction=SENT`이고 상태가 `RECEIVED`이면 `trackingRequired=true`, 그 외 상태이거나 `direction=RECEIVED`이면 `trackingRequired=false`인 실패 test를 작성한다.
- [x] **RED:** `direction=RECEIVED` 상세 응답이 `giftInfo.senderNickname`, `giftInfo.sizeName`, `giftInfo.categoryName`, `giftInfo.shippingRequestedAt`, `recipientAddress.name`, `recipientAddress.phoneNumber`, `recipientAddress.address`를 포함하는 실패 test를 작성한다.
- [x] **RED:** `direction=RECEIVED` 상세 응답에서 `giftInfo.recipientCreatorNickname`, `giftInfo.applicationNo`, `giftInfo.paidCan`, `giftInfo.tracking`, `senderInfo`, `mailbox`가 `null`이고 팬이 신청 시 등록한 이름/휴대폰 번호/주소가 노출되지 않는 실패 test를 작성한다.
- [x] **RED:** `direction=RECEIVED`이고 받는 주소가 미입력이며 종료 상태가 아니면 `recipientAddressRequired=true`, 주소 입력 완료 또는 종료 상태 또는 `direction=SENT`이면 `recipientAddressRequired=false`인 실패 test를 작성한다.
- [x] **RED:** 발신자/수신자 주소가 `(우편번호) 주소, 상세주소` 형식으로 조립되고, 로그인 회원이 알아야 하는 데이터가 아니거나 아직 입력되지 않은 데이터는 `null`인 실패 test를 작성한다.
- [x] **RED:** 상세 응답의 `statusTimeline`이 `RECEIVED`, `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED`, `DELIVERED` 순서로 내려오고 도달한 상태만 `occurredAt`을 가지며 미도달 상태는 `null`인 실패 test를 작성한다.
- [x] **RED:** `CANCELED`, `UNDELIVERABLE` 상세에서 정상 진행 `statusTimeline`은 유지되고 종료 정보는 각각 `delivery.canceledAt`, `delivery.undeliverableAt`, `delivery.undeliverableReason`으로 내려오는 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 상세 query와 권한 판정을 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 역할별 상세 응답 조립, 본인/상대방 개인정보 노출 경계, 상태 타임라인 조립 책임을 response mapper에서 명확히 하고 회귀 결과를 기록한다.
#### Task 3.3 크리에이터 배송지 입력 구현
**Goal 실행 `P3-T3`:** 크리에이터가 배송지와 수령 약관 2개를 등록할 수 있게 한다.
- **Files:** Modify `GiftCommandService.kt`, `GiftController.kt`, `GiftRecipientAddressRegistrationRequest.kt`; Test `GiftCommandServiceTest.kt`, `GiftControllerTest.kt`.
- [x] **RED:** 받는 크리에이터만 가능, 종료 상태 실패, 이름/휴대폰/우편번호/주소 필수, 수령 약관 2개 필수, 동의 시각 저장 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 배송지 입력 API를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 발신/수신 주소 snapshot 책임을 `GiftDelivery` 안으로 정리하고 회귀 결과를 기록한다.
#### Task 3.4 크리에이터 수령확인 구현
**Goal 실행 `P3-T4`:** 크리에이터가 `INSPECTION_COMPLETED` 선물을 `DELIVERED`로 완료 처리한다.
- **Files:** Modify `GiftCommandService.kt`, `GiftController.kt`; Test `GiftCommandServiceTest.kt`, `GiftControllerTest.kt`.
- [x] **RED:** 받는 크리에이터만 가능, `INSPECTION_COMPLETED`에서만 가능, `deliveredAt` 저장, 팬 푸시 발행 요청을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 수령확인 상태 전이를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 운영 전달완료와 공유 가능한 상태 전이 검증만 추출하고 회귀 결과를 기록한다.
### Phase 3 Gate
**Goal 실행 `P3-GATE`:** 조회와 크리에이터 배송지/수령 흐름을 판정한다.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"
./gradlew ktlintCheck
```
**Expected:** `GIFT-007`과 상세/목록 권한 정책이 통과한다.
## Phase 4: 자동 스케줄링
**Phase 결과:** 운송장 미등록과 배송지 미입력 기한 초과, 24시간 전 안내가 자동 처리된다.
**선행조건:** `P2-GATE`, `P3-GATE` 완료.
### 구현 항목
#### Task 4.1 운송장 미등록 자동취소 구현
**Goal 실행 `P4-T1`:** 신청 후 3일 초과 `RECEIVED` 선물을 `CANCELED`로 바꾸고 전액 환불한다.
- **Files:** Create `GiftScheduler.kt`; Modify `GiftRepository.kt`, `GiftCommandService.kt`; Test `GiftSchedulerTest.kt`.
- [x] **RED:** 기한 초과 대상만 취소, 환불 1회, 중복 실행 idempotent 실패 test를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftSchedulerTest"` 실패를 확인한다.
- [x] **GREEN:** 자동취소 job service를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** scheduler와 command service의 상태 전이 중복을 정리하고 회귀 결과를 기록한다.
#### Task 4.2 배송지 미입력 전달불가 구현
**Goal 실행 `P4-T2`:** 운송장 등록 후 7일 초과 배송지 미입력 선물을 `UNDELIVERABLE`로 바꾼다.
- **Files:** Modify `GiftScheduler.kt`, `GiftRepository.kt`; Test `GiftSchedulerTest.kt`.
- [x] **RED:** 배송지 미입력 기한 초과만 대상, 사유 `배송지 미입력 기한 초과`, 중복 실행 idempotent 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 전달불가 job service를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 대상 조회 query 이름과 상태 조건을 명확히 하고 회귀 결과를 기록한다.
#### Task 4.3 24시간 전 안내 작업 구현
**Goal 실행 `P4-T3`:** 운송장 등록과 배송지 입력 마감 24시간 전 안내 푸시 작업을 구현한다.
- **Files:** Modify `GiftScheduler.kt`, `GiftPushService.kt`; Test `GiftSchedulerTest.kt`, `GiftPushServiceTest.kt`.
- [x] **RED:** 마감 24시간 전 범위만 대상, 같은 선물 중복 안내 방지 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 안내 작업과 중복 방지 기준을 구현한다. 중복 방지는 별도 컬럼 또는 푸시 기록 조회 중 가장 단순한 방식을 사용한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 작업별 batch size와 로그에 개인정보가 남지 않는지 확인하고 회귀 결과를 기록한다.
### Phase 4 Gate
**Goal 실행 `P4-GATE`:** 자동 처리와 idempotency를 판정한다.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftSchedulerTest" --tests "kr.co.vividnext.sodalive.v2.gift.GiftPushServiceTest"
./gradlew ktlintCheck
```
**Expected:** `GIFT-006`, `GIFT-008` 자동 처리와 중복 실행 방지가 통과한다.
## Phase 5: 운영 상태변경 API
**Phase 결과:** 운영자가 사서함 도착, 검수완료, 전달불가, 전달완료를 상태별 전용 API로 처리한다.
**선행조건:** `P3-GATE` 완료.
### 구현 항목
#### Task 5.1 사서함 도착과 검수완료 API 구현
**Goal 실행 `P5-T1`:** 운영자가 `TRACKING_REGISTERED -> ARRIVED_AT_MAILBOX -> INSPECTION_COMPLETED` 전이를 처리한다.
- **Files:** Modify `GiftAdminService.kt`, `AdminGiftController.kt`; Test `GiftAdminServiceTest.kt`, `AdminGiftControllerTest.kt`.
- [x] **RED:** 허용 이전 상태 성공, 잘못된 이전 상태 실패, 상태 시각 저장, 팬/크리에이터 푸시 요청 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 두 운영 API를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 상태 전이 검증 함수 중복을 정리하고 회귀 결과를 기록한다.
#### Task 5.2 운영 전달불가 API 구현
**Goal 실행 `P5-T2`:** 운영자가 전달불가 사유를 등록하고 `UNDELIVERABLE`로 종료 처리한다.
- **Files:** Modify `GiftAdminService.kt`, `AdminGiftController.kt`, `AdminGiftMarkUndeliverableRequest.kt`; Test `GiftAdminServiceTest.kt`, `AdminGiftControllerTest.kt`.
- [x] **RED:** 허용 상태 성공, 사유 필수, 종료 상태 실패, 팬/크리에이터 전달불가 푸시 요청 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 전달불가 API를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 사유 길이와 개인정보 로그 금지를 확인하고 회귀 결과를 기록한다.
#### Task 5.3 운영 전달완료 API 구현
**Goal 실행 `P5-T3`:** 운영자가 `INSPECTION_COMPLETED` 선물을 `DELIVERED`로 완료 처리한다.
- **Files:** Modify `GiftAdminService.kt`, `AdminGiftController.kt`; Test `GiftAdminServiceTest.kt`, `AdminGiftControllerTest.kt`.
- [x] **RED:** `INSPECTION_COMPLETED`에서만 성공, 팬 전달완료 푸시, 운영 처리 시 크리에이터 배송완료 푸시 정책을 검증하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 운영 전달완료 API를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 크리에이터 직접 수령확인과 운영 전달완료의 푸시 차이를 명확히 하고 회귀 결과를 기록한다.
### Phase 5 Gate
**Goal 실행 `P5-GATE`:** 운영 상태변경 API를 판정한다.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.AdminGiftControllerTest"
./gradlew ktlintCheck
```
**Expected:** `GIFT-009`의 상태별 전용 API와 금지 상태 전이가 통과한다.
## Phase 6: 리뷰 작성
**Phase 결과:** 팬이 전달완료 선물에 별점, 여러 키워드, 추가의견 리뷰를 1회 작성한다.
**선행조건:** `P3-GATE` 또는 `P5-GATE`에서 `DELIVERED` 전이 구현 완료.
### 구현 항목
#### Task 6.1 리뷰 작성 service 구현
**Goal 실행 `P6-T1`:** 보낸 팬만 `DELIVERED` 선물에 리뷰를 1회 작성할 수 있게 한다.
- **Files:** Modify `GiftCommandService.kt`, `GiftReviewRepository.kt`; Test `GiftCommandServiceTest.kt`.
- [x] **RED:** 보낸 팬만 가능, `DELIVERED`에서만 가능, 중복 실패, 별점 1~5, `keywords` 각 255자, `comment` 255자 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 리뷰 작성 service를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** keyword 저장 방식을 현재 DB 타입 기준으로 단순화하고 회귀 결과를 기록한다.
#### Task 6.2 리뷰 작성 API 구현
**Goal 실행 `P6-T2`:** `POST /api/v2/gifts/{applicationNo}/review` 계약을 제공한다.
- **Files:** Modify `GiftController.kt`, `GiftReviewRequest.kt`, `GiftResponse.kt`; Test `GiftControllerTest.kt`.
- [x] **RED:** request/response JSON에서 `keywords` 배열을 사용하고 단수 `keyword`를 받지 않는 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 리뷰 API를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 상세 응답의 review 포함 여부를 PRD와 맞추고 회귀 결과를 기록한다.
### Phase 6 Gate
**Goal 실행 `P6-GATE`:** 리뷰 작성 요구사항을 판정한다.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"
./gradlew ktlintCheck
```
**Expected:** `GIFT-010`과 리뷰 API 계약이 통과한다.
## Phase 7: 푸시, 딥링크, 최종 회귀
**Phase 결과:** 상태별 팬/크리에이터 푸시와 `GIFT_DETAIL` 딥링크가 모든 상태 전이에서 일관되게 발행된다.
**선행조건:** `P2-GATE`~`P6-GATE` 완료.
### 구현 항목
#### Task 7.1 FCM 이벤트와 딥링크 확장
**Goal 실행 `P7-T1`:** `FcmEvent`에 선물 푸시와 `GIFT_DETAIL` 딥링크를 추가한다.
- **Files:** Modify `src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationCategory.kt`; Test `GiftPushServiceTest.kt`.
- [x] **RED:** `deepLinkValue=GIFT_DETAIL`, `deepLinkId=applicationNo`를 발행하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** FCM enum과 gift push helper를 최소 확장한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** 기존 FCM 이벤트 회귀를 실행하고 결과를 기록한다.
#### Task 7.2 상태별 팬/크리에이터 푸시 구현
**Goal 실행 `P7-T2`:** PRD `9. 푸시 알림 정책`의 수신자, 제목, 본문을 구현한다.
- **Files:** Modify `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftPushService.kt`; Test `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftPushServiceTest.kt`.
- [x] **RED:** 팬 대상 5종, 크리에이터 대상 6종, 크리에이터 직접 수령확인 시 크리에이터 푸시 없음, 줄바꿈 본문 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 상태별 푸시 이벤트 생성을 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
- [x] **REFACTOR:** message key 이름을 정리하고 한국어/영어/일본어 기본값 조회 결과를 기록한다.
#### Task 7.3 최종 통합 회귀
**Goal 실행 `P7-T3`:** 선물하기 전체 흐름과 공통 회귀를 검증한다.
- **Files:** Modify `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftPushServiceTest.kt`, `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` only if final verification finds missing coverage; otherwise update this document's Progress section only.
- **TDD 예외 사유:** 구현을 추가하지 않고 전체 완성 상태를 검증하는 Gate 성격의 Task다.
- **대체 검증 방법:** 아래 명령과 수동 흐름 대조표를 Progress에 기록한다.
- [x] 사용자 흐름 대조표를 작성한다: 신청 → 취소, 신청 → 운송장 → 배송지 → 운영 검수 → 수령확인 → 리뷰, 신청 → 자동취소, 운송장 → 배송지 미입력 전달불가.
- [x] focused test 전체를 실행한다.
- [x] 직접 영향 회귀와 ktlint를 실행한다.
- [x] 전체 `test` 실행 여부를 판단한다. 공통 결제/FCM/스케줄러 경계를 변경했으므로 최종 Gate에서는 전체 `test`를 실행한다.
### Phase 7 Gate
**Goal 실행 `P7-GATE`:** 선물하기 릴리스 후보를 최종 판정한다.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.*" --tests "kr.co.vividnext.sodalive.fcm.*"
./gradlew ktlintCheck
./gradlew test
```
**Expected:** 모든 명령 exit 0. PRD `GIFT-001`~`GIFT-013`이 구현·검증·문서 추적으로 연결된다.
## Phase 9: 카테고리 확장과 신청번호 정책 변경
**Phase 결과:** 카테고리는 분류번호, 내부 범용코드, 분류명, 접수코드, 대표품목, 파손면책 조건을 관리하고, 신청번호는 카테고리+날짜별 sequence로 발급된다.
**선행조건:** Phase 8 완료, 사용자 확정 요구사항 반영.
**Phase 완료 조건:** `P9-T1`~`P9-T3`와 `P9-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 9.1 문서와 DDL 계약 갱신
**Goal 실행 `P9-T1`:** PRD, 구현 계획, DDL에 확정된 카테고리 필드와 신청번호 정책을 반영한다.
- **Files:** Modify `prd.md`, `plan-task.md`, `gift-schema.sql`.
- [x] **RED:** 문서/DDL의 기존 `G{yyyyMMdd}{dailySequence6}`와 기존 카테고리 필드 계약을 확인한다.
- [x] **GREEN:** `classificationNumber`, `categoryCode`, `name`, `receiptCode`, `representativeItem`, `requiresDamageWaiver`, `isActive`와 새 신청번호 형식을 반영한다.
- [x] **검증:** 세 문서가 같은 제약과 응답 정책을 설명하는지 대조한다.
#### Task 9.2 카테고리 persistence/API 계약 변경
**Goal 실행 `P9-T2`:** 관리자 API는 새 카테고리 필드를 각각 관리하고, 사용자 폼 옵션은 `분류명/대표품목` 조합 name만 내려준다.
- **Files:** Modify `GiftCategory.kt`, `GiftAdminService.kt`, `AdminGiftRequest.kt`, `AdminGiftResponse.kt`, `GiftQueryService.kt`, `GiftResponse.kt` if needed.
- **Tests:** Modify `GiftEntityMappingTest.kt`, `GiftAdminServiceTest.kt`, `GiftQueryServiceTest.kt`, `AdminGiftControllerTest.kt`, `GiftControllerTest.kt`.
- [x] **RED:** 새 필드 저장/조회, 관리자 request/response 분리, 사용자 폼 옵션 name 조합, validation 실패 테스트를 작성하고 실패를 확인한다.
- [x] **GREEN:** 엔티티, DTO, service mapping, validation을 최소 구현한다.
- [x] **검증:** 카테고리 관련 focused test를 실행한다.
#### Task 9.3 신청번호 채번 정책 변경
**Goal 실행 `P9-T3`:** `receiptCode-classificationNumberYYMMDDSequenceNo` 형식과 카테고리+날짜별 sequence를 구현한다.
- **Files:** Modify `GiftApplicationNoSequence.kt`, `GiftApplicationNoSequenceRepository.kt`, `GiftApplicationNoGenerator.kt`, `GiftCommandService.kt`.
- **Tests:** Modify `GiftApplicationNoGeneratorTest.kt`, `GiftCommandServiceTest.kt`, `GiftEntityMappingTest.kt` if needed.
- [x] **RED:** 형식, 카테고리별 독립 sequence, 날짜별 독립 sequence, `9999` 초과, 병렬 unique 테스트를 작성하고 실패를 확인한다.
- [x] **GREEN:** 기존 transaction/locking 방식을 유지해 최소 구현한다.
- [x] **검증:** 신청번호 focused test와 직접 영향 테스트를 실행한다.
### Phase 9 Gate
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationNoGeneratorTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftEntityMappingTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"
./gradlew ktlintCheck
git diff --check
```
**Expected:** 모든 명령 exit 0. 기존 datetime UTC ISO 문자열 변경은 유지된다.
**결과:** 완료. 2026-09-30 기준 Phase 9 Gate 명령 3개 모두 성공.
## Phase 10: 관리자 선물함 목록과 상세 조회
**Phase 결과:** 관리자가 전체 선물 신청을 목록/상세로 조회하고, 현재 상태에서 가능한 운영 액션을 확인할 수 있다.
**선행조건:** Phase 9 완료, 사용자 확정 요구사항 반영.
**Phase 완료 조건:** `P10-T1`~`P10-T3`와 `P10-GATE` 완료, 검증 기록 누적.
### 구현 항목
#### Task 10.1 문서 계약 갱신
**Goal 실행 `P10-T1`:** PRD, 구현 계획, 클라이언트 요약에 관리자 선물함 목록/상세 API 계약을 추가한다.
- **Files:** Modify `prd.md`, `plan-task.md`, `client-api-summary.md`.
- [x] **RED:** 기존 관리자 API 문서에 전체 선물함 목록/상세 조회 계약이 없음을 확인한다.
- [x] **GREEN:** `GET /api/v2/admin/gifts`, `GET /api/v2/admin/gifts/{applicationNo}`와 `availableActions` 정책을 문서화한다.
- [x] **검증:** 세 문서가 같은 필터, 응답 필드, 개인정보 미입력 표현을 설명하는지 대조한다.
#### Task 10.2 관리자 선물함 query service 구현
**Goal 실행 `P10-T2`:** 관리자 선물함 목록/상세 조회 service와 repository query를 구현한다.
- **Files:** Create `GiftAdminQueryService.kt`; Modify `GiftRepository.kt`, `GiftDeliveryRepository.kt`.
- **Tests:** Create `GiftAdminQueryServiceTest.kt`.
- [x] **RED:** 필터, 페이징, 목록/상세 필드, 수취인 배송지 미입력 빈 문자열, 상태별 `availableActions` 테스트를 작성하고 실패를 확인한다.
- [x] **GREEN:** 조회 service와 repository query를 최소 구현한다.
- [x] **검증:** `GiftAdminQueryServiceTest`를 실행한다.
#### Task 10.3 관리자 선물함 HTTP API 구현
**Goal 실행 `P10-T3`:** 관리자 controller와 response DTO에 목록/상세 endpoint를 연결한다.
- **Files:** Modify `AdminGiftController.kt`, `AdminGiftResponse.kt`.
- **Tests:** Modify `AdminGiftControllerTest.kt`.
- [x] **RED:** MockMvc로 목록/상세 JSON 계약과 권한/오류를 테스트하고 실패를 확인한다.
- [x] **GREEN:** 두 GET endpoint와 response DTO를 최소 구현한다.
- [x] **검증:** `AdminGiftControllerTest`, `GiftAdminQueryServiceTest`를 실행한다.
### Phase 10 Gate
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.*"
./gradlew ktlintCheck
git diff --check
```
**결과:** 완료. 2026-09-30 기준 focused 테스트, `ktlintCheck`, `git diff --check` 성공.
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 1 | `P1-T1` | 없음 | 아니요 | DDL/엔티티 경계 재확인 |
| 2 | `P1-T2` | `P1-T1` | 아니요 | 신청번호 채번 방식 재검토 |
| 3 | `P1-T3` | `P1-T1` | `P1-T2`와 일부 병행 가능 | 관리자 권한 패턴 확인 |
| 4 | `P1-T4` | `P1-T3` | 아니요 | 폼 옵션 계약 재확인 |
| 5 | `P1-GATE` | Phase 1 Task 전체 | 아니요 | 실패 소유 Task 수정 |
| 6 | `P2-T1` | `P1-GATE` | 아니요 | 기존 캔 결제 패턴 재확인 |
| 7 | `P2-T2` | `P2-T1` | 아니요 | 신청 validation 범위 재확인 |
| 8 | `P2-T3` | `P2-T2` | `P2-T4`와 일부 병행 가능 | 환불 idempotency 보강 |
| 9 | `P2-T4` | `P2-T2` | `P2-T3`와 일부 병행 가능 | 받은 선물 노출 조건 재확인 |
| 10 | `P2-GATE` | Phase 2 Task 전체 | 아니요 | 실패 소유 Task 수정 |
| 11 | `P3-T1` | `P2-GATE` | 아니요 | Query 조건 재확인 |
| 12 | `P3-T2` | `P3-T1` | 아니요 | 권한 정책 재확인 |
| 13 | `P3-T3` | `P2-T4` | `P3-T1` 이후 병행 가능 | 수령 약관 저장 보강 |
| 14 | `P3-T4` | `P5-T1` 또는 검수완료 fixture | 아니요 | 상태 전이 선행 구현 확인 |
| 15 | `P3-GATE` | Phase 3 Task 전체 | 아니요 | 실패 소유 Task 수정 |
| 16 | `P4-T1` | `P2-GATE` | `P4-T2`와 병행 가능 | 스케줄러 대상 query 보강 |
| 17 | `P4-T2` | `P3-T3` | `P4-T1`과 병행 가능 | 전달불가 상태 정책 재확인 |
| 18 | `P4-T3` | `P7-T1` 선행 권장 | 아니요 | 푸시 중복 방지 방식 확정 |
| 19 | `P4-GATE` | Phase 4 Task 전체 | 아니요 | 실패 소유 Task 수정 |
| 20 | `P5-T1` | `P2-T4` | 아니요 | 운영 권한 패턴 확인 |
| 21 | `P5-T2` | `P5-T1` | 아니요 | 전달불가 사유 정책 재확인 |
| 22 | `P5-T3` | `P5-T1` | 아니요 | 전달완료 푸시 정책 재확인 |
| 23 | `P5-GATE` | Phase 5 Task 전체 | 아니요 | 실패 소유 Task 수정 |
| 24 | `P6-T1` | `DELIVERED` fixture 가능 | `P6-T2` 전에는 아니요 | 리뷰 저장 방식 보강 |
| 25 | `P6-T2` | `P6-T1` | 아니요 | API 계약 재확인 |
| 26 | `P6-GATE` | Phase 6 Task 전체 | 아니요 | 실패 소유 Task 수정 |
| 27 | `P7-T1` | FCM 패턴 확인 | 아니요 | 기존 FCM 딥링크 빌더 확인 |
| 28 | `P7-T2` | `P7-T1` | 아니요 | 메시지 키 누락 보강 |
| 29 | `P7-T3` | 모든 구현 Task | 아니요 | 회귀 실패 소유 Task 생성 |
| 30 | `P7-GATE` | Phase 7 Task 전체 | 아니요 | 릴리스 차단 기록 |
```text
P1-T1 → P1-T2/P1-T3 → P1-T4 → P1-GATE
P2-T1 → P2-T2 → P2-T3/P2-T4 → P2-GATE
P3-T1 → P3-T2 → P3-T3 → P3-T4 → P3-GATE
P4, P5, P6는 선행 상태 전이 완료 후 병행 가능
P7은 FCM 확장 후 전체 상태 전이를 연결하고 최종 회귀로 종료
```
## 변경 금지 항목
- PRD의 API path, request/response 필드, 상태 enum을 근거 없이 변경하지 않는다.
- 외부 택배사 코드, 배송조회 연동, 약관 본문 API를 추가하지 않는다.
- 카테고리 hard delete를 구현하지 않는다.
- 운송장 등록 후 팬 취소를 허용하지 않는다.
- `salePriceCan` 대신 `basePriceCan`으로 결제하지 않는다.
- `keyword` 단수 필드를 리뷰 API에 추가하지 않는다.
- `as any`, `@ts-ignore`, 타입 오류 우회, test skip/삭제로 Gate를 통과시키지 않는다.
- 개인정보 전체를 로그, 푸시 본문, 테스트 fixture에 불필요하게 남기지 않는다.
- 선물 상세 사용자 API에 상대방 이름, 휴대폰 번호, 우편번호, 주소, 상세주소를 노출하지 않는다. 팬에게는 본인이 신청 시 등록한 발신자 정보만, 크리에이터에게는 본인이 입력한 수신자 정보만 노출한다.
## 의사결정 및 중단 규칙
- PRD와 구현 중 발견한 기존 코드 제약이 충돌하면 PRD Decision Log와 이 문서를 먼저 갱신한다.
- 관리자 권한명처럼 코드 확인으로 확정 가능한 값은 구현 Task에서 확인하고 문서에 실제 값을 기록한다.
- 캔 결제/환불 또는 푸시 이벤트에서 기존 public contract 변경이 필요하면 작업을 멈추고 사용자 확인을 받는다.
- 같은 차단 사유가 3회 반복되고 독립 작업도 불가능하면 해당 Goal을 `blocked`로 기록한다.
- 완료 증거와 Progress 기록까지 충족한 뒤에만 Goal을 완료 처리한다.
## Progress
기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 차수별로 누적한다.
### 계획 작성 — 2026-09-29
- 상태: 완료
- 무엇을: PRD 기반 goal 실행형 구현 계획 문서를 작성했다.
- 왜: 코드 구현 전 PRD와 구현 계획/TASK 문서를 모두 준비해야 하는 저장소 규칙을 충족하기 위해서다.
- 어떻게:
- `docs/20260929_크리에이터_선물하기/prd.md` 확인
- `docs/sample/sample-plan-task.md` 확인
- 기존 관리자 API, 캔 결제, FCM, 스케줄러 테스트 패턴 확인
- 남은 항목: 구현 시작 전 사용자의 계획 검토/승인
- 다음 행동: 사용자 승인 후 `P1-T1`부터 단일 Goal로 실행
### P1-T1 DDL과 도메인 엔티티 생성 — 2026-09-29
- 상태: 완료
- 무엇을: 선물 상태/사이즈 enum, 선물·배송·카테고리·사이즈 가격·리뷰·신청번호 채번 JPA 엔티티와 repository, 운영 반영용 `gift-schema.sql`을 추가했다.
- 왜: PRD `10. 데이터 모델 요구사항`과 `P1-T1` 완료 증거를 충족하기 위해서다.
- 어떻게:
- RED test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftEntityMappingTest.kt` 작성
- RED 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftEntityMappingTest"` 실행 결과 `GiftRepository`, `Gift`, `GiftStatus`, `GiftSize` 등 미구현 참조로 `compileTestKotlin` 실패
- GREEN 구현: v2 관례에 맞춰 enum은 `domain`, JPA 엔티티/repository는 `adapter.out.persistence`에 배치
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftEntityMappingTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P1-T2` 신청번호 채번 구현
### P1-T2 신청번호 채번 구현 — 2026-09-29
- 상태: 완료
- 무엇을: `G{yyyyMMdd}{dailySequence6}` 형식의 선물 신청번호 generator를 추가했다.
- 왜: PRD `6.3 신청번호 정책`의 Asia/Seoul 날짜 기준, 날짜별 6자리 sequence, 병렬 unique 보장 요구를 충족하기 위해서다.
- 어떻게:
- RED test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftApplicationNoGeneratorTest.kt` 작성
- RED 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationNoGeneratorTest"` 실행 결과 `GiftApplicationNoGenerator` 미구현 참조로 `compileTestKotlin` 실패
- GREEN 구현: `GiftApplicationNoSequenceRepository.findBySequenceDateForUpdate`와 `TransactionTemplate`을 사용해 날짜별 sequence row를 갱신하고, 최초 row 생성 경합은 unique 제약 충돌 재시도로 처리
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationNoGeneratorTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P1-T3` 관리자 카테고리와 사이즈 가격 API 구현
### P1-T3 관리자 카테고리와 사이즈 가격 API 구현 — 2026-09-29
- 상태: 완료
- 무엇을: 관리자 카테고리 등록/수정/조회/논리삭제 API와 사이즈 가격 조회/수정 API를 추가했다.
- 왜: PRD `GIFT-012`, `GIFT-013`의 관리자 선물 카테고리와 사이즈별 가격 설정 요구를 충족하기 위해서다.
- 어떻게:
- RED test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt` 작성
- RED 확인: focused test 실행 결과 `GiftAdminService`, `GiftCategoryCommand`, `GiftSizePriceCommand` 미구현 참조로 `compileTestKotlin` 실패
- GREEN 구현: `GiftAdminService`, 관리자 request/response DTO, `AdminGiftController`를 추가하고 카테고리 삭제는 `isActive=false`로 처리
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P1-T4` 팬용 선물 신청 폼 옵션 조회 구현
### P1-T4 팬용 선물 신청 폼 옵션 조회 구현 — 2026-09-29
- 상태: 완료
- 무엇을: 팬용 `GET /api/v2/gifts/form-options` API와 조회 service/response DTO를 추가했다.
- 왜: PRD `GIFT-001`의 선물 신청 화면 옵션 조회 요구를 충족하기 위해서다.
- 어떻게:
- RED test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt` 작성
- RED 확인: focused test 실행 결과 `GiftQueryService` 미구현 참조로 실패 확인
- GREEN 구현: 활성 `GiftSizePrice`와 활성 `GiftCategory`만 조회하고, 사용자 응답에서는 `categoryCode`를 제외하도록 구현
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: Phase 1 Gate
- 다음 행동: `P1-GATE` 실행
### P1-GATE 데이터 기반과 관리자 설정/폼 옵션 계약 판정 — 2026-09-29
- 상태: 완료
- 무엇을: Phase 1의 도메인/관리자 API/폼 옵션 계약을 Gate 명령으로 검증했다.
- 왜: `P1-T1`~`P1-T4` 완료 후 Phase 2 진입 가능 여부를 판정하기 위해서다.
- 어떻게:
- `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P2-T1` 캔 사용내역 `선물하기` 연결
### P2-T1 캔 사용내역 `선물하기` 연결 — 2026-09-29
- 상태: 완료
- 무엇을: 선물 신청에서 사용할 `CanUsage.GIFT`, 선물용 캔 차감/환불 연결 메서드, 캔 사용내역 표시명을 추가했다.
- 왜: 선물 신청 시 캔 사용내역을 `선물하기`로 남기고 취소/실패 시 한 번만 환불할 수 있어야 하기 때문이다.
- 어떻게:
- RED test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt` 작성
- RED 확인: focused test 실행 결과 `CanUsage.GIFT`, `spendGiftCan`, `refundGiftCan` 미구현 참조로 실패 확인
- GREEN 구현: 기존 `CanPaymentService.spendCan`/환불 로직을 재사용해 `spendGiftCan`, `refundGiftCan`을 추가
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.can.*"`, `./gradlew ktlintCheck` 실행 결과 모두 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P2-T2` 선물 보내기 등록 구현
### P2-T2 선물 보내기 등록 구현 — 2026-09-29
- 상태: 완료
- 무엇을: `POST /api/v2/gifts` 선물 신청 API와 신청 service를 추가했다.
- 왜: 팬이 약관/파손면책/발신자 정보를 입력해 선물을 신청하고 즉시 캔을 차감해야 하기 때문이다.
- 어떻게:
- RED test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt` 작성
- RED 확인: focused test 실행 결과 `GiftCommandService`, request/result DTO 미구현 참조로 실패 확인
- GREEN 구현: 신청 검증, 신청번호 발급, `spendGiftCan` 차감, `Gift`/`GiftDelivery` 저장, 신청 응답 반환을 구현
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P2-T3` 선물 보내기 취소 구현
### P2-T3 선물 보내기 취소 구현 — 2026-09-29
- 상태: 완료
- 무엇을: `POST /api/v2/gifts/{applicationNo}/cancel` 취소 API와 취소 service를 추가했다.
- 왜: 팬이 운송장 등록 전 접수된 선물을 취소하고 전액 환불받을 수 있어야 하기 때문이다.
- 어떻게:
- RED test: `GiftCommandServiceTest`, `GiftControllerTest`에 발신자/상태 검증과 취소 응답 검증을 추가
- RED 확인: 취소 결과 타입과 service method 미구현으로 focused test compile 실패 확인
- GREEN 구현: 보낸 팬 + `RECEIVED` 상태만 `CANCELED`로 전이, `GiftDelivery.canceledAt` 저장, `refundGiftCan` 호출
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P2-T4` 운송장 등록 구현
### P2-T4 운송장 등록 구현 — 2026-09-29
- 상태: 완료
- 무엇을: `POST /api/v2/gifts/{applicationNo}/tracking` 운송장 등록 API와 service를 추가했다.
- 왜: 팬이 접수된 선물에 택배사명과 운송장 번호를 등록해 크리에이터가 받은 선물로 조회할 수 있는 상태로 전이해야 하기 때문이다.
- 어떻게:
- RED test: `GiftCommandServiceTest`, `GiftControllerTest`에 발신자/상태/택배사명/운송장번호 검증과 응답 검증을 추가
- RED 확인: 신규 command/result/request와 `registerTracking` 미구현으로 focused test compile 실패 확인
- GREEN 구현: 보낸 팬 + `RECEIVED` 상태만 `TRACKING_REGISTERED`로 전이, `trackingRegisteredAt`, `recipientAddressDeadlineAt = now + 7일`, 택배사명/운송장 번호 저장
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P3-T1` 선물함 목록 조회 구현
### P2-GATE 팬 신청/결제/취소/운송장 등록 판정 — 2026-09-29
- 상태: 완료
- 검증:
- `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 판정: `GIFT-002`~`GIFT-005` 핵심 mutation 통과, 운송장 등록 후 `TRACKING_REGISTERED` 상태와 받은 선물 노출 선행 상태 저장 확인
### P3-T1 선물함 목록 조회 구현 — 2026-09-29
- 상태: 완료
- 무엇을: `GET /api/v2/gifts?type=ALL|SENT|RECEIVED&page=&size=` 목록 조회 API와 query service를 추가했다.
- 왜: 팬/크리에이터가 보낸 선물과 받은 선물을 역할별 `direction`으로 구분해 조회해야 하기 때문이다.
- 어떻게:
- RED test: `GiftQueryServiceTest`, `GiftControllerTest`에 보낸 선물 즉시 노출, 받은 선물 `TRACKING_REGISTERED` 이상 노출, `ALL` direction, page/size 보정, controller 응답 검증 추가
- RED 확인: `GiftListType`, `getGifts` 미구현으로 focused test compile 실패 확인
- GREEN 구현: `GiftListType`, `GiftDirection`, repository page 조회, `GiftQueryService.getGifts`, `GiftListResponse`, `GET /api/v2/gifts` 추가
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P3-T2` 선물 상세 조회 구현
### P3-T2 선물 상세 조회 구현 — 2026-09-29
- 상태: 완료
- 무엇을: `GET /api/v2/gifts/{applicationNo}` 상세 조회 API와 role 기반 상세 query를 추가했다.
- 왜: 보낸 팬과 받는 크리에이터가 같은 선물을 보더라도 노출 가능한 개인정보와 액션 플래그가 달라야 하기 때문이다.
- 어떻게:
- RED test: `GiftQueryServiceTest`, `GiftControllerTest`에 SENT/RECEIVED 권한, 개인정보 경계, 주소 조립, `trackingRequired`, `recipientAddressRequired`, 상태 타임라인, 종료 정보 검증 추가
- RED 확인: `getGiftDetail` 미구현으로 focused test compile 실패 확인
- GREEN 구현: 상세 권한 판정, 발신/수신 응답 분기, 주소 포맷, 상태 타임라인, `canceledAt`/`undeliverableAt` 종료 정보 반환 구현
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P3-T3` 크리에이터 배송지 입력 구현
### P3-T3 크리에이터 배송지 입력 구현 — 2026-09-29
- 상태: 완료
- 무엇을: `POST /api/v2/gifts/{applicationNo}/recipient-address` 배송지 입력 API와 service를 추가했다.
- 왜: 크리에이터가 운송장 등록 이후 수령 주소와 수령 약관 동의를 등록해야 하기 때문이다.
- 어떻게:
- RED test: `GiftCommandServiceTest`, `GiftControllerTest`에 수령자 권한, `RECEIVED`/종료 상태 거부, 필수 입력, 약관 2개 동의, controller 응답 검증 추가
- RED 확인: 신규 command/request/result/service 미구현으로 focused test compile 실패 확인
- GREEN 구현: 수령자 + 등록 가능 상태만 배송지 저장, `Gift.recipientTermsAgreedAt`/`recipientPrivacyAgreedAt`에 동의 시각 저장, 응답 반환 구현
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P3-T4` 크리에이터 수령확인 구현
### P3-T4 크리에이터 수령확인 구현 — 2026-09-29
- 상태: 완료
- 무엇을: `POST /api/v2/gifts/{applicationNo}/delivery-complete` 수령확인 API와 service를 추가했다.
- 왜: 크리에이터가 검수 완료된 선물을 직접 전달 완료 처리할 수 있어야 하기 때문이다.
- 어떻게:
- RED test: `GiftCommandServiceTest`, `GiftControllerTest`에 수령자 권한, `INSPECTION_COMPLETED` 상태 제한, `deliveredAt` 저장, controller 응답 검증 추가
- RED 확인: 신규 결과 타입과 `confirmDelivery` 미구현으로 focused test compile 실패 확인
- GREEN 구현: 수령자 + `INSPECTION_COMPLETED` 상태만 `DELIVERED`로 전이, `GiftDelivery.deliveredAt` 저장, 응답 반환 구현
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P3-GATE` 조회와 크리에이터 배송지/수령 흐름 판정
### P3-GATE 조회와 크리에이터 배송지/수령 흐름 판정 — 2026-09-29
- 상태: 완료
- 검증:
- `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 판정: `GIFT-007`과 상세/목록 권한 정책, 크리에이터 배송지 입력/수령확인 흐름 통과
- 다음 행동: `P4-T1` 운송장 미등록 자동취소 구현
### P4-T1 운송장 미등록 자동취소 구현 — 2026-09-30
- 상태: 완료
- 무엇을: 운송장 등록 기한이 지난 `RECEIVED` 선물을 자동 `CANCELED` 처리하고 전액 환불하는 scheduler를 추가했다.
- 왜: 팬이 신청한 뒤 운송장을 등록하지 않은 선물은 기한 초과 시 자동 취소되어야 하기 때문이다.
- 어떻게:
- RED test: `GiftSchedulerTest`에 기한 초과 대상, 기한 동일 제외, 상태 조건, 재실행 idempotent와 환불 1회 검증 추가
- RED 확인: 신규 scheduler/service 미구현으로 focused test compile 실패 확인
- GREEN 구현: `GiftScheduler.cancelExpiredTrackingRegistrationGifts`, `GiftRepository.findReceivedGiftsWithTrackingDeadlineBefore` 추가
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftSchedulerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P4-T2` 배송지 미입력 전달불가 구현
### P4-T2 배송지 미입력 전달불가 구현 — 2026-09-30
- 상태: 완료
- 무엇을: 배송지 입력 기한이 지난 `TRACKING_REGISTERED` 선물을 자동 `UNDELIVERABLE` 처리하는 scheduler를 추가했다.
- 왜: 운송장 등록 후에도 크리에이터가 배송지를 입력하지 않으면 전달불가로 종료되어야 하기 때문이다.
- 어떻게:
- RED test: `GiftSchedulerTest`에 기한 초과 대상, 기한 동일 제외, 배송지 입력 완료 제외, 재실행 idempotent 검증 추가
- RED 확인: `markExpiredRecipientAddressGiftsUndeliverable` 미구현으로 focused test compile 실패 확인
- GREEN 구현: `GiftScheduler.markExpiredRecipientAddressGiftsUndeliverable`, `GiftRepository.findTrackingRegisteredGiftsWithRecipientAddressDeadlineBefore` 추가
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftSchedulerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P4-T3` 24시간 전 안내 작업 구현
### P4-T3 24시간 전 안내 작업 구현 — 2026-09-30
- 상태: 완료
- 무엇을: 운송장 등록 기한과 배송지 입력 기한 24시간 전 안내 작업을 추가했다.
- 왜: 팬과 크리에이터가 자동취소/전달불가 전에 남은 작업을 처리할 수 있어야 하기 때문이다.
- 어떻게:
- RED test: `GiftSchedulerTest`에 24시간 이내 대상만 안내, 24시간 초과 제외, 배송지 입력 완료 제외, 재실행 중복 방지 검증 추가
- RED 확인: `GiftPushService`, 안내 발송 시각 필드, `sendDeadlineReminderGifts` 미구현으로 focused test compile 실패 확인
- GREEN 구현: `GiftScheduler.sendDeadlineReminderGifts`, 안내 대상 repository query, `GiftDelivery` 안내 발송 시각 컬럼, `GiftPushService` 추가
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftSchedulerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `gift-schema.sql`에 안내 발송 시각 컬럼 반영, `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: `GIFT_DETAIL` applicationNo 딥링크 타입 확장은 계획상 `P7-T1`에서 처리
- 다음 행동: `P4-GATE` 자동 처리와 idempotency 판정
### P4-GATE 자동 처리와 idempotency 판정 — 2026-09-30
- 상태: 완료
- 검증:
- `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftSchedulerTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest"` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 판정: `GIFT-006`, `GIFT-008` 자동 처리와 24시간 전 안내 중복 방지 통과
- 다음 행동: `P5-T1` 사서함 도착과 검수완료 API 구현
### P5-T1 사서함 도착과 검수완료 API 구현 — 2026-09-30
- 상태: 완료
- 무엇을: 운영자용 사서함 도착/검수완료 상태 전이 API와 상태별 푸시 요청을 추가했다.
- 왜: 운영자가 `TRACKING_REGISTERED -> ARRIVED_AT_MAILBOX -> INSPECTION_COMPLETED` 흐름을 전용 API로 처리해야 하기 때문이다.
- 어떻게:
- RED test: `GiftAdminServiceTest`, `AdminGiftControllerTest`에 허용 상태, 금지 상태, 상태 시각 저장, 푸시 호출 검증 추가
- RED 확인: `arriveMailbox`, `completeInspection`, 운영 푸시 메서드 미구현으로 focused test compile 실패 확인
- GREEN 구현: `GiftAdminService.arriveMailbox`, `GiftAdminService.completeInspection`, 관리자 API 2개, 운영 상태 응답 DTO, `GiftPushService` 운영 푸시 3개 추가
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `GiftPushServiceTest`에 운영 푸시 이벤트 본문 검증 추가, `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest"` 실행 결과 `BUILD SUCCESSFUL`, `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P5-T2` 운영 전달불가 API 구현
### P5-T2 운영 전달불가 API 구현 — 2026-09-30
- 상태: 완료
- 무엇을: 운영자용 전달불가 상태 전이 API와 팬/크리에이터 전달불가 푸시 요청을 추가했다.
- 왜: 운영자가 배송/검수 흐름 중 전달 불가능한 선물을 사유와 함께 `UNDELIVERABLE`로 종료 처리해야 하기 때문이다.
- 어떻게:
- RED test: `GiftAdminServiceTest`, `AdminGiftControllerTest`, `GiftPushServiceTest`에 허용 상태, 사유 필수, 종료 상태 실패, 전달불가 푸시 호출/본문 검증 추가
- RED 확인: `markUndeliverable`, `sendUndeliverableToSender`, `sendUndeliverableToRecipient` 미구현으로 focused test compile 실패 확인
- GREEN 구현: `GiftAdminService.markUndeliverable`, `POST /api/v2/admin/gifts/{applicationNo}/mark-undeliverable`, `AdminGiftMarkUndeliverableRequest`, 전달불가 푸시 2개 추가
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `AdminGiftControllerTest` fixture cleanup과 import 순서 정리 후 같은 focused test 실행 결과 `BUILD SUCCESSFUL`, `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P5-T3` 운영 전달완료 API 구현
### P5-T3 운영 전달완료 API 구현 — 2026-09-30
- 상태: 완료
- 무엇을: 운영자용 전달완료 상태 전이 API와 팬 전달완료 푸시 요청을 추가했다.
- 왜: 운영자가 검수 완료된 선물을 `DELIVERED`로 종료 처리하고 팬에게 전달 완료를 알려야 하기 때문이다.
- 어떻게:
- RED test: `GiftAdminServiceTest`, `AdminGiftControllerTest`, `GiftPushServiceTest`에 `INSPECTION_COMPLETED` 허용, 금지 상태, `deliveredAt` 저장, 팬 전달완료 푸시와 크리에이터 푸시 미발송 검증 추가
- RED 확인: `completeDelivery`, `sendDeliveredToSender` 미구현으로 focused test compile 실패 확인
- GREEN 구현: `GiftAdminService.completeDelivery`, `POST /api/v2/admin/gifts/{applicationNo}/complete-delivery`, 팬 전달완료 푸시 추가
- GREEN 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: `P5-GATE`
- 다음 행동: `P5-GATE` 운영 상태변경 API 판정
### P5-GATE 운영 상태변경 API 판정 — 2026-09-30
- 상태: 완료
- 검증:
- `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`
- 판정: `GIFT-009` 운영 상태별 전용 API와 금지 상태 전이 통과
- 다음 행동: `P6-T1` 리뷰 작성 service 구현
### P6-T1 리뷰 작성 service 구현 — 2026-09-30
- 상태: 완료
- 무엇을: 보낸 팬이 `DELIVERED` 선물에 별점, 여러 키워드, 추가의견 리뷰를 1회 작성하는 service를 추가했다.
- 왜: PRD `GIFT-010`의 보낸 팬/전달완료/중복 방지/입력 길이 제한 요구를 충족하기 위해서다.
- 어떻게:
- RED test: `GiftCommandServiceTest`에 보낸 팬 성공, 수신자 실패, 비전달완료 실패, 중복 실패, 별점 1~5, keyword/comment 255자 제한 검증 추가
- RED 확인: `./gradlew test --rerun-tasks --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest"` 실행 결과 `GiftReviewCommand`, `GiftReviewResult`, `writeReview`, `reviewRepository` 미구현으로 `compileTestKotlin` 실패 확인
- GREEN 구현: `GiftCommandService.writeReview`, `GiftReviewCommand`, `GiftReviewResult`, `GiftReviewRepository` 주입을 추가하고 `GiftReview.keywords`에는 현재 DB `text` 타입 기준으로 comma-separated 문자열을 저장
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew ktlintCheck --rerun-tasks` 실행 결과 `BUILD SUCCESSFUL`, 같은 focused test 재실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P6-T2` 리뷰 작성 API 구현
### P6-T2 리뷰 작성 API 구현 — 2026-09-30
- 상태: 완료
- 무엇을: `POST /api/v2/gifts/{applicationNo}/review` 리뷰 작성 API와 request/response DTO를 추가했다.
- 왜: PRD `GIFT-010`의 리뷰 작성 API 계약에서 `keywords` 배열 요청/응답과 단수 `keyword` 미노출을 보장해야 하기 때문이다.
- 어떻게:
- RED test: `GiftControllerTest`에 리뷰 작성 성공 envelope, `keywords` 배열 응답, 단수 `keyword` 미노출 검증 추가
- RED 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"` 실행 결과 `GiftReviewRequest` 미구현으로 `compileTestKotlin` 실패 확인
- GREEN 구현: `GiftReviewRequest`, `GiftReviewResponse`, `GiftController.writeReview`를 추가하고 기존 `GiftCommandService.writeReview`에 연결
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest.shouldWriteReview"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest"`, `./gradlew ktlintCheck --rerun-tasks` 실행 결과 모두 `BUILD SUCCESSFUL`
- 남은 항목: `P6-GATE`
- 다음 행동: `P6-GATE` 리뷰 작성 요구사항 판정
### P6-GATE 리뷰 작성 요구사항 판정 — 2026-09-30
- 상태: 완료
- 검증:
- `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- 판정: `GIFT-010` 리뷰 작성 service와 `POST /api/v2/gifts/{applicationNo}/review` API 계약 통과
- 다음 행동: `P7-T1` FCM 이벤트와 딥링크 확장
### P7-T1 FCM 이벤트와 딥링크 확장 — 2026-09-30
- 상태: 완료
- 무엇을: 선물 푸시에 `GIFT` 카테고리와 `GIFT_DETAIL` 딥링크를 추가하고 `applicationNo`를 딥링크 ID로 발행하게 했다.
- 왜: PRD의 모든 선물 푸시가 단일 `GIFT_DETAIL` + `applicationNo` 딥링크로 상세 화면에 진입해야 하기 때문이다.
- 어떻게:
- RED test: `GiftPushServiceTest`에 선물 푸시 이벤트의 `deepLinkValue=GIFT_DETAIL`, `deepLinkId=applicationNo`, `category=GIFT` 검증 추가
- RED 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest"` 실행 결과 `PushNotificationCategory.GIFT`, `FcmDeepLinkValue.GIFT_DETAIL` 미구현으로 `compileTestKotlin` 실패 확인
- GREEN 구현: `FcmDeepLinkValue.GIFT_DETAIL`, `PushNotificationCategory.GIFT` 추가, `FcmEvent`/`FcmService`의 `deepLinkId`를 문자열 신청번호도 받을 수 있게 최소 확장, `GiftPushService`에서 신청번호 딥링크 발행
- GREEN 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest"` 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.fcm.*" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest"`, `./gradlew ktlintCheck` 실행 결과 모두 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P7-T2` 상태별 팬/크리에이터 푸시 구현
### DDL 날짜 컬럼 정책 변경 — 2026-09-30
- 상태: 완료
- 무엇을: `gift-schema.sql`의 날짜 컬럼 타입을 `DATETIME`에서 `TIMESTAMP`로 변경하고, 모든 `created_at`/`updated_at`을 `NOT NULL DEFAULT CURRENT_TIMESTAMP`로 통일했다.
- 왜: 운영 반영 DDL에서 날짜/시각 컬럼 정책을 `TIMESTAMP` 기반으로 맞추고 필수 audit 컬럼은 DB 기본값을 갖도록 하기 위해서다.
- 어떻게:
- `updated_at`에는 `ON UPDATE CURRENT_TIMESTAMP`를 적용
- 기한/상태 변경 시각 같은 비즈니스 날짜 컬럼은 앱에서 의미 있는 값을 넣어야 하므로 임의 `DEFAULT CURRENT_TIMESTAMP`는 추가하지 않음
- 확인: `rg -n "DATETIME|created_at DATETIME|updated_at DATETIME|created_at TIMESTAMP NULL|updated_at TIMESTAMP NULL" "docs/20260929_크리에이터_선물하기/gift-schema.sql"` 실행 결과 잔여 항목 없음
- 남은 항목: 없음
- 다음 행동: `P7-T2` 상태별 팬/크리에이터 푸시 구현
### P7-T2 상태별 팬/크리에이터 푸시 구현 — 2026-09-30
- 상태: 완료
- 무엇을: PRD 푸시 정책의 누락된 자동취소, 운송장 등록 완료, 크리에이터 배송완료 푸시를 추가하고 직접 수령확인 시 크리에이터 푸시가 없는 동작을 고정했다.
- 왜: 선물 상태 전이가 끝까지 진행될 때 팬/크리에이터가 각자 해야 할 작업과 결과를 푸시로 받아야 하기 때문이다.
- 어떻게:
- RED test: `GiftPushServiceTest`, `GiftCommandServiceTest`, `GiftSchedulerTest`, `GiftAdminServiceTest`에 누락 푸시 3종과 직접 수령확인 푸시 미발송 검증 추가
- RED 확인: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftPushServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftSchedulerTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest"` 실행 결과 신규 푸시 메서드와 `GiftCommandService.giftPushService` 미구현으로 `compileTestKotlin` 실패 확인
- GREEN 구현: `GiftPushService`에 `sendAutoCanceledToSender`, `sendTrackingRegisteredToRecipient`, `sendDeliveredToRecipient` 추가, 스케줄러/사용자 운송장 등록/운영 전달완료 호출부 연결
- GREEN 확인: 같은 focused test 실행 결과 `BUILD SUCCESSFUL`
- REFACTOR 확인: 푸시 본문은 현재 구현 패턴대로 이벤트의 기본 `title`/`message`를 사용하므로 message key 추가는 하지 않음. `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.gift.*"`, `./gradlew ktlintCheck` 실행 결과 모두 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P7-T3` 최종 통합 회귀
### P7-T3 최종 통합 회귀 — 2026-09-30
- 상태: 완료
- 사용자 흐름 대조표:
- 신청 → 취소: `registerGift`, `cancelGift`, 캔 환불 회귀 통과
- 신청 → 운송장 → 배송지 → 운영 검수 → 수령확인 → 리뷰: 사용자/관리자 command, controller, query 회귀 통과
- 신청 → 자동취소: 스케줄러 자동취소, 환불, 팬 자동취소 푸시 회귀 통과
- 운송장 → 배송지 미입력 전달불가: 스케줄러 전달불가, idempotency 회귀 통과
- 검증:
- focused/direct: `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.gift.*"` 실행 결과 `BUILD SUCCESSFUL`
- lint: `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- full: `./gradlew test` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
- 다음 행동: `P7-GATE` 릴리스 후보 판정
### P7-GATE 선물하기 릴리스 후보 판정 — 2026-09-30
- 상태: 완료
- 검증:
- `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.*" --tests "kr.co.vividnext.sodalive.v2.api.gift.*"` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- `./gradlew test` 실행 결과 `BUILD SUCCESSFUL`
- 판정: PRD `GIFT-001`~`GIFT-013` 구현·검증·문서 추적 완료
- 남은 항목: 없음
### DDL FK와 선물 금액 스냅샷 정리 — 2026-09-30
- 상태: 완료
- 무엇을: 운영 반영용 `gift-schema.sql`에 FK를 추가하고, 선물 신청 건에서는 최종 결제 금액 스냅샷만 저장하도록 `gift.base_price_can`/`Gift.basePriceCan`을 제거했다.
- 왜: 기존 운영 관례가 FK 없음이 아니고, 신규 선물 도메인은 운영 반영 DDL에서 참조 무결성을 DB로 보장하는 편이 안전하기 때문이다. 신청 건에는 환불/상세/내역에 필요한 최종 결제 금액만 있으면 충분하다.
- 어떻게:
- RED test: `Gift` 생성 fixture와 저장 검증에서 `basePriceCan` 제거 후 production `Gift` 생성자가 아직 요구해 `compileTestKotlin` 실패 확인
- GREEN 구현: `Gift` 엔티티와 `GiftCommandService`에서 기본 금액 스냅샷 제거, `gift-schema.sql`의 `gift.base_price_can` 제거
- DDL 구현: `gift -> member/use_can/gift_category`, `gift_delivery -> gift`, `gift_review -> gift/member` FK 추가. `gift_category`는 FK 참조 전에 생성되도록 DDL 순서 조정
- 검증: focused gift/API/scheduler/admin test 실행 결과 `BUILD SUCCESSFUL`, `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`
- 남은 항목: 없음
## Phase 8: 선물 응답 시각 UTC ISO 문자열화
**Phase 결과:** 선물 사용자/관리자 API가 모든 응답 시각을 클라이언트가 안전하게 해석할 수 있는 UTC ISO 문자열로 반환한다.
**선행조건:** `P7-GATE` 완료와 PRD `DEC-013` 확정.
**Phase 완료 조건:** `P8-T1` 완료와 focused/controller 회귀, gift 회귀, lint 검증 기록 누적.
### 구현 항목
#### Task 8.1 선물 response datetime 계약 수정
**Goal 실행 `P8-T1`:** `GiftResponse.kt`와 `AdminGiftResponse.kt`의 응답 시각 필드를 UTC ISO `String`으로 변환한다.
- **시작 조건:** PRD `DEC-013` 확인.
- **완료 증거:** RED/GREEN/REFACTOR 체크박스, focused controller test, gift 회귀 test, `ktlintCheck` 통과.
- **범위 밖:** 전역 Jackson 설정 변경, 선물 외 API datetime 마이그레이션, DB 저장 timezone 변경.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/gift/dto/GiftResponse.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/dto/AdminGiftResponse.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`
**Interfaces:**
- Produces: 선물 사용자/관리자 API의 모든 populated datetime JSON 값은 `2026-09-30T12:00:00Z` 같은 UTC ISO 문자열이다.
- [x] **RED:** controller test의 선물 날짜/시간 응답 기대값을 `Z` 포함 UTC ISO 문자열로 변경한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"`를 실행해 기존 `LocalDateTime` 직렬화가 `Z`를 포함하지 않아 실패함을 확인한다.
- [x] **GREEN:** response DTO의 `LocalDateTime` 필드를 `String`, `LocalDateTime?` 필드를 `String?`로 바꾸고 기존 `LocalDateTime.toUtcIso()` extension으로 변환한다.
- [x] **GREEN 확인:** 같은 focused controller test가 통과하는지 확인한다.
- [x] **REFACTOR:** 불필요한 import를 제거하고 `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*"`, `./gradlew ktlintCheck`, `git diff --check` 결과를 Progress에 기록한다.
### P8-T1 선물 response datetime 계약 수정 — 2026-09-30
- 상태: 완료
- 무엇을: 선물 사용자/관리자 response DTO의 날짜/시간 필드를 `String`으로 바꾸고 `toUtcIso()`로 변환했다. 클라이언트 요약 문서도 UTC ISO 문자열 계약으로 동기화했다.
- 왜: Jackson 기본 `LocalDateTime` 직렬화는 `2026-09-30T12:00:00`처럼 offset이 없어 클라이언트가 UTC로 안전하게 해석할 수 없기 때문이다.
- 어떻게:
- RED test: controller test 기대값을 `Z` 포함 문자열로 변경 후 focused test 실행 결과 11개 assertion 실패 확인.
- GREEN 구현: `GiftResponse.kt`, `AdminGiftResponse.kt`에서 응답 시각 필드를 `String`/`String?`로 변경하고 기존 `LocalDateTime.toUtcIso()` extension 재사용.
- 검증: focused controller test 실행 결과 `BUILD SUCCESSFUL`, `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.*"` 실행 결과 `BUILD SUCCESSFUL`, `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`, `git diff --check` 출력 없음, `rg -n -- "LocalDateTime"` 대상 response DTO/클라이언트 요약 문서 출력 없음, `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`.
- 남은 항목: 없음
### Phase 9 카테고리 확장과 신청번호 정책 변경 — 2026-09-30
- 상태: 완료
- 무엇을: 카테고리에 분류번호, 접수코드, 대표품목을 추가하고 사용자 폼 옵션의 카테고리명은 `분류명/대표품목`으로 조합했다. 신청번호는 `receiptCode-classificationNumberYYMMDDSequenceNo` 형식으로 변경하고 sequence는 카테고리+날짜별로 분리했다.
- 왜: `categoryCode`는 내부 범용코드로 유지하고, 운영용 분류번호/접수코드/대표품목을 별도 관리해야 하기 때문이다.
- 어떻게:
- RED 확인 1: 카테고리 추가 필드 테스트 작성 후 `compileTestKotlin`에서 `classificationNumber`, `receiptCode`, `representativeItem` 미구현 실패 확인
- GREEN 확인 1: 카테고리 focused suite 실행 결과 `BUILD SUCCESSFUL`
- RED 확인 2: 신청번호 generator 테스트 작성 후 기존 `generate(now)`와 sequence entity가 새 시그니처/필드를 지원하지 않아 `compileTestKotlin` 실패 확인
- GREEN 확인 2: 신청번호 focused suite 실행 결과 `BUILD SUCCESSFUL`
- Gate 확인: Phase 9 focused suite, `./gradlew ktlintCheck`, `git diff --check` 모두 성공
- 남은 항목: 없음
## Phase 11: 선물 받을 주소 설정
**Phase 결과:** 관리자는 전역 단일 선물 받을 주소를 등록/수정하고, 팬은 보낸 선물의 전달 완료 전 정상 진행 상세에서 해당 주소를 확인할 수 있다.
**선행조건:** `P3-GATE`, `P10-GATE` 완료.
### 구현 항목
#### Task 11.1 받을 주소 관리자 설정 API 구현
**Goal 실행 `P11-T1`:** 전역 단일 받을 주소 조회/저장 API를 구현한다.
- **Files:** Create `GiftMailbox.kt`, `GiftMailboxRepository.kt`; Modify `GiftAdminService.kt`, `AdminGiftController.kt`, `AdminGiftRequest.kt`, `AdminGiftResponse.kt`, `gift-schema.sql`; Test `GiftAdminServiceTest.kt`, `AdminGiftControllerTest.kt`.
- [x] **RED:** 설정 미등록 시 `GET /api/v2/admin/gift-mailbox`가 `data=null`을 반환하는 실패 test를 작성한다.
- [x] **RED:** `PUT /api/v2/admin/gift-mailbox`가 전역 단일 주소를 생성하고, 재호출 시 같은 row를 갱신하는 실패 test를 작성한다.
- [x] **RED:** 이름/연락처/우편번호/주소 공백 입력을 거부하는 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** 단일 row persistence, 관리자 service, request/response DTO, controller endpoint를 구현한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
#### Task 11.2 선물 상세 받을 주소 노출 구현
**Goal 실행 `P11-T2`:** 보낸 선물의 전달 완료 전 정상 진행 상세에만 받을 주소를 노출한다.
- **Files:** Modify `GiftQueryService.kt`; Test `GiftQueryServiceTest.kt`, `GiftControllerTest.kt`.
- [x] **RED:** `direction=SENT`이고 `status`가 `RECEIVED`, `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED`인 상세에서 `mailbox`를 반환하는 실패 test를 작성한다.
- [x] **RED:** 받을 주소 미등록, 종료 상태(`DELIVERED`, `UNDELIVERABLE`, `CANCELED`), 또는 다른 방향이면 `mailbox=null`인 실패 test를 작성한다.
- [x] **RED 확인:** focused test 실패를 확인한다.
- [x] **GREEN:** `GiftQueryService`에서 받을 주소 repository를 조회해 기존 `GiftMailboxResult`로 매핑한다.
- [x] **GREEN 확인:** focused test 통과를 확인한다.
### Phase 11 Gate
**Goal 실행 `P11-GATE`:** 관리자 설정과 사용자 상세 노출을 판정한다.
```bash
./gradlew test --tests "*GiftAdminServiceTest" --tests "*AdminGiftControllerTest" --tests "*GiftQueryServiceTest" --tests "*GiftControllerTest"
./gradlew ktlintCheck
```
**Expected:** 받을 주소 미등록/등록/수정, 상세 `mailbox` 노출 조건, 기존 선물 상세 권한 정책이 통과한다.
### P11-T2 선물 상세 받을 주소 노출 조건 확장 — 2026-10-02
- 상태: 완료
- 무엇을: 보낸 선물 상세의 `mailbox` 노출 조건을 `RECEIVED` 단일 상태에서 정상 진행 상태 중 `DELIVERED` 전(`RECEIVED`, `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED`)으로 확장했다.
- 왜: 팬이 운송장을 등록한 뒤에도 전달 완료 전까지 관리자 전역 선물 받을 주소를 상세에서 확인할 수 있어야 하기 때문이다.
- 어떻게:
- RED 확인: `GiftQueryServiceTest`, `GiftControllerTest`에 정상 진행 상태/종료 상태/받은 선물 방향 검증을 추가하고 기존 구현에서 focused test 실패를 확인했다.
- GREEN 구현: `GiftQueryService.mailboxFor` 조건을 `direction=SENT`이고 종료 상태가 아닌 경우로 단순화했다.
- 검증: `./gradlew test --tests '*GiftQueryServiceTest' --tests '*GiftControllerTest'`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.gift.*' --tests 'kr.co.vividnext.sodalive.v2.api.gift.*'`, `./gradlew ktlintCheck`, `git diff --check` 실행 결과 통과.
- 남은 항목: 없음
## Phase 12: 수취인 정보 전달 완료 불변조건 보강
**Phase 결과:** 수취인 필수 정보가 없는 선물은 전달 완료할 수 없고, 배송지 입력 기한이 지난 진행 중 선물은 관리자 진행 상태와 무관하게 전달 불가로 종료된다.
**선행조건:** `P4-T2`, `P5-T3` 완료.
### Task 12.1 전달 완료 수취인 정보 필수화
**Goal 실행 `P12-T1`:** `COMPLETE_DELIVERY`는 `INSPECTION_COMPLETED` 상태이며 수취인 이름, 휴대폰 번호, 우편번호, 주소가 모두 입력된 경우에만 허용한다.
- **Files:** Modify `GiftDelivery.kt`, `GiftAdminService.kt`, `GiftAdminQueryService.kt`; Test `GiftAdminServiceTest.kt`, `GiftAdminQueryServiceTest.kt`, `AdminGiftControllerTest.kt`.
- [x] **RED:** 수취인 필수 정보가 없는 `INSPECTION_COMPLETED` 선물의 전달 완료 요청이 거부되고 상태/전달 완료 시각/푸시가 바뀌지 않는 실패 test를 작성한다.
- [x] **RED:** 수취인 필수 정보가 불완전하면 관리자 목록/상세의 `availableActions`에서 `COMPLETE_DELIVERY`가 제외되는 실패 test를 작성한다.
- [x] **RED 확인:** focused test가 기존 구현에서 의도한 assertion으로 실패하는지 확인한다.
- [x] **GREEN:** 수취인 정보 완비 조건을 공통화하고 command와 관리자 action 계산에 적용한다.
- [x] **GREEN 확인:** 같은 focused test 통과를 확인한다.
### Task 12.2 배송지 미입력 기한 초과 상태 범위 보강
**Goal 실행 `P12-T2`:** 배송지 입력 기한이 지난 주소 미입력 선물을 `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED`에서 `UNDELIVERABLE`로 종료한다.
- **Files:** Modify `GiftRepository.kt`; Test `GiftSchedulerTest.kt`.
- [x] **RED:** 세 상태 모두 기한 초과 + `recipientAddress=null`이면 전달 불가가 되는 실패 test를 작성한다.
- [x] **RED:** 기한 동일/미도래, 주소 입력 완료, 대상 외 상태는 변경되지 않는 회귀 test를 유지한다.
- [x] **RED 확인:** `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED` case가 기존 query에서 실패하는지 확인한다.
- [x] **GREEN:** 기존 만료 조회 query의 상태 조건만 세 상태로 확장한다.
- [x] **GREEN 확인:** scheduler focused test 통과를 확인한다.
### Phase 12 Gate
- [x] `GiftAdminServiceTest`, `GiftAdminQueryServiceTest`, `AdminGiftControllerTest`, `GiftSchedulerTest`의 변경 관련 test 통과.
- [x] 전체 `./gradlew --no-daemon test` 통과(8분 33초), 종료 후 embedded Redis 및 Gradle test worker 잔존 없음.
- [x] `ktlintCheck`, `build -x test`, `git diff --check` 통과.
- [x] `AdminGiftControllerTest`에서 주소 미입력 실패와 주소 입력 완료 성공 HTTP 계약 확인.
**구현 결과:** 수취인 필수 정보 완비 조건을 `GiftDelivery.hasCompleteRecipientInformation()`으로 공통화해 전달 완료 command와 관리자 action 계산에 적용했다. 배송지 입력 기한 초과 조회는 주소 입력 가능 상태 3개로 확장했다.
### Task 12.3 Embedded Redis 종료 누수 수정
- [x] `com.github.codemonstur:embedded-redis:1.4.3`의 `RedisInstance.stop()`과 shutdown hook 등록 동작을 확인했다.
- [x] initializer의 중복 JVM shutdown hook을 제거했다.
- [x] 공식 `onShutdownForceStop(true)` 옵션으로 native Redis 프로세스 종료가 무기한 `waitFor()`에 걸리지 않도록 변경했다.
- [x] 후속 요청에 따라 전체 `./gradlew --no-daemon test`를 실행해 정상 종료를 확인했다.
## Phase 13: 선물함 리스트 표시 보강
**Phase 결과:** 선물함 리스트 item이 배송지 입력 여부, 배송지 입력 기한, 취소일을 내려줘 iOS가 리스트에서 배송지 CTA와 취소일을 표시할 수 있다.
**선행조건:** Phase 12 완료, 사용자 확정 요구사항 반영.
### Task 13.1 문서 계약 갱신
**Goal 실행 `P13-T1`:** PRD, 클라이언트 요약, 모바일 프롬프트에 리스트 item 추가 필드 3개를 반영한다.
- **Files:** Modify `prd.md`, `client-api-summary.md`, `gift-list-page-prompt.md`, `ios-gift-feature-implementation-prompt.md`, `plan-task.md`.
- [x] **RED:** 기존 리스트 응답 계약에 `recipientAddressRegistered`, `canceledAt`이 없고 배송지 deadline 규칙이 불명확함을 확인한다.
- [x] **GREEN:** `recipientAddressRegistered`, `recipientAddressDeadlineAt`, `canceledAt`을 문서화한다. 배송지 입력 완료 후 `recipientAddressDeadlineAt=null`, 취소일은 `createdAt` 대입이 아니라 `canceledAt` 별도 필드로 내려주는 정책을 명시한다.
### Task 13.2 리스트 응답 필드 구현
**Goal 실행 `P13-T2`:** 사용자 선물함 리스트 응답에 추가 필드 3개를 내려준다.
- **Files:** Modify `GiftQueryService.kt`, `GiftResponse.kt`.
- **Tests:** Modify `GiftQueryServiceTest.kt`, `GiftControllerTest.kt`.
- [x] **RED:** 배송지 미입력/입력 완료/취소 상태의 리스트 응답 필드 실패 테스트를 작성하고 실패를 확인했다.
- [x] **GREEN:** 기존 `GiftDelivery` 정보를 재사용해 `recipientAddressRegistered`, `recipientAddressDeadlineAt`, `canceledAt`을 매핑했다.
- [x] **검증:** focused 테스트와 `ktlintCheck`, `git diff --check`를 실행했다.
### Phase 14 문서 초안 — 2026-10-22
- 상태: 문서 초안 작성 완료, 최종 검증 대기
- 무엇을: PRD, 구현 계획, 클라이언트 요약에 ADMIN 선물 취소 계약을 추가하고 기존 화면에 취소 기능만 추가하는 프롬프트를 별도 문서로 분리한다. 전체 화면 구현용 `admin-page-prompts.md`는 보존한다.
- 왜: 승인된 `RECEIVED` 전용 취소 범위를 구현과 분리해 먼저 문서화하기 위해서다.
- 어떻게:
- 문서 변경만 수행했다. 코드/테스트/설정/DDL은 수정하지 않았다.
- 초안 작성 시점에는 문서 검증을 완료하지 않았다. 최종 검증 결과는 후속 기록에 누적한다.
- 남은 항목: 최종 문서 검증, 사용자 구현 승인, 이후 구현 및 검증
- 다음 행동: 문서 검증 후 P14-T1 체크를 갱신하고, 구현 승인은 별도로 받는다.
## Phase 14: 관리자 선물 취소
**Phase 결과:** 관리자가 `RECEIVED` 선물만 취소하고 발신자에게 전액 리워드캔을 환불하며, 목록/상세에서 `CANCEL`을 선택할 수 있다. 서버 구현과 관련 검증을 완료했다.
**선행조건:** Phase 13 완료, PRD `DEC-015` 및 `GIFT-016`~`GIFT-017` 확정.
**2026-10-22 실행 승인:** 사용자의 "문서대로 진행" 요청으로 `P14-T2`부터 재개한다. 현재 서버 저장소에서 취소 API·환불·액션 응답을 구현하며 별도 프론트엔드 화면 구현은 포함하지 않는다.
**Phase 완료 조건:** `P14-T1`~`P14-T3` 및 `P14-GATE` 완료. 전체 테스트는 후속 사용자 요청으로 제외하고 관련 회귀·실제 HTTP·리뷰·테스트 제외 빌드로 검증했다.
### 문서 작업
#### Task 14.1 관리자 취소 계약 문서화
**Goal 실행 `P14-T1`:** 승인된 관리자 취소 범위와 계획 상태를 PRD, 계획, 클라이언트 요약, 별도 기능 추가 프롬프트에 일치시킨다.
- **시작 조건:** 관리자 취소가 `RECEIVED` 전용이며 목록/상세 액션으로 제공된다는 사용자 승인.
- **완료 증거:** 네 문서의 endpoint, 액션, 상태/환불/응답 계약 일치, 기존 `admin-page-prompts.md` 무변경 및 문서 검증 기록.
- **범위 밖:** 서버/프론트엔드 구현, 테스트 실행, mock/fallback, 새 오류/푸시/스키마.
- **TDD 예외 사유:** 문서만 수정하며 production 동작을 변경하지 않는다.
- **대체 검증 방법:** 네 문서 간 계약 대조, `./gradlew tasks --all`, `git diff --check`, 변경 파일 범위 확인을 수행하고 결과를 누적한다.
**Files:**
- Modify: `docs/20260929_크리에이터_선물하기/prd.md`, `docs/20260929_크리에이터_선물하기/plan-task.md`, `docs/20260929_크리에이터_선물하기/client-api-summary.md`.
- Create: `docs/20260929_크리에이터_선물하기/admin-gift-cancel-addition-prompt.md` — 새 화면을 만들지 않고 기존 화면에 취소 기능만 추가하는 프롬프트.
- [x] 네 문서의 계약과 기존 전체 화면 프롬프트 무변경을 대조하고 검증 명령의 실제 결과를 기록한다.
### P14-T1 최종 문서 검증 — 2026-10-22
- 상태: 문서 반영 및 검증 완료. `P14-T2`, `P14-T3`, `P14-GATE`는 미착수다.
- 무엇을: PRD/계획/API 요약에 관리자 취소와 목록·상세 `CANCEL` 계약을 반영하고, 기존 화면의 기능 추가만 지시하는 `admin-gift-cancel-addition-prompt.md`를 신규 작성했다.
- 왜: 운송장 등록 전 취소 정책을 명시하고, 기존 전체 페이지 구현 프롬프트를 변경하지 말라는 사용자 요청을 반영하기 위해서다.
- 어떻게: 네 문서의 `RECEIVED` 전용 취소, 발신자 리워드캔 전액 환불, body 없는 POST, 기존 관리자 응답 네 필드와 UTC 시각, 목록/상세 액션 및 미구현 표시를 대조했다. 신규 프롬프트 전체와 문서 링크도 확인했다.
- 검증: `./gradlew tasks --all` exit 0, `git diff --check` exit 0, 신규 프롬프트의 `git diff --no-index --check /dev/null docs/20260929_크리에이터_선물하기/admin-gift-cancel-addition-prompt.md` exit 0, 기존 `admin-page-prompts.md`의 `git diff --exit-code HEAD -- docs/20260929_크리에이터_선물하기/admin-page-prompts.md` exit 0.
- 변경 범위: 기존 문서 3개 수정과 기능 추가 프롬프트 1개 생성만 확인했다. 소스/테스트/설정/DDL과 기존 완료 Task·검증 이력은 변경하지 않았다.
- 미실행: 문서만 변경했으므로 focused/전체 테스트, lint, 애플리케이션 빌드와 브라우저 QA는 실행하지 않았다. 명령 확인은 기능 동작 검증을 의미하지 않는다.
- 다음 행동: 사용자 구현 승인 후 `P14-T2`의 첫 미완료 RED 단계부터 시작한다.
### 구현 작업 (사용자 승인 후 완료)
#### Task 14.2 관리자 취소 상태 전이와 환불 구현
**Goal 실행 `P14-T2`:** 기존 관리자 권한으로 `RECEIVED` 선물만 취소하고 발신자에게 전액 리워드캔 환불을 원자적으로 처리한다.
- **시작 조건:** `P14-T1` 검증과 사용자 구현 승인.
- **완료 증거:** service/controller focused test에서 상태, 환불, 권한, 공통 오류와 응답 계약 통과.
- **범위 밖:** 새로운 환불 체계/스키마/의존성, 취소 사유/감사 테이블, 취소 전용 푸시, 사용자 취소/자동취소 정책 변경, 전면 리팩터링.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminService.kt` — `fun cancelGift(applicationNo: String, now: LocalDateTime = LocalDateTime.now()): GiftOperationStatusResult` 계약을 기존 관리자 operation method에 맞춰 추가한다.
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftController.kt` — body 없는 `POST /api/v2/admin/gifts/{applicationNo}/cancel`, 기존 `AdminGiftOperationStatusResponse`와 `ApiResponse.ok(...)` 사용.
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt`.
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`.
- 조건부 확인/수정: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt` — 경합 테스트가 입증한 상태 전이/환불 중복 원인에 한해 공통 경계를 최소 수정한다. 기존 사용자/자동취소 정책은 변경하지 않는다.
- [x] **RED:** service 테스트에서 `RECEIVED` 관리자 취소의 `CANCELED`, `GiftDelivery.canceledAt`, 원 결제액 전액 `refundGiftCan` 1회 호출을 검증한다. 발신자가 아닌 관리자도 가능하고, 존재하지 않거나 다른 상태면 기존 `common.error.invalid_request`이며 상태/환불은 변경되지 않는지 검증한다. 환불 실패 rollback 및 중복 환불 방지도 검증한다.
- [x] **RED:** `AdminGiftControllerTest`에 실제 Spring 트랜잭션을 통과하는 관리자 취소 통합 테스트를 추가한다. ADMIN 성공과 비관리자/미인증 거부, 모든 비허용 상태 거부, 응답 네 필드와 UTC 시각, 발신자의 실제 리워드캔/환불 내역 복원 및 관리자 잔액 불변을 검증한다. 사용자 취소·자동취소·운송장 등록·다른 관리자 취소와의 동시 요청, 환불 예외 시 DB 상태/취소 시각/환불 내역 롤백도 검증한다. mock 호출 횟수만으로 실제 환불/롤백/경합 검증을 대체하지 않는다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"` 실행. 새 assertion 또는 취소 메서드 미구현으로 실패해야 하며 설정/fixture 오류는 수정한 뒤 다시 확인한다.
- [x] **GREEN:** 기존 transaction, 예외, 캔 환불 경로와 `GiftOperationStatusResult`를 재사용해 최소 구현한다. 신규 push는 발행하지 않는다.
- [x] **GREEN 확인:** 같은 service focused test와 `./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"` 실행. 정상 envelope, 응답 필드 및 UTC `occurredAt=canceledAt`, 기존 ADMIN 권한 거부를 확인한다.
- [x] **REFACTOR:** 이번 변경으로 생긴 중복만 정리하고 아래 focused test와 `./gradlew ktlintCheck`를 실행해 실제 결과를 기록한다.
#### P14-T2 구현·검증 기록 — 2026-10-22
- 무엇을: ADMIN 취소 endpoint와 `GiftAdminService.cancelGift`를 추가하고 사용자 취소·운송장 등록·자동취소의 최초 선물 조회에 DB 쓰기 잠금을 적용했다. 기한 안내가 취소 시각을 덮어쓰는 경합도 RED로 확인해 동일 잠금을 적용했다. 기존 분산 job lock과 상태/환불 정책은 유지했다.
- RED: 기존 focused 92개 통과 후 신규 취소 HTTP 404 실패, service 메서드 미구현 실패, 잠금 없는 핵심 경합 7개 실패, 안내 경합 2개 실패를 각각 확인했다.
- GREEN: `./gradlew --no-daemon test --tests '*GiftAdminServiceTest' --tests '*AdminGiftControllerTest' --tests '*GiftCommandServiceTest' --tests '*GiftSchedulerTest' ktlintCheck` exit 0, 119개 테스트(25/30/50/14) 실패·오류·skip 0. 담당자 외 독립 재실행도 `test --rerun`으로 exit 0(48초)을 확인했다.
- 실제 DB 확인: 80캔 사용(충전 60 + 리워드 20)이 발신자 리워드 80으로 환불되고 관리자 잔액은 불변이다. 실제 환불/flush 후 예외 발생 시 상태·취소 시각·잔액·환불 내역이 모두 롤백된다. 9개 동시 요청 순서에서 최종 상태와 환불 1회 불변조건을 확인했다.
- 리뷰: 독립 T2 스펙/코드 리뷰 APPROVE, 차단 사항 없음. 커밋 `44bec81a`.
- 후속 사용자 요청: `44bec81a` 커밋은 취소하고 변경 파일을 작업 트리에 보존했다. 이후 커밋은 사용자가 확인 후 직접 수행하며 에이전트는 커밋하지 않는다.
- 검증 경계: H2/Spring 트랜잭션과 MockMvc 검증이다. 실제 socket HTTP는 후속 T3/Gate에서 수행하며 MySQL 실서버는 사용하지 않았다. LSP 도구가 없어 Kotlin 컴파일과 ktlint로 대체했다. 기존 타 스케줄러의 H2 `SKIP LOCKED` 오류 로그와 Gradle deprecation 경고는 별도 관찰로 남긴다.
#### Task 14.3 관리자 목록/상세 취소 액션 구현
**Goal 실행 `P14-T3`:** 관리자 목록과 상세에서 `RECEIVED`만 `CANCEL` 액션을 노출한다.
- **시작 조건:** `P14-T2` 완료.
- **완료 증거:** query service 테스트에서 모든 상태 매핑과 기존 `COMPLETE_DELIVERY` 수취인 정보 조건부 노출 통과.
- **범위 밖:** 다른 상태 액션 재설계, 관리자 UI 구현.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminQueryService.kt`.
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminQueryServiceTest.kt`.
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt` — 목록/상세 JSON 액션 및 취소 후 `CANCELED`/빈 액션 재조회 검증.
- [x] **RED:** 목록과 상세 각각 `RECEIVED -> ["CANCEL"]`이며 다른 상태의 기존 액션이 그대로이고 `INSPECTION_COMPLETED`의 `COMPLETE_DELIVERY` 조건이 유지되는 테스트를 작성한다.
- [x] **RED 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminQueryServiceTest"` 실행. 신규 assertion 실패를 확인한다.
- [x] **GREEN:** 두 응답의 공통 상태별 action 계산만 필요한 범위에서 수정한다.
- [x] **GREEN 확인:** `./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"` 실행. 목록/상세 JSON 매핑, 취소 후 빈 액션과 기존 조건부 액션 통과를 확인한다.
- [x] **REFACTOR:** 이번 Task가 만든 중복만 정리하고 focused test와 `./gradlew ktlintCheck` 결과를 기록한다.
#### P14-T3 구현·검증 기록 — 2026-10-22
- 무엇을: 공통 `GiftStatus.availableActions`의 `RECEIVED` 분기에 `CANCEL`만 추가했다. 기존 모든 상태 매핑과 수취인 정보 조건을 보존했다.
- RED: 기존 34개 통과 후 신규 상세/실제 HTTP 목록 액션 assertion 2개만 실패하는 것을 확인했다. RANDOM_PORT fixture의 request context 오류는 수정 후 유효 RED를 재확인했으며 기능 실패로 계산하지 않았다.
- GREEN: `./gradlew --no-daemon test --tests '*GiftAdminQueryServiceTest' --tests '*AdminGiftControllerTest' ktlintCheck` exit 0, 35개(query 4 + controller 31) 실패·오류·skip 0. 기존 9개 경합·롤백 테스트도 포함됐다.
- 실제 HTTP: RANDOM_PORT Tomcat과 실제 TokenProvider JWT를 사용해 익명 401, 모든 비ADMIN 역할의 공통 실패 응답, ADMIN 목록/상세의 `RECEIVED/[CANCEL]`, body 없는 취소 성공, 반복 거부, 재조회 `CANCELED/[]`를 확인했다. 원 발신자 충전 40/리워드 0에서 리워드 80으로 환불되며 관리자 잔액과 환불 1회 조건을 실제 DB로 검증했다.
- 증거: `AdminGiftControllerTest.shouldCancelReceivedGiftThroughRealJwtHttpAndRequeryActions`의 `GIFT_HTTP` 출력에서 취소 성공 `success=true`, `status=CANCELED`, `statusName=신청 취소`, UTC `occurredAt` 네 필드를 확인했다. GET 증거는 개인정보를 제거하고 JWT는 출력하지 않았다.
- 리뷰: T2/T3 전체 코드·테스트 최종 리뷰 APPROVE, 차단 사항 없음.
### Phase 14 Gate
**Goal 실행 `P14-GATE`:** 승인된 구현의 취소, query action 및 경합 회귀를 판정한다.
- **시작 조건:** `P14-T2`, `P14-T3` 완료.
- **완료 증거:** focused test, 직접 영향 회귀, lint 통과 및 사용자 흐름 기록.
- **범위 밖:** 실패와 무관한 구현, 전체 선물 도메인 리팩터링.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftSchedulerTest"
./gradlew ktlintCheck
```
**Expected:** focused 명령과 lint가 exit 0. 취소와 운송장 등록, 사용자 취소, 자동취소, 다른 관리자 취소가 경합해도 허용 상태 전이와 환불 1회 불변조건이 유지되고, 환불 실패 시 취소 상태가 커밋되지 않는다. 기존 테스트 구조 안에서 검증하며 새 의존성이나 스키마는 추가하지 않는다.
- [x] 위 focused test와 직접 영향 회귀, lint 결과를 기록한다.
- [x] 관리자 권한, 발신자 전액 환불, 목록/상세의 `CANCEL` 노출과 취소 후 빈 액션, 자동취소 전용 푸시 미발행을 검증한다.
- [x] 문서의 구현 예정 표시를 실제 검증된 상태로 갱신한다. 전체 회귀는 공통 경계 변경 또는 focused test로 영향 범위를 판단할 수 없는 경우에만 실행하고, 생략 시 근거와 대체 명령을 기록한다.
#### P14-GATE 최종 판정 — 2026-10-22
- 판정: 사용자 요청에 따른 전체 테스트 제외 범위에서 완료. 기능·실제 HTTP·권한·환불·롤백·경합 검증과 독립 최종 리뷰를 통과했다.
- 최종 빌드: `./gradlew --no-daemon build -x test ktlintCheck` exit 0, `BUILD SUCCESSFUL in 12s`. 최신 소스/테스트 컴파일 및 lint 산출물이 up-to-date이며 전체 build 작업이 성공했다.
- 전체 테스트: `./gradlew --no-daemon test --rerun ktlintCheck build` 1차는 도구의 600초 제한으로 중단됐다. 제한을 늘린 2차는 사용자가 중단했다. 이후 사용자가 전체 테스트를 제외하고 마무리하도록 지시해 재실행하지 않았다. 전체 테스트 성공/실패를 확정하지 않는다.
- 대체 증거: T2 focused 119개(독립 재실행 포함), T3 focused 35개(일부 중복), 실제 JWT socket HTTP 시나리오 및 최종 코드 리뷰 APPROVE. 테스트 삭제·skip·완화는 추가하지 않았다.
- 한계: LSP 도구가 없어 Kotlin 컴파일·ktlint로 대체했다. H2 검증이며 실제 MySQL REPEATABLE_READ/다중 인스턴스/운영 배포와 별도 프론트엔드 화면 QA는 수행하지 않았다. 기존 타 스케줄러 H2 문법 경고와 Gradle deprecation 경고는 수정 범위 밖이다.
- 프롬프트: 사용자 요청대로 서버 구현/준비 여부를 선행조건으로 두지 않고 확정 Request/Response와 테스트 fixture를 기준으로 프론트엔드 병행 구현을 안내한다. 기존 `admin-page-prompts.md`는 그대로 보존한다.
- 커밋: 생성했던 커밋은 사용자 요청으로 취소했다. 변경은 모두 unstaged/untracked 작업 트리에 유지하며 추가 커밋/푸시는 하지 않는다.
- 문서 검증: `./gradlew --no-daemon tasks --all` exit 0, `git diff --check` 통과. 기존 전체 프롬프트 비교와 index 비교는 각각 exit 0, HEAD는 작업 전 `45b97088`이다. `build/libs/sodalive-0.0.1-SNAPSHOT.jar` 산출물을 확인했다.
- 검증 기록 정정: P14-T1의 신규 파일 `git diff --no-index --check /dev/null ...` exit 0 표기는 부정확했다. 최종 확인에서는 공백 오류 출력 없이 신규 파일 차이로 exit 1이다. 기존 추적 파일 `git diff --check`는 exit 0이며, 이를 테스트/빌드 실패와 혼동하지 않는다.
- 자원 정리: 테스트 worker/Redis 프로세스 잔류 없음과 작업 전용 디렉터리에 열린 파일 없음 확인 후 `gift-p14.xbpOuF`, `gift-p14-impl.d9YMmw`, `gift-p14-t3.Hx06BJ` 및 임시 진행 노트를 삭제하고 네 경로의 부재를 확인했다. 검증 요약은 이 문서에 보존했으며 공유 Gradle 캐시와 build 산출물은 삭제하지 않았다.
### Phase 14 커밋 전 검증 — 2026-10-22
- 승인: 사용자가 `/commit-with-validation`으로 변경을 적절히 나누어 커밋하도록 후속 요청했다. 이전 커밋 보류 기록 이후의 새 승인으로 적용한다.
- 검증: `./gradlew --no-daemon test --rerun --tests '*GiftAdminServiceTest' --tests '*GiftAdminQueryServiceTest' --tests '*AdminGiftControllerTest' --tests '*GiftCommandServiceTest' --tests '*GiftSchedulerTest' ktlintCheck` exit 0, `BUILD SUCCESSFUL in 58s`.
- 결과: 관련 5개 클래스 124개 테스트에서 실패·오류·skip 0. lint는 UP-TO-DATE이며 `git diff --check`도 통과했다.
- 범위: 관리자 취소와 직접 영향받는 사용자 명령·스케줄러 회귀를 재실행했다. 기존 사용자 요청에 따라 전체 테스트는 제외했고 운영 MySQL 검증은 수행하지 않았다.
### Phase 14 후속 회귀 수정
#### Task 14.4 관리자 취소 시각 정밀도 회귀 수정
**Goal 실행 `P14-T4`:** 관리자 취소의 DB 재조회 `canceledAt`과 응답 `occurredAt`을 OS/JDK 시계 정밀도와 무관하게 일치시킨다.
- **시작 조건:** Jenkins의 `AdminGiftControllerTest` 458/526행 실패 보고 및 정밀도 정규화 수정 승인. H2 2.1.214 `timestamp`에 나노초 입력 시 마이크로초 반올림을 별도 JDBC로 재현했다.
- **정책:** 운영 DDL `canceled_at TIMESTAMP`에 맞춰 초 단위 절삭 후 저장·응답에 같은 값을 사용한다. 테스트 비교를 완화하거나 전역 `toUtcIso`, DDL, 사용자/자동취소 동작을 변경하지 않는다.
- **Files:** Modify `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminService.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt`; Verify `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`; Update 기존 `prd.md`, `plan-task.md`.
- **완료 증거:** 고정 나노초·초 경계 입력의 DB flush/clear 재조회 테스트, 기존 실패 두 HTTP 테스트를 포함한 focused test, lint/테스트 제외 빌드, 변경 리뷰 통과.
- **범위 밖:** 전체 테스트(기존 사용자 제외 요청 유지), 커밋/푸시, 운영 DB/CI 설정 변경.
- [x] **RED:** `0`, `123456789`, `999999999` 나노초 입력을 사용해 초 단위 기대값과 DB 재조회/응답의 완전 일치를 검증하고 기존 코드의 실패를 확인한다.
- [x] **GREEN:** 관리자 취소 메서드에서 취소 시각을 한 번 초 단위로 정규화하고 저장·응답에 사용한다.
- [x] **REFACTOR/검증:** `./gradlew --no-daemon test --tests '*GiftAdminServiceTest' --tests '*AdminGiftControllerTest' ktlintCheck` 및 `./gradlew --no-daemon build -x test`를 실행한다. 기존 458/526행 assertion과 실제 JWT HTTP 검증은 유지한다.
#### P14-T4 진행 기록 — 2026-10-22
- RED: 신규 parameterized DB roundtrip 테스트 3개 중 2개 실패(exit 1, 42초). `23:59:59.123456789`는 DB에서 `.123457`로, `23:59:59.999999999`는 다음 날 `00:00`으로 반올림되어 응답과 달랐다. 나노초 0 입력은 통과했다.
- GREEN: 관리자 취소의 `now`를 `ChronoUnit.SECONDS`로 한 번 절삭해 저장과 결과에 같은 값을 사용했다. `./gradlew --no-daemon test --tests '*GiftAdminServiceTest' --tests '*AdminGiftControllerTest' ktlintCheck` exit 0(1분 26초), service 28 + controller 31 = 59개 실패·오류·skip 0.
- 실제 HTTP: `shouldCancelReceivedGiftThroughRealJwtHttpAndRequeryActions`에서 body 없는 POST가 `success=true`, `status=CANCELED`, `occurredAt=2026-10-22T19:05:41Z`를 반환하고 DB 재조회값과 정확히 일치했다. Jenkins에서 실패한 기존 두 assertion과 환불·경합 검증은 변경하지 않았다.
- 빌드/문서: `./gradlew --no-daemon build -x test tasks --all` exit 0(14초). 전체 테스트는 기존 사용자 제외 요청을 유지해 실행하지 않았다. 커밋/푸시 없음.
- 한계: Jenkins 재실행과 실제 운영 DB 검증은 하지 않았다. CI 원본 expected/actual은 제공되지 않았으며 로컬 고정 나노초 입력으로 해당 실패 메커니즘을 재현·검증했다. LSP 도구가 없어 Kotlin 컴파일·ktlint로 대체했다.
- 최종 판정: 독립 변경 리뷰 APPROVE, 차단 사항 없음. 기존 HTTP 458/526행의 완전 일치 검증과 전역 날짜 변환/DDL/환불 로직이 변경되지 않았음을 확인했다. `git diff --check` 통과, 후속 커밋 없음.
### Phase 14 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 31 | `P14-T1` | 관리자 취소 승인 범위 문서화 | 아니요 | 문서 검증 |
| 32 | `P14-T2` | `P14-T1` 검증 및 사용자 구현 승인 | 아니요 | 승인 대기, 코드 변경 금지 |
| 33 | `P14-T3` | `P14-T2` 계약/승인 | 아니요 | 선행 구현 승인 대기 |
| 34 | `P14-GATE` | `P14-T2`, `P14-T3` | 아니요 | 실패 원인 Task에 반영 |
| 35 | `P14-T4` | Jenkins 정밀도 불일치 후속 수정 승인 | 아니요 | DB roundtrip 실패 원인 확인 |
```text
P14-T1 문서 검증 → 사용자 구현 승인 → P14-T2 → P14-T3 → P14-GATE
```
## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|---|---|---|---|---|---|
| 2026-09-29 | `PLAN-DEC-001` | 확정 | 신규 선물 도메인은 `kr.co.vividnext.sodalive.v2.gift`, 사용자 API는 `v2.api.gift`, 관리자 API는 `v2.api.admin.gift`에 둔다 | 기존 v2 API와 도메인 분리 패턴 | 전체 구현 |
| 2026-09-29 | `PLAN-DEC-002` | 확정 | 운영 DB DDL은 같은 작업 디렉터리의 `gift-schema.sql`로 작성한다 | 기존 docs 작업 디렉터리 SQL 문서 패턴 | `P1-T1` |
| 2026-09-29 | `PLAN-DEC-003` | 확정 | 최종 Gate에서는 전체 `./gradlew test`를 실행한다 | 캔 결제, FCM, 스케줄러 등 공통 경계를 변경함 | `P7-GATE` |
| 2026-09-29 | `PLAN-DEC-004` | 확정 | 신규 선물 구현은 v2 신규 도메인 관례대로 `domain`, `application`, `port`, `adapter` 구조를 사용한다 | 사용자 지시: v2 아래 구현된 관례만 확인하고 처리 | 전체 구현 |
| 2026-09-30 | `PLAN-DEC-005` | 확정 | 운영 반영용 DDL에는 선물 도메인 FK를 명시하고, 선물 신청 건에는 최종 결제 금액 스냅샷만 저장한다 | 사용자 결정: 제대로 가는 방향 선호, `basePriceCan`은 신청 건에 불필요 | `gift-schema.sql`, `Gift` |
| 2026-09-30 | `PLAN-DEC-006` | 확정 | 선물 API response datetime은 전역 Jackson 설정이 아니라 DTO response boundary에서 UTC ISO 문자열로 변환한다 | 선물 API만의 공개 계약 보강이며 전역 변경은 기존 API 영향이 큼 | `P8-T1`, `client-api-summary.md` |
| 2026-09-30 | `PLAN-DEC-007` | 확정 | 기존 `categoryCode`는 내부 범용코드로 유지하고 분류번호/접수코드/대표품목을 추가하며, 신청번호는 카테고리+날짜별 sequence로 발급한다 | 사용자 확정: 카테고리 필드 의미와 사용자 폼 옵션 표시 규칙 분리 | `P9-T1`~`P9-T3` |
| 2026-10-02 | `PLAN-DEC-008` | 확정 | `COMPLETE_DELIVERY`는 수취인 필수 정보가 모두 입력된 경우에만 허용하고, 배송지 미입력 기한 초과는 주소 입력 가능 상태 3개 전체에서 `UNDELIVERABLE`로 종료한다 | 배송지 없는 선물의 전달 완료와 관리자 상태 진행 후 만료 누락 방지 | `P12-T1`, `P12-T2` |
| 2026-10-22 | `PLAN-DEC-009` | 확정 | Phase 14는 문서화만 먼저 진행하며 관리자 취소 구현은 사용자 별도 승인 후 시작한다. `RECEIVED` 전액 환불은 기존 경로와 관리자 응답/action 계약을 재사용한다 | 승인된 범위와 구현 전 사람 승인 경계 | `P14-T1`~`P14-GATE`, PRD `DEC-015` |
| 2026-10-22 | `PLAN-DEC-010` | 확정 | 기존 `admin-page-prompts.md`는 보존하고 기존 화면에 취소 기능만 추가하는 프롬프트를 `admin-gift-cancel-addition-prompt.md`로 분리한다 | 사용자 수정 요청 | `P14-T1`, PRD `DEC-016` |
| 2026-10-22 | `PLAN-DEC-011` | 확정 | 프론트엔드 추가 프롬프트에서 서버 준비 대기 조건을 제거하고 확정 API 계약으로 병행 구현한다 | 사용자 후속 요청 | `admin-gift-cancel-addition-prompt.md` |
| 2026-10-22 | `PLAN-DEC-012` | 확정 | 전체 테스트를 제외하고 관련 검증·테스트 제외 빌드·문서·임시 자원 정리로 마무리한다. 커밋은 사용자가 직접 한다 | 사용자 마무리 및 커밋 금지 요청 | `P14-GATE` |
## 발견된 문제
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|---|---|---|---|---|---|
| `GIFT-BUG-001` | High | 해결 | 수취인 정보가 없는 `INSPECTION_COMPLETED` 선물이 전달 완료될 수 있고, 관리자 상태 진행 후 배송지 미입력 기한 초과 대상에서 누락된다 | `P12-T1`, `P12-T2` | command/action 불변조건과 만료 조회 상태 범위를 테스트 우선으로 보강 완료 |
| `TEST-INFRA-001` | Medium | 해결 | 테스트 timeout 후 embedded Redis native 프로세스와 Gradle worker가 남아 후속 테스트 실행을 방해했다 | `P12-T3` | 잔존 프로세스 정리, 중복 hook 제거, 공식 강제 종료 옵션 적용 후 전체 테스트 정상 종료 확인 |
## 최종 보고 형식
```markdown
구현 결과: 완료한 Phase와 사용자 흐름을 한 문장으로 작성
- 변경: 주요 파일과 동작 요약
- 결정: 중요한 Decision Log ID와 내용
- 검증:
- 실행 명령 — 성공/실패와 핵심 수치
- 수동 검증 — 성공/실패/불가 사유
- 남은 항목: 외부 의존, 후속 범위 또는 없음
- 문서: 갱신한 PRD/plan/review 링크
```
## Phase 15: 전달 완료 배송 개인정보 정리
**Phase 결과:** 전달 완료 후 7일 이상 지난 선물의 배송 개인정보만 DB에서 지우고, 기존 사용자/관리자 응답 형태와 결제·동의·리뷰·업무 이력을 유지한다.
**2026-10-23 실행 승인:** 사용자가 권장한 서브에이전트 단계별 실행을 승인하고 worktree 없이 현재 `test` 브랜치에서 진행하도록 지정했다. `P15-T2`부터 순차 TDD·리뷰를 수행한다. 운영 SQL/배포와 커밋·푸시는 하지 않는다. 후속 ULW 검증 요청에 따라 단계별 focused 검증 후 Gate에서 전체 테스트도 1회 시도하며, 기존 전체 테스트 제외 규칙은 이번 Gate에 한해 대체한다. 무관한 실패는 수정 범위를 넓히지 않고 보고한다.
**후속 지시:** 사용자가 `git commit/push는 진행하지 않는다. 나머지는 계속 진행한다.`고 재확인했다. 모든 변경은 현재 브랜치의 미커밋 상태로 남기고 구현·검증·리뷰만 완료한다.
**선행조건:** Phase 14와 P15-T1 문서 검증 후 사용자 실행 승인으로 T2~T4, R1 및 Gate를 완료했다. 요구사항은 PRD `GIFT-018`~`GIFT-021`을 따른다.
**Phase 완료 조건:** `P15-T1`~`P15-T4`, 후속 `P15-R1` 및 `P15-GATE` 완료. 실제 test·lint·HTTP QA·리뷰 기록을 누적했다. SQL은 문서로만 작성했으며 운영 적용 완료를 주장하지 않는다.
### 목표와 제약
- 적격 조건은 `Gift.status=DELIVERED`, `GiftDelivery.deliveredAt != null`, `deliveredAt <= now.minusDays(7)`이다. 7일 경계 포함, 그 이전의 과거 누적 및 앞으로 만료되는 건을 모두 처리한다.
- Kotlin `GiftDelivery`의 `senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail` 다섯 property는 모두 `var String?`가 된다. DB에서 현재 NOT NULL인 열은 앞의 네 개뿐이다. `sender_address_detail`은 이미 nullable이므로 ALTER하지 않는다.
- 정리 시 `senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail`, `recipientName`, `recipientPhoneNumber`, `recipientZipCode`, `recipientAddress`, `recipientAddressDetail`, `courierCompanyName`, `trackingNumber`만 null 처리한다.
- 적격 delivery를 `GiftRepository.findDeliveredGiftsWithDeliveryBeforeOrAt(cutoff: LocalDateTime): List<Gift>`로 조회한다. `GiftScheduler.eraseDeliveredGiftPersonalData(now: LocalDateTime): Int`가 `now.minusDays(7)`을 전달해 반환한 delivery 개인정보를 dirty checking으로 비운다. 이미 12개가 모두 null인 건은 건너뛰어 재실행 변경 수는 0으로 한다.
- 상태/ID, Member nickname/profile, 결제, 동의 값/시각, 리뷰와 모든 업무 lifecycle/deadline/reminder 시각 및 `createdAt`은 보존한다. `updatedAt`은 `BaseEntity @PreUpdate`의 정상 동작을 따르며 이를 우회할 native SQL update는 사용하지 않는다.
- 기존 필수 request 타입과 validation, public DTO schema, outer nullable object, visibility/derived flags/deadlines는 유지한다. 데이터가 없는 기존 non-null response 문자열은 `""`, 삭제된 전체 주소도 정확히 `""`이다. `SENT.senderInfo`는 객체와 빈 문자열을 유지하고 `RECEIVED.senderInfo`는 null이다. 모든 recipient address가 null이면 `recipientAddress=null`; user tracking과 admin courier/tracking nullable 값은 null이다. `recipientAddressRegistered`는 실제 필드 기준 false이며 `DELIVERED`는 재입력 prompt/action을 보이지 않는다.
- 새 erased flag/table/retention config/batch knob/dependency/index/fallback은 추가하지 않는다. 푸시/환불 동작은 추가하지 않는다. 테스트 범위는 위 실행 승인 기록에 따라 focused 우선, 최종 전체 테스트 1회 시도로 한다.
- 기존 scheduled entrypoint는 자동취소 `0 0 * * * *`뿐이다. 추가하는 독립 wrapper는 recipient expiry `0 10 * * * *`, erasure `0 20 * * * *`, reminders `0 40 * * * *`, 모두 `Asia/Seoul`이다. Wrapper는 기존 업무 메서드를 호출하며 각각 기존 방식의 독립 Redisson lock과 `@Transactional`을 사용한다. 작업 시간 차이를 보장하지 않으므로 작업 간 비중첩은 가정하지 않는다. 기존 `LocalDateTime` clock convention 그대로 wrapper에서 `LocalDateTime.now()`를 전달하며 Clock 설정/주입은 추가하지 않는다.
- SQL은 30줄 미만 변경이므로 별도 migration 파일 대신 이미 `gift-schema.sql` 끝에 5줄 ALTER로 작성됐다. 기존 CREATE block은 NOT NULL 상태로 유지되며, schema 전체를 fresh install 순서로 적용하면 trailing ALTER 뒤 최종 네 필드는 nullable이다. 기존 DB는 ALTER 구문만 적용해야 한다.
- 운영 rollout만 actual schema metadata 검토, DDL 별도 승인 및 적용, nullable schema/compatible reader의 전체 instance 준비를 요구한다. 이는 local code/H2 test 선행조건이 아니다. 첫 erase 전 모든 reader는 nullable 호환이어야 한다. 이후 non-null reader rollback은 unsafe하며 코드만으로 삭제 데이터를 복구할 수 없다. 자동 production SQL 실행/backup 변경은 없다.
### 리뷰 포커스
1. `deliveredAt` 경계, 오래된 이력, null 시각 및 다른 status 구별: `GiftSchedulerTest` 실제 DB predicate 검증.
2. 반복/부분 실행과 보호 데이터 보존: flush/clear/reload 후 `GiftSchedulerTest` 확인.
3. 삭제 이후 역할별 nullable/non-null API mapping과 정확한 주소 문자열: query 및 controller 테스트.
4. scheduler 분·lock·transaction wrapper 분리, 같은 클래스 delegation: scheduler annotation/reflection/delegation 테스트.
5. nullable 컬럼과 non-null reader 간 rollout 순서/rollback 위험: `P15-T1`, `P15-GATE` deployment checklist 대조.
### Task 15.1 Phase15 문서 및 추적 기준
**Goal 실행 `P15-T1`:** PRD와 이 계획에 승인된 데이터 정리 계약, 테스트 기준, 배포 안전 게이트를 일치시킨다.
- **시작 조건:** Phase 14 완료, 사용자 Phase15 승인 범위 확인.
- **완료 증거:** PRD `GIFT-018`~`GIFT-021`/Decision Log와 본 Phase의 계약·Tasks·dependency 일치. lead가 문서 최종 검증 명령을 실행하고 이 Task의 마지막 체크박스를 완료 처리한다.
- **범위 밖:** source/test/client-summary 변경, SQL 실행, 설정 변경 및 구현 완료 표시. `gift-schema.sql`은 lead가 이미 추가한 SQL 문서 범위에 포함하며 이 문서 편집에서는 수정하지 않는다.
- **TDD 예외 사유:** 문서 전용 작업이며 production 동작을 변경하지 않는다.
- **대체 검증 방법:** PRD/plan traceability와 변경 경로를 diff로 대조하고 `./gradlew --no-daemon tasks --all` 및 `git diff --check`를 실행한다. 실제 결과는 하단 `P15-T1 최종 문서 검증`에 기록한다.
**Files:**
- Modify: `docs/20260929_크리에이터_선물하기/prd.md`
- Modify: `docs/20260929_크리에이터_선물하기/plan-task.md`
- Include in scope: `docs/20260929_크리에이터_선물하기/gift-schema.sql`의 lead 추가 trailing ALTER section. SQL documentation은 준비됨; lead 검토 대상이며 이 Task에서는 SQL 파일을 수정/실행하지 않는다.
**Interfaces:**
- Consumes: 승인된 계약 `GIFT-018`~`GIFT-021`, `DEC-017`~`DEC-021`.
- Produces: `P15-T2`~`P15-GATE`의 구현 경계와 수용 조건.
- [x] **문서 초안:** PRD 요구사항/정책과 계획 task/deployment gate를 갱신한다. 문서 작성 완료만 표시하며 검증 완료를 의미하지 않는다.
- [x] **최종 검증:** 문서 검증 명령을 실행하고 PRD/plan과 `gift-schema.sql` 하단 ALTER만 변경됐음을 확인했다. Phase14 이력 보존 및 승인 계약 추적을 대조하고 하단에 실제 검증 결과를 기록했다. 소스 구현이나 DB 반영 완료를 의미하지 않는다.
### Task 15.2 nullable persistence와 안전한 응답 mapping
**Goal 실행 `P15-T2`:** 네 sender column nullable schema와 배송 개인정보 없는 기존 응답 표현을 함께 지원한다.
- **시작 조건:** `P15-T1` lead 검증 완료 및 사용자 구현 승인. 실제 schema metadata/DDL 승인이나 운영 access는 local code/H2 test의 선행조건이 아니다.
- **완료 증거:** entity mapping/DDL 검토 및 query/controller focused test에서 non-null 응답 문자열, nullable 필드, 기존 request validation/visibility/action 계약 통과.
- **범위 밖:** HTTP schema 변경, 개인정보 보관 정책 추가, 사용자 입력 nullable화, 운영 SQL 실행.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftDelivery.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminQueryService.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftEntityMappingTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminQueryServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`
- Verify only: request/response DTO types; mapping은 기존 query service 경계에서 처리하고 DTO 변경을 전제하지 않는다.
**Interfaces:**
- Consumes: 현재 response DTO와 mapping, 기존 필수 request validation.
- Produces: `GiftDelivery.senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail` 모두 `var String?`. 현행 NOT NULL DB sender 열 4개만 기존 `gift-schema.sql` trailing ALTER에서 nullable화. Response schema는 변경 없이 empty-string/null mapping 유지.
- [x] **RED:** `GiftEntityMappingTest.shouldPersistNullForAllSenderFields`, 양방향 query 및 관리자 query, 실제 controller 응답 검증을 추가했다. 필수 sender 빈 문자열 거부는 기존 `GiftCommandServiceTest.shouldRejectInvalidGiftApplication`의 네 case를 재사용하고, `GiftControllerTest.shouldKeepRequiredSenderRequestValidation`으로 누락 네 case를 추가했다.
- [x] **RED 확인:** entity focused 5개 중 1개 실패, 관련 6개 클래스 129개 중 신규 6개 실패를 확인했다. 저장 제약 3건과 query null 매핑 3건이며 기존 113개는 모두 통과했다. Kotlin reflection은 RED에서만 사용하고 최종 테스트에서는 제거했다.
- [x] **GREEN:** 발신자 다섯 property를 `var String?`로 바꾸고 query service 두 곳에서 이름/전화번호 `.orEmpty()` 및 nullable 주소 포맷을 적용했다. DTO·요청 검증·업무 시각은 유지했다.
- [x] **GREEN 확인:** 관련 focused 테스트 129개 실패·오류·skip 0, Kotlin 컴파일과 ktlint 통과. 12개 필드 null 저장을 flush/clear/reload로 확인했다. 기존 SQL ALTER와 엔티티의 네 nullable 컬럼을 대조했으며 SQL은 실행하지 않았다.
- [x] **REFACTOR:** source diff 3개 파일의 최소 변경을 확인했고 독립 query/entity 32개 재실행과 lint를 통과했다. 단계 리뷰는 명세·품질 모두 승인됐다. 상세 결과는 아래 P15-T2 기록을 따른다.
#### P15-T2 구현·검증 기록 — 2026-10-23
- Baseline: `./gradlew --no-daemon test --tests '*GiftEntityMappingTest' --tests '*GiftQueryServiceTest' --tests '*GiftAdminQueryServiceTest' --tests 'kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest' --tests '*AdminGiftControllerTest' ktlintCheck` exit 0, 113개 통과(1분 44초).
- RED: 동일 클래스 범위 신규 테스트 129개 중 6개 실패(1분 2초), null 저장 미지원 및 non-null 응답에 null 전달을 재현했다. HTTP 두 case는 저장 선행조건 실패였으며 HTTP 매핑 자체의 RED로 계산하지 않았다.
- GREEN: baseline과 같은 focused 명령 exit 0(1분 18초), entity 5 + user query 19 + admin query 8 + command 50 + user controller 15 + admin controller 32 = 129개, 실패·오류·skip 0. 신규 16 invocation과 기존 입력 검증이 통과했다.
- 독립 재검증: `./gradlew --no-daemon test --tests '*GiftEntityMappingTest' --tests '*GiftQueryServiceTest' --tests '*GiftAdminQueryServiceTest' ktlintCheck` exit 0(39초), XML에서 32개 실패·오류·skip 0 확인. LSP 도구가 없어 Kotlin 컴파일·ktlint로 대체했다.
- 리뷰: 독립 P15-T2 명세·품질 리뷰 승인, 차단 사항 없음. 보존된 실행 로그 및 diff를 대조했다. 전체129 XML은 독립32 재실행으로 덮어써졌으므로 reviewer는 전체 개별 XML까지 재검증했다고 주장하지 않는다.
- 한계/관찰: H2의 기존 chat schema 및 비관련 translation/charge scheduler `SKIP LOCKED` 경고, 중복 JSON/Netty/Security 경고, Gradle deprecation이 있었다. 새 실패는 없으며 무관한 코드는 수정하지 않았다. 이번 HTTP는 MockMvc 검증이며 삭제 후 real-socket 시나리오는 Gate에서 수행한다.
- 자원: 전용 TMPDIR/JVM temp 경로를 사용했고 Gradle 종료 후 관련 Gradle/Redis 프로세스 잔류가 없음을 확인했다. 커밋·worktree·운영 DB 작업 없음.
### Task 15.3 적격 query와 결정적 개인정보 정리
**Goal 실행 `P15-T3`:** 주어진 `now`에서 정확히 7일 이상 지난 DELIVERED 선물의 12개 배송 개인정보를 idempotent하게 지운다.
- **시작 조건:** `P15-T2` 완료. local JPA test는 변경한 nullable entity를 H2에서 검증하며 production schema 확인/DDL 실행을 요구하지 않는다.
- **완료 증거:** 실제 `GiftRepository` predicate와 scheduler/service의 embedded H2 JPA 테스트에서 경계/상태/null/보존/재실행 확인. 기존 `@DataJpaTest`가 지정 datasource를 대체함을 확인했으므로 MySQL-mode 또는 MySQL 실서버 검증으로 표시하지 않는다.
- **범위 밖:** native SQL로 auditing 우회, erased metadata, push/refund, 상태 전이, 미전달 상태 데이터 정리.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt`
**Interfaces:**
- Consumes: `P15-T2`의 nullable `GiftDelivery`.
- Produces: `GiftRepository.findDeliveredGiftsWithDeliveryBeforeOrAt(cutoff: LocalDateTime): List<Gift>`; `GiftScheduler.eraseDeliveredGiftPersonalData(now: LocalDateTime): Int`는 eligible delivery를 dirty checking으로 비우고 변경 건수를 반환한다.
- [x] **RED:** `shouldEraseDeliveredGiftWhenDeliveredAtEqualsSevenDayCutoff`, `shouldIgnoreDeliveredGiftOneSecondNewerThanSevenDayCutoff`, `shouldEraseOlderHistoricalDeliveredGifts`, `shouldIgnoreNullDeliveredAtAndNonDeliveredGifts`를 실제 repository/JPA fixture로 작성했다.
- [x] **RED:** `shouldEraseRemainingFieldsFromPartiallyClearedGift`로 12개 중 하나씩 남은 12개 행을 검증하고, 최초 all-null 및 재실행 변경 수 0 테스트를 추가했다.
- [x] **RED:** `shouldPreserveGiftMemberPaymentConsentReviewAndBusinessTimestamps`에서 실제 Member·UseCan·GiftReview와 선물·배송 업무 필드를 flush/clear/reload로 비교했다. 실제 변경된 행의 updatedAt은 고정하지 않고, no-op의 추가 수정 없음만 검증했다.
- [x] **RED 확인:** scheduler 기존 14개 통과 후 신규 8개가 미구현 query/service `NoSuchMethodException`으로 실패했다. 임시 reflection 호출은 최종 구현에서 정적 호출로 교체했다.
- [x] **GREEN:** 기존 JPQL exists 패턴의 적격 조회와 `@Transactional eraseDeliveredGiftPersonalData(now)`를 추가했다. 정확히 12개 필드만 dirty checking으로 null 처리하며 완전 null은 skip한다.
- [x] **GREEN 확인:** scheduler 22개 실패·오류·skip 0. 결제/동의/리뷰/Member/업무 시각 보존과 no push/refund를 확인했다.
- [x] **REFACTOR:** 새 테스트 한 줄의 lint 길이 위반을 줄바꿈으로 수정한 뒤 같은 focused/lint 명령을 통과했다. 독립 실제 재실행과 단계 리뷰도 통과했다.
#### P15-T3 구현·검증 기록 — 2026-10-23
- Baseline: `./gradlew --no-daemon test --tests '*GiftSchedulerTest' ktlintCheck` exit 0(27초), 기존 14개 통과.
- RED: `./gradlew --no-daemon test --tests '*GiftSchedulerTest'` exit 1(33초), 22개 중 신규 8개 메서드 부재 실패, 기존 14개 통과. fixture는 실제 DB 저장 후 실패했으며 오타/설정 실패가 아니다.
- GREEN: 첫 실행 JPA22개 통과 후 새 테스트의 130자 초과 lint 실패를 수정했다. 최종 baseline 명령 exit 0(36초), 22개 실패·오류·skip 0.
- 독립 검증: 첫 실행은 `UP-TO-DATE`였으므로 실제 재실행으로 계산하지 않았다. `./gradlew --no-daemon test --rerun --tests '*GiftSchedulerTest' ktlintCheck` exit 0(23초), XML 22개 실패·오류·skip 0, timestamp `2026-10-23T11:05:56` 확인.
- 범위: repository query 16줄, 삭제 업무 메서드 42줄 및 실제 DB 테스트 8개를 추가했다. T2/SQL/기존 업무 로직은 유지했다. 2020년 이력, 다른 상태 6개, 부분 삭제 각 필드, 재실행 및 실제 UseCan 보존을 포함한다.
- 리뷰: P15-T3 명세·품질 승인, 차단 사항 없음. reviewer가 본 최신 XML은 lead 재실행 산출물이며 implementer의 `11:00:42` 실행과 구분했다.
- 검증 한계 정정: 계획과 설정 문자열에는 MySQL-mode가 있으나 `@DataJpaTest`는 기본 embedded H2로 교체한다. 실제 JDBC URL에는 MODE가 없었으므로 H2 JPA roundtrip으로만 보고한다. 기존 무관한 DDL/JSON/Gradle 경고와 LSP 부재는 유지하며 MySQL 검증·운영 반영은 하지 않았다.
### Task 15.4 독립 scheduler entrypoint와 분산락
**2026-10-23 리뷰 정정:** 신규 wrapper의 즉시 `finally unlock`은 메서드 반환 후 이루어지는 Spring commit보다 빨라 동일 reminder 재진입을 허용할 수 있다. 아래 최초 패턴 요구는 이 정정이 우선한다. 신규 세 작업은 lock 획득 성공 시 `finally`에서 기존 `RecommendationSnapshotRefreshService.unlockAfterTransaction`과 같은 transaction synchronization 패턴으로 해제를 등록하고 `afterCompletion`에서 소유권을 확인해 해제한다. 실제 transaction이 없는 직접 호출에서는 업무 종료 후 즉시 해제한다. 기존 :00 동작은 범위 밖으로 유지한다. commit/rollback 직전에는 재진입이 거부되고 완료 뒤에는 허용되는 실제 transaction regression을 추가한다.
**Goal 실행 `P15-T4`:** 기존 만료/알림 업무 메서드를 신규 시간별 scheduler wrapper에 등록하고 각각 독립 lock lifecycle을 적용한다.
- **시작 조건:** `P15-T3` 완료.
- **완료 증거:** scheduler reflection/lock test와 기존 업무 메서드 delegation 회귀 통과.
- **범위 밖:** worker 실행시간 가정, 작업 간 전역 비중첩 보장, 기존 expiry/reminder 업무 로직 변경, Clock config 및 HTTP endpoint.
**Files:**
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt`
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt`
- Test reference: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/scheduler/CreatorRankingSnapshotSchedulerTest.kt`
**Interfaces:**
- Consumes: `GiftScheduler.eraseDeliveredGiftPersonalData(now: LocalDateTime): Int`, 기존 `markExpiredRecipientAddressGiftsUndeliverable(now: LocalDateTime): Int`, `sendDeadlineReminderGifts(now: LocalDateTime): Int`.
- Produces: `runExpiredRecipientAddressGiftExpiration()`, `runDeliveredGiftPersonalDataErasure()`, `runDeadlineReminderGifts()` parameterless `@Scheduled`/`@Transactional` wrapper. 각각 `0 10 * * * *`, `0 20 * * * *`, `0 40 * * * *`, `zone="Asia/Seoul"`. Lock key는 각각 `lock:gift-expired-recipient-address-undeliverable`, `lock:gift-delivered-personal-data-erasure`, `lock:gift-deadline-reminders`. 기존 `runExpiredTrackingRegistrationGiftCancellation()`의 :00 cron/key는 바꾸지 않는다.
- [x] **RED:** JUnit MethodSource로 세 진입점의 cron/zone/transaction, 실제 delegate 및 now, lock 성공/실패/예외/소유권 5종을 검증하는 15사례를 작성했다.
- [x] **RED 확인:** 기존 22개는 통과, 신규 15개는 wrapper 부재로 실패했다. 리뷰 후 실제 beforeCommit 조기 해제 및 rollback/소유권 회귀의 추가 RED도 확인했다.
- [x] **GREEN:** 세 parameterless wrapper와 독립 key를 등록했다. 리뷰 정정에 따라 획득 성공한 신규 lock은 finally에서 transaction synchronization을 등록하고 afterCompletion에 해제한다. 기존 :00과 업무 로직은 유지한다.
- [x] **GREEN 확인:** 최초 37개 통과 후 실제 transaction 완료 경계 15사례를 보강해 최종 52개 실패·오류·skip 0을 확인했다. Kotlin 컴파일과 lint도 통과했다.
- [x] **REFACTOR:** 신규 세 wrapper의 transaction 완료 해제를 작은 helper로 공통화했다. scoped 재리뷰는 차단 사항 해결 및 명세·품질 승인 판정이다. 작업 간 비중첩이나 Redis 장애 시 exactly-once를 주장하지 않는다.
#### P15-T4 구현·리뷰 수정 기록 — 2026-10-23
- 최초 TDD: `./gradlew --no-daemon test --rerun --tests '*GiftSchedulerTest' ktlintCheck` baseline22 통과(24초), 신규 wrapper RED37개 중15실패(35초), GREEN37개/lint 통과(45초). lead 실제 재실행37개/lint도 exit0(27초), XML `11:20:16` 확인.
- 리뷰 차단: @Transactional 메서드의 finally가 proxy commit보다 먼저 unlock하여 다른 인스턴스의 recipient reminder 중복 실행 구간을 만들 수 있음을 확인했다. `PLAN-DEC-020`으로 신규 세 작업의 해제 계약을 정정하고 재현 테스트를 먼저 추가했다.
- 수정 RED: 실제 JpaTransactionManager의 TestTransaction beforeCommit/rollback 경계에서 52개 중15실패(33초)를 확인했다. callback을 수동 호출해 통과시키지 않았다.
- 수정 GREEN: `./gradlew --no-daemon test --tests '*GiftSchedulerTest' ktlintCheck` 첫 실행은120초 도구 timeout으로 결과 미확정. 잔여 JVM 없음 확인 후300초 제한으로 재실행하여 exit0(23초), 52개 실패·오류·skip0, XML `2026-10-23T11:35:27` 확인. compile/lint 동일 입력은 UP-TO-DATE이며 테스트는 실제 실행됐다.
- 보장: 정상 transaction의 beforeCommit/afterCommit과 rollback 직전까지 모의 경쟁 lock 획득이 거부되고 완료 후 허용된다. 실패 획득은 callback 미등록, 완료 시 소유권 상실은 unlock 없음, non-transaction 직접 호출은 업무 후 해제를 검증했다. 실제 Redis 다중 인스턴스 검증은 아니다.
- 재리뷰: 원 reviewer가 단일 blocker를 ADDRESSED, Spec PASS/Quality PASS로 판정했고 신규 회귀 없음. 실제 scheduler bean proxy/등록은 Gate에서 별도로 확인한다.
- 자원/범위: commit 테스트는 fixture를 생성하지 않으며 종료 후 Gift/Delivery count0을 확인했다. 기존 fixture 테스트는 rollback 유지. 프로세스 잔류 없음, 기존00/T2/T3/SQL 보존, 커밋·푸시 없음.
### Phase 15 Gate
**최종 리뷰 차단 및 보강 범위:** 전체 테스트·빌드가 통과했으나 신규 :10/:40 작업이 HTTP 배송지 등록과 병행될 때 오래된 Gift/Delivery를 커밋해 주소·동의·상태를 덮어쓸 수 있다. 단순 scheduler 분산락만으로 HTTP 요청을 보호할 수 없으므로 아래 `P15-R1`을 먼저 완료한다. 이미 완료된 T2~T4 기록은 되돌리지 않는다.
**Goal 실행 `P15-GATE`:** nullable DB 배포 순서, 개인정보 제거, 기존 API와 업무 데이터 보존을 최종 확인한다.
- **시작 조건:** `P15-T1`~`P15-T4`의 구현 승인 및 완료 증거 충족.
- **완료 증거:** gift focused regression, lint, 테스트 제외 build, 격리된 실제 HTTP QA, schema/배포 checklist 기록.
- **범위 밖:** 운영 DB 변경/삭제 실행, 신규 production HTTP endpoint, 실제 push, backup/log/cache 삭제 주장, `updatedAt` 보존 강제. 전체 테스트는 최신 실행 승인에 따라 1회 시도하되 무관한 실패를 고치지 않는다.
```bash
./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftEntityMappingTest" --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftSchedulerTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest"
./gradlew ktlintCheck
./gradlew build -x test
```
**Expected:** 명령 결과는 실행 후 기록하며 현재 통과를 주장하지 않는다. HTTP QA는 독립 fixture를 가진 격리 환경에서 erase business method를 실행한 후 real socket의 기존 SENT/RECEIVED/admin GET으로 재조회한다. 실제 정리 HTTP endpoint는 추가하지 않는다. Production DB를 대상으로 하지 않으며 실제 push가 발생하지 않는다.
수동/deployment 확인:
- [x] 격리 실제 HTTP로 전/후 `SENT`/`RECEIVED`/admin 응답의 shape, 정확한 빈 주소/null, nickname, DELIVERED no-action/CTA를 확인했다.
- [x] SQL 문서와 엔티티를 대조해 네 sender column의 nullable 완화와 기존 길이/comment 보존 및 destructive DML 없음을 확인했다. 실제 운영 schema metadata/DDL 적용은 별도 rollout 조건이며 이번 Gate에서 수행하지 않았다.
- [x] 모든 instance의 nullable reader 준비 후 첫 정리 실행, 구버전 non-null reader rollback 위험, 코드만으로 삭제 데이터 복구 불가를 배포 안내에 명시했다. 운영 준비 자체를 확인했다는 의미는 아니다.
- [x] 실제 Spring에서 기존00과 신규10/20/40 등록을 확인하고 erasure의 no push/refund를 검증했다. PRD §8.15의 기존 expiry 팬 알림과 현재 코드의 불일치는 별도 관찰로 유지한다. R1은 새 예약 경로의 동시 저장 안전성만 보강하고 새 업무 정책/푸시는 추가하지 않았다.
### Phase 15 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 36 | `P15-T1` | 승인된 계약 | 아니요 | 문서 검증 완료 |
| 37 | `P15-T2` | `P15-T1` 검증 + 별도 구현 승인 | 아니요 | 승인 전 구현 금지 |
| 38 | `P15-T3` | `P15-T2` | 아니요 | nullable mapping 완료 후 진행 |
| 39 | `P15-T4` | `P15-T3` | 아니요 | query/service 완료 후 연결 |
| 40 | `P15-GATE` | `P15-T2`~`P15-T4` 완료 | 아니요 | Gate 실패 소유 Task 수정 |
```text
P15-T1 문서 검증 → 별도 구현 승인 → P15-T2 → P15-T3 → P15-T4 → P15-GATE
별도 rollout gate: DDL review + 별도 실행 승인 → compatible nullable schema 적용 및 모든 instance reader 확인 → 첫 erasure 허용
```
### Phase 15 Decision Log 추가
기존 결정 및 Phase14 기록은 변경하지 않고 다음 결정만 추가한다.
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|---|---|---|---|---|---|
| 2026-10-23 | `PLAN-DEC-013` | 확정 | 정리 조건은 DELIVERED 및 non-null `deliveredAt <= now.minusDays(7)`이며 과거 누적 대상도 포함한다. 정확한 12개 배송 개인정보만 null 처리하고 완전 null 재실행은 변경 0건이다 | 승인 계약, PRD `DEC-017` | `P15-T2`~`P15-T3`, `GIFT-018` |
| 2026-10-23 | `PLAN-DEC-014` | 확정 | sender 필수 request 계약과 기존 public response schema를 유지하고 non-null 응답 문자열은 empty string으로 매핑한다. `updatedAt`은 기존 `BaseEntity @PreUpdate`로 관리한다 | 승인 계약, PRD `DEC-018`; auditing 우회 금지 | `P15-T2`, `P15-GATE` |
| 2026-10-23 | `PLAN-DEC-015` | 확정 | 실제 schema 확인/승인된 네 sender column nullable DDL 및 모든 instance nullable reader 이후 첫 erase를 배포 gate로 둔다. 기존 SQL 하단 ALTER로 fresh install 최종 schema도 nullable을 지원한다. code rollback은 삭제 데이터 복구가 불가능하고 자동 운영 SQL/backup 변경은 하지 않는다 | 데이터 삭제와 nullable reader 전환의 기술적 배포 제약 | `P15-T1`~`P15-GATE` |
| 2026-10-23 | `PLAN-DEC-016` | 확정 | scheduler는 :00/:10/:20/:40에 독립 lock/transaction entrypoint를 두고 기존 expiry/reminder 처리는 바꾸지 않는다. 작업 간 비중첩을 보장하지 않는다 | 승인된 scheduler 등록 계약 | `P15-T4`, `P15-GATE` |
| 2026-10-23 | `PLAN-DEC-017` | 확정 | 변경 SQL은 30줄 미만이므로 기존 `gift-schema.sql` trailing ALTER에 기록한다. CREATE block은 고치지 않으며 기존 DB 적용 및 fresh install 순서, DB 실행 미수행을 구분한다 | latest SQL placement decision, 기존 5줄 ALTER | `P15-T1`, `P15-T2`, `P15-GATE`, PRD `DEC-019` |
| 2026-10-23 | `PLAN-DEC-018` | 확정 | Kotlin sender property 다섯 개를 nullable로 하고 이미 nullable인 `sender_address_detail`을 제외한 네 DB 열만 ALTER한다 | PRD `DEC-020` | `P15-T2`, `GIFT-019` |
| 2026-10-23 | `PLAN-DEC-019` | 확정 | 신규 schedule wrapper는 parameterless이고 각 wrapper가 기존 시간관례의 `LocalDateTime.now()`를 delegate에 전달한다. 정확한 독립 schedule와 lock key는 goal Interfaces에 명시한다 | 현재 `GiftScheduler.kt` 구현과 승인 cron | `P15-T4`, PRD `DEC-021` |
| 2026-10-23 | `PLAN-DEC-020` | 정정 | 신규 세 wrapper의 분산락은 DB commit/rollback 후 afterCompletion에서 해제한다. transaction이 없는 직접 호출은 기존 finally 해제 의미를 유지한다. 기존 :00은 변경하지 않는다 | T4 리뷰: 커밋 전 unlock의 중복 reminder 재진입 위험; 기존 recommendation transaction synchronization 패턴 재사용 | `P15-T4`, `P15-GATE` |
### Phase 15 문서 초안 기록
아래는 최종 검증 전 초안 작성 시점의 기록이며, 최신 상태는 다음 `P15-T1 최종 문서 검증`을 따른다.
- 상태: 문서 계획 승인, 구현 대기. `P15-T1`의 lead 문서 최종 검증이 미완료다.
- 무엇을: PRD 및 이 Phase 계획에 적격 기준, 개인정보 필드, response mapping, scheduler 분, migration 순서와 TDD 검증을 명시했다.
- 왜: 구현 전 승인된 정확한 동작과 data-loss 배포 순서를 고정하기 위해서다.
- 어떻게: PRD/plan만 편집했다. lead가 기존 `gift-schema.sql` 끝에 nullable ALTER 문서를 준비했으며, 이 수정 단계에서는 SQL을 실행하지 않았다. 계획에는 test/SQL 실행 결과를 기재하지 않았다.
- 남은 항목: lead가 `./gradlew --no-daemon tasks --all`과 `git diff --check` 실행 후 `P15-T1` 검증 checkbox를 갱신한다. 이후에도 `P15-T2` 시작에는 구현 별도 승인이 필요하다.
- 다음 행동: `P15-T1` 최종 문서 검증, 구현 승인 전 `P15-T2` 착수 금지.
### P15-T1 최종 문서 검증 — 2026-10-23
- 상태: 문서 작성 및 검증 완료. `P15-T2`~`P15-T4`, `P15-GATE`는 미착수다.
- 무엇을: 기존 PRD에 `GIFT-018`~`GIFT-021`과 결정 기록을 추가하고, 이 계획에 nullable 저장/응답 호환, 7일 경과 삭제, 세 스케줄 등록 및 focused TDD 검증을 추가했다. 별도 계획 폴더나 SQL 파일은 만들지 않았다.
- 왜: 승인된 삭제 대상 12개와 보존 데이터, 기존 데이터 소급 적용, 10/20/40분 실행 시각을 구현 전에 같은 기준으로 추적하기 위해서다.
- 검증: `./gradlew --no-daemon tasks --all` exit 0, `BUILD SUCCESSFUL in 12s`. `test`, `ktlintCheck`, `build` 명령이 등록돼 있음을 확인했으며 테스트나 빌드 성공을 뜻하지 않는다.
- 검증: `git diff --check` exit 0. `git diff --name-only`에서 이 디렉터리의 `prd.md`, `plan-task.md`, `gift-schema.sql` 세 파일만 변경됐음을 확인했다. Phase14의 완료 체크박스·결정·검증 기록은 보존했다.
- 수동 대조: 발신자 Kotlin 필드 다섯 개와 DB ALTER 열 네 개, 삭제 대상 열 12개, 정확히 7일인 경계 포함, 기존 데이터 포함, 응답의 빈 문자열/nullable 구분, 신규 cron/zone/lock 계약, 요구사항과 Task 연결을 확인했다. 추가 ALTER 본문은 5줄이며 기존 타입·길이·COMMENT를 유지하고 데이터 삭제 구문은 없다.
- 한계: 사용 가능한 LSP 도구가 없어 문서/SQL 대조와 diff 검사로 검증했다. 실제 MySQL 구문 실행, 운영 schema 확인, 애플리케이션 기동, 기능 테스트, ktlint 및 빌드는 수행하지 않았다. 문서/SQL 기록만 변경했으므로 focused/전체 테스트를 생략했다. 기존 Gradle deprecation 경고는 이번 범위 밖이다.
- 남은 항목: 사용자 구현 승인 후 `P15-T2`의 첫 미완료 RED부터 실행한다. 운영 DDL 적용과 실제 데이터 삭제는 별도 배포 승인/조건을 충족해야 하며, 커밋·푸시는 하지 않았다.
### P15-R1: 신규 스케줄과 배송지 등록의 경합 보강
**최종 재리뷰 정정:** 수취인 주소 경합은 공유 Gift→Delivery 잠금과 최신 refresh로 해결됐다. 다만 자동취소 후보의 기존 batch `@Lock` 제거는 동일 발신자 복수 선물의 환불 순서를 바꿀 수 있어 복원한다. `findReceivedGiftsWithTrackingDeadlineBefore`에 기존 `PESSIMISTIC_WRITE`와 `order by g.id`를 적용하여 첫 환불 전 후보를 모두 잠근다. reviewer는 이 복원으로 새 A/B 회귀가 차단됨을 확인했고 공통 `CanPaymentService` Member 잠금 변경 요구를 범위 확대로 철회했다. 공통 결제 로직은 수정하지 않는다.
**Goal:** 새로 자동 실행되는 배송지 만료·기한 알림이 동시에 성공한 수취인 배송지 등록 또는 진행 상태 변경을 덮어쓰지 않는다.
- **시작 조건:** 최종 Oracle 리뷰 S3 차단 재현. 전체 테스트·lint·build는 수정 전 `./gradlew --no-daemon test --rerun ktlintCheck build` exit0(9분38초).
- **Files:** `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/persistence/GiftRepository.kt`, 필요 시 같은 디렉터리의 `GiftDeliveryRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt`, `GiftAdminService.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt` 및 영향받는 기존 선물 테스트.
- **완료 증거:** 실제 독립 transaction·동시 호출에서 reminder 읽기/주소 등록/commit 순서와 반대 순서, expiry/주소 등록 경합의 회귀 통과. 동일 Gift 행 잠금 또는 기존 구조에 맞는 최소 직렬화로 최신 상태·배송지를 확인하며 입력을 잃지 않는다. 최종 scoped 재리뷰 승인.
- **범위 밖:** 신규 기한/푸시/환불 정책, 전역 isolation 변경, 의존성·테이블·버전 컬럼, 운영 DB 실행, 관련 없는 도메인 수정. 단순 DynamicUpdate만으로 상태 경합을 덮지 않는다.
- [x] **RED:** 실제 독립 transaction/latch 11개 사례에서 성공한 주소 등록 값이 reminder/운영 변경의 오래된 entity 커밋으로 null이 되거나 상태가 되돌아가는 실패를 재현했다.
- [x] **GREEN:** 참여 writer는 Gift→Delivery 잠금과 `refresh(..., PESSIMISTIC_WRITE)`를 사용하고, scheduler는 최신 상태·주소·기한·알림표시를 재검사한다. 자동취소 후보의 기존 batch lock도 ID 순서와 함께 복원했다. 공통 결제 로직과 기한 정책은 변경하지 않았다.
- [x] **GREEN 확인:** 지정4클래스181개와 실제 HTTP 삭제 QA·lint 통과 후, 자동취소 잠금 복원 RED/GREEN과2클래스104개 회귀·lint도 통과했다. 최종 전체2396개 테스트·lint·build를 재실행해 통과했다.
- [x] **REFACTOR/리뷰:** 기존 S3 차단 해소와 최신 Gift/Delivery 읽기를 reviewer가 승인했다. 최종 자동취소 batch lock3줄 복원 및 회귀도 별도 scoped evidence 리뷰 APPROVED. 최신 전체2396개/lint/build 통과로 Gate를 완료했다.
### P15-GATE 1차 검증 및 최종 리뷰 — 2026-10-23
- 실제 HTTP: 기존 AdminGiftControllerTest에 `shouldEraseDeliveredGiftAndPreserveRealJwtHttpContracts`를 추가했다. 실제 JWT localhost GET28회로 익명401/무관한 회원 거부, 발신자·수취인·관리자 전후 응답을 확인했다. 최초 삭제1/재실행0, persisted null12, Gift/Delivery 각1행과 목록 유지, 실제 결제/동의/리뷰/시각 보존 및 no refund/push를 확인했다.
- 실제 Spring: injected scheduler의 TransactionInterceptor proxy와 ScheduledAnnotationBeanPostProcessor 등록4개(00/10/20/40)를 확인했다. 등록 증거 후 테스트 context의 해당4개 작업만 cancel하여 벽시계와 fixture 경합을 막았다. production 설정이나 작업 등록은 변경하지 않았다.
- HTTP 검증 명령: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest.shouldEraseDeliveredGiftAndPreserveRealJwtHttpContracts' --no-daemon --console=plain` exit0(44초), 해당 클래스 전체33개 exit0(35초), `./gradlew ktlintCheck --no-daemon --console=plain` exit0(18초). 최초 lint 줄바꿈 위반은 수정 후 모두 재실행했다.
- 전체 검증: `./gradlew --no-daemon test --rerun ktlintCheck build` exit0, `BUILD SUCCESSFUL in 9m 38s`. JUnit XML322개 집계에서 테스트2377개, 실패·오류·skip0을 확인했다. 이 결과는 `P15-R1` 수정 전 기준이며 수정 후 결과로 재사용하지 않는다.
- 최종 Oracle 리뷰: S1(nullable/API), S2(삭제 범위/보존), S4(HTTP/DDL) PASS, S3는 배송지 입력과 신규 reminder/expiry의 stale entity update 경쟁으로 BLOCKED. callback 이후 락 해제는 승인됐으나 HTTP 요청과 공유되는 DB 잠금 보강이 필요하다. `P15-R1`로 실패 재현·최소 수정을 추가했고 기존 완료 Task를 되돌리지 않았다.
- 환경: LSP 도구가 없어 Kotlin 컴파일·ktlint로 대체했다. 실제 MySQL/운영 DDL/운영 배포/실제 Redis 다중 인스턴스는 검증하지 않았다. QA JVM/Redis/포트 종료를 확인했고 전체 suite 후에도 java/redis-server 프로세스 잔류가 없었다. 커밋·푸시 없음.
### P15-R1 경합 수정 및 마지막 잠금 복원 검증 — 2026-10-23
- RED: `./gradlew --no-daemon test --tests '*AdminGiftControllerTest.shouldSerializeRecipientMutations'` exit1(44초), 독립 transaction 11개 실패. reminder가 먼저 읽고 주소가 커밋된 뒤 recipient5개 필드가 null로 되돌아가는 것을 실제 재조회로 확인했다.
- 수정: Gift/Delivery의 공통 locking/current read와 refresh, scheduler 후보 정렬 및 잠금 뒤 최신 조건 재검사를 적용했다. 주소 등록·수령확인·관리자 상태 변경이 같은 잠금 순서를 사용하며 새 기한 거부·푸시·환불 정책은 추가하지 않았다.
- focused: `./gradlew --no-daemon test --tests '*AdminGiftControllerTest' --tests '*GiftCommandServiceTest' --tests '*GiftAdminServiceTest' --tests '*GiftSchedulerTest' ktlintCheck` exit0(1분4초), 181개 실패·오류·skip0. 신규 양방향 경합18사례와 실제 JWT HTTP 삭제 QA가 포함됐다. 최초 lint10건은 포맷만 수정한 뒤 재실행했다.
- 재리뷰: 기존 S3는 ADDRESSED. 자동취소 후보의 batch lock 제거가 만든 A/B 환불 순서 회귀 지적은 원래 `PESSIMISTIC_WRITE` 복원과 `order by g.id`로 해소하는 것으로 범위를 합의했다. reviewer는 공통 Member/결제 잠금 변경 요구를 범위 확대로 철회했다.
- 마지막 RED: `./gradlew --no-daemon test --tests '*AdminGiftControllerTest.shouldLockExpiredTrackingBatchUntilTransactionCommits'` exit1(44초). 후보 조회 transaction이 열린 동안 두 번째 선물 취소가 먼저 완료돼 `Expected TimeoutException ... but nothing was thrown`으로 실패했다. 별도의 fixture 분류번호 중복 실패는 RED에서 제외했다.
- 마지막 GREEN: `GiftRepository`의 Lock import/annotation/ID 정렬3줄만 복원했다. 같은 테스트 exit0(44초), `./gradlew --no-daemon test --tests '*AdminGiftControllerTest' --tests '*GiftSchedulerTest' ktlintCheck` exit0(1분1초), 104개 실패·오류·skip0. 실행 SQL `order by gift0_.id for update`, `P15_BATCH_LOCK orderedCandidates=2 contenderBlockedUntilCommit=true refundCan=80 refundCount=1` 확인.
- 최종 전체 실행: `./gradlew --no-daemon test --rerun ktlintCheck build` exit0, `BUILD SUCCESSFUL in 9m 30s`. JUnit XML322개에서 테스트2396개, 실패0·오류0·skip0. 앞선 수정 전2377개/중간2395개 결과와 구분한 최신 코드 기준이다. `build/libs/sodalive-0.0.1-SNAPSHOT.jar` 산출물을 확인했다.
- 최종 실표면 증거: 최신 `AdminGiftControllerTest` XML52개 통과, `P15_HTTP erased=true status=200 success=true senderStringsEmpty=true recipientAddressNull=true trackingNull=true requiredFlags=false nicknamePreserved=true timelinePreserved=true lists=1/1/1 auth=401/denied`, `P15_DB erased=1 repeated=0 persistedNulls=12 giftRows=1 deliveryRows=1 paymentConsentReviewHistoryPreserved=true` 확인.
- 한계: 실제 MySQL REPEATABLE_READ/Redis 다중 인스턴스 실험은 아니다. LSP 도구 부재는 Kotlin compile/ktlint로 대체했다. 비관련 H2 SKIP LOCKED/DDL 경고와 Gradle deprecation은 기존 현상으로 남겼다. 최종 추가 잠금 복원의 evidence 확인 리뷰와 임시 자원 정리는 아래 최종 판정에서 기록한다.
### P15-GATE 최종 판정
- 판정: 서버 구현·검증 완료. 최종 전체 테스트2396개(322 suites), 실패·오류·skip0, lint/build exit0(9분30초). 실제 JWT HTTP와 DB roundtrip, 스케줄 등록/transaction, commit/rollback 락 해제, 주소 등록 경합18사례 및 batch lock 회귀를 포함한다.
- 리뷰: 최초 전체 Oracle 리뷰의 S3 차단은 공유 Gift→Delivery 잠금/current-read로 ADDRESSED. 그 수정이 건드린 자동취소 batch lock은 원래 비관적 잠금+ID정렬로 복원했고 마지막 scoped 리뷰 APPROVED, 남은 blocker 없음. 원 세션 재개 실패 뒤 증거를 전달한 후속 reviewer가 실제 복원 diff와 테스트를 확인했다.
- 구현상 결정: 업무 시각 보존과 기존 updatedAt auditing을 유지했다. 같은 Gift의 writer는 직렬화되므로 대기 시간이 생길 수 있지만 주소·동의·상태 유실을 방지한다. 공통 결제·회원 잔액 정책은 범위 밖으로 변경하지 않았다.
- 운영 경계: 하단 ALTER는 미실행이다. 실제 MySQL schema와 nullable reader 전체 배포를 확인한 뒤 스케줄을 운영해야 한다. 삭제 후 구버전 reader 복귀는 안전하지 않다. 백업·로그·외부 저장소 삭제는 이번 기능이 아니다.
- 전달 방식: 사용자 지시대로 현재 `test` 브랜치에서 worktree 없이 작업했고 모든 변경은 미커밋 상태다. git commit/push/stage를 수행하지 않았다.
- 최종 문서 검증: `./gradlew --no-daemon tasks --all` exit0(5초), `git diff --check` exit0. HEAD는 시작 시점과 같은 `c837ff18f82e3260c692a7c9f1884dbefccba441`, index 변경 없음이다.
- 자원 정리: 전체 테스트 및 문서 검증 종료 후 java/redis-server 프로세스와 작업 전용5개 디렉터리의 열린 파일이 없음을 확인했다. 생성했던 `gift-p15-t2.mAk3Mv`, `gift-p15-t3.McSvmT`, `gift-p15-t4.mIILMF`, `gift-p15-r1.XJMSPV`, `gift-p15-qa.EJ30P6`와 그 안의 runtime cache/임시 Tomcat/Redis/로그를 삭제하고 각 경로의 부재를 확인했다. 검증 요약은 이 계획에 보존하며 최신 JUnit/HTML/build 산출물과 공유 Gradle 캐시는 유지했다. 작업 notepad도 요약 반영 후 제거한다.
### Phase 15 커밋 전 재검증 — 2026-10-26
- 사용자 후속 승인: `/commit-with-validation`으로 Phase 15 변경 커밋을 요청했다. 위 미커밋 기록은 당시 상태로 보존하며 이번 요청은 커밋을 승인한다.
- 검증 명령: `./gradlew --no-daemon test --rerun --tests '*GiftEntityMappingTest' --tests '*GiftSchedulerTest' --tests '*GiftQueryServiceTest' --tests '*GiftAdminQueryServiceTest' --tests '*GiftCommandServiceTest' --tests '*GiftAdminServiceTest' --tests '*GiftControllerTest' --tests '*AdminGiftControllerTest' ktlintCheck`.
- 결과: exit 0, `BUILD SUCCESSFUL in 1m 18s`. 8개 클래스 229개 테스트 실패·오류·skip 0. 테스트는 재실행됐으며 컴파일과 lint는 동일 입력의 UP-TO-DATE 결과다. 실제 JWT HTTP 삭제 전후 계약, 개인정보 12개 null 저장·재실행 0건, 경합 및 자동취소 batch lock 회귀를 포함한다.
- 범위: 기존 구현의 커밋 검증이므로 전체 테스트와 패키징 빌드는 반복하지 않았다. 2026-10-23 전체 2,396개 결과와 이번 focused 결과를 구분한다. 운영 MySQL DDL·배포·실제 Redis 다중 인스턴스 검증 및 push는 수행하지 않는다.
### Phase 16: 누락 발신자 푸시와 가격 스냅샷 검증
**Goal:** 수령확인·배송지 기한 초과 시 발신자 알림을 PRD대로 발행하고, 설정 변경이 기존 선물 신청과 결제를 바꾸지 않음을 실제 흐름으로 증명한다.
- 승인: 2026-10-26 사용자 후속 작업 진행 요청. 기존 확정 정책을 보완하며 재인터뷰하지 않는다. `test` 브랜치에서 worktree 없이 수행하고 커밋·푸시·운영 DB·실제 FCM 발송은 하지 않는다.
- 기준: PRD `9.3`, `DEC-022`; 기존 완료 Task/검증 이력은 보존한다. 두 호출자의 누락이 원인이며 GiftPushService/FCM 기반을 새로 만들지 않는다.
- 의존성: `P16-T1` → `P16-T2` → `P16-GATE`. 공유 controller test 때문에 한 구현 담당이 순서대로 실행하고, 변경 전체의 독립 리뷰는 Gate에서 수행한다.
#### Task 16.1 누락 발신자 푸시 보완
**Goal ID: `P16-T1`**
- 시작 조건: 승인된 기존 푸시 문구·대상·딥링크와 현재 누락 호출자 확인.
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftScheduler.kt`.
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/adapter/out/scheduler/GiftSchedulerTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`; 기존 `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftPushServiceTest.kt` 재사용.
- Interfaces: 기존 `confirmDelivery(...)` 및 `markExpiredRecipientAddressGiftsUndeliverable(now)` 성공 분기에 기존 send 메서드 호출만 추가한다. API·cron·잠금·상태/환불 정책 불변.
- 범위 밖: 새 푸시 문구/알림종류, 직접 취소/관리자 취소 알림, 기한 계산이나 완료 허용 조건 변경.
- 완료 증거: 발신자에게 정확한 applicationNo로 1회 호출, 수취인 추가 알림0, 거부/재실행의 추가 호출0, 기존 경합 및 rollback 회귀와 이벤트 계약 통과.
- [x] **RED:** 수령확인의 stale 무푸시 기대를 정확한 발신자1회·추가0회로 교체하고 반복/거절·만료3상태/비대상/재실행 검증을 보강했다. 승인된 새 발행에 관련된 경합 기대값만 변경했다.
- [x] **RED 확인:** `./gradlew --no-daemon test --tests '*GiftCommandServiceTest' --tests '*GiftSchedulerTest'` baseline102개 통과 후 production 수정 전103개 중5개가 실제 누락 호출 `WantedButNotInvoked`로 실패했다.
- [x] **GREEN:** 상태 전이 성공 분기에 기존 `sendDeliveredToSender`, `sendUndeliverableToSender` 호출 두 줄만 추가했다. 공통 푸시/금융/잠금/cron/DTO는 불변이다.
- [x] **GREEN 확인:** 관련4클래스168개 통과, 실제 JWT 수령확인 성공·권한 거부·반복 및 actual transaction scheduler 만료의 발신자1회·수취인0회·추가0을 확인했다. 기존 payload 테스트로 문구/딥링크를 대조했다.
- [x] **REFACTOR:** 기존 취소·rollback·erasure no-push 검증과18개 경합을 유지했다. 최종 관련216개와 lint 통과, 독립 리뷰 승인. 아래 Progress에 실제 한계를 기록했다.
#### Task 16.2 기존 신청 스냅샷 보존 통합 테스트
**Goal ID: `P16-T2`**
- 시작 조건: `P16-T1` 통과. 가격/카테고리 production 동작은 이미 구현되어 있으므로 테스트를 위해 임의로 깨뜨리지 않는다.
- Test: 기존 실제 command/admin/query/결제를 주입하는 `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`의 fixture/TransactionTemplate/JWT HTTP 헬퍼를 재사용한다.
- Interfaces: `registerGift`, `updateSizePrice`, `deleteCategory`, 기존 목록/상세 GET. 신규 endpoint/helper production 코드 없음.
- 범위 밖: 가격/카테고리 정책 재설계, 새 dependency, 전체 fixture framework, 새 snapshot 필드.
- 검증: 최초 판매가80으로 신청A→판매가120으로 변경→신청B. A의 저장·조회·결제80과 B의120을 flush/clear/reload 및 HTTP로 비교한다. 카테고리 비활성화 뒤 기존A/B와 결제는 유지, 옵션에서 제외, 신규 신청은 거부되며 추가 차감/행 생성이 없다.
- [x] **검증 추가:** 실제 registerGift로 만든 A80/B120, 실제 가격 변경/카테고리 비활성화, DB reload와 JWT HTTP 목록/상세/옵션을 연결했다. 최초 기존가격 fixture 격리를 위해 클래스 초기화의 sizePrice 삭제 한 줄만 보강했다.
- [x] **실행:** 단독 테스트와 관련 클래스 통과. 기존 가격 동작의 검증 보강이므로 임의 production RED는 만들지 않았다. 최초 테스트 끝의 Mockito matcher NPE는 fixture 검증 코드 오류로 구분하고 수정 후 같은 검증을 재실행했다. 가격 production은 변경하지 않았다.
- [x] **정리:** 실제 UseCan80/120·원 결제 연결·정산·잔액100을 보존하고 비활성화 후 추가 Gift/차감0을 확인했다. 운영 데이터·실제 FCM·공유 설정은 건드리지 않았다.
#### Phase 16 Gate
**Goal ID: `P16-GATE`**
- 시작 조건: 두 Task의 실제 증거 확보. 완료 기준: 누락 알림과 가격/카테고리 시나리오 PASS, 관련/전체 회귀·lint/build·독립 리뷰 및 임시자원 정리.
- [x] 관련6클래스216개 테스트/lint를 통과했고 최종 전체 실행에서도 해당 테스트를 포함해 통과했다. 아래 실제 명령을 기록했으며 별도 미실행 명령을 실행했다고 표시하지 않는다.
- [x] 최초 전체 실행15분 timeout을 성공으로 계산하지 않았다. 상세 로그를 남긴20분 제한 재실행은18분22초에 exit0, 전체2400개·lint·build 통과했다.
- [x] 실제 localhost HTTP로 가격/기존 신청 보존, 수령확인 푸시 경계와 만료 transaction을 검증했다. 실제 transport/기기 실수신은 미검증이다.
- [x] 독립 reviewer APPROVE, 차단 사항 없음. PRD 가격/푸시 성공 기준2개만 새 증거로 완료 표시하고 과거 나머지 항목은 유지했다.
- [x] `tasks --all`·diff 검사를 통과했고 전용 임시자원 삭제/부재를 확인했다. 기존 사용자 GradleDaemon과 공유캐시를 보존했으며 커밋·푸시는 하지 않았다.
#### Phase 16 Progress
- 2026-10-26: 기존 PRD에 따른 후속 작업을 승인받아 위 계획을 기록했다. P16-T1/T2/Gate는 아직 구현 검증 전이며 Phase15 기록은 보존했다.
#### P16-T1/T2 실행 결과 — 2026-10-26
- Baseline: `./gradlew --no-daemon test --tests '*GiftCommandServiceTest' --tests '*GiftSchedulerTest'` exit0(31초),102개 통과. 동일 명령 RED는103개 중5개 누락 호출 실패(52초), production 두 줄 수정 후 관련4클래스168개 통과(1분16초).
- Snapshot: 실제 creator/member/Charge300 및 활성카테고리를 준비해 A80결제(잔액220)→가격120→B120결제(잔액100)→카테고리 비활성화를 검증했다. 실제 등록·결제·DB flush/clear/reload 및 TestRestTemplate JWT GET이다. 기존 신청 이름/가격/UseCan ID·정산 보존, 옵션 제외, 이후 신청 거부/추가차감0을 확인했다.
- 단독 명령: `./gradlew --no-daemon test --tests '*AdminGiftControllerTest.shouldPreserveExistingGiftSnapshotsAfterPriceChangeAndCategoryDeactivation'` exit0(51초). 최초 테스트 matcher NPE는 업무결함 RED가 아니며 정확한80/120 호출 검증과 총2회 확인으로 수정했다.
- 관련 최종 명령: `./gradlew --no-daemon test --tests '*GiftCommandServiceTest' --tests '*GiftSchedulerTest' --tests '*GiftPushServiceTest' --tests '*AdminGiftControllerTest' --tests '*GiftAdminServiceTest' --tests '*GiftQueryServiceTest' ktlintCheck` exit0(1분26초),51+52+11+55+28+19=216개,실패·오류·skip0. 최초 들여쓰기 lint2건은 수정 후 재실행했다.
- 실표면: `P16_CONFIRM_HTTP status=200 success=true rejected=true repeated=false senderPush=1 recipientPush=0`, `P16_EXPIRY_DB transitioned=1 repeated=0 senderPush=1 recipientPush=0`. JWT 미인증401·발신자 수령확인 거부·재요청 거부, 원 결제/시각 보존을 함께 검증했다.
- 이벤트 범위: 실제 HTTP/transaction 호출자에서 mock GiftPushService 경계의 정확한 호출을 확인하고 기존 GiftPushServiceTest에서 실제 FcmEvent payload를 검증했다. 실제 FCM 전송/기기 수신이나 외부 provider 성공을 증명한 것은 아니다.
#### P16-GATE 최종 검증 — 2026-10-26
- 전체1차: `./gradlew --no-daemon test --rerun ktlintCheck build`는900초 도구 제한으로 종료됐다. 로그는 `:test`에서 끝났고 실패 메시지는 없었으나 성공으로 계산하지 않았다. 해당 실행의 JVM 잔류 없음 확인 후 재실행했다.
- 전체2차: `./gradlew --no-daemon test --rerun ktlintCheck build --info`를1200초 제한으로 실행해 exit0, `BUILD SUCCESSFUL in 18m 22s`. 최신 JUnit XML322개에서 테스트2400개,실패0·오류0·skip0 확인. lint/build도 성공했다.
- 최신 실제 HTTP 증거: AdminGiftControllerTest55개 성공 XML에서 수령확인·만료 senderPush1/recipientPush0 및 `P16_SNAPSHOT_DB deactivated=false/true A=80 B=120 usageIdsPreserved=true actualUseCan=80,120 memberBalance=100 chargeBalance=100 gifts=2 usesDelta=2 chargesDelta=0 listCount=2`를 확인했다.
- 리뷰: 최초 reviewer 결과 수집이 세션 소실로 실패해 승인 근거로 쓰지 않았다. 별도 read-only reviewer가 현재 diff/소스/테스트/보존로그를 대조해 범위 한정 APPROVE,차단 사항 없음으로 판정했고 보고서를 파일로 보존했다. reviewer가 통과를 확인한216개와 lead의 이후 전체2400개를 구분한다.
- 검증 한계: LSP 도구가 없어 Kotlin 컴파일·ktlint로 대체했다. 기존 H2/비선물 SKIP LOCKED·duplicate JSON·Netty·Gradle 경고는 수정 범위 밖이며 새 테스트 실패는 없다. 운영 MySQL/실제 FCM/배포는 미실행이다. 기존 DataGrip·작업 전부터 있던 GradleDaemon은 사용자 자원으로 보존한다.
- 최종 문서/자원: `./gradlew --no-daemon tasks --all` exit0(6초), `git diff --check` exit0, index 변경 없음, HEAD는 시작 시점의 `d052dd50` 유지. `build/libs/sodalive-0.0.1-SNAPSHOT.jar`와 최신 JUnit/HTML 산출물을 확인했다. 작업 root `gift-p16.xKuBMZ`의 열린 파일과 이번 JVM/Redis 잔류가 없음을 확인한 뒤 내부 캐시·임시 Tomcat·Redis·로그·메모를 제거하고 경로 부재를 확인했다. 작업 전08:44 시작한 기존 GradleDaemon PID25133과 공유 Gradle cache/build 산출물은 보존했다.
- 최종 판정: `P16-T1`, `P16-T2`, `P16-GATE` 완료. 이번 production 수정은 호출 두 줄이며 가격/카테고리 코드는 불변이다. 변경은 현재 test 브랜치의 미커밋 상태로 남긴다.
#### Phase 16 커밋 전 재검증 — 2026-10-26
- 사용자 `/commit-with-validation` 요청으로 기존 Phase 16 변경의 커밋을 승인받았다. 위 미커밋 기록은 당시 실행 결과로 보존한다.
- 검증 명령: `./gradlew --no-daemon test --rerun --tests '*GiftCommandServiceTest' --tests '*GiftSchedulerTest' --tests '*GiftPushServiceTest' --tests '*AdminGiftControllerTest' --tests '*GiftAdminServiceTest' --tests '*GiftQueryServiceTest' ktlintCheck`.
- 결과: exit 0, `BUILD SUCCESSFUL in 1m 19s`. 최신 JUnit XML 6개에서 테스트 216개, 실패·오류·skip 0을 확인했다. 테스트는 재실행됐으며 컴파일과 lint는 동일 입력의 UP-TO-DATE 결과다.
- 범위: 기존 변경의 커밋 검증이므로 관련 회귀를 재실행하고 전체 테스트·패키징 빌드는 반복하지 않았다. 위 전체 2,400개 결과는 이전 Gate 기록이며 이번 재실행 결과와 구분한다. 운영 MySQL·실제 FCM·배포는 미검증이다.
### P16-R1: 수령확인 완료 시각 정밀도 회귀 수정
**Goal:** OS/JDK의 소수초 정밀도와 무관하게 수령확인 응답 `deliveredAt`과 DB 재조회값을 정확히 일치시킨다.
- 시작 조건: 배포 중 `AdminGiftControllerTest.kt:581` 실패 보고 및 이전과 같은 수정·재발 방지 문서화 승인.
- 근거: 이전 `82ce6f88`/`P14-T4`는 관리자 취소 시각의 나노초→DB 반올림 차이를 초 단위 정규화로 해결했다. 이번 `confirmDelivery`는 raw `now()`를 그대로 쓰고 `delivered_at` DDL은 소수초 없는 TIMESTAMP다. CI actual/expected 원문은 미제공이므로 고정 입력의 실제 DB roundtrip으로 실패 메커니즘을 재현한다.
- Files: Modify `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt`; Test 기존 `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt`, 필요 시 `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/GiftCommandServiceTest.kt`; Verify 기존 `src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt`의 P14 회귀; Docs `AGENTS.md`, `docs/agent-guides/코드스타일.md`, `docs/agent-guides/테스트스타일.md`, 기존 PRD/plan.
- Interfaces: 기존 `confirmDelivery`와 UTC ISO 응답 스키마는 유지하고, 저장/응답에 같은 초 단위 단일 값을 사용한다. 푸시 호출/수신자·잠금·상태 전이는 그대로다.
- 범위 밖: 전역 `toUtcIso`/Jackson/timezone 변경, DDL 정밀도 변경, 다른 도메인/상태 메서드의 일괄 절삭, 테스트 허용오차/문자열 잘라 비교/skip, CI 설정·운영 DB·배포·커밋·푸시.
- [x] **RED:** 고정 입력3개 중 나노초0은 통과하고123456789/999999999 두 입력은 DB 재조회와 반환값 불일치로 실패했다. 실제 transaction/flush/clear/재조회로 이전과 같은 반올림 메커니즘을 확인했다.
- [x] **GREEN:** `now().truncatedTo(ChronoUnit.SECONDS)`를 한 번 계산하고 기존 변수로 저장·응답에 사용했다. production 변경은 import와 한 줄이며 원래581행 완전 일치 비교를 유지했다.
- [x] **검증:** 관련137개와 최신 전체2403개 테스트 실패·오류·skip0, lint/build 통과. 실제 JWT HTTP 수령확인과 세 나노초 입력 모두 저장·응답의 완전 일치를 확인했다.
- [x] **재발 방지/리뷰:** AGENTS에 연결 규칙을 추가하고 코드·테스트 가이드에 DDL 선확인/단일값/경계입력/실제 DB 왕복/assertion완화금지를 기록했다. 단일 독립 리뷰 APPROVE. 문서검증·자원 정리 결과는 아래 누적한다.
#### P16-R1 재현·수정·최종 검증 — 2026-10-26
- 이전 근거: `git log -S 'truncatedTo(ChronoUnit.SECONDS)'`와 `git show 82ce6f88`에서 관리자 취소의 동일 수정과 DB roundtrip 테스트를 확인했다. 원 실패581행은 DB `deliveredAt.toUtcIso()`와 HTTP 반환 문자열 비교다. `toUtcIso`는 값을 UTC ISO로 형식화할 뿐 저장 정밀도를 보정하지 않는다.
- RED: `AdminGiftControllerTest.shouldPersistAndReturnSameSecondPrecisionDeliveryTime`을 고정0/123456789/999999999나노초로 실행했다. 3개 중2개 실패: `2026-10-22T23:59:59.123456789`는 DB에서 `.123457`, `.999999999`는 다음날`2026-10-23T00:00`으로 반올림됐고 반환값은 원래 나노초였다. CI actual/expected 원문은 없으므로 CI 원문 비교가 아니라 동일 실패 메커니즘의 로컬 재현이다.
- 수정: 기존 시각 주입 seam과 실제 repository를 쓰는 local service를 transaction 안에서 호출하고 저장·응답·DTO의 초단위 완전일치를 검증했다. 응답만 절삭하거나 assertion tolerance/substring/skip을 추가하지 않았고 기존 실제 HTTP assertion도 유지했다.
- 진행 복구: 담당 세션이 RED와 source 수정 이후 GREEN 실행을 시작하지 않아 프로세스/세션 상태를 확인했다. task 취소 조회는 not found였고 active test worker는 없었다. lead가 직접 검증을 이어받았으며 동시에 Gradle을 중복 실행하지 않았다.
- GREEN: `./gradlew --no-daemon test --tests '*GiftCommandServiceTest' --tests '*AdminGiftControllerTest' --tests '*GiftAdminServiceTest' ktlintCheck` exit0(1분31초),51+58+28=137개 실패·오류·skip0. 세 고정 입력 모두 returned/reloaded=`2026-10-22T23:59:59`, DTO=`2026-10-22T23:59:59Z`.
- 최종 전체: `./gradlew --no-daemon test --rerun ktlintCheck build --info` exit0,`BUILD SUCCESSFUL in 16m 37s`. 최신 JUnit XML322개에서2403개 테스트,실패0·오류0·skip0. 실제 HTTP 수령확인 status200/senderPush1/recipientPush0 및 고정입력3개 통과 로그를 다시 확인했다.
- 리뷰: read-only reviewer가 source/test/DDL/이전수정 및 AGENTS 연결 문서를 확인하고 APPROVE,차단 사항 없음. 결과 수집 도구 오류는 reviewer가 저장한 보고서로 확인했으며 재검토를 중복 실행하지 않았다.
- 한계: LSP 도구 없음으로 Kotlin compile/ktlint 대체. 운영 MySQL/Jenkins 재실행·배포는 하지 않았다. 기존 H2/비관련 scheduler/Gradle 경고는 별도이며 테스트 실패는 없다. 기존 사용자 GradleDaemon PID25133은 이번 작업 소유가 아니어서 보존한다. 커밋·푸시 없음.
- 문서/자원: `./gradlew --no-daemon tasks --all` exit0(13초), `git diff --check` exit0, 기존 HEAD`52e5c974` 및 index 불변. AGENTS→코드/테스트 가이드 링크와 규칙을 확인했다. 작업전용 `gift-p16-timefix.UnkCmG`의 열린 파일/작업 JVM·Redis가 없음을 확인하고 로그·캐시·메모·임시 Tomcat/Redis를 제거했으며 경로 부재를 확인했다. 최신 build/JUnit 산출물과 공유캐시는 유지했다.
- 판정: P16-R1 수정·회귀·재발 방지 문서화 완료. 현재 test 브랜치 미커밋 변경이며 CI 재실행/운영 반영 완료를 의미하지 않는다.
#### P16-R1 커밋 전 재검증 — 2026-10-26
- 검증 명령: `./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<Gift>`로 모든 실제 호출자를 함께 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 다중 인스턴스·배포는 미검증이다.