mycode/myApp/HaruDanim/WatchShared/WatchPayload.swift
songyc macbook 92afa69f6d feat(1.5-p1): 워치 '행동 기록' 컴플리케이션 — 기간 옵트인·미니 행동 화면 딥링크
- WatchActionPeriod(하루/이번 주/이번 달/지난 7일/지난 30일) + WatchActionInfo에
  optional 기간 필드 3종(구버전 캐시 호환 — isFavorite 패턴)
- LocalPrefs.watchActions(uuid→기간 맵, 기기 로컬) + 전체 초기화 청소 목록 반영
- makeSnapshot: 옵트인 행동만 기간값 계산(rollingRange 재사용, StatSpan 무변경)
- 신규 ActionValueComplication(4패밀리·측정 중 노랑+타이머·유령 타이머 안전장치·
  빈 상태 안내·recommendations verbatim·widgetURL 딥링크)
- 워치 미니 행동 화면(WatchActionDetailView) + NavigationPath 딥링크 착지
  (뒤로가기 한 번에 메인 복귀), 행동 편집기 애플워치 섹션(토글+기간)
- 검증 인자: -watchActions <N>·-actionShowEditor·-watchOpenActionDetail
- 검증: Debug/Store/워치(26.2·11.5) 빌드, 자가 검증 49·20·27·10 ALL PASS(26.5+18.5 심),
  시각 QA 5장. 신규 문구 l10n은 계획 §10 R6대로 도움말 단계에서 일괄
- CLAUDE.md: 시뮬레이터 세트 리셋 반영(신규 UUID)·검증 인자 추가

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtZwRRjbtM9pZJDM4FkTq6
2026-08-22 05:18:42 +09:00

162 lines
6.2 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// WatchPayload.swift
// Haru_Danim
//
// iPhone Apple Watch (CLAUDE.md §9)
// - iOS + + Codable
// - iPhone WCSession , UI
// (App Group defaults )
//
import Foundation
nonisolated enum WatchSync {
/// WCSession / Data
static let snapshotKey = "snapshot"
/// App Group defaults ()
static let cachedSnapshotKey = "watch.cachedSnapshot"
///
static let commandKey = "command"
static let actionIDKey = "actionID"
/// (, since 1970)
static let sentAtKey = "sentAt"
///
static let runCommand = "run"
static let refreshCommand = "refresh"
/// (transferUserInfo)
static let queuedRunMaxAge: TimeInterval = 5 * 60
}
///
nonisolated struct WatchSnapshot: Codable {
var generatedAt: Date = .now
var isPremium: Bool = false
var tags: [WatchTagInfo] = []
var actions: [WatchActionInfo] = []
var goals: [WatchGoalInfo] = []
/// ( , CLAUDE.md §9.2-1)
var runningName: String?
var runningSymbol: String?
var runningStartAt: Date?
/// id ' ' .
/// optional : ( ) (isFavorite )
var runningActionID: UUID? = nil
///
var extraRunningCount: Int = 0
/// ' ' id , ( )
var runningAction: WatchActionInfo? {
if let runningActionID, let match = actions.first(where: { $0.id == runningActionID }) {
return match
}
guard let runningName else { return nil }
return actions.first { $0.isRunning && $0.name == runningName }
}
func encoded() -> Data? {
try? JSONEncoder().encode(self)
}
static func decode(_ data: Data) -> WatchSnapshot? {
try? JSONDecoder().decode(WatchSnapshot.self, from: data)
}
}
nonisolated struct WatchTagInfo: Codable, Identifiable, Hashable {
var id: UUID
var name: String
var colorHex: String
}
nonisolated struct WatchActionInfo: Codable, Identifiable, Hashable {
var id: UUID
var name: String
var symbolName: String
var colorHex: String
var isCount: Bool
/// ( ) , ( )
var todayValue: Double
var isRunning: Bool
/// ( - )
var tickingBase: Date?
/// id ( ' ' )
var tagIDs: [UUID]
/// .
/// optional : ( )
var isFavorite: Bool? = nil
/// ' ' raw (1.5) .
/// nil . optional : (isFavorite )
var complicationPeriodRaw: String? = nil
/// , ( )
var periodValue: Double? = nil
/// ( - )
var periodTickingBase: Date? = nil
}
/// ' ' (1.5 , LocalPrefs).
/// StatSpan StatSpan (§6.6).
/// (last7/last30) ' ' (1.4) : N .
nonisolated enum WatchActionPeriod: String, Codable, CaseIterable {
case day, week, month, last7, last30
var label: String {
switch self {
case .day: return String(localized: "하루")
case .week: return String(localized: "이번 주")
case .month: return String(localized: "이번 달")
case .last7: return String(localized: "지난 7일")
case .last30: return String(localized: "지난 30일")
}
}
}
nonisolated struct WatchGoalInfo: Codable, Identifiable, Hashable {
var id: UUID
var title: String
var symbolName: String
var colorHex: String
var dayRatio: Double
var weekRatio: Double
var monthRatio: Double
var quests: [WatchQuestInfo]
func ratio(forSpanRaw raw: String) -> Double {
switch raw {
case "week": return weekRatio
case "month": return monthRatio
default: return dayRatio
}
}
}
nonisolated struct WatchQuestInfo: Codable, Identifiable, Hashable {
var id: UUID
var name: String
var symbolName: String
var colorHex: String
var dayRatio: Double
var weekRatio: Double
var monthRatio: Double
/// ' ' ( , CLAUDE.md §9.2-3)
var isAtMost: Bool
var dayAchieved: Bool
var weekAchieved: Bool
var monthAchieved: Bool
func ratio(forSpanRaw raw: String) -> Double {
switch raw {
case "week": return weekRatio
case "month": return monthRatio
default: return dayRatio
}
}
func achieved(forSpanRaw raw: String) -> Bool {
switch raw {
case "week": return weekAchieved
case "month": return monthAchieved
default: return dayAchieved
}
}
}