# Mobile Release Strategy

| 항목 | 값 |
|---|---|
| Status | Active |
| Owner | APP/release: 주우철 · backend/provider counterpart: 정정일 |
| Last verified | 2026-07-20 KST |
| Supersedes | APP production 미구현 기능의 개별 TODO 목록. 현재 상태 요약은 [Current Progress](../70-progress/CURRENT.md)가 소유한다. |

`apps/mobile`의 EAS build, channel, OTA, store readiness 운영 기준이다. 현재 ZERRO APP은 Android EAS project와 cloud build profile을 사용하며, mock-backend 검증을 위한 development OTA만 활성화한다. production OTA와 store submit은 여전히 사용하지 않는다.

이 문서의 `Decision Gates Before Implementation Issues`가 APP production gate의 단일 owner다. [Decisions](../00-governance/DECISIONS.md)는 채택된 기술 기준선을, [API Contract](../20-contracts/API_CONTRACT.md)와 provider contract는 상세 shape를 소유하며 production gate를 이 문서에 다시 복제하지 않는다.

## Current State

| Topic | Decision |
|---|---|
| EAS project | `@bootalk/zerro` |
| Dashboard | `https://expo.dev/accounts/bootalk/projects/zerro` |
| Android package | `com.bootalk_appdev.zerro` |
| Build platform | Android only |
| Manual build workflow | `apps/mobile/.eas/workflows/android-build.yml` |
| iOS | bundle id 결정 전까지 build script 없음 |
| OTA | development channel에서 mock-backend 검증용 활성화. production channel은 비활성 |
| Store submit | credential과 승인 PR 전까지 비활성 |

Bootalk 앱의 EAS `projectId`, `updates.url`, store credential은 ZERRO에서 재사용하지 않는다.

## Production Function Closure Priority

아래 표는 현재 Android APP을 production 운영 가능한 상태로 올리기 위한 미구현 기능의 정본 backlog다. 순위는 업무 중단·데이터 손실·권한 사고 위험, 선행 의존성, 사용자 fallback 존재 여부를 함께 고려한 구현 순서다. `P0`는 실제 운영 전 완료해야 하고, `P1`은 제한 운영에 fallback이 있어도 정식 운영 전 닫아야 한다. `R0`는 기능 순위와 별개로 병렬 관리하는 출시 차단 gate다.

