Files
sodalive-ios/docs/20261006_선물_UI_추가/prd.md
T

404 lines
36 KiB
Markdown

# 크리에이터 선물하기 사용자 기능 PRD
## 문서 정보
| 항목 | 내용 |
|---|---|
| 문서 상태 | 구현 기준 확정 |
| 작성일 | `2026-10-06` |
| 최종 수정일 | `2026-10-08` |
| 대상 제품 | 크리에이터 선물 보내기, 선물함, 선물 상세, 운송장 등록, 배송지 입력, 선물 딥링크 |
| 작성자·결정권자 | 사용자 요청 기준 |
| 관련 API Contract | 이 문서 §11 API 계약 |
| 관련 구현 계획 | `docs/20261006_선물_UI_추가/plan-task.md` |
| 관련 review | 없음 |
### 요구사항 상태
| 상태 | 의미 | 구현 처리 |
|---|---|---|
| 확정 | 제품·기술 결정이 완료되어 구현 기준으로 사용 | `plan-task.md`의 Phase/Task와 완료 증거로 추적 |
| 제외 | 현재 릴리스에서 구현하지 않기로 결정 | Non-Goals와 Decision Log에 기록 |
## 1. Overview
크리에이터 선물하기는 팬이 크리에이터에게 오프라인 선물을 신청하고, 소다라이브 사서함과 검수 과정을 거쳐 안전하게 전달되도록 돕는 iOS 사용자 기능이다. 팬은 선물 신청, 운송장 등록, 신청 취소, 보낸 선물 상세 조회를 수행하고, 크리에이터는 받은 선물 목록 확인, 배송지 입력, 전달 상태 확인을 수행한다.
기존 선물 진입점 UI는 Phase 1에서 구현 완료됐다. 이번 문서는 해당 진입점을 실제 선물 보내기/선물함/상세/딥링크 흐름에 연결하는 후속 구현 기준이다.
## 2. Problem Statement
- 현재 iOS에는 선물 기능 진입점만 있고 실제 신청, 목록, 상세, 운송장, 배송지 입력 흐름이 연결되어 있지 않다.
- 선물 기능은 팬과 크리에이터의 개인정보 노출 경계가 다르므로, `direction`별 UI 분기와 표시 금지 필드를 명확히 고정해야 한다.
- 신청 성공, 목록 item, 푸시/딥링크가 서로 다른 상세 화면으로 흩어지면 상태 분기와 개인정보 노출 회귀 위험이 커진다.
문제를 해결했다는 판단은 모든 진입점이 같은 상세 조회 흐름을 사용하고, API 계약·Figma 구조·개인정보 표시 금지 조건이 검증되는 것으로 한다.
## 3. Goals
### 3.1 제품 목표
- 팬이 크리에이터 상세에서 선물 신청을 완료하고, 신청 완료 후 같은 신청번호의 상세로 이동한다.
- 사용자가 선물함에서 보낸 선물과 받은 선물을 `type=ALL` 목록으로 확인하고 상세로 진입한다.
- 보낸 팬은 운송장 등록과 신청 취소를 수행할 수 있다.
- 받은 크리에이터는 필요한 경우 배송지를 입력하고 전달 상태를 확인할 수 있다.
- 푸시/딥링크, 리스트 item, 신청 완료 이동이 모두 `GET /api/v2/gifts/{applicationNo}` 기반 상세 화면을 사용한다.
### 3.2 UX 목표
- Figma의 iPhone 13 402px 폭, black/dark surface, 14pt 좌우 여백, rounded card/input/banner, section divider 구조를 따른다.
- 상단 navigation/status 영역은 고정하고 navigation 아래 본문만 스크롤한다.
- bottom action bar, keyboard avoidance, loading/error/toast/dialog는 기존 iOS 앱 패턴을 재사용한다.
- Figma에 보이는 `신청 내역`, `선물 내역` 섹션 타이틀은 실제 선물함 리스트에 노출하지 않는다.
## 4. Non-Goals
- 관리자 화면.
- 새 디자인 시스템 생성.
- 외부 택배 조회 또는 운송장 실시간 검증.
- 약관 본문 제공 API 신규 구현.
- 선물 이미지, 첨부파일, 메시지 카드.
- 임의 URL 연결. 정책/문의/리뷰 등 기존 앱 이동 경로가 없으면 숨기거나 `준비 중입니다.` toast만 사용한다.
- API에 없는 취소일 기준 재정렬, 사이즈 설명 서버 필드 추정, 오류 status/key 추정.
## 5. Target Users and Permissions
| 사용자 | 목표 | 주요 작업 | 사용 환경 |
|---|---|---|---|
| 팬 | 크리에이터에게 선물을 신청하고 진행 상태를 확인 | 선물 신청, 선물함 조회, 운송장 등록, 신청 취소, 상세 조회 | iOS 앱 |
| 크리에이터 | 팬이 보낸 선물을 받을 수 있게 배송지를 입력하고 상태를 확인 | 선물함 조회, 배송지 입력, 상세 조회, 문의 | iOS 앱 |
권한은 기존 인증 토큰과 서버 리소스 소유권 검증을 따른다. 권한 오류 또는 미존재 오류는 기존 앱의 딥링크/상세 오류 처리 패턴을 사용한다.
## 6. 핵심 사용자 흐름
1. 팬이 크리에이터 상세의 `선물 보내기` 버튼으로 신청 화면에 진입한다.
2. 앱은 전달받은 크리에이터 닉네임과 `memberId`를 검증하고 `GET /api/v2/gifts/form-options`를 호출한다.
3. 팬은 사이즈, 카테고리, 발신자 정보, 약관 동의를 입력하고 `POST /api/v2/gifts`를 호출한다.
4. 신청 성공 시 완료 dialog에 머무르지 않고 응답의 `applicationNo`로 상세 화면을 replace 또는 stack 정리 방식으로 연다.
5. 사용자는 마이 탭 `선물함`에서 `GET /api/v2/gifts?type=ALL&page=0&size=20` 목록을 확인한다.
6. 리스트 item 탭, 신청 완료 이동, 푸시 딥링크는 모두 같은 선물 상세 route로 진입한다.
7. 상세 화면은 `direction`, `trackingRequired`, `recipientAddressRequired`, `status`에 따라 보낸 상세, 받은 상세, 운송장 CTA, 배송지 입력 UI, 취소 완료 UI를 분기한다.
## 7. 정보 구조와 라우팅
```text
크리에이터 상세
선물 보내기 버튼
-> GiftSendView(recipientMemberId, recipientCreatorNickname, recipientCreatorProfileImageUrl)
-> success replace GiftDetailView(applicationNo)
마이 탭
선물함 버튼
-> GiftBoxListView
-> GiftDetailView(applicationNo)
GiftDetailView(applicationNo)
direction=SENT
-> GiftTrackingRegistrationView(applicationNo)
-> GiftCancelConfirmDialog
-> GiftCancelCompleteView 또는 GiftDetailView(status=CANCELED)
direction=RECEIVED
-> inline recipient address form when recipientAddressRequired=true
DeepLink GIFT_DETAIL(applicationNo)
DeepLink gift-detail/applicationNo
-> GiftDetailView(applicationNo)
```
신규 route는 기존 `AppStep`/`AppState`/`ContentView` 방식에 맞춰 추가한다. 신청 성공과 취소 성공은 뒤로가기로 직전 입력/등록 화면에 돌아가지 않도록 `replaceCurrentAppStep` 또는 동등한 stack 정리 정책을 사용한다.
## 8. 기능 요구사항
### 8.1 기존 진입점
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `GIFT-001` | 확정 | 크리에이터 상세에 `선물 보내기` 버튼을 표시한다. | 내 채널이 아니고 `creator.isDmAvailable == true`, 한국 접속, `LanguageHeaderProvider.current == "ko"`일 때만 표시된다. | `P1-T3` |
| `GIFT-002` | 확정 | 마이 탭 카테고리 그리드 첫 항목에 `선물함`을 표시한다. | `ic_my_present` 아이콘과 `선물함` 텍스트가 보관함 앞에 표시된다. | `P1-T4` |
| `GIFT-003` | 확정 | 기존 inert action을 실제 선물 화면 이동으로 교체한다. | `선물 보내기`는 신청 화면, `선물함`은 리스트 화면으로 이동한다. | `P2-T3` |
| `GIFT-004` | 확정 | 크리에이터 채널의 `선물 보내기` 버튼을 흰색 capsule로 표시한다. | `ic_send_gift`가 문구 왼쪽에 표시되고 배경은 흰색, 문구는 검은색이며 기존 노출 조건과 이동 동작은 유지된다. | `P11-T3` |
### 8.2 선물 보내기 신청
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `SEND-001` | 확정 | 진입 시 크리에이터 닉네임과 `memberId`를 소비한다. | 닉네임은 받는 크리에이터 영역에 표시되고 `memberId`는 `recipientMemberId`로 전송된다. 둘 중 하나가 없으면 기존 오류/뒤로가기 패턴을 따른다. | `P3-T1` |
| `SEND-002` | 확정 | 화면 진입 시 폼 옵션 API를 호출한다. | `GET /api/v2/gifts/form-options`의 첫 번째 `sizes`를 기본 선택하고 `categories`를 picker/bottom sheet/dialog로 선택한다. | `P3-T1` |
| `SEND-003` | 확정 | 사이즈 설명은 로컬 매핑을 사용한다. | `SMALL`, `MEDIUM`, `LARGE` 설명이 요구 문구와 일치한다. | `P3-T2` |
| `SEND-004` | 확정 | 카테고리의 파손 면책 요구를 반영한다. | `requiresDamageWaiver=true`일 때만 면책 동의 row를 표시하고 필수로 검증한다. `false`로 바뀌면 숨기고 `damageWaiverAgreed=false`로 초기화한다. | `P3-T2` |
| `SEND-005` | 확정 | 필수 입력/동의가 충족될 때만 CTA를 활성화한다. | 미충족 상태에서는 `POST /api/v2/gifts`를 호출하지 않는다. submit 중 중복 탭이 방지된다. | `P3-T3` |
| `SEND-006` | 확정 | 신청 성공 후 상세 화면으로 이동한다. | 응답의 `applicationNo`로 `GiftDetailView`를 replace 방식으로 열고 뒤로가기로 입력 폼에 돌아가지 않는다. | `P3-T4` |
| `SEND-007` | 확정 | 보내는 사람 이름과 휴대폰 번호를 제한한다. | 이름은 한글·영문 1~12자이며 한글 조합 중 임시 문자열을 확정값으로 덮어쓰지 않는다. 휴대폰은 편집 중 ASCII 숫자 최대 11자, 제출 시 `010`으로 시작하는 정확히 11자리만 허용한다. | `P10-T1`~`P10-T4` |
| `SEND-008` | 확정 | 크리에이터 채널에서 받은 프로필 이미지 URL을 선물 보내기 화면까지 전달한다. | 받는 크리에이터 영역에 전달받은 프로필 이미지가 표시되며 기존 닉네임과 `recipientMemberId` 전달은 유지된다. | `P11-T2` |
### 8.3 선물함 리스트
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `LIST-001` | 확정 | 선물함은 `type=ALL` 목록만 호출한다. | `SENT`, `RECEIVED`를 따로 호출하지 않고 `items[].direction`으로 카드 UI를 결정한다. | `P4-T1` |
| `LIST-002` | 확정 | pagination과 refresh를 지원한다. | `hasNext=true`이면 하단 도달 시 다음 page를 조회하고, pull-to-refresh가 가능하면 page 0부터 다시 조회한다. | `P4-T2` |
| `LIST-003` | 확정 | 보낸/받은 선물함 상단 안내 배너와 empty를 분기한다. | 보낸 선물 목록은 상단 안내 배너 `사서함을 통해 안전하게 전달됩니다`를 표시한다. 받은 선물 목록에 item이 있으면 상단 안내 배너 `배송지 입력 기간 안내`를 표시한다. 보낸 empty는 보낸 선물 상단 안내 배너만 표시하고, 받은 empty는 제목/본문/`의견 남기기` 버튼을 표시한다. | `P4-T3` |
| `LIST-004` | 확정 | 받은 선물의 배송지 미입력 상태는 각 카드에서 표시한다. | 별도 파란 상단 배너를 표시하지 않는다. 목록 응답 기준 `direction=RECEIVED && status=TRACKING_REGISTERED`인 item 카드에 배송지 입력 필요 상태를 표시하고, 해당 카드 tap 시 그 선물 상세로 이동한다. `recipientAddressDeadlineAt`이 없으면 임의 남은 일수를 만들지 않는다. | `P4-T3` |
| `LIST-005` | 확정 | 리스트 item 탭은 상세 화면으로 이동한다. | `applicationNo`를 전달하고 상세 API 결과로 UI를 분기한다. | `P4-T4` |
### 8.4 공통 상세와 보낸 상세
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `DETAIL-001` | 확정 | 모든 상세 진입은 상세 API를 호출한다. | 리스트, 신청 성공, 푸시 딥링크가 모두 `GET /api/v2/gifts/{applicationNo}` 결과의 `direction`으로 렌더링한다. | `P5-T1`, `P7-T1` |
| `DETAIL-002` | 확정 | progress는 `statusTimeline` 5단계를 사용한다. | `RECEIVED`, `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED`, `DELIVERED` 순서를 사용하고 `UNDELIVERABLE`, `CANCELED`를 step으로 추가하지 않는다. | `P5-T2` |
| `DETAIL-003` | 확정 | 보낸 상세 개인정보 경계를 지킨다. | `recipientAddress`를 표시하지 않고, `senderInfo`와 `mailbox`는 있을 때만 표시한다. | `P5-T3` |
| `DETAIL-004` | 확정 | `trackingRequired=true`일 때만 운송장 등록 CTA를 표시한다. | CTA 탭 시 운송장 등록 화면으로 이동한다. | `P5-T4` |
| `DETAIL-005` | 확정 | `direction=SENT && status=RECEIVED`이면 신청 취소 action을 제공할 수 있다. | 확인 dialog의 destructive action 이후에만 취소 API를 호출한다. | `P5-T5` |
| `DETAIL-006` | 확정 | `status=CANCELED`는 취소 완료 UI를 표시한다. | 취소 전 상세/등록 화면으로 back 되지 않도록 replace 한다. | `P5-T5` |
| `DETAIL-007` | 확정 | 선물 상세의 모든 `문의하기` 버튼은 고객센터의 `TALK 문의`와 같은 action을 실행한다. | 발신·수신 전달 불가와 취소 완료 상세에서 앱 내 고객센터로 이동하지 않고 공용 TALK action으로 `http://pf.kakao.com/_lkxgxhG/chat`을 연다. 기존 문구·노출 조건·스타일은 유지한다. | `P12-T1` |
### 8.5 운송장 등록
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `TRACK-001` | 확정 | 운송장 등록 화면 진입 시 상세를 재조회한다. | 재조회 결과 `trackingRequired=false`이면 기존 오류 처리 후 상세로 복귀한다. | `P6-T1` |
| `TRACK-002` | 확정 | `mailbox`가 없으면 등록을 막는다. | `받을 주소가 등록되어 있지 않습니다.` 오류를 표시하고 submit을 허용하지 않는다. | `P6-T1` |
| `TRACK-003` | 확정 | 택배사 목록은 기존 공통 상수가 있으면 재사용하고 없으면 로컬 상수를 사용한다. | 선택한 한글 표시명을 그대로 `courierCompanyName`으로 보낸다. | `P6-T2` |
| `TRACK-004` | 확정 | 택배사와 운송장 번호가 있을 때만 등록 CTA를 활성화한다. | 공백 운송장 번호는 submit하지 않고, submit 중 중복 탭을 방지한다. | `P6-T3` |
| `TRACK-005` | 확정 | 등록 성공 후 상세를 갱신한다. | 성공 toast 후 상세로 replace/복귀하고 상세 API 재조회 결과로 `TRACKING_REGISTERED`를 표시한다. | `P6-T3` |
| `TRACK-006` | 확정 | 운송장 등록 화면의 `신청 취소`는 선물 신청을 취소한다. | 확인 dialog의 destructive action 이후에만 기존 취소 API를 호출하고, 성공 시 동일 신청번호 상세로 guarded replace해 취소 완료 UI를 표시한다. | `P6-T2`, `P6-T3` |
| `TRACK-007` | 확정 | 운송장 번호는 숫자 형식과 길이를 입력·제출 경계에서 검증한다. | 편집 중 ASCII 숫자 최대 14자만 허용하고, 택배사 선택과 함께 10~14자리일 때만 등록 CTA와 POST를 허용한다. | `P10-T5` |
### 8.6 받은 상세와 배송지 입력
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `RECV-001` | 확정 | 받은 상세 개인정보 경계를 지킨다. | `senderInfo`, `mailbox`, `giftInfo.applicationNo`, `giftInfo.paidCan`, `giftInfo.tracking`을 표시하지 않는다. | `P7-T1` |
| `RECV-002` | 확정 | `recipientAddressRequired=true`이면 배송지 입력 UI를 표시한다. | 이름, 휴대폰, 우편번호, 주소, 상세 주소와 약관 2개를 입력한다. | `P7-T2` |
| `RECV-003` | 확정 | 배송지 CTA는 필수값과 약관 2개가 충족될 때만 활성화한다. | validation 실패 시 `POST /recipient-address`를 호출하지 않는다. | `P7-T3` |
| `RECV-004` | 확정 | 배송지 입력 성공 후 상세를 재조회한다. | 성공 toast 후 배송지 입력 완료 UI를 표시한다. | `P7-T3` |
| `RECV-005` | 확정 | `recipientAddress=null`이면 받는 주소 섹션 전체를 숨긴다. | 빈 섹션, skeleton, `-` 값을 표시하지 않는다. | `P7-T4` |
| `RECV-006` | 확정 | `status=UNDELIVERABLE`이면 배송지 입력과 주소 섹션을 숨긴다. | 전달 불가 hero, 문의/정책 action, 사유/폐기 안내만 표시한다. | `P7-T4` |
| `RECV-007` | 확정 | 받는 크리에이터 배송지의 이름과 휴대폰 번호에 보내는 사람과 같은 규칙을 적용한다. | 이름은 한글·영문 1~12자이며 한글 조합을 보존한다. 휴대폰은 편집 중 ASCII 숫자 최대 11자, 제출 시 `010`으로 시작하는 정확히 11자리만 허용한다. | `P10-T1`~`P10-T4` |
| `RECV-008` | 확정 | 전달 불가 사유 아래 붉은 안내 문구는 앱 내 고정 문구로 표시한다. | 발신·수신 상세 모두 `해당 선물은 보관기간(입고일 포함 최대 7일) 내에 반송 등 필요한 조치가 완료되지 않을 경우, 사전에 동의한 약관 및 폐기 등의 내용에 따라 폐기될 수 있습니다. 자세한 내용은 고객센터로 문의해 주세요.`를 표시하고 서버 응답 필드는 추가하지 않는다. | `P7-R1` |
### 8.7 약관, 딥링크, 날짜
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|---|---|---|---|---|
| `COMMON-001` | 확정 | sender/recipient 약관 `(필수)` 링크는 같은 Notion 페이지로 이동한다. | URL은 `https://app.notion.com/p/3eb27b359673802b937ae741ab7b4ad3`를 사용한다. | `P3-T2`, `P7-T2` |
| `COMMON-002` | 확정 | 날짜는 UTC ISO 문자열을 앱 표시 형식으로 변환한다. | 상세 필드는 `YYYY.MM.DD`, progress는 `MM.DD`로 표시한다. | `P2-T2` |
| `COMMON-003` | 확정 | 푸시 딥링크는 `deepLinkValue=GIFT_DETAIL`, `deepLinkId=applicationNo`를 사용한다. | 딥링크 진입 시 상세 API를 먼저 호출하고 응답으로 UI를 결정한다. | `P8-T1` |
| `COMMON-004` | 확정 | API 실패는 기존 오류 노출 방식을 사용한다. | 입력값과 스크롤 상태를 불필요하게 초기화하지 않는다. | 각 Phase Gate |
| `COMMON-005` | 확정 | 선물 상세의 전달 불가 `선물 정책 확인`은 약관 `(필수)` 링크와 같은 페이지로 이동한다. | 발신·수신 상세 모두 `GiftConstants.termsURL`을 시스템 브라우저로 연다. | `P2-R3`, `P5-T4`, `P7-T4` |
| `COMMON-006` | 확정 | `voiceon://gift-detail/{applicationNo}`와 `voiceon-test://gift-detail/{applicationNo}` 경로형 딥링크를 지원한다. | `gift-detail` 뒤 첫 path component를 신청번호로 사용해 기존 `GiftDetailView(applicationNo)`로 이동하며 scheme별 별도 분기는 추가하지 않는다. | `P11-T1` |
| `COMMON-007` | 확정 | 파손 면책 동의 `(필수)`는 별도 파손 정책 페이지로 이동한다. | `requiresDamageWaiver=true`인 동의 행의 `(필수)`만 `https://app.notion.com/p/3f227b3596738018ba5dea2e5205293b`를 열고 기존 sender/recipient 약관 링크는 유지한다. | `P11-T2` |
## 9. 반응형 기능 범위
| 기능 | iPhone | iPad | 비고 |
|---|---:|---:|---|
| 선물 보내기 | 전체 | 전체 | iPhone 13 402px Figma 구조 우선, 기존 SwiftUI adaptive 정책 적용 |
| 선물함 | 전체 | 전체 | navigation 고정, 본문 scroll |
| 선물 상세 | 전체 | 전체 | status/action별 bottom bar safe area 유지 |
| 운송장 등록 | 전체 | 전체 | keyboard avoidance 필요 |
| 배송지 입력 | 전체 | 전체 | keyboard avoidance 필요 |
| 딥링크 | 전체 | 전체 | 권한/미존재 오류는 기존 패턴 |
## 10. UI/UX Expectations
### 10.1 Figma 기준
- 선물 보내기: `2481:18904`, `2481:18957`
- 선물함 리스트: `2481:19382`, `2520:35587`, `2531:36283`, `2531:36194`, `2531:36237`
- 보낸 상세/운송장: `2481:19011`, `2481:19152`, `2481:19320`, `2481:19456`, `2481:19500`, `2481:19521`, `2481:19066`, `2481:19109`
- 받은 상세/배송지: `2531:36290`, `2531:36399`, `2531:36330`, `2531:36422`
### 10.2 디자인과 component 원칙
- 기존 V2 dark surface, `TitleBar`/`DefaultTitleBar`, `SodaV2ActionModal`, `sodaToast`, `LottieRefreshableScrollView`, 기존 input/button/picker/dialog 패턴을 재사용한다.
- 재사용 근거가 없는 선물 전용 UI는 `SodaLive/Sources/V2/Gift/**/Components`에 둔다.
- 여러 선물 화면에서 같은 의미로 반복되는 card/banner/field/action bar만 `SodaLive/Sources/V2/Gift/Components/**` 또는 기존 `V2/Component/**`로 올린다.
- 새 디자인 시스템, 새 dependency, 새 전역 style token은 만들지 않는다.
### 10.3 화면 상태
- 모든 network 화면은 loading, error, empty/success 상태를 가진다.
- mutation은 submit 중 중복 탭을 막고 실패 시 입력값을 유지한다.
- destructive action인 신청 취소는 확인 dialog 없이 API를 호출하지 않는다.
- `P6-T2`는 운송장 등록·신청 취소의 입력 및 확인 UI만 구성하고 mutation은 `P6-T3`에서 연결한다.
## 11. API 계약
### 11.1 공통 규칙
- base URL과 인증 header는 기존 Moya `TargetType`/`UserDefaultsKey.token` 패턴을 따른다.
- 모든 응답은 공통 envelope를 사용하며 payload는 `ApiResponse<T>.data` 또는 기존 data unwrap 결과다.
- 이 문서의 endpoint/request/response 필드는 사용자 제공 프롬프트를 확정 계약으로 삼는다.
- 폼 옵션의 `sizes[]`, `categories[]` 표시명 key는 각각 `name`이며, 상세 `statusTimeline[]`은 `statusName`을 포함한다.
- 상세 `giftInfo.applicationNo`는 `null`일 수 있으며, 화면과 후속 요청에는 top-level `data.applicationNo`를 권위 값으로 사용한다.
- `page`는 이 기능 API 계약 기준으로 0-base 요청을 사용한다. 기존 앱의 1-base wrapper를 재사용할 경우 adapter에서 명시적으로 변환하지 않는다.
### 11.2 Endpoint 추적
| 요구사항 | Method | Path | 계약 상태 | 소유 Goal |
|---|---|---|---|---|
| `SEND-002` | `GET` | `/api/v2/gifts/form-options` | 확정 | `P2-T1`, `P3-T1` |
| `SEND-006` | `POST` | `/api/v2/gifts` | 확정 | `P2-T1`, `P3-T4` |
| `LIST-001` | `GET` | `/api/v2/gifts?type=ALL&page=0&size=20` | 확정 | `P2-T1`, `P4-T1` |
| `DETAIL-001` | `GET` | `/api/v2/gifts/{applicationNo}` | 확정 | `P2-T1`, `P5-T1`, `P7-T1` |
| `DETAIL-005`, `TRACK-006` | `POST` | `/api/v2/gifts/{applicationNo}/cancel` | 확정 | `P2-T1`, `P5-T5`, `P6-T3` |
| `TRACK-005` | `POST` | `/api/v2/gifts/{applicationNo}/tracking` | 확정 | `P2-T1`, `P6-T3` |
| `RECV-004` | `POST` | `/api/v2/gifts/{applicationNo}/recipient-address` | 확정 | `P2-T1`, `P7-T3` |
| fan review | `POST` | `/api/v2/gifts/{applicationNo}/review` | 기존 리뷰 플로우 있을 때만 연결 | 후속 |
### 11.3 Enum과 상태 표시
- direction: `SENT`, `RECEIVED`
- status: `RECEIVED`, `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED`, `DELIVERED`, `UNDELIVERABLE`, `CANCELED`
- size code: `SMALL`, `MEDIUM`, `LARGE`
### 11.4 운송장 등록 택배사 목록
기존 앱 공통 택배사 상수가 있으면 우선 재사용한다. 없으면 아래 한글 표시명을 로컬 상수로 사용하고, 선택한 값을 그대로 `courierCompanyName`으로 보낸다.
- `CJ대한통운`
- `우체국택배`
- `GS25 편의점택배`
- `CU 편의점택배`
- `한진택배`
- `롯데택배`
- `로젠택배`
- `경동택배`
- `대신택배`
- `일양로지스`
- `천일택배`
- `합동택배`
- `건영택배`
- `농협택배`
## 12. 보안과 데이터 취급
- 보낸 팬 상세에는 크리에이터가 입력한 `recipientAddress`를 표시하지 않는다.
- 받은 크리에이터 상세에는 팬의 `senderInfo`, `mailbox`, `giftInfo.applicationNo`, `giftInfo.paidCan`, `giftInfo.tracking`을 표시하지 않는다.
- `recipientAddress=null`이면 주소 섹션을 통째로 숨긴다.
- `UNDELIVERABLE` 받은 상세에서는 응답에 주소가 있어도 받는 주소 섹션을 표시하지 않는다.
- 이름, 휴대폰, 주소, 운송장 번호, token은 debug log, error log, fixture 문서에 기록하지 않는다.
## 13. 성능과 품질 요구사항
- 신규 의존성을 추가하지 않는다.
- 목록은 `hasNext` 기반 pagination을 사용하고 서버 응답 순서를 유지한다.
- 화면별 ViewModel은 기존 `ObservableObject`, Combine, `MoyaProvider`, `ApiResponse<T>` 패턴을 따른다.
- 현재 테스트 번들 타깃이 확인되지 않으므로 구현 계획은 가능한 ViewModel 순수 로직 test를 우선 요구하되, 불가능한 SwiftUI visual 분기는 build와 수동 검증으로 대체한다.
- 기본 검증 명령은 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`와 `SodaLive-dev` Debug build다.
## 14. 성공 기준
### 14.1 기능 수용 기준
- [ ] 선물 보내기 진입 시 전달받은 닉네임과 `memberId`가 화면/API에 연결된다.
- [ ] 선물 보내기 진입 시 전달받은 크리에이터 프로필 이미지 URL이 받는 크리에이터 영역에 표시된다.
- [ ] 필수 입력/동의 누락 시 신청 API를 호출하지 않는다.
- [ ] `requiresDamageWaiver=true` 카테고리에서만 파손 면책 동의가 표시되고 필수다.
- [ ] 신청 성공 후 `applicationNo` 상세로 replace 이동하고 뒤로가기로 입력 폼에 돌아가지 않는다.
- [ ] 선물함 리스트는 `type=ALL` 한 번 조회와 `direction` 카드 분기를 사용한다.
- [ ] Figma의 `신청 내역`, `선물 내역` 섹션 타이틀이 노출되지 않는다.
- [ ] 리스트 item, 푸시 딥링크, 신청 완료 이동이 같은 상세 조회 플로우를 사용한다.
- [ ] 양 앱 scheme의 `gift-detail/{applicationNo}` 경로형 딥링크가 같은 상세 조회 플로우를 사용한다.
- [ ] 파손 면책 `(필수)`는 파손 정책 페이지를 열고 기존 약관 `(필수)` 링크는 기존 페이지를 유지한다.
- [ ] 상세 progress는 `statusTimeline` 5단계를 사용한다.
- [ ] 운송장 등록과 배송지 입력 성공 후 상세를 재조회해 최신 상태를 표시한다.
- [ ] 신청 취소는 확인 dialog 이후에만 API를 호출하고 성공 시 취소 완료 UI로 replace 된다.
- [ ] 발신·수신 전달 불가와 취소 완료 상세의 `문의하기`가 고객센터의 `TALK 문의`와 같은 action을 실행한다.
### 14.2 개인정보 수용 기준
- [ ] `direction=SENT` 상세에서 `recipientAddress`를 표시하지 않는다.
- [ ] `direction=RECEIVED` 상세에서 `senderInfo`, `mailbox`, `giftInfo.paidCan`, `giftInfo.tracking`을 표시하지 않는다.
- [ ] `recipientAddress=null`이면 받는 주소 섹션을 통째로 숨긴다.
- [ ] `status=UNDELIVERABLE`이면 받은 상세에서 배송지 입력 UI와 받는 주소 섹션을 숨긴다.
### 14.3 추적성 완료 기준
- [ ] 모든 확정 요구사항이 `plan-task.md`의 Phase/Task와 연결된다.
- [ ] 모든 Phase Gate에 자동 또는 대체 수동 검증 기록이 누적된다.
- [ ] API 계약과 문서 요구사항이 충돌하면 Decision Log에 정정 기록을 남긴다.
## 15. Open Questions
없음. 약관 Notion URL과 API 계약은 사용자 답변으로 확정됐다.
## 16. 요구사항 추적표
| 요구사항 범위 | API Contract | 계획 Phase | 자동 검증 | 수동 검증 |
|---|---|---:|---|---|
| `GIFT-001~004` | 해당 없음 | 1, 2, 11 | Debug build/source check | 진입점 표시/이동·버튼 UI |
| `SEND-001~008` | §11.2 | 2, 3, 10, 11 | ViewModel validation/build/source check | 선물 신청 journey·프로필 이미지 |
| `LIST-001~005` | §11.2 | 2, 4 | pagination/build | 선물함 상태별 화면 |
| `DETAIL-001~007` | §11.2 | 2, 5, 12 | status mapping/inquiry source contract/build | 보낸 상세/취소/TALK 문의 |
| `TRACK-001~007` | §11.2 | 2, 6, 10 | validation/build | 운송장 등록·신청 취소 |
| `RECV-001~008` | §11.2 | 2, 7, 10 | privacy/validation/build | 받은 상세/배송지 |
| `COMMON-001~007` | §11.2 | 2, 8, 11 | deeplink parsing/link source/build | 푸시/딥링크·약관 링크 |
## 17. Decision Log
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항·계약·Goal |
|---|---|---|---|---|---|
| 2026-10-06 | `DEC-001` | 확정 | `선물 보내기`는 `DM 보내기` 노출 조건인 `isDmAvailable`을 재사용한다. | 사용자 답변: “해당 조건과 동일하게 설정하면 된다.” | `GIFT-001`, `P1-T3` |
| 2026-10-06 | `DEC-002` | 확정 | 한국 접속은 기존 `countryCode` 판정, 한국어 설정은 `LanguageHeaderProvider.current == "ko"`를 사용한다. | 기존 코드 검색 결과 | `GIFT-001`, `P1-T3` |
| 2026-10-06 | `DEC-003` | 확정 | 크리에이터 상세 `선물 보내기`는 아이콘 없이 타인 채널에서만 표시한다. | 사용자 추가 요청 | `GIFT-001`, `P1-T5` |
| 2026-10-06 | `DEC-004` | 확정 | 약관 `(필수)` 링크는 sender/recipient 모두 같은 Notion URL을 사용한다. | 사용자 제공 URL | `COMMON-001`, `P3-T2`, `P7-T2` |
| 2026-10-06 | `DEC-005` | 확정 | 선물 API 계약은 사용자 프롬프트의 endpoint/request/response 필드를 구현 기준으로 삼는다. | 사용자 답변 A | `SEND-*`, `LIST-*`, `DETAIL-*`, `TRACK-*`, `RECV-*` |
| 2026-10-06 | `DEC-006` | 확정 | 후속 기능 문서는 기존 `docs/20261006_선물_UI_추가/` PRD/plan-task를 보강한다. | 문서 정책: 연속 작업은 기존 문서에 기록 | 전체 |
| 2026-10-06 | `DEC-007` | 확정 | 선물함에서 받은 선물의 배송지 미입력 상태는 별도 파란 배너가 아니라 각 item 카드에서 표시한다. | 사용자 정정 요청 | `LIST-004`, `P4-T3` |
| 2026-10-06 | `DEC-008` | 확정 | 운송장 등록 화면의 `신청 취소`는 입력 중단이 아니라 선물 신청 취소이며, 확인 이후 기존 취소 API를 호출한다. | 사용자 선택 | `TRACK-006`, `P6-T2`, `P6-T3` |
| 2026-10-08 | `DEC-009` | 확정 | 휴대폰 제출은 `010`으로 시작하는 ASCII 숫자 11자리만 허용하고, 운송장 번호는 ASCII 숫자 10~14자리만 제출한다. | 사용자 선택과 현행 국내 이동전화 번호체계 조사 | `SEND-007`, `TRACK-007`, `RECV-007`, Phase 10 |
| 2026-10-08 | `DEC-010` | 정정 | `DEC-003`의 아이콘 없는 버튼 결정을 정정해 `ic_send_gift`, 흰색 배경, 검은색 문구를 적용한다. | 사용자 요청 및 설계 승인 | `GIFT-004`, `P11-T3` |
| 2026-10-08 | `DEC-011` | 확정 | 경로형 선물 상세 딥링크는 `gift-detail/{applicationNo}`를 사용하고 `voiceon`, `voiceon-test`는 기존 scheme 지원만 재사용한다. | 사용자 제공 딥링크 예시 및 설계 승인 | `COMMON-006`, `P11-T1` |
| 2026-10-08 | `DEC-012` | 확정 | 파손 면책 `(필수)`에만 별도 파손 정책 URL을 사용하고 기존 약관 링크는 유지한다. | 사용자 제공 URL 및 설계 승인 | `COMMON-007`, `P11-T2` |
| 2026-10-08 | `DEC-013` | 확정 | Gift 모듈의 브랜드 문구는 이미 모두 `소다라이브`이므로 추가 문자열 변경 없이 충족 처리한다. | 코드 검색 결과와 사용자 선택 A | Phase 11 |
| 2026-10-08 | `DEC-014` | 확정 | 전달 불가 상세의 붉은 안내 문구는 서버 필드 추가 없이 앱 내 고정 문구를 Figma 문구로 교체한다. | 사용자 확인: “동일하게 앱 내에 심으면 된다.” | `RECV-008`, `P7-R1` |
| 2026-10-08 | `DEC-015` | 확정 | 선물 상세의 발신·수신 전달 불가와 취소 완료 `문의하기`는 고객센터 화면으로 이동하지 않고 고객센터 `TALK 문의`와 같은 공용 action을 호출한다. | 사용자 요청과 기존 고객센터 TALK action 확인 | `DETAIL-007`, `P12-T1` |
## 18. 변경 관리
### Phase 9 주소 검색 및 제한적 Figma 정렬 (2026-10-07 확정)
- 기존 수동 우편번호/기본주소 입력 정책을 공용 `SodaAddressInputView`로 대체한다. 상세주소만 편집하며 기존 validation/request mapping과 상세주소 선택 입력은 유지한다.
- 내부 WKWebView에 HTML 문자열을 로드하고 HTTPS Kakao `postcode.v2.js`의 `kakao.Postcode.embed()`를 사용한다. 선택 결과는 정확한 `data.zonecode`, `data.address`만 반환한다. hosted HTTPS 권장과 HTML 문자열 방식의 런타임 호환성 위험을 사용자가 수용했다.
- 외부 메시지의 handler 이름, 문자열 타입, 공백 여부를 검사한다. 유효한 선택 때만 우편번호/기본주소를 함께 갱신하고 닫는다. 취소/오류에서는 기존 값을 유지하며 loading/error/닫기와 handler 해제를 제공한다.
- Figma `2481:18957`, `2531:36290`의 제공 geometry를 기준으로 기존 `SodaSpacing`, `SodaRadius`, `appFont`를 재사용한다. 그룹 간격 20, label 간격 4, 주소 행 간격 8, padding/radius 14, label 14 medium, 값 16 regular, gray900 입력 배경이다. 검색 capsule은 intrinsic 폭과 최소 높이 44를 갖는다.
- sender helper는 `발송 확인 외의 용도로 사용하지 않으며, 전달이 완료되면 파기됩니다`, recipient helper는 `제출 후 수정할 수 없으니 입력한 정보를 다시 한번 확인해 주세요`다. 이름 helper는 sender `택배 발송 시 이름과 동일하게 적어주세요`, recipient `입력한 이름으로 선물이 전달돼요`, 전화 helper는 공통 `문제 발생 시 연락이 가능한 번호를 입력해 주세요`다.
- 카테고리는 16 regular, padding/radius 14로 줄이고 77pt 최소 높이만 제거한다. `sizeSection`, `sizeTitle`, 크기 선택/가격/문구/스타일은 현재 작업본 그대로 보존한다. 배너/요약/약관/하단 action 재설계, dependency, route, backend, retry/timeout은 제외한다.
- 코드·계약 검증과 실제 Kakao 검색/픽셀 fidelity 검증은 분리 기록한다. 실기기 증거 없이는 runtime 또는 픽셀 일치 완료를 주장하지 않는다.
### Phase 10 연락처·운송장 입력 제한 (2026-10-08 확정)
- 이름은 한글·영문 1~12자만 허용하며 UIKit marked text를 조합 중 binding에 재할당하지 않는다.
- 휴대폰은 편집 중 ASCII 숫자 최대 11자를 허용하고 제출 시 `010`으로 시작하는 정확히 11자리만 허용한다. 하이픈 자동 입력과 SMS 실재성 검증은 제외한다.
- 운송장 번호는 편집 중 ASCII 숫자 최대 14자를 허용하고 제출 시 10~14자리만 허용한다. 외부 택배 조회와 실재 운송장 검증은 제외한다.
- 기존 request, repository, API, 택배사 선택, mutation/navigation lifecycle과 선물 사이즈 선택은 변경하지 않는다.
### Phase 11 선물 딥링크·정책 링크·크리에이터 표시 보정 (2026-10-08 확정)
- 기존 딥링크 parser에 `gift-detail/{applicationNo}` path route만 추가하고 query형 `GIFT_DETAIL`과 scheme 판정은 유지한다.
- 크리에이터 채널의 기존 `creator.profileImageUrl`을 선물 보내기 route와 화면에 전달하며 별도 크리에이터 조회 API는 추가하지 않는다.
- 공용 약관 행은 호출부가 링크 URL을 지정할 수 있게 최소 확장하고 파손 면책 행에만 새 정책 URL을 전달한다.
- 선물 보내기 버튼은 기존 노출 조건과 action을 유지하면서 사용자 제공 `ic_send_gift` asset, 흰색 배경, 검은색 문구를 적용한다.
- Gift 모듈의 브랜드 문구는 이미 `소다라이브`로 확인되어 변경하지 않는다.
### Phase 12 선물 상세 TALK 문의 연결 (2026-10-08 확정)
- 고객센터 `TALK 문의`의 기존 Kakao URL 열기 action을 작은 공용 함수로 두고 고객센터와 선물 상세가 함께 사용한다.
- 발신 전달 불가, 수신 전달 불가, 취소 완료 상세의 세 `문의하기`만 공용 action으로 교체한다.
- 버튼 문구·노출 조건·스타일·레이아웃과 다른 선물 action은 변경하지 않는다.
- 새 route, webview, dependency, API 변경은 추가하지 않는다.
### Phase 7 후속 전달 불가 안내 문구 보정 (2026-10-08 확정)
- 전달 불가 사유 아래 붉은 안내 문구는 서버에서 받지 않고 앱 내 고정 문구로 유지한다.
- 발신·수신 상세 모두 Figma 기준 문구 `해당 선물은 보관기간(입고일 포함 최대 7일) 내에 반송 등 필요한 조치가 완료되지 않을 경우, 사전에 동의한 약관 및 폐기 등의 내용에 따라 폐기될 수 있습니다. 자세한 내용은 고객센터로 문의해 주세요.`를 사용한다.
- `delivery.undeliverableReason`은 기존처럼 상단 흰색 사유에만 사용하고 API schema는 변경하지 않는다.
- 요구사항 변경 시 Decision Log에 정정 행을 추가한다.
- 구현 범위가 바뀌면 이 PRD를 먼저 갱신하고 `plan-task.md`의 Phase/Task를 갱신한 뒤 코드를 수정한다.
- 기존 완료된 Phase 1 기록은 삭제하지 않는다.