feat(1.5-p9): 건강 다짐 표시 색 — Quest.healthColorHex(기본 분홍=빈 값), 편집기 색 선택, 빌드 7
- targetColor 건강 분기: 빈 값=시스템 분홍(다크 적응), 지정 시 hex — 단일 지점이라 목표 탭·모음·일기·위젯·워치 자동 반영 - 편집기 건강 섹션 '다짐 색'(프리셋 12+피커), 저장 시 기본색은 ''로·행동/꼬리표 전환 시 '' 정규화 - 타임테이블 블록 색과 별개, 시드 걸음 다짐에 #3E7CB1(대비 렌더 검증+스키마 내보내기 트리거) - QA: 일기 목표 카드에서 걸음=파랑·나머지=분홍 대비 실측, 편집기 렌더, 61건 ALL PASS, 3종 빌드 그린, l10n 0/0 - ⚠️ CloudKit 스키마 추가 — 콘솔 Deploy가 빌드 7 설치보다 선행 필수(CLAUDE.md·plan §12 후속 3) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WtZwRRjbtM9pZJDM4FkTq6
This commit is contained in:
parent
ac68fddb14
commit
8b1236d6a9
File diff suppressed because one or more lines are too long
@ -182,3 +182,10 @@
|
|||||||
|
|
||||||
- **실기기 실측(사용자)**: 버블 메뉴 펼침 상태에서 각 원이 이웃 버블 방향으로 늘어나 찌그러짐("서로 인력을 가진 느낌"). 원인: 일기 버블 추가로 8개가 되며 이웃 가장자리 간격이 ~9.6pt로 줄었는데, 유리 합체 판정 거리 `glassBlendSpacing`이 7개 시절 기준 18pt 그대로라 **정지 상태에서도 이웃끼리 합체 판정에 걸림**(1.4까지 7개는 간격 ~20.7pt로 판정 밖 — "1.3까지는 문제없었다"는 증언과 일치).
|
- **실기기 실측(사용자)**: 버블 메뉴 펼침 상태에서 각 원이 이웃 버블 방향으로 늘어나 찌그러짐("서로 인력을 가진 느낌"). 원인: 일기 버블 추가로 8개가 되며 이웃 가장자리 간격이 ~9.6pt로 줄었는데, 유리 합체 판정 거리 `glassBlendSpacing`이 7개 시절 기준 18pt 그대로라 **정지 상태에서도 이웃끼리 합체 판정에 걸림**(1.4까지 7개는 간격 ~20.7pt로 판정 밖 — "1.3까지는 문제없었다"는 증언과 일치).
|
||||||
- 수정: 18→6 (펼침 상태 정원 분리 확인 — 시각 QA. FAB에서 피어나는 모핑은 겹침 구간에서 일어나므로 유지). 마케팅 06-bubble ×3 재촬영(구 샷에 왜곡 찍혀 있었음), 코드 주석에 "탭 추가 시 간격 하한 재확인" 경고.
|
- 수정: 18→6 (펼침 상태 정원 분리 확인 — 시각 QA. FAB에서 피어나는 모핑은 겹침 구간에서 일어나므로 유지). 마케팅 06-bubble ×3 재촬영(구 샷에 왜곡 찍혀 있었음), 코드 주석에 "탭 추가 시 간격 하한 재확인" 경고.
|
||||||
|
|
||||||
|
### §12 후속 3 — 건강 다짐 표시 색 (2026-08-23, 빌드 7)
|
||||||
|
|
||||||
|
- 사용자 요구: 건강 다짐이 표시 표면(목표 탭·모음 카드·위젯·다짐 현황)에서 고정 분홍 — 만들 때 색을 고를 수 있게(타임테이블 블록 색과 별개).
|
||||||
|
- 구현: **Quest 신규 필드 `healthColorHex`(기본 "")** + `targetColor` 건강 분기(빈 값=시스템 분홍 → 다크 적응 유지, 값 있으면 hex 고정 — 꼬리표 색과 같은 문법). 편집기 건강 섹션에 '다짐 색'(프리셋 12=AppTheme.tagPresets+컬러 피커), 저장 시 기본 분홍 그대로면 ""로 정규화, 행동·꼬리표 전환 시 "" 정리. targetColor가 단일 공급 지점이라 전 표면 자동 반영.
|
||||||
|
- ⚠️ **CloudKit 스키마 추가** — DiaryPageItem 전례에 따라 **배포(개발 내보내기→콘솔 Deploy)가 빌드 7 설치보다 선행**. 절차: 아리나 시뮬 내보내기 → 사용자 콘솔 Deploy → 그다음 업로드·설치. 구버전 기기는 분홍 폴백(표시 차이만 — 안전).
|
||||||
|
- 시드: -seedHealthQuest 걸음 다짐에 #3E7CB1 시드(신규·멱등 재실행 모두 — 렌더 검증+스키마 내보내기 트리거 겸용). 도움말·whats-new 3언어 갱신, l10n 0/0.
|
||||||
|
|||||||
@ -601,7 +601,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements;
|
CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -643,7 +643,7 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements;
|
CODE_SIGN_ENTITLEMENTS = IOS/Haru_Danim.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -686,7 +686,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -722,7 +722,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "Haru_DanimWatch Watch App/Haru_DanimWatch Watch App.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@ -758,7 +758,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
||||||
@ -794,7 +794,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Haru_DanimWatchWidgets/Haru_DanimWatchWidgetsExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
INFOPLIST_FILE = Haru_DanimWatchWidgets/Info.plist;
|
||||||
@ -827,7 +827,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Widgets/Info.plist;
|
INFOPLIST_FILE = Widgets/Info.plist;
|
||||||
@ -853,7 +853,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Widgets/Haru_DanimWidgets.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 6;
|
CURRENT_PROJECT_VERSION = 7;
|
||||||
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
DEVELOPMENT_TEAM = 5GNS4Z2SYT;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Widgets/Info.plist;
|
INFOPLIST_FILE = Widgets/Info.plist;
|
||||||
|
|||||||
@ -674,7 +674,16 @@ enum DebugSeed {
|
|||||||
// 마케팅 촬영에 쓰이므로 실행 로케일을 따른다 (-seedDemo 시드 이름과 같은 규칙)
|
// 마케팅 촬영에 쓰이므로 실행 로케일을 따른다 (-seedDemo 시드 이름과 같은 규칙)
|
||||||
let title = String(localized: "건강 습관 만들기")
|
let title = String(localized: "건강 습관 만들기")
|
||||||
let existing = (try? context.fetch(FetchDescriptor<Goal>())) ?? []
|
let existing = (try? context.fetch(FetchDescriptor<Goal>())) ?? []
|
||||||
guard !existing.contains(where: { $0.title == title }) else { return }
|
if let seeded = existing.first(where: { $0.title == title }) {
|
||||||
|
// 재실행(멱등) 경로: 걸음 다짐에 지정 색이 없으면 입혀 준다 — 표시 색(1.5(7))의
|
||||||
|
// 렌더 검증 + 새 healthColorHex 필드의 CloudKit 내보내기(스키마 생성) 트리거 겸용
|
||||||
|
if let steps = seeded.quests.first(where: { $0.healthMetricRaw == HealthMetric.steps.rawValue }),
|
||||||
|
steps.healthColorHex.isEmpty {
|
||||||
|
steps.healthColorHex = "#3E7CB1"
|
||||||
|
DataChange.commit(context: context)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
let goal = Goal(title: title, symbolName: "heart.fill", colorHex: "#E0558C",
|
let goal = Goal(title: title, symbolName: "heart.fill", colorHex: "#E0558C",
|
||||||
startDate: Calendar.current.date(byAdding: .day, value: -7, to: .now) ?? .now,
|
startDate: Calendar.current.date(byAdding: .day, value: -7, to: .now) ?? .now,
|
||||||
endDate: nil)
|
endDate: nil)
|
||||||
@ -689,6 +698,8 @@ enum DebugSeed {
|
|||||||
context.insert(quest)
|
context.insert(quest)
|
||||||
}
|
}
|
||||||
addQuest(HealthMetric.steps.rawValue, count: 8000, isDuration: false)
|
addQuest(HealthMetric.steps.rawValue, count: 8000, isDuration: false)
|
||||||
|
// 걸음 다짐에 지정 색 시드 — 나머지는 기본 분홍 유지 (색 지정/기본의 대비 렌더 검증, 1.5(7))
|
||||||
|
goal.quests.first { $0.healthMetricRaw == HealthMetric.steps.rawValue }?.healthColorHex = "#3E7CB1"
|
||||||
addQuest(HealthMetric.sleep.rawValue, seconds: 8 * 3600, isDuration: true)
|
addQuest(HealthMetric.sleep.rawValue, seconds: 8 * 3600, isDuration: true)
|
||||||
addQuest("workout.running", seconds: 30 * 60, isDuration: true)
|
addQuest("workout.running", seconds: 30 * 60, isDuration: true)
|
||||||
addQuest(HealthMetric.water.rawValue, count: 2000, isDuration: false)
|
addQuest(HealthMetric.water.rawValue, count: 2000, isDuration: false)
|
||||||
|
|||||||
@ -4835,6 +4835,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"다짐 색": {
|
||||||
|
"localizations": {
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Quest color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "クエストの色"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"다짐 수정": {
|
"다짐 수정": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
@ -5059,18 +5075,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"다짐을 만들 때 대상에서 '건강'을 고르면 '매일 8,000보', '수면 8시간 이상', '달리기 30분'처럼 건강 데이터를 목표로 삼을 수 있어요. 걸음 수·운동 시간·수면·활동 에너지·이동 거리·일어서기·마음챙김·물 섭취를 고를 수 있고, 운동은 달리기·수영·근력 운동 같은 종목별로도 만들 수 있어요 — 종목별 시간은 건강 앱에 기록된 운동 기준이라 활동 링의 '운동 시간'과는 값이 다를 수 있어요. 건강 다짐은 하루 단위로만 반복돼요(요일 지정은 돼요).\n수면 다짐은 '수면 구간'(기본 저녁 9시~다음 날 아침 9시)을 정해 두면 그 안에서 잔 시간을 통째로 그날 것으로 세요 — 자정을 넘겨 자도 하루가 쪼개지지 않아요. 진행률·연속 달성·목표 판정은 다른 다짐과 똑같은 규칙으로 계산돼요. 건강 데이터가 없는 기기(맥 등)에서는 건강 다짐이 '데이터 없음'으로 보이고 목표 진행률에서 빠져요 — 아이폰에서 보면 정상으로 보여요.": {
|
"다짐을 만들 때 대상에서 '건강'을 고르면 '매일 8,000보', '수면 8시간 이상', '달리기 30분'처럼 건강 데이터를 목표로 삼을 수 있어요. 걸음 수·운동 시간·수면·활동 에너지·이동 거리·일어서기·마음챙김·물 섭취를 고를 수 있고, 운동은 달리기·수영·근력 운동 같은 종목별로도 만들 수 있어요 — 종목별 시간은 건강 앱에 기록된 운동 기준이라 활동 링의 '운동 시간'과는 값이 다를 수 있어요. 건강 다짐은 하루 단위로만 반복돼요(요일 지정은 돼요). 목표 탭·위젯에서 이 다짐이 보일 색도 만들 때 직접 고를 수 있어요 — 기본은 분홍이고, 타임테이블의 수면·운동 블록 색과는 별개예요.\n수면 다짐은 '수면 구간'(기본 저녁 9시~다음 날 아침 9시)을 정해 두면 그 안에서 잔 시간을 통째로 그날 것으로 세요 — 자정을 넘겨 자도 하루가 쪼개지지 않아요. 진행률·연속 달성·목표 판정은 다른 다짐과 똑같은 규칙으로 계산돼요. 건강 데이터가 없는 기기(맥 등)에서는 건강 다짐이 '데이터 없음'으로 보이고 목표 진행률에서 빠져요 — 아이폰에서 보면 정상으로 보여요.": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"en": {
|
"en": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "Pick Health as the target when creating a quest, and health data becomes your goal — \"8,000 steps a day,\" \"at least 8 hours of sleep,\" \"30 minutes of running.\" You can choose steps, exercise time, sleep, active energy, distance, stand hours, mindfulness or water intake, and workouts can be tracked by type, like running, swimming or strength training — type-based time comes from workouts logged in the Health app, so it can differ from the activity ring's Exercise Time. Health quests repeat daily only (weekday schedules are fine).\nFor a sleep quest, set a sleep window (9 PM to 9 AM the next morning by default) and everything you sleep within it counts as that day's sleep — crossing midnight doesn't split your night. Progress, streaks and goal judgment follow exactly the same rules as other quests. On devices without health data (like the Mac), health quests show \"No data\" and drop out of goal progress — they look normal again on your iPhone."
|
"value": "Pick 'Health' as a quest's target to set goals from health data — '8,000 steps a day', 'at least 8 hours of sleep', '30 minutes of running'. You can choose steps, exercise time, sleep, active energy, distance, stand hours, mindful minutes or water intake, and workouts can be set by type, like running, swimming or strength training — type-based time comes from workouts logged in the Health app, so it can differ from the activity ring's 'exercise time'. Health quests repeat daily only (weekday schedules are fine). You can also pick the color the quest shows in the Goals tab and widgets — pink by default, and separate from the timetable's sleep and workout block colors.\nFor a sleep quest, set a sleep window (9 PM to 9 AM the next day by default) and everything you sleep within it counts as that day's — sleeping past midnight won't split your night. Progress, streaks and goal judgment follow the same rules as any other quest. On devices without health data (like the Mac), health quests show as 'no data' and drop out of goal progress — they look normal on your iPhone."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja": {
|
"ja": {
|
||||||
"stringUnit": {
|
"stringUnit": {
|
||||||
"state": "translated",
|
"state": "translated",
|
||||||
"value": "クエストを作るとき対象で「ヘルス」を選ぶと、「毎日8,000歩」「睡眠8時間以上」「ランニング30分」のように、ヘルスデータを目標にできます。歩数・運動時間・睡眠・アクティブエネルギー・移動距離・スタンド時間・マインドフルネス・水分摂取から選べて、運動はランニング・水泳・筋力トレーニングのような種目別でも作れます — 種目別の時間はヘルスケアアプリに記録された運動が基準なので、アクティビティリングの「運動時間」とは値が異なることがあります。ヘルスクエストは1日単位でのみ繰り返されます(曜日指定はできます)。\n睡眠クエストは「睡眠時間帯」(デフォルトは夜9時〜翌朝9時)を決めておくと、その中で眠った時間をまるごとその日の分として数えます — 日付をまたいで眠っても1日が分割されません。進捗・連続達成・目標判定はほかのクエストとまったく同じルールで計算されます。ヘルスデータのない端末(Macなど)ではヘルスクエストが「データなし」と表示され目標の進捗から外れます — iPhoneで見れば通常どおり表示されます。"
|
"value": "クエストの対象で「ヘルス」を選ぶと、「毎日8,000歩」「睡眠8時間以上」「ランニング30分」のようにヘルスデータを目標にできます。歩数・運動時間・睡眠・アクティブエネルギー・移動距離・スタンド時間・マインドフル分・水分摂取から選べ、運動はランニング・水泳・筋力トレーニングなど種目別でも作れます — 種目別の時間はヘルスケアAppに記録されたワークアウト基準なので、アクティビティリングの「エクササイズ時間」とは値が異なることがあります。ヘルスクエストは1日単位のみで繰り返します(曜日指定はできます)。目標タブやウィジェットでクエストが表示される色も作成時に選べます — 基本はピンクで、タイムテーブルの睡眠・運動ブロックの色とは別です。\n睡眠クエストは「睡眠時間帯」(初期設定は夜9時〜翌朝9時)を決めておくと、その中で眠った時間をまるごとその日のものとして数えます — 日付をまたいで眠っても1日が分割されません。進捗・継続・目標の判定は他のクエストと同じルールで計算されます。ヘルスデータのない端末(Macなど)ではヘルスクエストが「データなし」と表示され、目標の進捗から外れます — iPhoneで見れば正常に表示されます。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -317,7 +317,7 @@ struct HelpView: View {
|
|||||||
HelpTopic(
|
HelpTopic(
|
||||||
symbol: "figure.run",
|
symbol: "figure.run",
|
||||||
title: "건강 데이터로 다짐 만들기 (프리미엄)",
|
title: "건강 데이터로 다짐 만들기 (프리미엄)",
|
||||||
body: "다짐을 만들 때 대상에서 '건강'을 고르면 '매일 8,000보', '수면 8시간 이상', '달리기 30분'처럼 건강 데이터를 목표로 삼을 수 있어요. 걸음 수·운동 시간·수면·활동 에너지·이동 거리·일어서기·마음챙김·물 섭취를 고를 수 있고, 운동은 달리기·수영·근력 운동 같은 종목별로도 만들 수 있어요 — 종목별 시간은 건강 앱에 기록된 운동 기준이라 활동 링의 '운동 시간'과는 값이 다를 수 있어요. 건강 다짐은 하루 단위로만 반복돼요(요일 지정은 돼요).\n수면 다짐은 '수면 구간'(기본 저녁 9시~다음 날 아침 9시)을 정해 두면 그 안에서 잔 시간을 통째로 그날 것으로 세요 — 자정을 넘겨 자도 하루가 쪼개지지 않아요. 진행률·연속 달성·목표 판정은 다른 다짐과 똑같은 규칙으로 계산돼요. 건강 데이터가 없는 기기(맥 등)에서는 건강 다짐이 '데이터 없음'으로 보이고 목표 진행률에서 빠져요 — 아이폰에서 보면 정상으로 보여요."
|
body: "다짐을 만들 때 대상에서 '건강'을 고르면 '매일 8,000보', '수면 8시간 이상', '달리기 30분'처럼 건강 데이터를 목표로 삼을 수 있어요. 걸음 수·운동 시간·수면·활동 에너지·이동 거리·일어서기·마음챙김·물 섭취를 고를 수 있고, 운동은 달리기·수영·근력 운동 같은 종목별로도 만들 수 있어요 — 종목별 시간은 건강 앱에 기록된 운동 기준이라 활동 링의 '운동 시간'과는 값이 다를 수 있어요. 건강 다짐은 하루 단위로만 반복돼요(요일 지정은 돼요). 목표 탭·위젯에서 이 다짐이 보일 색도 만들 때 직접 고를 수 있어요 — 기본은 분홍이고, 타임테이블의 수면·운동 블록 색과는 별개예요.\n수면 다짐은 '수면 구간'(기본 저녁 9시~다음 날 아침 9시)을 정해 두면 그 안에서 잔 시간을 통째로 그날 것으로 세요 — 자정을 넘겨 자도 하루가 쪼개지지 않아요. 진행률·연속 달성·목표 판정은 다른 다짐과 똑같은 규칙으로 계산돼요. 건강 데이터가 없는 기기(맥 등)에서는 건강 다짐이 '데이터 없음'으로 보이고 목표 진행률에서 빠져요 — 아이폰에서 보면 정상으로 보여요."
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,9 @@ struct QuestEditorView: View {
|
|||||||
to: Calendar.current.startOfDay(for: .now)
|
to: Calendar.current.startOfDay(for: .now)
|
||||||
) ?? .now
|
) ?? .now
|
||||||
}()
|
}()
|
||||||
|
/// 건강 다짐 표시 색 (1.5(7)) — 기본 분홍(.pink). 저장 시 기본색 그대로면 빈 값으로
|
||||||
|
/// 정규화해 시스템 분홍의 라이트/다크 적응을 유지한다 (save() 참고)
|
||||||
|
@State private var healthColor: Color = .pink
|
||||||
/// 수량형 건강 목표 입력값 (지표별 기본 단위 — 걸음 보 / 에너지 kcal / 스탠드 시간)
|
/// 수량형 건강 목표 입력값 (지표별 기본 단위 — 걸음 보 / 에너지 kcal / 스탠드 시간)
|
||||||
@State private var healthIntTarget = 8000
|
@State private var healthIntTarget = 8000
|
||||||
/// 거리 목표 (km — 저장은 m)
|
/// 거리 목표 (km — 저장은 m)
|
||||||
@ -182,6 +185,34 @@ struct QuestEditorView: View {
|
|||||||
components: .hourAndMinute
|
components: .hourAndMinute
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 표시 색 (1.5(7)) — 목표 탭·모음 카드·일기·위젯에서 이 다짐이 보일 색.
|
||||||
|
// 행동·꼬리표 다짐은 대상의 색을 따르므로 건강 다짐에만 있는 선택이고,
|
||||||
|
// 타임테이블의 수면·운동 블록 색(설정 → 건강 데이터)과는 별개다
|
||||||
|
ColorPicker(selection: $healthColor, supportsOpacity: false) {
|
||||||
|
Text("다짐 색")
|
||||||
|
}
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 40), spacing: 10)], spacing: 10) {
|
||||||
|
ForEach(Array(AppTheme.tagPresets.enumerated()), id: \.offset) { index, hex in
|
||||||
|
Button {
|
||||||
|
healthColor = Color(hex: hex)
|
||||||
|
} label: {
|
||||||
|
Circle()
|
||||||
|
.fill(Color(hex: hex))
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
.overlay {
|
||||||
|
if healthColor.hexString == hex {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.font(.system(size: 14, weight: .bold))
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel(String(localized: "미리 설정된 색 \(index + 1)"))
|
||||||
|
.accessibilityAddTraits(healthColor.hexString == hex ? .isSelected : [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
case .action:
|
case .action:
|
||||||
if allActions.isEmpty {
|
if allActions.isEmpty {
|
||||||
@ -503,6 +534,9 @@ struct QuestEditorView: View {
|
|||||||
let dayStart = cal.startOfDay(for: .now)
|
let dayStart = cal.startOfDay(for: .now)
|
||||||
sleepStart = cal.date(byAdding: .minute, value: quest.sleepWindowStartMinutes, to: dayStart) ?? sleepStart
|
sleepStart = cal.date(byAdding: .minute, value: quest.sleepWindowStartMinutes, to: dayStart) ?? sleepStart
|
||||||
sleepEnd = cal.date(byAdding: .minute, value: quest.sleepWindowEndMinutes, to: dayStart) ?? sleepEnd
|
sleepEnd = cal.date(byAdding: .minute, value: quest.sleepWindowEndMinutes, to: dayStart) ?? sleepEnd
|
||||||
|
if !quest.healthColorHex.isEmpty {
|
||||||
|
healthColor = Color(hex: quest.healthColorHex)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
measure = quest.measure
|
measure = quest.measure
|
||||||
period = quest.period
|
period = quest.period
|
||||||
@ -533,11 +567,13 @@ struct QuestEditorView: View {
|
|||||||
target.targetTag = nil
|
target.targetTag = nil
|
||||||
target.measure = selectedAction?.trackingType ?? .time
|
target.measure = selectedAction?.trackingType ?? .time
|
||||||
target.healthMetricRaw = "" // 정규화 — 반대편 대상 정리 (plan §3.4)
|
target.healthMetricRaw = "" // 정규화 — 반대편 대상 정리 (plan §3.4)
|
||||||
|
target.healthColorHex = ""
|
||||||
case .tag:
|
case .tag:
|
||||||
target.targetTag = selectedTag
|
target.targetTag = selectedTag
|
||||||
target.targetAction = nil
|
target.targetAction = nil
|
||||||
target.measure = measure
|
target.measure = measure
|
||||||
target.healthMetricRaw = ""
|
target.healthMetricRaw = ""
|
||||||
|
target.healthColorHex = ""
|
||||||
case .health:
|
case .health:
|
||||||
target.targetAction = nil
|
target.targetAction = nil
|
||||||
target.targetTag = nil
|
target.targetTag = nil
|
||||||
@ -559,6 +595,10 @@ struct QuestEditorView: View {
|
|||||||
let endComps = cal.dateComponents([.hour, .minute], from: sleepEnd)
|
let endComps = cal.dateComponents([.hour, .minute], from: sleepEnd)
|
||||||
target.sleepWindowStartMinutes = (startComps.hour ?? 21) * 60 + (startComps.minute ?? 0)
|
target.sleepWindowStartMinutes = (startComps.hour ?? 21) * 60 + (startComps.minute ?? 0)
|
||||||
target.sleepWindowEndMinutes = (endComps.hour ?? 9) * 60 + (endComps.minute ?? 0)
|
target.sleepWindowEndMinutes = (endComps.hour ?? 9) * 60 + (endComps.minute ?? 0)
|
||||||
|
// 표시 색 — 기본 분홍 그대로면 빈 값(시스템 분홍의 다크 모드 적응 유지),
|
||||||
|
// 사용자가 바꿨을 때만 hex 고정 저장 (1.5(7))
|
||||||
|
target.healthColorHex =
|
||||||
|
healthColor.hexString == Color.pink.hexString ? "" : healthColor.hexString
|
||||||
period = .daily // 건강 다짐은 하루 단위 전용 (방어 — UI에서도 강제됨)
|
period = .daily // 건강 다짐은 하루 단위 전용 (방어 — UI에서도 강제됨)
|
||||||
}
|
}
|
||||||
target.period = period
|
target.period = period
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
● 애플 건강과 연결
|
● 애플 건강과 연결
|
||||||
• 모음 탭에서 걸음·운동·수면 같은 오늘의 건강 데이터를 타일로 볼 수 있어요 — 값은 기기 밖으로 나가지 않아요
|
• 모음 탭에서 걸음·운동·수면 같은 오늘의 건강 데이터를 타일로 볼 수 있어요 — 값은 기기 밖으로 나가지 않아요
|
||||||
• 건강 데이터로 다짐을 만들 수 있어요(프리미엄) — '매일 8,000보', '수면 8시간', '달리기 30분'처럼요. 운동은 종목별로도 돼요
|
• 건강 데이터로 다짐을 만들 수 있어요(프리미엄) — '매일 8,000보', '수면 8시간', '달리기 30분'처럼요. 운동은 종목별로도 되고, 다짐이 보일 색도 고를 수 있어요
|
||||||
• 수면은 '수면 구간'을 정해 두면 자정을 넘겨 자도 하루가 쪼개지지 않아요. 낮에 자는 날이 있다면 '수면 표시 구간'을 요일별로 맞출 수도 있어요
|
• 수면은 '수면 구간'을 정해 두면 자정을 넘겨 자도 하루가 쪼개지지 않아요. 낮에 자는 날이 있다면 '수면 표시 구간'을 요일별로 맞출 수도 있어요
|
||||||
|
|
||||||
● 일기가 더 넓어졌어요 (프리미엄)
|
● 일기가 더 넓어졌어요 (프리미엄)
|
||||||
@ -26,7 +26,7 @@ This update brings Haru Danim closer to Apple Health — and your Apple Watch. A
|
|||||||
|
|
||||||
● Apple Health, connected
|
● Apple Health, connected
|
||||||
• See today's health data — steps, exercise, sleep — as tiles on the Home tab. Values never leave your device
|
• See today's health data — steps, exercise, sleep — as tiles on the Home tab. Values never leave your device
|
||||||
• Build quests from health data (Premium): "8,000 steps a day," "8 hours of sleep," "30 minutes of running" — workouts by type, too
|
• Build quests from health data (Premium): "8,000 steps a day," "8 hours of sleep," "30 minutes of running" — workouts by type, and pick the quest's color too
|
||||||
• Set a sleep window and sleeping past midnight won't split your night. If some days you sleep in the daytime, tune the display window per weekday
|
• Set a sleep window and sleeping past midnight won't split your night. If some days you sleep in the daytime, tune the display window per weekday
|
||||||
|
|
||||||
● A bigger Diary (Premium)
|
● A bigger Diary (Premium)
|
||||||
@ -47,7 +47,7 @@ This update brings Haru Danim closer to Apple Health — and your Apple Watch. A
|
|||||||
|
|
||||||
● Appleヘルスケアと連携
|
● Appleヘルスケアと連携
|
||||||
• ホームタブで歩数・運動・睡眠など今日のヘルスデータをタイルで見られます — 値が端末の外に出ることはありません
|
• ホームタブで歩数・運動・睡眠など今日のヘルスデータをタイルで見られます — 値が端末の外に出ることはありません
|
||||||
• ヘルスデータでクエストを作れます(プレミアム)—「毎日8,000歩」「睡眠8時間」「ランニング30分」など。運動は種目別でもOK
|
• ヘルスデータでクエストを作れます(プレミアム)—「毎日8,000歩」「睡眠8時間」「ランニング30分」など。運動は種目別でもOK、クエストの色も選べます
|
||||||
• 「睡眠時間帯」を決めておけば、日付をまたいで眠っても1日が分割されません。昼間に眠る日があるなら、表示の時間帯を曜日ごとに調整できます
|
• 「睡眠時間帯」を決めておけば、日付をまたいで眠っても1日が分割されません。昼間に眠る日があるなら、表示の時間帯を曜日ごとに調整できます
|
||||||
|
|
||||||
● 日記がもっと広く(プレミアム)
|
● 日記がもっと広く(プレミアム)
|
||||||
|
|||||||
@ -396,6 +396,10 @@ final class Quest {
|
|||||||
/// 구간 '끝'이 속한 논리적 하루에 수면 전체가 귀속된다 (깨어난 날 규칙 — plan §3.4)
|
/// 구간 '끝'이 속한 논리적 하루에 수면 전체가 귀속된다 (깨어난 날 규칙 — plan §3.4)
|
||||||
var sleepWindowStartMinutes: Int = 1260
|
var sleepWindowStartMinutes: Int = 1260
|
||||||
var sleepWindowEndMinutes: Int = 540
|
var sleepWindowEndMinutes: Int = 540
|
||||||
|
/// 건강 다짐의 표시 색 "#RRGGBB" (1.5(7)) — 빈 값 = 기본(분홍). 행동·꼬리표 다짐은
|
||||||
|
/// 대상의 색을 쓰므로 항상 빈 값(저장 시 정규화). ⚠️ 새 필드 = CloudKit 스키마 추가 —
|
||||||
|
/// 배포 절차(개발 내보내기→콘솔 Deploy)가 새 빌드 설치보다 먼저여야 함(§3, plan §12)
|
||||||
|
var healthColorHex: String = ""
|
||||||
|
|
||||||
/// 목표 안에서의 표시 순서
|
/// 목표 안에서의 표시 순서
|
||||||
var sortOrder: Int = 0
|
var sortOrder: Int = 0
|
||||||
@ -452,7 +456,11 @@ extension Quest {
|
|||||||
var targetColor: Color {
|
var targetColor: Color {
|
||||||
if let targetAction { return targetAction.color }
|
if let targetAction { return targetAction.color }
|
||||||
if let targetTag { return targetTag.color }
|
if let targetTag { return targetTag.color }
|
||||||
if healthTarget != nil { return .pink }
|
// 건강 다짐: 사용자 지정 색(1.5(7)), 없으면 기본 분홍 — 이 접근자가 단일 공급
|
||||||
|
// 지점이라 목표 탭·모음 카드·일기·위젯·워치 스냅숏이 전부 함께 따라온다
|
||||||
|
if healthTarget != nil {
|
||||||
|
return healthColorHex.isEmpty ? .pink : Color(hex: healthColorHex)
|
||||||
|
}
|
||||||
return AppTheme.green
|
return AppTheme.green
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user