| 순위 | 등급 | 미구현 기능 | 현재 경계 | Production 완료 조건 |
|---:|---|---|---|---|
| 1 | P0 | 증빙·채팅 media production 저장소와 secure read | backend는 S3-compatible conditional finalize, signed read, RBAC, retention/deletion과 재시작 persistence를 enforce한다. APP chat attachment, driver loading/unloading·weighing, processor weighing transport는 secure session과 stable `fileId` 경계에 연결됐다. Driver loading/unloading은 각각 Android camera-only 사진 2장, driver weighing은 계근표 1장을 surface-specific app-private JPEG로 사용하고, loading 서명은 실제 canvas PNG file URI를 사용한다. loading/weighing은 session-local이고 unloading만 owner-scoped saved draft recovery를 제공한다. 2026-07-18 emulator loading positive/denial, unloading denial, driver weighing positive/denied source-bound flow가 canonical receipt에서 모두 `status=ok`를 기록했다. Driver weighing positive는 OCR 등록과 `7,000kg` 완료를, denied는 명시적 권한 안내·`0/1`·disabled CTA를 확인했다. #1049 shared auth/runtime API-base allowlist와 active driver role의 `/session/me?role=driver` 검증은 production gate다. | production bucket/credential, public access block, `staging/` lifecycle, provider smoke와 실패 UX, physical-device/OEM evidence를 `VERIFY`한다. |
| 2 | P0 | 반입확인서·리포트 실제 생성, 보관, 저장·공유 | backend가 `intake_pending`/`treatment_recorded` 전이에서 `zerro-pdf-v1` versioned PDF를 생성하고 private object, snapshot, SHA-256, renderer version을 보존한다. APP은 local/dev capability로 inline/download를 제공하고, Android 저장은 기존 `expo-file-system` SAF adapter로 사용자가 저장 위치를 선택한다. 만료 capability는 다시 준비하는 상태로 안내하고 공유 권한이 없으면 disabled reason을 표시한다. | production storage credential/bucket, 최종 한글 서식·글꼴, transaction 이후 발급, Android 실제 저장·만료 evidence와 공유 disabled evidence를 검증한다. `downloadAllowed`/`shareAllowed`는 backend capability와 감사 정책이 최종 권한을 소유한다. |
| 3 | P0 | 운전자 증빙·OCR durable offline queue | #1018에서 `expo-sqlite` queue metadata, app-private file store, handover별 순서 보존·최대 2개 병렬 drain coordinator, idempotency/quota/retention 정책 foundation을 추가했다. retryable backend failure의 loading/unloading/weighing payload를 실제 backend proof adapter에 넣고, backend client의 app-start·foreground recovery composition까지 연결했다. 상·하차·계근 사진은 Android camera-only app-private artifact를 사용하고 loading 서명은 실제 canvas PNG로 전달한다. Loading/weighing capture는 실패한 mounted submit에서 source를 유지하고, direct adapter consume 또는 durable queue copy 뒤 source를 정리한다. 하차 saved draft만 앱 재진입·ready board·actionability gate 뒤 복구하며 loading/weighing draft는 session-local이다. | network recovery listener, process-death·중복 replay·충돌 UX와 physical-device/OEM evidence를 실제 기기에서 `VERIFY`한다. queue 성공·실패·quota cleanup과 production credential/provider smoke도 별도 확인한다. |
| 4 | P0 | Google Cloud Vision production OCR와 수동 보정 | Google Vision 방향과 smoke는 확인했지만 runtime service는 deterministic fake result를 사용한다. | production adapter/credential, 합성 또는 privacy review를 거친 계근표 benchmark, confidence threshold, `needs_review`, 수동 수정, provider 장애 fallback을 고정한다. |
| 5 | P1 | Expo Push backend 등록·전송과 알림 tap 복원 | APP permission UI·token payload/client와 local/dev backend token 등록·폐기 저장이 구현됐다. delivery/receipt/tap runtime은 남아 있다. | 사용자·기기별 token 등록/회수, delivery retry/receipt, 권한 거부, logout 정리, 알림 tap의 session·role·detail route 복원을 검증한다. |
| 6 | P1 | production foreground realtime 운영 경계 | APP root STOMP 주입과 REST fallback은 local/dev에서 동작하지만 production broker와 replay 기준이 없다. | broker topology, 인증 갱신, reconnect/replay/idempotency, REST reconciliation, 장애·지연 관측성과 SLO를 검증한다. |
| 7 | P1 | 전화·네이버지도·고객센터 실제 external handoff | URL allowlist와 runtime value resolver까지만 있으며 사용자 action이 실제 외부 앱을 열지 않는다. | backend/config runtime source, adapter 단일 진입점, `Linking` 성공·미설치·취소·오프라인 분기, 최소 audit event를 검증한다. |
| 8 | P1 | APP 계정 lifecycle와 세부 RBAC | JWT login/session/role 선택은 연결됐지만 APP 가입·초대·비밀번호 복구, `duties[]`, multi-company active context가 없다. | 운영 계정 생성 정책을 정하고 필요한 APP surface, 승인 대기/거절/퇴사, active company 전환, duty 기반 route/action 권한을 contract와 E2E로 고정한다. |
| 9 | P1 | Olbaro 실제 계정 운영 검증 | backend protocol mapping과 queue/worker는 있으나 테스트 계정과 HALF MODE 실측이 끝나지 않았다. | 테스트 계정으로 role별 T200/T300/T400, HALF MODE, 재시도·중복·ledger·credential rotation을 검증하고 운영 runbook을 확정한다. |
| 10 | R0 | Android release candidate·store·운영 관측성 활성화 | EAS profile과 development mock OTA 설정은 있으나 첫 RC build, production OTA, store submit, release observability가 비활성이다. | `releases` APK와 production AAB, Google Play internal track, signing/versioning, Sentry release·source map, rollback 기준을 검증한다. production OTA는 별도 운영 승인과 runtimeVersion 검증 후 활성화한다. |

### Priority Boundary

