Files

204 KiB

크리에이터 선물하기 구현 계획

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.

  • RED: 엔티티 저장/조회, applicationNo unique, GiftReview.giftId unique, 카테고리 categoryCode unique를 검증하는 실패 test를 작성한다.

  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftEntityMappingTest"를 실행해 신규 엔티티 미존재 또는 매핑 미구현 실패를 확인한다.

  • GREEN: 엔티티, repository, DDL을 최소 구현한다. DDL의 모든 컬럼과 테이블에는 MySQL COMMENT를 추가하고 created_at, updated_at은 문서 규칙을 따른다.

  • GREEN 확인: 같은 focused test가 통과하는지 확인한다.

  • 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.

  • RED: 같은 날짜 연속 발급이 G20260929000001, G20260929000002가 되고 병렬 발급 결과가 모두 unique인 실패 test를 작성한다.

  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationNoGeneratorTest"로 실패를 확인한다.

  • GREEN: DB unique 제약, 원자적 채번 또는 충돌 재시도 중 가장 단순한 방식으로 구현한다.

  • GREEN 확인: 같은 focused test 통과를 확인한다.

  • 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.

  • RED: 카테고리 등록/수정/논리삭제, 비활성 전환, salePriceCan > basePriceCan 거부 test를 작성한다.

  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftAdminServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.AdminGiftControllerTest"로 실패를 확인한다.

  • GREEN: 관리자 service와 controller를 최소 구현한다. 삭제는 isActive=false만 수행한다.

  • GREEN 확인: 같은 focused test 통과를 확인한다.

  • 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.

  • RED: 비활성 카테고리 제외, categoryCode 미노출, basePriceCan/salePriceCan 응답을 검증하는 실패 test를 작성한다.

  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"로 실패를 확인한다.

  • GREEN: 폼 옵션 조회 service/controller/response를 최소 구현한다.

  • GREEN 확인: 같은 focused test 통과를 확인한다.

  • REFACTOR: page/DTO 책임 경계를 정리하고 focused test·ktlintCheck 결과를 기록한다.

Phase 1 Gate

Goal 실행 P1-GATE: 데이터 기반과 관리자 설정/폼 옵션 계약을 최종 판정한다.

./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 재사용을 우선한다.

  • RED: CanUsage.GIFT 또는 동등한 선물하기 사용내역이 생성되고 환불 시 1회만 환불되는 실패 test를 작성한다.

  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest"로 실패를 확인한다.

  • GREEN: 기존 결제 service를 최대한 재사용해 최소 연결만 추가한다.

  • GREEN 확인: 같은 focused test 통과를 확인한다.

  • 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.

  • RED: 필수 발신자 정보, 팬 약관 2개, 조건부 damageWaiverAgreed, 비활성 카테고리, 최종 결제 금액 스냅샷, 고유 applicationNo, 캔 차감을 검증하는 실패 test를 작성한다.

  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftCommandServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest"로 실패를 확인한다.

  • GREEN: 신청 API와 service를 최소 구현한다. trackingDeadlineAt은 신청 시각 + 3일로 저장한다.

  • GREEN 확인: 같은 focused test 통과를 확인한다.

  • 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.

  • RED: 보낸 팬만 취소 가능, RECEIVED에서만 가능, TRACKING_REGISTERED 이후 실패, 환불 1회만 발생하는 실패 test를 작성한다.

  • RED 확인: focused test 명령으로 실패를 확인한다.

  • GREEN: 취소 상태 전이와 환불을 최소 구현한다.

  • GREEN 확인: focused test 통과를 확인한다.

  • 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.

  • RED: 보낸 팬만 등록 가능, RECEIVED에서만 가능, courierCompanyName 필수/50자 이하, 크리에이터 배송지 입력 기한 생성, 상태 전이를 검증하는 실패 test를 작성한다.

  • RED 확인: focused test 명령으로 실패를 확인한다.

  • GREEN: 운송장 등록과 recipientAddressDeadlineAt = 등록 시각 + 7일 저장을 구현한다.

  • GREEN 확인: focused test 통과를 확인한다.

  • REFACTOR: 받은 선물 노출 조건과 충돌하지 않도록 상태 predicate 이름을 정리하고 회귀 결과를 기록한다.

Phase 2 Gate

Goal 실행 P2-GATE: 팬 신청/결제/취소/운송장 등록 흐름을 판정한다.

./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.
  • RED: 보낸 선물은 신청 직후 노출, 받은 선물은 TRACKING_REGISTERED 이상부터 노출, direction, pagination 보정을 검증하는 실패 test를 작성한다.
  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftQueryServiceTest" --tests "kr.co.vividnext.sodalive.v2.api.gift.GiftControllerTest" 실패를 확인한다.
  • GREEN: 목록 query와 response를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: QueryDSL 조건을 중복 없이 정리하고 회귀 결과를 기록한다.

Task 3.2 선물 상세 조회 구현

