From b980d7df7a0828294186400d8c7bbbeb616ef975 Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Mon, 13 Jul 2026 23:56:32 +0900 Subject: [PATCH] feat(watch): favorites list on the first screen, debounced snapshot push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 워치 첫 화면 최상단에 '즐겨찾기' 항목 추가 — 앱에서 즐겨찾기한 행동을 태그를 거치지 않고 한 탭 만에 연다. WatchActionInfo.isFavorite는 optional로 실어 구버전 캐시 스냅숏도 그대로 디코딩된다. - 워치 스냅숏 전송 디바운스(350ms): 모든 커밋마다 목표×다짐×3기간 진행률을 전부 재집계하던 것을, 연타 중에는 마지막 상태 한 번만 계산해 보내도록 변경 (명령 응답 경로의 즉시 스냅숏은 그대로). iOS·watchOS 스킴 빌드 확인. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7 --- .../ContentView.swift | 19 ++++++++++++++++++ .../HaruDanim/IOS/Core/WatchSyncManager.swift | 20 +++++++++++++++++-- .../HaruDanim/WatchShared/WatchPayload.swift | 3 +++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift b/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift index 12deb62..54f9b35 100644 --- a/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift +++ b/myApp/HaruDanim/Haru_DanimWatch Watch App/ContentView.swift @@ -91,6 +91,25 @@ struct ContentView: View { } } Section { + // 앱에서 즐겨찾기한 행동은 태그를 거치지 않고 첫 화면에서 바로 연다 + let favorites = snapshot.actions.filter { $0.isFavorite ?? false } + if !favorites.isEmpty { + NavigationLink { + WatchActionListView(title: String(localized: "즐겨찾기"), actions: favorites) + } label: { + HStack(spacing: 8) { + Image(systemName: "star.fill") + .font(.system(size: 11)) + .foregroundStyle(.yellow) + Text("즐겨찾기") + .lineLimit(1) + Spacer() + Text("\(favorites.count)") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } ForEach(snapshot.tags) { tag in let actions = snapshot.actions.filter { $0.tagIDs.contains(tag.id) } if !actions.isEmpty { diff --git a/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift b/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift index 88f1b04..88d0aa2 100644 --- a/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift +++ b/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift @@ -62,7 +62,8 @@ final class WatchSyncManager: NSObject { todayValue: todayValue, isRunning: action.isRunning, tickingBase: action.isRunning && !isCount ? now.addingTimeInterval(-todayValue) : nil, - tagIDs: action.sortedTags.map(\.uuid) + tagIDs: action.sortedTags.map(\.uuid), + isFavorite: action.isFavorite ) } @@ -126,8 +127,23 @@ final class WatchSyncManager: NSObject { private var lastComplicationStateKey = "" private var lastComplicationPushAt = Date.distantPast - /// 최신 스냅숏을 워치로 전송 (데이터 변경 시마다 호출) + /// 디바운스 중인 전송 작업 (연타 대비) + private var pushTask: Task? + + /// 최신 스냅숏을 워치로 전송 (데이터 변경 시마다 호출). + /// 스냅숏 생성은 모든 목표×다짐×3기간 진행률을 집계하는 비싼 작업이라, + /// 짧은 디바운스로 연타(횟수 버튼 연속 탭 등) 중의 중간 계산을 건너뛰고 + /// 마지막 상태 한 번만 만들어 보낸다. func pushSnapshot() { + pushTask?.cancel() + pushTask = Task { @MainActor in + try? await Task.sleep(for: .milliseconds(350)) + guard !Task.isCancelled else { return } + self.pushSnapshotNow() + } + } + + private func pushSnapshotNow() { guard WCSession.isSupported() else { return } let session = WCSession.default guard session.activationState == .activated, session.isPaired, session.isWatchAppInstalled else { return } diff --git a/myApp/HaruDanim/WatchShared/WatchPayload.swift b/myApp/HaruDanim/WatchShared/WatchPayload.swift index 1883f49..e1c9ab8 100644 --- a/myApp/HaruDanim/WatchShared/WatchPayload.swift +++ b/myApp/HaruDanim/WatchShared/WatchPayload.swift @@ -69,6 +69,9 @@ nonisolated struct WatchActionInfo: Codable, Identifiable, Hashable { var tickingBase: Date? /// 소속 꼬리표 id들 (비어 있으면 '꼬리표 없음' 그룹) var tagIDs: [UUID] + /// 앱에서 즐겨찾기로 등록한 행동 — 워치 첫 화면의 즐겨찾기 목록에 모인다. + /// optional인 이유: 구버전 워치에 캐시된 스냅숏(키 없음)도 디코딩되게 하기 위함 + var isFavorite: Bool? = nil } nonisolated struct WatchGoalInfo: Codable, Identifiable, Hashable {