mycode/myApp/HaruDanim/WatchShared/WatchPayload.swift
songyc macbook 081c388f00 fix: 워치-라이브 액티비티 동기화, 앱 이름 띄어쓰기, 차트 라벨 겹침, 위젯 '선택 안 함' 옵션
라이브 액티비티(다이내믹 아일랜드) 워치 연동:
- 워치 실행 명령이 세션 활성화 전이거나 sendMessage 실패 시 유실되던 문제를
  transferUserInfo 큐 폴백으로 보강 (iPhone didReceiveUserInfo 수신 처리 추가)
- 큐로 지연 도착한 실행 명령이 엉뚱한 토글을 일으키지 않도록 5분 유효 시간 부여
- 백그라운드에서 Live Activity 시작이 거부되면 pendingStartRetry로 기억했다가
  앱 포그라운드 복귀(scenePhase active) 시 재시도 + DB 상태 재동기화
- WatchSync 진단용 os_log(notice) 추가, DEBUG -autoRunFirstAction 검증 훅 추가
- 시뮬레이터 검증: 워치 시작 → 아이폰 세션 시작 + 다이내믹 아일랜드 생성,
  워치 종료 → 세션 종료 + 아일랜드 소멸 확인 (큐 폴백 채널은 시뮬레이터
  transferUserInfo 한계로 실기기 확인 필요)

앱 이름 전역 띄어쓰기 ('하루다님' → '하루 다님'):
- pbxproj CFBundleDisplayName 6곳, InfoPlist.xcstrings 3개(ko), 스플래시(ContentView),
  워치 내비게이션 타이틀, 컴플리케이션 문구, 문자열 카탈로그 키 일괄 변경
- 워치 카탈로그 영문 값 HaruDanim → Haru Danim 통일

통계 위젯 X축 라벨 겹침 방지:
- 날짜 축 포인트가 7개를 초과하면(한 달·일별 등) 라벨을 숨기고 눈금만 표시

위젯 설정 '선택 안 함' 옵션:
- Action/Goal/Quest 엔티티에 zero-UUID 센티널 추가, 제안 목록 최상단 노출
- 2·3번째 슬롯 선택을 되돌릴 수 있고, 위젯 조회 단계에서 자연스럽게 걸러짐
- ko/en/ja 번역 추가 (None / 選択しない)

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

123 lines
3.9 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?
///
var extraRunningCount: Int = 0
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]
}
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
}
}
}