Goal 실행 P3-T2: 단일 상세 API가 로그인 회원 기준으로 보낸/받은 선물을 판정한다.

  • Files: Modify GiftQueryService.kt, GiftController.kt, GiftResponse.kt; Test GiftQueryServiceTest.kt, GiftControllerTest.kt.
  • RED: sender는 SENT, recipient는 노출 조건 만족 시 RECEIVED, 무관한 회원은 권한 오류 또는 미존재 오류를 반환하는 실패 test를 작성한다.
  • RED: direction=SENT 상세 응답이 giftInfo.recipientCreatorNickname, giftInfo.sizeName, giftInfo.categoryName, giftInfo.applicationNo, giftInfo.paidCan, giftInfo.tracking, senderInfo.name, senderInfo.phoneNumber, senderInfo.address, mailbox를 포함하는 실패 test를 작성한다.
  • RED: direction=SENT 상세 응답에서 giftInfo.senderNickname, giftInfo.shippingRequestedAt, recipientAddress가 null이고 크리에이터가 입력한 이름/휴대폰 번호/주소가 노출되지 않는 실패 test를 작성한다.
  • RED: direction=SENT이고 상태가 RECEIVED이면 trackingRequired=true, 그 외 상태이거나 direction=RECEIVED이면 trackingRequired=false인 실패 test를 작성한다.
  • RED: direction=RECEIVED 상세 응답이 giftInfo.senderNickname, giftInfo.sizeName, giftInfo.categoryName, giftInfo.shippingRequestedAt, recipientAddress.name, recipientAddress.phoneNumber, recipientAddress.address를 포함하는 실패 test를 작성한다.
  • RED: direction=RECEIVED 상세 응답에서 giftInfo.recipientCreatorNickname, giftInfo.applicationNo, giftInfo.paidCan, giftInfo.tracking, senderInfo, mailbox가 null이고 팬이 신청 시 등록한 이름/휴대폰 번호/주소가 노출되지 않는 실패 test를 작성한다.
  • RED: direction=RECEIVED이고 받는 주소가 미입력이며 종료 상태가 아니면 recipientAddressRequired=true, 주소 입력 완료 또는 종료 상태 또는 direction=SENT이면 recipientAddressRequired=false인 실패 test를 작성한다.
  • RED: 발신자/수신자 주소가 (우편번호) 주소, 상세주소 형식으로 조립되고, 로그인 회원이 알아야 하는 데이터가 아니거나 아직 입력되지 않은 데이터는 null인 실패 test를 작성한다.
  • RED: 상세 응답의 statusTimeline이 RECEIVED, TRACKING_REGISTERED, ARRIVED_AT_MAILBOX, INSPECTION_COMPLETED, DELIVERED 순서로 내려오고 도달한 상태만 occurredAt을 가지며 미도달 상태는 null인 실패 test를 작성한다.
  • RED: CANCELED, UNDELIVERABLE 상세에서 정상 진행 statusTimeline은 유지되고 종료 정보는 각각 delivery.canceledAt, delivery.undeliverableAt, delivery.undeliverableReason으로 내려오는 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 상세 query와 권한 판정을 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 역할별 상세 응답 조립, 본인/상대방 개인정보 노출 경계, 상태 타임라인 조립 책임을 response mapper에서 명확히 하고 회귀 결과를 기록한다.

Task 3.3 크리에이터 배송지 입력 구현

Goal 실행 P3-T3: 크리에이터가 배송지와 수령 약관 2개를 등록할 수 있게 한다.

  • Files: Modify GiftCommandService.kt, GiftController.kt, GiftRecipientAddressRegistrationRequest.kt; Test GiftCommandServiceTest.kt, GiftControllerTest.kt.
  • RED: 받는 크리에이터만 가능, 종료 상태 실패, 이름/휴대폰/우편번호/주소 필수, 수령 약관 2개 필수, 동의 시각 저장 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 배송지 입력 API를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 발신/수신 주소 snapshot 책임을 GiftDelivery 안으로 정리하고 회귀 결과를 기록한다.

Task 3.4 크리에이터 수령확인 구현

Goal 실행 P3-T4: 크리에이터가 INSPECTION_COMPLETED 선물을 DELIVERED로 완료 처리한다.

  • Files: Modify GiftCommandService.kt, GiftController.kt; Test GiftCommandServiceTest.kt, GiftControllerTest.kt.
  • RED: 받는 크리에이터만 가능, INSPECTION_COMPLETED에서만 가능, deliveredAt 저장, 팬 푸시 발행 요청을 검증하는 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 수령확인 상태 전이를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 운영 전달완료와 공유 가능한 상태 전이 검증만 추출하고 회귀 결과를 기록한다.

Phase 3 Gate

Goal 실행 P3-GATE: 조회와 크리에이터 배송지/수령 흐름을 판정한다.

