diff --git a/myApp/HaruDanim/Shared/QuestProgress.swift b/myApp/HaruDanim/Shared/QuestProgress.swift index 9e691e6..20458fd 100644 --- a/myApp/HaruDanim/Shared/QuestProgress.swift +++ b/myApp/HaruDanim/Shared/QuestProgress.swift @@ -155,12 +155,43 @@ struct QuestProgress { case .month: range = math.monthRange(containing: now) } return QuestProgressResult( - value: value(in: range, now: now), + value: spanValue(for: span, range: range, now: now), target: scaledTarget(for: span, range: range, now: now), direction: quest.direction ) } + /// span 구간의 집계 값. + /// 주간·월간에서 '이상 달성' 하루 단위 다짐은 **하루별 기여를 그날의 목표치로 제한(캡)** 한 뒤 합산한다. + /// 어떤 하루의 과도한 초과 달성이 다른 날의 미달을 덮어써 전체 진행률을 왜곡하는 것을 막기 위함이다. + /// (예: 하루 목표 5분인데 어느 날 10분을 해도 그날은 5분어치만 인정 → 다른 날 미달이 가려지지 않음) + /// 원본 기록은 그대로 두고 오직 이 계산에서만 상한을 적용하므로 기록·통계 화면의 총 누적값에는 영향이 없다. + /// - 하루 span: 그날의 실제 누적을 그대로 써서 200% 같은 초과 달성 표기를 유지한다. + /// - '이하 유지'(atMost): 값이 아니라 하루 초과 여부(100%/0%)로 판정되므로 캡 대상이 아니다(기존 로직 유지). + /// - 주/월/특정기간 단위 다짐: 기간 내 자유 누적이 정상 동작이라 캡 없이 전체 누적값을 쓴다. + private func spanValue(for span: StatSpan, range: Range, now: Date) -> Double { + guard span != .day, + quest.period == .daily, + quest.direction == .atLeast, + quest.targetValue > 0 + else { + return value(in: range, now: now) + } + let dailyTarget = quest.targetValue + let agg = Aggregator(math: math) + var sum = 0.0 + for key in math.dayKeys(in: range) where isActiveDay(key) { + let dayRange = math.dayRange(forKey: key) + let dayValue: Double + switch quest.measure { + case .time: dayValue = agg.seconds(for: quest.targetActions, in: dayRange, now: now) + case .count: dayValue = Double(agg.count(for: quest.targetActions, in: dayRange)) + } + sum += min(dayValue, dailyTarget) + } + return sum + } + /// 주기 목표량을 span 길이에 맞춰 환산 private func scaledTarget(for span: StatSpan, range: Range, now: Date) -> Double { let target = quest.targetValue