mycode/myApp/HaruDanim/Shared/DiaryModels.swift
songyc macbook 5aae30983d feat(diary): 도형·텍스트 상자 편집 강화 — 크기 손잡이·자유 비율·회전·스타일 시트
"정해진 모양·크기 그대로만 나온다"는 실사용 보고: 핀치(균등 스케일)만 있어
가로세로 비율·회전·색·굵기를 바꿀 수 없었고 맥(마우스)은 크기 조절 수단 자체가
없었다. 일반 노트 앱 수준의 기본 편집을 채워 넣는다.

- DiaryPageItem 스타일 필드 8개 추가 (전부 기본값 — CloudKit·구버전 데이터 안전):
  heightRatio(자유 비율)·lineWidth·filled·cornerRadius·fontSize·textBold·
  textAlignRaw·textBorder. 0/음수/빈 값 = 기존 하드코딩과 동일한 룩
- 선택 시 오른쪽 아래 크기 손잡이: 도형·텍스트는 가로세로 따로, 사진은 원본
  비율 균등. rotationEffect 안쪽 overlay라 회전 상태에서도 로컬 좌표로 정확
- 두 손가락 돌리기 회전 제스처 + 시트의 회전 슬라이더(마우스·정밀 대체)
- '도형 수정' 시트: 색·선 굵기·채우기(사각/원)·모서리 둥글기(사각)·회전
- '글 수정' 시트 확장: 글자 크기·굵게·정렬(왼쪽/가운데/오른쪽)·테두리(+굵기)·회전
- 내보내기는 DiaryItemView 재사용이라 자동 일치, 시드가 새 필드 전부 렌더
- 검증 인자 -diaryShapeEdit 추가, 도움말 갱신 + en/ja 번역
- 검증: Debug/Store 빌드, 아이패드 심 ko/en/ja 시트 렌더, 기존 스토어 위
  경량 마이그레이션(덮어 설치) 정상

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNxA172hNDRpDsJ2qztKPf
2026-08-03 11:51:08 +09:00

236 lines
8.5 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
/// "" (widthRatio ).
/// 0 = (defaultAspect) ( · ).
/// .
var heightRatio: Double = 0
var rotationDegrees: Double = 0
/// · (pt). 0 =
var lineWidth: Double = 0
/// · ( )
var filled: Bool = false
/// (pt). = 10
var cornerRadius: Double = -1
/// (pt). 0 = 24
var fontSize: Double = 0
///
var textBold: Bool = false
/// "" = , "center", "trailing"
var textAlignRaw: String = ""
///
var textBorder: Bool = false
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
}
}
/// lineWidth (0) ( )
var defaultLineWidth: Double {
switch self {
case .arrow: return 6
case .text: return 2
default: return 5
}
}
}