From f91968f8ce168703e4c8a867c8c91e773dd4cc8d Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Sat, 29 Aug 2026 20:13:37 +0900 Subject: [PATCH] =?UTF-8?q?feat(keging):=20=EC=95=A0=ED=94=8C=20=EA=B1=B4?= =?UTF-8?q?=EA=B0=95=20=EC=9A=B4=EB=8F=99=20=EC=9C=A0=ED=98=95=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=E2=80=94=20=EA=B8=B0=ED=83=80=20=E2=86=94=20?= =?UTF-8?q?=EC=BD=94=EC=96=B4=20=ED=8A=B8=EB=A0=88=EC=9D=B4=EB=8B=9D=20(?= =?UTF-8?q?=EC=84=A4=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HealthWorkoutType(WorkoutConfig 필드, 워치 자동 동기화) — 폰·워치 저장 모두 반영 - 설정 → 애플 건강에 '운동 유형' 피커, 푸터·도움말 문구 유형 중립으로 갱신 - 신규 문자열 번역(en/ja), 옛 문구 stale 2건 정리 — 전 카탈로그 missing·stale 0 - 하루 다님 연계 조사 결과 CLAUDE.md 기록(종목 다짐·타임테이블=자동 집계, 운동 시간 지표=워치 실행분만 운동 링 경유 반영) - 검증: Debug·Release·워치 빌드 그린, 설정 화면 QA Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT --- myApp/Keging/CLAUDE.md | 4 +- myApp/Keging/IOS/AppSettingsView.swift | 5 +- myApp/Keging/IOS/HealthRecorder.swift | 6 +- myApp/Keging/IOS/HelpView.swift | 2 +- myApp/Keging/IOS/Localizable.xcstrings | 72 +++++++++++++++---- myApp/Keging/IOS/WorkoutCoordinator.swift | 7 +- .../Localizable.xcstrings | 68 +++++++++++++----- .../WatchWorkoutSession.swift | 2 +- .../KegingWidgets/Localizable.xcstrings | 68 +++++++++++++----- myApp/Keging/Shared/HealthKitCalls.swift | 9 +++ myApp/Keging/Shared/WorkoutSettings.swift | 21 +++++- 11 files changed, 207 insertions(+), 57 deletions(-) diff --git a/myApp/Keging/CLAUDE.md b/myApp/Keging/CLAUDE.md index 43d83ca..f279ddf 100644 --- a/myApp/Keging/CLAUDE.md +++ b/myApp/Keging/CLAUDE.md @@ -133,10 +133,12 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${app_files[@] - (하루 다님 전례) 인텐트 노출 문구에 'iPhone' 단어 금지 — ITMS-90626 ### 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` 가드로 운동만 기록 - 권한은 첫 운동 시작 때 요청(쓰기 전용 — 읽기 없음). 거부·미응답이면 조용히 건너뜀(앱 자체 기록·통계 무영향). 워치는 권한 응답이 늦어 운동이 끝났으면 세션을 열지 않는 가드 있음 - ⚠️ (하루 다님 ITMS-90683 전례) HealthKit 엔타이틀먼트 보유 → `NSHealthUpdateUsageDescription` 필수 — iOS·워치 Info.plist에 3언어(InfoPlist.xcstrings)로 등록됨. 엔타이틀먼트는 iOS·워치 두 타깃(위젯 제외) +- **하루 다님 연계(2026-08-29 확인 — 하루 다님 무수정)**: 하루 다님의 종목별 운동 다짐·타임테이블 운동 블록은 HKWorkout을 출처 무관 조회라 케깅 세트가 '기타 운동'으로 자동 집계됨. 단 '운동 시간' 지표는 `appleExerciseTime`(애플 전용 쓰기 불가) 기반이라 **아이폰 실행분은 미반영, 워치 실행분은 운동 링 적립을 통해 반영**(구조적 한계 — 케깅 측 해결 불가) - `effortScore`는 WorkoutConfig 필드로 워치에 자동 동기화. **WorkoutConfig는 필드 추가 대비 전 필드 decodeIfPresent 커스텀 디코더** — 새 필드를 추가하면 여기에도 기본값을 넣을 것(안 넣으면 기존 사용자 설정이 통째로 초기화됨) ### 3.6 워치 (`KegingWatch Watch App/`) diff --git a/myApp/Keging/IOS/AppSettingsView.swift b/myApp/Keging/IOS/AppSettingsView.swift index 4a61799..b852a45 100644 --- a/myApp/Keging/IOS/AppSettingsView.swift +++ b/myApp/Keging/IOS/AppSettingsView.swift @@ -37,6 +37,9 @@ struct AppSettingsView: View { Text("언어를 바꾸면 앱을 완전히 종료했다가 다시 실행했을 때 적용돼요.") } Section { + Picker("운동 유형", selection: $settings.config.healthWorkoutType) { + ForEach(HealthWorkoutType.allCases) { Text($0.label).tag($0) } + } Picker("운동 강도 자동 기록", selection: $settings.config.effortScore) { Text("안 함").tag(0) ForEach(1...10, id: \.self) { score in @@ -46,7 +49,7 @@ struct AppSettingsView: View { } header: { Text("애플 건강") } footer: { - Text("운동을 끝까지 마치면 그 시간이 애플 건강에 '기타' 운동으로 자동 기록되고, 여기서 고른 운동 강도(1~10)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.") + Text("운동을 끝까지 마치면 그 시간이 위에서 고른 유형으로 애플 건강에 자동 기록되고, 운동 강도(1~10)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.") } Section { LabeledContent("버전", value: versionText) diff --git a/myApp/Keging/IOS/HealthRecorder.swift b/myApp/Keging/IOS/HealthRecorder.swift index a37de11..e99b088 100644 --- a/myApp/Keging/IOS/HealthRecorder.swift +++ b/myApp/Keging/IOS/HealthRecorder.swift @@ -29,13 +29,13 @@ final class HealthRecorder { 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 } let start = record.startedAt let end = start.addingTimeInterval(Double(record.reps * (record.contractSeconds + record.relaxSeconds))) let configuration = HKWorkoutConfiguration() - configuration.activityType = .other + configuration.activityType = workoutType.activityType let builder = HKWorkoutBuilder(healthStore: store, configuration: configuration, device: .local()) Task { @MainActor in do { diff --git a/myApp/Keging/IOS/HelpView.swift b/myApp/Keging/IOS/HelpView.swift index 0d81292..68d6890 100644 --- a/myApp/Keging/IOS/HelpView.swift +++ b/myApp/Keging/IOS/HelpView.swift @@ -92,7 +92,7 @@ struct HelpView: View { helpRow( symbol: "heart.circle", title: Text("애플 건강 연동"), - body: Text("운동을 끝까지 마치면 그 시간이 애플 건강에 '기타' 운동으로 자동 기록돼요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 설정에서 고른 운동 강도(1~10)도 함께 기록돼요. 워치에서 하면 워치가 심박수와 함께 기록해요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.") + body: Text("운동을 끝까지 마치면 그 시간이 애플 건강에 운동으로 자동 기록돼요. 운동 유형(기타·코어 트레이닝)과 운동 강도(1~10)는 설정에서 고를 수 있어요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 워치에서 하면 워치가 심박수와 함께 기록해요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.") ) helpRow( symbol: "paintbrush", diff --git a/myApp/Keging/IOS/Localizable.xcstrings b/myApp/Keging/IOS/Localizable.xcstrings index b939609..ab8575d 100644 --- a/myApp/Keging/IOS/Localizable.xcstrings +++ b/myApp/Keging/IOS/Localizable.xcstrings @@ -25,12 +25,6 @@ }, "%lld · %@": { "localizations": { - "ko": { - "stringUnit": { - "state": "new", - "value": "%1$lld · %2$@" - } - }, "en": { "stringUnit": { "state": "translated", @@ -42,6 +36,12 @@ "state": "translated", "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": { "en": { @@ -1035,6 +1051,22 @@ } } }, + "운동 유형": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Workout Type" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ワークアウトの種類" + } + } + } + }, "운동 중 아래의 중단 버튼을 누르면 확인 후 세트가 끝나요. 중단한 세트는 아예 하지 않은 것으로 처리돼서 기록과 통계에 남지 않아요. 끝까지 마친 세트만 기록돼요.": { "localizations": { "en": { @@ -1067,34 +1099,34 @@ } } }, - "운동을 끝까지 마치면 그 시간이 애플 건강에 '기타' 운동으로 자동 기록돼요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 설정에서 고른 운동 강도(1~10)도 함께 기록돼요. 워치에서 하면 워치가 심박수와 함께 기록해요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.": { + "운동을 끝까지 마치면 그 시간이 애플 건강에 운동으로 자동 기록돼요. 운동 유형(기타·코어 트레이닝)과 운동 강도(1~10)는 설정에서 고를 수 있어요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 워치에서 하면 워치가 심박수와 함께 기록해요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.": { "localizations": { "en": { "stringUnit": { "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 (1–10) 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 (1–10) 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": { "stringUnit": { "state": "translated", - "value": "セットを最後まで終えると、その時間がヘルスケアに「その他」のワークアウトとして自動記録されます。初回開始時にヘルスケアの許可を求められ、許可すればその後は自動で保存されます。設定で選んだ運動強度(1〜10)も一緒に記録されます。ウォッチから実行した場合は、心拍数とともにウォッチが記録します。中断したセットはヘルスケアにも記録されず、記録したくない場合はヘルスケアアプリでKegingのアクセスをいつでもオフにできます。" + "value": "セットを最後まで終えると、その時間がヘルスケアにワークアウトとして自動記録されます。ワークアウトの種類(その他・コアトレーニング)と運動強度(1〜10)は設定で選べます。初回開始時にヘルスケアの許可を求められ、許可すればその後は自動で保存されます。ウォッチから実行した場合は、心拍数とともにウォッチが記録します。中断したセットはヘルスケアにも記録されず、記録したくない場合はヘルスケアアプリでKegingのアクセスをいつでもオフにできます。" } } } }, - "운동을 끝까지 마치면 그 시간이 애플 건강에 '기타' 운동으로 자동 기록되고, 여기서 고른 운동 강도(1~10)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.": { + "운동을 끝까지 마치면 그 시간이 위에서 고른 유형으로 애플 건강에 자동 기록되고, 운동 강도(1~10)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.": { "localizations": { "en": { "stringUnit": { "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 (1–10) 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 (1–10). If you don't want this, turn off Keging's access in the Health app." } }, "ja": { "stringUnit": { "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": { "en": { diff --git a/myApp/Keging/IOS/WorkoutCoordinator.swift b/myApp/Keging/IOS/WorkoutCoordinator.swift index b0e8e70..bd391b6 100644 --- a/myApp/Keging/IOS/WorkoutCoordinator.swift +++ b/myApp/Keging/IOS/WorkoutCoordinator.swift @@ -35,7 +35,12 @@ final class WorkoutCoordinator { BackgroundAudioKeeper.shared.stop() UIApplication.shared.isIdleTimerDisabled = false 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 + ) } } } diff --git a/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings b/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings index 6d57318..e557fbb 100644 --- a/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings +++ b/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings @@ -3,12 +3,6 @@ "strings": { "%lld / %lld": { "localizations": { - "ko": { - "stringUnit": { - "state": "new", - "value": "%1$lld / %2$lld" - } - }, "en": { "stringUnit": { "state": "translated", @@ -20,6 +14,12 @@ "state": "translated", "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": { "en": { @@ -101,12 +117,6 @@ }, "수축 %lld초 · 이완 %lld초 · %lld회": { "localizations": { - "ko": { - "stringUnit": { - "state": "new", - "value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회" - } - }, "en": { "stringUnit": { "state": "translated", @@ -118,6 +128,12 @@ "state": "translated", "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": { "en": { @@ -235,12 +267,6 @@ }, "현재 %lld번째, 전체 %lld회": { "localizations": { - "ko": { - "stringUnit": { - "state": "new", - "value": "현재 %1$lld번째, 전체 %2$lld회" - } - }, "en": { "stringUnit": { "state": "translated", @@ -252,6 +278,12 @@ "state": "translated", "value": "%lld回目、全%lld回" } + }, + "ko": { + "stringUnit": { + "state": "new", + "value": "현재 %1$lld번째, 전체 %2$lld회" + } } } }, diff --git a/myApp/Keging/KegingWatch Watch App/WatchWorkoutSession.swift b/myApp/Keging/KegingWatch Watch App/WatchWorkoutSession.swift index f924904..76b6a46 100644 --- a/myApp/Keging/KegingWatch Watch App/WatchWorkoutSession.swift +++ b/myApp/Keging/KegingWatch Watch App/WatchWorkoutSession.swift @@ -39,7 +39,7 @@ final class WatchWorkoutSession: NSObject { // 권한 응답이 늦어 운동이 이미 끝났으면 세션을 열지 않는다 guard KegelEngine.shared.isRunning, session == nil else { return } let configuration = HKWorkoutConfiguration() - configuration.activityType = .other + configuration.activityType = SettingsStore.shared.config.healthWorkoutType.activityType configuration.locationType = .unknown let newSession = try HKWorkoutSession(healthStore: store, configuration: configuration) let newBuilder = newSession.associatedWorkoutBuilder() diff --git a/myApp/Keging/KegingWidgets/Localizable.xcstrings b/myApp/Keging/KegingWidgets/Localizable.xcstrings index a8b6533..deb5806 100644 --- a/myApp/Keging/KegingWidgets/Localizable.xcstrings +++ b/myApp/Keging/KegingWidgets/Localizable.xcstrings @@ -3,12 +3,6 @@ "strings": { "%lld / %lld": { "localizations": { - "ko": { - "stringUnit": { - "state": "new", - "value": "%1$lld / %2$lld" - } - }, "en": { "stringUnit": { "state": "translated", @@ -20,17 +14,17 @@ "state": "translated", "value": "%lld / %lld" } + }, + "ko": { + "stringUnit": { + "state": "new", + "value": "%1$lld / %2$lld" + } } } }, "%lld/%lld": { "localizations": { - "ko": { - "stringUnit": { - "state": "new", - "value": "%1$lld/%2$lld" - } - }, "en": { "stringUnit": { "state": "translated", @@ -42,6 +36,28 @@ "state": "translated", "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회": { "localizations": { - "ko": { - "stringUnit": { - "state": "new", - "value": "수축 %1$lld초 · 이완 %2$lld초 · %3$lld회" - } - }, "en": { "stringUnit": { "state": "translated", @@ -112,6 +122,12 @@ "state": "translated", "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": { "en": { diff --git a/myApp/Keging/Shared/HealthKitCalls.swift b/myApp/Keging/Shared/HealthKitCalls.swift index a87f7f4..a6cea04 100644 --- a/myApp/Keging/Shared/HealthKitCalls.swift +++ b/myApp/Keging/Shared/HealthKitCalls.swift @@ -7,6 +7,15 @@ import HealthKit +extension HealthWorkoutType { + var activityType: HKWorkoutActivityType { + switch self { + case .other: .other + case .coreTraining: .coreTraining + } + } +} + nonisolated enum HealthKitCalls { static func beginCollection(_ builder: HKWorkoutBuilder, at date: Date) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in diff --git a/myApp/Keging/Shared/WorkoutSettings.swift b/myApp/Keging/Shared/WorkoutSettings.swift index 914969b..1e57e4d 100644 --- a/myApp/Keging/Shared/WorkoutSettings.swift +++ b/myApp/Keging/Shared/WorkoutSettings.swift @@ -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 { var contractSeconds: Int = 10 @@ -43,6 +57,8 @@ nonisolated struct WorkoutConfig: Codable, Equatable { var relaxPattern: HapticPattern = .once /// 완주 시 애플 건강에 함께 기록할 운동 강도 (0 = 기록 안 함, 1~10) var effortScore: Int = 2 + /// 애플 건강에 저장할 운동 유형 + var healthWorkoutType: HealthWorkoutType = .other init( contractSeconds: Int = 10, @@ -50,7 +66,8 @@ nonisolated struct WorkoutConfig: Codable, Equatable { reps: Int = 20, contractPattern: HapticPattern = .twice, relaxPattern: HapticPattern = .once, - effortScore: Int = 2 + effortScore: Int = 2, + healthWorkoutType: HealthWorkoutType = .other ) { self.contractSeconds = contractSeconds self.relaxSeconds = relaxSeconds @@ -58,6 +75,7 @@ nonisolated struct WorkoutConfig: Codable, Equatable { self.contractPattern = contractPattern self.relaxPattern = relaxPattern self.effortScore = effortScore + self.healthWorkoutType = healthWorkoutType } // 필드 추가에도 기존 저장 JSON이 깨지지 않게 전 필드 decodeIfPresent @@ -69,6 +87,7 @@ nonisolated struct WorkoutConfig: Codable, Equatable { contractPattern = try container.decodeIfPresent(HapticPattern.self, forKey: .contractPattern) ?? .twice relaxPattern = try container.decodeIfPresent(HapticPattern.self, forKey: .relaxPattern) ?? .once 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)) }