diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index ec3fb32..b2f3392 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -225,6 +225,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f - `ExportSnapshot`: SwiftData 비의존 값 타입 (title/periodLabel/filterNote/hero/sections). 섹션 종류: timetable / records / bars / lines / table - `ExportBuilder.history(...)` / `.stats(...)`: 화면과 동일한 집계 규칙으로 스냅숏 생성. `trimHours: false`면 기록이 없어도 24시간 전체 그리드(일기용) - `injectingCalendarEvents(_:)`: 하루 타임테이블에 캘린더 블록 주입. `replacingTimetableSections(with:)`: 일기 필터용 — 타임테이블 섹션만 교체하고 나머지는 유지 +- **내보내기 파일명은 언어별 현지화** — ko "하루다님-기록-…" / en "HaruDanim-History-…" / ja "HaruDanim-記録-…" (수신 환경이 한글을 지원하지 않을 수 있어 접두는 en/ja에서 라틴 통일). 리포트 PNG·일기 PDF/PNG·CSV 5종 모두 적용, 패턴은 카탈로그 포맷 키("하루다님-%@.png" 등). CSV 헤더는 데이터로 보고 한국어 유지 - `ExportPosterView`: 고정폭 430pt, ImageRenderer scale 3 → 1290px PNG. 라이트/다크는 `.environment(\.colorScheme)` 강제. `ExportSectionView`/`ExportHeroRow`는 일기 요약 화면과 공용 — **화면과 내보내기 결과가 항상 일치** - 일기 인쇄용: `DiaryPrintSummaryView`(768pt) + `DiaryPrintNotePageView`(768×1024, 라이트 고정) diff --git a/myApp/HaruDanim/IOS/Localizable.xcstrings b/myApp/HaruDanim/IOS/Localizable.xcstrings index e4da984..cade239 100644 --- a/myApp/HaruDanim/IOS/Localizable.xcstrings +++ b/myApp/HaruDanim/IOS/Localizable.xcstrings @@ -2105,6 +2105,38 @@ } } }, + "기록-%@": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "History-%@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "記録-%@" + } + } + } + }, + "기록-주간-%@": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "History-Weekly-%@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "記録-週間-%@" + } + } + } + }, "기록·통계 탭": { "localizations": { "en": { @@ -6254,6 +6286,23 @@ } } }, + "시간기록": { + "comment": "CSV 파일명용 (공백 없이)", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "TimeRecords" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "時間記録" + } + } + } + }, "시간을 측정 중이면 다이나믹 아일랜드와 잠금화면에 진행 시간이 표시돼요. 여러 개를 동시에 측정하면 설정의 '대표 시간 기준'(먼저/나중에 시작한 행동)에 따라 하나가 대표로 보이고 나머지는 +N으로 표시돼요.": { "localizations": { "en": { @@ -8192,6 +8241,44 @@ } } }, + "일기-%@": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Diary-%@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "日記-%@" + } + } + } + }, + "일기-%@~%@": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "일기-%1$@~%2$@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Diary-%@~%@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "日記-%@~%@" + } + } + } + }, "일기는 프리미엄 기능이에요": { "localizations": { "en": { @@ -9969,6 +10056,28 @@ } } }, + "통계-%@-%@": { + "localizations": { + "ko": { + "stringUnit": { + "state": "new", + "value": "통계-%1$@-%2$@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Stats-%@-%@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "統計-%@-%@" + } + } + } + }, "특정 기간": { "localizations": { "en": { @@ -10786,6 +10895,70 @@ } } }, + "하루다님-%@.csv": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "HaruDanim-%@.csv" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "HaruDanim-%@.csv" + } + } + } + }, + "하루다님-%@.pdf": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "HaruDanim-%@.pdf" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "HaruDanim-%@.pdf" + } + } + } + }, + "하루다님-%@.png": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "HaruDanim-%@.png" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "HaruDanim-%@.png" + } + } + } + }, + "하루다님-일기-%@.png": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "HaruDanim-Diary-%@.png" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "HaruDanim-日記-%@.png" + } + } + } + }, "하루의 기록·통계와 함께 애플 펜슬로 쓰는\n나만의 다이어리를 만들 수 있어요.": { "localizations": { "en": { @@ -11982,6 +12155,23 @@ } } } + }, + "횟수기록": { + "comment": "CSV 파일명용 (공백 없이)", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "CountRecords" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "回数記録" + } + } + } } }, "version": "1.0" diff --git a/myApp/HaruDanim/IOS/Views/DataExportView.swift b/myApp/HaruDanim/IOS/Views/DataExportView.swift index 5864767..dd89ffa 100644 --- a/myApp/HaruDanim/IOS/Views/DataExportView.swift +++ b/myApp/HaruDanim/IOS/Views/DataExportView.swift @@ -93,7 +93,8 @@ struct DataExportView: View { isRendering = true Task { @MainActor in var urls: [URL?] = [] - urls.append(write("행동", header: ["id", "이름", "추적방식", "꼬리표", "즐겨찾기", "만든날"], + // 파일명은 언어별 현지화 (수신 환경이 한글을 지원하지 않을 수 있음 — 헤더는 데이터라 유지) + urls.append(write(String(localized: "행동"), header: ["id", "이름", "추적방식", "꼬리표", "즐겨찾기", "만든날"], rows: actions.map { action in [action.uuid.uuidString, action.name, action.trackingType == .time ? "time" : "count", @@ -101,7 +102,7 @@ struct DataExportView: View { action.isFavorite ? "true" : "false", iso(action.createdAt)] })) - urls.append(write("시간기록", header: ["행동id", "행동", "시작", "종료", "초", "메모"], + urls.append(write(String(localized: "시간기록", comment: "CSV 파일명용 (공백 없이)"), header: ["행동id", "행동", "시작", "종료", "초", "메모"], rows: sessions.compactMap { session in guard let action = session.action else { return nil } return [action.uuid.uuidString, action.name, @@ -110,19 +111,19 @@ struct DataExportView: View { String(Int(session.duration())), session.note] })) - urls.append(write("횟수기록", header: ["행동id", "행동", "시각", "수량", "메모"], + urls.append(write(String(localized: "횟수기록", comment: "CSV 파일명용 (공백 없이)"), header: ["행동id", "행동", "시각", "수량", "메모"], rows: entries.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("목표", header: ["id", "내용", "시작일", "종료일", "상태", "달성기준%"], + urls.append(write(String(localized: "목표"), header: ["id", "내용", "시작일", "종료일", "상태", "달성기준%"], 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("다짐", header: ["id", "목표", "대상", "주기", "목표량", "방향"], + urls.append(write(String(localized: "다짐"), header: ["id", "목표", "대상", "주기", "목표량", "방향"], rows: goals.flatMap(\.sortedQuests).map { quest in [quest.uuid.uuidString, quest.goal?.title ?? "", quest.targetName, quest.scheduleLabel, @@ -162,7 +163,7 @@ struct DataExportView: View { // 엑셀이 UTF-8 한글을 바로 인식하도록 BOM을 붙인다 let content = "\u{FEFF}" + lines.joined(separator: "\n") let url = FileManager.default.temporaryDirectory - .appendingPathComponent("하루다님-\(name).csv") + .appendingPathComponent(String(localized: "하루다님-\(name).csv")) do { try content.write(to: url, atomically: true, encoding: .utf8) return url diff --git a/myApp/HaruDanim/IOS/Views/DiaryExportSheet.swift b/myApp/HaruDanim/IOS/Views/DiaryExportSheet.swift index 0241b81..c98d314 100644 --- a/myApp/HaruDanim/IOS/Views/DiaryExportSheet.swift +++ b/myApp/HaruDanim/IOS/Views/DiaryExportSheet.swift @@ -220,9 +220,10 @@ struct DiaryExportSheet: View { private var fileSlug: String { let entries = targetEntries - guard let first = entries.first else { return "일기" } - if entries.count == 1 { return "일기-\(slugDate(first.dayKey))" } - return "일기-\(slugDate(first.dayKey))~\(slugDate(entries.last!.dayKey))" + // 파일명은 언어별 현지화 (ExportSnapshot.fileName과 같은 이유) + guard let first = entries.first else { return String(localized: "일기") } + if entries.count == 1 { return String(localized: "일기-\(slugDate(first.dayKey))") } + return String(localized: "일기-\(slugDate(first.dayKey))~\(slugDate(entries.last!.dayKey))") } /// 렌더 대상 날짜(논리적 하루)에 겹치는 기록만 그때그때 조회 — 전량 @Query 로드 방지 @@ -306,7 +307,7 @@ struct DiaryExportSheet: View { @MainActor private func renderPDF(_ entries: [DiaryEntry]) -> URL? { let url = FileManager.default.temporaryDirectory - .appendingPathComponent("하루다님-\(fileSlug).pdf") + .appendingPathComponent(String(localized: "하루다님-\(fileSlug).pdf")) let bounds = CGRect(origin: .zero, size: DiaryPageMetrics.size) let renderer = UIGraphicsPDFRenderer(bounds: bounds) do { @@ -353,7 +354,7 @@ struct DiaryExportSheet: View { } guard let data = stitched.pngData() else { return nil } let url = FileManager.default.temporaryDirectory - .appendingPathComponent("하루다님-일기-\(slugDate(entry.dayKey)).png") + .appendingPathComponent(String(localized: "하루다님-일기-\(slugDate(entry.dayKey)).png")) try? data.write(to: url, options: .atomic) return url } diff --git a/myApp/HaruDanim/IOS/Views/ExportImageView.swift b/myApp/HaruDanim/IOS/Views/ExportImageView.swift index 4e2984a..07641c4 100644 --- a/myApp/HaruDanim/IOS/Views/ExportImageView.swift +++ b/myApp/HaruDanim/IOS/Views/ExportImageView.swift @@ -32,7 +32,9 @@ struct ExportSnapshot { /// 파일명용 (예: "기록-2026-07-13") var fileSlug: String - var fileName: String { "하루다님-\(fileSlug).png" } + // 파일명은 언어별로 현지화 — 수신 환경(PC·클라우드)이 한글을 지원하지 않을 수 있어 + // en/ja는 라틴 접두 "HaruDanim-"과 그 언어의 종류 단어를 쓴다 (fileSlug도 현지화됨) + var fileName: String { String(localized: "하루다님-\(fileSlug).png") } /// 하루짜리 타임테이블 섹션에 캘린더 일정 블록을 끼워 넣는다 (일기 화면·내보내기 공용) func injectingCalendarEvents(_ events: [ExportTimetableData.Block]) -> ExportSnapshot { @@ -272,7 +274,7 @@ enum ExportBuilder { filterNote: note, hero: recordHero(segments: segments, counts: counts), sections: sections, - fileSlug: "기록-주간-\(slugDate(keys.first ?? dayKey))" + fileSlug: String(localized: "기록-주간-\(slugDate(keys.first ?? dayKey))") ) } @@ -326,7 +328,7 @@ enum ExportBuilder { filterNote: note, hero: recordHero(segments: segments, counts: counts), sections: sections, - fileSlug: "기록-\(slugDate(dayKey))" + fileSlug: String(localized: "기록-\(slugDate(dayKey))") ) } @@ -761,7 +763,7 @@ enum ExportBuilder { filterNote: note, hero: hero, sections: sections, - fileSlug: "통계-\(span.rawValue)-\(slugDate(anchorDayKey))" + fileSlug: String(localized: "통계-\(span.rawValue)-\(slugDate(anchorDayKey))") ) } }