mycode/myApp/HaruDanim/IOS/Core/DebugSeed.swift
songyc macbook 3ae49faad7 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
2026-07-16 22:35:23 +09:00

684 lines
36 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// DebugSeed.swift
// Haru_Danim
//
// . `-seedDemo YES` . (DEBUG )
//
#if DEBUG
import Foundation
import SwiftData
private extension Error {
/// ( localizedStringResource )
var asIntentMessage: LocalizedStringResource {
if let convertible = self as? any CustomLocalizedStringResourceConvertible {
return convertible.localizedStringResource
}
return "\(localizedDescription)"
}
}
enum DebugSeed {
/// `-autoStart <>` (Live Activity )
static func autoStartIfRequested(context: ModelContext) {
guard let name = UserDefaults.standard.string(forKey: "autoStart") else { return }
let descriptor = FetchDescriptor<Action>(predicate: #Predicate { $0.name == name })
guard let action = try? context.fetch(descriptor).first else { return }
if action.runningSession == nil {
context.insert(TimeSession(action: action, startAt: .now))
}
LiveActivityManager.sync(context: context)
}
/// `-intentSmokeTest YES` : 6 perform()
/// . [IntentTest]
/// (`xcrun simctl launch --console` ). (-seedDemo) .
@MainActor
static func runIntentSmokeTestIfRequested() async {
guard UserDefaults.standard.bool(forKey: "intentSmokeTest") else { return }
var lines: [String] = []
func log(_ name: String, _ result: String) {
print("[IntentTest] \(name): \(result)")
lines.append("\(name): \(result)")
}
defer {
// CLI Documents
if let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
try? lines.joined(separator: "\n")
.write(to: docs.appendingPathComponent("intent-smoke-test.txt"),
atomically: true, encoding: .utf8)
}
}
func entity(_ name: String) -> ActionEntity? {
IntentStore.actions().first { $0.name == name }.map(ActionEntity.init)
}
guard let running = entity("달리기"), let water = entity("물 마시기") else {
log("준비", "실패 — 데모 행동 없음 (-seedDemo 필요)")
return
}
let math = DayMath()
let agg = Aggregator(math: math)
//
do {
let intent = StartTimeActionIntent()
intent.action = running
_ = try await intent.perform()
let model = try IntentStore.action(running.id)
log("측정 시작", model.isRunning ? "OK — 진행 중 세션 생성" : "실패 — 세션 없음")
} catch { log("측정 시작", "throw: \(String(localized: error.asIntentMessage))") }
//
do {
let intent = StartTimeActionIntent()
intent.action = running
_ = try await intent.perform()
let count = try IntentStore.action(running.id).sessions.filter { $0.endAt == nil }.count
log("중복 시작 방지", count == 1 ? "OK — 진행 세션 1개 유지" : "실패 — 진행 세션 \(count)")
} catch { log("중복 시작 방지", "throw: \(String(localized: error.asIntentMessage))") }
//
do {
let intent = StopTimeActionIntent()
intent.action = running
_ = try await intent.perform()
let model = try IntentStore.action(running.id)
log("측정 종료", model.isRunning ? "실패 — 아직 진행 중" : "OK — 종료됨")
} catch { log("측정 종료", "throw: \(String(localized: error.asIntentMessage))") }
// +3
do {
let before = agg.count(for: try IntentStore.action(water.id), in: math.dayRange(containing: .now))
let intent = AddCountIntent()
intent.action = water
intent.amount = 3
_ = try await intent.perform()
let after = agg.count(for: try IntentStore.action(water.id), in: math.dayRange(containing: .now))
log("횟수 추가", after == before + 3 ? "OK — \(before)\(after)" : "실패 — \(before)\(after)")
} catch { log("횟수 추가", "throw: \(String(localized: error.asIntentMessage))") }
// : IntentTargetError
do {
let intent = AddCountIntent()
intent.action = running
intent.amount = 1
_ = try await intent.perform()
log("타입 방어", "실패 — 시간형에 횟수 추가가 통과됨")
} catch { log("타입 방어", "OK — 거부: \(String(localized: error.asIntentMessage))") }
// -2 (RunActionIntent):
do {
_ = try await RunActionIntent(actionID: running.id.uuidString).perform()
let startedOK = try IntentStore.action(running.id).isRunning
_ = try await RunActionIntent(actionID: running.id.uuidString).perform()
let stoppedOK = try !IntentStore.action(running.id).isRunning
log("위젯 실행 토글", startedOK && stoppedOK ? "OK — 시작 후 토글 종료" : "실패 — 시작=\(startedOK) 종료=\(stoppedOK)")
} catch { log("위젯 실행 토글", "throw: \(String(localized: error.asIntentMessage))") }
// 3 throw
do {
let total = ActionTotalIntent()
total.action = running
total.span = .day
_ = try await total.perform()
log("누적값 조회", "OK — 하루 \(Format.durationShort(agg.seconds(for: try IntentStore.action(running.id), in: math.dayRange(containing: .now))))")
} catch { log("누적값 조회", "throw: \(String(localized: error.asIntentMessage))") }
do {
guard let goal = IntentStore.goals().first(where: { !$0.quests.isEmpty }) else {
log("목표 진행률", "건너뜀 — 다짐 있는 목표 없음"); return
}
let intent = GoalProgressIntent()
intent.goal = GoalEntity(goal)
intent.span = .day
_ = try await intent.perform()
log("목표 진행률", "OK — '\(goal.title)' 하루 \(Int((goal.combinedSpanRatio(.day) * 100).rounded()))%")
if let quest = goal.sortedQuests.first {
let questIntent = QuestProgressIntent()
questIntent.quest = QuestEntity(quest)
questIntent.span = .week
_ = try await questIntent.perform()
log("다짐 진행률", "OK — '\(quest.targetName)' 주간 \(Format.percent(QuestProgress(quest: quest).spanProgress(.week).displayRatio))")
}
} catch { log("목표/다짐 진행률", "throw: \(String(localized: error.asIntentMessage))") }
// : PremiumRequiredError
let wasPremium = PremiumGate.isPremium
PremiumGate.cache(false)
do {
let intent = StartTimeActionIntent()
intent.action = running
_ = try await intent.perform()
log("프리미엄 게이트", "실패 — 잠금 상태에서 실행됨")
} catch { log("프리미엄 게이트", "OK — 거부: \(String(localized: error.asIntentMessage))") }
PremiumGate.cache(wasPremium)
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 . · .
static func seedBulkIfRequested(context: ModelContext) {
guard UserDefaults.standard.bool(forKey: "seedBulk") else { return }
let marker = "벌크 시간 1"
let existing = (try? context.fetchCount(
FetchDescriptor<Action>(predicate: #Predicate { $0.name == marker })
)) ?? 0
guard existing == 0 else { return }
let tag = Tag(name: "벌크", colorHex: "#4F8A8B")
tag.sortOrder = 90
context.insert(tag)
var timeActions: [Action] = []
for (index, name) in ["벌크 시간 1", "벌크 시간 2"].enumerated() {
let action = Action(name: name, symbolName: "clock.fill", trackingType: .time,
sortOrder: 90 + index)
action.tags = [tag]
context.insert(action)
timeActions.append(action)
}
var countActions: [Action] = []
for (index, name) in ["벌크 횟수 1", "벌크 횟수 2"].enumerated() {
let action = Action(name: name, symbolName: "number", trackingType: .count,
sortOrder: 92 + index)
action.tags = [tag]
context.insert(action)
countActions.append(action)
}
let cal = Calendar.current
let today = cal.startOfDay(for: .now)
for dayOffset in 1...500 {
guard let day = cal.date(byAdding: .day, value: -dayOffset, to: today) else { continue }
for (slot, action) in timeActions.enumerated() {
for hour in [9 + slot * 2, 19 + slot] {
let start = day.addingTimeInterval(TimeInterval(hour) * 3600)
context.insert(TimeSession(action: action, startAt: start,
endAt: start.addingTimeInterval(35 * 60)))
}
}
for action in countActions {
for slot in 0..<8 {
context.insert(CountEntry(
action: action,
timestamp: day.addingTimeInterval(TimeInterval(8 + slot) * 3600)
))
}
}
}
try? context.save()
}
/// `-endGoalYesterday < >` :
/// ( )
static func endGoalYesterdayIfRequested(context: ModelContext) {
guard let title = UserDefaults.standard.string(forKey: "endGoalYesterday") else { return }
let descriptor = FetchDescriptor<Goal>(predicate: #Predicate { $0.title == title })
guard let goal = try? context.fetch(descriptor).first else { return }
goal.endDate = Calendar.current.date(byAdding: .day, value: -1, to: .now)
try? context.save()
}
/// `-streakDump` :
/// Documents/streak-dump.txt (CLI )
static func dumpStreaksIfRequested(context: ModelContext) {
guard UserDefaults.standard.bool(forKey: "streakDump") else { return }
let quests = (try? context.fetch(FetchDescriptor<Quest>())) ?? []
var lines: [String] = []
for quest in quests {
let goal = quest.goal
let streak = QuestProgress(quest: quest).streak()
lines.append(
"goal=\(goal?.title ?? "-") start=\(goal?.startDate.formatted(date: .numeric, time: .omitted) ?? "-") "
+ "quest=\(quest.targetName) period=\(quest.period) direction=\(quest.direction) "
+ "streak=\(streak.map { "\($0.count) (\($0.unit))" } ?? "nil") deadline=\(quest.deadlineMinutes)"
)
}
let url = URL.documentsDirectory.appending(path: "streak-dump.txt")
try? lines.joined(separator: "\n").write(to: url, atomically: true, encoding: .utf8)
}
/// `-pinGoals <N>` : N ( )
static func pinGoalsIfRequested(context: ModelContext) {
let n = UserDefaults.standard.integer(forKey: "pinGoals")
guard n > 0 else { return }
let descriptor = FetchDescriptor<Goal>(sortBy: [SortDescriptor(\.createdAt)])
guard let goals = try? context.fetch(descriptor) else { return }
// (LocalPrefs )
LocalPrefs.defaults.set(
LocalPrefs.rawValue(goals.prefix(n).map(\.uuid)),
forKey: LocalPrefsKeys.pinnedGoals
)
}
static func seedIfRequested(context: ModelContext) {
guard UserDefaults.standard.bool(forKey: "seedDemo") else { return }
let existing = (try? context.fetchCount(FetchDescriptor<Action>())) ?? 0
guard existing == 0 else { return }
let study = Tag(name: "공부", colorHex: "#4A7A9D")
let workout = Tag(name: "운동", colorHex: "#9D5B4A")
let life = Tag(name: "생활", colorHex: "#2F6B4F")
context.insert(study)
context.insert(workout)
context.insert(life)
let reading = Action(name: "독서", symbolName: "book.fill", trackingType: .time, sortOrder: 0)
reading.tags = [study]
reading.isFavorite = true
reading.promptsForNote = true
let english = Action(name: "영어 공부", symbolName: "graduationcap.fill", trackingType: .time, sortOrder: 1)
english.tags = [study]
let running = Action(name: "달리기", symbolName: "figure.run", trackingType: .time, sortOrder: 2)
running.tags = [workout]
let pushup = Action(name: "팔굽혀펴기", symbolName: "dumbbell.fill", trackingType: .count, sortOrder: 3)
pushup.tags = [workout]
let water = Action(name: "물 마시기", symbolName: "drop.fill", trackingType: .count, sortOrder: 4)
water.tags = [life]
water.promptsForNote = true
// ' ' ( 1 , )
let video = Action(name: "영상 시청", symbolName: "play.rectangle.fill", trackingType: .time, sortOrder: 5)
video.tags = [life]
for action in [reading, english, running, pushup, water, video] {
context.insert(action)
}
let now = Date.now
let cal = Calendar.current
func at(daysAgo: Int, hour: Int, minute: Int = 0) -> Date {
let day = cal.date(byAdding: .day, value: -daysAgo, to: now)!
return cal.date(bySettingHour: hour, minute: minute, second: 0, of: day)!
}
// ()
for daysAgo in 0...6 {
context.insert(TimeSession(action: english, startAt: at(daysAgo: daysAgo, hour: 8), endAt: at(daysAgo: daysAgo, hour: 9, minute: 10)))
if daysAgo % 2 == 0 {
context.insert(TimeSession(action: running, startAt: at(daysAgo: daysAgo, hour: 7), endAt: at(daysAgo: daysAgo, hour: 7, minute: 40)))
}
if daysAgo % 3 != 0 {
context.insert(TimeSession(action: reading, startAt: at(daysAgo: daysAgo, hour: 21), endAt: at(daysAgo: daysAgo, hour: 22, minute: 15)))
}
// : 40(1 ), 1 30()
if daysAgo % 2 == 0 {
context.insert(TimeSession(action: video, startAt: at(daysAgo: daysAgo, hour: 12, minute: 30), endAt: at(daysAgo: daysAgo, hour: 13, minute: 10)))
} else {
context.insert(TimeSession(action: video, startAt: at(daysAgo: daysAgo, hour: 12, minute: 30), endAt: at(daysAgo: daysAgo, hour: 14, minute: 0)))
}
}
// ( ) +
let crossing = TimeSession(action: reading, startAt: at(daysAgo: 1, hour: 23, minute: 20), endAt: at(daysAgo: 0, hour: 0, minute: 40))
crossing.note = "자기 전에 소설 읽음. 재밌어서 늦게 잠"
context.insert(crossing)
//
context.insert(TimeSession(action: reading, startAt: now.addingTimeInterval(-25 * 60)))
//
for daysAgo in 0...6 {
for i in 0..<(3 + daysAgo % 4) {
context.insert(CountEntry(action: water, timestamp: at(daysAgo: daysAgo, hour: 9 + i * 2)))
}
if daysAgo % 2 == 0 {
context.insert(CountEntry(action: pushup, timestamp: at(daysAgo: daysAgo, hour: 19), amount: 20))
}
}
// ( )
let notedWater = CountEntry(action: water, timestamp: at(daysAgo: 0, hour: 20))
notedWater.note = "자기 전 물 한 컵"
context.insert(notedWater)
// +
let toeic = Goal(
title: "토익 700점 이상 받기",
symbolName: "graduationcap.fill",
colorHex: "#4A7A9D",
startDate: cal.date(byAdding: .day, value: -10, to: now)!,
endDate: cal.date(byAdding: .day, value: 30, to: now)!
)
context.insert(toeic)
let englishQuest = Quest(goal: toeic)
englishQuest.targetAction = english
englishQuest.measure = .time
englishQuest.period = .daily
englishQuest.targetSeconds = 3600
englishQuest.direction = .atLeast
context.insert(englishQuest)
let health = Goal(
title: "건강한 생활 습관 만들기",
symbolName: "heart.fill",
colorHex: "#2F6B4F",
startDate: cal.date(byAdding: .day, value: -20, to: now)!,
endDate: nil
)
// 3( )
health.showsOnMain = true
context.insert(health)
let waterQuest = Quest(goal: health)
waterQuest.targetAction = water
waterQuest.measure = .count
waterQuest.period = .daily
waterQuest.targetCount = 8
waterQuest.direction = .atLeast
context.insert(waterQuest)
let runQuest = Quest(goal: health)
runQuest.targetAction = running
runQuest.measure = .time
runQuest.period = .daily
runQuest.scheduleMode = .weekdays
runQuest.weekdays = [2, 4, 6]
runQuest.targetSeconds = 30 * 60
runQuest.direction = .atLeast
context.insert(runQuest)
// 4: ' ' /
let pushupQuest = Quest(goal: health)
pushupQuest.targetAction = pushup
pushupQuest.measure = .count
pushupQuest.period = .daily
pushupQuest.targetCount = 20
pushupQuest.direction = .atLeast
context.insert(pushupQuest)
// ' ' : 1 ( )
let videoQuest = Quest(goal: health)
videoQuest.targetAction = video
videoQuest.measure = .time
videoQuest.period = .daily
videoQuest.targetSeconds = 3600
videoQuest.direction = .atMost
context.insert(videoQuest)
// ( )
let habit = Goal(
title: "매일 밤 독서 습관",
symbolName: "book.fill",
colorHex: "#9D5B4A",
startDate: cal.date(byAdding: .day, value: -5, to: now)!,
endDate: cal.date(byAdding: .day, value: 60, to: now)!
)
context.insert(habit)
let readingQuest = Quest(goal: habit)
readingQuest.targetAction = reading
readingQuest.measure = .time
readingQuest.period = .daily
readingQuest.targetSeconds = 45 * 60
readingQuest.direction = .atLeast
context.insert(readingQuest)
// ( )
let run = Goal(
title: "꾸준한 달리기",
symbolName: "figure.run",
colorHex: "#4A7A9D",
startDate: cal.date(byAdding: .day, value: -14, to: now)!,
endDate: cal.date(byAdding: .day, value: 45, to: now)!
)
context.insert(run)
let weeklyRunQuest = Quest(goal: run)
weeklyRunQuest.targetAction = running
weeklyRunQuest.measure = .time
weeklyRunQuest.period = .weekly
weeklyRunQuest.targetSeconds = 2 * 3600
weeklyRunQuest.direction = .atLeast
context.insert(weeklyRunQuest)
// : + ' '
// ( 4, 401)
let screen = Goal(
title: "스크린 타임 관리",
symbolName: "iphone",
colorHex: "#4A7A9D",
startDate: cal.date(byAdding: .day, value: -3, to: now)!,
endDate: nil
)
context.insert(screen)
let screenQuest = Quest(goal: screen)
screenQuest.targetAction = video
screenQuest.measure = .time
screenQuest.period = .daily
screenQuest.targetSeconds = 3 * 3600 // 90
screenQuest.direction = .atMost
context.insert(screenQuest)
// : " 8 1 "
// : 07:30( )+08:10( 100% 200% ),
// 08:30( ), 07:00( ).
// : 100%, 1( ), streak-dump deadline=480.
let wake = Action(name: "일찍 일어나기", symbolName: "sunrise.fill", trackingType: .count, sortOrder: 6)
wake.tags = [life]
context.insert(wake)
context.insert(CountEntry(action: wake, timestamp: at(daysAgo: 0, hour: 7, minute: 30)))
context.insert(CountEntry(action: wake, timestamp: at(daysAgo: 0, hour: 8, minute: 10)))
context.insert(CountEntry(action: wake, timestamp: at(daysAgo: 1, hour: 8, minute: 30)))
context.insert(CountEntry(action: wake, timestamp: at(daysAgo: 2, hour: 7, minute: 0)))
let morning = Goal(
title: "아침 루틴 지키기",
symbolName: "sunrise.fill",
colorHex: "#D9A621",
startDate: cal.date(byAdding: .day, value: -7, to: now)!,
endDate: nil
)
context.insert(morning)
let wakeQuest = Quest(goal: morning)
wakeQuest.targetAction = wake
wakeQuest.measure = .count
wakeQuest.period = .daily
wakeQuest.targetCount = 1
wakeQuest.direction = .atLeast
wakeQuest.deadlineMinutes = 8 * 60
context.insert(wakeQuest)
//
for (index, goal) in [toeic, health, habit, run, screen, morning].enumerated() {
goal.sortOrder = index
}
// ( 1 + 1)
let sleep = Goal(
title: "일찍 자기 챌린지",
symbolName: "moon.zzz.fill",
colorHex: "#2F6B4F",
startDate: cal.date(byAdding: .day, value: -60, to: now)!,
endDate: cal.date(byAdding: .day, value: -30, to: now)!
)
sleep.status = .achieved
sleep.sortOrder = 6
context.insert(sleep)
let diet = Goal(
title: "한 달 다이어트",
symbolName: "fork.knife",
colorHex: "#9D5B4A",
startDate: cal.date(byAdding: .day, value: -50, to: now)!,
endDate: cal.date(byAdding: .day, value: -20, to: now)!
)
diet.status = .notAchieved
diet.sortOrder = 7
context.insert(diet)
// UI : , (LocalPrefs )
LocalPrefs.setPromptsForNote(reading.uuid, enabled: true)
LocalPrefs.setPromptsForNote(water.uuid, enabled: true)
LocalPrefs.defaults.set(
LocalPrefs.rawValue([health.uuid]),
forKey: LocalPrefsKeys.pinnedGoals
)
}
}
#endif