fix(quest): cap prorated span display at 100% — weekly quest showed 350% daily
- 보고된 버그: 주 2회 다짐을 목요일에 1회 수행 → 목표 탭 하루 진행률 350% (1 ÷ 주목표 2/7). 주기와 다른 span의 환산 목표는 페이스 가이드일 뿐인데 초과분까지 퍼센트로 표기되던 왜곡 - 수정: spanProgress에서 '이상 달성'이면서 환산 목표 span일 때 값을 목표에서 캡 (usesScaledTarget 분기) — 자기 주기 span(하루 다짐의 '하루' 200% 등)과 특정 기간 다짐(환산 없음)은 초과 표기 유지, '이하 유지'는 값 무캡(UI 초과 판정·페이스 경고 유지). 게이지(ratio)·달성 판정·연속·목표 종료 판정은 원래 캡·별도 경로라 무영향 — 바뀌는 것은 퍼센트 문구뿐 - 검증 도구 신설: -progressSelfTest (DEBUG) — 인메모리 컨테이너에 12시나리오·33검증(보고 케이스, 주간/월간/요일 스케줄/마감/atMost/특정 기간/잔존 deadline/무기록 + 목표 합산 회귀) 구성해 손계산 기대값과 대조, ALL PASS. 설정·날짜 고정이라 언제 실행해도 동일 판정 - 화면 확인: 목표 탭 '꾸준한 달리기'(주 2시간) 하루 233%→100%, 주간 67%·월간 30% 유지, 매일 다짐 117%/133%(자기 주기)·atMost·마감 다짐 표기 불변 - 도움말 '진행률 읽는 법'에 페이스 기준 설명 추가(en/ja 완비), CLAUDE.md §4.2 규칙·§14 인자 기재 - §2.3 l10n 루틴 강화: stringsdata를 Objects-normal/arm64로 한정 — x86_64·Release 등 낡은 구성 파일이 수정 전 문구를 계속 추출시키는 오염 발견·수정 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
parent
bdc771455b
commit
3ae49faad7
@ -67,9 +67,12 @@ xcodebuild -project Haru_Danim.xcodeproj -scheme Haru_Danim-Store \
|
||||
# 1) Debug 빌드 → 2) stringsdata 동기화 — ⚠️ 카탈로그마다 "자기 타깃"의 stringsdata만 넣을 것
|
||||
# (경로의 <타깃명>.build로 구분. 전체를 한꺼번에 넣으면 위젯 전용 문자열이
|
||||
# IOS 카탈로그에 미번역으로 추가됐다가 Xcode 재저장 때 stale로 남는 오염이 생긴다)
|
||||
app_files=(${(f)"$(find ~/Library/Developer/Xcode/DerivedData/Haru_Danim-*/ -name '*.stringsdata' -path '*Debug-iphonesimulator/Haru_Danim.build/*')"})
|
||||
# ⚠️ 반드시 Objects-normal/arm64로 한정 — DerivedData에는 x86_64·Release·iphoneos 등
|
||||
# 다른 구성의 낡은 stringsdata가 남아 있어, 넓은 패턴으로 잡으면 방금 수정한 문구의
|
||||
# 옛 버전이 계속 "추출 중"으로 부활한다(진행률 도움말 수정 때 실제 발생)
|
||||
app_files=(${(f)"$(find ~/Library/Developer/Xcode/DerivedData/Haru_Danim-*/ -name '*.stringsdata' -path '*Debug-iphonesimulator/Haru_Danim.build/Objects-normal/arm64/*')"})
|
||||
xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${app_files[@]}"
|
||||
widget_files=(${(f)"$(find ~/Library/Developer/Xcode/DerivedData/Haru_Danim-*/ -name '*.stringsdata' -path '*Debug-iphonesimulator/Haru_DanimWidgets.build/*')"})
|
||||
widget_files=(${(f)"$(find ~/Library/Developer/Xcode/DerivedData/Haru_Danim-*/ -name '*.stringsdata' -path '*Debug-iphonesimulator/Haru_DanimWidgets.build/Objects-normal/arm64/*')"})
|
||||
xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_files[@]}"
|
||||
# 3) python으로 en/ja 번역 채움 → 4) sync 재실행으로 포맷 정규화 → 5) missing 확인
|
||||
```
|
||||
@ -112,7 +115,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
|
||||
### 4.2 다짐 진행률 (QuestProgress)
|
||||
- `ratio`(게이지용, 0...1): atLeast = min(value/target, 1) / **atMost = 한도 안이면 1, 넘으면 0**
|
||||
- `displayRatio`(퍼센트 문구): atLeast는 100% 초과 가능
|
||||
- `displayRatio`(퍼센트 문구): atLeast는 100% 초과 가능 — 단 **주기와 다른 span의 환산 목표(페이스 가이드)에서는 값을 목표에서 캡해 초과 표기 없음**(주 2회 다짐을 오늘 1회 한 것이 "하루 350%"로 표기되던 왜곡 수정 — `spanProgress`의 `usesScaledTarget` 분기). 자기 주기 span(하루 다짐의 '하루' 등)과 특정 기간 다짐(환산 없음)은 초과 표기 유지, atMost는 값 무캡(하루 게이지의 환산 한도 초과 0%는 페이스 경고로 유효). 수치 검증은 `-progressSelfTest`(§14)
|
||||
- **주간·월간 span에서 '이상 달성' 하루 단위 다짐은 하루별 기여를 그날 목표량으로 캡** — 어느 날의 초과 달성이 다른 날 미달을 가리지 않음 (하루 span·atMost·주/월/기간 다짐은 캡 없음, 원본 기록 무영향)
|
||||
- `scaledTarget`: 주기 목표량을 span 길이에 환산 (daily→적용일 수×목표, weekly→/7 등)
|
||||
- `isScheduled(on:)`: 하루 진행률 모수 판정 — 수행일 아닌 daily 다짐은 그날 모수에서 제외
|
||||
@ -320,7 +323,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
|---|---|
|
||||
| 모음 | `-startEditing` `-expandGoalCard` `-pinGoals <N>` `-openStatsFor "이름"` `-openHistoryFor "이름"` `-autoStart "이름"` |
|
||||
| 측정 알림 | `-settings.longSessionAlertHours <N>`(설정 주입) `-longSessionAlertTestSeconds <N>`(시간 대신 N초 발화, provisional 권한 자동) `-alertDump`(예약 상태 → Documents/session-alerts.txt) |
|
||||
| 목표 | `-goalShowEditor` `-goalShowFinished` `-goalReorder` `-goalScrollBottom` `-questShowEditor`(다짐 편집기 표시 — 마감 시각 다짐 우선, 없으면 첫 다짐) `-endGoalYesterday "제목"`(종료일을 어제로 — 실행 시점 자동 판정 검증) `-streakDump`(모든 다짐의 연속 계산 결과 → Documents/streak-dump.txt) |
|
||||
| 목표 | `-goalShowEditor` `-goalShowFinished` `-goalReorder` `-goalScrollBottom` `-questShowEditor`(다짐 편집기 표시 — 마감 시각 다짐 우선, 없으면 첫 다짐) `-endGoalYesterday "제목"`(종료일을 어제로 — 실행 시점 자동 판정 검증) `-streakDump`(모든 다짐의 연속 계산 결과 → Documents/streak-dump.txt) `-progressSelfTest`(spanProgress 수치 33케이스 자가 검증 → Documents/progress-self-test.txt — 인메모리 컨테이너·설정 고정이라 실데이터/기기 설정 무영향) |
|
||||
| 기록/통계 | `-historyMode timetable` `-historyWeekly` `-excludeActions "이름,이름"` `-historyShowFilter` `-statShowFilter` `-statSpan <day|week|month>` `-statScrollBottom` `-showExport` `-exportDump`(Documents/export-dump.png) |
|
||||
| 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-premiumPreview` `-helpPreview` `-dataExportPreview`(+`-dataExportRun`이면 CSV 자동 생성 후 Documents 복사) `-widgetPreview YES|lock` `-widgetPreviewScroll <앵커>` `-widgetRenderMode accented|vibrant`(틴트/투명 홈 렌더링 분기 검증) `cloudSync`(standard bool로 강제) `-intentSmokeTest`(시리 인텐트 10개 시나리오 실행 → Documents/intent-smoke-test.txt, -seedDemo·-premium과 함께) |
|
||||
| 버블 | `-radialExpanded` `-radialDoubleTapTest`(2초 뒤 FAB 더블탭 재현 — `-settings.radialDoubleTapTab <탭>`과 함께) |
|
||||
|
||||
@ -49,6 +49,7 @@ struct ContentView: View {
|
||||
DebugSeed.pinGoalsIfRequested(context: context)
|
||||
DebugSeed.endGoalYesterdayIfRequested(context: context)
|
||||
DebugSeed.dumpStreaksIfRequested(context: context)
|
||||
DebugSeed.runProgressSelfTestIfRequested()
|
||||
await DebugSeed.runIntentSmokeTestIfRequested()
|
||||
// 검증용: -themeAutoToggle YES → 3초 후 설정 화면과 같은 경로(App Group defaults)로
|
||||
// 테마를 반전시켜 '재시작 없이 즉시 적용'되는지 확인
|
||||
|
||||
@ -155,6 +155,183 @@ enum DebugSeed {
|
||||
log("완료", "스모크 테스트 끝")
|
||||
}
|
||||
|
||||
/// `-progressSelfTest YES` 런치 인자: 다짐 진행률(spanProgress) 수치 검증.
|
||||
/// 인메모리 컨테이너에 시나리오들을 구성해 하루/주간/월간의 퍼센트 문구(displayRatio)·
|
||||
/// 게이지(ratio)·값을 손계산 기대값과 대조하고 결과를 Documents/progress-self-test.txt로 남긴다.
|
||||
/// 설정은 주 시작 월요일·하루 시작 00:00으로 고정하고 기준 시각은 이번 주 목요일 12:00 —
|
||||
/// 기기 설정·실행 날짜와 무관하게 항상 같은 판정이 나온다 (실데이터 무영향).
|
||||
/// 핵심 규칙: 주기와 다른 span의 환산 목표는 '이상 달성' 초과 표기를 100%에서 캡,
|
||||
/// 자기 주기 span·특정 기간은 초과 표기 유지, '이하 유지'는 값 무캡 (§4.2).
|
||||
@MainActor
|
||||
static func runProgressSelfTestIfRequested() {
|
||||
guard UserDefaults.standard.bool(forKey: "progressSelfTest") else { return }
|
||||
var lines: [String] = []
|
||||
var failures = 0
|
||||
|
||||
func expect(_ label: String, _ actual: Double, _ expected: Double) {
|
||||
let pass = abs(actual - expected) < 0.0005
|
||||
if !pass { failures += 1 }
|
||||
lines.append("\(pass ? "PASS" : "FAIL") \(label): actual \(String(format: "%.4f", actual)) / expected \(String(format: "%.4f", expected))")
|
||||
}
|
||||
|
||||
do {
|
||||
let config = ModelConfiguration(isStoredInMemoryOnly: true)
|
||||
let container = try ModelContainer(for: DataStore.schema, configurations: [config])
|
||||
let context = container.mainContext
|
||||
|
||||
let math = DayMath(settings: TrackingSettings(weekStartWeekday: 2, dayStartMinutes: 0))
|
||||
let cal = math.calendar
|
||||
let weekStart = math.weekRange(containing: .now).lowerBound
|
||||
/// 주 시작(월) + offset일의 hour:minute 시각
|
||||
func at(_ dayOffset: Int, _ hour: Int, _ minute: Int = 0) -> Date {
|
||||
cal.date(byAdding: .minute, value: hour * 60 + minute,
|
||||
to: cal.date(byAdding: .day, value: dayOffset, to: weekStart)!)!
|
||||
}
|
||||
let now = at(3, 12) // 이번 주 목요일 12:00
|
||||
let monthDays = Double(math.dayKeys(in: math.monthRange(containing: now)).count)
|
||||
lines.append("기준: 목요일 12:00 (\(now)), 이번 달 일수 \(Int(monthDays))")
|
||||
|
||||
/// 시나리오 하나 = 전용 목표 + 전용 행동 + 다짐 (교차 오염 방지)
|
||||
func makeQuest(_ name: String, type: TrackingType, period: QuestPeriod,
|
||||
direction: QuestDirection = .atLeast,
|
||||
targetSeconds: Double = 3600, targetCount: Int = 1,
|
||||
configure: (Quest) -> Void = { _ in }) -> (Action, Quest) {
|
||||
let goal = Goal(title: name, symbolName: "flag.fill", colorHex: "#2F6B4F",
|
||||
startDate: cal.date(byAdding: .day, value: -30, to: now)!, endDate: nil)
|
||||
context.insert(goal)
|
||||
let action = Action(name: name, symbolName: "star.fill", trackingType: type, sortOrder: 0)
|
||||
context.insert(action)
|
||||
let quest = Quest(goal: goal)
|
||||
quest.targetAction = action
|
||||
quest.measure = type
|
||||
quest.period = period
|
||||
quest.direction = direction
|
||||
quest.targetSeconds = targetSeconds
|
||||
quest.targetCount = targetCount
|
||||
configure(quest)
|
||||
context.insert(quest)
|
||||
return (action, quest)
|
||||
}
|
||||
func add(_ action: Action, _ when: Date, amount: Int = 1) {
|
||||
context.insert(CountEntry(action: action, timestamp: when, amount: amount))
|
||||
}
|
||||
func addSession(_ action: Action, _ start: Date, _ end: Date) {
|
||||
context.insert(TimeSession(action: action, startAt: start, endAt: end))
|
||||
}
|
||||
func progress(_ quest: Quest, _ span: StatSpan) -> QuestProgressResult {
|
||||
QuestProgress(quest: quest, math: math).spanProgress(span, now: now)
|
||||
}
|
||||
|
||||
// ── A. 주 2회 다짐, 오늘 1회 (보고된 버그: 하루 350% → 100%)
|
||||
let (aAct, aQuest) = makeQuest("A 주2회", type: .count, period: .weekly, targetCount: 2)
|
||||
add(aAct, at(3, 10))
|
||||
expect("A 하루 표기(버그 수정)", progress(aQuest, .day).displayRatio, 1.0)
|
||||
expect("A 하루 게이지", progress(aQuest, .day).ratio, 1.0)
|
||||
expect("A 주간 표기", progress(aQuest, .week).displayRatio, 0.5)
|
||||
expect("A 월간 표기(환산 캡 이하)", progress(aQuest, .month).displayRatio, 7.0 / (2.0 * monthDays))
|
||||
if let goal = aQuest.goal {
|
||||
expect("A 목표 하루 진행률(회귀 무영향)", goal.combinedSpanRatio(.day, math: math, now: now), 1.0)
|
||||
expect("A 목표 주간 진행률", goal.combinedSpanRatio(.week, math: math, now: now), 0.5)
|
||||
}
|
||||
|
||||
// ── B. 주 2회 다짐, 월·화·목 3회 (자기 주기 초과 표기 유지)
|
||||
let (bAct, bQuest) = makeQuest("B 주2회 3번", type: .count, period: .weekly, targetCount: 2)
|
||||
add(bAct, at(0, 10)); add(bAct, at(1, 10)); add(bAct, at(3, 10))
|
||||
expect("B 주간 표기(자기 주기 150% 유지)", progress(bQuest, .week).displayRatio, 1.5)
|
||||
expect("B 주간 게이지", progress(bQuest, .week).ratio, 1.0)
|
||||
expect("B 하루 표기(캡)", progress(bQuest, .day).displayRatio, 1.0)
|
||||
|
||||
// ── C. 주 7시간 다짐, 오늘 2시간 (시간형도 동일 규칙: 하루 200% → 100%)
|
||||
let (cAct, cQuest) = makeQuest("C 주7시간", type: .time, period: .weekly, targetSeconds: 7 * 3600)
|
||||
addSession(cAct, at(3, 9), at(3, 11))
|
||||
expect("C 하루 표기(캡)", progress(cQuest, .day).displayRatio, 1.0)
|
||||
expect("C 주간 표기", progress(cQuest, .week).displayRatio, 2.0 / 7.0)
|
||||
|
||||
// ── D. 하루 5회 다짐, 오늘 10회 (자기 주기 200% 표기 유지 + 주간 하루 기여 캡)
|
||||
let (dAct, dQuest) = makeQuest("D 하루5회", type: .count, period: .daily, targetCount: 5)
|
||||
add(dAct, at(3, 9), amount: 10)
|
||||
expect("D 하루 표기(자기 주기 200% 유지)", progress(dQuest, .day).displayRatio, 2.0)
|
||||
expect("D 하루 게이지", progress(dQuest, .day).ratio, 1.0)
|
||||
expect("D 주간 표기(하루 기여 캡)", progress(dQuest, .week).displayRatio, 5.0 / 35.0)
|
||||
|
||||
// ── E. 월수금 하루 5회 다짐: 월 5회, 수 10회 (수는 5로 캡, 목표 = 5×적용일 3)
|
||||
let (eAct, eQuest) = makeQuest("E 월수금5회", type: .count, period: .daily, targetCount: 5) {
|
||||
$0.scheduleMode = .weekdays
|
||||
$0.weekdays = [2, 4, 6]
|
||||
}
|
||||
add(eAct, at(0, 9), amount: 5); add(eAct, at(2, 9), amount: 10)
|
||||
expect("E 주간 표기", progress(eQuest, .week).displayRatio, 10.0 / 15.0)
|
||||
expect("E 하루 표기(목요일 0회)", progress(eQuest, .day).displayRatio, 0.0)
|
||||
|
||||
// ── F. 월 10회 다짐, 오늘 1회 (하루 환산 캡 + 주간 환산 정확도)
|
||||
let (fAct, fQuest) = makeQuest("F 월10회", type: .count, period: .monthly, targetCount: 10)
|
||||
add(fAct, at(3, 10))
|
||||
expect("F 하루 표기(캡)", progress(fQuest, .day).displayRatio, 1.0)
|
||||
expect("F 주간 표기", progress(fQuest, .week).displayRatio, monthDays / 70.0)
|
||||
expect("F 월간 표기", progress(fQuest, .month).displayRatio, 0.1)
|
||||
|
||||
// ── G. 월 10회 다짐, 오늘 12회 (자기 주기 120% 유지, 하루·주간은 캡)
|
||||
let (gAct, gQuest) = makeQuest("G 월10회 12번", type: .count, period: .monthly, targetCount: 10)
|
||||
add(gAct, at(3, 10), amount: 12)
|
||||
expect("G 월간 표기(자기 주기 120% 유지)", progress(gQuest, .month).displayRatio, 1.2)
|
||||
expect("G 하루 표기(캡)", progress(gQuest, .day).displayRatio, 1.0)
|
||||
expect("G 주간 표기(캡)", progress(gQuest, .week).displayRatio, 1.0)
|
||||
|
||||
// ── H. '이하 유지' 주 7시간, 오늘 3시간 (값 무캡 — 하루 페이스 경고 0%, 주간 100%)
|
||||
let (hAct, hQuest) = makeQuest("H 이하7시간", type: .time, period: .weekly,
|
||||
direction: .atMost, targetSeconds: 7 * 3600)
|
||||
addSession(hAct, at(3, 9), at(3, 12))
|
||||
expect("H 하루 게이지(환산 한도 초과 경고)", progress(hQuest, .day).ratio, 0.0)
|
||||
expect("H 하루 값(무캡 — UI 초과 판정용)", progress(hQuest, .day).value, 3 * 3600)
|
||||
expect("H 주간 게이지(한도 안)", progress(hQuest, .week).ratio, 1.0)
|
||||
|
||||
// ── I. 특정 기간(10일) 10회 다짐, 오늘 12회 (환산 없음 — 전 span 초과 표기 유지)
|
||||
let (iAct, iQuest) = makeQuest("I 기간10회", type: .count, period: .custom, targetCount: 10) {
|
||||
$0.customStart = at(0, 0)
|
||||
$0.customEnd = at(9, 0)
|
||||
}
|
||||
add(iAct, at(3, 10), amount: 12)
|
||||
expect("I 하루 표기(기간 다짐 무캡)", progress(iQuest, .day).displayRatio, 1.2)
|
||||
expect("I 주간 표기(기간 다짐 무캡)", progress(iQuest, .week).displayRatio, 1.2)
|
||||
|
||||
// ── J. 주간 다짐에 잔존 deadlineMinutes (하루 단위 전용 — 무시돼 A와 동일해야 함)
|
||||
let (jAct, jQuest) = makeQuest("J 주2회+잔존마감", type: .count, period: .weekly, targetCount: 2) {
|
||||
$0.deadlineMinutes = 480
|
||||
}
|
||||
add(jAct, at(3, 10))
|
||||
expect("J 하루 표기(마감 무시)", progress(jQuest, .day).displayRatio, 1.0)
|
||||
expect("J 주간 표기(마감 무시)", progress(jQuest, .week).displayRatio, 0.5)
|
||||
|
||||
// ── K. 하루 1회 + 마감 08:00 (마감 회귀 확인: 07:30은 인정, 08:10은 제외)
|
||||
let (kAct, kQuest) = makeQuest("K 마감내", type: .count, period: .daily, targetCount: 1) {
|
||||
$0.deadlineMinutes = 480
|
||||
}
|
||||
add(kAct, at(3, 7, 30))
|
||||
expect("K 하루 표기(마감 안 1회)", progress(kQuest, .day).displayRatio, 1.0)
|
||||
let (k2Act, k2Quest) = makeQuest("K2 마감밖", type: .count, period: .daily, targetCount: 1) {
|
||||
$0.deadlineMinutes = 480
|
||||
}
|
||||
add(k2Act, at(3, 8, 10))
|
||||
expect("K2 하루 표기(마감 뒤 제외)", progress(k2Quest, .day).displayRatio, 0.0)
|
||||
|
||||
// ── L. 기록 없는 주간 다짐 (0% 기본)
|
||||
let (_, lQuest) = makeQuest("L 무기록", type: .count, period: .weekly, targetCount: 2)
|
||||
expect("L 하루 표기", progress(lQuest, .day).displayRatio, 0.0)
|
||||
expect("L 주간 표기", progress(lQuest, .week).displayRatio, 0.0)
|
||||
} catch {
|
||||
failures += 1
|
||||
lines.append("FAIL 컨테이너 생성: \(error)")
|
||||
}
|
||||
|
||||
lines.append(failures == 0 ? "== ALL PASS ==" : "== \(failures) FAILURES ==")
|
||||
for line in lines { print("[ProgressTest] \(line)") }
|
||||
if let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
try? lines.joined(separator: "\n")
|
||||
.write(to: docs.appendingPathComponent("progress-self-test.txt"),
|
||||
atomically: true, encoding: .utf8)
|
||||
}
|
||||
}
|
||||
|
||||
/// `-seedBulk YES` 런치 인자: 성능 검증용 대량 데이터 생성 —
|
||||
/// 과거 500일에 걸쳐 시간 행동 2개(하루 2세션) + 횟수 행동 2개(하루 8건),
|
||||
/// 총 약 1만 건. 범위 조회 전환 후에도 기록·통계 탭이 쾌적한지 확인하는 용도.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -135,7 +135,7 @@ struct HelpView: View {
|
||||
HelpTopic(
|
||||
symbol: "chart.bar.fill",
|
||||
title: "진행률 읽는 법",
|
||||
body: "다짐마다 하루·주간·월간 진행률이 표시돼요. 주간·월간 진행률은 하루 초과 달성이 다른 날의 미달을 가리지 않도록 하루 목표량까지만 인정해 합산해요. '이하 유지' 다짐은 한도를 넘지 않는 동안 100%로 표시돼요."
|
||||
body: "다짐마다 하루·주간·월간 진행률이 표시돼요. 주간·월간 진행률은 하루 초과 달성이 다른 날의 미달을 가리지 않도록 하루 목표량까지만 인정해 합산해요. 다짐 자신의 주기(하루 다짐의 '하루' 등)에서는 100%를 넘는 초과 달성이 그대로 표시되지만, 주간 다짐의 '하루'처럼 주기와 다른 기간은 목표를 그 기간 몫으로 나눈 페이스 기준이라 최대 100%까지만 표시돼요. '이하 유지' 다짐은 한도를 넘지 않는 동안 100%로 표시돼요."
|
||||
),
|
||||
HelpTopic(
|
||||
symbol: "clock.badge.checkmark",
|
||||
|
||||
@ -202,14 +202,34 @@ struct QuestProgress {
|
||||
}
|
||||
}
|
||||
|
||||
/// span 구간에서의 값과, 주기 목표량을 span에 맞게 환산한 목표
|
||||
/// span 구간에서의 값과, 주기 목표량을 span에 맞게 환산한 목표.
|
||||
/// 주기와 다른 span의 목표량은 비례 환산된 **페이스 가이드**일 뿐 실제 목표가 아니므로,
|
||||
/// '이상 달성'은 값을 환산 목표에서 캡해 초과분을 표기하지 않는다 — 주 2회 다짐을 오늘
|
||||
/// 1회 수행한 것이 "하루 350%"(1 ÷ 2/7)로 표시되던 왜곡 방지. 다짐 자신의 주기 span
|
||||
/// (하루 다짐의 '하루', 주간 다짐의 '주간' 등)은 실제 목표라 100% 초과를 그대로 보여 준다.
|
||||
/// '이하 유지'는 캡하지 않는다 — 환산 한도 대비 하루 게이지의 초과(0%) 표시는
|
||||
/// 주간 한도를 오늘 다 써 가고 있다는 페이스 경고로 유효하고, 값이 UI의 초과 판정
|
||||
/// (value > target)에 그대로 쓰인다.
|
||||
func spanProgress(_ span: StatSpan, now: Date = .now) -> QuestProgressResult {
|
||||
let range = spanRange(span, now: now)
|
||||
return QuestProgressResult(
|
||||
value: spanValue(for: span, range: range, now: now),
|
||||
target: scaledTarget(for: span, range: range, now: now),
|
||||
direction: quest.direction
|
||||
)
|
||||
let target = scaledTarget(for: span, range: range, now: now)
|
||||
var value = spanValue(for: span, range: range, now: now)
|
||||
if quest.direction == .atLeast, target > 0, usesScaledTarget(for: span) {
|
||||
value = min(value, target)
|
||||
}
|
||||
return QuestProgressResult(value: value, target: target, direction: quest.direction)
|
||||
}
|
||||
|
||||
/// 이 span의 목표량이 주기 목표를 비례 환산한 값인지.
|
||||
/// 자기 주기와 같은 span은 원 목표 그대로이고, 특정 기간 다짐은 모든 span에서
|
||||
/// 원 목표를 쓰므로(환산 없음) 항상 false — 실제 초과 달성 표기를 유지한다.
|
||||
private func usesScaledTarget(for span: StatSpan) -> Bool {
|
||||
switch quest.period {
|
||||
case .daily: return span != .day
|
||||
case .weekly: return span != .week
|
||||
case .monthly: return span != .month
|
||||
case .custom: return false
|
||||
}
|
||||
}
|
||||
|
||||
/// span 구간의 집계 값.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user