From f5f58e44f83cc7a092bae790754708640f3e12db Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 8 Oct 2026 17:38:56 +0900 Subject: [PATCH] =?UTF-8?q?docs(gift):=20iOS=20=EC=84=A0=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EB=B3=B4=EA=B0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift-list-page-prompt.md | 26 +- .../ios-gift-feature-implementation-prompt.md | 596 ++++++++++++++++++ .../20260929_크리에이터_선물하기/plan-task.md | 24 + 3 files changed, 631 insertions(+), 15 deletions(-) create mode 100644 docs/20260929_크리에이터_선물하기/ios-gift-feature-implementation-prompt.md diff --git a/docs/20260929_크리에이터_선물하기/gift-list-page-prompt.md b/docs/20260929_크리에이터_선물하기/gift-list-page-prompt.md index f0c70456..77f0e0f9 100644 --- a/docs/20260929_크리에이터_선물하기/gift-list-page-prompt.md +++ b/docs/20260929_크리에이터_선물하기/gift-list-page-prompt.md @@ -177,6 +177,9 @@ Response `data` 예시: "counterpartMemberId": 200, "counterpartNickname": "달빛수집가", "counterpartProfileImageUrl": "https://example.com/profile.png", + "recipientAddressRegistered": false, + "recipientAddressDeadlineAt": null, + "canceledAt": null, "createdAt": "2026-09-18T03:00:00Z" } ], @@ -202,6 +205,9 @@ Response `data` 필드: | `items[].counterpartMemberId` | number | 상대방 회원 ID. `SENT`에서는 받는 크리에이터, `RECEIVED`에서는 보낸 팬 | | `items[].counterpartNickname` | string | 상대방 닉네임 | | `items[].counterpartProfileImageUrl` | string 또는 null | 상대방 프로필 이미지 URL. 없으면 기본 프로필 이미지 표시 | +| `items[].recipientAddressRegistered` | boolean | 크리에이터 배송지 필수 정보가 모두 입력되었는지 여부 | +| `items[].recipientAddressDeadlineAt` | string 또는 null | `direction=RECEIVED`이고 배송지가 아직 입력되지 않은 경우의 배송지 입력 기한. 배송지 입력 완료 후에는 null | +| `items[].canceledAt` | string 또는 null | 취소 상태의 취소일. 취소되지 않은 선물은 null | | `items[].createdAt` | string 또는 null | 신청일. UTC ISO 문자열 | | `page` | number | 현재 페이지 | | `size` | number | 페이지 크기 | @@ -220,21 +226,11 @@ Response `data` 필드: - `direction=SENT`에서는 받는 크리에이터 정보로 해석한다. - `direction=RECEIVED`에서는 보낸 팬 정보로 해석한다. -현재 API만으로 부족한 Figma 표시값: -- 취소일 `canceledAt` -- 배송지 입력 마감일 또는 남은 일수 - -추가 API 보강이 가능하면 리스트 item에 아래 필드를 추가해달라고 요청한다. -```json -{ - "canceledAt": "2026-09-18T03:00:00Z", - "recipientAddressDeadlineAt": "2026-09-25T03:00:00Z", - "sortAt": "2026-09-18T03:00:00Z" -} -``` -- `canceledAt`: 취소 상태의 취소일 표시용 -- `recipientAddressDeadlineAt`: 크리에이터 배송지 입력 안내 배너의 남은 일수 계산용 -- `sortAt`: 신청/취소 최신순 정렬 기준. 취소 건은 취소일, 그 외에는 신청일 +배송지/취소 표시 규칙: +- 배송지 입력 완료 여부는 `recipientAddressRegistered`로 판단한다. +- 배송지 입력 남은 기간은 `recipientAddressDeadlineAt` 기준으로 앱에서 계산한다. +- 배송지가 이미 입력되었으면 `recipientAddressDeadlineAt`은 null이다. +- 취소 상태의 날짜 라벨은 `canceledAt`이 있으면 `{canceledAt} 취소`로 표시하고, 그 외에는 `{createdAt} 신청`으로 표시한다. 로딩/에러/빈 상태: - 최초 로딩 중에는 기존 앱 리스트 스켈레톤 또는 loading 패턴을 사용한다. diff --git a/docs/20260929_크리에이터_선물하기/ios-gift-feature-implementation-prompt.md b/docs/20260929_크리에이터_선물하기/ios-gift-feature-implementation-prompt.md new file mode 100644 index 00000000..538a0812 --- /dev/null +++ b/docs/20260929_크리에이터_선물하기/ios-gift-feature-implementation-prompt.md @@ -0,0 +1,596 @@ +# iOS 선물하기 전체 기능 단일 구현 프롬프트 + +이 문서는 iOS 앱에서 크리에이터 선물하기 사용자 기능을 한 번에 구현할 때 사용하는 단일 프롬프트다. 기존 분할 문서의 사용자 화면 범위를 합치고, Figma UI 확인 결과와 신청 완료 후 상세 이동 정책을 반영했다. + +관리자 화면은 iOS 앱 범위가 아니므로 제외한다. + +## 참고 문서 + +- `docs/20260929_크리에이터_선물하기/prd.md` +- `docs/20260929_크리에이터_선물하기/client-api-summary.md` +- `docs/20260929_크리에이터_선물하기/gift-send-page-prompt.md` +- `docs/20260929_크리에이터_선물하기/gift-list-page-prompt.md` +- `docs/20260929_크리에이터_선물하기/gift-detail-tracking-page-prompt.md` +- `docs/20260929_크리에이터_선물하기/gift-received-detail-page-prompt.md` + +## Figma 확인 노드 + +- 선물 보내기: 빈 입력 `2481:18904`, 입력 완료/CTA 활성 `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` + +Figma 재확인 메모: + +- 모든 화면은 iPhone 13 기준 402px 폭, black/dark surface 기반이다. +- 상단 status bar와 nav/top 영역은 고정하고, 본문은 `scroll area`로 분리한다. +- 주요 섹션은 14pt 좌우 여백, 카드/입력/배너는 rounded dark surface, 섹션 사이 divider를 사용한다. +- 선물 보내기 화면은 안내 배너, 받는 크리에이터, 선물 사이즈, 카테고리, 보내는 사람, 약관 동의, 하단 single CTA 구조다. +- 선물함 리스트는 안내 배너와 104pt 높이 카드 리스트 구조다. 실제 구현에서는 Figma에 있는 `신청 내역`, `선물 내역` 섹션 타이틀을 표시하지 않는다. +- 보낸 상세는 상태 hero, 사서함/등록 안내, 선물 정보, 하단 action bar를 중심으로 한다. +- 받은 상세는 상태 hero와 문의 버튼, 선물 정보, 받는 주소 카드 구조를 상태별로 바꾼다. + +--- + +## iOS 구현 프롬프트 + +```text +iOS 앱의 크리에이터 선물하기 사용자 기능 전체를 Figma와 API 계약 기준으로 구현해줘. + +범위: +- 선물 보내기 신청 화면 +- 선물함 리스트 화면 +- 선물 상세 화면 +- 보낸 선물의 운송장 등록 화면 +- 보낸 선물의 신청 취소 확인/취소 완료 화면 +- 받은 선물의 배송지 입력 UI +- 받은 선물의 배송지 입력 완료/전달 완료/전달 불가 상세 UI +- 푸시/딥링크 진입 시 선물 상세 이동 + +범위 밖: +- 관리자 화면 +- 새 디자인 시스템 생성 +- 외부 택배 조회/운송장 실시간 검증 +- 약관 본문 제공 API 신규 구현 +- 선물 이미지, 첨부파일, 메시지 카드 +- 임의 URL 연결 + +공통 전제: +- iOS 기존 디자인 시스템, 컴포넌트, API client, 인증, 상태 관리, toast/dialog, navigation, loading/error 패턴을 재사용한다. +- Figma visual structure와 문구를 우선 따르되, 데이터 노출과 CTA 조건은 API 계약을 우선한다. +- 모든 API 응답은 공통 envelope를 사용하며 실제 payload는 `response.data` 또는 기존 iOS API client의 data unwrap 결과다. +- 모든 날짜는 UTC ISO 문자열이다. 앱 공통 formatter가 있으면 재사용하고, 상세 필드 날짜는 Figma처럼 `YYYY.MM.DD`로 표시한다. +- 개인정보 노출 경계를 반드시 지킨다. 로그인 회원이 알아야 하는 본인 정보만 표시하고, 상대방의 이름/휴대폰/주소는 표시하지 않는다. +- Figma 기준으로 상단 navigation은 고정하고, navigation 아래 본문만 스크롤한다. +- iOS safe area, keyboard avoidance, bottom action bar는 기존 앱 패턴을 따른다. +- 새 route/deep link가 필요하면 기존 navigation/deep link 등록 방식에 맞춘다. + +가장 중요한 이동 정책: +- 선물 보내기 신청 `POST /api/v2/gifts` 성공 시 완료 dialog에서 멈추지 말고, 성공 응답의 `applicationNo`로 선물 상세 페이지로 이동한다. +- 신청 완료 직후 이동 대상은 `GET /api/v2/gifts/{applicationNo}` 기반 상세 페이지다. +- 신청 성공 후 뒤로가기로 입력 폼에 다시 돌아와 중복 신청하지 않도록 기존 iOS navigation 정책에 맞춰 replace 또는 stack 정리를 적용한다. +- 선물함 리스트 item 탭, 푸시 딥링크, 신청 완료 후 이동은 모두 같은 상세 화면으로 들어가고, 상세 API의 `direction`, `trackingRequired`, `recipientAddressRequired`, `status`로 UI를 분기한다. + +보낸 사람/받는 사람 상세 차이: +- 같은 `GET /api/v2/gifts/{applicationNo}` 상세 API를 사용하지만 `direction`에 따라 상세 페이지를 다르게 표시한다. +- `direction=SENT`는 보낸 팬 관점이다. + - 받는 크리에이터 닉네임은 `giftInfo.recipientCreatorNickname`으로 표시한다. + - 팬 본인이 신청 시 입력한 발신자 정보 `senderInfo`를 표시할 수 있다. + - 관리자 전역 사서함 `mailbox`가 있으면 `보내실 주소` 또는 `받는 사서함` 섹션에 표시한다. + - 크리에이터가 입력한 배송지 `recipientAddress`는 표시하지 않는다. + - `trackingRequired=true`이면 운송장 등록 CTA를 표시한다. + - `status=RECEIVED`이면 신청 취소 action을 제공할 수 있다. +- `direction=RECEIVED`는 받는 크리에이터 관점이다. + - 보낸 팬 닉네임은 `giftInfo.senderNickname`으로 표시한다. + - 팬이 신청 시 입력한 이름/휴대폰/주소 `senderInfo`는 표시하지 않는다. + - `mailbox`, `giftInfo.applicationNo`, `giftInfo.paidCan`, `giftInfo.tracking`은 표시하지 않는다. + - 크리에이터 본인이 입력한 배송지 `recipientAddress`가 있을 때만 받는 주소 섹션을 표시한다. + - `recipientAddressRequired=true`이면 배송지 입력 UI를 표시한다. + +1. 선물 보내기 화면 + +Figma: +- 빈 입력 상태: `2481:18904` +- 입력 완료/CTA 활성 상태: `2481:18957` + +진입 전제: +- 크리에이터 닉네임과 `memberId`를 받아 이 화면으로 이동하는 기존 흐름은 이미 있다. +- 이 작업에서 새 진입 흐름을 만들지 않는다. +- 전달받은 크리에이터 닉네임은 `받는 크리에이터` 영역에 표시한다. +- 전달받은 `memberId`는 `POST /api/v2/gifts`의 `recipientMemberId`로 보낸다. +- 둘 중 하나라도 없으면 기존 앱의 오류 처리 또는 안전한 뒤로가기 패턴을 따른다. + +화면 구조: +- 상단 타이틀: `선물 보내기` +- 안내 배너 + - 제목: `선물 보내기 베타 서비스 안내` + - 본문: `베타 기간 동안은 일부 기능만 제공됩니다. 크리에이터에게 마음을 잘 전할 수 있도록 더 넓어진 선물 보내기로 곧 다시 만나요!` +- 받는 크리에이터: Figma 프로필 row 형태로 전달받은 닉네임 표시 +- 선물 사이즈: `GET /api/v2/gifts/form-options`의 `sizes` 바인딩 +- 카테고리: 같은 응답의 `categories` 바인딩 +- 보내는 사람 입력: 이름, 휴대폰 번호, 우편번호, 주소, 상세 주소 +- 이용 약관 동의: 발송 규정 요약 박스와 체크박스 2개 +- 하단 CTA: `{salePriceCan}캔으로 선물 보내기` + +폼 옵션 조회: +- 화면 진입 시 `GET /api/v2/gifts/form-options` 호출 +- `sizes` 기본 선택은 서버가 내려준 첫 번째 항목 +- 가격 표시는 `salePriceCan`을 사용하고 신청 금액도 `salePriceCan`을 사용 +- `basePriceCan != salePriceCan`이면 기존 앱 할인/정가 표시 패턴이 있을 때만 정가를 함께 표시 +- 사이즈 설명은 API에 없으므로 앱 로컬 매핑 사용 + - `SMALL`: `세 변의 합 100cm 이하 · 5kg 이하` + - `MEDIUM`: `세 변의 합 120cm 이하 · 15kg 이하` + - `LARGE`: `세 변의 합 160cm 이하 · 20kg 이하` +- 카테고리 select는 기존 iOS picker/bottom sheet/dialog 패턴 중 앱에서 이미 쓰는 방식을 사용 +- `requiresDamageWaiver=true` 카테고리 선택 시에만 파손 면책 동의 row 표시 +- `requiresDamageWaiver=false` 선택 시 파손 면책 동의 row를 숨기고 `damageWaiverAgreed=false`로 초기화 + +입력 필드 매핑: +- 이름 input -> `senderName` +- 휴대폰 번호 input -> `senderPhoneNumber` +- 우편번호 input -> `senderZipCode` +- 주소 input -> `senderAddress` +- 상세 주소 input -> `senderAddressDetail` +- 우편번호 검색은 기존 주소 검색 기능을 재사용한다. + +동의 필드: +- `소다라이브 크리에이터 상품 전달 이용약관에 동의합니다. (필수)` -> `senderTermsAgreed` +- `상품 전달을 위한 개인정보 수집·이용에 동의합니다. (필수)` -> `senderPrivacyAgreed` +- `파손 및 분실 면책 사항에 동의합니다. (필수)` -> `damageWaiverAgreed`, 필요한 카테고리에서만 표시/필수 +- 각 약관 문구의 `(필수)` 부분은 터치 가능한 링크로 처리한다. +- `(필수)` 터치 시 약관 Notion 웹페이지로 이동한다. +- sender 약관 2개와 recipient 약관 2개는 모두 같은 Notion 페이지로 이동하면 된다. +- Notion 페이지 URL은 실제 구현 시 입력받도록 처리하고, 이 프롬프트에서 임의 URL을 하드코딩하지 않는다. + +CTA 활성 조건: +- `memberId` 있음 +- 사이즈 선택됨 +- 카테고리 선택됨 +- 이름, 휴대폰 번호, 우편번호, 주소 입력됨 +- 이용약관 동의 true +- 개인정보 동의 true +- 선택 카테고리의 `requiresDamageWaiver=true`이면 파손 면책 동의 true +- submit 중에는 중복 탭 방지와 loading 표시 +- validation 실패 시 `POST /api/v2/gifts`를 호출하지 않는다. + +선물 신청 API: +- Method: `POST` +- URL: `/api/v2/gifts` +- Request: +```json +{ + "recipientMemberId": 100, + "senderName": "김소다", + "senderPhoneNumber": "01000000000", + "senderZipCode": "12345", + "senderAddress": "서울시 강남구 ...", + "senderAddressDetail": "123동 456호", + "sizeCode": "SMALL", + "categoryId": 1, + "senderTermsAgreed": true, + "senderPrivacyAgreed": true, + "damageWaiverAgreed": true +} +``` +- Response data: +```json +{ + "applicationNo": "A-1002609300001", + "status": "RECEIVED", + "statusName": "접수 완료", + "priceCan": 80, + "trackingDeadlineAt": "2026-10-02T03:00:00Z" +} +``` +- 성공 시 `applicationNo`로 선물 상세 페이지로 이동한다. +- 실패 시 서버 메시지를 기존 앱 오류 노출 방식으로 표시하고 입력값은 유지한다. + +2. 선물함 리스트 화면 + +Figma: +- 보낸 내역 있음: `2481:19382` +- 보낸 내역 없음: `2520:35587` +- 받은 내역 없음: `2531:36283` +- 받은 내역 있음: `2531:36194` +- 받은 상태별 카드: `2531:36237` + +공통 요구사항: +- 상단 타이틀: `선물함` +- 상단 뒤로가기 navigation은 고정 +- navigation 아래 콘텐츠만 스크롤 +- Figma에 보이는 `신청 내역`, `선물 내역` 섹션 타이틀은 표시하지 않는다. +- 리스트는 서버 응답 순서를 우선 사용한다. 현재 API만으로 취소일 기준 재정렬을 임의 구현하지 않는다. +- item 탭 시 `applicationNo`를 전달해 선물 상세 페이지로 이동한다. + +API: +- Method: `GET` +- URL: `/api/v2/gifts?type=ALL&page=0&size=20` +- 이 화면에서는 `SENT`, `RECEIVED`를 따로 호출하지 않고 `type=ALL`을 사용한다. +- `items[].direction`으로 보낸/받은 카드 UI를 결정한다. +- `hasNext=true`이면 스크롤 하단에서 다음 page를 추가 조회한다. +- 기존 앱에 pull-to-refresh가 있으면 page 0부터 다시 조회한다. + +Response data 예시: +```json +{ + "totalCount": 2, + "items": [ + { + "applicationNo": "A-1002609300001", + "direction": "SENT", + "status": "RECEIVED", + "statusName": "접수 완료", + "priceCan": 80, + "categoryName": "아크릴/스탠드", + "sizeName": "소형", + "counterpartMemberId": 200, + "counterpartNickname": "달빛수집가", + "counterpartProfileImageUrl": "https://example.com/profile.png", + "recipientAddressRegistered": false, + "recipientAddressDeadlineAt": null, + "canceledAt": null, + "createdAt": "2026-09-18T03:00:00Z" + } + ], + "page": 0, + "size": 20, + "hasNext": false +} +``` + +보낸 선물 카드 (`direction=SENT`): +- 상대방은 선물을 받는 크리에이터다. +- 상단 안내 배너 표시 + - 제목: `사서함을 통해 안전하게 전달됩니다` + - 본문: `크리에이터 및 팬의 주소는 공개되지 않습니다. 선물은 소다라이브 사서함에 도착한 뒤 크리에이터에게 전달됩니다.` +- 카드에는 `statusName`, 날짜 라벨, 크리에이터 프로필/닉네임, `sizeName · categoryName`, chevron 표시 +- 날짜 라벨은 기본 `{createdAt} 신청`; 취소일 필드가 없으면 임의 취소일 계산 금지 +- `status=CANCELED`이고 `canceledAt`이 있으면 날짜 라벨은 `{canceledAt} 취소`로 표시한다. +- 보낸 선물 empty는 안내 배너만 표시하고 별도 empty title/body를 추가하지 않는다. + +받은 선물 카드 (`direction=RECEIVED`): +- 상대방은 선물을 보낸 팬이다. +- 받은 선물 empty + - 제목: `전달 예정인 선물이 없어요` + - 본문: `베타 기간에는 팬이 선물을 보내는 기능만 제공돼요. 더 다양해진 선물 기능으로 곧 다시 만나요!` + - 버튼: `의견 남기기`, 기존 문의/피드백 이동 패턴 사용 +- 받은 선물 있음 상단 안내 배너 + - 제목: `배송지 입력 기간 안내` + - 본문: `알림을 받은 날부터 7일 이내에 배송지를 입력해 주세요. 기한 내 입력하지 않으면 선물이 반송됩니다.` +- 배송지 입력이 필요한 선물이 있으면 리스트 상단에 파란 안내 배너 표시 + - 제목: `팬이 보낸 선물이 있어요!` + - 본문: `선물이 늦지 않게 전달될 수 있도록 배송지를 입력해 주세요.` + - 보조 문구는 `recipientAddressRegistered=false`이고 `recipientAddressDeadlineAt`이 있으면 남은 일수로 표시한다. + - 배송지가 이미 입력되어 `recipientAddressRegistered=true`이면 `recipientAddressDeadlineAt`은 null로 온다. + - 배너 탭 시 배송지 입력이 필요한 첫 번째 선물 상세로 이동 +- 카드에는 팬 프로필/닉네임, `{createdAt} 신청`, 상태 메인/서브 문구, chevron 표시 +- 하단 `문의하기` 버튼은 기존 앱 정책에 맞춰 고정 또는 스크롤 내부 버튼으로 둔다. + +상태 표시 매핑: +- 보낸 선물 카드 + - `RECEIVED`: `운송장 등록 필요` + - `TRACKING_REGISTERED`: `발송 확인` + - `ARRIVED_AT_MAILBOX`: `사서함 도착` + - `INSPECTION_COMPLETED`: `선물 검수 완료` + - `DELIVERED`: `전달 완료` + - `UNDELIVERABLE`: `전달 불가` + - `CANCELED`: `신청 취소` +- 받은 선물 카드 + - `TRACKING_REGISTERED`: main `배송지 입력 필요`, sub `전달 예정` + - `ARRIVED_AT_MAILBOX`: main `선물 검수`, sub `선물 확인 중` + - `INSPECTION_COMPLETED`: main `선물 검수 완료`, sub `배송 중` + - `DELIVERED`: main `전달 완료`, sub 없음 + - `UNDELIVERABLE`: main `전달 불가`, sub 없음 + +3. 공통 선물 상세 화면 + +API: +- Method: `GET` +- URL: `/api/v2/gifts/{applicationNo}` +- 리스트 item 탭, 신청 완료 후 이동, 푸시 딥링크 진입 모두 이 API로 상세를 조회한다. + +Response data 핵심 필드: +```json +{ + "applicationNo": "A-1002609300001", + "direction": "SENT", + "status": "RECEIVED", + "statusName": "접수 완료", + "giftInfo": { + "recipientCreatorNickname": "달빛수집가", + "senderNickname": null, + "sizeName": "소형", + "categoryName": "잡화", + "applicationNo": "A-1002609300001", + "paidCan": 100, + "tracking": null, + "shippingRequestedAt": null + }, + "senderInfo": { + "name": "김소다", + "phoneNumber": "01012345678", + "address": "(04030) 서울특별시 마포구 양화로 000, 3층" + }, + "recipientAddress": null, + "mailbox": { + "name": "소다라이브 사서함", + "address": "(06174) 서울특별시 강남구 테헤란로108길 8, 유민빌딩 4층", + "phoneNumber": "02-2055-1477" + }, + "trackingRequired": true, + "recipientAddressRequired": false, + "recipientAddressDeadlineAt": null, + "delivery": { + "canceledAt": null, + "undeliverableAt": null, + "undeliverableReason": null + }, + "statusTimeline": [] +} +``` + +진입 처리: +- 화면 진입 시 상세 API 호출 +- 로딩 중에는 기존 상세 로딩/스켈레톤 패턴 사용 +- 실패 시 기존 toast/dialog와 재시도 패턴 사용 +- `direction`이 현재 화면 분기와 맞지 않아도 별도 화면을 만들지 말고 같은 상세 화면 안에서 `direction` 기준으로 렌더링한다. + +진행 상태 progress: +- Figma의 진행 단계가 4단계처럼 보여도 4단계로 하드코딩하지 않는다. +- API `statusTimeline`의 정상 진행 5개 상태를 이 순서로 표시한다. + 1. `RECEIVED`: `신청 접수` + 2. `TRACKING_REGISTERED`: `발송 확인` + 3. `ARRIVED_AT_MAILBOX`: `사서함 도착` + 4. `INSPECTION_COMPLETED`: `검수 완료` + 5. `DELIVERED`: `전달 완료` +- `occurredAt`이 있으면 완료 색상과 `MM.DD` 표시 +- `occurredAt=null`이면 비활성 색상, 날짜 미표시 +- `UNDELIVERABLE`, `CANCELED`는 progress step으로 추가하지 않는다. + +4. 보낸 선물 상세와 운송장 등록 + +Figma: +- 상세 운송장 등록 전 `2481:19011` +- 상세 운송장 등록 후 `2481:19152` +- 상세 전달 완료 `2481:19320` +- 전달 불가 `2481:19456` +- 취소 다이얼로그 `2481:19500` +- 취소 완료 `2481:19521` +- 운송장 등록 입력 전 `2481:19066` +- 운송장 등록 입력 완료 `2481:19109` + +적용 조건: +- `direction=SENT` + +상단 타이틀: +- 일반 상세: `신청 상세` +- 취소 완료 페이지: `취소 완료` +- 운송장 등록: `운송장 등록` + +보낸 상세 hero 문구: +- `RECEIVED`: title `선물 신청 완료!`, body `선물이 정상적으로 접수되었어요!` +- `TRACKING_REGISTERED`: chip `발송 확인`, title `사서함으로 이동 중이에요` +- `ARRIVED_AT_MAILBOX`: chip `사서함 도착`, title `선물이 사서함에 도착했어요` +- `INSPECTION_COMPLETED`: chip `검수 완료`, title `선물 검수를 완료했어요` +- `DELIVERED`: chip `전달 완료`, title `크리에이터에게 선물을 전달했어요` +- `UNDELIVERABLE`: chip `전달 불가`, title `전달할 수 없는 품목입니다`, body `보내주신 선물은 SODALIVE 선물 정책에 따라 크리에이터에게 전달할 수 없는 품목으로 확인되었습니다.` +- `CANCELED`: 일반 상세 대신 취소 완료 UI 표시 + +보낸 상세 섹션: +- 선물 정보는 항상 표시 + - 크리에이터: `giftInfo.recipientCreatorNickname` + - 사이즈: `giftInfo.sizeName` + - 카테고리: `giftInfo.categoryName` + - 신청번호: `applicationNo` 또는 `giftInfo.applicationNo` + - 캔: `giftInfo.paidCan`이 null이 아닐 때 표시 + - 운송장: `giftInfo.tracking`이 null이 아닐 때 표시 + - 검수일/전달 완료일은 `statusTimeline`에서 찾는다. +- `senderInfo`가 있으면 보내는 사람 섹션 표시 +- `mailbox`가 있으면 보내실 주소/받는 사서함 섹션 표시 +- `delivery.undeliverableReason`이 있으면 사유 섹션 표시 +- `recipientAddress`는 보낸 팬 상세에 표시하지 않는다. + +보낸 상세 action: +- `trackingRequired=true`이면 주요 CTA `운송장 작성하기` 표시, 운송장 등록 화면으로 이동 +- `direction=SENT && status=RECEIVED`이면 보조 action `신청 취소` 표시 가능 +- `status=DELIVERED && direction=SENT`이면 기존 리뷰 작성 화면/플로우가 있으면 `리뷰 남기기` 연결 +- `문의하기`, `선물 정책 확인`은 기존 이동 경로가 있으면 연결하고, 없으면 숨기거나 기존 앱 정책에 맞춰 처리 + +신청 취소: +- `신청 취소` 탭 시 즉시 API 호출 금지, 기존 `v2 dialog` 확인 팝업 표시 +- 제목: `신청 취소` +- 본문: + - `{크리에이터}에게 보내는 선물을 취소할까요?` + - `선물 보내기 규정에 따라 선물 취소 및 사용한 캔이 모두 환불됩니다.` +- 왼쪽 action: `나가기`, dialog만 닫음 +- 오른쪽 destructive action: `취소하기`, `POST /api/v2/gifts/{applicationNo}/cancel` 호출 +- 실패 시 dialog를 닫지 말고 기존 에러 패턴 표시 +- 성공 시 취소 완료 페이지로 replace navigation + +취소 완료 페이지: +- 상세 API 응답이 `status=CANCELED`인 경우에도 표시 +- hero title: `선물 신청이 취소되었어요` +- hero body: `{크리에이터}에게 보내는 선물 신청({applicationNo})이 취소되었습니다. 취소한 신청은 되돌릴 수 없으며, 다시 보내려면 새로 신청해 주세요.` +- 취소 상세: 크리에이터, 사이즈, 카테고리, 신청번호, 신청일, 취소일, 환불 캔 +- 뒤로가기는 취소 전 상세/등록 화면으로 돌아가지 않게 한다. + +운송장 등록 화면: +- 상세에서 `trackingRequired=true`일 때만 진입 +- 진입 시 상세 API를 다시 조회 +- 조회 결과 `trackingRequired=false`이면 기존 오류 처리 후 상세로 복귀 +- 신청 건 요약 카드: `applicationNo`, 크리에이터 닉네임, `sizeName · categoryName · 신청일` +- 보내실 주소: `mailbox` 사용. `mailbox=null`이면 등록을 막고 `받을 주소가 등록되어 있지 않습니다.` 오류 표시 +- 운송장 정보: 택배사 select, 운송장 번호 input +- 등록 안내: Figma 안내 문구 유지 +- 하단 action bar: 왼쪽 `신청 취소`, 오른쪽 `등록 완료` + +택배사 로컬 상수: +```swift +let giftCourierCompanyNames = [ + "CJ대한통운", + "우체국택배", + "GS25 편의점택배", + "CU 편의점택배", + "한진택배", + "롯데택배", + "로젠택배", + "경동택배", + "대신택배", + "일양로지스", + "천일택배", + "합동택배", + "건영택배", + "농협택배" +] +``` +- 이미 앱 공통 택배사 상수가 있으면 그것을 우선 사용 +- API에는 선택한 한글 표시명을 그대로 `courierCompanyName`으로 보낸다. + +운송장 등록 CTA 활성 조건: +- 택배사 선택됨 +- 운송장 번호가 공백이 아님 +- submit 중 중복 탭 방지 + +운송장 등록 API: +- Method: `POST` +- URL: `/api/v2/gifts/{applicationNo}/tracking` +- Request: +```json +{ + "courierCompanyName": "한진택배", + "trackingNumber": "123456789012" +} +``` +- 성공 시 성공 toast 표시 후 상세 페이지로 돌아가거나 replace navigation으로 상세를 다시 열고, 상세 API를 재조회해 `TRACKING_REGISTERED` 상태를 표시한다. + +5. 받은 선물 상세와 배송지 입력 + +Figma: +- 배송지 입력 전 `2531:36290` +- 배송지 입력 완료 `2531:36399` +- 전달 완료 `2531:36330` +- 전달 불가 `2531:36422` + +적용 조건: +- `direction=RECEIVED` + +받은 상세 개인정보 규칙: +- `senderInfo`, `mailbox`, `giftInfo.applicationNo`, `giftInfo.paidCan`, `giftInfo.tracking`은 표시하지 않는다. +- `recipientAddress=null`이면 받는 주소 섹션 전체를 숨긴다. 빈 섹션, skeleton, `-` 값도 표시하지 않는다. +- 전달 불가 화면에서는 `recipientAddress`가 응답에 있어도 받는 주소 섹션을 표시하지 않는다. + +배송지 입력 전 UI: +- 적용 조건: `direction=RECEIVED && recipientAddressRequired=true` +- 상단 타이틀: `선물 받기` +- 개인정보 이용 안내 배너 + - 제목: `개인정보 이용 안내` + - 본문: `크리에이터 및 팬의 주소는 공개되지 않습니다. 입력한 배송지는 선물 전달 및 필요한 사고 처리에서 사용되며, 전달 완료 후 파기됩니다.` +- 전달 예정 선물 섹션 + - 발송인: `giftInfo.senderNickname` + - 사이즈: `giftInfo.sizeName` + - 카테고리: `giftInfo.categoryName` +- 배송지 입력 섹션 + - 이름 `recipientName` + - 휴대폰 번호 `recipientPhoneNumber` + - 우편번호 `recipientZipCode` + - 주소 `recipientAddress` + - 상세 주소 `recipientAddressDetail` +- 우편번호 검색은 기존 앱 주소 검색/Kakao 우편번호 래퍼를 재사용 +- 이용 약관 동의 + - `소다라이브 크리에이터 상품 전달 이용약관 및 전달 가능 제한 품목을 확인하고 동의합니다.(필수)` -> `recipientTermsAgreed` + - `상품 전달을 위해 수령인 성명·연락처·주소 등 필요한 개인정보를 일시적으로 수집·이용하는 것에 동의합니다.(필수)` -> `recipientPrivacyAgreed` + - 각 약관 문구의 `(필수)` 부분은 터치 가능한 링크로 처리한다. + - `(필수)` 터치 시 약관 Notion 웹페이지로 이동한다. + - sender 약관 2개와 recipient 약관 2개는 모두 같은 Notion 페이지로 이동하면 된다. + - Notion 페이지 URL은 실제 구현 시 입력받도록 처리하고, 이 프롬프트에서 임의 URL을 하드코딩하지 않는다. +- 하단 CTA: `선물 받기` +- CTA 활성 조건: 이름, 휴대폰, 우편번호, 주소 입력됨 + 약관 2개 동의됨 + +배송지 입력 API: +- Method: `POST` +- URL: `/api/v2/gifts/{applicationNo}/recipient-address` +- Request: +```json +{ + "recipientName": "크리에이터이름", + "recipientPhoneNumber": "01012345678", + "recipientZipCode": "04030", + "recipientAddress": "서울특별시 마포구 양화로 000", + "recipientAddressDetail": "3층", + "recipientTermsAgreed": true, + "recipientPrivacyAgreed": true +} +``` +- 성공 시 성공 toast 표시 후 상세 API를 다시 조회하거나 replace navigation으로 상세를 다시 열어 배송지 입력 완료 UI를 표시한다. +- 실패 시 기존 form error/toast/dialog 패턴을 따르고 입력값은 유지한다. + +배송지 입력 완료 상세: +- 적용 조건: `direction=RECEIVED && recipientAddressRequired=false && status`가 `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED` 중 하나이며 `recipientAddress`가 있음 +- 상단 타이틀: `선물 상세` +- hero title: `배송지 입력 완료!` +- hero body: `빠른 시일 내로 선물을 전달 드릴게요.` +- `문의하기` 버튼은 기존 카카오 채널 문의로 연결 +- 선물 정보: 발송인, 사이즈, 카테고리, 배송 신청일 +- 받는 주소: `recipientAddress.name`, `recipientAddress.phoneNumber`, `recipientAddress.address` + +전달 완료 상세: +- 적용 조건: `direction=RECEIVED && status=DELIVERED` +- hero title: `선물 전달 완료!` +- hero body: `팬이 보낸 선물이 도착했어요. 선물이 도착하지 않은 경우, 문의해 주세요.` +- 선물 정보: 발송인, 사이즈, 카테고리, 배송 신청일, 배송 완료일 +- 받는 주소가 있으면 표시하고 보조 문구 `배송 완료 3일 후 해당 정보는 사라져요` 표시 + +전달 불가 상세: +- 적용 조건: `direction=RECEIVED && status=UNDELIVERABLE` +- chip: `전달 불가` +- hero title: `전달할 수 없는 품목입니다` +- hero body: `해당 선물은 SODALIVE 선물 정책에 따라 크리에이터에게 전달할 수 없는 품목으로 확인되었습니다.` +- `문의하기`: 기존 카카오 채널 문의로 연결 +- `선물 정책 확인`: 실제 외부 URL을 임의 연결하지 않는다. 기존 앱 정책상 no-op 금지이면 `준비 중입니다.` toast만 표시 +- 사유 섹션 + - 제목: `delivery.undeliverableReason`이 있으면 그 값, 없으면 `전달 불가` + - 안내 문구: `해당 선물은 정책에 따라 폐기되며 반송되지 않습니다.` +- 배송지 입력 UI는 표시하지 않는다. + +6. 푸시/딥링크 + +- 선물 푸시 딥링크는 `deepLinkValue=GIFT_DETAIL`, `deepLinkId=applicationNo`를 사용한다. +- 딥링크 진입 시 바로 특정 보낸/받은 화면을 결정하지 말고, `GET /api/v2/gifts/{applicationNo}`를 호출한 뒤 응답의 `direction`, `trackingRequired`, `recipientAddressRequired`, `status`로 상세 UI를 결정한다. +- 권한 오류 또는 미존재 오류는 기존 앱 딥링크 오류 처리 패턴을 따른다. + +7. API 요약 + +- `GET /api/v2/gifts/form-options`: 선물 신청 폼 옵션 조회 +- `POST /api/v2/gifts`: 선물 신청 접수 +- `GET /api/v2/gifts?type=ALL&page=0&size=20`: 선물함 리스트 조회 +- `GET /api/v2/gifts/{applicationNo}`: 선물 상세 조회 +- `POST /api/v2/gifts/{applicationNo}/cancel`: 보낸 선물 신청 취소 +- `POST /api/v2/gifts/{applicationNo}/tracking`: 보낸 선물 운송장 등록 +- `POST /api/v2/gifts/{applicationNo}/recipient-address`: 받은 선물 배송지 입력 +- `POST /api/v2/gifts/{applicationNo}/review`: 전달 완료 후 팬 리뷰 작성, 기존 리뷰 플로우가 있을 때 연결 + +8. 검증 체크리스트 + +- 선물 보내기 진입 시 전달받은 크리에이터 닉네임과 `memberId`가 각각 화면/API에 연결되는지 확인한다. +- 화면 진입 시 `GET /api/v2/gifts/form-options`가 호출되는지 확인한다. +- 필수 입력/동의가 누락되면 신청 API를 호출하지 않는지 확인한다. +- `requiresDamageWaiver=true` 카테고리에서만 파손 면책 동의가 표시되고 필수인지 확인한다. +- 선물 신청 성공 후 `applicationNo`로 상세 페이지로 이동하는지 확인한다. +- 신청 완료 후 뒤로가기로 입력 폼에 돌아와 중복 신청하지 않는지 확인한다. +- 선물함 리스트는 `type=ALL`로 한 번 조회하고 `direction`으로 카드 UI를 나누는지 확인한다. +- Figma의 `신청 내역`, `선물 내역` 섹션 타이틀이 실제 화면에 노출되지 않는지 확인한다. +- 리스트 item 탭, 푸시 딥링크, 신청 완료 이동이 모두 같은 상세 조회 플로우를 사용하는지 확인한다. +- `direction=SENT` 상세에서 팬 본인 발신자 정보와 사서함만 표시하고, 크리에이터 배송지는 표시하지 않는지 확인한다. +- `direction=RECEIVED` 상세에서 팬 닉네임만 표시하고 팬의 이름/휴대폰/주소는 표시하지 않는지 확인한다. +- `direction=RECEIVED` 상세에서 `mailbox`, `giftInfo.paidCan`, `giftInfo.tracking`이 표시되지 않는지 확인한다. +- 상세 progress가 `statusTimeline` 5단계를 사용하고 Figma처럼 4단계로 하드코딩되지 않았는지 확인한다. +- `trackingRequired=true`일 때만 운송장 등록 CTA가 표시되는지 확인한다. +- 운송장 등록 성공 후 상세를 재조회해 `TRACKING_REGISTERED` 상태를 표시하는지 확인한다. +- 신청 취소는 확인 dialog 이후에만 API를 호출하고, 성공 시 취소 완료 페이지로 replace 되는지 확인한다. +- `recipientAddressRequired=true`일 때 받은 선물 배송지 입력 UI가 표시되는지 확인한다. +- 배송지 입력 성공 후 상세를 재조회해 배송지 입력 완료 UI가 표시되는지 확인한다. +- `recipientAddress=null`이면 받는 주소 섹션을 통째로 숨기는지 확인한다. +- `status=UNDELIVERABLE`이면 전달 불가 UI와 사유/폐기 안내를 표시하고 배송지 입력 UI를 숨기는지 확인한다. +- 모든 날짜는 UTC ISO 문자열을 앱 표시 형식으로 변환하는지 확인한다. +- API 실패 시 기존 앱 오류 표시 방식을 사용하고 입력값/스크롤 상태를 불필요하게 초기화하지 않는지 확인한다. +``` diff --git a/docs/20260929_크리에이터_선물하기/plan-task.md b/docs/20260929_크리에이터_선물하기/plan-task.md index 1aa30fbf..0800680d 100644 --- a/docs/20260929_크리에이터_선물하기/plan-task.md +++ b/docs/20260929_크리에이터_선물하기/plan-task.md @@ -1406,6 +1406,30 @@ P7은 FCM 확장 후 전체 상태 전이를 연결하고 최종 회귀로 종 - [x] 공식 `onShutdownForceStop(true)` 옵션으로 native Redis 프로세스 종료가 무기한 `waitFor()`에 걸리지 않도록 변경했다. - [x] 후속 요청에 따라 전체 `./gradlew --no-daemon test`를 실행해 정상 종료를 확인했다. +## Phase 13: 선물함 리스트 표시 보강 + +**Phase 결과:** 선물함 리스트 item이 배송지 입력 여부, 배송지 입력 기한, 취소일을 내려줘 iOS가 리스트에서 배송지 CTA와 취소일을 표시할 수 있다. + +**선행조건:** Phase 12 완료, 사용자 확정 요구사항 반영. + +### Task 13.1 문서 계약 갱신 + +**Goal 실행 `P13-T1`:** PRD, 클라이언트 요약, 모바일 프롬프트에 리스트 item 추가 필드 3개를 반영한다. + +- **Files:** Modify `prd.md`, `client-api-summary.md`, `gift-list-page-prompt.md`, `ios-gift-feature-implementation-prompt.md`, `plan-task.md`. +- [x] **RED:** 기존 리스트 응답 계약에 `recipientAddressRegistered`, `canceledAt`이 없고 배송지 deadline 규칙이 불명확함을 확인한다. +- [x] **GREEN:** `recipientAddressRegistered`, `recipientAddressDeadlineAt`, `canceledAt`을 문서화한다. 배송지 입력 완료 후 `recipientAddressDeadlineAt=null`, 취소일은 `createdAt` 대입이 아니라 `canceledAt` 별도 필드로 내려주는 정책을 명시한다. + +### Task 13.2 리스트 응답 필드 구현 + +**Goal 실행 `P13-T2`:** 사용자 선물함 리스트 응답에 추가 필드 3개를 내려준다. + +- **Files:** Modify `GiftQueryService.kt`, `GiftResponse.kt`. +- **Tests:** Modify `GiftQueryServiceTest.kt`, `GiftControllerTest.kt`. +- [x] **RED:** 배송지 미입력/입력 완료/취소 상태의 리스트 응답 필드 실패 테스트를 작성하고 실패를 확인했다. +- [x] **GREEN:** 기존 `GiftDelivery` 정보를 재사용해 `recipientAddressRegistered`, `recipientAddressDeadlineAt`, `canceledAt`을 매핑했다. +- [x] **검증:** focused 테스트와 `ktlintCheck`, `git diff --check`를 실행했다. + ## Decision Log | 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |