마이 탭 공지사항 Native QA 증거
판정: NEEDS WORK. 비활성 중 롤링 중지 및 활성 복귀 후 재개(S5)가 실패했다. 제품 코드는 수정하지 않았다.
실행 범위와 출처
이 증거는 전체 SodaLive 앱이 아니라 실제 UpdateBannerView를 실행한 native UIKit + SwiftUI UIHostingController 하네스다. 로그인·서버 요청·실사용자 데이터는 사용하지 않았다. 하네스 및 빌드 캐시는 작업 전용 TMP에만 생성했고 제품 프로젝트에 추가하지 않았다.
- 환경: Xcode 26.0 / SDK 26.0, Intel
x86_64, iPhone SE 3세대 시뮬레이터, iOS 18.3 런타임, 375pt 폭, PNG 750×1334px. - 소유 device:
B929E0F0-50F7-46C1-988E-95272D0CE7A2, bundle:local.sodalive.NoticeNativeQA. - 복수 구현 존재 확인 후 관련 제품 파일이 15초간 동일한 것을 확인했다. 마지막 네이티브 실행 종료 후에도 모든 제품 입력 해시가 동일했다. provenance.json 참고.
MyPageView.swiftSHA-256:86a3dee7a4f2ce5bfa99624ff08c0b8eb7c1e88556fc5c04458f7ed2f72c7638.- 원본
UpdateBannerView블록 SHA-256:828d5737650436db9a69fd98efc3475362d7045285d2fb8dbdd17870d57047a6. 추출본은 블록을 변경하지 않고import SwiftUI만 앞에 추가했다.
MyPageNoticeRoller, NoticeItem 포함 응답 파일, FontModifier, ColorExtension, SodaSpacing도 실제 소스를 컴파일했다. 실제 Pretendard-Regular.otf를 등록했다. AppState/AppStep은 title/content/date 수집 sink로, LanguageOption은 폰트 선택에 필요한 enum case로 대체했다. 마이 페이지 주변부만 fixture·mount·phase 드라이버 및 QA 설명으로 대체했다. 빈 배열의 부모 조건도 현재 제품의 !latestNotices.isEmpty와 동일하게 구성했다.
PhaseProbe는 배너 소스를 변경하지 않는 투명한 sibling이며 같은 환경의 실제 scenePhase 변경을 기록한다. 실제 OS 복귀 검사는 UIKit notification을 환경에 전달하는 하네스 bridge를 사용했다. 제품 앱의 전체 SwiftUI Scene/탭 라우팅을 검증했다는 뜻은 아니다.
증거 목록
| 경로 | 내용 |
|---|---|
matrix/ |
현재 소스의 0/1/2/3, 긴 제목, inactive/active, unmount/remount, 축소 시나리오 27 PNG 및 actions.json |
lifecycle/ |
phase 전달 자체를 기록한 별도 재현 6 PNG 및 actions.json |
os-*.png, os-lifecycle-actions.json |
실제 Settings 앱으로 background 이동 후 동일 PID로 복귀한 시뮬레이터 화면 3 PNG 및 native lifecycle notification |
state-callback.json, source/ActionCheck.swift |
실제 상태 helper와 원본 Button 콜백의 별도 드라이버. 10건의 title/전체 HTML 본문/date 동일성 검사와 빈 상태 검사 |
verification.json, ocr.json, provenance.json |
36 PNG의 signature/크기/해시/신선도, Vision OCR 보조 자료, 관측값과 실패 목록, 실제 소스·하네스 출처 |
matrix/와 lifecycle/는 실제 window에 대한 UIGraphicsImageRenderer + drawHierarchy(afterScreenUpdates: true)로 캡처했다. OS 전환 3장은 simctl io screenshot이다. 모든 PNG는 최종 하네스 소스 작성 이후 생성됐고 750×1334px다. 합성 화면이나 웹 대체물은 없다. two-transition-mid.png는 전환 시점 캡처 시도이며 정확한 중간 프레임이나 애니메이션 성능을 보증하지 않는다.
before 이미지는 만들지 않았다. 비교 기준은 제공된 기존 title + Spacer + 자세히/arrow 구조뿐이며, git HEAD 추출·기준 이미지 생성은 하지 않았다.
관측 결과
| 시나리오 | 판정 | 실제 관측과 증거 |
|---|---|---|
| 0개 | PASS | matrix/zero.png: 배너·화살표 없음 |
| 1개 | PASS | one-initial.png → one-after-6s.png: 시나리오 시작 6.89초 후에도 첫 항목 |
| 2개 | PASS | two-first.png → two-second.png → two-wrap-first.png: 첫째 → 둘째 → 첫째. 4.39초 샘플은 첫째, 7.03초는 둘째, 12.21초는 첫째 |
| 3개 | PASS | three-first.png → three-second.png → three-third.png → three-wrap-first.png: 0.82/6.89/12.05/17.01초에 첫째/둘째/셋째/첫째 |
| 긴 한국어 제목·arrow-only | PASS | long-korean-dark.png, long-korean-light.png: 자세히 없음, 제목 한 줄 tail truncation, chevron 유지, 겹침 없음. 375pt에서 소스상 제목 예산은 295pt = 375−48−24−8. before 대비 픽셀 증가는 측정하지 않음 |
| inactive 중 중지 | FAIL | inactive-start.png의 둘째가 inactive-after-6s.png에서 셋째가 됨. 두 시점 모두 probe는 inactive |
| active 복귀 후 재개 | FAIL | 실제 active 전달 이후 6.78초가 지나도 셋째 유지. 별도 lifecycle/phase-active-next.png에서도 active 주입 후 7.19초 이상 셋째 유지 |
| 실제 OS background/복귀 | FAIL / 하네스 범위 | Settings 앱 이동 시 willResignActive, 같은 PID 복귀 시 didBecomeActive 기록. os-resume-initial.png와 os-resume-after-7s.png가 같은 첫 항목이며 PNG 해시도 동일. 두 캡처 간 실제 간격은 9.71초 |
| unmount/remount | PASS / 관측 범위 | 6초 이상 배너 없음. remount 후 첫째, 5초 전 샘플 첫째, 이후 둘째. 장시간 중복 타이머 부재까지 증명하지 않음 |
| 3→1→0 | PASS | shrink-reset-first.png에서 첫째로 초기화, 6.88초 후 고정, shrink-empty.png에서 숨김 |
5초 간격 자체는 원본의 Timer.scheduledTimer(withTimeInterval: 5, repeats: true)를 그대로 사용했다. 표의 시간은 실제 callback 이후 캡처 완료 uptime이며 시나리오 시작 이후 값이다. 스케줄링 지연이 있으므로 정밀한 5.000초 주기·성능 통과로 해석하지 않는다. 배너 순환을 하네스 알고리즘으로 복제하거나 타이머를 빠르게 바꾸지 않았다.
생명주기 실패 재현
- 3개 항목을 표시하고 둘째 항목이 될 때까지 실제 타이머를 기다린다.
scenePhase = .inactive를 주입한다. probe가 inactive를 읽었음을 로그로 확인한다.- 6초 이상 기다린다. 둘째가 유지돼야 하지만 셋째로 바뀐다.
scenePhase = .active를 주입하고 7초 이상 기다린다. probe는 active지만 셋째가 그대로다.
이 실패는 matrix와 별도 lifecycle 실행에서 모두 재현했다. 조사 대상은 MyPageView.swift의 .onChange(of: scenePhase)와 restartRollTimer()다. new phase 인자를 무시하고 캡처한 환경 값을 다시 읽는 경로를 우선 확인할 필요가 있으나, 원인 수정이나 대체 제품 구현은 이 QA에서 수행하지 않았다.
상호작용 검증 경계
배너 탭·접근성 버튼 활성화는 통과가 아니다. 하네스의 공개 UIKit 접근성 traversal은 actionable node를 반환하지 않았다. accessibilityActivateResult=false, captureDelta=0, native GUI sink는 비어 있다. native 화면을 눌렀다거나 실제 detail 화면으로 이동했다고 주장하지 않는다.
별도 macOS Swift 드라이버는 실제 MyPageNoticeRoller.current/advance()와 제품 Button에서 그대로 추출한 AppState.shared.setAppStep(step: .noticeDetail(notice: item)) 콜백을 실행했다. 항목별 title, 전체 HTML content, 원문 date가 sink에 그대로 전달됨을 확인했다. 이것은 상태/콜백 수준 증거이며 화면에 보이는 버튼의 물리적 hit-testing 증거가 아니다.
미검증: 실제 터치·hit area·detail 화면 렌더링, full MyPage/탭 통합, 로그인·API·실서버·오류 정책, iOS16.6 실제 런타임, 실기기, Dynamic Type·가로모드·다른 폭, VoiceOver 포커스, Reduce Motion 설정 경로, 애니메이션 매끄러움·중단·장시간 타이머 중복. 버튼 label의 소스 높이는 24pt이고 배너 외부 세로 padding은 각각 6pt다. 44pt 터치 영역 확보 여부는 별도 검토가 필요하다.
실행 명령과 정리
전체 인자·exit code·stdout/stderr·실행 시각은 commands-matrix.json, commands-lifecycle.json, commands-os-lifecycle.json에 기록했다. 아래는 실제 사용한 명령 형태다. $QA_ROOT는 작업 종료 시 삭제된 전용 TMP다.
export QA_ROOT=/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/my-notice-native.MtXXGk
export TMPDIR="$QA_ROOT/tmp/"
export NODE_COMPILE_CACHE="$QA_ROOT/node-cache"
export BUN_RUNTIME_TRANSPILER_CACHE_PATH="$QA_ROOT/bun-cache"
xcrun --sdk iphonesimulator swiftc -sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)" -target x86_64-apple-ios16.6-simulator -module-cache-path "$QA_ROOT/swift-module-cache" "$QA_ROOT/source/"*.swift -o "$QA_ROOT/app/NoticeNativeQA.app/NoticeNativeQA"
xcrun simctl install B929E0F0-50F7-46C1-988E-95272D0CE7A2 "$QA_ROOT/app/NoticeNativeQA.app"
xcrun simctl launch B929E0F0-50F7-46C1-988E-95272D0CE7A2 local.sodalive.NoticeNativeQA
xcrun simctl launch B929E0F0-50F7-46C1-988E-95272D0CE7A2 local.sodalive.NoticeNativeQA --lifecycle
xcrun simctl launch B929E0F0-50F7-46C1-988E-95272D0CE7A2 com.apple.Preferences
xcrun simctl io B929E0F0-50F7-46C1-988E-95272D0CE7A2 screenshot "$QA_ROOT/os-resume-after-7s.png"
제품 소스 컴파일·설치·실행은 exit 0이었다. 초기 arm64 산출물은 Intel 시뮬레이터에서 설치 거부됐고 x86_64로 해결했다. 하네스 준비 중 read-only accessibilityReduceMotion 환경 주입은 컴파일 불가여서 제거했으며 Reduce Motion은 미검증으로 남겼다. 공유 캐시 삭제나 라이브러리 설치는 하지 않았다. 실행 당시 임시 컴파일 디렉터리에는 별도 ActionCheck.swift가 없었고, 보존본은 재현을 위해 source/에 함께 두었으므로 그 파일을 native 앱 컴파일에 함께 넣으면 안 된다.
cleanup.json이 소유 하네스/Settings 프로세스 종료, 소유 시뮬레이터 shutdown·삭제, TMP root 삭제의 명령과 최종 부재 확인 영수증이다. 전체 앱 빌드·제품 회귀 테스트·독립 리뷰는 부모 작업 범위이며 이 기록은 그 통과를 대신하지 않는다.