From f2f4d4d59e0669c0cb95ce58ab8f9dc1f819bf02 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 8 Oct 2026 12:33:15 +0900 Subject: [PATCH] =?UTF-8?q?docs(gift):=20=EC=84=A0=EB=AC=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B3=84=ED=9A=8D=EC=9D=84=20=EA=B0=B1=EC=8B=A0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/20261006_선물_UI_추가/plan-task.md | 1101 ++++++++++++++++++++--- docs/20261006_선물_UI_추가/prd.md | 354 ++++++-- 2 files changed, 1254 insertions(+), 201 deletions(-) diff --git a/docs/20261006_선물_UI_추가/plan-task.md b/docs/20261006_선물_UI_추가/plan-task.md index ff58deb3..57be5ab5 100644 --- a/docs/20261006_선물_UI_추가/plan-task.md +++ b/docs/20261006_선물_UI_추가/plan-task.md @@ -1,211 +1,972 @@ -# 선물 UI 추가 구현 계획 +# 크리에이터 선물하기 사용자 기능 구현 계획 | 문서 항목 | 내용 | |---|---| -| 상태 | 구현 완료 | +| 상태 | 계획 작성 완료 | | 작성일 | `2026-10-06` | | 요구사항 기준 | `docs/20261006_선물_UI_추가/prd.md` | -| API 기준 | 해당 없음 | -| 현재 Phase | Phase 1 선물 UI 진입점 추가 | -| 현재 활성 Goal | 없음 | +| API 기준 | `docs/20261006_선물_UI_추가/prd.md` §11 | +| 현재 Phase | Phase 9 공용 주소 검색과 제한적 Figma 정렬 | +| 현재 활성 Goal | `P9-GATE` runtime 검증 대기 | ## 목표 -한국 접속 및 한국어 설정 사용자가 크리에이터 상세과 마이 탭에서 선물 관련 진입점을 기존 UI 규칙대로 확인할 수 있게 한다. +팬과 크리에이터가 iOS 앱에서 선물 신청, 선물함 조회, 상세 확인, 운송장 등록, 배송지 입력, 푸시/딥링크 상세 진입을 기존 앱 패턴으로 완료할 수 있게 한다. ## 현재 상태 | Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | |---:|---|---:|---|---| -| 1 | 완료 | `5/5` | 없음 | iOS 실기기/시뮬레이터 화면 캡처 QA는 미수행 | +| 1 | 완료 | `5/5` | 없음 | 진입점 UI만 완료 | +| 2 | 완료 | `4/4` | 없음 | 없음 | +| 3 | 완료 | `4/4` | 없음 | 실서버 신청 QA는 최종 검증에서 수행 | +| 4 | 완료 | `4/4` | 없음 | 없음 | +| 5 | 완료 | `5/5` | 없음 | 실서버 취소·런타임 visual QA는 최종 검증에서 수행 | +| 6 | 진행 중 | `1/3` | `P6-T2` | 없음 | +| 7 | 대기 | `0/4` | `P7-T1` | Phase 5 필요 | +| 8 | 코드·정적 gate 승인, runtime 미완료 | `2/2` | `P8-GATE` 인증 runtime 검증 대기 | 인증 실행 환경과 Figma 접근 부재 | +| 9 | 구현·focused 검사 완료 | `2/2` | `P9-GATE` | 최종 build 및 Kakao/화면 runtime 미검증 | + +완료된 Task와 검증 기록은 삭제하지 않는다. 후속 수정은 새 Task 또는 정정 기록으로 누적한다. ## 범위 ### 포함 -- 크리에이터 상세 헤더 액션 영역의 조건부 `선물 보내기` 버튼 추가. -- 마이 탭 카테고리 그리드 첫 항목의 `선물함` 버튼 추가. -- `I18n` 문자열 추가. -- 후속 구현 연결을 위한 inert tap action 위치만 추가. +- 선물 보내기 신청 화면과 신청 API 연동. +- 선물함 리스트 화면과 `type=ALL` pagination. +- 공통 선물 상세 화면과 `direction` 기반 보낸/받은 UI 분기. +- 보낸 선물 운송장 등록, 신청 취소 확인, 취소 완료 UI. +- 받은 선물 배송지 입력, 배송지 입력 완료, 전달 완료, 전달 불가 UI. +- `deepLinkValue=GIFT_DETAIL`, `deepLinkId=applicationNo` 딥링크 진입. ### 제외 -- 선물 보내기 화면, 선물함 화면, AppStep, API, Repository, ViewModel 구현. -- 기존 UI 리팩터링과 공용 컴포넌트 추출. -- 신규 의존성 추가와 프로젝트 설정 변경. +- 관리자 화면. +- 새 디자인 시스템 또는 새 dependency. +- 외부 택배 조회/운송장 실시간 검증. +- 약관 본문 API. +- 선물 이미지, 첨부파일, 메시지 카드. +- 임의 외부 URL 연결. ## 기술적 제약 -- 기술 스택: SwiftUI, 기존 `I18n`, 기존 asset catalog. -- 아키텍처: 기존 view 파일에 최소 조건부 UI만 추가한다. -- 데이터·보안: 신규 네트워크 요청과 민감정보 기록 없음. -- 검증: 테스트 번들 타깃이 확인되지 않는 저장소 상태를 고려해 빌드와 수동 UI 매트릭스를 사용한다. -- 구현: 자동 테스트가 현실적으로 어려운 SwiftUI 표시 변경이므로 각 구현 Task에 TDD 예외 사유와 대체 검증 방법을 명시한다. +- 기술 스택: SwiftUI, Combine, Moya, 기존 `ApiResponse`, 기존 `AppState` navigation. +- 신규 기능 코드는 `SodaLive/Sources/V2/Gift/**` 아래에 작성한다. +- API는 `GiftApi: TargetType`, 저장소는 `final class GiftRepository`로 작성한다. +- 화면 상태는 `final class ...ViewModel: ObservableObject`에서 관리한다. +- 기존 `TitleBar`, `DefaultTitleBar`, `SodaV2ActionModal`, `sodaToast`, `LottieRefreshableScrollView`, 기존 picker/dialog/address search 패턴을 우선 재사용한다. +- 개인정보 필드는 display model 단계에서 숨긴다. View가 금지 필드를 우연히 그릴 수 없도록 `SENT`/`RECEIVED` display model을 분리한다. +- 현재 테스트 번들 타깃이 확인되지 않는다. ViewModel 순수 로직은 가능한 한 test 대상 후보로 계획하되, 실행 불가 시 Debug build와 수동 검증을 Phase Gate 증거로 남긴다. -## Phase 1 +## Task TDD 작성 규칙 -**Phase 결과:** 크리에이터 상세와 마이 탭에 선물 진입점 UI가 추가되고, 후속 연결 전까지 탭 동작은 inert 상태로 유지된다. +구현 Task는 가능한 경우 아래 순서를 따른다. -**선행조건:** PRD 확정. +- [ ] **RED:** 가장 작은 실패 test를 작성한다. +- [ ] **RED 확인:** focused test를 실행해 요구 동작 미구현 때문에 실패함을 확인한다. +- [ ] **GREEN:** RED를 통과시키는 최소 구현을 작성한다. +- [ ] **GREEN 확인:** 같은 focused test를 다시 실행해 성공을 확인한다. +- [ ] **REFACTOR:** 이번 Task가 만든 중복만 정리하고 focused test·직접 영향 회귀·build 결과를 Progress에 기록한다. -**Phase 완료 조건:** `P1-T1`~`P1-T4`와 `P1-GATE` 완료, 검증 기록 누적. +SwiftUI 화면 조립처럼 자동 테스트가 현실적으로 불가능한 Task는 TDD 예외 사유와 대체 검증 방법을 Task에 명시한다. + +## Phase 1 선물 진입점 UI 추가 + +**Phase 결과:** 크리에이터 상세와 마이 탭에 선물 진입점 UI가 추가되고, 후속 연결 전까지 tap action은 inert 상태로 유지됐다. + +**상태:** 완료. + +### 완료된 구현 항목 + +- `P1-T1`: PRD와 plan-task 생성. +- `P1-T2`: `I18n.CreatorChannelHome.sendGift`, `I18n.MyPage.Category.giftBox` 추가. +- `P1-T3`: 크리에이터 상세 조건부 `선물 보내기` 버튼 추가. +- `P1-T4`: 마이 탭 `선물함` 버튼 추가. +- `P1-T5`: 크리에이터 상세 `선물 보내기` 버튼에서 아이콘 제거, 타인 채널 조건 추가. + +### Phase 1 검증 기록 + +- `xcodebuild -workspace "SodaLive.xcworkspace" -list` 성공. +- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` 성공. +- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 성공. +- `git diff --check` 성공. +- iOS 실기기/시뮬레이터 화면 캡처 QA는 미수행. 코드 확인과 빌드로 대체. + +## Phase 2 선물 기능 기반 연결 + +**Phase 결과:** 선물 API, DTO, route, 공통 formatter/display policy가 준비되고 기존 진입점이 실제 화면 route로 연결된다. + +**상태:** 완료. + +**선행조건:** PRD 구현 기준 확정. + +**Phase 완료 조건:** `P2-T1`~`P2-T4`와 `P2-GATE` 완료. ### 구현 항목 -#### Task 1.1 문서 기준 확정 +#### Task 2.1 API/DTO/Repository 추가 -**Goal 실행 `P1-T1`:** 구현 전 PRD와 plan-task에 요구사항, 제외 범위, 검증 기준을 확정한다. +**Goal 실행 `P2-T1`:** 선물 기능 API 호출과 decoding에 필요한 기반 타입을 추가한다. -- **시작 조건:** 사용자 승인 완료. -- **완료 증거:** `prd.md`, `plan-task.md` 생성. -- **범위 밖:** production code 변경. -- **TDD 예외 사유:** 문서 작성 Task로 production 동작을 변경하지 않는다. -- **대체 검증 방법:** 샘플 문서 구조와 경로 규칙을 대조한다. +- **시작 조건:** PRD §11 확인. +- **완료 증거:** `GiftApi`, `GiftRepository`, request/response DTO가 compile 된다. +- **범위 밖:** 화면 UI 구현. **Files:** -- Create: `docs/20261006_선물_UI_추가/prd.md` -- Create: `docs/20261006_선물_UI_추가/plan-task.md` +- Create: `SodaLive/Sources/V2/Gift/Repository/GiftApi.swift` +- Create: `SodaLive/Sources/V2/Gift/Repository/GiftRepository.swift` +- Create: `SodaLive/Sources/V2/Gift/Models/GiftModels.swift` +- Modify: `SodaLive.xcodeproj/project.pbxproj` if the project requires explicit file registration. -- [x] 요구사항과 성공 기준을 PRD에 기록한다. -- [x] 구현 대상 파일과 검증 기준을 plan-task에 기록한다. +**Interfaces:** -#### Task 1.2 I18n 문자열 추가 +- Produces: `GiftApi`, `GiftRepository`, `GiftFormOptionsResponse`, `CreateGiftRequest`, `GiftListResponse`, `GiftDetailResponse`, `RegisterGiftTrackingRequest`, `SubmitGiftRecipientAddressRequest`. -**Goal 실행 `P1-T2`:** 새 버튼 텍스트를 기존 `I18n` 구조에서 사용할 수 있게 한다. +- [x] **RED:** DTO decoding sample을 사용해 `ApiResponse` decode가 실패하는 최소 test를 작성한다. 테스트 타깃이 없으면 `GiftModels.swift` Preview/helper가 아닌 문서상 compile 검증으로 대체한다. +- [x] **RED 확인:** focused test가 있으면 실행해 type 미정의 실패를 확인한다. 없으면 `xcodebuild` 전 compile 실패 예상 지점을 기록한다. +- [x] **GREEN:** 아래 endpoint를 모두 포함하는 `GiftApi`와 repository method를 작성한다. + - `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` +- [x] **GREEN 확인:** `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행해 API/DTO compile 성공을 확인한다. +- [x] **REFACTOR:** DTO 이름과 optional 필드를 PRD §11과 대조하고 Progress에 차이를 기록한다. -- **시작 조건:** `P1-T1` 완료. -- **완료 증거:** `I18n.CreatorChannelHome.sendGift`, `I18n.MyPage.Category.giftBox` 추가. -- **범위 밖:** String Catalog 전환. -- **TDD 예외 사유:** 정적 문자열 노출 추가로 테스트 타깃이 확인되지 않는다. -- **대체 검증 방법:** 컴파일과 실제 UI 렌더링에서 문자열 참조가 성공하는지 확인한다. +#### Task 2.2 공통 표시 정책 추가 + +**Goal 실행 `P2-T2`:** 날짜, status, size description, 개인정보 display policy를 한 곳에서 재사용한다. + +- **시작 조건:** `P2-T1` 완료. +- **완료 증거:** 화면 ViewModel이 사용할 pure helper가 compile 되고 status/date mapping이 PRD와 일치한다. +- **범위 밖:** SwiftUI 화면 배치. **Files:** -- Modify: `SodaLive/Sources/I18n/I18n.swift` +- Create: `SodaLive/Sources/V2/Gift/Models/GiftDisplayModels.swift` +- Create: `SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift` -- [x] `CreatorChannelHome`에 `선물 보내기` 문자열을 추가한다. -- [x] `MyPage.Category`에 `선물함` 문자열을 추가한다. -- [x] 기존 `pick(ko:en:ja:)` 패턴을 유지한다. +**Interfaces:** -#### Task 1.3 크리에이터 상세 선물 버튼 추가 +- Produces: `GiftStatus`, `GiftDirection`, `GiftProgressStep`, `GiftDisplayPolicy.sentDetail(from:)`, `GiftDisplayPolicy.receivedDetail(from:)`, `GiftDateFormatter.yyyyMMdd(fromUTC:)`, `GiftDateFormatter.mmDD(fromUTC:)`. -**Goal 실행 `P1-T3`:** 크리에이터 상세에서 조건에 맞는 사용자에게 `선물 보내기` 버튼을 표시한다. +- [x] **RED:** `statusTimeline` 5단계와 `UNDELIVERABLE/CANCELED` 제외를 검증하는 가장 작은 test를 작성한다. 테스트 타깃이 없으면 helper 사용 지점과 expected table을 문서에 기록한다. +- [x] **RED 확인:** focused test 또는 compile 실패를 확인한다. +- [x] **GREEN:** status mapping, card text mapping, size description mapping, UTC ISO date formatting helper를 작성한다. +- [x] **GREEN 확인:** `SodaLive-dev` Debug build를 실행한다. +- [x] **REFACTOR:** 개인정보 금지 필드가 `RECEIVED` display model에 포함되지 않는지 코드로 대조한다. -- **시작 조건:** `P1-T2` 완료. -- **완료 증거:** 조건부 버튼 구현과 수동 표시 매트릭스 통과. -- **범위 밖:** 실제 선물 보내기 action, 라우팅, API. -- **TDD 예외 사유:** SwiftUI view의 조건부 표시 변경이며 현재 테스트 번들 타깃이 확인되지 않는다. -- **대체 검증 방법:** 아래 매트릭스를 수동 확인하고 Debug build로 컴파일을 검증한다. +#### Task 2.3 AppStep/ContentView route 연결 + +**Goal 실행 `P2-T3`:** 선물 보내기, 선물함, 선물 상세, 운송장 등록 route를 기존 navigation에 등록한다. + +- **시작 조건:** `P2-T1` 완료. +- **완료 증거:** `AppStep` case와 `ContentView` switch가 compile 되고 기존 진입점 tap이 실제 route를 호출한다. +- **범위 밖:** 각 화면 내부 구현. +- **TDD 예외 사유:** navigation enum/switch 연결은 현재 UI integration test 기반이 없다. +- **대체 검증 방법:** 코드 대조와 Debug build. **Files:** +- Modify: `SodaLive/Sources/App/AppStep.swift` +- Modify: `SodaLive/Sources/ContentView.swift` - Modify: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift` - -**표시 매트릭스:** - -| `isOwnCreatorChannel` | `isDmAvailable` | `countryCode` | `LanguageHeaderProvider.current` | 기대 | -|---|---|---|---|---| -| `false` | `true` | `""` | `"ko"` | 표시 | -| `false` | `true` | `"KR"` | `"ko"` | 표시 | -| `false` | `true` | `"kr"` | `"ko"` | 표시 | -| `true` | `true` | `"KR"` | `"ko"` | 숨김 | -| `false` | `false` | `"KR"` | `"ko"` | 숨김 | -| `false` | `true` | `"JP"` | `"ko"` | 숨김 | -| `false` | `true` | `"KR"` | `"ja"` | 숨김 | - -- [x] 기존 `DM 보내기` 버튼 오른쪽에 같은 스타일의 `선물 보내기` 버튼을 추가한다. -- [x] 노출 조건은 `isOwnCreatorChannel == false && isDmAvailable && isKoreanCountry && LanguageHeaderProvider.current == "ko"`로 제한한다. -- [x] action은 빈 클로저로 둔다. - -#### Task 1.5 크리에이터 상세 선물 버튼 후속 수정 - -**Goal 실행 `P1-T5`:** 크리에이터 상세 `선물 보내기` 버튼에서 아이콘을 제거하고 타인 채널에서만 표시한다. - -- **시작 조건:** 사용자 추가 요청. -- **완료 증거:** `CreatorChannelTalkActionSection`의 조건과 버튼 content 수정, 빌드 검증. -- **범위 밖:** 마이 탭 `선물함` 아이콘 제거, 신규 라우팅/API. -- **TDD 예외 사유:** SwiftUI view의 조건부 표시 변경이며 현재 테스트 번들 타깃이 확인되지 않는다. -- **대체 검증 방법:** 코드 조건 확인과 Debug build로 컴파일을 검증한다. - -**Files:** - -- Modify: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelTalkActionSection.swift` -- Modify: `docs/20261006_선물_UI_추가/prd.md` -- Modify: `docs/20261006_선물_UI_추가/plan-task.md` - -- [x] `선물 보내기` 버튼에서 `ic_my_present` 이미지를 제거한다. -- [x] `선물 보내기` 버튼 노출 조건에 `isOwnCreatorChannel == false`를 추가한다. - -#### Task 1.4 마이 탭 선물함 버튼 추가 - -**Goal 실행 `P1-T4`:** 마이 탭 카테고리 그리드 맨 앞에 `선물함` 버튼을 추가한다. - -- **시작 조건:** `P1-T2` 완료. -- **완료 증거:** `CategoryButtonsView` 첫 항목 추가와 4열 유지 확인. -- **범위 밖:** 선물함 화면 이동, API, 신규 AppStep. -- **TDD 예외 사유:** SwiftUI view의 정적 항목 추가이며 현재 테스트 번들 타깃이 확인되지 않는다. -- **대체 검증 방법:** 코드 순서와 화면 렌더링에서 첫 항목/아이콘/4열을 확인한다. - -**Files:** - - Modify: `SodaLive/Sources/MyPage/MyPageView.swift` +- Create: `SodaLive/Sources/V2/Gift/Send/GiftSendView.swift` +- Create: `SodaLive/Sources/V2/Gift/List/GiftBoxListView.swift` +- Create: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` +- Create: `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationView.swift` -- [x] `CategoryButtonsView`의 첫 번째 `CategoryButtonItem`으로 `ic_my_present`와 `I18n.MyPage.Category.giftBox`를 추가한다. -- [x] 기존 4열 `LazyVGrid` 설정을 변경하지 않는다. -- [x] action은 빈 클로저로 둔다. +**Interfaces:** -### 완료 조건 +- Produces: `.giftSend(recipientMemberId: Int, recipientCreatorNickname: String)`, `.giftBoxList`, `.giftDetail(applicationNo: String)`, `.giftTrackingRegistration(applicationNo: String)` or names matching nearby `AppStep` style. -- [x] `P1-T1`~`P1-T5`가 완료됐다. -- [x] 구현 범위가 PRD의 제외 범위를 넘지 않는다. -- [x] 검증 기록이 누적됐다. +- [x] `CreatorChannelTalkActionSection`의 inert action을 `AppState.shared.setAppStep(step: .giftSend(...))`로 교체한다. +- [x] `CategoryButtonsView`의 inert action을 `.giftBoxList` 이동으로 교체한다. +- [x] 신청 성공/취소 성공에서 사용할 replace 정책은 `AppState.replaceCurrentAppStep` 사용으로 문서화한다. +- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`를 실행한다. + +#### Task 2.4 약관/주소/공통 action 정책 확정 + +**Goal 실행 `P2-T4`:** 약관 URL, 주소 검색 재사용 지점, 문의/정책 action fallback을 구현 기준으로 고정한다. + +- **시작 조건:** `P2-T2` 완료. +- **완료 증거:** 구현자가 추정 없이 사용할 수 있는 constant/action map이 compile 된다. +- **범위 밖:** 약관 본문 API 또는 새 외부 URL. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/Constants/GiftConstants.swift` + +- [x] `giftTermsURLString = "https://app.notion.com/p/3eb27b359673802b937ae741ab7b4ad3"`를 추가한다. +- [x] `giftCourierCompanyNames`는 기존 공통 상수가 있으면 alias, 없으면 PRD §11.4 목록 그대로 local constant로 추가한다. +- [x] 저장소에 production 주소 검색/Kakao 우편번호 wrapper가 없으므로 `addressSearchAvailable = false`로 고정하고 이름/휴대폰/우편번호/주소/상세주소 수동 입력을 사용한다. +- [x] 문의와 피드백은 기존 `AppStep.serviceCenter`를 각각 `inquiryStep`, `feedbackStep`으로 재사용하고, 별도 정책 URL이 필요한 action은 `unavailableActionMessage = "준비 중입니다."` fallback을 사용한다. + +#### Task 2.5 API 응답 계약 회귀 수정 + +**Goal 실행 `P2-R1`:** 실제 선물 API response key와 DTO를 일치시켜 폼 옵션과 상세 응답을 안정적으로 decode한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Models/GiftModels.swift` +- Modify: `SodaLive/Sources/V2/Gift/Send/GiftSendView.swift` +- Modify: `work/tests/ReceivedGiftDetailCheck.swift` +- Create: `work/tests/GiftDecodingContractCheck.swift` +- Create: `work/tests/check-gift-decoding-contract.sh` + +- [x] **RED:** 사용자 제공 response fixture로 `ApiResponse` decode 검사를 추가한다. +- [x] **RED 확인:** `data.categories[0].categoryName`의 `keyNotFound`를 확인한다. +- [x] **GREEN:** size/category의 `name`과 timeline의 `statusName`을 DTO에 반영하고 category UI 참조를 갱신한다. +- [x] **GREEN 확인:** 계약 fixture 21개 assertion과 기존 gift 회귀 검사, 양 스킴 generic iOS Debug build를 통과한다. +- [x] **REFACTOR:** 원인 확인용 DEBUG 로그, 진단 스크립트, debug journal을 제거하고 기존 오류 처리는 유지한다. + +#### Task 2.6 상세 중첩 신청번호 null 회귀 수정 + +**Goal 실행 `P2-R2`:** 상세 응답의 `giftInfo.applicationNo=null`을 허용하고 top-level 신청번호를 권위 값으로 표시한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Models/GiftModels.swift` +- Modify: `SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift` +- Modify: `work/tests/GiftDecodingContractCheck.swift` +- Modify: `work/tests/ReceivedGiftDetailCheck.swift` + +- [x] **RED:** nested 신청번호가 null인 실제 응답 fixture와 top-level/nested 값이 다른 sent-detail mapping 검사를 추가한다. +- [x] **RED 확인:** decode의 `valueNotFound`와 nested 신청번호를 사용하는 mapping assertion 실패를 각각 확인한다. +- [x] **GREEN:** `GiftInfoResponse.applicationNo`를 optional로 변경하고 sent detail에는 `GiftDetailResponse.applicationNo`를 사용한다. +- [x] **GREEN 확인:** focused gift 검사, Swift parse, `git diff --check`, 양 스킴 generic iOS Debug build를 통과한다. +- [x] **REFACTOR:** custom decoder나 empty-string fallback 없이 두 production line 변경만 유지한다. + +#### Task 2.7 전달 불가 정책 URL 연결 + +**Goal 실행 `P2-R3`:** 선물 상세의 전달 불가 `선물 정책 확인` 버튼을 약관 `(필수)` 링크와 같은 `GiftConstants.termsURL`에 연결한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` +- Modify: `work/tests/check-received-gift-detail.sh` + +- [x] **RED:** 발신·수신 정책 action의 `openURL` 연결과 기존 toast fallback 제거를 요구하는 source 검사를 추가한다. +- [x] **RED 확인:** `received policy action opens gift terms` 실패를 확인한다. +- [x] **GREEN:** 두 action bar의 `onPolicy`를 공통 `openGiftTerms` 함수로 연결하고 `GiftConstants.termsURL`을 SwiftUI `openURL`로 연다. +- [x] **GREEN 확인:** focused gift 검사, Swift parse, `git diff --check`, 양 스킴 generic iOS Debug build를 통과한다. +- [x] **REFACTOR:** 정책 전용 toast state와 `준비 중입니다.` fallback만 제거하고 문의 action은 유지한다. ### 검증 방법 -#### Phase 1 Gate +#### Phase 2 Gate -**Goal 실행 `P1-GATE`:** 선물 UI 진입점 추가의 빌드와 사용자 관찰 기준을 최종 판정한다. +**Goal 실행 `P2-GATE`:** API/route/common policy 기반이 후속 화면 구현을 막지 않는지 확인한다. ```bash -xcodebuild -workspace "SodaLive.xcworkspace" -list -xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build ``` -**Expected:** 명령 exit code 0. `선물 보내기`와 `선물함` 표시 조건이 PRD 성공 기준과 일치한다. +**Expected:** 두 명령 exit code 0. 신규 route case, API DTO, common policy compile 성공. 수동 검증: -- [x] 크리에이터 상세에서 `DM 보내기` 오른쪽에 `선물 보내기`가 표시된다. 코드 기준: `CreatorChannelTalkActionSection`에서 `if shouldShowGiftButton`이 `if isDmAvailable` 블록 다음에 위치한다. -- [x] 내 채널과 한국 접속/한국어 설정 외 조건에서는 `선물 보내기`가 숨겨진다. 코드 기준: `isOwnCreatorChannel == false && isDmAvailable && isKoreanCountry && LanguageHeaderProvider.current == "ko"`. -- [x] 크리에이터 상세 `선물 보내기` 버튼은 아이콘 없이 텍스트만 표시한다. 코드 기준: 버튼 content에 `Image("ic_my_present")` 없음. -- [x] 마이 탭 카테고리 그리드 첫 항목에 `선물함`이 표시된다. 코드 기준: `CategoryButtonsView`의 첫 `CategoryButtonItem`. -- [x] 새 버튼 탭으로 화면 이동이나 네트워크 요청이 발생하지 않는다. 코드 기준: 두 action 모두 빈 클로저. +- [x] 크리에이터 상세 `선물 보내기` tap이 `GiftSendView` route를 호출한다. (코드 경로 대조) +- [x] 마이 탭 `선물함` tap이 `GiftBoxListView` route를 호출한다. (코드 경로 대조) +- [x] 약관 URL과 courier constant가 PRD와 일치한다. + +## Phase 3 선물 보내기 신청 + +**Phase 결과:** 팬이 선물 보내기 폼을 작성하고 신청 성공 후 상세로 replace 이동한다. + +**상태:** 완료. + +**선행조건:** `P2-GATE` 완료. + +### 구현 항목 + +#### Task 3.1 폼 옵션 로딩 ViewModel + +**Goal 실행 `P3-T1`:** 선물 보내기 화면 진입 시 recipient context와 form options를 로딩한다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/Send/GiftSendViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/Send/GiftSendView.swift` + +- [x] **RED:** `memberId` 또는 nickname 누락 시 submit 불가 상태가 되는 validation test를 작성한다. +- [x] **RED 확인:** focused test 또는 compile 실패를 확인한다. +- [x] **GREEN:** `fetchFormOptions()`, 기본 size 첫 항목 선택, category list state, loading/error state를 구현한다. +- [x] **GREEN 확인:** focused test 또는 Debug build를 실행한다. +- [x] **REFACTOR:** `salePriceCan` CTA 문구와 `basePriceCan != salePriceCan` 표시 조건을 PRD와 대조한다. + +#### Task 3.2 선물 보내기 폼 UI + +**Goal 실행 `P3-T2`:** Figma 구조의 안내 배너, 크리에이터 row, size/category, sender input, 약관 동의를 구현한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Send/GiftSendView.swift` +- Create: `SodaLive/Sources/V2/Gift/Send/Components/GiftSendBannerView.swift` +- Create: `SodaLive/Sources/V2/Gift/Send/Components/GiftSenderFormView.swift` +- Create: `SodaLive/Sources/V2/Gift/Components/GiftAgreementRow.swift` + +- [x] **TDD 예외 사유:** SwiftUI layout 구현이며 현재 UI test 타깃이 없다. +- [x] Figma node `2481:18904`, `2481:18957` 기준으로 navigation 고정, 본문 scroll, bottom CTA 구조를 구현한다. +- [x] category 선택 시 `requiresDamageWaiver`에 따라 파손 면책 row를 표시/숨김 처리한다. +- [x] `(필수)` tap은 확정 Notion URL을 연다. +- [x] `GiftConstants.addressSearchAvailable == false`이므로 이름/휴대폰/우편번호/주소/상세주소를 수동 입력한다. +- [x] Debug build를 실행한다. + +#### Task 3.3 신청 validation과 CTA 상태 + +**Goal 실행 `P3-T3`:** 필수 입력/동의 조건이 충족될 때만 submit 가능하게 한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Send/GiftSendViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/Send/GiftSendView.swift` + +- [x] **RED:** 필수값 누락, 약관 미동의, 파손 면책 필요/미동의 case의 `canSubmit == false` test를 작성한다. +- [x] **RED 확인:** focused test 또는 compile 실패를 확인한다. +- [x] **GREEN:** `canSubmit`, submit 중 중복 탭 방지, request payload mapping을 구현한다. +- [x] **GREEN 확인:** focused test 또는 Debug build를 실행한다. +- [x] **REFACTOR:** `senderAddressDetail`은 request에는 포함하되 CTA 필수 조건에는 포함하지 않는지 확인한다. + +#### Task 3.4 신청 API와 성공 navigation + +**Goal 실행 `P3-T4`:** 신청 API 성공 시 `applicationNo` 상세로 replace 이동한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Send/GiftSendViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/Send/GiftSendView.swift` + +- [x] **RED:** 성공 응답의 `applicationNo`가 navigation callback으로 전달되는 test를 작성한다. +- [x] **RED 확인:** focused test 또는 compile 실패를 확인한다. +- [x] **GREEN:** `POST /api/v2/gifts` 호출, 실패 message 표시, 성공 toast/dialog 없이 상세 replace 이동을 구현한다. +- [x] **GREEN 확인:** focused test 또는 Debug build를 실행한다. +- [x] **REFACTOR:** 실패 시 입력값 유지와 submit state reset을 확인한다. + +### 검증 방법 + +#### Phase 3 Gate + +**Goal 실행 `P3-GATE`:** 선물 신청 사용자 흐름을 판정한다. + +```bash +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +``` + +수동 검증: + +- [x] `GET /api/v2/gifts/form-options`가 화면 진입 시 호출된다. (코드 경로 대조) +- [x] 필수 입력/동의 누락 시 `POST /api/v2/gifts`가 호출되지 않는다. (focused 로직 실행·코드 대조) +- [x] 신청 성공 후 상세로 replace 이동하고 back으로 입력 폼에 돌아가지 않는다. (guarded replace 코드 대조) + +## Phase 4 선물함 리스트 + +**Phase 결과:** 사용자가 선물함에서 보낸/받은 선물 목록, empty, 카드별 상태, pagination을 확인하고 상세로 이동한다. + +**선행조건:** `P2-GATE` 완료. + +### 구현 항목 + +#### Task 4.1 목록 ViewModel과 pagination + +**Goal 실행 `P4-T1`:** `type=ALL` 목록과 `hasNext` 기반 pagination을 구현한다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/List/GiftBoxListViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/List/GiftBoxListView.swift` + +- [x] **RED:** 첫 page가 `type=ALL&page=0&size=20`으로 요청되는 repository interaction test를 작성한다. +- [x] **RED 확인:** focused test 또는 compile 실패를 확인한다. +- [x] **GREEN:** first page, next page, refresh, duplicate load guard를 구현한다. +- [x] **GREEN 확인:** focused test 또는 Debug build를 실행한다. +- [x] **REFACTOR:** 서버 응답 순서를 변경하는 sort가 없는지 확인한다. + +#### Task 4.2 리스트 카드 display model + +**Goal 실행 `P4-T2`:** `direction`과 `status`에 따른 보낸/받은 카드 문구를 구현한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Models/GiftDisplayModels.swift` +- Create: `SodaLive/Sources/V2/Gift/List/Components/GiftBoxCardView.swift` + +- [x] **RED:** PRD의 보낸/받은 status mapping table test를 작성한다. +- [x] **RED 확인:** focused test 또는 compile 실패를 확인한다. +- [x] **GREEN:** 카드 display model과 card view를 구현한다. +- [x] **GREEN 확인:** Debug build를 실행한다. + +#### Task 4.3 리스트 화면 UI 상태 + +**Goal 실행 `P4-T3`:** 안내 배너, empty, 카드별 배송지 필요 상태, 문의/의견 버튼을 구현한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/List/GiftBoxListView.swift` +- Create: `SodaLive/Sources/V2/Gift/List/Components/GiftBoxInfoBannerView.swift` +- Create: `SodaLive/Sources/V2/Gift/List/Components/GiftBoxEmptyView.swift` + +- [x] **TDD 예외 사유:** SwiftUI 상태별 layout이며 UI test 타깃이 없다. +- [x] Figma section title `신청 내역`, `선물 내역`을 표시하지 않는다. +- [x] 보낸 선물 목록은 상단 안내 배너 `사서함을 통해 안전하게 전달됩니다`를 표시한다. +- [x] 받은 선물 목록에 item이 있으면 상단 안내 배너 `배송지 입력 기간 안내`를 표시한다. +- [x] 보낸 empty는 안내 배너만 표시한다. +- [x] 받은 empty는 제목/본문/`의견 남기기` 버튼을 표시한다. +- [x] 별도 파란 상단 배너를 만들지 않는다. +- [x] `direction=RECEIVED && status=TRACKING_REGISTERED` item은 해당 카드 안에 배송지 입력 필요 상태를 표시한다. +- [x] 배송지 입력 필요 카드를 tap하면 해당 item의 상세로 이동한다. +- [x] Debug build를 실행한다. + +#### Task 4.4 상세 navigation 연결 + +**Goal 실행 `P4-T4`:** 리스트 item tap이 같은 상세 화면으로 진입한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/List/GiftBoxListView.swift` + +- [x] 카드 tap 시 `.giftDetail(applicationNo:)` route를 호출한다. +- [x] pagination loading footer와 tap 영역이 충돌하지 않는지 확인한다. +- [x] Debug build를 실행한다. + +### 검증 방법 + +#### Phase 4 Gate + +**Goal 실행 `P4-GATE`:** 선물함 목록 사용자 흐름을 판정한다. + +```bash +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +``` + +수동 검증: + +- [x] 최초 목록 요청은 `type=ALL&page=0&size=20`이다. +- [x] `direction`으로 보낸/받은 카드가 분기된다. +- [x] 리스트 item tap이 상세 API 기반 화면으로 이동한다. + +## Phase 5 공통 상세와 보낸 선물 상세 + +**Phase 결과:** 상세 API 결과가 공통 상세 화면에서 `direction=SENT` 기준으로 안전하게 표시되고 취소 flow가 동작한다. + +**선행조건:** `P2-GATE` 완료. + +### 구현 항목 + +#### Task 5.1 상세 ViewModel + +**Goal 실행 `P5-T1`:** `applicationNo`로 상세 API를 조회하고 direction/status 기반 render state를 만든다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/Detail/GiftDetailViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` + +- [x] **RED:** `direction=SENT/RECEIVED/CANCELED` render state mapping test를 작성한다. +- [x] **RED 확인:** focused test 또는 compile 실패를 확인한다. +- [x] **GREEN:** load, retry, loading/error state, render state mapping을 구현한다. +- [x] **GREEN 확인:** Debug build를 실행한다. + +#### Task 5.2 progress와 공통 상세 UI + +**Goal 실행 `P5-T2`:** `statusTimeline` 기반 5단계 progress와 공통 card section을 구현한다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/Detail/Components/GiftProgressView.swift` +- Create: `SodaLive/Sources/V2/Gift/Detail/Components/GiftInfoSectionView.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` + +- [x] **RED:** `UNDELIVERABLE`, `CANCELED`가 progress step에 추가되지 않는 mapping test를 작성한다. +- [x] **GREEN:** progress, loading, retry UI를 구현한다. 카드 폭은 고정하지 않고 기기의 가용 폭을 채운다. +- [x] **GREEN 확인:** Debug build를 실행한다. + +#### Task 5.3 보낸 상세 UI와 개인정보 정책 + +**Goal 실행 `P5-T3`:** 보낸 팬 관점 상세 hero, 선물 정보, 보내는 사람, 사서함, 사유 섹션을 구현한다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/Detail/Components/SentGiftDetailContentView.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` + +- [x] **RED:** `recipientAddress`가 sent display model에 포함되지 않는 test를 작성한다. +- [x] **GREEN:** `status`별 hero 문구와 sections를 구현한다. +- [x] **GREEN 확인:** Debug build를 실행한다. +- [x] **REFACTOR:** `senderInfo`와 `mailbox`는 nil일 때 섹션이 숨겨지는지 확인한다. + +#### Task 5.4 보낸 상세 action bar + +**Goal 실행 `P5-T4`:** 운송장 등록, 신청 취소, 문의/정책/리뷰 action 조건을 구현한다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/Detail/Components/GiftDetailActionBar.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` + +- [x] `trackingRequired=true`일 때만 `운송장 작성하기`를 표시한다. +- [x] `direction=SENT && status=RECEIVED`일 때만 신청 취소 action을 표시한다. +- [x] 기존 리뷰 플로우가 있으면 `DELIVERED` 팬 리뷰 action을 연결하고, 없으면 숨긴다. +- [x] 기존 문의/정책 경로가 없으면 숨기거나 `준비 중입니다.` toast를 사용한다. +- [x] Debug build를 실행한다. + +#### Task 5.5 신청 취소 dialog와 취소 완료 UI + +**Goal 실행 `P5-T5`:** 확인 dialog 이후 취소 API를 호출하고 성공 시 취소 완료 UI로 replace 한다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/Detail/Components/GiftCancelCompleteView.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` + +- [x] **RED:** dialog confirm 전에는 cancel API가 호출되지 않는 test를 작성한다. +- [x] **GREEN:** `SodaV2ActionModal` 기반 확인 dialog와 cancel mutation을 구현한다. +- [x] **GREEN 확인:** Debug build를 실행한다. +- [x] **REFACTOR:** cancel 실패 시 dialog를 닫지 않고 오류를 표시하는지 확인한다. + +### 검증 방법 + +#### Phase 5 Gate + +**Goal 실행 `P5-GATE`:** 공통 상세와 보낸 상세 flow를 판정한다. + +```bash +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +``` + +수동 검증: + +- [x] `direction=SENT`에서 `recipientAddress`가 표시되지 않는다. +- [x] `trackingRequired=true`일 때만 운송장 CTA가 보인다. +- [x] 신청 취소는 확인 dialog confirm 이후에만 API를 호출한다. +- [x] `status=CANCELED`는 취소 완료 UI를 표시한다. + +## Phase 6 운송장 등록 + +**Phase 결과:** 보낸 팬이 운송장 등록 조건에서 택배사와 운송장 번호를 제출하고 상세 최신 상태로 돌아간다. + +**선행조건:** `P5-GATE` 완료. + +### 구현 항목 + +#### Task 6.1 운송장 등록 화면 진입 검증 + +**Goal 실행 `P6-T1`:** 화면 진입 시 상세를 재조회하고 등록 가능 상태를 검증한다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationView.swift` + +- [x] **RED:** `trackingRequired=false` 또는 `mailbox=nil`이면 submit 불가 test를 작성한다. +- [x] **GREEN:** 상세 재조회, mailbox 표시, 등록 불가 오류 표시를 구현한다. +- [x] **GREEN 확인:** Debug build를 실행한다. + +#### Task 6.1-R1 진입 화면 리뷰 보완 + +**Goal 실행 `P6-R1`:** 비활성 중 토스트 종료와 긴 오류 문구에서도 상세 복귀·재시도 접근성을 유지한다. + +- **시작 조건:** `REV-P6-001`, `REV-P6-002` +- **완료 증거:** lifecycle 보완, 오류 영역 scroll 적용, 양 스킴 Debug build, 재리뷰 승인. +- **범위 밖:** 택배사 선택, 운송장 입력, 등록 mutation. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationView.swift` +- Review: `docs/20261006_선물_UI_추가/reviews/phase6-tracking-entry-validation.md` + +- [x] 재등장 시 pending 상세 복귀를 기존 exactly-once callback으로 처리한다. +- [x] 긴 오류 메시지와 큰 글자에서도 재시도 버튼까지 scroll 접근 가능하게 한다. +- [x] focused 검증과 양 스킴 Debug build를 실행한다. + +#### Task 6.2 운송장 입력 UI + +**Goal 실행 `P6-T2`:** 신청 요약, 보내실 주소, 택배사 select, 운송장 input, 안내, bottom action bar와 신청 취소 확인 UI를 구현한다. 이 Task는 표시·입력 상태만 소유하며 mutation은 `P6-T3`에서 연결한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationView.swift` +- Modify: `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationViewModel.swift` +- Create: `SodaLive/Sources/V2/Gift/Tracking/Components/GiftCourierPickerView.swift` +- Modify: `SodaLive.xcodeproj/project.pbxproj` + +- [x] **TDD 예외 사유:** SwiftUI layout 구현이며 UI test 타깃이 없다. +- [x] Figma node `2481:19066`, `2481:19109` 기준으로 구현한다. +- [x] 왼쪽 `신청 취소`, 오른쪽 `등록 완료` bottom action bar를 구현한다. +- [x] Debug build를 실행한다. + +#### Task 6.3 운송장 등록과 신청 취소 mutation + +**Goal 실행 `P6-T3`:** 택배사와 운송장 번호 validation 후 등록 API를 호출하고, 확인된 신청 취소는 기존 취소 API로 처리한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationView.swift` + +- [x] **RED:** 택배사 미선택 또는 공백 운송장 번호면 `canSubmit == false` test를 작성한다. +- [x] **GREEN:** `POST /tracking`, submit 중 중복 탭 방지, 성공 toast, 상세 replace/복귀를 구현한다. +- [x] **GREEN:** 신청 취소 확인 이후에만 `POST /cancel`을 호출하고 등록/취소 mutation 상호 중복을 막는다. +- [x] **GREEN 확인:** Debug build를 실행한다. +- [x] **REFACTOR:** 성공 후 상세 API가 재조회되는지 확인한다. + +### 검증 방법 + +#### Phase 6 Gate + +**Goal 실행 `P6-GATE`:** 운송장 등록 flow를 판정한다. + +```bash +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +``` + +수동 검증: + +- [ ] `trackingRequired=false` 상세에서는 등록 화면에 머물지 않는다. +- [ ] `mailbox=null`이면 등록을 막고 오류를 표시한다. +- [ ] 등록 성공 후 상세 재조회 결과로 `TRACKING_REGISTERED`가 표시된다. + +## Phase 7 받은 상세와 배송지 입력 + +**Phase 결과:** 받은 크리에이터가 배송지 입력 필요 상태에서 배송지를 제출하고, 이후 상태별 받은 상세를 안전하게 확인한다. + +**선행조건:** `P5-GATE` 완료. + +### 구현 항목 + +#### Task 7.1 받은 상세 display policy + +**Goal 실행 `P7-T1`:** 받은 상세에서 표시 금지 필드가 UI 모델에 포함되지 않게 한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift` +- Create: `SodaLive/Sources/V2/Gift/Detail/Components/ReceivedGiftDetailContentView.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` +- Modify: `SodaLive.xcodeproj/project.pbxproj` +- Create: `work/tests/ReceivedGiftDetailCheck.swift` +- Create: `work/tests/check-received-gift-detail.sh` + +- [x] **RED:** `senderInfo`, `mailbox`, `applicationNo`, `paidCan`, `tracking`이 received display model에 포함되지 않는 compile-fail probe를 작성하고, 받은 상세 component 부재 RED를 확인한다. +- [x] **GREEN:** 기존 privacy-safe display model을 유지하고 받은 상세 hero와 선물 정보 component를 구현한다. +- [x] **GREEN 확인:** Debug build를 실행한다. + +#### Task 7.2 배송지 입력 UI + +**Goal 실행 `P7-T2`:** `recipientAddressRequired=true`일 때 배송지 입력 UI를 표시한다. + +**Files:** + +- Create: `SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressFormView.swift` +- Create: `SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` + +- [x] **TDD 예외 사유:** SwiftUI form layout 구현이며 UI test 타깃이 없다. 구조 RED/GREEN과 build로 대체한다. +- [ ] Figma node `2531:36290` 기준으로 개인정보 안내 배너, 전달 예정 선물, 배송지 입력, 약관 동의, bottom CTA를 구현한다. (node 접근 불가로 문서·기존 패턴 기반 구현만 완료, fresh 원본 대조 미수행) +- [x] `(필수)` tap은 확정 Notion URL을 연다. +- [x] `GiftConstants.addressSearchAvailable == false`이므로 이름/휴대폰/우편번호/주소/상세주소를 수동 입력한다. +- [x] Debug build를 실행한다. + +#### Task 7.3 배송지 입력 validation과 submit + +**Goal 실행 `P7-T3`:** 필수값과 약관 동의 후 배송지 입력 API를 호출하고 상세를 재조회한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressViewModel.swift` +- Modify: `SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressFormView.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` +- Create: `work/tests/GiftRecipientAddressCheck.swift` +- Create: `work/tests/check-gift-recipient-address.sh` +- Modify: `work/tests/check-received-gift-detail.sh` + +- [x] **RED:** 이름/휴대폰/우편번호/주소/약관 누락 case의 `canSubmit == false` test를 작성한다. +- [x] **GREEN:** `POST /recipient-address`, submit 중 중복 탭 방지, 성공 toast, 상세 reload를 구현한다. +- [x] **GREEN 확인:** Debug build를 실행한다. +- [x] **REFACTOR:** 실패 시 입력값 유지와 error 표시를 확인한다. + +#### Task 7.4 받은 상세 상태별 UI + +**Goal 실행 `P7-T4`:** 배송지 입력 완료, 전달 완료, 전달 불가 받은 상세 UI를 구현한다. + +**Files:** + +- Modify: `SodaLive/Sources/V2/Gift/Models/GiftDisplayModels.swift` +- Modify: `SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/Components/ReceivedGiftDetailContentView.swift` +- Modify: `SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift` +- Modify: `work/tests/ReceivedGiftDetailCheck.swift` +- Modify: `work/tests/check-received-gift-detail.sh` + +- [x] **RED:** `recipientAddress=null`이면 주소 섹션 hidden, `UNDELIVERABLE`이면 주소/입력 UI hidden test를 작성한다. +- [x] **GREEN:** `TRACKING_REGISTERED`, `ARRIVED_AT_MAILBOX`, `INSPECTION_COMPLETED`, `DELIVERED`, `UNDELIVERABLE` UI를 구현한다. +- [x] **GREEN 확인:** Debug build를 실행한다. +- [x] **REFACTOR:** 전달 불가 정책 action은 임의 URL을 열지 않는지 확인한다. + +### 검증 방법 + +#### Phase 7 Gate + +**Goal 실행 `P7-GATE`:** 받은 상세와 배송지 입력 flow를 판정한다. + +```bash +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +``` + +수동 검증: + +- [ ] 받은 상세에서 팬의 이름/휴대폰/주소가 표시되지 않는다. +- [ ] `recipientAddressRequired=true`일 때 배송지 입력 UI가 표시된다. +- [ ] 배송지 입력 성공 후 상세 재조회로 입력 완료 UI가 표시된다. +- [ ] `UNDELIVERABLE`이면 배송지 입력 UI와 주소 섹션이 숨겨진다. + +## Phase 8 푸시/딥링크와 최종 회귀 + +**Phase 결과:** 선물 푸시/딥링크가 상세 화면으로 진입하고 전체 사용자 흐름이 build와 수동 검증으로 확인된다. + +**선행조건:** `P5-GATE`, `P7-GATE` 완료. + +**현재 판정:** 코드·정적 gate 승인. 인증 환경의 전체 사용자 흐름 gate는 미완료이며, 아래 수동 검증 항목은 모두 미수행이다. + +### 구현 항목 + +#### Task 8.1 딥링크 action 추가 + +**Goal 실행 `P8-T1`:** `deepLinkValue=GIFT_DETAIL`, `deepLinkId=applicationNo`를 `.giftDetail` route로 연결한다. + +**Files:** + +- Modify: `SodaLive/Sources/App/AppDeepLinkHandler.swift` +- Create: `work/tests/AppDeepLinkGiftDetailCheck.swift` +- Create: `work/tests/check-app-deep-link-gift-detail.sh` + +- [x] **RED:** URL/query parser가 `GIFT_DETAIL`과 `applicationNo`를 action으로 만드는 test를 작성한다. 테스트 타깃이 없으면 parser case table을 Progress에 기록한다. +- [x] **GREEN:** `AppDeepLinkAction.giftDetail(applicationNo:)`와 parser/apply 분기를 추가한다. +- [x] **GREEN 확인:** Debug build를 실행한다. +- [x] **REFACTOR:** 딥링크 진입이 보낸/받은 화면을 미리 결정하지 않고 상세 API 결과를 기다리는지 확인한다. + +기존 `AppStep.giftDetail(applicationNo:)`를 재사용하므로 `AppStep.swift` 수정은 없다. 테스트 번들 대신 production-source Swift harness로 parser와 적용 경로를 검증한다. + +#### Task 8.2 최종 회귀와 문서 갱신 + +**Goal 실행 `P8-T2`:** 전체 선물 기능의 검증 결과와 남은 항목을 문서에 누적한다. + +**Files:** + +- Modify: `docs/20261006_선물_UI_추가/plan-task.md` +- Create: `docs/20261006_선물_UI_추가/reviews/phase8-gift-user-flow.md` + +- [x] `SodaLive` Debug build를 실행한다. +- [x] `SodaLive-dev` Debug build를 실행한다. +- [x] PRD §14 체크리스트를 기준으로 수동 검증 결과를 Progress에 기록한다. 인증 환경 부재로 미수행임을 기록하며, 수용 기준 통과를 뜻하지 않는다. +- [x] 코드 리뷰 리포트를 `docs/sample/sample-review.md` 구조로 작성한다. + +### 검증 방법 + +#### Phase 8 Gate + +**Goal 실행 `P8-GATE`:** 전체 선물 사용자 기능을 최종 판정한다. + +- [x] 코드·정적 gate: focused checks, 양 스킴 generic iOS Debug build, Swift/shell parse, `git diff --check`, Oracle 승인 근거를 기록한다. + +전체 인증 runtime 사용자 흐름 gate는 미완료다. 실제 푸시 수신, 인증 딥링크·navigation·replace 이동과 아래 수동 journey는 검증하지 않았다. + +```bash +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build +git diff --check +``` + +수동 검증: + +- [ ] 선물 보내기 신청 성공 후 상세 replace 이동. +- [ ] 선물함 리스트 item 상세 이동. +- [ ] 푸시/딥링크 상세 이동. +- [ ] 보낸 상세 개인정보 경계. +- [ ] 받은 상세 개인정보 경계. +- [ ] 운송장 등록 성공 후 상세 갱신. +- [ ] 배송지 입력 성공 후 상세 갱신. +- [ ] 취소 confirm 이후 API 호출과 취소 완료 replace. + +## Phase 9 공용 주소 검색과 제한적 Figma 정렬 + +**Goal 실행 `P9-T1`:** 현재 size source를 고정하고 typed result/source 계약의 feature-missing RED를 확인한다. + +**Files:** `work/tests/check-gift-address-search.sh`, `work/tests/KakaoPostcodeResultCheck.swift`. + +- [x] 정상 문자열, 선행 0, address/roadAddress 차이, 누락/타입/공백 거부와 source 계약 검사를 실행한다. +- [x] `GiftSendView.sizeSection`, `sizeTitle` 현재 source SHA256 보존 검사를 고정한다. + +**Goal 실행 `P9-T2`:** 공유 주소 컴포넌트와 Kakao embed sheet를 연결하고 제공된 Figma geometry/copy를 적용한다. + +**Files:** `SodaLive/Sources/V2/Component/Address/{KakaoPostcodeSearchView,SodaAddressInputView}.swift`, `SodaLive/Sources/V2/Gift/Send/Components/GiftSenderFormView.swift`, `SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressFormView.swift`, `SodaLive/Sources/V2/Gift/Send/GiftSendView.swift`, 필요 시 `SodaLive.xcodeproj/project.pbxproj`. + +- [x] HTTPS CDN/HTML/JS bridge와 경계 검증, 닫기/loading/error, handler/delegate 해제를 구현한다. +- [x] readonly zip/base 및 editable detail을 양 폼에 연결하고 category만 compact화한다. + +**Goal 실행 `P9-T3`:** 독립 리뷰에서 확인한 주소 재선택 및 Figma copy 결함을 수정한다. + +**Files:** `SodaLive/Sources/V2/Component/Address/SodaAddressInputView.swift`, `work/tests/check-gift-address-search.sh`. + +- [x] 다른 기본 주소 선택 시 이전 `addressDetail`을 초기화한다. +- [x] `우편번호`, `주소`, `상세주소` 가시 라벨과 `주소 검색` 버튼 문구를 적용한다. +- [x] 위 동작의 focused RED/GREEN을 기록한다. + +**Goal 실행 `P9-GATE`:** focused GREEN, 기존 recipient 회귀, Swift parse 및 project lint 결과를 기록한다. + +- [x] `bash work/tests/check-gift-address-search.sh` exit 0. +- [x] `bash work/tests/check-gift-recipient-address.sh`, `bash work/tests/check-received-gift-detail.sh recipient-form` exit 0. +- [x] 변경 Swift parse, project lint, `git diff --check`, 양 스킴 generic iOS Debug build가 성공했다. +- [ ] 실제 Kakao 검색/선택/취소/실패 및 screenshot 기반 fidelity는 runtime 증거 확보 후 판정한다. + +### Phase 9 검증 기록 + +- `P9-T3` 상세주소 재선택 수정: production 편집 전 focused check는 `FAIL gift-address-search: clear detail only when old postcode/base pair differs, before assigning selection`로 RED(exit 1). 기존 zip/base 쌍과 선택 결과가 다를 때만 상세주소를 비우고 이후 zip/base를 갱신하도록 수정한 뒤 같은 검사 GREEN(exit 0). 동일 쌍은 상세주소를 유지하며, 변경은 선택 callback에만 있어 취소 경로는 기존 값을 변경하지 않는다. +- `P9-T3` Figma 라벨/버튼 수정: 별도 source assertion 추가 후 `FAIL gift-address-search: visible address label 우편번호`로 RED(exit 1). `우편번호`, `주소`, `상세주소` visible label과 `주소 검색` 버튼/접근성 문구를 적용한 뒤 같은 검사 GREEN(exit 0). 기존 placeholder·토큰·간격·binding·sheet와 필드 접근성은 유지했다. +- `P9-T3` 두 GREEN 실행의 출력: `PASS gift-address-search: sizeSection and sizeTitle preserved`, `PASS gift-address-search: bridge, lifecycle, readonly shared forms and compact category source contracts`, `PASS KakaoPostcodeResult: 15 assertions`. 실행 명령은 모두 `bash work/tests/check-gift-address-search.sh`이며 runtime UI 검증 결과가 아닌 source/typed-result 검사 결과다. + +- 구현 전: 기존 source와 공용 spacing/radius/font를 확인했다. Xcode 프로젝트는 앱 source에 명시적 PBXGroup/PBXBuildFile을 사용하므로 두 새 파일의 양 타깃 등록이 필요하다. +- RED (production 편집 전): `bash work/tests/check-gift-address-search.sh` exit 1, `FAIL gift-address-search: missing KakaoPostcodeSearchView.swift`; 기존 `sizeSection and sizeTitle preserved`는 PASS. `xcrun swiftc -typecheck work/tests/KakaoPostcodeResultCheck.swift -parse-as-library`는 `cannot find 'KakaoPostcodeResult' in scope`로 exit 1. +- GREEN: 같은 focused script exit 0. 출력은 `PASS gift-address-search: sizeSection and sizeTitle preserved`, `PASS gift-address-search: bridge, lifecycle, readonly shared forms and compact category source contracts`, `PASS KakaoPostcodeResult: 15 assertions`. +- 회귀: `bash work/tests/check-gift-recipient-address.sh`는 `PASS: 266 assertions`; `bash work/tests/check-received-gift-detail.sh recipient-form`은 `PASS recipient-form: injection, submission, toast lifecycle, GET-only retry and fallback`. +- `xcrun swiftc -parse`로 신규 2개와 변경 기존 3개 Swift 파일을 검사해 exit 0. `plutil -lint SodaLive.xcodeproj/project.pbxproj` 결과 `OK`. +- 최종 source 재검증: focused script, 5개 Swift parse, `bash -n work/tests/check-gift-address-search.sh`, project lint 모두 exit 0. 신규 공용 2개와 sender 폼은 임시 theme stub을 사용한 `xcrun swiftc -typecheck -sdk "$(xcrun --sdk iphoneos --show-sdk-path)" -target arm64-apple-ios16.0`에서도 exit 0이며 임시 stub은 삭제했다. 이는 실제 앱 전체 빌드를 대체하지 않는다. +- 공용 두 source는 250 LOC 미만이며 새 dependency/route/API/ViewModel 변경이 없다. 크기 선택 source SHA256은 production 편집 전 작업본과 동일하다. +- 최종 gate: focused/recipient/received/decoding/deep-link 회귀, 5개 Swift parse, shell parse, project lint, `git diff --check`가 성공했다. `SodaLive-dev`, `SodaLive` generic iOS Debug build는 각각 `BUILD SUCCEEDED`이며 독립 재리뷰는 기능 `APPROVE`, Figma 구조·CJK 정적 검토 `APPROVE_STATIC`이다. +- 한계: LSP diagnostics 도구가 제공되지 않아 수행하지 않았다. 실제 WKWebView의 Kakao 네트워크 검색/선택/취소/실패와 인증 화면 screenshot 기반 픽셀·keyboard·Dynamic Type·VoiceOver QA는 미수행이다. 정적 승인과 build 성공은 runtime 또는 pixel fidelity PASS를 의미하지 않는다. + +## Phase 10 선물 연락처 입력 제한 + +**Phase 결과:** 보내는 사람과 받는 크리에이터 배송지 폼이 한글 IME 조합을 보존하면서 이름·휴대폰 입력 규칙을 동일하게 적용한다. + +### Task 10.1 입력 규칙 RED + +**Files:** `work/tests/GiftContactInputCheck.swift`, `work/tests/check-gift-contact-input.sh`. + +- [x] 이름의 한글·영문 1~12자 허용, 공백·숫자·기호·emoji 거부와 13번째 문자 거부를 RED로 확인한다. +- [x] 휴대폰의 ASCII 숫자 1~11자 허용, 비숫자와 12번째 문자 거부를 RED로 확인한다. +- [x] 양 폼과 양 ViewModel이 같은 규칙을 사용하는 source contract RED를 확인한다. + +### Task 10.2 IME-safe 공용 입력 필드 + +**Files:** `SodaLive/Sources/V2/Gift/Components/GiftContactTextField.swift`, `SodaLive.xcodeproj/project.pbxproj`. + +- [x] `UIViewRepresentable`과 `UITextFieldDelegate`를 사용하고 `markedTextRange != nil` 동안 binding을 재할당하지 않는다. +- [x] 조합이 끝난 prospective text만 `NSString`/`NSRange`로 계산해 이름 12자·휴대폰 11자 경계를 적용한다. +- [x] number pad는 휴대폰에만 표시하고 붙여넣기도 동일 규칙으로 거부한다. + +### Task 10.3 양 폼·제출 방어 연결 + +**Files:** `SodaLive/Sources/V2/Gift/Send/Components/GiftSenderFormView.swift`, `SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressFormView.swift`, `SodaLive/Sources/V2/Gift/Send/GiftSendViewModel.swift`, `SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressViewModel.swift`. + +- [x] 이름·휴대폰 SwiftUI `TextField`만 공용 입력 필드로 교체하고 기존 layout·copy·주소 입력을 보존한다. +- [x] 두 `canSubmit`이 이름 1~12자와 휴대폰 숫자 1~11자를 동일 규칙으로 검증한다. +- [x] 기존 request trim과 제출 lifecycle을 변경하지 않는다. + +### Task 10.4 휴대폰 제출 형식 강화 + +**Goal 실행 `P10-T4`:** 편집 중 삭제와 11자리 입력은 유지하면서 `010`으로 시작하는 정확히 11자리 휴대폰만 제출한다. 이 Task가 Task 10.3의 휴대폰 숫자 1~11자리 제출 계약을 대체한다. + +- **시작 조건:** `DEC-009`, `SEND-007`, `RECV-007` 확정. +- **완료 증거:** contact focused check와 실제 recipient ViewModel 회귀 GREEN. +- **범위 밖:** 자동 하이픈, SMS 인증, 이름 입력 구현 변경. + +**Files:** `SodaLive/Sources/V2/Gift/Components/GiftContactInputRule.swift`, `work/tests/GiftContactInputCheck.swift`, `work/tests/GiftRecipientAddressCheck.swift`. + +- [x] **RED:** 짧은 번호와 `011` 11자리가 제출되는 기존 동작을 실패 test로 고정한다. +- [x] **RED 확인:** `bash work/tests/check-gift-contact-input.sh`, `bash work/tests/check-gift-recipient-address.sh`에서 요구 동작 미구현 assertion 실패를 확인한다. +- [x] **GREEN:** `phoneNumber.acceptsSubmission`이 ASCII 숫자 `010` 11자리만 허용하도록 최소 수정한다. +- [x] **GREEN 확인:** 같은 focused test 두 개가 성공하고 이름 편집·제출 회귀가 유지된다. +- [x] **REFACTOR:** 새 추상화 없이 중복만 확인하고 focused test 결과를 Progress에 기록한다. + +### Task 10.5 운송장 번호 입력·제출 제한 + +**Goal 실행 `P10-T5`:** 운송장 입력은 ASCII 숫자 최대 14자만 보존하고 10~14자리일 때만 등록한다. + +- **시작 조건:** `DEC-009`, `TRACK-007` 확정. +- **완료 증거:** 실제 `GiftTrackingRegistrationViewModel` harness에서 경계·POST guard·기존 mutation 회귀 GREEN. +- **범위 밖:** request DTO, repository/API, 택배사 선택, 성공/취소 navigation 변경. + +**Files:** `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationView.swift`, `SodaLive/Sources/V2/Gift/Tracking/GiftTrackingRegistrationViewModel.swift`, `work/tests/GiftTrackingRegistrationCheck.swift`. + +- [x] **RED:** 9·15자리와 비ASCII/비숫자 거부, 10~14자리 허용 및 잘못된 값의 POST 차단 test를 작성한다. +- [x] **RED 확인:** tracking focused harness에서 기존 trim/nonempty 검증 때문에 발생하는 assertion 실패를 확인한다. +- [x] **GREEN:** ViewModel의 순수 편집·제출 predicate를 guarded `Binding`과 `canSubmit`에서 함께 사용하고 number pad를 적용한다. +- [x] **GREEN 확인:** 같은 harness가 기존 등록·실패·취소·navigation 회귀를 포함해 성공한다. +- [x] **REFACTOR:** request trim을 포함한 기존 lifecycle은 유지하고 focused test 결과를 Progress에 기록한다. + +### Task 10.6 연락처 입력의 언어별 폰트 정책 회귀 수정 + +**Goal 실행 `P10-R1`:** UIKit 연락처 입력 필드가 기존 SwiftUI `.appFont(.body3)`와 같은 한국어·일본어·기타 언어별 폰트 정책을 사용한다. + +- **시작 조건:** 리뷰 `REV-P10-001` 확정. +- **완료 증거:** 폰트 정책 source contract의 의도한 RED 후 GREEN, contact focused check, Swift parse, 양 스킴 build와 정적 재리뷰. +- **범위 밖:** 타이포그래피 token 변경, 입력 layout·색상·크기 변경, 다른 UIKit 입력 필드 리팩터링. +- **Review:** `docs/20261006_선물_UI_추가/reviews/phase10-contact-tracking-validation.md`. + +**Files:** `SodaLive/Sources/Extensions/FontModifier.swift`, `SodaLive/Sources/V2/Gift/Components/GiftContactTextField.swift`, `work/tests/check-gift-contact-input.sh`. + +- [x] **RED:** 연락처 래퍼의 Pretendard 직접 지정 금지와 공용 localized font resolver 사용을 검사한다. +- [x] **RED 확인:** `bash work/tests/check-gift-contact-input.sh`에서 언어별 폰트 정책 미연결 assertion 실패를 확인한다. +- [x] **GREEN:** 기존 `FontModifier`의 언어 해석을 재사용하는 최소 UIKit bridge를 추가하고 연락처 필드에 적용한다. +- [x] **GREEN 확인:** 같은 focused check가 성공하고 기존 IME·이름·휴대폰 source contract를 유지한다. +- [x] **REFACTOR:** 이번 수정의 중복만 확인하고 Swift parse·양 스킴 build·정적 재리뷰 결과를 Progress와 리뷰 문서에 기록한다. + +### Phase 10 Gate + +- [x] `bash work/tests/check-gift-contact-input.sh`가 GREEN이다. +- [x] 기존 gift address/recipient/detail 회귀, Swift/shell parse, project lint, `git diff --check`가 성공한다. +- [x] `SodaLive-dev`, `SodaLive` generic iOS Debug build가 성공한다. +- [ ] 실제 한글 조합·12/13자·숫자 11/12자 입력을 실행 화면에서 확인한다. +- [ ] 휴대폰 `010` 11자리와 운송장 숫자 10~14자리 경계, 15번째 문자 거부를 실행 화면에서 확인한다. ## 실행 순서와 의존성 | 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 | |---:|---|---|---|---| -| 1 | `P1-T1` | 없음 | 아니요 | 요구사항 재확인 | -| 2 | `P1-T2` | `P1-T1` | 아니요 | I18n 패턴 재확인 | -| 3 | `P1-T3` | `P1-T2` | `P1-T4`와 가능 | 조건 매트릭스 재확인 | -| 4 | `P1-T4` | `P1-T2` | `P1-T3`와 가능 | 그리드 순서 재확인 | -| 5 | `P1-GATE` | Task 전체 | 아니요 | 실패 Task로 회귀 수정 | +| 1 | `P2-T1` | PRD 확정 | 아니요 | API 계약 대조 | +| 2 | `P2-T2` | `P2-T1` | `P2-T3`와 일부 가능 | display policy 축소 | +| 3 | `P2-T3` | `P2-T1` | `P2-T2`와 일부 가능 | route shell만 연결 | +| 4 | `P2-T4` | `P2-T2` | 예 | 기존 wrapper 재확인 | +| 5 | `P2-GATE` | Phase 2 Task 전체 | 아니요 | 실패 Task 수정 | +| 6 | `P3-*` | `P2-GATE` | `P4-*`와 가능 | 신청 flow만 독립 진행 | +| 7 | `P4-*` | `P2-GATE` | `P3-*`와 가능 | 목록 flow만 독립 진행 | +| 8 | `P5-*` | `P2-GATE` | 일부 가능 | 상세 기반 먼저 완료 | +| 9 | `P6-*` | `P5-GATE` | `P7-*`와 가능 | 운송장 독립 진행 | +| 10 | `P7-*` | `P5-GATE` | `P6-*`와 가능 | 받은 상세 독립 진행 | +| 11 | `P8-*` | `P5-GATE`, `P7-GATE` | 아니요 | 딥링크만 분리 검증 | ```text -P1-T1 -> P1-T2 -> P1-T3/P1-T4 -> P1-GATE +P2 -> P3/P4/P5 -> P6/P7 -> P8 ``` ## 변경 금지 항목 -- 신규 AppStep, 화면, API, ViewModel, Repository를 만들지 않는다. -- `DM 보내기` 기존 동작을 변경하지 않는다. -- `CategoryButtonsView`의 4열 설정을 변경하지 않는다. -- 사용자 또는 다른 작업자가 추가한 asset 파일을 수정하지 않는다. +- 새 디자인 시스템과 새 dependency를 추가하지 않는다. +- `Pods/**`, `generated/**`, `build/**`를 수정하지 않는다. +- API에 없는 필드, 오류 status/key, 취소일, 남은 일수를 추정하지 않는다. +- 개인정보 표시 금지 필드를 UI 편의상 노출하지 않는다. +- test를 삭제·skip·완화하거나 타입 오류를 우회하지 않는다. +- 임의 외부 URL을 연결하지 않는다. 확정 URL은 약관 Notion URL뿐이다. + +## 의사결정 및 중단 규칙 + +- PRD와 API 응답이 충돌하면 구현을 멈추고 PRD Decision Log에 정정 후보를 남긴다. +- 개인정보 표시 여부가 애매하면 표시하지 않는다. +- 기존 앱에 문의/리뷰/정책 이동 경로가 없으면 새 외부 URL을 만들지 않는다. +- backend가 계약과 다른 payload를 내려주면 해당 Phase를 blocked로 표시하고 DTO를 추정 확장하지 않는다. ## Progress +기존 기록을 삭제하거나 덮어쓰지 않고 실제 실행 결과를 누적한다. + | 일시 | 항목 | 기록 | |---|---|---| | 2026-10-06 | `P1-T1` | PRD와 plan-task 생성. | @@ -213,11 +974,89 @@ P1-T1 -> P1-T2 -> P1-T3/P1-T4 -> P1-GATE | 2026-10-06 | `P1-T3` | `CreatorChannelTalkActionSection`에 `isDmAvailable`, 한국 접속, 한국어 설정 조건의 `선물 보내기` 버튼 추가. | | 2026-10-06 | `P1-T4` | `CategoryButtonsView` 첫 항목으로 `ic_my_present` 기반 `선물함` 버튼 추가. | | 2026-10-06 | `P1-T5` | 크리에이터 상세 `선물 보내기` 버튼에서 아이콘 제거, 타인 채널 조건 추가. | -| 2026-10-06 | `P1-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -list` 성공. 스킴 `SodaLive`, `SodaLive-dev` 확인. Xcode 기기 build number 경고는 있었으나 명령은 성공. | -| 2026-10-06 | `P1-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` 성공. 기존 중복 Compile Sources 등 경고는 유지. | +| 2026-10-06 | `P1-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -list` 성공. 스킴 `SodaLive`, `SodaLive-dev` 확인. | +| 2026-10-06 | `P1-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` 성공. 기존 경고는 유지. | | 2026-10-06 | `P1-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 성공. 일부 dependency scan 경고는 유지. | | 2026-10-06 | `P1-GATE` | `git diff --check` 성공. | -| 2026-10-06 | `P1-GATE` | iOS 실기기/시뮬레이터 화면 캡처 기반 visual QA는 현재 실행 환경에 렌더링/캡처 도구가 없어 미수행. 대신 변경 파일 직접 확인과 빌드 검증으로 대체. | -| 2026-10-06 | `P1-T5` | `git diff --check` 성공. | +| 2026-10-06 | `P1-GATE` | iOS 실기기/시뮬레이터 화면 캡처 기반 visual QA는 미수행. 변경 파일 직접 확인과 빌드 검증으로 대체. | | 2026-10-06 | `P1-T5` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 성공. | | 2026-10-06 | `P1-T5` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` 최초 병렬 실행은 DerivedData `build.db` 잠금으로 실패했고, 단독 재실행은 성공. | +| 2026-10-06 | 문서 보강 | 약관 Notion URL과 API 계약 확정 답변을 반영해 PRD와 plan-task를 선물 사용자 기능 전체 기준으로 보강. | +| 2026-10-06 | `P2-T1` | 7개 endpoint의 DTO, `GiftApi`, `GiftRepository`를 추가하고 계약·품질 리뷰 승인 및 `SodaLive-dev` Debug build, `git diff --check` 성공. 테스트 번들 부재로 compile 검증을 적용. | +| 2026-10-06 | `P2-T2` | 5단계 progress, SENT/RECEIVED 카드 문구, UTC 날짜, 사이즈 설명, 개인정보 display model 경계를 추가. `UNDELIVERABLE`은 배송지 입력/주소를 숨기도록 보정하고 계약·품질 리뷰 승인 및 Debug build 성공. | +| 2026-10-06 | `P2-T3` | 네 Gift route와 최소 route shell을 등록하고 크리에이터 `선물 보내기`, 마이 탭 `선물함` action을 연결. `SodaLive`, `SodaLive-dev` Debug simulator build와 `git diff --check` 성공, 계약·품질 리뷰 승인. | +| 2026-10-06 | `P2-T4` | 확정 약관 URL과 14개 택배사 상수를 양 타깃에 등록하고, 주소 검색 wrapper 부재(`addressSearchAvailable = false`), 문의/피드백 `AppStep.serviceCenter`, 외부 정책 URL 부재 시 `준비 중입니다.` fallback을 상수로 고정. `SodaLive-dev` Debug build와 `git diff --check` 성공. | +| 2026-10-06 | `P2-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 성공(`BUILD SUCCEEDED`). | +| 2026-10-06 | `P2-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build` 성공(`BUILD SUCCEEDED`). 기존 중복 Sources 및 script output 경고는 유지. | +| 2026-10-06 | `P2-GATE` | `git diff --check` 성공. 크리에이터 `giftSend`, 마이 탭 `giftBoxList`, 약관 URL·14개 courier 상수를 코드 경로로 대조해 Phase 2 완료. | +| 2026-10-06 | `P3-T1` | `GiftSendViewModel`에 recipient context 검증, form-options 로딩, 첫 size 기본 선택, category 미선택, loading/error 상태를 구현하고 `GiftSendView.onAppear`에 연결. 사양·품질 리뷰 승인, `SodaLive-dev` Debug build와 `git diff --check` 성공. 테스트 번들 부재로 compile 검증 적용. | +| 2026-10-06 | `P3-T2` | Figma `2481:18904`, `2481:18957` 기준 선물 보내기 폼, 고정 navigation, scroll 본문, bottom CTA, 조건부 면책 동의, 확정 약관 링크, 수동 주소 입력을 구현. 사양·품질 리뷰 승인, 양 스킴 Debug simulator build와 `git diff --check` 성공. 런타임 픽셀 QA는 미수행. | +| 2026-10-06 | `P3-T3` | 필수값·약관·조건부 면책 기반 `canSubmit`, 상세주소 비필수/요청 포함, 11개 request mapping, submit 중 중복 차단을 구현. 사양·품질 리뷰 승인 및 focused 임시 실행·Debug build 성공. | +| 2026-10-06 | `P3-T4` | `POST /api/v2/gifts` 성공 시 `applicationNo` 상세로 guarded replace 이동하고 실패 시 입력 유지·submit 상태 복구·서버 메시지 표시를 구현. 제출 중 back 차단과 현재 신청 route 검증으로 지연 응답 navigation race를 방지. 사양·품질 리뷰 승인. | +| 2026-10-06 | `P3-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 성공(`BUILD SUCCEEDED`), `git diff --check` 성공. form-options, validation guard, createGift, guarded replace 경로를 대조. 실서버 신청·기기 UI 조작은 미수행. | +| 2026-10-06 | `P4-T1` | `GiftBoxListViewModel`에 `type=ALL`, page 0/size 20 최초 조회, `hasNext` pagination, refresh, 중복 요청 차단, 실패 시 기존 목록 보존을 구현. 사양·품질 리뷰 승인, `SodaLive-dev` Debug build와 `git diff --check` 성공. 테스트 타깃 부재로 compile 검증 적용. | +| 2026-10-06 | `P4-T2` | 기존 `GiftDisplayPolicy`의 SENT/RECEIVED 상태 매핑을 재사용하는 `GiftBoxCardView`를 구현. 프로필 fallback, 날짜, 상태·메타 문구, 전체 카드 tap과 접근성 label을 적용하고 양 타깃 Debug build, `git diff --check`, 사양·품질 리뷰 승인. 테스트 타깃 부재로 compile·정적 검증 적용. 공용 `appFont`의 시스템 폰트 Dynamic Type 제한은 기존 잔존 위험으로 분류. | +| 2026-10-06 | `P4-T3` | Figma 보낸/받은/받은-empty 상태를 기준으로 안내 배너, creator empty와 의견 버튼, 카드 목록, refresh, pagination footer를 구현. section title과 별도 파란 배너는 제외하고 카드 내부 배송지 필요 상태를 유지. invalid raw tail에서도 다음 page를 조회하도록 end sentinel을 분리하고 native refresh가 실제 요청 완료를 기다리도록 보정. 양 타깃 Debug build와 `git diff --check` 성공, 사양·품질 리뷰 승인. 실기기 visual QA는 미수행. | +| 2026-10-06 | `P4-T4` | 모든 카드가 자신의 `applicationNo`로 공통 `.giftDetail` route를 호출하도록 연결됨을 확인. pagination footer는 카드 Button 밖에 있고 end sentinel은 hit testing·접근성에서 제외돼 카드 tap과 충돌하지 않음을 정적 검증. 사양 리뷰 승인 및 양 타깃 Debug build 성공. | +| 2026-10-06 | `P4-GATE` | `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug -destination "generic/platform=iOS" CODE_SIGNING_ALLOWED=NO build` 성공(`BUILD SUCCEEDED`), `git diff --check` 성공. `type=ALL&page=0&size=20`, direction display mapping, 카드별 상세 route를 코드 경로로 대조. 로그인·실서버·시뮬레이터 screenshot 기반 visual QA는 미수행. | +| 2026-10-06 | `P5-T1` | `GiftDetailViewModel`이 신청번호로 상세 API를 조회하고 승인된 개인정보 display policy를 통해 SENT, RECEIVED, SENT+CANCELED render state를 생성하도록 구현. 최초 loading/error/retry와 후속 재조회 실패 시 기존 성공 상태 유지·toast를 연결. focused 임시 driver 검증, 사양·품질 리뷰 승인, `SodaLive-dev` Debug build와 `git diff --check` 성공. | +| 2026-10-06 | `P5-T2` | `statusTimeline` 기반 5단계 progress와 direction별 개인정보-safe 공통 정보 section을 구현. `UNDELIVERABLE`, `CANCELED`에서는 progress를 숨기고, 카드에는 고정 화면 폭 대신 `maxWidth: .infinity`와 기존 좌우 padding을 적용해 기기 가용 폭을 채우도록 처리. Figma metadata geometry 대조와 사양·품질 리뷰 승인, `SodaLive-dev` generic iOS Debug build(`BUILD SUCCEEDED`) 및 `git diff --check` 성공. 실기기·시뮬레이터 screenshot 기반 visual QA는 미수행. | +| 2026-10-06 | `P5-T3` | `SentGiftDetailContentView`를 추가해 보낸 선물의 상태별 hero, 5단계 progress, 선물 정보, 선택적 보내는 사람·사서함, 전달 불가 사유/폐기 안내를 구현. `recipientAddress`는 sent display model에 없고 `senderInfo`·`mailbox`는 각각 nil이면 섹션 전체를 숨긴다. 전달 불가 사유 focused RED/GREEN과 `recipientAddress` compile-fail probe, Figma 원본 문구 대조, 사양·개인정보·정적 시각 리뷰 승인, `SodaLive-dev`·`SodaLive` generic iOS Debug build 및 `plutil -lint`, `git diff --check` 성공. 로그인·실서버 기반 현재 빌드 screenshot/VoiceOver visual QA는 미수행. | +| 2026-10-06 | `P5-T4` | `GiftDetailActionBar`를 추가해 `trackingRequired` 운송장 CTA, SENT+RECEIVED 취소 action, 전달 불가 문의/정책 fallback을 조건부 표시. 리뷰 경로 부재로 리뷰 action은 숨기고, 취소는 P5-T5 연결 전 nil/disabled seam으로 유지. 정책 toast를 화면 루트에 배치하고 이동 버튼 공유 잠금으로 연속 탭 중복 push를 방지. 사양·라우팅·품질 재리뷰 승인, `SodaLive-dev`·`SodaLive` generic iOS Debug build 및 `plutil -lint`, `git diff --check` 성공. 운송장 대상 화면 구현은 Phase 6이며 런타임 screenshot/VoiceOver 검증은 미수행. | +| 2026-10-06 | `P5-T5` | `SodaV2ActionModal` 확인 이후에만 취소 API를 호출하고, 중복 제출·GET/POST 중첩을 차단하며 실패 시 dialog와 기존 상세를 유지해 오류를 표시하도록 구현. 성공은 동일 신청번호 상세 route를 guarded replace하고, 거부 시 잠금을 풀고 권위 GET으로 복구한다. 취소 완료 화면은 크리에이터·사이즈·카테고리·신청번호·신청일·취소일·환불 캔만 allowlist로 표시. 실제 production ViewModel 기반 임시 harness 75개 assertion, Figma 원본 대조, 기능·상태 전이·정적 시각 리뷰 승인, 양 스킴 generic iOS Debug build와 `plutil -lint`, `git diff --check` 성공. 실서버 read-after-write·screenshot·VoiceOver QA는 미수행. | +| 2026-10-06 | `P5-GATE` | SENT 개인정보 경계, `trackingRequired` CTA, confirm 이후 cancel POST, CANCELED 완료 UI를 코드·focused harness로 대조. `SodaLive-dev`·`SodaLive` generic iOS Debug build 성공. 서버 취소 성공 직후 GET 일관성과 로그인 기반 실제 취소 flow는 최종 검증 잔여 항목으로 유지. | +| 2026-10-06 | `P6-T1` | 진입 시 상세를 재조회하고 SENT·비취소·`trackingRequired=true`·`mailbox != nil`일 때만 등록 가능 상태를 공개하도록 구현. `trackingRequired=false` 등 stale route는 toast 종료 후 동일 신청번호 상세로 guarded replace하고, mailbox 누락은 `받을 주소가 등록되어 있지 않습니다.`와 재시도를 유지. 실제 production ViewModel 기반 RED/GREEN harness 56개 assertion, 양 스킴 generic iOS Debug build, `plutil -lint`, Swift parse, `git diff --check` 성공. 기능 재리뷰 `APPROVE`, 정적 UI 재리뷰 `APPROVED_STATIC`. 로그인·실서버·screenshot·VoiceOver QA는 미수행. | +| 2026-10-06 | `P6-R1` | 비활성 중 toast dismissal 뒤 복귀가 유실되지 않도록 재등장 시 pending callback을 소비하고, 긴 오류·Dynamic Type에서도 재시도에 접근하도록 오류 영역을 scroll 처리. 수정 후 양 스킴 generic iOS Debug build와 독립 재리뷰 승인. | +| 2026-10-07 | `P6-T2` | 신청 요약, 보내실 주소, 14개 택배사 선택, 운송장 번호 입력, 5개 안내 문구와 반응형 bottom action bar를 구현. 실제 production ViewModel harness 71개 assertion, 양 스킴 generic iOS Debug build, `plutil -lint`, Swift parse, `git diff --check` 성공 및 기능 리뷰 승인. 현재 Figma desktop 활성 문서에서 node `2481:19066`, `2481:19109` 재조회가 실패해 fresh 원본 대조와 로그인 기반 screenshot·VoiceOver QA는 미수행 증거 공백으로 유지. | +| 2026-10-07 | `P6-T3` | 택배사·공백 운송장 validation, 운송장 등록과 확인된 신청 취소 POST, 공통 중복 제출 잠금, 등록 성공 toast 후 상세 이동, 취소 성공 즉시 상세 이동과 guarded replace 거부 시 권위 GET 복구를 구현. production ViewModel focused check에서 의도한 RED 후 62개 assertion 성공, Swift parse, `plutil -lint`, `git diff --check`, 양 스킴 generic iOS Debug build 성공 및 독립 기능 리뷰 `APPROVE`. 인증 실서버의 POST 직후 상세 일관성과 실제 toast·alert lifecycle은 최종 runtime 검증 잔여 항목으로 유지. | +| 2026-10-07 | `P7-T1` | 기존 `ReceivedGiftDetailDisplayModel`의 allowlist 개인정보 경계를 production-source check로 고정하고, 안전 모델만 받는 받은 상세 상태명·progress·선물 정보 component를 양 타깃에 연결. component 부재 RED 후 privacy 16 assertions, 허용 멤버 probe, `senderInfo`·`mailbox`·`applicationNo`·`paidCan`·`tracking` 5개 compile-fail probe와 foundation GREEN 성공. runner fail-open 리뷰 항목은 `set -euo pipefail`과 실패 exit 42 주입으로 수정·재승인. 양 스킴 generic iOS Debug build, Swift parse, `plutil -lint`, `git diff --check` 성공, 기능 리뷰 `APPROVE`, 정적 UI 리뷰 `APPROVED_STATIC`. 로그인 기반 screenshot·큰 글자·VoiceOver runtime QA는 미수행. | +| 2026-10-07 | `P7-T2` | `recipientAddressRequired=true`에서 개인정보 안내, privacy-safe 전달 예정 선물, 이름·휴대폰·우편번호·주소·상세주소 수동 입력, 독립 약관 2개와 비활성 bottom CTA를 표시하고 false에서는 기존 받은 상세를 유지. 확정 Notion URL은 기존 `GiftAgreementRow`를 재사용. privacy/foundation/recipient-form focused 검사, Swift parse, `plutil -lint`, `git diff --check`, 양 스킴 generic iOS Debug build 성공 및 기능 리뷰 `APPROVE`, 정적 UI 리뷰 `APPROVED_STATIC`. Figma node `2531:36290` 재조회 실패로 exact copy·pixel 대조와 로그인 기반 keyboard·VoiceOver QA는 미수행. | +| 2026-10-07 | `P7-T3` | 필수 4개 입력과 약관 2개 기반 validation, 선택 상세주소, 요청 시 양끝 공백 제거, 중복 POST 차단, 실패 입력 유지·오류 toast, 성공 toast 종료 후 동일 상세 ViewModel의 권위 GET을 구현. GET 실패 시 POST를 반복하지 않는 상세 재조회만 제공한다. production-source RED/GREEN harness 266 assertions, recipient-form 구조 검사, Swift parse, 양 스킴 generic iOS Debug build 성공. 독립 기능 리뷰 `APPROVE`, 정적 UI 리뷰 `APPROVED_STATIC`. 인증 실서버 read-after-write와 screenshot·keyboard·VoiceOver runtime QA는 미수행. | +| 2026-10-07 | `P7-T4` | 받은 상세에 배송지 입력 완료·사서함 도착·검수 완료·전달 완료·전달 불가 상태 hero를 구현하고, 정상 상태는 5단계 progress·privacy-safe 선물 정보·optional 받는 주소를 표시한다. `UNDELIVERABLE`은 응답 주소를 강제로 제거하고 hero·optional 사유·폐기 안내·문의/정책 fallback만 표시한다. production-source privacy 19 assertions와 금지 필드 5개 compiler probe, received-states 구조 검사, Swift parse, `git diff --check`, 양 스킴 generic iOS Debug build 성공. 독립 기능 리뷰 `APPROVE`, 정적 UI 리뷰 `APPROVED_STATIC`. Figma·인증 runtime screenshot·큰 글자·VoiceOver QA는 미수행. | +| 2026-10-07 | `P7-GATE` | received display allowlist, 배송지 입력 분기·validation·POST·권위 GET, null 주소 섹션 제거, `UNDELIVERABLE` form/address/progress/gift 숨김과 문의/정책 fallback을 코드·focused checks로 대조. privacy 19 assertions, 제출 266 assertions, foundation/recipient-form/received-states 검사, 양 스킴 generic iOS Debug build와 `git diff --check` 성공. 아래 수동 검증은 인증 실서버와 현재 화면 캡처 부재로 미완료 상태를 유지한다. | +| 2026-10-07 | `P8-T1` | `deepLinkValue=GIFT_DETAIL`과 양끝 공백 제거 후 비어 있지 않은 문자열 `deepLinkId`를 `applicationNo`로 파싱한다. splash에서는 pending action으로 보관하고 일반 진입에서는 기존 공통 `.giftDetail(applicationNo:)` route를 사용한다. SENT/RECEIVED를 미리 선택하지 않고 상세 API 결과로 분기한다. `AppStep.swift` 수정 없이 기존 route를 재사용. production-source Swift harness 결과 `PASS AppDeepLinkGiftDetailCheck assertions=14`. | +| 2026-10-07 | `P8-T2` | fresh 회귀 검증: received privacy 19 assertions와 금지 멤버 5개 compiler probe, recipient address 266 assertions, foundation·recipient-form·received-states 검사 성공. `SodaLive`·`SodaLive-dev` generic iOS Debug build 각각 `BUILD SUCCEEDED`, Swift parse·shell parse·`git diff --check` 성공. PRD §14의 기능·개인정보 항목은 자동/정적 근거와 runtime 공백을 `reviews/phase8-gift-user-flow.md`에 구분해 기록했고, 추적성은 기존 Phase/Task와 Progress에 연결했다. 인증 환경 부재로 §14의 실제 신청·목록·딥링크·상세·mutation·취소 journey는 모두 미수행이며 Figma·screenshot·VoiceOver·keyboard·Dynamic Type 검증도 미수행이다. PRD 체크박스는 변경하지 않았다. | +| 2026-10-07 | `P8-GATE` | Oracle `APPROVE`, Critical/Important 발견 사항 없음. focused checks와 양 스킴 build 및 정적 검사 근거로 Phase 8 코드·정적 gate 승인. 실제 푸시 수신, 인증 서버 응답과 POST 이후 상세 일관성, navigation/replace runtime은 검증하지 않았으므로 전체 인증 runtime 사용자 흐름 gate는 미완료다. Phase 8 수동 체크박스 8개를 모두 미완료로 유지하며, 인증 실행 환경 확보 후 재개한다. | +| 2026-10-07 | `P2-R1` | 사용자 제공 response fixture에서 `categories[].name`을 DTO가 `categoryName`으로 요구해 발생한 `keyNotFound`를 RED로 재현. `sizes[].name`, `categories[].name`, `statusTimeline[].statusName`을 계약에 맞추고 UI 참조를 갱신했다. 계약 harness 21 assertions, received privacy 19 assertions와 금지 멤버 5개 probe, recipient address 266 assertions, foundation·recipient-form·received-states 검사, Swift parse, `git diff --check`, 양 스킴 generic iOS Debug build가 성공했다. 원인 확인용 진단 로그·스크립트·journal은 제거했다. 인증 실서버 재현은 미수행이다. | +| 2026-10-07 | `P2-R2` | 인증 runtime에서 HTTP 200 응답의 `data.giftInfo.applicationNo=null` 때문에 `valueNotFound(String)`이 발생함을 확인하고, null/string toggle로 원인을 재현했다. null contract fixture와 top-level/nested 불일치 mapping RED를 각각 확인한 뒤 nested DTO를 optional로 변경하고 sent detail은 top-level 신청번호를 사용하도록 수정했다. 계약 harness 22 assertions, received privacy/display 21 assertions와 금지 멤버 5개 probe, recipient address 266 assertions, deep link 14 assertions, foundation·recipient-form·received-states 검사, Swift parse, `git diff --check`, 양 스킴 generic iOS Debug build가 성공했다. 수정 빌드의 인증 실서버 card tap 재확인은 잔여 항목이다. | +| 2026-10-07 | `P2-R3` | 전달 불가 상태의 발신·수신 `선물 정책 확인` action을 주소 입력 약관 `(필수)`과 같은 `GiftConstants.termsURL` 시스템 URL 열기로 연결하고 기존 `준비 중입니다.` toast fallback을 제거했다. source 검사 RED 후 received-states, privacy 21 assertions, recipient address 266 assertions, 계약 22 assertions, deep link 14 assertions, Swift/shell parse, `git diff --check`, 양 스킴 generic iOS Debug build가 성공했다. 인증 runtime에서 실제 Safari 전환 확인은 미수행이다. | +| 2026-10-07 | `P9-GATE` | 내부 Kakao postcode bridge와 공용 readonly 주소 입력, compact category를 구현하고 size source를 보존했다. 리뷰에서 확인한 주소 재선택 상세주소 잔존과 Figma 라벨·버튼 문구를 별도 RED/GREEN으로 수정했다. focused 검사와 15 payload assertions, recipient 266 assertions, received/decoding/deep-link 회귀, Swift/shell parse, project lint, `git diff --check`, 양 스킴 generic iOS Debug build가 성공했다. 독립 재리뷰는 `APPROVE`, `APPROVE_STATIC`. 실제 Kakao·인증 screenshot·접근성 runtime QA는 미수행이다. | +| 2026-10-08 | `P10-T4/P10-T5 계획` | 사용자 선택과 번호체계 조사에 따라 휴대폰 제출을 `010` ASCII 숫자 11자리, 운송장 입력/제출을 ASCII 숫자 최대 14자/10~14자리로 확정했다. 기존 request·API·mutation/navigation과 선물 사이즈 선택은 변경하지 않는다. | +| 2026-10-08 | `P10-T4` | contact test에서 짧은 번호가 제출되는 기존 동작과 recipient ViewModel의 `0` 허용을 각각 RED로 확인했다. 공용 `phoneNumber.acceptsSubmission`을 `010` ASCII 숫자 11자리로 제한한 뒤 contact 80 assertions와 recipient 280 assertions가 GREEN이다. 편집 중 빈 값과 최대 11자리 정책은 유지했다. | +| 2026-10-08 | `P10-T5` | tracking harness에서 9자리 `123456789`가 제출되는 기존 동작을 RED로 확인했다. 편집은 ASCII 숫자 최대 14자, 제출은 10~14자리인 순수 predicate를 guarded `Binding`과 `canSubmit`에서 재사용했다. 142 assertions로 경계, 잘못된 값의 POST 차단, leading zero, 등록·실패·취소·navigation 회귀가 GREEN이다. | +| 2026-10-08 | `P10-R1` | 정적 리뷰 `REV-P10-001`에서 연락처 UIKit 필드의 Pretendard 고정이 언어별 폰트 정책을 우회함을 확정했다. source contract RED 후 기존 `FontModifier` resolver를 재사용하는 `.body3.localizedFontName()` bridge로 수정했다. contact 80 assertions와 localized typography contract, recipient 280 assertions, tracking 142 assertions, Swift/shell parse, project lint, `git diff --check`, 양 스킴 generic iOS Debug build가 성공했고 재리뷰는 `APPROVE_STATIC`이다. | +| 2026-10-08 | `P10-GATE` | 기능 독립 리뷰 `APPROVE`, 폰트 회귀 수정 후 정적 UI 재리뷰 `APPROVE_STATIC`. 실제 한글 조합, 12/13자, 휴대폰 11/12자, 운송장 9/10/14/15자 입력·붙여넣기, keyboard·Dynamic Type·VoiceOver·screenshot QA는 부팅된 인증 실행 환경 부재로 미수행이며 runtime 체크박스를 미완료로 유지한다. | + +## Decision Log + +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 | +|---|---|---|---|---|---| +| 2026-10-06 | `DEC-001` | 확정 | 기존 선물 진입점 문서를 이어서 보강한다. | 문서 정책의 연속 작업 규칙 | PRD, 전체 plan | +| 2026-10-06 | `DEC-002` | 확정 | 약관 링크는 `https://app.notion.com/p/3eb27b359673802b937ae741ab7b4ad3`를 사용한다. | 사용자 제공 | `P2-T4`, `P3-T2`, `P7-T2` | +| 2026-10-06 | `DEC-003` | 확정 | API 계약은 사용자 프롬프트의 endpoint/request/response를 기준으로 한다. | 사용자 답변 A | `P2-T1` 이후 전체 | +| 2026-10-06 | `DEC-004` | 확정 | 구현은 신규 V2 선물 feature 폴더에 두고 기존 component/navigation/API 패턴을 재사용한다. | AGENTS.md, code-style.md, codegraph 확인 | 전체 | +| 2026-10-06 | `DEC-005` | 확정 | 선물함 배송지 미입력 상태는 별도 파란 배너 없이 각 item 카드 안에서 표시한다. | 사용자 정정 요청 | `P4-T3` | +| 2026-10-07 | `DEC-006` | 확정 | 폼 옵션의 size/category 표시명은 `name`, 상세 timeline 표시명은 `statusName`으로 decode한다. | 사용자 제공 endpoint response 계약과 fixture RED | `P2-R1`, `P3-T1`, `P5-T1` | +| 2026-10-07 | `DEC-007` | 확정 | 상세의 nested `giftInfo.applicationNo`는 nullable로 decode하고, 화면 신청번호는 top-level `data.applicationNo`를 사용한다. | 인증 runtime `valueNotFound`와 null/string toggle | `P2-R2`, `P5-T1` | +| 2026-10-07 | `DEC-008` | 확정 | 전달 불가 상세의 정책 버튼은 약관 `(필수)` 링크와 동일하게 `GiftConstants.termsURL`을 시스템 브라우저로 연다. | 사용자 확인 | `P2-R3`, `P5-T4`, `P7-T4` | +| 2026-10-08 | `DEC-009` | 확정 | 휴대폰 제출은 `010`으로 시작하는 ASCII 숫자 11자리만, 운송장 제출은 ASCII 숫자 10~14자리만 허용한다. | 사용자 선택과 국내 번호체계 조사 | `P10-T4`, `P10-T5`, PRD `SEND-007`, `TRACK-007`, `RECV-007` | + +## 발견된 문제 + +| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 | +|---|---|---|---|---|---| +| `ISSUE-001` | Low | 후보 | 현재 저장소는 테스트 번들 타깃이 확인되지 않아 계획의 RED/GREEN 자동 테스트 일부가 실행 불가할 수 있다. | 전체 | 가능한 pure logic test를 우선하고, 불가 시 Debug build와 수동 검증을 Progress에 기록한다. | + +## 최종 보고 형식 + +```markdown +구현 결과: 완료한 Phase 번호와 사용자가 실제로 완료할 수 있는 선물 흐름을 한 문장으로 기록한다. + +- 변경: 주요 파일과 동작을 요약한다. +- 결정: 관련 Decision Log ID와 내용을 기록한다. +- 검증: + - `실행한 명령` — 성공/실패와 핵심 수치를 기록한다. + - `수동 검증 항목` — 성공/실패/불가 사유를 기록한다. +- 남은 항목: 외부 의존, 후속 범위 또는 없음을 기록한다. +- 문서: 갱신한 PRD/API Contract/plan/review 경로를 기록한다. +``` diff --git a/docs/20261006_선물_UI_추가/prd.md b/docs/20261006_선물_UI_추가/prd.md index 68bd147f..8ba9bf2d 100644 --- a/docs/20261006_선물_UI_추가/prd.md +++ b/docs/20261006_선물_UI_추가/prd.md @@ -1,151 +1,365 @@ -# 선물 UI 추가 PRD +# 크리에이터 선물하기 사용자 기능 PRD ## 문서 정보 | 항목 | 내용 | |---|---| -| 문서 상태 | 구현 완료 | +| 문서 상태 | 구현 기준 확정 | | 작성일 | `2026-10-06` | -| 최종 수정일 | `2026-10-06` | -| 대상 제품 | 크리에이터 상세 선물 버튼, 마이 탭 선물함 버튼 | +| 최종 수정일 | `2026-10-08` | +| 대상 제품 | 크리에이터 선물 보내기, 선물함, 선물 상세, 운송장 등록, 배송지 입력, 선물 딥링크 | | 작성자·결정권자 | 사용자 요청 기준 | -| 관련 API Contract | 해당 없음 | +| 관련 API Contract | 이 문서 §11 API 계약 | | 관련 구현 계획 | `docs/20261006_선물_UI_추가/plan-task.md` | | 관련 review | 없음 | +### 요구사항 상태 + +| 상태 | 의미 | 구현 처리 | +|---|---|---| +| 확정 | 제품·기술 결정이 완료되어 구현 기준으로 사용 | `plan-task.md`의 Phase/Task와 완료 증거로 추적 | +| 제외 | 현재 릴리스에서 구현하지 않기로 결정 | Non-Goals와 Decision Log에 기록 | + ## 1. Overview -한국에서 접속하고 앱 언어가 한국어인 사용자가 크리에이터 상세와 마이 탭에서 선물 관련 진입점을 볼 수 있게 한다. 이번 범위는 UI 진입점 추가까지만 포함하며 실제 선물 보내기/선물함 화면 이동은 후속 작업에서 연결한다. +크리에이터 선물하기는 팬이 크리에이터에게 오프라인 선물을 신청하고, 소다라이브 사서함과 검수 과정을 거쳐 안전하게 전달되도록 돕는 iOS 사용자 기능이다. 팬은 선물 신청, 운송장 등록, 신청 취소, 보낸 선물 상세 조회를 수행하고, 크리에이터는 받은 선물 목록 확인, 배송지 입력, 전달 상태 확인을 수행한다. + +기존 선물 진입점 UI는 Phase 1에서 구현 완료됐다. 이번 문서는 해당 진입점을 실제 선물 보내기/선물함/상세/딥링크 흐름에 연결하는 후속 구현 기준이다. ## 2. Problem Statement -- 선물 기능 연결 전에도 사용자가 진입점을 인지할 수 있는 UI가 필요하다. -- 크리에이터 상세의 선물 진입점은 현재 `DM 보내기`와 같은 레벨의 액션으로 보여야 한다. -- 마이 탭의 선물함은 기존 보관함, 차단목록 등 카테고리 버튼과 같은 UI 레벨에 있어야 한다. +- 현재 iOS에는 선물 기능 진입점만 있고 실제 신청, 목록, 상세, 운송장, 배송지 입력 흐름이 연결되어 있지 않다. +- 선물 기능은 팬과 크리에이터의 개인정보 노출 경계가 다르므로, `direction`별 UI 분기와 표시 금지 필드를 명확히 고정해야 한다. +- 신청 성공, 목록 item, 푸시/딥링크가 서로 다른 상세 화면으로 흩어지면 상태 분기와 개인정보 노출 회귀 위험이 커진다. -문제를 해결했다는 판단은 조건에 맞는 사용자에게 두 진입점이 기존 UI 규칙대로 노출되고, 탭해도 아직 별도 기능으로 이동하지 않는 것으로 한다. +문제를 해결했다는 판단은 모든 진입점이 같은 상세 조회 흐름을 사용하고, API 계약·Figma 구조·개인정보 표시 금지 조건이 검증되는 것으로 한다. ## 3. Goals ### 3.1 제품 목표 -- 크리에이터 상세에서 `DM 보내기` 노출 조건과 동일한 크리에이터에 대해 한국 접속, 한국어 설정, 타인 채널 조건일 때 `선물 보내기` 버튼을 표시한다. -- 마이 탭 카테고리 그리드의 첫 항목으로 `선물함` 버튼을 표시한다. -- 후속 기능 연결을 위해 탭 액션 위치만 남기고 실제 화면 이동/API 호출은 하지 않는다. +- 팬이 크리에이터 상세에서 선물 신청을 완료하고, 신청 완료 후 같은 신청번호의 상세로 이동한다. +- 사용자가 선물함에서 보낸 선물과 받은 선물을 `type=ALL` 목록으로 확인하고 상세로 진입한다. +- 보낸 팬은 운송장 등록과 신청 취소를 수행할 수 있다. +- 받은 크리에이터는 필요한 경우 배송지를 입력하고 전달 상태를 확인할 수 있다. +- 푸시/딥링크, 리스트 item, 신청 완료 이동이 모두 `GET /api/v2/gifts/{applicationNo}` 기반 상세 화면을 사용한다. ### 3.2 UX 목표 -- `선물 보내기` 버튼은 `DM 보내기`와 같은 형태, 간격, 텍스트 스타일을 사용한다. -- `선물함` 버튼은 기존 카테고리 버튼과 같은 4열 그리드, 아이콘 박스, 텍스트 스타일을 사용한다. +- 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, 데이터 모델 구현은 제외한다. -- 신규 디자인 시스템, 공용 컴포넌트 추출, 기존 버튼 리팩터링은 제외한다. -- 새 의존성 추가와 프로젝트 설정 변경은 제외한다. +- 관리자 화면. +- 새 디자인 시스템 생성. +- 외부 택배 조회 또는 운송장 실시간 검증. +- 약관 본문 제공 API 신규 구현. +- 선물 이미지, 첨부파일, 메시지 카드. +- 임의 URL 연결. 정책/문의/리뷰 등 기존 앱 이동 경로가 없으면 숨기거나 `준비 중입니다.` toast만 사용한다. +- API에 없는 취소일 기준 재정렬, 사이즈 설명 서버 필드 추정, 오류 status/key 추정. ## 5. Target Users and Permissions | 사용자 | 목표 | 주요 작업 | 사용 환경 | |---|---|---|---| -| 한국 접속 및 한국어 설정 사용자 | 크리에이터에게 선물하거나 선물함을 확인할 진입점을 인지 | 버튼 확인 및 탭 | iOS 앱 | +| 팬 | 크리에이터에게 선물을 신청하고 진행 상태를 확인 | 선물 신청, 선물함 조회, 운송장 등록, 신청 취소, 상세 조회 | iOS 앱 | +| 크리에이터 | 팬이 보낸 선물을 받을 수 있게 배송지를 입력하고 상태를 확인 | 선물함 조회, 배송지 입력, 상세 조회, 문의 | iOS 앱 | -권한과 인증 흐름은 이번 범위에서 변경하지 않는다. `선물 보내기` 버튼 노출은 기존 `DM 보내기` 노출 조건인 `creator.isDmAvailable`을 재사용한다. +권한은 기존 인증 토큰과 서버 리소스 소유권 검증을 따른다. 권한 오류 또는 미존재 오류는 기존 앱의 딥링크/상세 오류 처리 패턴을 사용한다. ## 6. 핵심 사용자 흐름 -1. 사용자가 크리에이터 상세에 진입한다. -2. 앱이 내 채널 여부, `creator.isDmAvailable`, 접속 국가, 앱 언어를 기준으로 `선물 보내기` 버튼 노출 여부를 결정한다. -3. 조건이 맞으면 `DM 보내기` 오른쪽에 같은 형태의 `선물 보내기` 버튼이 보인다. -4. 로그인 사용자가 마이 탭에 진입하면 카테고리 그리드 첫 항목에 `선물함` 버튼이 보인다. -5. 두 버튼의 탭 액션은 후속 구현 전까지 아무 동작도 하지 않는다. +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 -크리에이터 상세 헤더 액션 영역 - DM 보내기 - 선물 보내기 +크리에이터 상세 + 선물 보내기 버튼 + -> GiftSendView(recipientMemberId, recipientCreatorNickname) + -> 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) + -> GiftDetailView(applicationNo) ``` -신규 route는 만들지 않는다. +신규 route는 기존 `AppStep`/`AppState`/`ContentView` 방식에 맞춰 추가한다. 신청 성공과 취소 성공은 뒤로가기로 직전 입력/등록 화면에 돌아가지 않도록 `replaceCurrentAppStep` 또는 동등한 stack 정리 정책을 사용한다. ## 8. 기능 요구사항 +### 8.1 기존 진입점 + | ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | |---|---|---|---|---| -| `GIFT-001` | 확정 | 크리에이터 상세에 `선물 보내기` 버튼을 추가한다. | 내 채널이 아니고 `creator.isDmAvailable == true`, 한국 접속, `LanguageHeaderProvider.current == "ko"`일 때만 `DM 보내기` 오른쪽에 표시된다. | `P1-T2` | -| `GIFT-002` | 확정 | 한국 접속 판정은 기존 패턴을 사용한다. | `UserDefaults.countryCode`를 trim/uppercase 후 빈 값 또는 `KR`이면 한국 접속으로 본다. | `P1-T2` | -| `GIFT-003` | 확정 | 한국어 설정 판정은 기존 언어 헤더를 사용한다. | `LanguageHeaderProvider.current == "ko"`일 때 한국어 설정으로 본다. | `P1-T2` | -| `GIFT-004` | 확정 | 마이 탭 카테고리 그리드 첫 항목에 `선물함`을 추가한다. | 4열 설정은 유지되고, `ic_my_present` 아이콘과 `선물함` 텍스트가 보관함 앞에 표시된다. | `P1-T3` | -| `GIFT-005` | 확정 | 두 버튼의 탭 액션은 후속 구현 전까지 inert placeholder로 둔다. | 탭해도 화면 이동, API 호출, 상태 변경이 없다. | `P1-T2`, `P1-T3` | -| `GIFT-006` | 확정 | 크리에이터 상세의 `선물 보내기` 버튼은 아이콘 없이 텍스트만 표시한다. | 캡슐 스타일은 유지하되 `ic_my_present` 등 아이콘을 포함하지 않는다. | `P1-T2` | +| `GIFT-001` | 확정 | 크리에이터 상세에 `선물 보내기` 버튼을 표시한다. | 내 채널이 아니고 `creator.isDmAvailable == true`, 한국 접속, `LanguageHeaderProvider.current == "ko"`일 때만 표시된다. | `P1-T3` | +| `GIFT-002` | 확정 | 마이 탭 카테고리 그리드 첫 항목에 `선물함`을 표시한다. | `ic_my_present` 아이콘과 `선물함` 텍스트가 보관함 앞에 표시된다. | `P1-T4` | +| `GIFT-003` | 확정 | 기존 inert action을 실제 선물 화면 이동으로 교체한다. | `선물 보내기`는 신청 화면, `선물함`은 리스트 화면으로 이동한다. | `P2-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` | + +### 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` | + +### 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` | + +### 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` | ## 9. 반응형 기능 범위 | 기능 | iPhone | iPad | 비고 | |---|---:|---:|---| -| 크리에이터 상세 버튼 | 전체 | 전체 | 기존 헤더 액션 레이아웃 유지 | -| 마이 탭 선물함 | 전체 | 전체 | 기존 4열 그리드 유지 | +| 선물 보내기 | 전체 | 전체 | iPhone 13 402px Figma 구조 우선, 기존 SwiftUI adaptive 정책 적용 | +| 선물함 | 전체 | 전체 | navigation 고정, 본문 scroll | +| 선물 상세 | 전체 | 전체 | status/action별 bottom bar safe area 유지 | +| 운송장 등록 | 전체 | 전체 | keyboard avoidance 필요 | +| 배송지 입력 | 전체 | 전체 | keyboard avoidance 필요 | +| 딥링크 | 전체 | 전체 | 권한/미존재 오류는 기존 패턴 | ## 10. UI/UX Expectations -- 크리에이터 상세 버튼은 `CreatorChannelTalkActionSection`의 기존 캡슐 버튼 스타일을 그대로 따른다. -- 마이 탭 버튼은 `CategoryButtonItem`을 그대로 사용한다. -- 새 아이콘은 기존 asset `ic_my_present`를 사용한다. -- 새 텍스트는 `I18n`에 추가해 기존 다국어 선택 방식을 따른다. +### 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 계약 -해당 없음. 신규 API 요청, 응답, 에러 계약은 이번 범위에 없다. +### 11.1 공통 규칙 + +- base URL과 인증 header는 기존 Moya `TargetType`/`UserDefaultsKey.token` 패턴을 따른다. +- 모든 응답은 공통 envelope를 사용하며 payload는 `ApiResponse.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. 성능과 품질 요구사항 - 신규 의존성을 추가하지 않는다. -- 기존 SwiftUI view 구조에서 조건부 렌더링만 추가한다. -- `xcodebuild` Debug build를 기본 검증으로 사용한다. +- 목록은 `hasNext` 기반 pagination을 사용하고 서버 응답 순서를 유지한다. +- 화면별 ViewModel은 기존 `ObservableObject`, Combine, `MoyaProvider`, `ApiResponse` 패턴을 따른다. +- 현재 테스트 번들 타깃이 확인되지 않으므로 구현 계획은 가능한 ViewModel 순수 로직 test를 우선 요구하되, 불가능한 SwiftUI visual 분기는 build와 수동 검증으로 대체한다. +- 기본 검증 명령은 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`와 `SodaLive-dev` Debug build다. ## 14. 성공 기준 ### 14.1 기능 수용 기준 -- [ ] `DM 보내기`가 보이는 타인 크리에이터 채널에서 한국 접속 및 한국어 설정이면 `선물 보내기`도 보인다. -- [ ] 내 채널에서는 `선물 보내기`가 보이지 않는다. -- [ ] `creator.isDmAvailable == false`이면 `선물 보내기`가 보이지 않는다. -- [ ] 한국 접속이 아니거나 한국어 설정이 아니면 `선물 보내기`가 보이지 않는다. -- [ ] 마이 탭 카테고리 그리드 첫 항목에 `선물함`이 보인다. -- [ ] 두 새 버튼을 탭해도 화면 이동/API 호출이 없다. +- [ ] 선물 보내기 진입 시 전달받은 닉네임과 `memberId`가 화면/API에 연결된다. +- [ ] 필수 입력/동의 누락 시 신청 API를 호출하지 않는다. +- [ ] `requiresDamageWaiver=true` 카테고리에서만 파손 면책 동의가 표시되고 필수다. +- [ ] 신청 성공 후 `applicationNo` 상세로 replace 이동하고 뒤로가기로 입력 폼에 돌아가지 않는다. +- [ ] 선물함 리스트는 `type=ALL` 한 번 조회와 `direction` 카드 분기를 사용한다. +- [ ] Figma의 `신청 내역`, `선물 내역` 섹션 타이틀이 노출되지 않는다. +- [ ] 리스트 item, 푸시 딥링크, 신청 완료 이동이 같은 상세 조회 플로우를 사용한다. +- [ ] 상세 progress는 `statusTimeline` 5단계를 사용한다. +- [ ] 운송장 등록과 배송지 입력 성공 후 상세를 재조회해 최신 상태를 표시한다. +- [ ] 신청 취소는 확인 dialog 이후에만 API를 호출하고 성공 시 취소 완료 UI로 replace 된다. -### 14.2 UI/UX 수용 기준 +### 14.2 개인정보 수용 기준 -- [ ] `선물 보내기`는 아이콘 없이 `DM 보내기`와 같은 캡슐 형태로 표시된다. -- [ ] `선물함`은 기존 카테고리 버튼과 같은 형태로 표시된다. -- [ ] 한 줄에 보이는 카테고리 개수는 기존 4열 설정 그대로다. +- [ ] `direction=SENT` 상세에서 `recipientAddress`를 표시하지 않는다. +- [ ] `direction=RECEIVED` 상세에서 `senderInfo`, `mailbox`, `giftInfo.paidCan`, `giftInfo.tracking`을 표시하지 않는다. +- [ ] `recipientAddress=null`이면 받는 주소 섹션을 통째로 숨긴다. +- [ ] `status=UNDELIVERABLE`이면 받은 상세에서 배송지 입력 UI와 받는 주소 섹션을 숨긴다. ### 14.3 추적성 완료 기준 -- [ ] 모든 확정 요구사항이 `plan-task.md`의 Task와 연결된다. -- [ ] 검증 결과가 `plan-task.md`에 누적된다. +- [ ] 모든 확정 요구사항이 `plan-task.md`의 Phase/Task와 연결된다. +- [ ] 모든 Phase Gate에 자동 또는 대체 수동 검증 기록이 누적된다. +- [ ] API 계약과 문서 요구사항이 충돌하면 Decision Log에 정정 기록을 남긴다. ## 15. Open Questions -없음. +없음. 약관 Notion URL과 API 계약은 사용자 답변으로 확정됐다. -## Decision Log +## 16. 요구사항 추적표 -| 일시 | 결정 | 근거 | -|---|---|---| -| 2026-10-06 | `선물 보내기`는 `DM 보내기` 노출 조건인 `isDmAvailable`을 재사용한다. | 사용자 답변: “해당 조건과 동일하게 설정하면 된다.” | -| 2026-10-06 | 한국 접속은 기존 `countryCode` 판정, 한국어 설정은 `LanguageHeaderProvider.current == "ko"`를 사용한다. | 기존 코드 검색 결과 `isKoreanCountry` 패턴과 언어 헤더 구조 확인 | -| 2026-10-06 | 크리에이터 상세 `선물 보내기`는 아이콘 없이 타인 채널에서만 표시한다. | 사용자 추가 요청 | +| 요구사항 범위 | API Contract | 계획 Phase | 자동 검증 | 수동 검증 | +|---|---|---:|---|---| +| `GIFT-001~003` | 해당 없음 | 1, 2 | Debug build | 진입점 표시/이동 | +| `SEND-001~007` | §11.2 | 2, 3, 10 | ViewModel validation/build | 선물 신청 journey | +| `LIST-001~005` | §11.2 | 2, 4 | pagination/build | 선물함 상태별 화면 | +| `DETAIL-001~006` | §11.2 | 2, 5 | status mapping/build | 보낸 상세/취소 | +| `TRACK-001~007` | §11.2 | 2, 6, 10 | validation/build | 운송장 등록·신청 취소 | +| `RECV-001~007` | §11.2 | 2, 7, 10 | privacy/validation/build | 받은 상세/배송지 | +| `COMMON-001~004` | §11.2 | 2, 8 | deeplink parsing/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 | + +## 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과 선물 사이즈 선택은 변경하지 않는다. + +- 요구사항 변경 시 Decision Log에 정정 행을 추가한다. +- 구현 범위가 바뀌면 이 PRD를 먼저 갱신하고 `plan-task.md`의 Phase/Task를 갱신한 뒤 코드를 수정한다. +- 기존 완료된 Phase 1 기록은 삭제하지 않는다.