feat(keging): 애플 건강 운동 유형 선택 — 기타 ↔ 코어 트레이닝 (설정)

- HealthWorkoutType(WorkoutConfig 필드, 워치 자동 동기화) — 폰·워치 저장 모두 반영
- 설정 → 애플 건강에 '운동 유형' 피커, 푸터·도움말 문구 유형 중립으로 갱신
- 신규 문자열 번역(en/ja), 옛 문구 stale 2건 정리 — 전 카탈로그 missing·stale 0
- 하루 다님 연계 조사 결과 CLAUDE.md 기록(종목 다짐·타임테이블=자동 집계,
  운동 시간 지표=워치 실행분만 운동 링 경유 반영)
- 검증: Debug·Release·워치 빌드 그린, 설정 화면 QA

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
This commit is contained in:
songyc macbook 2026-08-29 20:13:37 +09:00
parent 893545e7be
commit f91968f8ce
11 changed files with 207 additions and 57 deletions

View File

@ -133,10 +133,12 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${app_files[@]
- (하루 다님 전례) 인텐트 노출 문구에 'iPhone' 단어 금지 — ITMS-90626 - (하루 다님 전례) 인텐트 노출 문구에 'iPhone' 단어 금지 — ITMS-90626
### 3.7 애플 건강 기록 (`IOS/HealthRecorder.swift`, 워치 `WatchWorkoutSession.swift`, `Shared/HealthKitCalls.swift`) ### 3.7 애플 건강 기록 (`IOS/HealthRecorder.swift`, 워치 `WatchWorkoutSession.swift`, `Shared/HealthKitCalls.swift`)
- **완주한 세트만** 애플 건강에 `.other`('기타') 운동으로 자동 기록 — 중단 세트는 건강에도 없음(§3.1 규칙 일관). 실행한 기기가 기록(폰=사후 HKWorkoutBuilder, 워치=라이브 HKWorkoutSession — 심박 포함, 백그라운드 유지 겸용) - **완주한 세트만** 애플 건강에 운동으로 자동 기록 — 중단 세트는 건강에도 없음(§3.1 규칙 일관). 실행한 기기가 기록(폰=사후 HKWorkoutBuilder, 워치=라이브 HKWorkoutSession — 심박 포함, 백그라운드 유지 겸용). 출처 앱 아이콘은 시스템이 자동 표기
- **운동 유형 선택(설정 → 애플 건강)**: '기타'(기본) ↔ '코어 트레이닝' — `HealthWorkoutType`(WorkoutConfig 필드, 워치 동기화). 하루 다님의 종목 매핑에서는 둘 다 목록 외라 동일하게 '기타 운동'으로 잡힘(호환 무영향)
- **운동 강도(workout effort score, 1~10)**: 설정 → 애플 건강 → '운동 강도 자동 기록'(기본 2·쉬움, '안 함' 가능) — 완주 시 `relateWorkoutEffortSample`로 운동에 연결(애플 운동 앱의 강도 기록과 같은 데이터). iOS 18/워치 11+ — 워치 10은 `#available` 가드로 운동만 기록 - **운동 강도(workout effort score, 1~10)**: 설정 → 애플 건강 → '운동 강도 자동 기록'(기본 2·쉬움, '안 함' 가능) — 완주 시 `relateWorkoutEffortSample`로 운동에 연결(애플 운동 앱의 강도 기록과 같은 데이터). iOS 18/워치 11+ — 워치 10은 `#available` 가드로 운동만 기록
- 권한은 첫 운동 시작 때 요청(쓰기 전용 — 읽기 없음). 거부·미응답이면 조용히 건너뜀(앱 자체 기록·통계 무영향). 워치는 권한 응답이 늦어 운동이 끝났으면 세션을 열지 않는 가드 있음 - 권한은 첫 운동 시작 때 요청(쓰기 전용 — 읽기 없음). 거부·미응답이면 조용히 건너뜀(앱 자체 기록·통계 무영향). 워치는 권한 응답이 늦어 운동이 끝났으면 세션을 열지 않는 가드 있음
- ⚠️ (하루 다님 ITMS-90683 전례) HealthKit 엔타이틀먼트 보유 → `NSHealthUpdateUsageDescription` 필수 — iOS·워치 Info.plist에 3언어(InfoPlist.xcstrings)로 등록됨. 엔타이틀먼트는 iOS·워치 두 타깃(위젯 제외) - ⚠️ (하루 다님 ITMS-90683 전례) HealthKit 엔타이틀먼트 보유 → `NSHealthUpdateUsageDescription` 필수 — iOS·워치 Info.plist에 3언어(InfoPlist.xcstrings)로 등록됨. 엔타이틀먼트는 iOS·워치 두 타깃(위젯 제외)
- **하루 다님 연계(2026-08-29 확인 — 하루 다님 무수정)**: 하루 다님의 종목별 운동 다짐·타임테이블 운동 블록은 HKWorkout을 출처 무관 조회라 케깅 세트가 '기타 운동'으로 자동 집계됨. 단 '운동 시간' 지표는 `appleExerciseTime`(애플 전용 쓰기 불가) 기반이라 **아이폰 실행분은 미반영, 워치 실행분은 운동 링 적립을 통해 반영**(구조적 한계 — 케깅 측 해결 불가)
- `effortScore`는 WorkoutConfig 필드로 워치에 자동 동기화. **WorkoutConfig는 필드 추가 대비 전 필드 decodeIfPresent 커스텀 디코더** — 새 필드를 추가하면 여기에도 기본값을 넣을 것(안 넣으면 기존 사용자 설정이 통째로 초기화됨) - `effortScore`는 WorkoutConfig 필드로 워치에 자동 동기화. **WorkoutConfig는 필드 추가 대비 전 필드 decodeIfPresent 커스텀 디코더** — 새 필드를 추가하면 여기에도 기본값을 넣을 것(안 넣으면 기존 사용자 설정이 통째로 초기화됨)
### 3.6 워치 (`KegingWatch Watch App/`) ### 3.6 워치 (`KegingWatch Watch App/`)

View File

@ -37,6 +37,9 @@ struct AppSettingsView: View {
Text("언어를 바꾸면 앱을 완전히 종료했다가 다시 실행했을 때 적용돼요.") Text("언어를 바꾸면 앱을 완전히 종료했다가 다시 실행했을 때 적용돼요.")
} }
Section { Section {
Picker("운동 유형", selection: $settings.config.healthWorkoutType) {
ForEach(HealthWorkoutType.allCases) { Text($0.label).tag($0) }
}
Picker("운동 강도 자동 기록", selection: $settings.config.effortScore) { Picker("운동 강도 자동 기록", selection: $settings.config.effortScore) {
Text("안 함").tag(0) Text("안 함").tag(0)
ForEach(1...10, id: \.self) { score in ForEach(1...10, id: \.self) { score in
@ -46,7 +49,7 @@ struct AppSettingsView: View {
} header: { } header: {
Text("애플 건강") Text("애플 건강")
} footer: { } footer: {
Text("운동을 끝까지 마치면 그 시간이 애플 건강에 '기타' 운동으로 자동 기록되고, 여기서 고른 운동 강도(1~10)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.") Text("운동을 끝까지 마치면 그 시간이 위에서 고른 유형으로 애플 건강에 자동 기록되고, 운동 강도(1~10)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.")
} }
Section { Section {
LabeledContent("버전", value: versionText) LabeledContent("버전", value: versionText)

View File

@ -29,13 +29,13 @@ final class HealthRecorder {
store.requestAuthorization(toShare: Self.shareTypes, read: nil) { _, _ in } store.requestAuthorization(toShare: Self.shareTypes, read: nil) { _, _ in }
} }
/// '' /// , (/ )
func save(record: SessionRecord, effortScore: Int) { func save(record: SessionRecord, effortScore: Int, workoutType: HealthWorkoutType) {
guard HKHealthStore.isHealthDataAvailable() else { return } guard HKHealthStore.isHealthDataAvailable() else { return }
let start = record.startedAt let start = record.startedAt
let end = start.addingTimeInterval(Double(record.reps * (record.contractSeconds + record.relaxSeconds))) let end = start.addingTimeInterval(Double(record.reps * (record.contractSeconds + record.relaxSeconds)))
let configuration = HKWorkoutConfiguration() let configuration = HKWorkoutConfiguration()
configuration.activityType = .other configuration.activityType = workoutType.activityType
let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: .local()) let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: .local())
Task { @MainActor in Task { @MainActor in
do { do {

View File

@ -92,7 +92,7 @@ struct HelpView: View {
helpRow( helpRow(
symbol: "heart.circle", symbol: "heart.circle",
title: Text("애플 건강 연동"), title: Text("애플 건강 연동"),
body: Text("운동을 끝까지 마치면 그 시간이 애플 건강에 '기타' 운동으로 자동 기록돼요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 설정에서 고른 운동 강도(1~10)도 함께 기록돼요. 워치에서 하면 워치가 심박수와 함께 기록해요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.") body: Text("운동을 끝까지 마치면 그 시간이 애플 건강에 운동으로 자동 기록돼요. 운동 유형(기타·코어 트레이닝)과 운동 강도(1~10)는 설정에서 고를 수 있어요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 워치에서 하면 워치가 심박수와 함께 기록해요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.")
) )
helpRow( helpRow(
symbol: "paintbrush", symbol: "paintbrush",

View File

@ -25,12 +25,6 @@
}, },
"%lld · %@": { "%lld · %@": {
"localizations": { "localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld · %2$@"
}
},
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -42,6 +36,12 @@
"state": "translated", "state": "translated",
"value": "%lld · %@" "value": "%lld · %@"
} }
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld · %2$@"
}
} }
} }
}, },
@ -207,6 +207,22 @@
} }
} }
}, },
"기타": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Other"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "その他"
}
}
}
},
"날짜": { "날짜": {
"localizations": { "localizations": {
"en": { "en": {
@ -1035,6 +1051,22 @@
} }
} }
}, },
"운동 유형": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Workout Type"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ワークアウトの種類"
}
}
}
},
"운동 중 아래의 중단 버튼을 누르면 확인 후 세트가 끝나요. 중단한 세트는 아예 하지 않은 것으로 처리돼서 기록과 통계에 남지 않아요. 끝까지 마친 세트만 기록돼요.": { "운동 중 아래의 중단 버튼을 누르면 확인 후 세트가 끝나요. 중단한 세트는 아예 하지 않은 것으로 처리돼서 기록과 통계에 남지 않아요. 끝까지 마친 세트만 기록돼요.": {
"localizations": { "localizations": {
"en": { "en": {
@ -1067,34 +1099,34 @@
} }
} }
}, },
"운동을 끝까지 마치면 그 시간이 애플 건강에 '기타' 운동으로 자동 기록돼요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 설정에서 고른 운동 강도(1~10)도 함께 기록돼요. 워치에서 하면 워치가 심박수와 함께 기록해요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.": { "운동을 끝까지 마치면 그 시간이 애플 건강에 운동으로 자동 기록돼요. 운동 유형(기타·코어 트레이닝)과 운동 강도(1~10)는 설정에서 고를 수 있어요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 워치에서 하면 워치가 심박수와 함께 기록해요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.": {
"localizations": { "localizations": {
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "When you finish a set, that time is automatically saved to Apple Health as an 'Other' workout. You'll be asked for Health permission the first time you start, and after allowing it everything is saved automatically. The effort rating (110) chosen in Settings is recorded too. Workouts started on the watch are recorded by the watch, including heart rate. Stopped sets are not saved to Health either, and you can turn off Keging's access in the Health app anytime." "value": "When you finish a set, that time is automatically saved to Apple Health as a workout. You can choose the workout type (Other or Core Training) and the effort rating (110) in Settings. You'll be asked for Health permission the first time you start, and after allowing it everything is saved automatically. Workouts started on the watch are recorded by the watch, including heart rate. Stopped sets are not saved to Health either, and you can turn off Keging's access in the Health app anytime."
} }
}, },
"ja": { "ja": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "セットを最後まで終えると、その時間がヘルスケアに「その他」のワークアウトとして自動記録されます。初回開始時にヘルスケアの許可を求められ、許可すればその後は自動で保存されます。設定で選んだ運動強度1〜10も一緒に記録されます。ウォッチから実行した場合は、心拍数とともにウォッチが記録します。中断したセットはヘルスケアにも記録されず、記録したくない場合はヘルスケアアプリでKegingのアクセスをいつでもオフにできます。" "value": "セットを最後まで終えると、その時間がヘルスケアにワークアウトとして自動記録されます。ワークアウトの種類その他・コアトレーニングと運動強度1〜10は設定で選べます。初回開始時にヘルスケアの許可を求められ、許可すればその後は自動で保存されます。ウォッチから実行した場合は、心拍数とともにウォッチが記録します。中断したセットはヘルスケアにも記録されず、記録したくない場合はヘルスケアアプリでKegingのアクセスをいつでもオフにできます。"
} }
} }
} }
}, },
"운동을 끝까지 마치면 그 시간이 애플 건강에 '기타' 운동으로 자동 기록되고, 여기서 고른 운동 강도(1~10)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.": { "운동을 끝까지 마치면 그 시간이 위에서 고른 유형으로 애플 건강에 자동 기록되고, 운동 강도(1~10)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.": {
"localizations": { "localizations": {
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "When you finish a set, that time is automatically saved to Apple Health as an 'Other' workout, along with the effort rating (110) you choose here. If you don't want this, turn off Keging's access in the Health app." "value": "When you finish a set, that time is automatically saved to Apple Health as the workout type chosen above, along with the effort rating (110). If you don't want this, turn off Keging's access in the Health app."
} }
}, },
"ja": { "ja": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
"value": "セットを最後まで終えると、その時間がヘルスケアに「その他」のワークアウトとして自動記録され、ここで選んだ運動強度1〜10も一緒に保存されます。記録したくない場合は、ヘルスケアアプリでKegingのアクセスをオフにしてください。" "value": "セットを最後まで終えると、その時間が上で選んだ種類のワークアウトとしてヘルスケアに自動記録され、運動強度1〜10も一緒に保存されます。記録したくない場合は、ヘルスケアアプリでKegingのアクセスをオフにしてください。"
} }
} }
} }
@ -1547,6 +1579,22 @@
} }
} }
}, },
"코어 트레이닝": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Core Training"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "コアトレーニング"
}
}
}
},
"테마": { "테마": {
"localizations": { "localizations": {
"en": { "en": {

View File

@ -35,7 +35,12 @@ final class WorkoutCoordinator {
BackgroundAudioKeeper.shared.stop() BackgroundAudioKeeper.shared.stop()
UIApplication.shared.isIdleTimerDisabled = false UIApplication.shared.isIdleTimerDisabled = false
if completed, let record { if completed, let record {
HealthRecorder.shared.save(record: record, effortScore: SettingsStore.shared.config.effortScore) let config = SettingsStore.shared.config
HealthRecorder.shared.save(
record: record,
effortScore: config.effortScore,
workoutType: config.healthWorkoutType
)
} }
} }
} }

View File

@ -3,12 +3,6 @@
"strings": { "strings": {
"%lld / %lld": { "%lld / %lld": {
"localizations": { "localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld / %2$lld"
}
},
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -20,6 +14,12 @@
"state": "translated", "state": "translated",
"value": "%lld / %lld" "value": "%lld / %lld"
} }
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld / %2$lld"
}
} }
} }
}, },
@ -51,6 +51,22 @@
} }
} }
}, },
"기타": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Other"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "その他"
}
}
}
},
"두 번": { "두 번": {
"localizations": { "localizations": {
"en": { "en": {
@ -101,12 +117,6 @@
}, },
"수축 %lld초 · 이완 %lld초 · %lld회": { "수축 %lld초 · 이완 %lld초 · %lld회": {
"localizations": { "localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회"
}
},
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -118,6 +128,12 @@
"state": "translated", "state": "translated",
"value": "締める%lld秒 · ゆるめる%lld秒 · %lld回" "value": "締める%lld秒 · ゆるめる%lld秒 · %lld回"
} }
},
"ko": {
"stringUnit": {
"state": "new",
"value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회"
}
} }
} }
}, },
@ -217,6 +233,22 @@
} }
} }
}, },
"코어 트레이닝": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Core Training"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "コアトレーニング"
}
}
}
},
"한 번": { "한 번": {
"localizations": { "localizations": {
"en": { "en": {
@ -235,12 +267,6 @@
}, },
"현재 %lld번째, 전체 %lld회": { "현재 %lld번째, 전체 %lld회": {
"localizations": { "localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "현재 %1$lld번째, 전체 %2$lld회"
}
},
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -252,6 +278,12 @@
"state": "translated", "state": "translated",
"value": "%lld回目、全%lld回" "value": "%lld回目、全%lld回"
} }
},
"ko": {
"stringUnit": {
"state": "new",
"value": "현재 %1$lld번째, 전체 %2$lld회"
}
} }
} }
}, },