./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.
  • RED: 기한 초과 대상만 취소, 환불 1회, 중복 실행 idempotent 실패 test를 작성한다.
  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.GiftSchedulerTest" 실패를 확인한다.
  • GREEN: 자동취소 job service를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: scheduler와 command service의 상태 전이 중복을 정리하고 회귀 결과를 기록한다.

Task 4.2 배송지 미입력 전달불가 구현

Goal 실행 P4-T2: 운송장 등록 후 7일 초과 배송지 미입력 선물을 UNDELIVERABLE로 바꾼다.

  • Files: Modify GiftScheduler.kt, GiftRepository.kt; Test GiftSchedulerTest.kt.
  • RED: 배송지 미입력 기한 초과만 대상, 사유 배송지 미입력 기한 초과, 중복 실행 idempotent 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 전달불가 job service를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 대상 조회 query 이름과 상태 조건을 명확히 하고 회귀 결과를 기록한다.

Task 4.3 24시간 전 안내 작업 구현

Goal 실행 P4-T3: 운송장 등록과 배송지 입력 마감 24시간 전 안내 푸시 작업을 구현한다.

  • Files: Modify GiftScheduler.kt, GiftPushService.kt; Test GiftSchedulerTest.kt, GiftPushServiceTest.kt.
  • RED: 마감 24시간 전 범위만 대상, 같은 선물 중복 안내 방지 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 안내 작업과 중복 방지 기준을 구현한다. 중복 방지는 별도 컬럼 또는 푸시 기록 조회 중 가장 단순한 방식을 사용한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 작업별 batch size와 로그에 개인정보가 남지 않는지 확인하고 회귀 결과를 기록한다.

Phase 4 Gate

Goal 실행 P4-GATE: 자동 처리와 idempotency를 판정한다.

./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.
  • RED: 허용 이전 상태 성공, 잘못된 이전 상태 실패, 상태 시각 저장, 팬/크리에이터 푸시 요청 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 두 운영 API를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 상태 전이 검증 함수 중복을 정리하고 회귀 결과를 기록한다.

Task 5.2 운영 전달불가 API 구현

Goal 실행 P5-T2: 운영자가 전달불가 사유를 등록하고 UNDELIVERABLE로 종료 처리한다.

  • Files: Modify GiftAdminService.kt, AdminGiftController.kt, AdminGiftMarkUndeliverableRequest.kt; Test GiftAdminServiceTest.kt, AdminGiftControllerTest.kt.
  • RED: 허용 상태 성공, 사유 필수, 종료 상태 실패, 팬/크리에이터 전달불가 푸시 요청 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 전달불가 API를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 사유 길이와 개인정보 로그 금지를 확인하고 회귀 결과를 기록한다.

Task 5.3 운영 전달완료 API 구현

Goal 실행 P5-T3: 운영자가 INSPECTION_COMPLETED 선물을 DELIVERED로 완료 처리한다.

  • Files: Modify GiftAdminService.kt, AdminGiftController.kt; Test GiftAdminServiceTest.kt, AdminGiftControllerTest.kt.
  • RED: INSPECTION_COMPLETED에서만 성공, 팬 전달완료 푸시, 운영 처리 시 크리에이터 배송완료 푸시 정책을 검증하는 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 운영 전달완료 API를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 크리에이터 직접 수령확인과 운영 전달완료의 푸시 차이를 명확히 하고 회귀 결과를 기록한다.

Phase 5 Gate

Goal 실행 P5-GATE: 운영 상태변경 API를 판정한다.

./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.
  • RED: 보낸 팬만 가능, DELIVERED에서만 가능, 중복 실패, 별점 1~5, keywords 각 255자, comment 255자 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 리뷰 작성 service를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • 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.
  • RED: request/response JSON에서 keywords 배열을 사용하고 단수 keyword를 받지 않는 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 리뷰 API를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • REFACTOR: 상세 응답의 review 포함 여부를 PRD와 맞추고 회귀 결과를 기록한다.

Phase 6 Gate

Goal 실행 P6-GATE: 리뷰 작성 요구사항을 판정한다.

./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.
  • RED: deepLinkValue=GIFT_DETAIL, deepLinkId=applicationNo를 발행하는 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: FCM enum과 gift push helper를 최소 확장한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • 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.
  • RED: 팬 대상 5종, 크리에이터 대상 6종, 크리에이터 직접 수령확인 시 크리에이터 푸시 없음, 줄바꿈 본문 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 상태별 푸시 이벤트 생성을 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.
  • 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에 기록한다.
  • 사용자 흐름 대조표를 작성한다: 신청 → 취소, 신청 → 운송장 → 배송지 → 운영 검수 → 수령확인 → 리뷰, 신청 → 자동취소, 운송장 → 배송지 미입력 전달불가.
  • focused test 전체를 실행한다.
  • 직접 영향 회귀와 ktlint를 실행한다.
  • 전체 test 실행 여부를 판단한다. 공통 결제/FCM/스케줄러 경계를 변경했으므로 최종 Gate에서는 전체 test를 실행한다.

