- 달력(작성일 마커: 기분 이모지/연필 점) → 날짜별 일기, 이번 달 작성 수 표시 - 첫 페이지: 날짜·오늘 기분(이모지/사진, 끄기 옵션)·할 일 체크리스트(스크리블 입력)· 타임테이블·통계(내보내기 스냅숏 재사용으로 화면·내보내기 일치) - 노트 페이지: PencilKit 캔버스(도구 팔레트) + 줄 노트 배경 + 사진/도형(사각형·원·화살표·선) 배치 모드(드래그·핀치), 페이지 무한 추가 — 논리 크기 768×1024 고정으로 WYSIWYG 내보내기 - 내보내기: 구간/복수 날짜(MultiDatePicker) → PDF 1개 또는 날짜별 세로 스티치 PNG - SwiftData 모델 4종(CloudKit 호환 패턴) 스키마 추가, PremiumFeature.diary, iPhone 탭 컨텍스트(탭바·더보기·라디얼)에서 일기 제외, en/ja 번역 39건 - 검증 인자: -diarySeed/-diaryOpenToday/-diaryPage N/-diaryShowExport/-diaryExportRun pdf|image Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
151 lines
4.4 KiB
Swift
151 lines
4.4 KiB
Swift
//
|
|
// 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
|
|
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
|
|
/// 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
|
|
}
|
|
}
|
|
|
|
/// 노트 페이지 위에 배치하는 요소 (사진 또는 도형)
|
|
@Model
|
|
final class DiaryPageItem {
|
|
var uuid: UUID = UUID()
|
|
/// DiaryItemKind rawValue
|
|
var kindRaw: String = DiaryItemKind.photo.rawValue
|
|
/// 사진일 때만 사용
|
|
@Attribute(.externalStorage)
|
|
var imageData: Data? = nil
|
|
/// 도형 색 (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
|
|
|
|
/// 사진 대비 도형의 기본 세로 비율 (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
|
|
}
|
|
}
|
|
}
|