사용자 요구('이거까지 하고 배포'): 통계·기록에서도 건강 데이터를 하루다님 스타일로.
전부 표시 전용 — HealthCache 인메모리 읽기만, HK 신규 조회·스키마·집계 수학 무변경.
- 통계 탭 맨 아래: 하루=지표 값 칩 카드 / 주간·월간(롤링 포함)=
'건강 지표 (일별)' 추이 카드(지표 선택 캡슐 메뉴·요일/일 축·단위 라벨, 분홍 단일 선)
+ '건강 지표 합계·평균' 표(분모=기존 '이미 시작된 날/주' 규칙 상속, 0 지표 생략)
- 기록 탭 목록 맨 아래 '건강 데이터' 섹션 — 그날 칩(과거 날짜도 캐시로)
- 표시 규칙: 지표=타일 선택 상속, 마스터 스위치=설정 '건강 데이터 표시'(문구 확장),
미연결·맥=숨김, 내보내기 미포함(비교 카드 전례)
- 값 공급 단일 지점 HealthDisplayValues 신설(수면=유효 표시 구간 키, 일기 카드도 위임),
칩 UI HealthValueChipGrid 공용 — 빌드 9 최적화 그대로 유지
- 도움말·팝업·설정 푸터·whats-new 3언어 갱신(새 키 5·수정 3, 전 카탈로그 0/0)
검증: 자가 검증 62/20/27/10 ALL PASS(수치 불변), 시각 QA(하루·주간[합계 53,966보 시드
검산 일치]·월간 롤링[30일 분모]·기록 목록 과거 날짜·ja·18.5[경과일 분모 4일 검산 일치]),
Debug·Store·18.5 빌드 그린. 종목별 운동 통계는 1.6 후보 보류. CURRENT_PROJECT_VERSION 10.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtZwRRjbtM9pZJDM4FkTq6
165 lines
6.4 KiB
Swift
165 lines
6.4 KiB
Swift
//
|
|
// Settings.swift
|
|
// Haru_Danim
|
|
//
|
|
// 앱 설정 키, 무료 제한 (CLAUDE.md §6.6, §7)
|
|
//
|
|
|
|
import Foundation
|
|
|
|
enum SettingsKeys {
|
|
/// "light" | "dark"
|
|
static let theme = "settings.theme"
|
|
/// "ko" | "en" | "ja"
|
|
static let language = "settings.language"
|
|
/// Calendar.weekday 값 (1=일요일 ... 7=토요일). 기본값 2(월요일)
|
|
static let weekStartWeekday = "settings.weekStartWeekday"
|
|
/// 자정 기준 분 단위 오프셋. 기본값 0 (00:00)
|
|
static let dayStartMinutes = "settings.dayStartMinutes"
|
|
/// "latest"(가장 나중에 시작) | "earliest"(가장 먼저 시작). 기본값 latest
|
|
static let liveActivityMode = "settings.liveActivityMode"
|
|
// 프리미엄 여부는 Shared/Premium.swift의 PremiumGate.cacheKey (App Group defaults) 사용
|
|
/// 메인 탭 그리드 한 줄에 표시할 행동 버튼 개수. 기본값 3
|
|
static let gridColumns = "settings.gridColumns"
|
|
/// 이 시간(초)보다 짧게 측정된 세션은 기록하지 않음. 0 = 사용 안 함
|
|
static let minSessionSeconds = "settings.minSessionSeconds"
|
|
/// 측정이 이 시간(시)을 넘기면 로컬 알림 (끄는 걸 잊음 방지). 0 = 사용 안 함
|
|
static let longSessionAlertHours = "settings.longSessionAlertHours"
|
|
/// 탭바에 직접 노출할 탭(더보기 제외 1~3개). AppTab rawValue를 쉼표로 연결한 문자열
|
|
static let visibleTabs = "settings.visibleTabs"
|
|
/// 모음 탭 목표 카드 표시 방식. GoalCardStyle rawValue ("perQuest" | "combined")
|
|
static let goalCardStyle = "settings.goalCardStyle"
|
|
/// iPhone 내비게이션 방식. NavStyle rawValue ("tabBar" | "radial"). 기기 로컬(비동기화)
|
|
static let navStyle = "settings.navStyle"
|
|
/// 글래스 버블 메뉴에 펼칠 탭 순서. AppTab rawValue를 쉼표로 연결. 기기 로컬(비동기화)
|
|
static let radialTabOrder = "settings.radialTabOrder"
|
|
/// 글래스 버블 FAB '두 번 눌러 바로 이동' 대상 탭. AppTab rawValue, 빈 값 = 꺼짐. 기기 로컬(비동기화)
|
|
static let radialDoubleTapTab = "settings.radialDoubleTapTab"
|
|
/// 통계 탭 주간/월간을 달력 주기 대신 '오늘 기준 지난 7일/30일'로 보기 (1.4).
|
|
/// 표시 설정이라 기기 로컬(standard defaults) — 위젯 ⑤는 자체 파라미터를 쓴다
|
|
static let statsRollingWindow = "stats.rollingWindow"
|
|
/// 통계 탭 건강 추이 카드에서 마지막으로 고른 지표 raw (1.5(10)).
|
|
/// 표시 설정이라 기기 로컬(standard defaults) — 빈 값이면 타일 선택의 첫 지표
|
|
static let statsHealthMetric = "stats.healthMetric"
|
|
}
|
|
|
|
/// iPhone 전용 내비게이션 방식 (iPad·Mac 사이드바에는 적용되지 않음, 기기 로컬 설정)
|
|
enum NavStyle: String, CaseIterable, Identifiable {
|
|
/// 기본 하단 탭 바 (노출 탭 + 더보기)
|
|
case tabBar
|
|
/// 글래스 버블(Glass Bubbles) 메뉴 — rawValue는 저장 호환을 위해 radial 유지
|
|
case radial
|
|
|
|
var id: String { rawValue }
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .tabBar: return String(localized: "기본 하단 탭 바")
|
|
case .radial: return String(localized: "글래스 버블 메뉴")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 모음 탭 목표 진행 현황 카드의 표시 방식 (선택된 모든 목표 카드에 일괄 적용)
|
|
enum GoalCardStyle: String, CaseIterable, Identifiable {
|
|
/// 다짐(최대 3개)별로 하루/주간/월간 진행률을 각각 표시
|
|
case perQuest
|
|
/// 소속 다짐 전체의 하루/주간/월간 진행률을 합산(평균)해 한 번에 표시
|
|
case combined
|
|
|
|
var id: String { rawValue }
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .perQuest: return String(localized: "다짐별로 각각")
|
|
case .combined: return String(localized: "전체 다짐 합산")
|
|
}
|
|
}
|
|
}
|
|
|
|
enum TabBarConfig {
|
|
/// 기본 노출 탭: 메인 · 행동 · 기록 (+더보기)
|
|
static let defaultVisibleTabs = "main,action,history"
|
|
static let maxVisible = 3
|
|
static let minVisible = 1
|
|
}
|
|
|
|
/// 장시간 측정 알림 옵션 선택지 (시간)
|
|
enum LongSessionAlertOption {
|
|
static let choices: [(hours: Int, label: String)] = [
|
|
(0, String(localized: "사용 안 함")),
|
|
(1, String(localized: "1시간")),
|
|
(2, String(localized: "2시간")),
|
|
(3, String(localized: "3시간")),
|
|
(6, String(localized: "6시간")),
|
|
(12, String(localized: "12시간")),
|
|
]
|
|
}
|
|
|
|
/// 짧은 기록 무시 옵션 선택지 (초)
|
|
enum MinSessionOption {
|
|
static let choices: [(seconds: Int, label: String)] = [
|
|
(0, String(localized: "사용 안 함")),
|
|
(3, String(localized: "3초")),
|
|
(5, String(localized: "5초")),
|
|
(10, String(localized: "10초")),
|
|
(30, String(localized: "30초")),
|
|
(60, String(localized: "1분")),
|
|
]
|
|
}
|
|
|
|
enum LiveActivityMode: String, CaseIterable, Identifiable {
|
|
case latest
|
|
case earliest
|
|
|
|
var id: String { rawValue }
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .latest: return String(localized: "가장 나중에 시작한 행동")
|
|
case .earliest: return String(localized: "가장 먼저 시작한 행동")
|
|
}
|
|
}
|
|
}
|
|
|
|
enum AppLanguage: String, CaseIterable, Identifiable {
|
|
case ko, en, ja
|
|
|
|
var id: String { rawValue }
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .ko: return "한국어"
|
|
case .en: return "English"
|
|
case .ja: return "日本語"
|
|
}
|
|
}
|
|
}
|
|
|
|
// 무료 사용 한도(FreeLimits)는 Shared/Premium.swift로 이동 (위젯·인텐트에서도 사용)
|
|
|
|
/// 집계에 필요한 설정 스냅숏
|
|
struct TrackingSettings {
|
|
var weekStartWeekday: Int
|
|
var dayStartMinutes: Int
|
|
|
|
static func current() -> TrackingSettings {
|
|
// App Group 우선(위젯·인텐트와 공유), 이관 전 값은 standard에서 폴백
|
|
let group = AppGroup.defaults
|
|
let standard = UserDefaults.standard
|
|
func intValue(_ key: String, default defaultValue: Int) -> Int {
|
|
(group.object(forKey: key) ?? standard.object(forKey: key)) as? Int ?? defaultValue
|
|
}
|
|
return TrackingSettings(
|
|
weekStartWeekday: intValue(SettingsKeys.weekStartWeekday, default: 2),
|
|
dayStartMinutes: intValue(SettingsKeys.dayStartMinutes, default: 0)
|
|
)
|
|
}
|
|
|
|
var calendar: Calendar {
|
|
var cal = Calendar.current
|
|
cal.firstWeekday = weekStartWeekday
|
|
return cal
|
|
}
|
|
}
|