Phase 7 Gate

Goal 실행 P7-GATE: 선물하기 릴리스 후보를 최종 판정한다.

./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.
  • RED: 문서/DDL의 기존 G{yyyyMMdd}{dailySequence6}와 기존 카테고리 필드 계약을 확인한다.
  • GREEN: classificationNumber, categoryCode, name, receiptCode, representativeItem, requiresDamageWaiver, isActive와 새 신청번호 형식을 반영한다.
  • 검증: 세 문서가 같은 제약과 응답 정책을 설명하는지 대조한다.

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.
  • RED: 새 필드 저장/조회, 관리자 request/response 분리, 사용자 폼 옵션 name 조합, validation 실패 테스트를 작성하고 실패를 확인한다.
  • GREEN: 엔티티, DTO, service mapping, validation을 최소 구현한다.
  • 검증: 카테고리 관련 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.
  • RED: 형식, 카테고리별 독립 sequence, 날짜별 독립 sequence, 9999 초과, 병렬 unique 테스트를 작성하고 실패를 확인한다.
  • GREEN: 기존 transaction/locking 방식을 유지해 최소 구현한다.
  • 검증: 신청번호 focused test와 직접 영향 테스트를 실행한다.

Phase 9 Gate

./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.
  • RED: 기존 관리자 API 문서에 전체 선물함 목록/상세 조회 계약이 없음을 확인한다.
  • GREEN: GET /api/v2/admin/gifts, GET /api/v2/admin/gifts/{applicationNo}와 availableActions 정책을 문서화한다.
  • 검증: 세 문서가 같은 필터, 응답 필드, 개인정보 미입력 표현을 설명하는지 대조한다.

Task 10.2 관리자 선물함 query service 구현

Goal 실행 P10-T2: 관리자 선물함 목록/상세 조회 service와 repository query를 구현한다.

  • Files: Create GiftAdminQueryService.kt; Modify GiftRepository.kt, GiftDeliveryRepository.kt.
  • Tests: Create GiftAdminQueryServiceTest.kt.
  • RED: 필터, 페이징, 목록/상세 필드, 수취인 배송지 미입력 빈 문자열, 상태별 availableActions 테스트를 작성하고 실패를 확인한다.
  • GREEN: 조회 service와 repository query를 최소 구현한다.
  • 검증: GiftAdminQueryServiceTest를 실행한다.

Task 10.3 관리자 선물함 HTTP API 구현

Goal 실행 P10-T3: 관리자 controller와 response DTO에 목록/상세 endpoint를 연결한다.

  • Files: Modify AdminGiftController.kt, AdminGiftResponse.kt.
  • Tests: Modify AdminGiftControllerTest.kt.
  • RED: MockMvc로 목록/상세 JSON 계약과 권한/오류를 테스트하고 실패를 확인한다.
  • GREEN: 두 GET endpoint와 response DTO를 최소 구현한다.
  • 검증: AdminGiftControllerTest, GiftAdminQueryServiceTest를 실행한다.

Phase 10 Gate

./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 전체 아니요 릴리스 차단 기록
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 문자열이다.

  • RED: controller test의 선물 날짜/시간 응답 기대값을 Z 포함 UTC ISO 문자열로 변경한다.

  • 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를 포함하지 않아 실패함을 확인한다.

  • GREEN: response DTO의 LocalDateTime 필드를 String, LocalDateTime? 필드를 String?로 바꾸고 기존 LocalDateTime.toUtcIso() extension으로 변환한다.

  • GREEN 확인: 같은 focused controller test가 통과하는지 확인한다.

  • 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.
  • RED: 설정 미등록 시 GET /api/v2/admin/gift-mailbox가 data=null을 반환하는 실패 test를 작성한다.
  • RED: PUT /api/v2/admin/gift-mailbox가 전역 단일 주소를 생성하고, 재호출 시 같은 row를 갱신하는 실패 test를 작성한다.
  • RED: 이름/연락처/우편번호/주소 공백 입력을 거부하는 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: 단일 row persistence, 관리자 service, request/response DTO, controller endpoint를 구현한다.
  • GREEN 확인: focused test 통과를 확인한다.

Task 11.2 선물 상세 받을 주소 노출 구현

Goal 실행 P11-T2: 보낸 선물의 전달 완료 전 정상 진행 상세에만 받을 주소를 노출한다.

  • Files: Modify GiftQueryService.kt; Test GiftQueryServiceTest.kt, GiftControllerTest.kt.
  • RED: direction=SENT이고 status가 RECEIVED, TRACKING_REGISTERED, ARRIVED_AT_MAILBOX, INSPECTION_COMPLETED인 상세에서 mailbox를 반환하는 실패 test를 작성한다.
  • RED: 받을 주소 미등록, 종료 상태(DELIVERED, UNDELIVERABLE, CANCELED), 또는 다른 방향이면 mailbox=null인 실패 test를 작성한다.
  • RED 확인: focused test 실패를 확인한다.
  • GREEN: GiftQueryService에서 받을 주소 repository를 조회해 기존 GiftMailboxResult로 매핑한다.
  • GREEN 확인: focused test 통과를 확인한다.

