fix: correct daily goal progress calculation and optimize widget refresh logic

- 목표 하루 진행률은 오늘이 수행일인 다짐만 모수로 계산 (요일·날짜 지정으로
  오늘 쉬는 다짐은 분모에서 제외, QuestProgress.isScheduled 신설)
- 오늘 수행할 다짐이 하나도 없는 날은 0% 대신 100%(완료 상태)로 표기
- 주간/월간 진행률은 기존대로 전체 다짐 기준 유지
- 이벤트 기반 위젯 새로 고침: 앱 내 모든 핵심 Mutation(측정 시작/종료,
  횟수 추가, 기록 추가·수정·삭제, 행동·꼬리표·목표·다짐 CRUD, 순서 변경,
  목표 상태 변경)의 끝단에서 DataChange.commit() 호출 — 저장 즉시 확정 후
  Live Activity·위젯 타임라인·워치 스냅숏을 한 번에 동기화
- 주기적 백그라운드 새로 고침 없이 변경 시점에만 갱신해 WidgetKit
  업데이트 예산 절약 (타이머 표시는 기존 Text(style: .timer)로 시스템이
  자체 렌더링하므로 타임라인 소모 없음)
- 하루 시작 시간·주 시작 요일 설정 변경 시에도 위젯 집계 갱신
- 시뮬레이터 검증: 월·수·금 다짐이 있는 목표의 토요일 하루 진행률이
  33%가 아닌 50%(오늘 수행 다짐 2개 평균)로 표시됨 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-11 08:30:12 +09:00
parent 081c388f00
commit 2567b53b71
10 changed files with 421 additions and 333 deletions

View File

@ -0,0 +1,23 @@
//
// DataChange.swift
// Haru_Danim
//
// Mutation .
//
// WidgetKit . (/ IntentStore.commit() )
//
import SwiftData
import WidgetKit
@MainActor
enum DataChange {
/// ( App Group autosave )
/// Live Activity .
static func commit(context: ModelContext) {
try? context.save()
LiveActivityManager.sync(context: context)
WidgetCenter.shared.reloadAllTimelines()
WatchSyncManager.shared.pushSnapshot()
}
}

View File

