923 lines
46 KiB
Markdown
923 lines
46 KiB
Markdown
# PRD: 크리에이터 선물하기
|
|
|
|
## 문서 정보
|
|
|
|
| 항목 | 내용 |
|
|
|---|---|
|
|
| 문서 상태 | 구현 중 |
|
|
| 작성일 | 2026-09-29 |
|
|
| 최종 수정일 | 2026-09-30 |
|
|
| 대상 제품 | 크리에이터 선물하기 |
|
|
| 작성자·결정권자 | 사용자 |
|
|
| 관련 API Contract | 별도 문서 없음. 이 문서의 `8. API 계약`을 기준으로 사용 |
|
|
| 관련 구현 계획 | `docs/20260929_크리에이터_선물하기/plan-task.md` |
|
|
| 관련 review | 없음 |
|
|
|
|
## 1. Overview
|
|
|
|
팬이 내부 재화인 캔을 사용해 크리에이터에게 실물 선물을 보낼 수 있는 API를 제공한다.
|
|
팬은 선물 신청, 운송장 등록, 취소, 상세 조회와 리뷰 작성을 할 수 있고, 크리에이터는 운송장 등록 이후 받은 선물을 확인하고 배송지를 입력한 뒤 수령 확인을 할 수 있다.
|
|
관리자는 선물 카테고리와 사이즈별 가격을 관리하고, 운영자는 사서함 도착, 검수, 전달 완료/불가 상태를 처리한다.
|
|
|
|
## 2. Problem Statement
|
|
|
|
- 현재 팬이 크리에이터에게 실물 선물을 보내는 신청, 결제, 배송 상태, 검수 상태를 서버에서 일관되게 추적할 수 없다.
|
|
- 선물 가격은 사이즈별로 다르고 운영자가 변경해야 하므로 코드 상수만으로는 운영 요구를 충족할 수 없다.
|
|
- 운송장 미등록, 크리에이터 배송지 미입력, 검수 실패 같은 종료 상태와 환불/푸시 정책이 명확히 정의되어야 한다.
|
|
- 팬과 크리에이터가 서로 다른 시점에 약관과 개인정보 처리에 동의해야 하므로 신청 건 기준의 동의 스냅샷이 필요하다.
|
|
- 클라이언트가 날짜/시간 응답을 안정적으로 해석할 수 있도록 선물 API의 응답 시각은 timezone 없는 `LocalDateTime` JSON이 아니라 UTC ISO 문자열이어야 한다.
|
|
|
|
문제를 해결했다는 판단은 선물 신청부터 전달 완료 또는 종료 상태까지 모든 상태 전이가 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`는 내부 기본키와 별도로 발급한다.
|
|
- 형식은 `{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일 자동취소 시 사용 캔을 전액 환불한다.
|
|
- 운송장 등록 후에는 팬이 취소할 수 없다.
|
|
|
|
### 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`을 내려준다.
|
|
- 선물 사용자/관리자 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` |
|
|
|
|
## 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`, `classificationNumber`, `receiptCode`, `representativeItem`을 별도 필드로 응답하지 않고 활성 카테고리만 내려준다.
|
|
|
|
### 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": "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:
|
|
|
|
```json
|
|
{
|
|
"items": [
|
|
{
|
|
"applicationNo": "A-1002609300001",
|
|
"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": "A-1002609300001",
|
|
"direction": "SENT",
|
|
"status": "TRACKING_REGISTERED",
|
|
"statusName": "발송 확인",
|
|
"giftInfo": {
|
|
"recipientCreatorNickname": "크리에이터닉네임",
|
|
"senderNickname": null,
|
|
"sizeName": "소형",
|
|
"categoryName": "인형",
|
|
"applicationNo": "A-1002609300001",
|
|
"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": "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`는 코드상에서 정한 사서함 이름, 주소, 연락처를 내려준다. 구현 후 값이 제공되면 이 문서를 갱신한다.
|
|
`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": "A-1002609300001",
|
|
"status": "TRACKING_REGISTERED",
|
|
"recipientAddressDeadlineAt": "2026-10-09T03:00:00Z"
|
|
}
|
|
```
|
|
|
|
요구사항:
|
|
|
|
- 보낸 팬만 호출할 수 있다.
|
|
- `RECEIVED` 상태에서만 성공한다.
|
|
- 성공 시 크리에이터 받은 선물 목록에 노출된다.
|
|
- 성공 시 크리에이터에게 `팬 운송장 등록 완료` 푸시를 보낸다.
|
|
|
|
### 8.7 선물 보내기 취소
|
|
|
|
`POST /api/v2/gifts/{applicationNo}/cancel`
|
|
|
|
Request: 없음
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"applicationNo": "A-1002609300001",
|
|
"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": "A-1002609300001",
|
|
"recipientAddressRegisteredAt": "2026-10-03T03:00:00Z"
|
|
}
|
|
```
|
|
|
|
요구사항:
|
|
|
|
- 받는 크리에이터만 호출할 수 있다.
|
|
- `TRACKING_REGISTERED` 이상, 종료 상태가 아닌 선물에만 입력할 수 있다.
|
|
- 이름, 휴대폰 번호, 우편번호, 주소는 필수다.
|
|
- `recipientTermsAgreed`, `recipientPrivacyAgreed`가 모두 `true`여야 한다.
|
|
|
|
### 8.9 크리에이터 수령확인
|
|
|
|
`POST /api/v2/gifts/{applicationNo}/delivery-complete`
|
|
|
|
Request: 없음
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"applicationNo": "A-1002609300001",
|
|
"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": "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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```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` | 내부 기본키 |
|
|
| `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. 보안과 데이터 취급
|
|
|
|
- 발신자/수신자 이름, 휴대폰 번호, 주소, 우편번호는 개인정보로 취급한다.
|
|
- 운영 로그, 오류 로그, 푸시 본문에는 주소 전체와 전화번호 전체를 남기지 않는다.
|
|
- 상세 조회는 본인 관련 선물 또는 관리자/운영 권한에서만 허용한다.
|
|
- 상세 조회에서 상대방의 이름, 휴대폰 번호, 주소, 우편번호, 상세주소는 노출하지 않고 상대방 닉네임만 노출한다.
|
|
- 팬 상세에서는 팬 본인이 신청 시 등록한 발신자 정보와 사서함 정보만 노출하고, 크리에이터 배송지는 노출하지 않는다.
|
|
- 크리에이터 상세에서는 크리에이터 본인이 입력한 받는 주소만 노출하고, 팬이 신청 시 등록한 발신자 정보는 노출하지 않는다.
|
|
- 내부 운영 화면에서는 필요한 권한을 가진 사용자에게만 개인정보를 노출한다.
|
|
- 약관 동의 여부와 동의시각은 선물 신청 건 감사 근거로 보존한다.
|
|
|
|
## 12. 테스트와 품질 요구사항
|
|
|
|
- 선물 신청은 캔 부족, 비활성 카테고리, 필수 약관 미동의, 파손면책 미동의, 최종 결제 금액 스냅샷을 테스트한다.
|
|
- 취소/자동취소는 `RECEIVED`에서만 가능하고 환불이 1회만 발생하는지 테스트한다.
|
|
- 운송장 등록은 받은 선물 목록 노출과 크리에이터 배송지 입력 기한 생성을 테스트한다.
|
|
- 배송지 입력은 크리에이터 권한, 필수 배송지, 수령 약관 2개를 테스트한다.
|
|
- 상태별 운영 API는 허용 이전 상태와 금지 이전 상태를 모두 테스트한다.
|
|
- 리뷰는 작성 권한, 상태, 중복 작성, 글자수 제한을 테스트한다.
|
|
- 푸시는 팬/크리에이터 수신자 분리와 크리에이터 수령확인 시 크리에이터 푸시 억제를 테스트한다.
|
|
- 스케줄러는 중복 실행 시 중복 환불/중복 푸시가 없는지 테스트한다.
|
|
- 선물 사용자/관리자 API response DTO의 날짜/시간 필드는 UTC ISO 문자열(`Z` 포함)로 직렬화되는지 controller test로 고정한다.
|
|
- 카테고리 `classificationNumber`, `receiptCode`, `representativeItem`의 관리자 등록/수정/조회와 사용자 폼 옵션의 `분류명/대표품목` 조합을 테스트한다.
|
|
- 신청번호는 카테고리+날짜별 sequence, 4자리 padding, `9999` 초과 자리수 증가, 병렬 unique를 테스트한다.
|
|
|
|
## 13. 성공 기준
|
|
|
|
- [ ] 팬이 선물 신청 시 약관/파손면책/캔 결제를 포함해 `RECEIVED` 선물을 만들 수 있다.
|
|
- [ ] 팬이 운송장 등록 전 직접 취소하거나 스케줄러가 자동취소하면 사용 캔이 전액 환불된다.
|
|
- [ ] 운송장 등록 후 크리에이터 받은 선물 목록에 노출되고 배송지 입력 CTA에 필요한 기한 정보가 응답된다.
|
|
- [ ] 크리에이터는 배송지와 수령 약관 2개를 등록할 수 있다.
|
|
- [ ] 운영자는 상태별 전용 API로 상태를 전이하고, 잘못된 상태 전이는 실패한다.
|
|
- [ ] 팬은 `DELIVERED` 이후 리뷰를 1회 작성할 수 있다.
|
|
- [ ] 관리자 카테고리 논리삭제와 사이즈별 가격 변경이 새 신청에만 반영된다.
|
|
- [ ] 팬/크리에이터 푸시가 지정된 문구, 수신자, 딥링크로 발송된다.
|
|
- [ ] 선물 사용자/관리자 API의 날짜/시간 응답을 클라이언트가 UTC ISO 문자열로 해석할 수 있다.
|
|
- [ ] 카테고리 추가 필드와 새 신청번호 정책이 관리자 API, 사용자 폼 옵션, 선물 신청에 반영된다.
|
|
|
|
## 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 문자열 확인 |
|
|
|
|
## 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` |
|
|
| 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` |
|