Phase 11 Gate

Goal 실행 P11-GATE: 관리자 설정과 사용자 상세 노출을 판정한다.

./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.
  • RED: 수취인 필수 정보가 없는 INSPECTION_COMPLETED 선물의 전달 완료 요청이 거부되고 상태/전달 완료 시각/푸시가 바뀌지 않는 실패 test를 작성한다.
  • RED: 수취인 필수 정보가 불완전하면 관리자 목록/상세의 availableActions에서 COMPLETE_DELIVERY가 제외되는 실패 test를 작성한다.
  • RED 확인: focused test가 기존 구현에서 의도한 assertion으로 실패하는지 확인한다.
  • GREEN: 수취인 정보 완비 조건을 공통화하고 command와 관리자 action 계산에 적용한다.
  • GREEN 확인: 같은 focused test 통과를 확인한다.

Task 12.2 배송지 미입력 기한 초과 상태 범위 보강

Goal 실행 P12-T2: 배송지 입력 기한이 지난 주소 미입력 선물을 TRACKING_REGISTERED, ARRIVED_AT_MAILBOX, INSPECTION_COMPLETED에서 UNDELIVERABLE로 종료한다.

  • Files: Modify GiftRepository.kt; Test GiftSchedulerTest.kt.
  • RED: 세 상태 모두 기한 초과 + recipientAddress=null이면 전달 불가가 되는 실패 test를 작성한다.
  • RED: 기한 동일/미도래, 주소 입력 완료, 대상 외 상태는 변경되지 않는 회귀 test를 유지한다.
  • RED 확인: ARRIVED_AT_MAILBOX, INSPECTION_COMPLETED case가 기존 query에서 실패하는지 확인한다.
  • GREEN: 기존 만료 조회 query의 상태 조건만 세 상태로 확장한다.
  • GREEN 확인: scheduler focused test 통과를 확인한다.

Phase 12 Gate

  • GiftAdminServiceTest, GiftAdminQueryServiceTest, AdminGiftControllerTest, GiftSchedulerTest의 변경 관련 test 통과.
  • 전체 ./gradlew --no-daemon test 통과(8분 33초), 종료 후 embedded Redis 및 Gradle test worker 잔존 없음.
  • ktlintCheck, build -x test, git diff --check 통과.
  • AdminGiftControllerTest에서 주소 미입력 실패와 주소 입력 완료 성공 HTTP 계약 확인.

구현 결과: 수취인 필수 정보 완비 조건을 GiftDelivery.hasCompleteRecipientInformation()으로 공통화해 전달 완료 command와 관리자 action 계산에 적용했다. 배송지 입력 기한 초과 조회는 주소 입력 가능 상태 3개로 확장했다.

