mycode/myApp/Keging/Shared/WorkoutSettings.swift
songyc macbook f5f8d3bde4 fix(keging): 실기기 1차 피드백 3건 — DI 백그라운드 멈춤·진동 패턴 동일·칼로리 0
① 다이나믹 아일랜드 멈춤(2중 방어):
- 오디오 키퍼 하드닝 — volume 0.1(0.0은 유지 풀림 사례)·prepareToPlay·인터럽션/씬 전환 revive()
- 엔진을 벽시계 기준으로 재설계 — 전 국면이 sessionStart로부터 결정적(computeTarget),
  타이머는 경계 재장전용, scenePhase .active에서 resyncAfterWake()로 정확한 국면 자가 교정

② 진동 패턴 동일(톡=톡톡): 시스템 바이브는 재생 중 재호출이 무시됨(실측) —
  시간 오프셋 방식 폐기, AudioServicesPlaySystemSoundWithCompletion 완료 체이닝으로 재작성
  (톡 1방 / 톡톡 0.25s 쉬고 1방 / 드르륵 0.15s 3연타 / 지잉 즉시 이어붙인 3방)

③ 운동 칼로리 0: 빌더 운동은 에너지 샘플 직접 추가 필요 —
  iOS는 MET 추정(1.8+강도×0.17, 70kg 가정) activeEnergyBurned 샘플 add,
  워치는 심박·활성 에너지 읽기 권한 추가로 센서 실측 수집, 권한 문구 3언어 갱신

검증: Debug·Release·워치 빌드 그린, 완주 경로 시뮬 재검증(엔진 재설계 후).
DI 갱신·진동 구분·칼로리는 실기기 재확인 필요(재실행 시 '활성 에너지' 권한 시트 뜸)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
2026-08-29 22:42:00 +09:00

146 lines
5.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 {
/// (010)
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)
}
}
}