fix(diary): 1.3 검토 후속 — 인라인 편집 정리 단일화·빈 상자 자동 삭제·핀치 상한 정합

1.3 기능 검토(2026-08-03)에서 나온 4건의 후속 수정:

- 인라인 텍스트 편집 종료를 onChange(of: inlineEditingTextID)의 finalize
  단일 지점으로 통합 — 다른 요소를 선택하거나 편집 중 상자를 삭제해도
  편집 상태가 남지 않는다 (기존엔 빈 곳 탭·키보드 내림만 정리됐음)
- 내용이 빈 텍스트 상자는 편집 종료 시 자동 삭제 — '텍스트' 플레이스홀더가
  PDF·이미지 내보내기에 인쇄되는 것 방지. '스타일' 시트로 넘어가는 중이면
  유지(꾸민 뒤 쓰는 흐름 보존)
- 선·화살표 핀치 커밋 상한을 끝점 드래그와 같은 1.6으로 — 길게 늘린 선이
  핀치 순간 0.95로 잘려 짧아지던 경계값 불일치 해소
- 키보드 가림(검토 2번)은 소프트 키보드 실측 결과 줌 컨테이너의 fit
  재계산이 이미 자동 처리(페이지 전체 축소) — 코드 변경 불필요 확인

마케팅 스크린샷(ipad 04-diary-note 등)은 기본 스타일 요소만 담겨 1.3
렌더와 픽셀 동일 — 재촬영 불필요 확인.

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 13:16:17 +09:00
parent e6b721e368
commit b538fe9bba
2 changed files with 33 additions and 3 deletions

View File

