import SwiftUI import SwiftData /// 설정 화면: 화면 모드 선택 + 기간 지난 데이터 정리. struct SettingsView: View { @Environment(\.dismiss) private var dismiss @Environment(\.modelContext) private var context @Query private var allRecords: [ProductRecord] @AppStorage(appearanceStorageKey) private var appearanceRaw = AppearanceMode.dark.rawValue @State private var confirmDeleteExpired = false @State private var toastText: String? @State private var toastTask: Task? /// 유통기한이 이미 지난 기록(매장 무관 전체). private var expiredRecords: [ProductRecord] { allRecords.filter { KoreanCalendar.daysFromToday(to: $0.expiryDate) < 0 } } var body: some View { NavigationStack { ZStack { Theme.bg.ignoresSafeArea() ScrollView { VStack(alignment: .leading, spacing: 26) { appearanceSection dataSection } .padding(20) } if let toastText { VStack { Spacer() toast(toastText) .padding(.bottom, 30) } .transition(.move(edge: .bottom).combined(with: .opacity)) } } .navigationTitle("설정") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("완료") { dismiss() } .font(.body.weight(.semibold)) } } .animation(.snappy, value: toastText) } .confirmationDialog( deleteDialogTitle, isPresented: $confirmDeleteExpired, titleVisibility: .visible ) { Button("삭제", role: .destructive) { deleteExpired() } Button("취소", role: .cancel) {} } message: { Text("매장과 상관없이 유통기한이 지난 기록이 모두 삭제됩니다. 되돌릴 수 없어요.") } .onDisappear { toastTask?.cancel() } } // MARK: - 화면 모드 private var appearanceSection: some View { section(title: "화면 모드", subtitle: "앱 전체의 밝기 테마를 선택하세요.") { HStack(spacing: 10) { ForEach(AppearanceMode.allCases) { mode in appearanceChip(mode) } } } } private func appearanceChip(_ mode: AppearanceMode) -> some View { let isSelected = appearanceRaw == mode.rawValue return Button { guard !isSelected else { return } appearanceRaw = mode.rawValue Haptics.tap() } label: { VStack(spacing: 8) { Image(systemName: mode.icon) .font(.title3) .foregroundStyle(isSelected ? Theme.onAccent : Theme.textSecondary) Text(mode.title) .font(.system(.subheadline, design: .rounded).weight(.semibold)) .foregroundStyle(isSelected ? Theme.onAccent : Theme.textPrimary) } .frame(maxWidth: .infinity) .frame(height: 74) .background( RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(isSelected ? AnyShapeStyle(Theme.accentGradient) : AnyShapeStyle(Theme.surfaceHi)) ) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(isSelected ? .clear : Theme.stroke, lineWidth: 1) ) } .buttonStyle(.plain) } // MARK: - 데이터 관리 private var dataSection: some View { let count = expiredRecords.count return section(title: "데이터 관리", subtitle: "유통기한이 지난 기록을 한 번에 정리합니다.") { Button { confirmDeleteExpired = true } label: { HStack(spacing: 12) { Image(systemName: "trash") .font(.body.weight(.semibold)) VStack(alignment: .leading, spacing: 2) { Text("기간 지난 데이터 삭제") .font(.system(.body, design: .rounded).weight(.semibold)) Text(count > 0 ? "지난 기록 \(count)개" : "지난 기록이 없어요") .font(.caption) .foregroundStyle(count > 0 ? Theme.danger.opacity(0.8) : Theme.textSecondary) } Spacer() if count > 0 { Image(systemName: "chevron.right") .font(.footnote.weight(.semibold)) .foregroundStyle(Theme.textSecondary) } } .foregroundStyle(count > 0 ? Theme.danger : Theme.textSecondary) .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .cardStyle() } .buttonStyle(.plain) .disabled(count == 0) } } private var deleteDialogTitle: String { "지난 기록 \(expiredRecords.count)개를 삭제할까요?" } private func deleteExpired() { let targets = expiredRecords guard !targets.isEmpty else { return } for record in targets { context.delete(record) } try? context.save() Haptics.warning() showToast("지난 기록 \(targets.count)개를 삭제했어요") } // MARK: - 공용 private func section( title: String, subtitle: String, @ViewBuilder content: () -> Content ) -> some View { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 3) { Text(title) .font(.system(.headline, design: .rounded)) .foregroundStyle(Theme.textPrimary) Text(subtitle) .font(.caption) .foregroundStyle(Theme.textSecondary) } content() } } private func showToast(_ text: String) { toastTask?.cancel() toastText = text toastTask = Task { try? await Task.sleep(for: .seconds(1.8)) guard !Task.isCancelled else { return } toastText = nil } } private func toast(_ text: String) -> some View { Label(text, systemImage: "checkmark.circle.fill") .font(.system(.subheadline, design: .rounded).weight(.semibold)) .foregroundStyle(Theme.onAccent) .padding(.horizontal, 16) .padding(.vertical, 10) .background(Capsule().fill(Theme.accentGradient)) } }