12 KiB
P3-T1: 실제 터치 오탭과 경계 순환 수정
결과와 범위
- 제품 변경은
SodaLive/Sources/V2/Component/Banner/SubBannerCarousel.swift한 파일이다. 기존 mainBannerCarousel, 두 호출부, 모델/API/opener와 production project는 수정하지 않았다. - 실제 XCTest 좌표 터치 RED를 제품 변경 전에 확인했다. 최종 독립 native app/UI-test target은 11 tests, 0 failures,
xcodebuildexit 0이다. 소스 문자열이나 상태 직접 호출을 제스처 증거로 사용하지 않았다. check-sub-banners.sh와--ui모두 exit 0이다. 테스트용 앱, runner와 소유 root/cache를 정리했고 빌려 쓴 simulator와 parent root는 유지했다.
원인과 최소 수정
- 기본 Button과 simultaneous drag의 경합에서
@GestureState isSwiping이 release 시 초기화되어 이전 배너 action이 실행됐다.PrimitiveButtonStyle안에서drag.exclusively(before: TapGesture())로 상호 배제한다. 외부 부모 ScrollView에는 simultaneous로 참여하고 접근성 activation은configuration.trigger()를 유지한다. 고정 시간 탭 억제나 high-priority gesture를 사용하지 않았다. - 인식기를 이동하는 라벨에 붙인 뒤 로컬 좌표로는 112pt 이동이 페이지 임계값에 못 미치는 실제 회귀가 발생했다.
.global좌표계로 바꾼 같은 테스트가 통과했다. 17pt·34pt의 인식된 짧은 드래그는 tap 없이 제자리를 유지한다. - 실제 경계 이동에서 애니메이션을 끄던 분기를 제거했다.
[last] + originals + [first]에 별도 clone identity와 display index를 부여하고 0.35초 같은 방향 전환 후 같은 이미지 위치로만 무애니메이션 정렬한다. 정렬 callback은 revision/index로 보호하며 목록 교체·disappear가 revision을 무효화한다. 경계 정렬 전 display index를 더 넘기지 않는다.
API, 전체 목록, 색상/간격/font tokens, 링크 전달, 실제 페이지 수, URL별 원본 비율, 5초 간격과 scene task cancellation은 유지했다. isSwiping은 자동 전환 보류에만 쓰며 tap 판정에 사용하지 않는다.
하네스와 실행 환경
- 도구: Xcode 26.0 / Swift 5 mode, x86_64 simulator, Ruby
xcodeproj1.27.0. 별도 의존성 설치 없음. - 사용자 소유 기기:
B5F2BA0D-327C-4EE3-94EE-39710A0E1ACC, iPhone SE 3, 실행 결과의 OS 18.3.1 / 22D8075. 모든 run은-parallel-testing-enabled NO, 별도 device 생성 없음. - 자체 root:
/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sub-banner-touch.n2JrMa(검증 후 삭제). - 앱은 현재 production component/model과 실제
Color.swift,Spacing.swift,ColorExtension.swift,FontModifier.swift를 원본 경로에서 컴파일한다. 독립 앱에 필요한LanguageOptionenum만 테스트 대역이다. - 기존
/Users/klaus/Library/Developer/Xcode/DerivedData/SodaLive-clzdtvqrsfonoxcijatzkrqdzrsu/Build/Products/Debug-iphonesimulator/Kingfisher.o와 module을 읽기 전용으로 링크한다. 생성 이미지 3장을 실제 Kingfisher memory cache에 미리 저장한다. 모든 downloader 요청은 로컬 URLProtocol에서 차단하므로 live request가 없다. - 부모 ScrollView 안에서 actual component를 렌더한다.
XCUIApplication,coordinate.press(forDuration:thenDragTo:),.tap()으로 simulator 입력을 보낸다. 화면의 callback 횟수/마지막 ID와 기존 버튼 접근성 label을 assert한다. 입력 전용 scenario는 harness environment만.inactive; 자동/백그라운드 scenario는 실제 scene phase다. work/tests/run-sub-banner-gesture-tests.sh만 명시적으로 native tests를 실행한다. 기존check-*.sh자동 검사에는 추가하지 않았다. runner가 출력한 root는 호출자가 증거 추출 후 정리하는 방식이다.
실행 명령과 RED/GREEN
제품 수정 전 최초 RED (runner가 위 root를 생성):
bash work/tests/run-sub-banner-gesture-tests.sh \
-only-testing:GestureUITests/SubBannerGestureUITests/testSwipeDoesNotTap \
-only-testing:GestureUITests/SubBannerGestureUITests/testManualBoundaryFrames \
-only-testing:GestureUITests/SubBannerGestureUITests/testAutomaticFiveSecondWrapFrames
결과: exit 65. 좌표 drag 뒤 페이지 2 또는 3 도달 assertion은 성공했으나 taps=1;last=1이 taps=0;last=0과 달라 두 테스트가 실패했다. native-tests.log, test-summary.json. 빌드/도구 오류를 RED로 세지 않았다.
최종 GREEN:
SB_RUN_ROOT=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sub-banner-touch.n2JrMa \
SB_RUN_NAME=final-confirmed bash work/tests/run-sub-banner-gesture-tests.sh
결과: exit 0, 11/11 PASS, 테스트 수행 137.219초. native-tests.log, test-summary.json.
| 실제 실행 시나리오 | 확인 결과 |
|---|---|
| 정상 tap, left/right, 양방향 wrap, 직후 tap | drag callback 0, 현재 ID를 tap하면 정확히 1회. clone의 접근성 버튼 중복 없음 |
| 17pt·34pt subthreshold drag, 112pt 이동, 여러 release 위치/hold duration, 연속 입력 | 짧은 drag는 callback/페이지 이동 없음. 충분한 이동과 반복 좌우·wrap 입력은 기대 페이지 도달 |
| 배너 위에서 세로 drag | callback 0, 부모 콘텐츠 row의 실제 화면 Y가 30pt 이상 감소. vertical-scroll.png |
| single / empty / 2:1 및 3:1 ratio / 목록 교체 | single tap 유지, drag callback 없음, empty 높이/불필요 영역 없음, 원본 비율 프레임 확인, 교체 항목 ID 9 tap |
| 실제 두 번의 5초 전환 / scene inactive / delayed wrap 중 replace·hide | 자동 1→2→1, 실제 Home 버튼으로 background 6초 후 같은 페이지, 중간 프레임에서 교체·이탈 후 정상 tap/재표시 |
집중 검사 (각 명령에 TMPDIR=$SB_RUN_ROOT/tmp, NODE_COMPILE_CACHE=$SB_RUN_ROOT/node, BUN_RUNTIME_TRANSPILER_CACHE_PATH=$SB_RUN_ROOT/bun 지정):
bash work/tests/check-sub-banners.sh
bash work/tests/check-sub-banners.sh --ui
두 명령 exit 0. focused.log, ui-typecheck.log. --ui는 실제 Kingfisher와 theme/font를 사용하는 iOS16.6 target typecheck다.
경계 애니메이션: 실제 렌더 프레임
CADisplayLink 30fps에서 실제 UIWindow drawHierarchy PNG를 저장했다. 375×667 PNG signature/크기를 확인했고 보존한 중간/종료 이미지를 직접 열어 확인했다. 최종 전체 캡처는 자동 116, reverse 67, forward 70, replace 116, hide 119 frames다. 보존 PNG는 각 시퀀스의 시작/중간/종료 일부이며 모든 샘플의 timestamp·색 픽셀 수는 frame-analysis.json에 있다. 전체 frame manifest에는 보존하지 않은 샘플 이름도 포함한다.
| 증거 | 관찰 |
|---|---|
| 원본 자동 경계 0049 → 0050 | 117 samples 중 양 이미지가 섞인 프레임 0. 약 31.9ms 사이에 green 359px → red 359px로 통째 교체 |
| 수정 자동 경계 0063 | 10개 중간 프레임. red가 오른쪽에서 들어오며 green을 왼쪽으로 밀어냄 |
| 수정 수동 reverse 0029, forward 0032 | 각각 21/22개 중간 프레임. 들어오는 이미지 영역이 같은 방향으로 단조 증가. 3개 목록에서 중간 green 배너를 역주행하지 않음 |
| replace/hide events.json | 실제 red+green 중간 PNG를 감지한 뒤 각각 1.42ms / 1.00ms에 목록 교체/제거 실행. 고정 시각을 전환 중 증거로 간주하지 않음 |
| 정렬 후 종료 프레임 | 자동/양방향 수동 모두 마지막 5개 배너 영역 hash 동일. manual-settled.png |
초기 좁은 자동 캡처 창은 전환이 끝난 뒤만 잡았으므로 경계 RED로 쓰지 않았다. 원본 snapshot을 별도 project source로 명시해 8~12초 창으로 재실행하여 위 경계 RED를 확보했다. production은 되돌리거나 다시 편집하지 않았다:
SB_RUN_ROOT=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/sub-banner-touch.n2JrMa \
SB_RUN_NAME=red-wide-window \
SB_CAROUSEL_SOURCE="$PWD/docs/20261029_추천_서브_배너/reviews/evidence/native-final/production-source-snapshot/SodaLive/Sources/V2/Component/Banner/SubBannerCarousel.swift" \
bash work/tests/run-sub-banner-gesture-tests.sh \
-only-testing:GestureUITests/SubBannerGestureUITests/testAutomaticFiveSecondWrapFrames
이 테스트는 순환 도달만 assert하므로 원본에서도 exit 0이다. 애니메이션 RED는 위 연속 렌더 샘플의 직접 교체 현상이며 테스트 PASS를 애니메이션 PASS로 오인하지 않는다. baseline-replay.log에는 같은 snapshot의 오탭 재실행 실패도 포함한다.
시행 중 발견하고 해결한 검증 문제
- tap recognizer만 둔 첫 시도는 34pt drag에서 callback이 발생하여 폐기했다. exclusive recognition으로 해결했다.
- exclusive 인식기를 라벨로 옮긴 뒤 로컬 좌표 112pt 이동 회귀를 실제 입력에서 확인했다. 2초 대기 후에도 실패했으며 전역 좌표 수정으로 동일 테스트가 통과했다.
- 부모 scroll의 초기 preference 측정은 실제 화면이 스크롤되어도 0이었다. 실제 row frame 변위를 assert하도록 수정했다. UI-test의 element/query
.count컴파일 오류 1건도 수정했으며 이것은 제품 RED가 아니다. - 캡처가 중단되어도 오래된 manifest를 사용하지 않도록 매 frame 현재 metadata를 저장했다. 좁은 창 누락과 움직임이 거의 끝난 프레임의 hash 차이를 제품 결함으로 세지 않고, 실제 양 끝 이미지 존재와 완전 정착 후 hash를 검사했다.
소스 동결과 정리
| 단계 | SubBannerCarousel SHA-256 |
|---|---|
| 제품 수정 전 native touch RED 및 원본 replay compiled source | 7e25db3076d545ffc61ea3a1c5585e7b8a617964ef406d7570286d75a75e010a |
| 최종 native 실행 직전 / 직후 / compiled source / cleanup 후 | b3e4c5333cf8249ccd7773b5cabd372efb5a5565323bd7778034e110386151fd |
final-confirmed-before.sha256, after, compiled, production snapshot, harness.sha256. 최종 native 증거 이후 production 편집 없음. 현 소스와 snapshot cmp exit 0.
owned-processes.json에 소유 앱 ID와 관찰된 app PID를 기록했다. runner PID 숫자는 xcodebuild 로그에 제공되지 않아 미확인으로 구분한다. 종료 시 launchctl의 해당 app/runner job은 이미 없었고, 두 앱 uninstall exit 0, 소유 root/cache 부재, parent simulator 여전히 Booted, sub-banner-gesture.Fyv2wD 존재를 확인했다. cleanup.json.
미실행 범위
- 전체 main-app build, 전체 16개 check suite, 실제 서비스/외부 링크 launch, 실기기는 이번 범위 밖이다. 기존 native/native-final evidence는 수정하지 않았다.
- iOS16.6은 typecheck이며 실제 touch는 iOS18.3.1 simulator 한 기기다. VoiceOver/Switch Control/hardware keyboard 자체 조작, Reduce Motion, 동시 다중 터치는 실행하지 않았다. 접근성 Button 노출과 action은 유지했지만 전체 보조공학 검증으로 확대 주장하지 않는다.
- LSP 도구는 없으며 native compile/test 및
--uitypecheck로 대체했다. 독립 리뷰/추가 agent는 사용자 명시 금지에 따라 실행하지 않았다. commit/push/worktree/의존성 설치 없음.