mycode/myApp/HaruDanim/IOS/Core/DebugSeed.swift
songyc macbook 2a176fa8a2 fix(quest): count scheduled days only for daily-quest spans + rest-day state
진행률 검증 보고에서 확정한 결정 반영 (사용자 위임):

- [계산 수정] 하루 단위 다짐의 주/월 집계를 **방향 무관 수행일 기록만** 합산 — 목표량(scaledTarget)이 수행일 수 환산인데 '이하 유지'만 비수행일 기록까지 값에 섞여 분모·분자가 어긋나던 비대칭 해소 (월수금 1시간 이하 다짐이 목요일 시청으로 한도 초과 표시되던 문제). atLeast의 하루 기여 캡·마감 창 클립은 기존 유지
- [표시 수정] 수행일 아닌 날(요일·날짜 다짐의 쉬는 날, 기간 밖 custom)의 '하루' 게이지를 '수행일 아님' 상태로 — 쉬는 날 기록은 주/월 집계에 안 잡히는 값이라 퍼센트(예 133%)로 보여주면 "오늘 채웠다"는 오해 유발. 목표 탭 행·모음 카드(문구+빈 바), 위젯 ③(문구·누적값 숨김)·②(— 대시)·④(링 흐림, 스냅숏 isRestDay), 시리 '오늘' 조회("오늘은 수행일이 아니에요") 공유. 워치·잠금화면 점은 공간 제약으로 수치 유지(문서화)
- [의도 확정·유지] atMost 하루 게이지의 환산 한도 페이스 경고 / 다짐 생성 이전 기록 인정 / 월 경계 주 혼입 / 적용일 없는 달 0% / custom 종료 후 게이지 — CLAUDE.md §4.2에 트레이드오프로 명시
- 검증: -progressSelfTest 37건 ALL PASS(E 비수행일 제외·M 비대칭 수정·N 기간 밖 판정 신규), 목표 탭 스크린샷(달리기 월수금: 하루 '수행일 아님'/주간 0%/월간 7%), 위젯 미리보기 렌더 무결, Debug·Store·워치 3스킴 빌드 성공
- 도움말 '진행률 읽는 법' 전면 보강(수행일 집계·페이스 캡·이하 유지 이분법+하루 경고) en/ja 완비, 신규 키 2종('수행일 아님', 시리 응답) IOS·위젯 카탈로그 클린(763/213키)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-16 22:59:15 +09:00

707 lines
37 KiB
Swift

//
// 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( )
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); add(eAct, at(3, 9), amount: 5)
expect("E 주간 표기(목요일 기록 제외)", progress(eQuest, .week).displayRatio, 10.0 / 15.0)
expect("E 오늘 수행일 아님 판정",
QuestProgress(quest: eQuest, math: math).isScheduled(on: now) ? 1 : 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)
expect("L 주간 다짐 오늘 수행일 판정",
QuestProgress(quest: lQuest, math: math).isScheduled(on: now) ? 1 : 0, 1)
// M. ' ' 1: 30( ), 3( )
// ( )
let (mAct, mQuest) = makeQuest("M 월수금 이하1시간", type: .time, period: .daily,
direction: .atMost, targetSeconds: 3600) {
$0.scheduleMode = .weekdays
$0.weekdays = [2, 4, 6]
}
addSession(mAct, at(0, 9), at(0, 9, 30))
addSession(mAct, at(3, 13), at(3, 16))
expect("M 주간 값(목요일 시청 제외)", progress(mQuest, .week).value, 30 * 60)
expect("M 주간 게이지(한도 안 100%)", progress(mQuest, .week).ratio, 1.0)
// N. ( )
let (_, nQuest) = makeQuest("N 지난기간", type: .count, period: .custom, targetCount: 5) {
$0.customStart = at(-10, 0)
$0.customEnd = at(-3, 0)
}
expect("N 기간 밖 수행일 판정",
QuestProgress(quest: nQuest, math: math).isScheduled(on: now) ? 1 : 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