// // DiaryLock.swift // Haru_Danim // // 일기 잠금 (설정 → 일기, 기기 로컬) — 켜면 일기 탭 진입 시 생체 인증 // (Face ID·Touch ID·Optic ID) 또는 기기 암호로 잠금을 해제해야 한다. // .deviceOwnerAuthentication 정책이라 기기가 지원하는 수단을 시스템이 알아서 고르고 // 생체 실패 시 암호 폴백까지 처리한다 — 어떤 기기에서도 별도 분기 없이 동작. // 앱이 백그라운드로 가면 다시 잠긴다 (Haru_DanimApp scenePhase). // import Foundation import LocalAuthentication import Observation @MainActor @Observable final class DiaryLock { static let shared = DiaryLock() static let enabledKey = "diary.lockEnabled" /// 잠금 사용 여부 — 보안 설정은 기기별이 자연스러워 standard(비동기화)에 저장 var isEnabled: Bool = UserDefaults.standard.bool(forKey: DiaryLock.enabledKey) { didSet { UserDefaults.standard.set(isEnabled, forKey: Self.enabledKey) } } /// 이번 포그라운드 세션에서 해제됐는지 (백그라운드 진입 시 리셋) private(set) var isUnlocked = false private var authInFlight = false var requiresUnlock: Bool { #if DEBUG // 검증용: -diaryLockScreen YES → 인증 없이 잠금 게이트 화면 강제 (스크린샷용) if UserDefaults.standard.bool(forKey: "diaryLockScreen") { return true } #endif return isEnabled && !isUnlocked } /// 앱이 백그라운드로 가면 다시 잠근다 func relock() { isUnlocked = false } /// 생체/암호 인증. 잠글 수단이 아예 없는 기기(암호 미설정)는 통과시킨다 — /// 시스템이 해제 수단을 제공할 수 없는 상태에서 일기가 영영 잠기는 것 방지. func authenticate(reason: String, onDone: @escaping @MainActor (Bool) -> Void) { guard !authInFlight else { return } authInFlight = true let context = LAContext() var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else { authInFlight = false isUnlocked = true onDone(true) return } context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in Task { @MainActor in self.authInFlight = false if success { self.isUnlocked = true } onDone(success) } } } }