mycode/myApp/HaruDanim/Shared/DayMath.swift
songyc macbook f468fdf296 feat(data): SwiftData 저장소를 App Group으로 이관하고 CloudKit 동기화 지원
- DataStore: App Group 컨테이너의 HaruDanim.store 사용, 기존 default.store 1회 자동 이관
- 프리미엄 + 동기화 토글 시 CloudKit(.private) 구성으로 컨테이너 생성, 실패하면 로컬 폴백
- CloudKit 요건(모든 관계 optional)에 맞춰 to-many 관계를 optional 저장 + 기존 이름
  접근자로 리팩터링 (originalName 매핑으로 기존 데이터 유지)
- Tag/Action/Goal/Quest에 uuid 추가 (시리 단축어·위젯·워치 식별자) + 중복 보정
- 모델 레이어(Models/Settings/DayMath/QuestProgress/Formatters/Theme)를 Shared/로 이동
  — 위젯 확장과 공유
- 집계 설정(주 시작 요일·하루 시작 시간·대표 시간·짧은 기록 무시)을 App Group defaults로
  이관해 위젯·워치와 공유, 설정의 '기기 간 동기화' 토글 활성화
- Goal.combinedSpanRatio 공용화 (모음 탭 카드·위젯·시리가 동일 산식 사용)
- DEBUG 검증 인자: -cloudSync YES, -settingsScrollPremium YES

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

145 lines
5.3 KiB
Swift

//
// DayMath.swift
// Haru_Danim
//
// " " (CLAUDE.md §4.3)
//
import Foundation
/// ( ), / ,
struct DayMath {
let settings: TrackingSettings
init(settings: TrackingSettings = .current()) {
self.settings = settings
}
var calendar: Calendar { settings.calendar }
// MARK:
/// `date` ( Date).
/// : 06:00 7/7 05:00 7/6 .
func dayKey(for date: Date) -> Date {
let startOfDay = calendar.startOfDay(for: date)
let boundary = calendar.date(byAdding: .minute, value: settings.dayStartMinutes, to: startOfDay)!
if date < boundary {
return calendar.date(byAdding: .day, value: -1, to: startOfDay)!
}
return startOfDay
}
/// ( Date)
func dayRange(forKey key: Date) -> Range<Date> {
let start = calendar.date(byAdding: .minute, value: settings.dayStartMinutes, to: key)!
let nextKey = calendar.date(byAdding: .day, value: 1, to: key)!
let end = calendar.date(byAdding: .minute, value: settings.dayStartMinutes, to: nextKey)!
return start..<end
}
func dayRange(containing date: Date) -> Range<Date> {
dayRange(forKey: dayKey(for: date))
}
// MARK: /
/// `date` ( )
func weekRange(containing date: Date) -> Range<Date> {
var key = dayKey(for: date)
while calendar.component(.weekday, from: key) != calendar.firstWeekday {
key = calendar.date(byAdding: .day, value: -1, to: key)!
}
let lastKey = calendar.date(byAdding: .day, value: 6, to: key)!
return dayRange(forKey: key).lowerBound..<dayRange(forKey: lastKey).upperBound
}
/// `date`
func monthRange(containing date: Date) -> Range<Date> {
let key = dayKey(for: date)
let comps = calendar.dateComponents([.year, .month], from: key)
let firstKey = calendar.date(from: comps)!
let dayCount = calendar.range(of: .day, in: .month, for: firstKey)!.count
let lastKey = calendar.date(byAdding: .day, value: dayCount - 1, to: firstKey)!
return dayRange(forKey: firstKey).lowerBound..<dayRange(forKey: lastKey).upperBound
}
///
func dayKeys(in range: Range<Date>) -> [Date] {
var keys: [Date] = []
var key = dayKey(for: range.lowerBound)
while dayRange(forKey: key).lowerBound < range.upperBound {
keys.append(key)
key = calendar.date(byAdding: .day, value: 1, to: key)!
}
return keys
}
// MARK: ( )
struct DaySegment {
let dayKey: Date
let range: Range<Date>
}
/// ~
func splitByDay(start: Date, end: Date) -> [DaySegment] {
guard start < end else { return [] }
var segments: [DaySegment] = []
var cursor = start
while cursor < end {
let key = dayKey(for: cursor)
let dayEnd = dayRange(forKey: key).upperBound
let segmentEnd = min(dayEnd, end)
segments.append(DaySegment(dayKey: key, range: cursor..<segmentEnd))
cursor = segmentEnd
}
return segments
}
}
// MARK: -
/// / ( )
struct Aggregator {
let math: DayMath
init(math: DayMath = DayMath()) {
self.math = math
}
/// (). `now` .
func seconds(for action: Action, in range: Range<Date>, now: Date = .now) -> TimeInterval {
action.sessions.reduce(0) { total, session in
let end = session.endAt ?? now
let overlapStart = max(session.startAt, range.lowerBound)
let overlapEnd = min(end, range.upperBound)
return total + max(0, overlapEnd.timeIntervalSince(overlapStart))
}
}
///
func count(for action: Action, in range: Range<Date>) -> Int {
action.countEntries
.filter { range.contains($0.timestamp) }
.reduce(0) { $0 + $1.amount }
}
func seconds(for actions: [Action], in range: Range<Date>, now: Date = .now) -> TimeInterval {
actions.reduce(0) { $0 + seconds(for: $1, in: range, now: now) }
}
func count(for actions: [Action], in range: Range<Date>) -> Int {
actions.reduce(0) { $0 + count(for: $1, in: range) }
}
/// ( )
func todayValue(for action: Action, now: Date = .now) -> Double {
let range = math.dayRange(containing: now)
switch action.trackingType {
case .time: return seconds(for: action, in: range, now: now)
case .count: return Double(count(for: action, in: range))
}
}
}