Task 12.3 Embedded Redis 종료 누수 수정

  • com.github.codemonstur:embedded-redis:1.4.3의 RedisInstance.stop()과 shutdown hook 등록 동작을 확인했다.
  • initializer의 중복 JVM shutdown hook을 제거했다.
  • 공식 onShutdownForceStop(true) 옵션으로 native Redis 프로세스 종료가 무기한 waitFor()에 걸리지 않도록 변경했다.
  • 후속 요청에 따라 전체 ./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.
  • RED: 기존 리스트 응답 계약에 recipientAddressRegistered, canceledAt이 없고 배송지 deadline 규칙이 불명확함을 확인한다.
  • 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.
  • RED: 배송지 미입력/입력 완료/취소 상태의 리스트 응답 필드 실패 테스트를 작성하고 실패를 확인했다.
  • GREEN: 기존 GiftDelivery 정보를 재사용해 recipientAddressRegistered, recipientAddressDeadlineAt, canceledAt을 매핑했다.
  • 검증: 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 — 새 화면을 만들지 않고 기존 화면에 취소 기능만 추가하는 프롬프트.

  • 네 문서의 계약과 기존 전체 화면 프롬프트 무변경을 대조하고 검증 명령의 실제 결과를 기록한다.

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 — 경합 테스트가 입증한 상태 전이/환불 중복 원인에 한해 공통 경계를 최소 수정한다. 기존 사용자/자동취소 정책은 변경하지 않는다.

  • RED: service 테스트에서 RECEIVED 관리자 취소의 CANCELED, GiftDelivery.canceledAt, 원 결제액 전액 refundGiftCan 1회 호출을 검증한다. 발신자가 아닌 관리자도 가능하고, 존재하지 않거나 다른 상태면 기존 common.error.invalid_request이며 상태/환불은 변경되지 않는지 검증한다. 환불 실패 rollback 및 중복 환불 방지도 검증한다.

  • RED: AdminGiftControllerTest에 실제 Spring 트랜잭션을 통과하는 관리자 취소 통합 테스트를 추가한다. ADMIN 성공과 비관리자/미인증 거부, 모든 비허용 상태 거부, 응답 네 필드와 UTC 시각, 발신자의 실제 리워드캔/환불 내역 복원 및 관리자 잔액 불변을 검증한다. 사용자 취소·자동취소·운송장 등록·다른 관리자 취소와의 동시 요청, 환불 예외 시 DB 상태/취소 시각/환불 내역 롤백도 검증한다. mock 호출 횟수만으로 실제 환불/롤백/경합 검증을 대체하지 않는다.

  • 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 오류는 수정한 뒤 다시 확인한다.

  • GREEN: 기존 transaction, 예외, 캔 환불 경로와 GiftOperationStatusResult를 재사용해 최소 구현한다. 신규 push는 발행하지 않는다.

  • GREEN 확인: 같은 service focused test와 ./gradlew test --tests "kr.co.vividnext.sodalive.v2.api.admin.gift.adapter.in.web.AdminGiftControllerTest" 실행. 정상 envelope, 응답 필드 및 UTC occurredAt=canceledAt, 기존 ADMIN 권한 거부를 확인한다.

  • 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/빈 액션 재조회 검증.

  • RED: 목록과 상세 각각 RECEIVED -> ["CANCEL"]이며 다른 상태의 기존 액션이 그대로이고 INSPECTION_COMPLETED의 COMPLETE_DELIVERY 조건이 유지되는 테스트를 작성한다.

  • RED 확인: ./gradlew test --tests "kr.co.vividnext.sodalive.v2.gift.application.GiftAdminQueryServiceTest" 실행. 신규 assertion 실패를 확인한다.

  • GREEN: 두 응답의 공통 상태별 action 계산만 필요한 범위에서 수정한다.

  • 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 매핑, 취소 후 빈 액션과 기존 조건부 액션 통과를 확인한다.

  • 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 통과 및 사용자 흐름 기록.
  • 범위 밖: 실패와 무관한 구현, 전체 선물 도메인 리팩터링.
./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회 불변조건이 유지되고, 환불 실패 시 취소 상태가 커밋되지 않는다. 기존 테스트 구조 안에서 검증하며 새 의존성이나 스키마는 추가하지 않는다.

  • 위 focused test와 직접 영향 회귀, lint 결과를 기록한다.
  • 관리자 권한, 발신자 전액 환불, 목록/상세의 CANCEL 노출과 취소 후 빈 액션, 자동취소 전용 푸시 미발행을 검증한다.
  • 문서의 구현 예정 표시를 실제 검증된 상태로 갱신한다. 전체 회귀는 공통 경계 변경 또는 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 설정 변경.
  • RED: 0, 123456789, 999999999 나노초 입력을 사용해 초 단위 기대값과 DB 재조회/응답의 완전 일치를 검증하고 기존 코드의 실패를 확인한다.
  • GREEN: 관리자 취소 메서드에서 취소 시각을 한 번 초 단위로 정규화하고 저장·응답에 사용한다.
  • 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 실패 원인 확인
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 제거, 공식 강제 종료 옵션 적용 후 전체 테스트 정상 종료 확인

최종 보고 형식

