docs(event): 상세 API 계약과 검증 기록을 남긴다
This commit is contained in:
@@ -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 테스트는 수행하지 않았다.
|
||||||
Reference in New Issue
Block a user