View File

@ -39,7 +39,7 @@ final class WatchWorkoutSession: NSObject {
// //
guard KegelEngine.shared.isRunning, session == nil else { return } guard KegelEngine.shared.isRunning, session == nil else { return }
let configuration = HKWorkoutConfiguration() let configuration = HKWorkoutConfiguration()
configuration.activityType = .other configuration.activityType = SettingsStore.shared.config.healthWorkoutType.activityType
configuration.locationType = .unknown configuration.locationType = .unknown
let newSession = try HKWorkoutSession(healthStore: store, configuration: configuration) let newSession = try HKWorkoutSession(healthStore: store, configuration: configuration)
let newBuilder = newSession.associatedWorkoutBuilder() let newBuilder = newSession.associatedWorkoutBuilder()

View File

@ -3,12 +3,6 @@
"strings": { "strings": {
"%lld / %lld": { "%lld / %lld": {
"localizations": { "localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld / %2$lld"
}
},
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -20,17 +14,17 @@
"state": "translated", "state": "translated",
"value": "%lld / %lld" "value": "%lld / %lld"
} }
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld / %2$lld"
}
} }
} }
}, },
"%lld/%lld": { "%lld/%lld": {
"localizations": { "localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld/%2$lld"
}
},
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -42,6 +36,28 @@
"state": "translated", "state": "translated",
"value": "%lld/%lld" "value": "%lld/%lld"
} }
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$lld/%2$lld"
}
}
}
},
"기타": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Other"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "その他"
}
} }
} }
}, },
@ -95,12 +111,6 @@
}, },
"수축 %lld초 · 이완 %lld초 · %lld회": { "수축 %lld초 · 이완 %lld초 · %lld회": {
"localizations": { "localizations": {
"ko": {
"stringUnit": {
"state": "new",
"value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회"
}
},
"en": { "en": {
"stringUnit": { "stringUnit": {
"state": "translated", "state": "translated",
@ -112,6 +122,12 @@
"state": "translated", "state": "translated",
"value": "締める%lld秒 · ゆるめる%lld秒 · %lld回" "value": "締める%lld秒 · ゆるめる%lld秒 · %lld回"
} }
},
"ko": {
"stringUnit": {
"state": "new",
"value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회"
}
} }
} }
}, },
@ -147,6 +163,22 @@
} }
} }
}, },
"코어 트레이닝": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Core Training"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "コアトレーニング"
}
}
}
},
"한 번": { "한 번": {
"localizations": { "localizations": {
"en": { "en": {

View File

@ -7,6 +7,15 @@
import HealthKit import HealthKit
extension HealthWorkoutType {
var activityType: HKWorkoutActivityType {
switch self {
case .other: .other
case .coreTraining: .coreTraining
}
}
}
nonisolated enum HealthKitCalls { nonisolated enum HealthKitCalls {
static func beginCollection(_ builder: HKWorkoutBuilder, at date: Date) async throws { static func beginCollection(_ builder: HKWorkoutBuilder, at date: Date) async throws {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in

View File

@ -34,6 +34,20 @@ nonisolated enum HapticPattern: String, CaseIterable, Identifiable, Codable {
} }
} }
/// (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 { nonisolated struct WorkoutConfig: Codable, Equatable {
var contractSeconds: Int = 10 var contractSeconds: Int = 10
@ -43,6 +57,8 @@ nonisolated struct WorkoutConfig: Codable, Equatable {
var relaxPattern: HapticPattern = .once var relaxPattern: HapticPattern = .once
/// (0 = , 1~10) /// (0 = , 1~10)
var effortScore: Int = 2 var effortScore: Int = 2
///
var healthWorkoutType: HealthWorkoutType = .other
init( init(
contractSeconds: Int = 10, contractSeconds: Int = 10,
@ -50,7 +66,8 @@ nonisolated struct WorkoutConfig: Codable, Equatable {
reps: Int = 20, reps: Int = 20,
contractPattern: HapticPattern = .twice, contractPattern: HapticPattern = .twice,
relaxPattern: HapticPattern = .once, relaxPattern: HapticPattern = .once,
effortScore: Int = 2 effortScore: Int = 2,
healthWorkoutType: HealthWorkoutType = .other
) { ) {
self.contractSeconds = contractSeconds self.contractSeconds = contractSeconds
self.relaxSeconds = relaxSeconds self.relaxSeconds = relaxSeconds
@ -58,6 +75,7 @@ nonisolated struct WorkoutConfig: Codable, Equatable {
self.contractPattern = contractPattern self.contractPattern = contractPattern
self.relaxPattern = relaxPattern self.relaxPattern = relaxPattern
self.effortScore = effortScore self.effortScore = effortScore
self.healthWorkoutType = healthWorkoutType
} }
// JSON decodeIfPresent // JSON decodeIfPresent
@ -69,6 +87,7 @@ nonisolated struct WorkoutConfig: Codable, Equatable {
contractPattern = try container.decodeIfPresent(HapticPattern.self, forKey: .contractPattern) ?? .twice contractPattern = try container.decodeIfPresent(HapticPattern.self, forKey: .contractPattern) ?? .twice
relaxPattern = try container.decodeIfPresent(HapticPattern.self, forKey: .relaxPattern) ?? .once relaxPattern = try container.decodeIfPresent(HapticPattern.self, forKey: .relaxPattern) ?? .once
effortScore = try container.decodeIfPresent(Int.self, forKey: .effortScore) ?? 2 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 totalDuration: TimeInterval { Double(reps * (contractSeconds + relaxSeconds)) }