@ -1,6 +1,162 @@
{
"sourceLanguage" : "ko",
"strings" : {
"'%@ · %@' %@ 진행률: %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "'%@ · %@' %@ progress: %@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "「%@ · %@」の%@進捗率: %@"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "'%1$@ · %2$@' %3$@ 진행률: %4$@"
}
}
}
},
"'%@' %@ 진행률은 %lld%%예요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "'%@' %@ progress is %lld%%."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "「%@」の%@進捗率は%lld%%です。"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "'%1$@' %2$@ 진행률은 %3$lld%%예요."
}
}
}
},
"%@ 꼬리표를 삭제할까요? 행동은 삭제되지 않아요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete the tag %@? Its actions won't be deleted."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "タグ「%@」を削除しますか?アクションは削除されません。"
}
}
}
},
"%@ 목표를 달성했나요?" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Did you achieve the goal %@?"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標「%@」を達成しましたか?"
}
}
}
},
"%@ 목표를 삭제할까요? 다짐도 함께 삭제됩니다." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete the goal %@? Its quests will also be deleted."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標「%@」を削除しますか?クエストも一緒に削除されます。"
}
}
}
},
"'%@' 목표에는 아직 다짐이 없어요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The goal '%@' has no quests yet."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標「%@」にはまだクエストがありません。"
}
}
}
},
"%@ 행동을 삭제할까요? 기록도 함께 삭제됩니다." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete the action %@? Its records will also be deleted."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "アクション「%@」を削除しますか?記録も一緒に削除されます。"
}
}
}
},
"'%@'은(는) 시간 측정 행동이에요. 측정 시작/종료를 사용해 주세요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "'%@' is a time-tracking action. Use start/stop tracking instead."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "「%@」は時間計測タイプです。計測の開始/終了を使ってください。"
}
}
}
},
"'%@'은(는) 횟수 기록 행동이에요. 횟수 추가를 사용해 주세요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "'%@' is a count action. Use add count instead."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "「%@」は回数記録タイプです。回数の追加を使ってください。"
}
}
}
},
"%@" : {
"localizations" : {
"en" : {
@ -17,6 +173,50 @@
}
}
},
"%@ · %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ · %@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ · %@"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ · %2$@"
}
}
}
},
"%@ · %@ %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ · %@ %@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ · %@ %@"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ · %2$@ %3$@"
}
}
}
},
"%@ %@" : {
"localizations" : {
"en" : {
@ -149,50 +349,6 @@
}
}
},
"%@ · %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ · %@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ · %@"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ · %2$@"
}
}
}
},
"%@ · %@ %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ · %@ %@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%@ · %@ %@"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ · %2$@ %3$@"
}
}
}
},
"%@ 기록" : {
"localizations" : {
"en" : {
@ -407,22 +563,6 @@
}
}
},
"%lld%%" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld%%"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld%%"
}
}
}
},
"%lld/%lld" : {
"localizations" : {
"en" : {
@ -445,6 +585,22 @@
}
}
},
"%lld%%" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld%%"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld%%"
}
}
}
},
"%lld개" : {
"localizations" : {
"en" : {
@ -579,98 +735,6 @@
}
}
},
"'%@ · %@' %@ 진행률: %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "'%@ · %@' %@ progress: %@"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "「%@ · %@」の%@進捗率: %@"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "'%1$@ · %2$@' %3$@ 진행률: %4$@"
}
}
}
},
"'%@' %@ 진행률은 %lld%%예요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "'%@' %@ progress is %lld%%."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "「%@」の%@進捗率は%lld%%です。"
}
},
"ko" : {
"stringUnit" : {
"state" : "new",
"value" : "'%1$@' %2$@ 진행률은 %3$lld%%예요."
}
}
}
},
"'%@' 목표에는 아직 다짐이 없어요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The goal '%@' has no quests yet."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標「%@」にはまだクエストがありません。"
}
}
}
},
"'%@'은(는) 시간 측정 행동이에요. 측정 시작/종료를 사용해 주세요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "'%@' is a time-tracking action. Use start/stop tracking instead."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "「%@」は時間計測タイプです。計測の開始/終了を使ってください。"
}
}
}
},
"'%@'은(는) 횟수 기록 행동이에요. 횟수 추가를 사용해 주세요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "'%@' is a count action. Use add count instead."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "「%@」は回数記録タイプです。回数の追加を使ってください。"
}
}
}
},
"+%lld" : {
"localizations" : {
"en" : {
@ -719,22 +783,6 @@
}
}
},
"10초" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "10 sec"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "10秒"
}
}
}
},
"1분" : {
"localizations" : {
"en" : {
@ -751,22 +799,6 @@
}
}
},
"30초" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "30 sec"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "30秒"
}
}
}
},
"3초" : {
"localizations" : {
"en" : {
@ -799,82 +831,34 @@
}
}
},
"iCloud로 아이패드 등 다른 기기와 데이터가 동기화돼요. 변경 사항은 앱을 완전히 종료했다가 다시 실행하면 적용돼요." : {
"10초" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sync your data with iPad and other devices via iCloud. Changes take effect after fully quitting and relaunching the app."
"value" : "10 sec"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "iCloudでiPadなどの他のデバイスとデータを同期します。変更はアプリを完全に終了して再起動すると反映されます。"
"value" : "10秒"
}
}
}
},
"%@ 꼬리표를 삭제할까요? 행동은 삭제되지 않아요." : {
"30초" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete the tag %@? Its actions won't be deleted."
"value" : "30 sec"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "タグ「%@」を削除しますか?アクションは削除されません。"
}
}
}
},
"%@ 목표를 달성했나요?" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Did you achieve the goal %@?"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標「%@」を達成しましたか?"
}
}
}
},
"%@ 목표를 삭제할까요? 다짐도 함께 삭제됩니다." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete the goal %@? Its quests will also be deleted."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標「%@」を削除しますか?クエストも一緒に削除されます。"
}
}
}
},
"%@ 행동을 삭제할까요? 기록도 함께 삭제됩니다." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Delete the action %@? Its records will also be deleted."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "アクション「%@」を削除しますか?記録も一緒に削除されます。"
"value" : "30秒"
}
}
}
@ -927,6 +911,38 @@
}
}
},
"iCloud로 아이패드 등 다른 기기와 데이터가 동기화돼요. 변경 사항은 앱을 완전히 종료했다가 다시 실행하면 적용돼요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sync your data with iPad and other devices via iCloud. Changes take effect after fully quitting and relaunching the app."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "iCloudでiPadなどの他のデバイスとデータを同期します。変更はアプリを完全に終了して再起動すると反映されます。"
}
}
}
},
"값" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Value"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "値"
}
}
}
},
"가장 나중에 시작한 행동" : {
"localizations" : {
"en" : {
@ -959,23 +975,8 @@
}
}
},
"값" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Value"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "値"
}
}
}
},
"개발용" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@ -2471,6 +2472,23 @@
}
}
},
"목" : {
"comment" : "요일 축약(목요일)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Thu"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "木"
}
}
}
},
"모음" : {
"localizations" : {
"en" : {
@ -2503,23 +2521,6 @@
}
}
},
"목" : {
"comment" : "요일 축약(목요일)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Thu"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "木"
}
}
}
},
"목록" : {
"localizations" : {
"en" : {
@ -2904,22 +2905,6 @@
}
}
},
"목표의 하루/주간/월간 진행률(%)을 가져와요. 소속 다짐들의 진행률 평균이에요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gets a goal's daily/weekly/monthly progress (%), averaged across its quests."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標の日/週/月の進捗率(%)を取得します。所属クエストの平均です。"
}
}
}
},
"목표의 하루·주간·월간 진행률을 진행바로 확인해요. (프리미엄)" : {
"localizations" : {
"en" : {
@ -2936,6 +2921,22 @@
}
}
},
"목표의 하루/주간/월간 진행률(%)을 가져와요. 소속 다짐들의 진행률 평균이에요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gets a goal's daily/weekly/monthly progress (%), averaged across its quests."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "目標の日/週/月の進捗率(%)を取得します。所属クエストの平均です。"
}
}
}
},
"무료 버전에서는 목표당 다짐을 최대 %lld개까지 만들 수 있어요." : {
"localizations" : {
"en" : {
@ -4143,6 +4144,22 @@
}
}
},
"오른쪽 위 ‘순서’를 누르면 다짐 순서를 바꿀 수 있어요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tap Reorder in the top right to reorder quests."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "右上の「順序」からクエストを並べ替えられます。"
}
}
}
},
"오른쪽 위 + 버튼으로 꼬리표를 추가하세요." : {
"localizations" : {
"en" : {
@ -4175,22 +4192,6 @@
}
}
},
"오른쪽 위 ‘순서’를 누르면 다짐 순서를 바꿀 수 있어요." : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tap Reorder in the top right to reorder quests."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "右上の「順序」からクエストを並べ替えられます。"
}
}
}
},
"오른쪽 핸들을 끌어서 순서를 바꾼 뒤 ‘완료’를 누르세요. 순서는 저장되어 유지돼요." : {
"localizations" : {
"en" : {
@ -4448,6 +4449,23 @@
}
}
},
"일" : {
"comment" : "요일 축약(일요일)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sun"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "日"
}
}
}
},
"이동" : {
"localizations" : {
"en" : {
@ -4592,23 +4610,6 @@
}
}
},
"일" : {
"comment" : "요일 축약(일요일)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sun"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "日"
}
}
}
},
"일·업무" : {
"localizations" : {
"en" : {
@ -5683,6 +5684,7 @@
}
},
"프리미엄 상태 (테스트)" : {
"extractionState" : "stale",
"localizations" : {
"en" : {
"stringUnit" : {
@ -6588,4 +6590,4 @@
}
},
"version" : "1.0"
}
}

View File

@ -280,6 +280,7 @@ struct ActionDetailView: View {
.alert("행동 삭제", isPresented: $showingDelete) {
Button("삭제", role: .destructive) {
context.delete(action)
DataChange.commit(context: context)
dismiss()
}
Button("취소", role: .cancel) {}
@ -471,6 +472,7 @@ struct ActionEditorView: View {
newAction.tags = tags
context.insert(newAction)
}
DataChange.commit(context: context)
dismiss()
}
}

View File

@ -141,9 +141,13 @@ struct GoalListView: View {
}
.onAppear {
let math = DayMath()
var statusChanged = false
for goal in goals {
let before = goal.status
goal.evaluateIfEnded(math: math)
if goal.status != before { statusChanged = true }
}
if statusChanged { DataChange.commit(context: context) }
#if DEBUG
// : -goalReorder YES , -goalScrollBottom YES
if UserDefaults.standard.bool(forKey: "goalReorder") {
@ -179,6 +183,7 @@ struct GoalListView: View {
for (index, goal) in ordered.enumerated() {
goal.sortOrder = index
}
DataChange.commit(context: context)
}
/// /
@ -248,8 +253,14 @@ struct GoalRow: View {
}
.padding(.vertical, 2)
.alert("목표 확인", isPresented: $showingConfirm) {
Button("달성했어요") { goal.status = .achieved }
Button("달성하지 못했어요", role: .destructive) { goal.status = .notAchieved }
Button("달성했어요") {
goal.status = .achieved
DataChange.commit(context: context)
}
Button("달성하지 못했어요", role: .destructive) {
goal.status = .notAchieved
DataChange.commit(context: context)
}
Button("취소", role: .cancel) {}
} message: {
Text("\(goal.title) 목표를 달성했나요?")
@ -400,6 +411,7 @@ struct GoalDetailView: View {
.swipeActions {
Button("삭제", role: .destructive) {
context.delete(quest)
DataChange.commit(context: context)
}
}
}
@ -409,6 +421,7 @@ struct GoalDetailView: View {
for (index, quest) in ordered.enumerated() {
quest.sortOrder = index
}
DataChange.commit(context: context)
}
Button {
if !premium.canAddQuest(currentCount: goal.quests.count) {
@ -483,6 +496,8 @@ struct GoalDetailView: View {
Button("종료") {
if goal.manualFinish() == nil {
askManualResult = true
} else {
DataChange.commit(context: context)
}
}
Button("취소", role: .cancel) {}
@ -490,8 +505,14 @@ struct GoalDetailView: View {
Text("목표를 지금 종료할까요?")
}
.alert("목표 확인", isPresented: $askManualResult) {
Button("달성했어요") { goal.status = .achieved }
Button("달성하지 못했어요", role: .destructive) { goal.status = .notAchieved }
Button("달성했어요") {
goal.status = .achieved
DataChange.commit(context: context)
}
Button("달성하지 못했어요", role: .destructive) {
goal.status = .notAchieved
DataChange.commit(context: context)
}
Button("취소", role: .cancel) {}
} message: {
Text("다짐이 없는 목표예요. 달성했나요?")
@ -499,6 +520,7 @@ struct GoalDetailView: View {
.alert("목표 삭제", isPresented: $showingDelete) {
Button("삭제", role: .destructive) {
context.delete(goal)
DataChange.commit(context: context)
dismiss()
}
Button("취소", role: .cancel) {}
@ -617,6 +639,7 @@ struct GoalEditorView: View {
newGoal.sortOrder = (existing.map(\.sortOrder).max() ?? -1) + 1
context.insert(newGoal)
}
DataChange.commit(context: context)
dismiss()
}
}

View File

@ -71,6 +71,8 @@ struct MainView: View {
withAnimation {
isEditing.toggle()
}
//
if !isEditing { DataChange.commit(context: context) }
}
.fontWeight(isEditing ? .bold : .regular)
}
@ -97,7 +99,7 @@ struct MainView: View {
) { action in
Button("삭제", role: .destructive) {
context.delete(action)
LiveActivityManager.sync(context: context)
DataChange.commit(context: context)
deletingAction = nil
}
Button("취소", role: .cancel) { deletingAction = nil }
@ -326,11 +328,12 @@ struct MainView: View {
finish(session)
} else {
context.insert(TimeSession(action: action, startAt: .now))
LiveActivityManager.sync(context: context)
DataChange.commit(context: context)
}
case .count:
let entry = CountEntry(action: action, timestamp: .now)
context.insert(entry)
DataChange.commit(context: context)
if action.promptsForNote {
memoEntry = entry
}
@ -348,7 +351,7 @@ struct MainView: View {
memoSession = session
}
}
LiveActivityManager.sync(context: context)
DataChange.commit(context: context)
}
private func stop(_ session: TimeSession) {

View File

@ -314,6 +314,7 @@ struct QuestEditorView: View {
target.sortOrder = (goal.quests.map(\.sortOrder).max() ?? -1) + 1
context.insert(target)
}
DataChange.commit(context: context)
dismiss()
}
}

View File

@ -60,7 +60,7 @@ struct ActionRecordsSheet: View {
.swipeActions {
Button("삭제", role: .destructive) {
context.delete(session)
LiveActivityManager.sync(context: context)
DataChange.commit(context: context)
}
}
}
@ -80,6 +80,7 @@ struct ActionRecordsSheet: View {
.swipeActions {
Button("삭제", role: .destructive) {
context.delete(entry)
DataChange.commit(context: context)
}
}
}
@ -208,7 +209,7 @@ struct SessionEditorView: View {
Section {
Button("기록 삭제", role: .destructive) {
context.delete(session)
LiveActivityManager.sync(context: context)
DataChange.commit(context: context)
dismiss()
}
}
@ -224,7 +225,7 @@ struct SessionEditorView: View {
session.startAt = startAt
session.endAt = isFinished ? max(endAt, startAt) : nil
session.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
LiveActivityManager.sync(context: context)
DataChange.commit(context: context)
dismiss()
}
}
@ -276,6 +277,7 @@ struct SessionAddView: View {
let session = TimeSession(action: action, startAt: startAt, endAt: max(endAt, startAt))
session.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
context.insert(session)
DataChange.commit(context: context)
dismiss()
}
}
@ -313,6 +315,7 @@ struct CountEntryEditorView: View {
Section {
Button("기록 삭제 (증가 취소)", role: .destructive) {
context.delete(entry)
DataChange.commit(context: context)
dismiss()
}
}
@ -328,6 +331,7 @@ struct CountEntryEditorView: View {
entry.timestamp = timestamp
entry.amount = amount
entry.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
DataChange.commit(context: context)
dismiss()
}
}
@ -393,6 +397,7 @@ struct CountAddView: View {
)
entry.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
context.insert(entry)
DataChange.commit(context: context)
dismiss()
}
}

View File

@ -152,6 +152,13 @@ struct SettingsView: View {
// ' '
WidgetCenter.shared.reloadAllTimelines()
}
.onChange(of: weekStartWeekday) {
// / ·
WidgetCenter.shared.reloadAllTimelines()
}
.onChange(of: dayStartMinutes) {
WidgetCenter.shared.reloadAllTimelines()
}
.onChange(of: language) {
// :
UserDefaults.standard.set([language], forKey: "AppleLanguages")

View File

@ -49,6 +49,7 @@ struct TagListView: View {
for (index, tag) in ordered.enumerated() {
tag.sortOrder = index
}
DataChange.commit(context: context)
}
if tags.isEmpty {
ContentUnavailableView(
@ -91,6 +92,7 @@ struct TagListView: View {
) { tag in
Button("삭제", role: .destructive) {
context.delete(tag)
DataChange.commit(context: context)
deletingTag = nil
}
Button("취소", role: .cancel) { deletingTag = nil }
@ -182,6 +184,7 @@ struct TagEditorView: View {
newTag.sortOrder = (allTags.map(\.sortOrder).max() ?? -1) + 1
context.insert(newTag)
}
DataChange.commit(context: context)
dismiss()
}
}

View File

@ -90,6 +90,19 @@ struct QuestProgress {
math.dayKeys(in: range).filter(isActiveDay).count
}
/// ( ).
/// / , .
func isScheduled(on now: Date) -> Bool {
switch quest.period {
case .daily:
return isActiveDay(math.dayKey(for: now))
case .weekly, .monthly:
return true
case .custom:
return currentPeriodRange(now: now)?.contains(now) ?? false
}
}
// MARK:
/// . nil.
@ -185,10 +198,16 @@ extension Goal {
func combinedSpanRatio(_ span: StatSpan, math: DayMath = DayMath(), now: Date = .now) -> Double {
let quests = sortedQuests
guard !quests.isEmpty else { return 0 }
let sum = quests.reduce(0.0) {
// ( )
let counted = span == .day
? quests.filter { QuestProgress(quest: $0, math: math).isScheduled(on: now) }
: quests
// 0% (100%)
guard !counted.isEmpty else { return 1 }
let sum = counted.reduce(0.0) {
$0 + QuestProgress(quest: $1, math: math).spanProgress(span, now: now).ratio
}
return sum / Double(quests.count)
return sum / Double(counted.count)
}
/// .