docs(gift): 관리자 취소 요구사항과 검증 결과를 기록한다

This commit is contained in:
2026-10-22 18:25:36 +09:00
parent 30a2f7e37b
commit cd67112a8d
2 changed files with 225 additions and 10 deletions
@@ -4,16 +4,16 @@
| 문서 항목 | 내용 | | 문서 항목 | 내용 |
|---|---| |---|---|
| 상태 | 완료 | | 상태 | Phase 14 완료, 전체 테스트 사용자 요청으로 제외 |
| 작성일 | 2026-09-29 | | 작성일 | 2026-09-29 |
| 요구사항 기준 | `docs/20260929_크리에이터_선물하기/prd.md` | | 요구사항 기준 | `docs/20260929_크리에이터_선물하기/prd.md` |
| API 기준 | `docs/20260929_크리에이터_선물하기/prd.md`의 `8. API 계약` | | API 기준 | `docs/20260929_크리에이터_선물하기/prd.md`의 `8. API 계약` |
| 현재 Phase | Phase 12 구현 완료 | | 현재 Phase | Phase 14 관리자 선물 취소 |
| 현재 활성 Goal | 완료 | | 현재 활성 Goal | 없음, `P14-GATE` 완료 |
## 목표 ## 목표
팬이 캔으로 크리에이터에게 실물 선물을 신청하고, 운송장 등록부터 크리에이터 배송지 입력, 운영 검수, 전달 완료/불가, 리뷰와 푸시까지 서버에서 추적할 수 있게 한다. 팬이 캔으로 크리에이터에게 실물 선물을 신청하고, 운송장 등록부터 크리에이터 배송지 입력, 운영 검수, 전달 완료/불가, 리뷰와 푸시까지 서버에서 추적한다. Phase 14는 `RECEIVED` 선물의 ADMIN 취소·환불과 목록/상세 액션을 제공한다.
## 아키텍처 ## 아키텍처
@@ -41,6 +41,7 @@
| 8 | 완료 | `1/1` | 완료 | 없음 | | 8 | 완료 | `1/1` | 완료 | 없음 |
| 9 | 완료 | `3/3` | 완료 | 없음 | | 9 | 완료 | `3/3` | 완료 | 없음 |
| 10 | 완료 | `3/3` | 완료 | 관리자 선물함 목록/상세 API 추가 | | 10 | 완료 | `3/3` | 완료 | 관리자 선물함 목록/상세 API 추가 |
| 14 | 완료 | `3/3` | 완료 | 전체 테스트 제외, 운영 MySQL/배포/별도 프론트엔드 검증 범위 밖 |
- 동시에 하나의 미완료 goal만 운용한다. - 동시에 하나의 미완료 goal만 운용한다.
- 구현 중 요구사항이 바뀌면 이 문서의 체크박스와 범위를 먼저 갱신한 뒤 코드를 수정한다. - 구현 중 요구사항이 바뀌면 이 문서의 체크박스와 범위를 먼저 갱신한 뒤 코드를 수정한다.
@@ -52,6 +53,7 @@
- 선물 신청, 목록, 상세, 운송장 등록, 취소, 크리에이터 배송지 입력, 크리에이터 수령확인, 리뷰 작성 사용자 API - 선물 신청, 목록, 상세, 운송장 등록, 취소, 크리에이터 배송지 입력, 크리에이터 수령확인, 리뷰 작성 사용자 API
- 관리자 카테고리 API, 관리자 사이즈 가격 API, 운영 상태변경 API - 관리자 카테고리 API, 관리자 사이즈 가격 API, 운영 상태변경 API
- Phase 14 관리자 신청 취소·환불과 목록/상세 `CANCEL` 액션, 별도 관리자 기능 추가 프롬프트.
- 운송장 미등록 자동취소, 배송지 미입력 전달불가, 기한 24시간 전 안내 스케줄러 - 운송장 미등록 자동취소, 배송지 미입력 전달불가, 기한 24시간 전 안내 스케줄러
- 캔 사용내역 `선물하기` 추가와 선물 신청 내부 연결 - 캔 사용내역 `선물하기` 추가와 선물 신청 내부 연결
- 팬/크리에이터 대상 푸시 이벤트와 단일 딥링크 `GIFT_DETAIL` + `applicationNo` - 팬/크리에이터 대상 푸시 이벤트와 단일 딥링크 `GIFT_DETAIL` + `applicationNo`
@@ -1430,6 +1432,174 @@ P7은 FCM 확장 후 전체 상태 전이를 연결하고 최종 회귀로 종
- [x] **GREEN:** 기존 `GiftDelivery` 정보를 재사용해 `recipientAddressRegistered`, `recipientAddressDeadlineAt`, `canceledAt`을 매핑했다. - [x] **GREEN:** 기존 `GiftDelivery` 정보를 재사용해 `recipientAddressRegistered`, `recipientAddressDeadlineAt`, `canceledAt`을 매핑했다.
- [x] **검증:** focused 테스트와 `ktlintCheck`, `git diff --check`를 실행했다. - [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 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 31 | `P14-T1` | 관리자 취소 승인 범위 문서화 | 아니요 | 문서 검증 |
| 32 | `P14-T2` | `P14-T1` 검증 및 사용자 구현 승인 | 아니요 | 승인 대기, 코드 변경 금지 |
| 33 | `P14-T3` | `P14-T2` 계약/승인 | 아니요 | 선행 구현 승인 대기 |
| 34 | `P14-GATE` | `P14-T2`, `P14-T3` | 아니요 | 실패 원인 Task에 반영 |
```text
P14-T1 문서 검증 → 사용자 구현 승인 → P14-T2 → P14-T3 → P14-GATE
```
## Decision Log ## Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | | 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
@@ -1442,6 +1612,10 @@ P7은 FCM 확장 후 전체 상태 전이를 연결하고 최종 회귀로 종
| 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-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-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-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` |
## 발견된 문제 ## 발견된 문제
@@ -4,15 +4,17 @@
| 항목 | 내용 | | 항목 | 내용 |
|---|---| |---|---|
| 문서 상태 | 구현 중 | | 문서 상태 | Phase 14 서버 구현 완료, 전체 테스트 제외 검증 완료 |
| 작성일 | 2026-09-29 | | 작성일 | 2026-09-29 |
| 최종 수정일 | 2026-09-30 | | 최종 수정일 | 2026-10-22 |
| 대상 제품 | 크리에이터 선물하기 | | 대상 제품 | 크리에이터 선물하기 |
| 작성자·결정권자 | 사용자 | | 작성자·결정권자 | 사용자 |
| 관련 API Contract | 별도 문서 없음. 이 문서의 `8. API 계약`을 기준으로 사용 | | 관련 API Contract | 별도 문서 없음. 이 문서의 `8. API 계약`을 기준으로 사용 |
| 관련 구현 계획 | `docs/20260929_크리에이터_선물하기/plan-task.md` | | 관련 구현 계획 | `docs/20260929_크리에이터_선물하기/plan-task.md` |
| 관련 review | 없음 | | 관련 review | 없음 |
> Phase 14 관리자 취소와 목록·상세 `CANCEL` 액션은 서버 코드에 구현됐다. 관련 테스트·실제 JWT HTTP·리뷰·테스트 제외 빌드를 검증했으며, 전체 테스트는 사용자 요청으로 완료 기준에서 제외했다. 운영 배포 여부와 별도 프론트엔드 구현 여부는 이 문서의 완료 상태에 포함하지 않는다.
## 1. Overview ## 1. Overview
팬이 내부 재화인 캔을 사용해 크리에이터에게 실물 선물을 보낼 수 있는 API를 제공한다. 팬이 내부 재화인 캔을 사용해 크리에이터에게 실물 선물을 보낼 수 있는 API를 제공한다.
@@ -34,6 +36,7 @@
- 팬이 선물 사이즈, 카테고리, 발신자 정보, 약관 동의를 입력해 선물 보내기를 신청할 수 있다. - 팬이 선물 사이즈, 카테고리, 발신자 정보, 약관 동의를 입력해 선물 보내기를 신청할 수 있다.
- 선물 신청 시 캔을 즉시 차감하고 내부적으로 선물 신청과 캔 사용내역을 연결한다. - 선물 신청 시 캔을 즉시 차감하고 내부적으로 선물 신청과 캔 사용내역을 연결한다.
- 팬은 운송장 등록 전 선물 신청을 취소할 수 있고, 3일 내 운송장을 등록하지 않으면 자동취소 및 전액 환불된다. - 팬은 운송장 등록 전 선물 신청을 취소할 수 있고, 3일 내 운송장을 등록하지 않으면 자동취소 및 전액 환불된다.
- 관리자는 운송장 등록 전 `RECEIVED` 선물을 취소하고 발신자에게 사용 캔 전액을 리워드캔으로 환불할 수 있다.
- 크리에이터는 팬이 운송장을 등록한 선물부터 받은 선물 목록과 상세를 조회하고 7일 이내 배송지와 수령 약관 동의를 등록할 수 있다. - 크리에이터는 팬이 운송장을 등록한 선물부터 받은 선물 목록과 상세를 조회하고 7일 이내 배송지와 수령 약관 동의를 등록할 수 있다.
- 팬과 크리에이터는 같은 선물함 목록 API에서 보낸/받은 내역을 함께 또는 개별 조회할 수 있다. - 팬과 크리에이터는 같은 선물함 목록 API에서 보낸/받은 내역을 함께 또는 개별 조회할 수 있다.
- 운영자는 상태별 전용 관리자 API로 사서함 도착, 검수완료, 전달불가, 전달완료를 처리할 수 있다. - 운영자는 상태별 전용 관리자 API로 사서함 도착, 검수완료, 전달불가, 전달완료를 처리할 수 있다.
@@ -55,7 +58,7 @@
|---|---|---|---| |---|---|---|---|
| 팬 | 크리에이터에게 선물을 보낸다 | 사이즈/카테고리 조회, 신청, 운송장 등록, 취소, 보낸 내역 조회, 리뷰 작성 | 인증 회원 | | 팬 | 크리에이터에게 선물을 보낸다 | 사이즈/카테고리 조회, 신청, 운송장 등록, 취소, 보낸 내역 조회, 리뷰 작성 | 인증 회원 |
| 크리에이터 | 받은 선물을 확인하고 배송지를 입력한다 | 받은 내역 조회, 배송지 입력, 수령 확인 | 인증 회원 중 크리에이터 | | 크리에이터 | 받은 선물을 확인하고 배송지를 입력한다 | 받은 내역 조회, 배송지 입력, 수령 확인 | 인증 회원 중 크리에이터 |
| 관리자 | 선물 운영 설정을 관리한다 | 카테고리, 사이즈 가격 관리 | 관리자 | | 관리자 | 선물 운영 설정과 선물 상태를 관리한다 | 카테고리, 사이즈 가격 관리, `RECEIVED` 선물 취소 | 관리자 |
| 운영자 | 선물 검수/전달 상태를 처리한다 | 사서함 도착, 검수완료, 전달불가, 전달완료 처리 | 관리자 또는 운영 권한 | | 운영자 | 선물 검수/전달 상태를 처리한다 | 사서함 도착, 검수완료, 전달불가, 전달완료 처리 | 관리자 또는 운영 권한 |
| 스케줄러 | 기한 초과 건을 자동 처리한다 | 운송장 미등록 자동취소, 배송지 미입력 전달불가 | 시스템 | | 스케줄러 | 기한 초과 건을 자동 처리한다 | 운송장 미등록 자동취소, 배송지 미입력 전달불가 | 시스템 |
@@ -74,7 +77,7 @@
| `INSPECTION_COMPLETED` | 검수완료 | 운영자가 선물 검수를 통과 처리한 상태 | 아니오 | | `INSPECTION_COMPLETED` | 검수완료 | 운영자가 선물 검수를 통과 처리한 상태 | 아니오 |
| `DELIVERED` | 전달완료 | 크리에이터 수령확인 또는 운영자 전달완료 처리 상태 | 예 | | `DELIVERED` | 전달완료 | 크리에이터 수령확인 또는 운영자 전달완료 처리 상태 | 예 |
| `UNDELIVERABLE` | 전달불가 | 검수 실패 또는 배송지 미입력 기한 초과로 전달할 수 없는 상태 | 예 | | `UNDELIVERABLE` | 전달불가 | 검수 실패 또는 배송지 미입력 기한 초과로 전달할 수 없는 상태 | 예 |
| `CANCELED` | 신청 취소 | 운송장 등록 전 사용자 취소 또는 자동취소 상태 | 예 | | `CANCELED` | 신청 취소 | 운송장 등록 전 사용자 취소, ADMIN 취소 또는 자동취소 상태 | 예 |
상태 전이는 다음을 기본으로 한다. 상태 전이는 다음을 기본으로 한다.
@@ -171,6 +174,7 @@ INSPECTION_COMPLETED -> UNDELIVERABLE
- 선물 신청과 캔 사용내역은 내부적으로 연결되어야 한다. - 선물 신청과 캔 사용내역은 내부적으로 연결되어야 한다.
- 사용자 캔 사용내역 화면에서 선물 신청으로 이동할 수 없어도 된다. - 사용자 캔 사용내역 화면에서 선물 신청으로 이동할 수 없어도 된다.
- 운송장 등록 전 사용자 취소 또는 3일 자동취소 시 사용 캔을 전액 환불한다. - 운송장 등록 전 사용자 취소 또는 3일 자동취소 시 사용 캔을 전액 환불한다.
- ADMIN은 운송장 등록 전 `RECEIVED`에서 취소할 수 있으며 기존 `refundGiftCan` 경로로 발신자에게 전액 리워드캔 환불한다. 상태 변경, 취소 시각 저장, 환불은 같은 트랜잭션에서 처리한다.
- 운송장 등록 후에는 팬이 취소할 수 없다. - 운송장 등록 후에는 팬이 취소할 수 없다.
### 6.8 목록 노출 ### 6.8 목록 노출
@@ -248,6 +252,8 @@ INSPECTION_COMPLETED -> UNDELIVERABLE
| `GIFT-012` | 확정 | 관리자는 카테고리를 등록/수정/논리삭제할 수 있다 | 삭제는 `isActive=false`만 수행한다 | `P1` | | `GIFT-012` | 확정 | 관리자는 카테고리를 등록/수정/논리삭제할 수 있다 | 삭제는 `isActive=false`만 수행한다 | `P1` |
| `GIFT-013` | 확정 | 관리자는 사이즈별 가격을 수정할 수 있다 | 새 신청에는 변경 가격이 적용되고 기존 신청 스냅샷은 바뀌지 않는다 | `P1` | | `GIFT-013` | 확정 | 관리자는 사이즈별 가격을 수정할 수 있다 | 새 신청에는 변경 가격이 적용되고 기존 신청 스냅샷은 바뀌지 않는다 | `P1` |
| `GIFT-015` | 확정 | 관리자는 전체 선물함 목록과 상세를 조회하고 현재 상태에서 가능한 운영 액션을 확인할 수 있다 | 목록은 상태/신청번호/닉네임 검색과 페이징을 지원하고, 상세는 발송인/수취인 개인정보와 상품/배송/상태 정보를 내려준다 | `P10` | | `GIFT-015` | 확정 | 관리자는 전체 선물함 목록과 상세를 조회하고 현재 상태에서 가능한 운영 액션을 확인할 수 있다 | 목록은 상태/신청번호/닉네임 검색과 페이징을 지원하고, 상세는 발송인/수취인 개인정보와 상품/배송/상태 정보를 내려준다 | `P10` |
| `GIFT-016` | 확정 | 관리자는 운송장 등록 전 접수된 선물을 취소할 수 있다 | 관리자만 호출할 수 있고 `RECEIVED`에서만 `CANCELED`로 전이하며 `GiftDelivery.canceledAt`을 저장하고 발신자에게 사용 캔 전액을 리워드캔으로 환불한다 | `P14-T2` |
| `GIFT-017` | 확정 | 관리자 선물함 목록과 상세는 현재 가능한 액션으로 취소 가능 여부를 함께 제공한다 | `RECEIVED`에서만 `availableActions=["CANCEL"]`; 다른 상태의 기존 액션은 유지한다 | `P14-T3` |
## 8. API 계약 ## 8. API 계약
@@ -843,11 +849,11 @@ Request:
Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내려준다. `address`는 기존 주소 응답과 동일하게 `(우편번호) 주소, 상세주소` 형식이다. `name`, `phoneNumber`, `zipCode`, `address`는 필수이며 공백만 입력할 수 없다. `addressDetail`은 선택이다. Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내려준다. `address`는 기존 주소 응답과 동일하게 `(우편번호) 주소, 상세주소` 형식이다. `name`, `phoneNumber`, `zipCode`, `address`는 필수이며 공백만 입력할 수 없다. `addressDetail`은 선택이다.
`availableActions`는 현재 상태별로 다음 값만 내려준다. 관리자 목록과 상세의 `availableActions`는 상태별로 다음 값을 내려준다. `RECEIVED`에서는 `CANCEL`을 제공하고, 다른 상태의 기존 매핑은 유지한다.
| 현재 상태 | availableActions | | 현재 상태 | availableActions |
|---|---| |---|---|
| `RECEIVED` | `[]` | | `RECEIVED` | `["CANCEL"]` |
| `TRACKING_REGISTERED` | `["ARRIVE_MAILBOX", "MARK_UNDELIVERABLE"]` | | `TRACKING_REGISTERED` | `["ARRIVE_MAILBOX", "MARK_UNDELIVERABLE"]` |
| `ARRIVED_AT_MAILBOX` | `["COMPLETE_INSPECTION", "MARK_UNDELIVERABLE"]` | | `ARRIVED_AT_MAILBOX` | `["COMPLETE_INSPECTION", "MARK_UNDELIVERABLE"]` |
| `INSPECTION_COMPLETED`, 수취인 정보 입력 완료 | `["COMPLETE_DELIVERY", "MARK_UNDELIVERABLE"]` | | `INSPECTION_COMPLETED`, 수취인 정보 입력 완료 | `["COMPLETE_DELIVERY", "MARK_UNDELIVERABLE"]` |
@@ -864,6 +870,7 @@ Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내
| POST | `/api/v2/admin/gifts/{applicationNo}/complete-inspection` | `ARRIVED_AT_MAILBOX` | `INSPECTION_COMPLETED` | 없음 | | POST | `/api/v2/admin/gifts/{applicationNo}/complete-inspection` | `ARRIVED_AT_MAILBOX` | `INSPECTION_COMPLETED` | 없음 |
| POST | `/api/v2/admin/gifts/{applicationNo}/mark-undeliverable` | `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED` | `UNDELIVERABLE` | `reason` | | POST | `/api/v2/admin/gifts/{applicationNo}/mark-undeliverable` | `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED` | `UNDELIVERABLE` | `reason` |
| POST | `/api/v2/admin/gifts/{applicationNo}/complete-delivery` | `INSPECTION_COMPLETED` | `DELIVERED` | 저장된 수취인 이름, 휴대폰 번호, 우편번호, 주소 | | POST | `/api/v2/admin/gifts/{applicationNo}/complete-delivery` | `INSPECTION_COMPLETED` | `DELIVERED` | 저장된 수취인 이름, 휴대폰 번호, 우편번호, 주소 |
| POST | `/api/v2/admin/gifts/{applicationNo}/cancel` | `RECEIVED` | `CANCELED` | 없음 |
`complete-delivery`는 `recipientName`, `recipientPhoneNumber`, `recipientZipCode`, `recipientAddress`가 모두 null 또는 공백이 아닐 때만 성공한다. `recipientAddressDetail`은 선택 입력이다. 조건을 만족하지 않으면 상태, 전달 완료 시각, 푸시를 변경하지 않고 요청을 거부한다. `complete-delivery`는 `recipientName`, `recipientPhoneNumber`, `recipientZipCode`, `recipientAddress`가 모두 null 또는 공백이 아닐 때만 성공한다. `recipientAddressDetail`은 선택 입력이다. 조건을 만족하지 않으면 상태, 전달 완료 시각, 푸시를 변경하지 않고 요청을 거부한다.
@@ -875,6 +882,29 @@ Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내
} }
``` ```
#### 8.14.1 관리자 선물 신청 취소
`POST /api/v2/admin/gifts/{applicationNo}/cancel`
Request body: 없음
기존 관리자 API의 `hasRole('ADMIN')` 권한을 따르며 관리자 계정은 선물 발신자가 아니어도 취소할 수 있다. `RECEIVED`에서만 `CANCELED`로 전이하며 운송장 등록 이후에는 취소할 수 없다. 같은 트랜잭션에서 `GiftDelivery.canceledAt`을 기록하고 기존 `refundGiftCan(gift.senderMemberId, gift.canUsageId)` 경로로 발신자에게 원 결제액 전액을 리워드캔으로 환불한다. 관리자의 잔액은 변경하지 않는다.
Response `data`는 기존 `AdminGiftOperationStatusResponse`를 사용한다.
```json
{
"applicationNo": "A-1002609300001",
"status": "CANCELED",
"statusName": "신청 취소",
"occurredAt": "2026-09-30T12:00:00Z"
}
```
`occurredAt`은 `canceledAt`과 같은 UTC ISO `Z` 문자열이다. 누락되거나 취소할 수 없는 상태의 신청번호는 기존 `SodaException(messageKey = "common.error.invalid_request")`로 거부한다. 정상 `ApiResponse` envelope를 사용하며 신규 오류 스키마나 HTTP status 정책은 추가하지 않는다.
이 관리자 취소는 기존 사용자 직접 취소와 운송장 미등록 자동취소를 변경하지 않는다. 취소 전용 푸시는 추가하지 않으며, 자동취소 푸시 문구나 자동취소 사유를 관리자 취소에 재사용하지 않는다.
### 8.15 자동 스케줄링 작업 ### 8.15 자동 스케줄링 작업
| 작업 | 대상 | 처리 | 푸시 | | 작업 | 대상 | 처리 | 푸시 |
@@ -1014,6 +1044,9 @@ Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내
- 푸시는 팬/크리에이터 수신자 분리와 크리에이터 수령확인 시 크리에이터 푸시 억제를 테스트한다. - 푸시는 팬/크리에이터 수신자 분리와 크리에이터 수령확인 시 크리에이터 푸시 억제를 테스트한다.
- 스케줄러는 중복 실행 시 중복 환불/중복 푸시가 없는지 테스트한다. - 스케줄러는 중복 실행 시 중복 환불/중복 푸시가 없는지 테스트한다.
- 선물 사용자/관리자 API response DTO의 날짜/시간 필드는 UTC ISO 문자열(`Z` 포함)로 직렬화되는지 controller test로 고정한다. - 선물 사용자/관리자 API response DTO의 날짜/시간 필드는 UTC ISO 문자열(`Z` 포함)로 직렬화되는지 controller test로 고정한다.
- 관리자 취소는 `RECEIVED`만 허용하고, 발신자에게 전액 리워드캔 환불, `canceledAt` 저장, 환불 실패 시 트랜잭션 롤백 및 중복 환불 방지를 검증한다. 운송장 등록, 사용자 취소, 자동취소, 다른 관리자 취소와의 동시 경합에서도 잘못된 이중 처리/환불이 없어야 한다.
- 관리자 취소 controller는 기존 관리자 인증/권한, 공통 invalid request, 정상 `ApiResponse` envelope와 `AdminGiftOperationStatusResponse`의 UTC `occurredAt`을 검증한다.
- 관리자 목록과 상세 모두 `RECEIVED`에서만 `CANCEL` 액션을 제공하며 기존 `COMPLETE_DELIVERY` 조건부 액션을 포함한 나머지 상태 매핑은 유지한다.
- 카테고리 `classificationNumber`, `receiptCode`, `representativeItem`의 관리자 등록/수정/조회와 사용자 폼 옵션의 `분류명/대표품목` 조합을 테스트한다. - 카테고리 `classificationNumber`, `receiptCode`, `representativeItem`의 관리자 등록/수정/조회와 사용자 폼 옵션의 `분류명/대표품목` 조합을 테스트한다.
- 신청번호는 카테고리+날짜별 sequence, 4자리 padding, `9999` 초과 자리수 증가, 병렬 unique를 테스트한다. - 신청번호는 카테고리+날짜별 sequence, 4자리 padding, `9999` 초과 자리수 증가, 병렬 unique를 테스트한다.
@@ -1029,6 +1062,7 @@ Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내
- [ ] 팬/크리에이터 푸시가 지정된 문구, 수신자, 딥링크로 발송된다. - [ ] 팬/크리에이터 푸시가 지정된 문구, 수신자, 딥링크로 발송된다.
- [ ] 선물 사용자/관리자 API의 날짜/시간 응답을 클라이언트가 UTC ISO 문자열로 해석할 수 있다. - [ ] 선물 사용자/관리자 API의 날짜/시간 응답을 클라이언트가 UTC ISO 문자열로 해석할 수 있다.
- [ ] 카테고리 추가 필드와 새 신청번호 정책이 관리자 API, 사용자 폼 옵션, 선물 신청에 반영된다. - [ ] 카테고리 추가 필드와 새 신청번호 정책이 관리자 API, 사용자 폼 옵션, 선물 신청에 반영된다.
- [x] 관리자 취소는 `RECEIVED`에서만 전액 환불과 함께 성공하고, 목록/상세의 `CANCEL` 액션은 상태에 맞게 제공된다.
## 14. Open Questions ## 14. Open Questions
@@ -1045,6 +1079,7 @@ Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내
| `GIFT-010` | 5 | 리뷰 API 테스트 | 전달완료 후 리뷰 작성 확인 | | `GIFT-010` | 5 | 리뷰 API 테스트 | 전달완료 후 리뷰 작성 확인 |
| `GIFT-011` | 6 | 푸시 이벤트 테스트 | 딥링크와 수신자 확인 | | `GIFT-011` | 6 | 푸시 이벤트 테스트 | 딥링크와 수신자 확인 |
| `GIFT-014` | 8 | 선물 사용자/관리자 controller 날짜 응답 테스트 | UTC ISO 문자열 확인 | | `GIFT-014` | 8 | 선물 사용자/관리자 controller 날짜 응답 테스트 | UTC ISO 문자열 확인 |
| `GIFT-016`, `GIFT-017` | 14 | 관리자 취소 service/controller 및 query service 테스트 | `RECEIVED` 액션과 취소/환불 결과 확인 |
## 16. Decision Log ## 16. Decision Log
@@ -1064,3 +1099,9 @@ Response는 저장된 받을 주소를 `name`, `address`, `phoneNumber`로 내
| 2026-09-29 | `DEC-012` | 확정 | 파손면책 동의 필요 카테고리는 팬 신청 시 `damageWaiverAgreed=true`를 요구한다 | 사용자 선택 A | `GIFT-003` | | 2026-09-29 | `DEC-012` | 확정 | 파손면책 동의 필요 카테고리는 팬 신청 시 `damageWaiverAgreed=true`를 요구한다 | 사용자 선택 A | `GIFT-003` |
| 2026-09-30 | `DEC-013` | 확정 | 선물 사용자/관리자 API의 response datetime 필드는 `LocalDateTime` 기본 JSON 대신 UTC ISO 문자열로 내려준다 | 클라이언트가 offset 없는 `LocalDateTime` 문자열을 안전하게 해석하지 못함 | `GIFT-014` | | 2026-09-30 | `DEC-013` | 확정 | 선물 사용자/관리자 API의 response datetime 필드는 `LocalDateTime` 기본 JSON 대신 UTC ISO 문자열로 내려준다 | 클라이언트가 offset 없는 `LocalDateTime` 문자열을 안전하게 해석하지 못함 | `GIFT-014` |
| 2026-09-30 | `DEC-014` | 확정 | 카테고리는 분류번호, 내부 범용 카테고리 코드, 분류명, 접수코드, 대표품목, 파손면책 조건을 관리하고 신청번호는 `receiptCode-classificationNumberYYMMDDSequenceNo`로 발급한다 | 관리자 운영 필드와 사용자 표시 요구가 분리됨 | `GIFT-001`, `GIFT-002`, `GIFT-012` | | 2026-09-30 | `DEC-014` | 확정 | 카테고리는 분류번호, 내부 범용 카테고리 코드, 분류명, 접수코드, 대표품목, 파손면책 조건을 관리하고 신청번호는 `receiptCode-classificationNumberYYMMDDSequenceNo`로 발급한다 | 관리자 운영 필드와 사용자 표시 요구가 분리됨 | `GIFT-001`, `GIFT-002`, `GIFT-012` |
| 2026-10-22 | `DEC-015` | 확정 | 관리자 취소는 `RECEIVED`에서만 허용하고 기존 `refundGiftCan`으로 발신자에게 전액 리워드캔을 환불한다. 기존 operation status 응답을 재사용하며 관리자 취소 전용 푸시는 이번 범위에서 추가하지 않는다 | 사용자 확정: 취소 허용 상태와 문서 우선 반영. 응답/환불은 기존 계약 재사용, 신규 푸시는 요청 범위 밖 | `GIFT-016`, `GIFT-017`, `P14` |
| 2026-10-22 | `DEC-016` | 확정 | 기존 관리자 화면 전체 구현 프롬프트는 보존하고 취소 기능 추가 프롬프트를 `admin-gift-cancel-addition-prompt.md`로 분리한다 | 사용자 요청: 기존 페이지에 해당 기능만 추가하는 별도 프롬프트 | `GIFT-016`, `GIFT-017`, `P14-T1` |
## 문서 상태 안내
Phase 14 서버 구현과 관련 검증은 완료했다. 전체 테스트는 1차 실행이 도구의 10분 제한으로 중단되고 2차는 사용자 중단 후 제외됐다. 전체 테스트 통과를 주장하지 않으며 검증 명령과 한계는 `plan-task.md`의 P14-GATE 기록을 따른다. 관리자 프론트엔드는 별도 기능 추가 프롬프트의 확정 계약을 사용한다.