구현 결과: 완료한 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 문서 검증 후 사용자 실행 승인으로 T2T4, R1 및 Gate를 완료했다. 요구사항은 PRD GIFT-018GIFT-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-018GIFT-021, DEC-017DEC-021.

  • Produces: P15-T2~P15-GATE의 구현 경계와 수용 조건.

  • 문서 초안: PRD 요구사항/정책과 계획 task/deployment gate를 갱신한다. 문서 작성 완료만 표시하며 검증 완료를 의미하지 않는다.

  • 최종 검증: 문서 검증 명령을 실행하고 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 유지.

  • RED: GiftEntityMappingTest.shouldPersistNullForAllSenderFields, 양방향 query 및 관리자 query, 실제 controller 응답 검증을 추가했다. 필수 sender 빈 문자열 거부는 기존 GiftCommandServiceTest.shouldRejectInvalidGiftApplication의 네 case를 재사용하고, GiftControllerTest.shouldKeepRequiredSenderRequestValidation으로 누락 네 case를 추가했다.

  • RED 확인: entity focused 5개 중 1개 실패, 관련 6개 클래스 129개 중 신규 6개 실패를 확인했다. 저장 제약 3건과 query null 매핑 3건이며 기존 113개는 모두 통과했다. Kotlin reflection은 RED에서만 사용하고 최종 테스트에서는 제거했다.

  • GREEN: 발신자 다섯 property를 var String?로 바꾸고 query service 두 곳에서 이름/전화번호 .orEmpty() 및 nullable 주소 포맷을 적용했다. DTO·요청 검증·업무 시각은 유지했다.

  • GREEN 확인: 관련 focused 테스트 129개 실패·오류·skip 0, Kotlin 컴파일과 ktlint 통과. 12개 필드 null 저장을 flush/clear/reload로 확인했다. 기존 SQL ALTER와 엔티티의 네 nullable 컬럼을 대조했으며 SQL은 실행하지 않았다.

  • 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으로 비우고 변경 건수를 반환한다.

  • RED: shouldEraseDeliveredGiftWhenDeliveredAtEqualsSevenDayCutoff, shouldIgnoreDeliveredGiftOneSecondNewerThanSevenDayCutoff, shouldEraseOlderHistoricalDeliveredGifts, shouldIgnoreNullDeliveredAtAndNonDeliveredGifts를 실제 repository/JPA fixture로 작성했다.

  • RED: shouldEraseRemainingFieldsFromPartiallyClearedGift로 12개 중 하나씩 남은 12개 행을 검증하고, 최초 all-null 및 재실행 변경 수 0 테스트를 추가했다.

  • RED: shouldPreserveGiftMemberPaymentConsentReviewAndBusinessTimestamps에서 실제 Member·UseCan·GiftReview와 선물·배송 업무 필드를 flush/clear/reload로 비교했다. 실제 변경된 행의 updatedAt은 고정하지 않고, no-op의 추가 수정 없음만 검증했다.

  • RED 확인: scheduler 기존 14개 통과 후 신규 8개가 미구현 query/service NoSuchMethodException으로 실패했다. 임시 reflection 호출은 최종 구현에서 정적 호출로 교체했다.

  • GREEN: 기존 JPQL exists 패턴의 적격 조회와 @Transactional eraseDeliveredGiftPersonalData(now)를 추가했다. 정확히 12개 필드만 dirty checking으로 null 처리하며 완전 null은 skip한다.

  • GREEN 확인: scheduler 22개 실패·오류·skip 0. 결제/동의/리뷰/Member/업무 시각 보존과 no push/refund를 확인했다.

  • 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는 바꾸지 않는다.

  • RED: JUnit MethodSource로 세 진입점의 cron/zone/transaction, 실제 delegate 및 now, lock 성공/실패/예외/소유권 5종을 검증하는 15사례를 작성했다.

  • RED 확인: 기존 22개는 통과, 신규 15개는 wrapper 부재로 실패했다. 리뷰 후 실제 beforeCommit 조기 해제 및 rollback/소유권 회귀의 추가 RED도 확인했다.

  • GREEN: 세 parameterless wrapper와 독립 key를 등록했다. 리뷰 정정에 따라 획득 성공한 신규 lock은 finally에서 transaction synchronization을 등록하고 afterCompletion에 해제한다. 기존 :00과 업무 로직은 유지한다.

  • GREEN 확인: 최초 37개 통과 후 실제 transaction 완료 경계 15사례를 보강해 최종 52개 실패·오류·skip 0을 확인했다. Kotlin 컴파일과 lint도 통과했다.

  • 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회 시도하되 무관한 실패를 고치지 않는다.
./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 확인:

  • 격리 실제 HTTP로 전/후 SENT/RECEIVED/admin 응답의 shape, 정확한 빈 주소/null, nickname, DELIVERED no-action/CTA를 확인했다.
  • SQL 문서와 엔티티를 대조해 네 sender column의 nullable 완화와 기존 길이/comment 보존 및 destructive DML 없음을 확인했다. 실제 운영 schema metadata/DDL 적용은 별도 rollout 조건이며 이번 Gate에서 수행하지 않았다.
  • 모든 instance의 nullable reader 준비 후 첫 정리 실행, 구버전 non-null reader rollback 위험, 코드만으로 삭제 데이터 복구 불가를 배포 안내에 명시했다. 운영 준비 자체를 확인했다는 의미는 아니다.
  • 실제 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 수정
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만으로 상태 경합을 덮지 않는다.
  • RED: 실제 독립 transaction/latch 11개 사례에서 성공한 주소 등록 값이 reminder/운영 변경의 오래된 entity 커밋으로 null이 되거나 상태가 되돌아가는 실패를 재현했다.
  • GREEN: 참여 writer는 Gift→Delivery 잠금과 refresh(..., PESSIMISTIC_WRITE)를 사용하고, scheduler는 최신 상태·주소·기한·알림표시를 재검사한다. 자동취소 후보의 기존 batch lock도 ID 순서와 함께 복원했다. 공통 결제 로직과 기한 정책은 변경하지 않았다.
  • GREEN 확인: 지정4클래스181개와 실제 HTTP 삭제 QA·lint 통과 후, 자동취소 잠금 복원 RED/GREEN과2클래스104개 회귀·lint도 통과했다. 최종 전체2396개 테스트·lint·build를 재실행해 통과했다.
  • 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 회귀와 이벤트 계약 통과.
  • RED: 수령확인의 stale 무푸시 기대를 정확한 발신자1회·추가0회로 교체하고 반복/거절·만료3상태/비대상/재실행 검증을 보강했다. 승인된 새 발행에 관련된 경합 기대값만 변경했다.
  • RED 확인: ./gradlew --no-daemon test --tests '*GiftCommandServiceTest' --tests '*GiftSchedulerTest' baseline102개 통과 후 production 수정 전103개 중5개가 실제 누락 호출 WantedButNotInvoked로 실패했다.
  • GREEN: 상태 전이 성공 분기에 기존 sendDeliveredToSender, sendUndeliverableToSender 호출 두 줄만 추가했다. 공통 푸시/금융/잠금/cron/DTO는 불변이다.
  • GREEN 확인: 관련4클래스168개 통과, 실제 JWT 수령확인 성공·권한 거부·반복 및 actual transaction scheduler 만료의 발신자1회·수취인0회·추가0을 확인했다. 기존 payload 테스트로 문구/딥링크를 대조했다.
  • 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와 결제는 유지, 옵션에서 제외, 신규 신청은 거부되며 추가 차감/행 생성이 없다.
  • 검증 추가: 실제 registerGift로 만든 A80/B120, 실제 가격 변경/카테고리 비활성화, DB reload와 JWT HTTP 목록/상세/옵션을 연결했다. 최초 기존가격 fixture 격리를 위해 클래스 초기화의 sizePrice 삭제 한 줄만 보강했다.
  • 실행: 단독 테스트와 관련 클래스 통과. 기존 가격 동작의 검증 보강이므로 임의 production RED는 만들지 않았다. 최초 테스트 끝의 Mockito matcher NPE는 fixture 검증 코드 오류로 구분하고 수정 후 같은 검증을 재실행했다. 가격 production은 변경하지 않았다.
  • 정리: 실제 UseCan80/120·원 결제 연결·정산·잔액100을 보존하고 비활성화 후 추가 Gift/차감0을 확인했다. 운영 데이터·실제 FCM·공유 설정은 건드리지 않았다.

