qa-agent-kit 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qa_agent_kit-0.6.0.dist-info/METADATA +157 -0
- qa_agent_kit-0.6.0.dist-info/RECORD +32 -0
- qa_agent_kit-0.6.0.dist-info/WHEEL +4 -0
- qa_agent_kit-0.6.0.dist-info/entry_points.txt +6 -0
- qa_agent_kit-0.6.0.dist-info/licenses/LICENSE +21 -0
- qakit/AGENTS.md +48 -0
- qakit/__init__.py +21 -0
- qakit/_cli.py +46 -0
- qakit/docs/README.md +14 -0
- qakit/docs/agents.md +40 -0
- qakit/docs/case-studies.md +146 -0
- qakit/docs/device-qa-protocol.md +426 -0
- qakit/docs/getting-started.md +100 -0
- qakit/docs/ko/device-qa-protocol.md +433 -0
- qakit/docs/manifest.md +96 -0
- qakit/docs/mutation-testing.md +69 -0
- qakit/docs/prose-qa.md +49 -0
- qakit/docs/pypi.md +40 -0
- qakit/docs/report.md +64 -0
- qakit/skills/README.md +51 -0
- qakit/skills/device-qa/SKILL.md +87 -0
- qakit/skills/prose-qa/SKILL.md +49 -0
- qakit/skills/qa-rigor/SKILL.md +105 -0
- qakit/tools/device_lock.py +151 -0
- qakit/tools/device_qa.py +838 -0
- qakit/tools/install_skills.py +120 -0
- qakit/tools/mutate.py +684 -0
- qakit/tools/prose_check.py +291 -0
- qakit/tools/recording_frames.swift +78 -0
- qakit/tools/release_checks.py +287 -0
- qakit/tools/screenshot_contrast.swift +42 -0
- qakit/tools/shots_gif.py +81 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# Device QA protocol (출시 QA 절차, 한국어판)
|
|
2
|
+
|
|
3
|
+
> 영어판 [../device-qa-protocol.md](../device-qa-protocol.md) 이 기준이고, 이 문서는 같은 내용의 한국어판이다. 검사 ID 는 두 판이 같다.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
이 문서는 `apps/` 아래 Apple 앱을 내보내기 전에 실기기에서 무엇을 어떤 순서로 확인하는지 정한다.
|
|
7
|
+
개발자는 한 명(주인)이고, QA 팀은 AI 에이전트다.
|
|
8
|
+
테스트 아이폰(iPhone 14 Pro)은 집 서버에 USB 로 꽂혀 있고, 에이전트가 `tools/device_qa.py` 로 몬다.
|
|
9
|
+
검사마다 ID, 통과 기준, 증거(xcresult, 스크린샷, 로그, ASC read-back)가 있다.
|
|
10
|
+
'테스트가 다 초록'은 '출시 준비 끝'이 아니다. 댕오늘 1.0 준비(2026-10-26)는 테스트가 다 초록이었는데 required-reason 선언 누락, 4,000바이트 넘는 심사 메모, 잘린 캡션을 놓쳤다.
|
|
11
|
+
|
|
12
|
+
이 문서는 아래 문서의 내용을 되풀이하지 않고 검사 ID 로 가리킨다.
|
|
13
|
+
|
|
14
|
+
- 모든 코드에 공통인 QA 원칙(경계값·단언·결정론·런타임 검사기·실환경)과 변이 테스트 엔진: `skills/qa-rigor/SKILL.md`. 이 문서는 그 위에 얹는 Apple 앱 전용 층이다.
|
|
15
|
+
- 코드 품질·HIG 체크리스트와 검수 순서: [Apple app quality checklist](../review/apple-app-quality-checklist.md)
|
|
16
|
+
- 시나리오 매니페스트와 `verify_release_scenarios.py`: [Release scenario test protocol](release-scenario-test-protocol.md)
|
|
17
|
+
- 기준선·sanitizer 레인과 `run_crash_free_gates.py`: [Crash-free app protocol](crash-free-app-protocol.md)
|
|
18
|
+
- 접근성·현지화·성능 원칙: [Accessibility, localization, and performance protocol](../development/accessibility-localization-performance-protocol.md)
|
|
19
|
+
- 업로드·메타데이터·제출 절차: [App Store release runbook](../release/app-store-release-runbook.md)
|
|
20
|
+
|
|
21
|
+
도구는 넷이다. `tools/device_qa.py`(`doctor`, `devmode`, `test --app --suite --variant`, `shots`, `crashes`, `perf`, `upgrade`, `release`, `traits`, 앱별 매니페스트 `apps/<slug>/release/testing/device_qa.json`), UI 테스트 공용 헬퍼 `Sources/QAKit/QAKit.swift`(`QA.snap`, `QA.note`, `QA.audit`, `QA.answerSystemAlert`, `QA.sweepButtons`, `QA.assertInFront`), 정적 출시 검사 `tools/release_checks.py`, 변이 테스트 `tools/mutate.py`(앱별 설정 `apps/<slug>/release/testing/mutation*.json`, 게이트는 `crash_free_gates.json` 의 `mutation` 레인)다.
|
|
22
|
+
|
|
23
|
+
## 실행 순서
|
|
24
|
+
|
|
25
|
+
1. **준비 확인.** `python3 tools/device_qa.py doctor --app <slug>` 가 `ready` 를 내야 한다. 기기 페어링, Developer Mode, 암호 없음, 개발자 디스크 이미지, 서명 신원, ASC 키를 본다. Developer Mode 가 꺼져 있으면 `device_qa.py devmode reveal` 뒤 주인이 설정에서 켠다. 기기 환경 기준선도 이때 남긴다(SUITE-09).
|
|
26
|
+
2. **위험 목록.** 지난 출시 태그부터의 `git diff` 로 위험 목록, 사분면 표, 회귀 범위를 만든다. 바뀐 기능의 수용 기준은 코드 전에 주인에게 받는다(PLAN-01~03, PLAN-06).
|
|
27
|
+
3. **스모크.** `device_qa.py test --app <slug> --suite smoke` 가 5분 안에 초록이어야 다음으로 간다(FUNC-01). 빨강이면 다른 일을 멈추고 고치거나 되돌린다.
|
|
28
|
+
4. **전체 묶음과 변형.** `device_qa.py test --app <slug> --suite unit --suite ui --variant default --variant dark --variant large-text --variant small-text --variant english` 로 기기 전체 설정을 바꿔 가며 돈다. 실행 수, 실패, 새 크래시가 `summary.json` 에 남는다(SUITE-01, STAB-01). 이 단계의 빌드는 Debug 이고 증거에도 그렇게 적는다(BUILD-11).
|
|
29
|
+
5. **QA 워크스루와 스크린샷 검토.** `--suite qa` 로 화면마다 `QA.sweepButtons`, `QA.snap`, `QA.audit` 을 돌린다. `device_qa.py shots <xcresult> --out <dir>` 가 만든 `index.md` 접촉 시트를 한 장씩 본다(UI-01, A11Y-01, FUNC-06). 눈에 걸리는 것은 모두 버그로 적는다(DEF-01).
|
|
30
|
+
6. **실패하는 테스트부터 고친다.** 버그마다 먼저 실패하는 테스트를 쓰고, 고친 뒤 통과를 확인한다(SUITE-05). 3단계부터 다시 돌고, 새 결함이 안 나올 때까지 반복한다.
|
|
31
|
+
6a. **변이 게이트.** `python3 apps/scripts/run_crash_free_gates.py --app <App> --run mutation` 으로 지난 출시 뒤 바뀐 로직 줄을 일부러 망가뜨려 테스트가 잡는지 본다. 살아남은 줄마다 테스트를 쓰고 다시 돈다(SUITE-08, qa-rigor 스킬).
|
|
32
|
+
7. **정적 출시 검사.** `python3 tools/release_checks.py --app <slug> --ipa <ipa>` 와 기존 게이트(`run_crash_free_gates.py`, `verify_release_scenarios.py`)를 돌린다. 플레이스홀더, 링크, required-reason API, 목적 문자열 현지화, 심사 메모 바이트, What's New 를 포함한 메타데이터, Apple CDN 의 AASA, 업로드 IPA entitlements 를 본다.
|
|
33
|
+
8. **Release 아카이브를 기기에서.** 깨끗한 트리에서 `apps/<slug>/scripts/archive_ios.sh`(지금은 Pawday 와 Elsewhen 에 있다)로 아카이브하고 업로드용 IPA 를 만든다. 같은 아카이브를 method `debugging` 으로 한 번 더 내보내 기기에 깔고, 블랙박스 스모크와 디버거 없는 콜드 실행을 돌린다(BUILD-01, BUILD-06, BUILD-07, BUILD-11).
|
|
34
|
+
9. **TestFlight 소크와 ASC 확인.** `apps/<slug>/scripts/appstore_release.py --apply --wait-build --metadata --screenshots --attach-build --beta-group --build-version <n>`(Pawday·Elsewhen 형식)로 올리고 붙인다. TestFlight 설치본에 블랙박스 스모크와 프로덕션 왕복을 돌리고, 소크 동안 크래시와 피드백을 읽는다(BUILD-08, CK-06, FIELD-01~03, SUB 영역).
|
|
35
|
+
10. **출시 판정.** 아래 '출시 판정'을 모두 채우면 release_gate 결과와 '테스트 안 한 것' 목록을 주인에게 보낸다. 제출(`appstore_release.py --apply --submit`)과 출시 요청은 주인이 말로 go 를 준 뒤에만 한다.
|
|
36
|
+
|
|
37
|
+
## 검사 영역
|
|
38
|
+
|
|
39
|
+
표 읽는 법이다. 심각도는 놓쳤을 때의 피해다. **차단**은 출시를 막고, **주요**는 고치거나 주인이 위험을 받아야 하고, **경미**는 기록만 한다. "(해당 앱만)"은 그 기능을 쓰는 앱에만 적용되고, "(사람)"은 사람 손이 필요한 검사다. "도구 없음"은 아직 만들어야 할 도구다. `devicectl`, `xcresulttool`, `cktool`, `xctrace`, `altool`, `simctl` 앞에는 `xcrun` 이 붙고, 서버에서는 `DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer` 가 필요하다(`device_qa.py` 는 알아서 넣는다).
|
|
40
|
+
|
|
41
|
+
검사는 어떤 바이너리에서 돌았는지가 결과만큼 중요하다. 증거 행마다 아래 이름 하나를 적는다(BUILD-11).
|
|
42
|
+
|
|
43
|
+
| 바이너리 | 무엇 | 쓰는 곳 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| QA 구성 | Release 최적화에 테스트 훅(launch argument, `URLProtocol` 스텁, `-uiState`, `-flag.x`, 키체인 비우기)을 컴파일한 빌드 | 장애·상태 주입 검사. 도구 없음: pbxproj 생성기에 QA 구성 추가. 그 전까지는 Debug 로 돌리고 증거에 Debug 라고 적는다 |
|
|
46
|
+
| debugging export | 순수 Release 아카이브를 method `debugging`(예전 이름 development)으로 내보낸 IPA, CloudKit Development 와 샌드박스 APNs 를 쓴다 | 기기 설치, 컨테이너 복사, 훅 부재 확인. 프로덕션을 봐야 하면 `iCloudContainerEnvironment=Production` 을 준다 |
|
|
47
|
+
| 업로드 IPA | method `app-store-connect`, export plist 에 `manageAppVersionAndBuildNumber=NO` | entitlements, 번들 내용, 크기 감사(BUILD-07, BUILD-12) |
|
|
48
|
+
| TestFlight·App Store 설치본 | Apple 이 처리한 바이너리, CloudKit Production 과 프로덕션 APNs 를 쓴다 | 블랙박스 묶음. 외부 조작(`devicectl device uninstall app`, Network Link Conditioner, 설정, 두 번째 계정)만 된다 |
|
|
49
|
+
|
|
50
|
+
### 1. 출시 계획과 범위 (Planning)
|
|
51
|
+
|
|
52
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
53
|
+
|---|---|---|---|---|
|
|
54
|
+
| PLAN-01 | 이번 릴리스의 위험 목록(ACC 표: 속성 x 구성요소 → 능력)을 다시 만들고 가능성 x 영향으로 테스트 깊이를 정한다 | `apps/<app>/docs/qa/RISK.md` 가 이번 버전으로 갱신되고, 높은 칸(예: 12점 이상)마다 기법, `.tags(.riskHigh)` 테스트, 차터가 있고, 새 능력의 영향 점수는 주인이 한 번 확인했다 | `git diff --stat <last-tag>..HEAD` 와 DEVLOG·KNOWN_ISSUES 반복 항목을 구성요소(앱, 위젯, 확장, CloudKit, 외부 API 클라이언트)에 매핑한다. 도구 없음: 위험 목록 생성기(높은 칸에 태그 테스트가 없으면 실패) | 주요 |
|
|
55
|
+
| PLAN-02 | 바뀐 기능마다 Agile Testing Quadrants 네 칸을 다 채운다 | 기능당 한 행에 빈 칸이 없다: Q1 Swift Testing 유닛, Q2 USB 아이폰 XCUITest, Q3 탐색 차터, Q4 성능·접근성 감사·보안(ATS 예외, `strings` 로 키 노출 없음, Keychain) | git diff 와 테스트 태그로 표를 만들고, 유닛 70 · 통합 20 · E2E 10 을 첫 목표로 둔다. 도구 없음: 사분면 표 생성기 | 주요 |
|
|
56
|
+
| PLAN-03 | 회귀 범위는 영향 분석과 RCRCRC 로 고르되 아카이브 전에는 전체를 돌린다 | Recent, Core, Risky, Configuration-sensitive, Repaired, Chronic 여섯 칸에 테스트가 있고, `*.plist`·`*.entitlements`·`PrivacyInfo.xcprivacy`·`project.pbxproj`·CloudKit 스키마가 바뀌면 전체 계획을 돈다 | 빠른 루프는 `xcodebuild test -only-testing:<Target>/<Suite>`, 아카이브 전에는 `device_qa.py test --suite unit --suite ui` 전체 | 주요 |
|
|
57
|
+
| PLAN-04 | '테스트 안 한 것' 목록을 쓰고 남은 위험은 명시적으로만 받는다 | 안 돈 차터·현장 항목·검사마다 위험 한 문장이 있고, 주인이 자기 말로 수락하거나 거절했고, 같은 목록이 TestFlight What to Test 에 들어갔다 | 게이트가 건너뛴 검사(시뮬레이터 멈춤, 설정 자동화 실패, 두 번째 기기 없음)를 모은다. 도구 없음: 건너뛴 검사 수집기 | 주요 |
|
|
58
|
+
| PLAN-05 | release_gate 하나가 업로드 전, 제출 전, 출시 전에 PASS 나 BLOCK 을 낸다 | 행마다 기계 증거(xcresult, 로그, 스크린샷, ASC read-back)가 링크되고, BLOCK 은 실패 검사와 증거를 이름 대고, 주인이 넘긴 BLOCK 은 그의 말로 수락된 위험으로 DEVLOG 와 RELEASE_CHECKLIST.md 에 남는다 | 기준은 테스트 전에 정하고 측정 행에는 문턱값을 둔다. 도구 없음: release_gate(`run_app_release_readiness.py` 확장) | 차단 |
|
|
59
|
+
| PLAN-06 | 코드 전에 주인 말로 수용 기준을 쓰고, diff 를 안 본 별도 모델이 수용 테스트를 쓴다 | 바뀐 기능마다 Given/When/Then 기준과 주인의 한 줄 OK 가 있고, 수용 테스트는 다른 모델이나 세션이 기준만 보고 썼다 | 같은 에이전트가 코드와 테스트를 다 쓰면 테스트가 구현을 닮는다(ISTQB CTFL §1.5.3, §4.5). 요구를 잘못 읽은 것이 가장 잦은 누락이다 | 주요 |
|
|
60
|
+
|
|
61
|
+
### 2. 빌드 산출물·설치·업그레이드 (Build)
|
|
62
|
+
|
|
63
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
64
|
+
|---|---|---|---|---|
|
|
65
|
+
| BUILD-01 | 정확한 Release 빌드를 USB 아이폰에서 디버거 없이 돌린다 | 깔린 export 의 CFBundleVersion 과 GitCommit 이 실행 전후 같고, Smoke 와 전체 UI 계획이 통과하고, 콜드 실행 5회에 새 크래시·watchdog(0x8badf00d)·jetsam 이 없고, 실행마다 첫 화면에 핵심 데이터가 붙어 있다(프로세스는 살아 있는데 카드가 비면 실패, FEED-07) | `device_qa.py release --app <slug> --archive <xcarchive>`: 그 아카이브를 debugging 으로 내보내 설치하고, 디버거 없이 5번 콜드 실행해 매번 살아 있는지·스크린샷·이후 크래시/jetsam 을 본다(`--cloudkit Production` 으로 운영 왕복 준비). `-exportArchive` method `debugging` → `devicectl device install app` → `devicectl device info apps` 로 버전 확인, 구동은 블랙박스 타깃(BUILD-08)이나 `UseDestinationArtifacts` xctestrun 으로 한다(`test-without-building` 은 build-for-testing 앱을 덮어 깐다) | 차단 |
|
|
66
|
+
| BUILD-02 | Debug·Release 설정 차이를 검토하고 디버그 코드가 순수 Release 에 못 들어가게 한다 | `-showBuildSettings` 차이가 모두 허용 목록에 있고, debugging export 에 `#if DEBUG` 기호, 디버그 메뉴, 테스트 launch argument 처리기가 없다(QA 구성은 대상 아님) | 두 구성의 `xcodebuild -showBuildSettings` 를 `apps/<app>/qa/release_settings_allowlist.txt` 와 비교하고 `strings -a` 로 확인한다. 도구 없음: 설정 차이 비교기 | 주요 |
|
|
67
|
+
| BUILD-03 | 새 설치 첫 실행을 오프라인 먼저, 온라인 나중으로 본다 | 온보딩과 의미 있는 빈 상태가 나오고, 맥락 없는 권한 팝업 연속과 크래시가 없고, 망이 돌아오면 재실행 없이 회복하고, 녹화본을 검토했다 | `devicectl device uninstall app`(App Group 형제 앱 포함) 뒤 설치, Release 에서는 Network Link Conditioner 100% Loss 로 끊고, `devicectl device capture screen-record --duration 30` 으로 녹화한다. 키체인 비우기는 QA 구성에서만 된다 | 차단 |
|
|
68
|
+
| BUILD-04 | 이전 출시본(N-1, 저장 형식이 바뀌면 N-2) 위에 덮어 깔아 실제 데이터로 올린다 | 첫 실행에 크래시가 없고, 마이그레이션은 한 번만 돌고, 온보딩이 다시 안 나오고, 심어 둔 즐겨찾기·투표·설정·카드·위젯 설정이 남는다 | 로컬은 태그 worktree 를 debugging export 로 깔고 `devicectl device copy from --domain-type appDataContainer`(와 `appGroupDataContainer`)를 전후 diff 한다. 실제 경로는 App Store N-1 → 프로덕션에서 데이터 심기 → TestFlight N 설치다. `device_qa.py upgrade --app <slug> --from <이전 태그>`: 이전 소스를 임시 worktree 로 빌드해 깔고, 매니페스트 `upgrade.seed_file`(블랙박스 UI 테스트)로 실사용 데이터를 심은 뒤 현재 빌드를 덮어 깔아 `upgrade.check` 테스트(QA_UPGRADE=1)를 돈다. 남은 것: 컨테이너 diff, App Store 경로 | 차단 |
|
|
69
|
+
| BUILD-05 | 삭제 후 재설치에서 키체인과 App Group 잔여물을 본다 | 키체인 잔여물로 온보딩을 잘못 건너뛰거나 죽은 토큰을 다시 쓰지 않고, 형제 앱이 남긴 App Group 데이터를 맞게 쓰거나 무시하고, 옛 형식에서 크래시가 없다 | 시드 → `devicectl device uninstall app`(위젯·형제 앱은 남김) → 재설치 → 상태 단언을, 키체인 유지와 삭제(QA 구성 훅) 두 경로로 돈다 | 주요 |
|
|
70
|
+
| BUILD-06 | ASC 에 붙은 빌드가 게이트를 통과한 그 빌드인지 증명하고 핫픽스 재료를 남긴다 | Info.plist `GitCommit` 과 빌드 번호가 게이트 기록과 ASC read-back 에서 같고, 출시 뒤 그 SHA 에 `vX.Y.Z` 태그와 .xcarchive·dSYM 이 있다 | export plist 에 `manageAppVersionAndBuildNumber=NO` 를 두고 `GET /v1/appStoreVersions/{id}/build` 로 비교한다. 도구 없음: 게이트 기록 대조(불일치면 제출·출시 거부) | 차단 |
|
|
71
|
+
| BUILD-07 | 업로드하는 IPA 가 프로덕션 entitlements 를 가진다 | 앱과 모든 .appex 에 `aps-environment=production`(푸시 앱), `com.apple.developer.icloud-container-environment=Production`, 출시용 associated domains 와 App Groups 가 있다 | method `app-store-connect` IPA 에 `release_checks.py --app <slug> --ipa <ipa>`(`codesign -d --entitlements - --xml`)를 돌린다. debugging export 는 development 값이 맞으므로 대상이 아니다 | 차단 |
|
|
72
|
+
| BUILD-08 | 처리된 TestFlight 바이너리 자체를 스모크한다 | TestFlight 로 깔린 빌드 번호가 후보와 같고 블랙박스 스모크가 통과한다 | 앱 타깃 의존이 없는 UI 테스트 타깃에서 `XCUIApplication(bundleIdentifier:)` 로 띄우고, 버전은 `devicectl device info apps --include-default-apps` 로 읽는다(필요 여부 미확인). TestFlight 앱의 Install 도 같은 API 로 누른다 | 주요 |
|
|
73
|
+
| BUILD-09 | 시스템이 업데이트 너머로 기억하는 식별자를 계약 파일로 묶는다 | 위젯 kind, AppIntent 타입·파라미터 id, 알림 카테고리 id 와 userInfo 키, BGTask id, SceneStorage·UserDefaults 키, App Group 파일 이름, URL 경로가 마이그레이션 없이 바뀌지 않았다 | 앱마다 계약 파일을 두고 diff 한다. BUILD-04 는 N-1 위젯을 놓고 N-1 로컬 알림을 걸어 둔 채 올린 뒤 둘 다 누른다. 도구 없음: 계약 파일 diff 게이트 | 차단 |
|
|
74
|
+
| BUILD-10 | Xcode·SDK 메이저가 바뀌면 'linked on or after' 동작 변화를 따로 본다 | `DTXcode` 가 지난 출시본과 다르면 릴리스 노트 체크리스트를 만들고, 최신과 가장 오래된 지원 런타임에서 전체 계획과 시각 기준선(UI-02)을 다시 돌렸다 | Info.plist 를 훑어 `UIDesignRequiresCompatibility`(iOS 27 SDK 에서 무시), UIScene 수명주기(TN3187, 없으면 실행 안 됨), `LSApplicationQueriesSchemes` 25개 상한을 본다. 도구 없음: SDK 변경 감지와 plist 키 검사 | 차단 |
|
|
75
|
+
| BUILD-11 | 빌드 출처를 고정하고 증거 행마다 어떤 바이너리였는지 적는다 | 아카이브 때 `git status --porcelain` 이 비어 있고 동시 편집이 없으며, 모든 증거 행에 바이너리 종류와 CloudKit·APNs 환경이 적혀 있다 | 위 '바이너리' 표의 이름을 쓴다. 도구 없음: release_gate 의 출처 기록 | 주요 |
|
|
76
|
+
| BUILD-12 | 번들 안에 무엇이 실리는지와 크기를 감사한다 | .app 과 .appex 내용이 허용 목록과 맞고, 골든·패리티 픽스처, provenance JSON, 문서, 스크립트, 테스트 리소스가 없고, 크기 증가가 설명된다 | 업로드 IPA 를 풀어 목록을 비교하고 `App Thinning Size Report.txt` 를 지난 릴리스와 비교한다. 도구 없음: 번들 내용 감사 | 주요 |
|
|
77
|
+
| BUILD-13 | 업로드와 스토어가 강제하는 업데이트 규칙을 지킨다 | 모든 .appex 의 `CFBundleShortVersionString` 과 `CFBundleVersion` 이 앱과 같고(ITMS-90473), 빌드 번호가 더 크고, `UIRequiredDeviceCapabilities` 는 유지나 완화만 했고, 배포 타깃을 올렸으면 N-1 디코딩(FEED-01)과 N-1 쿼리(CK-02)가 산다 | 생성기(`generate_ios_pbxproj.py`)가 `MARKETING_VERSION`·`CURRENT_PROJECT_VERSION` 을 프로젝트 수준에 두는지 단언한다. 도구 없음: 업로드 규칙 검사(`release_checks.py` 에 추가) | 주요 |
|
|
78
|
+
|
|
79
|
+
### 3. 기능 설계와 오라클 (Functional)
|
|
80
|
+
|
|
81
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
82
|
+
|---|---|---|---|---|
|
|
83
|
+
| FUNC-01 | 5분 안쪽 Smoke 를 남기는 모든 빌드와 아카이브 전에 돌리고, 빨간 Smoke 는 그 빌드의 다른 테스트를 멈춘다 | 기기에서 콜드 실행, 모든 탭, 상세 열기, 투표와 취소, 딥 링크 하나, 백그라운드 왕복 하나가 통과하고, 새 핵심 흐름은 나올 때 Smoke 에 들어간다 | `device_qa.py test --app <slug> --suite smoke`(매니페스트에 smoke 묶음을 더한다) | 차단 |
|
|
84
|
+
| FUNC-02 | 바뀐 입력·필터·카운터·범위에 동등 분할과 3값 경계 분석을 쓴다 | 바뀐 함수마다 분할 표가 테스트 주석에 있고 분할 커버리지가 100% 다(예: 반경 {<0, 0, (0,1 km], (1,5 km], >max}, 사유 길이 {0, 1, max-1, max, max+1}) | Swift Testing `@Test(arguments:)` 매개변수 테스트로 표를 옮긴다 | 주요 |
|
|
85
|
+
| FUNC-03 | 여러 조건 규칙(추천, 문장 조립)을 결정 표로 모델링하고 가능한 열을 모두 테스트한다 (해당 앱만) | 위치 권한, WeatherKit 결과, 영업 중, 견종 크기, 내 지난 👎, 망, Apple Intelligence 가용성 조합마다 통과 테스트가 있고, 기대값이 없는 열은 주인에게 한 번 물어 고정했다 | 가짜를 주입한 표 기반 Swift Testing, 조건은 코드 분기와 RECOMMENDATION.md 에서 뽑는다 | 주요 |
|
|
86
|
+
| FUNC-04 | 화면과 생명주기를 상태 표로 모델링해 유효 전이 100% 와 무효 시도를 덮는다 | 상태 표가 docs/qa 에 있고, 두 번 탭 투표, 저장 중 뒤로, 지운 장소로 딥 링크, 타입이 다른 NavigationLink 값마다 테스트가 있고, 화면은 accessibility identifier 로 확인한다 | 이벤트는 콜드 실행, 딥 링크, 위젯 탭, 알림 탭, `press(.home)`, `activate()`, 메모리 경고, 망 끊김, 권한 변경이다(실제 사례: `[Place]` 경로에 guide 값을 넣은 NavigationLink 가 조용히 무시됨, 2026-10-25) | 주요 |
|
|
87
|
+
| FUNC-05 | 주장 투어: 제품이 하는 모든 주장에 테스트와 증거가 있다 | `appstore_config.json` 의 부제·설명·What's New·심사 메모, 온보딩 문구, 빈 상태, 목적 문자열의 주장마다 테스트와 스크린샷이 있고, 통과하거나 주인 OK 로 고치거나 뺐다(예: '매시 갱신'이면 임의 3시각에 카드가 60분 안) | 메타데이터와 문자열 카탈로그에서 주장 행을 만들고 행마다 `QA.snap` 을 붙인다. 도구 없음: 주장 표 생성기 | 주요 |
|
|
88
|
+
| FUNC-06 | 골든 사례와 목적 오라클을 파이프라인 숫자가 아니라 사용자 눈에 보이는 결과에서 본다 | 주인이 이름 댄 장소가 최종 화면(top 10 카드, 추천)에 맞게 나오고, 개가 못 가는 곳(고양이 카페), 한 목록 안 중복, 터무니없이 먼 테마(175 km)가 없고, 거리와 영업 시간이 그럴듯하다 | 골든 위치에서 기기로 추천 흐름을 돌려 스크린샷과 데이터 덤프를 골든 목록과 대조한다(Pawday `tools/golden.py`). 새 결함이 안 나올 때까지 냉정 검증 루프를 돈다 | 차단 |
|
|
89
|
+
| FUNC-07 | 화면 사이의 일관성(Product 오라클) | 같은 장소의 이름, 평점, 거리, 영업 상태, 투표 수가 목록·상세·위젯·알림·공유 카드에서 같고, 날짜·시간·거리 형식도 같다 | 한 테스트에서 장소 하나를 모든 화면으로 따라가며 `app.debugDescription` 과 위젯 스크린샷의 글자를 비교한다 | 주요 |
|
|
90
|
+
| FUNC-08 | 사실을 비슷한 제품과 현실에 대본다 | 임의 장소 10곳의 이름·주소·시간·거리가 Apple 지도와 허용 오차 안에서 맞고, WeatherKit 기온이 같은 분의 날씨 앱과 맞고, Elsewhen 도시 시각이 DST 무렵 시계 앱과 맞다 | `devicectl device process openURL 'https://maps.apple.com/?q=<name>'` 와 `devicectl device capture screenshot` 으로 두 화면을 남긴다 | 경미 |
|
|
91
|
+
|
|
92
|
+
### 4. 테스트 묶음의 무결성 (Suite)
|
|
93
|
+
|
|
94
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
95
|
+
|---|---|---|---|---|
|
|
96
|
+
| SUITE-01 | 실행 자체를 검증한다: 실행·건너뜀·예상 실패 수와 목적지 | 실행 수가 0보다 크고 지난 릴리스와 소스의 `@Test`·`func test` 수 이상이며, 새로 건너뛴 테스트가 없고, 목적지가 실제 아이폰 UDID 다 | `device_qa.py test` 의 `summary.json`, `xcresulttool get test-results summary`, `xcresulttool compare --baseline-path`, `xcodebuild test -enumerate-tests`(실제 사례: 생성 pbxproj 고정 목록 탓에 새 테스트 파일이 0개 돌고 성공, 2026-10-24). 도구 없음: 지난 릴리스 대비 수와 목적지 비교 | 차단 |
|
|
97
|
+
| SUITE-02 | 재시도가 빨강을 초록으로 바꾸지 못하게 한다 | 새 UI 테스트는 기기에서 재실행 20회를 통과한 뒤 게이트에 들고, 릴리스 실행에서 실패 후 통과한 테스트는 FLAKY 로 막고, 격리 테스트는 `withKnownIssue`·`XCTExpectFailure` 와 `qa/flaky.md` 버그 링크를 단다 | `xcodebuild test -only-testing:<id> -test-iterations 20 -run-tests-until-failure -test-repetition-relaunch-enabled YES`, `-retry-tests-on-failure` 는 진단 실행에서만 쓰고, 실행 순서는 random 이다 | 주요 |
|
|
98
|
+
| SUITE-03 | 고정 sleep 없이 상한 있는 대기와 테스트별 시간 한도를 쓴다 | UI 테스트 파일에 `sleep(`·`Thread.sleep` 이 없고(`QA.answerSystemAlert` 같은 상한 있는 폴링은 예외 목록에 둔다), 대기는 `waitForExistence`·`wait(for:toEqual:)`·`XCTNSPredicateExpectation` 과 상수 파일의 타임아웃을 쓴다 | `executionTimeAllowance` 나 계획 Test Timeouts 를 두되 계획의 최소값은 60초다. 도구 없음: sleep 검사 가드 | 경미 |
|
|
99
|
+
| SUITE-04 | 잎 요소에 안정적이고 유일한 accessibility identifier 를 두고 페이지 객체 뒤에 숨긴다 | 화면별 identifier 덤프 테스트가 중복과 누락에 실패하고, identifier 는 Text·Button·ScrollView 에 있고 ZStack·GeometryReader 에는 없고, 지역화 라벨로 찾는 테스트가 없다 | 화면마다 `app.debugDescription` 을 `QA.note` 로 붙여 파싱한다(실제 사례: 컨테이너 id 가 자식 id 를 덮고 `.accessibilityElement(children: .combine)` 이 자식 id 를 삼킴, 2026-10-25) | 주요 |
|
|
100
|
+
| SUITE-05 | 고친 버그마다 확인 테스트가 고치기 전엔 실패하고 고친 뒤엔 통과한다 | DEVLOG 에 실패·통과 증거 쌍이 있고, `.bug(...)` 트레이트나 테스트 이름에 버그 id 가 있으며, 이 증거 없이 버그를 닫지 않는다 | `git worktree add /tmp/pre <fix>^` 에서 그 테스트 하나를 돌리고 고친 커밋에서 다시 돌린다 | 주요 |
|
|
101
|
+
| SUITE-06 | 테스트 계획이 있고 생성 스킴에 연결된다 | 앱마다 Smoke, Full(random 순서, 타임아웃, 스크린샷 모두 보관), L10n, Perf, Checkers `.xctestplan` 이 있고 `xcodebuild -showTestPlans` 에 나온다(2026-10-28 기준 `apps/` 에 0개) | 생성기(예: `apps/pawday/scripts/generate_ios_pbxproj.py`)에 계획을 넣는다. 계획은 빌드 구성을 못 고르므로 Perf 의 Release 는 `xcodebuild -configuration Release` 나 Test 가 Release 인 별도 스킴으로 준다 | 주요 |
|
|
102
|
+
| SUITE-07 | 익숙한 문제 목록(error guessing)을 아카이브 전 자동 공격으로 돌린다 | `docs/qa/FAMILIAR_PROBLEMS.md` 항목마다 자동 검사가 있고 모두 통과하며(pbxproj 테스트 목록 대 디스크, 컨테이너 identifier, XCTest 중 CloudKit 진입 차단, zsh `$args` 분할, ATS 가 막는 http 이미지), 늦게 찾은 버그마다 항목이 는다 | 항목마다 작은 rg·python 검사를 게이트에서 부른다. 도구 없음: FAMILIAR_PROBLEMS 검사 묶음 | 주요 |
|
|
103
|
+
| SUITE-08 | 변이 테스트로 테스트가 버그를 실제로 잡는지 잰다 | 지난 출시 태그 뒤로 바뀐 로직 줄의 변이가 모두 죽었거나(survived·uncovered 0), 동등 변이 줄에 `mutation-ok: <이유>` 가 있다. 화면 코드는 설정의 `exclude` 로 빠지고 걷기·스크린샷 검사가 맡는다 | `run_crash_free_gates.py --app <App> --run mutation`(= `tools/mutate.py --config apps/<slug>/release/testing/mutation*.json --changed-since 'latest-tag:<slug>/*'`), `run_app_release_readiness.py --profile final` 이 같은 레인을 돈다. SwiftPM 은 맥에서 커버리지와 함께(변이 하나에 10~20초), Xcode 앱은 전용 시뮬레이터 `QA Mutation` 에서(1~2분). 태그가 없는 앱은 표본 전체 측정이 되므로 출시 때 `release_tags.py tag` 를 남긴다. 절차·함정은 qa-rigor 스킬 | 차단 |
|
|
104
|
+
| SUITE-09 | 테스트 시작 전에 기기 환경을 기준선과 맞춘다 | 외형, 글자 크기, 허용된 권한, 남은 가상 위치, 집중 모드, 저전력, 배터리, 여유 공간, 대기 중 iOS 업데이트, 로케일, 자동 시간대, iCloud 계정이 기준선과 같고, 끝나면 복원된다 | `device_qa.py doctor` 를 먼저 돌리고 `devicectl device info appearance` 등을 저장한다(지금 `device_qa.py` 는 변형 뒤 원래 값이 아니라 iOS 기본값으로 되돌린다). 도구 없음: doctor 의 환경 기준선 비교와 복원 | 주요 |
|
|
105
|
+
|
|
106
|
+
### 5. 중단과 앱 생명주기 (Interruptions)
|
|
107
|
+
|
|
108
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
109
|
+
|---|---|---|---|---|
|
|
110
|
+
| INT-01 | 핵심 흐름마다 작업 중에 끊었다가 돌아온다 | (사유 입력, 로딩, 투표 중, 시트, 검색) x (홈 후 복귀, 알림 센터, Siri, 알림 배너)마다 같은 화면·입력값·스크롤 위치가 돌아오고, 중복 쓰기와 멈춘 스피너가 없다 | `XCUIDevice.shared.press(.home)` → `app.activate()`, 상단 좌표 드래그, `XCUIDevice.shared.siriService.activate(voiceRecognitionText:)`. `devicectl device process suspend`·`resume` 은 사용자 중단이 아니라 타임아웃·타이머 스트레스로 따로 센다 | 주요 |
|
|
111
|
+
| INT-02 | 백그라운드에서 시스템 종료, 쓰기 중 종료 뒤 재실행한다 | 재실행이 탭과 NavigationPath(SceneStorage)를 복원하고, 저장 중 종료가 중복이나 반쪽 CloudKit 레코드를 남기지 않는다(사용자가 쓸어 닫아 SceneStorage 가 사라지는 것은 정상) | `press(.home)` → `devicectl device process terminate --kill --pid <pid>`(기본은 앱이 잡을 수 있는 SIGTERM) → `devicectl device process launch`, 쓰기는 `cktool query-records` 로 확인한다 | 주요 |
|
|
112
|
+
| INT-03 | 무거운 화면(사진 캐러셀, 지도, 긴 목록)에서 메모리 경고 | 스크롤 중 경고 3번에 크래시와 입력 손실이 없고, 버린 이미지는 다시 뜨고, 뒤에 jetsam 보고가 없다 | XCUITest 스크롤 중 `devicectl device process sendMemoryWarning --pid <pid>`, STAB-01 과 짝짓는다 | 주요 |
|
|
113
|
+
| INT-04 | 회전: 가로로 띄우고, 반쯤 채운 폼과 열린 시트로 여러 번 돌린다 | 가로 실행이 되고, 5번 연속 회전 뒤 값·시트·키보드·safe area 가 맞고, iPhone 용 `UISupportedInterfaceOrientations` 에 portraitUpsideDown 이 없다(Face ID 아이폰은 무시하므로 뒤집힘은 iPad 에서만 본다) | `XCUIDevice.shared.orientation = .landscapeLeft` 나 `devicectl device orientation set` 회전 루프, Info.plist 는 `plutil -p` 로 단언한다 | 경미 |
|
|
114
|
+
| INT-05 | 쓰기 도중 실제 전화와 화면 잠금 (사람) | 통화가 끝나거나 잠금을 풀면 같은 화면과 입력 텍스트가 남고 중복 투표가 없다 | 전화를 만드는 API 가 없다. 에이전트의 XCUITest 가 투표 중에 멈춰 기다리는 동안 주인이 전화를 걸고 잠갔다 푼다 | 주요 |
|
|
115
|
+
| INT-06 | 화면마다 글자 크기와 다크 모드를 앞에서, 그리고 앱이 백그라운드에서 기다릴 때 바꾼다 | 화면마다 바꾸기 전과 같은 프로세스가 살아 돌아오고(재실행 아님), 크래시·watchdog(0x8BADF00D scene-update) 보고가 없다 | `device_qa.py traits --app <slug>`: 매니페스트 `traits.screens` 의 launch argument 로 화면마다 띄워 가장 작은 글자 → 가장 큰 접근성 크기 → xxxL → 기본 → 다크 → 라이트를 앞에서 바꾸고, 설정 앱을 앞에 둔 채 한 번 더 바꾼 뒤 돌아와 pid 와 크래시 보고를 본다. iOS 는 기다리는 앱에 새 설정으로 스냅숏을 다시 그리게 하고 10초를 넘기면 죽인다. 댕오늘 지도는 사진 줄 높이가 지도 맞춤을 다시 부르는 루프로 초당 약 45번 다시 그리다 죽었다(2026-10-28) | 차단 |
|
|
116
|
+
|
|
117
|
+
### 6. 권한과 동의 (Permissions)
|
|
118
|
+
|
|
119
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
120
|
+
|---|---|---|---|---|
|
|
121
|
+
| PERM-01 | 앱이 건드리는 보호 자원마다 권한 상태 행렬 | 위치·사진·카메라 등에서 미정→허용, 거부, 한 번 허용, 축소 허용(정확한 위치 끔, 제한된 사진), 백그라운드에서 설정 취소, 재허용이 모두 되고, 대체 UI 에 닿고, 재실행 없이 회복한다 | 실행 전 `app.resetAuthorizationStatus(for: .location)`, 팝업은 `QA.answerSystemAlert`(springboard, BusinessActionSheet, ko·en 라벨), 취소는 `com.apple.Preferences` 구동이다. iOS 27 시뮬레이터에서는 위치 팝업이 트리 조회를 막으므로 실기기에서 한다 | 차단 |
|
|
122
|
+
| PERM-02 | 알림 권한: 거부·임시·취소 경로 | 알림을 거부해도 핵심 흐름이 돌고, 예약 전에 `getNotificationSettings` 를 보고, 취소가 앱 UI 에 반영된다 | Xcode 27.1 `XCUIProtectedResource` 에 알림이 없으므로 삭제 후 재설치로 초기화한다. iOS 27 에서 팝업이 바로 다시 뜨는지 한 번 확인한다 | 주요 |
|
|
123
|
+
| PERM-03 | 권한 사전 화면과 동의 문구 | 사전 화면에는 계속·다음 버튼 하나만 있고 나중에·Not Now 같은 나가기가 없고, 푸시·위치·추적 없이 앱을 쓸 수 있고, 홍보·브리핑 푸시는 앱 안 동의와 끄기 토글이 있다. 법정 동의 화면의 동의·동의 안 함은 HIG 예외로 허용한다 | SwiftUI 소스에서 사전 화면 버튼 제목(Allow, 허용, 권한, 설정하기, Not Now, 나중에)을 검사하고 알림 카테고리마다 설정 토글을 확인한다. 도구 없음: 사전 화면 lint | 주요 |
|
|
124
|
+
| PERM-04 | 앱·SDK 코드가 참조하는 보호 API 마다 목적 문자열을 감사한다 | 모든 Mach-O 의 프레임워크 참조마다 `NS*UsageDescription` 이 base 와 ko·en 에 있고, 기능과 구체 예를 한 문장으로 말하고, 주장이 SUB-05 데이터 흐름 목록과 맞고, 업로드 뒤 ITMS-90683 메일이 없다(5.1.1(ii)) | `release_checks.py` 목적 문자열 검사(`otool -L`, `nm -u`, `plutil -p`, InfoPlist.xcstrings). 지금 댕오늘은 '정확한 위치는 기기 안에서만' 문구가 WeatherKit 정밀 좌표 전송과 어긋나고 en InfoPlist 가 없다 | 차단 |
|
|
125
|
+
| PERM-05 | 제한 상태(스크린 타임)를 거부와 따로 다룬다 (사람) | 위치가 제한되면 이유를 설명하고, 사용자가 못 쓰는 '설정 열기' 버튼을 보이지 않는다 | 주인이 테스트 아이폰에 스크린 타임 위치 제한을 한 번 걸고, 에이전트가 XCUITest 단언을 돌린다 | 경미 |
|
|
126
|
+
|
|
127
|
+
### 7. 위치 (Location)
|
|
128
|
+
|
|
129
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
130
|
+
|---|---|---|---|---|
|
|
131
|
+
| LOC-01 | 고정 지점: 데이터 없는 곳과 한국 밖 (해당 앱만) | 서울 중심, 부산, 제주, 장소 0개인 시골, 바다 위, 도쿄가 각각 내용이나 명시적 빈 상태와 정상 거리 문자열을 보이고, 한국 전용 API 의 한국 밖 실패를 우아하게 처리한다 | 테스트 안에서는 `XCUIDevice.shared.location = XCUILocation(...)`, 기기 전체는 `device_qa.py test --variant seoul`(busan, jeju, 그 밖의 지점은 매니페스트에 더한다)이다. 계획의 Simulated Location 은 UI 테스트 대상 앱에 안 닿는다 | 차단 |
|
|
132
|
+
| LOC-02 | 멈춤이 섞인 이동 경로 (해당 앱만) | 2 km 도보(1.4 m/s)와 15 m/s 경로를 멈춤과 함께 돌 때 목록이 다시 정렬되고 '내 주변'이 갱신되고, 스피너 루프와 요청 폭주가 없다(NET-06 예산 안) | `devicectl device simulate location route` 는 실행당 속도 하나에 멈춤 옵션이 없으므로 여러 번 이어 돌리고 사이에 `simulate location coordinate` 로 머문다. 끝나면 `simulate location clear` | 주요 |
|
|
133
|
+
| LOC-03 | 대략적 위치(정확한 위치 끔)와 정확도 변화 (해당 앱만) | reducedAccuracy(약 5 km 양자화, 시간당 약 4회 갱신, 지역 감시 없음)에서 순위와 거리 라벨이 정직하고('약 3 km'), 지오펜스에 기대지 않고, 전체 정확도를 요청하면 `NSLocationTemporaryUsageDescriptionDictionary` 가 있다 | QA 구성 플래그로 reducedAccuracy 경로를 강제하고, 설정 자동화로 정확한 위치를 끈다(iOS 27.2 미확인) | 주요 |
|
|
134
|
+
|
|
135
|
+
### 8. 네트워크와 외부 서비스 (Network)
|
|
136
|
+
|
|
137
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
138
|
+
|---|---|---|---|---|
|
|
139
|
+
| NET-01 | 외부 의존 호스트마다 장애를 주입한다 | 호스트마다 타임아웃, 401, 403, 429, 500, 빈 본문, 깨진 JSON, 잘못된 타입, 필드 누락, 거대 응답, 낡은 타임스탬프, Low Data Mode 에서 10초 안에 오류나 캐시를 보이고, 재시도에 상한이 있고, 행 하나가 목록을 비우지 않고, 401·403 은 그 실행 동안 그 소스를 끈다 | QA 구성의 `URLProtocol` 스텁(`NET_SCENARIO=offline·slow·500·badjson·host=kakao`)과 CKError 주입이다. CloudKit(cloudd)과 아마 WeatherKit 은 앱 URLSession 을 안 타므로, 호출 지점 seam 이 필요한지 기기에서 한 번 확인한다 | 차단 |
|
|
140
|
+
| NET-02 | 앱 실행이 외부 서비스에 기대지 않는다 | 모든 호스트가 실패하거나 느려도 첫 화면이 캐시나 번들 데이터로 실행 예산(PERF-01) 안에 뜨고, 실행 경로에 동기 네트워크와 SDK 초기화가 없다 | 모든 호스트 실패 스텁으로 콜드 실행해 첫 화면 identifier 를 단언하고, App init 과 scene 설정을 grep 한다 | 차단 |
|
|
141
|
+
| NET-03 | 실행 때 오프라인, 요청 중 끊김, 회복 | 끊긴 요청은 한계를 말하는 메시지로 끝나고, 오프라인에서 쌓인 투표는 회복 뒤 정확히 한 번 가고(cktool 확인), 망 전환에 데이터 손실이 없다 | QA 구성에서는 단계 사이에 스텁 시나리오를 바꾸고, Release·TestFlight 에서는 Network Link Conditioner 100% Loss 로 끊은 뒤 서버 상태를 조회한다 | 차단 |
|
|
142
|
+
| NET-04 | Network Link Conditioner(100% Loss, Very Bad Network, 3G)로 실제 나쁜 망 | 프로필마다 Smoke 가 오류 UI 와 재시도로 통과하고, USB devicectl·XCTest 터널이 살아남는지 확인했거나 미실행으로 적었다 | 설정 > 개발자 > Network Link Conditioner 를 `com.apple.Preferences` 로 구동한다(iOS 27.2 요소 이름 미확인, devicectl 에는 망 명령이 없다). 흔들리면 '테스트 안 한 것'에 넣는다 | 주요 |
|
|
143
|
+
| NET-05 | IPv6 전용(NAT64/DNS64) 호환 | 바이너리와 리소스에 IPv4 리터럴이 없고, 모든 통신이 호스트 이름으로 가고, 망 코드가 바뀌면 Mac 의 'Create NAT64 Network' Wi-Fi 에서 ReviewerJourney 가 통과한다(심사망의 근사일 뿐) | `strings -a` 와 리소스 grep `([0-9]{1,3}[.]){3}[0-9]{1,3}`, NAT64 Wi-Fi 는 시스템 설정 > 일반 > 공유에서 주인이 한 번 만든다. 도구 없음: IPv4 리터럴 검사(`release_checks.py` 에 추가) | 차단 |
|
|
144
|
+
| NET-06 | 화면별 네트워크 트래픽 감사 | 모든 호스트가 허용 목록에 있고, http:// 와 키 같은 쿼리 파라미터가 없고, 화면별 요청 수가 예산 안이라 할당량을 태우는 중복 요청이 없다 | `xctrace record --template Network --device <UDID> --time-limit 60s --launch -- <bundleId>` → `xctrace export --har` 를 파싱한다(HAR 이 기기 URLSession 을 잡는지 한 번 확인, 실제 사례: ATS 가 막은 http 사진 URL). 도구 없음: HAR 감사기 | 주요 |
|
|
145
|
+
| NET-07 | 새 버전의 할당량과 API 키 적합성 (해당 앱만) | 세션당 호출 수 x 현재 DAU 가 WeatherKit 월 50만 회와 Google·Kakao 할당량 안이고, 제한 걸린 키마다 설치된 Release 빌드에서 한 번 성공한다 | UI 테스트의 `URLProtocol` 카운터로 실행당·위젯 시간당 호출 수를 잰다(CloudKit·WeatherKit 은 NET-01 seam 확인 뒤). 제공자 콘솔은 손으로 본다. 도구 없음: 호출 카운터와 추정기 | 주요 |
|
|
146
|
+
| NET-08 | 스텁 표에 없는 망 장애: 종속 포털, DNS 실패, 시계 오류 TLS 실패, 잘린 본문 | 200 HTML 본문(호텔·지하철 Wi-Fi 포털)이나 잘못된 content-type 이 데이터로 캐시되지 않고, DNS·TLS 실패와 느린 망의 잘린 본문이 오류 상태로 끝난다 | NET-01 스텁에 `captive`, `dns`, `tls`, `truncated` 시나리오를 더한다(근거는 실무 경험이고 1차 출처는 없다) | 주요 |
|
|
147
|
+
|
|
148
|
+
### 9. CloudKit 과 공유 데이터 (CloudKit)
|
|
149
|
+
|
|
150
|
+
CloudKit 을 쓰는 앱에만 해당한다. `cktool query-records`·`delete-records` 는 몇 시간짜리 user 토큰(또는 Pawday `tools/cloud_export.py` 의 server-to-server 키)이 필요하고, 키체인의 management 토큰은 스키마 명령만 된다.
|
|
151
|
+
|
|
152
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
153
|
+
|---|---|---|---|---|
|
|
154
|
+
| CK-01 | 업로드 전마다 스키마 일치 게이트 (해당 앱만) | 빌드가 읽고 쓰는 레코드 타입과 필드가 프로덕션에 있고, 조건 필드는 QUERYABLE, 정렬 키는 SORTABLE, 검색 필드는 SEARCHABLE 이고, 빈틈은 업로드를 막고, 주인의 Deploy 뒤 재추출로 확인한다 | `cktool export-schema --environment production`(와 development)을 diff 하고 `cktool validate-schema`, 소스에서 CKQuery 타입·NSPredicate 필드·NSSortDescriptor 키를 뽑는다. Daynest 는 `check_cloudkit_schema.py`. 도구 없음: 앱 공용 스키마 대조 | 차단 |
|
|
155
|
+
| CK-02 | 프로덕션 스키마 배포 전 옛 클라이언트 호환 검토 (해당 앱만) | 아직 깔린 출시 버전마다 쓰는 필드·인덱스 표가 있고, 대기 중 배포가 그중 무엇도 지우거나 바꾸지 않고(예: 대기 중인 PlaceVote.voter 인덱스 제거), 인덱스 제거는 버리는 컨테이너에서 먼저 해 봤다 | 태그마다 `git worktree add` 로 CloudKit 쿼리 코드를 훑어 배포할 development 스키마와 비교한다. 프로덕션 변경은 되돌릴 수 없다. 도구 없음: 옛 클라이언트 필드 표 생성기 | 차단 |
|
|
156
|
+
| CK-03 | 레코드 타입별 공개 DB 보안 역할과 적대적 쓰기 테스트 (해당 앱만) | 사용자 생성 타입은 READ _world, CREATE _icloud, WRITE _creator 이고 설정·플래그 타입은 READ _world 만이며, 계정 B 가 A 의 투표를 고치거나 지울 수 없고 UI 가 우아하게 알린다 | 내보낼 때마다 `prod.ckdb` 의 GRANT 줄을 파싱하고, 두 번째 테스트 Apple ID 로 적대 XCUITest 를 돌린다(로그인은 사람이 한 번) | 차단 |
|
|
157
|
+
| CK-04 | 계정과 CKError 상태 행렬 (해당 앱만) | 로그아웃이면 둘러보기는 되고 투표는 로그인을 설명하고, 계정 변경은 다시 확인하고, `.restricted`·`.temporarilyUnavailable` 은 캐시를 지우거나 작업을 쌓지 않고, networkUnavailable 은 백오프, requestRateLimited 는 retry-after, serverRecordChanged 는 업데이트 손실 없음, quotaExceeded·notAuthenticated 는 분명한 메시지다 | QA 구성 플래그로 CKError 를 주입하고, 실제 로그아웃은 릴리스마다 iCloud 계정 없는 기기나 시뮬레이터에서 한 번 본다 | 차단 |
|
|
158
|
+
| CK-05 | 공유 쓰기의 멱등성과 동시성 (해당 앱만) | 빠른 두 번 탭, 50번 탭, 타임아웃 뒤 재시도, 저장 중 종료, 두 기기 동시 투표 뒤에도 투표자·장소당 레코드가 정확히 하나고 사유 텍스트가 정확하다 | 투표에 결정적 레코드 이름을 쓰고 `cktool query-records --database-type public --record-type PlaceVote` 로 센다. 두 기기 실행은 다른 계정의 두 번째 기기가 필요하다 | 차단 |
|
|
159
|
+
| CK-06 | Release·TestFlight 빌드로 프로덕션 왕복 (해당 앱만) | 프로덕션과 말하는 빌드에서 👍 만들기, 사유와 함께 👎 로 바꾸기, 지우기, 다시 만들기가 목록 배지·상세·위젯·공유 카드에 맞게 보인다 | 테스트 전용 Apple ID 로 TestFlight 설치본에 블랙박스 묶음(BUILD-08)을 외부 조작만으로 돌리고 CK-08 로 치운다 | 차단 |
|
|
160
|
+
| CK-07 | 공개 DB 노출 검토 (해당 앱만) | 공개 DB 의 어떤 필드도 처리방침과 라벨이 말하는 것 이상으로 사용자 식별자(예: 설치별 고정 투표자 UUID)나 개인 데이터를 드러내지 않는다 | 프로덕션 스키마 필드를 데이터 흐름 목록(SUB-05)과 비교한다(실제 사례: 댕오늘 투표자 UUID 가 '사용자 id 없음' 방침과 달리 모든 클라이언트로 감, 2026-10-27). 도구 없음: 스키마 대 목록 diff | 차단 |
|
|
161
|
+
| CK-08 | 프로덕션 테스트 데이터 정리 (해당 앱만) | TestFlight·프로덕션 실행의 쓰기는 creatorUserRecordID 로 가릴 수 있는 테스트 Apple ID 로 하고, 실행 뒤 지워 레코드 수가 0 이 되고, 실제 집계는 그대로다 | `cktool delete-records ... --environment production --database-type public --dry-run false --yes`(기본이 dry-run 이고 확인을 묻는다) 뒤 `query-records` 로 0 을 확인한다 | 주요 |
|
|
162
|
+
| CK-09 | 크라우드 신호(투표)의 남용 내성 (해당 앱만) | 재설치 후 재투표, 두 번째 계정의 대량 생성, 한 투표자의 100번 뒤집기 차터를 돌렸고, 집계가 creatorUserRecordID 로 중복을 거르는지와 정리 경로를 정했다 | 지금 투표 키는 UserDefaults 의 설치별 UUID 라 재설치가 새 투표자가 되고, _icloud 는 누구나 CREATE 할 수 있다(CloudVotes.swift). 도구 없음: 남용 차터 스크립트 | 차단 |
|
|
163
|
+
| CK-10 | 실제 규모의 CloudKit (해당 앱만) | development 컨테이너에 장소 1만 곳과 한 장소 투표 1,000건 이상을 넣어도 커서 페이지네이션 뒤 수가 정확하고, `CKError.limitExceeded`(작업당 400 레코드, 2 MB)는 반으로 나눠 재시도하고, 실행 예산과 메모리를 지킨다 | `CKQueryOperation` 기본 한도는 동적이라 첫 페이지에서 조용히 멈출 수 있다. 도구 없음: 대량 시드 스크립트 | 주요 |
|
|
164
|
+
|
|
165
|
+
### 10. 원격 피드·플래그·백엔드 (Feeds)
|
|
166
|
+
|
|
167
|
+
원격 피드, 플래그, 자체 백엔드를 쓰는 앱에만 해당한다.
|
|
168
|
+
|
|
169
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
170
|
+
|---|---|---|---|---|
|
|
171
|
+
| FEED-01 | 깔린 모든 앱 버전이 곧 낼 페이로드를 디코딩한다 (해당 앱만) | 골든 JSON 픽스처가 레포에 있고, 아직 쓰이는 태그마다 그 태그의 디코더 테스트가 후보 페이로드로 통과하고, 변경은 추가뿐이고, 모르는 enum 은 대체값, 배열은 요소별로 디코딩한다 | 태그마다 `git worktree add` 해 코어 패키지 `swift test` 나 xcodebuild 테스트 타깃을 후보로 돌린다. 실패는 앱 출시가 아니라 발행을 막는다. 도구 없음: 태그별 디코더 실행기 | 차단 |
|
|
172
|
+
| FEED-02 | 데이터 발행(예: `daily.py` 의 `today/<sido>.json`)을 출시처럼 게이트와 롤백으로 다룬다 (해당 앱만) | pages 단계 전에 스키마, 필수 필드, 시도별 행 수 변화(30% 넘게 줄면 막음), 한국 안 좌표, URL, generatedAt 을 확인하고, 롤백은 `git revert` 이고, 앱은 디코딩되고 캐시보다 새 피드만 받는다 | 실패하면 1 로 끝나 push 를 막는 단계를 넣고, 깨진 피드를 주는 UI 테스트로 캐시 카드가 남는지 본다. 도구 없음: 발행 게이트 단계 | 차단 |
|
|
173
|
+
| FEED-03 | 위험 기능의 킬 스위치를 두 상태로 테스트한다 (해당 앱만) | 공유 데이터, 새 외부 연동, 마이그레이션은 컴파일된 안전 기본값과 캐시된 마지막 값을 가진 원격 플래그 뒤에 있고, ON·OFF(플래그 쌍별)가 통과하고, 플래그 파일이 없거나 404 거나 깨지면 기본값으로 돈다 | Pages 피드 옆 `flags.json` 이나 CloudKit AppFeatures 레코드(READ _world), QA 구성에서 `-flag.newVoting 0` 과 `1` 로 강제한다 | 주요 |
|
|
174
|
+
| FEED-04 | 최소 지원 버전 스위치를 필요하기 전에 준비한다 (해당 앱만) | 원격 minimum_version 을 숫자로 비교하고(1.9 < 1.10 = 1.10.0 < 2.0 유닛 테스트), 강제되면 닫을 수 없는 업데이트 화면과 App Store 링크가 뜨고, 주인 OK 없이 올리지 않는다 | 킬 스위치와 같은 원격 설정에서 읽어 `CFBundleShortVersionString` 을 성분별로 비교하고, 차단 화면을 UI 테스트한다 | 주요 |
|
|
175
|
+
| FEED-05 | 모든 프로덕션 의존을 매시간, 제출과 출시 직전에 블랙박스로 찔러 본다 (해당 앱만) | 피드(앱의 실제 모델로 디코딩), AASA, 개인정보·지원 URL, 프록시, 데이터 제공자 상태가 지난 1시간 안에 초록이고, 키와 할당량이 제출 뒤 48시간 이상 버틴다(심사 90% 가 48시간 안) | curl(상태, content-type, 크기, 지연)과 코어 패키지로 만든 디코드 CLI 를 launchd 로 돌리고 실패만 iMessage 로 알린다. 도구 없음: 합성 프로브(`core/server_services.py` 등록) | 차단 |
|
|
176
|
+
| FEED-06 | 사용자에게 보이는 생성 문장은 저장된 사실로만 쓴다 (해당 앱만) | 장소 요약과 지역 소개의 사실 문장마다 저장 필드로 거슬러 가고, 근거 없는 '반려견 동반 가능', 크기 정책, 건강 주장, 업체에 대한 부정 문장은 발행(FEED-02)을 막는다 | Pawday `summaries`·`guides` 단계(codex exec) 산출을 필드와 대조한다. 도구 없음: 문장별 근거 검사기 | 주요 |
|
|
177
|
+
| FEED-07 | 운영 데이터 경로가 자기 시계 경계를 넘는다 (해당 앱만) | 가짜 데이터 실행 플래그 없이 실제 데이터 경로를 호스트 테스트로 돌리고, 시계를 주입해 데이터가 기대는 경계를 모두 넘긴다: 자정 직전과 직후, 아침 데이터 잡 전과 후, 캐시 만료. 어느 순간에도 메인 화면에 핵심 데이터가 있고(빈 구간 없음), 오늘 데이터가 오기 전까지 어제 데이터를 지킨다. 예: 댕오늘 2026-10-29, 자정 직후 날씨가 최대 22분 비었다. 어제 카드를 버리고 15분 동안 다시 묻지 않았기 때문이고, UI·기기 실행은 모두 낮에 가짜 날씨로 돌았다 | 데이터 층의 시계 이음새(댕오늘 `WeatherRefresh` 호스트 테스트)와 릴리스마다 가짜 데이터 플래그 없는 Smoke 한 번. L10N-04 는 같은 시점의 표시 형식을, 이 행은 데이터가 있는지를 본다 | 차단 |
|
|
178
|
+
|
|
179
|
+
### 11. 진입점: 딥 링크·유니버설 링크·위젯·인텐트 (Entry points)
|
|
180
|
+
|
|
181
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
182
|
+
|---|---|---|---|---|
|
|
183
|
+
| ENTRY-01 | 라우터 경로마다 딥 링크·유니버설 링크 행렬 (해당 앱만) | 경로 x {콜드, 웜, 백그라운드, 온보딩 중} x {유효 id, 모르거나 지운 id, 깨짐, 누락, 중복 파라미터, 퍼센트 인코딩 한글, 2,000자, 대문자 호스트, 끝 슬래시}가 목표 화면이나 우아한 대체로 가고, 예전 버전이 대화방에 남긴 옛 URL 모음도 통과한다 | 콜드는 `devicectl device process launch --payload-url <url> <bundleId>`, 웜은 `devicectl device process openURL`, 테스트 안은 `XCUIDevice.shared.system.open(url)` 이다. 표는 라우터 코드와 옛 URL 모음에서 만든다 | 차단 |
|
|
184
|
+
| ENTRY-02 | AASA 파일이 Apple CDN 에서 보기에 유효하다 (해당 앱만) | `/.well-known/apple-app-site-association` 이 리다이렉트 없이 200 과 유효 JSON(앱 ID, 경로)을 주고, `swcutil verify` 가 통과하고, AASA 변경은 출시보다 충분히 먼저 낸다(CDN 은 24시간 안에 받지만 기존 설치는 약 주 1회 다시 보고, CDN 무효화는 없다) | `release_checks.py` AASA 검사(Apple CDN 경유), `sudo swcutil verify -d <domain> -j aasa.json -u <url>`. Associated Domains 개발자 모드는 증거가 아니다 | 차단 |
|
|
185
|
+
| ENTRY-03 | 위젯 탭, App Intents, 빠른 동작이 모든 앱 상태에서 맞게 간다 (해당 앱만) | 위젯 kind·family 마다 탭이 콜드와 웜에서 widgetURL 목적지를 열고, 인텐트와 단축어가 맞는 화면과 데이터를 연다 | 주인이 위젯을 한 번 놓은 뒤 `XCUIApplication(bundleIdentifier: "com.apple.springboard")` 로 누르고 목적지 identifier 를 단언한다 | 주요 |
|
|
186
|
+
| ENTRY-04 | 밖으로 나가는 앱 링크와 실제 공유 경로 (해당 앱만) | 'Kakao 맵·네이버 지도·Instagram 에서 열기'가 그 앱이 있을 때와 없을 때 모두 웹, Apple 지도, App Store 로 가고 죽은 버튼이 없고(4.2.3(i)), 카카오톡·메시지·메모에서 누른 공유 링크가 동작하고, 앱 없는 수신자는 실제 내용이 있는 웹 페이지(200)를 받는다 | `LSApplicationQueriesSchemes` 의 스킴마다 대상 앱을 지운 상태로 UI 테스트한다. 대화방 공유 링크는 현장 항목(FIELD-03)으로 돈다 | 주요 |
|
|
187
|
+
| ENTRY-05 | App Intents, 엔티티 쿼리, Spotlight 쿼리를 위젯을 손으로 놓지 않고 헤드리스로 돌린다 (해당 앱만) | 앱의 인텐트와 쿼리마다 AppIntentsTesting 테스트가 있고 통과한다 | Xcode 27.1 의 AppIntentsTesting 프레임워크를 쓴다(API 안정성과 문서는 미확인) | 경미 |
|
|
188
|
+
|
|
189
|
+
### 12. 알림 (Notifications)
|
|
190
|
+
|
|
191
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
192
|
+
|---|---|---|---|---|
|
|
193
|
+
| NOTIF-01 | 포그라운드·백그라운드·종료 상태의 로컬 알림 경로 (해당 앱만) | 포그라운드는 willPresent 배너를 보이고, 백그라운드와 종료 상태에서는 배너 탭이 홈이 아닌 정확한 화면을 열고, 알림에서 한 동작이 앱에 보인다 | QA 구성 launch argument 로 `UNTimeIntervalNotificationTrigger(timeInterval: 5)` 를 걸고 `app.terminate()` 나 `press(.home)` 뒤 springboard 에서 배너를 누른다(iOS 27.2 배너 identifier 미확인) | 주요 |
|
|
194
|
+
| NOTIF-02 | APNs 샌드박스 원격 푸시와 TestFlight 빌드의 프로덕션 APNs (해당 앱만) | 샌드박스 푸시가 HTTP 200 이고 페이로드가 디코딩되고 탭이 맞게 가며, TestFlight 설치본 토큰으로 보낸 프로덕션 푸시가 도착한다 | debugging 설치본의 토큰은 `devicectl device copy from --domain-type appDataContainer` 로, TestFlight 토큰은 앱이 서버 등록이나 QA 화면으로 내놓아야 한다. `curl --http2 -H 'apns-push-type: alert' https://api.sandbox.push.apple.com/3/device/<token>` 에 주인의 APNs .p8 이 필요하다 | 주요 |
|
|
195
|
+
| NOTIF-03 | 로컬 알림 64개 상한과 여행 (해당 앱만) | 긴 세션 뒤에도 대기 알림이 64개 아래고, 시간대가 바뀌면 다시 예약된다 | `getPendingNotificationRequests` 수를 단언하고, `Calendar.current` 로 시각을 만드는 스케줄러(예: MorningBriefScheduler)는 시간대 변경 뒤 다시 확인한다 | 경미 |
|
|
196
|
+
|
|
197
|
+
### 13. 위젯과 백그라운드 작업 (Widgets & background)
|
|
198
|
+
|
|
199
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
200
|
+
|---|---|---|---|---|
|
|
201
|
+
| BG-01 | 위젯 TimelineProvider 를 시간이 움직이는 코드로 유닛 테스트한다 (해당 앱만) | 주입한 시계로 자정과 DST 를 넘길 때 항목 간격이 5분 이상이고(시계 위젯만 배터리·예산 확인 뒤 더 짧게), reload 정책이 의도와 맞고, placeholder 와 snapshot 이 비지 않고, 움직이는 시각은 스스로 갱신되는 형식을 쓴다 | 가짜 시계로 `getTimeline` 을 부른다. 움직이는 시각은 `.relative`·`.offset`·`.timer`·`Text(timerInterval:)` 나 iOS 18+ `Text(.currentDate, format:)` 로 쓴다(`.time`·`.date` 는 멈춘다, Elsewhen 1.2 위젯 30분 정지) | 주요 |
|
|
202
|
+
| BG-02 | 모든 위젯 family 를 크기와 외형별로 그려 본다 (해당 앱만) | family x 라이트·다크 x 기본·AX 글자 x ko·en 에 잘림과 겹침이 없고, 날씨 데이터에 표시(SUB-13)가 있다 | family·렌더링 모드별 위젯 preview 와 기기 홈 화면 스크린샷을 기준선으로 쓴다. ImageRenderer 는 Core Animation 레이어와 WidgetKit 환경을 못 그린다. 디버깅은 `_XCWidgetFamily`·`_XCWidgetKind` | 주요 |
|
|
203
|
+
| BG-03 | 위젯 데이터가 앱과 같고 앱이 reload 를 부른다 (해당 앱만) | 앱 안 쓰기 뒤 `WidgetCenter.reloadTimelines(ofKind:)` 가 불리고, App Group 데이터가 같은 순간 앱 데이터와 같고, reload 창 안의 홈 화면 스크린샷이 새 데이터를 보인다 | debugging 설치본에서 `devicectl device copy from --domain-type appGroupDataContainer` 로 비교하고, 주인이 위젯을 놓은 뒤 전후 `devicectl device capture screenshot` 을 남긴다 | 주요 |
|
|
204
|
+
| BG-04 | 백그라운드 작업을 강제로 띄우고 만료시키고, 디버거 밖에서 백그라운드 URLSession 을 본다 (해당 앱만) | BGTask id 마다 실행과 만료 경로가 끝나고(setTaskCompleted, 반쪽 캐시 없음, os_log 확인), 홈 화면 실행에서 백그라운드 URLSession 이 끝나고, `_simulate` 호출이 출시 바이너리에 없다 | 기기 LLDB 에서 `e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"ID"]` 와 `_simulateExpirationForTaskWithIdentifier:`, 그다음 `strings -a` | 주요 |
|
|
205
|
+
| BG-05 | Data Protection 이 켜진 잠금 상태 동작 (해당 앱만) (사람) | 위젯, 알림 확장, 백그라운드 작업이 잠금 중에 크래시하거나 빈 데이터를 쓰지 않고 우아하게 물러난다 | 테스트 아이폰은 암호가 없어 Data Protection 이 꺼져 있다. 보호 파일이나 WhenUnlocked 키체인을 읽는 코드가 바뀌면 암호를 켠 기기에서 한 번 돈다 | 경미 |
|
|
206
|
+
| BG-06 | 라이트·다크 너머의 위젯과 아이콘 외형 (해당 앱만) | family 마다 기본·다크·tinted·clear 홈 화면, 잠금 화면(vibrant), StandBy 기기 스크린샷에서 사진 카드가 흰 덩어리가 되지 않고, 앱 아이콘이 기본·다크·clear·tinted 에서 멀쩡하다 | `devicectl device settings appearance --look-and-feel tinted`(또는 `clear`)와 `--liquid-glass-opacity 0.0`~`1.0`, `devicectl device settings standby --show --duration 30`, 그다음 `devicectl device capture screenshot` | 주요 |
|
|
207
|
+
|
|
208
|
+
### 14. 텍스트 입력과 데이터 공격 (Input & attacks)
|
|
209
|
+
|
|
210
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
211
|
+
|---|---|---|---|---|
|
|
212
|
+
| INPUT-01 | 실제 한글 2벌식 키보드로 한 자모씩 입력한다 | '강아지 카페'를 자모 단위로 칠 때 음절마다 값이 맞고, 한도-1·한도·한도+1 음절과 한도에서 음절 중간에 멈출 때 자모가 갈라지거나 겹치지 않고, 반쯤 만든 음절에 검색이 파괴적으로 굴지 않고, 한도는 한 단위(Character 나 utf16)로 센다 | 주인이 키보드를 한 번 더하고, 지구본 키로 바꿔 `app.keys["ㄱ"]` 를 누른다. `typeText` 는 IME 를 안 탄다고 가정하고, 조합 중 `markedTextRange` 가 nil 이라 onChange 자르기가 입력을 깬다 | 주요 |
|
|
213
|
+
| INPUT-02 | 모든 입력 필드와 서버 문자열에 데이터 타입 공격 표를 넣고 정확한 왕복을 본다 | 공백, 앞 공백, 줄바꿈, 255~10,000자, 악센트, 아시아 문자, ZWJ 이모지, 국기, 아랍어 RTL, 구분 문자, SQL 같은 값이 저장되고 `app.terminate()` 재실행 뒤 그대로 읽히고, 크래시·조용한 잘림·textClipped 감사가 없고, 같은 표를 스텁으로 서버 필드에 넣어 카드마다 스크린샷을 남긴다 | UI 테스트 타깃의 공유 공격 픽스처를 매개변수화하고, 한도가 있는 곳은 `String.count` 와 `utf16.count` 를 비교한다 | 주요 |
|
|
214
|
+
| INPUT-03 | 붙여넣기 경로와 키보드 사용감 | 한글 1,000자 붙여넣기가 한도에 맞게 저장되고, 키보드 종류와 return 동작이 필드에 맞고, 포커스 필드가 키보드에 가리지 않는다 | `devicectl device pasteboard copy` 뒤 길게 눌러 붙여넣고, 키보드가 뜬 상태를 `QA.snap` 한다 | 경미 |
|
|
215
|
+
| INPUT-04 | 모바일 보안 기준선(OWASP MASVS v2.1.0) | 릴리스마다 STORAGE(os_log `.public`, 앱 전환 스냅숏, 대지의 위치·개인정보), NETWORK, PLATFORM(URL 스킴 입력, WebView), CODE(의존성 버전), PRIVACY 항목을 확인했고, 세션 뒤 sysdiagnose 에 좌표와 id 가 없다 | `devicectl device sysdiagnose` 결과를 좌표·식별자 패턴으로 훑는다. 도구 없음: MASVS 표와 sysdiagnose 검사 | 주요 |
|
|
216
|
+
|
|
217
|
+
### 15. 현지화·형식·시간 (Localization & time)
|
|
218
|
+
|
|
219
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
220
|
+
|---|---|---|---|---|
|
|
221
|
+
| L10N-01 | 지원 언어와 까다로운 지역별로 전체 화면을 훑는다 | ko-KR, en-US(가능하면 de-DE 소수점 쉼표, ar-SA RTL, 두 배 길이 의사 언어)에서 한국어 화면의 영어, 날 키, 잘림, 틀린 복수형과 숫자 형식, 잘못된 미러링이 없고, 테스트는 identifier 로만 찾는다 | `device_qa.py test --variant english`(`QA_LANG`)나 `xcodebuild test -testLanguage ko -testRegion KR`, 의사 언어 `-NSDoubleLocalizedStrings YES`, RTL `-NSForceRightToLeftWritingDirection YES`(의사 언어가 기기에서 되는지 확인) | 주요 |
|
|
222
|
+
| L10N-02 | 로케일, 시계 설정, 달력별 형식기와 파서 유닛 테스트 | 고정 형식 파싱은 모두 `en_US_POSIX` 와 명시 TimeZone 이나 `ISO8601DateFormatter` 를 쓰고, 표시 형식기는 반대로 덮은 12·24시간, 불교·일본 달력, 비라틴 숫자, de_DE 에서 통과한다 | Locale·Calendar·TimeZone 매개변수 테스트와 POSIX 없는 파싱 경로 검사다(사용자가 12·24시간을 덮으면 DateFormatter 가 'HH:mm' 를 바꿔 쓴다) | 차단 |
|
|
223
|
+
| L10N-03 | 기기 24시간 덮기와 지역 변경 | 24시간 표시를 지역 기본과 반대로 두어도 모든 시각 라벨과 시각 입력이 맞다(Elsewhen 은 출시 차단) | 설정 > 일반 > 날짜 및 시간을 `com.apple.Preferences` 로 구동한다(iOS 27.2 미확인). 흔들리면 두 줄짜리 수동 항목으로 '테스트 안 한 것'에 넣는다 | 주요 |
|
|
224
|
+
| L10N-04 | DST, 자정, 달력 경계에서 시계를 주입해 시간 여행 테스트를 한다 | 시간대 {Seoul, New_York, London, Sydney, Santiago, Lord_Howe, Kiritimati(+14), Etc/GMT+12, Kathmandu(+5:45)} x 시점 {전환 ±1초·1분, 없는 시각 2026-03-08 02:30 NY, 겹치는 시각 2026-11-01 01:30 NY, 23:59:59, 00:00:00, 2월 29일, 월말·연말, Santiago 2026-09-06 startOfDay 01:00, 날짜변경선}에서 '오늘', 라벨, 위젯 항목이 맞다 | Clock·TimeZone 제공자를 주입하고 `TimeZone.nextDaylightSavingTimeTransition(after:)` 로 경계를 만든다 | 차단 |
|
|
225
|
+
| L10N-05 | 앱을 연 채 '오늘'이 넘어가고, 시간대 변경이 UI 를 바로 바꾼다 | 날짜가 바뀌면 '오늘' 내용이 새로 고쳐지고, 밤샘 실행(충전 중 포그라운드, 자동 잠금 안 함) 뒤 아침에 새 날이 보이고 메모리가 평평하며, 포그라운드 시간대 변경이 재실행 없이 반영된다 | `NSCalendarDayChanged` 와 `NSSystemTimeZoneDidChange`(또는 `TimeZone.autoupdatingCurrent`)를 관찰한다. `significantTimeChangeNotification` 은 자정·통신사 시각·DST 용이다. 시간대 변경은 설정 자동화로 한다(devicectl 에 날짜 명령 없음) | 주요 |
|
|
226
|
+
| L10N-06 | 장소의 시각을 기기 시간대가 아니라 장소의 시간대로 계산한다 (해당 앱만) | 추천과 영업 시간 코드에 `Calendar.current`·`TimeZone.current` 가 없고 Asia/Seoul 달력을 명시하며, ReviewerAbroad 가 KST 진리표로 '영업 중'과 주말을 단언한다 | 미국 시간대 UI 테스트로 확인한다(지금 댕오늘 `TodayEngine.isOpen` 과 `isDateInWeekend` 가 `Calendar.current` 를 쓴다). 도구 없음: `Calendar.current` 검사 게이트 | 차단 |
|
|
227
|
+
| L10N-07 | 틀린 기기 시계(자동 설정 끔, 몇 시간이나 며칠 어긋남) | 신선도 라벨, 캐시 나이, '오늘', '영업 중'이 무너지지 않고, WeatherKit JWT·TLS·CloudKit 실패가 오류 상태로 끝난다 | 설정 자동화로 시계를 돌린다(iOS 27.2 미확인). 흔들리면 '테스트 안 한 것'에 넣는다(근거는 실무 경험) | 주요 |
|
|
228
|
+
|
|
229
|
+
### 16. 접근성 (Accessibility)
|
|
230
|
+
|
|
231
|
+
원칙은 [접근성·현지화·성능 프로토콜](../development/accessibility-localization-performance-protocol.md)에 있고, 여기서는 기기에서 무엇을 재는지만 정한다.
|
|
232
|
+
|
|
233
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
234
|
+
|---|---|---|---|---|
|
|
235
|
+
| A11Y-01 | 모든 화면의 모든 상태에 자동 접근성 감사를 돌린다 | 화면 x 상태(빈, 로딩, 로드, 오류, 오프라인, 권한 거부, 시트, 경고)마다 `performAccessibilityAudit` 의 설명 안 된 이슈가 0 이고, 예외는 검토된 파일 하나에서 auditType 과 요소로만 거른다 | 페이지 객체에서 `QA.audit(app, step, in: self)`, 상태는 QA 구성의 `-uiState` empty·error·offline 이다. 감사 종류는 contrast, elementDetection, hitRegion, sufficientElementDescription, dynamicType, textClipped, trait | 주요 |
|
|
236
|
+
| A11Y-02 | 가장 작은 글자부터 가장 큰 접근성 크기까지 Dynamic Type, 그리고 Bold Text | xs, l, xxxl, AX1, AX5 에서 잘림, 겹침, 화면 밖 컨트롤이 없고, 필요한 곳은 세로로 쌓이고, Bold Text 스냅숏이 통과하고, 원래 설정이 복원된다 | `device_qa.py test --variant small-text --variant large-text`(xxxl·AX1 변형은 매니페스트에 더한다, AX 크기는 `--larger-accessibility-sizes on` 이 필요한데 `device_qa.py` 는 아직 이 플래그를 안 준다)에 `.dynamicType`·`.textClipped` 감사, Bold Text 는 `.environment(\.legibilityWeight, .bold)`(실제 사례: `.lineLimit(1)` 캡션을 ViewThatFits 로 고침, 2026-10-26) | 주요 |
|
|
237
|
+
| A11Y-03 | VoiceOver 자동 훑기와 발화 확인 | ko·en 주요 화면의 발화 기록에 빈 문자열, 역할어만('button'), 이미지 파일 이름, 중복이 없고 순서가 말이 되며, 주요 과업 완수는 사람이 들어 본다 | `device_qa.py test --variant voiceover` 와 `XCUIDevice.shared.voiceOverService` 의 `enable`·`moveForward`·`moveIn`·`moveOut`·`currentSpeech` 를 `QA.note` 로 붙인다. 활성화·두 번 탭 API 가 없어 과업 완수는 자동화하지 못한다 | 주요 |
|
|
238
|
+
| A11Y-04 | 시각 접근성 설정 행렬 | 다크·라이트, 대비 증가, 동작 줄이기, 투명도 줄이기, 색 필터(흑백, 적색맹, 녹색맹), 테두리 표시, look and feel, Liquid Glass 불투명도, 색 없이 구별에서 대비가 17pt 이하 4.5:1·18pt 이상 3:1, 히트 영역이 44x44pt(28 미만 금지)이고, 상태를 색으로만 보이지 않는다 | `devicectl device settings appearance --mode dark --increase-contrast on --reduce-motion on --reduce-transparency on --color-filter on --color-filter-type protanopia --show-borders on --look-and-feel tinted --liquid-glass-opacity 1.0` 뒤 `.contrast`·`.hitRegion` 감사, 색 없이 구별은 설정 자동화다(Show Borders 가 Button Shapes 를 대체하는지 미확인). 도구 없음: `device_qa.py` 변형 키에 이 플래그들 추가 | 주요 |
|
|
239
|
+
| A11Y-05 | Accessibility Nutrition Label 주장은 통과한 테스트로 뒷받침한다 | VoiceOver, Larger Text 등은 A11Y-01~04 와 A11Y-06 에서 흔한 과업이 모두 통과할 때만 ASC 에 주장한다 | 주장 목록을 테스트 결과 옆에 두고, 앱별 선택은 주인이 한 번 정한다(지금은 자율, 나중에 의무, 날짜 미발표) | 경미 |
|
|
240
|
+
| A11Y-06 | VoiceOver 밖의 보조 입력: Voice Control, Full Keyboard Access, Switch Control, Assistive Access, Smart Invert | 탭할 수 있는 요소가 모두 Voice Control 'Show names' 에 보이고 라벨이 보이는 글자와 맞고, 스와이프·길게 누르기에 음성 경로가 있고, 아이콘 버튼(👍/👎)에 `accessibilityInputLabels` 가 있고, 키보드 포커스 순서가 맞고, Smart Invert 에서 사진이 뒤집히지 않는다 | 포커스는 `XCUIElement.typeKey` 로 몰고, 사진에는 `accessibilityIgnoresInvertColors` 를 단다. Switch Control 과 Assistive Access 는 사람이 본다. 도구 없음: 아이콘 버튼 inputLabels lint | 주요 |
|
|
241
|
+
|
|
242
|
+
### 17. 화면 검토와 기기·OS 범위 (Visual & coverage)
|
|
243
|
+
|
|
244
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
245
|
+
|---|---|---|---|---|
|
|
246
|
+
| UI-01 | 모든 흐름의 모든 단계를 스크린샷으로 남기고, 통과한 실행도 보관해 한 장씩 본다 | 단계마다 이름 붙은 첨부(예: `03-detail-ko-dark-AX5`)가 있고, 잘림, 겹침, 낮은 대비, 플레이스홀더, 틀린 로케일, safe area 침범, 키보드에 가린 필드, 빈 상태를 한 장씩 봤고, 출시 빌드는 접촉 시트가 주인에게 간다 | `QA.snap`(`.keepAlways`), 계획의 Automatic Screen Capture 'On, and keep all', `device_qa.py shots <xcresult> --out <dir>` 가 `index.md` 접촉 시트를 만든다 | 주요 |
|
|
247
|
+
| UI-02 | 지난 출시와 시각 회귀를 비교한다(History 오라클) | 화면 x 구성마다 지난 출시 기준선과의 지각 차이가 커밋으로 설명되거나 버그로 등록되고, 의도한 변경은 기준선을 다시 만들고 전후 쌍을 주인에게 보인다 | `devicectl device simulate statusBar preset screenshot`(9:41)으로 고정하고 끝나면 지운다. 기준선은 `apps/<app>/qa/baselines/<config>/`, swift-snapshot-testing 추가는 주인의 의존성 결정이다. 도구 없음: 기준선 지각 diff | 주요 |
|
|
248
|
+
| UI-03 | 기본 구성 하나 대신 쌍별 구성 행렬을 돈다 | PICT 행마다 Smoke 와 스크린샷이 돌고 위험 영역은 3원으로 돈다: 외형 {light, dark}, 글자 {large, AX5}, 대비 {on, off}, 언어 {ko, en}, 지역 {KR, US}, 위치 {allow, deny}, 망 {ok, offline 스텁} | `pict model.txt` 로 행을 만들고 행마다 `device_qa.py test --variant ...` 를 USB 아이폰에서 밤새 돌린다. 도구 없음: pict 설치와 행→변형 변환 | 주요 |
|
|
249
|
+
| UI-04 | 아이폰 앱이 내려받히는 다른 곳: iPad 호환 모드, Apple 실리콘 Mac, Vision Pro | iPad 시뮬레이터에서 ReviewerJourney 가 잘림 없이 통과하고(iPad 는 끌 수 없다), Mac 과 Vision Pro 는 통과하거나 ASC 에서 끈 상태를 기록했다 | `-destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M5)'`, 시뮬레이터가 멈추면 한 번 다시 하고 그래도 안 되면 미실행으로 적는다. `UIRequiredDeviceCapabilities` 는 배포 선호용이 아니다 | 주요 |
|
|
250
|
+
| UI-05 | 가장 작은·큰 아이폰과 iPhone Duo | iOS 27.0 런타임의 SE 급과 Pro Max, iOS 27.1 런타임의 iPhone Duo 바깥·안쪽 화면에서 잘림과 못 닿는 컨트롤이 없다 | 같은 투어를 시뮬레이터마다 돌리고, 멈춤은 통과가 아니라 미실행으로 적는다(Duo 스크린샷은 iOS 27.1 SDK 빌드에 2027년 4월부터 필수) | 경미 |
|
|
251
|
+
| UI-06 | 가장 오래된 지원 iOS 를 실제로 테스트한다 | 배포 타깃과 같은 런타임(댕오늘·Daynest 는 iOS 18)에서 전체 묶음이 통과했거나 타깃을 테스트한 버전으로 올렸고, 가용성 경고는 오류이며, 타깃이 테스트한 런타임보다 낮으면 게이트가 실패한다 | `simctl runtime list`, `xcodebuild -downloadPlatform iOS -buildVersion 18.x`(Xcode 27.1 지원 미확인, 지금은 27.0·27.1 만 설치). 타깃 올리기는 주인 결정이다 | 주요 |
|
|
252
|
+
| UI-07 | 온디바이스 Foundation Models: 테스트 아이폰은 불가 경로, 가능 경로는 다른 곳에서 (해당 앱만) | `SystemLanguageModel` 이 `.unavailable`(deviceNotEligible, appleIntelligenceNotEnabled, modelNotReady)이면 결정론 출력으로 물러나 지어낸 장소 이름이 없고, `PrivateCloudComputeLanguageModel` 의 불가 사유(deviceNotEligible, systemNotReady)도 덮고, 가능 경로는 그 기능을 건드린 릴리스마다 돌렸다 | iPhone 14 Pro 가 deviceNotEligible 이라 대체 경로는 자동으로 돈다. 스킴 옵션 'Simulated Foundation Models Availability' 로 사유별 강제(`xcodebuild test` 적용 미확인), 가능 경로는 macOS Tahoe 이상 Mac 의 시뮬레이터나 주인의 폰, 골든 셋 출력은 Evaluations 프레임워크로 회귀 비교를 시도한다 | 주요 |
|
|
253
|
+
| UI-08 | 저사양 기기와 320pt 레이아웃 | 테스트 아이폰의 디스플레이 확대(Display Zoom)에서 잘림이 없고, `os_proc_available_memory` 여유를 지키며 메모리 소크가 통과하고, iOS 18 이 도는 A12·A13 급(XS, XR, SE 2세대)은 중고 두 번째 기기로 돌리거나 '테스트 안 한 것'에 적었다 | 릴리스마다 Display Zoom 을 한 번 켜고 스크린샷 투어를 돈다 | 주요 |
|
|
254
|
+
| UI-09 | 크기가 바뀌는 레이아웃과 자세 | iPhone Duo 바깥·안쪽 화면과 작업 중 접기·펴기에서 상태가 남고, iPadOS 26 창 크기 조절에서 깨지지 않는다(`UIRequiresFullScreen` 은 26.0 부터 deprecated) | `devicectl device appResize start` 와 `set --preferred-size WxH` 로 크기를 훑는다(지원 목적지 미확인). Duo 자세는 Device Hub 에서만 바꾼다 | 주요 |
|
|
255
|
+
|
|
256
|
+
### 18. 성능과 자원 조건 (Performance)
|
|
257
|
+
|
|
258
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
259
|
+
|---|---|---|---|---|
|
|
260
|
+
| PERF-01 | 지난 출시 기준선 대비 콜드 실행 시간 | `XCTApplicationLaunchMetric(waitUntilResponsive: true)` 중앙값이 저장된 지난 CSV 보다 15% 넘게 느려지지 않고, 첫 프레임이 iPhone 14 Pro 에서 약 400 ms 안이고, 느린 망 스텁에서도 응답 전에 캐시가 뜬다 | Release 는 `xcodebuild -configuration Release` 나 Release 스킴으로 준다. `measure()` 는 iterationCount+1 회 돌고 첫 회를 버리므로 재부팅 직후 콜드는 따로 재고 `-test-iterations` 는 곱해지니 쓰지 않는다. `xcresulttool export metrics`. 도구 없음: 지난 CSV 대비 비교 | 주요 |
|
|
261
|
+
| PERF-02 | 목록, 캐러셀, 지도의 스크롤·전환 끊김 | 메인 피드, 장소 목록, 사진 캐러셀, 지도 이동의 hitch ratio 가 5 ms/s 미만이고, 5~10 은 조사하고, 10 이상은 막는다 | `measure(metrics: [XCTHitchMetric(application: app)])`(iOS 26+)나 `XCTOSSignpostMetric.scrollingAndDecelerationMetric`, 또는 `xctrace record --template 'Animation Hitches'` | 주요 |
|
|
262
|
+
| PERF-03 | 메모리·CPU·저장소 소크와 누수 추적 | 상세 열고 닫기 100회와 탐색 루프 50회에서 최대 메모리 증가가 지난 출시 대비 여유 안이고, jetsam 이 없고, 큰 이미지는 다운샘플하고(4K JPEG 1.5 MB 가 약 31.6 MB 로 풀린다), 제출 전 Leaks 에 앱 소유 누수가 없다 | `XCTMemoryMetric`·`XCTCPUMetric`·`XCTStorageMetric`, `xctrace record --template Leaks --time-limit 60s` 뒤 `xctrace export --toc` | 주요 |
|
|
263
|
+
| PERF-04 | 런타임 검사기: 기기에서 ASan, Main Thread Checker, UBSan, 시뮬레이터에서 TSan | Checkers-Device 구성의 전체 UI 묶음에서 런타임 이슈가 0 이고, 시뮬레이터 TSan 경합이 0 이거나 멈췄으면 미실행으로 적는다 | `run_crash_free_gates.py --app <App> --run diagnostic --required-only`([crash-free 프로토콜](crash-free-app-protocol.md))나 `-enableAddressSanitizer YES -enableUndefinedBehaviorSanitizer YES`, 타임아웃은 늘린다(ASan 2~5배, TSan 2~20배) | 주요 |
|
|
264
|
+
| PERF-05 | 행과 메인 스레드 막힘 | 핵심 흐름에 250 ms 이상 행이 없고, 메인 스레드에 동기 네트워크, 큰 JSON 디코드, SCNetworkReachability 동기 호출, 동기 마이그레이션이 없다 | `device_qa.py perf --app <slug> --seconds 40`: 앱을 띄우고 Time Profiler 를 붙여 Instruments 의 potential-hangs·hang-risks 를 센다(0 이 아니면 실패). `xctrace --launch` 는 Xcode SDK 보다 새 iOS 에서 부팅 대기로 멈춰 attach 로 잰다. 현장은 Organizer Hangs 와 ASC diagnosticSignatures HANGS 다 | 주요 |
|
|
265
|
+
| PERF-06 | 저전력 모드와 열 압박 | 저전력에서 Smoke 와 위치 테스트가 통과하고 재량 백그라운드 작업이 멈추며, `thermalState` .serious·.critical 주입에 무거운 작업(이미지 프리페치, 애니메이션)이 물러나고 데이터 손실이 없다 | 저전력은 설정 > 배터리 자동화나 단축어 URL 을 `devicectl device process openURL` 로 켠다(둘 다 미확인). 열은 QA 구성 seam 으로 주입하고, 실제 열 조건과 4시간 배터리 소모는 가끔 손으로 본다 | 경미 |
|
|
266
|
+
|
|
267
|
+
### 19. 안정성 오라클과 견고성 (Stability)
|
|
268
|
+
|
|
269
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
270
|
+
|---|---|---|---|---|
|
|
271
|
+
| STAB-01 | 기기 테스트 세션마다 크래시·watchdog·jetsam 로그를 전후 비교한다 | 앱, 위젯, 확장 이름의 새 .ips, 0x8badf00d, 앱이 largestProcess 인 JetsamEvent 가 없고, 걸리면 단언이 다 통과해도 게이트가 실패하고 기호화한 로그를 버그에 붙인다 | `device_qa.py test` 가 끝에 자동으로 모으고, 따로는 `device_qa.py crashes --app <slug> --since-minutes 60 --out <dir>`(`devicectl device info files`·`copy from --domain-type systemCrashLogs`)다. 기호화는 아카이브 dSYM 으로 한다. 도구 없음: 확장 프로세스 이름 대조 | 차단 |
|
|
272
|
+
| STAB-02 | 조용한 실패를 잡는 콘솔 로그 오라클 | 세션 로그에 'Publishing changes from within view updates', 'AttributeGraph: cycle', 'Unable to simultaneously satisfy constraints', Thread Performance Checker 경고, 삼킨 CKError 가 새로 없고, 새 패턴은 FAMILIAR_PROBLEMS 에 더한다 | `devicectl device process launch --console` 은 표준 스트림만 잡는다. os_log 는 `sudo /usr/bin/log collect --device-udid <UDID> --last 30m` 로 모은다(root 필요, zsh 에서 `log` 는 내장 명령). 도구 없음: 로그 수집과 패턴 검사 | 주요 |
|
|
273
|
+
| STAB-03 | 기기에서 시드를 고정한 랜덤 탐색기(몽키)를 돌린다 | 릴리스마다 시드 3개 x 500걸음(큰 릴리스는 30~60분 소크) 동안 매 걸음 `app.state == .runningForeground` 이고 새 크래시가 없으며, 실패 시드는 재현해 시드와 함께 버그가 된다 | `app.snapshot()` 에서 hittable 요소를 골라 누르거나 INPUT-02 경계 문자열을 치고, 25걸음마다 `QA.snap`, 외부 링크와 구매는 identifier 허용 목록으로 막는다. `QA.sweepButtons` 는 결정적 훑기다. 도구 없음: 시드 랜덤 탐색기(실기기용으로 유지되는 크롤러가 없다) | 주요 |
|
|
274
|
+
| STAB-04 | 대본 있는 스트레스 투어 | 강박 투어(👍 50번, 당겨서 새로고침 30번, 시트 100번)에 중복 레코드와 크래시가 없고 메모리가 평평하며, rained-out 투어(비동기 동작 직후 뒤로·닫기)에 낡은 UI 와 고아 쓰기가 없고, 뒷골목 투어(설정, 라이선스, 오류, 빈 상태)의 화면이 모두 그려지고 감사를 통과한다 | 앱마다 투어 테스트 클래스 하나를 두고, 레코드는 cktool, 메모리는 `XCTMemoryMetric` 으로 본다 | 주요 |
|
|
275
|
+
| STAB-05 | watchdog·jetsam 밖의 종료 사유 | 새 .ips 에 Termination Reason 이 있으면 실패한다: 0xdead10cc(일시정지 중 파일·SQLite 잠금, 앱과 위젯 App Group 버그), 0x2182bad2·0x2182bad3·0x2182bad4(백그라운드 시간 초과), 0xc00010ff(열), 0xbaddd15c(저장 공간 부족), CPU·디스크 쓰기 리소스 보고도 모은다 | App Group 쓰기 도중 일시정지시키는 테스트를 둔다. 도구 없음: .ips Termination Reason 파서(`device_qa.py crashes` 확장) | 차단 |
|
|
276
|
+
| STAB-06 | 저장 공간 부족과 디스크 가득 | 쓰기, 캐시, SwiftData·Core Data 저장, 사진 프리페치가 데이터 손실 없이 우아하게 실패하고, 실행 사이에 Caches 가 비워져도 돈다 | `devicectl device copy to --domain-type temporary` 로 큰 파일을 채우는 방법은 사용자 저장 공간에 잡히는지 미확인이다. 도구 없음: 디스크 채우기 시나리오 | 주요 |
|
|
277
|
+
| STAB-07 | Swift 6 언어 모드의 런타임 격리 트랩 (해당 앱만) | 언어 모드를 바꾼 릴리스에서 알림 권한, 위치 delegate, BGTask, CloudKit 완료, WidgetKit provider, Progress 같은 시스템 콜백 경로를 Release 로 기기에서 한 번씩 돌렸고, STAB-01 로그에 `dispatch_assert_queue`·`swift_task_checkIsolated` 트랩이 없다 | 지금 모든 앱이 `SWIFT_VERSION` 5.0 이라 언어 모드가 바뀔 때만 돈다. 컴파일러 경고 없이 트랩한다(SE-0423) | 주요 |
|
|
278
|
+
|
|
279
|
+
### 20. 탐색적 테스트 (Exploratory)
|
|
280
|
+
|
|
281
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
282
|
+
|---|---|---|---|---|
|
|
283
|
+
| EXP-01 | diff 에서 뽑은 차터로 시간 상자 탐색 세션(SBTM)을 돈다 | 릴리스가 건드린 I SLICED UP FUN 글자나 투어마다 45~90분 세션이 있고, `docs/qa/sessions/<version>/` 시트에 차터, 영역, 시작 시각, 작업 비율, 데이터, 노트, 버그, 질문, 안 덮은 영역, 디브리프가 있다 | 에이전트가 탐색 XCUITest 로 기기를 몰며 `QA.sweepButtons`·`QA.snap`·`app.debugDescription` 을 남기고, FEW HICCUPPS 로 판단해 발견마다 오라클을 단다. 못 돈 차터는 '테스트 안 한 것'에 넣는다 | 주요 |
|
|
284
|
+
| EXP-02 | 조건을 겹친 페르소나 연속극 세 개 | 페르소나마다 핵심 목표 하나('지금 영업 중인 반려견 동반 장소를 찾아 사유와 함께 투표하고 공유')를 한 XCUITest 로 끝낸다. A 65세(AX5, 대비 증가, VoiceOver), B 지하철 통근자(Very Bad Network 스텁, 멈춤 있는 10 m/s 경로, 투표 중 Siri), C 여행자(도쿄·제주, en_US, Low Data Mode) | devicectl 설정, 위치 경로, 망 스텁을 한 테스트에 묶는다. 한 손·이동 중 감각은 주인의 세션(EXP-03) 몫이다 | 주요 |
|
|
285
|
+
| EXP-03 | 릴리스마다 짧은 사람 테스트 세션 (사람) | 주인이 에이전트가 준비한 차터(새 기능, 위험 변경, 스크린샷 접촉 시트, 열린 발견)로 TestFlight 빌드를 10~20분 쓰고 노트가 발견으로 남으며, 이 세션이 없으면 '탐색 안 함'으로 보고한다 | 에이전트가 차터와 접촉 시트를 쓰고 노트를 버그로 바꾼다. 자동화는 확인이지 테스트가 아니다 | 주요 |
|
|
286
|
+
| EXP-04 | 사용성 휴리스틱 검토 | 새로 생기거나 바뀐 화면마다 스크린샷에 Nielsen 10 휴리스틱을 대고, 발견에 휴리스틱 번호를 단다(행동할 수 있는 오류 문구, 투표 되돌리기, 일관된 용어) | UI-01 접촉 시트를 입력으로 쓴다 | 경미 |
|
|
287
|
+
|
|
288
|
+
### 21. App Store 제출 요건 (Submission)
|
|
289
|
+
|
|
290
|
+
업로드·메타데이터·제출 절차는 [App Store release runbook](../release/app-store-release-runbook.md)이 정하고, 여기서는 제출 전에 증거로 닫을 검사만 둔다.
|
|
291
|
+
|
|
292
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
293
|
+
|---|---|---|---|---|
|
|
294
|
+
| SUB-01 | 순수 Release 빌드에서 플레이스홀더, 디버그, 숨은 기능 잔여물을 찾는다 | 사용자에게 보이는 문자열과 리소스에 Lorem, TODO, FIXME, TBD, placeholder, sample, test, 테스트, 샘플, 디버그, localhost, 127.0.0.1, staging, example.com 이 없고, 기능을 바꾸는 launch argument, URL 경로, 원격 키는 지웠거나 심사 메모에 적었다 | `release_checks.py` 플레이스홀더 검사, `strings -a` 와 .xcstrings·JSON·SwiftUI 리터럴 검사, debugging export 에서 `#if DEBUG` 기호가 없는지 확인한다(QA 구성은 대상 아님) | 차단 |
|
|
295
|
+
| SUB-02 | 모든 링크가 동작한다 | 바이너리, 문자열 카탈로그, 번들 JSON, ASC 메타데이터(지원, 마케팅, 개인정보), 심사 메모의 URL 이 리다이렉트 뒤 200 이고, 본문이 500바이트를 넘고 'Lorem'·'Coming soon' 이 없으며, 지원 페이지에 실제 연락처가 있고, UI 테스트가 앱 안 개인정보·지원 링크에 닿는다 | `release_checks.py` 링크 검사, ASC URL 은 appInfoLocalizations 와 appStoreVersionLocalizations 에서 읽는다. 이름을 바꾼 뒤에는 GitHub Pages 가 가장 잘 404 난다 | 차단 |
|
|
296
|
+
| SUB-03 | 모든 번들(앱, .appex, 임베디드 프레임워크)의 required-reason API 선언 | required-reason API 를 참조하는 Mach-O 마다 자기 `PrivacyInfo.xcprivacy` 에 범주와 실제 쓰임에 맞는 사유가 있다(예: 위젯과 공유하는 App Group UserDefaults 1C8F.1, 앱 전용 CA92.1, 내 컨테이너 타임스탬프 C617.1, systemUptime 경과 35F9.1) | `release_checks.py` required-reason 검사, 아카이브 Mach-O 마다 `nm -u`·`strings` 와 `plutil -p PrivacyInfo.xcprivacy` 를 대조한다(실제 사례: 댕오늘 2026-10-26 미선언) | 차단 |
|
|
297
|
+
| SUB-04 | 외부 SDK 매니페스트와 서명 | `Package.resolved`·`Podfile.lock` 의 의존 중 Apple 목록에 있는 것은 아카이브 안에 `PrivacyInfo.xcprivacy` 가 있고, 바이너리 .xcframework 는 서명이 유효하며, Apple 목록은 날짜와 함께 레포에 두고 매달 갱신한다 | 해결된 패키지와 저장 목록의 교집합을 보고 `codesign -dv --verbose=4 <x>.xcframework` 를 돌리고, ITMS-91061 메일이 없어야 한다. 도구 없음: SDK 목록 교차 검사 | 차단 |
|
|
298
|
+
| SUB-05 | 개인정보 영양 라벨이 실제 데이터 흐름과 맞다 | 코드, 프로덕션 CloudKit 스키마, 패킷 캡처로 만든 데이터 흐름 목록(흐름, 종류, 연결, 추적, 목적)이 `NSPrivacyCollectedDataTypes` 와 ASC App Privacy 답과 맞고(소수 3자리 이상은 정밀 위치, 자유 텍스트는 Other User Content, 고정 id 는 연결), 목록이 바뀌면 주인이 ASC 답을 다시 확인한다 | 캡처는 `/Library/Apple/usr/bin/rvictl -s <UDID>` 와 `sudo tcpdump -i rvi0` 인데 iOS 27 에서 rvi0 가 안 뜬다는 보고(FB23780118)가 있어 대안이 필요하다. Organizer 개인정보 보고서는 GUI 전용이라 번들별 PrivacyInfo 를 `plutil` 로 합친다. ASC 답은 API 키로 못 읽는다. 도구 없음: 데이터 흐름 목록 생성기 | 차단 |
|
|
299
|
+
| SUB-06 | 개인정보 처리방침 내용과 일관성 | ko·en 방침이 수집 데이터, 방법, 모든 용도, 제3자 동등 보호, 보관·삭제·동의 철회, 외부 AI 공유를 말하고, SUB-05 목록과 모순이 없다('사용자 id 없음'인데 있으면 막는다) | ASC privacyPolicyUrl 과 앱 안 링크를 받아 키워드와 데이터 종류를 목록과 비교하고, 릴리스마다와 방침을 발행할 때마다 돈다. 도구 없음: 방침 대 목록 diff | 차단 |
|
|
300
|
+
| SUB-07 | 계정과 사용자 생성 콘텐츠 게이트 | 계정이 있으면(자동 게스트 포함) 앱 안 계정 삭제가 끝까지 되고 공유 콘텐츠를 지우고 Sign in with Apple 토큰을 철회하며, 다른 사용자에게 보이는 자유 텍스트·사진이 있으면 필터, 신고, 차단, 연락처와 연령 등급 UGC '예'가 있다(enum 만인 👎 사유는 자유 텍스트가 아니다) | 공개 DB 저장(`publicCloudDatabase.save`, `modifyRecords`)과 서버 POST 를 훑어 필드를 enum, 숫자, 자유 텍스트로 나누고, 새 공유 필드는 주인의 UGC 판단으로 보낸다. 도구 없음: 공유 필드 분류기 | 차단 |
|
|
301
|
+
| SUB-08 | 심사 메모를 명세처럼 쓰고 바이트로 제한한다 | 개념과 대상, 바뀐 것과 위치, 테스트한 기기와 OS, 외부 서비스, 지역 차이와 한국 밖 심사자가 보는 경로(SUB-09), 잠긴 기능, 필요하면 만료 없는 데모 계정, + 형식 전화번호를 담고, UTF-8 4,000바이트 이하이며(한글은 3바이트), PATCH 뒤 다시 읽었다 | `release_checks.py` 심사 메모 바이트 검사, 메모는 테스트 로그·SUB-05 호스트·SUB-09 단계·git log 에서 만들고 `appstore_release.py` 로 appStoreReviewDetails 를 PATCH 한다(실제 사례: 빌드 35 attach 409) | 차단 |
|
|
302
|
+
| SUB-09 | 해외 심사자 시나리오: 쿠퍼티노의 새 기기가 보는 화면 (해당 앱만) | 미국 좌표(37.3349, -122.0090), en_US, 새 설치, iCloud 계정 없음, Apple Intelligence 없음에서 첫 화면이 비지 않고, 한국 지역 선택이나 샘플 내용이 보이고, 심사 메모가 그 경로를 설명한다 | ReviewerAbroad UI 테스트(`XCUIDevice.shared.location`, `-testLanguage en -testRegion US`)에 L10N-06 의 KST 진리표 단언을 넣는다. 심사자 위치는 Apple 이 공개하지 않는다 | 차단 |
|
|
303
|
+
| SUB-10 | What's New 가 사용자 눈에 보이는 모든 변경을 덮는다 | 지난 승인 버전 이후 git log, DEVLOG, .xcstrings diff, 새 화면, 새 권한, 새 알림의 변경이 모든 로케일 whatsNew(4,000자 이하)와 심사 메모에 있고, 일반 문구는 순수 버그 수정 릴리스에만 쓴다 | `release_checks.py` 메타데이터 lint(What's New). 문구는 저장소 글쓰기 방침대로 Codex 가 주인 말투로 쓴다 | 주요 |
|
|
304
|
+
| SUB-11 | 스크린샷이 제출 빌드와 맞고 규칙을 지킨다 | 이 빌드로 다시 찍어 업로드본과의 지각 차이에 낡은 UI 가 없고, 세트마다 절반 넘게 앱 사용 장면이며, 필수 'iPhone with Dynamic Island (medium display)' 1179x2556 또는 1206x2622 가 있고(14 Pro 원본이 1179x2556), 13인치 iPad 는 iPadOS 를 지원할 때만이고, 알파 채널, 다른 플랫폼 이름, 실제 개인정보, 가격이 없다 | `apps/<slug>/scripts/capture_ios_store_screenshots.sh`, `sips -g hasAlpha`, OCR, appScreenshotSets 다운로드 비교 뒤 `appstore_release.py --screenshots`(Duo 요구 2027-04 를 추적한다). 도구 없음: 업로드본 대비 지각 diff | 주요 |
|
|
305
|
+
| SUB-12 | 텍스트 메타데이터 lint | 이름 2~30자, 부제 30자, 키워드 100바이트에 키워드마다 3자 이상(두 글자 '애견'은 안 된다), 홍보 170자, 설명 4,000자이고, 모든 메타데이터 필드에 다른 앱·회사 이름(카카오, 네이버, Google, 다이닝코드, Instagram, 경쟁사)이 없고, 이름·부제·스크린샷에 가격(₩, $, 무료, free)이 없고, 검증 못 할 주장(#1, 최고, best)과 실제 데이터와 다른 숫자가 없고, 홈 화면 이름은 스토어 이름과 헷갈리지 않을 만큼 비슷하다(2.3.8) | `release_checks.py` 메타데이터 lint, 길이는 `.encode('utf-8')` 바이트로 잰다 | 주요 |
|
|
306
|
+
| SUB-13 | WeatherKit 데이터의 Apple 날씨 표시 (해당 앱만) | 날씨 데이터를 보이는 곳에 ' Weather' 상표와 다른 데이터 출처 법적 링크(`legalPageURL` 200)가 있고, 가공한 값(산책 지수 등)에는 ' Weather' 와 수정 고지가 있고, 경보는 Apple 경보 상세 링크와 기관 전체 이름을 보이며 경보 문구를 고치지 않는다(링크를 못 담는 위젯·알림의 규칙은 미확인) | `WeatherService` 소비처를 훑고 `WeatherService.shared.attribution` 뷰에 identifier 를 달아 화면 테스트와 위젯 스크린샷에서 단언한다. 주인 은 5.2.5 거절 경험이 있다 | 차단 |
|
|
307
|
+
| SUB-14 | 외부 콘텐츠 권리·표시 장부 (해당 앱만) | 소스마다(Kakao Local, Google Places, Wikimedia Commons, 공공누리, Daum) iOS 앱 표시를 허락하는 약관 조항, 필요 표시, 저장 한도가 적혀 있고, UI 테스트가 화면마다 표시 요소를 단언하고, Commons CC BY·BY-SA 사진은 저자와 라이선스를 보이고, ASC 콘텐츠 권리 답이 맞다 | 앱별 DATA_TERMS 표를 코드 상수, UI identifier, 서버 저장소, 발행 피드, 지도 화면과 대조한다(앱 캐시 TTL 만 보면 안 된다). 도구 없음: 권리 장부 대조기 | 차단 |
|
|
308
|
+
| SUB-15 | 연령 등급 답을 출시 기능에서 다시 끌어낸다 | 코드 신호(제한 없는 웹 뷰, UGC, 광고, 경품, 건강 주제, 메시지, 술집·양조장 같은 술 관련 장소)로 낸 답이 ASC ageRatingDeclarations 와 같고, Age Assurance·Parental Controls·Social Media 항목이 채워졌고, 'For Kids'·'For Children' 문구와 Apple 이모지를 래스터로 만든 이미지가 없고, 바뀐 답은 주인이 승인했다 | 신호를 훑고 ASC 값을 읽어 비교한다(socialMedia 가 새로 필수였던 적이 있다). 도구 없음: 연령 신호 매퍼 | 주요 |
|
|
309
|
+
| SUB-16 | 증거로 하는 수출 규정 선언 | 비 OS 암호를 번들한 바이너리가 없을 때만 `ITSAppUsesNonExemptEncryption=NO` 이고, 번들한 암호가 있으면 주인의 법적 판단으로 멈춘다 | `plutil -extract ITSAppUsesNonExemptEncryption raw`, Mach-O 마다 `otool -L` 과 `nm` 으로 `SSL_`·`EVP_`·`crypto_box` 를 찾고, ASC usesNonExemptEncryption 을 읽는다 | 주요 |
|
|
310
|
+
| SUB-17 | 툴체인, 업로드 검증, 비동기 ITMS 경고 | DTSDKName 이 iOS 26 SDK 이상(2026-04-28 부터)이고 MinimumOSVersion 이 13 이상(2026-09-09 부터)이며, `altool --validate-app` 이 통과하고 processingState 가 VALID 이고, 이 빌드 번호로 ITMS-90683·91053·91061 메일이 없다 | 앱별 `archive_ios.sh`(altool 검증 포함), `GET /v1/builds?filter[app]=..&filter[version]=..` 폴링, Gmail 에서 'ITMS-' 와 빌드 번호를 찾는다 | 차단 |
|
|
311
|
+
| SUB-18 | 선언한 능력과 백그라운드 모드가 코드와 맞다 | 앱과 모든 .appex 의 UIBackgroundModes, BGTaskSchedulerPermittedIdentifiers, entitlement 마다 맞는 코드 사용이 있고, 위젯·확장·알림은 앱과 관련되며 광고가 없다 | 번들마다 `plutil -p Info.plist` 와 `codesign -d --entitlements - --xml` 을 `allowsBackgroundLocationUpdates`·`BGTaskScheduler.shared.register`·`registerForRemoteNotifications` 검색과 대조한다. 도구 없음: 능력 대 코드 대조(`release_checks.py` 에 추가) | 주요 |
|
|
312
|
+
| SUB-19 | 네이티브 가치 하한(최소 기능, 4.2.2) | 웹 클리핑, 콘텐츠 모음, 링크 묶음이 아니라 앱이 기기에서 계산하고 판단하는 것이 분명하다(웹 뷰가 0개인 장소 목록 앱도 해당된다) | 오라클은 웹 뷰 비율이 아니라 심사 메모 첫머리의 설명과 앱이 계산하는 것을 보이는 화면 녹화(`devicectl device capture screen-record`)다. 설명 문구는 주인이 앱별로 한 번 정한다 | 경미 |
|
|
313
|
+
| SUB-20 | 외부 TestFlight Beta App Review 를 예행연습으로 쓴다 | 새 버전 라인이나 권한·서비스·UGC 를 더한 빌드는, 그 버전에서 외부 그룹에 보낸 첫 빌드일 때만 예행연습으로 세고, App Store 승인의 증거로는 쓰지 않는다(이후 빌드는 전체 심사를 안 받을 수 있고 메타데이터는 보지 않는다) | betaBuildLocalizations(What to Test) POST, 외부 betaGroup 추가, betaAppReviewSubmissions POST 뒤 상태를 폴링한다(`appstore_release.py --beta-group`) | 주요 |
|
|
314
|
+
| SUB-21 | 계정 수준 배포 전제 | 인증된 ASC API 호출이 약관 403 없이 돌고, 국가 가용성이 의도와 맞고, 앱마다 DSA 거래자 상태가 선언됐고(거래자 아님도 유효) 공개 연락처 선택이 기록됐다 | appAvailabilities 와 territoryAvailabilities 를 읽고 값싼 인증 호출을 한다. 약관과 DSA 는 빠졌을 때만 주인이 ASC 화면에서 처리한다 | 주요 |
|
|
315
|
+
| SUB-22 | 위치정보법 위치기반서비스사업 신고 판단 (해당 앱만) (사람) | 무료 1인 앱이 '사업'인지에 대한 주인의 판단이 기록됐고, 해당하면 신고(제9조, 소상공인·1인 창조기업은 제9조의2 로 시작 뒤 1개월 안)와 위치기반서비스 약관·동의 화면이 있다 | 에이전트가 체크리스트 초안을 쓰고 적용 여부는 주인이 정한다(2021년 미신고 사업자 90곳이 경찰에 넘겨졌다, Apple §5 현지법 준수) | 주요 |
|
|
316
|
+
| SUB-23 | 개인정보 보호법 처리방침 필수 항목과 앱 접근권한 고지 (해당 앱만) | 처리방침에 처리 항목, 보관 기간, 파기, 권리, 안전 조치, 개인정보 보호책임자 연락처, 국외 이전 근거와 제28조의8 ②항 사항(CloudKit, Google 은 국외 수신자)이 있고, 정보통신망법 제22조의2 필수·선택 접근권한 고지가 있다 | SUB-06 키워드 검사를 이 항목으로 넓힌다. 적용 여부는 주인의 법적 판단이다 | 주요 |
|
|
317
|
+
| SUB-24 | Apple 지도 로고와 법적 링크가 늘 보인다 (해당 앱만) | 지도 화면에서 아래 카드가 떠 있어도 법적 링크를 누를 수 있다(HIG Maps: 잠깐 가리는 것은 되지만 늘 가리면 안 된다) | 지도 스크린샷 테스트에서 법적 링크의 `isHittable` 을 단언한다 | 경미 |
|
|
318
|
+
| SUB-25 | 지역 게이트: 중국 본토 ICP 와 의도한 국가 목록 | appAvailabilities 가 의도 국가 파일과 맞고, MIIT ICP 신고 번호가 없으면 중국 본토를 빼고 그 결정을 기록했다 | ASC appAvailabilities 를 읽어 파일과 비교한다. 도구 없음: 의도 국가 파일과 비교기 | 경미 |
|
|
319
|
+
| SUB-26 | Google Maps Platform 약관 (해당 앱만) | Places 콘텐츠를 Google 지도가 아닌 지도에 쓰지 않고, 캐시는 place ID 와 30일 위경도만이며, 이름·주소·리뷰·평점·사진을 서버, 피드, 이력에 저장하지 않는다(Service Specific Terms §14.2~14.3, ToS §3.2.3) | 서버 파이프라인(`daily.py` google 단계, `history.sqlite`, Pages 피드)과 모든 지도 화면을 Google 출처 필드로 훑는다. 지금 댕오늘은 googleRating·googlePhotoURL 30일 보관과 MapKit 위 PhotoPin 이 어긋나고, 허용 경로는 Places UI Kit(§15.1)다. 도구 없음: Google 필드 스캐너 | 차단 |
|
|
320
|
+
| SUB-27 | Kakao Local 결과 저장 금지 (해당 앱만) | Kakao Local 결과(원본과 가공)를 기기나 서버에 저장하지 않고 실시간 호출로만 쓰거나, Kakao 가 서면으로 허락했다 | Pawday 야간 kakao 단계의 교차 확인 저장은 Kakao 확인 전까지 BLOCK 이다. 근거는 2026-08-20 DevTalk 운영자 답변이고 약관 문구는 아니다 | 차단 |
|
|
321
|
+
| SUB-28 | 긁거나 파생한 소스마다 권리 꾸러미를 둔다(5.2.2) (해당 앱만) | 다이닝코드 일요일 크롤(robots.txt 는 약관 허락이 아니다), Instagram·네이버 언급 수, MapKit 서버 교차 확인마다 약관 근거나 서면 허락이 있어 요청받으면 낼 수 있다 | SUB-14 장부에 행을 더하고 계약서나 메일 같은 증빙 위치를 적는다 | 주요 |
|
|
322
|
+
| SUB-29 | 미국 주 연령 확인법(텍사스 SB 2420, 2026-06-04 부터) | 국가 목록이 규제 지역을 빼거나 `AgeRangeService.requiredRegulatoryFeatures` 를 처리하고, 릴리스마다 '중대한 변경'인지 기록한다(연령 등급 변경은 해당) | Declared Age Range API, PermissionKit Significant Change API, StoreKit 연령 등급 속성, 동의 철회 서버 알림을 확인한다 | 주요 |
|
|
323
|
+
| SUB-30 | 리뷰 요청 가드(5.6.1) | 평점 요청은 제공 API 와 사용자가 누른 리뷰 쓰기 링크뿐이고, 직접 만든 평점 대화상자, '앱이 마음에 드세요?' 거르기, 보상이 없다 | 지금은 Elsewhen ClocksView 와 Daynest TimelineView 가 API 를, Daynest SettingsView 가 링크를 쓴다. 도구 없음: 리뷰 요청 검사 게이트 | 경미 |
|
|
324
|
+
| SUB-31 | 위치를 받는 제3자를 공유 전에 밝힌다(5.1.2(i), 5.1.5) (해당 앱만) | 새 수신자마다 공유에 해당하는지 판단을 한 번 기록하고, 해당하면 목적 문자열이나 동의 화면이 수신자를 밝힌다 | 지금 댕오늘은 반올림한 좌표를 Kakao 로 보내는데 목적 문자열에 수신자가 없다(KakaoNearby.swift) | 주요 |
|
|
325
|
+
| SUB-32 | 아이콘과 브랜드(2.3.8, 2.3.9, 4.1) | 작은·큰·대체 아이콘이 서로 비슷하고, 아이콘과 이름에 남의 브랜드나 SF Symbol 같은 글리프가 없고, AI 생성 아이콘의 권리가 기록됐고, 1024 아이콘에 알파가 없다(Icon Composer 는 ITMS-90717 이 흔하다) | `sips -g hasAlpha` 와 아이콘 세트 비교, 권리는 DESIGN.md 에 적는다 | 주요 |
|
|
326
|
+
| SUB-33 | Mac App Store 분기(2.4.5) (해당 앱만) | 샌드박스이고, 동의 없는 로그인 시 자동 실행, 자체 업데이터, 내려받는 코드가 없고, 모든 지역화가 한 번들에 있고, quarantine 속성을 뗐다(2025-02-18 부터) | Elsewhen Mac 같은 Mac 앱에 `codesign -d --entitlements - --xml` 로 샌드박스를, `xattr` 로 quarantine 을 확인한다(PlainClips 는 2.4.5 로 거절된 적이 있다) | 주요 |
|
|
327
|
+
| SUB-34 | 생성형 AI 규칙(1.2, Foundation Models 허용 사용) (해당 앱만) | 온디바이스 대화 탭이 'AI 콘텐츠 생성 앱'인지 판단이 한 번 기록됐고, guardrail·거절 오류가 안전한 대체 화면을 보인다 | `LanguageModelSession.GenerationError` 를 주입하는 UI 테스트를 둔다 | 주요 |
|
|
328
|
+
| SUB-35 | 메타데이터에 서비스 지역을 밝힌다 (해당 앱만) | 영어 부제와 설명이 미국 심사자에게 '근처 장소'를 약속하지 않고 대상이 한국이라고 말하며, 심사 메모가 지역 차이를 설명하거나 같다고 확인한다(2.3.1(a)) | `release_checks.py` 메타데이터 lint 에 지역 문구 규칙을 더한다 | 주요 |
|
|
329
|
+
| SUB-36 | 공개 데이터베이스에서도 개인정보를 모으지 않는다(5.1.1(viii)) (해당 앱만) | 발행 피드와 이력 저장소에 인허가 기록이나 크롤 목록에서 온 개인 데이터(대표자 이름, 개인 전화번호, 개인 Instagram 계정)가 없다 | 피드와 `history.sqlite` 를 이름·전화번호·핸들 패턴으로 훑는다. 도구 없음: 개인정보 패턴 스캐너 | 주요 |
|
|
330
|
+
|
|
331
|
+
### 22. TestFlight 현장 테스트 (Field)
|
|
332
|
+
|
|
333
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
334
|
+
|---|---|---|---|---|
|
|
335
|
+
| FIELD-01 | 내부 TestFlight 소크를 현장 테스트 단계로 쓴다 (사람) | 기능 릴리스는 후보를 최소 2일 실제 생활에서 쓰고, What to Test 는 위험 목록과 '테스트 안 한 것'에서 온다(예: 산책하다 중간에 Wi-Fi 끄기) | 에이전트가 업로드하고 What to Test 를 쓰고 내부 테스터를 넣는다(최대 100명, 심사 없음, 90일 만료) | 주요 |
|
|
336
|
+
| FIELD-02 | 제출 전 소크 원격 측정을 기계로 확인한다 | 후보 빌드의 betaBuildUsages crashCount 가 0 이고, 해결 안 된 betaFeedbackCrashSubmissions 와 ScreenshotSubmissions 가 없고, 크래시 로그는 기호화해 차단 이슈가 된다 | 소크 동안 launchd 폴러가 매시간 `.local-secrets` 의 ASC .p8 JWT 로 `/v1/builds/{id}/metrics/betaBuildUsages` 와 `/v1/apps/{id}/betaFeedbackCrashSubmissions` 를 읽는다. 도구 없음: 소크 폴러 | 차단 |
|
|
337
|
+
| FIELD-03 | 소크 동안 현장 전용 체크리스트 (사람) | 산책 중 Wi-Fi→셀룰러 전환, 음영 지역, 햇빛 아래 가독성, 실제 GPS 흔들림, 프로덕션 푸시와 아침 브리핑 시각, 집중 모드가 끝난 뒤 알림 몰림, 메시지·메모·카카오톡에서 누른 유니버설 링크, 제스처와 한 손 사용이 끝났거나 '테스트 안 한 것'에 있다 | 에이전트가 빌드와 함께 목록을 보내고 주인의 답을 발견으로 남긴다 | 주요 |
|
|
338
|
+
|
|
339
|
+
### 23. 출시·단계적 배포·출시 후 (Release)
|
|
340
|
+
|
|
341
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
342
|
+
|---|---|---|---|---|
|
|
343
|
+
| REL-01 | 사소하지 않은 업데이트는 수동 출시와 단계적 출시로 낸다 | 제출은 'Manually release this version' 이고, 마이그레이션, CloudKit 스키마, StoreKit, 핵심 탐색을 건드린 업데이트는 단계적 출시(7일 동안 1·2·5·10·20·50·100%)가 켜져 있고, 되돌릴 수 없는 출시 요청은 주인의 명시적 go 뒤 48시간 지켜볼 수 있는 시각에 보낸다 | 제출 때 `POST /v1/appStoreVersionPhasedReleases`, 승인 뒤 `POST /v1/appStoreVersionReleaseRequests`(취소 불가). 첫 버전에는 단계적 출시가 없다 | 주요 |
|
|
344
|
+
| REL-02 | 미리 합의한 멈춤 규칙으로 7일 배포를 감시한다 | T+1h, T+24h, 5·6일차 단계 전에 확인하고, 핵심 흐름의 새 행 시그니처, 데이터 손실·동기화 신고, 크래시를 말하는 리뷰, CloudKit 오류 급증이면 멈추고 증거와 함께 주인에게 알리며, 멈춤은 합쳐 30일까지라 30일째 결정 지점을 두고, 판매 중단은 브레이크로 쓰지 않는다 | perfPowerMetrics, diagnosticSignatures(HANGS·DISK_WRITES·LAUNCHES 만, 크래시는 없다), customerReviews 를 읽고, 크래시는 MetricKit(REL-04)이나 Organizer 로 본다. 멈춤은 `PATCH phasedReleaseState=PAUSED`. 도구 없음: 배포 감시 잡 | 주요 |
|
|
345
|
+
| REL-03 | 스토어 바이너리의 0일차 스모크 | 스토어에 새 버전이 보이면 테스트 아이폰을 App Store 에서 업데이트하고 버전을 확인하고 블랙박스 스모크가 통과하며, 실행 실패는 바로 올린다 | `XCUIApplication(bundleIdentifier: "com.apple.AppStore")`(깨지기 쉽다)나 주인의 업데이트 탭, `devicectl device info apps --include-default-apps`, BUILD-08 묶음(2012년 App Store 가 깨진 FairPlay 바이너리를 낸 적이 있다) | 주요 |
|
|
346
|
+
| REL-04 | 직접 모으는 현장 진단(MetricKit) | 앱이 MetricKit 진단·지표 페이로드를 버전·빌드와 함께 저장해 관리하는 엔드포인트로 올리고, debug 빌드의 강제 크래시·행이 다음 실행에 도착하고, 개인정보 라벨이 진단을 덮는다 | `MXMetricManager` 구독자(iOS 18~26)나 iOS 27 MetricManager async sequence, 테스트 페이로드는 debugging 설치본에서 `devicectl device copy from --domain-type appDataContainer` 로 꺼낸다. 도구 없음: MetricKit 수집 엔드포인트 | 주요 |
|
|
347
|
+
| REL-05 | 크래시 너머의 출시 후 감시 | 7일 동안 새 버전 리뷰를 매일 훑고, CloudKit Console Telemetry·Logs 를 1·3일차에 보고, 지원 메일함을 훑고, 피드 프로브가 초록이며, 리뷰 답은 주인 승인 뒤에만 낸다 | `GET /v1/appStoreVersions/{id}/customerReviews`(Elsewhen `asc_metrics.py` 리뷰 섹션 재사용), CloudKit Telemetry 는 로그인된 Playwright 브라우저나 주인이 보고, 지원 메일은 Gmail 에서 찾는다 | 주요 |
|
|
348
|
+
| REL-06 | 출시 전에 고쳐 나가기(fix-forward) 런북을 준비한다 | 출시 태그에서 핫픽스 브랜치를 낼 수 있고, 아카이브와 dSYM 이 있고, 재현 단계가 담긴 긴급 심사 요청 초안이 있고, 멈춤·심사에서 빼기·마지막 정상 코드를 더 높은 버전으로 재제출 중 고르는 결정 표가 있으며, 판매 중단이 나쁜 브레이크인 이유(단계적 출시 중단, 복귀 때 전원에게)가 적혀 있다 | 앱 문서에 런북을 두고, 긴급 심사는 주인이 developer.apple.com/contact/app-store/?topic=expedite 에서 낸다(심사 50% 24시간, 90% 48시간) | 주요 |
|
|
349
|
+
| REL-07 | 출시 뒤에도 심사를 계속 통과하도록 주간 프로덕션 스모크를 돈다 | 예약 잡이 설치된 스토어·TestFlight 빌드로 ReviewerJourney 와 ReviewerAbroad 를 돌리고, 데이터 엔드포인트와 키 상태를 curl 하고, 실패만 iMessage 로 알리고, `core/server_services.py` 에 등록돼 있다 | 헤드리스 Mac 과 USB 아이폰의 launchd 잡이다(2025년 4.0 위반이 두 번째로 큰 삭제 사유, 72,271개). 도구 없음: 주간 스모크 잡 | 주요 |
|
|
350
|
+
| REL-08 | 만료와 폐기 달력 | 개발자 프로그램 갱신, 도메인과 Pages, ASC·APNs 키, Kakao·Google 키 상태와 가격, 제공자 폐기 공지를 매달 확인한다(멤버십이 끊기면 판매가 멈추고, 복귀 때 단계적 출시 없이 전원에게 간다) | 매달 잡을 돈다(Google Places API Legacy 는 2025-03-01 부터 새 프로젝트에 막혔고 12개월 예고 뒤 종료된다). 도구 없음: 월간 만료 점검 잡 | 주요 |
|
|
351
|
+
|
|
352
|
+
### 24. 결함 관리와 학습 (Defects)
|
|
353
|
+
|
|
354
|
+
| ID | 검사 | 통과 기준 | 방법·도구 | 심각도 |
|
|
355
|
+
|---|---|---|---|---|
|
|
356
|
+
| DEF-01 | 완전하고 최소화한 버그 보고를 쓰고 출시 차단 심각도로 분류한다 | 버그마다 ID, 제목, 빌드(버전과 빌드 번호), 기기와 iOS, 설정 행(언어, 글자 크기, 권한, 망), 최소 단계, 기대와 실제, 스크린샷·녹화, xcresult 경로, .ips 가 있고, S1 크래시·데이터 손실·개인정보·거절 위험, S2 핵심 흐름 고장, S3 우회 가능 저하, S4 겉모습으로 나누고, 고치기 전에 다른 화면의 같은 버그를 찾는다(RIMGEA) | `docs/qa/bugs/<id>.md` 나 KNOWN_ISSUES 템플릿, 캡처는 `devicectl device capture screenshot`·`screen-record` | 주요 |
|
|
357
|
+
| DEF-02 | 빠져나간 결함 장부가 다음 릴리스를 먹인다 | 출시 뒤나 주인이 찾은 버그마다 증상, 원인 부류(마이그레이션, 스키마, 계약, 실행 경로 의존, entitlement, UI 상태, l10n, 시간), 버전, 회귀 테스트 경로, 더한 게이트 줄이 있고, 게이트가 이번 diff 의 상위 위험 부류를 내 PLAN-01 점수와 EXP-01 차터를 정한다 | 앱별 YAML·markdown 장부를 두고, SUITE-05 증거와 FAMILIAR_PROBLEMS 항목 없이는 버그를 닫지 않는다 | 주요 |
|
|
358
|
+
|
|
359
|
+
## 출시 판정 (exit criteria)
|
|
360
|
+
|
|
361
|
+
아래를 모두 채워야 주인에게 제출 go 를 묻는다. 빨강이나 건너뛴 행은 '테스트 안 한 것'에 붙인다.
|
|
362
|
+
|
|
363
|
+
1. release_gate 가 ASC 에 붙은 바로 그 빌드에 PASS 를 냈고, 빌드 번호와 GitCommit 이 게이트 기록과 같고, 깨끗한 트리에서 아카이브했다(PLAN-05, BUILD-06, BUILD-11).
|
|
364
|
+
2. Smoke 와 전체 계획이 USB 아이폰의 Release 빌드에서 디버거 없이 초록이다. 실행 수는 0보다 크고 지난 릴리스 이상이며, 목적지는 실기기이고, 설명 안 된 FLAKY 가 없다(FUNC-01, BUILD-01, SUITE-01, SUITE-02).
|
|
365
|
+
3. 이 빌드의 모든 기기 실행에서 앱이나 확장을 이름 댄 새 크래시·watchdog·jetsam·종료 사유 로그가 0 이고, 런타임 검사기 이슈가 0 이다(STAB-01, STAB-05, PERF-04).
|
|
366
|
+
3a. 변이 게이트가 초록이다: 지난 출시 뒤 바뀐 로직 줄에 살아남은 변이와 테스트가 닿지 않는 변이가 0 이다(SUITE-08). 테스트가 초록이어도 아무것도 확인하지 않는 줄은 여기서 드러난다.
|
|
367
|
+
4. 새 설치, N-1(저장 형식이 바뀌면 N-2) 위 업그레이드, App Store N-1 → TestFlight N, 삭제 후 재설치가 심어 둔 데이터를 지키며 통과하고, 계약 식별자가 바뀌지 않았다(BUILD-03~05, BUILD-09).
|
|
368
|
+
5. 열린 S1·S2 버그가 0 이고, 함께 나가는 S3·S4 는 모두 출시 메모에 있다(DEF-01).
|
|
369
|
+
6. 접근성 감사가 모든 화면 상태에서 설명 안 된 이슈 0 이고, Dynamic Type xs~AX5, ko·en, 시각 설정 훑기를 잘림·겹침 없이 검토했다(A11Y-01~04, L10N-01).
|
|
370
|
+
7. 성능이 예산 안이다: 실행 중앙값이 지난 출시 대비 15% 안, hitch ratio 10 ms/s 미만, 메모리 증가가 여유 안, 앱 소유 누수 없음(PERF-01~03).
|
|
371
|
+
8. CloudKit 앱은 프로덕션 스키마에 빌드가 쓰는 타입·필드·인덱스가 다 있고, 대기 중 배포가 출시본을 깨지 않고, GRANT 가 맞고, TestFlight 빌드의 프로덕션 왕복이 통과했다(CK-01~06).
|
|
372
|
+
9. 피드, 백엔드, AASA, 링크, API 키가 지난 1시간 안에 초록이고 심사 동안 버틸 것으로 보이며, 깔린 모든 앱 버전이 지금 페이로드를 디코딩한다(FEED-01, FEED-05, ENTRY-02, SUB-02).
|
|
373
|
+
10. `release_checks.py` 와 제출 정적 검사가 초록이다: 플레이스홀더 없음, 번들별 개인정보 매니페스트, SDK 매니페스트, 목적 문자열, IPv4 리터럴 없음, 백그라운드 모드, 수출 규정, processingState VALID, 이 빌드의 ITMS 메일 없음(SUB-01~04, SUB-16~18, NET-05, PERM-04).
|
|
374
|
+
11. 개인정보 라벨, 처리방침, 데이터 흐름 목록이 서로 맞고, 목록이 바뀌었으면 주인이 ASC App Privacy 답을 확인했고, 외부 약관 게이트(SUB-26, SUB-27)가 BLOCK 이 아니다(SUB-05, SUB-06, CK-07).
|
|
375
|
+
12. 심사 메모가 4,000 UTF-8 바이트 이하이고 해외 심사자 경로를 덮으며, What's New 가 모든 변경을 덮고, 스크린샷이 이 빌드와 맞고, 메타데이터 lint 가 통과하고, WeatherKit 과 외부 출처 표시가 모든 화면에 있고, 연령 등급 답이 맞다(SUB-08~15).
|
|
376
|
+
13. TestFlight 소크(기능 릴리스는 실사용 2일 이상)에서 crashCount 가 0 이고 후보에 열린 피드백이 없으며, 현장 체크리스트와 주인의 탐색 세션이 끝났거나 '테스트 안 한 것'에 있다(FIELD-01~03, EXP-03).
|
|
377
|
+
14. 수동 출시와 단계적 출시가 설정됐고, 30일 결정 지점이 있는 멈춤 규칙, 배포 감시 잡, fix-forward 런북이 준비됐다(REL-01, REL-02, REL-06).
|
|
378
|
+
15. '테스트 안 한 것' 목록과 빨강·건너뛴 행이 모두 붙어 있고, 주인이 남은 위험마다 자기 말로 수락했다(PLAN-04).
|
|
379
|
+
|
|
380
|
+
## 사람만 할 수 있는 것
|
|
381
|
+
|
|
382
|
+
- **한 번만 하는 설정.** 테스트 아이폰의 Developer Mode 켜기, 한글 2벌식 키보드 추가, 위젯을 홈·잠금 화면에 놓기, Hang Detection 켜기, 테스트 전용 Apple ID 로그인(TestFlight, App Store, iCloud)을 한다. 두 번째 iCloud 계정이나 기기, 스크린 타임 제한 상태, Mac 의 NAT64 Wi-Fi, `rvictl`·`tcpdump`·`swcutil` sudoers 항목, 원격 푸시 앱의 APNs .p8 도 준비한다.
|
|
383
|
+
- **릴리스마다 30초.** 선을 뺀 채 홈 화면에서 후보나 TestFlight 설치본을 한 번 콜드 실행한다. devicectl 실행이 watchdog 을 거는지 확인되지 않았기 때문이다.
|
|
384
|
+
- **릴리스마다 전화와 잠금.** 에이전트가 앱을 투표 중에 붙잡고 있는 동안 테스트 아이폰에 전화를 걸고 잠갔다 푼다(INT-05).
|
|
385
|
+
- **릴리스마다 10~20분 탐색.** 에이전트의 차터와 접촉 시트로 TestFlight 빌드를 써 본다(EXP-03). 메이저 릴리스는 VoiceOver 과업 흐름 하나를 들어 본다(A11Y-03).
|
|
386
|
+
- **TestFlight 1~3일 현장 사용.** 산책 중 Wi-Fi→셀룰러 전환, 바깥 가독성, 실제 푸시와 아침 브리핑 시각, 집중 모드 뒤 알림 몰림, 메시지에서 누른 유니버설 링크를 본다(FIELD-01, FIELD-03).
|
|
387
|
+
- **되돌릴 수 없는 버튼.** 에이전트의 diff 를 읽고 CloudKit Console 의 Deploy Schema Changes 를 누르거나 에이전트에게 명시적 OK 를 준다(CK-01). 멈춤 규칙은 한 번 승인하고, Submit, 출시 요청, minimum_version 올리기, 긴급 심사 제출에는 매번 go 를 준다.
|
|
388
|
+
- **판단.** 데이터 흐름 diff 가 생기면 ASC App Privacy 답을 고르고, Accessibility Nutrition Label, 연령 등급 변경, DSA·약관, 위치정보법·수출 암호·외부 약관 같은 법적 판단을 내린다. 위험 목록의 새 능력 영향 점수, 결정 표의 비어 있는 기대값, '테스트 안 한 것'의 남은 위험도 그가 정한다.
|
|
389
|
+
- **기능이나 저장 형식이 바뀔 때만.** Apple Intelligence 를 쓸 수 있는 폰(예: 주인의 폰)에서 가능 경로를 돌리고, 키체인이나 저장 형식이 바뀌면 다른 기기 백업에서 복원해 본다.
|
|
390
|
+
|
|
391
|
+
## 출처
|
|
392
|
+
|
|
393
|
+
날짜는 문서의 게시·수정일이고, 없으면 확인일(2026-10-28)이다.
|
|
394
|
+
|
|
395
|
+
- App Review Guidelines, https://developer.apple.com/app-store/review/guidelines/ (2026-10-16)
|
|
396
|
+
- App Review, https://developer.apple.com/distribute/app-review/ (2026-10-28 확인)
|
|
397
|
+
- Complete review, https://developer.apple.com/help/app-review/before-submitting-for-review/complete-review (2026-10-05)
|
|
398
|
+
- Test compatible devices, https://developer.apple.com/help/app-review/before-submitting-for-review/test-compatible-devices (2026-10-05)
|
|
399
|
+
- 5.1.1(ii) Purpose strings, https://developer.apple.com/help/app-review/guideline-reference/5-1-1-purpose-strings (2026-10-05)
|
|
400
|
+
- 4.1 Prevent copycat rejection, https://developer.apple.com/help/app-review/guideline-reference/4-1-prevent-copycat-rejection (2026-10-28 확인)
|
|
401
|
+
- Platform version information, https://developer.apple.com/help/app-store-connect/reference/app-information/platform-version-information (2026-10-06)
|
|
402
|
+
- Screenshot specifications, https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications (2026-10-15)
|
|
403
|
+
- Age ratings values and definitions, https://developer.apple.com/help/app-store-connect/reference/app-information/age-ratings-values-and-definitions (2026-10-23)
|
|
404
|
+
- App privacy details, https://developer.apple.com/app-store/app-privacy-details/ (2026-10-08)
|
|
405
|
+
- Describing use of required reason API, https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api (2026-10-28 확인)
|
|
406
|
+
- Third-party SDK requirements, https://developer.apple.com/support/third-party-SDK-requirements/ (2026-10-23)
|
|
407
|
+
- Upcoming requirements, https://developer.apple.com/news/upcoming-requirements/ (2026-10-28 확인)
|
|
408
|
+
- Release a version update in phases, https://developer.apple.com/help/app-store-connect/update-your-app/release-a-version-update-in-phases/ (2026-10-28 확인)
|
|
409
|
+
- TestFlight overview, https://developer.apple.com/help/app-store-connect/test-a-beta-version/testflight-overview (2026-10-28 확인)
|
|
410
|
+
- EU DSA trader requirements, https://developer.apple.com/help/app-store-connect/manage-compliance-information/manage-european-union-digital-services-act-trader-requirements/ (2026-10-28 확인)
|
|
411
|
+
- Age assurance, https://developer.apple.com/support/age-assurance/ (2026-10-28 확인), 공지 https://developer.apple.com/news/?id=sg176nne (2026-06-03)
|
|
412
|
+
- ASC API diagnosticSignatures, https://developer.apple.com/documentation/appstoreconnectapi/get-v1-builds-_id_-diagnosticsignatures (2026-10-28 확인)
|
|
413
|
+
- Testing a release build, https://developer.apple.com/documentation/xcode/testing-a-release-build (2026-10-28 확인)
|
|
414
|
+
- Organizing tests into test plans, https://developer.apple.com/documentation/xcode/organizing-tests-to-improve-feedback (2026-10-28 확인)
|
|
415
|
+
- Performing accessibility audits, https://developer.apple.com/documentation/accessibility/performing-accessibility-audits-for-your-app (2026-10-28 확인)
|
|
416
|
+
- Voice Control evaluation criteria, https://developer.apple.com/help/app-store-connect/manage-app-accessibility/voice-control-evaluation-criteria (2026-10-28 확인)
|
|
417
|
+
- Addressing watchdog terminations, https://developer.apple.com/documentation/xcode/addressing-watchdog-terminations, SIGKILL, https://developer.apple.com/documentation/xcode/sigkill (2026-10-28 확인)
|
|
418
|
+
- Keeping a widget up to date, https://developer.apple.com/documentation/widgetkit/keeping-a-widget-up-to-date, accented rendering, https://developer.apple.com/documentation/widgetkit/optimizing-your-widget-for-accented-rendering-mode-and-liquid-glass (2026-10-28 확인)
|
|
419
|
+
- TN3155 Debugging universal links, https://developer.apple.com/documentation/technotes/tn3155-debugging-universal-links (2025-05-15)
|
|
420
|
+
- TN3187 Migrating to the UIKit scene-based life cycle, https://developer.apple.com/documentation/technotes/tn3187-migrating-to-the-uikit-scene-based-life-cycle (2026-10-28 확인)
|
|
421
|
+
- CloudKit text-based schema, https://developer.apple.com/documentation/cloudkit/integrating-a-text-based-schema-into-your-workflow, Using cktool, https://developer.apple.com/icloud/ck-tool/ (2026-10-28 확인)
|
|
422
|
+
- WeatherKit get started, https://developer.apple.com/weatherkit/get-started/ (2026-10-28 확인)
|
|
423
|
+
- HIG Privacy, https://developer.apple.com/design/human-interface-guidelines/privacy, HIG Maps, https://developer.apple.com/design/human-interface-guidelines/maps (2026-10-28 확인)
|
|
424
|
+
- 로컬 도구 도움말: `xcodebuild -help`, `man 5 xcodebuild.xctestrun`, devicectl 651.13.5, `cktool --help`, `XCUIVoiceOverService.h` (Xcode 27.1, 2026-10-28 확인)
|
|
425
|
+
- Google Maps Platform Service Specific Terms, https://cloud.google.com/maps-platform/terms/maps-service-terms (2026-10-15), Places policies, https://developers.google.com/maps/documentation/places/web-service/policies (2026-10-23)
|
|
426
|
+
- Kakao DevTalk Local API 저장 문의, https://devtalk.kakao.com/t/local-api/151263 (2026-08-20)
|
|
427
|
+
- 위치정보법 제9조의2, https://casenote.kr/법령/위치정보의_보호_및_이용_등에_관한_법률/제9조의2 (시행 2025-10-01), 개인정보 보호법 제30조, https://casenote.kr/법령/개인정보_보호법/제30조 (시행 2023-09-15)
|
|
428
|
+
- OWASP MASVS v2.1.0, https://mas.owasp.org/MASVS/ (2024-01-18)
|
|
429
|
+
- ISTQB CTFL v4.0.1, https://www.istqb.org/wp-content/uploads/2024/11/ISTQB_CTFL_Syllabus_v4.0.1.pdf (2024-09-15), CT-MAT, https://www.istqb.org/wp-content/uploads/2024/11/ISTQB-CT-MAT_Syllabus_v1.0_2019.pdf (2019-05-03)
|
|
430
|
+
- J. Bach, HTSM, https://developsense.com/htsm (2024-02-08), M. Bolton, FEW HICCUPPS, https://developsense.com/blog/2012/07/few-hiccupps (2012-07-23)
|
|
431
|
+
- J. Kohl, I SLICED UP FUN, http://www.kohl.ca/articles/ISLICEDUPFUN.pdf (2010-10), Session-Based Test Management, https://www.satisfice.com/download/session-based-test-management (2000)
|
|
432
|
+
- Test Heuristics Cheat Sheet, https://www.ministryoftesting.com/articles/test-heuristics-cheat-sheet (2022-03-31)
|
|
433
|
+
- Nielsen, 10 usability heuristics, https://www.nngroup.com/articles/ten-usability-heuristics/ (2024-01-30 검토)
|