fix(startup): survive store open failures instead of crashing before splash

일기 기능의 스키마 변경(모델 4종 추가) 후 기존 데이터가 있는 기기에서 첫 실행 시
마이그레이션이 필요한데, 이때 위젯 프로세스와의 스토어 경합이나 마이그레이션 실패가
발생하면 makeContainer()가 곧장 fatalError로 죽어 스플래시도 못 띄우는 문제 방어:
- 일시적 경합 대비 최대 3회 재시도(점증 대기)
- 그래도 실패하면 스토어를 타임스탬프 백업으로 보존 후 새로 생성해 앱은 반드시 켜지게
- 검증: 스토어 파일 고의 손상 → 백업 생성 + 정상 부팅 확인

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-13 03:15:38 +09:00
parent 8c803d4d6e
commit 1f77264852

View File

@ -64,12 +64,44 @@ nonisolated enum DataStore {
/// ·· .
/// CloudKit .
/// (DataStore.shared) , .
///
/// :
/// 1) (
/// ) ,
/// 2) ( / )
/// fatalError
/// . (HaruDanim.store.backup-*) .
static func makeContainer() -> ModelContainer {
do {
return try makeContainerThrowing()
} catch {
fatalError("[DataStore] 컨테이너 생성 실패: \(error)")
print("[DataStore] 컨테이너 생성 실패, 재시도: \(error)")
for attempt in 1...3 {
Thread.sleep(forTimeInterval: 0.4 * Double(attempt))
if let container = try? makeContainerThrowing() {
return container
}
}
print("[DataStore] 재시도 실패 — 스토어를 백업하고 새로 시작: \(error)")
backupBrokenStore()
do {
return try makeContainerThrowing()
} catch {
fatalError("[DataStore] 컨테이너 생성 실패(백업 후 재생성도 실패): \(error)")
}
}
}
/// (-wal/-shm ) .
/// .
private static func backupBrokenStore() {
let fileManager = FileManager.default
let stamp = Int(Date.now.timeIntervalSince1970)
for suffix in ["", "-wal", "-shm"] {
let source = URL(fileURLWithPath: storeURL.path + suffix)
guard fileManager.fileExists(atPath: source.path) else { continue }
let destination = URL(fileURLWithPath: storeURL.path + ".backup-\(stamp)" + suffix)
try? fileManager.moveItem(at: source, to: destination)
}
}