diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index eb9a883..43695c0 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -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개(사진은 원본 비율 균등). **정사각형·정원 스냅**: 조절 중 |w−h|≤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개(사진은 원본 비율 균등). **정사각형·정원 스냅**: 조절 중 |w−h|≤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 오버라이드) diff --git a/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift b/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift index a2a5d8c..f4195cd 100644 --- a/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift +++ b/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift @@ -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)