feat(1.5-p6): 소킹 중 추가 5건 — 아이폰 일기 탭·일기 건강 카드·타임테이블 수면/운동 실구간·수면 표시 구간(요일별)·맥 필터 안내, 빌드 4

- 아이폰 일기 개방: AppTab.phoneCases·radialOrder, '그리기' 토글(손가락)·가로 도구줄, 일기 잠금 설정 전 기기
- 건강 카드: DiarySection.health(목록 끝), DiaryHealthCard(지표 선택 공용화·타일 상속·맥 숨김), 인쇄 요약 동승
- 타임테이블 실구간: HealthIntervals 캐시(App Group 400일, CloudKit 금지), injectingHealthBlocks(수면 인디고·운동 주황), 필터 '건강 데이터' 토글+맥 안내
- 수면 표시 구간: SleepWindowPrefs(전체+요일별), 평문 sleep 키=유효 구간 값, 백필 서명 매핑 포함, 새 수면 다짐 기본값 시드
- 실측 수정: 일기 목표 카드 건강 다짐 값 단위, ja 건강 칩 말줄임
- 검증 61/20/27/10 ALL PASS·3종 빌드·카탈로그 0/0·시각 QA 10여 장(26.5/18.5/맥 분기)
- 마케팅: 06-bubble 재촬영, 10-week→10-diary 교체, 아이패드 03 재촬영(3언어), whats-new/설명 갱신

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtZwRRjbtM9pZJDM4FkTq6
This commit is contained in:
songyc macbook 2026-08-22 21:56:57 +09:00
parent ce99d70830
commit 0a55b45515
53 changed files with 1255 additions and 173 deletions

File diff suppressed because one or more lines are too long

View File

@ -157,3 +157,16 @@
- 업데이트 후 첫 실행 시 스플래시 뒤 **버전당 1회** 표시되는 신기능 소개 시트(통상의 "이번 업데이트" 팝업 관습): 1.5 신기능 카드(행동 컴플리케이션·건강 타일·건강 다짐·도움말 개편) + **"여러 기기를 쓰신다면 모든 기기에서 업데이트해 주세요" 안내 1줄**(혼합 버전 완화 — §10 R16 연계).
- 표시 규칙: standard defaults에 마지막 표시 버전 기록(예: `whatsnew.lastShownVersion`) — 저장된 버전과 현재 버전이 다르고 소개 콘텐츠가 있는 버전일 때만 표시, 닫는 즉시 기록(재표시 없음). **신규 설치는 미표시**(첫 실행 안내는 온보딩 담당 — 최초 실행 시 현재 버전을 시드). 온보딩과 동시 표시 금지(온보딩이 우선).
- 구조는 OnboardingView 스타일 재사용, 향후 버전에서 콘텐츠만 교체 가능한 구조로. l10n 3언어, 검증 인자(예: `-showWhatsNew`) 신설.
## 12. 1.5 추가분 — 소킹 중 사용자 요구 5건 (2026-08-22 저녁, 빌드 4) ✅구현 완료
사용자 결정: 1.6이 아니라 **1.5에서 해결**. 세부 UX는 아리나 위임(전제: 기존 기능·사용성 무변화, 버그·충돌 없음). 확정 4결정: ①아이폰 노트 필기= b안(맥 '그리기' 토글 문법, 손가락) ②타임테이블 건강 블록 기본 켬 ③건강 카드 지표 선택= 전 날짜 공통(기기 로컬) ④새 수면 다짐 기본 구간= 타일 설정 시드.
1. **맥 일기 필터창 건강 안내** — RecordFilterSheet 건강 섹션이 맥(HealthDataStore 불가)에선 토글 대신 안내 문구.
2. **아이폰 일기 탭** — AppTab.phoneCases 개방(+radialOrder 필터 제거, 버블 8개 자동 배치). 노트 도구줄= 맥과 같은 가로 스크롤+'그리기' 토글(DeviceLayout.isPhone 신설, drawingPolicy anyInput, 배치↔그리기 상호 배타). 일기 잠금 설정 전 기기 노출, 아이패드 경로 무변경.
3. **일기 '건강 데이터' 카드** — DiarySection.health(케이스 마지막 — 기존 사용자 목록 끝 추가), DiaryHealthCard(HealthCache 하루 값, 수면은 SleepWindowPrefs 유효 구간 키), 지표 선택 diary.healthMetrics(비면 타일 상속), 맥 숨김, 인쇄 요약 DiaryPrintHealthRow 동승.
4. **일기 타임테이블 수면·운동 실구간****HealthIntervals 캐시 신설**(App Group health.intervals.v1, 400일, 실구간을 논리적 하루로 분할·병합 저장 — 다짐의 '깨어난 날 통귀속'과 다른 사실 그대로 표시, CloudKit 금지 동일). refreshRecent(3일)+백필이 유지, 화면·내보내기는 injectingHealthBlocks 주입(캘린더 전례, 최하층 반투명+실선 스타일), 색= 수면 인디고·운동 주황(지표 고정색), 필터 시트 '건강 데이터' 토글(diary.timetableHealthHidden — 전 날짜 공통, 기본 켬), 일기 필터 버튼 상시 노출화.
5. **수면 표시 구간 설정** — SleepWindowPrefs(전체 스펙+요일별, 시작=끝이면 24시간 창), 설정 → 건강 데이터 → '수면 표시 구간'. 평문 "sleep" 캐시 키= 그날 유효 구간 값(타일·카드 공유), 백필 서명에 요일 매핑 포함, QuestEditor 새 수면 다짐 기본값 시드. 발단: 사용자 교대·야간 근무 실측(주말 낮 수면이 기본 창 밖 → 타일 0).
6. 부수 실측 수정: 일기 목표 카드 건강 다짐 값 단위("8,432회/8,000보" → 지표 단위).
검증: 자가 검증 61/20/27/10 ALL PASS, 3종 빌드 그린, 전 카탈로그 missing/stale 0, 시각 QA 10여 장(아이폰 요약·카드·블록·노트·필터·버블8·수면 구간, 아이패드, 맥 분기[카드 숨김], 18.5). 마케팅: 아이폰 06-bubble 재촬영(버블 8개)·10-week→**10-diary** 교체·아이패드 03-diary-summary 재촬영(건강 카드·블록), whats-new-1.5/설명 3언어 갱신. 빌드 4.

View File

@ -2,8 +2,10 @@
> **상태: 보류** — 사용자 결정(2026-08-22): "다음 업데이트 사항으로 두자. 생각이 바뀔 수도, 더 다양한 방법이 떠오를 수도 있으니 일단 보류."
> ⚠️ **구현 금지** — 사용자가 재논의·진행을 지시할 때만 착수. 그 전까지 이 문서는 아이디어 기록 전용.
>
> 📌 **같은 날 저녁 대부분 1.5로 조기 흡수됨**(사용자 재결정 — plan-1.5.md §12): 후보 1의 **일기 표면**(하루 정리 타임테이블 수면·운동 블록)과 후보 2(수면 표시 구간)는 **1.5(4)에 구현 완료**. 1.6에 남은 잔여분: **기록 탭 타임테이블에도 건강 블록 표시**(HealthIntervals 캐시·주입 문법·색은 이미 있어 표시 계층만 — 난이도 하), 물 점 표시(미구현 — 1.5(4)는 수면·운동만).
## 후보 1 — 타임테이블에 건강 데이터 표시
## 후보 1 — 타임테이블에 건강 데이터 표시 (일기 표면은 1.5(4)에 구현 — 잔여: 기록 탭)
- **발단**: 사용자 제안 2026-08-22 (1.5 TestFlight 소킹 시작 직전). "일기나 기록의 타임테이블에서도 건강 데이터가 보여졌으면. 꼭 필요한 건 아니고 있으면 좋을 것 같은 정도."
- **범위 시안(v1)**: 수면·운동(·마음챙김 — 구간 샘플 보유) = **구간 블록**, 물 = **시각+양 점**(횟수 기록과 같은 문법). 걸음·kcal·거리·스탠드는 연속/파생 지표라 타임테이블에 부적합 → **제외로 못 박기**.

View File

@ -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 = 3;
CURRENT_PROJECT_VERSION = 4;
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 = 3;
CURRENT_PROJECT_VERSION = 4;
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 = 3;
CURRENT_PROJECT_VERSION = 4;
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 = 3;
CURRENT_PROJECT_VERSION = 4;
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 = 3;
CURRENT_PROJECT_VERSION = 4;
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 = 3;
CURRENT_PROJECT_VERSION = 4;
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 = 3;
CURRENT_PROJECT_VERSION = 4;
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 = 3;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Widgets/Info.plist;

View File