@ -235,7 +235,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
- **프리미엄 만료 열람 모드**: 이미 쓴 일기(hasContent)가 있으면 잠금 화면 대신 달력을 열람 전용으로 연다 — 배너 + 일기 있는 날짜만 활성, `environment(\.diaryReadOnly)`로 전 화면의 편집 UI(기분/할일/필기/필터/캘린더/⋯메뉴) 비활성, 열람 진입은 엔트리를 생성하지 않음. 일기를 쓴 적 없으면 기존 잠금 안내
- **양식 페이지 펜 색 고정(2026-08-03, 사용자 요청)**: 양식이 깔린 페이지(`template != nil`)는 캔버스 `overrideUserInterfaceStyle``PKToolPicker.colorUserInterfaceStyle``.light`로 고정 — PencilKit의 다크/라이트 잉크 자동 반전 때문에 다크 모드 아이패드에서 쓴 획이 라이트 모드 맥의 흰 양식 위에서 흰색으로 반전돼 안 보이던 실사용 보고의 해소(동기화 문제로 오인됐던 건). 팔레트 색 해석까지 함께 고정해야 WYSIWYG(트레잇만 고정하면 다크에서 고른 '검정'이 흰 획으로 저장되는 반전이 남음). 고정 이전에 다크에서 쓴 기존 획은 라이트 기준 색 그대로(흰색)라 계속 안 보임 — 데이터 무변경, 사용자 인지 완료. 양식 없는 페이지는 기존 동적 반전 유지(다크 배경 종이에선 그게 맞음). 내보내기의 `PKDrawing.image`도 라이트 트레잇으로 고정(전 페이지 공통 — 호출 시점 트레잇 의존이라 다크에서 내보내면 획이 반전 렌더될 수 있던 잠재 결함 제거, 인쇄 페이지는 원래 라이트 고정이라 화면과 일치)
- **양식(속지)** (`DiaryTemplates.swift`): 달력 툴바 '양식 관리'에서 PDF(≤20MB)·사진을 가져와 라이브러리로 관리. 페이지 추가 시 [빈 캔버스/줄 노트/내 양식] 선택(양식 0개면 시트 없이 기존처럼 즉시 추가), 여러 쪽 PDF는 2단계 쪽 썸네일 선택(최대 24쪽, 원본은 통째 1개 저장 + 쪽 인덱스 참조). 렌더: 페이지 논리 크기에 aspect-fit(좌표계 불변), **PDF는 줌 종료 시 현재 배율로 백그라운드 재래스터**(`DiaryTemplateImageView` — 펜슬 캔버스 선명도 훅에 함께 연결), 이미지는 가져올 때 긴 변 3072px 리샘플(필기 래스터 상한 3×와 동일 → 흐림 없음). 양식 페이지에선 줄 노트 토글 숨김. 양식 삭제 시 사용 중 페이지 수 경고 → 해당 페이지는 빈 캔버스 폴백(필기·요소 보존). 내보내기에도 동일 반영
- 노트 페이지: 768×1024 논리 좌표 고정 + scaleEffect. 배치 요소: 사진·도형(사각/원/화살표/선)·**텍스트 상자**(키보드 입력). **요소 편집(2026-08-03 개편, 2차까지)**: 배치 모드에서 드래그 이동 + 핀치(모양 비율 유지 균등 스케일) + 두 손가락 돌리기 회전. **종류별 모양 손잡이**(선택 시 표시, 드래그 값은 `.named(DiaryPageMetrics.coordSpace)` 페이지 좌표로 받아 실시간 회전에도 안정): 사각형=꼭짓점 4개(끈 모서리만 움직이고 반대편 고정 — 로컬 축 변환 후 중심을 절반씩 이동), 원=상하좌우 4개(타원 만들기), 선·화살표=양 끝점(반대 끝 고정, 길이+방향 동시 — atan2로 rotationDegrees 갱신), 사진·텍스트=우하단 1개(사진은 원본 비율 균등). **정사각형·정원 스냅**: 조절 중 |wh|≤10pt면 딱 맞추고 손잡이·선택 테두리가 노란색+햅틱. **텍스트 상자는 제자리 입력**: 선택된 상자를 한 번 더 탭 → 그 자리에서 TextEditor로 입력·줄바꿈(UITextView 기본 여백 보정으로 표시 Text와 동일 위치), 탭 밖·배치 모드 종료·키보드 내림에 저장. 생성 직후에도 바로 입력 시작(시트 없음). 선택 후 **'도형 수정' 시트**(색·선 굵기·채우기[사각/원]·모서리 둥글기[사각]·회전 슬라이더 — 모델 직접 바인딩이라 실시간 미리보기), 텍스트는 **'스타일' 시트**(글자 색·크기·굵게·정렬[왼쪽/가운데/오른쪽]·테두리[켬/끔+색(borderColorHex, 빈 값=글자 색)+굵기]·회전 — 내용 입력 필드는 제자리 입력으로 대체돼 없음). 스타일 기본값 규칙은 §3 DiaryPageItem — 내보내기는 DiaryItemView를 그대로 재사용해 자동 일치(인라인 편집은 기본값 바인딩이라 비활성). ⋯ 메뉴 '페이지 순서 변경'으로 노트 페이지 드래그 재배열(요약은 항상 첫 장). **실기기 필기는 펜슬 전용, 손가락은 이동/줌만**(시뮬레이터는 anyInput). 핀치줌 최대 4배, `SharpPencilCanvasView`가 줌에 맞춰 래스터 배율 유지(획 선명도). ⚠️ 배율 적용은 **뷰의 contentScaleFactor만** — 서브레이어 contentsScale+setNeedsDisplay를 직접 건드리면 PencilKit 획 타일 픽셀이 지워진 채 재렌더되지 않아 확대 시 필기가 사라진다(수정된 버그). 줄 노트(간격 4단계) 배치 모드 저장은 비율 좌표
- 노트 페이지: 768×1024 논리 좌표 고정 + scaleEffect. 배치 요소: 사진·도형(사각/원/화살표/선)·**텍스트 상자**(키보드 입력). **요소 편집(2026-08-03 개편, 2차까지)**: 배치 모드에서 드래그 이동 + 핀치(모양 비율 유지 균등 스케일) + 두 손가락 돌리기 회전. **종류별 모양 손잡이**(선택 시 표시, 드래그 값은 `.named(DiaryPageMetrics.coordSpace)` 페이지 좌표로 받아 실시간 회전에도 안정): 사각형=꼭짓점 4개(끈 모서리만 움직이고 반대편 고정 — 로컬 축 변환 후 중심을 절반씩 이동), 원=상하좌우 4개(타원 만들기), 선·화살표=양 끝점(반대 끝 고정, 길이+방향 동시 — atan2로 rotationDegrees 갱신), 사진·텍스트=우하단 1개(사진은 원본 비율 균등). **정사각형·정원 스냅**: 조절 중 |wh|≤10pt면 딱 맞추고 손잡이·선택 테두리가 노란색+햅틱. **텍스트 상자는 제자리 입력**: 선택된 상자를 한 번 더 탭 → 그 자리에서 TextEditor로 입력·줄바꿈(UITextView 기본 여백 보정으로 표시 Text와 동일 위치), 탭 밖·배치 모드 종료·키보드 내림·**다른 요소 선택** 어느 경로든 `onChange(of: inlineEditingTextID)`의 finalize 단일 지점 정리·저장. **내용이 빈 상자는 편집 종료 시 자동 삭제**('텍스트' 플레이스홀더가 내보내기에 인쇄되는 것 방지 — 단 '스타일' 시트로 넘어가는 중이면 유지). 생성 직후에도 바로 입력 시작(시트 없음). **키보드 회피는 줌 컨테이너의 fit 재계산이 자동 처리**(키보드가 올라오면 페이지 전체가 축소돼 위에 다 보임 — 소프트 키보드 실측 확인, 별도 코드 불필요). 선·화살표 핀치 커밋 상한은 끝점 드래그와 같은 1.6(다른 종류 0.95 — 긴 선이 핀치에 잘리는 것 방지). 선택 후 **'도형 수정' 시트**(색·선 굵기·채우기[사각/원]·모서리 둥글기[사각]·회전 슬라이더 — 모델 직접 바인딩이라 실시간 미리보기), 텍스트는 **'스타일' 시트**(글자 색·크기·굵게·정렬[왼쪽/가운데/오른쪽]·테두리[켬/끔+색(borderColorHex, 빈 값=글자 색)+굵기]·회전 — 내용 입력 필드는 제자리 입력으로 대체돼 없음). 스타일 기본값 규칙은 §3 DiaryPageItem — 내보내기는 DiaryItemView를 그대로 재사용해 자동 일치(인라인 편집은 기본값 바인딩이라 비활성). ⋯ 메뉴 '페이지 순서 변경'으로 노트 페이지 드래그 재배열(요약은 항상 첫 장). **실기기 필기는 펜슬 전용, 손가락은 이동/줌만**(시뮬레이터는 anyInput). 핀치줌 최대 4배, `SharpPencilCanvasView`가 줌에 맞춰 래스터 배율 유지(획 선명도). ⚠️ 배율 적용은 **뷰의 contentScaleFactor만** — 서브레이어 contentsScale+setNeedsDisplay를 직접 건드리면 PencilKit 획 타일 픽셀이 지워진 채 재렌더되지 않아 확대 시 필기가 사라진다(수정된 버그). 줄 노트(간격 4단계) 배치 모드 저장은 비율 좌표
### 6.8 설정 탭 (`SettingsView`)
- 테마(**시스템 설정 따름(기본)**/라이트/다크 — 즉시 적용, App Group 저장으로 위젯 공유. "system"은 스킴을 강제하지 않아 앱·위젯 모두 시스템 모드를 따름 — 2026-07-23 UX 감사에서 추가, 기본값도 light→system) / 언어(재시작 적용, AppleLanguages 오버라이드)

