From 0d058c77348eb6794c0d1d27480f92e36e89d75f Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Sat, 29 Aug 2026 20:34:22 +0900 Subject: [PATCH] =?UTF-8?q?fix(keging):=20=EC=A7=84=EB=8F=99=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=204=EC=A2=85=20=EC=9E=AC=EC=84=A4=EA=B3=84=20?= =?UTF-8?q?=E2=80=94=20=ED=9A=9F=EC=88=98=20=EC=84=B8=EA=B8=B0=20=EB=8C=80?= =?UTF-8?q?=EC=8B=A0=20=EC=B2=AB=20=EC=88=9C=EA=B0=84=EC=9D=98=20=EC=A7=88?= =?UTF-8?q?=EA=B0=90=EC=9C=BC=EB=A1=9C=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 톡(짧게)/톡톡/드르륵/지잉(길게, 0.3s 겹침으로 끊기지 않는 긴 진동) — 사용자 피드백 '한 번·두 번 방식은 아니고 간격도 길다' 반영, 연타 간격 0.5s로 단축 - 기본값: 수축=지잉(조이고 유지), 이완=톡(탁 풀기) — 첫 진동 즉시 구분 - 워치는 반복 재생 대신 패턴별 단일 시스템 햅틱(.start/.directionUp/.retry/.notification) — 워치 연타 뭉개짐·긴 간격 문제 자체를 제거 - 구버전 저장값(once/twice/thrice/heartbeat) 자동 이관, 라벨 3언어 번역·stale 정리 - 검증: Debug·Release·워치 빌드 그린, 세팅 화면 QA, 카탈로그 missing·stale 0 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT --- myApp/Keging/CLAUDE.md | 6 +- myApp/Keging/IOS/HelpView.swift | 2 +- myApp/Keging/IOS/InfoPlist.xcstrings | 116 ++++++++++-------- myApp/Keging/IOS/Localizable.xcstrings | 108 ++++++++-------- .../KegingWatch Watch App/InfoPlist.xcstrings | 116 ++++++++++-------- .../Localizable.xcstrings | 76 ++++++------ .../KegingWatch Watch App/WatchHaptics.swift | 18 ++- .../KegingWidgets/Localizable.xcstrings | 76 ++++++------ myApp/Keging/Shared/WorkoutSettings.swift | 52 +++++--- 9 files changed, 309 insertions(+), 261 deletions(-) diff --git a/myApp/Keging/CLAUDE.md b/myApp/Keging/CLAUDE.md index f279ddf..b3c9296 100644 --- a/myApp/Keging/CLAUDE.md +++ b/myApp/Keging/CLAUDE.md @@ -120,9 +120,9 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${app_files[@] - 주간·월간은 **달력 기준(이번 주·이번 달) ↔ 오늘 기준 롤링(지난 7일·30일)** 세그먼트 전환. **주 시작 = 월요일** (`calendar.firstWeekday = 2`) ### 3.3 진동 (`Shared/WorkoutSettings.swift`, `IOS/Haptics.swift`, 워치 `WatchHaptics.swift`) -- 패턴 4종: 한 번 / 두 번 / 세 번 / 심장 박동 (`beatOffsets` 시퀀스). 기본: 수축=두 번, 이완=한 번. 세팅에서 국면별 선택 + "미리 느끼기" -- iOS는 `AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)` 연타 — 무음 스위치 무관, **무음 오디오 세션이 살아 있으면 백그라운드에서도 울린다** -- 워치는 `WKInterfaceDevice.play(.notification)` 연타(간격 1.5배). **워치에서 실행하면 진동은 워치에서만** (완주 시 .success 1회) +- 패턴 4종 — **횟수 세기가 아니라 첫 순간의 질감으로 구분**(1.5(사용자 피드백)에서 횟수 기반 4종 교체): 톡(짧게 1방) / 톡톡(0.5s 간격 2방) / 드르륵(0.5s 간격 3연타) / 지잉(0.3s 겹침 3방=끊기지 않는 긴 진동). 기본: **수축=지잉(조이고 유지), 이완=톡(탁 풀기)**. 세팅에서 국면별 선택 + "미리 느끼기". 구버전 저장값(once/twice/thrice/heartbeat)은 `HapticPattern(migrating:)`으로 이관 +- iOS는 `AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)`(1회 ≈0.4초) 시퀀스 — 0.35초 이하 간격은 겹쳐 긴 진동이 되고 0.5초 간격은 빠른 연타가 된다. 무음 스위치 무관, **무음 오디오 세션이 살아 있으면 백그라운드에서도 울린다** +- 워치는 반복 재생이 뭉개져서 **패턴마다 성격이 다른 시스템 햅틱 1회**로 매핑: 톡=.start, 톡톡=.directionUp, 드르륵=.retry, 지잉=.notification. **워치에서 실행하면 진동은 워치에서만** (완주 시 .success 1회) ### 3.4 백그라운드·Live Activity (`IOS/BackgroundAudioKeeper.swift`, `LiveActivityManager.swift`, `KegingWidgets/`) - 운동 시작 시 무음 WAV 무한 루프(`UIBackgroundModes=audio`, `.playback + .mixWithOthers`) → 앱을 나가도 타이머·진동·Live Activity 갱신 유지. 종료·중단 시 즉시 해제. 운동 중 `isIdleTimerDisabled`로 화면 꺼짐 방지(종료 시 복원) diff --git a/myApp/Keging/IOS/HelpView.swift b/myApp/Keging/IOS/HelpView.swift index 68d6890..6281073 100644 --- a/myApp/Keging/IOS/HelpView.swift +++ b/myApp/Keging/IOS/HelpView.swift @@ -27,7 +27,7 @@ struct HelpView: View { helpRow( symbol: "iphone.radiowaves.left.and.right", title: Text("진동으로 구분하기"), - body: Text("수축이 시작될 때와 이완이 시작될 때 서로 다른 진동이 울려요. 그래서 화면을 보지 않아도 지금 조여야 할지 풀어야 할지 알 수 있어요. 진동 패턴은 운동 세팅에서 국면별로 고를 수 있고(한 번·두 번·세 번·심장 박동), '미리 느끼기' 버튼으로 실제 진동을 확인할 수 있어요. 진동은 실기기에서만 느껴져요.") + body: Text("수축이 시작될 때와 이완이 시작될 때 서로 다른 진동이 울려요. 그래서 화면을 보지 않아도 지금 조여야 할지 풀어야 할지 알 수 있어요. 기본은 수축=지잉(길게), 이완=톡(짧게)이라 첫 순간의 질감으로 바로 구분돼요. 패턴은 운동 세팅에서 국면별로 고를 수 있고(톡·톡톡·드르륵·지잉), '미리 느끼기' 버튼으로 실제 진동을 확인할 수 있어요. 진동은 실기기에서만 느껴져요.") ) helpRow( symbol: "stop.circle", diff --git a/myApp/Keging/IOS/InfoPlist.xcstrings b/myApp/Keging/IOS/InfoPlist.xcstrings index c4bd58b..3665428 100644 --- a/myApp/Keging/IOS/InfoPlist.xcstrings +++ b/myApp/Keging/IOS/InfoPlist.xcstrings @@ -1,75 +1,87 @@ { - "sourceLanguage": "ko", - "strings": { - "CFBundleDisplayName": { - "extractionState": "manual", - "localizations": { - "ko": { - "stringUnit": { - "state": "translated", - "value": "케깅" + "sourceLanguage" : "ko", + "strings" : { + "CFBundleDisplayName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging" } }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Keging" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Keging" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "케깅" } } } }, - "NSHealthUpdateUsageDescription": { - "extractionState": "manual", - "localizations": { - "ko": { - "stringUnit": { - "state": "translated", - "value": "케겔 운동을 마치면 그 시간을 애플 건강에 운동으로 기록하기 위해 필요해요." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Used to save your finished Kegel workouts to Apple Health." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "完了したケーゲル運動をヘルスケアにワークアウトとして記録するために使用します。" + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "new", + "value" : "Keging" } } } }, - "NSHealthShareUsageDescription": { - "extractionState": "manual", - "localizations": { - "ko": { - "stringUnit": { - "state": "translated", - "value": "케깅은 건강 데이터를 읽지 않아요." + "NSHealthShareUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging does not read your health data." } }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Keging does not read your health data." + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kegingはヘルスケアデータを読み取りません。" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Kegingはヘルスケアデータを読み取りません。" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "케깅은 건강 데이터를 읽지 않아요." + } + } + } + }, + "NSHealthUpdateUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Used to save your finished Kegel workouts to Apple Health." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完了したケーゲル運動をヘルスケアにワークアウトとして記録するために使用します。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "케겔 운동을 마치면 그 시간을 애플 건강에 운동으로 기록하기 위해 필요해요." } } } } }, - "version": "1.0" + "version" : "1.0" } \ No newline at end of file diff --git a/myApp/Keging/IOS/Localizable.xcstrings b/myApp/Keging/IOS/Localizable.xcstrings index ab8575d..fb6d3e1 100644 --- a/myApp/Keging/IOS/Localizable.xcstrings +++ b/myApp/Keging/IOS/Localizable.xcstrings @@ -335,18 +335,18 @@ } } }, - "두 번": { + "드르륵": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Twice" + "value": "Triple burst" } }, "ja": { "stringUnit": { "state": "translated", - "value": "2回" + "value": "トトトン" } } } @@ -495,22 +495,6 @@ } } }, - "세 번": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Three Times" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "3回" - } - } - } - }, "세트": { "localizations": { "en": { @@ -635,18 +619,18 @@ } } }, - "수축이 시작될 때와 이완이 시작될 때 서로 다른 진동이 울려요. 그래서 화면을 보지 않아도 지금 조여야 할지 풀어야 할지 알 수 있어요. 진동 패턴은 운동 세팅에서 국면별로 고를 수 있고(한 번·두 번·세 번·심장 박동), '미리 느끼기' 버튼으로 실제 진동을 확인할 수 있어요. 진동은 실기기에서만 느껴져요.": { + "수축이 시작될 때와 이완이 시작될 때 서로 다른 진동이 울려요. 그래서 화면을 보지 않아도 지금 조여야 할지 풀어야 할지 알 수 있어요. 기본은 수축=지잉(길게), 이완=톡(짧게)이라 첫 순간의 질감으로 바로 구분돼요. 패턴은 운동 세팅에서 국면별로 고를 수 있고(톡·톡톡·드르륵·지잉), '미리 느끼기' 버튼으로 실제 진동을 확인할 수 있어요. 진동은 실기기에서만 느껴져요.": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "A different vibration plays when a squeeze starts and when a relax starts, so you always know whether to squeeze or release without looking at the screen. You can choose a pattern for each phase in the workout setup (Once, Twice, Three Times, Heartbeat) and try them with the preview buttons. Haptics can only be felt on a real device." + "value": "A different vibration plays when a squeeze starts and when a relax starts, so you always know whether to squeeze or release without looking at the screen. By default, squeeze = a long buzz and relax = a short tap, so the very first moment tells them apart. You can choose a pattern for each phase in the workout setup (tap, double tap, triple burst, long buzz) and try them with the preview buttons. Haptics can only be felt on a real device." } }, "ja": { "stringUnit": { "state": "translated", - "value": "締め始めとゆるめ始めで異なる振動が鳴るので、画面を見なくても今どちらなのか分かります。振動パターンはワークアウト設定でそれぞれ選べて(1回・2回・3回・心拍)、「試す」ボタンで実際に確認できます。振動は実機でのみ感じられます。" + "value": "締め始めとゆるめ始めで異なる振動が鳴るので、画面を見なくても今どちらなのか分かります。デフォルトは締める=長いブーッ、ゆるめる=短いトンなので、最初の瞬間の感触ですぐ区別できます。パターンはワークアウト設定でそれぞれ選べて(トン・トントン・トトトン・ブーッ)、「試す」ボタンで実際に確認できます。振動は実機でのみ感じられます。" } } } @@ -731,22 +715,6 @@ } } }, - "심장 박동": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Heartbeat" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "心拍" - } - } - } - }, "안 함": { "localizations": { "en": { @@ -1467,6 +1435,22 @@ } } }, + "지잉 (길게)": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Long buzz" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブーッ(長く)" + } + } + } + }, "진동 패턴": { "localizations": { "en": { @@ -1627,6 +1611,38 @@ } } }, + "톡 (짧게)": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Tap (short)" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "トン(短く)" + } + } + } + }, + "톡톡": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Double tap" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "トントン" + } + } + } + }, "통계": { "localizations": { "en": { @@ -1739,22 +1755,6 @@ } } }, - "한 번": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Once" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "1回" - } - } - } - }, "현재 %lld번째, 전체 %lld회": { "localizations": { "en": { diff --git a/myApp/Keging/KegingWatch Watch App/InfoPlist.xcstrings b/myApp/Keging/KegingWatch Watch App/InfoPlist.xcstrings index c4bd58b..06104e0 100644 --- a/myApp/Keging/KegingWatch Watch App/InfoPlist.xcstrings +++ b/myApp/Keging/KegingWatch Watch App/InfoPlist.xcstrings @@ -1,75 +1,87 @@ { - "sourceLanguage": "ko", - "strings": { - "CFBundleDisplayName": { - "extractionState": "manual", - "localizations": { - "ko": { - "stringUnit": { - "state": "translated", - "value": "케깅" + "sourceLanguage" : "ko", + "strings" : { + "CFBundleDisplayName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging" } }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Keging" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Keging" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "케깅" } } } }, - "NSHealthUpdateUsageDescription": { - "extractionState": "manual", - "localizations": { - "ko": { - "stringUnit": { - "state": "translated", - "value": "케겔 운동을 마치면 그 시간을 애플 건강에 운동으로 기록하기 위해 필요해요." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Used to save your finished Kegel workouts to Apple Health." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "完了したケーゲル運動をヘルスケアにワークアウトとして記録するために使用します。" + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "new", + "value" : "KegingWatch Watch App" } } } }, - "NSHealthShareUsageDescription": { - "extractionState": "manual", - "localizations": { - "ko": { - "stringUnit": { - "state": "translated", - "value": "케깅은 건강 데이터를 읽지 않아요." + "NSHealthShareUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Keging does not read your health data." } }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Keging does not read your health data." + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kegingはヘルスケアデータを読み取りません。" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Kegingはヘルスケアデータを読み取りません。" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "케깅은 건강 데이터를 읽지 않아요." + } + } + } + }, + "NSHealthUpdateUsageDescription" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Used to save your finished Kegel workouts to Apple Health." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "完了したケーゲル運動をヘルスケアにワークアウトとして記録するために使用します。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "케겔 운동을 마치면 그 시간을 애플 건강에 운동으로 기록하기 위해 필요해요." } } } } }, - "version": "1.0" + "version" : "1.0" } \ No newline at end of file diff --git a/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings b/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings index e557fbb..3e60be7 100644 --- a/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings +++ b/myApp/Keging/KegingWatch Watch App/Localizable.xcstrings @@ -67,34 +67,18 @@ } } }, - "두 번": { + "드르륵": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Twice" + "value": "Triple burst" } }, "ja": { "stringUnit": { "state": "translated", - "value": "2回" - } - } - } - }, - "세 번": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Three Times" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "3回" + "value": "トトトン" } } } @@ -153,22 +137,6 @@ } } }, - "심장 박동": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Heartbeat" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "心拍" - } - } - } - }, "완료!": { "localizations": { "en": { @@ -217,6 +185,22 @@ } } }, + "지잉 (길게)": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Long buzz" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブーッ(長く)" + } + } + } + }, "케겔 운동 시작": { "localizations": { "en": { @@ -249,18 +233,34 @@ } } }, - "한 번": { + "톡 (짧게)": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Once" + "value": "Tap (short)" } }, "ja": { "stringUnit": { "state": "translated", - "value": "1回" + "value": "トン(短く)" + } + } + } + }, + "톡톡": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Double tap" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "トントン" } } } diff --git a/myApp/Keging/KegingWatch Watch App/WatchHaptics.swift b/myApp/Keging/KegingWatch Watch App/WatchHaptics.swift index a760161..dc557af 100644 --- a/myApp/Keging/KegingWatch Watch App/WatchHaptics.swift +++ b/myApp/Keging/KegingWatch Watch App/WatchHaptics.swift @@ -2,7 +2,8 @@ // WatchHaptics.swift // KegingWatch Watch App // -// 진동 패턴 재생 (워치) — 워치 햅틱은 한 번이 길어서 간격을 1.5배로 벌린다 +// 진동 패턴 재생 (워치) — 반복 재생 대신 패턴마다 성격이 다른 시스템 햅틱 1회로 +// 즉시 구분되게 한다 (연타는 워치에서 뭉개져 간격만 길어지던 문제의 해소) // import WatchKit @@ -10,14 +11,19 @@ import WatchKit @MainActor enum WatchHaptics { static func play(_ pattern: HapticPattern) { - for offset in pattern.beatOffsets { - DispatchQueue.main.asyncAfter(deadline: .now() + offset * 1.5) { - WKInterfaceDevice.current().play(.notification) - } - } + WKInterfaceDevice.current().play(hapticType(for: pattern)) } static func playSuccess() { WKInterfaceDevice.current().play(.success) } + + private static func hapticType(for pattern: HapticPattern) -> WKHapticType { + switch pattern { + case .short: .start // 단타 + case .doubleTap: .directionUp // 두 단계로 올라가는 느낌 + case .rattle: .retry // 연타 질감 + case .long: .notification // 길고 묵직한 알림 + } + } } diff --git a/myApp/Keging/KegingWidgets/Localizable.xcstrings b/myApp/Keging/KegingWidgets/Localizable.xcstrings index deb5806..0f2c24a 100644 --- a/myApp/Keging/KegingWidgets/Localizable.xcstrings +++ b/myApp/Keging/KegingWidgets/Localizable.xcstrings @@ -61,34 +61,18 @@ } } }, - "두 번": { + "드르륵": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Twice" + "value": "Triple burst" } }, "ja": { "stringUnit": { "state": "translated", - "value": "2回" - } - } - } - }, - "세 번": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Three Times" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "3回" + "value": "トトトン" } } } @@ -131,22 +115,6 @@ } } }, - "심장 박동": { - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Heartbeat" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "心拍" - } - } - } - }, "이완": { "localizations": { "en": { @@ -163,6 +131,22 @@ } } }, + "지잉 (길게)": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Long buzz" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ブーッ(長く)" + } + } + } + }, "코어 트레이닝": { "localizations": { "en": { @@ -179,18 +163,34 @@ } } }, - "한 번": { + "톡 (짧게)": { "localizations": { "en": { "stringUnit": { "state": "translated", - "value": "Once" + "value": "Tap (short)" } }, "ja": { "stringUnit": { "state": "translated", - "value": "1回" + "value": "トン(短く)" + } + } + } + }, + "톡톡": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Double tap" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "トントン" } } } diff --git a/myApp/Keging/Shared/WorkoutSettings.swift b/myApp/Keging/Shared/WorkoutSettings.swift index 1e57e4d..d9ee003 100644 --- a/myApp/Keging/Shared/WorkoutSettings.swift +++ b/myApp/Keging/Shared/WorkoutSettings.swift @@ -8,28 +8,45 @@ import Foundation import Combine -/// 진동 패턴 — 수축/이완 각각 선택. 진동 횟수·리듬으로 구분한다 +/// 진동 패턴 — 수축/이완 각각 선택. 횟수 세기가 아니라 첫 순간의 질감으로 구분한다 +/// (짧은 톡 / 빠른 톡톡 / 드르륵 연타 / 끊기지 않는 긴 지잉) nonisolated enum HapticPattern: String, CaseIterable, Identifiable, Codable { - case once, twice, thrice, heartbeat + case short, doubleTap, rattle, long var id: String { rawValue } var label: String { switch self { - case .once: String(localized: "한 번") - case .twice: String(localized: "두 번") - case .thrice: String(localized: "세 번") - case .heartbeat: String(localized: "심장 박동") + case .short: String(localized: "톡 (짧게)") + case .doubleTap: String(localized: "톡톡") + case .rattle: String(localized: "드르륵") + case .long: String(localized: "지잉 (길게)") } } - /// 각 진동이 울리는 시점(초) — 첫 진동은 0 + /// 각 진동이 울리는 시점(초) — 시스템 바이브 1회가 약 0.4초라 + /// 0.35초 간격은 겹쳐서 하나의 긴 진동, 0.5초 간격은 빠른 연타가 된다 var beatOffsets: [TimeInterval] { switch self { - case .once: [0] - case .twice: [0, 0.45] - case .thrice: [0, 0.45, 0.9] - case .heartbeat: [0, 0.3, 1.0, 1.3] + case .short: [0] + case .doubleTap: [0, 0.5] + case .rattle: [0, 0.5, 1.0] + case .long: [0, 0.3, 0.6] + } + } + + /// 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 } } } @@ -53,8 +70,9 @@ nonisolated struct WorkoutConfig: Codable, Equatable { var contractSeconds: Int = 10 var relaxSeconds: Int = 5 var reps: Int = 20 - var contractPattern: HapticPattern = .twice - var relaxPattern: HapticPattern = .once + /// 기본: 수축 = 긴 지잉(조이고 유지), 이완 = 짧은 톡(탁 풀기) + var contractPattern: HapticPattern = .long + var relaxPattern: HapticPattern = .short /// 완주 시 애플 건강에 함께 기록할 운동 강도 (0 = 기록 안 함, 1~10) var effortScore: Int = 2 /// 애플 건강에 저장할 운동 유형 @@ -64,8 +82,8 @@ nonisolated struct WorkoutConfig: Codable, Equatable { contractSeconds: Int = 10, relaxSeconds: Int = 5, reps: Int = 20, - contractPattern: HapticPattern = .twice, - relaxPattern: HapticPattern = .once, + contractPattern: HapticPattern = .long, + relaxPattern: HapticPattern = .short, effortScore: Int = 2, healthWorkoutType: HealthWorkoutType = .other ) { @@ -84,8 +102,8 @@ nonisolated struct WorkoutConfig: Codable, Equatable { 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 = try container.decodeIfPresent(HapticPattern.self, forKey: .contractPattern) ?? .twice - relaxPattern = try container.decodeIfPresent(HapticPattern.self, forKey: .relaxPattern) ?? .once + 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 }