// // WorkoutSettings.swift // Keging // // 세트 구성(수축·이완·횟수)과 진동 패턴 — 폰에서 설정, 워치로 동기화 // import Foundation import Combine /// 진동 패턴 — 수축/이완 각각 선택. 횟수 세기가 아니라 첫 순간의 질감으로 구분한다 /// (짧은 톡 / 빠른 톡톡 / 드르륵 연타 / 끊기지 않는 긴 지잉) nonisolated enum HapticPattern: String, CaseIterable, Identifiable, Codable { case short, doubleTap, rattle, long var id: String { rawValue } var label: String { switch self { case .short: String(localized: "톡 (짧게)") case .doubleTap: String(localized: "톡톡") case .rattle: String(localized: "드르륵") case .long: String(localized: "지잉 (길게)") } } // 재생 방식은 플랫폼별: iOS = 완료 체이닝 시퀀스(IOS/Haptics.swift), // 워치 = 패턴별 단일 시스템 햅틱(WatchHaptics.swift) /// 1.0 초기 패턴(횟수 기반) 저장값 이관 init?(migrating raw: String?) { guard let raw else { return nil } if let direct = HapticPattern(rawValue: raw) { self = direct return } switch raw { case "once": self = .short case "twice": self = .doubleTap case "thrice", "heartbeat": self = .rattle 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 = .short /// 완주 시 애플 건강에 함께 기록할 운동 강도 (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 = .short, 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)) ?? .short 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) } } }