docs(recommendation): 서브 배너 리뷰와 QA 증거를 보존한다
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# P1-T1 구현 증거
|
||||
|
||||
작성일: 2026-10-29. 범위: 현재 checkout에서 P1-T1 구현과 집중 검사. worktree/commit/push/하위 에이전트/실서비스 요청 없음. P2 빌드·native QA·독립 리뷰 완료를 주장하지 않는다.
|
||||
|
||||
## 변경 파일
|
||||
|
||||
| 경로 | 변경 |
|
||||
|---|---|
|
||||
| `SodaLive/Sources/V2/Component/Banner/SubBanner.swift` | 6개 필드, lossless `Int`, placement enum, 원문 동일 URL 검증, 이미지·iOS/web 자격 |
|
||||
| `SodaLive/Sources/V2/Component/Banner/SubBannerLinkOpener.swift` | 주입 가능한 completion opener, 실제 UIKit open, iOS 우선/실패 시 웹 1회, Android 실행 없음 |
|
||||
| `SodaLive/Sources/V2/Component/Banner/SubBannerCarousel.swift` | 전체 목록, 현재 이미지 실측 비율/컨테이너 폭, swipe/counter, 5초 자동 순환, 단일·inactive·disappear 중단 |
|
||||
| `SodaLive/Sources/V2/Main/Home/Recommendation/Models/MainHomeRecommendationResponse.swift` | 필수 `subBanners`, 순서 보존 홈 자격 목록 |
|
||||
| `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift` | 추천 GET만 익명/공백 토큰 Authorization 생략, 로그인 원문 및 follow 유지 |
|
||||
| `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift` | 디코딩 전 HTTP 200 검증, 기존 실패 정책/데이터 보존 |
|
||||
| `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationView.swift` | active creators 뒤, 유효 목록 비어 있으면 미삽입, 기존 s24 |
|
||||
| `SodaLive/Sources/V2/Main/Content/Recommendation/Models/AudioRecommendationsResponse.swift` | 필수 `subBanners`, 콘텐츠 자격 목록, `isEmpty` 반영 |
|
||||
| `SodaLive/Sources/V2/Main/Content/Recommendation/Repository/MainContentRecommendationApi.swift` | 익명 GET Authorization 생략, 로그인 원문 유지 |
|
||||
| `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationViewModel.swift` | 디코딩 전 HTTP 200 검증, 기존 실패 UI/hasLoaded/데이터 보존 |
|
||||
| `SodaLive/Sources/V2/Main/Content/Recommendation/MainContentRecommendationView.swift` | main banner 뒤/original-only 앞, 유효 목록 비어 있으면 미삽입, 기존 s24 |
|
||||
| `SodaLive.xcodeproj/project.pbxproj` | 신규 3개 파일을 두 앱의 Sources 및 기존 Banner group에 추가 |
|
||||
| `work/tests/SubBannerCheck.swift`, `work/tests/check-sub-banners.sh` | 실제 프로덕션 API/repository/VM/DTO/opener 실행, native Combine, Moya transport double, 선택적 실제 iOS UI 타입 검사 |
|
||||
| `docs/20261029_추천_서브_배너/plan-task.md`, 이 문서 | Phase 1 체크·진행·검증 증거만 기록 |
|
||||
|
||||
## RED / GREEN
|
||||
|
||||
구현 전 기존 파일만 컴파일하도록 둔 임시 `--baseline` 모드에서 아래 두 실행이 컴파일 성공 후 assertion으로 실패했다. 미존재 타입의 컴파일 오류를 RED로 간주하지 않았다. 최종 runner에서는 이 임시 모드를 제거하고 전체 계약 검사를 항상 컴파일한다.
|
||||
|
||||
| 실행 | 실제 결과 |
|
||||
|---|---|
|
||||
| `bash work/tests/check-sub-banners.sh --baseline` | `FAIL: Anonymous GET must omit Authorization; authenticated/follow must preserve raw token: /api/v2/home/recommendations` |
|
||||
| `bash work/tests/check-sub-banners.sh --baseline --http` | `FAIL: HTTP 201/http must not become successful recommendations (home=true)` |
|
||||
| `bash work/tests/check-sub-banners.sh` | 5개 PASS 출력, exit 0 |
|
||||
| `bash work/tests/check-sub-banners.sh --ui` | `PASS: production carousel/model/UIKit opener iOS16.6 typecheck with installed Kingfisher and real theme/font files`, exit 0 |
|
||||
|
||||
집중 검사의 실행 범위:
|
||||
- 실제 두 추천 GET의 path/method/requestPlain/요청 횟수, 빈 값·공백 토큰 헤더 생략, 원문 bearer, 인접 follow POST/body/header 보존.
|
||||
- 두 VM의 HTTP 201/401/503, HTML503, envelope false/data null, malformed JSON, transport 실패 및 기존 오류 메시지/팝업/로딩 종료. 성공 이후 실패가 기존 목록을 빈 데이터로 바꾸지 않음.
|
||||
- DTO `9_007_199_254_740_993`, `Int.max`, fractional ID 거부, enum/미지원 placement, 필수 배열 누락/null 거부, 23개 역순 목록 보존, placement/Android-only/잘못된 이미지 필터, 기존 메인 배너 보존, sub-banner-only/filtered-empty 콘텐츠.
|
||||
- HTTP(S)+host, 절대 deep URI, 대소문자 무관 금지 scheme, blank/상대/잘못된 escape/자동 percent encoding을 유발하는 입력 거부, accepted escaped URI 원문 보존. iOS/web/Android 존재 여부 8개 조합 각각 성공·실패 completion으로 열기 순서·횟수 검증, 웹 대체 실패 후 재시도 없음.
|
||||
|
||||
## 회귀·소스 포함·진단
|
||||
|
||||
실행 shell마다 아래 패턴으로 소유 임시 root와 캐시를 격리했다. 각 root의 EXIT trap 및 내부 runner trap으로 해당 root만 정리했다.
|
||||
|
||||
```bash
|
||||
TASK_ROOT=$(mktemp -d /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sub-banner-verify.XXXXXX)
|
||||
export TMPDIR="$TASK_ROOT" NODE_COMPILE_CACHE="$TASK_ROOT/node" BUN_RUNTIME_TRANSPILER_CACHE_PATH="$TASK_ROOT/bun"
|
||||
trap 'rm -rf "$TASK_ROOT"' EXIT
|
||||
bash work/tests/check-sub-banners.sh
|
||||
```
|
||||
|
||||
- 전체 회귀 명령: `for script in work/tests/check-*.sh; do bash "$script"; done`와 같은 순서로 각 결과를 수집하는 루프를 600000ms 제한으로 실행했다. `check-app-deep-link-gift-detail`, `check-auth-content-settings-exit`, `check-auth-verification-mock`, `check-event-deep-link`, `check-event-detail`, `check-gift-address-search`, `check-gift-contact-input`, `check-gift-decoding-contract`, `check-gift-detail-cancel-loading`, `check-gift-detail-talk-inquiry`, `check-gift-phase11`, `check-gift-recipient-address`, `check-my-page-notices`, `check-received-gift-detail`, `check-sub-banners` 총 15개 PASS 후 마지막 채팅 날짜 검사에서 tool timeout/`Terminated: 15`. 테스트 assertion 실패가 아니다.
|
||||
- 마지막 검사 분리 실행: `bash work/tests/check-user-creator-chat-date-presentation.sh` → `PASS: 113 assertions`, exit 0. 총 16개 검사 개별 PASS, 단일 전체 루프 완주 아님.
|
||||
- 아래 membership 명령: 두 타깃 `SodaLive-dev`, `SodaLive`에서 신규 파일 3개가 각각 정확히 한 번 Sources에 존재하고 동일 Banner group에 포함됨을 assertion으로 확인, exit 0.
|
||||
- `GIT_MASTER=1 git diff --check`: 출력 없음, exit 0. diff에서 기존 `BannerCarousel.swift`, 메인 배너 section, 공유 `AuthPlugin`, endpoint 및 기타 추천 필드의 미변경 확인.
|
||||
- UI 검사: `xcrun swiftc -swift-version 5 -typecheck -D SB_UI_CHECK -sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" -target x86_64-apple-ios16.6-simulator ...`를 runner에서 실행. 실제 설치된 Kingfisher 모듈과 production 신규 3개 파일/Color/Spacing/ColorExtension/FontModifier 사용. test 파일의 LanguageOption만 최소 double이며 SwiftUI/Kingfisher/UIKit은 double 아님. 로컬 캐시 경로는 `KINGFISHER_MODULE_PATH`로 지정 가능.
|
||||
- 최종 `xcrun swiftc -swift-version 5 -parse`에 변경 production Swift 11개와 `work/tests/SubBannerCheck.swift`를 전달: 출력 없음, exit 0. 집중 검사와 `--ui`도 runner 정리 후 다시 실행해 위 PASS를 재확인했다.
|
||||
|
||||
```bash
|
||||
plutil -convert json -o - SodaLive.xcodeproj/project.pbxproj | python3 -c '
|
||||
import json,sys
|
||||
objects=json.load(sys.stdin)["objects"]
|
||||
names={"SubBanner.swift","SubBannerLinkOpener.swift","SubBannerCarousel.swift"}
|
||||
refs={key for key,value in objects.items() if value.get("isa")=="PBXFileReference" and value.get("path") in names}
|
||||
assert len(refs)==3
|
||||
assert sum(value.get("isa")=="PBXGroup" and value.get("path")=="Banner" and refs.issubset(value.get("children",[])) for value in objects.values())==1
|
||||
targets=[value for value in objects.values() if value.get("isa")=="PBXNativeTarget" and value.get("name") in {"SodaLive","SodaLive-dev"}]
|
||||
assert len(targets)==2
|
||||
for target in targets:
|
||||
paths=[objects[objects[file]["fileRef"]]["path"] for phase in target["buildPhases"] if objects[phase]["isa"]=="PBXSourcesBuildPhase" for file in objects[phase]["files"]]
|
||||
assert all(paths.count(name)==1 for name in names)
|
||||
print("PASS:",target["name"],"three files exactly once; Banner group verified")'
|
||||
```
|
||||
|
||||
## 구현 결정과 남은 검증
|
||||
|
||||
- 새 state/controller 계층 없이 View 내부 index·실측 URL별 ratio와 SwiftUI `.task(id:)`를 사용한다. task ID에 활성 scene과 전체 items가 반영되어 비활성/목록 변경 시 취소·재시작하며 disappear는 SwiftUI가 취소한다. 단일 항목은 sleep 루프가 없다.
|
||||
- HStack의 KFImage는 높이 0에서도 `startLoadingBeforeViewAppear()`로 body 평가 시 로딩한다. `onSuccess`의 실제 image.size만 URL별로 저장하며 현재 URL의 ratio로 높이를 계산하므로 다른 항목의 늦은 성공이 현재 높이를 덮지 않는다. 최초 임의 ratio 없음, 다음 이미지 미로드 시 마지막 실측 비율 유지. 개별 실패는 Kingfisher placeholder에만 남는다.
|
||||
- 실제 화면 렌더, 스와이프와 세로 scroll 공존, 서로 다른 원본 비율, empty gap, 정확한 5초/순환/scene 생명주기, 실제 UIApplication completion은 이 작업에서 native 실행하지 않았다. parent의 P2 native QA 대상이다. 이 문서는 렌더 PASS 증거가 아니다.
|
||||
- full app build/독립 리뷰는 요청에 따라 parent 소유다. `lsp_diagnostics` 도구는 제공되지 않아 수행하지 않았으며 CLI 컴파일·타입 검사·parse만 별도 근거로 삼는다. 실서비스 계약 검증이나 외부 네트워크 호출도 하지 않았다.
|
||||
- 소유 root만 정리한다. parent의 `sub-banner-ios-run.x6GLEo`, shared DerivedData/cache/device를 삭제·변경하지 않는다.
|
||||
- 정리 영수증: timeout 후 남은 소유 `sub-banner-regression.GWjNjC`는 Read로 빈 디렉터리임을 확인하고 `rmdir` 및 `test ! -e` PASS. 마지막 승인된 temp 부모 디렉터리 Read에서 이번 실행의 `sub-banner-red.*`, `red-http.*`, `green.*`, `ui-typecheck.*`, `regression.*`, `membership.*`, `membership-final.*`, `diff.*`, `process-check.*`, `chat-rerun.*`, `final-check.*`, `final-ui.*`, `final-parse.*`, `final-diff.*`, `cleanup-verify.*` root 부재를 확인했다. parent의 `sub-banner-ios-run.x6GLEo`는 그대로 존재한다.
|
||||
Reference in New Issue
Block a user