- 배포 타깃 26.0→18.0 (앱·위젯. 워치 10.0 불변) - RadialNavigationView: glassEffect 계열을 @available(iOS 26) 선언 격리 + 18 머티리얼 원 폴백 (배치·연출 동일) - DiaryZoomContainer.Coordinator → 비제네릭 톱레벨 DiaryZoomCoordinator + AnyView 소거 (하한 18 Release wholemodule에서 swift-frontend SILPerformanceInliner 무한 재귀 크래시 실측·우회) - Image(safeSymbol:)/SymbolCompat: 카탈로그의 상위 OS 전용 심볼(18 기준 3개)이 교차 기기에서 빈 아이콘이 되지 않게 사용자 심볼 렌더 48곳+워치 7곳 폴백 - -symbolAuditDump 검증 인자 추가, iOS 18.5 시뮬 QA(빌드·radial 폴백·일기 줌·progressSelfTest 49 ALL PASS) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNxA172hNDRpDsJ2qztKPf
607 lines
24 KiB
Swift
607 lines
24 KiB
Swift
//
|
||
// ActionViews.swift
|
||
// Haru_Danim
|
||
//
|
||
// 행동 탭: 꼬리표별 그룹 리스트 + 상세 + 추가/수정 (CLAUDE.md §6.2)
|
||
//
|
||
|
||
import SwiftUI
|
||
import SwiftData
|
||
|
||
// MARK: - 행동 리스트 (꼬리표별 그룹)
|
||
|
||
struct ActionListView: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Query(sort: [SortDescriptor(\Tag.sortOrder), SortDescriptor(\Tag.createdAt)]) private var tags: [Tag]
|
||
@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 showingPremiumSheet = false
|
||
@State private var showingTagOrder = false
|
||
@State private var showingFavoriteOrder: Bool = {
|
||
#if DEBUG
|
||
// 검증용: -actionShowFavoriteOrder YES → 즐겨찾기 순서 시트 바로 표시
|
||
if UserDefaults.standard.bool(forKey: "actionShowFavoriteOrder") { return true }
|
||
#endif
|
||
return 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)
|
||
}
|
||
|
||
private var untaggedActions: [Action] {
|
||
actions.filter { $0.tags.isEmpty && !$0.isFavorite }
|
||
}
|
||
|
||
var body: some View {
|
||
List {
|
||
if !favoriteActions.isEmpty {
|
||
Section {
|
||
ForEach(favoriteActions) { action in
|
||
actionLink(action)
|
||
}
|
||
} header: {
|
||
Label("즐겨찾기", systemImage: "star.fill")
|
||
.foregroundStyle(AppTheme.yellow)
|
||
}
|
||
}
|
||
ForEach(tags) { tag in
|
||
let group = actions.filter { $0.tags.contains(tag) && !$0.isFavorite }
|
||
if !group.isEmpty {
|
||
Section {
|
||
ForEach(group) { action in
|
||
actionLink(action)
|
||
}
|
||
} header: {
|
||
HStack(spacing: 6) {
|
||
Circle().fill(tag.color).frame(width: 10, height: 10)
|
||
Text(tag.name)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if !untaggedActions.isEmpty {
|
||
Section("꼬리표 없음") {
|
||
ForEach(untaggedActions) { action in
|
||
actionLink(action)
|
||
}
|
||
}
|
||
}
|
||
if actions.isEmpty {
|
||
ContentUnavailableView(
|
||
"등록된 행동이 없어요",
|
||
systemImage: "figure.walk",
|
||
description: Text("오른쪽 위 + 버튼으로 행동을 추가하세요.")
|
||
)
|
||
}
|
||
}
|
||
.scrollContentBackground(.hidden)
|
||
.background(AppTheme.background)
|
||
.navigationTitle("행동")
|
||
.toolbar {
|
||
ToolbarItem(placement: .topBarTrailing) {
|
||
Menu {
|
||
Button {
|
||
showingTagOrder = true
|
||
} label: {
|
||
Label("꼬리표 순서 변경", systemImage: "arrow.up.arrow.down")
|
||
}
|
||
// 순서 변경은 2개 이상일 때만 의미가 있다
|
||
if favoriteActions.count >= 2 {
|
||
Button {
|
||
showingFavoriteOrder = true
|
||
} label: {
|
||
Label("즐겨찾기 순서 변경", systemImage: "star.fill")
|
||
}
|
||
}
|
||
} label: {
|
||
Image(systemName: "ellipsis.circle")
|
||
}
|
||
.accessibilityLabel(Text("메뉴"))
|
||
}
|
||
ToolbarItem(placement: .topBarTrailing) {
|
||
Button {
|
||
if !premium.canAddAction(currentCount: actions.count) {
|
||
showLimitAlert = true
|
||
} else {
|
||
showingAdd = true
|
||
}
|
||
} label: {
|
||
Image(systemName: "plus")
|
||
}
|
||
.accessibilityLabel(Text("행동 추가"))
|
||
}
|
||
}
|
||
.sheet(isPresented: $showingAdd) {
|
||
ActionEditorView(action: nil)
|
||
}
|
||
.sheet(isPresented: $showingTagOrder) {
|
||
TagOrderSheet()
|
||
}
|
||
.sheet(isPresented: $showingFavoriteOrder) {
|
||
FavoriteOrderSheet()
|
||
}
|
||
.alert("무료 사용 한도", isPresented: $showLimitAlert) {
|
||
Button("프리미엄 알아보기") { showingPremiumSheet = true }
|
||
Button("확인", role: .cancel) {}
|
||
} message: {
|
||
Text("무료 버전에서는 행동을 최대 \(FreeLimits.actions)개까지 만들 수 있어요. 프리미엄으로 업그레이드하면 무제한으로 사용할 수 있습니다.")
|
||
}
|
||
.sheet(isPresented: $showingPremiumSheet) {
|
||
PremiumSheetView()
|
||
}
|
||
}
|
||
|
||
private func actionLink(_ action: Action) -> some View {
|
||
NavigationLink {
|
||
ActionDetailView(action: action)
|
||
} label: {
|
||
ActionRow(action: action)
|
||
}
|
||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||
Button {
|
||
action.isFavorite.toggle()
|
||
// 새 즐겨찾기는 모음 탭 배치 순서상 즐겨찾기 블록 끝으로 (§6.1 "지정한 순서" 규칙)
|
||
if action.isFavorite { LocalPrefs.placeNewFavoriteAtEnd(action, context: context) }
|
||
DataChange.commit(context: context)
|
||
} label: {
|
||
Label(
|
||
action.isFavorite ? String(localized: "즐겨찾기 해제") : String(localized: "즐겨찾기"),
|
||
systemImage: action.isFavorite ? "star.slash" : "star.fill"
|
||
)
|
||
}
|
||
.tint(AppTheme.yellow)
|
||
}
|
||
}
|
||
}
|
||
|
||
struct ActionRow: View {
|
||
let action: Action
|
||
|
||
var body: some View {
|
||
HStack(spacing: 12) {
|
||
Image(safeSymbol: action.symbolName)
|
||
.font(.system(size: 16, weight: .semibold))
|
||
.foregroundStyle(.white)
|
||
.frame(width: 34, height: 34)
|
||
.background(action.color, in: RoundedRectangle(cornerRadius: 9, style: .continuous))
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
HStack(spacing: 4) {
|
||
Text(action.name)
|
||
.font(.body.weight(.medium))
|
||
if action.isFavorite {
|
||
Image(systemName: "star.fill")
|
||
.font(.caption2)
|
||
.foregroundStyle(AppTheme.yellow)
|
||
}
|
||
}
|
||
Text(action.trackingType.label)
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 꼬리표 순서 변경 시트 (행동 탭 그룹 순서에 반영)
|
||
|
||
struct TagOrderSheet: View {
|
||
@Environment(\.dismiss) private var dismiss
|
||
@Environment(\.modelContext) private var context
|
||
@Query(sort: [SortDescriptor(\Tag.sortOrder), SortDescriptor(\Tag.createdAt)]) private var tags: [Tag]
|
||
|
||
/// 드래그로 순서가 바뀌었는지 — 시트가 닫힐 때 한 번만 커밋해
|
||
/// 드래그마다 위젯·워치 갱신이 연발되지 않게 한다
|
||
@State private var orderChanged = false
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
List {
|
||
ForEach(tags) { tag in
|
||
HStack(spacing: 10) {
|
||
Circle().fill(tag.color).frame(width: 14, height: 14)
|
||
Text(tag.name)
|
||
}
|
||
}
|
||
.onMove { source, destination in
|
||
var ordered = tags
|
||
ordered.move(fromOffsets: source, toOffset: destination)
|
||
for (index, tag) in ordered.enumerated() {
|
||
tag.sortOrder = index
|
||
}
|
||
orderChanged = true
|
||
}
|
||
}
|
||
.environment(\.editMode, .constant(.active))
|
||
.navigationTitle("꼬리표 순서")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("완료") { dismiss() }
|
||
}
|
||
}
|
||
}
|
||
.presentationDetents([.medium, .large])
|
||
.onDisappear {
|
||
// 꼬리표 순서는 워치 앱 그룹핑 순서에도 쓰이므로 닫힐 때 커밋
|
||
// ('완료' 버튼과 스와이프 내리기 둘 다 이 경로를 지난다)
|
||
if orderChanged { DataChange.commit(context: context) }
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 즐겨찾기 순서 변경 시트 (모음 탭 즐겨찾기 섹션·애플워치 즐겨찾기에 반영)
|
||
|
||
struct FavoriteOrderSheet: View {
|
||
@Environment(\.dismiss) private var dismiss
|
||
@Environment(\.modelContext) private var context
|
||
@Query(sort: \Action.createdAt) private var allActions: [Action]
|
||
// 이동 즉시 목록이 새 순서로 그려지도록 배치 순서를 관찰한다
|
||
@AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = ""
|
||
|
||
/// 드래그로 순서가 바뀌었는지 — 시트가 닫힐 때 한 번만 커밋해
|
||
/// 드래그마다 위젯·워치 갱신이 연발되지 않게 한다 (TagOrderSheet와 동일 패턴)
|
||
@State private var orderChanged = false
|
||
|
||
private var favorites: [Action] {
|
||
LocalPrefs.orderedActions(allActions, raw: actionOrderRaw).filter(\.isFavorite)
|
||
}
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
List {
|
||
Section {
|
||
ForEach(favorites) { action in
|
||
HStack(spacing: 10) {
|
||
Image(safeSymbol: action.symbolName)
|
||
.font(.system(size: 13, weight: .semibold))
|
||
.foregroundStyle(.white)
|
||
.frame(width: 28, height: 28)
|
||
.background(action.color, in: RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||
Text(action.name)
|
||
.lineLimit(1)
|
||
}
|
||
}
|
||
.onMove { source, destination in
|
||
LocalPrefs.reorderFavorites(from: source, to: destination, context: context)
|
||
orderChanged = true
|
||
}
|
||
} footer: {
|
||
Text("손잡이를 끌어 순서를 바꿔요. 모음 탭 '즐겨찾기'와 애플워치의 즐겨찾기 목록에 같은 순서로 적용돼요.")
|
||
}
|
||
}
|
||
.environment(\.editMode, .constant(.active))
|
||
.navigationTitle("즐겨찾기 순서")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("완료") { dismiss() }
|
||
}
|
||
}
|
||
}
|
||
.presentationDetents([.medium, .large])
|
||
.onDisappear {
|
||
// 순서는 기기 로컬 설정이지만 위젯·워치 스냅숏이 이를 읽는다 — 닫힐 때 갱신 경로 1회
|
||
if orderChanged { DataChange.commit(context: context) }
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 행동 상세
|
||
|
||
struct ActionDetailView: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Environment(\.dismiss) private var dismiss
|
||
let action: Action
|
||
|
||
@State private var showingEdit = false
|
||
@State private var showingDelete = false
|
||
|
||
var body: some View {
|
||
List {
|
||
Section("기본 정보") {
|
||
HStack {
|
||
Text("이름")
|
||
Spacer()
|
||
Text(action.name).foregroundStyle(.secondary)
|
||
}
|
||
// 리스트 스와이프 토글과 동일하게 변경 즉시 커밋 (저장 확정)
|
||
Toggle(isOn: Binding(
|
||
get: { action.isFavorite },
|
||
set: {
|
||
action.isFavorite = $0
|
||
// 새 즐겨찾기는 모음 탭 배치 순서상 즐겨찾기 블록 끝으로 (§6.1 "지정한 순서" 규칙)
|
||
if $0 { LocalPrefs.placeNewFavoriteAtEnd(action, context: context) }
|
||
DataChange.commit(context: context)
|
||
}
|
||
)) {
|
||
Label {
|
||
Text("즐겨찾기")
|
||
} icon: {
|
||
Image(systemName: "star.fill")
|
||
.foregroundStyle(AppTheme.yellow)
|
||
}
|
||
}
|
||
// 메모 창 표시 여부는 기기별 로컬 설정 (CloudKit 동기화 제외, LocalPrefs 참고)
|
||
Toggle(isOn: Binding(
|
||
get: { LocalPrefs.promptsForNote(action.uuid) },
|
||
set: { LocalPrefs.setPromptsForNote(action.uuid, enabled: $0) }
|
||
)) {
|
||
Label {
|
||
Text("기록 시 메모 창 표시")
|
||
} icon: {
|
||
Image(systemName: "note.text")
|
||
.foregroundStyle(AppTheme.green)
|
||
}
|
||
}
|
||
HStack {
|
||
Text("아이콘")
|
||
Spacer()
|
||
Image(safeSymbol: action.symbolName)
|
||
.foregroundStyle(.white)
|
||
.frame(width: 30, height: 30)
|
||
.background(action.color, in: RoundedRectangle(cornerRadius: 8))
|
||
}
|
||
HStack {
|
||
Text("추적 방식")
|
||
Spacer()
|
||
Label(action.trackingType.label, systemImage: action.trackingType.symbol)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
Section("꼬리표") {
|
||
if action.tags.isEmpty {
|
||
Text("지정된 꼬리표 없음").foregroundStyle(.secondary)
|
||
} else {
|
||
ForEach(action.sortedTags) { tag in
|
||
HStack(spacing: 8) {
|
||
Circle().fill(tag.color).frame(width: 10, height: 10)
|
||
Text(tag.name)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Section("누적 기록") {
|
||
statsRows
|
||
}
|
||
Section {
|
||
Button("행동 삭제", role: .destructive) {
|
||
showingDelete = true
|
||
}
|
||
}
|
||
}
|
||
.scrollContentBackground(.hidden)
|
||
.background(AppTheme.background)
|
||
.navigationTitle(action.name)
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar(.hidden, for: .tabBar)
|
||
.toolbar {
|
||
ToolbarItem(placement: .topBarTrailing) {
|
||
Button("수정") { showingEdit = true }
|
||
}
|
||
}
|
||
.sheet(isPresented: $showingEdit) {
|
||
ActionEditorView(action: action)
|
||
}
|
||
.alert("행동 삭제", isPresented: $showingDelete) {
|
||
Button("삭제", role: .destructive) {
|
||
context.delete(action)
|
||
DataChange.commit(context: context)
|
||
dismiss()
|
||
}
|
||
Button("취소", role: .cancel) {}
|
||
} message: {
|
||
Text("‘\(action.name)’ 행동을 삭제할까요? 기록도 함께 삭제됩니다.")
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var statsRows: some View {
|
||
let agg = Aggregator()
|
||
let math = agg.math
|
||
let now = Date.now
|
||
switch action.trackingType {
|
||
case .time:
|
||
statRow(String(localized: "오늘"), Format.durationShort(agg.seconds(for: action, in: math.dayRange(containing: now))))
|
||
statRow(String(localized: "이번 주"), Format.durationShort(agg.seconds(for: action, in: math.weekRange(containing: now))))
|
||
statRow(String(localized: "이번 달"), Format.durationShort(agg.seconds(for: action, in: math.monthRange(containing: now))))
|
||
case .count:
|
||
statRow(String(localized: "오늘"), String(localized: "\(agg.count(for: action, in: math.dayRange(containing: now)))회"))
|
||
statRow(String(localized: "이번 주"), String(localized: "\(agg.count(for: action, in: math.weekRange(containing: now)))회"))
|
||
statRow(String(localized: "이번 달"), String(localized: "\(agg.count(for: action, in: math.monthRange(containing: now)))회"))
|
||
}
|
||
}
|
||
|
||
private func statRow(_ title: String, _ value: String) -> some View {
|
||
HStack {
|
||
Text(title)
|
||
Spacer()
|
||
Text(value).foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 행동 추가/수정
|
||
|
||
struct ActionEditorView: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Environment(\.dismiss) private var dismiss
|
||
@Query(sort: \Tag.createdAt) private var allTags: [Tag]
|
||
@Query private var allActions: [Action]
|
||
|
||
/// nil이면 새 행동 추가
|
||
let action: Action?
|
||
|
||
@State private var name = ""
|
||
@State private var symbolName = "star.fill"
|
||
@State private var trackingType: TrackingType = .time
|
||
@State private var promptsForNote = false
|
||
@State private var selectedTags: Set<PersistentIdentifier> = []
|
||
@State private var showingSymbolPicker = false
|
||
@State private var showingNewTag = false
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
Section("이름") {
|
||
TextField("행동 이름 (예: 독서)", text: $name)
|
||
}
|
||
Section("아이콘") {
|
||
Button {
|
||
showingSymbolPicker = true
|
||
} label: {
|
||
HStack {
|
||
Image(safeSymbol: symbolName)
|
||
.font(.system(size: 20))
|
||
.foregroundStyle(AppTheme.green)
|
||
.frame(width: 36, height: 36)
|
||
Text("아이콘 선택")
|
||
Spacer()
|
||
Image(systemName: "chevron.right")
|
||
.font(.caption)
|
||
.foregroundStyle(.tertiary)
|
||
}
|
||
.contentShape(Rectangle())
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
Section {
|
||
ForEach(allTags) { tag in
|
||
Button {
|
||
toggleTag(tag)
|
||
} label: {
|
||
HStack(spacing: 8) {
|
||
Circle().fill(tag.color).frame(width: 12, height: 12)
|
||
Text(tag.name)
|
||
.foregroundStyle(.primary)
|
||
Spacer()
|
||
if selectedTags.contains(tag.persistentModelID) {
|
||
Image(systemName: "checkmark")
|
||
.foregroundStyle(AppTheme.green)
|
||
.fontWeight(.semibold)
|
||
}
|
||
}
|
||
.contentShape(Rectangle())
|
||
}
|
||
}
|
||
Button {
|
||
showingNewTag = true
|
||
} label: {
|
||
Label("새 꼬리표 만들기", systemImage: "plus.circle")
|
||
.foregroundStyle(AppTheme.green)
|
||
}
|
||
} header: {
|
||
Text("꼬리표 (복수 선택 가능)")
|
||
} footer: {
|
||
Text("행동 버튼의 색은 지정한 꼬리표의 색을 따라요. 여러 개를 지정하면 가장 먼저 만든 꼬리표의 색이 쓰여요.")
|
||
}
|
||
Section {
|
||
Picker("추적 방식", selection: $trackingType) {
|
||
ForEach(TrackingType.allCases) { type in
|
||
Label(type.label, systemImage: type.symbol).tag(type)
|
||
}
|
||
}
|
||
.pickerStyle(.inline)
|
||
.labelsHidden()
|
||
// 기존 행동의 추적 방식을 바꾸면 이미 쌓인 기록이 화면·통계에서
|
||
// 보이지 않는 것처럼 되므로 생성 시에만 선택 가능 (도움말 안내와 일치)
|
||
.disabled(action != nil)
|
||
} header: {
|
||
Text("추적 방식")
|
||
} footer: {
|
||
if action != nil {
|
||
Text("추적 방식은 행동을 만들 때만 선택할 수 있어요.")
|
||
}
|
||
}
|
||
Section {
|
||
Toggle(isOn: $promptsForNote) {
|
||
Label {
|
||
Text("기록 시 메모 창 표시")
|
||
} icon: {
|
||
Image(systemName: "note.text")
|
||
.foregroundStyle(AppTheme.green)
|
||
}
|
||
}
|
||
} footer: {
|
||
Text(trackingType == .time
|
||
? String(localized: "켜면 시간 측정을 종료할 때 메모 창이 떠요. 건너뛸 수 있어요.")
|
||
: String(localized: "켜면 횟수를 추가할 때마다 메모 창이 떠요. 건너뛸 수 있어요."))
|
||
}
|
||
}
|
||
.navigationTitle(action == nil ? String(localized: "행동 추가") : String(localized: "행동 수정"))
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .cancellationAction) {
|
||
Button("취소") { dismiss() }
|
||
}
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("저장") { save() }
|
||
.disabled(name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||
}
|
||
}
|
||
.sheet(isPresented: $showingSymbolPicker) {
|
||
SymbolPickerView(selection: $symbolName)
|
||
}
|
||
.sheet(isPresented: $showingNewTag) {
|
||
TagEditorView(tag: nil)
|
||
}
|
||
.onAppear(perform: load)
|
||
}
|
||
}
|
||
|
||
private func load() {
|
||
guard let action else { return }
|
||
name = action.name
|
||
symbolName = action.symbolName
|
||
trackingType = action.trackingType
|
||
promptsForNote = LocalPrefs.promptsForNote(action.uuid)
|
||
selectedTags = Set(action.tags.map(\.persistentModelID))
|
||
}
|
||
|
||
private func toggleTag(_ tag: Tag) {
|
||
if selectedTags.contains(tag.persistentModelID) {
|
||
selectedTags.remove(tag.persistentModelID)
|
||
} else {
|
||
selectedTags.insert(tag.persistentModelID)
|
||
}
|
||
}
|
||
|
||
private func save() {
|
||
let tags = allTags.filter { selectedTags.contains($0.persistentModelID) }
|
||
let trimmed = name.trimmingCharacters(in: .whitespaces)
|
||
if let action {
|
||
action.name = trimmed
|
||
action.symbolName = symbolName
|
||
action.trackingType = trackingType
|
||
action.tags = tags
|
||
LocalPrefs.setPromptsForNote(action.uuid, enabled: promptsForNote)
|
||
} else {
|
||
// sortOrder는 로컬 배치 순서가 없는 기기(첫 실행·다른 기기)에서의 폴백 정렬용으로만 기록
|
||
let maxOrder = allActions.map(\.sortOrder).max() ?? -1
|
||
let newAction = Action(
|
||
name: trimmed,
|
||
symbolName: symbolName,
|
||
trackingType: trackingType,
|
||
sortOrder: maxOrder + 1
|
||
)
|
||
newAction.tags = tags
|
||
context.insert(newAction)
|
||
// 이 기기의 로컬 배치 맨 끝에 추가 + 메모 창 설정 저장
|
||
LocalPrefs.appendActionToOrder(newAction.uuid)
|
||
LocalPrefs.setPromptsForNote(newAction.uuid, enabled: promptsForNote)
|
||
}
|
||
DataChange.commit(context: context)
|
||
dismiss()
|
||
}
|
||
}
|