diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index cfe92a6..08201fd 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -240,7 +240,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}" |---|---| | ① 행동 실행 `HaruActionRunWidget` | `Button(intent: RunActionIntent)` — 시간형 토글/횟수형 +1. 누적값 표시 기간(오늘/주/월/숨김) 선택. 소형 1(풀블리드)/중형 4(2×2 행 레이아웃)/대형 4 또는 8. 측정 중 노란 테두리+타이머(`Text(style:.timer)`) | | ② 목표 진행률 `HaruGoalBarsWidget` | 하루/주간/월간 가로 진행바 3줄. 중형: 목표2 또는 목표1+다짐. 대형: 목표4 / 목표+다짐×2 / 목표1+모든 다짐. 다짐 목록은 높이에 맞춰 적응 채움(+N) | -| ③ 다짐 진행률 `HaruQuestRingWidget` | 원형 링(행동 대상이면 눌러 실행). 기간 선택. 소형1/중형2/대형4. atMost는 "한도 지킴/초과" | +| ③ 다짐 진행률 `HaruQuestRingWidget` | 원형 링(행동 대상이면 눌러 실행). 기간 선택. 소형1/중형2/대형4. atMost는 "한도 지킴/초과". 퍼센트(한도) 문구 아래에 선택 기간의 **실제 누적값**("1시간 20분"/"3회") 한 줄 — atMost는 퍼센트가 없어 이 줄이 유일한 현재 수치(진행률 캡과 무관한 원본 값, 미리보기 앵커 CM) | | ④ 다짐 현황 `HaruGoalQuestGridWidget` | 표시 전용 링 그리드 + 목표 헤더. 소형 2×2 / 중형 4×2 / 대형: 다짐16 / 목표4 / 목표2 | | ⑤ 행동 통계 `HaruStatsChartWidget` | 꺾은선(행동 색). 기간: 일주일/한 달(일별, 소형 제외→주별 대체)/한 달(주별). 선 수 제한 소2/중4/대6 | | 잠금화면 `HaruLockGoalWidget` | 목표 1개 달성률 — 원형 게이지/숫자만/다짐 점(달성=채움). circular/rectangular/inline | diff --git a/myApp/HaruDanim/IOS/InfoPlist.xcstrings b/myApp/HaruDanim/IOS/InfoPlist.xcstrings index 077527d..76375fa 100644 --- a/myApp/HaruDanim/IOS/InfoPlist.xcstrings +++ b/myApp/HaruDanim/IOS/InfoPlist.xcstrings @@ -1,90 +1,89 @@ { - "sourceLanguage": "ko", - "strings": { - "CFBundleDisplayName": { - "comment": "홈 화면에 표시되는 앱 이름", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Haru Danim" + "sourceLanguage" : "ko", + "strings" : { + "CFBundleDisplayName" : { + "comment" : "홈 화면에 표시되는 앱 이름", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haru Danim" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Haru Danim" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haru Danim" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "하루 다님" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "하루 다님" } } } }, - "CFBundleName": { - "comment": "Bundle name", - "extractionState": "extracted_with_value", - "localizations": { - "ko": { - "stringUnit": { - "state": "new", - "value": "Haru_Danim" + "CFBundleName" : { + "comment" : "Bundle name", + "extractionState" : "extracted_with_value", + "localizations" : { + "ko" : { + "stringUnit" : { + "state" : "new", + "value" : "Haru_Danim" } } }, - "shouldTranslate": false + "shouldTranslate" : false }, - "NSCalendarsFullAccessUsageDescription": { - "comment": "Privacy - Calendars Full Access Usage Description", - "extractionState": "extracted_with_value", - "localizations": { - "ko": { - "stringUnit": { - "state": "translated", - "value": "일기 타임테이블에 캘린더 일정을 함께 표시하기 위해 필요해요." + "NSCalendarsFullAccessUsageDescription" : { + "comment" : "Privacy - Calendars Full Access Usage Description", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Needed to show your calendar events alongside the diary timetable." } }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Needed to show your calendar events alongside the diary timetable." + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "日記のタイムテーブルにカレンダーの予定を一緒に表示するために必要です。" } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "日記のタイムテーブルにカレンダーの予定を一緒に表示するために必要です。" + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "일기 타임테이블에 캘린더 일정을 함께 표시하기 위해 필요해요." } } } }, - "NSPhotoLibraryAddUsageDescription": { - "comment": "이미지 내보내기 → 사진에 저장 권한 문구", - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Needed to save exported report images to your photo library." + "NSPhotoLibraryAddUsageDescription" : { + "comment" : "이미지 내보내기 → 사진에 저장 권한 문구", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Needed to save exported report images to your photo library." } }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "書き出したレポート画像を写真アルバムに保存するために必要です。" + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "書き出したレポート画像を写真アルバムに保存するために必要です。" } }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "내보낸 리포트 이미지를 사진 앨범에 저장하기 위해 필요해요." + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "내보낸 리포트 이미지를 사진 앨범에 저장하기 위해 필요해요." } } } } }, - "version": "1.0" + "version" : "1.0" } \ No newline at end of file diff --git a/myApp/HaruDanim/IOS/Localizable.xcstrings b/myApp/HaruDanim/IOS/Localizable.xcstrings index 9f41030..72f1d56 100644 --- a/myApp/HaruDanim/IOS/Localizable.xcstrings +++ b/myApp/HaruDanim/IOS/Localizable.xcstrings @@ -915,6 +915,22 @@ } } }, + "0분": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "0m" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "0分" + } + } + } + }, "1.0": { "localizations": { "en": { diff --git a/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift b/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift index 402ee86..4711f69 100644 --- a/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift +++ b/myApp/HaruDanim/IOS/Views/WidgetPreviewScreen.swift @@ -30,6 +30,8 @@ struct WidgetPreviewScreen: View { @State private var ringSmall: QuestRingEntry? @State private var ringMedium: QuestRingEntry? @State private var ringLarge: QuestRingEntry? + /// '이하 유지' 다짐이 있으면 소형 한 칸 추가 (한도 문구 + 누적값 표시 확인용) + @State private var ringAtMostSmall: QuestRingEntry? // ④ 다짐 현황 @State private var statusSmall: QuestStatusEntry? @State private var statusMedium: QuestStatusEntry? @@ -117,6 +119,11 @@ struct WidgetPreviewScreen: View { ringSmall = QuestRingProvider.makeEntry(ringConfig, family: .systemSmall) ringMedium = QuestRingProvider.makeEntry(ringConfig, family: .systemMedium) ringLarge = QuestRingProvider.makeEntry(ringConfig, family: .systemLarge) + if let atMost = IntentStore.quests().first(where: { $0.direction == .atMost }) { + var atMostConfig = QuestRingConfigIntent() + atMostConfig.quests = [QuestEntity(atMost)] + ringAtMostSmall = QuestRingProvider.makeEntry(atMostConfig, family: .systemSmall) + } // ④ 다짐 현황: 대형 3구성 let statusConfig = QuestStatusConfigIntent() @@ -214,6 +221,12 @@ struct WidgetPreviewScreen: View { } .id("C") } + if let atMostSmall = ringAtMostSmall { + row("③ 다짐 진행률 · 이하 유지(소형)") { + widgetBox(.systemSmall) { QuestRingWidgetView(previewFamily: .systemSmall, entry: atMostSmall) } + } + .id("CM") + } if let large = ringLarge { row("③ 다짐 진행률 · 대형(2×2)") { widgetBox(.systemLarge) { QuestRingWidgetView(previewFamily: .systemLarge, entry: large) } diff --git a/myApp/HaruDanim/Shared/QuestProgress.swift b/myApp/HaruDanim/Shared/QuestProgress.swift index ec66e0d..aa4b340 100644 --- a/myApp/HaruDanim/Shared/QuestProgress.swift +++ b/myApp/HaruDanim/Shared/QuestProgress.swift @@ -146,14 +146,18 @@ struct QuestProgress { // MARK: 하루/주간/월간 진행률 (CLAUDE.md §6.4 표시 구조) + /// span의 실제 시각 범위 (하루 시작 시간·주 시작 요일 반영) + func spanRange(_ span: StatSpan, now: Date = .now) -> Range { + switch span { + case .day: return math.dayRange(containing: now) + case .week: return math.weekRange(containing: now) + case .month: return math.monthRange(containing: now) + } + } + /// span 구간에서의 값과, 주기 목표량을 span에 맞게 환산한 목표 func spanProgress(_ span: StatSpan, now: Date = .now) -> QuestProgressResult { - let range: Range - switch span { - case .day: range = math.dayRange(containing: now) - case .week: range = math.weekRange(containing: now) - case .month: range = math.monthRange(containing: now) - } + let range = spanRange(span, now: now) return QuestProgressResult( value: spanValue(for: span, range: range, now: now), target: scaledTarget(for: span, range: range, now: now), diff --git a/myApp/HaruDanim/Widgets/Localizable.xcstrings b/myApp/HaruDanim/Widgets/Localizable.xcstrings index 66ef3ae..9709116 100644 --- a/myApp/HaruDanim/Widgets/Localizable.xcstrings +++ b/myApp/HaruDanim/Widgets/Localizable.xcstrings @@ -3282,6 +3282,22 @@ } } } + }, + "0분": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "0m" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "0分" + } + } + } } }, "version": "1.0" diff --git a/myApp/HaruDanim/Widgets/QuestRingWidget.swift b/myApp/HaruDanim/Widgets/QuestRingWidget.swift index 7cab055..b552a8b 100644 --- a/myApp/HaruDanim/Widgets/QuestRingWidget.swift +++ b/myApp/HaruDanim/Widgets/QuestRingWidget.swift @@ -133,6 +133,11 @@ struct QuestRingActionCellView: View { Text(percentText) .font(.caption2.monospacedDigit()) .foregroundStyle(.secondary) + // span 누적값 — '이하 유지'는 퍼센트가 없어 이 줄이 유일한 현재 수치 + Text(cell.valueLabel) + .font(.system(size: 9, weight: .medium).monospacedDigit()) + .foregroundStyle(.secondary) + .lineLimit(1) if let streak = cell.streakLabel { HStack(spacing: 2) { Image(systemName: "flame.fill") diff --git a/myApp/HaruDanim/Widgets/WidgetSupport.swift b/myApp/HaruDanim/Widgets/WidgetSupport.swift index cac17e4..fbed7e3 100644 --- a/myApp/HaruDanim/Widgets/WidgetSupport.swift +++ b/myApp/HaruDanim/Widgets/WidgetSupport.swift @@ -95,6 +95,11 @@ struct QuestCellSnapshot: Identifiable { let isRunning: Bool /// 연속 달성 문구 ("연속 3일" 등). 1 미만이거나 특정 기간 다짐이면 nil (표시 생략) let streakLabel: String? + /// span 구간의 실제 누적값 문구 ("1시간 20분" / "3회") — ③ 다짐 진행률 위젯에서 + /// 퍼센트(또는 '한도 지킴') 문구 아래에 표시. 특히 '이하 유지'는 퍼센트가 없어 + /// 이 값이 없으면 지금 얼마나 했는지 알 수 없다. 진행률 캡(주간·월간 하루 기여 제한)과 + /// 무관한 원본 누적값. + let valueLabel: String var color: Color { Color(hex: colorHex) } @@ -104,6 +109,14 @@ struct QuestCellSnapshot: Identifiable { let result = progress.spanProgress(span, now: now) let streak = progress.streak(now: now) let action = quest.targetAction + let rawValue = progress.value(in: progress.spanRange(span, now: now), now: now) + let valueLabel: String + switch quest.measure { + case .time: + valueLabel = rawValue < 60 ? String(localized: "0분") : Format.durationShort(rawValue) + case .count: + valueLabel = String(localized: "\(Int(rawValue))회") + } return QuestCellSnapshot( id: quest.uuid, targetName: quest.targetName, @@ -116,7 +129,8 @@ struct QuestCellSnapshot: Identifiable { isAchieved: result.isAchieved, runTarget: action.map(ActionEntity.init), isRunning: action?.isRunning ?? false, - streakLabel: (streak?.count ?? 0) > 0 ? streak?.label : nil + streakLabel: (streak?.count ?? 0) > 0 ? streak?.label : nil, + valueLabel: valueLabel ) } } @@ -180,19 +194,23 @@ extension QuestCellSnapshot { QuestCellSnapshot(id: UUID(), targetName: String(localized: "독서"), symbolName: "book.fill", colorHex: "#2F6B4F", ratio: 0.75, displayRatio: 0.75, isAtMost: false, isAchieved: false, runTarget: nil, isRunning: true, - streakLabel: QuestStreakInfo(count: 5, unit: .day).label), + streakLabel: QuestStreakInfo(count: 5, unit: .day).label, + valueLabel: Format.durationShort(45 * 60)), QuestCellSnapshot(id: UUID(), targetName: String(localized: "달리기"), symbolName: "figure.run", colorHex: "#9D5B4A", ratio: 0.4, displayRatio: 0.4, isAtMost: false, isAchieved: false, runTarget: nil, isRunning: false, - streakLabel: nil), + streakLabel: nil, + valueLabel: Format.durationShort(12 * 60)), QuestCellSnapshot(id: UUID(), targetName: String(localized: "물 마시기"), symbolName: "drop.fill", colorHex: "#4A7A9D", ratio: 1, displayRatio: 1.1, isAtMost: false, isAchieved: true, runTarget: nil, isRunning: false, - streakLabel: QuestStreakInfo(count: 12, unit: .day).label), + streakLabel: QuestStreakInfo(count: 12, unit: .day).label, + valueLabel: String(localized: "\(9)회")), QuestCellSnapshot(id: UUID(), targetName: String(localized: "영상 시청"), symbolName: "play.rectangle.fill", colorHex: "#7A5C9D", ratio: 1, displayRatio: 1, isAtMost: true, isAchieved: true, runTarget: nil, isRunning: false, - streakLabel: nil), + streakLabel: nil, + valueLabel: Format.durationShort(40 * 60)), ] }