mycode/myApp/HaruDanim/Shared/HealthMetric.swift
songyc macbook e04cdfdcb9 feat(1.5-p2): 건강 데이터 타일 — HealthCache 기반·모음 탭 3섹션 순서·안내/지표 시트
- HealthMetric 8종(표준 단위·표기) + HealthStore(HK 조회: 누적 통계·스탠드·마음챙김·
  수면 구간[합집합 병합, 깨어난 날 귀속 — plan §3.4 규칙]) + HealthCache(App Group,
  지표×dayKey, 400일 보존 — 5.1.3 준수: CloudKit 비저장)
- 모음 탭 건강 섹션(타일 줄+접기·연결 CTA[명시적 탭 권한]·안내 시트[사용자 요구 가이드]·
  지표 선택 시트[체크+드래그 순서]) — actionGrid·레이아웃 금지구역 무접촉, 편집 모드 숨김
- 3섹션(즐겨찾기·나머지·건강) 순서 렌더러 + 설정 탭 표시 토글·순서 화면(기기 로컬)
- HealthKit 엔타이틀먼트(iOS 앱만)·NSHealthShareUsageDescription, 전체 초기화에
  건강 키·캐시 청소 추가
- 검증: Debug/Store 빌드, 자가 검증 106건 ALL PASS(26.5+18.5), 시각 QA 7장
  (타일·순서·설정·안내·지표·CTA·18.5 빈 상태). 신규 인자 3종 §14 기록

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtZwRRjbtM9pZJDM4FkTq6
2026-08-22 05:38:50 +09:00

82 lines
3.3 KiB
Swift

//
// HealthMetric.swift
// Haru_Danim
//
// (1.5 Docs/plan-1.5.md §3)
// - · (HealthKit )
// - : (··)=, =, =kcal,
// =m, =, =L. HealthKit ··
//
import Foundation
nonisolated enum HealthMetric: String, CaseIterable, Identifiable, Codable {
case steps, exerciseMinutes, sleep, activeEnergy, distance, standHours, mindfulMinutes, water
var id: String { rawValue }
var name: String {
switch self {
case .steps: return String(localized: "걸음 수")
case .exerciseMinutes: return String(localized: "운동 시간")
case .sleep: return String(localized: "수면")
case .activeEnergy: return String(localized: "활동 에너지")
case .distance: return String(localized: "이동 거리")
case .standHours: return String(localized: "일어서기")
case .mindfulMinutes: return String(localized: "마음챙김")
case .water: return String(localized: "물 섭취")
}
}
var symbolName: String {
switch self {
case .steps: return "figure.walk"
case .exerciseMinutes: return "figure.run"
case .sleep: return "bed.double.fill"
case .activeEnergy: return "flame.fill"
case .distance: return "map.fill"
case .standHours: return "figure.stand"
case .mindfulMinutes: return "brain.head.profile"
case .water: return "drop.fill"
}
}
/// ( ) ·
var isDuration: Bool {
switch self {
case .exerciseMinutes, .sleep, .mindfulMinutes: return true
default: return false
}
}
/// (·· )
func valueLabel(_ value: Double) -> String {
switch self {
case .steps:
return String(localized: "\(Int(value).formatted())")
case .exerciseMinutes, .sleep, .mindfulMinutes:
return Format.durationShort(value)
case .activeEnergy:
return String(localized: "\(Int(value).formatted())kcal")
case .distance:
let km = value / 1000
let text = km.formatted(.number.precision(.fractionLength(km >= 10 ? 0 : 1)))
return String(localized: "\(text)km")
case .standHours:
return String(localized: "\(Int(value))시간")
case .water:
let text = value.formatted(.number.precision(.fractionLength(0...2)))
return String(localized: "\(text)L")
}
}
/// (·· )
static let defaultTileMetrics: [HealthMetric] = [.steps, .exerciseMinutes, .sleep]
/// ( )
static func selectedList(raw: String) -> [HealthMetric] {
let parsed = raw.split(separator: ",").compactMap { HealthMetric(rawValue: String($0)) }
return parsed.isEmpty ? defaultTileMetrics : parsed
}
}