From e427641915acef17737c3ab7c62f0e163aa01173 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 29 Oct 2026 12:03:17 +0900 Subject: [PATCH] =?UTF-8?q?docs(event):=20=EC=83=81=EC=84=B8=20API=20?= =?UTF-8?q?=EA=B3=84=EC=95=BD=EA=B3=BC=20=EA=B2=80=EC=A6=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=EC=9D=84=20=EB=82=A8=EA=B8=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api-contract.md | 29 +++ .../plan-task.md | 213 ++++++++++++++++++ docs/20261029_이벤트_상세_API_연동/prd.md | 89 ++++++++ 3 files changed, 331 insertions(+) create mode 100644 docs/20261029_이벤트_상세_API_연동/api-contract.md create mode 100644 docs/20261029_이벤트_상세_API_연동/plan-task.md create mode 100644 docs/20261029_이벤트_상세_API_연동/prd.md diff --git a/docs/20261029_이벤트_상세_API_연동/api-contract.md b/docs/20261029_이벤트_상세_API_연동/api-contract.md new file mode 100644 index 00000000..cc2acf12 --- /dev/null +++ b/docs/20261029_이벤트_상세_API_연동/api-contract.md @@ -0,0 +1,29 @@ +# 이벤트 상세 API Contract + +## 1. Request +`GET {BASE_URL}/api/v2/events/{id}`. 양수 `Int`(지원 iOS에서 signed 64-bit), query/body 없는 `requestPlain`이다. 0/음수는 network 없이 unavailable이다. + +토큰이 비어 있으면 Authorization을 생략하고, 있으면 `Bearer {token}`을 전달한다. 기존 MoyaProvider의 AuthPlugin/AcceptLanguagePlugin을 유지한다. 기존 `/event`, `/event/popup`의 헤더·경로는 변경하지 않는다. + +## 2. Response와 판정 순서 +1. HTTP 200이 아닌 응답은 본문과 무관하게 error다. 404나 오류 본문 모양을 추정하지 않는다. 401은 기존 AuthPlugin의 세션/main 처리를 유지한다. +2. HTTP 200에서 success=false이면 data를 파싱하지 않고 unavailable로 처리한다. localized message의 문구로 분류하지 않는다. +3. success=true이면 기존 `ApiResponse`의 단일 object가 필요하다. id는 양수이며 요청 ID와 일치해야 한다. thumbnailImageUrl은 required String이다. +4. data 누락/null/array/깨진 JSON/필수 필드 누락·타입 오류는 response error다. 이전 payload fallback은 없다. + +```json +{"success":true,"data":{"id":42,"thumbnailImageUrl":"https://example.invalid/thumb.png","detailImageUrl":null,"popupImageUrl":null,"link":"https://example.invalid/join"},"message":null,"errorProperty":null} +``` + +```json +{"success":false,"data":null,"message":"이용할 수 없는 이벤트입니다.","errorProperty":null} +``` + +기존 EventItem: id Int, thumbnailImageUrl String, detailImageUrl/popupImageUrl/link String?. 새 중복 DTO는 만들지 않는다. + +## 3. 화면·생명주기 계약 +idle/loading/loaded/unavailable/error를 구별한다. loading은 success/error/transport failure/cancel에서 종료한다. 응답은 main queue에서 반영하고, 첫 응답만 처리한다. loaded 화면 재등장은 요청하지 않는다. 이탈 시 구독 취소와 request identity 무효화로 늦은 응답을 차단한다. + +unavailable dismissal은 해당 View에서 한 번만 소비한다. 전역 AppState.back을 호출하지 않아 live sheet나 다음 route를 pop하지 않는다. 선택적인 이미지와 링크는 서로 독립적이며 둘 다 없는 유효한 object도 loaded다. popup/thumbnail을 상세 이미지로 대체하지 않는다. + +참여 URL은 absolute HTTP 또는 HTTPS와 host가 있어야 한다. 빈 값, 상대 경로, scheme-relative, file/javascript/data/기타 custom scheme은 버튼을 숨긴다. canOpenURL 후 사용자의 Button 탭에만 open한다. 다른 화면의 URL 정책은 변경하지 않는다. diff --git a/docs/20261029_이벤트_상세_API_연동/plan-task.md b/docs/20261029_이벤트_상세_API_연동/plan-task.md new file mode 100644 index 00000000..9ce91373 --- /dev/null +++ b/docs/20261029_이벤트_상세_API_연동/plan-task.md @@ -0,0 +1,213 @@ +# 이벤트 상세 API 연동 실행 계획 + +| 문서 항목 | 내용 | +|---|---| +| 상태 | 클라이언트 구현·로컬 검증 완료 | +| 작성일 | 2026-10-29 | +| 요구사항 기준 | [prd.md](prd.md) | +| API 기준 | [api-contract.md](api-contract.md) | +| 현재 Phase | 1 상세 API와 ID-only 탐색 | +| 현재 활성 Goal | 없음 | + +## 목표 +기존 진입점에서 ID로 이벤트 최신 상세를 안전하게 조회하고 참여한다. + +## 현재 상태 +| Phase | 상태 | 완료 Task | 활성/다음 Goal | 남은 조건 | +|---|---|---|---|---| +| 1 | 완료 | 3/3 | 없음 | 실화면 QA는 도구 부재로 미실행 | + +## 범위와 기술적 제약 +실제 EventApi/EventRepository/EventItem, 신규 V2 VM, 기존 상세 View 및 7개 탐색 호출만 변경한다. Swift/Combine/Moya와 기존 UI 스타일을 유지하며 dependency·테스트 타깃·공통 인증 변경은 없다. 딥링크 parser와 사용자 지시에 따른 실서버 테스트는 제외한다. 상위 실행자가 양 스킴 빌드·전체 CLI 회귀·단일 리뷰를 완료했다. + +## Phase 1 +**Phase 결과:** ID만으로 상세를 조회하고 안전한 참여 링크를 제공한다. +**선행조건:** 사용자 확정 계약, 가이드/샘플과 기존 notice CLI seam 확인 완료. + +### Task 1.1 API·상태·생명주기 +**Goal 실행 `P1-T1`:** 실제 production API/repository/VM/DTO가 응답·취소·링크 계약을 만족한다. +**완료 증거:** focused CLI RED/GREEN 및 직접 production driver 실행 결과. + +**Files:** +- Create: `work/tests/EventDetailCheck.swift`, `work/tests/check-event-detail.sh`, `SodaLive/Sources/V2/Settings/Event/EventDetailViewModel.swift` +- Modify: `SodaLive/Sources/Settings/Event/EventApi.swift`, `SodaLive/Sources/Settings/Event/EventRepository.swift` +- Consumes: 기존 `EventItem`, `ApiResponse`, `ApiResponseWithoutData`, provider plugin. +- Produces: `getEventDetail(id:)`, `EventDetailViewModel(eventId:)`, loading/loaded/unavailable/error와 일회성 dismissal 소비. + +- [x] **RED:** 실제 production 파일을 컴파일하는 notice 방식 CLI를 먼저 작성한다. +- [x] **RED 확인:** `bash work/tests/check-event-detail.sh`로 신규 인터페이스 미구현 실패를 기록한다. +- [x] **GREEN:** status-first·success-first decoding, 안전 URL, 취소/요청 중복 제어를 최소 구현한다. +- [x] **GREEN 확인:** 동일 CLI에서 요청/토큰/64-bit/비양수/nullable·malformed/HTTP/transport/main/cancel/중복 검사를 통과한다. +- [x] **REFACTOR:** 신규 중복만 정리하고 focused regression을 재실행한다. + +### Task 1.2 View·ID-only 라우팅·타깃 등록 +**Goal 실행 `P1-T2`:** 모든 기존 진입점에서 ID를 전달하고 기존 외형으로 상세를 표시한다. +**시작 조건:** P1-T1의 VM 계약. +**완료 증거:** ID-only source 보완 검사, Swift parse, pbxproj 검증. + +**Files:** +- Modify: `SodaLive/Sources/App/AppStep.swift`, `SodaLive/Sources/ContentView.swift`, `SodaLive/Sources/Settings/Event/EventDetailView.swift`, `SodaLive.xcodeproj/project.pbxproj` +- Modify: `SodaLive/Sources/Settings/Event/EventListView.swift`, `SodaLive/Sources/Main/EventPopupDialogView.swift`, `SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift`, `SodaLive/Sources/Content/Main/Banner/ContentMainBannerView.swift`, `SodaLive/Sources/Content/Main/V2/ContentMainBannerViewV2.swift`, `SodaLive/Sources/V2/Main/MainView.swift` + +- [x] **RED:** CLI 보완 검사로 payload route 잔존을 검출한다. +- [x] **RED 확인:** 기존 payload route assertion 실패를 기록한다. +- [x] **GREEN:** StateObject, BaseView/toast, scoped dismiss와 optional image/참여 Button을 연결한다. 7개 호출과 양 target membership을 갱신한다. +- [x] **GREEN 확인:** CLI 보완 검사와 `swiftc -frontend -parse`, `plutil -lint` 성공을 확인한다. +- [x] **REFACTOR:** 기존 외형·선택·인증 분기의 불필요한 변경이 없는지 diff를 대조한다. + +### Task 1.3 검증 runner와 최종 기록 정리 +**Goal 실행 `P1-T3`:** REV-P1-001/002의 비차단 지적을 해결하고 반복 가능한 로컬 검증 결과를 남긴다. +- Files: `work/tests/check-event-detail.sh`, `prd.md`, `plan-task.md`, `reviews/phase1-event-detail-api.md`. +- [x] **RED:** `env -u TMPDIR bash work/tests/check-event-detail.sh`가 unset 변수로 exit 1임을 확인했다. runner에 EXIT trap이 없음을 확인했다. +- [x] **GREEN:** 기존 notice runner의 `mktemp -d`와 EXIT trap을 적용한다. +- [x] **GREEN 확인:** TMPDIR 제거 실행과 격리된 TMPDIR의 성공/실패 실행 후 생성 디렉터리 부재를 확인한다. +- [x] **REFACTOR:** 추가 앱 코드 변경 없이 최신 빌드·전체 회귀·리뷰 결과를 문서에 누적하고 소유 임시 자원을 정리한다. + +### Phase 1 Gate (최초 구현 인계 기록) +**Goal 실행 `P1-GATE`:** 집중 검증과 문서 결과를 인계한다. +- [x] 두 Task 완료 증거와 RED/GREEN 기록. +- [x] `bash work/tests/check-event-detail.sh` exit 0. +- [x] 영향 회귀 `bash work/tests/check-my-page-notices.sh` exit 0. +- [x] 양 앱 타깃 등록·Swift parse·diff 공백 검사 통과. +- [x] 빌드·시뮬레이터 QA·최종 리뷰 미실행을 명시하고 상위 실행자에 인계. + +## 실행 순서와 의존성 +P1-T1 → P1-T2 → P1-GATE. 서브에이전트/commit/push/별도 worktree는 사용하지 않는다. + +## Progress +### 계약·설계 확인 1차 실행 - 2026-10-29 +- 상태: 완료. +- 무엇을: 저장소 가이드, 문서 샘플, 기존 notice CLI, EventItem/API/Repository/View, 전체 진입점, AppState.back과 AuthPlugin을 확인했다. +- 왜: signed 64-bit ID-only와 false-first/status-first 계약, scoped dismiss의 실제 경계를 확정하기 위해서다. +- 어떻게: read 및 rg로 실제 파일을 대조했다. 기존 UI/토큰 10개 이상을 확인하고 레거시 상세 외형을 그대로 보존하기로 했다. +- 남은 항목: 테스트부터 작성하고 실행한다. + +### RED 1차 실행 - 2026-10-29 +- `bash work/tests/check-event-detail.sh`: exit 1. 신규 VM 입력 파일이 아직 없어 컴파일 실패했다. 이는 인터페이스 준비 단계의 RED이며 동작 assertion 증거와 구분한다. +- `swiftc -swift-version 5 -parse-as-library -module-cache-path "$TMPDIR/event-route-module-cache" -D EVENT_ROUTE_CHECK work/tests/EventDetailCheck.swift -o "$TMPDIR/check-event-routes-red" && "$TMPDIR/check-event-routes-red" "$PWD"`: exit 1, `FAIL: event detail route still retains payload instead of Int ID`. +- 다음 행동: API 인터페이스와 최소 VM 선언을 추가해 실제 VM 동작 assertion의 RED를 확인한다. 이후에만 VM 상태 로직을 구현한다. + +### RED 2차 실행 - 2026-10-29 +- `bash work/tests/check-event-detail.sh`: exit 1, `FAIL: Loading must deduplicate`. +- API 선언과 VM 인터페이스만 준비한 실제 production VM에 대해 native Combine driver를 실행했다. 요청 경로·헤더 검사는 PASS했고 상태 로직 미구현 assertion이 실패했다. 테스트를 완화하지 않고 VM 동작 구현으로 진행한다. + +### GREEN 및 P1-GATE 실행 - 2026-10-29 +- 상태: 완료. 이벤트 API/Repository/VM/DTO를 native Combine으로 실제 실행하는 CLI와 직접 영향 notice 회귀를 실행했다. +- 왜: 서버 payload와 무관한 ID-only 진입, 익명 헤더, 응답 상태 분류와 이탈 안전성을 검증하고 기존 공유 envelope 동작이 유지되는지 확인했다. +- 모든 shell 검증에 다음 환경을 사용했다. 생성된 event-detail 디렉터리와 route RED 실행 파일은 이 TMPDIR 내부에 보존했고 정리는 상위 실행자 소유다. + +```bash +export TMPDIR=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/tmp.UN5uRreooU +export NODE_COMPILE_CACHE=$TMPDIR/node-compile-cache +export BUN_RUNTIME_TRANSPILER_CACHE_PATH=$TMPDIR/bun-transpiler +bash work/tests/check-event-detail.sh +bash work/tests/check-my-page-notices.sh +swiftc -frontend -parse SodaLive/Sources/App/AppStep.swift SodaLive/Sources/ContentView.swift SodaLive/Sources/Settings/Event/EventApi.swift SodaLive/Sources/Settings/Event/EventRepository.swift SodaLive/Sources/Settings/Event/EventDetailView.swift SodaLive/Sources/Settings/Event/EventListView.swift SodaLive/Sources/Main/EventPopupDialogView.swift SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift SodaLive/Sources/Content/Main/Banner/ContentMainBannerView.swift SodaLive/Sources/Content/Main/V2/ContentMainBannerViewV2.swift SodaLive/Sources/V2/Main/MainView.swift SodaLive/Sources/V2/Settings/Event/EventDetailViewModel.swift work/tests/EventDetailCheck.swift +plutil -lint SodaLive.xcodeproj/project.pbxproj +bash -n work/tests/check-event-detail.sh +GIT_MASTER=1 git diff --check +``` + +각 명령 exit 0. 이벤트 CLI 실제 출력: + +```text +PASS: actual API/repository exact GET/requestPlain, anonymous/bearer, legacy routes/headers, >32-bit and Int.max +PASS: actual DTO/VM single object, optional omitted/null, image-only/link-only/both/neither, 64-bit IDs, main delivery, no refetch +PASS: nonpositive no request; false ignores malformed data/localized message; malformed success/HTML503/abnormal HTTP/401/transport stay error, loading ends, one dismissal +PASS: actual participation URL HTTP(S)+host boundary; cancel/queued/late/stale/duplicate responses, reload after cancellation, disappearance suppresses dismissal +PASS: Event detail regression checks (actual production API/repository/VM/DTO, native Combine; no server calls) +PASS: supplementary ID-only route/destination and 7 existing navigation call sites +``` + +notice CLI는 다음 네 줄로 완료됐다: + +```text +PASS: actual NoticeApi/Repository public v2 GET requestPlain without Authorization; legacy list auth/timezone unchanged +PASS: actual DTO/roller 0/1/2/3, omitted/null envelope, repeated order/wrap/selection, exact HTML/date, legacy no-id, shrink/reset and lifecycle predicate +PASS: actual MyPageViewModel background delivery on main, 0/1/2/3 success, empty hides, HTTP/envelope/decode/transport failure preserves data, log-only/loading semantics +PASS: My Page notices regression checks (actual production API/Repository/VM/DTO/roller, native Combine; no server calls) +``` + +`plutil`은 `SodaLive.xcodeproj/project.pbxproj: OK`, Swift parse·shell 문법·diff 공백 검사는 출력 없이 exit 0이었다. 타깃·경로 그래프는 다음 명령으로 별도 검증했다: + +```bash +plutil -convert json -o - SodaLive.xcodeproj/project.pbxproj | python3 -c 'import json,sys; project=json.load(sys.stdin); objects=project["objects"]; refs=[key for key,obj in objects.items() if obj.get("isa")=="PBXFileReference" and obj.get("path")=="EventDetailViewModel.swift"]; assert len(refs)==1, refs; ref=refs[0]; paths=[] +def walk(key,path=""): + obj=objects[key]; path="/".join(part for part in [path,obj.get("path","")] if part) + if key==ref: paths.append(path) + for child in obj.get("children",[]): walk(child,path) +walk(objects[project["rootObject"]]["mainGroup"]); assert paths==["SodaLive/Sources/V2/Settings/Event/EventDetailViewModel.swift"], paths +for name in ["SodaLive","SodaLive-dev"]: + target=next(obj for obj in objects.values() if obj.get("isa")=="PBXNativeTarget" and obj.get("name")==name); files=[file for phase in target["buildPhases"] if objects[phase]["isa"]=="PBXSourcesBuildPhase" for file in objects[phase]["files"]]; count=sum(objects[file].get("fileRef")==ref for file in files); assert count==1,(name,count); print("PASS: "+name+" has exactly one EventDetailViewModel.swift source membership at "+paths[0])' +``` + +exit 0, 실제 출력: + +```text +PASS: SodaLive has exactly one EventDetailViewModel.swift source membership at SodaLive/Sources/V2/Settings/Event/EventDetailViewModel.swift +PASS: SodaLive-dev has exactly one EventDetailViewModel.swift source membership at SodaLive/Sources/V2/Settings/Event/EventDetailViewModel.swift +``` + +- 직접 사용 검증: production 모듈 driver로 object 정상 로드, link-only, 비양수 입력, transport/HTTP/파싱 오류와 취소/지연 응답을 실행했다. 실제 서버·UIApplication.open은 실행하지 않았다. +- 미실행: 전체 build/test·시뮬레이터/실기기 시각 QA·VoiceOver·최종 리뷰. 사용자 위임 범위에 따라 상위 실행자가 수행한다. 이 환경에는 lsp_diagnostics 도구가 없어서 해당 clean 판정은 하지 않았다. 대신 VM/API/repository/DTO의 swiftc typecheck/실행과 변경 Swift 전체 parse를 확인했다. +- diff 대조: 상세의 레거시 외형 값, list/live의 상세 대 외부 URL 분기와 로그인 게이트를 유지했다. AppDeepLinkHandler, AuthPlugin, AcceptLanguagePlugin, EventItem, 기존 notice 테스트는 변경하지 않았다. +- 관찰: 최종 project.pbxproj diff에는 이번 패치에 없는 MyPageNoticeRoller 참조·그룹 블록의 순서 이동도 보인다. 공유 작업 변경으로 간주하여 되돌리지 않았다. 이벤트 VM 추가 membership과 경로는 독립적으로 검증했다. +- 남은 구현 항목: 없음. 다음 행동: 상위 실행자의 앱 빌드·실화면 QA·단일 리뷰. + +## Decision Log +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal | +|---|---|---|---|---|---| +| 2026-10-29 | DEC-001 | 확정 | ID-only, Int.max 검증, DTO 재사용 | PRD DEC-001/002 | P1-T1/P1-T2 | +| 2026-10-29 | DEC-002 | 확정 | View 환경 dismiss와 일회성 소비 | 전역 back의 live sheet 우선 처리 | P1-T1/P1-T2 | +| 2026-10-29 | DEC-003 | 확정 | 현재 브랜치 직접 작업, 실서버 연결 테스트 제외 | 사용자 후속 지시, PRD DEC-004 | 최종 검증 | + +### 상위 실행자 검증 1차 - 2026-10-29 +- 현재 브랜치에서 작업했으며 별도 worktree/commit/push는 생성하지 않았다. +- `bash work/tests/check-event-detail.sh`: 실제 production API/repository/VM/DTO와 native Combine, ID-only 7개 호출 검사 PASS, exit 0. +- `for script in work/tests/check-*.sh; do bash "$script"; done`: 신규 이벤트 검사를 포함한 14개 스크립트 모두 PASS. 변경 전 13개 스크립트도 모두 PASS였다. +- `xcodebuild -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -destination 'generic/platform=iOS Simulator' -disableAutomaticPackageResolution CODE_SIGNING_ALLOWED=NO build`: BUILD SUCCEEDED, exit 0. +- 같은 명령의 `-scheme SodaLive-dev`: BUILD SUCCEEDED, exit 0. 변경한 Swift 코드가 양 타깃에서 컴파일됐다. +- 기존 중복 Compile Sources, Run Script 출력 미지정, AppIntents 메타데이터 경고는 수정 범위 밖이라 유지했다. +- 초기 standard 빌드는 120초 timeout, 다음 시도는 구현 도중 VM 타깃 등록 전이라 scope 오류였다. 등록 완료 후 위 최종 빌드는 성공했다. +- Xcode 실행이 만든 MyPageNoticeRoller 블록의 순서 이동만 원위치로 정리했다. 정리 전후 plutil JSON 정렬 SHA-256은 `c010f8053c481722451be769f8dbfe6532866ad3301e20054465106c020a13e2`로 동일하여 프로젝트 의미와 빌드 입력은 변하지 않았다. +- 실서버 연결 테스트: 사용자 지시로 구현 후 수행하지 않았다. 지시 전 조사에서 관찰한 401은 서버 개발 중의 정보이며 클라이언트 완료 차단 사유가 아니다. +- 네이티브 화면 조작/시각 QA, VoiceOver 및 `lsp_diagnostics`는 해당 도구가 없어 미실행이다. Swift 컴파일과 로컬 상태 드라이버 검사로 대체했으며 실제 화면 검증 통과를 주장하지 않는다. +- 단일 Oracle 리뷰 진행 중. 추가 확인: `env -u TMPDIR bash work/tests/check-event-detail.sh`는 5행의 unset 변수로 exit 1. 반복 실행 시 생성된 검사 임시 디렉터리도 자체 정리가 없어 리뷰와 수정 대상으로 기록한다. + +## 발견된 문제 +양 스킴 빌드·14개 CLI 회귀는 최종 PASS이며 Oracle 단일 리뷰는 APPROVE다. MyPage 블록 순서 이동은 상위 실행자의 Xcode 실행 산출물로 확인 후 제거했다. 네이티브 실화면 QA/VoiceOver/LSP는 도구 부재로 미실행이며 실서버 테스트는 사용자 지시로 제외한다. + +### P1-T3 및 최종 리뷰 - 2026-10-29 +- Oracle 단일 리뷰: 앱 계약 위반의 확정 차단 사항 없음, APPROVE. 비차단 runner/문서 지적은 후속 수정했다. +- runner 수정 후 `env -u TMPDIR bash work/tests/check-event-detail.sh`: exit 0, 모든 집중 검사 PASS. +- `bash -x work/tests/check-event-detail.sh`: exit 0, 생성한 임시 디렉터리의 EXIT trap 실행과 이후 `test ! -e` PASS를 확인했다. +- `bash -c 'swiftc() { return 42; }; export -f swiftc; bash -x work/tests/check-event-detail.sh; result=$?; test "$result" -eq 42'`: 의도한 컴파일 실패 코드 42 유지, EXIT trap 실행, 이후 `test ! -e` PASS를 확인했다. +- `bash -n work/tests/check-event-detail.sh`: exit 0. 앱 production 코드는 리뷰 승인·양 스킴 빌드 이후 변경하지 않았다. 최종 수정은 runner 정리 2줄과 문서뿐이라 해당 runner 경로만 재검증했다. +- 자원 정리: xcodebuild/이벤트 swiftc 실행 프로세스 부재를 확인하고 이번 실행이 생성한 parent/구현자 임시 디렉터리와 notepad를 삭제했다. 두 run root의 `test ! -e` PASS. 공유 Xcode 캐시·사용자 파일·worktree는 삭제하지 않았다. 일회성 raw 빌드 로그는 함께 제거했고 명령·종료 코드·주요 결과는 이 문서에 보존했다. + +## Phase 2: 목록 이미지 원본 비율 +**Phase 결과:** EVT-005에 따라 기존 너비에서 이미지 전체를 원본 비율로 표시한다. +**선행조건:** 사용자 요청 확정, 기존 디자인/Kingfisher 조사 완료. 현재 브랜치에서 최소 수정한다. + +### Task 2.1 목록 이미지 표시 +**Goal 실행 `P2-T1`:** 고정 1000:300 표시 높이를 제거하고 기존 너비를 유지한다. +**Files:** `SodaLive/Sources/Settings/Event/EventListView.swift`. +**완료 증거:** 변경 소스 확인, Swift parse와 diff 공백 검사; 실제 화면·빌드·리뷰는 상위 실행자 담당. +- [x] EVT-005/DEC-005 및 TDD 예외를 소스 수정 전에 기록한다. +- [x] scaledToFill을 scaledToFit으로 바꾸고 표시 frame의 높이만 제거한다. downsampling·간격·탭·상태·API는 유지한다. +- [x] `swiftc -frontend -parse SodaLive/Sources/Settings/Event/EventListView.swift`와 `GIT_MASTER=1 git diff --check`의 exit 0을 확인한다. +- [x] 상위 실행자가 로컬 렌더·빌드·테스트·리뷰를 수행하고 증거를 누적한다. + +### P2-T1 검증 기록 +- 무엇/왜: 목록 이미지의 고정 표시 비율만 제거한다. 새 로직 seam이 없는 UI modifier 변경이므로 TDD 예외이며 소스 문자열 의사 테스트는 만들지 않는다. +- 검증 범위: 구현자는 소스 확인·parse·diff 검사만 수행한다. 로컬 렌더·빌드·리뷰 증거는 상위 실행자 검증 대기이며 서버/CDN 호출은 금지한다. +- 실행 결과: 위 Swift parse·diff 공백 검사는 모두 출력 없이 exit 0. 기존 ID-only 라우팅 변경은 보존했다. 지정 TMPDIR를 사용했으며 정리는 상위 실행자 소유다. + +### P2-T1 상위 실행자 검증 - 2026-10-29 +- 독립 macOS SwiftUI `ImageRenderer`에 실제 변경 전후 modifier를 적용했다. 원본 fixture는 로컬 CoreGraphics로 생성하여 서버/CDN 호출이 없었다. 너비 363.3/717.3pt에서 1000x300, 600x600, 400x600의 6개 조합을 측정하고 전체 상하단 색상 밴드가 남는지 assertion했다. parent도 driver를 직접 실행하여 6/6 PASS, exit 0을 확인하고 비교 PNG 두 장을 직접 읽었다. +- 363.3pt 너비의 새 높이는 각각 108.99/363.30/544.95pt, 717.3pt 너비는 215.19/717.30/1075.95pt다. 오차 0.01pt 이내로 원본 비율을 유지한다. 이 검증은 격리된 네이티브 image modifier 검증이며 실제 iOS 전체 화면이나 Kingfisher 다운로드 검증이 아니다. +- `for script in work/tests/check-*.sh; do bash "$script"; done`: 첫 12개 PASS 후 13번째 실행 중 전체 명령이 120초 timeout. 남은 `bash work/tests/check-received-gift-detail.sh && bash work/tests/check-user-creator-chat-date-presentation.sh`를 240초 제한으로 별도 재실행하여 둘 다 PASS/exit 0. 합계 기존 14개 검사 모두 통과했다. +- `xcodebuild -workspace SodaLive.xcworkspace -scheme SodaLive -configuration Debug -destination 'generic/platform=iOS Simulator' -disableAutomaticPackageResolution CODE_SIGNING_ALLOWED=NO build`: BUILD SUCCEEDED, exit 0. 동일 명령의 SodaLive-dev 스킴도 BUILD SUCCEEDED, exit 0. +- 단일 독립 리뷰: [phase2-event-list-image-aspect.md](reviews/phase2-event-list-image-aspect.md), APPROVE. 추가 수정 사항 없음. +- `git diff --check` exit 0. LSP 도구가 없어 Swift parse 및 양 타깃 실제 컴파일로 대체했다. 현재 브랜치 작업이며 commit/worktree/실서버 호출은 없다. +- 정리: 관련 실행 프로세스 부재를 확인하고 소유한 build/probe 임시 디렉터리(스크립트·이미지·바이너리·캐시)와 notepad를 제거했다. 두 run root 부재 검사 PASS. 공유 Xcode 캐시·기존 변경은 유지했고 측정값/명령/리뷰 판정은 위 기록에 보존했다. diff --git a/docs/20261029_이벤트_상세_API_연동/prd.md b/docs/20261029_이벤트_상세_API_연동/prd.md new file mode 100644 index 00000000..af32c3b0 --- /dev/null +++ b/docs/20261029_이벤트_상세_API_연동/prd.md @@ -0,0 +1,89 @@ +# 이벤트 상세 API 연동 PRD + +## 문서 정보 +| 항목 | 내용 | +|---|---| +| 문서 상태 | 클라이언트 구현·로컬 검증 완료 | +| 작성일 / 최종 수정일 | 2026-10-29 | +| 대상 제품 | SodaLive iOS 이벤트 상세 | +| 작성자·결정권자 | 사용자 계약, 구현 에이전트에 실행 위임 | +| 관련 API Contract | [api-contract.md](api-contract.md) | +| 관련 구현 계획 | [plan-task.md](plan-task.md) | +| 관련 review | [phase1-event-detail-api.md](reviews/phase1-event-detail-api.md) | + +## 1. Overview +기존 이벤트 진입점에서 ID만 전달하고 상세 API의 최신 단일 이벤트를 표시한다. 후속 딥링크도 ID만으로 진입할 수 있도록 payload 보존을 제거한다. + +## 2. Problem Statement +기존 상세 화면은 목록 payload를 직접 표시하고 optional 상세 이미지를 강제 해제한다. 목록에 없는 이벤트의 독립 조회와 접근 불가 판단이 불가능하다. 이전 이벤트 목록 중복 로딩 개선 문서와는 별개의 신규 작업이다. + +## 3. Goals +양수 Int ID로 익명/인증 상세 조회를 수행하고 loading, loaded, unavailable, 네트워크·응답 error를 구별한다. 기존 이미지 비율, 참여 버튼 모양·위치, toast와 navigation bar를 유지한다. + +## 4. Non-Goals +딥링크 parser/등록, 목록·팝업 조회와 로그인 게이트 변경, 공통 인증/URL 정책 리팩터링, 새 UI·의존성, 자동 외부 이동, payload fallback은 제외한다. 서버도 동시 구현 중이므로 사용자 지시에 따라 클라이언트 구현 후 실서버 연결 테스트는 하지 않는다. 현재 브랜치에서 직접 작업하며 worktree를 만들지 않는다. + +## 5. Target Users and Permissions +공개 상세는 비로그인 사용자도 조회한다. 토큰이 있으면 Bearer를 전달한다. 기존 진입점의 로그인 게이트는 유지한다. 401의 세션 초기화·main 이동은 기존 AuthPlugin 소유다. + +## 6. 핵심 사용자 흐름 +1. 기존 목록·팝업·배너에서 이벤트를 선택한다. +2. `AppStep.eventDetail(eventId: Int)`로 이동하고 상세를 한 번 요청한다. +3. 유효한 응답은 optional 상세 이미지와 안전한 참여 링크를 표시한다. 이미지 없는 link-only도 정상이다. +4. 비양수 ID 또는 HTTP 200 success=false는 해당 상세 화면만 닫는다. 네트워크/HTTP/파싱 오류는 toast로 알린다. +5. 화면 이탈 시 취소하고 지연 응답·중복 응답의 화면 변경을 차단한다. + +## 7. 정보 구조와 라우팅 +`AppStep.eventDetail(eventId: Int)` → `EventDetailView(eventId:)` → 소유 StateObject VM → 기존 EventRepository/EventApi. iOS 16.6+의 Int는 signed 64-bit이며 Int.max까지 손실 없이 전달한다. 실제 딥링크 추가는 후속 범위다. + +## 8. 기능 요구사항 +| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 | +|---|---|---|---|---| +| EVT-001 | 확정 | 모든 기존 진입점은 ID만 전달 | 7개 호출 및 destination 변경, 선택·인증 분기 보존 | §1, P1-T2 | +| EVT-002 | 확정 | 공개 v2 상세 GET | requestPlain, 빈 토큰 Authorization 없음, 기존 list/popup 불변 | §1, P1-T1 | +| EVT-003 | 확정 | status → success → 단일 object 순서 검증 | false의 data 모양 무시, malformed success는 error | §2, P1-T1 | +| EVT-004 | 확정 | 생명주기·링크 안전성 | cancel/중복 로딩·dismiss 방지, HTTP(S)+host만 참여 가능 | §3, P1-T1/P1-T2 | + +## 10. UI/UX Expectations +기존 7개 관련 화면과 BaseView, DetailNavigationBar, GiftBoxCardView 및 Color/Spacing/Radius/FontModifier를 확인했다. 기존 화면은 black/gray22, Pretendard 기반 appFont, 13.3pt 레거시 간격을 사용한다. 재설계 없이 상세 이미지 scaledToFit과 기존 버튼 값을 보존한다. 참여 action은 동일 외형의 plain Button으로 바꾼다. popup 이미지 fallback·가짜 제목/날짜는 없다. + +## 11. API 계약 +[api-contract.md](api-contract.md)의 확정 계약을 사용한다. DTO는 기존 EventItem 그대로 재사용한다. + +## 12. 보안과 데이터 취급 +외부 응답은 status와 envelope/필수 필드/일치 ID를 검증한다. 외부 참여 URL은 로컬에서 절대 HTTP(S)+비어 있지 않은 host만 허용한 뒤 기존 canOpenURL/open을 사용한다. 이는 좁은 scheme 경계이며 도메인 신뢰·리디렉션 안전성을 보증하지 않는다. 토큰/응답 본문을 추가 로깅하지 않는다. + +## 13. 성능과 품질 요구사항 +실제 Combine과 production API/repository/VM/DTO를 swiftc CLI로 검사한다. 같은 loaded 화면은 재조회하지 않는다. 새 VM은 두 앱 타깃에 등록한다. 빌드·시뮬레이터·최종 리뷰는 상위 실행자 검증 대상으로 명확히 구분한다. + +## 14. 성공 기준 +- [x] 집중 CLI에서 요청·인증·ID 경계·상태·URL·취소/중복·main delivery 검증 통과. +- [x] 모든 ID-only 탐색 호출과 두 target membership 확인. +- [x] RED/GREEN 실제 결과와 미실행 검증을 계획 문서에 기록. + +## 15. Open Questions +없음. 사용자 계약을 구현 기준으로 확정했다. + +## 16. 요구사항 추적표 +EVT-001~004는 api-contract §1~3, P1-T1/P1-T2/P1-GATE 및 `work/tests/check-event-detail.sh`로 추적한다. 네이티브 화면 검증은 상위 실행자 담당이다. + +## 17. Decision Log +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 | +|---|---|---|---|---|---| +| 2026-10-29 | DEC-001 | 확정 | payload 대신 Int ID만 라우팅 | 향후 이벤트 딥링크가 예정되어 독립 조회 필요 | EVT-001 | +| 2026-10-29 | DEC-002 | 확정 | 기존 EventItem 재사용, thumbnail 필수, link-only 정상 | 사용자 확정 계약 | EVT-003 | +| 2026-10-29 | DEC-003 | 확정 | unavailable은 View-scoped dismiss, AuthPlugin 유지 | AppState.back은 live sheet를 우선 닫음 | EVT-004 | +| 2026-10-29 | DEC-004 | 확정 | 현재 브랜치 직접 작업, 실서버 연결 테스트 제외 | 사용자 후속 지시, 서버 동시 구현 중 | 구현·검증 범위 | + +## 후속 요구사항: 목록 이미지 원본 비율 - 2026-10-29 +| ID | 상태 | 요구사항 | 수용 기준 | Goal 연결 | +|---|---|---|---|---| +| EVT-005 | 확정 | 기존 이벤트 목록 이미지를 원본 비율로 표시 | 기존 너비 유지, scaledToFit과 높이 없는 frame으로 이미지 전체 표시 | P2-T1 | + +### 후속 결정 +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 | +|---|---|---|---|---|---| +| 2026-10-29 | DEC-005 | 확정 | 목록의 표시 modifier만 변경하고 downsampling은 유지 | 사용자 명시 요청; Kingfisher는 max(width, height)로 다운샘플링하며 원본 비율을 유지 | EVT-005 | +- 기존 간격·탭·상태·API·상세 화면은 변경하지 않는다. 서버/CDN 호출은 하지 않는다. +- 순수 UI modifier 변경으로 새 로직 테스트 seam이 없어 TDD 예외로 처리한다. 소스 문자열 의사 테스트는 추가하지 않으며 로컬 렌더·빌드·리뷰 증거는 상위 실행자 검증 대기다. +- 후속 검증 완료: 독립 macOS SwiftUI 원본 비율 렌더 6/6, 양 앱 Debug 시뮬레이터 빌드, 로컬 회귀 14개 PASS 및 [Phase 2 리뷰](reviews/phase2-event-list-image-aspect.md) APPROVE. 실제 iOS 앱 화면 조작·서버/CDN 테스트는 수행하지 않았다.