어 앱을 업뎃할때마다 설정이 날아가지 않도록 하는 것만으로 충분한니까 , 그거 위주로 작업해줘!
# Data Survival Blueprint — 업데이트·재설치 후에도 데이터·웹 로그인 유지
> **목적:** Isle이 쓰는 “데이터가 안 날아가는” 시스템을 **플랫폼·프레임워크 무관**하게 설명한다.
> 다른 Flutter/네이티브 앱에 이 문서만 주고 동일 아키텍처를 이식할 수 있게 작성했다.
> **정본 구현:** `isle/lib/core/services/`, `isle/lib/main.dart`, `isle/ios/Runner/ScreenTimeBridge.swift`
---
## 1. 한 줄 요약
| 상황 | 사용자 데이터 (Hive) | 웹 로그인 (WebView 쿠키) | OS 차단 설정 (Screen Time 등) |
|------|---------------------|--------------------------|-------------------------------|
| **앱 스토어 업데이트** (같은 Bundle ID) | ✅ 샌드박스 유지 → 자동 유지 | ✅ WKWebView/Android WebView 기본 영구 저장소 | ✅ Hive + (iOS) Keychain 미러 |
| **앱 강제 종료·재실행** | ✅ + 25초 체크포인트 | ✅ | ✅ |
| **앱 삭제 후 재설치** | ⚠️ 샌드박스 삭제 → **Survival Backup**으로 복구 | ⚠️ 쿠키 삭제 → **다시 로그인** (백업 JSON에 쿠키 없음) | ✅ (iOS) Keychain 미러로 스케줄·앱 선택 복구 가능 |
| **기기 교체** | 수동 JSON/백업 코드 또는 iCloud 백업 폴더 | 수동 로그인 | 백업 JSON + Screen Time 재승인 |
**핵심:** “업데이트해도 안 날아감” = **① 같은 설치의 OS 샌드박스** + **② 샌드박스 밖 미러 백업** + **③ Keychain(삭제 후에도 남는 계층)** 의 **3층 방어**.
---
## 2. 아키텍처 — 4계층 모델
```mermaid
flowchart TB
subgraph L1["Layer 1 — Hot path (매 실행)"]
Hive["Hive boxes\n(app sandbox Documents)"]
WebView["WebView cookie jar\n(OS-managed, same sandbox)"]
end
subgraph L2["Layer 2 — Change-triggered mirror"]
Survival["Survival Backup\ncurrent.json (sandbox 밖)"]
Debounce["debounce 800ms\nisleNotifyLocalDataChanged()"]
end
subgraph L3["Layer 3 — Crash / partial loss"]
Checkpoint["Checkpoint\nisle_state_checkpoint.b64\n(Documents, 25s)"]
end
subgraph L4["Layer 4 — OS survives app delete (iOS)"]
Keychain["Keychain\nscreen_time_persistence"]
AppGroup["App Group file\nscreen_time_persistence_v1.json"]
end
UI[UI / Stores] --> Hive
UI --> WebView
Hive --> Debounce --> Survival
Hive --> Checkpoint
Hive --> Keychain
Keychain --> AppGroup
Bootstrap[main bootstrap] --> Survival
Bootstrap --> Keychain
Bootstrap --> Checkpoint
hive_flutter), 박스당 String JSON.Application Documents (앱 업데이트 시 Bundle ID 동일하면 경로 유지).webview_flutter 기본 동작 — 별도 쿠키 export 없음. 같은 샌드박스에 두면 YouTube/Instagram 세션은 업데이트 후에도 유지.IsleWebviewFactory에서 쿠키/캐시를 clear 하지 말 것. UA만 고정.{BackupRoot}/current.json (atomic write: .tmp → rename).~/Documents/{AppName} Backup%USERPROFILE%\Documents\{AppName} BackupgetExternalStorageDirectory()/Isle Backup (앱 삭제 전까지 유지되는 경로; 사용자가 Downloads/iCloud 등으로 변경 가능)isleNotifyLocalDataChanged() → 800ms debounce → syncNow().localLooksFresh() 이면 current.json에서 restoreNow(confirm: false).Documents/isle_state_checkpoint.b64 (Base64(JSON), 25초 주기 + 종료 시).localLooksFresh() 일 때만 복구 시도 (덮어쓰기 방지).restoreFromNativeMirrorIfNeeded().reapplyIfAuthorized() 로 OS 쉴드 재적용.runApp() 전에 반드시 이 순서:
1. WidgetsFlutterBinding.ensureInitialized()
2. Hive.initFlutter()
3. openBox(*) — 모든 박스 (실패해도 앱은 뜨게 _safe 래퍼 권장)
4. [선택] 마스터 플래그 복구 — 캐시만 리셋됐을 때 도메인 데이터로 플래그 재설정
예: BlockSchedule 있으면 youtubeScheduleLockEnabled = true
5. SurvivalBackupService.bootstrapOnLaunch()
→ fresh install + meaningful current.json → restore
→ _safeToSync = true
6. NativePersistence.bootstrapAfterDataLoad() // iOS Keychain 등
7. CheckpointService.restoreFromCheckpointIfLocalEmpty()
8. CheckpointService.startPeriodicCheckpoint() // 25s Timer
9. runApp()
Isle 정본: isle/lib/main.dart _bootstrap() 83–154행.
class ExampleStore {
static const _boxName = 'my_app_example';
static Box<String>? _box;
static Future<void> ensureOpen() async {
if (_box != null && _box!.isOpen) return;
_box = await Hive.openBox<String>(_boxName);
}
static Future<void> saveAll(List<ExampleModel> items) async {
await ensureOpen();
await _box!.put('items_v1', jsonEncode(items.map((e) => e.toJson()).toList()));
isleNotifyLocalDataChanged(); // ← 필수: Survival debounce
}
}
localLooksFresh)재설치·초기화 감지용 — 의미 있는 데이터가 하나도 없으면 true:
정본: IsleSurvivalBackupService.localLooksFresh().
모든 백업 경로가 같은 JSON 스키마를 쓴다:
| 소비자 | 포맷 | 파일 |
|---|---|---|
Survival current.json |
JSON indent | 샌드박스 밖 |
| Checkpoint | Base64(JSON) | Documents |
| 사용자 공유 백업 | JSON 파일 + Share | 임시 |
| 백업 코드 | Base64 + schema version | 클립보드 |
정본 빌더: IsleDataExportService.buildExportPayload() + export_schema 버전 번호.
스키마 버전 올릴 때: export_schema 증가, importFromJsonString 에 migration 분기.
새 앱에 YourAppSurvivalBackupService 만들 때:
snapshot_version 상수 (스키마 호환)buildSnapshotMap() = DataExportService.buildExportPayload() + prefs + extra boxessyncNow(): tmp 쓰기 → rename (원자적)localLooksFresh() + _snapshotHasMeaningfulData() — 빈 백업으로 덮어쓰기 방지onDataChanged() debounce 500–1000msbootstrapOnLaunch(): restore → then sync_ensureAndroidDefaultFolderIfNeeded 패턴)정본: isle/lib/core/services/isle_survival_backup_service.dart
Timer.periodic (배터리: Survival debounce와 별도 — 크래시 대비만)buildSnapshotMap() 재사용 (중복 직렬화 로직 금지)restoreFromCheckpointIfLocalEmpty() — fresh 일 때만paused/detached 에서 snapshot() 1회 (선택)정본: isle/lib/core/services/isle_checkpoint_service.dart
applicationId / Bundle ID 로 업데이트하면 샌드박스가 유지 → 쿠키·localStorage 그대로.| 규칙 | Isle |
|---|---|
| 로그아웃 시에만 쿠키 삭제 | 수동 “로그아웃” 메뉴에서만 |
| WebView controller 재생성해도 동일 process/data store | YoutubeSession.park() — controller 유지 |
| UA 고정 (봇 감지 방지) | InstagramWebConfig.mobileSafariUserAgent, kYoutubeSafariUserAgent |
| Instagram 키보드 | resizesToAvoidBottomInset: true + viewport interactive-widget=resizes-content |
IsleUserDefaults.youtubeWebLoginRemembered = “가이드 다시 안 띄움” 플래그.다른 앱에서 쿠키까지 백업하려면 (고급, 비권장):
별도 암호화 Keychain + 도메인별 cookie export — 심사·보안 리스크 큼. Isle은 하지 않음.
ScreenTimePersistenceService.exportBundle():
external_app_shield (FamilyActivity 인코딩)schedule_screen_time (스케줄별 앱 선택)app_usage_limits, quick_block_sessionsblock_schedules, schedule_master_enabled| 동작 | Dart | Native |
|---|---|---|
| 저장 | syncMirror() |
writePersistenceMirror (App Group) + savePersistenceKeychain |
| 복구 | restoreFromNativeMirrorIfNeeded() |
loadPersistenceKeychain → fallback loadPersistenceMirror |
| 재적용 | reapplyIfAuthorized() |
BlockScheduleEnforcementService + applySavedAppShield |
정본 Swift: ScreenTimeBridge.swift persistenceKeychainService, IsleAppGroupFiles.
{bundleId}/your_persistence.onAuthorizationGranted() 에서 mirror + OS 재적용.| 기능 | 클래스 | 용도 |
|---|---|---|
| JSON 파일 공유 | IsleDataExportService.shareExport() |
기기 간 이동 |
| 붙여넣기 복원 | importFromJsonString |
Replace All / Merge 모드 |
| 백업 코드 (Base64) | IsleBackupCodec + IsleBackupService |
메신저로 짧게 전달 |
| 설정 UI | DataBackupScreen |
폴더 선택·동기화·복원 |
Replace All 전: clearAllUserData() 로 고아 키 제거.
Isle이 쓰는 추가 방어 (다른 앱에 권장):
마스터 스위치 복구 (main.dart schedule_master_recovery):
isle_cache만 초기화되고 스케줄 박스는 남은 경우 → 플래그 재켜기.
Screen Time 재승인 (reapplyIfAuthorized):
업데이트 후 authorization 리셋되어도 저장된 pick으로 자동 재요청·재적용.
YouTube WebView OS 차단 해제 (ensureIsleYoutubeWebViewUnblocked):
ManagedSettings가 WebView 스트림까지 막은 상태 복구.
export_schema / snapshot_version:
구버전 백업 import 시 필드 기본값.
| 플랫폼 | 업데이트 | 앱 삭제 후 | 웹 로그인 |
|---|---|---|---|
| iOS | Hive+WebView 유지 | Survival(iCloud 폴더) + Keychain | 재로그인 |
| Android | Hive+WebView 유지 | Survival(외부/Drive) + Auto Backup | 재로그인 |
| macOS | Documents 밖 Survival | 동일 | 동일 |
iOS 사용자 안내 문구 패턴:
「앱을 삭제하면 이 기기 안 데이터는 사라집니다. iCloud Drive에 백업 폴더를 지정하세요.」
| 역할 | Isle 경로 | 새 앱에서 만들 이름 예시 |
|---|---|---|
| 부팅 | lib/main.dart |
main.dart |
| Export 스키마 | core/services/isle_data_export_service.dart |
app_data_export_service.dart |
| Survival | core/services/isle_survival_backup_service.dart |
app_survival_backup_service.dart |
| Checkpoint | core/services/isle_checkpoint_service.dart |
app_checkpoint_service.dart |
| 변경 알림 | core/services/isle_local_data_sync.dart |
notifyLocalDataChanged() |
| Base64 백업 코드 | core/services/isle_backup_codec.dart |
동일 패턴 |
| Keychain 미러 | core/intervention/screen_time_persistence_service.dart |
native_prefs_persistence_service.dart |
| iOS Native | ios/Runner/ScreenTimeBridge.swift |
PersistenceBridge.swift |
| 설정 UI | features/settings/presentation/data_backup_screen.dart |
data_backup_screen.dart |
| Store 예시 | *_store.dart + ensureOpen + isleNotifyLocalDataChanged |
동일 |
localLooksFresh() 없이 매 부팅마다 current.json 덮어쓰기clearCache / deleteAllData 를 화면 전환마다 호출DataExportService.buildExportPayload() + export_schema 정의.*Store.save* 끝에 notifyLocalDataChanged().main() bootstrap 순서 §3 적용.bootstrapAfterDataLoad.buildExportPayload() + Survival extras:
screen_time_persistence 번들isle_pref_*), ai_config, island_snapshot, local_shelves포함 안 함: WebView 쿠키, Supabase refresh token (별도 auth), 바이너리 미디어.
문서 버전: 2026-06-03 · Isle export_schema 10 · survival snapshot_version 10