fix(diary): stop pencil strokes vanishing when zooming the note page

아이패드 일기 노트에서 필기 후 핀치줌하면 획이 사라지고, 다시 그리는
동안엔 잠깐 보였다가 획을 떼면 또 사라지던 버그 수정.

원인: 줌 배율이 바뀔 때마다 SharpPencilCanvasView가 PencilKit 내부의
모든 서브레이어에 contentsScale 변경 + setNeedsDisplay()를 강제했는데,
PencilKit의 획 타일 레이어는 렌더러가 contents를 직접 채우는 방식이라
setNeedsDisplay()가 기존 픽셀만 지우고 다시 그려 주지 않음. 그리는
동안엔 라이브 렌더로 보이다가 canvasViewDrawingDidChange →
applySharpness에서 다시 지워짐.

수정: 배율 적용을 뷰 수준 contentScaleFactor 재귀로 한정하고 레이어
직접 조작·setNeedsDisplay 제거 — PencilKit이 자기 타이밍에 새 배율로
재렌더링해 획이 유지되면서 선명도(5916e7의 목적)도 유지된다.

검증(iPad 시뮬레이터): 시드에 필기 획 2줄 추가 + -diaryZoomScale
런치 인자(프로그램 줌) 신설 → 수정 전 2배 줌에서 획 소실 재현,
수정 후 동일 조건에서 획 유지·선명 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
songyc macbook 2026-07-14 03:12:42 +09:00
parent 973bb50cba
commit 430ec89341
4 changed files with 7257 additions and 7226 deletions

View File

