mycode/myApp/HaruDanim/Shared/DiaryModels.swift
songyc macbook bdb97e3c9f feat(diary): imported page templates — PDF/photo backgrounds with per-page picker
일기 노트 페이지 양식(속지) 기능 (합의된 설계 그대로):

- 모델: DiaryTemplate 신설(원본 externalStorage — 여러 쪽 PDF도
  통째 1개만 저장 + pageCount) + DiaryPage.templateID/
  templatePageIndex(기본값 있는 새 필드, uuid 문자열 참조 —
  CloudKit 규칙·동기화 순서에 안전). 스키마에 등록.
- 양식 관리: 달력 툴바에서 진입 — PDF(파일, ≤20MB)·사진 가져오기,
  이름 변경·삭제(사용 중 페이지 수 경고 → 빈 캔버스 폴백, 필기 보존),
  첫 쪽 썸네일 저장. 쪽 상한 24쪽(초과분 안내).
- 페이지 추가: 양식이 있으면 [빈 캔버스/줄 노트/내 양식] 선택 시트,
  여러 쪽 PDF는 2단계 쪽 썸네일 선택(지연 생성). 양식이 없으면
  기존처럼 즉시 추가 — 흔한 경로의 반응성 유지.
- 렌더: 페이지 논리 좌표(768×1024)에 aspect-fit — 필기 좌표계 불변.
  PDF는 벡터 원본을 보존하고 줌 종료 시 현재 배율로 백그라운드
  재래스터(펜슬 캔버스 선명도 훅에 함께 연결 — 확대해도 흐림 없음).
  이미지는 가져올 때 긴 변 3072px 리샘플(필기 래스터 상한 3×와 동일).
  양식 페이지는 줄 노트 토글 숨김(상호 배타), 양식만 깔린 페이지도
  작성함 취급(hasContent). PencilKit 캔버스 코드는 건드리지 않음.
- 내보내기(PDF/PNG)에도 양식 배경 동일 반영.

검증(iPad 시뮬레이터, -diarySeedTemplate 등 신규 DEBUG 인자):
양식 관리·페이지 선택 시트·양식 페이지 렌더(2× 프로그램 줌에서
점 노트 선명) 스크린샷 + 이미지 내보내기 산출물에 양식 포함 확인.
Debug·Store 빌드 성공, 신규 문자열 22종 ko/en/ja(missing 0).
도움말 '나만의 양식(속지)' 항목 추가, CLAUDE.md §3·§6.7·§14 갱신.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
2026-07-14 19:10:50 +09:00

