- 의도(사용자 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
139 lines
10 KiB
Swift
139 lines
10 KiB
Swift
//
|
||
// HelpView.swift
|
||
// Keging
|
||
//
|
||
// 도움말 — 앱의 모든 기능과 사용법. 첫 화면 물음표 버튼으로 진입.
|
||
//
|
||
|
||
import SwiftUI
|
||
|
||
struct HelpView: View {
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
List {
|
||
Section {
|
||
helpRow(
|
||
symbol: "play.circle.fill",
|
||
title: Text("바로 시작하기"),
|
||
body: Text("첫 화면의 큰 시작 버튼을 누르면 저장된 세팅대로 운동이 시작돼요. 준비 시간을 설정해 두면 그만큼 카운트다운 후에 첫 수축이 시작돼요. 화면의 원이 수축할 때 함께 조이고, 커질 때 힘을 풀면 돼요. 정해진 횟수를 모두 마치면 완료 화면이 뜨고 기록이 저장돼요. 아이폰에서 시작하면 워치 앱이 자동으로 열려 심박수를 재요 — 워치가 없거나 열지 못하면 심박 없이 아이폰만으로 기록되고, 그때 뜨는 확인 창은 설정에서 끌 수 있어요.")
|
||
)
|
||
helpRow(
|
||
symbol: "slider.horizontal.3",
|
||
title: Text("운동 세팅 바꾸기"),
|
||
body: Text("오른쪽 위 슬라이더 버튼이 운동 세팅이에요. 준비 시간(0~10초), 수축 시간(1~60초), 이완 시간(1~60초), 반복 횟수(1~100회)를 정할 수 있어요. 예를 들어 수축 10초·이완 5초·20회로 설정하면, 수축과 이완을 20번 반복하는 한 세트가 약 5분이 돼요. 시작 버튼 아래의 카드를 눌러도 이 화면이 열려요.")
|
||
)
|
||
helpRow(
|
||
symbol: "iphone.radiowaves.left.and.right",
|
||
title: Text("진동으로 구분하기"),
|
||
body: Text("수축이 시작될 때와 이완이 시작될 때 서로 다른 진동이 울려요. 그래서 화면을 보지 않아도 지금 조여야 할지 풀어야 할지 알 수 있어요. 기본값은 수축=지이이잉(아주 긴 진동), 이완=기본(짧은 한 번)이라 첫 순간의 질감으로 바로 구분돼요. 패턴은 운동 세팅에서 국면별로 고를 수 있고(기본·톡톡·지이이잉), '미리 느끼기' 버튼으로 실제 진동을 확인할 수 있어요. 세트를 끝까지 마치면 아주 길게 축하 진동이 울려요. 화면이 꺼져 있거나 다른 앱을 쓰는 중에는 시스템 진동으로 대신 울려서 질감이 조금 다를 수 있어요. 워치는 손목에 맞춘 더 강한 전용 진동 세트를 쓰고 워치에서 따로 골라요. 진동은 실기기에서만 느껴져요.")
|
||
)
|
||
helpRow(
|
||
symbol: "stop.circle",
|
||
title: Text("중간에 중단하면"),
|
||
body: Text("운동 중 아래의 중단 버튼을 누르면 확인 후 세트가 끝나요. 중단한 세트는 아예 하지 않은 것으로 처리돼서 기록과 통계에 남지 않아요. 끝까지 마친 세트만 기록돼요.")
|
||
)
|
||
} header: {
|
||
Text("기본 사용법")
|
||
}
|
||
|
||
Section {
|
||
helpRow(
|
||
symbol: "chart.xyaxis.line",
|
||
title: Text("통계 열기"),
|
||
body: Text("첫 화면을 위로 쓸어올리거나 아래 가운데의 손잡이를 누르면 통계 화면이 올라와요. 닫을 때는 아래로 쓸어내리거나 X 버튼을 누르면 돼요.")
|
||
)
|
||
helpRow(
|
||
symbol: "calendar",
|
||
title: Text("하루 · 주간 · 월간"),
|
||
body: Text("하루 탭 맨 위의 타임라인에서 오늘 언제 운동했는지 24시간 축 위의 점으로 한눈에 볼 수 있어요(점이 클수록 횟수가 많은 세트). 아래로는 시간대별 꺾은선 그래프와 오늘 기록 목록이 이어져요. 주간·월간 탭에서는 일별 횟수를 꺾은선 그래프와 합계(세트·횟수)로 보여줘요. 주는 월요일에 시작해요.")
|
||
)
|
||
helpRow(
|
||
symbol: "arrow.left.arrow.right",
|
||
title: Text("이번 주 ↔ 지난 7일 전환"),
|
||
body: Text("주간·월간 화면 위의 스위치로 두 가지 방식을 오갈 수 있어요. '이번 주 / 이번 달'은 달력 기준(월요일~일요일, 1일~말일)이고, '지난 7일 / 지난 30일'은 오늘을 기준으로 거슬러 세는 방식이에요.")
|
||
)
|
||
} header: {
|
||
Text("통계")
|
||
}
|
||
|
||
Section {
|
||
helpRow(
|
||
symbol: "bolt.horizontal.circle",
|
||
title: Text("다이나믹 아일랜드"),
|
||
body: Text("운동 중에 앱을 나가면 다이나믹 아일랜드와 잠금화면에 실시간 현황이 떠요. 세트 전체를 국면 색(민트=수축·앰버=이완)으로 칠한 타임라인 바 위로 밝은 줄이 실제 시간에 맞춰 차오르는데, 줄 끝이 걸친 색이 지금 국면이에요. 남은 시간 숫자와 함께 전부 시스템이 직접 그려서 세트가 끝날 때까지 멈추지 않아요. 지금 조여야 할지 풀어야 할지는 진동으로도 늘 알 수 있고, 정확한 횟수는 앱을 열면 보여요.")
|
||
)
|
||
helpRow(
|
||
symbol: "square.and.arrow.up",
|
||
title: Text("단축어와 시리"),
|
||
body: Text("단축어 앱에서 '케겔 운동 시작' 동작을 추가할 수 있어요. 실행하면 케깅이 열리면서 저장된 세팅대로 바로 시작돼요. 홈 화면 아이콘·잠금화면 위젯·뒷면 탭 등 단축어를 부를 수 있는 곳이라면 어디서든 쓸 수 있어요.")
|
||
)
|
||
} header: {
|
||
Text("앱 밖에서")
|
||
}
|
||
|
||
Section {
|
||
helpRow(
|
||
symbol: "applewatch",
|
||
title: Text("애플워치에서 운동하기"),
|
||
body: Text("워치 앱을 열면 시작 버튼이 있고, 누르면 바로 시작돼요. 화면에 수축·이완 상태와 남은 시간, 몇 번째인지가 한눈에 보여요. 시작 화면에서 왼쪽으로 쓸어넘기면 워치 전용 세팅(준비·수축·이완·횟수·진동·애플 건강)을 바꿀 수 있어요. 워치에서 실행하면 진동은 워치에서만 울려요. 중단 버튼을 누르면 그 세트는 기록되지 않아요.")
|
||
)
|
||
helpRow(
|
||
symbol: "arrow.triangle.2.circlepath",
|
||
title: Text("워치와 아이폰 동기화"),
|
||
body: Text("운동 세팅은 아이폰과 워치가 각자 따로 저장해요 — 기기마다 진동 느낌이 달라서 각자에게 맞게 다르게 세팅할 수 있게요. 워치 세팅은 워치의 시작 화면에서 왼쪽으로 쓸어넘겨 바꿔요. 아이폰에서 시작하면 워치 앱이 자동으로 열려 심박수만 재 주고 진동은 아이폰에서 울려요. 워치에서 시작하면 워치가 진동하고, 끝까지 마친 기록은 아이폰으로 넘어와 통계에 합쳐져요 — 워치가 아이폰과 떨어져 있어도 기록은 쌓였다가 연결되면 자동으로 전송돼요.")
|
||
)
|
||
} header: {
|
||
Text("애플워치")
|
||
}
|
||
|
||
Section {
|
||
helpRow(
|
||
symbol: "heart.circle",
|
||
title: Text("애플 건강 연동"),
|
||
body: Text("운동을 끝까지 마치면 그 시간이 애플 건강에 운동으로 자동 기록돼요. 운동 유형(기타·코어 트레이닝)과 운동 강도(1~10)는 설정에서 고를 수 있어요. 처음 시작할 때 건강 권한을 물어보고, 허용하면 그다음부터는 알아서 저장돼요. 아이폰에서 시작하든 워치에서 시작하든 워치를 차고 있으면 워치가 심박수와 활성 에너지를 실측해 함께 기록해요. 워치를 못 쓴 세트는 아이폰이 애플워치의 '기타 운동' 방식(빠르게 걷기 상당)으로 칼로리를 추정해 저장해요 — 그래서 세트마다 칼로리가 조금 다를 수 있어요. 중단한 세트는 건강에도 기록되지 않고, 원치 않으면 건강 앱에서 케깅의 권한을 끄면 돼요.")
|
||
)
|
||
helpRow(
|
||
symbol: "paintbrush",
|
||
title: Text("테마와 언어"),
|
||
body: Text("왼쪽 위 톱니바퀴 버튼이 설정이에요. 화면 모드(시스템·라이트·다크)와 언어(시스템·한국어·English·日本語)를 고를 수 있어요. 언어는 앱을 완전히 종료했다가 다시 실행하면 적용돼요. 워치는 워치의 시스템 언어를 따라요.")
|
||
)
|
||
helpRow(
|
||
symbol: "lock.shield",
|
||
title: Text("데이터와 개인정보"),
|
||
body: Text("케깅은 광고와 결제가 없는 완전 무료 앱이에요. 모든 기록은 이 기기(그리고 워치)에만 저장되고, 외부 서버로 전송되지 않아요.")
|
||
)
|
||
} header: {
|
||
Text("설정·기타")
|
||
}
|
||
}
|
||
.navigationTitle("도움말")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("완료") { dismiss() }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func helpRow(symbol: String, title: Text, body: Text) -> some View {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
HStack(spacing: 8) {
|
||
Image(systemName: symbol)
|
||
.font(.body)
|
||
.foregroundStyle(AppTheme.green)
|
||
.frame(width: 24)
|
||
title
|
||
.font(.headline)
|
||
}
|
||
body
|
||
.font(.subheadline)
|
||
.foregroundStyle(.secondary)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
}
|
||
.padding(.vertical, 4)
|
||
}
|
||
}
|