// // WatchPayload.swift // Haru_Danim // // iPhone ↔ Apple Watch 동기화 페이로드 (CLAUDE.md §9) // - iOS 앱 타깃 + 워치 앱 타깃 + 워치 위젯 확장에 함께 포함되는 순수 Codable 구조체 // - iPhone이 스냅숏을 만들어 WCSession으로 보내고, 워치는 UI 표시와 // 컴플리케이션(App Group defaults에 캐시)에 사용한다 // import Foundation nonisolated enum WatchSync { /// WCSession 메시지/컨텍스트에서 스냅숏 Data가 담기는 키 static let snapshotKey = "snapshot" /// 워치 App Group defaults에 최신 스냅숏을 캐시하는 키 (컴플리케이션용) static let cachedSnapshotKey = "watch.cachedSnapshot" /// 메시지 명령 키 static let commandKey = "command" static let actionIDKey = "actionID" /// 명령이 워치에서 만들어진 시각 (초, since 1970) — 큐 지연 전달 판별용 static let sentAtKey = "sentAt" /// 명령 종류 static let runCommand = "run" static let refreshCommand = "refresh" /// 큐 채널(transferUserInfo)로 지연 도착한 실행 명령을 무시하는 한도 static let queuedRunMaxAge: TimeInterval = 5 * 60 } /// 워치에 보여줄 전체 상태 스냅숏 nonisolated struct WatchSnapshot: Codable { var generatedAt: Date = .now var isPremium: Bool = false var tags: [WatchTagInfo] = [] var actions: [WatchActionInfo] = [] var goals: [WatchGoalInfo] = [] /// 현재 측정 중 대표 행동 (다이나믹 아일랜드와 동일 기준, CLAUDE.md §9.2-1) var runningName: String? var runningSymbol: String? var runningStartAt: Date? /// 대표 외에 함께 측정 중인 행동 개수 var extraRunningCount: Int = 0 func encoded() -> Data? { try? JSONEncoder().encode(self) } static func decode(_ data: Data) -> WatchSnapshot? { try? JSONDecoder().decode(WatchSnapshot.self, from: data) } } nonisolated struct WatchTagInfo: Codable, Identifiable, Hashable { var id: UUID var name: String var colorHex: String } nonisolated struct WatchActionInfo: Codable, Identifiable, Hashable { var id: UUID var name: String var symbolName: String var colorHex: String var isCount: Bool /// 오늘(논리적 하루) 누적값 — 시간형은 초, 횟수형은 회 (스냅숏 시점 기준) var todayValue: Double var isRunning: Bool /// 측정 중일 때 실시간 타이머 기준 시각 (스냅숏 시점 - 오늘 누적초) var tickingBase: Date? /// 소속 꼬리표 id들 (비어 있으면 '꼬리표 없음' 그룹) var tagIDs: [UUID] /// 앱에서 즐겨찾기로 등록한 행동 — 워치 첫 화면의 즐겨찾기 목록에 모인다. /// optional인 이유: 구버전 워치에 캐시된 스냅숏(키 없음)도 디코딩되게 하기 위함 var isFavorite: Bool? = nil } nonisolated struct WatchGoalInfo: Codable, Identifiable, Hashable { var id: UUID var title: String var symbolName: String var colorHex: String var dayRatio: Double var weekRatio: Double var monthRatio: Double var quests: [WatchQuestInfo] func ratio(forSpanRaw raw: String) -> Double { switch raw { case "week": return weekRatio case "month": return monthRatio default: return dayRatio } } } nonisolated struct WatchQuestInfo: Codable, Identifiable, Hashable { var id: UUID var name: String var symbolName: String var colorHex: String var dayRatio: Double var weekRatio: Double var monthRatio: Double /// '이하 유지' 다짐 여부 (달성 표현을 다르게, CLAUDE.md §9.2-3) var isAtMost: Bool var dayAchieved: Bool var weekAchieved: Bool var monthAchieved: Bool func ratio(forSpanRaw raw: String) -> Double { switch raw { case "week": return weekRatio case "month": return monthRatio default: return dayRatio } } func achieved(forSpanRaw raw: String) -> Bool { switch raw { case "week": return weekAchieved case "month": return monthAchieved default: return dayAchieved } } }