docs(recommendation): 서브 배너 요구사항과 검증 기록을 남긴다
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
# 추천 서브 배너 구현 계획
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | 후속 제스처·순환 수정 및 시각적 카운트 제거 검증 완료 |
|
||||
| 작성일 | 2026-10-29 |
|
||||
| 요구사항·API 기준 | [prd.md](prd.md), 사용자 제공 구현 완료 API 계약 |
|
||||
| 현재 Phase | Phase 4 완료 |
|
||||
| 실행 방식 | 현재 작업 트리, worktree 없음, 단일 구현자와 독립 최종 리뷰 |
|
||||
|
||||
## 목표와 범위
|
||||
두 추천 화면이 기존 추천 GET의 서브 배너 전체를 지정 위치에서 원본 비율로 자동 전환하며 안전한 iOS/웹 링크를 실행한다. 기존 메인 배너와 기타 추천 콘텐츠는 유지한다.
|
||||
|
||||
**기술:** Swift 5, iOS 16.6+, SwiftUI, Combine, Moya, 기존 Kingfisher. 새 의존성 없음.
|
||||
|
||||
**제약:** 별도 endpoint/query/두 번째 추천 요청 없음. 서버 순서·기간 판단 보존. 빈 목록 컴포넌트 자체 미표시. 5초 전환, 현재 이미지 비율 높이, 좌우 padding 0. 기존 메인 배너의 20개 제한과 정사각형 형식을 수정하지 않는다. fixture는 명시 실행에 한정한다. 원문 URI 변경·다른 OS 링크 실행 금지. 공유 인증 plugin은 수정하지 않는다.
|
||||
|
||||
## 현재 상태
|
||||
| Phase | 상태 | 완료 Task | 다음 Goal | 남은 조건 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 완료 | 1/1 | 없음 | 없음 |
|
||||
| 2 | 완료 | 2/2 | 없음 | physical input·실서비스·전체 앱 UI는 공개된 미검증 범위 |
|
||||
| 3 | 완료 | 1/1 | 없음 | 실기기·VoiceOver 자체 조작은 미검증 |
|
||||
| 4 | 완료 | 1/1 | 없음 | 시각적 삭제 범위의 UI typecheck·native 2개·PNG 확인 |
|
||||
|
||||
## Phase 1: 두 추천 화면 통합
|
||||
**결과:** 앱에서 서브 배너가 표시되고 기존 요청·화면·오류 정책을 보존한다.
|
||||
**선행조건:** PRD DEC-007~008 반영 완료.
|
||||
|
||||
### Task 1.1 통합 계약과 캐러셀 구현
|
||||
**Goal P1-T1:** 기존 추천 응답을 통해 두 화면의 서브 배너와 안전한 탭 흐름을 제공한다.
|
||||
|
||||
**Files:**
|
||||
- Create: `SodaLive/Sources/V2/Component/Banner/SubBanner.swift`, `SubBannerLinkOpener.swift`, `SubBannerCarousel.swift`.
|
||||
- Modify: `SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift`, `Repository/MainHomeRecommendationApi.swift`, `MainHomeRecommendationViewModel.swift`, `MainHomeRecommendationView.swift`.
|
||||
- Modify: `SodaLive/Sources/V2/Main/Content/Recommendation/Models/AudioRecommendationsResponse.swift`, `Repository/MainContentRecommendationApi.swift`, `MainContentRecommendationViewModel.swift`, `MainContentRecommendationView.swift`.
|
||||
- Modify: `SodaLive.xcodeproj/project.pbxproj` (신규 production 파일 두 앱 target membership만).
|
||||
- Test: `work/tests/SubBannerCheck.swift`, `work/tests/check-sub-banners.sh`; native QA는 같은 tests 또는 작업 소유 임시 harness로 작성한다.
|
||||
|
||||
**Interfaces:**
|
||||
- `SubBanner`: `Int` id, placement enum, imageUrl 및 nullable webUrl/androidDeepLink/iosDeepLink 그대로 디코딩. 날짜 필드 없음.
|
||||
- 두 추천 DTO의 `subBanners: [SubBanner]` 및 화면별 유효 목록. 입력 순서 보존, 콘텐츠 `isEmpty`에 유효 서브 배너 포함.
|
||||
- `SubBannerLinkOpener.open(_:using:)`: URL과 completion을 받는 opener를 주입하여 iOS 우선/웹 1회 대체. 실제 앱에서는 `UIApplication.open` completion 사용.
|
||||
- `SubBannerCarousel(items:onTap:)`: 전체 목록, 실제 이미지 비율, 현재 컨테이너 너비, 5초 자동 전환, swipe, 기존 page count. 빈 목록일 때 부모에서 삽입하지 않는다.
|
||||
|
||||
**실행 체크리스트:**
|
||||
- [x] **RED:** 기존 EventDetailCheck의 production API/repository/VM + Moya double/native Combine 패턴으로 두 응답·정수 경계·URL·헤더·상태·링크 시도 횟수 테스트 작성.
|
||||
- [x] **RED 확인:** `bash work/tests/check-sub-banners.sh`에서 미구현 기능에 의한 실패를 기록.
|
||||
- [x] **GREEN:** DTO와 유효성/순서/placement 필터, 두 GET 익명 헤더, 두 VM HTTP200 검증, 기존 실패 UI 유지, completion 기반 opener 구현.
|
||||
- [x] **GREEN:** 공통 캐러셀과 두 화면 통합. 홈 active creators 뒤, 콘텐츠 main banner와 exclusive series 사이, s24. 목록 교체/축소 시 안전한 index, 사라짐·inactive 시 timer 중지, 늦은 이미지 응답이 다른 현재 항목 높이를 바꾸지 않게 처리.
|
||||
- [x] **GREEN 확인:** 동일 집중 checks 통과. 이미지 처음 로딩은 임의 비율을 만들지 않고 실제 로드가 높이 0에 막히지 않게 한다.
|
||||
- [x] **REFACTOR:** 이번 코드의 중복만 점검, focused checks와 관련 기존 회귀 실행. 필요 없는 state abstraction/새 인프라는 만들지 않는다.
|
||||
|
||||
**완료 증거:** 집중 검사 로그와 변경 diff, 두 target membership, 다음 Phase의 실제 native 렌더.
|
||||
|
||||
## Phase 2: 검증과 리뷰
|
||||
### Task 2.1 검증 Gate
|
||||
**Goal P2-GATE:** 요구 동작과 기존 회귀를 실제 실행 증거로 확인한다.
|
||||
**선행조건:** P1-T1 코드 구현과 집중 테스트 통과.
|
||||
**Files:** `work/tests/check-*.sh`, 앱 workspace/두 scheme, 이 계획, `reviews/phase1-sub-banner-integration.md`, `reviews/evidence/`.
|
||||
**TDD 예외:** 검증·리뷰 작업은 새 제품 로직이 아니며 기존 tests/native surface로 증명한다.
|
||||
|
||||
| 시나리오 | 이진 통과 기준 | 검증 surface |
|
||||
|---|---|---|
|
||||
| API/익명/오류 | 실제 두 GET만 호출, 익명 Authorization 없음, 로그인 보존, 비200/false/transport 오류가 빈 성공 아님 | production API/repository/VM을 컴파일한 CLI checks |
|
||||
| 목록/정밀도 | Int.max 및 >2^53 보존, 21개 이상 전체 순서 유지, placement 격리, sub-banner-only 콘텐츠 표시 | production DTO/filter CLI checks + native 화면 |
|
||||
| URI/실패 격리 | 원문 동일, iOS 성공 웹0회/실패 웹1회, Android0회, 금지 scheme0회, 양쪽 실패 비치명 | production opener driver |
|
||||
| 배치/비율/empty | 양쪽 지정 위치+s24, 전체 폭, 서로 다른 원본 비율 높이, empty 추가 gap0 | 실제 SwiftUI native harness PNG/geometry, production source 사용 |
|
||||
| 자동/생명주기/이미지 | 실제 5초 전환·순환, 단일 고정, 목록 교체 안전, 이미지 실패가 다른 추천 콘텐츠에 영향 없음 | native harness와 focused state checks |
|
||||
| 인접 회귀 | 기존 main banner 변경 없음, follow 헤더 유지, 기존 checks 통과, 양 앱 build 성공 | diff + 전체 local checks + xcodebuild |
|
||||
|
||||
- [x] 집중 검사와 전체 `work/tests/check-*.sh` 실행. 상위 실행자가 16개 스크립트 한 루프 exit 0 확인.
|
||||
- [x] 양 앱 Debug x86_64 시뮬레이터 빌드 exit 0. generic clean build는 tool timeout 후 소유 simulator destination과 기존 cache로 전환. 상세 명령은 parent-verification.md.
|
||||
- [x] 실제 native 화면 캡처/상태 변화 확인. 실제 production View composition + 명시 주변 대역 harness, worker 42장 및 parent 직접 smoke. physical tap/swipe·실서비스·전체 앱 UI 통합은 미검증으로 구분.
|
||||
- [x] 변경 Swift parse/typecheck/build 진단. LSP 도구 미제공, 실제 대체 검증 결과만 기록.
|
||||
- [x] 단일 독립 리뷰 및 delta 재검토 APPROVE. REV-P1-001을 P2-R1로 보완하고 해소 판정 기록.
|
||||
- [x] QA 프로세스·앱·소유 임시 device/cache/root 정리 및 부재 영수증 확인. 공유 cache/device는 삭제하지 않음.
|
||||
|
||||
### Task P2-R1: REV-P1-001 현재 소스와 검증 증거 연결
|
||||
**시작 조건:** [리뷰 REV-P1-001](reviews/phase1-sub-banner-integration.md)의 소스 해시 불일치 확인.
|
||||
**Goal:** 현재 캐러셀 소스와 최신 native 실행·build 증거를 동일 버전으로 연결한다.
|
||||
**Files:** production `SubBannerCarousel.swift`는 읽기 전용; `reviews/evidence/native-final/**`, `reviews/phase1-sub-banner-integration.md`, 이 계획만 갱신.
|
||||
**TDD 예외:** 제품 수정이 아닌 실행 증거 보완. 기존 tests/실제 native matrix 재실행으로 대체.
|
||||
- [x] 기존 소스 대비 제스처 변경 범위와 현재 hash를 리뷰 §9에 기록한다. 전체 snapshot은 최신 native evidence에 포함한다.
|
||||
- [x] 현재 production 소스로 native matrix를 재실행하고 같은 hash의 캡처·geometry·cleanup 증거를 `reviews/evidence/native-final/`에 기록한다. 상위 실행자가 현재 소스와 snapshot을 cmp하여 동일성도 확인했다.
|
||||
- [x] 현재 hash로 SodaLive build와 집중 체크 exit0 재확인. SodaLive-dev는 최종 소스 변경 이후 exit0 기록 보존.
|
||||
- [x] 같은 독립 reviewer의 delta 재검토: REV-P1-001 ADDRESSED, APPROVE, 남은 차단 항목 없음.
|
||||
|
||||
## 사전 계획 대조표
|
||||
| 대상 | 검토 결과 |
|
||||
|---|---|
|
||||
| P1 내부 DTO → UI/opener | 같은 SubBanner 모델, 서버 필드는 추가 추정 없음. enum은 사용자 계약을 따른다. |
|
||||
| P1 → P2 | 실제 production 코드·native surface 검증, 소스 문자열 검사만으로 동작 PASS 하지 않음. |
|
||||
| P1 단독 | 기존 배너 이동·20개 cap 재사용·공통 인증 수정 없음. |
|
||||
| P2 단독 | 도구 부재와 실제 실패를 구별하며 fake PASS 금지. |
|
||||
|
||||
## Progress / 검증 기록
|
||||
- 2026-10-29: 사용자 후속 승인과 기존 코드/plan agent 검토에 근거해 계획 작성. worktree 없음.
|
||||
- 기존 `check-event-detail.sh`: PASS. 전체 baseline 루프는 첫 4개 스크립트 통과 후 120초 tool timeout으로 중단됐으며 테스트 실패로 판정하지 않음. 최종 전체 실행은 더 긴 timeout 사용.
|
||||
- 구현 기준 결정: 기존 page count/swipe와 회색 placeholder 계열, 단일 항목 timer 없음. 최초 치수 미확인 시 임의 비율 없음. 현재 항목 원본 비율 확정 시 높이 변경.
|
||||
- 정상 운영 코드에 QA fixture fallback을 넣지 않는다. native harness용 코드가 필요하면 tests/소유 임시 root에 제한한다.
|
||||
- 상위 실행자 검증: [parent-verification.md](reviews/evidence/parent-verification.md). 집중 검사·전체16개 check 스크립트 단일 루프·두 앱 빌드 exit0, native smoke 직접 실행. 상세 native geometry와 timing은 [native/README.md](reviews/evidence/native/README.md). 독립 리뷰 진행 중.
|
||||
- 2026-10-29 P1-T1: 프로덕션 두 API/repository/VM을 실제 Combine으로 컴파일한 검사에서 익명 GET Authorization 및 HTTP 201 성공 처리의 RED를 확인한 뒤 수정했다. `bash work/tests/check-sub-banners.sh` GREEN, `bash work/tests/check-sub-banners.sh --ui`에서 실제 설치된 Kingfisher 및 theme/font를 사용한 iOS 16.6 simulator 타입 검사 PASS. 신규 3개 파일의 두 앱 Sources membership을 파싱해 각각 정확히 1회 확인했다.
|
||||
- 2026-10-29 P1 회귀: 전체 `work/tests/check-*.sh` 루프는 15개 PASS 후 마지막 `check-user-creator-chat-date-presentation.sh`에서 600초 도구 제한으로 종료됐다. 해당 스크립트 분리 재실행은 113 assertions PASS. 합계 16개 스크립트 PASS이며 단일 전체 루프 완주로 표현하지 않는다.
|
||||
- P1 구현 증거: [reviews/evidence/implementation.md](reviews/evidence/implementation.md). 높이 0의 초기 로드는 Kingfisher `startLoadingBeforeViewAppear()`로 시작하며 URL별 실측 비율만 저장한다. 캐러셀 렌더·swipe·5초 전환·생명주기 실동작은 다음 Phase의 native QA 대상이다. 이 체크 완료는 앱 빌드/native QA/독립 리뷰 완료를 뜻하지 않는다.
|
||||
- 2026-10-30 P2-R1: 최신 native 42장과 snapshot/hash 불변, 기존 evidence 미변경 확인. 현재 캐러셀과 snapshot cmp PASS, 최종 SodaLive build log BUILD SUCCEEDED 확인. 동일 reviewer에게 증거 정합성 delta만 재검토 요청했다.
|
||||
- 소유 simulator `482F7827-2E3A-4559-BAF4-48B68CC5725B` shutdown/delete 후 device 디렉터리 부재 확인: `Owned simulator teardown: PASS`. 각 QA worker own app/PID/root도 정리 영수증 보유. 상위 root의 리뷰 로그만 재검토 종료까지 보존한다.
|
||||
- 2026-10-30 종료: 독립 재검토 APPROVE. 상위 run root/cache와 소유 simulator 부재 검사 `Owned run root/cache and simulator absent: PASS`, 임시 메모 삭제 완료. 지속 기록은 이 문서·리뷰·native-final 증거에 남겼다. worktree/commit/push 없이 현재 작업 트리에 변경을 보존한다.
|
||||
|
||||
## Phase 3: 스와이프 오탭 회귀 수정
|
||||
### Task P3-T1: 드래그와 탭 인식 분리 및 연속 순환
|
||||
**Goal:** 실제 드래그는 페이지 전환/세로 스크롤만 수행하고, 정상 탭만 현재 배너 링크를 한 번 실행한다. 끝→처음도 일반 전환과 같은 방향으로 연속 슬라이드한다.
|
||||
**시작 조건:** 사용자 스와이프 오탭 보고, PRD SB-021.
|
||||
**Files:**
|
||||
- Modify: `SodaLive/Sources/V2/Component/Banner/SubBannerCarousel.swift` (공통 원인 한 곳만).
|
||||
- Test: `work/tests/SubBannerGestureHarness.swift`, `work/tests/SubBannerGestureUITests.swift`, `work/tests/run-sub-banner-gesture-tests.sh` 또는 같은 범위의 최소 native XCTest 진입점.
|
||||
- Evidence: `reviews/evidence/gesture-fix/**`, `reviews/evidence/gesture-fix/implementation.md`.
|
||||
- 두 추천 View 호출부, 기존 `BannerCarousel.swift`, DTO/API/opener는 변경하지 않는다.
|
||||
|
||||
**확인된 코드/가설:** `@GestureState isSwiping`은 드래그 종료/취소 시 초기화된다. Button과 simultaneousGesture는 함께 인식되므로 Button action 시점의 guard가 그 이전 드래그를 기억하지 못할 수 있다. 기존 main carousel의 지연 state 초기화는 참고하되 임의 시간 지연을 새 해결책으로 추가하지 않는다.
|
||||
|
||||
- [x] **RED:** 실제 production 캐러셀과 로컬 fixture를 사용하는 작은 native app + XCTest UI test 구성. 설치된 xcodeproj gem으로 임시 project를 생성하며 production project에는 test infrastructure를 추가하지 않는다.
|
||||
- [x] **RED 확인:** 좌표 drag 후 `onTap` 횟수 0 조건이 실패함을 확인한다. 도구/빌드 실패를 RED로 계산하지 않는다.
|
||||
- [x] **GREEN:** 테스트로 확인된 제스처 경합을 최소 수정한다. 선택지는 native tap/drag 상호 배제이며 vertical ScrollView를 빼앗는 highPriority만의 변경이나 고정 지연 억제는 통과 검증 없이 채택하지 않는다. 접근성 버튼 action 유지.
|
||||
- [x] **GREEN 확인:** 같은 실제 touch tests로 좌우/wrap/짧은 드래그 callback0, 정상·직후 탭 현재 ID 1회, 세로 scroll, 단일 항목을 확인한다. 자동 timer가 입력 검사를 오염시키지 않게 harness fixture만 명시 제어하고 production 5초 코드는 바꾸지 않는다.
|
||||
- [x] **RED/GREEN 경계 전환:** 기존 무애니메이션 경계 분기를 native 전환 프레임으로 확인한다. 기존 main carousel의 경계 복제/정렬 패턴을 적용하되 원본 비율과 실제 페이지/클릭 대상 매핑을 보존한다. 수동 양방향 wrap과 실제5초 자동 wrap의 중간·종료 프레임, 목록 변경/화면 이탈 시 지연 정렬 안전성을 검증한다.
|
||||
- [x] **REFACTOR:** 이번 변경만 정리하고 `check-sub-banners.sh`, `--ui`, 독립 native app/UI-test build를 검증한다. 전체 관련 앱 build는 사용자 지시에 따라 상위 실행자 소유로 남긴다.
|
||||
- [x] **Gate(이번 위임 범위):** 현재 소스 해시 연결, 소유 QA app/root/cache 정리, 실제 실행/미실행 범위 기록. 빌린 simulator는 유지한다. 단일 독립 리뷰는 사용자 명시 no-agents 지시에 따라 미실행이며 상위 확인 대상으로 구분한다.
|
||||
- [x] **최종 Gate:** 상위 실행자가 native11개·기존16개 check·양 앱 build를 재검증했다. 독립 리뷰 APPROVE. 앞 문장의 no-agents는 구현자에게 내린 하위 위임 제한이며 사용자 전체 작업의 리뷰 금지가 아니다.
|
||||
|
||||
**완료 조건:** native touch assertion의 RED/GREEN, 페이지/링크 시도 횟수 증거 및 정상 탭·세로스크롤 회귀 통과. 이전 native-final 증거는 덮어쓰지 않는다.
|
||||
- [x] **검증 진입점 정리:** 임시 UDID를 필수 `SB_SIMULATOR_ID` 인자로 바꾸고 TMPDIR/Kingfisher 경로를 선택 가능하게 했다. 부모가 같은 native suite를 재실행하여 11/11 PASS 확인.
|
||||
- 후속 추가 범위 SB-022: 마지막→첫 번째 snap 수정은 같은 파일/검증 실행에서 처리한다. 별도 리팩터링이나 공용 main carousel 변경은 하지 않는다.
|
||||
|
||||
### Phase 3 Progress / 검증 기록
|
||||
- 2026-10-30 P3-T1: 제품 수정 전 실제 XCTest 좌표 swipe에서 페이지 이동 후 `taps=1;last=1`이 발생해 callback0 assertion RED를 확인했다. 단순 tap recognizer는 34pt drag를 막지 못해 배타 인식으로 교체했고, 이동 라벨의 좌표 회귀도 실제 112pt 입력 실패를 확인한 뒤 `.global`로 해결했다.
|
||||
- 최종 독립 native build/test exit0: 11 tests, 0 failures. 좌우·양방향 wrap·17/34pt drag·정상/직후 tap·세로 부모 스크롤·single/empty·2:1/3:1·목록 교체·실제5초·background6초·전환 중 replace/hide를 검증했다. 최종 소스는 `b3e4c5333cf8249ccd7773b5cabd372efb5a5565323bd7778034e110386151fd`로 실행 전후 및 cleanup 후 동일하다.
|
||||
- 원본 경계 RED는 117개 실제 native frame 중 31.9ms 사이 green→red 직접 교체로 확인했다. 최종 수정본은 자동/수동 reverse/forward에서 각각 10/21/22개 중간 프레임, 같은 방향 진행, 정착 후 동일 배너 hash를 확인했다. 목록 교체/이탈은 실제 중간 PNG 감지 후 약1ms에 실행했다.
|
||||
- `check-sub-banners.sh`, `--ui` exit0. 제품 수정은 공통 컴포넌트 1개뿐이다. 소유 app/runner uninstall exit0 및 root/cache 부재 확인, 빌린 `B5F2BA0D-327C-4EE3-94EE-39710A0E1ACC`는 Booted 유지, parent `sub-banner-gesture.Fyv2wD` 미변경. 전체 앱 build·독립 리뷰·보조공학 자체 조작 등 미실행 범위는 [implementation.md](reviews/evidence/gesture-fix/implementation.md)에 분리했다.
|
||||
- 최종 상위 검증: native XCTest 11/11 PASS(실패/skip0), 기존 check16개 단일 루프 PASS, SodaLive/SodaLive-dev Debug simulator build exit0, 현재 product/테스트 snapshot cmp 동일. [Phase3 리뷰](reviews/phase3-sub-banner-gesture.md) APPROVE.
|
||||
- 최종 정리: parent 재실행의 앱/runner uninstall exit0, 소유 simulator shutdown/delete 및 device 경로 부재, parent root/cache 삭제 및 부재, 임시 note 삭제 확인. 출력 `Gesture fix owned root/cache/device absent: PASS`. 공유 cache·사용자 simulator·기존 미커밋 변경은 정리하지 않았다. worktree/commit/push 없음.
|
||||
|
||||
## Phase 4: 시각적 페이지 카운트 제거
|
||||
### Task P4-T1: 우측 상단 배지만 삭제
|
||||
**Goal:** PRD DEC-009에 따라 서브 배너에 시각적 페이지 표시를 없애고 상호작용·접근성을 보존한다.
|
||||
**선행조건:** 사용자 명시 결정, Phase 3 완료 소스 `b3e4c5333cf8249ccd7773b5cabd372efb5a5565323bd7778034e110386151fd` 확인.
|
||||
**Files:** `SodaLive/Sources/V2/Component/Banner/SubBannerCarousel.swift`, 이 계획, `prd.md`, `reviews/evidence/no-count/README.md` 및 PNG 1~2장. 기존 `work/tests/SubBannerGestureHarness.swift`와 runner는 수정 없이 재사용한다.
|
||||
**TDD 예외:** 로직 없는 overlay/helper 삭제이며 새 단위 테스트는 만들지 않는다. 대체 검증은 `bash work/tests/check-sub-banners.sh --ui`, 기존 `testManualBoundaryFrames`(선택적으로 `testSwipeDoesNotTap`) 및 native 캡처 직접 확인이다. 전체 회귀·전체 앱 빌드·새 인프라는 범위 밖이다.
|
||||
- [x] overlay와 고립된 `pageIndicator`만 삭제하고 나머지 소스·메인 배너 불변을 대조한다.
|
||||
- [x] UI typecheck와 전용 iPhone SE3/iOS18.3의 기존 native 테스트를 통과하고 배지 없는 PNG를 직접 확인한다.
|
||||
- [x] 명령·해시·결과·캡처 경로 및 소유 앱/runner/PID/device/temp root 정리·부재 확인을 증거에 기록한다.
|
||||
**완료 증거:** [no-count/README.md](reviews/evidence/no-count/README.md). 구현 전 임시 root와 정리 TODO를 생성했으며 공유 cache/device는 정리하지 않는다.
|
||||
|
||||
### Phase 4 Progress / 검증 기록
|
||||
- 2026-10-30 P4-T1 완료: 순수 삭제 23줄(181→158), 나머지 소스 완전 일치. 최종 SHA256 `15ade1c2e0cd26fca4b0e1833bbb08aa42627335a8c79099528caedc337015d8`와 runner 실행 전/컴파일/실행 후 해시 동일. 메인 배너·기존 검증 인프라 불변.
|
||||
- `bash work/tests/check-sub-banners.sh --ui` exit0. 기존 `testManualBoundaryFrames`, `testSwipeDoesNotTap`만 실행하여 2/2 PASS, 실패/skip0, 24.711초. native PNG 1장을 직접 확인했고 우측 상단 배지 없음. 전체 회귀/전체 앱 빌드는 순수 렌더링 삭제 범위이므로 생략했다.
|
||||
- 소유 앱/runner uninstall 및 부재, PID 종료, 전용 simulator shutdown/delete와 디렉터리 부재, 임시 root/cache/note 부재 모두 PASS. 상세 명령·한계·캡처는 완료 증거에 기록했다.
|
||||
- 상위 실행자가 UI typecheck 재실행 exit0, 소스/PNG 및 소유 자원 부재를 확인했다. [Phase4 단일 독립 리뷰](reviews/phase4-sub-banner-count.md) APPROVE. 추가 제품 변경 없음.
|
||||
@@ -0,0 +1,237 @@
|
||||
# 추천 서브 배너 PRD
|
||||
|
||||
## 문서 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 문서 상태 | 구현·로컬 검증 완료 |
|
||||
| 작성일 | 2026-10-29 |
|
||||
| 최종 수정일 | 2026-10-30 |
|
||||
| 대상 제품 | 홈·콘텐츠 추천 서브 배너 |
|
||||
| 작성자·결정권자 | 작성: 구현 에이전트, 제품 결정: 사용자 |
|
||||
| 관련 API Contract | 없음, 계약은 이 PRD에 임시 기록 |
|
||||
| 관련 구현 계획 | [plan-task.md](plan-task.md) |
|
||||
| 관련 review | [통합 리뷰](reviews/phase1-sub-banner-integration.md) |
|
||||
|
||||
### 요구사항 상태
|
||||
|
||||
| 상태 | 의미 | 구현 처리 |
|
||||
|---|---|---|
|
||||
| 확정 | 사용자 요청으로 명시된 요구 | 구현 기준 |
|
||||
| 미결 | 최종 제품 선택이 필요한 UX | 결정 전 임의 구현 금지 |
|
||||
| 권고 | 검토를 위한 제안 기본값 | 승인 전 수용 기준 아님 |
|
||||
| 외부 의존 | 서버 응답·정책 제공 필요 | 제공 전 추정 구현 금지 |
|
||||
|
||||
## 1. Overview
|
||||
|
||||
기존 홈 추천과 오디오 추천 조회 응답의 `data.subBanners`를 사용해 각 추천 화면에 배너 목록을 표시한다. 별도 endpoint나 추가 조회 없이 서버 순서와 자격 필터링 결과를 유지하고, 플랫폼별로 열 수 있는 링크만 처리한다.
|
||||
|
||||
## 2. Problem Statement
|
||||
|
||||
두 추천 화면에는 배치 목적이 다른 서브 배너가 필요하다. 기존 `BannerCarousel`은 최대 20개 제한, 정사각형 이미지, 좌우 여백을 전제로 하므로 이 요구의 배너 목록과 원본 이미지 비율을 그대로 충족하지 않는다. 기존 컴포넌트를 그대로 재사용할 수 없다.
|
||||
|
||||
## 3. Goals
|
||||
|
||||
- 기존 응답의 `subBanners`를 홈과 콘텐츠 추천의 지정 위치에 표시한다.
|
||||
- 서버가 정한 순서를 보존하고 placement별로 분리해 보여 준다.
|
||||
- 안전한 플랫폼별 링크 열기를 제공하고, 이미지·링크 오류가 추천 화면 전체를 막지 않게 한다.
|
||||
|
||||
## 4. Non-Goals
|
||||
|
||||
- `/api/v2/sub-banners` 추가, 두 번째 요청, placement query 추가는 하지 않는다.
|
||||
- 기존 `data` 필드나 `banners`를 바꾸지 않는다.
|
||||
- Android 전용 배너 노출 또는 Android 링크 실행을 지원하지 않는다.
|
||||
- 클라이언트 날짜 계산, 타이머를 이용한 노출 자격 판정, 설치 가능 여부 사전 필터링을 하지 않는다.
|
||||
- 기존 메인 배너 캐러셀의 동작을 변경하지 않는다.
|
||||
|
||||
## 5. Target Users and Permissions
|
||||
|
||||
- 비로그인 사용자와 로그인 사용자는 모두 추천 조회 결과의 서브 배너를 본다.
|
||||
- 비로그인 요청에는 `Authorization`을 보내지 않는다.
|
||||
- 로그인 요청은 기존 인증 헤더를 그대로 유지한다.
|
||||
- 확인한 두 `TargetType`은 현재 `Authorization: Bearer {token}` 헤더를 항상 구성한다. 익명에서 헤더를 생략하라는 요구와 차이가 있어, 공통 plugin 동작 및 endpoint별 수정 seam은 구현 전 확인이 필요하다.
|
||||
|
||||
## 6. 핵심 사용자 흐름
|
||||
|
||||
1. 홈 또는 콘텐츠 추천 화면이 기존 추천 API를 한 번 호출한다.
|
||||
2. HTTP 200이며 envelope의 `success == true`인 응답에서 `data.subBanners`를 읽는다.
|
||||
3. 화면별 placement에 해당하며 iOS 또는 웹 링크가 하나 이상 유효한 배너만 서버 순서대로 표시한다.
|
||||
4. 사용자가 배너를 탭하면 iOS 앱 링크를 먼저 열고 실패할 때 웹 링크를 한 번 시도한다.
|
||||
5. 두 링크가 모두 없거나 열기에 실패해도 추천 화면은 계속 사용할 수 있다.
|
||||
|
||||
## 7. 정보 구조와 라우팅
|
||||
|
||||
```text
|
||||
홈 추천: 최근 활동 크리에이터 다음
|
||||
콘텐츠 추천: 기존 상단 배너 다음, 독점 시리즈 이전
|
||||
공용 신규 컴포넌트 위치: SodaLive/Sources/V2/Component/Banner
|
||||
```
|
||||
|
||||
## 8. 기능 요구사항
|
||||
|
||||
### 8.1 추천 API와 응답
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| SB-001 | 확정 | `GET /api/v2/home/recommendations`와 `GET /api/v2/audio/recommendations` 응답의 기존 envelope `success/message/data/errorProperty` 및 기존 `data` 필드·`banners`를 보존하고 `data.subBanners`를 추가한다. | 두 경로에서 기존 필드가 유지되고 서브 배너를 같은 응답에서 읽는다. | 기존 API 응답 |
|
||||
| SB-002 | 확정 | API 성공은 HTTP 200과 `success == true`를 모두 만족해야 한다. | HTTP 비 200 또는 `success == false`는 성공 데이터·빈 성공으로 취급되지 않는다. | SB-001 |
|
||||
| SB-003 | 확정 | 별도 `/api/v2/sub-banners` 요청이나 두 번째 요청, placement query를 추가하지 않는다. | 추천 화면 진입 시 서브 배너 때문에 추가 네트워크 요청이 발생하지 않는다. | SB-001 |
|
||||
| SB-004 | 확정 | 비로그인 요청에서 `Authorization`을 생략하고 로그인 요청은 현재 endpoint의 기존 헤더를 보존한다. | 양쪽 인증 상태에서 헤더가 기대값과 일치하며 다른 기존 헤더도 유지된다. | 기존 인증 정책 |
|
||||
| SB-005 | 확정 | 서버는 `id` 오름차순으로 정렬하고 날짜 자격 조건을 적용한 목록을 내려준다. 클라이언트는 순서를 보존하며 날짜나 timer로 자격을 다시 판정하지 않는다. | 제공된 계약 기반 fixture의 순서가 화면까지 유지되고 클라이언트 날짜 기반 제외·재정렬이 없다. | 사용자 제공 구현 완료 계약 |
|
||||
|
||||
### 8.2 SubBanner 데이터와 자격
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| SB-006 | 확정 | `SubBanner` 필드는 `id`(JSON integer), `placement`(정확히 `HOME_RECOMMENDATION` 또는 `CONTENT_RECOMMENDATION`), `imageUrl`(절대 http(s) URL), nullable `webUrl`, `androidDeepLink`, `iosDeepLink`다. 추가 서버 필드를 가정하지 않는다. | 제공된 필드만으로 디코딩하고 알 수 없는 필드는 요구하지 않는다. | 서버 응답 계약 |
|
||||
| SB-007 | 확정 | `id`는 정밀도 손실 없는 native `Int` decoding 전략으로 처리한다. | 큰 정수 식별자가 반올림·문자열 변환 없이 정확히 보존된다. | SB-006 |
|
||||
| SB-008 | 확정 | placement를 화면별로 분리하고 리스트의 서버 순서를 그대로 유지한다. | 홈에는 홈 placement만, 콘텐츠에는 콘텐츠 placement만 표시하며 각 내부 순서가 입력과 같다. | SB-005, SB-006 |
|
||||
| SB-009 | 확정 | iOS/web URL 유효성 중 하나 이상을 만족하는 항목만 노출한다. Android 링크만 있는 항목은 제외하며 앱 설치 가능 여부를 사전 확인하지 않는다. | Android-only 항목은 숨기고, 설치 상태 조회로 제외되는 항목이 없다. | SB-006 |
|
||||
| SB-010 | 확정 | 필터 후 목록이 비면 배너 영역과 그 영역으로 추가된 간격을 모두 숨긴다. | 빈 목록 전후에 서브 배너 전용 영역 높이나 간격이 남지 않는다. | SB-008, SB-009 |
|
||||
|
||||
### 8.3 노출과 상호작용
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| SB-011 | 확정 | 자격을 통과한 전체 목록을 캐러셀에 제공한다. 20개 제한 없이 모든 항목을 순환 대상으로 유지한다. | 21개 이상 목록의 마지막 항목까지 도달 가능하고 입력 순환 순서가 보존된다. | SB-008 |
|
||||
| SB-012 | 확정 | 홈에서는 최근 활동 크리에이터 다음, 콘텐츠에서는 기존 상단 배너 다음이자 독점 시리즈 이전에 둔다. 주변 기존 섹션과 동일한 `SodaSpacing.s24` 간격을 사용한다. | 기존 메인 배너를 이동하지 않고 새 섹션을 지정 위치에 추가하며 비어 있을 때 추가 간격이 없다. | SB-010 |
|
||||
| SB-013 | 확정 | 배너는 가로 전체 폭, 수평 padding 0으로 표시하고 이미지 원본 비율을 유지한다. | 양쪽 화면 경계까지 폭을 쓰며 이미지를 정사각형으로 강제하지 않는다. | SB-012 |
|
||||
| SB-014 | 확정 | 5초마다 자동 순환하고 현재 이미지 원본 비율에 맞춰 높이를 변경한다. 기존 패턴의 swipe를 제공하고 항목이 하나면 timer를 실행하지 않는다. | 자동 전환 전후 이미지 비율과 높이가 일치하고 빈 목록이면 컴포넌트 자체가 없다. | OQ-001 |
|
||||
| SB-015 | 확정 | 기존 메인 배너 캐러셀 동작은 바꾸지 않고, 새 공용 코드는 `V2/Component/Banner` 아래에 둔다. | 기존 메인 캐러셀에 변경이 없고 서브 배너 전용 요구가 기존 제약과 섞이지 않는다. | SB-011, SB-013 |
|
||||
|
||||
### 8.4 링크와 오류 처리
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| SB-016 | 확정 | 웹 URL은 scheme이 http 또는 https이고 host가 있어야 한다. 판정은 대소문자를 구분하지 않는다. | host 누락, 다른 scheme, 잘못된 URL은 web 후보로 인정되지 않는다. | SB-009 |
|
||||
| SB-017 | 확정 | deep link는 절대 URI여야 하며 `javascript`, `data`, `file`, `content` scheme은 대소문자 무관하게 거부한다. | 상대 URI와 금지 scheme은 실행되지 않는다. 허용 URI는 검증용 파싱 후에도 원래 문자열 그대로 전달한다. | SB-009 |
|
||||
| SB-018 | 확정 | 사용자 추천안을 구현 기본값으로 채택한다. 유효한 iOS 링크를 먼저 한 번 열고, 없거나 열기에 실패하면 유효한 웹 링크를 한 번 연다. Android 링크는 쓰지 않는다. | iOS 성공 시 웹 시도 0회, iOS 실패 및 웹 유효 시 웹 시도 1회, 모두 실패 시 비치명 상태를 검증한다. | SB-016, SB-017 |
|
||||
| SB-019 | 확정 | 이미지 로드 실패와 개별 링크 실패는 해당 배너에만 국한한다. | 실패한 이미지나 링크가 다른 배너 및 추천 화면 로딩·사용을 막지 않는다. | SB-018 |
|
||||
| SB-020 | 확정 | 추천 조회 실패를 빈 성공이나 빈 배너 목록으로 바꾸지 않고 기존 추천 오류 의미를 유지한다. | HTTP 비 200, `success == false`, 디코딩·전송 실패가 기존 추천 오류 처리로 전달되고 정상 empty와 구분된다. | SB-002 |
|
||||
|
||||
## 9. UI/UX Expectations
|
||||
|
||||
- 홈과 콘텐츠 화면의 기존 `VStack` 섹션 간격을 기준으로 서브 배너를 배치한다. 배너가 비면 그 섹션 때문에 추가된 간격도 없어야 한다.
|
||||
- 이미지 및 링크의 오류는 국소 처리한다.
|
||||
- page indicator는 기존 우측 상단 현재/전체 표시를 따른다. 이미지 오류는 기존 회색 placeholder 계열로 국소 처리하고 별도 오류 팝업을 추가하지 않는다.
|
||||
- 구현 기본값: 이미지 치수를 알기 전 임의 원본 비율을 만들지 않는다. 최초 치수가 없으면 이미지 영역 높이를 예약하지 않고, 다음 이미지 로딩 중에는 마지막 확인된 높이를 유지할 수 있다. 로드 성공 후 현재 이미지 비율로 높이를 갱신한다.
|
||||
- 콘텐츠의 `AudioRecommendationsResponse.isEmpty`는 현재 기존 배열만 검사한다. 서브 배너만 있는 응답도 표시되도록 유효 서브 배너 목록을 빈 화면 판정에 포함해야 한다.
|
||||
- 기존 `BannerCarousel`은 최대 20개, 5초 timer, 좌우 여백이 있는 정사각형 이미지 및 우측 상단 page count 표시를 가진다. 따라서 그대로 재사용할 수 없다.
|
||||
|
||||
## 10. API 계약
|
||||
|
||||
### 10.1 공통 규칙
|
||||
|
||||
- 성공 조건: HTTP 200 및 `success == true`.
|
||||
- envelope: 기존 `success`, `message`, `data`, `errorProperty` 보존.
|
||||
- `data`의 기존 필드와 `banners` 보존, `subBanners` 추가.
|
||||
- `subBanners` 항목: `id: JSON integer`, `placement`, `imageUrl`, nullable `webUrl`, nullable `androidDeepLink`, nullable `iosDeepLink`.
|
||||
- 서버는 id 순서 및 날짜 자격 필터링을 담당한다. 이는 사용자가 구현 완료로 제공한 확정 계약이며 재확인으로 구현을 차단하지 않는다. 실제 서버 실행 검증 여부는 별도로 기록한다.
|
||||
- 공개 DTO에 `startDate`, `endDate`, `isActive`를 추가하지 않는다. 정상 빈 결과는 `subBanners: []`이며 삭제된 별도 endpoint의 오류 status는 가정하지 않는다.
|
||||
|
||||
### 10.2 Endpoint 추적
|
||||
|
||||
| 요구사항 | Method | Path | 계약 상태 |
|
||||
|---|---|---|---|
|
||||
| SB-001~020 | GET | `/api/v2/home/recommendations` | 사용자 제공 구현 완료 계약, placement HOME_RECOMMENDATION |
|
||||
| SB-001~020 | GET | `/api/v2/audio/recommendations` | 사용자 제공 구현 완료 계약, placement CONTENT_RECOMMENDATION |
|
||||
|
||||
## 11. 보안과 데이터 취급
|
||||
|
||||
- 사용자가 제공한 URL을 실행하기 전 scheme, host, 절대 URI 조건을 검증한다.
|
||||
- 금지 deep-link scheme을 대소문자 무관하게 차단한다.
|
||||
- URI를 임의 정규화하지 않으며 검증에 통과한 입력 문자열을 그대로 연다.
|
||||
- 인증 헤더는 로그인 여부에 따른 기존 정책을 유지한다.
|
||||
|
||||
## 12. 성능과 품질 요구사항
|
||||
|
||||
- 추가 요청 없이 기존 추천 응답에 포함된 목록을 쓴다.
|
||||
- 이미지 크기는 실제 이미지 치수로 계산하며 개별 로딩 실패는 추천 응답 오류로 승격하지 않는다.
|
||||
- 검증은 API 디코딩, 화면별 placement/순서, URL 판정 및 링크 fallback, 이미지·링크 실패 격리를 다룬 fixture 기반 테스트를 포함해야 한다.
|
||||
- 아래 수용 시나리오는 요구사항 검증 기준이며 아직 실행되지 않았다.
|
||||
- fixture 검증은 명시적인 테스트 모드에 한정한다. 운영 요청에서 fixture를 자동 사용하거나 통신 오류를 fixture로 대체하지 않는다.
|
||||
|
||||
## 13. 성공 기준
|
||||
|
||||
### 13.1 기능 수용 기준
|
||||
|
||||
- [x] 두 추천 endpoint의 production DTO/API/repository 검사에서 기존 필드·`banners`와 통합 `subBanners` 소비를 확인했다.
|
||||
- [x] HTTP 200과 `success == true`가 함께 만족될 때만 성공이며, false/비200 실패를 실제 VM 검사로 확인했다.
|
||||
- [x] 익명 Authorization 생략, 로그인 헤더 보존, 추가 endpoint/request/query 없음을 production API/repository 검사로 확인했다.
|
||||
- [x] `Int` 정밀도 및 23개 목록 순서 보존, native에서 21번째 항목 도달을 확인했다.
|
||||
- [x] placement 필터 분리와 빈 필터 결과를 검사하고 native composition에서 empty 추가 gap0을 확인했다.
|
||||
- [x] web http(s)+host 및 deep 절대 URI/금지 scheme을 production URL 검사로 확인했다.
|
||||
- [x] URI 원문 및 iOS/웹/Android 8조합의 opener 시도 순서·횟수를 driver로 확인했다. 실제 외부 앱 launch 검증과 구분한다.
|
||||
- [x] 이미지 실패 placeholder의 국소 처리와 링크 실패 종료를 native harness/production opener 검사로 확인했다.
|
||||
- [x] 실행하지 않은 physical input·실서비스·전체 앱 UI 검증은 별도 한계로 명시했다.
|
||||
|
||||
### 13.2 화면 수용 기준
|
||||
|
||||
- [x] 홈 위치는 최근 활동 크리에이터 다음이다.
|
||||
- [x] 콘텐츠 위치는 상단 배너 다음이자 독점 시리즈 이전이다.
|
||||
- [x] 섹션 간격은 기존 s24이며, 비어 있으면 컴포넌트와 추가 간격이 없다.
|
||||
- [x] native 양 화면에서 가로 전체 폭, 수평 padding 0, 두 원본 비율 높이를 확인했다.
|
||||
- [x] 기존 메인 배너 캐러셀의 코드·연결은 변경하지 않았다.
|
||||
|
||||
## 14. Open Questions
|
||||
|
||||
| ID | 상태 | 결정 필요 사항 | 현재 권고 | 결정 주체 | 결정 시점 |
|
||||
|---|---|---|---|---|---|
|
||||
| OQ-001 | 해소 | 전환 주기와 높이, 빈 목록 처리 | 사용자 승인: 5초 자동 전환, 현재 이미지 원본 비율 높이, 빈 목록 컴포넌트 미표시. swipe/단일 항목 timer 중지는 기존 패턴을 따른다. | 사용자 | 확인 완료 |
|
||||
| OQ-002 | 해소 | 이미지 로딩·실패 표시 | 기존 회색 placeholder 계열, 첫 치수 확인 전 임의 비율 없음, 개별 오류 격리. §9 구현 기본값 적용. | 구현 담당 | 구현 기본값 |
|
||||
| OQ-003 | 해소 | indicator의 표시 여부와 모양 | 기존 우측 상단 현재/전체 표시를 따른다. | 구현 담당 | 기존 패턴 |
|
||||
| OQ-004 | 부분 확인 | 정확한 섹션 spacing과 추천 요청의 인증·실패 처리 seam | 두 View의 VStack은 `SodaSpacing.s24`다. 두 API target의 익명 헤더 및 HTTP status 검증은 구현 단계에서 최소 수정한다. 공통 plugin 영향은 추가 코드 확인 대상이다. | 구현 담당, 사용자 질문 불필요 | 구현 전 |
|
||||
| OQ-005 | 해소 | 새 필드 fixture와 서버 정렬·날짜 필터링 계약 | 사용자 메시지에 계약과 홈 두 항목/빈 목록 및 콘텐츠 빈 목록 예시가 제공됐다. 실제 서버 검증과 계약 확정은 구분한다. | 구현 담당 | 확인 완료 |
|
||||
|
||||
## 15. Decision Log
|
||||
|
||||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항 |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-10-29 | DEC-001 | 확정 | 기존 두 추천 응답에 `subBanners`를 추가하며 별도 조회는 하지 않는다. | 사용자 요구 | SB-001~004 |
|
||||
| 2026-10-29 | DEC-002 | 확정 | 링크 검증과 iOS 우선 후 웹 fallback을 요구하고 Android 링크를 사용하지 않는다. | 사용자 요구 | SB-016~018 |
|
||||
| 2026-10-29 | DEC-003 | 권고 | 5초 자동 순환 등 UX 기본값은 승인 전까지 제안으로만 둔다. | 기존 `BannerCarousel`의 5초 설정은 참고 근거이며 신규 UX 결정은 아님 | SB-014, OQ-001 |
|
||||
| 2026-10-29 | DEC-004 | 정정 대기 | 정확한 spacing/auth/error seam은 추가 코드 확인 전 확정하지 않는다. | 현재까지 조회한 추천 화면·API와 기존 carousel 코드 | OQ-004 |
|
||||
| 2026-10-29 | DEC-005 | 정정 | DEC-002의 링크 우선순위는 사용자 권고안이며 최종 설계 승인 전 확정으로 취급하지 않는다. URI 안전성과 Android 실행 금지는 확정이다. | 최초 요청의 계약/추천 구분 | SB-018 |
|
||||
| 2026-10-29 | DEC-006 | 정정 | 서버 정렬·기간 필터와 응답 예시는 이미 제공된 확정 계약이다. 기존 섹션 간격 s24 및 콘텐츠 isEmpty 수정 필요를 코드로 확인했다. | 사용자 계약, MainHomeRecommendationView:27, MainContentRecommendationView:28, AudioRecommendationsResponse:13 | SB-005, SB-012, OQ-004~005 |
|
||||
| 2026-10-29 | DEC-007 | 확정 | worktree를 만들지 않고 현재 작업 트리에서 구현. 5초 자동 전환, 현재 이미지 원본 비율 높이, 빈 목록 컴포넌트 미표시. | 사용자 후속 답변 | SB-010, SB-014, 실행 방식 |
|
||||
| 2026-10-29 | DEC-008 | 확정 | 나머지는 기존 패턴과 사용자 추천안을 기본값으로 적용: swipe/페이지 카운터, 단일 항목 timer 없음, iOS 우선 후 웹 1회 대체, 실패 팝업 추가 없음. | 사용자 진행 승인 및 기존 앱 패턴 지시 | SB-014, SB-018, OQ-002~003 |
|
||||
|
||||
## 16. 요구사항 추적표
|
||||
|
||||
| 요구사항 범위 | API 계약 | 계획 Phase | 자동 검증 | 수동 검증 |
|
||||
|---|---|---|---|---|
|
||||
| SB-001~005 | §10 | 미작성 | fixture에서 envelope, status, auth, 요청 횟수 확인 | 요청 헤더와 추가 호출 없음 확인 |
|
||||
| SB-006~010 | §10 | 미작성 | lossless 정수, placement, 순서, empty gap 검사 | 두 화면에서 분리 및 빈 상태 확인 |
|
||||
| SB-011~015 | §8.3 | 미작성 | 21개 이상 입력 및 배치 검증 | 폭·비율·섹션 위치 확인 |
|
||||
| SB-016~020 | §8.4 | 미작성 | URL 안전성, 원문 보존, fallback 횟수, 실패 격리 | 실제 대상 없이 mock open 결과로 흐름 확인 |
|
||||
|
||||
## 17. 변경 관리
|
||||
|
||||
- UX 결정이 추가되면 먼저 Open Questions와 Decision Log를 갱신한다.
|
||||
- 사용자 답변과 설계 승인을 PRD에 반영한 뒤 계획/TASK 문서를 작성한다. 이미 제공된 서버 계약을 다시 요청하지 않는다.
|
||||
- 이 문서의 수용 시나리오는 아직 실행하지 않았으며 구현 완료를 뜻하지 않는다.
|
||||
|
||||
## 탐색 검증 기록
|
||||
|
||||
- 2026-10-29: 기존 carousel, 두 추천 화면, 콘텐츠 DTO 및 관련 문서 샘플을 읽고 요구사항을 대조했다. 탐색 결과 중 기존 메인 배너 이동·정사각형 재사용 제안은 사용자 조건과 충돌하여 채택하지 않았다.
|
||||
- Xcode 26.0과 swift/xcrun은 사용 가능하다. 부팅된 시뮬레이터는 없으며 빌드·테스트·네이티브 QA는 미실행이다. 현재 도구 목록에 `lsp_diagnostics`가 없어 해당 검증을 수행했다고 주장하지 않는다.
|
||||
- 제품 코드 수정 없이 PRD만 작성했다. 5초 자동 전환과 서로 다른 원본 비율에 따른 현재 배너 높이 변경 질문에 사용자 답변 대기 중이다.
|
||||
|
||||
## 구현·검증 후속 기록
|
||||
- 2026-10-30: 위 탐색 기록 이후 사용자 답변 DEC-007과 실행 기준 DEC-008에 따라 구현했다. 데이터/API/opener 집중 checks, 전체16개 local scripts, 양 앱 Debug simulator build가 통과했다.
|
||||
- 최신 native 증거는 [native-final/README.md](reviews/evidence/native-final/README.md)다. 최종 캐러셀 hash `7e25db3076d545ffc61ea3a1c5585e7b8a617964ef406d7570286d75a75e010a`와 snapshot·실행 전후 해시가 일치한다. 이전 native 증거는 최종 gesture 변경 전 기록으로 보존한다.
|
||||
- 네이티브 검증은 실제 production 두 View composition/캐러셀과 주변 대역을 사용하는 명시 harness다. physical tap/swipe, 실제 외부 앱 열기, Reduce Motion ON, 실서비스·실기기·전체 앱 UI는 미검증이며 LSP 도구는 제공되지 않았다.
|
||||
- 독립 리뷰의 소스/증거 불일치는 최신 native 재실행으로 해소했다. 최종 판정 APPROVE, 남은 차단 항목 없음. worktree 없이 구현했고 소유 QA 자원은 정리했다.
|
||||
|
||||
## 후속 버그: 스와이프 중 배너 탭 실행 (2026-10-30)
|
||||
- 사용자 재현: 다음 이미지로 넘기려고 드래그했지만 이전 이미지의 링크가 실행된다.
|
||||
- `SB-021` 확정: 배너를 넘기는 수평 드래그와 인식된 짧은 드래그·세로 스크롤은 링크를 실행하지 않는다. 정상 탭은 현재 표시 항목을 정확히 한 번 실행하며 스와이프 직후 탭도 유실하지 않는다.
|
||||
- 원본 비율·5초 자동 전환·전체 목록·빈 영역 숨김·두 화면 배치·링크 정책은 변경하지 않는다. worktree 없이 기존 작업에 이어 수정한다.
|
||||
- 기존 native 증거는 physical tap/swipe 미검증이었다. 이번 검증은 실제 XCTest 좌표 tap/drag를 사용하며 스크립트가 state/opener를 직접 호출한 결과로 대체하지 않는다.
|
||||
- `SB-022` 확정: 마지막→첫 번째 및 첫 번째→마지막 순환도 일반 전환과 같은 방향의 연속 슬라이드로 표시한다. 경계에서 전체 목록을 역방향으로 빠르게 이동하거나 순간 교체하지 않는다. 자동 5초 전환과 수동 swipe 모두 적용한다.
|
||||
- 추가 사용자 보고의 코드 원인: `move(by:)` 경계 분기가 `disablesAnimations = true`로 실제 마지막/첫 번째 사이 이동 자체를 수행한다. 기존 main BannerCarousel처럼 복제 경계 항목으로 애니메이션 후 동일 이미지 위치로만 무애니메이션 정렬하는 방식을 참고한다.
|
||||
- 후속 완료: primitive button style의 배타적인 drag/tap 인식과 전역 drag 좌표, 복제 경계 페이지의 연속 slide/동일 이미지 정렬로 수정했다. 실제 swipe 오탭 RED 및 native11개 GREEN, 자동/수동 경계 중간 프레임 확인. 부모도 native11개·기존16개 check·양 앱 build 통과를 확인했다. [Phase3 리뷰](reviews/phase3-sub-banner-gesture.md) APPROVE.
|
||||
|
||||
## 후속 사용자 결정: 서브 배너 카운트 숨김 (2026-10-30)
|
||||
- DEC-009 확정: 홈·콘텐츠 공용 `SubBannerCarousel`의 우측 상단 현재/전체 배지만 제거한다. §9, OQ-003 및 DEC-008의 시각적 페이지 카운터 기준은 이 결정으로 대체한다.
|
||||
- 접근성 페이지/전체 라벨, 탭·스와이프·연속 순환·5초 타이머·레이아웃과 기존 메인 `BannerCarousel`은 그대로 유지한다. 순수 렌더링 삭제이므로 기존 UI typecheck와 소규모 native XCTest/PNG로 검증하며 새 테스트나 전체 앱 빌드는 추가하지 않는다.
|
||||
Reference in New Issue
Block a user