@ -37,6 +37,9 @@ struct ContentView: View {
NavigationStack { DataImportView() }
} else if UserDefaults.standard.bool(forKey: "dataResetPreview") {
NavigationStack { DataResetView() }
} else if UserDefaults.standard.bool(forKey: "sleepWindowPreview") {
// : -sleepWindowPreview YES (1.5 )
NavigationStack { SleepWindowSettingsView() }
} else {
RootNavigationView()
}
@ -266,9 +269,11 @@ enum AppTab: String, CaseIterable, Identifiable {
static let moreTabValue = "more"
/// iPhone ( iPad // )
/// iPhone . 1.0~1.5(3) iPad 1.5
/// '' (§6.7).
/// ·
static var phoneCases: [AppTab] {
allCases.filter { $0 != .diary }
allCases
}
/// (iPad·Mac) .
@ -326,7 +331,7 @@ enum AppTab: String, CaseIterable, Identifiable {
for tab in AppTab.phoneCases where !seen.contains(tab) {
result.append(tab)
}
return result.filter { $0 != .diary }
return result
}
@ViewBuilder

View File

@ -117,6 +117,16 @@ final class HealthDataStore {
values = values.mapValues { ($0 * 0.9).rounded() }
}
HealthCache.setValues(values, dayKey: key)
// · ·.
// ( + ),
let dayStart = math.dayRange(forKey: key).lowerBound
func at(_ minutes: Double) -> Double {
dayStart.addingTimeInterval(minutes * 60).timeIntervalSince1970
}
HealthIntervals.merge([
HealthIntervals.sleepKey: [key: [[at(0), at(7 * 60 + 28)], [at(23 * 60 + 20), at(24 * 60)]]],
HealthIntervals.workoutKey(.running): [key: [[at(7 * 60 + 42), at(8 * 60 + 13)]]],
])
}
return
}
@ -144,6 +154,7 @@ final class HealthDataStore {
sleepWindows: keys.sleepWindows, workoutKinds: keys.workoutKinds
)
HealthCache.setValues(values, dayKey: key)
HealthIntervals.merge(await dayIntervals(forDayKey: key, math: math))
if offset == 0 {
var tiles: [HealthMetric: Double] = [:]
for metric in HealthMetric.allCases { tiles[metric] = values[metric.rawValue] ?? 0 }
@ -152,9 +163,11 @@ final class HealthDataStore {
}
}
/// ( )·
/// ( )· .
/// · (SleepWindowPrefs +)
private func questHealthKeys() -> (sleepWindows: Set<String>, workoutKinds: Set<HealthWorkoutKind>) {
var windows: Set<String> = [Self.defaultSleepSpec]
windows.formUnion(SleepWindowPrefs.allSpecs)
var kinds: Set<HealthWorkoutKind> = []
let quests = (try? DataStore.shared.mainContext.fetch(FetchDescriptor<Quest>())) ?? []
for quest in quests {
@ -194,10 +207,13 @@ final class HealthDataStore {
for spec in sleepWindows {
guard let parsed = Self.windowSpec(spec) else { continue }
let window = Self.sleepWindow(forDayRange: range, endMinutes: parsed.endMinutes, length: parsed.length)
let value = await sleepDuration(window: window)
result["sleep@\(spec)"] = value
if spec == Self.defaultSleepSpec { result[HealthMetric.sleep.rawValue] = value }
result["sleep@\(spec)"] = await sleepDuration(window: window)
}
// "sleep" (· ) = (1.5
// · )
let effectiveSpec = SleepWindowPrefs.spec(forDayKey: key)
result[HealthMetric.sleep.rawValue] =
result["sleep@\(effectiveSpec)"] ?? result["sleep@\(Self.defaultSleepSpec)"] ?? 0
if !workoutKinds.isEmpty {
let byKind = await workoutDurations(in: range)
for kind in workoutKinds {
@ -269,6 +285,50 @@ final class HealthDataStore {
return Self.mergedDuration(intervals)
}
/// ( , ) mergedDuration .
/// +
static func mergedIntervals(_ intervals: [(Date, Date)]) -> [(Date, Date)] {
let sorted = intervals.sorted { $0.0 < $1.0 }
var result: [(Date, Date)] = []
for (start, end) in sorted {
if let last = result.last, start <= last.1 {
result[result.count - 1].1 = max(last.1, end)
} else {
result.append((start, end))
}
}
return result
}
/// · (HealthIntervals refreshRecent ).
/// ( )
private func dayIntervals(forDayKey dayKey: Date, math: DayMath) async -> [String: [Date: [[Double]]]] {
let range = math.dayRange(forKey: dayKey)
var perKey: [String: [[Double]]] = [:]
for existing in HealthIntervals.workoutKeys(dayKey: dayKey) { perKey[existing] = [] }
let samples = await categorySamples(.sleepAnalysis, in: range)
let asleepValues = Set(HKCategoryValueSleepAnalysis.allAsleepValues.map(\.rawValue))
let sleepIntervals = Self.mergedIntervals(samples.compactMap { sample in
guard asleepValues.contains(sample.value) else { return nil }
let start = max(sample.startDate, range.lowerBound)
let end = min(sample.endDate, range.upperBound)
return end > start ? (start, end) : nil
})
perKey[HealthIntervals.sleepKey] = sleepIntervals.map {
[$0.0.timeIntervalSince1970, $0.1.timeIntervalSince1970]
}
for workout in await workoutSamples(in: range) {
let start = max(workout.startDate, range.lowerBound)
let end = min(workout.endDate, range.upperBound)
guard start < end else { continue }
perKey[HealthIntervals.workoutKey(Self.workoutKind(for: workout.workoutActivityType)), default: []]
.append([start.timeIntervalSince1970, end.timeIntervalSince1970])
}
return perKey.mapValues { [dayKey: $0] }
}
/// ( )
static func mergedDuration(_ intervals: [(Date, Date)]) -> TimeInterval {
let sorted = intervals.sorted { $0.0 < $1.0 }
@ -373,7 +433,7 @@ final class HealthDataStore {
let cal = math.calendar
let todayKey = math.dayKey(for: now)
let keys = questHealthKeys()
let signature = "\(HealthCache.dayToken(todayKey))|\(keys.sleepWindows.sorted().joined(separator: ","))|\(keys.workoutKinds.map(\.rawValue).sorted().joined(separator: ","))"
let signature = "\(HealthCache.dayToken(todayKey))|\(keys.sleepWindows.sorted().joined(separator: ","))|\(keys.workoutKinds.map(\.rawValue).sorted().joined(separator: ","))|\(SleepWindowPrefs.mappingSignature)"
guard AppGroup.defaults.string(forKey: "health.backfillSignature") != signature else { return }
guard let startKey = cal.date(byAdding: .day, value: -(HealthCache.retentionDays - 1), to: todayKey)
@ -446,30 +506,60 @@ final class HealthDataStore {
key = next
}
merged["sleep@\(spec)"] = days
if spec == Self.defaultSleepSpec { merged[HealthMetric.sleep.rawValue] = days }
}
// "sleep" = (SleepWindowPrefs allValues )
var plainSleep: [Date: Double] = [:]
var plainKey = startKey
while plainKey <= todayKey {
let spec = SleepWindowPrefs.spec(forDayKey: plainKey)
if let value = merged["sleep@\(spec)"]?[plainKey] { plainSleep[plainKey] = value }
guard let next = cal.date(byAdding: .day, value: 1, to: plainKey) else { break }
plainKey = next
}
merged[HealthMetric.sleep.rawValue] = plainSleep
// ( , HealthIntervals.
// ' ' )
var sleepIntervalDays: [Date: [(Date, Date)]] = [:]
for (start, end) in asleepIntervals {
let clippedStart = max(start, lower)
let clippedEnd = min(end, upper)
guard clippedStart < clippedEnd else { continue }
for segment in math.splitByDay(start: clippedStart, end: clippedEnd)
where segment.range.lowerBound < segment.range.upperBound {
sleepIntervalDays[segment.dayKey, default: []]
.append((segment.range.lowerBound, segment.range.upperBound))
}
}
var intervalMerged: [String: [Date: [[Double]]]] = [:]
intervalMerged[HealthIntervals.sleepKey] = sleepIntervalDays.mapValues { list in
Self.mergedIntervals(list).map { [$0.0.timeIntervalSince1970, $0.1.timeIntervalSince1970] }
}
// 1 ×
if !keys.workoutKinds.isEmpty {
let workouts = await workoutSamples(in: lower..<upper)
var byKindDays: [HealthWorkoutKind: [Date: Double]] = [:]
for workout in workouts {
let kind = Self.workoutKind(for: workout.workoutActivityType)
let start = max(workout.startDate, lower)
let end = min(workout.endDate, upper)
guard start < end else { continue }
for segment in math.splitByDay(start: start, end: end)
where segment.range.lowerBound < segment.range.upperBound {
byKindDays[kind, default: [:]][segment.dayKey, default: 0]
+= segment.range.upperBound.timeIntervalSince(segment.range.lowerBound)
}
}
for kind in keys.workoutKinds {
merged["workout.\(kind.rawValue)"] = byKindDays[kind] ?? [:]
// 1 × ( ,
// )
let workouts = await workoutSamples(in: lower..<upper)
var byKindDays: [HealthWorkoutKind: [Date: Double]] = [:]
for workout in workouts {
let kind = Self.workoutKind(for: workout.workoutActivityType)
let start = max(workout.startDate, lower)
let end = min(workout.endDate, upper)
guard start < end else { continue }
for segment in math.splitByDay(start: start, end: end)
where segment.range.lowerBound < segment.range.upperBound {
byKindDays[kind, default: [:]][segment.dayKey, default: 0]
+= segment.range.upperBound.timeIntervalSince(segment.range.lowerBound)
intervalMerged[HealthIntervals.workoutKey(kind), default: [:]][segment.dayKey, default: []]
.append([segment.range.lowerBound.timeIntervalSince1970,
segment.range.upperBound.timeIntervalSince1970])
}
}
for kind in keys.workoutKinds {
merged["workout.\(kind.rawValue)"] = byKindDays[kind] ?? [:]
}
HealthCache.merge(merged)
HealthIntervals.merge(intervalMerged)
AppGroup.defaults.set(signature, forKey: "health.backfillSignature")
}

View File

@ -347,6 +347,28 @@
}
}
},
"%@ ~ 다음 날 %@": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "%1$@ %2$@ next day"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%1$@〜翌日%2$@"
}
},
"ko": {
"stringUnit": {
"state": "new",
"value": "%1$@ ~ 다음 날 %2$@"
}
}
}
},
"%@ · %@": {
"localizations": {
"en": {
@ -691,6 +713,22 @@
}
}
},
"%@에 끝나는 24시간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "24 hours ending at %@"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "%@に終わる24時間"
}
}
}
},
"%@요일": {
"localizations": {
"en": {
@ -2797,6 +2835,38 @@
}
}
},
"구간 끝": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Window ends"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "終了時刻"
}
}
}
},
"구간 시작": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Window starts"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "開始時刻"
}
}
}
},
"구간 지정": {
"localizations": {
"en": {
@ -3278,6 +3348,22 @@
}
}
},
"기록 없음": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "No data"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "記録なし"
}
}
}
},
"기록 직접 추가": {
"localizations": {
"en": {
@ -6070,6 +6156,22 @@
}
}
},
"맥에서는 애플 건강 데이터를 지원하지 않아 수면·운동을 타임테이블에 표시할 수 없어요. 아이폰·아이패드에서 확인해 주세요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Health data isn't available on the Mac, so sleep and workouts can't be shown on the timetable. Check them on your iPhone or iPad."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "MacではAppleヘルスケアのデータを利用できないため、睡眠・運動をタイムテーブルに表示できません。iPhone・iPadでご確認ください。"
}
}
}
},
"먼저 공유로 파일을 저장했는지 확인해 주세요. 삭제는 되돌릴 수 없어요.": {
"localizations": {
"en": {
@ -6390,22 +6492,6 @@
}
}
},
"모음 탭 건강 데이터": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Health Data on Home"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ホームのヘルスデータ"
}
}
}
},
"모음 탭 목표 진행 현황": {
"localizations": {
"en": {
@ -6470,18 +6556,34 @@
}
}
},
"모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '모음 탭 건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 전날 밤부터 오늘 아침까지의 수면 구간을 통째로 '오늘 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요.": {
"모음 탭에서 애플 건강을 연결하면 이 날의 걸음·운동·수면 데이터가 여기 보여요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Tap Connect Apple Health in the Health data row of the Home tab to see today's steps, exercise time, sleep and more as tiles. Choose which metrics to show and in what order via the metrics button on the right of the row, and under Settings → Health Data on Home you can turn the row off or reorder it against Favorites and Other actions.\nThe numbers are read from the Apple Health app — Haru Danim never sends them to a server or stores them in iCloud. Values like steps and exercise are today's totals based on your Day start time, while sleep alone counts the whole window from last night to this morning as \"today's sleep.\" Data collected on Apple Watch appears after it syncs with your iPhone, so if a number looks off, check again in a moment. If values stay at zero, check Haru Danim's read access on iPhone under Settings → Privacy & Security → Health."
"value": "Connect Apple Health from the Home tab to see this day's steps, exercise and sleep here."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ホームタブの「ヘルスデータ」の行で「Appleヘルスケアと連携」を押すと、歩数・運動時間・睡眠など今日のデータをタイルで見られます。どの指標をどの順で見せるかは行の右にある指標ボタンで選び、設定 → 「ホームのヘルスデータ」で行そのものをオフにしたり、お気に入り・その他のアクションとの並び順を変えたりできます。\n値はAppleヘルスケアが持つデータを読み取ったものです — Haru Danimがサーバーに送ったりiCloudに保存したりすることはありません。歩数や運動などの値は設定の「1日の開始時刻」を基準にした今日1日の合計で、睡眠だけは前夜から今朝までの睡眠時間帯をまるごと「今日眠った分」として数えます。Apple WatchにたまったデータはウォッチとiPhoneが同期した後に表示されるので、値が合わないように見えたら少し待って確認してください。値がずっと0のままなら、iPhoneの設定 → プライバシーとセキュリティ → ヘルスケアでHaru Danimの読み取り許可を確認してください。"
"value": "ホームタブでAppleヘルスケアを連携すると、この日の歩数・運動・睡眠がここに表示されます。"
}
}
}
},
"모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요.\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."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ホームタブの「ヘルスデータ」行で「Appleヘルスケアと連携」をタップすると、歩数・運動時間・睡眠など今日のデータをタイルで見られます。どの指標をどの順番で表示するかは行の右にある指標ボタンで選び、設定 →「ヘルスデータ」で行そのものをオフにしたり、お気に入り・その他のアクションとの順番を変えたりできます。\n値はAppleヘルスケアAppが持つデータを読み込んだものです — Haru Danimがサーバーへ送信したりiCloudに保存したりすることはありません。歩数・運動などの値は設定の「1日の開始時刻」を基準に今日1日を合計したもので、睡眠だけは決めておいた睡眠時間帯初期設定は夜9時〜翌朝9時に眠った時間をまるごと「その日の睡眠」として数えます。Apple WatchにたまったデータはウォッチとiPhoneが同期した後に表示されるので、値が合わないように見えたら少し待ってから確認してください。値がずっと0の場合は、iPhoneの設定 → プライバシーとセキュリティ → ヘルスケアでHaru Danimの読み取り許可を確認してください。"
}
}
}
@ -7325,18 +7427,18 @@
}
}
},
"무료로는 행동 10개·목표 2개·목표당 다짐 3개까지 만들 수 있어요. 프리미엄에서는 개수 제한이 풀리고 위젯, 애플워치 앱, 시리 단축어, 기기 간 동기화, 아이패드·맥 일기, CSV 가져오기, 건강 다짐을 쓸 수 있어요. 월·년 구독이나 한 번만 결제하는 평생 이용권 중에 고르면 돼요. 구독이 끝나도 기록한 데이터는 그대로 남고, 이미 만든 건강 다짐도 계속 보이고, 쓴 일기도 계속 열람할 수 있어요.": {
"무료로는 행동 10개·목표 2개·목표당 다짐 3개까지 만들 수 있어요. 프리미엄에서는 개수 제한이 풀리고 위젯, 애플워치 앱, 시리 단축어, 기기 간 동기화, 일기(아이폰·아이패드·맥), CSV 가져오기, 건강 다짐을 쓸 수 있어요. 월·년 구독이나 한 번만 결제하는 평생 이용권 중에 고르면 돼요. 구독이 끝나도 기록한 데이터는 그대로 남고, 이미 만든 건강 다짐도 계속 보이고, 쓴 일기도 계속 열람할 수 있어요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The free tier lets you create up to 10 actions, 2 goals and 3 quests per goal. Premium removes those limits and unlocks widgets, the Apple Watch app, Siri shortcuts, device sync, the iPad and Mac diary, CSV import and health quests. Choose a monthly or yearly subscription, or a one-time lifetime purchase. Even after a subscription ends, your data stays, health quests you've made keep showing, and diaries you've written stay readable."
"value": "For free you can create up to 10 actions, 2 goals and 3 quests per goal. Premium removes the limits and unlocks widgets, the Apple Watch app, Siri Shortcuts, cross-device sync, the Diary (iPhone, iPad & Mac), CSV import and health quests. Choose a monthly or yearly subscription, or a one-time lifetime purchase. Even if your subscription ends, your data stays, health quests you've made remain visible, and you can still read the diary you've written."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "無料ではアクション10個・目標2個・目標ごとのクエスト3個まで作れます。プレミアムでは個数制限がなくなり、ウィジェット、Apple Watchアプリ、Siriショートカット、端末間同期、iPad・Macの日記、CSV読み込み、ヘルスクエストが使えます。月額・年額のサブスクリプションか、一度だけ支払う買い切りの生涯利用権から選べます。サブスクリプションが終わっても記録したデータはそのまま残り、作ったヘルスクエストも表示され続け、書いた日記も引き続き読めます。"
"value": "無料ではアクション10個・目標2個・目標ごとのクエスト3個まで作れます。プレミアムでは個数制限がなくなり、ウィジェット、Apple Watch App、Siriショートカット、デバイス間同期、日記iPhone・iPad・Mac、CSV読み込み、ヘルスクエストが使えます。月額・年額のサブスクリプションか、一度だけ支払う買い切りから選べます。サブスクリプションが終わっても記録したデータはそのまま残り、作成済みのヘルスクエストも表示され続け、書いた日記もずっと読み返せます。"
}
}
}
@ -8912,6 +9014,38 @@
}
}
},
"수면 표시 구간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Sleep display window"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "睡眠の表示時間帯"
}
}
}
},
"수면 표시 구간 맞추기": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Tuning the sleep display window"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "睡眠の表示時間帯を合わせる"
}
}
}
},
"수영": {
"localizations": {
"en": {
@ -9618,22 +9752,6 @@
}
}
},
"아이패드·맥 일기": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Diary on iPad and Mac"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "iPad・Macの日記"
}
}
}
},
"아이폰·아이패드·맥을 함께 쓰신다면 모든 기기에서 하루 다님을 업데이트해 주세요. 버전이 섞이면 새 기능이 일부 기기에서 다르게 보일 수 있어요.": {
"localizations": {
"en": {
@ -9650,6 +9768,22 @@
}
}
},
"아이폰에서도 일기 쓰기": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The Diary, now on iPhone"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "iPhoneでも日記を"
}
}
}
},
"아주 넓게": {
"localizations": {
"en": {
@ -9763,6 +9897,22 @@
}
}
},
"애플 건강의 수면·운동 시간을 실제 구간 그대로 타임테이블에 보여줘요. 이 설정은 모든 날짜에 함께 적용돼요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Shows sleep and workout time from Apple Health on the timetable, at their actual times. This setting applies to all dates."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "Appleヘルスケアの睡眠・運動時間を、実際の時間どおりタイムテーブルに表示します。この設定はすべての日付に適用されます。"
}
}
}
},
"애플 펜슬로 자유롭게 쓰고 그려 보세요": {
"localizations": {
"en": {
@ -9923,6 +10073,22 @@
}
}
},
"야간 근무 등으로 특정 요일엔 낮에 잔다면, 그 요일만 구간을 따로 정할 수 있어요. 정하지 않은 요일은 전체 구간을 따라요.\n이 설정은 모음 탭 타일과 일기의 건강 카드에 적용돼요. 다짐의 수면 구간은 다짐 편집에서 따로 정해요 — 새로 만드는 수면 다짐은 여기의 전체 구간으로 시작해요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "If you sleep in the daytime on certain weekdays — night shifts, for example — you can set a separate window for just those days. Weekdays you don't set follow the default window.\nThis setting applies to the Home tab tiles and the Diary's health card. A quest's sleep window is set separately in the quest editor — new sleep quests start with the default window here."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "夜勤などで特定の曜日は昼間に眠るなら、その曜日だけ時間帯を別に決められます。決めていない曜日は基本の時間帯に従います。\nこの設定はホームタブのタイルと日記のヘルスカードに適用されます。クエストの睡眠時間帯はクエスト編集で別に決めます — 新しく作る睡眠クエストは、ここの基本の時間帯で始まります。"
}
}
}
},
"야구": {
"localizations": {
"en": {
@ -10701,6 +10867,22 @@
}
}
},
"요일마다 다르게": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Vary by weekday"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "曜日ごとに変える"
}
}
}
},
"운동": {
"localizations": {
"en": {
@ -11070,6 +11252,22 @@
}
}
},
"이 구간 안에서 잔 시간이 '구간이 끝나는 날'의 수면으로 집계돼요. 기본은 저녁 9시부터 다음 날 아침 9시까지예요. 시작과 끝을 같은 시각으로 두면 하루 전체(24시간)를 봐요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Sleep within this window counts toward the day the window ends on. The default is 9 PM to 9 AM the next day. Set the start and end to the same time to cover a full 24 hours."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "この時間帯の中で眠った時間が「時間帯が終わる日」の睡眠として集計されます。初期設定は夜9時から翌朝9時までです。開始と終了を同じ時刻にすると、丸1日24時間を見ます。"
}
}
}
},
"이 기록에 대한 메모 (선택)": {
"localizations": {
"en": {
@ -11883,18 +12081,34 @@
}
}
},
"일기 (아이패드·맥 · 프리미엄)": {
"일기 (아이폰·아이패드·맥)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Diary (iPad & Mac · Premium)"
"value": "Diary (iPhone, iPad & Mac)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "日記 (iPad・Mac・プレミアム)"
"value": "日記iPhone・iPad・Mac"
}
}
}
},
"일기 (프리미엄)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Diary (Premium)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "日記(プレミアム)"
}
}
}
@ -11979,18 +12193,34 @@
}
}
},
"일기 탭 달력에서 날짜를 누르면 그날의 일기가 열려요. 첫 페이지에는 오늘 기분·요약·목표 진행 현황·할 일·24시간 타임테이블·기록이 모여 하루가 한눈에 정리돼요. 오른쪽 위 ⋯ 메뉴의 '첫 화면 구성'에서 섹션을 보일지 말지와 순서를 바꿀 수 있어요. 기분 사진을 남긴 날은 달력의 그 칸에 사진이 배경으로 깔려요. 일기는 아이패드와 맥에서 쓸 수 있고, iCloud 동기화를 켜면 두 기기에서 같은 일기가 이어져요.": {
"일기 탭 달력에서 날짜를 누르면 그날의 일기가 열려요. 첫 페이지에는 오늘 기분·요약·목표 진행 현황·할 일·24시간 타임테이블·기록·건강 데터가 모여 하루가 한눈에 정리돼요. 오른쪽 위 ⋯ 메뉴의 '첫 화면 구성'에서 섹션을 보일지 말지와 순서를 바꿀 수 있어요. 기분 사진을 남긴 날은 달력의 그 칸에 사진이 배경으로 깔려요. 일기는 아이폰·아이패드·맥에서 쓸 수 있고, iCloud 동기화를 켜면 어느 기기에서든 같은 일기가 이어져요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Tap a date on the Diary tab's calendar to open that day. The first page gathers your mood, a summary, goal progress, to-dos, a 24-hour timetable and records — the whole day at a glance. Use First Page Layout in the ⋯ menu at the top right to show, hide and reorder sections. On days with a mood photo, that photo becomes the background of its calendar cell. The diary lives on iPad and Mac, and with iCloud sync on, the same diary continues across both."
"value": "Tap a date on the Diary tab calendar to open that day's diary. The first page gathers your mood, summary, goal progress, to-dos, a 24-hour timetable, records and health data — your whole day at a glance. Use 'First Page Layout' in the ⋯ menu at the top right to choose which sections appear and in what order. On days you saved a mood photo, it becomes the background of that calendar cell. The Diary works on iPhone, iPad and Mac, and with iCloud sync on, the same diary continues on whichever device you pick up."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "日記タブのカレンダーで日付を押すと、その日の日記が開きます。最初のページには今日の気分・サマリー・目標の進捗・やること・24時間タイムテーブル・記録が集まり、1日がひと目で整理されます。右上の ⋯ メニューの「最初のページの構成」で、セクションの表示・非表示と順番を変えられます。気分の写真を残した日は、カレンダーのそのマスに写真が背景として敷かれます。日記はiPadとMacで使え、iCloud同期をオンにすれば2つの端末で同じ日記が続きます。"
"value": "日記タブのカレンダーで日付をタップすると、その日の日記が開きます。最初のページには今日の気分・サマリー・目標の進み具合・やること・24時間タイムテーブル・記録・ヘルスデータが集まり、1日がひと目で整理されます。右上の⋯メニューの「最初のページの構成」でセクションの表示と順番を変えられます。気分写真を残した日は、カレンダーのそのマスに写真が背景として敷かれます。日記はiPhone・iPad・Macで使え、iCloud同期をオンにすればどの端末でも同じ日記が続きます。"
}
}
}
},
"일기 탭이 아이폰에도 생겼어요. 하루 정리를 확인하고, 노트에서 '그리기'를 켜면 손가락으로 쓰고 그릴 수 있어요. (프리미엄)": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The Diary tab is now on iPhone. Review your day, and turn on Draw in a note to write and sketch with your finger. (Premium)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "日記タブがiPhoneにも登場しました。1日のまとめを確認し、ートで「描く」をオンにすれば指で書いたり描いたりできます。プレミアム"
}
}
}
@ -12163,6 +12393,22 @@
}
}
},
"일기에서 건강 데이터 보기": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Health data in the Diary"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "日記でヘルスデータを見る"
}
}
}
},
"일기의 필기·사진·할 일이 함께 삭제돼요. 시간·횟수 기록과 행동·목표, 일기 양식은 남아요. iCloud 동기화를 켰다면 다른 기기에서도 함께 삭제돼요.": {
"localizations": {
"en": {
@ -12179,6 +12425,22 @@
}
}
},
"일기의 하루 정리 첫 페이지에 '건강 데이터' 카드가 있어요 — 그날의 걸음·운동·수면 같은 값을 일기와 함께 봐요. 카드 오른쪽 위 버튼으로 보여줄 지표를 고르면 모든 날짜에 함께 적용돼요. 타임테이블에는 수면·운동이 실제 시간 그대로 옅은 블록으로 깔려요(수면 남보라·운동 주황) — 타임테이블 필터의 '건강 데이터' 토글로 켜고 끌 수 있어요.\n건강 값은 기기 안에만 있어서 카드의 값도 기기마다 다를 수 있어요 — 아이패드는 시스템의 건강 iCloud 동기화를 켠 경우에만 아이폰과 같은 값이 보이고, 맥에서는 카드와 블록이 보이지 않아요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "The Diary's daily summary page has a 'Health data' card — see that day's steps, exercise, sleep and more alongside your diary. Pick which metrics to show with the button at the card's top right; the choice applies to all dates. The timetable shows sleep and workouts as light blocks at their actual times (indigo for sleep, orange for workouts) — toggle them with 'Health data' in the timetable filter.\nHealth values stay on each device, so the card's values can differ between devices — an iPad shows the same values as your iPhone only when the system's Health iCloud sync is on, and on the Mac the card and blocks don't appear."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "日記の1日のまとめページに「ヘルスデータ」カードがあります — その日の歩数・運動・睡眠などの値を日記と一緒に見られます。カード右上のボタンで表示する指標を選ぶと、すべての日付に適用されます。タイムテーブルには睡眠・運動が実際の時間どおり薄いブロックで敷かれます(睡眠は藍色・運動はオレンジ)— タイムテーブルフィルターの「ヘルスデータ」トグルでオン・オフできます。\nヘルスの値は端末の中だけにあるため、カードの値も端末ごとに異なることがあります — iPadはシステムのヘルスケアiCloud同期をオンにした場合のみiPhoneと同じ値になり、Macではカードとブロックは表示されません。"
}
}
}
},
"일어서기": {
"localizations": {
"en": {
@ -12568,6 +12830,22 @@
}
}
},
"전체 구간": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Default window"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "基本の時間帯"
}
}
}
},
"전체 다짐 합산": {
"localizations": {
"en": {
@ -13998,18 +14276,18 @@
}
}
},
"첫 페이지 타임테이블 카드의 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 보여줄 수 있어요 — 일정은 점선 테두리라 행동과 바로 구분돼요. 필터 버튼으로는 그날 기록된 행동 중 타임테이블에 올릴 것만 고르고, '이 날의 목표' 카드의 필터로는 보고 싶은 목표만 남겨요. 이 선택들은 날짜마다 따로 저장되고, 타임테이블 선택은 일기를 내보낼 때도 그대로 담겨요.\n매일 같은 선택을 되풀이하기 싫다면 달력 화면 오른쪽 위의 '기본 표시 설정'을 써 보세요. 캘린더 일정을 기본으로 넣을지(모든 캘린더/선택한 캘린더만), 어떤 목표·행동을 기본으로 숨길지 한 번만 정하면 모든 날짜에 적용돼요. 날짜별 필터로 다르게 고른 날은 그 선택이 우선하고, '기본값으로 되돌리기'로 언제든 돌아갈 수 있어요.": {
"첫 페이지 타임테이블 카드의 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 보여줄 수 있어요 — 일정은 점선 테두리라 행동과 바로 구분돼요. 필터 버튼으로는 그날 기록된 행동 중 타임테이블에 올릴 것만 고르고, '이 날의 목표' 카드의 필터로는 보고 싶은 목표만 남겨요. 이 선택들은 날짜마다 따로 저장되고, 타임테이블 선택은 일기를 내보낼 때도 그대로 담겨요.\n애플 건강의 수면·운동 시간도 실제 잔 시간·운동한 시간 그대로 옅은 블록으로 깔려요 — 수면은 남보라, 운동은 주황이에요. 필터의 '건강 데이터' 토글로 켜고 끌 수 있고(모든 날짜 공통), 건강 데이터가 없는 맥에서는 보이지 않아요.\n매일 같은 선택을 되풀이하기 싫다면 달력 화면 오른쪽 위의 '기본 표시 설정'을 써 보세요. 캘린더 일정을 기본으로 넣을지(모든 캘린더/선택한 캘린더만), 어떤 목표·행동을 기본으로 숨길지 한 번만 정하면 모든 날짜에 적용돼요. 날짜별 필터로 다르게 고른 날은 그 선택이 우선하고, '기본값으로 되돌리기'로 언제든 돌아갈 수 있어요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Use the calendar button at the top right of the first page's timetable card to pick that day's calendar events and show them alongside — events have dashed borders, so they're easy to tell from actions. The filter button chooses which of the day's recorded actions appear on the timetable, and the filter on the This Day's Goals card keeps just the goals you want. These choices are saved per date, and timetable choices carry into diary exports as is.\nTired of repeating the same choices daily? Try Default Display at the top right of the calendar. Decide once whether calendar events are included by default (all calendars or selected ones) and which goals and actions start hidden, and it applies to every date. Dates where you filtered differently keep their own choice, and Reset to Defaults takes you back anytime."
"value": "Use the calendar button at the top right of the first page's timetable card to pick that day's calendar events to show alongside — events have dashed borders, so they're easy to tell from actions. The filter button picks which of that day's recorded actions go on the timetable, and the filter on the This Day's Goals card keeps only the goals you want. These choices are saved per date, and the timetable selection carries into diary exports as is.\nSleep and workout time from Apple Health also appear as light blocks, at the times you actually slept and worked out — indigo for sleep, orange for workouts. Toggle them with 'Health data' in the filter (applies to all dates); they don't appear on the Mac, which has no health data.\nTired of repeating the same choices every day? Try 'Default Display' at the top right of the calendar screen. Decide once whether to include calendar events by default (all calendars, or selected ones only) and which goals and actions to hide by default, and it applies to every date. Days where you picked differently keep their own choice, and 'Reset to Default' takes you back anytime."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "最初のページのタイムテーブルカード右上のカレンダーボタンで、その日のカレンダー予定を選んで一緒に表示できます — 予定は点線の枠なので、アクションとすぐ見分けられます。フィルタボタンでは、その日記録されたアクションのうちタイムテーブルに載せるものだけを選び、「この日の目標」カードのフィルタでは見たい目標だけを残します。これらの選択は日付ごとに別々に保存され、タイムテーブルの選択は日記を書き出すときにもそのまま反映されます。\n毎日同じ選択を繰り返したくなければ、カレンダー画面右上の「デフォルト表示設定」を使ってみてください。カレンダー予定をデフォルトで入れるか(すべてのカレンダー選んだカレンダーのみ)、どの目標・アクションをデフォルトで隠すかを一度だけ決めれば、すべての日付に適用されます。日付別フィルタで違う選択をした日はその選択が優先され、「デフォルトに戻す」でいつでも戻れます。"
"value": "最初のページのタイムテーブルカード右上のカレンダーボタンで、その日のカレンダー予定を選んで一緒に表示できます — 予定は破線の枠なのでアクションとすぐ区別できます。フィルターボタンではその日に記録したアクションのうちタイムテーブルに載せるものを選び、「この日の目標」カードのフィルタでは見たい目標だけを残します。これらの選択は日付ごとに保存され、タイムテーブルの選択は日記の書き出しにもそのまま反映されます。\nAppleヘルスケアの睡眠・運動時間も、実際に眠った・運動した時間どおり薄いブロックで敷かれます — 睡眠は藍色、運動はオレンジです。フィルターの「ヘルスデータ」トグルでオン・オフできすべての日付に共通、ヘルスデータのないMacでは表示されません。\n毎日同じ選択を繰り返したくないなら、カレンダー画面右上の「デフォルト表示設定」を使ってみてください。カレンダー予定をデフォルトで入れるか(すべてのカレンダー/選んだカレンダーのみ)、どの目標・アクションをデフォルトで隠すかを一度決めれば、すべての日付に適用されます。日付別フィルターで別の選択をした日はその選択が優先され、「デフォルトに戻す」でいつでも戻れます。"
}
}
}
@ -14532,18 +14810,18 @@
}
}
},
"켜면 애플 건강 앱의 오늘 데이터(걸음·운동·수면 등)를 모음 탭에서 타일로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요.": {
"켜면 애플 건강 앱의 오늘 데이터(걸음·운동·수면 등)를 모음 탭에서 타일로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Turn this on to see today's Apple Health data (steps, exercise, sleep and more) as tiles on the Home tab. Use Section order to arrange Favorites, Other actions and Health data."
"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."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "オンにすると、Appleヘルスケアの今日のデータ歩数・運動・睡眠などをホームタブのタイルで見られます。「セクションの表示順」でお気に入り・その他の行動・ヘルスデータの並びを決められます。"
"value": "オンにすると、AppleヘルスケアAppの今日のデータ(歩数・運動・睡眠など)をホームタブのタイルで見られます。セクションの表示順でお気に入り・その他のアクション・ヘルスデータの配置を決められます。睡眠の表示時間帯は、どの時間帯の眠りを「その日の睡眠」と見なすかを決めます — 昼夜が入れ替わる日があっても合わせられます。"
}
}
}
@ -14676,6 +14954,22 @@
}
}
},
"타일의 수면이 실제와 다르게 0으로 보인다면, 그 잠이 수면 구간 밖에 있었을 가능성이 커요 — 기본 구간(저녁 9시~다음 날 아침 9시)은 아침 9시 이후에 잔 잠을 세지 않거든요. 설정 → 건강 데이터 → '수면 표시 구간'에서 구간을 바꿔 보세요. 시작과 끝을 같은 시각으로 두면 그 시각에 끝나는 하루 전체(24시간)를 보고, '요일마다 다르게'를 켜면 야간 근무처럼 특정 요일만 낮에 자는 패턴도 요일별 구간으로 맞출 수 있어요.\n이 설정은 모음 탭 타일과 일기의 건강 카드에 적용돼요. 다짐의 수면 구간은 다짐 편집에서 따로 정하고, 새로 만드는 수면 다짐은 여기의 전체 구간으로 시작해요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "If the sleep tile shows 0 when you actually slept, that sleep probably fell outside the sleep window — the default window (9 PM to 9 AM the next day) doesn't count sleep after 9 AM. Try changing it in Settings → Health data → 'Sleep display window'. Set the start and end to the same time to cover the full 24 hours ending then, or turn on 'Vary by weekday' to match patterns like night shifts, where you sleep in the daytime only on certain days.\nThis setting applies to the Home tab tiles and the Diary's health card. A quest's sleep window is set separately in the quest editor, and new sleep quests start with the default window here."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "実際に眠ったのに睡眠タイルが0に見えるなら、その眠りが睡眠時間帯の外にあった可能性が高いです — 基本の時間帯夜9時〜翌朝9時は朝9時以降の眠りを数えません。設定 → ヘルスデータ →「睡眠の表示時間帯」で時間帯を変えてみてください。開始と終了を同じ時刻にすればその時刻に終わる丸1日24時間を見ますし、「曜日ごとに変える」をオンにすれば、夜勤のように特定の曜日だけ昼間に眠るパターンも曜日別の時間帯で合わせられます。\nこの設定はホームタブのタイルと日記のヘルスカードに適用されます。クエストの睡眠時間帯はクエスト編集で別に決め、新しく作る睡眠クエストはここの基本の時間帯で始まります。"
}
}
}
},
"타임테이블": {
"localizations": {
"en": {
@ -15288,18 +15582,18 @@
}
}
},
"페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 마음껏 쓰고 그리세요 — 손가락은 페이지 넘기기와 확대·축소를 맡아요. 두 손가락으로 벌려 확대하면 작은 글씨도 또박또박 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 차례를 바꿔요. 맥에서는 키보드 입력이 기본이고, 도구 줄의 '그리기'를 켠 동안에만 마우스·트랙패드로 그려져요 — 평소의 클릭과 스크롤은 이동과 버튼에 쓰여요.": {
"페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 마음껏 쓰고 그리세요 — 손가락은 페이지 넘기기와 확대·축소를 맡아요. 두 손가락으로 벌려 확대하면 작은 글씨도 또박또박 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 차례를 바꿔요.\n아이폰에서는 도구 줄의 '그리기'를 켠 동안 손가락으로 그려지고, 끄면 손가락이 다시 페이지 넘기기·확대에 쓰여요. 맥에서는 키보드 입력이 기본이고, 마찬가지로 '그리기'를 켠 동안에만 마우스·트랙패드로 그려져요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Turn the page and free-form notes continue. Write and draw to your heart's content with Apple Pencil — fingers handle page turning and zoom. Spread two fingers to zoom in and even tiny letters come out neat, and with lined paper on you can pick the line spacing too. With multiple pages, reorder them via Change Page Order in the ⋯ menu. On the Mac, typing is the default; the mouse and trackpad draw only while Draw is on in the toolbar — otherwise clicks and scrolls do their usual moving and button work."
"value": "Turn the page and free-form notes continue. Write and draw all you like with Apple Pencil — fingers handle page turns and pinch zoom. Spread two fingers to zoom in for neat small writing, and turn on lined paper to pick the line spacing. With multiple pages, use 'Reorder Pages' in the ⋯ menu at the top right.\nOn iPhone, your finger draws while 'Draw' in the toolbar is on — turn it off and your finger goes back to turning and zooming pages. On the Mac, keyboard input is the default, and likewise the mouse and trackpad draw only while 'Draw' is on."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "ページをめくると自由ートが続きます。Apple Pencilで思いのままに書いて描いてください — 指はページめくりと拡大・縮小を担当します。2本指で広げて拡大すれば小さな字もくっきり書けて、罫線ノートをオンにすれば行間も選べます。ページが複数あるときは、右上の ⋯ メニューの「ページの並べ替え」で順番を変えます。Macではキーボード入力が基本で、ツールバーの「描画」をオンにしている間だけマウス・トラックパッドで描けます — ふだんのクリックとスクロールは移動やボタンに使われます。"
"value": "ページをめくると自由ートが続きます。Apple Pencilで思いのままに書いて描いてください — 指はページめくりと拡大・縮小を担当します。2本の指で広げて拡大すれば小さな字もきれいに書け、罫線ノートをオンにすれば行間も選べます。ページが複数あるときは右上の⋯メニューの「ページの順序を変更」で並べ替えます。\niPhoneではツールバーの「描く」をオンにしている間だけ指で描け、オフにすると指はページめくり・拡大に戻ります。Macではキーボード入力が基本で、同じく「描く」をオンにしている間だけマウス・トラックパッドで描けます。"
}
}
}
@ -15993,18 +16287,34 @@
}
}
},
"하루 정리와 애플 펜슬 다이어리 — 맥에서는 마우스·키보드로 써요.": {
"하루 정리에 그날의 건강 값을 담는 카드가 생기고, 타임테이블에는 수면·운동이 실제 시간 그대로 깔려요. 낮에 자는 날이 있다면 설정의 '수면 표시 구간'을 요일별로 맞춰 보세요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Daily summary pages and an Apple Pencil diary — on Mac, write with mouse and keyboard."
"value": "The daily summary gains a card with that day's health values, and sleep and workouts now appear on the timetable at their actual times. If you sleep in the daytime some days, tune the 'Sleep display window' per weekday in Settings."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "一日のまとめとApple Pencil日記 — Macではマウス・キーボードで書けます。"
"value": "1日のまとめにその日のヘルス値をまとめるカードが加わり、タイムテーブルには睡眠・運動が実際の時間どおりに表示されます。昼間に眠る日があるなら、設定の「睡眠の表示時間帯」を曜日ごとに合わせてみてください。"
}
}
}
},
"하루 정리와 다이어리 — 아이패드는 애플 펜슬, 아이폰은 손가락 그리기, 맥은 마우스·키보드로 써요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "A daily summary and a free-form diary — Apple Pencil on iPad, finger drawing on iPhone, mouse and keyboard on Mac."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "1日のまとめと自由なダイアリー — iPadはApple Pencil、iPhoneは指描き、Macはマウス・キーボードで。"
}
}
}
@ -16105,18 +16415,18 @@
}
}
},
"하루의 기록·통계와 함께 애플 펜슬로 쓰는\n나만의 다이어리를 만들 수 있어요.": {
"하루의 기록·통계·건강 데이터와 함께\n나만의 다이어리를 만들 수 있어요.": {
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Create your own diary with Apple Pencil,\nalongside your daily records and stats."
"value": "Build your own diary alongside\nyour day's records, stats and health data."
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "1日の記録・統計とともに、Apple Pencilで書く\n自分だけのダイアリーが作れます。"
"value": "1日の記録・統計・ヘルスデータと一緒に\n自分だけのダイアリーを作れます。"
}
}
}

