mycode/myApp/HaruDanim/WatchShared/WatchPayload.swift
songyc macbook 79c16b9038 feat(watch): tap the running row on watch home to stop tracking
- 워치 첫 화면 '측정 중' 행이 표시 전용이라 종료하려면 꼬리표/즐겨찾기로 행동을 찾아가야 했던 불편 수정 — 행을 탭하면 대표 측정이 바로 종료됨
- 종료는 행동 목록 탭과 완전히 같은 run 명령 경로 재사용 — 아이폰 쪽 처리(짧은 기록 무시·위젯·라이브 액티비티·동기화)는 기존 그대로라 동기화가 엉킬 여지 없음
- WatchSnapshot에 runningActionID(optional) 추가 — isFavorite와 같은 구버전 캐시 호환 패턴, 구캐시는 이름 매칭 폴백(runningAction 헬퍼)
- UI: 오른쪽 노란 정지 아이콘 + "누르면 측정이 종료돼요" 푸터 + 접근성 라벨. 대표를 못 찾는 이례적 경우엔 기존 표시 전용 유지
- 검증: 페어링 심 E2E — 워치 첫 화면에 종료 버튼 렌더 → -autoStopRunning(신설 인자, 탭과 동일 경로) → 아이폰 세션 종료·모음 탭 반영 스크린샷 확인
- 도움말 워치 항목 문장 추가(ko/en/ja), 워치 카탈로그 2키 번역, 전 카탈로그 클린. CLAUDE.md §11·§14 갱신. iOS Debug/Store + 워치(최소 OS 26.2·11.5 둘 다) 빌드 성공

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014CkLDi8Lr21vGqo4SjZaTp
2026-07-18 05:32:10 +09:00

138 lines
4.8 KiB
Swift

//
// 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
}
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
}
}
}