// // WorkoutSettings.swift // Keging // // 세트 구성(수축·이완·횟수)과 진동 패턴 — 폰에서 설정, 워치로 동기화 // import Foundation import Combine /// 진동 패턴 3종 — 수축/이완 각각 선택. 첫 순간의 질감으로 구분한다 /// (기본=짧은 한 번 / 톡톡=빠른 두 번 / 지이이잉=아주 긴 연속 진동) nonisolated enum HapticPattern: String, CaseIterable, Identifiable, Codable { case basic, doubleTap, long var id: String { rawValue } var label: String { switch self { case .basic: String(localized: "기본") case .doubleTap: String(localized: "톡톡") case .long: String(localized: "지이이잉") } } /// Core Haptics 이벤트 스펙 (시작 시각, 길이, 세기, 날카로움) — 아이폰·워치 공용. /// 포그라운드 정밀 재생용(HapticEnginePlayer). 톡톡의 시작 간격 0.28초(사용자 요구 0.5초 미만), /// 지이이잉은 1.2초 연속 — 시스템 바이브로는 불가능한 형태라 Core Haptics를 쓴다 var hapticEvents: [(time: Double, duration: Double, intensity: Float, sharpness: Float)] { switch self { case .basic: [(0, 0.25, 1.0, 0.55)] case .doubleTap: [(0, 0.14, 1.0, 0.65), (0.28, 0.14, 1.0, 0.65)] case .long: [(0, 1.2, 1.0, 0.45)] } } /// 구버전 패턴 저장값 이관 (1.0 횟수 기반 · 4종 시절) init?(migrating raw: String?) { guard let raw else { return nil } if let direct = HapticPattern(rawValue: raw) { self = direct return } switch raw { case "once", "short": self = .basic case "twice", "thrice", "heartbeat", "rattle": self = .doubleTap default: return nil } } } /// 애플 건강에 저장할 운동 유형 — 취향 선택 (HKWorkoutActivityType 매핑은 HealthKitCalls.swift) nonisolated enum HealthWorkoutType: String, CaseIterable, Identifiable, Codable { case other, coreTraining var id: String { rawValue } var label: String { switch self { case .other: String(localized: "기타") case .coreTraining: String(localized: "코어 트레이닝") } } } /// 세트 구성 — 세트 = (수축 → 이완) × 횟수 nonisolated struct WorkoutConfig: Codable, Equatable { /// 시작 버튼 → 첫 수축 사이의 준비 카운트다운 (0~10초) var prepSeconds: Int = 3 var contractSeconds: Int = 10 var relaxSeconds: Int = 5 var reps: Int = 20 /// 기본: 수축 = 지이이잉(조이고 유지), 이완 = 기본(탁 풀기) var contractPattern: HapticPattern = .long var relaxPattern: HapticPattern = .basic /// 완주 시 애플 건강에 함께 기록할 운동 강도 (0 = 기록 안 함, 1~10) var effortScore: Int = 2 /// 애플 건강에 저장할 운동 유형 var healthWorkoutType: HealthWorkoutType = .other init( prepSeconds: Int = 3, contractSeconds: Int = 10, relaxSeconds: Int = 5, reps: Int = 20, contractPattern: HapticPattern = .long, relaxPattern: HapticPattern = .basic, effortScore: Int = 2, healthWorkoutType: HealthWorkoutType = .other ) { self.prepSeconds = prepSeconds self.contractSeconds = contractSeconds self.relaxSeconds = relaxSeconds self.reps = reps self.contractPattern = contractPattern self.relaxPattern = relaxPattern self.effortScore = effortScore self.healthWorkoutType = healthWorkoutType } // 필드 추가에도 기존 저장 JSON이 깨지지 않게 전 필드 decodeIfPresent init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) prepSeconds = try container.decodeIfPresent(Int.self, forKey: .prepSeconds) ?? 3 contractSeconds = try container.decodeIfPresent(Int.self, forKey: .contractSeconds) ?? 10 relaxSeconds = try container.decodeIfPresent(Int.self, forKey: .relaxSeconds) ?? 5 reps = try container.decodeIfPresent(Int.self, forKey: .reps) ?? 20 contractPattern = HapticPattern(migrating: try container.decodeIfPresent(String.self, forKey: .contractPattern)) ?? .long relaxPattern = HapticPattern(migrating: try container.decodeIfPresent(String.self, forKey: .relaxPattern)) ?? .basic effortScore = try container.decodeIfPresent(Int.self, forKey: .effortScore) ?? 2 healthWorkoutType = try container.decodeIfPresent(HealthWorkoutType.self, forKey: .healthWorkoutType) ?? .other } var totalDuration: TimeInterval { Double(reps * (contractSeconds + relaxSeconds)) } var summaryText: String { String(localized: "수축 \(contractSeconds)초 · 이완 \(relaxSeconds)초 · \(reps)회") } } @MainActor final class SettingsStore: ObservableObject { static let shared = SettingsStore() @Published var config: WorkoutConfig { didSet { save() if config != oldValue { onConfigChange?(config) } } } /// 폰 → 워치 동기화 훅 (iOS에서만 연결) var onConfigChange: ((WorkoutConfig) -> Void)? private static let key = "workout.config.v1" private init() { if let data = AppGroup.defaults.data(forKey: Self.key), let saved = try? JSONDecoder().decode(WorkoutConfig.self, from: data) { config = saved } else { config = WorkoutConfig() } } private func save() { if let data = try? JSONEncoder().encode(config) { AppGroup.defaults.set(data, forKey: Self.key) } } }