View File

@ -98,6 +98,17 @@ struct DiaryNotePageView: View {
.sheet(item: $editingShapeItem) { item in
DiaryShapeEditSheet(item: item, onCommit: save)
}
// ( · ·
// · ·) +
.onChange(of: inlineEditingTextID) { oldValue, _ in
if let oldValue { finalizeInlineText(oldValue) }
}
// ( )
.onChange(of: selectedItemID) {
if let editing = inlineEditingTextID, selectedItemID != editing {
inlineEditingTextID = nil
}
}
.onAppear {
debugOpenShapeEditIfNeeded()
debugOpenTextEditIfNeeded()
@ -352,10 +363,23 @@ struct DiaryNotePageView: View {
}
}
/// ( · )
/// · onChange(of: inlineEditingTextID) finalize
/// ( DiaryItemView )
private func endInlineTextEditing() {
guard inlineEditingTextID != nil else { return }
inlineEditingTextID = nil
}
/// : (
/// ). , '' .
private func finalizeInlineText(_ id: UUID) {
if let item = page.items.first(where: { $0.uuid == id }),
item.kind == .text,
item.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
editingTextItem?.uuid != id {
context.delete(item)
if selectedItemID == id { selectedItemID = nil }
}
save()
}
@ -397,6 +421,9 @@ struct DiaryNotePageView: View {
private func deleteItem(_ item: DiaryPageItem) {
context.delete(item)
selectedItemID = nil
// ( uuid
// finalize )
if inlineEditingTextID == item.uuid { inlineEditingTextID = nil }
save()
}
@ -952,7 +979,10 @@ struct DiaryItemView: View {
pinchScale = value
}
.onEnded { value in
item.widthRatio = min(max(item.widthRatio * value, 0.08), 0.95)
// · 1.6
// 0.95
let maxWidthRatio: Double = (item.kind == .arrow || item.kind == .line) ? 1.6 : 0.95
item.widthRatio = min(max(item.widthRatio * value, 0.08), maxWidthRatio)
//
if item.kind != .photo, item.heightRatio > 0 {
item.heightRatio = min(max(item.heightRatio * value, 0.02), 1.25)