mycode/myApp/HaruDanim/IOS/Core/DebugSeed.swift
songyc macbook 0aed7a9ec5 fix(goal): evaluate ended goals at launch and foreground, not only on goal tab
종료일이 지난 목표의 자동 판정(evaluateIfEnded)이 목표 탭 진입 시에만
실행돼, 목표 탭을 열지 않으면 위젯·워치·모음 탭 카드에 '진행 중'으로
계속 남아 있었다. 앱 실행/포그라운드 복귀(scenePhase .active) 시점에
같은 판정을 추가로 실행한다.

- 목표 탭 onAppear 로직과 동일한 코드 — 진행 중 목표만 조회, 변경이
  있을 때만 DataChange.commit (위젯·워치·라이브 액티비티 갱신)
- 다짐 없는 목표는 evaluateIfEnded가 건드리지 않으므로 '확인 필요'
  직접 선택 흐름은 그대로
- 위젯 확장에서는 판정하지 않음 (프로바이더 경합·CloudKit 내보내기
  지연 위험 회피)
- 검증용 DEBUG 인자 -endGoalYesterday "제목" 추가. 시뮬레이터에서
  목표 탭을 열지 않은 콜드 스타트만으로 statusRaw가 achieved로
  전환되는 것을 스토어 조회로 확인

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-13 19:37:43 +09:00

254 lines
11 KiB
Swift

//
// DebugSeed.swift
// Haru_Danim
//
// . `-seedDemo YES` . (DEBUG )
//
#if DEBUG
import Foundation
import SwiftData
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)
}
/// `-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()
}
/// `-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)
//
for (index, goal) in [toeic, health, habit, run].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 = 4
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 = 5
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