mycode/myApp/Keging/IOS/AppSettingsView.swift
songyc macbook b751247ba7 feat(keging): 아이폰 시작 세트의 워치 심박 측정 — startWatchApp + 미러링 세션, 배지·사전 경고 폐지 (9차)
- 의도(사용자 TestFlight 피드백): 폰으로 운동해도 워치를 차고 있으면 워치가 심박을 재고,
  진동은 시작한 기기에서만. 종전 '워치 연동됨' 배지는 워치 앱 포그라운드 여부(isReachable)만
  보여 주던 것이라 폐지
- iOS: WatchHeartRateBridge — 시작 시 HKHealthStore.startWatchApp(폰 설정 운동 유형)으로 워치 앱을
  깨우고, 워치가 미러링한 HKWorkoutSession을 workoutSessionMirroringStartHandler로 받아 심박 수신,
  완주/중단 명령 전송(finish/discard). 워치를 못 열거나 30초 안에 미러링이 안 붙으면 폰 단독
  (+설정에 따라 '워치 앱을 열지 못했어요' 알럿, app.watchWarn 재사용) 및 WatchConnectivity로
  워치 세션 폐기 명령. 워치 저장 결과(saved/failed)는 WC로 회신받고 15초 내 미회신이면 폰 사후 저장 폴백.
  워치 깨우기는 폰 건강 권한 응답 뒤에(첫 실행 경합 방지). 건강 읽기 권한(심박·활성 에너지) 추가
  — 워치 세션 센서 수집용(권한 공유)
- 워치: WatchAppDelegate.handle(workoutConfiguration) → MirroredWorkoutSession(라이브 빌더, 무진동·무타이머,
  심박을 미러링 채널로 전송, finish 시 저장+강도 relate 후 WC로 결과 통보, discard/끊김 시 폐기,
  미러링 30초 타임아웃, 앱 재기동 시 잔류 세션 복구·폐기). 화면 '아이폰에서 진행 중 ♥bpm'(중단 버튼).
  WatchHealthAuth로 권한 공용화
- UI: 첫 화면 배지·확인 창 제거, 운동 화면 횟수 아래 워치 상태 줄(연결 중/♥bpm/아이폰만),
  설정 토글 '워치 연결 실패 경고', 도움말 3건 갱신. l10n 카탈로그 3종 missing/stale 0
- DEBUG: -testConfig(35초 세트), -mockWatchHeartRate <bpm>(상태 줄 QA·스크린샷). watchHR 카테고리 notice 로그
- 시뮬 검증(페어링 심): startWatchApp → 워치 기동·권한·세션·가짜 심박·'아이폰에서 진행 중'까지 동작,
  워치 startMirroring 성공 반환 — 그러나 폰 미러링 핸들러는 시뮬에서 불리지 않음(애플 샘플도 실기기 전용)
  → 30초 타임아웃·알럿·WC 폐기·폰 폴백 저장 경로 확인, 워치 자체 세트 정상. 3타깃 빌드 그린·경고 0.
  미러링 수신·완주 저장·회신은 실기기 TestFlight 재테스트 필요
- 문서·마케팅: CLAUDE.md §3.8 신설·§1/§3.7/§4/§6/§7 갱신, 설명 3언어·whats-new·review-notes·PRIVACY.md
  (아이폰 읽기 권한 설명 — GitHub 게시본 갱신 필요), 스크린샷 01·08(배지 제거)·02·03(♥78 상태 줄) 재촬영·합성

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UPmp29DQHY6Ahfss5SRopT
2026-09-05 10:13:48 +09:00

103 lines
4.5 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.

//
// AppSettingsView.swift
// Keging
//
//
//
import SwiftUI
struct AppSettingsView: View {
@EnvironmentObject private var settings: SettingsStore
@AppStorage("app.theme", store: AppGroup.defaults) private var themeMode = "system"
@AppStorage("app.language", store: AppGroup.defaults) private var appLanguage = "system"
@AppStorage("app.watchWarn", store: AppGroup.defaults) private var warnNoWatch = true
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
Form {
Section("테마") {
Picker("화면 모드", selection: $themeMode) {
Text("시스템").tag("system")
Text("라이트").tag("light")
Text("다크").tag("dark")
}
.pickerStyle(.segmented)
}
Section {
Picker("언어", selection: $appLanguage) {
Text("시스템").tag("system")
Text(verbatim: "한국어").tag("ko")
Text(verbatim: "English").tag("en")
Text(verbatim: "日本語").tag("ja")
}
} header: {
Text("언어")
} footer: {
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
Text(effortLabel(score)).tag(score)
}
}
} header: {
Text("애플 건강")
} footer: {
Text("운동을 끝까지 마치면 그 시간이 위에서 고른 유형으로 애플 건강에 자동 기록되고, 운동 강도(110)도 함께 저장돼요. 기록을 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.")
}
Section {
Toggle("워치 연결 실패 경고", isOn: $warnNoWatch)
} header: {
Text("애플워치")
} footer: {
Text("아이폰에서 시작하면 워치 앱을 자동으로 열어 심박수를 재요. 워치 앱을 열지 못하면 확인 창을 띄워요 — 심박 없이 아이폰만으로 기록돼요.")
}
Section {
LabeledContent("버전", value: versionText)
} footer: {
Text("케깅은 광고와 결제가 없는 완전 무료 앱이에요.")
}
}
.navigationTitle("설정")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("완료") { dismiss() }
}
}
.onChange(of: appLanguage) {
// ( )
if appLanguage == "system" {
UserDefaults.standard.removeObject(forKey: "AppleLanguages")
} else {
UserDefaults.standard.set([appLanguage], forKey: "AppleLanguages")
}
}
}
}
/// : 1~3 · 4~6 · 7~8 · 9~10
private func effortLabel(_ score: Int) -> String {
let word: String = switch score {
case 1...3: String(localized: "쉬움")
case 4...6: String(localized: "보통")
case 7...8: String(localized: "힘듦")
default: String(localized: "전력")
}
return String(localized: "\(score) · \(word)")
}
private var versionText: String {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0"
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
return "\(version) (\(build))"
}
}