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:
songyc macbook 2026-08-03 11:51:08 +09:00
parent 7475a963bd
commit 5aae30983d
6 changed files with 590 additions and 42 deletions

File diff suppressed because one or more lines are too long

View File

@ -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" : {

View File

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

View File

@ -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)

View File

@ -315,7 +315,7 @@ struct HelpView: View {
HelpTopic(
symbol: "applepencil",
title: "펜슬 노트 페이지",
body: "페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 쓰고 그리고, '사진'·'도형'·'텍스트'(키보드로 쓰는 글 상자)를 추가한 뒤 '배치 모드'에서 드래그·핀치로 옮기고 크기를 바꿔요. 두 손가락 핀치로 페이지를 확대해 세밀하게 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 순서를 바꿀 수 있어요. 맥에서는 텍스트 입력이 기본이고, 도구 줄의 '그리기'를 켜면 마우스나 트랙패드로도 그릴 수 있어요 — 평소에는 클릭과 스크롤이 이동·버튼에 쓰여요."
body: "페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 쓰고 그리고, '사진'·'도형'·'텍스트'(키보드로 쓰는 글 상자)를 추가한 뒤 '배치 모드'에서 꾸며요 — 드래그로 옮기고, 선택하면 나타나는 오른쪽 아래 손잡이나 핀치로 크기를 바꾸고(도형·텍스트는 가로세로를 따로), 두 손가락 돌리기로 돌릴 수 있어요. 도형을 선택하고 '도형 수정'을 누르면 색·선 굵기·채우기·모서리 둥글기·회전을, 텍스트는 '글 수정'에서 내용과 글자 색·크기·굵게·정렬·테두리를 바꿀 수 있어요. 두 손가락 핀치로 페이지를 확대해 세밀하게 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 순서를 바꿀 수 있어요. 맥에서는 텍스트 입력이 기본이고, 도구 줄의 '그리기'를 켜면 마우스나 트랙패드로도 그릴 수 있어요 — 평소에는 클릭과 스크롤이 이동·버튼에 쓰여요."
),
HelpTopic(
symbol: "doc.text.image",

View File

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