diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index 3a8cde4..85e1a5a 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -19,7 +19,7 @@ | 번들 ID | `com.yechan.HaruDanim` / App Group `group.com.yechan.HaruDanim` / CloudKit `iCloud.com.yechan.HaruDanim` | | 수익 모델 | 부분 유료화. 무료 한도(행동 10·목표 2·목표당 다짐 3) + 프리미엄 결제 시 해제 | | 결제 상품 | `com.yechan.HaruDanim.premium.monthly`(월 4,400원) / `.yearly`(년 44,000원) / `.lifetime`(77,000원) — **ASC 등록·판매 중(실제 가격, 2026-07-28)**. 로컬 테스트 구성(HaruDanim.storekit)도 동일 가격. 가격은 코드에 없고 `product.displayPrice`로 동적 표기, 년 구독 절약률 배지도 동적 계산 | -| 현재 상태 (2026-08-22) | **버전 1.5 (빌드 9) — 소킹 사용자 요구·실측 수정+성능 최적화 반영(2026-08-26), TestFlight 재업로드 대기. 사용자 예고: 기능 수정은 이것으로 종료(이후는 버그·오류 수정만)**. ✅**동기화 정지 이슈 해결 확인(2026-08-22 밤, 사용자 실기기)**: 일기에 도형·텍스트 상자(DiaryPageItem)가 있으면 그 기기의 CloudKit 동기화 전체가 정지하던 문제(내보내기 대기열 직렬 — 거부된 아이템 뒤로 전부 막힘) — 원인은 **1.3의 DiaryPageItem 스타일 필드 10종이 프로덕션 스키마에 없었던 것**(1.0 출시 때가 마지막 배포였고, 1.3 이후 개발 빌드가 iCloud 내보내기를 한 적이 없어 개발 스키마에도 미생성이었음). 해결 절차 실행: 시뮬 519869F7(사용자 iCloud 로그인·재인증)에서 일기 요소 시드 포함 전체 내보내기로 개발 스키마 필드 생성(success·error nil) → 사용자가 콘솔 재배포 → **도형·텍스트 상자 있는 상태의 동기화 왕복 정상 확인(사용자)**. 교훈: **새 모델 필드는 배포 전 '개발 빌드로 그 타입 레코드를 실제 내보냈는지'까지 확인할 것** — 콘솔 diff에 타입 이름이 떠도 필드가 전부 실렸는지 봐야 한다. 업로드 검증 실측 2건: 빌드 1=ITMS-90683(HealthKit 엔타이틀먼트 보유 시 읽기 전용이어도 `NSHealthUpdateUsageDescription` 필수 — 쓰기 사유 3언어 추가, 쓰기 요청이 없어 표시될 일 없음), 빌드 2=ITMS-90626(**앱 인텐트 메타데이터에 'iPhone' 단어 금지** — 신규 컴플리케이션 인텐트·위젯 설명에서 '하루 다님 앱'으로 교체. 화면 뷰 문구의 iPhone은 스캔 대상 아님[1.4 전례]) → **빌드 3**으로 재업로드. **1.5 추가분(빌드 4, 2026-08-22 — 소킹 중 사용자 요구, 스키마 무변경·전부 로컬 설정/표시 계층)**: ⑦**아이폰 일기 탭 개방**(§6.7 — AppTab.phoneCases 개방[radialOrder 필터 제거·버블 8개 자동 배치 확인], 노트 필기는 맥 '그리기' 토글 문법 재사용[DeviceLayout.isPhone 신설, drawingPolicy anyInput, 가로 스크롤 도구 줄, 배치↔그리기 상호 배타 — 아이패드 경로 무변경], 일기 잠금 설정 전 기기 노출) ⑧**일기 하루 정리 '건강 데이터' 카드**(§6.7 — DiarySection.health[케이스 마지막이라 기존 사용자 목록 끝에 추가], DiaryHealthCard: 지표 선택 시트 공용화[HealthMetricPickerSheet Binding화], 저장은 기기 로컬 diary.healthMetrics[비면 타일 선택 상속], 맥은 목록·화면 모두 숨김, 인쇄 요약에 값 줄 DiaryPrintHealthRow 동승) ⑨**일기 타임테이블 수면·운동 실구간 블록**(§6.7 — **HealthIntervals 신설**[App Group health.intervals.v1, 400일, 실구간을 논리적 하루로 분할 저장·병합, CloudKit 금지 동일], refreshRecent/백필이 유지, ExportSnapshot.injectingHealthBlocks 주입[캘린더 전례·최하층 렌더 healthBlockView 반투명+실선], 지표 고정색 수면 인디고·운동 주황, 필터 시트 '건강 데이터' 토글[RecordFilterSheet showsHealthSection — 전 날짜 공통 diary.timetableHealthHidden, 기본 켬]+**맥 안내 문구**, 일기 필터 버튼 상시 노출로 변경, 내보내기 렌더러 동일 주입) ⑩**수면 표시 구간 설정**(§6.8 — SleepWindowPrefs[LocalPrefs]: 전체 스펙+요일별 7종[켤 때만], 설정 → 건강 데이터 → '수면 표시 구간'[SleepWindowSettingsView — 시작=끝이면 24시간 창], 평문 'sleep' 캐시 키=그날 유효 구간 값으로 변경[타일·일기 카드 공유], 백필 서명에 요일 매핑 포함[배정만 바뀌어도 재백필], **새 수면 다짐 기본 구간을 여기서 시드**) ⑪일기 목표 카드의 건강 다짐 값 단위 실측 수정("8,432회/8,000보"→지표 단위 — DiaryGoalsCard.valueLabel healthTarget 분기) ⑫**소킹 실기기 실측 버그 2건 수정(빌드 5)**: ⓐ더보기 탭 안 일기 달력에서 날짜 탭이 조용히 무시됨 — `AppRouter.morePath`가 `[AppTab]` 고정 타입이라 Date 푸시 불가 → **NavigationPath로 변경**(회귀 감시 인자 `-morePushDiaryToday`, 라디얼·사이드바는 원래 무사) ⓑ수면 블록이 수면 단계(코어·렘) 로우데이터대로 칸칸이 갈라짐 → **10분 이하의 깸은 이어 붙여 한 덩어리로**(DiaryHealthTimetable.sleepDisplayMergeGap — 표시 전용, 값 무영향. 20분쯤 깼다 자면 나뉘는 게 맞다는 사용자 규칙) ⑬**기록 탭 타임테이블에도 수면·운동 블록**(하루·주간 — DiaryHealthTimetable.blocks 단일 공급 지점·숨김 토글 공유[기록 필터 시트에도 건강 섹션], 내보내기는 `injectingHealthBlocks(forDayKeys:math:)`로 하루·주간 주입 + trim 표시 범위를 블록만큼 확장) ⑭**타임테이블 표시 색 설정**(설정 → 건강 데이터 → '타임테이블 표시 색' — 수면·운동 ColorPicker, 기본 인디고·주황[빈 값], 전 타임테이블·내보내기 공통 적용, 인자 `-healthColorPreview`) ⑮**버블 찌그러짐 실측 수정(빌드 6, 2026-08-23)**: 일기 버블 추가로 8개가 되자 이웃 간격(~9.6pt)이 유리 합체 판정 거리(18pt) 안으로 들어와 정지 상태에서도 원들이 서로를 향해 늘어남 — `glassBlendSpacing` 18→6(펼침 상태 정원 분리, FAB 모핑은 겹침 구간이라 유지. 마케팅 06-bubble 3언어 재촬영) ⑯**건강 다짐 표시 색(빌드 7, 2026-08-23 — 사용자 요구)**: 건강 다짐이 고정 분홍이던 것을 편집기 건강 섹션에서 색 선택(프리셋 12+피커, 기본 분홍=빈 값[시스템 분홍의 다크 적응 유지]) — **Quest 신규 필드 `healthColorHex`**(기본 "", targetColor 단일 지점 분기라 목표 탭·모음 카드·일기·위젯·워치 자동 반영, 행동·꼬리표 다짐은 저장 시 "" 정규화, 타임테이블 블록 색과 별개). ⚠️**CloudKit 스키마 추가 — 배포 절차(개발 내보내기→콘솔 Deploy)가 빌드 7 설치보다 선행 필수**(DiaryPageItem 전례 — 미배포 상태로 다짐 수정 시 그 기기 동기화 정지). → 배포·설치 완료(사용자). ⑰**위젯·워치 색 미반영 실측 수정(빌드 8)**: 위젯 QuestCellSnapshot(WidgetSupport)과 워치 스냅숏(WatchSyncManager)이 targetColor를 안 타고 자체 폴백을 쓰고 있었음 — 위젯은 분홍 #E0558C 하드코딩, **워치는 아예 초록 폴백(1.5 출시분부터 건강 다짐이 일반색으로 보이던 잠복 표시 버그)** → 두 곳 다 healthColorHex 우선·기본 #E0558C로 통일(④ 그리드에서 걸음=파랑·나머지=분홍 실측). 값 라벨은 두 표면 모두 원래 건강 단위 정상. ⑱**성능 최적화(빌드 9, 2026-08-26 — 소킹 중 '1.4 대비 버벅임 증가' 보고, 동작·수치 완전 불변 원칙)**: 원인 진단 결과 1.5 건강 캐시가 값 1개 읽을 때마다 전체 딕셔너리(지표×400일)를 브리징 — 건강 다짐 행 하나 렌더에 ~38회+, 워치 스냅숏에 다짐당 ~76회가 메인 스레드에서 반복. 적용 7건(전부 등가 최적화): ⓐHealthCache·HealthIntervals **인메모리 캐시**(세대 카운터 `health.cache.gen`/`health.intervals.gen`으로 프로세스 간 무효화 — 쓰기는 메인 앱 단일 작성자, workoutKeys의 load() 이중 호출도 제거) ⓑ**DayMath.calendar 저장 프로퍼티화**(접근마다 Calendar 사본 생성하던 전역 곱셈 비용 제거) ⓒ**수면 백필 포인터 스위프**(400일×전체 수면 샘플 O(n×m) 전수 클리핑 → 정렬+lo/hi 스위프, 무작위 퍼즈 3,000회 등가 증명 — 하루 첫 실행 정지 해소) ⓓrefreshRecent 캐시 기록 배칭(일수×2회→총 2회) ⓔ**QuestProgress.spanValue/spanRawValue 버킷화**(하루 단위 다짐의 주/월 게이지가 날마다 전체 기록 재순회하던 O(일수×기록수) → perDayGaugeValues 1회 순회. ⚠️연속 계산 perDayValues와 달리 **미래 기록을 now로 자르지 않는 게이지 스펙 보존** — spanRawValue는 예전 Aggregator-only 동작 보존 위해 perDayRecordValues 직결) ⓕ위젯 라이브 타임라인 makeEntry에 refresh 파라미터(미래 엔트리 6개가 각각 새 ModelContainer를 열던 것 → 타임라인당 1회, ②③④·잠금 4곳) ⓖ일기 건강 카드 칩 value() 이중 호출 제거. **의도적으로 제외**: 재조회 스로틀(값 신선도가 변해 동작 변화), 워치 스냅숏 백그라운드화(SwiftData 메인 컨텍스트 위험), 모음 탭 셀 오늘 값 버킷화(구조 변경 — 체감 보고 재논의). 검증: A/B 등가(옛/새 빌드를 각각 새 컨테이너+동일 시드로 캡처 — streak·건강·인텐트 덤프 3종 값 동일[정렬 후 diff 0], 목표·모음·위젯④·일기 4화면 **픽셀 동일**[상태바 시계·시뮬 iCloud 배너 제외]), 자가 검증 62/20/27/10 ALL PASS(26.5+**18.5 심 62건도 ALL PASS**), 벌크 1만 건 3탭 렌더 정상, 퍼즈 3,000회, 3종 빌드(Store·워치 포함) 그린, 신규 현지화 문자열 0(카탈로그 무변경). whats-new-1.5 3언어에 성능 개선 줄 추가. ⑲**워치 미니 행동 화면 재구성(빌드 9 동일, 2026-08-26 — 사용자 울트라 실측: 하단 실행 버튼이 살짝 잘림)**: 하단 버튼 줄 제거, **아이콘=실행 버튼**(우하단 +/▶/■ 배지, 측정 중=노란 테두리+노란 ■ 배지+노란 타이머, 별도 '측정 중' 줄은 중복이라 제거) + 상태별 한 줄 힌트('아이콘을 누르면 시작/종료/1회 추가' — lineLimit 1+축소 0.75로 ja 40mm 한 줄 유지). 작은 워치(높이<220pt)는 컴팩트 메트릭(아이콘 54·간격 6)으로 **40mm에서도 힌트까지 스크롤 없이 한 화면**. a11y 라벨은 기존 버튼 문구 유지, 실행 경로는 동일한 store.run. 도움말 '행동 기록 컴플리케이션' 문구 갱신(3언어), 검증: 49mm(울트라)·46mm·40mm(11.5) × 3상태 + ja 렌더 스크린샷 전부 잘림 0, 워치·iOS·Store 빌드 그린, 전 카탈로그 0/0. 신규 검증 인자 `-watchSeedSnapshot`(페어링 없는 합성 스냅숏)·`-watchOpenActionIndex N`(§14). ⑳**출시 전 최종 점검 완료(2026-08-26)**: 1.4 기준 커밋(b62808f) 대비 카탈로그 추가 키 **241건 전수 번역 검수** — 발견·수정 3건(워치·워치위젯 ja 용어 위반 '行動'→'アクション' 2곳, 신기능 팝업 en 'tap it to act'→'run the action' 자연화), whats-new·프로모션·설명 3언어와 신기능 팝업 6항목·도움말(기기 분기 포함) 모두 1.5 기능 인벤토리 대비 누락 0, 스크린샷 세트 원본 57+합성 63장 완비·촬영 후 변경과 어긋남 없음(버블 재촬영·기본 분홍·워치=페이스 합성), **keywords.txt 1.5 보강**(ko '건강,수면,걸음'·ja 'ヘルスケア,睡眠' 추가[기존 무삭제, 88/95자] — en은 97/100자라 보류, ASC에 새로 붙여넣기 필요), InfoPlist 건강 문구 ko/en/ja 3언어 확인. 검증: 61/20/27/10 ALL PASS(26.5), 3종 빌드 그린, 전 카탈로그 missing/stale 0, 시각 QA(아이폰 요약·건강 카드·타임테이블 블록·노트 도구 줄·버블 8개·수면 구간 화면·아이패드·맥 분기·18.5). 스토어 자료: whats-new-1.5에 일기 확장 절 추가·설명 3언어 일기 항목 갱신. 신규 인자 `-sleepWindowPreview`(§14), `-seedHealthCache`가 실구간(수면 자정 분할+달리기)도 시드. 계획·결정 이력은 `Docs/plan-1.5.md`(전 섹션 ✅구현 완료). 1.5 내용: ①**워치 '행동 기록' 컴플리케이션**(§11 — 행동 편집 옵트인+표시 기간 5종[하루/이번 주/이번 달/지난 7일/지난 30일, 롤링은 rollingRange 재사용], 측정 중 노란 타이머, 탭=워치 앱 미니 행동 화면 push[뒤로가기=메인], recommendations 상한 없음) ②**모음 탭 건강 데이터 타일**(무료, §6.1 — 걸음·운동·수면 등 8지표 선택·순서, 접기, 연결 CTA[명시적 탭 권한], 안내 시트, 즐겨찾기·나머지·건강 3섹션 순서 설정[§6.8], 맥은 섹션 숨김) ③**건강 다짐**(프리미엄, §4.2·§6.4 — 8지표+운동 종목 20종[workout. 인코딩], 수면은 다짐별 수면 구간[기본 21:00~09:00]을 '구간 끝이 속한 논리적 하루'에 통귀속[겹침 분할과 다른 의도된 예외], 하루 단위 전용·마감 시각 미지원, QuestProgress 값 공급 단일 지점 3곳만 분기[수학 무변경 — 기존 49건 그린이 증거], HealthCache[App Group, 400일, ⚠️5.1.3: 값은 CloudKit 저장 금지 — 정의만 동기화]·백필·부트스트랩, 데이터 없는 기기는 모수 제외+'데이터 없음' 표기+판정 보류, 구버전 이중 상태는 행동/꼬리표 우선 정규화) ④**신기능 소개 팝업**(WhatsNewView — 버전당 1회, 신규 설치 시드로 미표시, 온보딩 우선, 全기기 업데이트 안내) ⑤**도움말 전면 자연화+신규 주제 3개**(사용자 예약 작업 — 오타·번역투·UI 명칭 정정, en/ja 전면 재번역·용어 통일[Action/アクション 등]) ⑥HealthKit 엔타이틀먼트(iOS 앱만)+NSHealthShareUsageDescription 3언어+PRIVACY.md 건강 문단. 검증: `-progressSelfTest` **61건**(건강 H 시리즈 12 신규)·20·27·10 ALL PASS(26.5+18.5 심), Debug/Store/워치(26.2·11.5) 빌드, 시각 QA 20여 장(워치 컴플리케이션 실데이터·미니 화면·타일·다짐 행·편집기·팝업·ja 도움말), 실버전 업그레이드 팝업 경로·벌크 1만 건+건강 결합 확인. 스토어 자료: whats-new-1.5·promotional-text-1.5·설명 3언어 갱신, **스크린샷 전면 신규 — 아이폰 10·아이패드 10·워치 1 × ko/en/ja 원본 57장+합성 63장 재생성 완료**(09-records→09-health 교체, Marketing/README 절차·주의 갱신). **CloudKit 프로덕션 스키마 배포 완료(2026-08-22, 사용자 콘솔 실행)** — Quest 신규 필드 3종(healthMetricRaw·sleepWindow*)은 개발 스키마에 없어서(디버그 내보내기 이력 없음이 원인) 시뮬 519869F7에 사용자 iCloud 로그인 후 디버그 빌드를 `-premium YES -cloudSync YES -seedHealthQuest YES`로 실행 → Sandbox 환경 전체 내보내기 성공(98레코드, error nil)으로 필드 생성 → 콘솔 배포(이 시뮬은 iCloud 로그인 유지 — 향후 스키마 작업 재사용 가능). **실측 발견: 1.3의 DiaryPageItem 스타일 필드가 프로덕션 미배포 상태였음**(마지막 배포=1.0 출시 시점 — 1.3 출시[8/6] 이후 도형·텍스트 상자 신규 스타일 필드 동기화가 조용히 실패했을 가능성, 이번 배포로 해소·밀린 내보내기는 자동 재시도. 소킹 중 아이패드↔맥 요소 동기화 교차 확인 권장). **남은 출시 절차(사용자)**: ①TestFlight 실기기 소킹(진행 중 — 실제 건강 데이터·워치 컴플리케이션·기기 간 건강 다짐 정의 동기화 중점) ②ASC 심사 제출(스크린샷 framed 세트 교체·whats-new/프로모션/설명 반영). **1.6 후보 보류 기록: `Docs/plan-1.6.md`**(타임테이블 건강 데이터 표시 — 2026-08-22 사용자 "일단 보류", 구현 금지·재논의 시 착수). 운동 종목 목록(20종)은 §3.4 — 사용자 실사용 종목 빠졌으면 추가 요청. **버전 1.4 (빌드 3) App Store 출시 완료(2026-08-13, 사용자 확인)** — 2026-08-13 심사 제출 후 같은 날 출시 확인. TestFlight 실기기 테스트(문제 없음 확인) 후 제출했음. iOS 18 실기기는 미확보로 미테스트(시뮬 검증 106건 ALL PASS로 갈음 — 해당 사용자 유입 시 피드백 대응 예정). 1.4 내용(사용자 A안 승인, 2026-08-11 세션에서 전부 구현·검증): ①**iOS 18.0 하한 인하**(위 플랫폼 행 — 글라스 버블 머티리얼 폴백·사용자 심볼 안전 렌더 `Image(safeSymbol:)`[iOS 55곳+워치 7곳, 18에 없는 카탈로그 심볼 3개 물음표 폴백]·`-symbolAuditDump`. ⚠️하한 18 Release(-O wholemodule)에서 swift-frontend가 제네릭 중첩 Coordinator의 deinit 최적화 중 크래시하는 컴파일러 버그 실측 — DiaryZoomCoordinator를 비제네릭 톱레벨+AnyView 소거로 우회, 되돌리지 말 것) ②**펜슬 저장 디바운스**(§6.7 — 획마다 전체 직렬화·블랍 재기록하던 버벅임 해소) ③**통계 '오늘 기준' 롤링**(§6.6·§10 ⑤ — 지난 7일/30일) ④**CSV 구간 내보내기**(§6.8, 코어 CSVExport 추출) ⑤**일기 내보내기 후 삭제**(§6.7, 렌더 코어 DiaryExportRenderer 추출) ⑥**데이터 정리·초기화**(§6.8 — 전체/처음~날짜까지+백업 3종[CSV·일기 PDF·정리 리포트 ArchiveReport]+**'일기만' 대상 축**[1.4(3), 2026-08-12 — 기록·구조·양식 보존, 백업은 일기 PDF만, `DataReset.deleteDiaries`·`-dataResetScope diary`·자가 검증 27건으로 확장]) ⑧**라디얼 모드 FAB 가림 수정**(1.4(3) — 목록 마지막 행이 FAB에 가려지던 실측 문제: 각 탭 루트에 하단 safeAreaInset 78pt, ⚠️NavigationStack '안쪽'에 붙일 것[바깥은 TabView가 safe area 재설정해 무시됨, 실측]) ⑦도움말 재구성(1.4(2) 검수 후속, 2026-08-12 — 뭉친 주제 3곳 분리: 기록·통계 그룹에 '지난 7일·30일로 보기'·'데이터 정리·초기화' 독립 주제 신설[CSV 주제는 백업·복원만, 비교 주제는 1.3 원문 복귀], 일기 그룹 '펜슬 노트 페이지'에서 '사진·도형·텍스트 꾸미기' 분리. 이미지는 그룹당 대표 1주제 원칙 유지 — 18장 전수 실사로 참조·언어 매칭 무결 확인)·l10n(전 카탈로그 missing/stale 0)·**워치 실행 햅틱 .click→.success 상향**(1.4(2) — 실사용 '너무 약함' 보고)·MARKETING_VERSION 1.4/**빌드 3**(TestFlight 재업로드용 — 같은 버전은 빌드 번호만 올려 업로드하면 1.4(2)로 기존 테스터에 배포됨)·`Marketing/AppStore/whats-new-1.4.txt`. 검증: 자가 검증 `-progressSelfTest` 49·`-csvImportTest` 20·`-intentSmokeTest` 10·`-dataResetTest` 27 ALL PASS(26 심+18.5 심), Debug/Store/워치 빌드 성공, 18.5·26 시각 QA(버블 폴백·일기 줌·롤링 통계·초기화 화면·리포트 PDF 3페이지 실물). **스토어 제출 시**: 하한 인하는 ASC에서 바꿀 것 없음(바이너리에서 자동 파생), 스크린샷 재촬영 불필요(통계 토글·설정 행 추가는 사소 — 교체 여부는 사용자 판단), 심사 특이사항 없음. 스토어 텍스트는 1.4(3)에서 한국어 문장 흐름 자연화(사용자 피드백 — 설명 ko 자연화+3언어에 1.4 기능 2줄 추가, whats-new 전면 재작성, 프로모션 1.4 전용 `promotional-text-1.4.txt`). **📌 다음 버전(1.5) 확정 계획 — 상세는 `Docs/plan-1.5.md`(2026-08-20 사용자 결정 완료)**: ①워치 행동 컴플리케이션(행동 편집에서 옵트인+기간 선택[하루/이번 주/이번 달/지난 7일/지난 30일], 탭=미니 행동 화면 착지·뒤로가기로 메인 복귀) ②건강 데이터 통합(모음 탭 타일 **무료**+건강 다짐 **프리미엄**, HealthCache 구조[값은 CloudKit 저장 금지 — 5.1.3], 수면은 사용자 지정 수면 구간→깨어난 날 귀속, 모음 탭 3섹션[즐겨찾기·나머지·건강] 순서 설정) ③도움말(HelpView) 전체 문구 한국어 기준 자연화(사용자 지시 2026-08-12 — ~해요체 유지·번역투 제거·en/ja 재검토·l10n 루틴) ④업데이트 신기능 소개 팝업(버전당 1회, 全기기 업데이트 안내 — 계획서 §11). 운동 세부 종목 다짐 포함(§3.4), 스크린샷 전면 신규(아이폰·아이패드 각 10장 — §6), 모든 사전 결정 완료(결정 이력 8건·위험 검토 R1~R16). 하위 OS 인하는 검토 후 기각(근거는 계획서 §4 — 재제안 금지). ⚠️**구현은 사용자의 "진행/시작" 지시가 있을 때만 착수** — 그 전까지 코드 무변경, 추가 요구는 계획서에 계속 반영. **버전 1.3 App Store 출시 2026-08-06** — 3일 실사용+심층 검증(2026-08-05: 자가 검증 3종 ALL PASS·벌크 1만 건·일기 내보내기 일치·위젯 스모크·만료 열람·도움말 — 회귀 0) 통과 후 제출. 심층 검증에서 기록한 사소 잔여 엣지 9건은 후속 후보로만 보류(전부 극단 한정·자가 복구형, 필기 blob 동시 편집 한계는 §6.7). 1.3 내용: 아래 소기능 이력의 일기 개선 묶음(양식 펜 색 고정 + 도형·텍스트 상자 편집 개편 + 검토 후속 수정), `Marketing/AppStore/whats-new-1.3.txt`. 잔여 엣지 9건 상세: 최소 크기 커밋 스냅백·일기 상세 이탈 시 빈 텍스트 상자 잔존·페이지 가장자리 손잡이 클리핑·인라인 편집 중 페이지 스와이프 상태 잔존·회전 상자 커서 어색함 등(1.0부터의 구조 한계인 필기 blob 동시 편집 덮어쓰기·열린 캔버스 원격 미갱신은 §6.7, 같은 페이지 동시 필기 회피 권장). 1.2 내용: 주기 몫 완료 상태 표기(§4.2)+도움말 '진행률 읽는 법' 갱신(상세는 아래 소기능 이력). **버전 1.1 출시 2026-07-30.** 1.1 내용: ①**맥 일기 개방**(§6.7 — 사이드바 노출 + '그리기' 토글로 마우스 필기, 텍스트 우선 도구줄. 아이패드 경로 불변. 맥 실기 확인 완료) ②**CSV 가져오기**(§6.8 — 프리미엄, 추가 전용·미리 보기·멱등. 검증 `-csvImportTest` 20건 ALL PASS) ③**일기 기본 표시 설정**(§6.7 — 캘린더·목표·행동 전 날짜 기본값 + 날짜별 재정의, 스키마 무변경 마커 방식) ④**'짧은 기록 무시' iCloud KVS 동기화**(§6.8 — 전 기기 공통) ⑤스크린샷 전면 개편(아이폰 10·아이패드 10·워치 합성 1 × ko/en/ja — ASC 업로드 완료) ⑥스토어 메타데이터 변경(앱 이름·부제목·키워드·프로모션 — 검색·색인 강화, `Marketing/AppStore/app-name-subtitle.txt`·`keywords.txt`·`promotional-text.txt`. 스토어 표기만 변경, 설치 이름 불변) ⑦MARKETING_VERSION 1.1. **버전 1.0 출시 2026-07-28.** 이후 작업은 사용자 실사용 중 발견한 버그 수정·사용자가 원하는 소기능 추가만 한다(선제적 기능 제안·대규모 리팩터링 금지). 출시 후 소기능 이력: **일기 도형·텍스트 상자 UX 2차 개편**(2026-08-03, 사용자 요청 — §6.7. 종류별 모양 손잡이[사각형 꼭짓점 4개·원 상하좌우 4개·선/화살표 양 끝점]와 정사각형·정원 스냅 피드백, 텍스트 상자 제자리 입력[탭→바로 키보드]·테두리 색 독립, '글 수정'→'스타일' 시트로 재편, borderColorHex 필드 추가. 1.3으로 버전 상향)·**일기 도형·텍스트 상자 편집 강화**(2026-08-03, 사용자 요청 — §6.7. "정해진 모양·크기 그대로만 나온다"는 보고: 크기 손잡이(가로세로 자유·맥 마우스 대응)·회전 제스처·'도형 수정' 시트[색·선 굵기·채우기·모서리 둥글기·회전]·텍스트 시트 확장[글자 크기·굵게·정렬·테두리], DiaryPageItem에 기본값 필드 8개 추가)·**양식 페이지 펜 색 고정**(2026-08-03, 사용자 요청 — §6.7. 다크/라이트 잉크 반전으로 맥·아이패드에서 필기가 안 보이던 실사용 보고 해소, 내보내기 라이트 트레잇 고정 포함)·**주기 몫 완료 상태 표기**(2026-08-01, 사용자 요청 — §4.2. 도움말 '진행률 읽는 법' 문구도 갱신. 같은 날 MARKETING_VERSION 1.2(빌드 1)로 상향 + `Marketing/AppStore/whats-new-1.2.txt` 작성·TestFlight 업로드, **심사 통과 → App Store 출시 완료(2026-08-02, 사용자 확인)**). 업데이트 제출: 버전 올려 빌드 업로드 → 심사 제출(IAP는 첫 심사에서 승인됐으므로 첨부 불필요). 출시 전 이력 — RC는 커밋 `f05e611`(버전 1.0 (1)), 이후 실사용 보고 기반 수정 라운드(전부 커밋·푸시·실기기 확인 완료): l10n 누락 일괄·ja 앱 이름 'Haru Danim' 통일·워치 컴플리케이션 추천 캡 제거(§11)·⑥ 위젯 종료 버튼(§10)·진행률 환산 캡+수행일 집계+수행일 아님 표기(§4.2)·파일명/CSV 헤더 현지화(§7)·워치 컴플리케이션 목표 필터(§6.4·§11, 사용자 승인 소기능)·기록 시각 입력 상시 휠 다이얼+분 절사(§6.5)·일기 달력 기분 사진 백레이어(§6.7)·다짐 없는 목표 '다짐 없음' 표기(§4.3)·워치 측정 중 행 탭 종료(§11)·도움말 예시 스크린샷 18장(§6.8)·설정 하루 시작 시간 접힘 행(§6.5)·**모음 탭 즐겨찾기 모아 보기**(§6.1 — 행동 많아질 때 혼잡도 대응, 사용자 승인 소기능. v1 별도 카드 가로 줄[대안 A~F 중 A]이 "따로 노는 느낌" 실사용 피드백으로 v2 그리드 섹션 통합+나머지 접기로 교체, 같은 날 후속 피드백으로 편집 모드 섹션 인지·섹션 간 드래그 지정/해제·"지정 순서대로 뒤에" 규칙 추가, 2026-07-22)·**en 복수형 정리**(2026-07-22 — 수량 1일 때 "1 times/quests" 류 오류를 카탈로그 plural variation으로 14건 전환[iOS 10·위젯 3·워치 1], 모음 탭 섹션 제목 en "Favorite"→"Favorites"로 워치와 통일. ko/ja 무영향). **App Store 등록 자료는 `Marketing/`에 완비**(설명·키워드·구독 메타·스크린샷 원본/합성판, README에 재생성 절차 — 앱 변경이 자료에 영향 주면 요청 없이 함께 갱신할 것. 2026-07-22 즐겨찾기 섹션(v2) 반영해 아이폰·아이패드 01-main과 help-main ko/en/ja 재촬영·합성판 재생성. **2026-07-29 스크린샷 세트 확장** — 아이폰 8장(+위젯·버블 메뉴·iCloud 동기화 합성·다크 모드)/아이패드 7장(+주간 타임테이블·위젯·동기화 합성) × ko/en/ja, 촬영·합성 절차는 Marketing/README '추가 스크린샷' 절. **1.1 제출 때 ASC 업로드 완료**). **사업자 등록·통신판매업 신고 완료(2026-07-22)** — 서류 절차 끝, ASC 등록·유료 계약 진행 가능(순서는 Marketing/README 체크리스트). 사용자는 계속 실사용 테스트 중이며 버그 보고 시: 최소 변경 + 시뮬레이터 재검증 + l10n 루틴(§2.3) + Debug/Store(워치 파일이면 워치까지) 빌드 + 커밋·푸시 | +| 현재 상태 (2026-08-22) | **버전 1.5 (빌드 10) — 통계·기록 탭 건강 데이터 추가(2026-08-27), TestFlight 업로드 대기. 사용자 예고: 기능 수정은 이것으로 종료(이후는 버그·오류 수정만)**. ✅**동기화 정지 이슈 해결 확인(2026-08-22 밤, 사용자 실기기)**: 일기에 도형·텍스트 상자(DiaryPageItem)가 있으면 그 기기의 CloudKit 동기화 전체가 정지하던 문제(내보내기 대기열 직렬 — 거부된 아이템 뒤로 전부 막힘) — 원인은 **1.3의 DiaryPageItem 스타일 필드 10종이 프로덕션 스키마에 없었던 것**(1.0 출시 때가 마지막 배포였고, 1.3 이후 개발 빌드가 iCloud 내보내기를 한 적이 없어 개발 스키마에도 미생성이었음). 해결 절차 실행: 시뮬 519869F7(사용자 iCloud 로그인·재인증)에서 일기 요소 시드 포함 전체 내보내기로 개발 스키마 필드 생성(success·error nil) → 사용자가 콘솔 재배포 → **도형·텍스트 상자 있는 상태의 동기화 왕복 정상 확인(사용자)**. 교훈: **새 모델 필드는 배포 전 '개발 빌드로 그 타입 레코드를 실제 내보냈는지'까지 확인할 것** — 콘솔 diff에 타입 이름이 떠도 필드가 전부 실렸는지 봐야 한다. 업로드 검증 실측 2건: 빌드 1=ITMS-90683(HealthKit 엔타이틀먼트 보유 시 읽기 전용이어도 `NSHealthUpdateUsageDescription` 필수 — 쓰기 사유 3언어 추가, 쓰기 요청이 없어 표시될 일 없음), 빌드 2=ITMS-90626(**앱 인텐트 메타데이터에 'iPhone' 단어 금지** — 신규 컴플리케이션 인텐트·위젯 설명에서 '하루 다님 앱'으로 교체. 화면 뷰 문구의 iPhone은 스캔 대상 아님[1.4 전례]) → **빌드 3**으로 재업로드. **1.5 추가분(빌드 4, 2026-08-22 — 소킹 중 사용자 요구, 스키마 무변경·전부 로컬 설정/표시 계층)**: ⑦**아이폰 일기 탭 개방**(§6.7 — AppTab.phoneCases 개방[radialOrder 필터 제거·버블 8개 자동 배치 확인], 노트 필기는 맥 '그리기' 토글 문법 재사용[DeviceLayout.isPhone 신설, drawingPolicy anyInput, 가로 스크롤 도구 줄, 배치↔그리기 상호 배타 — 아이패드 경로 무변경], 일기 잠금 설정 전 기기 노출) ⑧**일기 하루 정리 '건강 데이터' 카드**(§6.7 — DiarySection.health[케이스 마지막이라 기존 사용자 목록 끝에 추가], DiaryHealthCard: 지표 선택 시트 공용화[HealthMetricPickerSheet Binding화], 저장은 기기 로컬 diary.healthMetrics[비면 타일 선택 상속], 맥은 목록·화면 모두 숨김, 인쇄 요약에 값 줄 DiaryPrintHealthRow 동승) ⑨**일기 타임테이블 수면·운동 실구간 블록**(§6.7 — **HealthIntervals 신설**[App Group health.intervals.v1, 400일, 실구간을 논리적 하루로 분할 저장·병합, CloudKit 금지 동일], refreshRecent/백필이 유지, ExportSnapshot.injectingHealthBlocks 주입[캘린더 전례·최하층 렌더 healthBlockView 반투명+실선], 지표 고정색 수면 인디고·운동 주황, 필터 시트 '건강 데이터' 토글[RecordFilterSheet showsHealthSection — 전 날짜 공통 diary.timetableHealthHidden, 기본 켬]+**맥 안내 문구**, 일기 필터 버튼 상시 노출로 변경, 내보내기 렌더러 동일 주입) ⑩**수면 표시 구간 설정**(§6.8 — SleepWindowPrefs[LocalPrefs]: 전체 스펙+요일별 7종[켤 때만], 설정 → 건강 데이터 → '수면 표시 구간'[SleepWindowSettingsView — 시작=끝이면 24시간 창], 평문 'sleep' 캐시 키=그날 유효 구간 값으로 변경[타일·일기 카드 공유], 백필 서명에 요일 매핑 포함[배정만 바뀌어도 재백필], **새 수면 다짐 기본 구간을 여기서 시드**) ⑪일기 목표 카드의 건강 다짐 값 단위 실측 수정("8,432회/8,000보"→지표 단위 — DiaryGoalsCard.valueLabel healthTarget 분기) ⑫**소킹 실기기 실측 버그 2건 수정(빌드 5)**: ⓐ더보기 탭 안 일기 달력에서 날짜 탭이 조용히 무시됨 — `AppRouter.morePath`가 `[AppTab]` 고정 타입이라 Date 푸시 불가 → **NavigationPath로 변경**(회귀 감시 인자 `-morePushDiaryToday`, 라디얼·사이드바는 원래 무사) ⓑ수면 블록이 수면 단계(코어·렘) 로우데이터대로 칸칸이 갈라짐 → **10분 이하의 깸은 이어 붙여 한 덩어리로**(DiaryHealthTimetable.sleepDisplayMergeGap — 표시 전용, 값 무영향. 20분쯤 깼다 자면 나뉘는 게 맞다는 사용자 규칙) ⑬**기록 탭 타임테이블에도 수면·운동 블록**(하루·주간 — DiaryHealthTimetable.blocks 단일 공급 지점·숨김 토글 공유[기록 필터 시트에도 건강 섹션], 내보내기는 `injectingHealthBlocks(forDayKeys:math:)`로 하루·주간 주입 + trim 표시 범위를 블록만큼 확장) ⑭**타임테이블 표시 색 설정**(설정 → 건강 데이터 → '타임테이블 표시 색' — 수면·운동 ColorPicker, 기본 인디고·주황[빈 값], 전 타임테이블·내보내기 공통 적용, 인자 `-healthColorPreview`) ⑮**버블 찌그러짐 실측 수정(빌드 6, 2026-08-23)**: 일기 버블 추가로 8개가 되자 이웃 간격(~9.6pt)이 유리 합체 판정 거리(18pt) 안으로 들어와 정지 상태에서도 원들이 서로를 향해 늘어남 — `glassBlendSpacing` 18→6(펼침 상태 정원 분리, FAB 모핑은 겹침 구간이라 유지. 마케팅 06-bubble 3언어 재촬영) ⑯**건강 다짐 표시 색(빌드 7, 2026-08-23 — 사용자 요구)**: 건강 다짐이 고정 분홍이던 것을 편집기 건강 섹션에서 색 선택(프리셋 12+피커, 기본 분홍=빈 값[시스템 분홍의 다크 적응 유지]) — **Quest 신규 필드 `healthColorHex`**(기본 "", targetColor 단일 지점 분기라 목표 탭·모음 카드·일기·위젯·워치 자동 반영, 행동·꼬리표 다짐은 저장 시 "" 정규화, 타임테이블 블록 색과 별개). ⚠️**CloudKit 스키마 추가 — 배포 절차(개발 내보내기→콘솔 Deploy)가 빌드 7 설치보다 선행 필수**(DiaryPageItem 전례 — 미배포 상태로 다짐 수정 시 그 기기 동기화 정지). → 배포·설치 완료(사용자). ⑰**위젯·워치 색 미반영 실측 수정(빌드 8)**: 위젯 QuestCellSnapshot(WidgetSupport)과 워치 스냅숏(WatchSyncManager)이 targetColor를 안 타고 자체 폴백을 쓰고 있었음 — 위젯은 분홍 #E0558C 하드코딩, **워치는 아예 초록 폴백(1.5 출시분부터 건강 다짐이 일반색으로 보이던 잠복 표시 버그)** → 두 곳 다 healthColorHex 우선·기본 #E0558C로 통일(④ 그리드에서 걸음=파랑·나머지=분홍 실측). 값 라벨은 두 표면 모두 원래 건강 단위 정상. ⑱**성능 최적화(빌드 9, 2026-08-26 — 소킹 중 '1.4 대비 버벅임 증가' 보고, 동작·수치 완전 불변 원칙)**: 원인 진단 결과 1.5 건강 캐시가 값 1개 읽을 때마다 전체 딕셔너리(지표×400일)를 브리징 — 건강 다짐 행 하나 렌더에 ~38회+, 워치 스냅숏에 다짐당 ~76회가 메인 스레드에서 반복. 적용 7건(전부 등가 최적화): ⓐHealthCache·HealthIntervals **인메모리 캐시**(세대 카운터 `health.cache.gen`/`health.intervals.gen`으로 프로세스 간 무효화 — 쓰기는 메인 앱 단일 작성자, workoutKeys의 load() 이중 호출도 제거) ⓑ**DayMath.calendar 저장 프로퍼티화**(접근마다 Calendar 사본 생성하던 전역 곱셈 비용 제거) ⓒ**수면 백필 포인터 스위프**(400일×전체 수면 샘플 O(n×m) 전수 클리핑 → 정렬+lo/hi 스위프, 무작위 퍼즈 3,000회 등가 증명 — 하루 첫 실행 정지 해소) ⓓrefreshRecent 캐시 기록 배칭(일수×2회→총 2회) ⓔ**QuestProgress.spanValue/spanRawValue 버킷화**(하루 단위 다짐의 주/월 게이지가 날마다 전체 기록 재순회하던 O(일수×기록수) → perDayGaugeValues 1회 순회. ⚠️연속 계산 perDayValues와 달리 **미래 기록을 now로 자르지 않는 게이지 스펙 보존** — spanRawValue는 예전 Aggregator-only 동작 보존 위해 perDayRecordValues 직결) ⓕ위젯 라이브 타임라인 makeEntry에 refresh 파라미터(미래 엔트리 6개가 각각 새 ModelContainer를 열던 것 → 타임라인당 1회, ②③④·잠금 4곳) ⓖ일기 건강 카드 칩 value() 이중 호출 제거. **의도적으로 제외**: 재조회 스로틀(값 신선도가 변해 동작 변화), 워치 스냅숏 백그라운드화(SwiftData 메인 컨텍스트 위험), 모음 탭 셀 오늘 값 버킷화(구조 변경 — 체감 보고 재논의). 검증: A/B 등가(옛/새 빌드를 각각 새 컨테이너+동일 시드로 캡처 — streak·건강·인텐트 덤프 3종 값 동일[정렬 후 diff 0], 목표·모음·위젯④·일기 4화면 **픽셀 동일**[상태바 시계·시뮬 iCloud 배너 제외]), 자가 검증 62/20/27/10 ALL PASS(26.5+**18.5 심 62건도 ALL PASS**), 벌크 1만 건 3탭 렌더 정상, 퍼즈 3,000회, 3종 빌드(Store·워치 포함) 그린, 신규 현지화 문자열 0(카탈로그 무변경). whats-new-1.5 3언어에 성능 개선 줄 추가. ⑲**워치 미니 행동 화면 재구성(빌드 9 동일, 2026-08-26 — 사용자 울트라 실측: 하단 실행 버튼이 살짝 잘림)**: 하단 버튼 줄 제거, **아이콘=실행 버튼**(우하단 +/▶/■ 배지, 측정 중=노란 테두리+노란 ■ 배지+노란 타이머, 별도 '측정 중' 줄은 중복이라 제거) + 상태별 한 줄 힌트('아이콘을 누르면 시작/종료/1회 추가' — lineLimit 1+축소 0.75로 ja 40mm 한 줄 유지). 작은 워치(높이<220pt)는 컴팩트 메트릭(아이콘 54·간격 6)으로 **40mm에서도 힌트까지 스크롤 없이 한 화면**. a11y 라벨은 기존 버튼 문구 유지, 실행 경로는 동일한 store.run. 도움말 '행동 기록 컴플리케이션' 문구 갱신(3언어), 검증: 49mm(울트라)·46mm·40mm(11.5) × 3상태 + ja 렌더 스크린샷 전부 잘림 0, 워치·iOS·Store 빌드 그린, 전 카탈로그 0/0. 신규 검증 인자 `-watchSeedSnapshot`(페어링 없는 합성 스냅숏)·`-watchOpenActionIndex N`(§14). ㉑**통계·기록 탭 건강 데이터(빌드 10, 2026-08-27 — 사용자 요구 '이거까지 하고 배포', 표시 전용·수치 규칙 상속)**: 통계 탭 맨 아래에 건강 카드 — 하루=지표 값 칩(일기 카드와 동일 문법·HealthValueChipGrid 공용), 주간/월간(롤링 포함)=**'건강 지표 (일별)' 추이 카드**(우상단 분홍 캡슐 메뉴로 지표 선택[`stats.healthMetric` 기기 로컬]·주간=요일 축·Y축 단위[시간(h)/보/kcal/km/L], 분홍 단일 선)+**'건강 지표 합계·평균' 표**(합계·하루 평균[+달력 월간만 주 평균], 분모=기존 '이미 시작된 날' 규칙 상속, 값 0 지표 행 생략, 전부 없으면 '이 기간의 건강 데이터가 없어요'). 기록 탭 목록 보기 맨 아래 '건강 데이터' 섹션(그날 칩 — 과거 날짜도 캐시로). **표시 규칙**: 지표=타일 선택 상속, 마스터 스위치=설정 '건강 데이터 표시'(healthTilesEnabled — 문구를 '모음 탭 타일과 통계·기록 탭의 건강 카드'로 확장), 연결 전·맥은 조용히 숨김, **내보내기 미포함**(비교 카드 전례). 값 공급은 HealthDisplayValues(신설 단일 지점 — 수면=유효 표시 구간 키, DiaryHealthCard도 경유) → 인메모리 HealthCache라 ⑱ 최적화 유지. 도움말·팝업·설정 푸터·whats-new 3언어 갱신(새 키 5·수정 3, 전 카탈로그 0/0). 검증: 자가 검증 62/20/27/10 ALL PASS(수치 불변 확인), 시각 QA(하루 칩·주간 요일축+합계 53,966보=시드 합산 일치·월간 롤링 30일 분모·기록 목록 과거 날짜 7,589보=시드×0.9·ja·**18.5에서 경과일 분모 4일 실측 일치**), Debug·Store·18.5 빌드 그린. 종목별 운동 통계는 캐시 확장 필요라 1.6 후보로 보류. ⑳**출시 전 최종 점검 완료(2026-08-26)**: 1.4 기준 커밋(b62808f) 대비 카탈로그 추가 키 **241건 전수 번역 검수** — 발견·수정 3건(워치·워치위젯 ja 용어 위반 '行動'→'アクション' 2곳, 신기능 팝업 en 'tap it to act'→'run the action' 자연화), whats-new·프로모션·설명 3언어와 신기능 팝업 6항목·도움말(기기 분기 포함) 모두 1.5 기능 인벤토리 대비 누락 0, 스크린샷 세트 원본 57+합성 63장 완비·촬영 후 변경과 어긋남 없음(버블 재촬영·기본 분홍·워치=페이스 합성), **keywords.txt 1.5 보강**(ko '건강,수면,걸음'·ja 'ヘルスケア,睡眠' 추가[기존 무삭제, 88/95자] — en은 97/100자라 보류, ASC에 새로 붙여넣기 필요), InfoPlist 건강 문구 ko/en/ja 3언어 확인. 검증: 61/20/27/10 ALL PASS(26.5), 3종 빌드 그린, 전 카탈로그 missing/stale 0, 시각 QA(아이폰 요약·건강 카드·타임테이블 블록·노트 도구 줄·버블 8개·수면 구간 화면·아이패드·맥 분기·18.5). 스토어 자료: whats-new-1.5에 일기 확장 절 추가·설명 3언어 일기 항목 갱신. 신규 인자 `-sleepWindowPreview`(§14), `-seedHealthCache`가 실구간(수면 자정 분할+달리기)도 시드. 계획·결정 이력은 `Docs/plan-1.5.md`(전 섹션 ✅구현 완료). 1.5 내용: ①**워치 '행동 기록' 컴플리케이션**(§11 — 행동 편집 옵트인+표시 기간 5종[하루/이번 주/이번 달/지난 7일/지난 30일, 롤링은 rollingRange 재사용], 측정 중 노란 타이머, 탭=워치 앱 미니 행동 화면 push[뒤로가기=메인], recommendations 상한 없음) ②**모음 탭 건강 데이터 타일**(무료, §6.1 — 걸음·운동·수면 등 8지표 선택·순서, 접기, 연결 CTA[명시적 탭 권한], 안내 시트, 즐겨찾기·나머지·건강 3섹션 순서 설정[§6.8], 맥은 섹션 숨김) ③**건강 다짐**(프리미엄, §4.2·§6.4 — 8지표+운동 종목 20종[workout. 인코딩], 수면은 다짐별 수면 구간[기본 21:00~09:00]을 '구간 끝이 속한 논리적 하루'에 통귀속[겹침 분할과 다른 의도된 예외], 하루 단위 전용·마감 시각 미지원, QuestProgress 값 공급 단일 지점 3곳만 분기[수학 무변경 — 기존 49건 그린이 증거], HealthCache[App Group, 400일, ⚠️5.1.3: 값은 CloudKit 저장 금지 — 정의만 동기화]·백필·부트스트랩, 데이터 없는 기기는 모수 제외+'데이터 없음' 표기+판정 보류, 구버전 이중 상태는 행동/꼬리표 우선 정규화) ④**신기능 소개 팝업**(WhatsNewView — 버전당 1회, 신규 설치 시드로 미표시, 온보딩 우선, 全기기 업데이트 안내) ⑤**도움말 전면 자연화+신규 주제 3개**(사용자 예약 작업 — 오타·번역투·UI 명칭 정정, en/ja 전면 재번역·용어 통일[Action/アクション 등]) ⑥HealthKit 엔타이틀먼트(iOS 앱만)+NSHealthShareUsageDescription 3언어+PRIVACY.md 건강 문단. 검증: `-progressSelfTest` **61건**(건강 H 시리즈 12 신규)·20·27·10 ALL PASS(26.5+18.5 심), Debug/Store/워치(26.2·11.5) 빌드, 시각 QA 20여 장(워치 컴플리케이션 실데이터·미니 화면·타일·다짐 행·편집기·팝업·ja 도움말), 실버전 업그레이드 팝업 경로·벌크 1만 건+건강 결합 확인. 스토어 자료: whats-new-1.5·promotional-text-1.5·설명 3언어 갱신, **스크린샷 전면 신규 — 아이폰 10·아이패드 10·워치 1 × ko/en/ja 원본 57장+합성 63장 재생성 완료**(09-records→09-health 교체, Marketing/README 절차·주의 갱신). **CloudKit 프로덕션 스키마 배포 완료(2026-08-22, 사용자 콘솔 실행)** — Quest 신규 필드 3종(healthMetricRaw·sleepWindow*)은 개발 스키마에 없어서(디버그 내보내기 이력 없음이 원인) 시뮬 519869F7에 사용자 iCloud 로그인 후 디버그 빌드를 `-premium YES -cloudSync YES -seedHealthQuest YES`로 실행 → Sandbox 환경 전체 내보내기 성공(98레코드, error nil)으로 필드 생성 → 콘솔 배포(이 시뮬은 iCloud 로그인 유지 — 향후 스키마 작업 재사용 가능). **실측 발견: 1.3의 DiaryPageItem 스타일 필드가 프로덕션 미배포 상태였음**(마지막 배포=1.0 출시 시점 — 1.3 출시[8/6] 이후 도형·텍스트 상자 신규 스타일 필드 동기화가 조용히 실패했을 가능성, 이번 배포로 해소·밀린 내보내기는 자동 재시도. 소킹 중 아이패드↔맥 요소 동기화 교차 확인 권장). **남은 출시 절차(사용자)**: ①TestFlight 실기기 소킹(진행 중 — 실제 건강 데이터·워치 컴플리케이션·기기 간 건강 다짐 정의 동기화 중점) ②ASC 심사 제출(스크린샷 framed 세트 교체·whats-new/프로모션/설명 반영). **1.6 후보 보류 기록: `Docs/plan-1.6.md`**(타임테이블 건강 데이터 표시 — 2026-08-22 사용자 "일단 보류", 구현 금지·재논의 시 착수). 운동 종목 목록(20종)은 §3.4 — 사용자 실사용 종목 빠졌으면 추가 요청. **버전 1.4 (빌드 3) App Store 출시 완료(2026-08-13, 사용자 확인)** — 2026-08-13 심사 제출 후 같은 날 출시 확인. TestFlight 실기기 테스트(문제 없음 확인) 후 제출했음. iOS 18 실기기는 미확보로 미테스트(시뮬 검증 106건 ALL PASS로 갈음 — 해당 사용자 유입 시 피드백 대응 예정). 1.4 내용(사용자 A안 승인, 2026-08-11 세션에서 전부 구현·검증): ①**iOS 18.0 하한 인하**(위 플랫폼 행 — 글라스 버블 머티리얼 폴백·사용자 심볼 안전 렌더 `Image(safeSymbol:)`[iOS 55곳+워치 7곳, 18에 없는 카탈로그 심볼 3개 물음표 폴백]·`-symbolAuditDump`. ⚠️하한 18 Release(-O wholemodule)에서 swift-frontend가 제네릭 중첩 Coordinator의 deinit 최적화 중 크래시하는 컴파일러 버그 실측 — DiaryZoomCoordinator를 비제네릭 톱레벨+AnyView 소거로 우회, 되돌리지 말 것) ②**펜슬 저장 디바운스**(§6.7 — 획마다 전체 직렬화·블랍 재기록하던 버벅임 해소) ③**통계 '오늘 기준' 롤링**(§6.6·§10 ⑤ — 지난 7일/30일) ④**CSV 구간 내보내기**(§6.8, 코어 CSVExport 추출) ⑤**일기 내보내기 후 삭제**(§6.7, 렌더 코어 DiaryExportRenderer 추출) ⑥**데이터 정리·초기화**(§6.8 — 전체/처음~날짜까지+백업 3종[CSV·일기 PDF·정리 리포트 ArchiveReport]+**'일기만' 대상 축**[1.4(3), 2026-08-12 — 기록·구조·양식 보존, 백업은 일기 PDF만, `DataReset.deleteDiaries`·`-dataResetScope diary`·자가 검증 27건으로 확장]) ⑧**라디얼 모드 FAB 가림 수정**(1.4(3) — 목록 마지막 행이 FAB에 가려지던 실측 문제: 각 탭 루트에 하단 safeAreaInset 78pt, ⚠️NavigationStack '안쪽'에 붙일 것[바깥은 TabView가 safe area 재설정해 무시됨, 실측]) ⑦도움말 재구성(1.4(2) 검수 후속, 2026-08-12 — 뭉친 주제 3곳 분리: 기록·통계 그룹에 '지난 7일·30일로 보기'·'데이터 정리·초기화' 독립 주제 신설[CSV 주제는 백업·복원만, 비교 주제는 1.3 원문 복귀], 일기 그룹 '펜슬 노트 페이지'에서 '사진·도형·텍스트 꾸미기' 분리. 이미지는 그룹당 대표 1주제 원칙 유지 — 18장 전수 실사로 참조·언어 매칭 무결 확인)·l10n(전 카탈로그 missing/stale 0)·**워치 실행 햅틱 .click→.success 상향**(1.4(2) — 실사용 '너무 약함' 보고)·MARKETING_VERSION 1.4/**빌드 3**(TestFlight 재업로드용 — 같은 버전은 빌드 번호만 올려 업로드하면 1.4(2)로 기존 테스터에 배포됨)·`Marketing/AppStore/whats-new-1.4.txt`. 검증: 자가 검증 `-progressSelfTest` 49·`-csvImportTest` 20·`-intentSmokeTest` 10·`-dataResetTest` 27 ALL PASS(26 심+18.5 심), Debug/Store/워치 빌드 성공, 18.5·26 시각 QA(버블 폴백·일기 줌·롤링 통계·초기화 화면·리포트 PDF 3페이지 실물). **스토어 제출 시**: 하한 인하는 ASC에서 바꿀 것 없음(바이너리에서 자동 파생), 스크린샷 재촬영 불필요(통계 토글·설정 행 추가는 사소 — 교체 여부는 사용자 판단), 심사 특이사항 없음. 스토어 텍스트는 1.4(3)에서 한국어 문장 흐름 자연화(사용자 피드백 — 설명 ko 자연화+3언어에 1.4 기능 2줄 추가, whats-new 전면 재작성, 프로모션 1.4 전용 `promotional-text-1.4.txt`). **📌 다음 버전(1.5) 확정 계획 — 상세는 `Docs/plan-1.5.md`(2026-08-20 사용자 결정 완료)**: ①워치 행동 컴플리케이션(행동 편집에서 옵트인+기간 선택[하루/이번 주/이번 달/지난 7일/지난 30일], 탭=미니 행동 화면 착지·뒤로가기로 메인 복귀) ②건강 데이터 통합(모음 탭 타일 **무료**+건강 다짐 **프리미엄**, HealthCache 구조[값은 CloudKit 저장 금지 — 5.1.3], 수면은 사용자 지정 수면 구간→깨어난 날 귀속, 모음 탭 3섹션[즐겨찾기·나머지·건강] 순서 설정) ③도움말(HelpView) 전체 문구 한국어 기준 자연화(사용자 지시 2026-08-12 — ~해요체 유지·번역투 제거·en/ja 재검토·l10n 루틴) ④업데이트 신기능 소개 팝업(버전당 1회, 全기기 업데이트 안내 — 계획서 §11). 운동 세부 종목 다짐 포함(§3.4), 스크린샷 전면 신규(아이폰·아이패드 각 10장 — §6), 모든 사전 결정 완료(결정 이력 8건·위험 검토 R1~R16). 하위 OS 인하는 검토 후 기각(근거는 계획서 §4 — 재제안 금지). ⚠️**구현은 사용자의 "진행/시작" 지시가 있을 때만 착수** — 그 전까지 코드 무변경, 추가 요구는 계획서에 계속 반영. **버전 1.3 App Store 출시 2026-08-06** — 3일 실사용+심층 검증(2026-08-05: 자가 검증 3종 ALL PASS·벌크 1만 건·일기 내보내기 일치·위젯 스모크·만료 열람·도움말 — 회귀 0) 통과 후 제출. 심층 검증에서 기록한 사소 잔여 엣지 9건은 후속 후보로만 보류(전부 극단 한정·자가 복구형, 필기 blob 동시 편집 한계는 §6.7). 1.3 내용: 아래 소기능 이력의 일기 개선 묶음(양식 펜 색 고정 + 도형·텍스트 상자 편집 개편 + 검토 후속 수정), `Marketing/AppStore/whats-new-1.3.txt`. 잔여 엣지 9건 상세: 최소 크기 커밋 스냅백·일기 상세 이탈 시 빈 텍스트 상자 잔존·페이지 가장자리 손잡이 클리핑·인라인 편집 중 페이지 스와이프 상태 잔존·회전 상자 커서 어색함 등(1.0부터의 구조 한계인 필기 blob 동시 편집 덮어쓰기·열린 캔버스 원격 미갱신은 §6.7, 같은 페이지 동시 필기 회피 권장). 1.2 내용: 주기 몫 완료 상태 표기(§4.2)+도움말 '진행률 읽는 법' 갱신(상세는 아래 소기능 이력). **버전 1.1 출시 2026-07-30.** 1.1 내용: ①**맥 일기 개방**(§6.7 — 사이드바 노출 + '그리기' 토글로 마우스 필기, 텍스트 우선 도구줄. 아이패드 경로 불변. 맥 실기 확인 완료) ②**CSV 가져오기**(§6.8 — 프리미엄, 추가 전용·미리 보기·멱등. 검증 `-csvImportTest` 20건 ALL PASS) ③**일기 기본 표시 설정**(§6.7 — 캘린더·목표·행동 전 날짜 기본값 + 날짜별 재정의, 스키마 무변경 마커 방식) ④**'짧은 기록 무시' iCloud KVS 동기화**(§6.8 — 전 기기 공통) ⑤스크린샷 전면 개편(아이폰 10·아이패드 10·워치 합성 1 × ko/en/ja — ASC 업로드 완료) ⑥스토어 메타데이터 변경(앱 이름·부제목·키워드·프로모션 — 검색·색인 강화, `Marketing/AppStore/app-name-subtitle.txt`·`keywords.txt`·`promotional-text.txt`. 스토어 표기만 변경, 설치 이름 불변) ⑦MARKETING_VERSION 1.1. **버전 1.0 출시 2026-07-28.** 이후 작업은 사용자 실사용 중 발견한 버그 수정·사용자가 원하는 소기능 추가만 한다(선제적 기능 제안·대규모 리팩터링 금지). 출시 후 소기능 이력: **일기 도형·텍스트 상자 UX 2차 개편**(2026-08-03, 사용자 요청 — §6.7. 종류별 모양 손잡이[사각형 꼭짓점 4개·원 상하좌우 4개·선/화살표 양 끝점]와 정사각형·정원 스냅 피드백, 텍스트 상자 제자리 입력[탭→바로 키보드]·테두리 색 독립, '글 수정'→'스타일' 시트로 재편, borderColorHex 필드 추가. 1.3으로 버전 상향)·**일기 도형·텍스트 상자 편집 강화**(2026-08-03, 사용자 요청 — §6.7. "정해진 모양·크기 그대로만 나온다"는 보고: 크기 손잡이(가로세로 자유·맥 마우스 대응)·회전 제스처·'도형 수정' 시트[색·선 굵기·채우기·모서리 둥글기·회전]·텍스트 시트 확장[글자 크기·굵게·정렬·테두리], DiaryPageItem에 기본값 필드 8개 추가)·**양식 페이지 펜 색 고정**(2026-08-03, 사용자 요청 — §6.7. 다크/라이트 잉크 반전으로 맥·아이패드에서 필기가 안 보이던 실사용 보고 해소, 내보내기 라이트 트레잇 고정 포함)·**주기 몫 완료 상태 표기**(2026-08-01, 사용자 요청 — §4.2. 도움말 '진행률 읽는 법' 문구도 갱신. 같은 날 MARKETING_VERSION 1.2(빌드 1)로 상향 + `Marketing/AppStore/whats-new-1.2.txt` 작성·TestFlight 업로드, **심사 통과 → App Store 출시 완료(2026-08-02, 사용자 확인)**). 업데이트 제출: 버전 올려 빌드 업로드 → 심사 제출(IAP는 첫 심사에서 승인됐으므로 첨부 불필요). 출시 전 이력 — RC는 커밋 `f05e611`(버전 1.0 (1)), 이후 실사용 보고 기반 수정 라운드(전부 커밋·푸시·실기기 확인 완료): l10n 누락 일괄·ja 앱 이름 'Haru Danim' 통일·워치 컴플리케이션 추천 캡 제거(§11)·⑥ 위젯 종료 버튼(§10)·진행률 환산 캡+수행일 집계+수행일 아님 표기(§4.2)·파일명/CSV 헤더 현지화(§7)·워치 컴플리케이션 목표 필터(§6.4·§11, 사용자 승인 소기능)·기록 시각 입력 상시 휠 다이얼+분 절사(§6.5)·일기 달력 기분 사진 백레이어(§6.7)·다짐 없는 목표 '다짐 없음' 표기(§4.3)·워치 측정 중 행 탭 종료(§11)·도움말 예시 스크린샷 18장(§6.8)·설정 하루 시작 시간 접힘 행(§6.5)·**모음 탭 즐겨찾기 모아 보기**(§6.1 — 행동 많아질 때 혼잡도 대응, 사용자 승인 소기능. v1 별도 카드 가로 줄[대안 A~F 중 A]이 "따로 노는 느낌" 실사용 피드백으로 v2 그리드 섹션 통합+나머지 접기로 교체, 같은 날 후속 피드백으로 편집 모드 섹션 인지·섹션 간 드래그 지정/해제·"지정 순서대로 뒤에" 규칙 추가, 2026-07-22)·**en 복수형 정리**(2026-07-22 — 수량 1일 때 "1 times/quests" 류 오류를 카탈로그 plural variation으로 14건 전환[iOS 10·위젯 3·워치 1], 모음 탭 섹션 제목 en "Favorite"→"Favorites"로 워치와 통일. ko/ja 무영향). **App Store 등록 자료는 `Marketing/`에 완비**(설명·키워드·구독 메타·스크린샷 원본/합성판, README에 재생성 절차 — 앱 변경이 자료에 영향 주면 요청 없이 함께 갱신할 것. 2026-07-22 즐겨찾기 섹션(v2) 반영해 아이폰·아이패드 01-main과 help-main ko/en/ja 재촬영·합성판 재생성. **2026-07-29 스크린샷 세트 확장** — 아이폰 8장(+위젯·버블 메뉴·iCloud 동기화 합성·다크 모드)/아이패드 7장(+주간 타임테이블·위젯·동기화 합성) × ko/en/ja, 촬영·합성 절차는 Marketing/README '추가 스크린샷' 절. **1.1 제출 때 ASC 업로드 완료**). **사업자 등록·통신판매업 신고 완료(2026-07-22)** — 서류 절차 끝, ASC 등록·유료 계약 진행 가능(순서는 Marketing/README 체크리스트). 사용자는 계속 실사용 테스트 중이며 버그 보고 시: 최소 변경 + 시뮬레이터 재검증 + l10n 루틴(§2.3) + Debug/Store(워치 파일이면 워치까지) 빌드 + 커밋·푸시 | | 출시·운영 메모 (2026-07-28) | **ASC 등록·심사·출시 전부 완료** — 제출 1건에 앱 1.0+구독 그룹+상품 3종 동반(신형 ASC는 각 항목의 "심사에 추가"로 하나의 제출 초안에 담는 방식), '앱 개인정보 보호'="데이터 수집 안 함", 유료 앱 계약·대한민국 세금 양식 활성, 소규모 사업자 프로그램 가입. 개발자 본인 프리미엄은 **평생 이용권 특가 코드(무료 Offer Code) 교환**으로 스토어 버전에 적용됨(IAP용 프로모션 코드는 2026-03 폐지 — 이후 무료 지급은 특가 코드만 가능, 생성 후 교환 가능까지 최대 1시간). 상시 참고: `ITSAppUsesNonExemptEncryption=NO`, PrivacyInfo.xcprivacy 4개 번들(수집·트래킹 없음), 개인정보처리방침=`github.com/notalentprogrammer/HaruDanim` PRIVACY.md·이용약관=Apple 표준 EULA(결제 화면·앱 설명 모두 링크). **iCloud 프로덕션 동기화 확인 완료(2026-07-28)** — 아이폰·아이패드·맥 3기기 반나절 실사용에서 기존 데이터 포함 정상 왕복(개발→프로덕션 CloudKit 환경 전환에 따른 재업로드 우려 해소. 향후 동기화 문제 보고 시 이 전환 이력 참고). **정산 계좌 변경 완료** — 1.0 출시 시점에 이미 처리(2026-08-06 사용자 확인). 남은 운영 예정 작업 없음. 절차 상세 기록은 Marketing/README.md 체크리스트 | --- diff --git a/myApp/HaruDanim/Docs/plan-1.5.md b/myApp/HaruDanim/Docs/plan-1.5.md index 094bdab..4cfcf87 100644 --- a/myApp/HaruDanim/Docs/plan-1.5.md +++ b/myApp/HaruDanim/Docs/plan-1.5.md @@ -206,3 +206,15 @@ - **새 구성**: [뒤로가기(시스템)] / **아이콘 타일=실행 버튼**(64pt, 우하단 배지 +(횟수)·▶(대기)·■(측정 중), 측정 중=노란 테두리+노란 배지 — 앱 전체 '측정 중=노랑' 문법) / 이름 / 기간 라벨+큰 값(측정 중 노란 타이머) / **상태별 한 줄 힌트**("아이콘을 누르면 시작/종료/1회 추가"). 별도 '측정 중' 줄은 사중 중복이라 제거. 실행 경로는 기존과 동일한 store.run(햅틱 포함) — 트리거 뷰만 바뀜, a11y 라벨은 기존 버튼 문구 재사용. - **작은 화면 대응**: 화면 높이<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 업로드 전). + +### §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. diff --git a/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj b/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj index c52cfc1..cfbe81b 100644 --- a/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj +++ b/myApp/HaruDanim/Haru_Danim.xcodeproj/project.pbxproj @@ -601,7 +601,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5GNS4Z2SYT; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -643,7 +643,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5GNS4Z2SYT; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -686,7 +686,7 @@ CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5GNS4Z2SYT; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -722,7 +722,7 @@ CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5GNS4Z2SYT; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -758,7 +758,7 @@ CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5GNS4Z2SYT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist; @@ -794,7 +794,7 @@ CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5GNS4Z2SYT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist; @@ -827,7 +827,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5GNS4Z2SYT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; @@ -853,7 +853,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5GNS4Z2SYT; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Widgets/Info.plist; diff --git a/myApp/HaruDanim/IOS/Localizable.xcstrings b/myApp/HaruDanim/IOS/Localizable.xcstrings index ead2fa1..895c614 100644 --- a/myApp/HaruDanim/IOS/Localizable.xcstrings +++ b/myApp/HaruDanim/IOS/Localizable.xcstrings @@ -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": { "en": { @@ -2706,18 +2738,18 @@ } } }, - "걸음·운동·수면 같은 애플 건강 앱의 오늘 데이터를 모음 탭 타일로 볼 수 있어요. 값은 기기 밖으로 나가지 않아요.": { + "걸음·운동·수면 같은 애플 건강 앱의 오늘 데이터를 모음 탭 타일로 볼 수 있어요. 통계·기록 탭에서도 함께 보여요. 값은 기기 밖으로 나가지 않아요.": { "localizations": { "en": { "stringUnit": { "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": { "stringUnit": { "state": "translated", - "value": "歩数・運動・睡眠など、Appleヘルスケアの今日のデータをホームタブのタイルで見られます。値が端末の外に出ることはありません。" + "value": "歩数・運動・睡眠など、Appleヘルスケアの今日のデータをホームタブのタイルで見られます。統計・記録タブにも一緒に表示されます。値が端末の外に出ることはありません。" } } } @@ -6604,18 +6636,18 @@ } } }, - "모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요.": { + "모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요. 골라 둔 지표는 통계 탭 아래의 건강 카드(하루 값·지표별 추이·합계와 하루 평균)와 기록 탭 목록의 그날 건강 카드에도 그대로 쓰여요 — '건강 데이터 표시'를 끄면 이 카드들도 함께 사라져요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요.": { "localizations": { "en": { "stringUnit": { "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": { "stringUnit": { "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": { "en": { @@ -11300,6 +11348,22 @@ } } }, + "이 기간의 건강 데이터가 없어요": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No health data for this period" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "この期間のヘルスデータがありません" + } + } + } + }, "이 기록에 대한 메모 (선택)": { "localizations": { "en": { @@ -14522,6 +14586,22 @@ } } }, + "추이를 볼 지표 선택": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a metric to chart" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "推移を見る指標を選ぶ" + } + } + } + }, "추적 방식": { "localizations": { "en": { @@ -14858,18 +14938,18 @@ } } }, - "켜면 애플 건강 앱의 오늘 데이터(걸음·운동·수면 등)를 모음 탭에서 타일로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.": { + "켜면 애플 건강 앱의 데이터(걸음·운동·수면 등)를 모음 탭 타일과 통계·기록 탭의 건강 카드로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.": { "localizations": { "en": { "stringUnit": { "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": { "stringUnit": { "state": "translated", - "value": "オンにすると、AppleヘルスケアAppの今日のデータ(歩数・運動・睡眠など)をホームタブのタイルで見られます。セクションの表示順でお気に入り・その他のアクション・ヘルスデータの配置を決められます。睡眠の表示時間帯は、どの時間帯の眠りを「その日の睡眠」と見なすかを決めます — 昼夜が入れ替わる日があっても合わせられます。" + "value": "オンにすると、AppleヘルスケアAppのデータ(歩数・運動・睡眠など)をホームタブのタイルと、統計・記録タブのヘルスカードで見られます。セクションの表示順でお気に入り・その他のアクション・ヘルスデータの配置を決められます。睡眠の表示時間帯は、どの時間帯の眠りを「その日の睡眠」と見なすかを決めます — 昼夜が入れ替わる日があっても合わせられます。" } } } diff --git a/myApp/HaruDanim/IOS/Views/HealthSection.swift b/myApp/HaruDanim/IOS/Views/HealthSection.swift index 5d01928..9a7dc60 100644 --- a/myApp/HaruDanim/IOS/Views/HealthSection.swift +++ b/myApp/HaruDanim/IOS/Views/HealthSection.swift @@ -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 소비) /// 일기 하루 정리·내보내기의 타임테이블에 얹는 수면·운동 블록의 단일 공급 지점. @@ -447,10 +511,8 @@ struct DiaryHealthCard: View { } private func value(_ metric: HealthMetric) -> Double? { - let key = metric == .sleep - ? "sleep@\(SleepWindowPrefs.spec(forDayKey: dayKey))" - : metric.rawValue - return HealthCache.value(key, dayKey: dayKey) + // 통계·기록 카드와 같은 단일 지점(1.5(10)) — 수면=유효 표시 구간 키 규칙 공유 + HealthDisplayValues.value(metric, dayKey: dayKey) } var body: some View { diff --git a/myApp/HaruDanim/IOS/Views/HelpView.swift b/myApp/HaruDanim/IOS/Views/HelpView.swift index 5a43fdc..5daea31 100644 --- a/myApp/HaruDanim/IOS/Views/HelpView.swift +++ b/myApp/HaruDanim/IOS/Views/HelpView.swift @@ -302,7 +302,7 @@ struct HelpView: View { HelpTopic( symbol: "heart.fill", title: "모음 탭에서 건강 데이터 보기", - body: "모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요." + body: "모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요. 골라 둔 지표는 통계 탭 아래의 건강 카드(하루 값·지표별 추이·합계와 하루 평균)와 기록 탭 목록의 그날 건강 카드에도 그대로 쓰여요 — '건강 데이터 표시'를 끄면 이 카드들도 함께 사라져요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요." ), HelpTopic( symbol: "bed.double.fill", diff --git a/myApp/HaruDanim/IOS/Views/HistoryView.swift b/myApp/HaruDanim/IOS/Views/HistoryView.swift index 3b69469..7951ce5 100644 --- a/myApp/HaruDanim/IOS/Views/HistoryView.swift +++ b/myApp/HaruDanim/IOS/Views/HistoryView.swift @@ -367,6 +367,11 @@ private struct HistoryRecordsView: View { @Query private var sessions: [TimeSession] @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() init(dayKey: Date, mode: HistoryMode, timetableWeekly: Binding, @@ -549,6 +554,17 @@ private struct HistoryRecordsView: View { .buttonStyle(.plain) } } + // 1.5(10): 그날의 건강 값 칩 — 타임테이블의 수면·운동 블록과 짝을 이루는 목록 보완. + // 표시 전용(HealthCache 읽기만)이라 기록 집계·필터·내보내기에는 관여하지 않고, + // 기존 두 섹션 아래에 덧붙어 화면 배치도 그대로다 (통계 탭 건강 카드와 같은 표시 규칙) + if HealthDataStore.isAvailable && healthTilesEnabled && HealthDataStore.shared.hasRequestedAuth { + Section("건강 데이터") { + HealthValueChipGrid( + dayKey: dayKey, + metrics: HealthMetric.selectedList(raw: healthMetricsRaw) + ) + } + } } .scrollContentBackground(.hidden) } diff --git a/myApp/HaruDanim/IOS/Views/SettingsView.swift b/myApp/HaruDanim/IOS/Views/SettingsView.swift index 22cfaf6..cd302fc 100644 --- a/myApp/HaruDanim/IOS/Views/SettingsView.swift +++ b/myApp/HaruDanim/IOS/Views/SettingsView.swift @@ -228,7 +228,7 @@ struct SettingsView: View { } header: { Text("건강 데이터") } footer: { - Text("켜면 애플 건강 앱의 오늘 데이터(걸음·운동·수면 등)를 모음 탭에서 타일로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.") + Text("켜면 애플 건강 앱의 데이터(걸음·운동·수면 등)를 모음 탭 타일과 통계·기록 탭의 건강 카드로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.") } .id("healthSection") } diff --git a/myApp/HaruDanim/IOS/Views/StatsTabView.swift b/myApp/HaruDanim/IOS/Views/StatsTabView.swift index 6897306..f666f7b 100644 --- a/myApp/HaruDanim/IOS/Views/StatsTabView.swift +++ b/myApp/HaruDanim/IOS/Views/StatsTabView.swift @@ -335,6 +335,13 @@ private struct StatsChartsView: View { @Query private var sessions: [TimeSession] @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() init(span: StatSpan, anchorDayKey: Date, rolling: Bool, @@ -509,6 +516,8 @@ private struct StatsChartsView: View { case .month: monthCharts(data) } + // 건강 카드는 행동 통계 아래에 덧붙인다 (1.5(10) — 기존 화면 배치 불변) + healthSection } .padding() 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: 공통 private func chartCard(_ title: LocalizedStringKey, @ViewBuilder content: () -> some View) -> some View { diff --git a/myApp/HaruDanim/IOS/Views/WhatsNewView.swift b/myApp/HaruDanim/IOS/Views/WhatsNewView.swift index c8d3474..add5c91 100644 --- a/myApp/HaruDanim/IOS/Views/WhatsNewView.swift +++ b/myApp/HaruDanim/IOS/Views/WhatsNewView.swift @@ -68,7 +68,7 @@ struct WhatsNewView: View { symbol: "heart.fill", tint: .pink, title: String(localized: "모음 탭에서 건강 데이터 보기"), - body: String(localized: "걸음·운동·수면 같은 애플 건강 앱의 오늘 데이터를 모음 탭 타일로 볼 수 있어요. 값은 기기 밖으로 나가지 않아요.") + body: String(localized: "걸음·운동·수면 같은 애플 건강 앱의 오늘 데이터를 모음 탭 타일로 볼 수 있어요. 통계·기록 탭에서도 함께 보여요. 값은 기기 밖으로 나가지 않아요.") ) feature( symbol: "figure.run", diff --git a/myApp/HaruDanim/Marketing/AppStore/whats-new-1.5.txt b/myApp/HaruDanim/Marketing/AppStore/whats-new-1.5.txt index fc519e4..ea29b33 100644 --- a/myApp/HaruDanim/Marketing/AppStore/whats-new-1.5.txt +++ b/myApp/HaruDanim/Marketing/AppStore/whats-new-1.5.txt @@ -7,6 +7,7 @@ • 모음 탭에서 걸음·운동·수면 같은 오늘의 건강 데이터를 타일로 볼 수 있어요 — 값은 기기 밖으로 나가지 않아요 • 건강 데이터로 다짐을 만들 수 있어요(프리미엄) — '매일 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 • 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 +• 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) • 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、クエストの色も選べます • 「睡眠時間帯」を決めておけば、日付をまたいで眠っても1日が分割されません。昼間に眠る日があるなら、表示の時間帯を曜日ごとに調整できます +• 統計タブで指標ごとの推移と合計・1日平均を、記録タブでその日のヘルス値を一緒に見られます ● 日記がもっと広く(プレミアム) • 日記タブがiPhoneにも登場 — 1日のまとめを確認し、「描く」をオンにすれば指でノートに書けます diff --git a/myApp/HaruDanim/Shared/Settings.swift b/myApp/HaruDanim/Shared/Settings.swift index 173949d..d2ac9b5 100644 --- a/myApp/HaruDanim/Shared/Settings.swift +++ b/myApp/HaruDanim/Shared/Settings.swift @@ -38,6 +38,9 @@ enum SettingsKeys { /// 통계 탭 주간/월간을 달력 주기 대신 '오늘 기준 지난 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 사이드바에는 적용되지 않음, 기기 로컬 설정)