// // CSVExport.swift // Haru_Danim // // 원시 데이터 CSV 생성 코어 (1.4에서 DataExportView에서 추출). // 설정 → 데이터 내보내기 화면과 '데이터 정리·초기화'(삭제 전 백업)가 공유한다. // 형식 규칙은 기존과 동일: ISO 8601 시각, UTF-8 BOM, 파일명·헤더 현지화(§7), // 값의 rawValue(time/count 등)는 데이터라 언어 무관 유지. // import Foundation enum CSVExport { /// 다섯 파일(행동·시간기록·횟수기록·목표·다짐)을 임시 디렉터리에 만들어 URL을 돌려준다. /// - range: nil = 전체. 지정하면 **기록(시간·횟수)만** 구간으로 거른다 — /// 행동·목표·다짐은 목록(구조)이라 항상 전체(가져오기 연결·백업 온전성 유지). /// 세션은 구간 겹침 포함(기록 탭 범위 조회와 같은 문법), 횟수는 시각 포함 여부. /// - fileSuffix: 파일명 뒤에 붙일 구분자(예: "-20260713-20260811"). 빈 값 = 없음 static func makeFiles( actions: [Action], sessions: [TimeSession], entries: [CountEntry], goals: [Goal], range: Range? = nil, fileSuffix: String = "" ) -> [URL] { let filteredSessions: [TimeSession] let filteredEntries: [CountEntry] if let range { let now = Date.now filteredSessions = sessions.filter { $0.startAt < range.upperBound && ($0.endAt ?? now) > range.lowerBound } filteredEntries = entries.filter { range.contains($0.timestamp) } } else { filteredSessions = sessions filteredEntries = entries } var urls: [URL?] = [] urls.append(write(String(localized: "행동") + fileSuffix, header: [ "id", String(localized: "이름"), String(localized: "추적 방식"), String(localized: "꼬리표"), String(localized: "즐겨찾기"), String(localized: "만든날"), ], rows: actions.map { action in [action.uuid.uuidString, action.name, action.trackingType == .time ? "time" : "count", action.sortedTags.map(\.name).joined(separator: "; "), action.isFavorite ? "true" : "false", iso(action.createdAt)] })) urls.append(write(String(localized: "시간기록", comment: "CSV 파일명용 (공백 없이)") + fileSuffix, header: [ String(localized: "행동id"), String(localized: "행동"), String(localized: "시작"), String(localized: "종료"), String(localized: "초"), String(localized: "메모"), ], rows: filteredSessions.compactMap { session in guard let action = session.action else { return nil } return [action.uuid.uuidString, action.name, iso(session.startAt), session.endAt.map(iso) ?? "", String(Int(session.duration())), session.note] })) urls.append(write(String(localized: "횟수기록", comment: "CSV 파일명용 (공백 없이)") + fileSuffix, header: [ String(localized: "행동id"), String(localized: "행동"), String(localized: "시각"), String(localized: "수량"), String(localized: "메모"), ], rows: filteredEntries.compactMap { entry in guard let action = entry.action else { return nil } return [action.uuid.uuidString, action.name, iso(entry.timestamp), String(entry.amount), entry.note] })) urls.append(write(String(localized: "목표") + fileSuffix, header: [ "id", String(localized: "내용"), String(localized: "시작일"), String(localized: "종료일"), String(localized: "상태"), String(localized: "달성기준%"), ], rows: goals.map { goal in [goal.uuid.uuidString, goal.title, iso(goal.startDate), goal.endDate.map(iso) ?? "", goal.statusRaw, String(goal.achieveThresholdPercent)] })) urls.append(write(String(localized: "다짐") + fileSuffix, header: [ "id", String(localized: "목표"), String(localized: "대상"), String(localized: "주기"), String(localized: "목표량"), String(localized: "방향"), ], rows: goals.flatMap(\.sortedQuests).map { quest in [quest.uuid.uuidString, quest.goal?.title ?? "", quest.targetName, quest.scheduleLabel, quest.measure == .time ? String(Int(quest.targetSeconds)) + "s" : String(quest.targetCount), quest.direction == .atLeast ? "atLeast" : "atMost"] })) return urls.compactMap(\.self) } /// 구간 파일명 접미("-yyyyMMdd-yyyyMMdd") — 하루 키 기준, 언어 무관 고정 포맷 static func fileSuffix(for range: Range, math: DayMath) -> String { let keys = math.dayKeys(in: range) guard let first = keys.first, let last = keys.last else { return "" } let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "yyyyMMdd" return "-\(formatter.string(from: first))-\(formatter.string(from: last))" } private static func iso(_ date: Date) -> String { date.formatted(.iso8601) } /// CSV 필드 이스케이프 (쉼표·따옴표·줄바꿈 포함 시 따옴표로 감쌈) private static func escape(_ field: String) -> String { if field.contains(",") || field.contains("\"") || field.contains("\n") { return "\"" + field.replacingOccurrences(of: "\"", with: "\"\"") + "\"" } return field } private static func write(_ name: String, header: [String], rows: [[String]]) -> URL? { let lines = [header.map(escape).joined(separator: ",")] + rows.map { $0.map(escape).joined(separator: ",") } // 엑셀이 UTF-8 한글을 바로 인식하도록 BOM을 붙인다 let content = "\u{FEFF}" + lines.joined(separator: "\n") let url = FileManager.default.temporaryDirectory .appendingPathComponent(String(localized: "하루다님-\(name).csv")) do { try content.write(to: url, atomically: true, encoding: .utf8) return url } catch { return nil } } }