docs(gift): 크리에이터 선물하기 요구사항을 기록한다

This commit is contained in:
2026-09-30 13:34:51 +09:00
parent 7c578972f7
commit c02f5f7fa8
3 changed files with 2188 additions and 0 deletions
@@ -0,0 +1,111 @@
CREATE TABLE gift_category (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 기본키',
category_code VARCHAR(50) NOT NULL COMMENT '내부 분류번호',
name VARCHAR(50) NOT NULL COMMENT '카테고리 표시명',
requires_damage_waiver TINYINT(1) NOT NULL COMMENT '파손면책 동의 필요 여부',
is_active TINYINT(1) NOT NULL COMMENT '활성 여부',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
PRIMARY KEY (id),
UNIQUE KEY uk_gift_category_code (category_code)
) COMMENT='선물 카테고리';
CREATE TABLE gift (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 기본키',
application_no VARCHAR(20) NOT NULL COMMENT '신청번호',
sender_member_id BIGINT NOT NULL COMMENT '보내는 팬 회원번호',
recipient_member_id BIGINT NOT NULL COMMENT '받는 크리에이터 회원번호',
status VARCHAR(30) NOT NULL COMMENT '선물 상태',
size_code VARCHAR(20) NOT NULL COMMENT '선물 사이즈 코드',
category_id BIGINT NOT NULL COMMENT '신청 시 카테고리 ID',
category_name_snapshot VARCHAR(50) NOT NULL COMMENT '신청 시 카테고리명 스냅샷',
sale_price_can INT NOT NULL COMMENT '신청 시 결제 금액 스냅샷',
can_usage_id BIGINT NOT NULL COMMENT '캔 사용내역 ID',
sender_terms_agreed TINYINT(1) NOT NULL COMMENT '팬 이용약관 동의 여부',
sender_privacy_agreed TINYINT(1) NOT NULL COMMENT '팬 개인정보 동의 여부',
sender_terms_agreed_at TIMESTAMP NOT NULL COMMENT '팬 이용약관 동의 시각',
sender_privacy_agreed_at TIMESTAMP NOT NULL COMMENT '팬 개인정보 동의 시각',
recipient_terms_agreed TINYINT(1) NOT NULL COMMENT '크리에이터 이용약관 동의 여부',
recipient_privacy_agreed TINYINT(1) NOT NULL COMMENT '크리에이터 개인정보 동의 여부',
recipient_terms_agreed_at TIMESTAMP NULL COMMENT '크리에이터 이용약관 동의 시각',
recipient_privacy_agreed_at TIMESTAMP NULL COMMENT '크리에이터 개인정보 동의 시각',
damage_waiver_agreed TINYINT(1) NOT NULL COMMENT '파손면책 동의 여부',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
PRIMARY KEY (id),
UNIQUE KEY uk_gift_application_no (application_no),
CONSTRAINT fk_gift_sender_member FOREIGN KEY (sender_member_id) REFERENCES member (id),
CONSTRAINT fk_gift_recipient_member FOREIGN KEY (recipient_member_id) REFERENCES member (id),
CONSTRAINT fk_gift_category FOREIGN KEY (category_id) REFERENCES gift_category (id),
CONSTRAINT fk_gift_can_usage FOREIGN KEY (can_usage_id) REFERENCES use_can (id)
) COMMENT='선물 신청';
CREATE TABLE gift_delivery (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 기본키',
gift_id BIGINT NOT NULL COMMENT '선물 ID',
sender_name VARCHAR(50) NOT NULL COMMENT '발신자 이름',
sender_phone_number VARCHAR(30) NOT NULL COMMENT '발신자 휴대폰 번호',
sender_zip_code VARCHAR(20) NOT NULL COMMENT '발신자 우편번호',
sender_address VARCHAR(255) NOT NULL COMMENT '발신자 주소',
sender_address_detail VARCHAR(255) NULL COMMENT '발신자 상세주소',
recipient_name VARCHAR(50) NULL COMMENT '수신자 이름',
recipient_phone_number VARCHAR(30) NULL COMMENT '수신자 휴대폰 번호',
recipient_zip_code VARCHAR(20) NULL COMMENT '수신자 우편번호',
recipient_address VARCHAR(255) NULL COMMENT '수신자 주소',
recipient_address_detail VARCHAR(255) NULL COMMENT '수신자 상세주소',
courier_company_name VARCHAR(50) NULL COMMENT '택배사 한글 표시명',
tracking_number VARCHAR(100) NULL COMMENT '운송장 번호',
tracking_deadline_at TIMESTAMP NOT NULL COMMENT '운송장 등록 기한',
recipient_address_deadline_at TIMESTAMP NULL COMMENT '수신자 배송지 입력 기한',
tracking_deadline_reminder_sent_at TIMESTAMP NULL COMMENT '운송장 등록 기한 안내 발송 시각',
recipient_address_deadline_reminder_sent_at TIMESTAMP NULL COMMENT '수신자 배송지 입력 기한 안내 발송 시각',
tracking_registered_at TIMESTAMP NULL COMMENT '운송장 등록 시각',
arrived_at_mailbox_at TIMESTAMP NULL COMMENT '사서함 도착 시각',
inspection_completed_at TIMESTAMP NULL COMMENT '검수완료 시각',
delivered_at TIMESTAMP NULL COMMENT '전달완료 시각',
canceled_at TIMESTAMP NULL COMMENT '취소 시각',
undeliverable_at TIMESTAMP NULL COMMENT '전달불가 시각',
undeliverable_reason VARCHAR(255) NULL COMMENT '전달불가 사유',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
PRIMARY KEY (id),
UNIQUE KEY uk_gift_delivery_gift_id (gift_id),
CONSTRAINT fk_gift_delivery_gift FOREIGN KEY (gift_id) REFERENCES gift (id)
) COMMENT='선물 배송 정보';
CREATE TABLE gift_size_price (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 기본키',
size_code VARCHAR(20) NOT NULL COMMENT '선물 사이즈 코드',
base_price_can INT NOT NULL COMMENT '기본 금액',
sale_price_can INT NOT NULL COMMENT '결제 금액',
is_active TINYINT(1) NOT NULL COMMENT '활성 여부',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
PRIMARY KEY (id),
UNIQUE KEY uk_gift_size_price_size_code (size_code)
) COMMENT='선물 사이즈별 가격';
CREATE TABLE gift_review (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 기본키',
gift_id BIGINT NOT NULL COMMENT '선물 ID',
sender_member_id BIGINT NOT NULL COMMENT '리뷰 작성 팬 회원번호',
rating INT NOT NULL COMMENT '별점',
keywords TEXT NULL COMMENT '리뷰 키워드 목록',
comment VARCHAR(255) NULL COMMENT '추가의견',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
PRIMARY KEY (id),
UNIQUE KEY uk_gift_review_gift_id (gift_id),
CONSTRAINT fk_gift_review_gift FOREIGN KEY (gift_id) REFERENCES gift (id),
CONSTRAINT fk_gift_review_sender_member FOREIGN KEY (sender_member_id) REFERENCES member (id)
) COMMENT='선물 리뷰';
CREATE TABLE gift_application_no_sequence (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 기본키',
sequence_date VARCHAR(8) NOT NULL COMMENT '채번 기준일 yyyyMMdd',
last_sequence INT NOT NULL COMMENT '마지막 일련번호',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
PRIMARY KEY (id),
UNIQUE KEY uk_gift_application_no_sequence_date (sequence_date)
) COMMENT='선물 신청번호 날짜별 채번';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,899 @@
# PRD: 크리에이터 선물하기
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 구현 중 |
| 작성일 | 2026-09-29 |
| 최종 수정일 | 2026-09-29 |
| 대상 제품 | 크리에이터 선물하기 |
| 작성자·결정권자 | 사용자 |
| 관련 API Contract | 별도 문서 없음. 이 문서의 `8. API 계약`을 기준으로 사용 |
| 관련 구현 계획 | `docs/20260929_크리에이터_선물하기/plan-task.md` |
| 관련 review | 없음 |
## 1. Overview
팬이 내부 재화인 캔을 사용해 크리에이터에게 실물 선물을 보낼 수 있는 API를 제공한다.
팬은 선물 신청, 운송장 등록, 취소, 상세 조회와 리뷰 작성을 할 수 있고, 크리에이터는 운송장 등록 이후 받은 선물을 확인하고 배송지를 입력한 뒤 수령 확인을 할 수 있다.
관리자는 선물 카테고리와 사이즈별 가격을 관리하고, 운영자는 사서함 도착, 검수, 전달 완료/불가 상태를 처리한다.
## 2. Problem Statement
- 현재 팬이 크리에이터에게 실물 선물을 보내는 신청, 결제, 배송 상태, 검수 상태를 서버에서 일관되게 추적할 수 없다.
- 선물 가격은 사이즈별로 다르고 운영자가 변경해야 하므로 코드 상수만으로는 운영 요구를 충족할 수 없다.
- 운송장 미등록, 크리에이터 배송지 미입력, 검수 실패 같은 종료 상태와 환불/푸시 정책이 명확히 정의되어야 한다.
- 팬과 크리에이터가 서로 다른 시점에 약관과 개인정보 처리에 동의해야 하므로 신청 건 기준의 동의 스냅샷이 필요하다.
문제를 해결했다는 판단은 선물 신청부터 전달 완료 또는 종료 상태까지 모든 상태 전이가 API/스케줄링/운영 API로 추적되고, 캔 사용내역과 푸시가 상태별 정책대로 처리되는 것으로 한다.
## 3. Goals
- 팬이 선물 사이즈, 카테고리, 발신자 정보, 약관 동의를 입력해 선물 보내기를 신청할 수 있다.
- 선물 신청 시 캔을 즉시 차감하고 내부적으로 선물 신청과 캔 사용내역을 연결한다.
- 팬은 운송장 등록 전 선물 신청을 취소할 수 있고, 3일 내 운송장을 등록하지 않으면 자동취소 및 전액 환불된다.
- 크리에이터는 팬이 운송장을 등록한 선물부터 받은 선물 목록과 상세를 조회하고 7일 이내 배송지와 수령 약관 동의를 등록할 수 있다.
- 팬과 크리에이터는 같은 선물함 목록 API에서 보낸/받은 내역을 함께 또는 개별 조회할 수 있다.
- 운영자는 상태별 전용 관리자 API로 사서함 도착, 검수완료, 전달불가, 전달완료를 처리할 수 있다.
- 선물 카테고리와 사이즈별 가격은 관리자 API로 관리할 수 있다.
- 상태별 팬/크리에이터 푸시 문구와 딥링크를 명확히 정의한다.
## 4. Non-Goals
- 약관 본문 제공 API는 포함하지 않는다. 약관 내용은 별도 API 또는 콘텐츠로 제공한다.
- 실물 택배사 API 연동, 송장 유효성 실시간 조회, 배송 추적 자동화는 포함하지 않는다.
- 선물 이미지, 첨부파일, 메시지 카드는 포함하지 않는다.
- 선물 카테고리 hard delete는 허용하지 않는다.
- 선물 사이즈 정의 자체를 관리자에서 추가/삭제하는 기능은 포함하지 않는다.
- 캔 사용내역 화면에서 사용자가 선물 신청 상세로 이동하는 기능은 포함하지 않는다.
## 5. Target Users and Permissions
| 사용자 | 목표 | 주요 작업 | 권한 |
|---|---|---|---|
| 팬 | 크리에이터에게 선물을 보낸다 | 사이즈/카테고리 조회, 신청, 운송장 등록, 취소, 보낸 내역 조회, 리뷰 작성 | 인증 회원 |
| 크리에이터 | 받은 선물을 확인하고 배송지를 입력한다 | 받은 내역 조회, 배송지 입력, 수령 확인 | 인증 회원 중 크리에이터 |
| 관리자 | 선물 운영 설정을 관리한다 | 카테고리, 사이즈 가격 관리 | 관리자 |
| 운영자 | 선물 검수/전달 상태를 처리한다 | 사서함 도착, 검수완료, 전달불가, 전달완료 처리 | 관리자 또는 운영 권한 |
| 스케줄러 | 기한 초과 건을 자동 처리한다 | 운송장 미등록 자동취소, 배송지 미입력 전달불가 | 시스템 |
기본 권한 정책은 기존 인증 필요 API와 관리자 API 정책을 따른다.
팬과 크리에이터는 본인과 관련된 선물만 조회/변경할 수 있다.
## 6. 핵심 정책
### 6.1 선물 상태
| 상태 코드 | 표시명 | 의미 | 종료 상태 |
|---|---|---|---|
| `RECEIVED` | 접수 완료 | 팬이 신청하고 캔 결제를 완료한 최초 상태 | 아니오 |
| `TRACKING_REGISTERED` | 발송 확인 | 팬이 택배사와 운송장 번호를 등록한 상태 | 아니오 |
| `ARRIVED_AT_MAILBOX` | 사서함 도착 | 운영자가 소다라이브 사서함 도착을 확인한 상태 | 아니오 |
| `INSPECTION_COMPLETED` | 검수완료 | 운영자가 선물 검수를 통과 처리한 상태 | 아니오 |
| `DELIVERED` | 전달완료 | 크리에이터 수령확인 또는 운영자 전달완료 처리 상태 | 예 |
| `UNDELIVERABLE` | 전달불가 | 검수 실패 또는 배송지 미입력 기한 초과로 전달할 수 없는 상태 | 예 |
| `CANCELED` | 신청 취소 | 운송장 등록 전 사용자 취소 또는 자동취소 상태 | 예 |
상태 전이는 다음을 기본으로 한다.
```text
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`는 내부 기본키와 별도로 발급한다.
- 형식은 `G{yyyyMMdd}{dailySequence6}`로 한다.
- `yyyyMMdd`는 Asia/Seoul 기준 신청일이다.
- `dailySequence6`는 해당 날짜 안에서 1부터 증가하는 6자리 숫자다.
- 예시는 `G20260929000001`이다.
- `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 선물 카테고리
카테고리는 관리자 페이지에서 등록, 수정, 논리 삭제할 수 있다.
| 필드 | 타입 | 제약 | 설명 |
|---|---|---|---|
| `categoryCode` | string | 50자 이하, unique | 내부 분류번호. 사용자에게 표시하지 않음 |
| `name` | string | 50자 이하 | 사용자 표시명 |
| `requiresDamageWaiver` | boolean | DDL은 `tinyint(1)` | 파손면책 동의 필요 여부 |
| `isActive` | boolean | DDL은 `tinyint(1)` | 활성 여부 |
`requiresDamageWaiver == true`인 카테고리를 선택한 팬은 선물 신청 시 `damageWaiverAgreed=true`를 추가로 보내야 한다.
### 6.6 약관 동의
약관 본문은 이번 API 범위 밖이다. API는 동의 여부만 확인하고 선물 신청 건에 동의 스냅샷을 저장한다.
팬 신청 동의는 선물 보내기 등록 시점에 저장한다.
| API 필드 | 표시명 | 저장 시각 |
|---|---|---|
| `senderTermsAgreed` | 상품 전달 이용약관 동의 | `senderTermsAgreedAt` |
| `senderPrivacyAgreed` | 상품 전달을 위한 개인정보 수집 및 이용 동의 | `senderPrivacyAgreedAt` |
크리에이터 수령 동의는 배송지 입력 시점에 저장한다.
| API 필드 | 표시명 | 저장 시각 |
|---|---|---|
| `recipientTermsAgreed` | 상품 전달 이용약관 동의 | `recipientTermsAgreedAt` |
| `recipientPrivacyAgreed` | 상품 전달을 위한 개인정보 수집 및 이용 동의 | `recipientPrivacyAgreedAt` |
각 동의 필드가 `true`가 아니면 해당 API는 실패한다.
### 6.7 결제와 환불
- 결제는 내부 재화인 캔으로 한다.
- 캔 사용내역에 `선물하기` 용도를 추가한다.
- 선물 보내기 등록 API 성공 시 즉시 `salePriceCan`만큼 캔을 차감한다.
- 선물 신청과 캔 사용내역은 내부적으로 연결되어야 한다.
- 사용자 캔 사용내역 화면에서 선물 신청으로 이동할 수 없어도 된다.
- 운송장 등록 전 사용자 취소 또는 3일 자동취소 시 사용 캔을 전액 환불한다.
- 운송장 등록 후에는 팬이 취소할 수 없다.
### 6.8 목록 노출
- 선물함 목록 API는 `type=ALL|SENT|RECEIVED`를 지원한다.
- 기본값은 `ALL`이다.
- 응답 item에는 `direction=SENT|RECEIVED`를 포함한다.
- 보낸 선물은 신청 직후부터 팬에게 노출한다.
- 받은 선물은 팬이 운송장 번호를 등록해 상태가 `TRACKING_REGISTERED` 이상이 된 뒤 크리에이터에게 노출한다.
- 배송지 미입력 상태라면 받은 선물 item/detail에 `recipientAddressRequired=true`, `recipientAddressDeadlineAt`을 내려준다.
- 상세 조회는 팬과 크리에이터가 서로의 개인정보를 알 필요 없이 선물을 보내거나 받을 수 있게 설계한다.
- 상세 조회에서 상대방 정보는 닉네임만 노출한다. 로그인 회원 본인이 신청 또는 입력한 개인정보는 본인에게만 노출한다.
- 로그인 회원이 받는 크리에이터이면 보내는 팬의 닉네임만 알 수 있고, 팬이 신청 시 등록한 이름/휴대폰 번호/주소는 알 수 없다.
- 로그인 회원이 보내는 팬이면 받는 크리에이터의 닉네임만 알 수 있고, 크리에이터가 입력한 이름/휴대폰 번호/주소는 알 수 없다.
- 보내는 팬 상세에는 팬 본인이 신청 시 등록한 이름, 휴대폰 번호, 주소를 내려준다.
- 받는 크리에이터 상세에는 크리에이터 본인이 입력한 받는 주소의 이름, 휴대폰 번호, 주소를 내려준다.
- 주소 형식은 `(우편번호) 주소, 상세주소` 문자열로 내려준다.
- 로그인 회원이 알아야 하는 데이터가 아니거나 아직 입력되지 않은 데이터는 `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`을 내려준다.
### 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` |
## 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:
```json
{
"sizes": [
{
"sizeCode": "SMALL",
"name": "소형",
"maxTotalLengthCm": 100,
"maxWeightKg": 5,
"basePriceCan": 100,
"salePriceCan": 80
}
],
"categories": [
{
"categoryId": 1,
"name": "인형",
"requiresDamageWaiver": true
}
]
}
```
사용자 API는 `categoryCode`를 응답하지 않고 활성 카테고리만 내려준다.
### 8.3 선물 보내기 등록
`POST /api/v2/gifts`
Request:
```json
{
"recipientMemberId": 100,
"senderName": "홍길동",
"senderPhoneNumber": "01012345678",
"senderZipCode": "06234",
"senderAddress": "서울시 강남구 ...",
"senderAddressDetail": "101동 1001호",
"sizeCode": "SMALL",
"categoryId": 1,
"senderTermsAgreed": true,
"senderPrivacyAgreed": true,
"damageWaiverAgreed": true
}
```
Response:
```json
{
"applicationNo": "G20260929000001",
"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:
```json
{
"items": [
{
"applicationNo": "G20260929000001",
"direction": "SENT",
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
"counterpartMemberId": 100,
"counterpartName": "크리에이터닉네임",
"sizeName": "소형",
"categoryName": "인형",
"priceCan": 80,
"recipientAddressRequired": false,
"recipientAddressDeadlineAt": null,
"createdAt": "2026-09-29T03:00:00Z"
}
],
"page": 0,
"size": 20,
"hasNext": false
}
```
받은 선물은 `TRACKING_REGISTERED` 이상 상태부터 노출한다.
### 8.5 선물 상세 조회
`GET /api/v2/gifts/{applicationNo}`
Response:
```json
{
"applicationNo": "G20260929000001",
"direction": "SENT",
"status": "TRACKING_REGISTERED",
"statusName": "발송 확인",
"giftInfo": {
"recipientCreatorNickname": "크리에이터닉네임",
"senderNickname": null,
"sizeName": "소형",
"categoryName": "인형",
"applicationNo": "G20260929000001",
"paidCan": 80,
"tracking": "CJ대한통운_1234567890",
"shippingRequestedAt": null
},
"senderInfo": {
"name": "홍길동",
"phoneNumber": "01012345678",
"address": "(06234) 서울시 강남구 ..., 101동 1001호"
},
"recipientAddress": null,
"mailbox": {
"name": "소다라이브 사서함",
"address": "구현 후 제공",
"phoneNumber": "구현 후 제공"
},
"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": null
},
{
"status": "INSPECTION_COMPLETED",
"statusName": "검수완료",
"occurredAt": null
},
{
"status": "DELIVERED",
"statusName": "전달완료",
"occurredAt": null
}
],
"review": null
}
```
받는 크리에이터 관점 응답 예시는 다음과 같다.
```json
{
"applicationNo": "G20260929000001",
"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`는 코드상에서 정한 사서함 이름, 주소, 연락처를 내려준다. 구현 후 값이 제공되면 이 문서를 갱신한다.
`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:
```json
{
"courierCompanyName": "CJ대한통운",
"trackingNumber": "1234567890"
}
```
Response:
```json
{
"applicationNo": "G20260929000001",
"status": "TRACKING_REGISTERED",
"recipientAddressDeadlineAt": "2026-10-09T03:00:00Z"
}
```
요구사항:
- 보낸 팬만 호출할 수 있다.
- `RECEIVED` 상태에서만 성공한다.
- 성공 시 크리에이터 받은 선물 목록에 노출된다.
- 성공 시 크리에이터에게 `팬 운송장 등록 완료` 푸시를 보낸다.
### 8.7 선물 보내기 취소
`POST /api/v2/gifts/{applicationNo}/cancel`
Request: 없음
Response:
```json
{
"applicationNo": "G20260929000001",
"status": "CANCELED",
"refundedCan": 80
}
```
요구사항:
- 보낸 팬만 호출할 수 있다.
- `RECEIVED` 상태에서만 성공한다.
- 성공 시 사용 캔을 전액 환불한다.
### 8.8 크리에이터 배송지 입력
`POST /api/v2/gifts/{applicationNo}/recipient-address`
Request:
```json
{
"recipientName": "김소다",
"recipientPhoneNumber": "01098765432",
"recipientZipCode": "04524",
"recipientAddress": "서울시 중구 ...",
"recipientAddressDetail": "202호",
"recipientTermsAgreed": true,
"recipientPrivacyAgreed": true
}
```
Response:
```json
{
"applicationNo": "G20260929000001",
"recipientAddressRegisteredAt": "2026-10-03T03:00:00Z"
}
```
요구사항:
- 받는 크리에이터만 호출할 수 있다.
- `TRACKING_REGISTERED` 이상, 종료 상태가 아닌 선물에만 입력할 수 있다.
- 이름, 휴대폰 번호, 우편번호, 주소는 필수다.
- `recipientTermsAgreed`, `recipientPrivacyAgreed`가 모두 `true`여야 한다.
### 8.9 크리에이터 수령확인
`POST /api/v2/gifts/{applicationNo}/delivery-complete`
Request: 없음
Response:
```json
{
"applicationNo": "G20260929000001",
"status": "DELIVERED",
"deliveredAt": "2026-10-10T03:00:00Z"
}
```
요구사항:
- 받는 크리에이터만 호출할 수 있다.
- `INSPECTION_COMPLETED` 상태에서만 성공한다.
- 성공 시 팬에게 전달 완료 푸시를 보낸다.
- 성공 시 크리에이터에게는 배송 완료/전달완료 푸시를 보내지 않는다.
### 8.10 리뷰 작성
`POST /api/v2/gifts/{applicationNo}/review`
Request:
```json
{
"rating": 5,
"keywords": ["배송이 빨라요", "안내가 친절해요"],
"comment": "선물 전달 과정이 만족스러웠습니다."
}
```
Response:
```json
{
"reviewId": 1,
"applicationNo": "G20260929000001",
"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:
```json
{
"categoryCode": "DOLL",
"name": "인형",
"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:
```json
{
"basePriceCan": 100,
"salePriceCan": 80,
"isActive": true
}
```
`salePriceCan`은 0보다 커야 하고 `basePriceCan`보다 클 수 없다.
### 8.13 운영 상태변경 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` | 없음 |
전달불가 Request:
```json
{
"reason": "선물 접수 규격에 맞지 않습니다."
}
```
### 8.14 자동 스케줄링 작업
| 작업 | 대상 | 처리 | 푸시 |
|---|---|---|---|
| 운송장 등록 기한 24시간 전 안내 | `RECEIVED`, 마감 24시간 전 | 상태 변경 없음 | 팬에게 안내 |
| 운송장 미등록 자동취소 | `RECEIVED`, 신청 후 3일 초과 | `CANCELED`, 전액 환불 | 팬에게 자동취소 안내 |
| 배송지 입력 기한 24시간 전 안내 | `TRACKING_REGISTERED` 이상, 배송지 미입력, 마감 24시간 전 | 상태 변경 없음 | 크리에이터에게 안내 |
| 배송지 미입력 전달불가 | 배송지 미입력, 운송장 등록 후 7일 초과 | `UNDELIVERABLE`, 사유 저장 | 팬에게 전달불가 안내. 크리에이터에게 추가 푸시 없음 |
스케줄러는 중복 실행되어도 같은 선물에 중복 환불 또는 중복 푸시가 발생하지 않아야 한다.
## 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`가 된 경우에는 크리에이터 대상 `배송 완료` 푸시를 보내지 않는다.
## 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` | 내부 기본키 |
| `categoryCode` | 50자 이하 내부 분류번호 |
| `name` | 50자 이하 카테고리명 |
| `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. 보안과 데이터 취급
- 발신자/수신자 이름, 휴대폰 번호, 주소, 우편번호는 개인정보로 취급한다.
- 운영 로그, 오류 로그, 푸시 본문에는 주소 전체와 전화번호 전체를 남기지 않는다.
- 상세 조회는 본인 관련 선물 또는 관리자/운영 권한에서만 허용한다.
- 상세 조회에서 상대방의 이름, 휴대폰 번호, 주소, 우편번호, 상세주소는 노출하지 않고 상대방 닉네임만 노출한다.
- 팬 상세에서는 팬 본인이 신청 시 등록한 발신자 정보와 사서함 정보만 노출하고, 크리에이터 배송지는 노출하지 않는다.
- 크리에이터 상세에서는 크리에이터 본인이 입력한 받는 주소만 노출하고, 팬이 신청 시 등록한 발신자 정보는 노출하지 않는다.
- 내부 운영 화면에서는 필요한 권한을 가진 사용자에게만 개인정보를 노출한다.
- 약관 동의 여부와 동의시각은 선물 신청 건 감사 근거로 보존한다.
## 12. 테스트와 품질 요구사항
- 선물 신청은 캔 부족, 비활성 카테고리, 필수 약관 미동의, 파손면책 미동의, 최종 결제 금액 스냅샷을 테스트한다.
- 취소/자동취소는 `RECEIVED`에서만 가능하고 환불이 1회만 발생하는지 테스트한다.
- 운송장 등록은 받은 선물 목록 노출과 크리에이터 배송지 입력 기한 생성을 테스트한다.
- 배송지 입력은 크리에이터 권한, 필수 배송지, 수령 약관 2개를 테스트한다.
- 상태별 운영 API는 허용 이전 상태와 금지 이전 상태를 모두 테스트한다.
- 리뷰는 작성 권한, 상태, 중복 작성, 글자수 제한을 테스트한다.
- 푸시는 팬/크리에이터 수신자 분리와 크리에이터 수령확인 시 크리에이터 푸시 억제를 테스트한다.
- 스케줄러는 중복 실행 시 중복 환불/중복 푸시가 없는지 테스트한다.
## 13. 성공 기준
- [ ] 팬이 선물 신청 시 약관/파손면책/캔 결제를 포함해 `RECEIVED` 선물을 만들 수 있다.
- [ ] 팬이 운송장 등록 전 직접 취소하거나 스케줄러가 자동취소하면 사용 캔이 전액 환불된다.
- [ ] 운송장 등록 후 크리에이터 받은 선물 목록에 노출되고 배송지 입력 CTA에 필요한 기한 정보가 응답된다.
- [ ] 크리에이터는 배송지와 수령 약관 2개를 등록할 수 있다.
- [ ] 운영자는 상태별 전용 API로 상태를 전이하고, 잘못된 상태 전이는 실패한다.
- [ ] 팬은 `DELIVERED` 이후 리뷰를 1회 작성할 수 있다.
- [ ] 관리자 카테고리 논리삭제와 사이즈별 가격 변경이 새 신청에만 반영된다.
- [ ] 팬/크리에이터 푸시가 지정된 문구, 수신자, 딥링크로 발송된다.
## 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 | 푸시 이벤트 테스트 | 딥링크와 수신자 확인 |
## 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로 제공한다 | 사용자 선택 B | `GIFT-007` |
| 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` |