- 순위 1의 production storage/read 경계가 순위 2 문서, 순위 3 offline upload, 순위 4 OCR의 공통 선행 조건이다.
- Cycle 1 운전자 capture의 auth 경계는 production auth 진입에서 `mock-direct`로 암묵적으로 fallback하지 않는 것이다. pending camera-result recovery는 `login-entry` 인증 상태의 운전자가 schedule/unloading proof surface를 실제로 열고 backend today board가 ready일 때만 local owner scope로 시도하며, APP-root/role-route auto-open 또는 active driver notification/deep-link target 선점을 하지 않는다. authenticated `login-entry` 하차 draft는 app-private persisted storage에 남지만 이 recovery gate를 통과해야 다시 연다. 반대로 loading draft와 `e2c41743` 이후 `mock-direct` draft는 capture instance-local 메모리만 사용하는 non-durable 상태라서 이후 새 session은 이전 camera artifact를 restore/view할 수 없다. 이 현재 경계는 active driver role을 `/session/me?role=driver`로 검증하지 않는다. #1049는 shared auth/runtime API-base allowlist와 active-role verification이라는 두 개의 서로 다른 P0 production gate이며, strict driver runtime base enforcement는 system-wide enforcement가 아닌 별도 driver 전용 경계다. Loading UI는 fixture URI를 만들지 않고 Android camera-only artifact 2장만 받으며, unloading `origin=fixture`는 runtime/base와 무관하게 운영 upload file/network I/O 전에 거부된다. `driver-unloading-camera-denied`는 별도의 `mock-direct` permission-edge flow로 guarded backend subflows보다 먼저 실행되며 exact base/runtime preflight의 선행 flow가 아니다. 이는 P0 physical-device `VERIFY` gate를 대신하지 않는다.
- Loading의 session-local source는 driver role shell 시작 시 `loading-*.jpg` 전용 process-wide 1회 sweep으로 process-death/account-switch 잔여물을 정리하고, 후속 manager는 같은 promise를 재사용하며 실패한 sweep만 재시도한다. Sweep 완료 전에는 camera를 열지 않는다. Submit 중 surface가 unmount되면 direct upload/queue copy가 photo 2장과 signature source ownership을 확정할 때까지 삭제를 보류한다. 실패가 mounted surface로 돌아오면 retry를 위해 유지하고, unmount 뒤 실패가 확정되면 정리한다. 여기서 `camera` provenance는 standard-client flow invariant이며 server/device attestation이나 보안 등급의 촬영 진위 주장이 아니다.
- 순위 10은 마지막에 시작한다는 뜻이 아니다. credential 없이 가능한 read-only 준비와 RC build는 병렬 진행하되, production submit은 P0 기능과 승인 gate가 닫힌 뒤 수행한다.
- background/continuous location, direct FCM/APNs, native Naver Map SDK, iOS build, OAuth/SMS/SSO는 현재 Android APP launch blocker가 아니다. 제품 범위가 바뀌면 [Product Scope](../10-product/PRODUCT_SCOPE.md)와 관련 contract를 먼저 갱신한다.
- backend dev deploy issue `#666`과 Web/PC 구현은 이 APP backlog의 소유 범위가 아니다.
- 완료 선언은 코드 존재가 아니라 production-mode adapter/backend proof, 사용자-visible 실패 분기, 관련 Maestro 또는 실제 기기 evidence까지 확보한 경우에만 가능하다.

## Decision Gates Before Implementation Issues

