# 선물하기 클라이언트 API 요약 ## 문서 목적 - 이 문서는 클라이언트 구현 프롬프트 작성용 빠른 참조다. - 상세 request/response 계약은 `prd.md`의 `8. API 계약`을 기준으로 한다. - 여기서는 사용자 역할별 URI, 역할, 핵심 request/response 필드만 요약한다. ## 문서 분리 결정 PRD에는 상세 API 계약, request/response 예시, 요구사항을 유지한다. 클라이언트 프롬프트용 역할별 인벤토리는 별도 문서로 둔다. - 이유: PRD에 같은 URI 정보를 역할별로 다시 넣으면 상세 계약과 요약표를 동시에 갱신해야 한다. - 기준: PRD는 제품 요구사항과 API 계약의 원본, 이 문서는 클라이언트 구현을 위한 탐색용 색인이다. ## 공통/조회 API | 사용자 | Method | URI | 역할 | Request | Response | |---|---:|---|---|---|---| | 비회원/회원 | GET | `/api/v2/gifts/form-options` | 선물 신청 폼에 필요한 활성 카테고리와 사이즈별 가격 옵션을 조회한다. | 없음 | `sizes[]`, `categories[]` | | 팬/크리에이터 | GET | `/api/v2/gifts?type=ALL&page=0&size=20` | 내 선물함 목록을 조회하고 `type=ALL|SENT|RECEIVED`로 전체/보낸/받은 선물을 필터링한다. | query: `type`, `page`, `size` | `items[]`, `page`, `size`, `hasNext` | | 팬/크리에이터 | GET | `/api/v2/gifts/{applicationNo}` | 선물 상세를 조회하며 로그인 회원이 팬인지 크리에이터인지에 따라 노출 정보와 CTA flag가 달라진다. | path: `applicationNo` | `applicationNo`, `direction`, `status`, `statusName`, `giftInfo`, `senderInfo`, `recipientAddress`, `mailbox`, `trackingRequired`, `recipientAddressRequired`, `delivery`, `statusTimeline`, `review` | ## 팬 API | 사용자 | Method | URI | 역할 | Request | Response | |---|---:|---|---|---|---| | 팬 | POST | `/api/v2/gifts` | 크리에이터에게 선물을 신청하고 최종 결제 금액만큼 캔을 차감한다. | body: `recipientMemberId`, `senderName`, `senderPhoneNumber`, `senderZipCode`, `senderAddress`, `senderAddressDetail`, `sizeCode`, `categoryId`, `senderTermsAgreed`, `senderPrivacyAgreed`, `damageWaiverAgreed` | `applicationNo`, `status`, `statusName`, `priceCan`, `trackingDeadlineAt` | | 팬 | POST | `/api/v2/gifts/{applicationNo}/cancel` | 운송장 등록 전 선물 신청을 취소하고 전액 환불한다. | path: `applicationNo` | `applicationNo`, `status`, `statusName`, `priceCan`, `canceledAt` | | 팬 | POST | `/api/v2/gifts/{applicationNo}/tracking` | 발송 후 택배사와 운송장 번호를 등록한다. | path: `applicationNo`, body: `courierCompanyName`, `trackingNumber` | `applicationNo`, `status`, `statusName`, `courierCompanyName`, `trackingNumber`, `trackingRegisteredAt`, `recipientAddressDeadlineAt` | | 팬 | POST | `/api/v2/gifts/{applicationNo}/review` | 전달 완료된 선물에 리뷰를 작성한다. | path: `applicationNo`, body: `rating`, `keywords`, `comment` | `reviewId`, `applicationNo`, `rating`, `keywords`, `comment`, `createdAt` | ## 크리에이터 API | 사용자 | Method | URI | 역할 | Request | Response | |---|---:|---|---|---|---| | 크리에이터 | POST | `/api/v2/gifts/{applicationNo}/recipient-address` | 운송장 등록 이후 배송지와 수령 약관 동의를 등록한다. | path: `applicationNo`, body: `recipientName`, `recipientPhoneNumber`, `recipientZipCode`, `recipientAddress`, `recipientAddressDetail`, `recipientTermsAgreed`, `recipientPrivacyAgreed` | `applicationNo`, `status`, `statusName`, `recipientAddressRegisteredAt` | | 크리에이터 | POST | `/api/v2/gifts/{applicationNo}/delivery-complete` | 검수 완료된 선물을 직접 수령 완료 처리한다. | path: `applicationNo` | `applicationNo`, `status`, `statusName`, `deliveredAt` | ## 관리자 API | 사용자 | Method | URI | 역할 | Request | Response | |---|---:|---|---|---|---| | 관리자 | GET | `/api/v2/admin/gift-categories` | 선물 카테고리 목록을 조회한다. | 없음 | `AdminGiftCategoryResponse[]` | | 관리자 | GET | `/api/v2/admin/gifts` | 전체 선물함 목록을 조회하고 상태/신청번호/닉네임으로 검색한다. | query: `status`, `applicationNo`, `nickname`, `page`, `size` | `totalCount`, `items[]`, `page`, `size`, `hasNext` | | 관리자 | GET | `/api/v2/admin/gifts/{applicationNo}` | 선물 상세와 발송인/수취인 개인정보, 상품/배송/상태 정보를 조회한다. | path: `applicationNo` | `applicationNo`, `senderInfo`, `recipientInfo`, `productInfo`, `inboundDeliveryInfo`, `status`, `statusName`, `availableActions` | | 관리자 | POST | `/api/v2/admin/gift-categories` | 선물 카테고리를 등록한다. | body: `classificationNumber`, `categoryCode`, `name`, `receiptCode`, `representativeItem`, `requiresDamageWaiver`, `isActive` | `categoryId`, `classificationNumber`, `categoryCode`, `name`, `receiptCode`, `representativeItem`, `requiresDamageWaiver`, `isActive` | | 관리자 | PUT | `/api/v2/admin/gift-categories/{categoryId}` | 선물 카테고리를 수정한다. | path: `categoryId`, body: `classificationNumber`, `categoryCode`, `name`, `receiptCode`, `representativeItem`, `requiresDamageWaiver`, `isActive` | `categoryId`, `classificationNumber`, `categoryCode`, `name`, `receiptCode`, `representativeItem`, `requiresDamageWaiver`, `isActive` | | 관리자 | DELETE | `/api/v2/admin/gift-categories/{categoryId}` | 선물 카테고리를 비활성화한다. | path: `categoryId` | `categoryId`, `classificationNumber`, `categoryCode`, `name`, `receiptCode`, `representativeItem`, `requiresDamageWaiver`, `isActive=false` | | 관리자 | GET | `/api/v2/admin/gift-size-prices` | 사이즈별 기본가와 판매가를 조회한다. | 없음 | `AdminGiftSizePriceResponse[]` | | 관리자 | PUT | `/api/v2/admin/gift-size-prices/{sizeCode}` | 사이즈별 기본가와 판매가를 수정한다. | path: `sizeCode`, body: `basePriceCan`, `salePriceCan`, `isActive` | `sizeCode`, `name`, `basePriceCan`, `salePriceCan`, `isActive` | | 관리자 | POST | `/api/v2/admin/gifts/{applicationNo}/arrive-mailbox` | 발송 확인 선물을 사서함 도착 처리한다. | path: `applicationNo` | `applicationNo`, `status`, `statusName`, `occurredAt` | | 관리자 | POST | `/api/v2/admin/gifts/{applicationNo}/complete-inspection` | 사서함 도착 선물을 검수 완료 처리한다. | path: `applicationNo` | `applicationNo`, `status`, `statusName`, `occurredAt` | | 관리자 | POST | `/api/v2/admin/gifts/{applicationNo}/mark-undeliverable` | 진행 중 선물을 전달 불가 처리하고 사유를 저장한다. | path: `applicationNo`, body: `reason` | `applicationNo`, `status`, `statusName`, `occurredAt` | | 관리자 | POST | `/api/v2/admin/gifts/{applicationNo}/complete-delivery` | 검수 완료 선물을 운영자가 전달 완료 처리한다. | path: `applicationNo` | `applicationNo`, `status`, `statusName`, `occurredAt` | 관리자 응답 DTO 요약: - `AdminGiftCategoryResponse`: `categoryId`, `classificationNumber`, `categoryCode`, `name`, `receiptCode`, `representativeItem`, `requiresDamageWaiver`, `isActive` - `AdminGiftListResponse`: `totalCount`, `items`, `page`, `size`, `hasNext` - `AdminGiftListItemResponse`: `applicationNo`, `senderNickname`, `recipientNickname`, `sizeCode`, `sizeName`, `categoryName`, `classificationNumber`, `courierCompanyName`, `trackingNumber`, `status`, `statusName`, `availableActions` - `AdminGiftDetailResponse`: `applicationNo`, `senderInfo`, `recipientInfo`, `productInfo`, `inboundDeliveryInfo`, `status`, `statusName`, `availableActions` - `AdminGiftSizePriceResponse`: `sizeCode`, `name`, `basePriceCan`, `salePriceCan`, `isActive` - `AdminGiftOperationStatusResponse`: `applicationNo`, `status`, `statusName`, `occurredAt` ## Kotlin DTO 형태 아래는 JSON으로 변환되기 직전의 API request/response `data class` 형태다. `companion object` 변환 함수는 클라이언트 입력/출력 형태와 직접 관련이 없어 생략했다. 응답 시각 필드는 UTC ISO 문자열이다. 예: `2026-09-30T12:00:00Z`. ### 사용자 Request DTO ```kotlin data class GiftApplicationRequest( val recipientMemberId: Long, val senderName: String, val senderPhoneNumber: String, val senderZipCode: String, val senderAddress: String, val senderAddressDetail: String?, val sizeCode: GiftSize, val categoryId: Long, val senderTermsAgreed: Boolean, val senderPrivacyAgreed: Boolean, val damageWaiverAgreed: Boolean ) data class GiftTrackingRegistrationRequest( val courierCompanyName: String, val trackingNumber: String ) data class GiftRecipientAddressRegistrationRequest( val recipientName: String, val recipientPhoneNumber: String, val recipientZipCode: String, val recipientAddress: String, val recipientAddressDetail: String?, val recipientTermsAgreed: Boolean, val recipientPrivacyAgreed: Boolean ) data class GiftReviewRequest( val rating: Int, val keywords: List, val comment: String? ) ``` ### 사용자 Response DTO ```kotlin data class GiftFormOptionsResponse( val sizes: List, val categories: List ) data class GiftSizeOptionResponse( val sizeCode: String, val name: String, val basePriceCan: Int, val salePriceCan: Int ) data class GiftCategoryOptionResponse( val categoryId: Long, val name: String, val requiresDamageWaiver: Boolean ) // name은 사용자 폼 옵션에서만 "분류명/대표품목" 형태로 조합된다. data class GiftListResponse( val totalCount: Long, val items: List, val page: Int, val size: Int, val hasNext: Boolean ) data class GiftListItemResponse( val applicationNo: String, val direction: String, val status: String, val statusName: String, val priceCan: Int, val categoryName: String, val sizeName: String, val createdAt: String? ) data class GiftDetailResponse( val applicationNo: String, val direction: String, val status: String, val statusName: String, val giftInfo: GiftDetailInfoResponse, val senderInfo: GiftAddressResponse?, val recipientAddress: GiftAddressResponse?, val mailbox: GiftMailboxResponse?, val trackingRequired: Boolean, val recipientAddressRequired: Boolean, val recipientAddressDeadlineAt: String?, val delivery: GiftDeliveryInfoResponse, val statusTimeline: List ) data class GiftDetailInfoResponse( val recipientCreatorNickname: String?, val senderNickname: String?, val sizeName: String, val categoryName: String, val applicationNo: String?, val paidCan: Int?, val tracking: String?, val shippingRequestedAt: String? ) data class GiftAddressResponse( val name: String, val phoneNumber: String, val address: String ) data class GiftMailboxResponse( val name: String, val address: String, val phoneNumber: String ) // mailbox는 direction=SENT이고 status가 RECEIVED, TRACKING_REGISTERED, // ARRIVED_AT_MAILBOX, INSPECTION_COMPLETED일 때만 값이 있다. // DELIVERED, UNDELIVERABLE, CANCELED 또는 direction=RECEIVED이면 null이다. data class GiftDeliveryInfoResponse( val canceledAt: String?, val undeliverableAt: String?, val undeliverableReason: String? ) data class GiftStatusTimelineResponse( val status: String, val statusName: String, val occurredAt: String? ) data class GiftApplicationResponse( val applicationNo: String, val status: String, val statusName: String, val priceCan: Int, val trackingDeadlineAt: String ) data class GiftCancellationResponse( val applicationNo: String, val status: String, val statusName: String, val priceCan: Int, val canceledAt: String ) data class GiftTrackingRegistrationResponse( val applicationNo: String, val status: String, val statusName: String, val courierCompanyName: String, val trackingNumber: String, val trackingRegisteredAt: String, val recipientAddressDeadlineAt: String ) data class GiftRecipientAddressRegistrationResponse( val applicationNo: String, val status: String, val statusName: String, val recipientAddressRegisteredAt: String ) data class GiftDeliveryConfirmationResponse( val applicationNo: String, val status: String, val statusName: String, val deliveredAt: String ) data class GiftReviewResponse( val reviewId: Long, val applicationNo: String, val rating: Int, val keywords: List, val comment: String?, val createdAt: String? ) ``` ### 관리자 Request DTO ```kotlin data class AdminGiftCategoryRequest( val classificationNumber: String, val categoryCode: String, val name: String, val receiptCode: String, val representativeItem: String, val requiresDamageWaiver: Boolean, val isActive: Boolean ) data class AdminGiftSizePriceRequest( val basePriceCan: Int, val salePriceCan: Int, val isActive: Boolean ) data class AdminGiftMarkUndeliverableRequest( val reason: String ) ``` ### 관리자 Response DTO ```kotlin data class AdminGiftCategoryResponse( val categoryId: Long, val classificationNumber: String, val categoryCode: String, val name: String, val receiptCode: String, val representativeItem: String, val requiresDamageWaiver: Boolean, val isActive: Boolean ) data class AdminGiftListResponse( val totalCount: Long, val items: List, val page: Int, val size: Int, val hasNext: Boolean ) data class AdminGiftListItemResponse( val applicationNo: String, val senderNickname: String, val recipientNickname: String, val sizeCode: String, val sizeName: String, val categoryName: String, val classificationNumber: String, val courierCompanyName: String?, val trackingNumber: String?, val status: String, val statusName: String, val availableActions: List ) data class AdminGiftDetailResponse( val applicationNo: String, val senderInfo: AdminGiftMemberInfoResponse, val recipientInfo: AdminGiftMemberInfoResponse, val productInfo: AdminGiftProductInfoResponse, val inboundDeliveryInfo: AdminGiftInboundDeliveryInfoResponse, val status: String, val statusName: String, val availableActions: List ) data class AdminGiftMemberInfoResponse( val nickname: String, val name: String, val phoneNumber: String, val address: String ) data class AdminGiftProductInfoResponse( val sizeCode: String, val sizeName: String, val categoryName: String, val classificationNumber: String ) data class AdminGiftInboundDeliveryInfoResponse( val courierCompanyName: String?, val trackingNumber: String? ) data class AdminGiftSizePriceResponse( val sizeCode: String, val name: String, val basePriceCan: Int, val salePriceCan: Int, val isActive: Boolean ) data class AdminGiftOperationStatusResponse( val applicationNo: String, val status: GiftStatus, val statusName: String, val occurredAt: String ) ``` ## 클라이언트 구현 참고 - 선물 푸시 딥링크는 `deepLinkValue=GIFT_DETAIL`, `deepLinkId=applicationNo`를 사용한다. - 딥링크 진입 후에는 `GET /api/v2/gifts/{applicationNo}`로 상세를 조회해 `direction`, `trackingRequired`, `recipientAddressRequired`를 기준으로 화면과 CTA를 결정한다. - 운송장 미등록 자동취소는 서버 스케줄러가 처리하며, 팬 클라이언트는 상세 조회의 `status=CANCELED`와 푸시를 기준으로 표시한다. - 모든 응답은 기존 `ApiResponse.ok(...)` envelope 안의 `data`로 내려간다.