// // 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 } } }