feat(1.5-p14): 통계·기록 탭 건강 데이터 — 추이·합계 카드와 그날 값 칩, 빌드 10
사용자 요구('이거까지 하고 배포'): 통계·기록에서도 건강 데이터를 하루다님 스타일로.
전부 표시 전용 — 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
This commit is contained in:
parent
ebe3551e07
commit
0d78789a05
File diff suppressed because one or more lines are too long
@ -206,3 +206,15 @@
|
|||||||
- **새 구성**: [뒤로가기(시스템)] / **아이콘 타일=실행 버튼**(64pt, 우하단 배지 +(횟수)·▶(대기)·■(측정 중), 측정 중=노란 테두리+노란 배지 — 앱 전체 '측정 중=노랑' 문법) / 이름 / 기간 라벨+큰 값(측정 중 노란 타이머) / **상태별 한 줄 힌트**("아이콘을 누르면 시작/종료/1회 추가"). 별도 '측정 중' 줄은 사중 중복이라 제거. 실행 경로는 기존과 동일한 store.run(햅틱 포함) — 트리거 뷰만 바뀜, a11y 라벨은 기존 버튼 문구 재사용.
|
- **새 구성**: [뒤로가기(시스템)] / **아이콘 타일=실행 버튼**(64pt, 우하단 배지 +(횟수)·▶(대기)·■(측정 중), 측정 중=노란 테두리+노란 배지 — 앱 전체 '측정 중=노랑' 문법) / 이름 / 기간 라벨+큰 값(측정 중 노란 타이머) / **상태별 한 줄 힌트**("아이콘을 누르면 시작/종료/1회 추가"). 별도 '측정 중' 줄은 사중 중복이라 제거. 실행 경로는 기존과 동일한 store.run(햅틱 포함) — 트리거 뷰만 바뀜, a11y 라벨은 기존 버튼 문구 재사용.
|
||||||
- **작은 화면 대응**: 화면 높이<220pt(40·41mm)는 컴팩트 메트릭(아이콘 54·간격 6) — 40mm에서도 힌트까지 스크롤 없이 한 화면. 힌트는 lineLimit 1+minimumScaleFactor 0.75(ja "アイコンをタップで計測終了"가 40mm에서 두 줄로 꺾여 폴드 밑으로 가던 실측 수정 — 일기 건강 칩 전례 문법).
|
- **작은 화면 대응**: 화면 높이<220pt(40·41mm)는 컴팩트 메트릭(아이콘 54·간격 6) — 40mm에서도 힌트까지 스크롤 없이 한 화면. 힌트는 lineLimit 1+minimumScaleFactor 0.75(ja "アイコンをタップで計測終了"가 40mm에서 두 줄로 꺾여 폴드 밑으로 가던 실측 수정 — 일기 건강 칩 전례 문법).
|
||||||
- **검증**: 신규 인자 `-watchSeedSnapshot`(페어링 없는 합성 스냅숏 — 3상태)+`-watchOpenActionIndex`로 **49mm(울트라)·46mm(26.2)·40mm SE(11.5) × 횟수형/측정 중/대기 + ja** 스크린샷 전부 잘림 0(임시 심은 QA 후 삭제). 도움말 문구 갱신+전 카탈로그 missing/stale 0, 워치·iOS Debug·Store 빌드 그린. 스키마·빌드 번호 무변경(9 유지 — ASC 업로드 전).
|
- **검증**: 신규 인자 `-watchSeedSnapshot`(페어링 없는 합성 스냅숏 — 3상태)+`-watchOpenActionIndex`로 **49mm(울트라)·46mm(26.2)·40mm SE(11.5) × 횟수형/측정 중/대기 + ja** 스크린샷 전부 잘림 0(임시 심은 QA 후 삭제). 도움말 문구 갱신+전 카탈로그 missing/stale 0, 워치·iOS Debug·Store 빌드 그린. 스키마·빌드 번호 무변경(9 유지 — ASC 업로드 전).
|
||||||
|
|
||||||
|
### §12 후속 6 — 통계·기록 탭 건강 데이터 (2026-08-27, 빌드 10)
|
||||||
|
|
||||||
|
- **발단(사용자 소킹 요구)**: "통계·기록에서 건강 데이터를 하루다님 스타일로 정리해 보고 싶다 — 기존 동작·논리·스타일 유지, 건강만 추가" → 아리나 제안 형태 승인.
|
||||||
|
- **구현(전부 표시 전용 — HealthCache 인메모리 읽기만, HK 신규 조회·스키마·집계 수학 무변경)**:
|
||||||
|
- 통계 탭 맨 아래: 하루='건강 데이터' 칩 카드(선택 날짜 값·값 없으면 대시), 주간/월간(롤링 포함)='건강 지표 (일별)' 추이 카드(지표 선택 캡슐 메뉴, stats.healthMetric 기기 로컬 저장, 주간=요일 축/월간=일 숫자 축, Y축 단위 시간(h)·보·kcal·km·L, 분홍 단일 선)+'건강 지표 합계·평균' 표(합계·하루 평균, 달력 월간만 주 평균 — 분모는 기존 elapsedDay/WeekCount 규칙 그대로, 값 0 지표 생략, 전무 시 빈 문구 카드 1장).
|
||||||
|
- 기록 탭 목록 보기 맨 아래 '건강 데이터' 섹션(그날 칩 — 시간·횟수 섹션 뒤라 기존 배치 불변).
|
||||||
|
- 표시 규칙: 지표=모음 탭 타일 선택 상속(HealthMetric.selectedList), **마스터 스위치=설정 '건강 데이터 표시'**(healthTilesEnabled 의미 확장 — 푸터 문구 갱신), 미연결·맥=조용히 숨김, 내보내기 리포트 미포함(비교 카드 전례).
|
||||||
|
- 값 공급 단일 지점 `HealthDisplayValues` 신설(수면=SleepWindowPrefs 유효 구간 키) — DiaryHealthCard도 위임으로 통일, 칩 UI는 `HealthValueChipGrid` 공용(그리드+refreshToday 신선도).
|
||||||
|
- **문구**: 도움말 건강 첫 주제·신기능 팝업 타일 항목·설정 푸터에 통계·기록 카드 반영, whats-new-1.5 3언어 bullet 추가. 새 키 5(추이 카드 제목·표 제목·빈 문구·'보'·메뉴 a11y)+수정 3, 전 카탈로그 missing/stale 0.
|
||||||
|
- **한계(의도)**: 종목별 운동 통계 제외(하루 합계 캐시가 다짐 종목만 — 백필 확장 필요, 1.6 후보), 400일 보존 밖 과거=빈 값, 신선도=타일과 동일(캐시).
|
||||||
|
- **검증**: 자가 검증 62/20/27/10 ALL PASS(⑱ 최적화·수치 불변 재확인), 시각 QA — 하루 칩(8,432보 시드 일치)·주간(합계 53,966보=시드 7일 합산 검산 일치, 요일 축)·월간 롤링(30일 분모 2,051보)·기록 목록 과거 날짜(7,589보=시드×0.9)·ja(ヘルス指標(日別)·指標別合計・平均)·18.5(경과일 분모 4일 실측 7,799보=31,199÷4 검산 일치), Debug·Store·18.5 빌드 그린. 빌드 10.
|
||||||
|
|||||||
@ -601,7 +601,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements;
|
CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -643,7 +643,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements;
|
CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -686,7 +686,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -722,7 +722,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -758,7 +758,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
||||||
@ -794,7 +794,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
||||||
@ -827,7 +827,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Widgets/Info.plist;
|
INFOPLIST_FILE = Widgets/Info.plist;
|
||||||
@ -853,7 +853,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 9;
|
CURRENT_PROJECT_VERSION = 10;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Widgets/Info.plist;
|
INFOPLIST_FILE = Widgets/Info.plist;
|
||||||
|
|||||||
@ -2587,6 +2587,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"건강 지표 (일별)": {
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Health metrics (daily)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "ヘルス指標(日別)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"건강 지표 합계·평균": {
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Totals & averages by metric"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "指標別合計・平均"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"건강한 생활 습관": {
|
"건강한 생활 습관": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
@ -2706,18 +2738,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"걸음·운동·수면 같은 애플 건강 앱의 오늘 데이터를 모음 탭 타일로 볼 수 있어요. 값은 기기 밖으로 나가지 않아요.": {
|
"걸음·운동·수면 같은 애플 건강 앱의 오늘 데이터를 모음 탭 타일로 볼 수 있어요. 통계·기록 탭에서도 함께 보여요. 값은 기기 밖으로 나가지 않아요.": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "See today's Apple Health data — steps, exercise, sleep and more — as tiles on the Home tab. Values never leave your device."
|
"value": "See today's Apple Health data — steps, exercise, sleep and more — as tiles on the Home tab, and on the Stats and History tabs too. Values never leave your device."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja": {
|
"ja": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "歩数・運動・睡眠など、Appleヘルスケアの今日のデータをホームタブのタイルで見られます。値が端末の外に出ることはありません。"
|
"value": "歩数・運動・睡眠など、Appleヘルスケアの今日のデータをホームタブのタイルで見られます。統計・記録タブにも一緒に表示されます。値が端末の外に出ることはありません。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6604,18 +6636,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요.": {
|
"모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요. 골라 둔 지표는 통계 탭 아래의 건강 카드(하루 값·지표별 추이·합계와 하루 평균)와 기록 탭 목록의 그날 건강 카드에도 그대로 쓰여요 — '건강 데이터 표시'를 끄면 이 카드들도 함께 사라져요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요.": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Tap 'Connect Apple Health' in the Health data row on the Home tab to see today's data — steps, exercise time, sleep — as tiles. Choose which metrics appear, and in what order, with the metrics button on the right of the row; in Settings → Health data you can turn the row off or reorder it against Favorites and the other actions.\nThe values are read from the Apple Health app — Haru Danim never sends them to a server or stores them in iCloud. Steps, exercise and the like are summed for today based on the 'Day start time' setting, while sleep counts everything you slept within your sleep window (9 PM to 9 AM the next day by default) as that day's sleep. Data gathered on Apple Watch shows up after the watch syncs with your iPhone, so if a value looks off, check again in a moment. If a value stays at 0, check Haru Danim's read permission in iPhone Settings → Privacy & Security → Health."
|
"value": "Tap 'Connect Apple Health' in the Health data row on the Home tab to see today's data — steps, exercise time, sleep — as tiles. Choose which metrics appear, and in what order, with the metrics button on the right of the row; in Settings → Health data you can turn the row off or reorder it against Favorites and the other actions. The metrics you pick also drive the health cards at the bottom of the Stats tab (the day's values, per-metric trends, totals and daily averages) and the day's health card in the History tab's list — turning off 'Show health data' hides these cards too.\nThe values are read from the Apple Health app — Haru Danim never sends them to a server or stores them in iCloud. Steps, exercise and the like are summed for today based on the 'Day start time' setting, while sleep counts everything you slept within your sleep window (9 PM to 9 AM the next day by default) as that day's sleep. Data gathered on Apple Watch shows up after the watch syncs with your iPhone, so if a value looks off, check again in a moment. If a value stays at 0, check Haru Danim's read permission in iPhone Settings → Privacy & Security → Health."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja": {
|
"ja": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "ホームタブの「ヘルスデータ」行で「Appleヘルスケアと連携」をタップすると、歩数・運動時間・睡眠など今日のデータをタイルで見られます。どの指標をどの順番で表示するかは行の右にある指標ボタンで選び、設定 →「ヘルスデータ」で行そのものをオフにしたり、お気に入り・その他のアクションとの順番を変えたりできます。\n値はAppleヘルスケアAppが持つデータを読み込んだものです — Haru Danimがサーバーへ送信したりiCloudに保存したりすることはありません。歩数・運動などの値は設定の「1日の開始時刻」を基準に今日1日を合計したもので、睡眠だけは決めておいた睡眠時間帯(初期設定は夜9時〜翌朝9時)に眠った時間をまるごと「その日の睡眠」として数えます。Apple WatchにたまったデータはウォッチとiPhoneが同期した後に表示されるので、値が合わないように見えたら少し待ってから確認してください。値がずっと0の場合は、iPhoneの設定 → プライバシーとセキュリティ → ヘルスケアでHaru Danimの読み取り許可を確認してください。"
|
"value": "ホームタブの「ヘルスデータ」行で「Appleヘルスケアと連携」をタップすると、歩数・運動時間・睡眠など今日のデータをタイルで見られます。どの指標をどの順番で表示するかは行の右にある指標ボタンで選び、設定 →「ヘルスデータ」で行そのものをオフにしたり、お気に入り・その他のアクションとの順番を変えたりできます。選んでおいた指標は、統計タブ下部のヘルスカード(その日の値・指標別の推移・合計と1日平均)と記録タブ一覧のその日のヘルスカードにもそのまま使われます —「ヘルスデータを表示」をオフにすると、これらのカードも一緒に消えます。\n値はAppleヘルスケアAppが持つデータを読み込んだものです — Haru Danimがサーバーへ送信したりiCloudに保存したりすることはありません。歩数・運動などの値は設定の「1日の開始時刻」を基準に今日1日を合計したもので、睡眠だけは決めておいた睡眠時間帯(初期設定は夜9時〜翌朝9時)に眠った時間をまるごと「その日の睡眠」として数えます。Apple WatchにたまったデータはウォッチとiPhoneが同期した後に表示されるので、値が合わないように見えたら少し待ってから確認してください。値がずっと0の場合は、iPhoneの設定 → プライバシーとセキュリティ → ヘルスケアでHaru Danimの読み取り許可を確認してください。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7908,6 +7940,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"보": {
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "steps"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "歩"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"보기": {
|
"보기": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
@ -11300,6 +11348,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"이 기간의 건강 데이터가 없어요": {
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "No health data for this period"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "この期間のヘルスデータがありません"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"이 기록에 대한 메모 (선택)": {
|
"이 기록에 대한 메모 (선택)": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
@ -14522,6 +14586,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"추이를 볼 지표 선택": {
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Choose a metric to chart"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "推移を見る指標を選ぶ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"추적 방식": {
|
"추적 방식": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
@ -14858,18 +14938,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"켜면 애플 건강 앱의 오늘 데이터(걸음·운동·수면 등)를 모음 탭에서 타일로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.": {
|
"켜면 애플 건강 앱의 데이터(걸음·운동·수면 등)를 모음 탭 타일과 통계·기록 탭의 건강 카드로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Turn it on to see today's data from the Apple Health app (steps, exercise, sleep and more) as tiles on the Home tab. Use Section order to arrange Favorites, the other actions and Health data. The sleep display window decides which hours of sleep count as 'that day's sleep' — so it fits even when your days and nights swap."
|
"value": "Turn it on to see data from the Apple Health app (steps, exercise, sleep and more) as tiles on the Home tab and as health cards on the Stats and History tabs. Use Section order to arrange Favorites, the other actions and Health data. The sleep display window decides which hours of sleep count as 'that day's sleep' — so it fits even when your days and nights swap."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja": {
|
"ja": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "オンにすると、AppleヘルスケアAppの今日のデータ(歩数・運動・睡眠など)をホームタブのタイルで見られます。セクションの表示順でお気に入り・その他のアクション・ヘルスデータの配置を決められます。睡眠の表示時間帯は、どの時間帯の眠りを「その日の睡眠」と見なすかを決めます — 昼夜が入れ替わる日があっても合わせられます。"
|
"value": "オンにすると、AppleヘルスケアAppのデータ(歩数・運動・睡眠など)をホームタブのタイルと、統計・記録タブのヘルスカードで見られます。セクションの表示順でお気に入り・その他のアクション・ヘルスデータの配置を決められます。睡眠の表示時間帯は、どの時間帯の眠りを「その日の睡眠」と見なすかを決めます — 昼夜が入れ替わる日があっても合わせられます。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -355,6 +355,70 @@ struct HealthGuideSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - 표시용 하루 값 조회 (1.5(10) — 일기 카드·통계·기록 카드가 공유하는 단일 지점)
|
||||||
|
|
||||||
|
/// HealthCache의 "표시용" 하루 값 — 수면만 그날의 유효 표시 구간(SleepWindowPrefs) 키를 쓴다.
|
||||||
|
/// 타일(todayValues)·일기 건강 카드와 같은 값 규칙이라 어느 표면에서 봐도 수치가 일치한다.
|
||||||
|
enum HealthDisplayValues {
|
||||||
|
static func value(_ metric: HealthMetric, dayKey: Date) -> Double? {
|
||||||
|
let key = metric == .sleep
|
||||||
|
? "sleep@\(SleepWindowPrefs.spec(forDayKey: dayKey))"
|
||||||
|
: metric.rawValue
|
||||||
|
return HealthCache.value(key, dayKey: dayKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 하루 값 칩 그리드 (1.5(10) — 통계 탭 '하루' 카드·기록 탭 목록 섹션 공용)
|
||||||
|
|
||||||
|
/// 한 날짜의 지표 값 칩 그리드 — 일기 건강 카드의 칩과 같은 모습(값 없으면 대시).
|
||||||
|
/// 지표 목록은 호출부가 넘긴다(모음 탭 타일 선택 상속). 오늘 값 신선도를 위해
|
||||||
|
/// 타일·일기 카드와 같은 refreshToday를 나타날 때 1회 수행한다.
|
||||||
|
struct HealthValueChipGrid: View {
|
||||||
|
let dayKey: Date
|
||||||
|
let metrics: [HealthMetric]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 96), spacing: 8)], spacing: 8) {
|
||||||
|
ForEach(metrics) { metric in
|
||||||
|
chip(metric)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task { await HealthDataStore.shared.refreshToday() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chip(_ metric: HealthMetric) -> some View {
|
||||||
|
let value = HealthDisplayValues.value(metric, dayKey: dayKey)
|
||||||
|
return VStack(alignment: .leading, spacing: 3) {
|
||||||
|
HStack(spacing: 5) {
|
||||||
|
Image(safeSymbol: metric.symbolName)
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
.foregroundStyle(.pink)
|
||||||
|
Text(metric.name)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
if let value {
|
||||||
|
// 긴 값(ja "7時間28分" 등)이 칩 폭에서 말줄임되지 않게 살짝 축소 허용 (일기 카드 전례)
|
||||||
|
Text(metric.valueLabel(value))
|
||||||
|
.font(.system(.subheadline, design: .rounded).weight(.bold).monospacedDigit())
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.7)
|
||||||
|
} else {
|
||||||
|
Text(verbatim: "–")
|
||||||
|
.font(.system(.subheadline, design: .rounded).weight(.bold))
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(Color.primary.opacity(0.045), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
.accessibilityLabel(Text(verbatim: "\(metric.name) \(value.map(metric.valueLabel) ?? String(localized: "기록 없음"))"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 일기 타임테이블 건강 블록 (1.5 추가분 — 수면·운동 실구간, HealthIntervals 소비)
|
// MARK: - 일기 타임테이블 건강 블록 (1.5 추가분 — 수면·운동 실구간, HealthIntervals 소비)
|
||||||
|
|
||||||
/// 일기 하루 정리·내보내기의 타임테이블에 얹는 수면·운동 블록의 단일 공급 지점.
|
/// 일기 하루 정리·내보내기의 타임테이블에 얹는 수면·운동 블록의 단일 공급 지점.
|
||||||
@ -447,10 +511,8 @@ struct DiaryHealthCard: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func value(_ metric: HealthMetric) -> Double? {
|
private func value(_ metric: HealthMetric) -> Double? {
|
||||||
let key = metric == .sleep
|
// 통계·기록 카드와 같은 단일 지점(1.5(10)) — 수면=유효 표시 구간 키 규칙 공유
|
||||||
? "sleep@\(SleepWindowPrefs.spec(forDayKey: dayKey))"
|
HealthDisplayValues.value(metric, dayKey: dayKey)
|
||||||
: metric.rawValue
|
|
||||||
return HealthCache.value(key, dayKey: dayKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@ -302,7 +302,7 @@ struct HelpView: View {
|
|||||||
HelpTopic(
|
HelpTopic(
|
||||||
symbol: "heart.fill",
|
symbol: "heart.fill",
|
||||||
title: "모음 탭에서 건강 데이터 보기",
|
title: "모음 탭에서 건강 데이터 보기",
|
||||||
body: "모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요."
|
body: "모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요. 골라 둔 지표는 통계 탭 아래의 건강 카드(하루 값·지표별 추이·합계와 하루 평균)와 기록 탭 목록의 그날 건강 카드에도 그대로 쓰여요 — '건강 데이터 표시'를 끄면 이 카드들도 함께 사라져요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요."
|
||||||
),
|
),
|
||||||
HelpTopic(
|
HelpTopic(
|
||||||
symbol: "bed.double.fill",
|
symbol: "bed.double.fill",
|
||||||
|
|||||||
@ -367,6 +367,11 @@ private struct HistoryRecordsView: View {
|
|||||||
@Query private var sessions: [TimeSession]
|
@Query private var sessions: [TimeSession]
|
||||||
@Query private var entries: [CountEntry]
|
@Query private var entries: [CountEntry]
|
||||||
|
|
||||||
|
// 1.5(10): 목록 보기의 '건강 데이터' 섹션 — 표시 여부·지표는 모음 탭 타일 설정 상속.
|
||||||
|
// '건강 데이터 표시'(설정 → 건강 데이터)가 마스터 스위치 (통계 탭 건강 카드와 동일 규칙)
|
||||||
|
@AppStorage(LocalPrefsKeys.healthTilesEnabled, store: AppGroup.defaults) private var healthTilesEnabled = true
|
||||||
|
@AppStorage(LocalPrefsKeys.healthMetrics, store: AppGroup.defaults) private var healthMetricsRaw = ""
|
||||||
|
|
||||||
private let math = DayMath()
|
private let math = DayMath()
|
||||||
|
|
||||||
init(dayKey: Date, mode: HistoryMode, timetableWeekly: Binding<Bool>,
|
init(dayKey: Date, mode: HistoryMode, timetableWeekly: Binding<Bool>,
|
||||||
@ -549,6 +554,17 @@ private struct HistoryRecordsView: View {
|
|||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 1.5(10): 그날의 건강 값 칩 — 타임테이블의 수면·운동 블록과 짝을 이루는 목록 보완.
|
||||||
|
// 표시 전용(HealthCache 읽기만)이라 기록 집계·필터·내보내기에는 관여하지 않고,
|
||||||
|
// 기존 두 섹션 아래에 덧붙어 화면 배치도 그대로다 (통계 탭 건강 카드와 같은 표시 규칙)
|
||||||
|
if HealthDataStore.isAvailable && healthTilesEnabled && HealthDataStore.shared.hasRequestedAuth {
|
||||||
|
Section("건강 데이터") {
|
||||||
|
HealthValueChipGrid(
|
||||||
|
dayKey: dayKey,
|
||||||
|
metrics: HealthMetric.selectedList(raw: healthMetricsRaw)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -228,7 +228,7 @@ struct SettingsView: View {
|
|||||||
} header: {
|
} header: {
|
||||||
Text("건강 데이터")
|
Text("건강 데이터")
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("켜면 애플 건강 앱의 오늘 데이터(걸음·운동·수면 등)를 모음 탭에서 타일로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.")
|
Text("켜면 애플 건강 앱의 데이터(걸음·운동·수면 등)를 모음 탭 타일과 통계·기록 탭의 건강 카드로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.")
|
||||||
}
|
}
|
||||||
.id("healthSection")
|
.id("healthSection")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -335,6 +335,13 @@ private struct StatsChartsView: View {
|
|||||||
@Query private var sessions: [TimeSession]
|
@Query private var sessions: [TimeSession]
|
||||||
@Query private var entries: [CountEntry]
|
@Query private var entries: [CountEntry]
|
||||||
|
|
||||||
|
// 1.5(10): 건강 카드 — 표시 여부·지표는 모음 탭 타일 설정을 그대로 상속 (별도 설정 없음).
|
||||||
|
// '건강 데이터 표시'(설정 → 건강 데이터)가 마스터 스위치라 끄면 타일과 함께 사라진다
|
||||||
|
@AppStorage(LocalPrefsKeys.healthTilesEnabled, store: AppGroup.defaults) private var healthTilesEnabled = true
|
||||||
|
@AppStorage(LocalPrefsKeys.healthMetrics, store: AppGroup.defaults) private var healthMetricsRaw = ""
|
||||||
|
/// 추이 카드에서 마지막으로 고른 지표 (기기 로컬 표시 설정 — statsRollingWindow와 같은 계층)
|
||||||
|
@AppStorage(SettingsKeys.statsHealthMetric) private var statsHealthMetricRaw = ""
|
||||||
|
|
||||||
private let math = DayMath()
|
private let math = DayMath()
|
||||||
|
|
||||||
init(span: StatSpan, anchorDayKey: Date, rolling: Bool,
|
init(span: StatSpan, anchorDayKey: Date, rolling: Bool,
|
||||||
@ -509,6 +516,8 @@ private struct StatsChartsView: View {
|
|||||||
case .month:
|
case .month:
|
||||||
monthCharts(data)
|
monthCharts(data)
|
||||||
}
|
}
|
||||||
|
// 건강 카드는 행동 통계 아래에 덧붙인다 (1.5(10) — 기존 화면 배치 불변)
|
||||||
|
healthSection
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
Color.clear
|
Color.clear
|
||||||
@ -1081,6 +1090,224 @@ private struct StatsChartsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: 건강 데이터 (1.5(10) — HealthCache 인메모리 읽기만 하는 표시 전용 카드.
|
||||||
|
// 통계 내보내기 리포트에는 넣지 않는다 — '이전 기간 비교 카드'와 같은 의도된 화면 전용)
|
||||||
|
|
||||||
|
/// 표시 판정 — 건강 지원 기기 + 설정 '건강 데이터 표시' 켬 + 애플 건강 연결됨.
|
||||||
|
/// 연결 전에는 조용히 숨긴다 (연결 유도는 모음 탭 CTA 담당 — 통계에서 권유하지 않음)
|
||||||
|
private var showsHealth: Bool {
|
||||||
|
HealthDataStore.isAvailable && healthTilesEnabled && HealthDataStore.shared.hasRequestedAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 표시 지표 = 모음 탭 타일 선택 상속 (일기 건강 카드와 같은 상속 문법)
|
||||||
|
private var healthMetricsList: [HealthMetric] {
|
||||||
|
HealthMetric.selectedList(raw: healthMetricsRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 추이 카드의 현재 지표 — 저장값이 목록에 없으면(타일 선택 변경) 첫 지표로 폴백
|
||||||
|
private var chartHealthMetric: HealthMetric {
|
||||||
|
if let saved = HealthMetric(rawValue: statsHealthMetricRaw),
|
||||||
|
healthMetricsList.contains(saved) {
|
||||||
|
return saved
|
||||||
|
}
|
||||||
|
return healthMetricsList.first ?? .steps
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var healthSection: some View {
|
||||||
|
if showsHealth {
|
||||||
|
if span == .day {
|
||||||
|
// 하루: 그날 지표 값 칩 (일기 건강 카드와 같은 모습 — 값 없으면 대시)
|
||||||
|
chartCard("건강 데이터") {
|
||||||
|
HealthValueChipGrid(dayKey: anchorDayKey, metrics: healthMetricsList)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let days = math.dayKeys(in: spanRange)
|
||||||
|
let hasAny = days.contains { day in
|
||||||
|
healthMetricsList.contains { HealthDisplayValues.value($0, dayKey: day) != nil }
|
||||||
|
}
|
||||||
|
if hasAny {
|
||||||
|
healthTrendCard(days: days)
|
||||||
|
healthSummaryCard(days: days)
|
||||||
|
} else {
|
||||||
|
// 캐시 보존(400일) 밖 과거 등 — 빈 카드 하나로 정리
|
||||||
|
chartCard("건강 데이터") { emptyHealthText }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct HealthDayPoint: Identifiable {
|
||||||
|
let dayKey: Date
|
||||||
|
let value: Double
|
||||||
|
var id: Date { dayKey }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 차트에 그릴 값 변환 — 시간류는 시간(h), 거리·물은 km·L, 나머지는 원 단위
|
||||||
|
private func healthChartValue(_ metric: HealthMetric, _ raw: Double) -> Double {
|
||||||
|
if metric.isDuration { return raw / 3600 }
|
||||||
|
switch metric {
|
||||||
|
case .distance, .water: return raw / 1000
|
||||||
|
default: return raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 차트 Y축 단위 라벨 (kcal·km·L은 라틴 표기라 언어 무관)
|
||||||
|
@ViewBuilder
|
||||||
|
private func healthUnitLabel(_ metric: HealthMetric) -> some View {
|
||||||
|
if metric.isDuration || metric == .standHours {
|
||||||
|
Text("시간(h)")
|
||||||
|
} else {
|
||||||
|
switch metric {
|
||||||
|
case .steps: Text("보")
|
||||||
|
case .activeEnergy: Text(verbatim: "kcal")
|
||||||
|
case .distance: Text(verbatim: "km")
|
||||||
|
case .water: Text(verbatim: "L")
|
||||||
|
default: Text(verbatim: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 지표별 일별 추이 카드 — 행동 꺾은선 카드와 같은 문법, 지표는 우상단 메뉴에서 선택
|
||||||
|
private func healthTrendCard(days: [Date]) -> some View {
|
||||||
|
let metric = chartHealthMetric
|
||||||
|
let points = days.map { day in
|
||||||
|
HealthDayPoint(
|
||||||
|
dayKey: day,
|
||||||
|
value: healthChartValue(metric, HealthDisplayValues.value(metric, dayKey: day) ?? 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return VStack(alignment: .leading, spacing: 12) {
|
||||||
|
HStack {
|
||||||
|
Text("건강 지표 (일별)")
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
Spacer()
|
||||||
|
Menu {
|
||||||
|
ForEach(healthMetricsList) { candidate in
|
||||||
|
Button {
|
||||||
|
statsHealthMetricRaw = candidate.rawValue
|
||||||
|
} label: {
|
||||||
|
if candidate == metric {
|
||||||
|
Label(candidate.name, systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text(candidate.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(safeSymbol: metric.symbolName)
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
Text(metric.name)
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
Image(systemName: "chevron.up.chevron.down")
|
||||||
|
.font(.system(size: 9, weight: .semibold))
|
||||||
|
}
|
||||||
|
.foregroundStyle(.pink)
|
||||||
|
.padding(.horizontal, 9)
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
.background(Color.pink.opacity(0.12), in: Capsule())
|
||||||
|
}
|
||||||
|
.accessibilityLabel(Text("추이를 볼 지표 선택"))
|
||||||
|
}
|
||||||
|
Chart(points) { point in
|
||||||
|
LineMark(
|
||||||
|
x: .value("날짜", point.dayKey, unit: .day),
|
||||||
|
y: .value(metric.name, point.value)
|
||||||
|
)
|
||||||
|
.foregroundStyle(.pink)
|
||||||
|
.symbol(.circle)
|
||||||
|
.interpolationMethod(.monotone)
|
||||||
|
}
|
||||||
|
.chartXAxis {
|
||||||
|
if span == .week {
|
||||||
|
AxisMarks(values: .stride(by: .day)) { value in
|
||||||
|
AxisGridLine()
|
||||||
|
AxisValueLabel {
|
||||||
|
if let date = value.as(Date.self) {
|
||||||
|
Text(Format.weekdayShort(math.calendar.component(.weekday, from: date)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
AxisMarks { _ in
|
||||||
|
AxisGridLine()
|
||||||
|
AxisValueLabel(format: .dateTime.day())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chartYAxisLabel { healthUnitLabel(metric) }
|
||||||
|
.frame(height: 200)
|
||||||
|
}
|
||||||
|
.padding(14)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 지표별 합계·하루 평균 표 — 행동 요약 표와 같은 문법 (값 없는 지표 행은 생략)
|
||||||
|
private func healthSummaryCard(days: [Date]) -> some View {
|
||||||
|
let rows: [(metric: HealthMetric, total: Double)] = healthMetricsList.compactMap { metric in
|
||||||
|
let total = days.reduce(0.0) { $0 + (HealthDisplayValues.value(metric, dayKey: $1) ?? 0) }
|
||||||
|
return total > 0 ? (metric, total) : nil
|
||||||
|
}
|
||||||
|
return chartCard("건강 지표 합계·평균") {
|
||||||
|
if rows.isEmpty {
|
||||||
|
emptyHealthText
|
||||||
|
} else {
|
||||||
|
Grid(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 12) {
|
||||||
|
GridRow {
|
||||||
|
Text("지표")
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
Text("합계")
|
||||||
|
.gridColumnAlignment(.trailing)
|
||||||
|
Text("하루 평균")
|
||||||
|
.gridColumnAlignment(.trailing)
|
||||||
|
if span == .month && !rolling {
|
||||||
|
Text("주 평균")
|
||||||
|
.gridColumnAlignment(.trailing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
ForEach(rows, id: \.metric) { row in
|
||||||
|
GridRow {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(safeSymbol: row.metric.symbolName)
|
||||||
|
.font(.system(size: 11, weight: .semibold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
.background(Color.pink, in: RoundedRectangle(cornerRadius: 7, style: .continuous))
|
||||||
|
Text(row.metric.name)
|
||||||
|
.font(.footnote)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
Text(row.metric.valueLabel(row.total))
|
||||||
|
.font(.footnote.weight(.semibold).monospacedDigit())
|
||||||
|
.foregroundStyle(.pink)
|
||||||
|
Text(row.metric.valueLabel(row.total / Double(elapsedDayCount)))
|
||||||
|
.font(.footnote.monospacedDigit())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
if span == .month && !rolling {
|
||||||
|
Text(row.metric.valueLabel(row.total / Double(elapsedWeekCount)))
|
||||||
|
.font(.footnote.monospacedDigit())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyHealthText: some View {
|
||||||
|
Text("이 기간의 건강 데이터가 없어요")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.vertical, 24)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: 공통
|
// MARK: 공통
|
||||||
|
|
||||||
private func chartCard(_ title: LocalizedStringKey, @ViewBuilder content: () -> some View) -> some View {
|
private func chartCard(_ title: LocalizedStringKey, @ViewBuilder content: () -> some View) -> some View {
|
||||||
|
|||||||
@ -68,7 +68,7 @@ struct WhatsNewView: View {
|
|||||||
symbol: "heart.fill",
|
symbol: "heart.fill",
|
||||||
tint: .pink,
|
tint: .pink,
|
||||||
title: String(localized: "모음 탭에서 건강 데이터 보기"),
|
title: String(localized: "모음 탭에서 건강 데이터 보기"),
|
||||||
body: String(localized: "걸음·운동·수면 같은 애플 건강 앱의 오늘 데이터를 모음 탭 타일로 볼 수 있어요. 값은 기기 밖으로 나가지 않아요.")
|
body: String(localized: "걸음·운동·수면 같은 애플 건강 앱의 오늘 데이터를 모음 탭 타일로 볼 수 있어요. 통계·기록 탭에서도 함께 보여요. 값은 기기 밖으로 나가지 않아요.")
|
||||||
)
|
)
|
||||||
feature(
|
feature(
|
||||||
symbol: "figure.run",
|
symbol: "figure.run",
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
• 모음 탭에서 걸음·운동·수면 같은 오늘의 건강 데이터를 타일로 볼 수 있어요 — 값은 기기 밖으로 나가지 않아요
|
• 모음 탭에서 걸음·운동·수면 같은 오늘의 건강 데이터를 타일로 볼 수 있어요 — 값은 기기 밖으로 나가지 않아요
|
||||||
• 건강 데이터로 다짐을 만들 수 있어요(프리미엄) — '매일 8,000보', '수면 8시간', '달리기 30분'처럼요. 운동은 종목별로도 되고, 다짐이 보일 색도 고를 수 있어요
|
• 건강 데이터로 다짐을 만들 수 있어요(프리미엄) — '매일 8,000보', '수면 8시간', '달리기 30분'처럼요. 운동은 종목별로도 되고, 다짐이 보일 색도 고를 수 있어요
|
||||||
• 수면은 '수면 구간'을 정해 두면 자정을 넘겨 자도 하루가 쪼개지지 않아요. 낮에 자는 날이 있다면 '수면 표시 구간'을 요일별로 맞출 수도 있어요
|
• 수면은 '수면 구간'을 정해 두면 자정을 넘겨 자도 하루가 쪼개지지 않아요. 낮에 자는 날이 있다면 '수면 표시 구간'을 요일별로 맞출 수도 있어요
|
||||||
|
• 통계 탭에서 지표별 추이와 합계·하루 평균을, 기록 탭에서 그날의 건강 값을 함께 볼 수 있어요
|
||||||
|
|
||||||
● 일기가 더 넓어졌어요 (프리미엄)
|
● 일기가 더 넓어졌어요 (프리미엄)
|
||||||
• 일기 탭이 아이폰에도 생겼어요 — 하루 정리를 확인하고, '그리기'를 켜면 손가락으로 노트에 쓸 수 있어요
|
• 일기 탭이 아이폰에도 생겼어요 — 하루 정리를 확인하고, '그리기'를 켜면 손가락으로 노트에 쓸 수 있어요
|
||||||
@ -29,6 +30,7 @@ This update brings Haru Danim closer to Apple Health — and your Apple Watch. A
|
|||||||
• See today's health data — steps, exercise, sleep — as tiles on the Home tab. Values never leave your device
|
• See today's health data — steps, exercise, sleep — as tiles on the Home tab. Values never leave your device
|
||||||
• Build quests from health data (Premium): "8,000 steps a day," "8 hours of sleep," "30 minutes of running" — workouts by type, and pick the quest's color too
|
• Build quests from health data (Premium): "8,000 steps a day," "8 hours of sleep," "30 minutes of running" — workouts by type, and pick the quest's color too
|
||||||
• Set a sleep window and sleeping past midnight won't split your night. If some days you sleep in the daytime, tune the display window per weekday
|
• Set a sleep window and sleeping past midnight won't split your night. If some days you sleep in the daytime, tune the display window per weekday
|
||||||
|
• See metric trends with totals and daily averages on the Stats tab, and the day's health values on the History tab
|
||||||
|
|
||||||
● A bigger Diary (Premium)
|
● A bigger Diary (Premium)
|
||||||
• The Diary tab is now on iPhone — review your day, and turn on Draw to write on note pages with your finger
|
• The Diary tab is now on iPhone — review your day, and turn on Draw to write on note pages with your finger
|
||||||
@ -51,6 +53,7 @@ This update brings Haru Danim closer to Apple Health — and your Apple Watch. A
|
|||||||
• ホームタブで歩数・運動・睡眠など今日のヘルスデータをタイルで見られます — 値が端末の外に出ることはありません
|
• ホームタブで歩数・運動・睡眠など今日のヘルスデータをタイルで見られます — 値が端末の外に出ることはありません
|
||||||
• ヘルスデータでクエストを作れます(プレミアム)—「毎日8,000歩」「睡眠8時間」「ランニング30分」など。運動は種目別でもOK、クエストの色も選べます
|
• ヘルスデータでクエストを作れます(プレミアム)—「毎日8,000歩」「睡眠8時間」「ランニング30分」など。運動は種目別でもOK、クエストの色も選べます
|
||||||
• 「睡眠時間帯」を決めておけば、日付をまたいで眠っても1日が分割されません。昼間に眠る日があるなら、表示の時間帯を曜日ごとに調整できます
|
• 「睡眠時間帯」を決めておけば、日付をまたいで眠っても1日が分割されません。昼間に眠る日があるなら、表示の時間帯を曜日ごとに調整できます
|
||||||
|
• 統計タブで指標ごとの推移と合計・1日平均を、記録タブでその日のヘルス値を一緒に見られます
|
||||||
|
|
||||||
● 日記がもっと広く(プレミアム)
|
● 日記がもっと広く(プレミアム)
|
||||||
• 日記タブがiPhoneにも登場 — 1日のまとめを確認し、「描く」をオンにすれば指でノートに書けます
|
• 日記タブがiPhoneにも登場 — 1日のまとめを確認し、「描く」をオンにすれば指でノートに書けます
|
||||||
|
|||||||
@ -38,6 +38,9 @@ enum SettingsKeys {
|
|||||||
/// 통계 탭 주간/월간을 달력 주기 대신 '오늘 기준 지난 7일/30일'로 보기 (1.4).
|
/// 통계 탭 주간/월간을 달력 주기 대신 '오늘 기준 지난 7일/30일'로 보기 (1.4).
|
||||||
/// 표시 설정이라 기기 로컬(standard defaults) — 위젯 ⑤는 자체 파라미터를 쓴다
|
/// 표시 설정이라 기기 로컬(standard defaults) — 위젯 ⑤는 자체 파라미터를 쓴다
|
||||||
static let statsRollingWindow = "stats.rollingWindow"
|
static let statsRollingWindow = "stats.rollingWindow"
|
||||||
|
/// 통계 탭 건강 추이 카드에서 마지막으로 고른 지표 raw (1.5(10)).
|
||||||
|
/// 표시 설정이라 기기 로컬(standard defaults) — 빈 값이면 타일 선택의 첫 지표
|
||||||
|
static let statsHealthMetric = "stats.healthMetric"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// iPhone 전용 내비게이션 방식 (iPad·Mac 사이드바에는 적용되지 않음, 기기 로컬 설정)
|
/// iPhone 전용 내비게이션 방식 (iPad·Mac 사이드바에는 적용되지 않음, 기기 로컬 설정)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user