@ -191,7 +191,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}"
- **요약 페이지 섹션** (표시·순서는 "첫 화면 구성" 시트, 전 날짜 공통, standard defaults): 오늘 기분(대형 정사각 타일: 이모지/사진) · 요약 지표 · 이 날의 목표(그날 기준 다짐 현황, 주간/월간은 그날까지 누적) · 오늘 할 일 · 타임테이블 · 기록 상세 · 통계 막대. 화면 720pt 이상이면 타임테이블 좌측 고정 2컬럼
- 타임테이블 카드 우상단 버튼: **필터**(그날 기록된 행동만 나열된 팝업에서 표시 선택, 날짜별 저장 `hiddenActionIDs`, 숨긴 개수 노란 배지 — 타임테이블에만 적용, 요약 수치·기록 목록은 전체 유지) + **캘린더**(그날의 EKEvent 선택, 날짜별 저장, 외곽선 스타일 블록). 둘 다 내보내기에 동일 반영
- **프리미엄 만료 열람 모드**: 이미 쓴 일기(hasContent)가 있으면 잠금 화면 대신 달력을 열람 전용으로 연다 — 배너 + 일기 있는 날짜만 활성, `environment(\.diaryReadOnly)`로 전 화면의 편집 UI(기분/할일/필기/필터/캘린더/⋯메뉴) 비활성, 열람 진입은 엔트리를 생성하지 않음. 일기를 쓴 적 없으면 기존 잠금 안내
- 노트 페이지: 768×1024 논리 좌표 고정 + scaleEffect. 배치 요소: 사진·도형(사각/원/화살표/선)·**텍스트 상자**(키보드 입력, 배치 모드에서 '글 수정'으로 내용·색 편집). ⋯ 메뉴 '페이지 순서 변경'으로 노트 페이지 드래그 재배열(요약은 항상 첫 장). **실기기 필기는 펜슬 전용, 손가락은 이동/줌만**(시뮬레이터는 anyInput). 핀치줌 최대 4배, `SharpPencilCanvasView`contentsScale을 줌에 맞춰 유지(획 선명도). 줄 노트(간격 4단계)·사진·도형(사각/원/화살표/선) 배치 모드(드래그·핀치 크기 조절, 저장은 비율 좌표)
- 노트 페이지: 768×1024 논리 좌표 고정 + scaleEffect. 배치 요소: 사진·도형(사각/원/화살표/선)·**텍스트 상자**(키보드 입력, 배치 모드에서 '글 수정'으로 내용·색 편집). ⋯ 메뉴 '페이지 순서 변경'으로 노트 페이지 드래그 재배열(요약은 항상 첫 장). **실기기 필기는 펜슬 전용, 손가락은 이동/줌만**(시뮬레이터는 anyInput). 핀치줌 최대 4배, `SharpPencilCanvasView`줌에 맞춰 래스터 배율 유지(획 선명도). ⚠️ 배율 적용은 **뷰의 contentScaleFactor만** — 서브레이어 contentsScale+setNeedsDisplay를 직접 건드리면 PencilKit 획 타일 픽셀이 지워진 채 재렌더되지 않아 확대 시 필기가 사라진다(수정된 버그). 줄 노트(간격 4단계)·사진·도형(사각/원/화살표/선) 배치 모드(드래그·핀치 크기 조절, 저장은 비율 좌표)
### 6.8 설정 탭 (`SettingsView`)
- 테마(라이트/다크 — 즉시 적용, App Group 저장으로 위젯 공유) / 언어(재시작 적용, AppleLanguages 오버라이드)
@ -300,7 +300,7 @@ xcrun xcstringstool sync IOS/Localizable.xcstrings --stringsdata "${files[@]}"
| 기록/통계 | `-historyMode timetable` `-historyWeekly` `-excludeActions "이름,이름"` `-historyShowFilter` `-statShowFilter` `-statSpan <day|week|month>` `-statScrollBottom` `-showExport` `-exportDump`(Documents/export-dump.png) |
| 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-premiumPreview` `-helpPreview` `-dataExportPreview`(+`-dataExportRun`이면 CSV 자동 생성 후 Documents 복사) `-widgetPreview YES|lock` `-widgetPreviewScroll <앵커>` `cloudSync`(standard bool로 강제) `-intentSmokeTest`(시리 인텐트 10개 시나리오 실행 → Documents/intent-smoke-test.txt, -seedDemo·-premium과 함께) |
| 버블 | `-radialExpanded` `-radialDoubleTapTest`(2초 뒤 FAB 더블탭 재현 — `-settings.radialDoubleTapTab <탭>`과 함께) |
| 일기 | `-diarySeed` `-diaryOpenToday`(**`-startTab diary` 필수**) `-diaryPage <N>` `-diaryShowConfig` `-diaryShowExport`(달력 화면 onAppear — `-diaryOpenToday`와 함께 쓰면 안 뜸) `-diaryExportRun pdf|image`(결과를 Documents로 복사) `-diaryShowCalendarPicker` `-diarySeedEvents` `-diaryHideFirstAction` `-diaryShowActionFilter` / 섹션 강제: `-diary.sectionOrder "timetable,hero,goals"` `-diary.hiddenSections "mood,todos,records,bars"` |
| 일기 | `-diarySeed`(필기 획 2줄 포함 — 달력 화면에서 먼저 시드 후 재실행으로 열 것) `-diaryOpenToday`(**`-startTab diary` 필수**) `-diaryPage <N>` `-diaryZoomScale <배>`(2.5초 뒤 fit×N 프로그램 줌 — 획 유지·선명도 확인) `-diaryShowConfig` `-diaryShowExport`(달력 화면 onAppear — `-diaryOpenToday`와 함께 쓰면 안 뜸) `-diaryExportRun pdf|image`(결과를 Documents로 복사) `-diaryShowCalendarPicker` `-diarySeedEvents` `-diaryHideFirstAction` `-diaryShowActionFilter` / 섹션 강제: `-diary.sectionOrder "timetable,hero,goals"` `-diary.hiddenSections "mood,todos,records,bars"` |
| 워치 | `-complicationPreview` `-complicationScroll` `-autoRunFirstAction` |
---

File diff suppressed because it is too large Load Diff

View File

@ -312,6 +312,20 @@ private struct DiaryZoomContainer<Content: View>: UIViewRepresentable {
scroll.contentSize = pageSize
scroll.zoomedView = hosting.view
context.coordinator.hosting = hosting
#if DEBUG
// : -diaryZoomScale 2 2.5 fit N
// ( · )
let zoomFactor = UserDefaults.standard.double(forKey: "diaryZoomScale")
if zoomFactor > 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { [weak scroll] in
guard let scroll else { return }
scroll.setZoomScale(scroll.minimumZoomScale * zoomFactor, animated: false)
scroll.delegate?.scrollViewDidEndZooming?(
scroll, with: scroll.zoomedView, atScale: scroll.zoomScale
)
}
}
#endif
return scroll
}
@ -599,25 +613,20 @@ final class SharpPencilCanvasView: PKCanvasView {
Self.apply(targetContentsScale, view: self)
}
/// contentScaleFactor '' contentsScale
/// setNeedsDisplay() , PencilKit contents
///
/// (
/// ). PencilKit
/// .
private static func apply(_ scale: CGFloat, view: UIView) {
if abs(view.contentScaleFactor - scale) > 0.01 {
view.contentScaleFactor = scale
}
apply(scale, layer: view.layer)
for subview in view.subviews {
apply(scale, view: subview)
}
}
private static func apply(_ scale: CGFloat, layer: CALayer) {
if abs(layer.contentsScale - scale) > 0.01 {
layer.contentsScale = scale
layer.setNeedsDisplay()
}
for sublayer in layer.sublayers ?? [] {
apply(scale, layer: sublayer)
}
}
}
struct DiaryPencilCanvas: UIViewRepresentable {

View File

@ -15,6 +15,9 @@
import SwiftUI
import SwiftData
import PhotosUI
#if DEBUG
import PencilKit // ( )
#endif
// MARK: - ( )
@ -259,6 +262,25 @@ struct DiaryRootView: View {
textBox.centerX = 0.42; textBox.centerY = 0.62; textBox.widthRatio = 0.5
textBox.page = page
context.insert(textBox)
// · ( 2)
var strokes: [PKStroke] = []
for row in 0..<2 {
let baseY = 780 + CGFloat(row) * 90
let points = (0...60).map { i -> PKStrokePoint in
let t = CGFloat(i) / 60
return PKStrokePoint(
location: CGPoint(x: 120 + t * 520, y: baseY + sin(t * .pi * 5) * 32),
timeOffset: TimeInterval(i) * 0.01,
size: CGSize(width: 5, height: 5),
opacity: 1, force: 1, azimuth: 0, altitude: .pi / 2
)
}
strokes.append(PKStroke(
ink: PKInk(.pen, color: row == 0 ? .black : .systemBlue),
path: PKStrokePath(controlPoints: points, creationDate: .now)
))
}
page.drawingData = PKDrawing(strokes: strokes).dataRepresentation()
try? context.save()
}
#endif