73 KiB
PRD: 크리에이터 선물하기
문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | Phase 16 누락 푸시 보완·가격 스냅샷 검증 완료, 운영 반영 전 |
| 작성일 | 2026-09-29 |
| 최종 수정일 | 2026-10-26 |
| 대상 제품 | 크리에이터 선물하기 |
| 작성자·결정권자 | 사용자 |
| 관련 API Contract | 별도 문서 없음. 이 문서의 8. API 계약을 기준으로 사용 |
| 관련 구현 계획 | docs/20260929_크리에이터_선물하기/plan-task.md |
| 관련 review | 없음 |
Phase 14 관리자 취소와 목록·상세
CANCEL액션은 서버 코드에 구현됐다. 관련 테스트·실제 JWT HTTP·리뷰·테스트 제외 빌드를 검증했으며, 전체 테스트는 사용자 요청으로 완료 기준에서 제외했다. 운영 배포 여부와 별도 프론트엔드 구현 여부는 이 문서의 완료 상태에 포함하지 않는다.
Phase 15 서버 구현과 검증을 완료했다. 최종 2,396개 테스트·lint·build와 실제 JWT HTTP 삭제 전후 QA 및 리뷰를 통과했다.
gift-schema.sql하단의 nullable ALTER는 기록만 했으며 운영 DB 실행·배포·커밋·푸시는 하지 않았다.
Phase 16은 수령확인·배송지 만료의 발신자 알림 두 곳과 기존 신청 가격 보존 검증을 완료했다. 최신 전체 2,400개 테스트·lint·build 및 독립 리뷰를 통과했다. 실제 FCM/기기 수신과 운영 배포는 미검증이며, 후속 커밋 승인에 따른 관련 216개 테스트 재검증 결과는
plan-task.md에 기록했다.
1. Overview
팬이 내부 재화인 캔을 사용해 크리에이터에게 실물 선물을 보낼 수 있는 API를 제공한다. 팬은 선물 신청, 운송장 등록, 취소, 상세 조회와 리뷰 작성을 할 수 있고, 크리에이터는 운송장 등록 이후 받은 선물을 확인하고 배송지를 입력한 뒤 수령 확인을 할 수 있다. 관리자는 선물 카테고리와 사이즈별 가격을 관리하고, 운영자는 사서함 도착, 검수, 전달 완료/불가 상태를 처리한다.
2. Problem Statement
- 현재 팬이 크리에이터에게 실물 선물을 보내는 신청, 결제, 배송 상태, 검수 상태를 서버에서 일관되게 추적할 수 없다.
- 선물 가격은 사이즈별로 다르고 운영자가 변경해야 하므로 코드 상수만으로는 운영 요구를 충족할 수 없다.
- 운송장 미등록, 크리에이터 배송지 미입력, 검수 실패 같은 종료 상태와 환불/푸시 정책이 명확히 정의되어야 한다.
- 팬과 크리에이터가 서로 다른 시점에 약관과 개인정보 처리에 동의해야 하므로 신청 건 기준의 동의 스냅샷이 필요하다.
- 클라이언트가 날짜/시간 응답을 안정적으로 해석할 수 있도록 선물 API의 응답 시각은 timezone 없는
LocalDateTimeJSON이 아니라 UTC ISO 문자열이어야 한다.
문제를 해결했다는 판단은 선물 신청부터 전달 완료 또는 종료 상태까지 모든 상태 전이가 API/스케줄링/운영 API로 추적되고, 캔 사용내역과 푸시가 상태별 정책대로 처리되는 것으로 한다.
3. Goals
- 팬이 선물 사이즈, 카테고리, 발신자 정보, 약관 동의를 입력해 선물 보내기를 신청할 수 있다.
- 선물 신청 시 캔을 즉시 차감하고 내부적으로 선물 신청과 캔 사용내역을 연결한다.
- 팬은 운송장 등록 전 선물 신청을 취소할 수 있고, 3일 내 운송장을 등록하지 않으면 자동취소 및 전액 환불된다.
- 관리자는 운송장 등록 전
RECEIVED선물을 취소하고 발신자에게 사용 캔 전액을 리워드캔으로 환불할 수 있다. - 크리에이터는 팬이 운송장을 등록한 선물부터 받은 선물 목록과 상세를 조회하고 7일 이내 배송지와 수령 약관 동의를 등록할 수 있다.
- 팬과 크리에이터는 같은 선물함 목록 API에서 보낸/받은 내역을 함께 또는 개별 조회할 수 있다.
- 운영자는 상태별 전용 관리자 API로 사서함 도착, 검수완료, 전달불가, 전달완료를 처리할 수 있다.
- 선물 카테고리와 사이즈별 가격은 관리자 API로 관리할 수 있다.
- 상태별 팬/크리에이터 푸시 문구와 딥링크를 명확히 정의한다.
4. Non-Goals
- 약관 본문 제공 API는 포함하지 않는다. 약관 내용은 별도 API 또는 콘텐츠로 제공한다.
- 실물 택배사 API 연동, 송장 유효성 실시간 조회, 배송 추적 자동화는 포함하지 않는다.
- 선물 이미지, 첨부파일, 메시지 카드는 포함하지 않는다.
- 선물 카테고리 hard delete는 허용하지 않는다.
- 선물 사이즈 정의 자체를 관리자에서 추가/삭제하는 기능은 포함하지 않는다.
- 캔 사용내역 화면에서 사용자가 선물 신청 상세로 이동하는 기능은 포함하지 않는다.
5. Target Users and Permissions
| 사용자 | 목표 | 주요 작업 | 권한 |
|---|---|---|---|
| 팬 | 크리에이터에게 선물을 보낸다 | 사이즈/카테고리 조회, 신청, 운송장 등록, 취소, 보낸 내역 조회, 리뷰 작성 | 인증 회원 |
| 크리에이터 | 받은 선물을 확인하고 배송지를 입력한다 | 받은 내역 조회, 배송지 입력, 수령 확인 | 인증 회원 중 크리에이터 |
| 관리자 | 선물 운영 설정과 선물 상태를 관리한다 | 카테고리, 사이즈 가격 관리, RECEIVED 선물 취소 |
관리자 |
| 운영자 | 선물 검수/전달 상태를 처리한다 | 사서함 도착, 검수완료, 전달불가, 전달완료 처리 | 관리자 또는 운영 권한 |
| 스케줄러 | 기한 초과 건을 자동 처리한다 | 운송장 미등록 자동취소, 배송지 미입력 전달불가 | 시스템 |
기본 권한 정책은 기존 인증 필요 API와 관리자 API 정책을 따른다. 팬과 크리에이터는 본인과 관련된 선물만 조회/변경할 수 있다.
6. 핵심 정책
6.1 선물 상태
| 상태 코드 | 표시명 | 의미 | 종료 상태 |
|---|---|---|---|
RECEIVED |
접수 완료 | 팬이 신청하고 캔 결제를 완료한 최초 상태 | 아니오 |
TRACKING_REGISTERED |
발송 확인 | 팬이 택배사와 운송장 번호를 등록한 상태 | 아니오 |
ARRIVED_AT_MAILBOX |
사서함 도착 | 운영자가 소다라이브 사서함 도착을 확인한 상태 | 아니오 |
INSPECTION_COMPLETED |
검수완료 | 운영자가 선물 검수를 통과 처리한 상태 | 아니오 |
DELIVERED |
전달완료 | 크리에이터 수령확인 또는 운영자 전달완료 처리 상태 | 예 |
UNDELIVERABLE |
전달불가 | 검수 실패 또는 배송지 미입력 기한 초과로 전달할 수 없는 상태 | 예 |
CANCELED |
신청 취소 | 운송장 등록 전 사용자 취소, ADMIN 취소 또는 자동취소 상태 | 예 |
상태 전이는 다음을 기본으로 한다.
RECEIVED -> TRACKING_REGISTERED -> ARRIVED_AT_MAILBOX -> INSPECTION_COMPLETED -> DELIVERED
RECEIVED -> CANCELED
TRACKING_REGISTERED -> UNDELIVERABLE
ARRIVED_AT_MAILBOX -> UNDELIVERABLE
INSPECTION_COMPLETED -> UNDELIVERABLE
6.2 기한 정책
- 팬은 선물 신청 후 3일 이내에 운송장 번호를 등록해야 한다.
- 운송장 등록 기한 24시간 전 팬에게 안내 푸시를 보낸다.
- 운송장 등록 기한을 넘긴
RECEIVED선물은 스케줄러가CANCELED로 변경하고 사용 캔을 전액 환불한다. - 크리에이터는 팬이 운송장을 등록한 뒤 7일 이내에 배송지를 입력해야 한다.
- 배송지 입력 기한 24시간 전 크리에이터에게 안내 푸시를 보낸다.
- 배송지 입력 기한을 넘긴 선물은 스케줄러가
UNDELIVERABLE로 변경하고 전달 불가 사유를배송지 미입력 기한 초과로 저장한다.
6.3 신청번호 정책
- 신청번호
applicationNo는 내부 기본키와 별도로 발급한다. - 형식은
{receiptCode}-{classificationNumber}{YYMMDD}{SequenceNo}로 한다. receiptCode는 선택한 카테고리의 접수코드이고,classificationNumber는 선택한 카테고리의 분류번호다.YYMMDD는 Asia/Seoul 기준 신청일이다.SequenceNo는 선택한 카테고리와 신청일 조합 안에서 1부터 증가한다.SequenceNo는 최소 4자리로 왼쪽을 0으로 채우고,9999를 초과하면 자리수를 늘린다.- 예시는
A-1002609300001,C-20026093010000이다. applicationNo는 unique이며 생성 후 변경하지 않는다.- 동시에 여러 선물 신청이 발생해도 같은
applicationNo가 발급되지 않아야 한다. - 구현은 DB unique 제약, 원자적 채번, 충돌 시 재시도 중 하나 이상의 방식으로 경합을 처리해야 한다.
6.4 선물 사이즈와 가격
선물 사이즈는 고정 정책이므로 코드 enum으로 관리한다.
| 코드 | 표시명 | 기준 | 초기 기본금액 |
|---|---|---|---|
SMALL |
소형 | 가로+세로+높이 100cm 이하, 무게 5kg 이하 | 100캔 |
MEDIUM |
중형 | 가로+세로+높이 120cm 이하, 무게 15kg 이하 | 150캔 |
LARGE |
대형 | 가로+세로+높이 160cm 이하, 무게 20kg 이하 | 200캔 |
가격은 관리자 설정 DB에서 관리한다.
basePriceCan: 기본 금액. 클라이언트가 취소선 표시 등에 사용할 수 있다.salePriceCan: 실제 표시/결제 금액. 할인 금액이 아니라 최종 결제 금액이다.- 할인 없음은
salePriceCan == basePriceCan으로 표현한다. - 선물 신청 시 실제 차감 금액은 선택한 사이즈 가격 설정의
salePriceCan이다. - 선물 신청 건에는 신청 시점의 최종 결제 금액인
salePriceCan만 스냅샷으로 저장한다.
6.5 선물 카테고리
카테고리는 관리자 페이지에서 등록, 수정, 논리 삭제할 수 있다.
| 필드 | 타입 | 제약 | 설명 |
|---|---|---|---|
classificationNumber |
string | 숫자 3자리, unique | 신청번호에 사용하는 분류번호. 사용자에게 별도 필드로 표시하지 않음 |
categoryCode |
string | 50자 이하, unique | 내부 범용코드. 사용자에게 표시하지 않음 |
name |
string | 50자 이하 | 분류명 |
receiptCode |
string | 영문 대문자 1~9자, unique | 신청번호에 사용하는 접수코드 |
representativeItem |
string | 100자 이하 | 대표품목. 사용자 폼 옵션의 name 조합에 사용 |
requiresDamageWaiver |
boolean | DDL은 tinyint(1) |
파손면책 동의 필요 여부 |
isActive |
boolean | DDL은 tinyint(1) |
활성 여부 |
requiresDamageWaiver == true인 카테고리를 선택한 팬은 선물 신청 시 damageWaiverAgreed=true를 추가로 보내야 한다.
사용자 선물 신청 폼 옵션에서는 카테고리 응답의 name을 분류명/대표품목 형태로 조합해 내려준다. 관리자 API에서는 name과 representativeItem을 각각 내려주고 각각 수정할 수 있어야 한다.
6.6 약관 동의
약관 본문은 이번 API 범위 밖이다. API는 동의 여부만 확인하고 선물 신청 건에 동의 스냅샷을 저장한다.
팬 신청 동의는 선물 보내기 등록 시점에 저장한다.
| API 필드 | 표시명 | 저장 시각 |
|---|---|---|
senderTermsAgreed |
상품 전달 이용약관 동의 | senderTermsAgreedAt |
senderPrivacyAgreed |
상품 전달을 위한 개인정보 수집 및 이용 동의 | senderPrivacyAgreedAt |
크리에이터 수령 동의는 배송지 입력 시점에 저장한다.
| API 필드 | 표시명 | 저장 시각 |
|---|---|---|
recipientTermsAgreed |
상품 전달 이용약관 동의 | recipientTermsAgreedAt |
recipientPrivacyAgreed |
상품 전달을 위한 개인정보 수집 및 이용 동의 | recipientPrivacyAgreedAt |
각 동의 필드가 true가 아니면 해당 API는 실패한다.
6.7 결제와 환불
- 결제는 내부 재화인 캔으로 한다.
- 캔 사용내역에
선물하기용도를 추가한다. - 선물 보내기 등록 API 성공 시 즉시
salePriceCan만큼 캔을 차감한다. - 선물 신청과 캔 사용내역은 내부적으로 연결되어야 한다.
- 사용자 캔 사용내역 화면에서 선물 신청으로 이동할 수 없어도 된다.
- 운송장 등록 전 사용자 취소 또는 3일 자동취소 시 사용 캔을 전액 환불한다.
- ADMIN은 운송장 등록 전
RECEIVED에서 취소할 수 있으며 기존refundGiftCan경로로 발신자에게 전액 리워드캔 환불한다. 상태 변경, 취소 시각 저장, 환불은 같은 트랜잭션에서 처리한다. - 운송장 등록 후에는 팬이 취소할 수 없다.
6.8 목록 노출
- 선물함 목록 API는
type=ALL|SENT|RECEIVED를 지원한다. - 기본값은
ALL이다. - 응답 item에는
direction=SENT|RECEIVED를 포함한다. - 보낸 선물은 신청 직후부터 팬에게 노출한다.
- 받은 선물은 팬이 운송장 번호를 등록해 상태가
TRACKING_REGISTERED이상이 된 뒤 크리에이터에게 노출한다. - 배송지 미입력 상태라면 받은 선물 item/detail에
recipientAddressRequired=true,recipientAddressDeadlineAt을 내려준다. - 상세 조회는 팬과 크리에이터가 서로의 개인정보를 알 필요 없이 선물을 보내거나 받을 수 있게 설계한다.
- 상세 조회에서 상대방 정보는 닉네임만 노출한다. 로그인 회원 본인이 신청 또는 입력한 개인정보는 본인에게만 노출한다.
- 로그인 회원이 받는 크리에이터이면 보내는 팬의 닉네임만 알 수 있고, 팬이 신청 시 등록한 이름/휴대폰 번호/주소는 알 수 없다.
- 로그인 회원이 보내는 팬이면 받는 크리에이터의 닉네임만 알 수 있고, 크리에이터가 입력한 이름/휴대폰 번호/주소는 알 수 없다.
- 보내는 팬 상세에는 팬 본인이 신청 시 등록한 이름, 휴대폰 번호, 주소를 내려준다.
- 보내는 팬 상세에는 관리자가 등록한 전역 선물 받을 주소를
mailbox로 내려준다. 단, 정상 진행 상태 중DELIVERED전 상태(RECEIVED,TRACKING_REGISTERED,ARRIVED_AT_MAILBOX,INSPECTION_COMPLETED)에서만 내려준다. - 받는 크리에이터 상세에는 크리에이터 본인이 입력한 받는 주소의 이름, 휴대폰 번호, 주소를 내려준다.
- 주소 형식은
(우편번호) 주소, 상세주소문자열로 내려준다. - 로그인 회원이 알아야 하는 데이터가 아니거나 아직 입력되지 않은 데이터는
null로 내려준다. - 클라이언트의 입력 필요 상태 판단은
null값이 아니라 행동 필요 flag로 한다. - 상세 조회는
trackingRequired와recipientAddressRequired를 내려준다. trackingRequired=true는direction=SENT인 팬이 아직 운송장 번호를 등록해야 하는 상태임을 의미한다.recipientAddressRequired=true는direction=RECEIVED인 크리에이터가 아직 받는 주소를 입력해야 하는 상태임을 의미한다.- 현재 로그인 회원이 처리할 일이 아니거나 이미 처리할 수 없는 상태이면 해당 flag는
false다. - 상세 조회는 정상 진행 상태 5개(
RECEIVED,TRACKING_REGISTERED,ARRIVED_AT_MAILBOX,INSPECTION_COMPLETED,DELIVERED)의 날짜/시간을 순서대로 내려준다. CANCELED,UNDELIVERABLE은 정상 진행 타임라인 상태가 아니며, 각각 별도 종료 정보로canceledAt,undeliverableAt,undeliverableReason을 내려준다.- 선물 사용자/관리자 API의 모든 응답 시각 필드는 UTC ISO 문자열로 내려준다. 예:
2026-09-30T12:00:00Z. - 응답 시각 필드는
Z또는 offset 없이 내려가는LocalDateTime기본 직렬화 형식을 사용하지 않는다.
6.9 리뷰
- 리뷰는 선물을 보낸 팬만 작성할 수 있다.
- 리뷰는
DELIVERED상태 이후 선물 건당 1회만 작성할 수 있다. - 별점은 1~5점이다.
- 키워드는 여러 개 선택해 등록할 수 있다.
- 각 키워드는 최대 255자다.
- 추가의견은 최대 255자다.
6.10 푸시와 딥링크
- 푸시 본문의
\n은 줄바꿈 문자로 처리한다. - 푸시 터치 시 선물 상세 페이지로 이동하도록 단일 딥링크를 작성한다.
- 딥링크는
deepLinkValue=GIFT_DETAIL,deepLinkId=applicationNo를 사용한다. - 보낸 선물/받은 선물 판정은 딥링크가 아니라 선물 상세 조회 API가 로그인 회원 기준으로 수행한다.
- 상세 조회 API는 로그인 회원이
senderMemberId이면direction=SENT,recipientMemberId이고 받은 선물 노출 조건을 만족하면direction=RECEIVED로 응답한다. - 둘 다 아니거나 받은 선물 노출 조건을 만족하지 않으면 권한 오류 또는 미존재 오류를 반환한다.
6.11 택배사 입력 정책
- 현재 외부 택배 조회, PG 에스크로, 커머스 플랫폼 연동 계획은 없다.
- 국가 단일 택배사 코드 표준이나 외부 provider별 코드를 이번 PRD의 기준으로 삼지 않는다.
- 클라이언트는 직접 입력이 아니라 별도 Dropdown으로 택배사 선택지를 제공한다.
- 운송장 등록 API는 선택된 택배사의 한글 표시명
courierCompanyName을 받는다. - 서버는
courierCompanyName을 표시/확인용 스냅샷으로 그대로 저장한다. courierCompanyName은 필수이며 최대 50자로 제한한다.- 외부 배송조회/PG/커머스 연동이 후속 범위로 추가되면 그때
courierCompanyCode와 provider별 매핑 정책을 별도 PRD에서 정의한다. - 크리에이터가 수령확인 API로
DELIVERED처리해도 팬에게는 전달 완료 푸시를 보낸다. - 크리에이터가 직접 수령확인한 경우 크리에이터에게는 배송 완료/전달완료 푸시를 보내지 않는다.
7. 기능 요구사항
| ID | 상태 | 요구사항 | 수용 기준 | 계획 연결 |
|---|---|---|---|---|
GIFT-001 |
확정 | 팬은 활성 카테고리와 사이즈별 가격을 조회할 수 있다 | 비활성 카테고리는 사용자 조회에 노출되지 않고, 사이즈 응답에는 basePriceCan, salePriceCan이 포함된다 |
P1 |
GIFT-002 |
확정 | 팬은 선물 보내기 등록 시 캔을 즉시 결제하고 고유 신청번호를 받는다 | 성공 시 상태는 RECEIVED, 캔 사용내역 선물하기가 생성되고 선물과 연결되며 동시 신청에도 중복 없는 applicationNo가 발급된다 |
P2 |
GIFT-003 |
확정 | 팬 신청 약관 2개와 조건부 파손면책 동의를 검증한다 | 필수 동의가 true가 아니면 신청이 실패한다 |
P2 |
GIFT-004 |
확정 | 팬은 운송장 등록 전 신청을 취소할 수 있다 | RECEIVED 상태에서만 취소되고 전액 환불된다 |
P2 |
GIFT-005 |
확정 | 팬은 3일 내 운송장을 등록해야 한다 | 등록 성공 시 상태는 TRACKING_REGISTERED, 크리에이터 받은 목록에 노출된다 |
P3 |
GIFT-006 |
확정 | 스케줄러는 운송장 미등록 건을 자동취소한다 | 기한 초과 RECEIVED가 CANCELED로 바뀌고 전액 환불된다 |
P4 |
GIFT-007 |
확정 | 크리에이터는 운송장 등록 후 받은 선물을 조회하고 배송지를 입력한다 | 배송지 입력 시 수령자 정보와 수령 약관 2개를 저장한다 | P3 |
GIFT-008 |
확정 | 스케줄러는 배송지 미입력 기한 초과 건을 전달불가 처리한다 | 기한 초과 건이 UNDELIVERABLE로 바뀌고 사유가 저장된다 |
P4 |
GIFT-009 |
확정 | 운영자는 상태별 관리자 API로 선물 상태를 처리한다 | 잘못된 이전 상태에서 상태변경 API를 호출하면 실패한다 | P5 |
GIFT-010 |
확정 | 팬은 DELIVERED 이후 리뷰를 1회 작성할 수 있다 |
중복 리뷰, 비전달완료 리뷰, 수신자 리뷰는 실패한다 | P6 |
GIFT-011 |
확정 | 팬/크리에이터 대상 푸시를 상태별로 분리 발송한다 | 각 상태 전이에서 지정된 수신자에게만 지정 문구와 딥링크가 발송된다 | P7 |
GIFT-012 |
확정 | 관리자는 카테고리를 등록/수정/논리삭제할 수 있다 | 삭제는 isActive=false만 수행한다 |
P1 |
GIFT-013 |
확정 | 관리자는 사이즈별 가격을 수정할 수 있다 | 새 신청에는 변경 가격이 적용되고 기존 신청 스냅샷은 바뀌지 않는다 | P1 |
GIFT-015 |
확정 | 관리자는 전체 선물함 목록과 상세를 조회하고 현재 상태에서 가능한 운영 액션을 확인할 수 있다 | 목록은 상태/신청번호/닉네임 검색과 페이징을 지원하고, 상세는 발송인/수취인 개인정보와 상품/배송/상태 정보를 내려준다 | P10 |
GIFT-016 |
확정 | 관리자는 운송장 등록 전 접수된 선물을 취소할 수 있다 | 관리자만 호출할 수 있고 RECEIVED에서만 CANCELED로 전이하며 GiftDelivery.canceledAt을 저장하고 발신자에게 사용 캔 전액을 리워드캔으로 환불한다 |
P14-T2 |
GIFT-017 |
확정 | 관리자 선물함 목록과 상세는 현재 가능한 액션으로 취소 가능 여부를 함께 제공한다 | RECEIVED에서만 availableActions=["CANCEL"]; 다른 상태의 기존 액션은 유지한다 |
P14-T3 |
GIFT-018 |
확정 | 전달 완료 후 7일이 지난 선물의 배송 개인정보를 지운다 | 실행 시 기존 업무 시각 관례의 now에 대해 status=DELIVERED, deliveredAt != null, deliveredAt <= now.minusDays(7)인 과거 누적 건과 이후 조건을 충족하는 모든 건의 GiftDelivery 이름/연락처/우편번호/주소/상세주소, 택배사명/운송장번호 12개 필드를 null 처리한다. 식별자·상태·회원 닉네임·결제·동의·리뷰와 업무 lifecycle/deadline/reminder 시각은 보존하며 재실행은 추가 변경 없이 끝난다 |
P15-T2, P15-T3 |
GIFT-019 |
확정 | 배송 개인정보를 nullable persistence로 저장한다 | GiftDelivery의 sender 다섯 Kotlin property는 모두 var String?; 현행 NOT NULL sender DB 열 네 개만 nullable로 완화한다. sender_address_detail은 이미 nullable이며 ALTER하지 않는다. 기존 CREATE 정의는 유지하고 gift-schema.sql 끝에 추가된 ALTER가 fresh install 최종 schema에도 적용된다 |
P15-T1, P15-T2 |
GIFT-020 |
확정 | 개인정보 null 상태에서도 기존 사용자/관리자 API 계약을 유지한다 | 기존 request 필수 타입/validation, response schema/바깥 객체/visibility와 flags/deadlines 계산 규칙을 유지한다. 데이터 없는 non-null 문자열은 ""; 주소 전체가 null이면 주소 문자열은 정확히 "". nullable 바깥 객체와 tracking/admin courier/tracking은 기존 null 정책 유지 |
P15-T2, P15-GATE |
GIFT-021 |
확정 | 배송지 기한 만료와 deadline reminder 업무 메서드를 시간별 스케줄로 등록한다 | 기존 매시 00분 자동취소 등록은 유지한다. Asia/Seoul 기준 매시 10분은 기존 recipient expiry, 20분은 신규 erasure, 40분은 기존 reminders를 각자 독립 @Scheduled/lock/transaction wrapper로 위임한다. 기존 업무 로직은 바꾸지 않고 작업 간 비중첩을 보장하지 않는다 |
P15-T3, P15-T4 |
8. API 계약
8.1 공통 규칙
- 인증: 기존 인증 필요 API와 동일한 인증 헤더를 사용한다.
- 관리자 API: 기존 관리자 인증/권한 정책을 따른다.
- 날짜/시간 응답: UTC ISO-8601 문자열을 사용한다.
- 페이징:
page기본값0,size기본값20, 허용 범위20..50으로 보정한다. - 목록 응답은
items,page,size,hasNext를 포함한다. - 오류 envelope와 메시지 키는 기존 공통 오류 정책을 따른다.
8.2 선물 신청 폼 옵션 조회
GET /api/v2/gifts/form-options
선물 신청 페이지 진입 시 필요한 팬용 사이즈와 카테고리 옵션을 한 번에 조회한다.
Response:
{
"sizes": [
{
"sizeCode": "SMALL",
"name": "소형",
"maxTotalLengthCm": 100,
"maxWeightKg": 5,
"basePriceCan": 100,
"salePriceCan": 80
}
],
"categories": [
{
"categoryId": 1,
"name": "인형/피규어",
"requiresDamageWaiver": true
}
]
}
사용자 API는 categoryCode, classificationNumber, receiptCode, representativeItem을 별도 필드로 응답하지 않고 활성 카테고리만 내려준다.
8.3 선물 보내기 등록
POST /api/v2/gifts
Request:
{
"recipientMemberId": 100,
"senderName": "홍길동",
"senderPhoneNumber": "01012345678",
"senderZipCode": "06234",
"senderAddress": "서울시 강남구 ...",
"senderAddressDetail": "101동 1001호",
"sizeCode": "SMALL",
"categoryId": 1,
"senderTermsAgreed": true,
"senderPrivacyAgreed": true,
"damageWaiverAgreed": true
}
Response:
{
"applicationNo": "A-1002609300001",
"status": "RECEIVED",
"statusName": "접수 완료",
"priceCan": 80,
"trackingDeadlineAt": "2026-10-02T03:00:00Z"
}
요구사항:
- 수신자는 크리에이터 회원이어야 한다.
- 발신자 이름, 휴대폰 번호, 우편번호, 주소는 필수다.
- 선택한 카테고리가 비활성이면 실패한다.
- 선택한 카테고리가
requiresDamageWaiver=true이면damageWaiverAgreed=true가 필수다. senderTermsAgreed와senderPrivacyAgreed가 모두true여야 한다.- 성공 시
salePriceCan만큼 캔을 차감한다.
8.4 선물함 목록 조회
GET /api/v2/gifts?type=ALL&page=0&size=20
Request Query:
| 이름 | 필수 | 기본값 | 설명 |
|---|---|---|---|
type |
아니오 | ALL |
ALL, SENT, RECEIVED |
page |
아니오 | 0 |
0 기반 page index |
size |
아니오 | 20 |
20..50 보정 |
Response:
{
"items": [
{
"applicationNo": "A-1002609300001",
"direction": "SENT",
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
"counterpartMemberId": 100,
"counterpartName": "크리에이터닉네임",
"sizeName": "소형",
"categoryName": "인형",
"priceCan": 80,
"recipientAddressRegistered": false,
"recipientAddressDeadlineAt": null,
"canceledAt": null,
"createdAt": "2026-09-29T03:00:00Z"
}
],
"page": 0,
"size": 20,
"hasNext": false
}
받은 선물은 TRACKING_REGISTERED 이상 상태부터 노출한다.
recipientAddressRegistered는 크리에이터 배송지 필수 정보가 모두 입력되었는지 여부다.
recipientAddressDeadlineAt은 direction=RECEIVED이고 배송지가 아직 입력되지 않은 경우에만 배송지 입력 기한 UTC ISO 문자열로 내려주며, 배송지 입력 완료 후에는 null로 내려준다.
canceledAt은 취소 상태의 취소일 표시용 UTC ISO 문자열이며, 취소되지 않은 선물은 null이다.
8.5 선물 상세 조회
GET /api/v2/gifts/{applicationNo}
Response:
{
"applicationNo": "A-1002609300001",
"direction": "SENT",
"status": "RECEIVED",
"statusName": "접수 완료",
"giftInfo": {
"recipientCreatorNickname": "크리에이터닉네임",
"senderNickname": null,
"sizeName": "소형",
"categoryName": "인형",
"applicationNo": "A-1002609300001",
"paidCan": 80,
"tracking": null,
"shippingRequestedAt": null
},
"senderInfo": {
"name": "홍길동",
"phoneNumber": "01012345678",
"address": "(06234) 서울시 강남구 ..., 101동 1001호"
},
"recipientAddress": null,
"mailbox": {
"name": "소다라이브 선물 담당자",
"address": "(06234) 서울시 강남구 ..., 3층 선물 접수처",
"phoneNumber": "01012345678"
},
"trackingRequired": true,
"recipientAddressRequired": false,
"recipientAddressDeadlineAt": "2026-10-09T03:00:00Z",
"delivery": {
"canceledAt": null,
"undeliverableAt": null,
"undeliverableReason": null
},
"statusTimeline": [
{
"status": "RECEIVED",
"statusName": "접수 완료",
"occurredAt": "2026-09-29T03:00:00Z"
},
{
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
"occurredAt": "2026-10-02T03:00:00Z"
},
{
"status": "ARRIVED_AT_MAILBOX",
"statusName": "사서함 도착",
"occurredAt": null
},
{
"status": "INSPECTION_COMPLETED",
"statusName": "검수완료",
"occurredAt": null
},
{
"status": "DELIVERED",
"statusName": "전달완료",
"occurredAt": null
}
],
"review": null
}
mailbox는 관리자가 등록한 전역 단일 받을 주소다. 보낸 사람 관점(direction=SENT)이고 정상 진행 상태 중 DELIVERED 전 상태(RECEIVED, TRACKING_REGISTERED, ARRIVED_AT_MAILBOX, INSPECTION_COMPLETED)일 때 내려준다. 받을 주소가 등록되지 않았거나, 다른 방향이거나, DELIVERED/UNDELIVERABLE/CANCELED 상태이면 mailbox는 null이다. 주소는 상세 조회 시점의 최신 관리자 설정을 사용한다.
받는 크리에이터 관점 응답 예시는 다음과 같다.
{
"applicationNo": "A-1002609300001",
"direction": "RECEIVED",
"status": "INSPECTION_COMPLETED",
"statusName": "검수완료",
"giftInfo": {
"recipientCreatorNickname": null,
"senderNickname": "팬닉네임",
"sizeName": "소형",
"categoryName": "인형",
"applicationNo": null,
"paidCan": null,
"tracking": null,
"shippingRequestedAt": "2026-10-03T03:00:00Z"
},
"senderInfo": null,
"recipientAddress": {
"name": "김소다",
"phoneNumber": "01098765432",
"address": "(04524) 서울시 중구 ..., 202호"
},
"mailbox": null,
"trackingRequired": false,
"recipientAddressRequired": false,
"recipientAddressDeadlineAt": "2026-10-09T03:00:00Z",
"delivery": {
"canceledAt": null,
"undeliverableAt": null,
"undeliverableReason": null
},
"statusTimeline": [
{
"status": "RECEIVED",
"statusName": "접수 완료",
"occurredAt": "2026-09-29T03:00:00Z"
},
{
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
"occurredAt": "2026-10-02T03:00:00Z"
},
{
"status": "ARRIVED_AT_MAILBOX",
"statusName": "사서함 도착",
"occurredAt": "2026-10-03T03:00:00Z"
},
{
"status": "INSPECTION_COMPLETED",
"statusName": "검수완료",
"occurredAt": "2026-10-04T03:00:00Z"
},
{
"status": "DELIVERED",
"statusName": "전달완료",
"occurredAt": null
}
],
"review": null
}
팬과 크리에이터 모두 같은 상세 API를 사용하되 권한에 따라 본인 관련 선물만 조회할 수 있다.
direction=SENT이면 보내는 팬 관점의 상세 응답이다.
giftInfo.recipientCreatorNickname에는 받는 크리에이터 닉네임을 내려주고, giftInfo.senderNickname, giftInfo.shippingRequestedAt, recipientAddress는 null이다.
senderInfo에는 팬 본인이 신청 시 등록한 이름, 휴대폰 번호, 주소를 내려준다.
mailbox는 관리자가 등록한 전역 단일 받을 주소의 이름, 주소, 연락처를 내려준다. 보낸 사람 관점의 정상 진행 상태 중 DELIVERED 전 상태에서만 값이 있고, 그 외에는 null이다.
direction=SENT이고 현재 상태가 RECEIVED이면 trackingRequired=true이며, 팬 클라이언트는 운송장 등록 CTA를 표시한다.
direction=RECEIVED이면 받는 크리에이터 관점의 상세 응답이다.
giftInfo.senderNickname에는 발송인인 팬 닉네임을 내려주고, giftInfo.recipientCreatorNickname, giftInfo.applicationNo, giftInfo.paidCan, giftInfo.tracking, senderInfo, mailbox는 null이다.
giftInfo.shippingRequestedAt에는 배송신청일을 내려준다.
recipientAddress에는 크리에이터 본인이 입력한 이름, 휴대폰 번호, 주소를 내려준다.
direction=RECEIVED이고 받은 주소가 미입력이며 종료 상태가 아니면 recipientAddressRequired=true이며, 크리에이터 클라이언트는 받는 주소 입력 CTA를 표시한다.
주소는 발신자/수신자 모두 (우편번호) 주소, 상세주소 형식의 단일 문자열로 내려준다.
운송장 미입력, 받는 주소 미입력 상태의 상세 보완은 API 개발 후 후속으로 처리한다.
로그인 회원이 알아야 하는 데이터가 아니면 null로 내려준다.
정상 진행 5개 상태의 statusTimeline은 항상 같은 순서로 내려주며, 아직 도달하지 않은 상태의 occurredAt은 null이다.
CANCELED와 UNDELIVERABLE 상태에서도 statusTimeline은 도달한 정상 진행 상태의 시각을 유지하고, 종료 시각/사유는 delivery.canceledAt, delivery.undeliverableAt, delivery.undeliverableReason으로 확인한다.
8.6 운송장 번호 등록
POST /api/v2/gifts/{applicationNo}/tracking
Request:
{
"courierCompanyName": "CJ대한통운",
"trackingNumber": "1234567890"
}
Response:
{
"applicationNo": "A-1002609300001",
"status": "TRACKING_REGISTERED",
"recipientAddressDeadlineAt": "2026-10-09T03:00:00Z"
}
요구사항:
- 보낸 팬만 호출할 수 있다.
RECEIVED상태에서만 성공한다.- 성공 시 크리에이터 받은 선물 목록에 노출된다.
- 성공 시 크리에이터에게
팬 운송장 등록 완료푸시를 보낸다.
8.7 선물 보내기 취소
POST /api/v2/gifts/{applicationNo}/cancel
Request: 없음
Response:
{
"applicationNo": "A-1002609300001",
"status": "CANCELED",
"refundedCan": 80
}
요구사항:
- 보낸 팬만 호출할 수 있다.
RECEIVED상태에서만 성공한다.- 성공 시 사용 캔을 전액 환불한다.
8.8 크리에이터 배송지 입력
POST /api/v2/gifts/{applicationNo}/recipient-address
Request:
{
"recipientName": "김소다",
"recipientPhoneNumber": "01098765432",
"recipientZipCode": "04524",
"recipientAddress": "서울시 중구 ...",
"recipientAddressDetail": "202호",
"recipientTermsAgreed": true,
"recipientPrivacyAgreed": true
}
Response:
{
"applicationNo": "A-1002609300001",
"recipientAddressRegisteredAt": "2026-10-03T03:00:00Z"
}
요구사항:
- 받는 크리에이터만 호출할 수 있다.
TRACKING_REGISTERED이상, 종료 상태가 아닌 선물에만 입력할 수 있다.- 이름, 휴대폰 번호, 우편번호, 주소는 필수다.
recipientTermsAgreed,recipientPrivacyAgreed가 모두true여야 한다.
8.9 크리에이터 수령확인
POST /api/v2/gifts/{applicationNo}/delivery-complete
Request: 없음
Response:
{
"applicationNo": "A-1002609300001",
"status": "DELIVERED",
"deliveredAt": "2026-10-10T03:00:00Z"
}
요구사항:
- 받는 크리에이터만 호출할 수 있다.
INSPECTION_COMPLETED상태에서만 성공한다.- 성공 시 팬에게 전달 완료 푸시를 보낸다.
- 성공 시 크리에이터에게는 배송 완료/전달완료 푸시를 보내지 않는다.
8.10 리뷰 작성
POST /api/v2/gifts/{applicationNo}/review
Request:
{
"rating": 5,
"keywords": ["배송이 빨라요", "안내가 친절해요"],
"comment": "선물 전달 과정이 만족스러웠습니다."
}
Response:
{
"reviewId": 1,
"applicationNo": "A-1002609300001",
"rating": 5,
"keywords": ["배송이 빨라요", "안내가 친절해요"],
"comment": "선물 전달 과정이 만족스러웠습니다.",
"createdAt": "2026-10-11T03:00:00Z"
}
요구사항:
- 보낸 팬만 호출할 수 있다.
DELIVERED상태에서만 작성할 수 있다.- 선물 건당 1회만 작성할 수 있다.
rating은 1 이상 5 이하여야 한다.keywords는 배열이며, 각 keyword는 255자 이하여야 한다.comment는 255자 이하여야 한다.
8.11 관리자 카테고리 API
| Method | Path | 설명 |
|---|---|---|
| GET | /api/v2/admin/gift-categories |
카테고리 목록 조회 |
| POST | /api/v2/admin/gift-categories |
카테고리 등록 |
| PUT | /api/v2/admin/gift-categories/{categoryId} |
카테고리 수정 |
| DELETE | /api/v2/admin/gift-categories/{categoryId} |
카테고리 논리 삭제 |
등록/수정 Request:
{
"classificationNumber": "100",
"categoryCode": "DOLL",
"name": "인형",
"receiptCode": "A",
"representativeItem": "피규어",
"requiresDamageWaiver": true,
"isActive": true
}
삭제는 isActive=false로만 처리한다.
8.12 관리자 사이즈 가격 API
| Method | Path | 설명 |
|---|---|---|
| GET | /api/v2/admin/gift-size-prices |
사이즈별 가격 목록 조회 |
| PUT | /api/v2/admin/gift-size-prices/{sizeCode} |
사이즈별 가격 수정 |
수정 Request:
{
"basePriceCan": 100,
"salePriceCan": 80,
"isActive": true
}
salePriceCan은 0보다 커야 하고 basePriceCan보다 클 수 없다.
8.13 관리자 선물함 조회 API
관리자 페이지는 전체 선물함을 목록과 상세로 조회한다. 상태 변경은 8.14 운영 상태변경 API의 기존 전용 endpoint를 사용한다.
8.13.1 관리자 선물함 목록 조회
GET /api/v2/admin/gifts?status=TRACKING_REGISTERED&applicationNo=A-100&nickname=fan&page=0&size=20
Query:
| 이름 | 필수 | 설명 |
|---|---|---|
status |
아니오 | 선물 상태. 지정하면 해당 상태만 조회 |
applicationNo |
아니오 | 신청번호 부분 검색 |
nickname |
아니오 | 발송인 또는 수취인 닉네임 부분 검색 |
page |
아니오 | 0 기반 page index. 기본값 0 |
size |
아니오 | page size. 기본값 20 |
필터는 AND로 조합한다. nickname은 발송인 닉네임 또는 수취인 닉네임 중 하나에 포함되면 매칭한다.
Response:
{
"totalCount": 1,
"items": [
{
"applicationNo": "A-1002609300001",
"senderNickname": "팬닉네임",
"recipientNickname": "크리에이터닉네임",
"sizeCode": "SMALL",
"sizeName": "소형",
"categoryName": "인형",
"classificationNumber": "100",
"courierCompanyName": "CJ대한통운",
"trackingNumber": "1234567890",
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
"availableActions": ["ARRIVE_MAILBOX", "MARK_UNDELIVERABLE"]
}
],
"page": 0,
"size": 20,
"hasNext": false
}
8.13.2 관리자 선물 상세 조회
GET /api/v2/admin/gifts/{applicationNo}
Response:
{
"applicationNo": "A-1002609300001",
"senderInfo": {
"nickname": "팬닉네임",
"name": "홍길동",
"phoneNumber": "01012345678",
"address": "(06234) 서울시 강남구 ..., 101동 1001호"
},
"recipientInfo": {
"nickname": "크리에이터닉네임",
"name": "김소다",
"phoneNumber": "01098765432",
"address": "(04524) 서울시 중구 ..., 202호"
},
"productInfo": {
"sizeCode": "SMALL",
"sizeName": "소형",
"categoryName": "인형",
"classificationNumber": "100"
},
"inboundDeliveryInfo": {
"courierCompanyName": "CJ대한통운",
"trackingNumber": "1234567890"
},
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
"availableActions": ["ARRIVE_MAILBOX", "MARK_UNDELIVERABLE"]
}
크리에이터가 배송지를 아직 입력하지 않은 경우에도 recipientInfo.nickname은 내려주고, recipientInfo.name, recipientInfo.phoneNumber, recipientInfo.address는 빈 문자열 ""로 내려준다.
8.13.3 관리자 선물 받을 주소 설정 API
선물 받을 주소는 전역 단일 설정이다. 팬은 정상 진행 상태 중 DELIVERED 전인 보낸 선물 상세에서 이 주소를 확인할 수 있다.
GET /api/v2/admin/gift-mailbox
Response:
{
"name": "소다라이브 선물 담당자",
"address": "(06234) 서울시 강남구 ..., 3층 선물 접수처",
"phoneNumber": "01012345678"
}
설정이 없으면 공통 envelope의 data는 null이다.
PUT /api/v2/admin/gift-mailbox
Request:
{
"name": "소다라이브 선물 담당자",
"phoneNumber": "01012345678",
"zipCode": "06234",
"address": "서울시 강남구 ...",
"addressDetail": "3층 선물 접수처"
}
Response는 저장된 받을 주소를 name, address, phoneNumber로 내려준다. address는 기존 주소 응답과 동일하게 (우편번호) 주소, 상세주소 형식이다. name, phoneNumber, zipCode, address는 필수이며 공백만 입력할 수 없다. addressDetail은 선택이다.
관리자 목록과 상세의 availableActions는 상태별로 다음 값을 내려준다. RECEIVED에서는 CANCEL을 제공하고, 다른 상태의 기존 매핑은 유지한다.
| 현재 상태 | availableActions |
|---|---|
RECEIVED |
["CANCEL"] |
TRACKING_REGISTERED |
["ARRIVE_MAILBOX", "MARK_UNDELIVERABLE"] |
ARRIVED_AT_MAILBOX |
["COMPLETE_INSPECTION", "MARK_UNDELIVERABLE"] |
INSPECTION_COMPLETED, 수취인 정보 입력 완료 |
["COMPLETE_DELIVERY", "MARK_UNDELIVERABLE"] |
INSPECTION_COMPLETED, 수취인 정보 미입력/불완전 |
["MARK_UNDELIVERABLE"] |
DELIVERED |
[] |
UNDELIVERABLE |
[] |
CANCELED |
[] |
8.14 운영 상태변경 API
| Method | Path | 이전 상태 | 변경 상태 | 필수 입력 |
|---|---|---|---|---|
| POST | /api/v2/admin/gifts/{applicationNo}/arrive-mailbox |
TRACKING_REGISTERED |
ARRIVED_AT_MAILBOX |
없음 |
| POST | /api/v2/admin/gifts/{applicationNo}/complete-inspection |
ARRIVED_AT_MAILBOX |
INSPECTION_COMPLETED |
없음 |
| POST | /api/v2/admin/gifts/{applicationNo}/mark-undeliverable |
TRACKING_REGISTERED, ARRIVED_AT_MAILBOX, INSPECTION_COMPLETED |
UNDELIVERABLE |
reason |
| POST | /api/v2/admin/gifts/{applicationNo}/complete-delivery |
INSPECTION_COMPLETED |
DELIVERED |
저장된 수취인 이름, 휴대폰 번호, 우편번호, 주소 |
| POST | /api/v2/admin/gifts/{applicationNo}/cancel |
RECEIVED |
CANCELED |
없음 |
complete-delivery는 recipientName, recipientPhoneNumber, recipientZipCode, recipientAddress가 모두 null 또는 공백이 아닐 때만 성공한다. recipientAddressDetail은 선택 입력이다. 조건을 만족하지 않으면 상태, 전달 완료 시각, 푸시를 변경하지 않고 요청을 거부한다.
전달불가 Request:
{
"reason": "선물 접수 규격에 맞지 않습니다."
}
8.14.1 관리자 선물 신청 취소
POST /api/v2/admin/gifts/{applicationNo}/cancel
Request body: 없음
기존 관리자 API의 hasRole('ADMIN') 권한을 따르며 관리자 계정은 선물 발신자가 아니어도 취소할 수 있다. RECEIVED에서만 CANCELED로 전이하며 운송장 등록 이후에는 취소할 수 없다. 같은 트랜잭션에서 GiftDelivery.canceledAt을 기록하고 기존 refundGiftCan(gift.senderMemberId, gift.canUsageId) 경로로 발신자에게 원 결제액 전액을 리워드캔으로 환불한다. 관리자의 잔액은 변경하지 않는다.
Response data는 기존 AdminGiftOperationStatusResponse를 사용한다.
{
"applicationNo": "A-1002609300001",
"status": "CANCELED",
"statusName": "신청 취소",
"occurredAt": "2026-09-30T12:00:00Z"
}
occurredAt은 canceledAt과 같은 UTC ISO Z 문자열이다. 누락되거나 취소할 수 없는 상태의 신청번호는 기존 SodaException(messageKey = "common.error.invalid_request")로 거부한다. 정상 ApiResponse envelope를 사용하며 신규 오류 스키마나 HTTP status 정책은 추가하지 않는다.
관리자 취소 시각은 gift-schema.sql의 canceled_at TIMESTAMP 정밀도에 맞춰 초 단위로 절삭한 단일 값을 저장과 응답에 사용한다. OS/JDK 시계가 나노초를 반환해도 DB 재조회 시각과 응답 시각은 일치해야 한다. 전역 날짜 변환 및 다른 상태 변경의 시각 정책은 변경하지 않는다.
이 관리자 취소는 기존 사용자 직접 취소와 운송장 미등록 자동취소를 변경하지 않는다. 취소 전용 푸시는 추가하지 않으며, 자동취소 푸시 문구나 자동취소 사유를 관리자 취소에 재사용하지 않는다.
8.15 자동 스케줄링 작업
| 작업 | 대상 | 처리 | 푸시 |
|---|---|---|---|
| 운송장 등록 기한 24시간 전 안내 | RECEIVED, 마감 24시간 전 |
상태 변경 없음 | 팬에게 안내 |
| 운송장 미등록 자동취소 | RECEIVED, 신청 후 3일 초과 |
CANCELED, 전액 환불 |
팬에게 자동취소 안내 |
| 배송지 입력 기한 24시간 전 안내 | TRACKING_REGISTERED 이상, 배송지 미입력, 마감 24시간 전 |
상태 변경 없음 | 크리에이터에게 안내 |
| 배송지 미입력 전달불가 | TRACKING_REGISTERED, ARRIVED_AT_MAILBOX, INSPECTION_COMPLETED 중 하나이며 배송지 미입력, 운송장 등록 후 7일 초과 |
UNDELIVERABLE, 사유 저장 |
팬에게 전달불가 안내. 크리에이터에게 추가 푸시 없음 |
스케줄러는 중복 실행되어도 같은 선물에 중복 환불 또는 중복 푸시가 발생하지 않아야 한다.
배송지 입력 기한과 현재 시각이 같으면 아직 기한 초과가 아니며, RECEIVED, DELIVERED, UNDELIVERABLE, CANCELED 상태는 배송지 미입력 전달불가 대상에서 제외한다.
8.16 전달 완료 배송 개인정보 정리
- 매 실행 시각
now기준Gift.status=DELIVERED,GiftDelivery.deliveredAt != null,deliveredAt <= now.minusDays(7)인 모든 건을 처리한다. 7일 경계와 그 이전의 과거 누적 건을 포함한다. GiftDelivery의 다음 12개 값만 null로 지운다:senderName,senderPhoneNumber,senderZipCode,senderAddress,senderAddressDetail,recipientName,recipientPhoneNumber,recipientZipCode,recipientAddress,recipientAddressDetail,courierCompanyName,trackingNumber.- 기존 입력 request의 필수/non-null 계약과 검증은 유지한다. 공개 DTO 스키마와 바깥 nullable 객체는 유지하고, 기존 non-null 응답 문자열에서 데이터가 없으면
""로 표현한다. 주소는 완전 삭제 상태에서 정확히 빈 문자열이며"() ","(null) null", 문자열"null"을 만들지 않는다. SENT의senderInfo객체는 유지하고 각 문자열은 빈 문자열로 응답한다.RECEIVED의senderInfo는 계속 null이다. 모든 받는 주소 값이 null이면recipientAddress는 null이다. 사용자 tracking과 관리자 courier/tracking nullable 값은 null이다.- 기존
recipientAddressRegistered는 실제 필드 완비 여부를 따르므로 삭제 뒤 false다. 별도 등록 이력을 만들지 않는다.DELIVERED상태, 가시성, 기존 derived flags/deadlines, 관리자 액션(빈 목록)을 유지해 재입력/운영 액션을 요구하지 않는다. - 발신/수신 배송 개인정보만 지운다. Member nickname/profile, Gift 및 GiftDelivery 식별자·status, 결제, 동의 여부/시각, 리뷰,
createdAt, 업무 lifecycle/deadline/reminder 시각은 보존한다.updatedAt은 기존BaseEntity @PreUpdate동작을 따른다. - 비대상 상태, null
deliveredAt, 아직 7일이 안 된 건은 유지한다. 부분 삭제된 적격 건은 나머지를 지우며 완전 null 건의 재실행 변경 수는 0이다. - 이 작업은 푸시/환불/상태 전이를 추가하지 않는다. 앱 서버 내 DB 배송정보 정리이며 백업·로그·캐시의 삭제를 뜻하지 않는다.
- 기존 자동취소만 이미
:00에 예약되어 있다. 다음 3개 업무 메서드를 별도 Asia/Seoul 시간별 wrapper로 등록한다. 배송지 만료0 10 * * * *, 개인정보 정리0 20 * * * *, 기한 안내0 40 * * * *. 업무 로직은 기존 메서드 또는 새 정리 메서드로 위임한다. - 세 신규 wrapper는
tryLock(0, -1, SECONDS),@Transactional과 서로 다른 lock key를 사용한다. 획득한 lock은finally에서 해제를 예약하고 DB commit/rollback 이후afterCompletion에서 소유권 확인 후 해제한다. transaction 없는 직접 호출에서는 업무 종료 후 해제한다. 기존:00취소 wrapper 및 key는 변경하지 않는다. 다른 작업 간 비중첩은 보장하지 않는다. LocalDateTime에 KST를 자동 변환하지 않는다. 기존 작업의 clock convention을 따르고 고정now를 전달해 판단한다.
9. 푸시 알림 정책
9.1 팬 대상 푸시
| 트리거 | 제목 | 본문 | 딥링크 |
|---|---|---|---|
| 선물 신청 후 자동 취소까지 24시간 남은 경우 | 운송장 번호를 입력해 주세요 | 24시간 이내에 운송장 번호를 등록해 주세요.\n기한 내 등록하지 않으면 신청이 자동 취소됩니다. | GIFT_DETAIL, applicationNo |
| 선물 자동 취소 | 선물 신청이 자동 취소됐어요 | 신청 후 3일 이내에 운송장 번호가 등록되지 않아 신청이 취소됐어요.\n사용한 캔은 모두 환불되었습니다. | GIFT_DETAIL, applicationNo |
| 사서함 도착 | 선물이 사서함에 도착했어요 | 보내주신 선물이 소다라이브 사서함에 도착했어요.\n안전하게 전달할 수 있도록 선물을 확인하고 있어요. | GIFT_DETAIL, applicationNo |
| 전달 완료 | 선물이 전달됐어요 | 보내주신 선물이 크리에이터에게 안전하게 전달됐어요. | GIFT_DETAIL, applicationNo |
| 전달 불가 | 선물을 전달할 수 없어요 | 도착한 선물이 선물 접수 규격에 맞지 않아 크리에이터에게 전달할 수 없어요.\n자세한 사유는 신청 상세에서 확인해 주세요. | GIFT_DETAIL, applicationNo |
9.2 크리에이터 대상 푸시
| 트리거 | 제목 | 본문 | 딥링크 |
|---|---|---|---|
| 팬 운송장 등록 완료 | 팬이 보낸 선물이 있어요! | 7일 이내에 배송지를 입력해 주세요.\n기간이 지나면 선물을 받을 수 없어요 | GIFT_DETAIL, applicationNo |
| 배송지 입력 기한 24시간 전 | 배송지 입력 기한이 1일 남았어요 | 24시간 안에 배송지를 입력해 주세요.\n입력하지 않으면 선물은 반송되지 않고 폐기돼요 | GIFT_DETAIL, applicationNo |
| 사서함 도착 · 검수 중 | 선물이 사서함에 도착했어요 | 팬이 보낸 선물을 검수하고 있어요.\n검수가 끝나면 다시 알려드릴게요 | GIFT_DETAIL, applicationNo |
| 검수 완료 (통과) | 선물 검수가 완료됐어요 | 확인된 선물을 빠른 시일 내에 입력하신 배송지로 보내드릴게요. | GIFT_DETAIL, applicationNo |
| 검수 완료 (전달 불가) | 전달할 수 없는 선물이 있어요 | 검수 결과 선물 정책에 맞지 않아 해당 선물은 배송되지 않아요.\n자세한 사유를 확인해 주세요. | GIFT_DETAIL, applicationNo |
| 배송 완료 | 선물이 도착했어요! | 팬이 보낸 선물이 배송지에 도착했어요.\n지금 확인해 보세요. | GIFT_DETAIL, applicationNo |
크리에이터가 직접 수령확인 API를 호출해 DELIVERED가 된 경우에는 크리에이터 대상 배송 완료 푸시를 보내지 않는다.
9.3 Phase 16 호출 누락 보완
- 수취인 직접 수령확인 성공 시 기존
sendDeliveredToSender(applicationNo, senderMemberId)를 호출한다. 수취인에게는 추가 발송하지 않으며 실패·반복 요청으로 중복 발행하지 않는다. - 배송지 입력 기한 초과로 실제
UNDELIVERABLE전환한 선물만 기존sendUndeliverableToSender(applicationNo, senderMemberId)를 호출한다. 비대상·기한 동일·주소 입력 완료·재실행은 추가 발행하지 않는다. - 기존 GiftPushService의 제목·본문·수신자 지정·
GIFT_DETAIL/신청번호 딥링크와 transaction 이벤트 경계를 재사용한다. 새 문구·수신자·알림 종류·스케줄·테이블은 추가하지 않는다. - 가격·카테고리 검증은 실제 신청→관리자 가격 변경→재신청→카테고리 비활성화 흐름으로 수행한다. 이전 신청의
salePriceCan,categoryNameSnapshot, 결제 연결/금액은 유지하고, 새 신청에는 최신 가격을 적용하며 비활성화 이후 신규 신청은 거부한다. - 이번 검증은 서버 이벤트 발행·DB/HTTP 계약까지이며 운영 FCM 및 실제 기기 수신 성공을 의미하지 않는다.
10. 데이터 모델 요구사항
10.1 Gift
| 필드 | 설명 |
|---|---|
id |
내부 기본키 |
applicationNo |
신청번호. 기본키와 별도이며 사용자/운영 식별자로 사용 |
senderMemberId |
보내는 팬 회원번호 |
recipientMemberId |
받는 크리에이터 회원번호 |
status |
선물 상태 |
sizeCode |
신청 시 선택한 사이즈 코드 |
categoryId |
신청 시 선택한 카테고리 ID |
categoryNameSnapshot |
신청 시 카테고리명 스냅샷 |
salePriceCan |
신청 시 실제 결제 금액 스냅샷 |
canUsageId |
캔 사용내역 연결 ID |
senderTermsAgreed, senderPrivacyAgreed |
팬 동의 여부 |
senderTermsAgreedAt, senderPrivacyAgreedAt |
팬 동의 시각 |
recipientTermsAgreed, recipientPrivacyAgreed |
크리에이터 동의 여부 |
recipientTermsAgreedAt, recipientPrivacyAgreedAt |
크리에이터 동의 시각 |
damageWaiverAgreed |
팬 파손면책 동의 여부 |
createdAt, updatedAt |
생성/수정 시각 |
10.2 GiftDelivery
| 필드 | 설명 |
|---|---|
giftId |
선물 ID |
senderName, senderPhoneNumber |
발신자 이름/휴대폰 번호 |
senderZipCode, senderAddress, senderAddressDetail |
발신자 주소 |
recipientName, recipientPhoneNumber |
수신자 이름/휴대폰 번호 |
recipientZipCode, recipientAddress, recipientAddressDetail |
수신자 주소 |
courierCompanyName, trackingNumber |
택배사 한글 표시명/운송장 번호 |
trackingDeadlineAt |
운송장 등록 기한 |
recipientAddressDeadlineAt |
크리에이터 배송지 입력 기한 |
trackingRegisteredAt |
발송 확인 시각 |
arrivedAtMailboxAt |
사서함 도착 시각 |
inspectionCompletedAt |
검수완료 시각 |
deliveredAt |
전달완료 시각 |
canceledAt |
신청 취소 시각 |
undeliverableAt |
전달불가 시각 |
undeliverableReason |
전달불가 사유 |
10.3 GiftCategory
| 필드 | 설명 |
|---|---|
id |
내부 기본키 |
classificationNumber |
숫자 3자리 unique 분류번호 |
categoryCode |
50자 이하 unique 내부 범용코드 |
name |
50자 이하 분류명 |
receiptCode |
영문 대문자 1~9자 unique 접수코드 |
representativeItem |
100자 이하 대표품목 |
requiresDamageWaiver |
파손면책 동의 필요 여부. DDL은 tinyint(1) |
isActive |
활성 여부. DDL은 tinyint(1) |
createdAt, updatedAt |
생성/수정 시각 |
10.4 GiftSizePrice
| 필드 | 설명 |
|---|---|
id |
내부 기본키 |
sizeCode |
SMALL, MEDIUM, LARGE |
basePriceCan |
기본 금액 |
salePriceCan |
실제 표시/결제 금액 |
isActive |
활성 여부. DDL은 tinyint(1) |
createdAt, updatedAt |
생성/수정 시각 |
10.5 GiftReview
| 필드 | 설명 |
|---|---|
id |
내부 기본키 |
giftId |
선물 ID. 선물당 1개 unique |
senderMemberId |
리뷰 작성 팬 회원번호 |
rating |
1~5점 |
keywords |
선택한 리뷰 키워드 목록. 각 키워드는 최대 255자 |
comment |
최대 255자 |
createdAt, updatedAt |
생성/수정 시각 |
11. 보안과 데이터 취급
- 발신자/수신자 이름, 휴대폰 번호, 주소, 우편번호는 개인정보로 취급한다.
- 운영 로그, 오류 로그, 푸시 본문에는 주소 전체와 전화번호 전체를 남기지 않는다.
- 상세 조회는 본인 관련 선물 또는 관리자/운영 권한에서만 허용한다.
- 상세 조회에서 상대방의 이름, 휴대폰 번호, 주소, 우편번호, 상세주소는 노출하지 않고 상대방 닉네임만 노출한다.
- 팬 상세에서는 팬 본인이 신청 시 등록한 발신자 정보와 사서함 정보만 노출하고, 크리에이터 배송지는 노출하지 않는다.
- 크리에이터 상세에서는 크리에이터 본인이 입력한 받는 주소만 노출하고, 팬이 신청 시 등록한 발신자 정보는 노출하지 않는다.
- 내부 운영 화면에서는 필요한 권한을 가진 사용자에게만 개인정보를 노출한다.
- 약관 동의 여부와 동의시각은 선물 신청 건 감사 근거로 보존한다.
11.1 전달 완료 배송 개인정보 수명주기
- 정리 범위는 선물 도메인 DB의 지정된
GiftDelivery열 12개다. 배송지 입력 당시 사용한 요청 필드와 검증 규칙은 그대로 필수다. - 이 정리는 회원 계정,
Membernickname/profile, Gift 신청/상태/ID, 캔 결제 내역, 약관 동의 snapshot, 리뷰를 삭제하지 않는다. GiftDelivery의 업무 시각 및 기한은 운영 이력으로 보존한다.createdAt은 유지하고updatedAt은 공통 entity auditing 규칙대로 갱신될 수 있다.- 이 정책은 백업, 애플리케이션/인프라 로그, 외부 관측 시스템, 캐시의 삭제나 보존기간을 정의하지 않는다. 전체 개인정보 삭제 완료로 설명하지 않는다.
- 정리 작업 자체는 상태 전이·환불·알림을 일으키지 않는다. 기존 업무 정책이 정리 task를 통해 달라지지 않도록 한다.
GiftDelivery의 sender 다섯 Kotlin property를 nullable로 선언한다.sender_address_detail은 이미 nullable이다. 현재 NOT NULL인sender_name,sender_phone_number,sender_zip_code,sender_address만 schema에서 nullable로 바꾼다.- 네 열 DDL은 30줄 미만이므로 기존
gift-schema.sql끝에 5줄 ALTER 구문으로 기록한다. 기존 CREATE block의 NOT NULL 정의를 고치지 않는다. SQL 전체를 재실행하지 않는 운영 인스턴스에는 ALTER section만 별도로 승인·적용한다. Fresh install에는 뒤따르는 ALTER를 포함한 전체 schema 순서가 적용되어 최종 네 열이 nullable이다. - 운영 schema metadata 검토, 별도 DDL 실행 승인, 실행과 결과 확인은 rollout gate다. 코드 작성/H2 test의 선행조건이나 production DB 접속 요건이 아니다. 애플리케이션이 SQL을 자동 실행하지 않는다.
- nullable 값 호환 버전이 모든 서버에서 동작하기 전에 첫 정리를 실행하지 않는다. 이미 정리된 값을 읽지 못하는 non-null reader로의 rollback은 안전하지 않으며 코드 rollback으로 원 개인정보를 복구할 수 없다.
12. 테스트와 품질 요구사항
- 선물 신청은 캔 부족, 비활성 카테고리, 필수 약관 미동의, 파손면책 미동의, 최종 결제 금액 스냅샷을 테스트한다.
- 취소/자동취소는
RECEIVED에서만 가능하고 환불이 1회만 발생하는지 테스트한다. - 운송장 등록은 받은 선물 목록 노출과 크리에이터 배송지 입력 기한 생성을 테스트한다.
- 배송지 입력은 크리에이터 권한, 필수 배송지, 수령 약관 2개를 테스트한다.
- 상태별 운영 API는 허용 이전 상태와 금지 이전 상태를 모두 테스트한다.
- 리뷰는 작성 권한, 상태, 중복 작성, 글자수 제한을 테스트한다.
- 푸시는 팬/크리에이터 수신자 분리와 크리에이터 수령확인 시 크리에이터 푸시 억제를 테스트한다.
- 스케줄러는 중복 실행 시 중복 환불/중복 푸시가 없는지 테스트한다.
- 선물 사용자/관리자 API response DTO의 날짜/시간 필드는 UTC ISO 문자열(
Z포함)로 직렬화되는지 controller test로 고정한다. - 관리자 취소는
RECEIVED만 허용하고, 발신자에게 전액 리워드캔 환불,canceledAt저장, 환불 실패 시 트랜잭션 롤백 및 중복 환불 방지를 검증한다. 운송장 등록, 사용자 취소, 자동취소, 다른 관리자 취소와의 동시 경합에서도 잘못된 이중 처리/환불이 없어야 한다. - 관리자 취소 controller는 기존 관리자 인증/권한, 공통 invalid request, 정상
ApiResponseenvelope와AdminGiftOperationStatusResponse의 UTCoccurredAt을 검증한다. - 관리자 목록과 상세 모두
RECEIVED에서만CANCEL액션을 제공하며 기존COMPLETE_DELIVERY조건부 액션을 포함한 나머지 상태 매핑은 유지한다. - 카테고리
classificationNumber,receiptCode,representativeItem의 관리자 등록/수정/조회와 사용자 폼 옵션의분류명/대표품목조합을 테스트한다. - 신청번호는 카테고리+날짜별 sequence, 4자리 padding,
9999초과 자리수 증가, 병렬 unique를 테스트한다. - 개인정보 정리는 7일 경계 포함/1초 미경과 제외, 더 오래된 이력 포함, null 완료시각 및 비전달완료 상태 제외, 부분 삭제/완전 삭제 재실행 0건을 실제 JPA 저장·flush/clear/reload로 검증한다. 보호할 결제/동의/리뷰/회원/업무 시각도 재조회해 불변임을 확인한다.
GiftDelivery매핑은 sender 다섯 property 모두 null 저장을 허용해야 한다. DDL review는 네 nonnull sender columns만 변경하고 기존gift-schema.sqltrailing ALTER를 확인한다.- 사용자 및 관리자 query/controller는 삭제 후 기존 DTO 형태, 빈 non-null 문자열의 정확한 주소 형식, nullable 필드, nickname,
DELIVERED의 액션/CTA 미노출을 검증한다. - 실제
GiftRepositoryquery를 기존@DataJpaTest의 embedded H2에서 실행한다. 실제 datasource replacement를 확인해 MySQL-mode/운영 MySQL 검증과 구분한다. 고정now에서 cutoff 동일 시각은 포함하고 cutoff보다 1초 늦은 시각은 제외한다. - 오래된 과거 이력, null
deliveredAt, 다른 Gift status도 각 fixture에서 독립 확인한다. 미래에 cutoff를 지나는 데이터도 매번 조회되어야 하며 첫 정리에서만 처리되는 신규 데이터 제한을 두지 않는다. - 정리 전후 DB를 flush, clear, reload해 null 저장이 실제 반영됐는지 확인한다. 재실행 후 반환 변경 수가 0인지 확인한다.
- 매핑 test는 12개 필드를 모두 null로 저장할 수 있음을 확인하되 지정 외 상태 시각 필드는 그대로 남는지 검증한다.
- controller 테스트는 기존 JSON object shape를 비교하고 사용자 요청 DTO 필수 validation이 유지되는지 확인한다.
- 관리자 조회는 삭제 뒤 이름/전화/주소 non-null field를 빈 문자열로 반환하되 courier/tracking은 기존 nullable field로 null을 반환한다.
- 스케줄 테스트는 expression, zone, 분산락 소유 확인/unlock, 예외 경로와 적절한 delegate를 검증한다. 스케줄 실행시간으로 다른 진입점이 완료될 것이라는 보장은 테스트 요구로 만들지 않는다.
13. 성공 기준
- 팬이 선물 신청 시 약관/파손면책/캔 결제를 포함해
RECEIVED선물을 만들 수 있다. - 팬이 운송장 등록 전 직접 취소하거나 스케줄러가 자동취소하면 사용 캔이 전액 환불된다.
- 운송장 등록 후 크리에이터 받은 선물 목록에 노출되고 배송지 입력 CTA에 필요한 기한 정보가 응답된다.
- 크리에이터는 배송지와 수령 약관 2개를 등록할 수 있다.
- 운영자는 상태별 전용 API로 상태를 전이하고, 잘못된 상태 전이는 실패한다.
- 팬은
DELIVERED이후 리뷰를 1회 작성할 수 있다. - 관리자 카테고리 논리삭제와 사이즈별 가격 변경이 새 신청에만 반영된다. 실제 신청A80→가격120→신청B120→비활성화 후 기존 DB/HTTP/결제 보존과 신규 신청 거부를 검증했다. (
P16-T2) - 팬/크리에이터 푸시가 지정된 문구, 수신자, 딥링크로 발행된다. 누락된 두 호출과 발신자1회·수취인추가0회·거부/반복추가0, 기존 payload 계약을 서버 경계에서 검증했다. 실제 FCM·기기 수신은 별도다. (
P16-T1) - 선물 사용자/관리자 API의 날짜/시간 응답을 클라이언트가 UTC ISO 문자열로 해석할 수 있다.
- 카테고리 추가 필드와 새 신청번호 정책이 관리자 API, 사용자 폼 옵션, 선물 신청에 반영된다.
- 관리자 취소는
RECEIVED에서만 전액 환불과 함께 성공하고, 목록/상세의CANCEL액션은 상태에 맞게 제공된다. - 7일 이상 지난 전달 완료 건의 지정된 배송 개인정보만 DB에서 지워지고 기존 업무 데이터와 API 계약은 유지된다. 격리 DB 및 실제 HTTP로 검증했다. (
GIFT-018,P15-GATE) - 다섯 sender property nullable mapping 및 네 DB 열의 ALTER 문서가 schema 요구를 충족한다. 운영 적용은 별도다. (
GIFT-019,P15-T1,P15-T2) - 삭제 후 빈 문자열/null 표현으로 기존 API schema와 사용자 입력 검증을 유지한다. (
GIFT-020,P15-T2,P15-GATE) - 만료/정리/reminder 업무 메서드가 각각 Asia/Seoul
:10/:20/:40schedule wrapper로 등록된다. 실제 Spring 등록을 확인했다. (GIFT-021,P15-T3,P15-T4)
Phase 15 서버 구현 완료 판정에는 P15-T2~P15-T4 focused test와 격리 HTTP QA, MySQL DDL 및 배포 순서 체크리스트 검토가 필요하다. 실제 운영 DDL 적용과 nullable reader 선행 배포 확인은 별도 rollout 조건이며, 이 문서 수정은 구현 검증이나 운영 반영 완료의 증거가 아니다.
14. Open Questions
없음. 구현 중 관리자 권한명의 정확한 값이 필요하면 기존 코드 패턴 확인 후 이 PRD를 먼저 갱신한다.
15. 요구사항 추적표
| 요구사항 | 계획 Phase | 자동 검증 | 수동 검증 |
|---|---|---|---|
GIFT-001, GIFT-012, GIFT-013 |
1 | 관리자 설정 API 테스트 | 관리자 설정 조회/수정 확인 |
GIFT-002~005 |
2 | 신청/취소/운송장 API 테스트 | 팬 선물 신청 흐름 확인 |
GIFT-007~008 |
3 | 받은 목록/배송지/스케줄러 테스트 | 크리에이터 받은 선물 흐름 확인 |
GIFT-009 |
4 | 운영 상태변경 API 테스트 | 운영 상태 전이 확인 |
GIFT-010 |
5 | 리뷰 API 테스트 | 전달완료 후 리뷰 작성 확인 |
GIFT-011 |
6 | 푸시 이벤트 테스트 | 딥링크와 수신자 확인 |
GIFT-014 |
8 | 선물 사용자/관리자 controller 날짜 응답 테스트 | UTC ISO 문자열 확인 |
GIFT-016, GIFT-017 |
14 | 관리자 취소 service/controller 및 query service 테스트 | RECEIVED 액션과 취소/환불 결과 확인 |
GIFT-018 |
15 | repository predicate 및 erasure method test | 격리 fixture 이후 삭제 필드와 보존 데이터 재조회 |
GIFT-019 |
15 | GiftEntityMappingTest, SQL section review | no production DB execution in Gate |
GIFT-020 |
15 | query/controller contract regression | fixture erase 후 실제 socket HTTP SENT/RECEIVED/admin 조회 |
GIFT-021 |
15 | scheduler reflection/lock/delegation tests | schedule/zone/lock key 대조 |
GIFT-019 rollout은 운영 schema 확인, DDL review 및 별도 실행 승인, nullable schema와 호환 reader 준비를 거친다. Gate는 운영 DB에서 SQL 또는 정리 작업을 실행하지 않는다.
16. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항 |
|---|---|---|---|---|---|
| 2026-09-29 | DEC-001 |
확정 | 1차 PRD는 사용자 API, 관리자 설정/API, 운영 상태변경, 자동 스케줄링, 푸시까지 포함한다 | 사용자 선택 A | 전체 |
| 2026-09-29 | DEC-002 |
확정 | 크리에이터 배송지는 팬 운송장 등록 후 크리에이터가 7일 이내 입력한다 | 사용자 선택 A | GIFT-005, GIFT-007, GIFT-008 |
| 2026-09-29 | DEC-003 |
확정 | 팬 약관 동의는 별도 회원 약관 이력이 아니라 선물 신청 건에 스냅샷 저장한다 | 사용자 선택 A | GIFT-002, GIFT-003 |
| 2026-09-29 | DEC-004 |
확정 | 선물 신청 시 캔을 즉시 차감하고 취소/자동취소 시 전액 환불한다 | 사용자 선택 A | GIFT-002, GIFT-004, GIFT-006 |
| 2026-09-29 | DEC-005 |
확정 | 리뷰는 보낸 팬이 DELIVERED 이후 선물 건당 1회 작성한다 |
사용자 선택 B | GIFT-010 |
| 2026-09-29 | DEC-006 |
확정 | 크리에이터 수령확인 API는 DELIVERED로 변경하고 팬에게 전달 완료 푸시를 보낸다 |
사용자 정정 | GIFT-009, GIFT-011 |
| 2026-09-29 | DEC-007 |
확정 | 배송지 입력 기한 초과는 UNDELIVERABLE, 사유 배송지 미입력 기한 초과로 처리한다 |
사용자 선택 A | GIFT-008 |
| 2026-09-29 | DEC-008 |
확정 | 선물함 목록은 `type=ALL | SENT | RECEIVED` 단일 API로 제공한다 |
| 2026-09-29 | DEC-009 |
확정 | 팬 발신자 이름/휴대폰 번호/우편번호/주소를 모두 필수로 받는다 | 사용자 선택 A | GIFT-002 |
| 2026-09-29 | DEC-010 |
확정 | 가격은 basePriceCan과 실제 결제 금액인 salePriceCan으로 표현한다 |
사용자 정정 | GIFT-001, GIFT-002, GIFT-013 |
| 2026-09-29 | DEC-011 |
확정 | 크리에이터 배송지 입력 시에도 약관 2개 동의와 동의시각을 별도 저장한다 | 사용자 정정 | GIFT-007 |
| 2026-09-29 | DEC-012 |
확정 | 파손면책 동의 필요 카테고리는 팬 신청 시 damageWaiverAgreed=true를 요구한다 |
사용자 선택 A | GIFT-003 |
| 2026-09-30 | DEC-013 |
확정 | 선물 사용자/관리자 API의 response datetime 필드는 LocalDateTime 기본 JSON 대신 UTC ISO 문자열로 내려준다 |
클라이언트가 offset 없는 LocalDateTime 문자열을 안전하게 해석하지 못함 |
GIFT-014 |
| 2026-09-30 | DEC-014 |
확정 | 카테고리는 분류번호, 내부 범용 카테고리 코드, 분류명, 접수코드, 대표품목, 파손면책 조건을 관리하고 신청번호는 receiptCode-classificationNumberYYMMDDSequenceNo로 발급한다 |
관리자 운영 필드와 사용자 표시 요구가 분리됨 | GIFT-001, GIFT-002, GIFT-012 |
| 2026-10-22 | DEC-015 |
확정 | 관리자 취소는 RECEIVED에서만 허용하고 기존 refundGiftCan으로 발신자에게 전액 리워드캔을 환불한다. 기존 operation status 응답을 재사용하며 관리자 취소 전용 푸시는 이번 범위에서 추가하지 않는다 |
사용자 확정: 취소 허용 상태와 문서 우선 반영. 응답/환불은 기존 계약 재사용, 신규 푸시는 요청 범위 밖 | GIFT-016, GIFT-017, P14 |
| 2026-10-22 | DEC-016 |
확정 | 기존 관리자 화면 전체 구현 프롬프트는 보존하고 취소 기능 추가 프롬프트를 admin-gift-cancel-addition-prompt.md로 분리한다 |
사용자 요청: 기존 페이지에 해당 기능만 추가하는 별도 프롬프트 | GIFT-016, GIFT-017, P14-T1 |
| 2026-10-23 | DEC-017 |
확정 | DELIVERED이고 deliveredAt <= now.minusDays(7)인 과거 누적 및 미래 대상의 GiftDelivery 배송 개인정보 12개를 null 처리하고 업무·결제·동의·리뷰·회원 데이터를 보존한다 |
승인된 Phase15 계약 | GIFT-018, 8.16, P15-T2~P15-T3 |
| 2026-10-23 | DEC-018 |
확정 | 기존 public request/response 스키마는 유지하고, 개인정보 없는 non-null 응답 문자열은 빈 문자열로 매핑한다. KST :20 스케줄은 기존 분산락/트랜잭션 패턴을 따르며 다른 작업의 기존 동작을 바꾸지 않는다 | 승인된 API 보존 및 스케줄러 등록 계약 | GIFT-018, 8.16, P15-T2~P15-T4 |
| 2026-10-23 | DEC-019 |
확정 | 네 sender DB column 변경은 30줄 미만이므로 신규 SQL 파일 대신 기존 gift-schema.sql 말미에 작성한다. 기존 CREATE block은 두고 추가 ALTER로 fresh install 최종 schema도 nullable로 만든다. 실제 운영 SQL 실행은 별도 승인 rollout gate이며 아직 실행하지 않았다 |
사용자의 latest SQL placement decision; 기존 파일에 추가된 5줄 ALTER | GIFT-019, P15-T1, P15-T2, P15-GATE |
| 2026-10-23 | DEC-020 |
확정 | sender 다섯 Kotlin property 중 senderAddressDetail 포함 다섯 모두 nullable, DB상 현재 NOT NULL인 sender 열 네 개만 변경한다 |
entity 필드와 DDL nullable 경계의 구별 | GIFT-019, P15-T2 |
| 2026-10-23 | DEC-021 |
확정 | 신규 시간별 scheduler wrapper는 기존 만료/안내 업무 메서드에 위임하며, 기존 예약은 자동취소 :00뿐이다. 신규 등록은 :10, :20, :40이다 |
현재 scheduler 코드와 승인 일정 | GIFT-021, P15-T3~P15-T4 |
| 2026-10-26 | DEC-022 |
확정 | 기존 PRD의 수령확인·배송지 만료 발신자 알림 호출 누락 두 곳을 보완하고 관리자 가격 변경/카테고리 비활성화 후 기존 신청 스냅샷 보존 통합 테스트를 추가한다 | 사용자 후속 작업 승인 | GIFT-008, GIFT-011, GIFT-013, P16-T1~P16-GATE |
문서 상태 안내
Phase 14의 당시 전체 테스트 제외 및 검증 이력은 plan-task.md의 P14-GATE 기록을 보존한다. Phase 15는 후속 실행 승인에 따라 서버 구현과 최종 전체 테스트 2,396개·lint·build·실소켓 QA·리뷰를 완료했다. 운영 MySQL DDL 실행 및 배포는 별도 승인/조건이며 이번 완료 범위에 포함하지 않는다.
Phase 16은 2026-10-26 후속 승인 범위의 서버 구현·검증을 완료했다. 최종 전체 테스트 2,400개·lint·build 통과 및 실제 localhost HTTP/DB 시나리오와 독립 리뷰 승인을 확인했다. Phase15의 과거 기록과 달리 이번 두 성공 기준은 새 검증 근거로 완료 표시했으며, 나머지 과거 성공 기준은 일괄 체크하지 않았다.