- 데이터 정리·초기화에 '정리할 데이터' 축(기록과 일기/일기만) 추가 — 일기만은 기록·구조·양식 보존, 백업은 일기 PDF만, DataReset.deleteDiaries(전체/날짜까지) - 자가 검증 18→27건(일기만 9건: 기준일 포함 삭제·기록/행동/양식 보존·cascade) — 26·18.5 심 ALL PASS - 라디얼 모드 FAB가 목록 마지막 행을 가리던 실측 문제 수정 — 탭 루트(NavigationStack 안쪽) 하단 safeAreaInset 78pt, 26·18.5 시각 확인 - 도움말 '데이터 정리·초기화'에 일기만 문구, 신규 12키 en/ja 완역(카탈로그 missing/stale 0) - whats-new-1.4에 일기만 반영 + promotional-text-1.4.txt 신설(ko 154/en 169/ja 139자 — 이용자 3명 감사 멘트) - iOS 18.5 심층 QA: 자가 검증 106건 ALL PASS + 라디얼/위젯/초기화 화면 확인 - CURRENT_PROJECT_VERSION 2→3, Debug/Store/워치 3빌드 성공 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WtZwRRjbtM9pZJDM4FkTq6
518 lines
23 KiB
Swift
518 lines
23 KiB
Swift
//
|
||
// DataResetView.swift
|
||
// Haru_Danim
|
||
//
|
||
// 데이터 정리·초기화 (1.4, 설정 → 데이터). 무료 — 자기 데이터에 대한 접근권.
|
||
// 범위: 전체 / 처음부터 지정한 날짜까지(기록·일기만, 구조 보존 — DataReset 참고).
|
||
// 삭제 전 백업(CSV·일기 PDF·정리 리포트)을 같은 화면에서 만들어 공유하고,
|
||
// 2단계 확인(다이얼로그 → 얼럿)을 거쳐 삭제한다. 삭제 코어는 DataReset,
|
||
// 백업 부품은 CSVExport·DiaryExportRenderer·ArchiveReport 재사용.
|
||
//
|
||
|
||
import SwiftUI
|
||
import SwiftData
|
||
|
||
struct DataResetView: View {
|
||
@Environment(\.modelContext) private var context
|
||
@Environment(\.dismiss) private var dismiss
|
||
@AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = ""
|
||
|
||
private enum RangeMode: String, CaseIterable, Identifiable {
|
||
case all, upTo
|
||
var id: String { rawValue }
|
||
var label: String {
|
||
switch self {
|
||
case .all: return String(localized: "전체")
|
||
case .upTo: return String(localized: "날짜 지정")
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 정리 대상 (1.4(2)): 기록·일기 전부 vs 일기만.
|
||
/// 일기만 모드는 사진·필기로 커진 일기 용량만 줄이고 기록·구조는 남긴다
|
||
private enum ResetScope: String, CaseIterable, Identifiable {
|
||
case recordsAndDiary, diaryOnly
|
||
var id: String { rawValue }
|
||
var label: String {
|
||
switch self {
|
||
case .recordsAndDiary: return String(localized: "기록과 일기")
|
||
case .diaryOnly: return String(localized: "일기만")
|
||
}
|
||
}
|
||
}
|
||
|
||
private enum ReportFormat: String, CaseIterable, Identifiable {
|
||
case pdf, image
|
||
var id: String { rawValue }
|
||
var label: String {
|
||
switch self {
|
||
case .pdf: return "PDF"
|
||
case .image: return String(localized: "이미지")
|
||
}
|
||
}
|
||
}
|
||
|
||
@State private var scope: ResetScope = .recordsAndDiary
|
||
@State private var rangeMode: RangeMode = .all
|
||
@State private var endDate: Date = Calendar.current.date(byAdding: .month, value: -3, to: .now) ?? .now
|
||
@State private var includeCSV = true
|
||
@State private var includeDiary = true
|
||
@State private var includeReport = true
|
||
@State private var reportFormat: ReportFormat = .pdf
|
||
@State private var isGenerating = false
|
||
@State private var backupURLs: [URL] = []
|
||
@State private var confirmingDelete = false
|
||
@State private var finalConfirm = false
|
||
@State private var isDeleting = false
|
||
@State private var deletedCounts: DataReset.Counts?
|
||
|
||
private var math: DayMath { DayMath() }
|
||
|
||
/// 선택한 달력일(포함)까지 — DatePicker의 달력일을 키로 직접 사용 (§4.1)
|
||
private var endKey: Date { math.calendar.startOfDay(for: endDate) }
|
||
private var upperBound: Date { math.dayRange(forKey: endKey).upperBound }
|
||
|
||
/// 백업(CSV·리포트)용 범위 — 삭제와 달리 경계 걸침 세션까지 담는 게 안전해 겹침 포함
|
||
private var backupRange: Range<Date>? {
|
||
rangeMode == .all ? nil : Date.distantPast..<upperBound
|
||
}
|
||
|
||
/// 이 구성에서 만들 백업이 있는지 — 일기만 모드는 일기 PDF만 대상
|
||
/// (CSV·정리 리포트는 기록 백업이라 일기만 지울 땐 만들지 않는다)
|
||
private var wantsBackup: Bool {
|
||
scope == .diaryOnly ? includeDiary : (includeCSV || includeDiary || includeReport)
|
||
}
|
||
|
||
/// 백업을 하나도 안 만들거나, 만들었을 때만 삭제로 진행 가능
|
||
private var canDelete: Bool {
|
||
(!wantsBackup || !backupURLs.isEmpty) && !isGenerating && !isDeleting
|
||
&& deletedCounts == nil
|
||
}
|
||
|
||
// MARK: 삭제 대상 집계 (fetchCount — 전량 로드 없음)
|
||
|
||
private var targetSessionCount: Int {
|
||
let farFuture = Date.distantFuture
|
||
let descriptor: FetchDescriptor<TimeSession>
|
||
if rangeMode == .all {
|
||
descriptor = FetchDescriptor<TimeSession>()
|
||
} else {
|
||
let upper = upperBound
|
||
descriptor = FetchDescriptor<TimeSession>(
|
||
predicate: #Predicate { ($0.endAt ?? farFuture) <= upper }
|
||
)
|
||
}
|
||
return (try? context.fetchCount(descriptor)) ?? 0
|
||
}
|
||
|
||
private var targetEntryCount: Int {
|
||
let descriptor: FetchDescriptor<CountEntry>
|
||
if rangeMode == .all {
|
||
descriptor = FetchDescriptor<CountEntry>()
|
||
} else {
|
||
let upper = upperBound
|
||
descriptor = FetchDescriptor<CountEntry>(predicate: #Predicate { $0.timestamp < upper })
|
||
}
|
||
return (try? context.fetchCount(descriptor)) ?? 0
|
||
}
|
||
|
||
private var targetDiaryCount: Int {
|
||
let descriptor: FetchDescriptor<DiaryEntry>
|
||
if rangeMode == .all {
|
||
descriptor = FetchDescriptor<DiaryEntry>()
|
||
} else {
|
||
let key = endKey
|
||
descriptor = FetchDescriptor<DiaryEntry>(predicate: #Predicate { $0.dayKey <= key })
|
||
}
|
||
return (try? context.fetchCount(descriptor)) ?? 0
|
||
}
|
||
|
||
private var targetActionCount: Int {
|
||
(try? context.fetchCount(FetchDescriptor<Action>())) ?? 0
|
||
}
|
||
|
||
private var targetGoalCount: Int {
|
||
(try? context.fetchCount(FetchDescriptor<Goal>())) ?? 0
|
||
}
|
||
|
||
var body: some View {
|
||
List {
|
||
if let counts = deletedCounts {
|
||
doneSection(counts)
|
||
} else {
|
||
rangeSection
|
||
targetSection
|
||
backupSection
|
||
deleteSection
|
||
}
|
||
}
|
||
.tint(AppTheme.green)
|
||
.scrollContentBackground(.hidden)
|
||
.background(AppTheme.background)
|
||
.navigationTitle("데이터 정리·초기화")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar(.hidden, for: .tabBar)
|
||
.onChange(of: scope) { backupURLs = [] }
|
||
.onChange(of: rangeMode) { backupURLs = [] }
|
||
.onChange(of: endDate) { backupURLs = [] }
|
||
.onChange(of: includeCSV) { backupURLs = [] }
|
||
.onChange(of: includeDiary) { backupURLs = [] }
|
||
.onChange(of: includeReport) { backupURLs = [] }
|
||
.onChange(of: reportFormat) { backupURLs = [] }
|
||
.confirmationDialog(
|
||
confirmTitle,
|
||
isPresented: $confirmingDelete,
|
||
titleVisibility: .visible
|
||
) {
|
||
Button("삭제 진행", role: .destructive) { finalConfirm = true }
|
||
} message: {
|
||
Text(confirmMessage)
|
||
}
|
||
.alert("정말 삭제할까요?", isPresented: $finalConfirm) {
|
||
Button("취소", role: .cancel) {}
|
||
Button("삭제", role: .destructive) { performDelete() }
|
||
} message: {
|
||
Text("삭제는 되돌릴 수 없어요. 백업 파일을 저장했는지 다시 확인해 주세요.")
|
||
}
|
||
#if DEBUG
|
||
.onAppear {
|
||
// 검증용: -dataResetScope diary → '일기만' 선택 상태로 시작
|
||
if UserDefaults.standard.string(forKey: "dataResetScope") == "diary" {
|
||
scope = .diaryOnly
|
||
}
|
||
// 검증용: -dataResetRangeDays <N> → '날짜 지정'(오늘-N일까지)로 시작
|
||
let days = UserDefaults.standard.integer(forKey: "dataResetRangeDays")
|
||
if days > 0 {
|
||
rangeMode = .upTo
|
||
endDate = math.calendar.date(byAdding: .day, value: -days, to: .now) ?? .now
|
||
}
|
||
// 검증용: -dataResetBackupRun pdf|image → 백업 3종 자동 생성 후 Documents 복사
|
||
if let raw = UserDefaults.standard.string(forKey: "dataResetBackupRun") {
|
||
includeCSV = true
|
||
includeDiary = true
|
||
includeReport = true
|
||
reportFormat = raw == "image" ? .image : .pdf
|
||
makeBackups(copyToDocuments: true)
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
// MARK: 섹션들
|
||
|
||
private var rangeSection: some View {
|
||
Section {
|
||
Picker("정리할 데이터", selection: $scope.animation()) {
|
||
ForEach(ResetScope.allCases) { scope in
|
||
Text(scope.label).tag(scope)
|
||
}
|
||
}
|
||
.pickerStyle(.segmented)
|
||
Picker("범위", selection: $rangeMode) {
|
||
ForEach(RangeMode.allCases) { mode in
|
||
Text(mode.label).tag(mode)
|
||
}
|
||
}
|
||
.pickerStyle(.segmented)
|
||
if rangeMode == .upTo {
|
||
DatePicker("까지", selection: $endDate, in: ...Date.now, displayedComponents: .date)
|
||
}
|
||
} header: {
|
||
Text("정리할 범위")
|
||
} footer: {
|
||
switch (scope, rangeMode) {
|
||
case (.recordsAndDiary, .all):
|
||
Text("모든 행동·꼬리표·목표·다짐·기록·일기·양식을 삭제하고 처음 상태로 돌아가요. 앱 설정(테마·하루 시작 시간 등)과 프리미엄은 그대로예요.")
|
||
case (.recordsAndDiary, .upTo):
|
||
Text("처음부터 지정한 날짜(포함)까지의 시간·횟수 기록과 일기만 삭제해요. 행동·꼬리표·목표·다짐과 양식 라이브러리는 남아요. 지정한 날짜에 걸쳐 있는 측정과 진행 중인 측정은 남아요.")
|
||
case (.diaryOnly, .all):
|
||
Text("모든 일기(필기·사진·할 일 포함)를 삭제해요. 시간·횟수 기록과 행동·목표, 일기 양식은 그대로 남아요 — 사진·필기로 커진 일기 용량만 줄일 때 좋아요.")
|
||
case (.diaryOnly, .upTo):
|
||
Text("처음부터 지정한 날짜(포함)까지의 일기만 삭제해요. 시간·횟수 기록과 행동·목표, 일기 양식은 그대로 남아요.")
|
||
}
|
||
}
|
||
}
|
||
|
||
private var targetSection: some View {
|
||
Section("삭제 대상") {
|
||
if scope == .recordsAndDiary {
|
||
countRow("timer", String(localized: "시간 기록"), String(localized: "\(targetSessionCount)건"))
|
||
countRow("number", String(localized: "횟수 기록"), String(localized: "\(targetEntryCount)건"))
|
||
}
|
||
countRow("book.closed.fill", String(localized: "일기"), String(localized: "\(targetDiaryCount)일"))
|
||
if scope == .recordsAndDiary && rangeMode == .all {
|
||
countRow("figure.walk", String(localized: "행동"), String(localized: "\(targetActionCount)개"))
|
||
countRow("flag.checkered", String(localized: "목표"), String(localized: "\(targetGoalCount)개"))
|
||
}
|
||
}
|
||
}
|
||
|
||
private var backupSection: some View {
|
||
Section {
|
||
// 일기만 모드에서는 기록 백업(CSV·정리 리포트) 토글을 숨긴다 — 대상이 아님
|
||
if scope == .recordsAndDiary {
|
||
Toggle(isOn: $includeCSV) {
|
||
Label("CSV 내보내기", systemImage: "tablecells")
|
||
}
|
||
}
|
||
Toggle(isOn: $includeDiary) {
|
||
Label("일기 PDF", systemImage: "book.closed")
|
||
}
|
||
if scope == .recordsAndDiary {
|
||
Toggle(isOn: $includeReport) {
|
||
Label("정리 리포트", systemImage: "doc.richtext")
|
||
}
|
||
if includeReport {
|
||
Picker("리포트 형식", selection: $reportFormat) {
|
||
ForEach(ReportFormat.allCases) { format in
|
||
Text(format.label).tag(format)
|
||
}
|
||
}
|
||
.pickerStyle(.segmented)
|
||
}
|
||
}
|
||
if isGenerating {
|
||
HStack {
|
||
ProgressView()
|
||
Text("백업 만드는 중…")
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
} else if backupURLs.isEmpty {
|
||
if wantsBackup {
|
||
Button {
|
||
makeBackups()
|
||
} label: {
|
||
Label("백업 파일 만들기", systemImage: "square.and.arrow.down.on.square")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
}
|
||
} else {
|
||
ShareLink(items: backupURLs) {
|
||
Label("백업 파일 \(backupURLs.count)개 공유", systemImage: "square.and.arrow.up")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
Button("다시 만들기") {
|
||
backupURLs = []
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
} header: {
|
||
Text("삭제 전 백업")
|
||
} footer: {
|
||
Text(scope == .diaryOnly
|
||
? "삭제할 일기들을 PDF 하나로 만들어 보관할 수 있어요. 백업을 켰다면 파일을 만들어 저장한 뒤에 삭제할 수 있어요."
|
||
: "CSV는 '데이터 가져오기'로 복원할 수 있는 원본 백업이에요. 정리 리포트는 삭제할 기록을 한눈에 정리한 문서예요(기간·행동별 누적·꼬리표·월별·목표 이력). 백업을 켰다면 파일을 만들어 저장한 뒤에 삭제할 수 있어요.")
|
||
}
|
||
}
|
||
|
||
private var deleteSection: some View {
|
||
Section {
|
||
if isDeleting {
|
||
HStack {
|
||
ProgressView()
|
||
Text("삭제하는 중…")
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
} else {
|
||
Button(role: .destructive) {
|
||
confirmingDelete = true
|
||
} label: {
|
||
Label(deleteButtonTitle, systemImage: "trash")
|
||
.frame(maxWidth: .infinity)
|
||
}
|
||
.disabled(!canDelete)
|
||
}
|
||
} footer: {
|
||
if !canDelete && !isDeleting && deletedCounts == nil {
|
||
Text("먼저 위에서 백업 파일을 만들어 주세요. 백업 없이 삭제하려면 백업 항목을 모두 꺼요.")
|
||
}
|
||
}
|
||
}
|
||
|
||
private func doneSection(_ counts: DataReset.Counts) -> some View {
|
||
Section {
|
||
Label {
|
||
Text("삭제를 마쳤어요")
|
||
.font(.headline)
|
||
} icon: {
|
||
Image(systemName: "checkmark.circle.fill")
|
||
.foregroundStyle(AppTheme.green)
|
||
}
|
||
if scope == .recordsAndDiary {
|
||
countRow("timer", String(localized: "시간 기록"), String(localized: "\(counts.sessions)건"))
|
||
countRow("number", String(localized: "횟수 기록"), String(localized: "\(counts.entries)건"))
|
||
}
|
||
countRow("book.closed.fill", String(localized: "일기"), String(localized: "\(counts.diaryEntries)일"))
|
||
if counts.actions > 0 || counts.goals > 0 {
|
||
countRow("figure.walk", String(localized: "행동"), String(localized: "\(counts.actions)개"))
|
||
countRow("flag.checkered", String(localized: "목표"), String(localized: "\(counts.goals)개"))
|
||
}
|
||
Button("완료") { dismiss() }
|
||
.frame(maxWidth: .infinity)
|
||
} footer: {
|
||
Text("iCloud 동기화를 켰다면 다른 기기에는 잠시 뒤에 반영돼요.")
|
||
}
|
||
}
|
||
|
||
private var confirmTitle: String {
|
||
switch (scope, rangeMode) {
|
||
case (.recordsAndDiary, .all):
|
||
return String(localized: "모든 데이터를 삭제할까요?")
|
||
case (.recordsAndDiary, .upTo):
|
||
return String(localized: "\(Format.fullDate(endKey))까지의 기록·일기를 삭제할까요?")
|
||
case (.diaryOnly, .all):
|
||
return String(localized: "모든 일기를 삭제할까요?")
|
||
case (.diaryOnly, .upTo):
|
||
return String(localized: "\(Format.fullDate(endKey))까지의 일기를 삭제할까요?")
|
||
}
|
||
}
|
||
|
||
private var confirmMessage: String {
|
||
switch (scope, rangeMode) {
|
||
case (.recordsAndDiary, .all):
|
||
return String(localized: "행동·꼬리표·목표·다짐·기록·일기·양식이 모두 삭제돼요. iCloud 동기화를 켰다면 다른 기기에서도 함께 삭제돼요.")
|
||
case (.recordsAndDiary, .upTo):
|
||
return String(localized: "지정한 날짜까지의 시간·횟수 기록과 일기가 삭제돼요. 과거 기록이 사라지면 다짐의 연속 달성 기록이 끊겨요. iCloud 동기화를 켰다면 다른 기기에서도 함께 삭제돼요.")
|
||
case (.diaryOnly, _):
|
||
return String(localized: "일기의 필기·사진·할 일이 함께 삭제돼요. 시간·횟수 기록과 행동·목표, 일기 양식은 남아요. iCloud 동기화를 켰다면 다른 기기에서도 함께 삭제돼요.")
|
||
}
|
||
}
|
||
|
||
private var deleteButtonTitle: String {
|
||
switch (scope, rangeMode) {
|
||
case (.recordsAndDiary, .all): return String(localized: "전체 데이터 삭제…")
|
||
case (.recordsAndDiary, .upTo): return String(localized: "지정한 날짜까지 삭제…")
|
||
case (.diaryOnly, .all): return String(localized: "모든 일기 삭제…")
|
||
case (.diaryOnly, .upTo): return String(localized: "지정한 날짜까지 일기 삭제…")
|
||
}
|
||
}
|
||
|
||
private func countRow(_ symbol: String, _ title: String, _ value: String) -> some View {
|
||
HStack {
|
||
Label {
|
||
Text(title)
|
||
} icon: {
|
||
Image(systemName: symbol)
|
||
.foregroundStyle(AppTheme.green)
|
||
}
|
||
Spacer()
|
||
Text(value).foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
|
||
// MARK: 백업 생성 (부품 재사용 — CSVExport·DiaryExportRenderer·ArchiveReport)
|
||
|
||
private func makeBackups(copyToDocuments: Bool = false) {
|
||
isGenerating = true
|
||
Task { @MainActor in
|
||
try? await Task.sleep(for: .milliseconds(60))
|
||
var urls: [URL] = []
|
||
do {
|
||
let actions = try context.fetch(
|
||
FetchDescriptor<Action>(sortBy: [SortDescriptor(\.createdAt)])
|
||
)
|
||
let sessions = try context.fetch(
|
||
FetchDescriptor<TimeSession>(sortBy: [SortDescriptor(\.startAt)])
|
||
)
|
||
let entries = try context.fetch(
|
||
FetchDescriptor<CountEntry>(sortBy: [SortDescriptor(\.timestamp)])
|
||
)
|
||
let goals = try context.fetch(FetchDescriptor<Goal>(
|
||
sortBy: [SortDescriptor(\.sortOrder), SortDescriptor(\.createdAt)]
|
||
))
|
||
let diaryDescriptor: FetchDescriptor<DiaryEntry> = {
|
||
if rangeMode == .all {
|
||
return FetchDescriptor<DiaryEntry>(sortBy: [SortDescriptor(\.dayKey)])
|
||
}
|
||
let key = endKey
|
||
return FetchDescriptor<DiaryEntry>(
|
||
predicate: #Predicate { $0.dayKey <= key },
|
||
sortBy: [SortDescriptor(\.dayKey)]
|
||
)
|
||
}()
|
||
let diaryEntries = try context.fetch(diaryDescriptor).filter(\.hasContent)
|
||
|
||
if scope == .recordsAndDiary && includeCSV {
|
||
// ⚠️ CSVExport.fileSuffix(for:)는 dayKeys 순회라 distantPast 범위 금지 —
|
||
// '처음~까지'는 끝 날짜만 접미로 표기한다
|
||
var suffix = ""
|
||
if rangeMode == .upTo {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||
formatter.dateFormat = "yyyyMMdd"
|
||
suffix = "-upto-\(formatter.string(from: endKey))"
|
||
}
|
||
urls += CSVExport.makeFiles(
|
||
actions: actions, sessions: sessions, entries: entries, goals: goals,
|
||
range: backupRange, fileSuffix: suffix
|
||
)
|
||
}
|
||
if includeDiary, !diaryEntries.isEmpty {
|
||
let orderedActions = LocalPrefs.orderedActions(actions, raw: actionOrderRaw)
|
||
let templates = try context.fetch(FetchDescriptor<DiaryTemplate>())
|
||
let renderer = DiaryExportRenderer(
|
||
context: context, orderedActions: orderedActions,
|
||
templates: templates, math: math
|
||
)
|
||
let slug = String(localized: "일기-정리-\(DiaryExportRenderer.slugDate(diaryEntries.first!.dayKey))~\(DiaryExportRenderer.slugDate(diaryEntries.last!.dayKey))")
|
||
if let url = renderer.pdfURL(entries: diaryEntries, fileSlug: slug) {
|
||
urls.append(url)
|
||
}
|
||
}
|
||
if scope == .recordsAndDiary && includeReport {
|
||
if let url = ArchiveReport.makeURL(
|
||
format: reportFormat == .pdf ? .pdf : .image,
|
||
actions: actions, sessions: sessions, entries: entries, goals: goals,
|
||
diaryDayKeys: diaryEntries.map(\.dayKey),
|
||
range: backupRange, math: math
|
||
) {
|
||
urls.append(url)
|
||
}
|
||
}
|
||
} catch {
|
||
urls = []
|
||
}
|
||
backupURLs = urls
|
||
isGenerating = false
|
||
#if DEBUG
|
||
if copyToDocuments,
|
||
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||
for url in urls {
|
||
let dest = docs.appendingPathComponent(url.lastPathComponent)
|
||
try? FileManager.default.removeItem(at: dest)
|
||
try? FileManager.default.copyItem(at: url, to: dest)
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
|
||
// MARK: 삭제 실행
|
||
|
||
private func performDelete() {
|
||
isDeleting = true
|
||
Task { @MainActor in
|
||
try? await Task.sleep(for: .milliseconds(60))
|
||
do {
|
||
let counts: DataReset.Counts
|
||
switch (scope, rangeMode) {
|
||
case (.diaryOnly, .all):
|
||
counts = try DataReset.deleteDiaries(upToDayKey: nil, context: context)
|
||
case (.diaryOnly, .upTo):
|
||
counts = try DataReset.deleteDiaries(upToDayKey: endKey, context: context)
|
||
case (.recordsAndDiary, .all):
|
||
counts = try DataReset.deleteAll(context: context)
|
||
case (.recordsAndDiary, .upTo):
|
||
counts = try DataReset.deleteRecords(upToDayKey: endKey, context: context, math: math)
|
||
}
|
||
// 저장 + 위젯·워치·LiveActivity 동기화 (진행 중 측정도 전체 삭제면 함께 정리됨)
|
||
DataChange.commit(context: context)
|
||
deletedCounts = counts
|
||
} catch {
|
||
deletedCounts = nil
|
||
}
|
||
isDeleting = false
|
||
}
|
||
}
|
||
}
|