[P0][A02] Android·iOS 암호화 백업을 실제 문서 저장 API로 내보내고 성공·취소·실패를 구분
문제와 사용자 영향
현재 BackupService.exportToUserSelectedFile()은 암호화 후 file_selector.getSaveLocation()을 호출한다. 고정된 Android/iOS 플러그인은 이 메서드를 구현하지 않으므로 모바일에서 파일 저장 단계가 완료될 수 없다. OnTime Backup이 기기 이동 및 데이터 복구의 유일한 지원 경로라는 제품 정책에 비추어 P0다.
2026-09-23 감사 A02에 대응한다. 현 판정은 고정 dependency 및 소스 계약에서 실패 경로 확인이며, 이 문서 작성이 실기기 재현·구현·검증 완료를 뜻하지 않는다.
조사 기준과 근거
조사한 HEAD: 44067d7ab26b6290c16cdb13b99f57387dec08f6.
| 근거 |
확인한 사실 |
| backup_service.dart:77-95 |
snapshot 암호화 후 getSaveLocation과 XFile.saveTo(location.path)를 호출하고 이후 markExported 한다. |
| pubspec.lock:323-346 |
file_selector 1.1.0, file_selector_android 0.5.2+10, file_selector_ios 0.5.3+6으로 고정돼 있다. |
고정 package의 Android lib/src/file_selector_android.dart, iOS lib/file_selector_ios.dart |
두 구현 모두 getSaveLocation/getSavePath override가 없다. |
file_selector_platform_interface 2.7.0 file_selector_interface.dart:69-97 |
기본 getSaveLocation은 getSavePath를 호출하고, 기본 getSavePath는 UnimplementedError를 던진다. |
| my_data_screen.dart |
bool 결과가 true인 경우만 저장 완료 안내 및 freshness 재조회를 한다. _busy는 해당 화면 인스턴스에만 있어 서비스 수준 경합을 막지 못한다. |
| user_dao.dart:109-118 |
markExported는 전달받은 revision/cutoff를 unconditional update한다. 이전 lifecycle의 늦은 완료를 구분하지 못한다. |
| backup_service_test.dart |
암호화 bytes 생성·검증·복원·잘못된 비밀번호만 검사하며 실제 내보내기 포트와 freshness 성공/취소/실패 검사가 없다. |
| Android app build / iOS project |
minSdk 23, iOS deployment target 15.0이다. 선택한 native document API의 적용 범위를 검토할 기준이다. |
문서 API 근거: Android 문서 저장 및 SAF, Apple document export picker. Android URI는 일반 파일 경로가 아니며, ACTION_CREATE_DOCUMENT의 정상 동작은 기존 동일 이름 문서를 직접 덮어쓰지 않고 새 이름을 부여할 수 있다. provider별 실제 이름·동작을 검증한다.
제품 정책과 용어
- OnTime Backup은 Android/iOS 사이를 이동할 수 있는 사용자 명시적 암호화 백업이다.
- Backup Cutoff는 일관된 snapshot의 시점이며 파일 저장 완료 시각이 아니다.
- Backup Freshness는 현재 설치에서 성공한 내보내기의 cutoff/revision과 현재 데이터의 비교다. 외부 파일의 현재 존재, cloud 동기화, 복원 성공을 보장하지 않는다.
- 기존 ADR 0012, 0026, 0027, 0031, 0032, 0035를 유지한다. 기존 암호화 컨테이너·비밀번호 규칙·포맷 버전은 바꾸지 않는다.
- 기존 용어만으로 충분하다. CONTEXT.md에 구현 세부사항을 추가하지 않으며, 쉽게 교체할 수 있는 좁은 native adapter 선택을 별도 ADR로 과장하지 않는다.
합의한 구현 범위
문서 저장 경계
- 주입 가능한
BackupFileExportPort로 문서 내보내기를 분리한다. 서비스 테스트에서 성공·취소·오류·지연 완료를 제어할 수 있어야 한다.
- Android는
ACTION_CREATE_DOCUMENT로 사용자 목적지를 받고 ContentResolver stream으로 쓴다. content URI를 File 경로로 바꾸지 않는다. 쓰기·flush·close가 성공한 후만 completed를 반환한다.
- iOS는 암호화된 앱 소유 임시파일을
UIDocumentPickerViewController(forExporting:asCopy:)에 제공한다. delegate의 완료·취소 및 presentation/플랫폼 오류를 구별한다.
- picker 표시, 목적지 선택, share sheet에 넘겼다는 사실은 저장 성공이 아니다. 결과는 destination-free receipt로 표현한다.
- 외부 선택 경로, persistable URI 권한, 비밀번호를 앱 저장소에 남기지 않는다. 외부 provider의 cloud upload/sync 완료를 보장하지 않는다.
- 저장용 대형 새 패키지를 무조건 도입하지 않고 좁은 native adapter를 사용한다. 기존 import용 file_selector는 A03과 분리한다.
임시 암호문과 실패 처리
- picker 전에 앱 전용 no-backup 임시 디렉터리에 완성된 암호문만 만든다. 평문 JSON·DB 복사·비밀번호 파일을 만들지 않는다.
- 저장 receipt 전 bytes 완결성과 무결성을 확인한다. 추가 비밀번호 KDF를 항상 재실행하는 방식으로 고정하지 말고, 보장 범위와 비용을 검토하여 선택한 검증 방법 및 음성 테스트를 기록한다.
- 고유 attempt ID로 앱 소유 파일 수명을 관리한다. 성공·취소·오류 이후 finally 정리하며, 다음 시작에 남은 orphan도 정리한다. 진행 중 export와 startup 청소는 경합하지 않아야 한다.
- Android 문서 쓰기 실패 시 이번 attempt가 새로 생성했음을 확신하는 문서만 best-effort 삭제한다. 사용자가 기존 파일을 덮어쓰는 provider 경로를 밟았다면 기존 파일을 함부로 삭제하지 않는다.
- 삭제 불가·부분 쓰기 가능성은 사용자 친화적인 오류로 안내한다. 불완전한 결과를 유효한 OnTime Backup으로 표시하지 않는다. 타사 provider에서 외부 파일이 완벽하게 원자적으로 사라진다고 약속하지 않는다.
- 앱 종료·delegate 미수신·OS 취소를 성공으로 추정하지 않는다. 비밀번호/URI를 보관해 자동 이어쓰기하지 않으며 재시작 시 freshness는 보수적으로 유지한다.
데이터 일관성과 동시 실행
- snapshot을 확정한 뒤 picker 대기 동안 DB transaction을 유지하지 않는다. 일반 데이터 편집은 허용한다.
- snapshot revision 10을 저장하는 동안 현재 데이터가 revision 11로 바뀌면, 성공 후 10만 기록해 11은 Unexported Changes로 남긴다.
- 서비스 수준 single-flight를 적용한다. 두 번째 export는 명시적 busy 결과로 거절한다. 다른 비밀번호·목적지의 요청에 첫 요청 Future를 성공처럼 공유하지 않는다. UI도 중복 진입을 막는다.
- reset/restore/export가 같은 local-data-operation gate를 사용하여 동시에 실행되지 않게 한다. export 중 파괴적 작업 요청은 busy로 알린다. 일반 편집은 이 배타 범위에 포함하지 않는다.
- lifecycle generation도 확인하여 강제 재초기화나 예상 밖 DB 교체 후 늦게 돌아온 완료가 새 DB에 과거 freshness를 쓰지 못하게 한다.
- 외부 파일 저장 성공과 로컬 freshness 기록 성공을 별도로 표현한다. 저장 뒤
markExported가 실패하면 파일을 지우지 않고 “파일은 저장됐지만 백업 상태를 갱신하지 못했습니다”라는 안내와 재시도 경로를 제공한다. 파일 저장 자체가 실패했다고 말하지 않는다.
- A09/C01 후속 workflow가 재사용할 gate contract를 먼저 테스트한다.
검증 시나리오
| 시나리오 |
기대 결과 |
| Android 로컬 문서 저장 |
선택한 URI에 전체 bytes가 기록되고 다시 읽은 파일이 인증·복호화된다. 정상 종료 뒤에만 freshness가 갱신된다. |
| iOS 로컬 Files 저장 |
실제 export picker 완료 뒤 저장 파일을 다시 읽고 인증·복호화한다. delegate 전에 성공 안내가 나오지 않는다. |
| picker 취소 |
성공 안내·freshness 변경 없이 busy가 해제되고 임시파일이 정리된다. |
| 암호화/임시파일 생성 실패 |
외부 문서 선택 또는 저장 성공으로 진행하지 않으며 현재 DB를 보존한다. |
| 쓰기/flush/close/provider 오류 |
오류가 성공으로 변환되지 않으며 partial document와 임시파일 정책을 따른다. |
| 앱 background/정상 복귀 |
실행 중 attempt와 picker callback이 정확히 한 번 완료되고 UI 생명주기 오류가 없다. |
| 앱/프로세스 종료, callback 유실 |
다음 시작에서 orphan 정리, 성공 추정 금지, 기존 freshness 유지. |
| 같은 파일 이름 |
OS/provider의 중복 이름 처리 결과를 기록하고 기존 사용자 파일을 잘못 삭제하지 않는다. |
| export 중 일반 편집 |
cutoff 이전 데이터만 파일에 포함되고 이후 revision은 미백업으로 남는다. |
| 두 번째 export/다른 비밀번호 |
busy를 반환하며 첫 attempt의 password·파일·결과를 섞지 않는다. |
| export 중 reset/restore |
gate가 중복 작업을 거절한다. 별도 강제 lifecycle 교체 후 늦은 완료도 새 DB를 오염시키지 않는다. |
| 외부 저장 성공, DB mark 실패 |
저장된 파일은 유지하고 상태 기록 실패만 알린다. 실패/성공을 한 bool로 왜곡하지 않는다. |
| 외부 provider 선택 |
명시적인 OS 동작만 사용한다. OnTime network client/권한을 추가하지 않고 cloud sync 성공을 표시하지 않는다. |
| Android→iOS 및 iOS→Android |
각 플랫폼 실제 picker에서 저장한 동일 암호문을 반대 플랫폼에서 복원하고 전체 durable 데이터가 일치한다. |
단위 테스트는 포트 결과와 DB 상태를 함께 검증한다. adapter/native 테스트는 결과 중복·취소·stream 오류·presentation 실패 등을 검증하며, 실제 picker 및 파일 전달 검증을 대체하지 않는다.
교차 복원에는 합성 QA 데이터를 사용한다. 일정, 장소, 기본/일정별 준비, 템플릿 생성·수정 시각, 앱 설정, 점수 및 보존 이력의 전체 durable 필드를 비교한다. 활성 준비 세션, 알림 등록, OS 권한 등 제외 대상이 옮겨지지 않았음도 기록한다. 비밀번호와 실 사용자 데이터를 증거에 포함하지 않는다.
의존 관계와 제외 범위
- A03: iOS import picker UTI 설정. A02의 native export 수정과 독립 구현하지만 교차 UI 복원 완료 증거의 선행 조건이다.
- D07: 템플릿 createdAt 보존. 교차 복원 동등성 검사에서 드러나면 이 의존성을 해결한 뒤 재검증한다.
- A09: restore 후 runtime 세션 정리. A02의 gate를 재사용하며 복원 제외 대상 검증의 의존성이 된다.
- C01: workflow 통합은 이 gate와 결과 contract를 이어받는다. 모든 서비스 구조 개선을 A02에 넣지는 않는다.
- D05/C03: 초대형 백업 입력 제한·snapshot N+1/성능은 별도 개선이다. A02의 임시파일 완결성 및 bounded lifecycle 검증은 생략하지 않는다.
- D06/백업 안내 기능 후보: 30일 reminder 및 별도 백업 UX 확장은 분리한다. 여기서는 실제 저장 결과 안내를 정확하게 만든다.
- 암호화 suite 교체, 포맷 버전 변경, 클라우드 자동 업로드, 비밀번호 복구, 외부 경로 기억, share 성공을 저장으로 간주하는 우회는 제외한다.
완료 기준
완료 판정과 제한
현재 환경의 공간 부족 및 실기기 연결 여부는 구현/실행 시 다시 확인한다. 자동 테스트가 통과해도 native picker와 교차 복원 실증을 대체하지 않는다. 기기 검증이 막히면 구현 완료/검증 대기를 구분하고 이슈를 열어 두며 다음 우선순위 이슈 구현을 계속한다. 외부 provider의 원격 동기화 완료나 파일의 영구 존재는 이 이슈의 보장 범위가 아니다.
grill-with-docs 전체 실제 문답
전담 서브에이전트가 grill-with-docs/SKILL.md, CONTEXT-FORMAT.md, ADR-FORMAT.md, AGENTS.md, CONTEXT.md, 관련 ADR, 감사 A02, 실제 서비스·UI·DAO·고정 플랫폼 package 및 native 진입점을 읽었다. 코드로 확인할 수 있는 사항은 직접 조사했고, 남은 결정을 한 번에 하나씩 root에게 질문했다. 사용자 위임에 따라 root가 답했다.
질문 1 — 서브에이전트
A02 질문 1/순차: ADR 0027은 암호화·무결성·전체 쓰기 성공 뒤만 완료, ADR 0035는 외부 경로/권한 미보관을 요구합니다. 현재 모바일 plugin은 getSaveLocation을 구현하지 않습니다. 모바일 문서 내보내기를 주입 가능한 전용 포트로 분리하고 Android ACTION_CREATE_DOCUMENT + ContentResolver stream, iOS UIDocumentPickerViewController(forExporting:asCopy:) + delegate 완료를 사용하는 좁은 native adapter로 갈까요? 권장: 예. share sheet의 '전달됨'을 저장 성공으로 간주하지 않고, content URI를 파일 경로로 변환하지 않으며, 저장 완료/취소/실패를 명시적으로 구분합니다. 기존 .ontimebackup 포맷·암호화 suite는 보존. 새 대형 파일관리 의존성보다 이 좁은 경계가 현재 실패를 직접 해결합니다.
추가 조사 메시지 — 질문 아님
A02 조사 보강: test/core/backup/backup_service_test.dart는 createEncryptedBackup/preview/apply만 검사하며 실제 export 및 freshness 검사 없음. Android minSdk 23, iOS deployment 15라 제안 native API 가용범위 충족. 현재 markExported는 unconditional update라 export 중 reset/restore 또는 동시 export의 이전 cutoff가 최신 상태를 덮는 문제도 경계에 명시해야 합니다. Q1 답 뒤 이 lifecycle을 한 질문씩 진행하겠습니다. 공식 근거 확인: https://developer.android.com/training/data-storage/shared/documents-files 및 https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller/init(forexporting:ascopy:)
답변 1 — root
예, 전용 주입 가능 BackupFileExportPort와 플랫폼별 좁은 adapter로 갑니다. Android ACTION_CREATE_DOCUMENT/ContentResolver stream은 content URI를 File 경로로 바꾸지 않고 write/flush/close 성공 후만 completed를 반환합니다. iOS UIDocumentPicker(forExporting:asCopy:)는 암호화된 앱 소유 임시파일을 사용하고 delegate 완료/취소/오류를 구분합니다. 외부 선택 경로·persistable URI grant·비밀번호는 저장하지 않습니다. share sheet가 열렸다는 사실은 성공이 아닙니다. 기존 컨테이너·암호화 버전은 바꾸지 않으며 A03 import 필터 수정은 별도 issue로 유지하되 교차 복원 E2E 완료 의존성으로 명시합니다. 성공 receipt는 앱 내부 destination-free 결과로만 표현하고 markExported는 성공한 snapshot revision/cutoff에 대해 실행합니다.
질문 2 — 서브에이전트
A02 질문 2: ADR 0027의 '실패/취소는 유효한 백업 산출물로 표시되지 않음'을 제3자 문서 provider의 실패/앱 종료까지 어떻게 구현·표현할까요? 권장: picker를 열기 전 앱 전용 임시 디렉터리에 완전히 암호화되고 읽기 검증된 컨테이너만 생성(평문 파일 금지), 고유 attempt ID로 수명 관리. 성공/취소/오류 finally에서 제거, 다음 시작에서도 버려진 자체 임시파일 정리(현재 진행 중 파일 제외). Android 쓰기 실패 시 이번 작업이 새로 생성한 문서만 best-effort 삭제하고 삭제 불가면 '저장하지 못했으며 불완전한 파일이 남을 수 있음'을 명확히 안내, 절대 freshness 성공 처리하지 않기. 프로세스 종료/OS 취소에서 완료 여부를 확인하지 못하면 보수적으로 미완료 유지, 지속 URI·비밀번호 저장으로 자동 이어쓰기는 안 함. iOS delegate 미수신·presentation 불가도 완료가 아님. 이 경계를 채택할까요?
용어 문서 검토 메시지 — 질문 아님
A02 용어 문서 검토: 기존 CONTEXT.md의 OnTime Backup/Backup Cutoff/Backup Freshness 정의가 본 이슈를 충분히 포괄합니다. 아직 새 도메인 용어를 추가할 필요가 없습니다. Q2 답에서 '성공한 백업 내보내기' 경계가 확정되면 기존 Relationships에 1문장 보강 제안을 드리고, 기술 adapter 선택은 되돌리기 쉬운 변경이므로 새 ADR은 만들지 않을 예정입니다.
답변 2 — root
채택합니다. picker 전 앱 전용 no-backup 임시 위치에 암호화된 컨테이너만 생성하고 무결성 검증이 끝난 파일만 export 대상으로 씁니다. 이미 암호화 과정에서 인증 가능한 완전한 결과임을 검증하는 방법은 전체 추가 비밀번호 KDF를 무조건 재실행하기보다는 비용/보장 검토 후 구현하되, 저장 receipt 전에 bytes 완결성을 확인해야 합니다. attempt ID로 앱 소유 파일만 정리하고 startup orphan 정리는 진행 중 export와 경합하지 않게 직렬화합니다. Android best-effort 삭제는 이번에 생성했다고 확실한 문서만 대상으로 하고, 기존 파일 덮어쓰기를 선택한 경우 사용자 파일을 삭제하면 안 됩니다. 불완전 파일 잔존 가능성은 사용자 친화적인 오류로 알려주며 freshness는 바꾸지 않습니다. 앱 종료/미수신/취소를 성공으로 추정하지 않고 persistent URI나 password로 이어쓰기하지 않습니다. 기존 glossary는 충분하므로 implementation 상세를 CONTEXT에 넣지 말고, 기존 용어의 의미 변화가 없다면 CONTEXT 수정 없이 이슈의 수용기준으로 기록하세요.
질문 3 — 서브에이전트
A02 질문 3: export가 revision 10을 캡처한 뒤 사용자가 편집하여 11이 되거나, export 진행 중 reset/restore·두 번째 export가 들어오는 경우 freshness와 UI 결과는 어떻게 정할까요? 권장: 일반 편집은 허용하며 성공 후 cutoff/revision 10만 mark하여 11은 미백업 유지. export는 service 수준 single-flight로 겹침 거절/기존 작업 대기 중 한 정책을 명확히 하고, reset/restore와 내보내기 최종 mark는 동일 lifecycle 가드로 직렬화하거나 epoch로 오래된 attempt 완료를 거절하여 새 DB에 과거 freshness를 쓰지 않기. 외부 저장은 성공했는데 markExported DB 기록만 실패하면 '파일은 저장됐지만 앱의 백업 상태를 갱신하지 못함'으로 구분하고 freshness는 보수적으로 유지(파일 지우거나 저장 실패로 오인시키지 않음). 이 정책과 경계를 채택할까요?
답변 3 — root
채택합니다. snapshot revision10을 캡처한 뒤 일반 edit11은 허용하고 성공하면10만 기록해11은 미백업으로 남깁니다. export 중 두 번째 export는 명시적 busy 결과로 거절(다른 비밀번호/목적지인데 같은 작업 future를 성공처럼 공유하지 않음)하며 UI도 중복 진입을 막습니다. reset/restore와 export는 동일한 local-data-operation gate로 동시에 실행되지 않게 합니다. 파괴적 작업이 export 중 시작되려 하면 busy를 사용자에게 알리고, picker 대기 내내 DB transaction을 붙잡지는 않습니다. lifecycle generation을 함께 확인해 강제 lifecycle 교체/예상외 재초기화 뒤 늦게 돌아온 완료가 새 DB를 쓰지 않도록 방어합니다. 외부 파일 저장과 DB freshness 기록은 별도 결과로 표현해 mark 실패 시 '파일은 저장됐지만 백업 상태를 갱신하지 못했습니다'와 재시도 가능한 안내를 합니다. 파일 삭제하지 않고 거짓 실패/성공을 보이지 않습니다. A09/C01 등 후속 workflow가 같은 gate를 재사용할 의존성을 issue에 명시하고 gate contract tests부터 둡니다.
질문 4 — 서브에이전트
A02 질문 4(완료 증거): 이슈 완료 기준을 Android/iOS 각 문서 picker 실경로에서 export→같은 파일 재선택·전체 인증→반대 플랫폼 restore까지로 둘까요? 권장: 단위/adapter 테스트만으로 완료 처리하지 않고, OS/앱 버전·파일 SHA-256·암호문만 공유한 fixture·복원 전후 durable 필드 비교를 포함한 Android→iOS 및 iOS→Android 증거 2건을 남깁니다(정상 export 결과와 freshness, picker 취소, storage/provider 오류, background/종료, same filename, 중복 버튼, export 중 일반 편집 포함). A03 import 필터 때문에 교차 UI 검증이 막히면 구현 완료·검증 대기로 유지하고 A03 수정 후 closure. 장비/여유 공간이 없으면 불가능한 실기기 항목을 통과로 간주하지 않고 issue를 열어 두되 다음 이슈 구현은 진행. 기본 보장 범위는 기기 로컬 문서 provider, 외부 cloud provider는 사용자가 명시적으로 선택한 OS 작업이며 OnTime 자체 네트워크/원격 업로드는 추가하지 않고 cloud 동기화 완료까지 보장하지 않는 것으로 할까요?
답변 4 — root
예. 기본 보장은 각 OS 로컬 문서 provider의 export 완료와 다시 읽을 수 있는 유효한 OnTime Backup입니다. 사용자가 선택한 외부 provider 동작은 OS 주도이며 OnTime 자체 network client/권한/업로드를 넣지 않고 cloud sync 완료도 보장하지 않습니다. Android→iOS, iOS→Android 각각 fixture의 전체 durable 필드(일정/준비/템플릿 생성·수정시각/설정/점수·보존 이력)와 excluded runtime을 비교합니다. 실 데이터/비밀번호가 아닌 합성 QA 데이터로 증거를 남깁니다. 파일 SHA256와 app SHA/version/OS/provider 및 단계별 기대/실제 기록을 필수로 하고, 실패 케이스도 권장 목록대로 검증합니다. A03/D07 등 기존 복원 결함으로 동등성이 실패하면 이슈 dependency로 연결해 해결 후 재검증합니다. 자동 테스트 완료만으로 해당 기기 checkbox를 체크하지 않습니다. 새 ADR/용어 추가는 이번 결정이 기존 정책을 구체화할 뿐이면 불필요합니다. 남은 모순이 없다면 전체 실제 문답 포함 상세 본문 작성해주세요.
질문 5 — 실제 테스트에서 발견한 비밀번호 dialog 수명 문제
A02 보충 질문 5: 실제 export widget 테스트에서 Navigator.pop 결과를 받은 직후 controller를 dispose하여 dialog 퇴장 transition 중 used after dispose가 발생한 것이 확인됐습니다. export 진입을 막는 동일 workflow 결함이므로 A02에 포함하고, 비밀번호 입력 dialog를 private StatefulWidget으로 분리해 각 TextEditingController를 State가 생성·소유·dispose하며, 부모 _askPassword는 검증된 문자열 결과만 받게 할까요? 권장: 예. 생성/복원 두 진입의 검증·확인 규칙은 그대로 유지하고, 제출·취소 후 transition 완료까지 pump하는 widget 회귀 테스트에서 dispose 예외가 없고 결과/비밀번호 저장 정책이 변하지 않음을 검증합니다. 별도 UX 확대/암호 정책 변경은 하지 않습니다.
답변 5 — root
예 동의합니다. 기존 export workflow 테스트가실제진입bug를잡았으므로A02범위에포함. private StatefulWidget에서 controller를 State lifecycle에묶고 부모는 검증된문자열결과만받습니다. 생성/복원15~128문자정책과확인조건동일, 전환중사용보장/종료후dispose, 비밀번호저장없음. 제출·취소 exit transition끝까지pump 회귀테스트추가하며 UX/암호정책확대없음. Root구현합니다.
구현 중 보충 검토 — 저장 receipt 이후 native lifecycle
Review 반영: cleanup 예외 격리/terminal result 보장, startup child별 예외 격리, requestCode 16bit 상한 도달 시 재시작 요구로 거절(프로세스 내 재사용 금지로 stale callback 충돌 없음), iOS present 즉시 실패 탐지+scene disconnect failure 추가했습니다. Android detach가 쓰기 중이면 interrupted로 fail, 실제 write/flush/close 완료 후 cleanup 중 detach일 때도 현재는 보수적으로 fail입니다. 이 후자 경계는 파일이 완성됐을 수 있으므로 generic failure에 잔존 가능성을 포함합니다. 정확히 완료가 이미 확정된 경우 saved를 유지하도록 할지 검토하며 저는 'finish(saved)가 이미 시작된 뒤의 detach는 saved 유지, 그 이전 detach는 fail'이 의미상 더 정확하다고 봅니다. Dart generation 가드가 새 DB mark는 차단합니다.
root 답변
이미 write/flush/close 완료하고 finish(saved)가 시작된 뒤 detach는 saved보존 동의. 그전detach 보수failure 잔존가능메시지. terminal receipt 정확성이 목적이며 lifecyclegeneration이새DBmark차단합니다. 이root답변도필요하면보충문답기록.
보충 수용 기준
결정: 다섯 가지 순차 질문과 구현 중 추가 lifecycle 검토로 범위·저장 성공의 경계·암호문 lifecycle·경합·증거·비밀번호 dialog 수명을 확정했다. CONTEXT/ADR 의미 변경은 없다. 제품 구현 중 추가 문답을 포함해 GitHub 이슈를 갱신한다.
[P0][A02] Android·iOS 암호화 백업을 실제 문서 저장 API로 내보내고 성공·취소·실패를 구분
문제와 사용자 영향
현재
BackupService.exportToUserSelectedFile()은 암호화 후file_selector.getSaveLocation()을 호출한다. 고정된 Android/iOS 플러그인은 이 메서드를 구현하지 않으므로 모바일에서 파일 저장 단계가 완료될 수 없다. OnTime Backup이 기기 이동 및 데이터 복구의 유일한 지원 경로라는 제품 정책에 비추어 P0다.2026-09-23 감사 A02에 대응한다. 현 판정은 고정 dependency 및 소스 계약에서 실패 경로 확인이며, 이 문서 작성이 실기기 재현·구현·검증 완료를 뜻하지 않는다.
조사 기준과 근거
조사한 HEAD:
44067d7ab26b6290c16cdb13b99f57387dec08f6.getSaveLocation과XFile.saveTo(location.path)를 호출하고 이후markExported한다.lib/src/file_selector_android.dart, iOSlib/file_selector_ios.dartgetSaveLocation/getSavePathoverride가 없다.file_selector_interface.dart:69-97getSaveLocation은getSavePath를 호출하고, 기본getSavePath는 UnimplementedError를 던진다._busy는 해당 화면 인스턴스에만 있어 서비스 수준 경합을 막지 못한다.markExported는 전달받은 revision/cutoff를 unconditional update한다. 이전 lifecycle의 늦은 완료를 구분하지 못한다.문서 API 근거: Android 문서 저장 및 SAF, Apple document export picker. Android URI는 일반 파일 경로가 아니며, ACTION_CREATE_DOCUMENT의 정상 동작은 기존 동일 이름 문서를 직접 덮어쓰지 않고 새 이름을 부여할 수 있다. provider별 실제 이름·동작을 검증한다.
제품 정책과 용어
합의한 구현 범위
문서 저장 경계
BackupFileExportPort로 문서 내보내기를 분리한다. 서비스 테스트에서 성공·취소·오류·지연 완료를 제어할 수 있어야 한다.ACTION_CREATE_DOCUMENT로 사용자 목적지를 받고ContentResolverstream으로 쓴다. content URI를File경로로 바꾸지 않는다. 쓰기·flush·close가 성공한 후만 completed를 반환한다.UIDocumentPickerViewController(forExporting:asCopy:)에 제공한다. delegate의 완료·취소 및 presentation/플랫폼 오류를 구별한다.임시 암호문과 실패 처리
데이터 일관성과 동시 실행
markExported가 실패하면 파일을 지우지 않고 “파일은 저장됐지만 백업 상태를 갱신하지 못했습니다”라는 안내와 재시도 경로를 제공한다. 파일 저장 자체가 실패했다고 말하지 않는다.검증 시나리오
단위 테스트는 포트 결과와 DB 상태를 함께 검증한다. adapter/native 테스트는 결과 중복·취소·stream 오류·presentation 실패 등을 검증하며, 실제 picker 및 파일 전달 검증을 대체하지 않는다.
교차 복원에는 합성 QA 데이터를 사용한다. 일정, 장소, 기본/일정별 준비, 템플릿 생성·수정 시각, 앱 설정, 점수 및 보존 이력의 전체 durable 필드를 비교한다. 활성 준비 세션, 알림 등록, OS 권한 등 제외 대상이 옮겨지지 않았음도 기록한다. 비밀번호와 실 사용자 데이터를 증거에 포함하지 않는다.
의존 관계와 제외 범위
완료 기준
완료 판정과 제한
현재 환경의 공간 부족 및 실기기 연결 여부는 구현/실행 시 다시 확인한다. 자동 테스트가 통과해도 native picker와 교차 복원 실증을 대체하지 않는다. 기기 검증이 막히면 구현 완료/검증 대기를 구분하고 이슈를 열어 두며 다음 우선순위 이슈 구현을 계속한다. 외부 provider의 원격 동기화 완료나 파일의 영구 존재는 이 이슈의 보장 범위가 아니다.
grill-with-docs 전체 실제 문답
전담 서브에이전트가
grill-with-docs/SKILL.md, CONTEXT-FORMAT.md, ADR-FORMAT.md, AGENTS.md, CONTEXT.md, 관련 ADR, 감사 A02, 실제 서비스·UI·DAO·고정 플랫폼 package 및 native 진입점을 읽었다. 코드로 확인할 수 있는 사항은 직접 조사했고, 남은 결정을 한 번에 하나씩 root에게 질문했다. 사용자 위임에 따라 root가 답했다.질문 1 — 서브에이전트
추가 조사 메시지 — 질문 아님
답변 1 — root
질문 2 — 서브에이전트
용어 문서 검토 메시지 — 질문 아님
답변 2 — root
질문 3 — 서브에이전트
답변 3 — root
질문 4 — 서브에이전트
답변 4 — root
질문 5 — 실제 테스트에서 발견한 비밀번호 dialog 수명 문제
답변 5 — root
구현 중 보충 검토 — 저장 receipt 이후 native lifecycle
root 답변
보충 수용 기준
결정: 다섯 가지 순차 질문과 구현 중 추가 lifecycle 검토로 범위·저장 성공의 경계·암호문 lifecycle·경합·증거·비밀번호 dialog 수명을 확정했다. CONTEXT/ADR 의미 변경은 없다. 제품 구현 중 추가 문답을 포함해 GitHub 이슈를 갱신한다.