diff --git a/myApp/HaruDanim/IOS/Core/DebugSeed.swift b/myApp/HaruDanim/IOS/Core/DebugSeed.swift index adf9b90..8a9edd3 100644 --- a/myApp/HaruDanim/IOS/Core/DebugSeed.swift +++ b/myApp/HaruDanim/IOS/Core/DebugSeed.swift @@ -27,9 +27,11 @@ enum DebugSeed { guard n > 0 else { return } let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.createdAt)]) guard let goals = try? context.fetch(descriptor) else { return } - for (index, goal) in goals.enumerated() { - goal.showsOnMain = index < n - } + // 노출 목표는 기기별 로컬 설정 (LocalPrefs 참고) + LocalPrefs.defaults.set( + LocalPrefs.rawValue(goals.prefix(n).map(\.uuid)), + forKey: LocalPrefsKeys.pinnedGoals + ) } static func seedIfRequested(context: ModelContext) { @@ -220,6 +222,14 @@ enum DebugSeed { diet.status = .notAchieved diet.sortOrder = 5 context.insert(diet) + + // 기기별 로컬 UI 설정 시드: 메모 창 행동, 모음 탭 노출 목표 (LocalPrefs 참고) + LocalPrefs.setPromptsForNote(reading.uuid, enabled: true) + LocalPrefs.setPromptsForNote(water.uuid, enabled: true) + LocalPrefs.defaults.set( + LocalPrefs.rawValue([health.uuid]), + forKey: LocalPrefsKeys.pinnedGoals + ) } } #endif diff --git a/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift b/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift index ea78745..88f1b04 100644 --- a/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift +++ b/myApp/HaruDanim/IOS/Core/WatchSyncManager.swift @@ -46,7 +46,10 @@ final class WatchSyncManager: NSObject { ))) ?? [] snapshot.tags = tags.map { WatchTagInfo(id: $0.uuid, name: $0.name, colorHex: $0.colorHex) } - let actions = (try? context.fetch(FetchDescriptor(sortBy: [SortDescriptor(\.sortOrder)]))) ?? [] + // 워치의 행동 나열 순서도 이 기기(페어링된 폰)의 모음 탭 배치(로컬 설정)를 따른다 + let actions = LocalPrefs.orderedActions( + (try? context.fetch(FetchDescriptor(sortBy: [SortDescriptor(\.createdAt)]))) ?? [] + ) snapshot.actions = actions.map { action in let todayValue = agg.todayValue(for: action, now: now) let isCount = action.trackingType == .count diff --git a/myApp/HaruDanim/IOS/Haru_DanimApp.swift b/myApp/HaruDanim/IOS/Haru_DanimApp.swift index 6126f56..056fa47 100644 --- a/myApp/HaruDanim/IOS/Haru_DanimApp.swift +++ b/myApp/HaruDanim/IOS/Haru_DanimApp.swift @@ -21,6 +21,8 @@ struct Haru_DanimApp: App { SettingsMigration.runIfNeeded() container = DataStore.shared DataStore.ensureUniqueEntityIDs(context: container.mainContext) + // 배치·노출 등 기기별 UI 설정: 구버전에 모델(@Model)로 저장하던 값을 로컬로 1회 이관 + LocalPrefs.adoptLegacyModelValuesIfNeeded(context: container.mainContext) // 시리 단축어/위젯 인텐트가 앱 프로세스에서 실행될 때 Live Activity·워치도 갱신되도록 주입 IntentHooks.afterMutation = { context in LiveActivityManager.sync(context: context) diff --git a/myApp/HaruDanim/IOS/Views/ActionViews.swift b/myApp/HaruDanim/IOS/Views/ActionViews.swift index 1b62b7c..ead4474 100644 --- a/myApp/HaruDanim/IOS/Views/ActionViews.swift +++ b/myApp/HaruDanim/IOS/Views/ActionViews.swift @@ -13,13 +13,20 @@ import SwiftData struct ActionListView: View { @Environment(\.modelContext) private var context @Query(sort: [SortDescriptor(\Tag.sortOrder), SortDescriptor(\Tag.createdAt)]) private var tags: [Tag] - @Query(sort: \Action.sortOrder) private var actions: [Action] + @Query(sort: \Action.createdAt) private var allActions: [Action] private let premium = PremiumManager.shared @State private var showingAdd = false @State private var showLimitAlert = false @State private var showingTagOrder = false + // 행동 표시 순서는 모음 탭 배치와 같은 기기별 로컬 설정을 따른다 (LocalPrefs 참고) + @AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = "" + + private var actions: [Action] { + LocalPrefs.orderedActions(allActions, raw: actionOrderRaw) + } + private var favoriteActions: [Action] { actions.filter(\.isFavorite) } @@ -41,7 +48,7 @@ struct ActionListView: View { } } ForEach(tags) { tag in - let group = tag.sortedActions.filter { !$0.isFavorite } + let group = actions.filter { $0.tags.contains(tag) && !$0.isFavorite } if !group.isEmpty { Section { ForEach(group) { action in @@ -220,7 +227,11 @@ struct ActionDetailView: View { .foregroundStyle(AppTheme.yellow) } } - Toggle(isOn: Bindable(action).promptsForNote) { + // 메모 창 표시 여부는 기기별 로컬 설정 (CloudKit 동기화 제외, LocalPrefs 참고) + Toggle(isOn: Binding( + get: { LocalPrefs.promptsForNote(action.uuid) }, + set: { LocalPrefs.setPromptsForNote(action.uuid, enabled: $0) } + )) { Label { Text("기록 시 메모 창 표시") } icon: { @@ -439,7 +450,7 @@ struct ActionEditorView: View { name = action.name symbolName = action.symbolName trackingType = action.trackingType - promptsForNote = action.promptsForNote + promptsForNote = LocalPrefs.promptsForNote(action.uuid) selectedTags = Set(action.tags.map(\.persistentModelID)) } @@ -458,9 +469,10 @@ struct ActionEditorView: View { action.name = trimmed action.symbolName = symbolName action.trackingType = trackingType - action.promptsForNote = promptsForNote action.tags = tags + LocalPrefs.setPromptsForNote(action.uuid, enabled: promptsForNote) } else { + // sortOrder는 로컬 배치 순서가 없는 기기(첫 실행·다른 기기)에서의 폴백 정렬용으로만 기록 let maxOrder = allActions.map(\.sortOrder).max() ?? -1 let newAction = Action( name: trimmed, @@ -468,9 +480,11 @@ struct ActionEditorView: View { trackingType: trackingType, sortOrder: maxOrder + 1 ) - newAction.promptsForNote = promptsForNote newAction.tags = tags context.insert(newAction) + // 이 기기의 로컬 배치 맨 끝에 추가 + 메모 창 설정 저장 + LocalPrefs.appendActionToOrder(newAction.uuid) + LocalPrefs.setPromptsForNote(newAction.uuid, enabled: promptsForNote) } DataChange.commit(context: context) dismiss() diff --git a/myApp/HaruDanim/IOS/Views/GoalViews.swift b/myApp/HaruDanim/IOS/Views/GoalViews.swift index a86ccbb..6ec7389 100644 --- a/myApp/HaruDanim/IOS/Views/GoalViews.swift +++ b/myApp/HaruDanim/IOS/Views/GoalViews.swift @@ -20,6 +20,9 @@ struct GoalListView: View { @State private var isReordering = false @State private var editMode: EditMode = .inactive + // 다짐 목록 접힘 상태는 기기별 로컬 설정 (CloudKit 동기화 제외, LocalPrefs 참고) + @AppStorage(LocalPrefsKeys.collapsedGoals, store: AppGroup.defaults) private var collapsedGoalsRaw = "" + /// 진행 중(확인 필요 포함) 목표만 기본 목록에 표시 private var activeGoals: [Goal] { goals.filter { $0.status == .inProgress } } /// 달성/미달성으로 끝난 목표는 별도 화면으로 분리 @@ -63,7 +66,7 @@ struct GoalListView: View { } label: { GoalRow(goal: goal) } - if !goal.isCollapsed { + if !LocalPrefs.contains(goal.uuid, in: collapsedGoalsRaw) { ForEach(goal.sortedQuests) { quest in QuestRow(quest: quest) } @@ -188,14 +191,15 @@ struct GoalListView: View { /// 다짐 접기/펼치기 토글 헤더 private func collapseHeader(_ goal: Goal) -> some View { - Button { + let collapsed = LocalPrefs.contains(goal.uuid, in: collapsedGoalsRaw) + return Button { withAnimation(.spring(duration: 0.3)) { - goal.isCollapsed.toggle() + collapsedGoalsRaw = LocalPrefs.toggling(goal.uuid, in: collapsedGoalsRaw) } } label: { HStack(spacing: 4) { Text("다짐 \(goal.quests.count)개") - Image(systemName: goal.isCollapsed ? "chevron.down" : "chevron.up") + Image(systemName: collapsed ? "chevron.down" : "chevron.up") .font(.caption2.weight(.semibold)) } .font(.caption) diff --git a/myApp/HaruDanim/IOS/Views/HistoryView.swift b/myApp/HaruDanim/IOS/Views/HistoryView.swift index 26348a3..1a5d46c 100644 --- a/myApp/HaruDanim/IOS/Views/HistoryView.swift +++ b/myApp/HaruDanim/IOS/Views/HistoryView.swift @@ -50,7 +50,14 @@ struct HistoryView: View { @Query private var sessions: [TimeSession] @Query private var entries: [CountEntry] @Query(sort: \Tag.sortOrder) private var tags: [Tag] - @Query(sort: \Action.sortOrder) private var allActions: [Action] + @Query(sort: \Action.createdAt) private var allActionsQuery: [Action] + + // 표시 순서는 모음 탭 배치와 같은 기기별 로컬 설정을 따른다 (LocalPrefs 참고) + @AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = "" + + private var allActions: [Action] { + LocalPrefs.orderedActions(allActionsQuery, raw: actionOrderRaw) + } @State private var selectedDayKey: Date = DayMath().dayKey(for: .now) @State private var mode: HistoryMode = { diff --git a/myApp/HaruDanim/IOS/Views/MainView.swift b/myApp/HaruDanim/IOS/Views/MainView.swift index e586882..d59f58d 100644 --- a/myApp/HaruDanim/IOS/Views/MainView.swift +++ b/myApp/HaruDanim/IOS/Views/MainView.swift @@ -11,13 +11,23 @@ import UniformTypeIdentifiers struct MainView: View { @Environment(\.modelContext) private var context - @Query(sort: \Action.sortOrder) private var actions: [Action] + @Query(sort: \Action.createdAt) private var actions: [Action] @Query(filter: #Predicate { $0.endAt == nil }, sort: \TimeSession.startAt) private var runningSessions: [TimeSession] - @Query( - filter: #Predicate { $0.showsOnMain }, - sort: [SortDescriptor(\Goal.sortOrder), SortDescriptor(\Goal.createdAt)] - ) private var pinnedGoals: [Goal] + @Query(sort: [SortDescriptor(\Goal.sortOrder), SortDescriptor(\Goal.createdAt)]) + private var allGoals: [Goal] + + // 배치 순서·노출 목표는 기기별 로컬 설정 (CloudKit 동기화 제외, LocalPrefs 참고) + @AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = "" + @AppStorage(LocalPrefsKeys.pinnedGoals, store: AppGroup.defaults) private var pinnedGoalsRaw = "" + + private var orderedActions: [Action] { + LocalPrefs.orderedActions(actions, raw: actionOrderRaw) + } + + private var pinnedGoals: [Goal] { + allGoals.filter { LocalPrefs.contains($0.uuid, in: pinnedGoalsRaw) } + } @Environment(AppRouter.self) private var router @@ -251,11 +261,11 @@ struct MainView: View { private var grid: some View { LazyVGrid(columns: gridLayout, spacing: 12) { - ForEach(actions) { action in + ForEach(orderedActions) { action in cell(for: action) } } - .animation(.default, value: actions.map(\.persistentModelID)) + .animation(.default, value: orderedActions.map(\.persistentModelID)) .animation(.default, value: gridColumns) } @@ -358,7 +368,7 @@ struct MainView: View { let entry = CountEntry(action: action, timestamp: .now) context.insert(entry) DataChange.commit(context: context) - if action.promptsForNote { + if LocalPrefs.promptsForNote(action.uuid) { memoEntry = entry } } @@ -371,7 +381,7 @@ struct MainView: View { context.delete(session) } else { session.endAt = .now - if session.action?.promptsForNote == true { + if let action = session.action, LocalPrefs.promptsForNote(action.uuid) { memoSession = session } } @@ -384,14 +394,13 @@ struct MainView: View { private func moveDragging(to target: Action) { guard let dragging = draggingAction, dragging != target else { return } - var ordered = actions + var ordered = orderedActions guard let from = ordered.firstIndex(of: dragging), let to = ordered.firstIndex(of: target) else { return } ordered.remove(at: from) ordered.insert(dragging, at: to) - for (index, action) in ordered.enumerated() { - action.sortOrder = index - } + // 배치는 기기별 로컬 설정 — CloudKit으로 동기화되는 모델(sortOrder)에는 쓰지 않는다 + actionOrderRaw = LocalPrefs.rawValue(ordered.map(\.uuid)) } } diff --git a/myApp/HaruDanim/IOS/Views/QuestEditorView.swift b/myApp/HaruDanim/IOS/Views/QuestEditorView.swift index ae0ac20..34cb723 100644 --- a/myApp/HaruDanim/IOS/Views/QuestEditorView.swift +++ b/myApp/HaruDanim/IOS/Views/QuestEditorView.swift @@ -11,9 +11,14 @@ import SwiftData struct QuestEditorView: View { @Environment(\.modelContext) private var context @Environment(\.dismiss) private var dismiss - @Query(sort: \Action.sortOrder) private var allActions: [Action] + @Query(sort: \Action.createdAt) private var allActionsQuery: [Action] @Query(sort: \Tag.createdAt) private var allTags: [Tag] + // 표시 순서는 모음 탭 배치와 같은 기기별 로컬 설정을 따른다 (LocalPrefs 참고) + private var allActions: [Action] { + LocalPrefs.orderedActions(allActionsQuery) + } + let goal: Goal /// nil이면 새 다짐 let quest: Quest? diff --git a/myApp/HaruDanim/IOS/Views/SettingsView.swift b/myApp/HaruDanim/IOS/Views/SettingsView.swift index 2d4fedf..049cdb1 100644 --- a/myApp/HaruDanim/IOS/Views/SettingsView.swift +++ b/myApp/HaruDanim/IOS/Views/SettingsView.swift @@ -23,6 +23,8 @@ struct SettingsView: View { private let premium = PremiumManager.shared @AppStorage(SettingsKeys.visibleTabs) private var visibleTabsRaw = TabBarConfig.defaultVisibleTabs @AppStorage(SettingsKeys.goalCardStyle) private var goalCardStyle = GoalCardStyle.perQuest.rawValue + // 모음 탭 노출 목표는 기기별 로컬 설정 (CloudKit 동기화 제외, LocalPrefs 참고) + @AppStorage(LocalPrefsKeys.pinnedGoals, store: AppGroup.defaults) private var pinnedGoalsRaw = "" private var visibleTabs: [AppTab] { AppTab.visibleTabs(from: visibleTabsRaw) @@ -188,7 +190,7 @@ struct SettingsView: View { private func pinnedGoalRow(_ goal: Goal) -> some View { Button { - goal.showsOnMain.toggle() + pinnedGoalsRaw = LocalPrefs.toggling(goal.uuid, in: pinnedGoalsRaw) } label: { HStack { Label { @@ -200,7 +202,7 @@ struct SettingsView: View { .foregroundStyle(goal.color) } Spacer() - if goal.showsOnMain { + if LocalPrefs.contains(goal.uuid, in: pinnedGoalsRaw) { Image(systemName: "checkmark") .foregroundStyle(AppTheme.green) .fontWeight(.semibold) diff --git a/myApp/HaruDanim/IOS/Views/StatsTabView.swift b/myApp/HaruDanim/IOS/Views/StatsTabView.swift index d70d868..09ab371 100644 --- a/myApp/HaruDanim/IOS/Views/StatsTabView.swift +++ b/myApp/HaruDanim/IOS/Views/StatsTabView.swift @@ -17,7 +17,14 @@ struct StatsTabView: View { @Query private var sessions: [TimeSession] @Query private var entries: [CountEntry] @Query(sort: \Tag.sortOrder) private var tags: [Tag] - @Query(sort: \Action.sortOrder) private var allActions: [Action] + @Query(sort: \Action.createdAt) private var allActionsQuery: [Action] + + // 표시 순서는 모음 탭 배치와 같은 기기별 로컬 설정을 따른다 (LocalPrefs 참고) + @AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = "" + + private var allActions: [Action] { + LocalPrefs.orderedActions(allActionsQuery, raw: actionOrderRaw) + } @State private var span: StatSpan = { #if DEBUG diff --git a/myApp/HaruDanim/Shared/HaruDanimIntents.swift b/myApp/HaruDanim/Shared/HaruDanimIntents.swift index 9691195..5c077d0 100644 --- a/myApp/HaruDanim/Shared/HaruDanimIntents.swift +++ b/myApp/HaruDanim/Shared/HaruDanimIntents.swift @@ -43,7 +43,10 @@ enum IntentStore { } static func actions() -> [Action] { - (try? context.fetch(FetchDescriptor(sortBy: [SortDescriptor(\.sortOrder)]))) ?? [] + // 위젯·단축어의 행동 나열 순서도 이 기기의 모음 탭 배치(로컬 설정)를 따른다 + LocalPrefs.orderedActions( + (try? context.fetch(FetchDescriptor(sortBy: [SortDescriptor(\.createdAt)]))) ?? [] + ) } static func goals() -> [Goal] { diff --git a/myApp/HaruDanim/Shared/LocalPrefs.swift b/myApp/HaruDanim/Shared/LocalPrefs.swift new file mode 100644 index 0000000..458f569 --- /dev/null +++ b/myApp/HaruDanim/Shared/LocalPrefs.swift @@ -0,0 +1,148 @@ +// +// LocalPrefs.swift +// Haru_Danim +// +// 기기별 로컬 UI 설정 (CloudKit 동기화 제외 대상) +// +// 원칙: 행동·목표·기록 같은 "데이터"는 CloudKit으로 동기화하고, +// "이 기기에서 어떻게 보여줄지"(배치 순서, 노출 여부, 접힘, 메모 창 팝업)는 +// App Group UserDefaults — 기기 로컬이며 iCloud로 절대 동기화되지 않음 — 에 저장한다. +// 위젯·워치 스냅숏·App Intents도 같은 App Group defaults를 읽어 +// 한 기기 안에서는 어디서든 같은 배치를 보게 된다. +// +// 대응하는 @Model 속성(Action.sortOrder, Action.promptsForNote, Goal.showsOnMain, +// Goal.isCollapsed)은 CloudKit 스키마 안정성을 위해 모델에 남겨 두되(스키마 변경 없음), +// UI는 더 이상 그 값을 읽지 않는다. 값은 최초 1회 이곳으로 이관된다. +// + +import Foundation +import SwiftData + +nonisolated enum LocalPrefsKeys { + /// 모음 탭 행동 버튼 배치 순서 — Action.uuid 문자열을 쉼표로 연결 + static let actionOrder = "local.actionOrder" + /// 모음 탭 상단에 진행 현황을 표시할 목표 — Goal.uuid 문자열을 쉼표로 연결 + static let pinnedGoals = "local.pinnedGoals" + /// 목표 탭에서 다짐 목록을 접어 둔 목표 — Goal.uuid 문자열을 쉼표로 연결 + static let collapsedGoals = "local.collapsedGoals" + /// 기록 시 메모 작성 창을 띄울 행동 — Action.uuid 문자열을 쉼표로 연결 + static let notePromptActions = "local.notePromptActions" +} + +nonisolated enum LocalPrefs { + static var defaults: UserDefaults { AppGroup.defaults } + + // MARK: 공통 변환 (쉼표 연결 UUID 문자열 ↔ 배열) + + static func idList(_ raw: String) -> [UUID] { + raw.split(separator: ",").compactMap { UUID(uuidString: String($0)) } + } + + static func rawValue(_ ids: [UUID]) -> String { + ids.map(\.uuidString).joined(separator: ",") + } + + static func contains(_ id: UUID, in raw: String) -> Bool { + raw.contains(id.uuidString) + } + + /// 집합형 값(노출 목표, 접힘, 메모 창)에서 id를 넣거나 빼서 새 raw 문자열을 돌려준다 + static func toggling(_ id: UUID, in raw: String) -> String { + var ids = idList(raw) + if let index = ids.firstIndex(of: id) { + ids.remove(at: index) + } else { + ids.append(id) + } + return rawValue(ids) + } + + // MARK: 행동 배치 순서 + + /// 저장된 이 기기의 배치 순서대로 행동을 정렬한다. + /// 방어 로직: 순서 목록에 없는 행동(다른 기기에서 만들어져 CloudKit으로 새로 수신된 것 포함)은 + /// 목록 맨 끝에 자동으로 붙는다. 끝에 붙는 순서는 (구버전) sortOrder → 생성순. + static func orderedActions(_ actions: [Action], raw: String) -> [Action] { + var position: [UUID: Int] = [:] + for (index, id) in idList(raw).enumerated() { + position[id] = index + } + return actions.sorted { + switch (position[$0.uuid], position[$1.uuid]) { + case let (a?, b?): return a < b + case (.some, nil): return true + case (nil, .some): return false + case (nil, nil): + if $0.sortOrder != $1.sortOrder { return $0.sortOrder < $1.sortOrder } + return $0.createdAt < $1.createdAt + } + } + } + + static func orderedActions(_ actions: [Action]) -> [Action] { + orderedActions(actions, raw: defaults.string(forKey: LocalPrefsKeys.actionOrder) ?? "") + } + + /// 배치 편집 결과 전체를 저장 (모음 탭 드래그 완료 시) + static func saveActionOrder(_ actions: [Action]) { + defaults.set(rawValue(actions.map(\.uuid)), forKey: LocalPrefsKeys.actionOrder) + } + + /// 새 행동을 배치 맨 끝에 추가. + /// 순서를 한 번도 저장한 적 없으면(키 없음) 아무것도 하지 않는다 — + /// 이때 정렬 폴백(sortOrder→생성순)이 이미 새 행동을 맨 끝에 두기 때문. + static func appendActionToOrder(_ id: UUID) { + guard let raw = defaults.string(forKey: LocalPrefsKeys.actionOrder) else { return } + var ids = idList(raw) + guard !ids.contains(id) else { return } + ids.append(id) + defaults.set(rawValue(ids), forKey: LocalPrefsKeys.actionOrder) + } + + // MARK: 메모 창 팝업 여부 (기록 시점에 뷰 밖에서도 읽음) + + static func promptsForNote(_ id: UUID) -> Bool { + contains(id, in: defaults.string(forKey: LocalPrefsKeys.notePromptActions) ?? "") + } + + static func setPromptsForNote(_ id: UUID, enabled: Bool) { + let raw = defaults.string(forKey: LocalPrefsKeys.notePromptActions) ?? "" + let current = contains(id, in: raw) + guard current != enabled else { return } + defaults.set(toggling(id, in: raw), forKey: LocalPrefsKeys.notePromptActions) + } + + // MARK: 구버전 @Model 속성 값 1회 이관 + + /// 각 키가 아직 없을 때만, 지금까지 모델에 저장돼 있던 값으로 채운다 (앱 시작 시 호출). + /// 해당 종류의 데이터가 하나도 없으면(예: 새 기기에서 CloudKit 수신 전) 키를 만들지 않고 + /// 다음 실행에서 다시 시도한다 — 동기화로 넘어온 기존 설정을 한 번은 물려받기 위함. + @MainActor + static func adoptLegacyModelValuesIfNeeded(context: ModelContext) { + let actions = (try? context.fetch(FetchDescriptor( + sortBy: [SortDescriptor(\.sortOrder), SortDescriptor(\.createdAt)] + ))) ?? [] + if defaults.string(forKey: LocalPrefsKeys.actionOrder) == nil, !actions.isEmpty { + saveActionOrder(actions) + } + if defaults.string(forKey: LocalPrefsKeys.notePromptActions) == nil, !actions.isEmpty { + defaults.set( + rawValue(actions.filter(\.promptsForNote).map(\.uuid)), + forKey: LocalPrefsKeys.notePromptActions + ) + } + let goals = (try? context.fetch(FetchDescriptor())) ?? [] + if defaults.string(forKey: LocalPrefsKeys.pinnedGoals) == nil, !goals.isEmpty { + defaults.set( + rawValue(goals.filter(\.showsOnMain).map(\.uuid)), + forKey: LocalPrefsKeys.pinnedGoals + ) + } + if defaults.string(forKey: LocalPrefsKeys.collapsedGoals) == nil, !goals.isEmpty { + defaults.set( + rawValue(goals.filter(\.isCollapsed).map(\.uuid)), + forKey: LocalPrefsKeys.collapsedGoals + ) + } + } +} diff --git a/myApp/HaruDanim/Shared/Models.swift b/myApp/HaruDanim/Shared/Models.swift index a3bb4aa..ff0de5f 100644 --- a/myApp/HaruDanim/Shared/Models.swift +++ b/myApp/HaruDanim/Shared/Models.swift @@ -86,10 +86,13 @@ final class Action { var name: String = "" var symbolName: String = "star.fill" var trackingTypeRaw: String = TrackingType.time.rawValue + /// [레거시] 배치 순서는 기기별 로컬 설정(LocalPrefs.actionOrder)으로 이전. + /// CloudKit 스키마 유지를 위해 남겨 두며, 로컬 순서가 없는 기기의 폴백 정렬로만 쓰인다. var sortOrder: Int = 0 /// 행동 탭에서 최상단 "즐겨찾기" 섹션에 표시 var isFavorite: Bool = false - /// 기록 시(시간 측정 종료 / 횟수 추가) 메모 작성 창을 띄울지 + /// [레거시] 메모 창 표시 여부는 기기별 로컬 설정(LocalPrefs.notePromptActions)으로 이전. + /// CloudKit 스키마 유지를 위해 남겨 두며, 최초 1회 이관 시에만 읽는다. var promptsForNote: Bool = false var createdAt: Date = Date() @@ -226,9 +229,11 @@ final class Goal { var startDate: Date = Date() var endDate: Date? var statusRaw: String = GoalStatus.inProgress.rawValue - /// 목표 탭에서 하위 다짐 목록 접힘 여부 + /// [레거시] 접힘 상태는 기기별 로컬 설정(LocalPrefs.collapsedGoals)으로 이전. + /// CloudKit 스키마 유지를 위해 남겨 두며, 최초 1회 이관 시에만 읽는다. var isCollapsed: Bool = false - /// 모음 탭 상단에 진행 현황을 표시할 목표 (설정에서 다중 선택) + /// [레거시] 노출 여부는 기기별 로컬 설정(LocalPrefs.pinnedGoals)으로 이전. + /// CloudKit 스키마 유지를 위해 남겨 두며, 최초 1회 이관 시에만 읽는다. var showsOnMain: Bool = false /// 목표 탭 리스트 정렬 순서 (드래그로 변경, 값이 같으면 createdAt 순) var sortOrder: Int = 0