View File

@ -331,8 +331,9 @@ struct DiaryExportRenderer {
))
}
}
// ( ) 24 +
// ( ) 24 + + (1.5)
snapshot = snapshot.injectingCalendarEvents(DiaryCalendarEvents.timetableBlocks(for: entry, math: math))
snapshot = snapshot.injectingHealthBlocks(DiaryHealthTimetable.blocks(dayKey: entry.dayKey, math: math))
let summary = DiaryPrintSummaryView(entry: entry, snapshot: snapshot)
let summaryRenderer = ImageRenderer(content: summary)
summaryRenderer.scale = 2
@ -482,6 +483,7 @@ struct DiaryPrintSummaryView: View {
if !snapshot.hero.isEmpty {
ExportHeroRow(stats: snapshot.hero)
}
DiaryPrintHealthRow(dayKey: entry.dayKey)
ForEach(Array(snapshot.sections.enumerated()), id: \.offset) { _, section in
ExportSectionView(section: section, innerWidth: innerWidth)
}
@ -493,6 +495,61 @@ struct DiaryPrintSummaryView: View {
}
}
/// (1.5) (DiaryHealthCard) · .
/// (· )
struct DiaryPrintHealthRow: View {
let dayKey: Date
private var entries: [(metric: HealthMetric, value: Double)] {
guard HealthDataStore.isAvailable else { return [] }
let hiddenRaw = UserDefaults.standard.string(forKey: DiarySectionConfig.hiddenKey) ?? ""
guard !DiarySectionConfig.hidden(from: hiddenRaw).contains(.health) else { return [] }
let diaryRaw = UserDefaults.standard.string(forKey: "diary.healthMetrics") ?? ""
let tileRaw = AppGroup.defaults.string(forKey: LocalPrefsKeys.healthMetrics) ?? ""
let metrics = HealthMetric.selectedList(raw: diaryRaw.isEmpty ? tileRaw : diaryRaw)
return metrics.compactMap { metric in
let key = metric == .sleep
? "sleep@\(SleepWindowPrefs.spec(forDayKey: dayKey))"
: metric.rawValue
guard let value = HealthCache.value(key, dayKey: dayKey) else { return nil }
return (metric, value)
}
}
var body: some View {
let list = entries
if !list.isEmpty {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
Image(systemName: "heart.fill")
.font(.caption)
.foregroundStyle(.pink)
Text("건강 데이터")
.font(.subheadline.weight(.semibold))
}
HStack(spacing: 14) {
ForEach(list, id: \.metric) { item in
HStack(spacing: 5) {
Image(safeSymbol: item.metric.symbolName)
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(.pink)
Text(item.metric.name)
.font(.caption2)
.foregroundStyle(.secondary)
Text(item.metric.valueLabel(item.value))
.font(.footnote.weight(.semibold).monospacedDigit())
}
}
Spacer(minLength: 0)
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
}
// MARK: - (768×1024 )
struct DiaryPrintNotePageView: View {

View File

@ -50,15 +50,15 @@ struct DiaryNotePageView: View {
@State private var debugShapeEditDone = false
@State private var debugTextEditDone = false
#endif
/// . (Designed for iPad) ·
/// '' ( UX, §6.7).
/// . ()·(, 1.5 ) ·
/// '' (§6.7).
/// true( ) .
@State private var drawMode: Bool = {
#if DEBUG
// ·: -diaryDrawMode YES
// ·: -diaryDrawMode YES ·
if UserDefaults.standard.bool(forKey: "diaryDrawMode") { return true }
#endif
return !DeviceLayout.isMac
return !(DeviceLayout.isMac || DeviceLayout.isPhone)
}()
/// ( nil )
@ -153,10 +153,11 @@ struct DiaryNotePageView: View {
/// : '' , '' .
/// xLarge +
/// ( ).
/// (1.5 ) ''
@ViewBuilder
private var pageToolbar: some View {
Group {
if DeviceLayout.isMac {
if DeviceLayout.isMac || DeviceLayout.isPhone {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
textAddButton
@ -241,7 +242,7 @@ struct DiaryNotePageView: View {
}
}
///
/// · /
private var drawModeToggle: some View {
Toggle(isOn: $drawMode.animation()) {
Label("그리기", systemImage: "pencil.tip")
@ -288,8 +289,8 @@ struct DiaryNotePageView: View {
selectedItemID = nil
endInlineTextEditing()
}
// :
if arranging && DeviceLayout.isMac { drawMode = false }
// ·: /
if arranging && (DeviceLayout.isMac || DeviceLayout.isPhone) { drawMode = false }
}
}
@ -336,7 +337,7 @@ struct DiaryNotePageView: View {
// . ,
// ( ) .
// drawMode ( true , §6.7)
// drawMode · ( true , §6.7)
DiaryPencilCanvas(
drawingData: page.drawingData,
isActive: isActive && !arranging && !readOnly && drawMode,
@ -1277,9 +1278,9 @@ struct DiaryPencilCanvas: UIViewRepresentable {
canvas.drawingPolicy = .anyInput
#else
// : / .
// (Designed for iPad) ·
// (·)·(, 1.5 ) anyInput
// '' (drawMode) (§6.7)
canvas.drawingPolicy = DeviceLayout.isMac ? .anyInput : .pencilOnly
canvas.drawingPolicy = (DeviceLayout.isMac || DeviceLayout.isPhone) ? .anyInput : .pencilOnly
#endif
canvas.delegate = context.coordinator
if let drawing = try? PKDrawing(data: drawingData), !drawingData.isEmpty {

View File

@ -499,7 +499,7 @@ struct DiaryLockedView: View {
.foregroundStyle(AppTheme.yellow)
Text("일기는 프리미엄 기능이에요")
.font(.title3.weight(.semibold))
Text("하루의 기록·통계와 함께 애플 펜슬로 쓰는\n나만의 다이어리를 만들 수 있어요.")
Text("하루의 기록·통계·건강 데이터와 함께\n나만의 다이어리를 만들 수 있어요.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
@ -947,7 +947,9 @@ private struct DiaryPageOrderSheet: View {
/// . rawValue .
enum DiarySection: String, CaseIterable, Identifiable {
case mood, hero, goals, todos, timetable, records, bars
// health 1.5 ·
// ( )
case mood, hero, goals, todos, timetable, records, bars, health
var id: String { rawValue }
@ -960,6 +962,7 @@ enum DiarySection: String, CaseIterable, Identifiable {
case .timetable: return "타임테이블"
case .records: return "기록 상세"
case .bars: return "통계 막대"
case .health: return "건강 데이터"
}
}
@ -972,6 +975,7 @@ enum DiarySection: String, CaseIterable, Identifiable {
case .timetable: return "calendar.day.timeline.left"
case .records: return "list.bullet.rectangle"
case .bars: return "chart.bar.fill"
case .health: return "heart.fill"
}
}
}
@ -1017,7 +1021,11 @@ struct DiarySectionConfigSheet: View {
@AppStorage(DiarySectionConfig.orderKey) private var orderRaw = ""
@AppStorage(DiarySectionConfig.hiddenKey) private var hiddenRaw = ""
private var sections: [DiarySection] { DiarySectionConfig.order(from: orderRaw) }
/// ()
private var sections: [DiarySection] {
DiarySectionConfig.order(from: orderRaw)
.filter { $0 != .health || HealthDataStore.isAvailable }
}
private var hiddenSections: Set<DiarySection> { DiarySectionConfig.hidden(from: hiddenRaw) }
var body: some View {
@ -1110,6 +1118,8 @@ private struct DiarySummaryPage: View {
@AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = ""
@AppStorage(DiarySectionConfig.orderKey) private var sectionOrderRaw = ""
@AppStorage(DiarySectionConfig.hiddenKey) private var hiddenSectionsRaw = ""
/// (/) (1.5)
@AppStorage(DiaryHealthTimetable.hiddenKey) private var healthHiddenRaw = ""
/// ( .task )
@State private var calendarBlocks: [ExportTimetableData.Block] = []
@ -1131,10 +1141,13 @@ private struct DiarySummaryPage: View {
private var math: DayMath { DayMath() }
/// ( , )
/// ( , ).
/// ()
private var visibleSections: [DiarySection] {
let hidden = DiarySectionConfig.hidden(from: hiddenSectionsRaw)
return DiarySectionConfig.order(from: sectionOrderRaw).filter { !hidden.contains($0) }
return DiarySectionConfig.order(from: sectionOrderRaw).filter {
!hidden.contains($0) && ($0 != .health || HealthDataStore.isAvailable)
}
}
private var orderedActions: [Action] {
@ -1288,6 +1301,7 @@ private struct DiarySummaryPage: View {
tags: allTags,
actions: dayRecordedActions,
excludedActionIDs: $filterExcluded,
showsHealthSection: true,
onResetToDefault: {
// ( ) .
// filterExcluded onChange ( )
@ -1371,6 +1385,8 @@ private struct DiarySummaryPage: View {
recordSections(innerWidth: innerWidth)
case .bars:
barSections(innerWidth: innerWidth)
case .health:
DiaryHealthCard(dayKey: entry.dayKey)
}
}
@ -1395,15 +1411,23 @@ private struct DiarySummaryPage: View {
/// - :
@ViewBuilder
private func timetableSections(innerWidth: CGFloat) -> some View {
let injected = timetableSnapshot.injectingCalendarEvents(calendarBlocks)
// (· ) healthHiddenRaw
// (1.5 )
let injected = timetableSnapshot
.injectingCalendarEvents(calendarBlocks)
.injectingHealthBlocks(DiaryHealthTimetable.blocks(
dayKey: entry.dayKey, math: math,
hidden: Set(healthHiddenRaw.split(separator: ",").map(String.init))
))
let hiddenCount = timetableExcludedIDs.count
ForEach(Array(injected.sections.enumerated()), id: \.offset) { _, section in
if case .timetable = section {
ZStack(alignment: .topTrailing) {
ExportSectionView(section: section, innerWidth: innerWidth)
// (·)
// (·) .
// (1.5 · )
HStack(spacing: 6) {
if !readOnly, !dayRecordedActions.isEmpty || hiddenCount > 0 {
if !readOnly {
Button {
filterExcluded = timetableExcludedIDs
showingActionFilter = true
@ -1727,6 +1751,11 @@ private struct DiaryGoalsCard: View {
}
private func valueLabel(_ value: Double, quest: Quest) -> String {
// (·kcal·km·L ) targetValueLabel .
// ("8,432 / 8,000" , 1.5(4))
if let health = quest.healthTarget {
return health.valueLabel(value)
}
switch quest.measure {
case .time: return Format.durationShort(value)
case .count: return String(localized: "\(Int(value))")

View File

@ -49,6 +49,20 @@ struct ExportSnapshot {
return copy
}
/// (·) (1.5
/// · . , )
func injectingHealthBlocks(_ blocks: [ExportTimetableData.Block]) -> ExportSnapshot {
guard !blocks.isEmpty else { return self }
var copy = self
copy.sections = sections.map { section in
guard case .timetable(let title, let subtitle, var data) = section,
data.days.count == 1 else { return section }
data.days[0].healthBlocks = blocks
return .timetable(title: title, subtitle: subtitle, data)
}
return copy
}
/// .
/// · () .
func replacingTimetableSections(with other: ExportSnapshot) -> ExportSnapshot {
@ -140,6 +154,8 @@ struct ExportTimetableData {
var markers: [Marker]
/// (EventKit) ( )
var eventBlocks: [Block] = []
/// (·) ( , 1.5)
var healthBlocks: [Block] = []
}
/// (). (startHour + ) % 24
@ -1175,6 +1191,9 @@ private struct ExportTimetableView: View {
Spacer(minLength: 0)
}
}
ForEach(day.healthBlocks) { block in
healthBlockView(block, columnWidth: columnWidth)
}
ForEach(day.eventBlocks) { block in
eventBlockView(block, columnWidth: columnWidth)
}
@ -1239,6 +1258,36 @@ private struct ExportTimetableView: View {
.offset(x: 2, y: y)
}
/// (·) : + .
/// ( )· ( ) ,
/// (1.5 )
private func healthBlockView(_ block: ExportTimetableData.Block, columnWidth: CGFloat) -> some View {
let y = CGFloat(block.startFrac - Double(data.hourLo)) * hourHeight
let height = max(CGFloat(block.endFrac - block.startFrac) * hourHeight, 5)
return RoundedRectangle(cornerRadius: 4, style: .continuous)
.fill(block.color.opacity(0.16))
.overlay(
RoundedRectangle(cornerRadius: 4, style: .continuous)
.strokeBorder(block.color.opacity(0.55), lineWidth: 1)
)
.overlay(alignment: .topLeading) {
if !weekly && height >= 18 {
Label(block.name, systemImage: block.symbol)
.font(.caption2.weight(.semibold))
.foregroundStyle(block.color)
.lineLimit(1)
.padding(4)
} else if height >= 12 {
Image(safeSymbol: block.symbol)
.font(.system(size: 7, weight: .bold))
.foregroundStyle(block.color)
.padding(2)
}
}
.frame(width: max(columnWidth - 4, 8), height: height)
.offset(x: 2, y: y)
}
private func markerView(_ marker: ExportTimetableData.Marker, index: Int, columnWidth: CGFloat) -> some View {
let size: CGFloat = weekly ? 8 : 14
let x = 4 + CGFloat(index % 5) * (size + 3)

View File

@ -33,7 +33,7 @@ struct HealthSectionView: View {
}
}
.sheet(isPresented: $showingMetricPicker) {
HealthMetricPickerSheet()
HealthMetricPickerSheet(metricsRaw: $metricsRaw)
}
.sheet(isPresented: $showingGuide) {
HealthGuideSheet()
@ -161,12 +161,14 @@ struct HealthSectionView: View {
}
}
// MARK: - ( + )
// MARK: - ( + · )
struct HealthMetricPickerSheet: View {
@Environment(\.dismiss) private var dismiss
@AppStorage(LocalPrefsKeys.healthMetrics, store: AppGroup.defaults)
private var metricsRaw = ""
/// raw ( =AppGroup healthMetrics, =standard diary.healthMetrics)
@Binding var metricsRaw: String
/// metricsRaw ( )
var fallbackRaw: String = ""
/// : ( ) +
@State private var selected: [HealthMetric] = []
@ -209,7 +211,7 @@ struct HealthMetricPickerSheet: View {
}
}
.onAppear {
selected = HealthMetric.selectedList(raw: metricsRaw)
selected = HealthMetric.selectedList(raw: metricsRaw.isEmpty ? fallbackRaw : metricsRaw)
}
}
.presentationDetents([.medium, .large])
@ -352,3 +354,290 @@ struct HealthGuideSheet: View {
}
}
}
// MARK: - (1.5 · , HealthIntervals )
/// · · .
/// ·' ' , .
enum DiaryHealthTimetable {
/// (standard defaults , . "sleep"/"workout" )
static let hiddenKey = "diary.timetableHealthHidden"
static let sleepColor = Color.indigo
static let workoutColor = Color.orange
static var hidden: Set<String> {
Set((UserDefaults.standard.string(forKey: hiddenKey) ?? "")
.split(separator: ",").map(String.init))
}
static func setHidden(_ set: Set<String>) {
UserDefaults.standard.set(set.sorted().joined(separator: ","), forKey: hiddenKey)
}
/// (DiarySummaryPage) (DiaryExportRenderer)
/// . (· ) .
/// hidden @AppStorage (=)
static func blocks(dayKey: Date, math: DayMath,
hidden: Set<String>? = nil) -> [ExportTimetableData.Block] {
guard HealthDataStore.isAvailable else { return [] }
let hiddenSet = hidden ?? Self.hidden
let dayStart = math.dayRange(forKey: dayKey).lowerBound
func frac(_ date: Date) -> Double { date.timeIntervalSince(dayStart) / 3600 }
var blocks: [ExportTimetableData.Block] = []
if !hiddenSet.contains("sleep") {
for interval in HealthIntervals.intervals(HealthIntervals.sleepKey, dayKey: dayKey) {
blocks.append(ExportTimetableData.Block(
startFrac: frac(interval.start), endFrac: frac(interval.end),
color: sleepColor, symbol: HealthMetric.sleep.symbolName,
name: HealthMetric.sleep.name
))
}
}
if !hiddenSet.contains("workout") {
for key in HealthIntervals.workoutKeys(dayKey: dayKey).sorted() {
guard let kind = HealthWorkoutKind(rawValue: String(key.dropFirst("workout.".count)))
else { continue }
for interval in HealthIntervals.intervals(key, dayKey: dayKey) {
blocks.append(ExportTimetableData.Block(
startFrac: frac(interval.start), endFrac: frac(interval.end),
color: workoutColor, symbol: kind.symbolName, name: kind.name
))
}
}
}
return blocks
}
}
// MARK: - ' ' (1.5 , · )
/// . HealthCache( ) (§ ),
/// (SleepWindowPrefs ) .
struct DiaryHealthCard: View {
let dayKey: Date
@Environment(\.diaryReadOnly) private var readOnly
/// · .
@AppStorage("diary.healthMetrics") private var diaryMetricsRaw = ""
@AppStorage(LocalPrefsKeys.healthMetrics, store: AppGroup.defaults) private var tileMetricsRaw = ""
@State private var store = HealthDataStore.shared
@State private var showingPicker = false
private var metrics: [HealthMetric] {
HealthMetric.selectedList(raw: diaryMetricsRaw.isEmpty ? tileMetricsRaw : diaryMetricsRaw)
}
private func value(_ metric: HealthMetric) -> Double? {
let key = metric == .sleep
? "sleep@\(SleepWindowPrefs.spec(forDayKey: dayKey))"
: metric.rawValue
return HealthCache.value(key, dayKey: dayKey)
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 6) {
Image(systemName: "heart.fill")
.font(.caption)
.foregroundStyle(.pink)
Text("건강 데이터")
.font(.subheadline.weight(.semibold))
Spacer(minLength: 0)
if !readOnly, store.hasRequestedAuth {
Button {
showingPicker = true
} label: {
Image(systemName: "slider.horizontal.3")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(AppTheme.green)
.padding(.horizontal, 8)
.padding(.vertical, 5)
.background(AppTheme.green.opacity(0.12), in: Capsule())
}
.buttonStyle(.plain)
.accessibilityLabel(Text("표시할 지표 선택"))
}
}
if store.hasRequestedAuth {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 96), spacing: 8)], spacing: 8) {
ForEach(metrics) { metric in
chip(metric)
}
}
} else {
Text("모음 탭에서 애플 건강을 연결하면 이 날의 걸음·운동·수면 데이터가 여기 보여요.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
.sheet(isPresented: $showingPicker) {
HealthMetricPickerSheet(metricsRaw: $diaryMetricsRaw, fallbackRaw: tileMetricsRaw)
}
.task { await store.refreshToday() }
}
private func chip(_ metric: HealthMetric) -> some View {
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 = value(metric) {
// ja "728" (1.5(4) )
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(metric).map(metric.valueLabel) ?? String(localized: "기록 없음"))"))
}
}
// MARK: - (1.5 , SleepWindowPrefs)
/// · . ·
/// (= 24) .
/// , .
struct SleepWindowSettingsView: View {
@AppStorage(LocalPrefsKeys.sleepWindowGlobal, store: AppGroup.defaults)
private var globalRaw = ""
@AppStorage(LocalPrefsKeys.sleepWindowWeekdayEnabled, store: AppGroup.defaults)
private var weekdayEnabled = false
/// defaults
@State private var weekdaySpecs: [String: String] = SleepWindowPrefs.weekdaySpecs
/// : ~ ( )
private let weekdayOrder = [2, 3, 4, 5, 6, 7, 1]
var body: some View {
List {
Section {
SleepWindowEditorRows(spec: Binding(
get: { globalRaw.isEmpty ? SleepWindowPrefs.defaultSpec : globalRaw },
set: { globalRaw = $0 }
))
} header: {
Text("전체 구간")
} footer: {
Text("이 구간 안에서 잔 시간이 '구간이 끝나는 날'의 수면으로 집계돼요. 기본은 저녁 9시부터 다음 날 아침 9시까지예요. 시작과 끝을 같은 시각으로 두면 하루 전체(24시간)를 봐요.")
}
Section {
Toggle(isOn: $weekdayEnabled.animation()) {
Text("요일마다 다르게")
}
.tint(AppTheme.green)
if weekdayEnabled {
ForEach(weekdayOrder, id: \.self) { weekday in
weekdayGroup(weekday)
}
}
} footer: {
Text("야간 근무 등으로 특정 요일엔 낮에 잔다면, 그 요일만 구간을 따로 정할 수 있어요. 정하지 않은 요일은 전체 구간을 따라요.\n이 설정은 모음 탭 타일과 일기의 건강 카드에 적용돼요. 다짐의 수면 구간은 다짐 편집에서 따로 정해요 — 새로 만드는 수면 다짐은 여기의 전체 구간으로 시작해요.")
}
}
.scrollContentBackground(.hidden)
.background(AppTheme.background)
.navigationTitle("수면 표시 구간")
.navigationBarTitleDisplayMode(.inline)
}
@ViewBuilder
private func weekdayGroup(_ weekday: Int) -> some View {
let binding = Binding<String>(
get: {
let raw = weekdaySpecs["\(weekday)"] ?? ""
return raw.isEmpty ? (globalRaw.isEmpty ? SleepWindowPrefs.defaultSpec : globalRaw) : raw
},
set: { newValue in
weekdaySpecs["\(weekday)"] = newValue
AppGroup.defaults.set(weekdaySpecs, forKey: LocalPrefsKeys.sleepWindowByWeekday)
}
)
DisclosureGroup {
SleepWindowEditorRows(spec: binding)
} label: {
HStack {
Text("\(Format.weekdayShort(weekday))요일")
Spacer()
Text(Self.summary(binding.wrappedValue))
.font(.caption)
.foregroundStyle(weekdaySpecs["\(weekday)"].map { $0.isEmpty } ?? true ? .secondary : AppTheme.green)
}
}
}
/// "1260-540" "21:00 ~ 09:00"
static func summary(_ spec: String) -> String {
let parts = spec.split(separator: "-").compactMap { Int(String($0)) }
guard parts.count == 2 else { return "" }
func hhmm(_ minutes: Int) -> String {
String(format: "%02d:%02d", minutes / 60, minutes % 60)
}
if parts[0] == parts[1] {
return String(localized: "\(hhmm(parts[1]))에 끝나는 24시간")
}
return parts[0] > parts[1]
? String(localized: "\(hhmm(parts[0])) ~ 다음 날 \(hhmm(parts[1]))")
: String(localized: "\(hhmm(parts[0])) ~ \(hhmm(parts[1]))")
}
}
/// ("-") ·
private struct SleepWindowEditorRows: View {
@Binding var spec: String
private func minutes(_ index: Int) -> Int {
let parts = spec.split(separator: "-").compactMap { Int(String($0)) }
guard parts.count == 2 else { return index == 0 ? 1260 : 540 }
return parts[index]
}
private func dateBinding(_ index: Int) -> Binding<Date> {
Binding(
get: {
Calendar.current.date(
byAdding: .minute, value: minutes(index),
to: Calendar.current.startOfDay(for: .now)
) ?? .now
},
set: { newValue in
let comps = Calendar.current.dateComponents([.hour, .minute], from: newValue)
let value = (comps.hour ?? 0) * 60 + (comps.minute ?? 0)
let start = index == 0 ? value : minutes(0)
let end = index == 1 ? value : minutes(1)
spec = "\(start)-\(end)"
}
)
}
var body: some View {
CollapsibleTimeWheel(
label: String(localized: "구간 시작"),
selection: dateBinding(0),
components: .hourAndMinute
)
CollapsibleTimeWheel(
label: String(localized: "구간 끝"),
selection: dateBinding(1),
components: .hourAndMinute
)
}
}

View File

@ -58,9 +58,9 @@ struct HelpView: View {
result.append(health)
}
result.append(sync)
if DeviceLayout.isPad {
result.append(diary)
} else {
// 1.5 ( §6.7)
result.append(diary)
if !DeviceLayout.isPad {
result.append(iphoneUI)
result.append(watch)
}
@ -302,7 +302,17 @@ struct HelpView: View {
HelpTopic(
symbol: "heart.fill",
title: "모음 탭에서 건강 데이터 보기",
body: "모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '모음 탭 건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 전날 밤부터 오늘 아침까지의 수면 구간을 통째로 '오늘 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요."
body: "모음 탭의 '건강 데이터' 줄에서 '애플 건강 연결하기'를 누르면 걸음 수·운동 시간·수면 같은 오늘 데이터를 타일로 볼 수 있어요. 어떤 지표를 어떤 순서로 보여줄지는 줄 오른쪽의 지표 버튼에서 고르고, 설정 → '건강 데이터'에서 줄 자체를 끄거나 즐겨찾기·나머지 행동과의 순서를 바꿀 수 있어요.\n값은 애플 건강 앱이 가진 데이터를 읽어 온 거예요 — 하루 다님이 서버로 보내거나 iCloud에 저장하는 일은 없어요. 걸음·운동 같은 값은 설정의 '하루 시작 시간' 기준으로 오늘 하루를 더한 것이고, 수면만은 정해 둔 수면 구간(기본 저녁 9시~다음 날 아침 9시)에서 잔 시간을 통째로 '그날 잔 것'으로 세요. 애플워치에 쌓인 데이터는 워치와 아이폰이 동기화된 뒤에 보이니 값이 안 맞아 보이면 잠시 뒤 다시 확인해 주세요. 값이 계속 0이라면 아이폰의 설정 → 개인정보 보호 및 보안 → 건강에서 하루 다님의 읽기 권한을 확인해 주세요."
),
HelpTopic(
symbol: "bed.double.fill",
title: "수면 표시 구간 맞추기",
body: "타일의 수면이 실제와 다르게 0으로 보인다면, 그 잠이 수면 구간 밖에 있었을 가능성이 커요 — 기본 구간(저녁 9시~다음 날 아침 9시)은 아침 9시 이후에 잔 잠을 세지 않거든요. 설정 → 건강 데이터 → '수면 표시 구간'에서 구간을 바꿔 보세요. 시작과 끝을 같은 시각으로 두면 그 시각에 끝나는 하루 전체(24시간)를 보고, '요일마다 다르게'를 켜면 야간 근무처럼 특정 요일만 낮에 자는 패턴도 요일별 구간으로 맞출 수 있어요.\n이 설정은 모음 탭 타일과 일기의 건강 카드에 적용돼요. 다짐의 수면 구간은 다짐 편집에서 따로 정하고, 새로 만드는 수면 다짐은 여기의 전체 구간으로 시작해요."
),
HelpTopic(
symbol: "heart.text.square.fill",
title: "일기에서 건강 데이터 보기",
body: "일기의 하루 정리 첫 페이지에 '건강 데이터' 카드가 있어요 — 그날의 걸음·운동·수면 같은 값을 일기와 함께 봐요. 카드 오른쪽 위 버튼으로 보여줄 지표를 고르면 모든 날짜에 함께 적용돼요. 타임테이블에는 수면·운동이 실제 시간 그대로 옅은 블록으로 깔려요(수면 남보라·운동 주황) — 타임테이블 필터의 '건강 데이터' 토글로 켜고 끌 수 있어요.\n건강 값은 기기 안에만 있어서 카드의 값도 기기마다 다를 수 있어요 — 아이패드는 시스템의 건강 iCloud 동기화를 켠 경우에만 아이폰과 같은 값이 보이고, 맥에서는 카드와 블록이 보이지 않아요."
),
HelpTopic(
symbol: "figure.run",
@ -329,25 +339,25 @@ struct HelpView: View {
])
}
// MARK: iPad
// MARK: (1.5 )
private var diary: HelpGroup {
HelpGroup(title: "일기 (아이패드·맥 · 프리미엄)", topics: [
HelpGroup(title: "일기 (프리미엄)", topics: [
HelpTopic(
symbol: "book.closed.fill",
title: "하루 정리 첫 페이지",
body: "일기 탭 달력에서 날짜를 누르면 그날의 일기가 열려요. 첫 페이지에는 오늘 기분·요약·목표 진행 현황·할 일·24시간 타임테이블·기록이 모여 하루가 한눈에 정리돼요. 오른쪽 위 ⋯ 메뉴의 '첫 화면 구성'에서 섹션을 보일지 말지와 순서를 바꿀 수 있어요. 기분 사진을 남긴 날은 달력의 그 칸에 사진이 배경으로 깔려요. 일기는 아이패드와 맥에서 쓸 수 있고, iCloud 동기화를 켜면 두 기기에서 같은 일기가 이어져요.",
body: "일기 탭 달력에서 날짜를 누르면 그날의 일기가 열려요. 첫 페이지에는 오늘 기분·요약·목표 진행 현황·할 일·24시간 타임테이블·기록·건강 데터가 모여 하루가 한눈에 정리돼요. 오른쪽 위 ⋯ 메뉴의 '첫 화면 구성'에서 섹션을 보일지 말지와 순서를 바꿀 수 있어요. 기분 사진을 남긴 날은 달력의 그 칸에 사진이 배경으로 깔려요. 일기는 아이폰·아이패드·맥에서 쓸 수 있고, iCloud 동기화를 켜면 어느 기기에서든 같은 일기가 이어져요.",
imageName: "help-diary"
),
HelpTopic(
symbol: "calendar.badge.plus",
title: "타임테이블에 일정·필터",
body: "첫 페이지 타임테이블 카드의 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 보여줄 수 있어요 — 일정은 점선 테두리라 행동과 바로 구분돼요. 필터 버튼으로는 그날 기록된 행동 중 타임테이블에 올릴 것만 고르고, '이 날의 목표' 카드의 필터로는 보고 싶은 목표만 남겨요. 이 선택들은 날짜마다 따로 저장되고, 타임테이블 선택은 일기를 내보낼 때도 그대로 담겨요.\n매일 같은 선택을 되풀이하기 싫다면 달력 화면 오른쪽 위의 '기본 표시 설정'을 써 보세요. 캘린더 일정을 기본으로 넣을지(모든 캘린더/선택한 캘린더만), 어떤 목표·행동을 기본으로 숨길지 한 번만 정하면 모든 날짜에 적용돼요. 날짜별 필터로 다르게 고른 날은 그 선택이 우선하고, '기본값으로 되돌리기'로 언제든 돌아갈 수 있어요."
body: "첫 페이지 타임테이블 카드의 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 보여줄 수 있어요 — 일정은 점선 테두리라 행동과 바로 구분돼요. 필터 버튼으로는 그날 기록된 행동 중 타임테이블에 올릴 것만 고르고, '이 날의 목표' 카드의 필터로는 보고 싶은 목표만 남겨요. 이 선택들은 날짜마다 따로 저장되고, 타임테이블 선택은 일기를 내보낼 때도 그대로 담겨요.\n애플 건강의 수면·운동 시간도 실제 잔 시간·운동한 시간 그대로 옅은 블록으로 깔려요 — 수면은 남보라, 운동은 주황이에요. 필터의 '건강 데이터' 토글로 켜고 끌 수 있고(모든 날짜 공통), 건강 데이터가 없는 맥에서는 보이지 않아요.\n매일 같은 선택을 되풀이하기 싫다면 달력 화면 오른쪽 위의 '기본 표시 설정'을 써 보세요. 캘린더 일정을 기본으로 넣을지(모든 캘린더/선택한 캘린더만), 어떤 목표·행동을 기본으로 숨길지 한 번만 정하면 모든 날짜에 적용돼요. 날짜별 필터로 다르게 고른 날은 그 선택이 우선하고, '기본값으로 되돌리기'로 언제든 돌아갈 수 있어요."
),
HelpTopic(
symbol: "applepencil",
title: "펜슬 노트 페이지",
body: "페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 마음껏 쓰고 그리세요 — 손가락은 페이지 넘기기와 확대·축소를 맡아요. 두 손가락으로 벌려 확대하면 작은 글씨도 또박또박 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 차례를 바꿔요. 맥에서는 키보드 입력이 기본이고, 도구 줄의 '그리기'를 켠 동안에만 마우스·트랙패드로 그려져요 — 평소의 클릭과 스크롤은 이동과 버튼에 쓰여요."
body: "페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 마음껏 쓰고 그리세요 — 손가락은 페이지 넘기기와 확대·축소를 맡아요. 두 손가락으로 벌려 확대하면 작은 글씨도 또박또박 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 차례를 바꿔요.\n아이폰에서는 도구 줄의 '그리기'를 켠 동안 손가락으로 그려지고, 끄면 손가락이 다시 페이지 넘기기·확대에 쓰여요. 맥에서는 키보드 입력이 기본이고, 마찬가지로 '그리기'를 켠 동안에만 마우스·트랙패드로 그려져요."
),
HelpTopic(
symbol: "square.on.circle",
@ -440,7 +450,7 @@ struct HelpView: View {
HelpTopic(
symbol: "crown.fill",
title: "무료와 프리미엄의 차이",
body: "무료로는 행동 10개·목표 2개·목표당 다짐 3개까지 만들 수 있어요. 프리미엄에서는 개수 제한이 풀리고 위젯, 애플워치 앱, 시리 단축어, 기기 간 동기화, 아이패드·맥 일기, CSV 가져오기, 건강 다짐을 쓸 수 있어요. 월·년 구독이나 한 번만 결제하는 평생 이용권 중에 고르면 돼요. 구독이 끝나도 기록한 데이터는 그대로 남고, 이미 만든 건강 다짐도 계속 보이고, 쓴 일기도 계속 열람할 수 있어요."
body: "무료로는 행동 10개·목표 2개·목표당 다짐 3개까지 만들 수 있어요. 프리미엄에서는 개수 제한이 풀리고 위젯, 애플워치 앱, 시리 단축어, 기기 간 동기화, 일기(아이폰·아이패드·맥), CSV 가져오기, 건강 다짐을 쓸 수 있어요. 월·년 구독이나 한 번만 결제하는 평생 이용권 중에 고르면 돼요. 구독이 끝나도 기록한 데이터는 그대로 남고, 이미 만든 건강 다짐도 계속 보이고, 쓴 일기도 계속 열람할 수 있어요."
),
HelpTopic(
symbol: "mic.fill",

View File

@ -46,13 +46,23 @@ struct QuestEditorView: View {
/// raw HealthMetric raw "workout"( )
@State private var healthChoiceRaw: String = HealthMetric.steps.rawValue
@State private var healthWorkoutKind: HealthWorkoutKind = .running
/// ( )
@State private var sleepStart: Date = Calendar.current.date(
bySettingHour: 21, minute: 0, second: 0, of: .now
) ?? .now
@State private var sleepEnd: Date = Calendar.current.date(
bySettingHour: 9, minute: 0, second: 0, of: .now
) ?? .now
/// ( ).
/// ' ' (1.5
/// load() )
@State private var sleepStart: Date = {
let minutes = SleepWindowPrefs.globalSpec.split(separator: "-").compactMap { Int(String($0)) }
return Calendar.current.date(
byAdding: .minute, value: minutes.first ?? 1260,
to: Calendar.current.startOfDay(for: .now)
) ?? .now
}()
@State private var sleepEnd: Date = {
let minutes = SleepWindowPrefs.globalSpec.split(separator: "-").compactMap { Int(String($0)) }
return Calendar.current.date(
byAdding: .minute, value: minutes.count == 2 ? minutes[1] : 540,
to: Calendar.current.startOfDay(for: .now)
) ?? .now
}()
/// ( / kcal / )
@State private var healthIntTarget = 8000
/// (km m)

View File

@ -17,10 +17,27 @@ struct RecordFilterSheet: View {
let tags: [Tag]
let actions: [Action]
@Binding var excludedActionIDs: Set<PersistentIdentifier>
/// (1.5): · .
/// ( ) . · false
var showsHealthSection: Bool = false
/// : .
/// nil(· )
var onResetToDefault: (() -> Void)? = nil
/// ("sleep"/"workout" ) ,
@AppStorage(DiaryHealthTimetable.hiddenKey) private var healthHiddenRaw = ""
private func healthVisibleBinding(_ key: String) -> Binding<Bool> {
Binding(
get: { !healthHiddenRaw.split(separator: ",").map(String.init).contains(key) },
set: { visible in
var hidden = Set(healthHiddenRaw.split(separator: ",").map(String.init))
if visible { hidden.remove(key) } else { hidden.insert(key) }
healthHiddenRaw = hidden.sorted().joined(separator: ",")
}
)
}
private var allActionIDs: Set<PersistentIdentifier> {
Set(actions.map(\.persistentModelID))
}
@ -60,6 +77,45 @@ struct RecordFilterSheet: View {
}
}
}
if showsHealthSection {
Section {
if HealthDataStore.isAvailable {
Toggle(isOn: healthVisibleBinding("sleep")) {
Label {
Text("수면")
} icon: {
Image(systemName: HealthMetric.sleep.symbolName)
.foregroundStyle(DiaryHealthTimetable.sleepColor)
}
}
.tint(AppTheme.green)
Toggle(isOn: healthVisibleBinding("workout")) {
Label {
Text("운동")
} icon: {
Image(systemName: "figure.run")
.foregroundStyle(DiaryHealthTimetable.workoutColor)
}
}
.tint(AppTheme.green)
} else {
Label {
Text("맥에서는 애플 건강 데이터를 지원하지 않아 수면·운동을 타임테이블에 표시할 수 없어요. 아이폰·아이패드에서 확인해 주세요.")
.font(.footnote)
.foregroundStyle(.secondary)
} icon: {
Image(systemName: "heart.slash")
.foregroundStyle(.secondary)
}
}
} header: {
Text("건강 데이터")
} footer: {
if HealthDataStore.isAvailable {
Text("애플 건강의 수면·운동 시간을 실제 구간 그대로 타임테이블에 보여줘요. 이 설정은 모든 날짜에 함께 적용돼요.")
}
}
}
if let onResetToDefault {
Section {
Button("기본값으로 되돌리기") {

View File

@ -199,10 +199,23 @@ struct SettingsView: View {
}
}
}
// 1.5 : (· · )
NavigationLink {
SleepWindowSettingsView()
} label: {
HStack {
Text("수면 표시 구간")
Spacer()
Text(SleepWindowSettingsView.summary(SleepWindowPrefs.globalSpec))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
} header: {
Text("모음 탭 건강 데이터")
Text("건강 데이터")
} footer: {
Text("켜면 애플 건강 앱의 오늘 데이터(걸음·운동·수면 등)를 모음 탭에서 타일로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요.")
Text("켜면 애플 건강 앱의 오늘 데이터(걸음·운동·수면 등)를 모음 탭에서 타일로 볼 수 있어요. 섹션 표시 순서에서 즐겨찾기·나머지 행동·건강 데이터의 배치를 정할 수 있어요. 수면 표시 구간은 어느 시간대의 잠을 '그날 잔 것'으로 볼지 정해요 — 밤낮이 바뀌는 날이 있어도 맞출 수 있어요.")
}
.id("healthSection")
}
@ -261,22 +274,20 @@ struct SettingsView: View {
} footer: {
Text("여러 행동을 동시에 추적 중일 때 다이나믹 아일랜드·잠금화면과 워치 컴플리케이션에 대표로 표시할 행동이에요.")
}
// · (§6.7)
if DeviceLayout.isPad {
Section {
Toggle(isOn: diaryLockBinding) {
Label {
Text("일기 잠금")
} icon: {
Image(systemName: "lock.fill")
.foregroundStyle(AppTheme.green)
}
// 1.5 (§6.7)
Section {
Toggle(isOn: diaryLockBinding) {
Label {
Text("일기 잠금")
} icon: {
Image(systemName: "lock.fill")
.foregroundStyle(AppTheme.green)
}
} header: {
Text("일기")
} footer: {
Text("켜면 일기 탭을 열 때 Face ID·Touch ID 또는 기기 암호로 잠금을 해제해야 해요. 기기가 지원하는 방식이 자동으로 쓰이고, 앱을 나갔다 오면 다시 잠겨요. 이 설정은 이 기기에만 적용돼요.")
}
} header: {
Text("일기")
} footer: {
Text("켜면 일기 탭을 열 때 Face ID·Touch ID 또는 기기 암호로 잠금을 해제해야 해요. 기기가 지원하는 방식이 자동으로 쓰이고, 앱을 나갔다 오면 다시 잠겨요. 이 설정은 이 기기에만 적용돼요.")
}
Section("프리미엄") {
NavigationLink {
@ -521,7 +532,7 @@ struct PremiumView: View {
featureRow("switch.2", "제어 센터에서 실행", "제어 센터·잠금화면 버튼·액션 버튼에서 행동을 바로 실행해요.")
featureRow("icloud.fill", "기기 간 동기화", "iCloud로 아이폰·아이패드·맥의 데이터가 동기화돼요.")
featureRow("applewatch", "애플워치 앱", "워치에서 추적하고 컴플리케이션으로 확인해요.")
featureRow("book.closed.fill", "아이패드·맥 일기", "하루 정리와 애플 펜슬 다이어리 — 맥에서는 마우스·키보드로 써요.")
featureRow("book.closed.fill", "일기 (아이폰·아이패드·맥)", "하루 정리와 다이어리 — 아이패드는 애플 펜슬, 아이폰은 손가락 그리기, 맥은 마우스·키보드로 써요.")
featureRow("square.and.arrow.down", "CSV 가져오기", "내보냈던 CSV에서 행동과 기록을 복원해요.")
featureRow("mic.fill", "시리 단축어", "\"시리야, 하루 다님에서 측정 시작\" — 말과 단축어 자동화로 기록해요.")
} header: {

View File

@ -15,6 +15,8 @@ import UIKit
/// iPhone( ) UI .
enum DeviceLayout {
static let isPad = UIDevice.current.userInterfaceIdiom == .pad
/// (1.5 '' · )
static let isPhone = UIDevice.current.userInterfaceIdiom == .phone
/// (Designed for iPad) .
/// ,
/// · / (· )

View File

@ -76,6 +76,18 @@ struct WhatsNewView: View {
title: String(localized: "건강 데이터로 다짐 만들기"),
body: String(localized: "'매일 8,000보', '수면 8시간', '달리기 30분'처럼 건강 데이터를 목표로 삼는 다짐을 만들 수 있어요. 운동은 종목별로도 돼요. (프리미엄)")
)
feature(
symbol: "book.closed.fill",
tint: AppTheme.green,
title: String(localized: "아이폰에서도 일기 쓰기"),
body: String(localized: "일기 탭이 아이폰에도 생겼어요. 하루 정리를 확인하고, 노트에서 '그리기'를 켜면 손가락으로 쓰고 그릴 수 있어요. (프리미엄)")
)
feature(
symbol: "heart.text.square.fill",
tint: .pink,
title: String(localized: "일기에서 건강 데이터 보기"),
body: String(localized: "하루 정리에 그날의 건강 값을 담는 카드가 생기고, 타임테이블에는 수면·운동이 실제 시간 그대로 깔려요. 낮에 자는 날이 있다면 설정의 '수면 표시 구간'을 요일별로 맞춰 보세요.")
)
feature(
symbol: "questionmark.circle.fill",
tint: AppTheme.yellow,

View File

@ -29,7 +29,7 @@ A habit and time tracker: log time and counts with a single tap, and shape your
• Apple Watch app and complications — track from your wrist, with an Action Log complication showing totals on your watch face
• Siri Shortcuts — "Hey Siri, start tracking Reading in Haru Danim"
• iCloud sync across iPhone, iPad and Mac
• Diary on iPad and Mac — a daily summary page plus free-form Apple Pencil notes (mouse and keyboard on Mac), with your own PDF page templates
• Diary on iPhone, iPad and Mac — a daily summary page plus free-form notes (Apple Pencil on iPad, finger drawing on iPhone, mouse and keyboard on Mac), with your own PDF page templates. The summary now includes a health-data card and a timetable with your sleep and workouts laid in
• CSV import — bring actions and records back from your exported CSVs
• Health quests — goals from Apple Health data like steps, sleep and workout types ("8,000 steps a day," "8 hours of sleep")

View File

@ -29,7 +29,7 @@ Haru Danim — あなたの一日に立ち寄る
• Apple Watchアプリとコンプリケーション — 手首で記録し、「アクションの記録」でフェイスに累計まで
• Siriショートカット — 「Hey Siri、Haru Danimで読書の計測を開始」
• iCloudでiPhone・iPad・Macのデータを同期
iPad・Macの日記 — 一日のまとめページとApple Pencilの自由ートMacではマウス・キーボード、自分のPDF用紙テンプレート
日記iPhone・iPad・Mac— 一日のまとめページと自由ートiPadはApple Pencil、iPhoneは指で描く、Macはマウス・キーボード、自分のPDF用紙テンプレート。まとめにはその日のヘルス値カードと、睡眠・運動が敷かれたタイムテーブルも
• CSV読み込み — 書き出したCSVからアクションと記録を復元
• ヘルスクエスト — 歩数・睡眠・運動種目などAppleヘルスケアのデータを目標に「毎日8,000歩」「睡眠8時間」

View File

@ -29,7 +29,7 @@
• 애플워치 앱과 컴플리케이션 — 손목에서 기록하고, '행동 기록' 컴플리케이션으로 페이스에서 누적까지
• 시리 단축어 — "시리야, 하루 다님에서 독서 측정 시작"
• iCloud로 아이폰·아이패드·맥 데이터 동기화
아이패드·맥 일기 — 하루 정리 요약과 애플 펜슬 자유 노트(맥은 마우스·키보드), 나만의 PDF 양식(속지)
일기 (아이폰·아이패드·맥) — 하루 정리 요약과 자유 노트(아이패드는 애플 펜슬, 아이폰은 손가락 그리기, 맥은 마우스·키보드), 나만의 PDF 양식(속지). 하루 정리에는 그날의 건강 값 카드와 수면·운동이 깔린 타임테이블까지
• CSV 가져오기 — 내보냈던 CSV에서 행동과 기록을 다시 불러와 복원
• 건강 다짐 — 걸음·수면·운동 종목 같은 애플 건강 데이터를 목표로 ('매일 8,000보', '수면 8시간')

View File

@ -1,12 +1,17 @@
# 버전 1.5 '새로운 기능' (로케일별)
[ko]
이번 업데이트는 애플 건강, 그리고 애플워치와 더 가까워졌어요.
이번 업데이트는 애플 건강, 그리고 애플워치와 더 가까워졌어요. 일기는 이제 아이폰에서도 열려요.
● 애플 건강과 연결
• 모음 탭에서 걸음·운동·수면 같은 오늘의 건강 데이터를 타일로 볼 수 있어요 — 값은 기기 밖으로 나가지 않아요
• 건강 데이터로 다짐을 만들 수 있어요(프리미엄) — '매일 8,000보', '수면 8시간', '달리기 30분'처럼요. 운동은 종목별로도 돼요
• 수면은 '수면 구간'을 정해 두면 자정을 넘겨 자도 하루가 쪼개지지 않아요
• 수면은 '수면 구간'을 정해 두면 자정을 넘겨 자도 하루가 쪼개지지 않아요. 낮에 자는 날이 있다면 '수면 표시 구간'을 요일별로 맞출 수도 있어요
● 일기가 더 넓어졌어요 (프리미엄)
• 일기 탭이 아이폰에도 생겼어요 — 하루 정리를 확인하고, '그리기'를 켜면 손가락으로 노트에 쓸 수 있어요
• 하루 정리에 그날의 건강 값을 담는 '건강 데이터' 카드가 생겼어요
• 타임테이블에 수면·운동이 실제 시간 그대로 깔려요 — 하루가 정말 한눈에 정리돼요
● 애플워치 '행동 기록' 컴플리케이션
• 행동의 누적 기록(하루·이번 주·이번 달·지난 7일·지난 30일)을 워치 페이스에서 바로 봐요
@ -17,12 +22,17 @@
• 여러 기기를 쓰신다면 모든 기기에서 함께 업데이트해 주세요
[en]
This update brings Haru Danim closer to Apple Health — and your Apple Watch.
This update brings Haru Danim closer to Apple Health — and your Apple Watch. And the Diary now opens on iPhone, too.
● Apple Health, connected
• 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, too
• Set a sleep window and sleeping past midnight won't split your night
• 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
● 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 daily summary gains a Health Data card with that day's values
• Sleep and workouts now appear on the timetable at their actual times — your whole day, truly at a glance
● Action Log complication for Apple Watch
• See an action's running total (day, this week, this month, last 7/30 days) right on your watch face
@ -33,12 +43,17 @@ This update brings Haru Danim closer to Apple Health — and your Apple Watch.
• Using several devices? Please update Haru Danim on all of them
[ja]
今回のアップデートで、Appleヘルスケア、そしてApple Watchともっと近くなりました。
今回のアップデートで、Appleヘルスケア、そしてApple Watchともっと近くなりました。日記はiPhoneでも開けるようになりました。
● Appleヘルスケアと連携
• ホームタブで歩数・運動・睡眠など今日のヘルスデータをタイルで見られます — 値が端末の外に出ることはありません
• ヘルスデータでクエストを作れますプレミアム—「毎日8,000歩」「睡眠8時間」「ランニング30分」など。運動は種目別でもOK
• 「睡眠時間帯」を決めておけば、日付をまたいで眠っても1日が分割されません
• 「睡眠時間帯」を決めておけば、日付をまたいで眠っても1日が分割されません。昼間に眠る日があるなら、表示の時間帯を曜日ごとに調整できます
● 日記がもっと広く(プレミアム)
• 日記タブがiPhoneにも登場 — 1日のまとめを確認し、「描く」をオンにすれば指でートに書けます
• 1日のまとめに、その日のヘルス値をまとめる「ヘルスデータ」カードが加わりました
• タイムテーブルに睡眠・運動が実際の時間どおりに表示されます — 1日が本当にひと目でわかります
● Apple Watch「アクションの記録」コンプリケーション
• アクションの累計1日・今週・今月・過去7日・過去30日をウォッチフェイスで直接確認

View File

@ -75,7 +75,7 @@ FormatStyle에 반영되지 않는다. 로케일마다 첫 시드 실행에 `-ma
# 06-bubble : -navStyle radial -radialExpanded YES
# 08-dark : 기본 실행 후 `simctl ui <sim> appearance dark` → 촬영 → light 복귀
# 09-records : -startTab history -historyDaysAgo 1 (어제 목록 — 메모 노랑 표시)
# 10-week : (벌크 시드 후) -startTab history -historyMode timetable -historyWeekly YES -historyDaysAgo 7
# 10-diary : 1.5(4)부터 — 시드에 -diarySeed 추가 후 -startTab diary -diaryOpenToday YES -seedHealthCache YES -diary.sectionOrder "mood,health,timetable" (구 10-week[주간 타임테이블]는 폐기)
# 아이패드: 01/10-goals/09-dark 동일 요령. 일기는 첫 실행에
# -startTab diary -diarySeed YES -diarySeedPhoto YES -diarySeedTemplate YES 로 시드 후:
# 02: -startTab diary 03: +-diaryOpenToday 04: +-diaryPage 1 08: +-diaryApplyTemplate
@ -103,7 +103,9 @@ MW=0A2CB37C-A755-4784-9D14-C7AB8FD72C3E # 'MarketingWatch' — 비페어링 Se
> ✅ **2026-07-28: 아래 절차 전부 완료 — 버전 1.0 App Store 출시됨.** 이 체크리스트는 절차 기록으로 유지.
> 📸 **2026-08-22: 1.5용 스크린샷 전면 재촬영 — 기존 세트 폐기, 아이폰 10·아이패드 10·워치 1 × ko/en/ja (원본 57장+합성 63장).**
> 구성 변경: 아이폰 09-records → **09-health**(건강 다짐 목표 탭 — `-seedHealthQuest`+`-seedHealthCache`, 데모 시드 없이 단독. ⚠️행동이 없어 온보딩이 뜨므로 촬영 전 `defaults write com.yechan.HaruDanim onboarding.done -bool YES` 필요). 01-main은 건강 타일이 보이게 `-mainSectionOrder "favorites,health,others"` 표시 오버라이드 사용.
> 시뮬레이터 리셋(2026-08-22)으로 마케팅 심 재생성: iPhone 17 Pro Max(26.5) `0255B56E-A9B8-44DE-9C0B-8570BB0C6315`, iPad Pro 13"(26.5) `9A8DE7A7-D7B1-4C95-92FE-0AA4B5D3034E`, MarketingWatch(26.2, 비페어링) `071FD7ED-9047-498D-B6BA-73A898FDE03A`. 시간대는 Pacific/Auckland 부팅 주입(오전 9시대).
> 시뮬레이터 리셋(2026-08-22)으로 마케팅 심 재생성: iPhone 17 Pro Max(26.5) `0255B56E-A9B8-44DE-9C0B-8570BB0C6315`, iPad Pro 13"(26.5) `9A8DE7A7-D7B1-4C95-92FE-0AA4B5D3034E`, MarketingWatch(26.2, 비페어링) `071FD7ED-9047-498D-B6BA-73A898FDE03A`. 시간대는 Pacific/Auckland 부팅 주입(오전 9시대 — **KST 저녁 촬영이면 America/Argentina/Buenos_Aires(UTC-3)가 오전 9시대**, 1.5(4) 실측).
>
> 📸 **2026-08-22 저녁, 1.5(4) 추가분 3장 갱신**: ①아이폰 `06-bubble` 재촬영(일기 개방으로 버블 8개) ②아이폰 `10-week`**`10-diary` 교체**(아이폰 일기 — 인자: 시드 `-seedDemo -diarySeed -seedHealthCache``-startTab diary -diaryOpenToday -seedHealthCache -diary.sectionOrder "mood,health,timetable"`) ③아이패드 `03-diary-summary` 재촬영(건강 카드+타임테이블 수면·운동 블록 — 같은 요령, sectionOrder `"mood,health,timetable,hero,goals"`). 합성 문구는 frame_screenshots.swift에 반영(10-diary 신설·03/02 서브라인 갱신).
> ⚠️촬영 실측 주의 3가지: ①`simctl io screenshot`**기존 파일을 덮어쓰지 않음** — 재촬영 전 rm 필수 ②워치 절차의 그룹 컨테이너 경로는 **부팅 중에만 조회 가능**하고 아이폰 앱 재설치마다 페어링 워치 쪽 경로가 바뀜 — 로케일마다 재조회(셧다운 후 조회하면 빈 값→엉뚱한 cp) ③아이폰 언인스톨→워치 앱 삭제 전파가 늦어 직후 설치가 지워질 수 있음 — 언인스톨 후 8초 대기.
>
> 📸 **2026-07-29: 1.1용 스크린샷 전면 개편 — 아이폰 10장·아이패드 10장·워치 합성 1장 × ko/en/ja (전부 재촬영·재합성).**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 999 KiB

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 625 KiB

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 993 KiB

After

Width:  |  Height:  |  Size: 1007 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 605 KiB

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 942 KiB

After

Width:  |  Height:  |  Size: 962 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 835 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

After

Width:  |  Height:  |  Size: 568 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 581 KiB

After

Width:  |  Height:  |  Size: 595 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 512 KiB

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

View File

@ -53,9 +53,10 @@ let iphoneCopies: [String: Copy] = [
"08-dark": Copy(
headline: ["ko": "밤에는 다크 모드로 편안하게", "en": "Easy on the eyes in Dark Mode", "ja": "夜はダークモードで心地よく"],
subline: ["ko": "라이트·다크·시스템 설정 따름 중 선택", "en": "Light, dark, or match the system", "ja": "ライト・ダーク・システム連動から選択"]),
"10-week": Copy(
headline: ["ko": "일주일의 흐름이 한눈에", "en": "Your week at a glance", "ja": "一週間の流れをひと目で"],
subline: ["ko": "24시간 축 위에 그려지는 주간 타임테이블", "en": "Seven days drawn on a 24-hour timetable", "ja": "24時間軸に描かれる週間タイムテーブル"]),
// 1.5(4): 10-week 10-diary
"10-diary": Copy(
headline: ["ko": "일기가 이제 아이폰에도", "en": "The Diary, now on iPhone", "ja": "日記がiPhoneにも"],
subline: ["ko": "하루 정리와 건강 데이터, 손가락으로 쓰는 노트", "en": "Daily summary, health data and finger-drawn notes", "ja": "1日のまとめとヘルスデータ、指で書くート"]),
]
let ipadCopies: [String: Copy] = [
@ -64,10 +65,10 @@ let ipadCopies: [String: Copy] = [
subline: ["ko": "사이드바와 넓은 그리드로 한 화면에", "en": "Sidebar navigation and a wide grid", "ja": "サイドバーと広いグリッドで一画面に"]),
"02-diary-calendar": Copy(
headline: ["ko": "하루를 남기는 일기", "en": "A diary for every day", "ja": "毎日を残す日記"],
subline: ["ko": "기분 이모지와 사진으로 채워지는 달력 — 맥에서도", "en": "A calendar of moods and photos — on Mac too", "ja": "気分の絵文字と写真で埋まるカレンダー — Macでも"]),
subline: ["ko": "기분 이모지와 사진으로 채워지는 달력 — 아이폰·맥에서도", "en": "A calendar of moods and photos — on iPhone and Mac too", "ja": "気分の絵文字と写真で埋まるカレンダー — iPhone・Macでも"]),
"03-diary-summary": Copy(
headline: ["ko": "하루 정리를 한 페이지에", "en": "Your whole day on one page", "ja": "一日のまとめを1ページに"],
subline: ["ko": "기록·목표·할 일·타임테이블까지", "en": "Records, goals, to-dos and a timetable", "ja": "記録・目標・やること・タイムテーブル"]),
subline: ["ko": "기록·목표·건강 데이터·타임테이블까지", "en": "Records, goals, health data and a timetable", "ja": "記録・目標・ヘルスデータ・タイムテーブル"]),
"04-diary-note": Copy(
headline: ["ko": "애플 펜슬로 자유롭게", "en": "Free-form with Apple Pencil", "ja": "Apple Pencilで自由に"],
subline: ["ko": "필기·사진·도형·텍스트 — 맥에서는 마우스와 키보드로", "en": "Ink, photos, shapes and text — mouse and keyboard on Mac", "ja": "手書き・写真・図形・テキスト — Macではマウスとキーボードで"]),

View File

@ -265,6 +265,69 @@ nonisolated enum HealthCache {
}
/// ( )
static func clearAll() {
AppGroup.defaults.removeObject(forKey: storageKey)
HealthIntervals.clearAll()
}
}
// MARK: - (1.5 · )
/// '' HealthCache , .
/// : "sleep" "workout.<kind>", : dayToken [[ epoch, epoch], ...].
///
/// ( ' ' , ).
/// HealthCache defaults CloudKit (5.1.3)
nonisolated enum HealthIntervals {
static let storageKey = "health.intervals.v1"
static let retentionDays = HealthCache.retentionDays
static let sleepKey = "sleep"
static func workoutKey(_ kind: HealthWorkoutKind) -> String { "workout.\(kind.rawValue)" }
private static func load() -> [String: [String: [[Double]]]] {
(AppGroup.defaults.dictionary(forKey: storageKey) as? [String: [String: [[Double]]]]) ?? [:]
}
/// · ( )
static func intervals(_ key: String, dayKey: Date) -> [(start: Date, end: Date)] {
let stored = load()[key]?[HealthCache.dayToken(dayKey)] ?? []
return stored.compactMap { pair in
guard pair.count == 2, pair[1] > pair[0] else { return nil }
return (Date(timeIntervalSince1970: pair[0]), Date(timeIntervalSince1970: pair[1]))
}.sorted { $0.start < $1.start }
}
/// ( )
static func workoutKeys(dayKey: Date) -> [String] {
let token = HealthCache.dayToken(dayKey)
return load().keys.filter { $0.hasPrefix("workout.") && !(load()[$0]?[token] ?? []).isEmpty }
}
/// × (· ).
/// values: [ : [dayKey: [[ epoch, epoch]]]] " "
static func merge(_ values: [String: [Date: [[Double]]]]) {
guard !values.isEmpty else { return }
var all = load()
for (key, days) in values {
var stored = all[key] ?? [:]
for (dayKey, list) in days {
let token = HealthCache.dayToken(dayKey)
if list.isEmpty {
stored.removeValue(forKey: token)
} else {
stored[token] = list
}
}
if stored.count > retentionDays {
let sorted = stored.keys.sorted(by: >)
for old in sorted.dropFirst(retentionDays) { stored.removeValue(forKey: old) }
}
all[key] = stored
}
AppGroup.defaults.set(all, forKey: storageKey)
}
static func clearAll() {
AppGroup.defaults.removeObject(forKey: storageKey)
}

View File

@ -46,6 +46,61 @@ nonisolated enum LocalPrefsKeys {
/// HealthMetric raw ( , 1.5).
/// : ··
static let healthMetrics = "local.healthMetrics"
/// (· ) "-" , =(1260-540) (1.5)
static let sleepWindowGlobal = "local.sleepWindowGlobal"
/// Bool (1.5)
static let sleepWindowWeekdayEnabled = "local.sleepWindowWeekdayEnabled"
/// ["1"()~"7"(): ] . (1.5)
static let sleepWindowByWeekday = "local.sleepWindowByWeekday"
}
// MARK: - (1.5 , · )
/// ' '(
/// ) . .
/// (App Group defaults) CloudKit .
nonisolated enum SleepWindowPrefs {
static let defaultSpec = "1260-540"
/// ( )
static var globalSpec: String {
let raw = AppGroup.defaults.string(forKey: LocalPrefsKeys.sleepWindowGlobal) ?? ""
return raw.isEmpty ? defaultSpec : raw
}
static var weekdayEnabled: Bool {
AppGroup.defaults.bool(forKey: LocalPrefsKeys.sleepWindowWeekdayEnabled)
}
static var weekdaySpecs: [String: String] {
(AppGroup.defaults.dictionary(forKey: LocalPrefsKeys.sleepWindowByWeekday) as? [String: String]) ?? [:]
}
/// (1= ~ 7=)
static func spec(forWeekday weekday: Int) -> String {
guard weekdayEnabled, let raw = weekdaySpecs["\(weekday)"], !raw.isEmpty else { return globalSpec }
return raw
}
/// ( )
static func spec(forDayKey dayKey: Date) -> String {
spec(forWeekday: Calendar.current.component(.weekday, from: dayKey))
}
/// ( + )
static var allSpecs: Set<String> {
var specs: Set<String> = [globalSpec]
if weekdayEnabled {
for raw in weekdaySpecs.values where !raw.isEmpty { specs.insert(raw) }
}
return specs
}
/// ,
/// (: ) "sleep"
static var mappingSignature: String {
(1...7).map { spec(forWeekday: $0) }.joined(separator: "/")
}
}
/// 3 (1.5 · , )