# 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` | | `GIFT-015` | 확정 | 관리자는 전체 선물함 목록과 상세를 조회하고 현재 상태에서 가능한 운영 액션을 확인할 수 있다 | 목록은 상태/신청번호/닉네임 검색과 페이징을 지원하고, 상세는 발송인/수취인 개인정보와 상품/배송/상태 정보를 내려준다 | `P10` | ## 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 관리자 페이지는 전체 선물함을 목록과 상세로 조회한다. 상태 변경은 `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: ```json { "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: ```json { "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`는 빈 문자열 `""`로 내려준다. `availableActions`는 현재 상태별로 다음 값만 내려준다. | 현재 상태 | availableActions | |---|---| | `RECEIVED` | `[]` | | `TRACKING_REGISTERED` | `["ARRIVE_MAILBOX", "MARK_UNDELIVERABLE"]` | | `ARRIVED_AT_MAILBOX` | `["COMPLETE_INSPECTION", "MARK_UNDELIVERABLE"]` | | `INSPECTION_COMPLETED` | `["COMPLETE_DELIVERY", "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` | 없음 | 전달불가 Request: ```json { "reason": "선물 접수 규격에 맞지 않습니다." } ``` ### 8.15 자동 스케줄링 작업 | 작업 | 대상 | 처리 | 푸시 | |---|---|---|---| | 운송장 등록 기한 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` |