From ddd46a40845fedda8628d1d817517abb3130d8ae Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Sun, 19 Jul 2026 05:36:46 +0900 Subject: [PATCH] feat(widget): lock-screen quest dots fill by progress ratio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 잠금화면 위젯 '다짐 점'이 달성/미달성 이진 채움이라 진행 정도를 알 수 없다는 보고 → 각 점이 진행률(ratio)만큼 아래에서부터 차오르게 변경. 표시 전용 소형 뷰 수정이라 위험 낮음 판단 후 진행 - ratio를 그대로 쓰므로 달성=완전 채움(기존과 동일), '이하 유지'는 ratio가 1/0이라 한도 안=채움/초과=빈 점 동작 불변. 작은 크기(5~6pt)에선 부채꼴보다 물높이 방식이 가독성 좋아 채택 - 위젯 미리보기 잠금 섹션에 '다짐 여러 개+주간' 항목 추가 — 부분 채움(반쯤 찬 점/빈 점/가득 점) 시뮬 스크린샷으로 circular/rectangular 모두 확인 - 새 문자열 없음(카탈로그 클린), 도움말 무변경(스타일 세부 서술 없음). CLAUDE.md §10 갱신. Debug/Store 빌드 성공 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014CkLDi8Lr21vGqo4SjZaTp --- myApp/HaruDanim/CLAUDE.md | 2 +- .../IOS/Views/WidgetPreviewScreen.swift | 8 ++++++++ myApp/HaruDanim/Widgets/LockScreenWidgets.swift | 17 +++++++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index fe69b8d..8fe0e27 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -274,7 +274,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f | ④ 다짐 현황 `HaruGoalQuestGridWidget` | 표시 전용 링 그리드 + 목표 헤더. 소형 2×2 / 중형 4×2 / 대형: 다짐16 / 목표4 / 목표2 | | ⑤ 행동 통계 `HaruStatsChartWidget` | 꺾은선(행동 색). 기간: 일주일/한 달(일별, 소형 제외→주별 대체)/한 달(주별). 선 수 제한 소2/중4/대6 | | ⑥ 현재 진행 중 `HaruNowRunningWidget` | 측정 중인 대표 행동(설정 '대표 시간 기준' — 다이나믹 아일랜드와 같은 규칙)을 꼬리표 색 배경 + 실시간 타이머(`Text(style:.timer)`) + 종료 버튼(`RunActionIntent` 토글의 종료 방향)으로 표시. 소형/중형: 대표 1개 + "+N개 함께 추적 중". 대형: 대표 카드 + 함께 측정 중 나열(각각 종료 버튼, 4개 초과분은 "외 N개") / 단독이면 오늘 누적(실시간)·시작 시각 카드. 측정 중 아니면 빈 상태 안내, 갤러리 미리보기는 샘플 표시 | -| 잠금화면 `HaruLockGoalWidget` | 목표 1개 달성률 — 원형 게이지/숫자만/다짐 점(달성=채움). circular/rectangular/inline | +| 잠금화면 `HaruLockGoalWidget` | 목표 1개 달성률 — 원형 게이지/숫자만/다짐 점(**진행률만큼 아래에서부터 차오름**, 달성=완전 채움 — ratio 사용이라 이하 유지는 기존대로 한도 안=채움/초과=빈 점). circular/rectangular/inline | | 제어 센터 `HaruActionRunControl` | iOS 18 ControlWidget — 행동 1개 토글(시간형 시작/종료, 횟수형 +1). `ToggleActionControlIntent`(SetValueIntent+LiveActivityIntent, 앱 프로세스 실행) | - 슬롯 규칙: 미선택이면 기본 대상 채움, 선택했으면 순서 존중 + 모자란 슬롯은 점선 빈 칸. '선택 안 함' 센티널 `NoneEntityID` diff --git a/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift b/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift index ed694d8..6dc1650 100644 --- a/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift +++ b/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift @@ -86,10 +86,18 @@ struct WidgetPreviewScreen: View { number.style = .number var dots = LockGoalConfigIntent() dots.style = .dots + // 다짐이 여러 개고 부분 진행이 섞인 목표 + 주간 기간 — 점의 '차오름' 확인용 + var dotsWeek = LockGoalConfigIntent() + dotsWeek.style = .dots + dotsWeek.span = .week + if let manyQuests = WidgetStore.defaultGoals(8).max(by: { $0.quests.count < $1.quests.count }) { + dotsWeek.goal = GoalEntity(manyQuests) + } lockEntries = [ ("gauge", LockGoalProvider.makeEntry(gauge)), ("number", LockGoalProvider.makeEntry(number)), ("dots", LockGoalProvider.makeEntry(dots)), + ("dots·주간·다짐多", LockGoalProvider.makeEntry(dotsWeek)), ] return } diff --git a/myApp/HaruDanim/Widgets/LockScreenWidgets.swift b/myApp/HaruDanim/Widgets/LockScreenWidgets.swift index 869aec5..d6248aa 100644 --- a/myApp/HaruDanim/Widgets/LockScreenWidgets.swift +++ b/myApp/HaruDanim/Widgets/LockScreenWidgets.swift @@ -214,13 +214,26 @@ struct LockGoalWidgetView: View { } } - /// 다짐 달성 여부 점 표시 (달성 = 채움) + /// 다짐 진행률 점 표시 — 진행률만큼 아래에서부터 차오른다 (달성 = 완전히 채움). + /// ratio를 그대로 쓰므로 '이하 유지'는 기존과 동일하게 한도 안=채움/초과=빈 점. + /// 작은 크기에서는 부채꼴보다 바닥부터 차는 물높이 방식이 읽기 쉽다. private func questDots(_ goal: GoalSnapshot, size: CGFloat) -> some View { HStack(spacing: 3) { ForEach(goal.quests.prefix(6)) { quest in + let fill = min(max(quest.ratio, 0), 1) Circle() .strokeBorder(.primary, lineWidth: 1) - .background(Circle().fill(quest.isAchieved ? AnyShapeStyle(.primary) : AnyShapeStyle(.clear))) + .background( + GeometryReader { geo in + VStack(spacing: 0) { + Spacer(minLength: 0) + Rectangle() + .fill(.primary) + .frame(height: geo.size.height * fill) + } + } + .clipShape(Circle()) + ) .frame(width: size, height: size) } }