209 lines
7.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// DiaryModels.swift
// Haru_Danim
//
// () iPad .
// ( ) DiaryEntry :
// - (): ( ) + (+ / )
// - : PencilKit ( ) + /
// CloudKit ( , to-many optional storage + computed ) .
//
import Foundation
import SwiftData
@Model
final class DiaryEntry {
var uuid: UUID = UUID()
/// (DayMath.dayKey )
var dayKey: Date = Date()
/// ( , = )
var moodEmoji: String = ""
/// ( , nil = )
@Attribute(.externalStorage)
var moodImageData: Data? = nil
/// ID (EKEvent.eventIdentifier).
/// .
var calendarEventIDs: [String] = []
/// uuid ( ).
/// , .
var hiddenActionIDs: [String] = []
/// ' ' uuid ( ).
/// CloudKit (§3) .
var hiddenGoalIDs: [String] = []
var createdAt: Date = Date()
var updatedAt: Date = Date()
@Relationship(deleteRule: .cascade, inverse: \DiaryTodo.entry)
var todosStorage: [DiaryTodo]? = []
@Relationship(deleteRule: .cascade, inverse: \DiaryPage.entry)
var pagesStorage: [DiaryPage]? = []
init(dayKey: Date) {
self.dayKey = dayKey
}
var todos: [DiaryTodo] {
get { (todosStorage ?? []).sorted { $0.sortOrder < $1.sortOrder } }
set { todosStorage = newValue }
}
var pages: [DiaryPage] {
get { (pagesStorage ?? []).sorted { $0.index < $1.index } }
set { pagesStorage = newValue }
}
/// : ""
var hasContent: Bool {
!moodEmoji.isEmpty || moodImageData != nil
|| !(todosStorage ?? []).isEmpty
|| (pagesStorage ?? []).contains { $0.hasContent }
}
}
/// ( )
@Model
final class DiaryTodo {
var uuid: UUID = UUID()
var text: String = ""
var isDone: Bool = false
var sortOrder: Int = 0
var entry: DiaryEntry?
init(text: String = "", sortOrder: Int = 0) {
self.text = text
self.sortOrder = sortOrder
}
}
///
@Model
final class DiaryPage {
var uuid: UUID = UUID()
/// (0 = )
var index: Int = 0
/// true = , false =
var lined: Bool = false
/// (pt, 768×1024 )
var lineSpacing: Double = 44
/// (DiaryTemplate.uuid , = ).
/// uuid (/) .
var templateID: String = ""
/// PDF (0)
var templatePageIndex: Int = 0
/// PKDrawing.dataRepresentation()
@Attribute(.externalStorage)
var drawingData: Data = Data()
var entry: DiaryEntry?
@Relationship(deleteRule: .cascade, inverse: \DiaryPageItem.page)
var itemsStorage: [DiaryPageItem]? = []
init(index: Int) {
self.index = index
}
var items: [DiaryPageItem] {
get { (itemsStorage ?? []).sorted { $0.createdAt < $1.createdAt } }
set { itemsStorage = newValue }
}
var hasContent: Bool {
// ( )
!drawingData.isEmpty || !(itemsStorage ?? []).isEmpty || !templateID.isEmpty
}
}
// MARK: - ( )
/// .
/// PDF ** 1** (pageCount ),
/// · . 1 .
@Model
final class DiaryTemplate {
var uuid: UUID = UUID()
var name: String = ""
/// DiaryTemplateKind rawValue
var kindRaw: String = DiaryTemplateKind.pdf.rawValue
/// (PDF )
@Attribute(.externalStorage)
var data: Data = Data()
/// ( )
@Attribute(.externalStorage)
var thumbnailData: Data? = nil
/// ( = 1, PDF = )
var pageCount: Int = 1
var createdAt: Date = Date()
init(name: String, kind: DiaryTemplateKind) {
self.name = name
self.kindRaw = kind.rawValue
}
var kind: DiaryTemplateKind {
get { DiaryTemplateKind(rawValue: kindRaw) ?? .pdf }
set { kindRaw = newValue.rawValue }
}
}
nonisolated enum DiaryTemplateKind: String, CaseIterable {
case pdf
case image
}
/// (, )
@Model
final class DiaryPageItem {
var uuid: UUID = UUID()
/// DiaryItemKind rawValue
var kindRaw: String = DiaryItemKind.photo.rawValue
///
@Attribute(.externalStorage)
var imageData: Data? = nil
/// ( )
var text: String = ""
/// · (hex)
var colorHex: String = "#2F6B4F"
/// (0...1)
var centerX: Double = 0.5
var centerY: Double = 0.3
///
var widthRatio: Double = 0.35
var rotationDegrees: Double = 0
var createdAt: Date = Date()
var page: DiaryPage?
init(kind: DiaryItemKind) {
self.kindRaw = kind.rawValue
}
var kind: DiaryItemKind {
get { DiaryItemKind(rawValue: kindRaw) ?? .photo }
set { kindRaw = newValue.rawValue }
}
}
nonisolated enum DiaryItemKind: String, CaseIterable {
case photo
case rectangle
case ellipse
case arrow
case line
case text
/// (widthRatio )
var defaultAspect: Double {
switch self {
case .photo: return 1
case .rectangle: return 0.7
case .ellipse: return 0.7
case .arrow: return 0.35
case .line: return 0.1
case .text: return 0.4
}
}
}