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
This commit is contained in:
parent
7475a963bd
commit
5aae30983d
File diff suppressed because one or more lines are too long
@ -1796,6 +1796,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"가운데" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Center"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "中央"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"가장 나중에 시작한 행동" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2300,6 +2316,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"굵게" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bold"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "太字"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"그래프로 보여줄 행동들과 기간, 테마를 선택하세요. 크기에 따라 소형 2개·중형 4개·대형 6개까지 그려져요." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2380,6 +2412,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"글자 크기" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Font Size"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "文字サイズ"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"금" : {
|
||||
"comment" : "요일 축약(금요일)",
|
||||
"localizations" : {
|
||||
@ -4685,6 +4733,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"도형 색" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Shape Color"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "図形の色"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"도형 수정" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Edit Shape"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "図形を編集"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"독서" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -5205,6 +5285,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"모서리 둥글기" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Corner Radius"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "角の丸み"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"모음" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -6830,6 +6926,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"선 굵기" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Line Width"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "線の太さ"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"선택" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -8496,6 +8608,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"오른쪽" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Right"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "右"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"오른쪽 위 '배치 편집'을 누르면 버튼이 흔들리며 드래그로 순서를 바꿀 수 있어요. 한 줄에 몇 개를 놓을지도 고를 수 있어요. '완료'를 누르면 저장되고, 이 순서는 위젯의 기본 표시 순서에도 쓰여요." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -8688,6 +8816,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"왼쪽" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Left"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "左"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"요소 %lld개" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -10417,6 +10561,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"정렬" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Alignment"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "配置"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"정리하고 돌아보기" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -11265,6 +11425,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"채우기" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Fill"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "塗りつぶし"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"첫 페이지 타임테이블 카드 오른쪽 위 캘린더 버튼으로 그날의 캘린더 일정을 골라 함께 표시할 수 있어요(일정은 점선 테두리로 구분돼요). 필터 버튼을 누르면 그날 기록된 행동 중 타임테이블에 보여줄 것만 고를 수 있고, '이 날의 목표' 카드에도 필터 버튼이 있어 보고 싶은 목표만 골라 둘 수 있어요. 선택은 모두 날짜마다 따로 저장되고, 타임테이블 선택은 일기를 내보낼 때도 그대로 반영돼요.\n매일 같은 선택을 반복하지 않으려면 달력 화면 오른쪽 위 '기본 표시 설정'을 써 보세요 — 캘린더 일정을 기본으로 넣을지(모든 캘린더/선택한 캘린더만), 어떤 목표·행동을 기본으로 숨길지를 한 번만 정하면 모든 날짜에 적용돼요. 날짜별 필터로 다르게 고른 날짜는 그 선택이 우선하고, 필터의 '기본값으로 되돌리기'로 언제든 기본값으로 돌아갈 수 있어요." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -11783,6 +11959,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"크기 조절" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Resize"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "サイズ変更"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"큰 '목표' 아래에 실천 단위인 '다짐'을 만들어요. 하루·주간·월간 진행률과 연속 달성이 자동으로 계산되고, 목표가 끝나면 달성 여부도 판정돼요." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -11975,6 +12167,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"테두리" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Border"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "枠線"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"테두리 굵기" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Border Width"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "枠線の太さ"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"테마" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -12363,18 +12587,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 쓰고 그리고, '사진'·'도형'·'텍스트'(키보드로 쓰는 글 상자)를 추가한 뒤 '배치 모드'에서 드래그·핀치로 옮기고 크기를 바꿔요. 두 손가락 핀치로 페이지를 확대해 세밀하게 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 순서를 바꿀 수 있어요. 맥에서는 텍스트 입력이 기본이고, 도구 줄의 '그리기'를 켜면 마우스나 트랙패드로도 그릴 수 있어요 — 평소에는 클릭과 스크롤이 이동·버튼에 쓰여요." : {
|
||||
"페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 쓰고 그리고, '사진'·'도형'·'텍스트'(키보드로 쓰는 글 상자)를 추가한 뒤 '배치 모드'에서 꾸며요 — 드래그로 옮기고, 선택하면 나타나는 오른쪽 아래 손잡이나 핀치로 크기를 바꾸고(도형·텍스트는 가로세로를 따로), 두 손가락 돌리기로 돌릴 수 있어요. 도형을 선택하고 '도형 수정'을 누르면 색·선 굵기·채우기·모서리 둥글기·회전을, 텍스트는 '글 수정'에서 내용과 글자 색·크기·굵게·정렬·테두리를 바꿀 수 있어요. 두 손가락 핀치로 페이지를 확대해 세밀하게 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 순서를 바꿀 수 있어요. 맥에서는 텍스트 입력이 기본이고, 도구 줄의 '그리기'를 켜면 마우스나 트랙패드로도 그릴 수 있어요 — 평소에는 클릭과 스크롤이 이동·버튼에 쓰여요." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Swipe to continue onto free-form note pages. Write and draw with Apple Pencil, add Photos, Shapes, and Text (typed text boxes), then use Arrange mode to drag and pinch them into place. Pinch with two fingers to zoom in for detail, and turn on lined paper to pick a line spacing. With multiple pages, use 'Reorder Pages' in the ⋯ menu at the top right. On Mac, typing is the default — turn on 'Draw' in the toolbar to draw with the mouse or trackpad; otherwise clicks and scrolling move the page and press buttons."
|
||||
"value" : "Turn the page and free-form notes continue. Write and draw with Apple Pencil, add 'Photos', 'Shapes', and 'Text' (a text box you type in), then style them in 'Arrange mode' — drag to move, resize with the bottom-right handle that appears when selected or by pinching (shapes and text can be sized freely in width and height), and rotate with a two-finger twist. Select a shape and tap 'Edit Shape' to change its color, line width, fill, corner radius, and rotation; for text, 'Edit Text' lets you change the content plus text color, size, bold, alignment, and border. Pinch with two fingers to zoom in for fine writing, and turn on lined paper to pick the line spacing. With multiple pages, use 'Reorder Pages' in the ⋯ menu at the top right. On Mac, typing is the default — turn on 'Draw' in the toolbar to draw with your mouse or trackpad; otherwise clicks and scrolling are used for moving around and buttons."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ページをめくると自由なノートが続きます。Apple Pencilで書いて描いて、「写真」「図形」「テキスト」(キーボードで書くテキストボックス)を追加し、「配置モード」でドラッグ・ピンチで動かしてサイズを変えられます。2本指ピンチで拡大して細かく書け、罫線ノートをオンにすると行間も選べます。ページが複数あるときは右上の⋯メニューの「ページの順序を変更」で並べ替えられます。 Macではテキスト入力が基本です。ツールバーの「描く」をオンにするとマウスやトラックパッドでも描けます — 普段はクリックとスクロールが移動やボタンに使われます。"
|
||||
"value" : "ページをめくると自由ノートが続きます。Apple Pencilで書いて描いて、「写真」「図形」「テキスト」(キーボードで入力するテキストボックス)を追加したら「配置モード」で仕上げます — ドラッグで移動し、選択すると現れる右下のハンドルやピンチでサイズを変え(図形・テキストは縦横を自由に)、2本指でひねって回転できます。図形を選んで「図形を編集」を押すと色・線の太さ・塗りつぶし・角の丸み・回転を、テキストは「テキストを編集」で内容と文字の色・サイズ・太字・配置・枠線を変えられます。2本指ピンチでページを拡大して細かく書き込め、罫線ノートをオンにすると行間も選べます。ページが複数あるときは右上の⋯メニューの「ページの順序を変更」で並べ替えられます。Macでは文字入力が基本で、ツールバーの「描く」をオンにするとマウスやトラックパッドでも描けます — ふだんはクリックとスクロールが移動やボタン操作に使われます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14220,6 +14444,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"회전" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Rotation"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "回転"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"횟수" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@ -39,6 +39,12 @@ struct DiaryNotePageView: View {
|
||||
@State private var photoSelection: PhotosPickerItem?
|
||||
/// 텍스트 상자 내용 편집 대상
|
||||
@State private var editingTextItem: DiaryPageItem?
|
||||
/// 도형 스타일 편집 대상 (색·굵기·채우기·둥글기·회전)
|
||||
@State private var editingShapeItem: DiaryPageItem?
|
||||
#if DEBUG
|
||||
/// -diaryShapeEdit 소비 여부 (활성 페이지에서 1회만)
|
||||
@State private var debugShapeEditDone = false
|
||||
#endif
|
||||
/// 필기 입력 게이트. 맥(Designed for iPad)은 마우스가 이동·버튼 조작과 겸용이라
|
||||
/// 도구 줄의 '그리기' 토글을 켠 동안만 캔버스가 입력을 받는다 (텍스트 우선 UX, §6.7).
|
||||
/// 아이패드는 항상 true(토글 미노출) — 펜슬 필기 동작이 기존과 완전히 동일하다.
|
||||
@ -84,6 +90,25 @@ struct DiaryNotePageView: View {
|
||||
.sheet(item: $editingTextItem) { item in
|
||||
DiaryTextEditSheet(item: item, onCommit: save)
|
||||
}
|
||||
.sheet(item: $editingShapeItem) { item in
|
||||
DiaryShapeEditSheet(item: item, onCommit: save)
|
||||
}
|
||||
.onAppear { debugOpenShapeEditIfNeeded() }
|
||||
.onChange(of: isActive) { debugOpenShapeEditIfNeeded() }
|
||||
}
|
||||
|
||||
/// 검증·촬영용: -diaryShapeEdit YES → 활성 페이지의 첫 도형을 선택하고 '도형 수정' 시트 표시
|
||||
private func debugOpenShapeEditIfNeeded() {
|
||||
#if DEBUG
|
||||
guard !debugShapeEditDone, isActive,
|
||||
UserDefaults.standard.bool(forKey: "diaryShapeEdit"),
|
||||
let shape = page.items.first(where: { $0.kind != .photo && $0.kind != .text })
|
||||
else { return }
|
||||
debugShapeEditDone = true
|
||||
arranging = true
|
||||
selectedItemID = shape.uuid
|
||||
editingShapeItem = shape
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: 페이지 도구 줄
|
||||
@ -201,6 +226,12 @@ struct DiaryNotePageView: View {
|
||||
} label: {
|
||||
Label("글 수정", systemImage: "square.and.pencil")
|
||||
}
|
||||
} else if item.kind != .photo {
|
||||
Button {
|
||||
editingShapeItem = item
|
||||
} label: {
|
||||
Label("도형 수정", systemImage: "slider.horizontal.3")
|
||||
}
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
deleteItem(item)
|
||||
@ -517,19 +548,33 @@ struct DiaryItemView: View {
|
||||
|
||||
@State private var dragTranslation: CGSize = .zero
|
||||
@State private var pinchScale: CGFloat = 1
|
||||
@State private var rotationDelta: Angle = .zero
|
||||
/// 크기 손잡이 드래그 중의 실시간 변화량 (요소 로컬 좌표 — 끝났을 때만 모델에 반영)
|
||||
@State private var resizeDelta: CGSize = .zero
|
||||
|
||||
private var isSelected: Bool { selectedID == item.uuid }
|
||||
|
||||
private var baseWidth: CGFloat { pageSize.width * item.widthRatio }
|
||||
|
||||
private var baseHeight: CGFloat {
|
||||
if item.kind == .photo,
|
||||
let data = item.imageData, let image = UIImage(data: data), image.size.width > 0 {
|
||||
return baseWidth * image.size.height / image.size.width
|
||||
}
|
||||
let ratio = item.heightRatio > 0 ? item.heightRatio : item.widthRatio * item.kind.defaultAspect
|
||||
return pageSize.width * ratio
|
||||
}
|
||||
|
||||
private var width: CGFloat {
|
||||
pageSize.width * item.widthRatio * pinchScale
|
||||
max(24, baseWidth * pinchScale + resizeDelta.width)
|
||||
}
|
||||
|
||||
private var height: CGFloat {
|
||||
if item.kind == .photo,
|
||||
let data = item.imageData, let image = UIImage(data: data), image.size.width > 0 {
|
||||
return width * image.size.height / image.size.width
|
||||
// 사진은 원본 비율 고정 — 손잡이·핀치 모두 균등 크기 조절
|
||||
if item.kind == .photo, baseWidth > 0 {
|
||||
return baseHeight * (width / baseWidth)
|
||||
}
|
||||
return width * item.kind.defaultAspect
|
||||
return max(24, baseHeight * pinchScale + resizeDelta.height)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -541,14 +586,54 @@ struct DiaryItemView: View {
|
||||
.strokeBorder(AppTheme.green, lineWidth: 2)
|
||||
}
|
||||
}
|
||||
.rotationEffect(.degrees(item.rotationDegrees))
|
||||
// 손잡이는 rotationEffect보다 안쪽에 붙어 요소와 함께 회전 — 드래그 translation이
|
||||
// 요소 로컬 좌표(회전 전 축)로 들어와 회전 상태에서도 폭/높이 계산이 그대로 맞는다
|
||||
.overlay(alignment: .bottomTrailing) {
|
||||
if arranging && isSelected {
|
||||
resizeHandle
|
||||
}
|
||||
}
|
||||
.rotationEffect(.degrees(item.rotationDegrees) + rotationDelta)
|
||||
.position(
|
||||
x: pageSize.width * item.centerX + dragTranslation.width,
|
||||
y: pageSize.height * item.centerY + dragTranslation.height
|
||||
)
|
||||
.allowsHitTesting(arranging)
|
||||
.onTapGesture { selectedID = item.uuid }
|
||||
.gesture(arranging ? moveAndResize : nil)
|
||||
.gesture(arranging ? moveResizeRotate : nil)
|
||||
}
|
||||
|
||||
/// 오른쪽 아래 크기 손잡이 — 도형·텍스트는 가로세로 자유 비율, 사진은 원본 비율 유지.
|
||||
/// 핀치가 어려운 맥(마우스)에서도 크기를 바꿀 수 있는 유일한 수단이기도 하다.
|
||||
private var resizeHandle: some View {
|
||||
ZStack {
|
||||
Circle().fill(AppTheme.green)
|
||||
Image(systemName: "arrow.up.left.and.arrow.down.right")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.frame(width: 26, height: 26)
|
||||
.padding(6)
|
||||
.contentShape(Rectangle())
|
||||
.offset(x: 16, y: 16)
|
||||
.accessibilityLabel(Text("크기 조절"))
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 1)
|
||||
.onChanged { value in
|
||||
selectedID = item.uuid
|
||||
resizeDelta = value.translation
|
||||
}
|
||||
.onEnded { value in
|
||||
let newWidth = max(24, baseWidth + value.translation.width)
|
||||
item.widthRatio = min(max(newWidth / pageSize.width, 0.04), 0.95)
|
||||
if item.kind != .photo {
|
||||
let newHeight = max(24, baseHeight + value.translation.height)
|
||||
item.heightRatio = min(max(newHeight / pageSize.width, 0.02), 1.25)
|
||||
}
|
||||
resizeDelta = .zero
|
||||
onCommit()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@ -566,30 +651,62 @@ struct DiaryItemView: View {
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
}
|
||||
case .rectangle:
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.strokeBorder(Color(hex: item.colorHex), lineWidth: 5)
|
||||
let radius = item.cornerRadius >= 0 ? item.cornerRadius : 10
|
||||
let rect = RoundedRectangle(cornerRadius: radius, style: .continuous)
|
||||
if item.filled {
|
||||
rect.fill(Color(hex: item.colorHex))
|
||||
} else {
|
||||
rect.strokeBorder(Color(hex: item.colorHex), lineWidth: strokeWidth)
|
||||
}
|
||||
case .ellipse:
|
||||
Ellipse()
|
||||
.strokeBorder(Color(hex: item.colorHex), lineWidth: 5)
|
||||
if item.filled {
|
||||
Ellipse().fill(Color(hex: item.colorHex))
|
||||
} else {
|
||||
Ellipse().strokeBorder(Color(hex: item.colorHex), lineWidth: strokeWidth)
|
||||
}
|
||||
case .arrow:
|
||||
DiaryArrowShape()
|
||||
.stroke(Color(hex: item.colorHex),
|
||||
style: StrokeStyle(lineWidth: 6, lineCap: .round, lineJoin: .round))
|
||||
style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round, lineJoin: .round))
|
||||
case .line:
|
||||
DiaryLineShape()
|
||||
.stroke(Color(hex: item.colorHex),
|
||||
style: StrokeStyle(lineWidth: 5, lineCap: .round))
|
||||
style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round))
|
||||
case .text:
|
||||
Text(item.text.isEmpty ? String(localized: "텍스트") : item.text)
|
||||
.font(.system(size: 24, weight: .medium))
|
||||
.foregroundStyle(Color(hex: item.colorHex).opacity(item.text.isEmpty ? 0.4 : 1))
|
||||
.minimumScaleFactor(0.3)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
textContent
|
||||
}
|
||||
}
|
||||
|
||||
/// 드래그(이동) + 핀치(크기) 동시 제스처. 끝났을 때만 모델에 반영한다.
|
||||
private var moveAndResize: some Gesture {
|
||||
/// 선 굵기 — 0(미지정)이면 종류별 기본값 (기존 하드코딩과 동일한 룩)
|
||||
private var strokeWidth: CGFloat {
|
||||
item.lineWidth > 0 ? item.lineWidth : item.kind.defaultLineWidth
|
||||
}
|
||||
|
||||
private var textContent: some View {
|
||||
let size = item.fontSize > 0 ? item.fontSize : 24
|
||||
let (textAlign, frameAlign): (TextAlignment, Alignment) = switch item.textAlignRaw {
|
||||
case "center": (.center, .top)
|
||||
case "trailing": (.trailing, .topTrailing)
|
||||
default: (.leading, .topLeading)
|
||||
}
|
||||
return Text(item.text.isEmpty ? String(localized: "텍스트") : item.text)
|
||||
.font(.system(size: size, weight: item.textBold ? .semibold : .medium))
|
||||
.multilineTextAlignment(textAlign)
|
||||
.foregroundStyle(Color(hex: item.colorHex).opacity(item.text.isEmpty ? 0.4 : 1))
|
||||
.minimumScaleFactor(0.5)
|
||||
.padding(item.textBorder ? 8 : 0)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: frameAlign)
|
||||
.overlay {
|
||||
if item.textBorder {
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.strokeBorder(Color(hex: item.colorHex).opacity(0.85), lineWidth: strokeWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 드래그(이동) + 핀치(균등 크기) + 두 손가락 돌리기(회전) 동시 제스처.
|
||||
/// 끝났을 때만 모델에 반영한다. 정밀 조절은 크기 손잡이·'도형 수정'/'글 수정' 시트로.
|
||||
private var moveResizeRotate: some Gesture {
|
||||
let drag = DragGesture()
|
||||
.onChanged { value in
|
||||
selectedID = item.uuid
|
||||
@ -608,10 +725,130 @@ struct DiaryItemView: View {
|
||||
}
|
||||
.onEnded { value in
|
||||
item.widthRatio = min(max(item.widthRatio * value, 0.08), 0.95)
|
||||
// 자유 비율로 조절해 둔 높이도 같은 배율로 — 핀치는 항상 균등 스케일
|
||||
if item.kind != .photo, item.heightRatio > 0 {
|
||||
item.heightRatio = min(max(item.heightRatio * value, 0.02), 1.25)
|
||||
}
|
||||
pinchScale = 1
|
||||
onCommit()
|
||||
}
|
||||
return drag.simultaneously(with: pinch)
|
||||
let rotate = RotationGesture()
|
||||
.onChanged { value in
|
||||
selectedID = item.uuid
|
||||
rotationDelta = value
|
||||
}
|
||||
.onEnded { value in
|
||||
item.rotationDegrees += value.degrees
|
||||
rotationDelta = .zero
|
||||
onCommit()
|
||||
}
|
||||
return drag.simultaneously(with: pinch).simultaneously(with: rotate)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 편집 시트 공용 행
|
||||
|
||||
/// 값 라벨이 붙은 슬라이더 행 (도형·텍스트 편집 시트 공용).
|
||||
/// 모델에 직접 바인딩되므로 시트 밖 페이지에서 실시간 미리보기가 된다.
|
||||
private struct DiaryValueSliderRow: View {
|
||||
let title: LocalizedStringKey
|
||||
@Binding var value: Double
|
||||
let range: ClosedRange<Double>
|
||||
let step: Double
|
||||
var unit: String = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(title)
|
||||
Spacer()
|
||||
Text(verbatim: "\(Int(value))\(unit)")
|
||||
.font(.callout.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Slider(value: $value, in: range, step: step) {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 회전 슬라이더 행 — 도형·텍스트 공용. 두 손가락 돌리기 제스처의 정밀·마우스 대체 수단
|
||||
private struct DiaryRotationRow: View {
|
||||
@Bindable var item: DiaryPageItem
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text("회전")
|
||||
Spacer()
|
||||
Text(verbatim: "\(Int(item.rotationDegrees))°")
|
||||
.font(.callout.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Slider(value: $item.rotationDegrees, in: -180...180, step: 5) {
|
||||
Text("회전")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 도형 스타일 편집 시트
|
||||
|
||||
private struct DiaryShapeEditSheet: View {
|
||||
@Bindable var item: DiaryPageItem
|
||||
var onCommit: () -> Void
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
private var isClosedShape: Bool { item.kind == .rectangle || item.kind == .ellipse }
|
||||
|
||||
private var lineWidthBinding: Binding<Double> {
|
||||
Binding(get: { item.lineWidth > 0 ? item.lineWidth : item.kind.defaultLineWidth },
|
||||
set: { item.lineWidth = $0 })
|
||||
}
|
||||
|
||||
private var cornerBinding: Binding<Double> {
|
||||
Binding(get: { item.cornerRadius >= 0 ? item.cornerRadius : 10 },
|
||||
set: { item.cornerRadius = $0 })
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
ColorPicker("도형 색", selection: Binding(
|
||||
get: { Color(hex: item.colorHex) },
|
||||
set: { item.colorHex = $0.hexString }
|
||||
), supportsOpacity: false)
|
||||
if isClosedShape {
|
||||
Toggle("채우기", isOn: $item.filled)
|
||||
}
|
||||
if !(isClosedShape && item.filled) {
|
||||
DiaryValueSliderRow(title: "선 굵기", value: lineWidthBinding,
|
||||
range: 1...14, step: 1, unit: "pt")
|
||||
}
|
||||
if item.kind == .rectangle {
|
||||
DiaryValueSliderRow(title: "모서리 둥글기", value: cornerBinding,
|
||||
range: 0...40, step: 2, unit: "pt")
|
||||
}
|
||||
DiaryRotationRow(item: item)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(AppTheme.background)
|
||||
.navigationTitle("도형 수정")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button("완료") {
|
||||
onCommit()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
}
|
||||
}
|
||||
|
||||
@ -624,21 +861,52 @@ private struct DiaryTextEditSheet: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@FocusState private var focused: Bool
|
||||
|
||||
private var fontBinding: Binding<Double> {
|
||||
Binding(get: { item.fontSize > 0 ? item.fontSize : 24 },
|
||||
set: { item.fontSize = $0 })
|
||||
}
|
||||
|
||||
private var borderWidthBinding: Binding<Double> {
|
||||
Binding(get: { item.lineWidth > 0 ? item.lineWidth : item.kind.defaultLineWidth },
|
||||
set: { item.lineWidth = $0 })
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
TextField("내용을 입력하세요", text: $item.text, axis: .vertical)
|
||||
.lineLimit(3...8)
|
||||
.padding(10)
|
||||
.background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.focused($focused)
|
||||
ColorPicker("글자 색", selection: Binding(
|
||||
get: { Color(hex: item.colorHex) },
|
||||
set: { item.colorHex = $0.hexString }
|
||||
), supportsOpacity: false)
|
||||
Spacer()
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
TextField("내용을 입력하세요", text: $item.text, axis: .vertical)
|
||||
.lineLimit(3...8)
|
||||
.padding(10)
|
||||
.background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.focused($focused)
|
||||
ColorPicker("글자 색", selection: Binding(
|
||||
get: { Color(hex: item.colorHex) },
|
||||
set: { item.colorHex = $0.hexString }
|
||||
), supportsOpacity: false)
|
||||
DiaryValueSliderRow(title: "글자 크기", value: fontBinding,
|
||||
range: 12...64, step: 1, unit: "pt")
|
||||
Toggle("굵게", isOn: $item.textBold)
|
||||
HStack {
|
||||
Text("정렬")
|
||||
Spacer()
|
||||
Picker("정렬", selection: $item.textAlignRaw) {
|
||||
Text("왼쪽").tag("")
|
||||
Text("가운데").tag("center")
|
||||
Text("오른쪽").tag("trailing")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(maxWidth: 280)
|
||||
}
|
||||
Toggle("테두리", isOn: $item.textBorder)
|
||||
if item.textBorder {
|
||||
DiaryValueSliderRow(title: "테두리 굵기", value: borderWidthBinding,
|
||||
range: 1...8, step: 1, unit: "pt")
|
||||
}
|
||||
DiaryRotationRow(item: item)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
.background(AppTheme.background)
|
||||
.navigationTitle("텍스트 상자")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@ -651,7 +919,7 @@ private struct DiaryTextEditSheet: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationDetents([.medium, .large])
|
||||
.onAppear { focused = true }
|
||||
}
|
||||
}
|
||||
|
||||
@ -304,19 +304,32 @@ struct DiaryRootView: View {
|
||||
page.lined = true
|
||||
page.entry = entry
|
||||
context.insert(page)
|
||||
// 도형·텍스트 스타일 필드(굵기·둥글기·채움·자유 비율·회전·글자 크기·정렬·테두리)를
|
||||
// 모두 건드려 두면 화면·내보내기 렌더 검증이 스크린샷만으로 된다
|
||||
let rect = DiaryPageItem(kind: .rectangle)
|
||||
rect.centerX = 0.3; rect.centerY = 0.25; rect.widthRatio = 0.35
|
||||
rect.heightRatio = 0.16; rect.lineWidth = 8; rect.cornerRadius = 22
|
||||
rect.rotationDegrees = -6
|
||||
rect.page = page
|
||||
context.insert(rect)
|
||||
let ellipse = DiaryPageItem(kind: .ellipse)
|
||||
ellipse.colorHex = "#2F6B4F"; ellipse.filled = true
|
||||
ellipse.centerX = 0.78; ellipse.centerY = 0.2
|
||||
ellipse.widthRatio = 0.2; ellipse.heightRatio = 0.11
|
||||
ellipse.page = page
|
||||
context.insert(ellipse)
|
||||
let arrow = DiaryPageItem(kind: .arrow)
|
||||
arrow.colorHex = "#D9A621"
|
||||
arrow.centerX = 0.62; arrow.centerY = 0.4; arrow.widthRatio = 0.3
|
||||
arrow.lineWidth = 3; arrow.rotationDegrees = 18
|
||||
arrow.page = page
|
||||
context.insert(arrow)
|
||||
let textBox = DiaryPageItem(kind: .text)
|
||||
textBox.text = String(localized: "오늘은 여기까지!\n내일은 더 일찍 시작하자.")
|
||||
textBox.colorHex = "#3A3A3A"
|
||||
textBox.centerX = 0.42; textBox.centerY = 0.62; textBox.widthRatio = 0.5
|
||||
textBox.heightRatio = 0.16; textBox.fontSize = 30; textBox.textBold = true
|
||||
textBox.textAlignRaw = "center"; textBox.textBorder = true
|
||||
textBox.page = page
|
||||
context.insert(textBox)
|
||||
// 펜슬 획 시드 — 확대 시 획이 유지·선명한지 검증용 (물결 곡선 2줄)
|
||||
|
||||
@ -315,7 +315,7 @@ struct HelpView: View {
|
||||
HelpTopic(
|
||||
symbol: "applepencil",
|
||||
title: "펜슬 노트 페이지",
|
||||
body: "페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 쓰고 그리고, '사진'·'도형'·'텍스트'(키보드로 쓰는 글 상자)를 추가한 뒤 '배치 모드'에서 드래그·핀치로 옮기고 크기를 바꿔요. 두 손가락 핀치로 페이지를 확대해 세밀하게 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 순서를 바꿀 수 있어요. 맥에서는 텍스트 입력이 기본이고, 도구 줄의 '그리기'를 켜면 마우스나 트랙패드로도 그릴 수 있어요 — 평소에는 클릭과 스크롤이 이동·버튼에 쓰여요."
|
||||
body: "페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 쓰고 그리고, '사진'·'도형'·'텍스트'(키보드로 쓰는 글 상자)를 추가한 뒤 '배치 모드'에서 꾸며요 — 드래그로 옮기고, 선택하면 나타나는 오른쪽 아래 손잡이나 핀치로 크기를 바꾸고(도형·텍스트는 가로세로를 따로), 두 손가락 돌리기로 돌릴 수 있어요. 도형을 선택하고 '도형 수정'을 누르면 색·선 굵기·채우기·모서리 둥글기·회전을, 텍스트는 '글 수정'에서 내용과 글자 색·크기·굵게·정렬·테두리를 바꿀 수 있어요. 두 손가락 핀치로 페이지를 확대해 세밀하게 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 순서를 바꿀 수 있어요. 맥에서는 텍스트 입력이 기본이고, 도구 줄의 '그리기'를 켜면 마우스나 트랙패드로도 그릴 수 있어요 — 평소에는 클릭과 스크롤이 이동·버튼에 쓰여요."
|
||||
),
|
||||
HelpTopic(
|
||||
symbol: "doc.text.image",
|
||||
|
||||
@ -171,7 +171,25 @@ final class DiaryPageItem {
|
||||
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?
|
||||
@ -205,4 +223,13 @@ nonisolated enum DiaryItemKind: String, CaseIterable {
|
||||
case .text: return 0.4
|
||||
}
|
||||
}
|
||||
|
||||
/// lineWidth 미지정(0)일 때의 기본 선 굵기 — 기존 하드코딩 값과 동일 (텍스트는 테두리 굵기)
|
||||
var defaultLineWidth: Double {
|
||||
switch self {
|
||||
case .arrow: return 6
|
||||
case .text: return 2
|
||||
default: return 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user