아래 gate는 backlog 자체가 아니라 **production 구현 issue를 어떤 종류로 열 수 있는지** 결정한다. 원천 근거는 [`~/Bootalk/_docs/ZERRO` validity index](../60-requirements/SOURCE_PROVENANCE.md#106-local-mirror-validity-index-2026-07-10)와 current contract를 함께 사용한다.

| 상태 | issue 발행 규칙 |
|---|---|
| `DECIDE` | 제품/운영 책임자의 답을 decision issue에 먼저 기록한다. feature implementation issue를 열지 않는다. |
| `VERIFY` | 외부 계정·표본·실측이 필요하다. spike/evidence issue만 열고 production 완료로 닫지 않는다. |
| `READY` | 권장 기본값과 완료 조건이 충분하다. implementation issue를 바로 열 수 있다. |

| Gate | 상태 | 원천과 현재 경계 | 구현 전 확정할 질문 | 권장 기본값 | 차단하는 항목 |
|---|---|---|---|---|---|
| `DG-FILE-01` file retention/read | `READY` | [#1009 owner decision](https://github.com/uitiorg/zerro/issues/1009#issuecomment-4967609073)으로 policy를 확정했고 #1010 backend slice가 local/Testcontainers 및 S3-compatible adapter 경계의 signed read/RBAC·retention·legal-hold enforcement를 구현했다. Source ETag conditional finalize, final rehash, 48시간 cleanup reconciliation, retention anchor fail-closed, 15분 purge claim lease까지 backend core에 포함한다. Production bucket/credential write는 별도 `VERIFY` gate다. | 정책과 backend core enforcement는 닫혔다. production bucket/credential, public access block, `staging/` lifecycle, provider smoke, malware scan, 회사 종료 30일 export, 운영·컴플라이언스 승인 wiring과 launch compliance evidence를 검증한다. | private object + 참여 company RBAC, read TTL 10분, soft delete 후 retention 만료 purge, 최소 legal-hold state/metadata 보존 및 held file purge fail-closed, hold set/release는 내부 ops/compliance application boundary, company_admin 요청 → ZERRO 운영·컴플라이언스 승인 → backend retention job, `chat_attachment` 1년/`operational_evidence` 3년/`regulatory_document` 5년, 탈퇴·회사 종료 lifecycle, download 허용, chat/evidence OS share 차단·regulatory share는 future company_admin capability + audit | production storage, secure read, chat media, 문서 보관 |
| `DG-DOC-01` document semantics | `READY` | [#1009 owner decision](https://github.com/uitiorg/zerro/issues/1009#issuecomment-4967609073)으로 문서 lifecycle/version 기준을 확정했다. G010에서 `zerro-pdf-v1` renderer, private generated object, immutable snapshot/hash/version persistence와 local/dev read capability를 구현했고 #1026에서 Android SAF 저장 adapter와 실패·취소 정리를 연결했다. | 정책 결정은 닫혔다. production bucket/credential, 최종 한글 서식·글꼴, correction history access, transaction 이후 발급(outbox/after-commit), 운영·컴플라이언스 승인 wiring과 Android 실제 저장·만료 evidence를 검증한다. 공유는 v1 정책상 disabled 상태를 유지하고 그 evidence를 검증한다. | `dispatch_confirmed`에서 반입의뢰, weighing complete로 `intake_pending` 진입 시 반입확인, 최종 `treatment_recorded`에서 처리확인; immutable snapshot/append-only version, 정정은 새 version + reason, 일반 사용자는 latest, 참여 company_admin/ops는 history, v1 서명·직인 제외, driver safety signature는 operational evidence | production PDF template/font, storage, Android save/expiry, Olbaro 문서 대조 |
| `DG-AUTH-01` account onboarding/RBAC | `READY` | 2026-07-16 owner approval로 v1 account당 one company, 최초 구성원 company admin, driver 초대, backend-owned duties 방향을 확정했다. 구현·권한 evidence는 남아 있다. | 정책 결정은 닫혔다. duties claim/권한과 승인·초대·복구 surface를 구현·검증한다. | v1은 account당 one company. 최초 구성원은 운영 승인 후 company admin이 되고, driver는 dispatcher/company admin 초대로 가입하며 duties는 backend-owned claim/permission으로 관리 | APP 가입·초대·복구, 관리자 승인, duty route/action guard |
| `DG-OFFLINE-01` durable proof queue | `READY` | [#1009 owner decision](https://github.com/uitiorg/zerro/issues/1009#issuecomment-4967609073)으로 queue 대상·한도·복구·충돌 기준을 확정했다. #1018에서 `expo-sqlite` metadata, app-private file store, 실제 파일 크기 기반 quota preflight, owner-scoped atomic enqueue와 connection-level serialization, legacy row 격리·schema rebuild transaction·orphan reconciliation, retention policy, handover별 ordered drain과 실제 loading/unloading/weighing backend proof handler를 연결했고 app-start·foreground recovery composition을 추가했다. | queue policy와 mobile code boundary는 닫혔다. network recovery listener, process-death/replay/conflict device evidence, native compatibility, production provider smoke를 별도 검증한다. logout/account switch의 sync/discard/cancel 선택은 owner decision으로 유지한다. | driver loading/unloading/weighing proof만 queue; `expo-sqlite` metadata + app-private file; 7일/100 jobs/500 MiB; handover별 ordered drain·최대 2개 병렬; idempotency·foreground retry·process-death restore; logout/account switch sync/discard/cancel; free/queue 1 GiB·400 MiB warning, 500 MiB hard stop; backend 접수 전 cancel·이후 correction; 409 affected job manual review, duplicate success 완료 | 현장 무신호 운영, process-death/replay/conflict device evidence, production upload/OCR |
| `DG-OFFLINE-EXIT-01` logout/account switch | `READY` | 2026-07-16 owner approval로 미전송 queue 작업의 명시적 선택 정책을 확정했다. | 정책 결정은 닫혔다. sync and exit·keep pending·cancel의 실제 UI, queue ownership과 recovery evidence를 구현·검증한다. | pending 작업이 있으면 silent discard하지 않고 세 가지 선택을 명시한다. | logout/account switch UI, queue ownership transfer, support recovery policy |
| `DG-OCR-01` acceptance benchmark | `VERIFY` | [#1009 owner decision](https://github.com/uitiorg/zerro/issues/1009#issuecomment-4967609073)으로 acceptance 값과 backend `OcrAcceptancePolicy` 경계는 확정·구현했지만 실제 대표 production benchmark는 아직 VERIFY다. 현재 runtime은 deterministic fake OCR이며 production GCV adapter/credential은 없다. | 결정은 닫혔다. 최소 표본 benchmark, production adapter/credential, redacted evidence와 실제 `needs_review`/수동 보정 분기를 검증한다. benchmark 통과를 문서만으로 주장하지 않는다. | 50장 이상·5개 이상 양식·양식당 10장 이상·challenging 20% 이상; required gross/tare/vehicle/measuredAt; net backend 계산·deduction 사용자 입력; gross/tare exact 97% 이상, vehicle/time 95% 이상, required recall 95% 이상; required confidence <0.90 또는 invalid/missing/mismatch는 `needs_review`; low confidence는 수정 허용·영구 차단 금지; driver/processor 각 surface 최종 확인·자동 domain mutation 금지 | production GCV adapter, representative benchmark, 운영 품질 기준 |
| `DG-PUSH-01` delivery/privacy/tap | `READY` | 2026-07-16 owner approval로 [Push Notification Matrix](../20-contracts/PUSH_NOTIFICATION_MATRIX.md)의 Google Sheet 역할 matrix, 개인정보 최소화, self suppression, tap fallback, driver chat 제외를 확정했고, #1023에서 local/dev device token 등록·폐기·upsert 저장과 `notification:register` RBAC를 구현·검증했다. | 정책 결정은 닫혔다. delivery receipt/retry, production credential, production registration lifecycle, tap session/role/detail 복원 evidence를 구현·검증한다. | Expo Push primary, current 12-row matrix 유지, `상차 시작` 별도 발행은 상태 모델 결정 전 gap으로 유지, actor suppression, driver chat deep link 금지, lock screen에는 업체·폐기물·전화·인계번호 원문을 넣지 않음, tap은 session 복구→role 확인→detail→해당 role 알림함 fallback | push delivery, 알림 tap, 운영 delivery analytics |
| `DG-REALTIME-01` production topology | `READY` | 2026-07-16 owner approval로 초기 제한 운영은 single-instance Spring simple broker와 DB/REST 정본을 사용하도록 확정했다. | 정책 결정은 닫혔다. single replica 운영 evidence와 2 replica 승격 시 external broker/replay/idempotency/SLA를 별도 검증한다. | 초기 제한 운영은 **backend single replica를 배포 정책으로 강제**하고 메시지 정본은 DB/REST로 유지한다. 2 replica 이상 또는 무중단 요구 전 external broker/durable replay로 승격한다 | production foreground realtime, horizontal scaling |
| `DG-HANDOFF-01` phone/map/support | `READY` | 2026-07-16 owner approval로 typed backend runtime value, 단일 APP adapter, audit redaction과 네이버지도 방향을 확정했다. runtime source와 opener/evidence는 없다. | 정책 결정은 닫혔다. 연락처·좌표·주소·고객센터 URL의 owner API와 opener/fallback/audit evidence를 구현·검증한다. | backend detail/config가 typed runtime value를 제공하고 APP은 단일 adapter로 연다. audit에는 actor/role/handover/action/result만 남기고 전화번호·주소·URL 원문은 남기지 않는다 | 실제 dialer, Naver Map, 고객센터 handoff |
| `DG-OLBARO-01` real operation | `VERIFY` | spec/protocol mapping과 local queue/worker는 있으나 테스트 계정과 HALF MODE 실측이 없다. | 테스트 계정 owner, credential rotation, HALF MODE와 예약/확정 mapping, T200/T300/T400 재시도·중복·수동 복구, 운영 담당자 | 테스트 계정에서 role별 happy/error/replay ledger를 먼저 확보하고, HALF MODE mapping을 공단 확인 또는 실측 전 단정하지 않는다 | 실제 Olbaro 제출, 신고 완료 production claim |
| `DG-RELEASE-01` Android operation | `READY` | 2026-07-16 owner approval로 Android Internal Track first, 초기 OTA off, immutable AAB/Sentry release/staged rollout 방향을 확정했다. 실제 owner·store write·RC evidence는 없다. | 정책 결정은 닫혔다. Play Console·signing owner, application id ownership, privacy/support URL, version 승인자, rollback/hotfix 책임, crash-free 기준과 RC를 운영 승인·검증한다. | Android internal track first, initial production은 OTA off, immutable AAB + Sentry release/source map, staged rollout. 첫 안정 release 이후 runtimeVersion/rollback이 검증될 때 OTA를 별도 활성화 | RC/store submit, 운영 관측성, release 승인 |

### Already Resolved, Do Not Reopen

- Driver lifecycle order: `confirm → loading-start → unloading-complete → weighing`.
- Emitter cancellation: 배차 전 상태만 허용한다.
- No available dispatch date: 자동 취소하지 않고 대기한다. 배차담당자의 명시적 action만 `no_available_date` 취소를 만든다.
- Emitter detail: domain status와 5-node display stepper를 분리한다.
- Driver bottom navigation: `홈/오늘 일정/배차표`; 운행·정비·주유 기록은 `배차표` surface 안에서 제공한다.
- Chat membership: emitter/dispatcher/processor 3자이며 driver는 lifecycle notification/assignment 대상이지 chat viewer가 아니다.

### Issue Template Rule

1. 모든 production implementation issue는 관련 `DG-*` ID와 gate 상태를 본문에 적는다.
2. `DECIDE`는 decision record URL과 확정값이 없으면 implementation 시작을 금지한다.
3. `VERIFY`는 표본·외부 계정·실측 evidence를 산출물로 삼고, provider가 동작한다는 일반화된 완료 선언을 금지한다.
4. `READY` issue도 current contract, user-visible failure state, static/backend/device evidence를 DoD에 포함한다.
5. gate가 닫히면 상세 결정은 [기술 결정 등록부](../00-governance/DECISIONS.md)와 owner contract에 반영하고 이 표에는 날짜와 링크를 남긴다.

## Native Foundation

Native dependency는 EAS build 비용을 줄이기 위해 foundation batch로 관리한다. native layer 변경은 runtime boundary 변경으로 본다.

| Dependency | Current use |
|---|---|
| `expo-location` | foreground 운전자 위치. background location과 continuous `gps_track`은 `docs/20-contracts/LOCATION_TRACKING_CONTRACT.md` 기준 보류 |
| `expo-notifications` | Expo push 1차 provider client 준비. Expo project credential과 direct FCM/APNs credential은 보류 |
| `expo-image-picker`, `expo-document-picker`, `expo-file-system` | 증빙/문서 첨부, upload staging, durable queue app-private file |
| `expo-sqlite` | 운전자 증빙 queue metadata와 ordered drain foundation |
| `expo-secure-store` | token/session/local sensitive flag |
| `expo-network` | offline/failure UX |
| `expo-haptics` | 핵심 CTA feedback |
| `@shopify/flash-list` | chat, dispatch, inbound 대용량 list |
| `expo-updates` | development channel mock-backend OTA. production channel은 보류 |

보류: background task packages, `react-native-maps`.

## Build Profiles

| Profile | Channel | EAS env | Artifact | Use |
|---|---|---|---|---|
| `development` | `development` | `development` | APK dev client | local/internal dev |
| `releases` | `releases` | `preview` | APK internal distribution | QA/RC/stakeholder preview |
| `production` | `production` | `production` | AAB | store candidate |

`releases`는 production이 아니라 internal verification lane이다.

## Build Trigger Policy

EAS build는 every-PR gate가 아니다. PR 기본 gate는 static/local verification이고, cloud build는 release candidate 또는 native boundary 변경처럼 artifact가 필요한 시점에 수동 실행한다.

| Gate | When |
|---|---|
| `pnpm ci:mobile-static` | PR/CI static gate |
| `pnpm --filter @zerro/mobile exec expo install --check` | dependency compatibility |
| `cd apps/mobile && pnpm dlx expo-doctor@latest` | native dependency/config PR 또는 release 준비 |
| `pnpm eas:mobile:android-build -- -F profile=releases -F message="ZERRO Android RC"` | manual Android cloud build |
| `pnpm --filter @zerro/mobile eas:releases:android` | release candidate APK |
| `pnpm --filter @zerro/mobile eas:prod:android` | production AAB candidate |

Cloud build 결과는 profile, channel, commit SHA, EAS build URL, 실행자, 실행 시간을 PR/release note에 남긴다.

## OTA Policy

### Development mock lane

development channel은 backend URL이 없는 공유 테스트 앱에서 mock backend와 오류 메시지 QA를 반복하기 위한 OTA lane이다. 이 lane의 APK는 `developmentClient: true`이므로 일반 release 앱처럼 channel update를 백그라운드 자동 적용하는 흐름이 아니다. tester는 dev-client launcher의 Extensions 또는 게시 결과의 Update URL/QR로 compatible update를 명시적으로 연다.

1. `expo-updates` dependency와 config plugin은 `apps/mobile/app.json`에 포함한다.
2. `updates.url`은 ZERRO 전용 EAS project(`5915ffbd-884c-4047-b911-3219536a3ed6`)의 `https://u.expo.dev/...` 주소만 사용한다.
3. `runtimeVersion`은 Expo 권장 `appVersion` policy를 사용한다. 현재 `appVersion`과 runtimeVersion은 `1.0.0`이며, native/config 변경 시 새 development build를 먼저 만든다.
4. publish는 `eas-cli@20.3.0`으로 `channel=development`, `platform=android`, `environment=development`를 명시한 `eas:update:development:android` script만 사용한다. `eas update --auto`는 사용하지 않는다.
5. development EAS environment에서는 `EXPO_PUBLIC_API_BASE`를 설정하지 않는다. 이 값이 없으면 APP은 mock backend를 선택한다. `EXPO_PUBLIC_APP_ENV=development`와 `NODE_ENV=development`는 development build profile의 환경값으로 유지한다.
6. 최근 update 확인은 `eas:update:list:development:android`를 사용한다. EAS CLI 20.3.0의 `update:list`는 `--channel`/`--environment`를 지원하지 않으므로 최초 publish에서 development channel과 함께 생성·연결되는 `development` branch와 Android만 필터한다.
7. 첫 공유 QA는 EAS Update URL을 dev-client launcher에서 여는 방식으로 검증한다. 설치 후 자동 channel 적용이 필요한 stakeholder APK는 `developmentClient: false`인 별도 internal profile과 실제 device gate를 거쳐 분리한다.

### 2026-07-20 first development deployment evidence

| Field | Evidence |
|---|---|
| Source | `origin/develop` `64a3d7d10524d66078863949233c0de299cc94f0` |
| Build | [`50bd5924-2ebd-40bb-bffa-dbe1fa384e37`](https://expo.dev/accounts/bootalk/projects/zerro/builds/50bd5924-2ebd-40bb-bffa-dbe1fa384e37), `FINISHED`, Android APK development client |
| Build boundary | profile/channel/environment `development`, runtimeVersion `1.0.0`, fingerprint `2390af15842599940f1b980ece0fc112b9da3716` |
| OTA | group `dba9385f-7e39-46ad-bc77-edbb0a200b4c`, Android update [`019f7edf-70ca-72c2-876d-1eff62f82d29`](https://u.expo.dev/update/019f7edf-70ca-72c2-876d-1eff62f82d29), branch/channel `development` |
| EAS env | development environment의 `EXPO_PUBLIC_APP_ENV=development`; `EXPO_PUBLIC_API_BASE` 없음 |
| Backend | API base가 없으므로 mock backend |
| Device gate | cloud APK SHA-256 `67477ede2554d1a582236981cd645981ec0c738ef048d88ae7023a41f7355b44`; LG V30(`LGM-V300L`, Android 9)에 `adb install --no-streaming -r` 성공, launcher와 group-specific OTA deep link 실행, runtime `1.0.0`과 mock 역할 선택 surface 확인, foreground fatal crash 없음 |

Build와 OTA의 source SHA, runtimeVersion, fingerprint가 일치한다. development client는 embedded/channel update를 일반 앱처럼 자동 적용하는 artifact가 아니므로 물리 기기에서 launcher Extensions 또는 Update URL을 명시적으로 연 뒤 QA한다. 최초 LG V30 검증은 `zerro://expo-development-client/?url=https://u.expo.dev/5915ffbd-884c-4047-b911-3219536a3ed6/group/dba9385f-7e39-46ad-bc77-edbb0a200b4c` deep link로 수행했다. EAS Update Insights 집계는 즉시 반영되지 않을 수 있으므로 device foreground surface와 fatal crash log를 당일 실행 증적으로 사용하고 adoption 수치는 후속 관측한다.

### Production lane

production channel OTA는 비활성 상태를 유지한다. production backend URL, 운영 credential, crash/rollback 기준, 실제 release candidate와 운영 승인 evidence가 준비된 별도 PR에서만 runtimeVersion과 channel을 승격한다.

OTA 활성화 후 branch naming:

| Purpose | Format |
|---|---|
| dev | `development` |
| RC | `rc/<appVersion>-r<runtimeVersion>` |
| production | `prod/<appVersion>-r<runtimeVersion>` |
| hotfix | `hotfix/<appVersion>-r<runtimeVersion>-<yyyymmdd>-<slug>` |

## Runtime Boundary

다음 변경은 OTA만으로 배포하지 않는다. 새 build와 새 runtimeVersion이 필요하다.

- Expo SDK, React Native, native module dependency 변경 (`expo-updates` 최초 도입 포함)
- config plugin, permission, package id, scheme, splash/icon native 설정 변경
- 현장 증빙처럼 앱 전용 저장소에 남는 민감 파일의 Android backup 정책 변경. 현재 `android.allowBackup=false`로 app-private 운전자 증빙 draft가 Android Auto Backup에 포함되지 않도록 한다.
- Sentry, camera, dev-client처럼 native layer를 포함하는 dependency 변경
- Android Gradle, iOS project, EAS credential, store signing 변경

JS, style, copy, fixture, mock selector처럼 native layer를 바꾸지 않는 변경만 같은 runtime의 OTA 후보가 될 수 있다.

## Verification Gates

Latest read-only Android preflight checked on 2026-07-14:

```bash
pnpm ci:mobile-static
pnpm --filter @zerro/mobile exec expo install --check
cd apps/mobile && pnpm dlx expo-doctor@latest
pnpm --filter @zerro/mobile exec expo prebuild --platform android --clean --no-install
cd apps/mobile/android && ./gradlew clean assembleDebug --no-daemon
pnpm store:readiness:doctor -- --json
pnpm store:readiness:release-plan -- --platform google-play --track internal
pnpm --filter @zerro/mobile e2e:android:doctor
pnpm --filter @zerro/mobile e2e:maestro:login-entry
pnpm --filter @zerro/mobile e2e:maestro:role-routing
```

이 preflight는 clean Android prebuild, local debug APK build, 기존 dev-client 위 `adb install -r`, launch와 mock-mode Maestro까지 수행한다. 앱 data를 지우는 uninstall/reset, EAS cloud build, production signing, store submit, OTA publish, production credential write는 수행하지 않는다. development OTA는 위 script를 통한 별도 release 작업이며, 첫 RC와 production 운영 배포 준비는 `DG-RELEASE-01`의 운영 승인·검증 gate다.

Build-only:

```bash
pnpm --filter @zerro/mobile eas:project
pnpm --filter @zerro/mobile exec expo install --check
cd apps/mobile && pnpm dlx expo-doctor@latest
pnpm --filter @zerro/mobile check
git diff --check
```

Native dependency/config 추가:

```bash
pnpm --filter @zerro/mobile exec expo prebuild --platform android --no-install
pnpm --filter @zerro/mobile exec expo run:android --variant debug --no-bundler
```

## Release Evidence

Release/EAS/store PR은 아래만 남긴다.

| Field | Required |
|---|---|
| Scope | CI/static, EAS build, store read-only, submit, OTA 중 하나 |
| Build profile/channel | `development`, `releases`, `production`, or `N/A` |
| Runtime boundary | native/config 변경 여부 |
| Commands | 실행한 local/CI/static 명령 |
| EAS/store evidence | URL 또는 실행하지 않은 이유 |
| Device QA | UX 변경이면 mobile working agreement 기준 evidence |

## Store Readiness

Store deployment tool은 Bootalk 앱 도구를 재사용하지 않고 ZERRO 앱 값으로 분리한다.

| Step | Scope |
|---|---|
| Read-only doctor | env shape, app identifiers, secret file reference readiness |
| Release plan | EAS build, app version, package/track, blocked submit/OTA 상태 출력 |
| Validate-only write | internal/TestFlight metadata validation, no production write |
| Submit | package id, bundle id, credential, reviewer note 확정 뒤 별도 승인 PR |

Production write는 같은 턴의 명시 승인 없이 실행하지 않는다.

## Maintenance

- 미구현 기능의 순위·현재 경계·완료 조건은 이 문서에서만 상세 관리한다.
- 상태가 바뀌면 [Current Progress](../70-progress/CURRENT.md), 관련 contract/policy, [Mobile Test Coverage Graph](../../apps/mobile/e2e/TEST_COVERAGE_GRAPH.md)를 함께 점검한다.
- 완료된 항목은 표에서 즉시 삭제하지 않고 `완료` 상태와 검증 링크를 남긴 뒤 milestone/archive로 이동한다.
- provider 선택, 권한, 상태 enum 같은 새 결정은 이 문서에서 확정하지 않고 [기술 결정 등록부](../00-governance/DECISIONS.md)와 해당 contract를 먼저 갱신한다.
