docs(event): 상세 API 계약과 검증 기록을 남긴다

This commit is contained in:
Yu Sung
2026-10-29 12:03:17 +09:00
parent 96bb86bbad
commit e427641915
3 changed files with 331 additions and 0 deletions
@@ -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<EventItem>`의 단일 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 정책은 변경하지 않는다.
@@ -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 캐시·기존 변경은 유지했고 측정값/명령/리뷰 판정은 위 기록에 보존했다.
@@ -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 테스트는 수행하지 않았다.