Phase 16 Gate

Goal ID: P16-GATE

  • 시작 조건: 두 Task의 실제 증거 확보. 완료 기준: 누락 알림과 가격/카테고리 시나리오 PASS, 관련/전체 회귀·lint/build·독립 리뷰 및 임시자원 정리.
  • 관련6클래스216개 테스트/lint를 통과했고 최종 전체 실행에서도 해당 테스트를 포함해 통과했다. 아래 실제 명령을 기록했으며 별도 미실행 명령을 실행했다고 표시하지 않는다.
  • 최초 전체 실행15분 timeout을 성공으로 계산하지 않았다. 상세 로그를 남긴20분 제한 재실행은18분22초에 exit0, 전체2400개·lint·build 통과했다.
  • 실제 localhost HTTP로 가격/기존 신청 보존, 수령확인 푸시 경계와 만료 transaction을 검증했다. 실제 transport/기기 실수신은 미검증이다.
  • 독립 reviewer APPROVE, 차단 사항 없음. PRD 가격/푸시 성공 기준2개만 새 증거로 완료 표시하고 과거 나머지 항목은 유지했다.
  • 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·배포·커밋·푸시.
  • RED: 고정 입력3개 중 나노초0은 통과하고123456789/999999999 두 입력은 DB 재조회와 반환값 불일치로 실패했다. 실제 transaction/flush/clear/재조회로 이전과 같은 반올림 메커니즘을 확인했다.
  • GREEN: now().truncatedTo(ChronoUnit.SECONDS)를 한 번 계산하고 기존 변수로 저장·응답에 사용했다. production 변경은 import와 한 줄이며 원래581행 완전 일치 비교를 유지했다.
  • 검증: 관련137개와 최신 전체2403개 테스트 실패·오류·skip0, lint/build 통과. 실제 JWT HTTP 수령확인과 세 나노초 입력 모두 저장·응답의 완전 일치를 확인했다.
  • 재발 방지/리뷰: 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, 기존 HEAD52e5c974 및 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 상태별 보존기간과 통합 파기 회귀 구현

  • 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회다.

  • RED: 기존 production 이름을 유지한 채 UNDELIVERABLE의 정확한 7일 경계 파기 테스트를 추가한다.

  • RED 확인: ./gradlew --no-daemon --console=plain test --tests '*GiftSchedulerTest.shouldEraseGiftWhenMatchingTimestampEqualsSevenDayCutoff'로 미구현 assertion 실패를 확인했다. compilation/fixture 오류는 RED 증거가 아니다.

  • GREEN: 최소 JPQL 조건 확장과 내부 symbol/caller rename만 적용했다. 두 상태의 경계·과거 이력·null/다른 시각 배제·unsupported 상태·부분 파기·재실행·보존 검증을 기존 테스트 parameterization으로 보강했다.

  • GREEN 확인: 같은 테스트 2개 및 GiftSchedulerTest 전체 59개를 재실행해 성공을 확인했다.

  • 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

  • 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 다중 인스턴스·배포는 미검증이다.