diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index ff2f7e1..10ab500 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -18,7 +18,7 @@ | 번들 ID | `com.yechan.HaruDanim` / App Group `group.com.yechan.HaruDanim` / CloudKit `iCloud.com.yechan.HaruDanim` | | 수익 모델 | 부분 유료화. 무료 한도(행동 10·목표 2·목표당 다짐 3) + 프리미엄 결제 시 해제 | | 결제 상품 | `com.yechan.HaruDanim.premium.monthly`(월 4,400원) / `.yearly`(년 44,000원) / `.lifetime`(77,000원) — **ASC 등록·판매 중(실제 가격, 2026-07-28)**. 로컬 테스트 구성(HaruDanim.storekit)도 동일 가격. 가격은 코드에 없고 `product.displayPrice`로 동적 표기, 년 구독 절약률 배지도 동적 계산 | -| 현재 상태 (2026-08-02) | **버전 1.2 App Store 출시 완료(2026-08-02, 사용자 확인) — 유지보수 모드. 1.3 또는 메이저 업데이트 준비 대기 상태**(사용자의 버그 보고·기능 요청을 받아 진행). 1.2 내용: 주기 몫 완료 상태 표기(§4.2)+도움말 '진행률 읽는 법' 갱신(상세는 아래 소기능 이력). **버전 1.1 출시 2026-07-30.** 1.1 내용: ①**맥 일기 개방**(§6.7 — 사이드바 노출 + '그리기' 토글로 마우스 필기, 텍스트 우선 도구줄. 아이패드 경로 불변. 맥 실기 확인 완료) ②**CSV 가져오기**(§6.8 — 프리미엄, 추가 전용·미리 보기·멱등. 검증 `-csvImportTest` 20건 ALL PASS) ③**일기 기본 표시 설정**(§6.7 — 캘린더·목표·행동 전 날짜 기본값 + 날짜별 재정의, 스키마 무변경 마커 방식) ④**'짧은 기록 무시' iCloud KVS 동기화**(§6.8 — 전 기기 공통) ⑤스크린샷 전면 개편(아이폰 10·아이패드 10·워치 합성 1 × ko/en/ja — ASC 업로드 완료) ⑥스토어 메타데이터 변경(앱 이름·부제목·키워드·프로모션 — 검색·색인 강화, `Marketing/AppStore/app-name-subtitle.txt`·`keywords.txt`·`promotional-text.txt`. 스토어 표기만 변경, 설치 이름 불변) ⑦MARKETING_VERSION 1.1. **버전 1.0 출시 2026-07-28.** 이후 작업은 사용자 실사용 중 발견한 버그 수정·사용자가 원하는 소기능 추가만 한다(선제적 기능 제안·대규모 리팩터링 금지). 출시 후 소기능 이력: **양식 페이지 펜 색 고정**(2026-08-03, 사용자 요청 — §6.7. 다크/라이트 잉크 반전으로 맥·아이패드에서 필기가 안 보이던 실사용 보고 해소, 내보내기 라이트 트레잇 고정 포함)·**주기 몫 완료 상태 표기**(2026-08-01, 사용자 요청 — §4.2. 도움말 '진행률 읽는 법' 문구도 갱신. 같은 날 MARKETING_VERSION 1.2(빌드 1)로 상향 + `Marketing/AppStore/whats-new-1.2.txt` 작성·TestFlight 업로드, **심사 통과 → App Store 출시 완료(2026-08-02, 사용자 확인)**). 업데이트 제출: 버전 올려 빌드 업로드 → 심사 제출(IAP는 첫 심사에서 승인됐으므로 첨부 불필요). 출시 전 이력 — RC는 커밋 `f05e611`(버전 1.0 (1)), 이후 실사용 보고 기반 수정 라운드(전부 커밋·푸시·실기기 확인 완료): l10n 누락 일괄·ja 앱 이름 'Haru Danim' 통일·워치 컴플리케이션 추천 캡 제거(§11)·⑥ 위젯 종료 버튼(§10)·진행률 환산 캡+수행일 집계+수행일 아님 표기(§4.2)·파일명/CSV 헤더 현지화(§7)·워치 컴플리케이션 목표 필터(§6.4·§11, 사용자 승인 소기능)·기록 시각 입력 상시 휠 다이얼+분 절사(§6.5)·일기 달력 기분 사진 백레이어(§6.7)·다짐 없는 목표 '다짐 없음' 표기(§4.3)·워치 측정 중 행 탭 종료(§11)·도움말 예시 스크린샷 18장(§6.8)·설정 하루 시작 시간 접힘 행(§6.5)·**모음 탭 즐겨찾기 모아 보기**(§6.1 — 행동 많아질 때 혼잡도 대응, 사용자 승인 소기능. v1 별도 카드 가로 줄[대안 A~F 중 A]이 "따로 노는 느낌" 실사용 피드백으로 v2 그리드 섹션 통합+나머지 접기로 교체, 같은 날 후속 피드백으로 편집 모드 섹션 인지·섹션 간 드래그 지정/해제·"지정 순서대로 뒤에" 규칙 추가, 2026-07-22)·**en 복수형 정리**(2026-07-22 — 수량 1일 때 "1 times/quests" 류 오류를 카탈로그 plural variation으로 14건 전환[iOS 10·위젯 3·워치 1], 모음 탭 섹션 제목 en "Favorite"→"Favorites"로 워치와 통일. ko/ja 무영향). **App Store 등록 자료는 `Marketing/`에 완비**(설명·키워드·구독 메타·스크린샷 원본/합성판, README에 재생성 절차 — 앱 변경이 자료에 영향 주면 요청 없이 함께 갱신할 것. 2026-07-22 즐겨찾기 섹션(v2) 반영해 아이폰·아이패드 01-main과 help-main ko/en/ja 재촬영·합성판 재생성. **2026-07-29 스크린샷 세트 확장** — 아이폰 8장(+위젯·버블 메뉴·iCloud 동기화 합성·다크 모드)/아이패드 7장(+주간 타임테이블·위젯·동기화 합성) × ko/en/ja, 촬영·합성 절차는 Marketing/README '추가 스크린샷' 절. **1.1 제출 때 ASC 업로드 완료**). **사업자 등록·통신판매업 신고 완료(2026-07-22)** — 서류 절차 끝, ASC 등록·유료 계약 진행 가능(순서는 Marketing/README 체크리스트). 사용자는 계속 실사용 테스트 중이며 버그 보고 시: 최소 변경 + 시뮬레이터 재검증 + l10n 루틴(§2.3) + Debug/Store(워치 파일이면 워치까지) 빌드 + 커밋·푸시 | +| 현재 상태 (2026-08-02) | **버전 1.2 App Store 출시 완료(2026-08-02, 사용자 확인) — 유지보수 모드. 1.3 또는 메이저 업데이트 준비 대기 상태**(사용자의 버그 보고·기능 요청을 받아 진행). 1.2 내용: 주기 몫 완료 상태 표기(§4.2)+도움말 '진행률 읽는 법' 갱신(상세는 아래 소기능 이력). **버전 1.1 출시 2026-07-30.** 1.1 내용: ①**맥 일기 개방**(§6.7 — 사이드바 노출 + '그리기' 토글로 마우스 필기, 텍스트 우선 도구줄. 아이패드 경로 불변. 맥 실기 확인 완료) ②**CSV 가져오기**(§6.8 — 프리미엄, 추가 전용·미리 보기·멱등. 검증 `-csvImportTest` 20건 ALL PASS) ③**일기 기본 표시 설정**(§6.7 — 캘린더·목표·행동 전 날짜 기본값 + 날짜별 재정의, 스키마 무변경 마커 방식) ④**'짧은 기록 무시' iCloud KVS 동기화**(§6.8 — 전 기기 공통) ⑤스크린샷 전면 개편(아이폰 10·아이패드 10·워치 합성 1 × ko/en/ja — ASC 업로드 완료) ⑥스토어 메타데이터 변경(앱 이름·부제목·키워드·프로모션 — 검색·색인 강화, `Marketing/AppStore/app-name-subtitle.txt`·`keywords.txt`·`promotional-text.txt`. 스토어 표기만 변경, 설치 이름 불변) ⑦MARKETING_VERSION 1.1. **버전 1.0 출시 2026-07-28.** 이후 작업은 사용자 실사용 중 발견한 버그 수정·사용자가 원하는 소기능 추가만 한다(선제적 기능 제안·대규모 리팩터링 금지). 출시 후 소기능 이력: **일기 도형·텍스트 상자 편집 강화**(2026-08-03, 사용자 요청 — §6.7. "정해진 모양·크기 그대로만 나온다"는 보고: 크기 손잡이(가로세로 자유·맥 마우스 대응)·회전 제스처·'도형 수정' 시트[색·선 굵기·채우기·모서리 둥글기·회전]·텍스트 시트 확장[글자 크기·굵게·정렬·테두리], DiaryPageItem에 기본값 필드 8개 추가)·**양식 페이지 펜 색 고정**(2026-08-03, 사용자 요청 — §6.7. 다크/라이트 잉크 반전으로 맥·아이패드에서 필기가 안 보이던 실사용 보고 해소, 내보내기 라이트 트레잇 고정 포함)·**주기 몫 완료 상태 표기**(2026-08-01, 사용자 요청 — §4.2. 도움말 '진행률 읽는 법' 문구도 갱신. 같은 날 MARKETING_VERSION 1.2(빌드 1)로 상향 + `Marketing/AppStore/whats-new-1.2.txt` 작성·TestFlight 업로드, **심사 통과 → App Store 출시 완료(2026-08-02, 사용자 확인)**). 업데이트 제출: 버전 올려 빌드 업로드 → 심사 제출(IAP는 첫 심사에서 승인됐으므로 첨부 불필요). 출시 전 이력 — RC는 커밋 `f05e611`(버전 1.0 (1)), 이후 실사용 보고 기반 수정 라운드(전부 커밋·푸시·실기기 확인 완료): l10n 누락 일괄·ja 앱 이름 'Haru Danim' 통일·워치 컴플리케이션 추천 캡 제거(§11)·⑥ 위젯 종료 버튼(§10)·진행률 환산 캡+수행일 집계+수행일 아님 표기(§4.2)·파일명/CSV 헤더 현지화(§7)·워치 컴플리케이션 목표 필터(§6.4·§11, 사용자 승인 소기능)·기록 시각 입력 상시 휠 다이얼+분 절사(§6.5)·일기 달력 기분 사진 백레이어(§6.7)·다짐 없는 목표 '다짐 없음' 표기(§4.3)·워치 측정 중 행 탭 종료(§11)·도움말 예시 스크린샷 18장(§6.8)·설정 하루 시작 시간 접힘 행(§6.5)·**모음 탭 즐겨찾기 모아 보기**(§6.1 — 행동 많아질 때 혼잡도 대응, 사용자 승인 소기능. v1 별도 카드 가로 줄[대안 A~F 중 A]이 "따로 노는 느낌" 실사용 피드백으로 v2 그리드 섹션 통합+나머지 접기로 교체, 같은 날 후속 피드백으로 편집 모드 섹션 인지·섹션 간 드래그 지정/해제·"지정 순서대로 뒤에" 규칙 추가, 2026-07-22)·**en 복수형 정리**(2026-07-22 — 수량 1일 때 "1 times/quests" 류 오류를 카탈로그 plural variation으로 14건 전환[iOS 10·위젯 3·워치 1], 모음 탭 섹션 제목 en "Favorite"→"Favorites"로 워치와 통일. ko/ja 무영향). **App Store 등록 자료는 `Marketing/`에 완비**(설명·키워드·구독 메타·스크린샷 원본/합성판, README에 재생성 절차 — 앱 변경이 자료에 영향 주면 요청 없이 함께 갱신할 것. 2026-07-22 즐겨찾기 섹션(v2) 반영해 아이폰·아이패드 01-main과 help-main ko/en/ja 재촬영·합성판 재생성. **2026-07-29 스크린샷 세트 확장** — 아이폰 8장(+위젯·버블 메뉴·iCloud 동기화 합성·다크 모드)/아이패드 7장(+주간 타임테이블·위젯·동기화 합성) × ko/en/ja, 촬영·합성 절차는 Marketing/README '추가 스크린샷' 절. **1.1 제출 때 ASC 업로드 완료**). **사업자 등록·통신판매업 신고 완료(2026-07-22)** — 서류 절차 끝, ASC 등록·유료 계약 진행 가능(순서는 Marketing/README 체크리스트). 사용자는 계속 실사용 테스트 중이며 버그 보고 시: 최소 변경 + 시뮬레이터 재검증 + l10n 루틴(§2.3) + Debug/Store(워치 파일이면 워치까지) 빌드 + 커밋·푸시 | | 출시·운영 메모 (2026-07-28) | **ASC 등록·심사·출시 전부 완료** — 제출 1건에 앱 1.0+구독 그룹+상품 3종 동반(신형 ASC는 각 항목의 "심사에 추가"로 하나의 제출 초안에 담는 방식), '앱 개인정보 보호'="데이터 수집 안 함", 유료 앱 계약·대한민국 세금 양식 활성, 소규모 사업자 프로그램 가입. 개발자 본인 프리미엄은 **평생 이용권 특가 코드(무료 Offer Code) 교환**으로 스토어 버전에 적용됨(IAP용 프로모션 코드는 2026-03 폐지 — 이후 무료 지급은 특가 코드만 가능, 생성 후 교환 가능까지 최대 1시간). 상시 참고: `ITSAppUsesNonExemptEncryption=NO`, PrivacyInfo.xcprivacy 4개 번들(수집·트래킹 없음), 개인정보처리방침=`github.com/notalentprogrammer/HaruDanim` PRIVACY.md·이용약관=Apple 표준 EULA(결제 화면·앱 설명 모두 링크). **iCloud 프로덕션 동기화 확인 완료(2026-07-28)** — 아이폰·아이패드·맥 3기기 반나절 실사용에서 기존 데이터 포함 정상 왕복(개발→프로덕션 CloudKit 환경 전환에 따른 재업로드 우려 해소. 향후 동기화 문제 보고 시 이 전환 이력 참고). 예정: 정산 계좌 변경(시기 무관, 예금주 명의 일치·지급 회차 밀림 고지 확인). 절차 상세 기록은 Marketing/README.md 체크리스트 | --- @@ -111,7 +111,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f | `DiaryTodo` | text, isDone, sortOrder | 요약 페이지 체크리스트 | | `DiaryPage` | index, lined, lineSpacing, drawingData(PKDrawing, externalStorage), **templateID**(DiaryTemplate.uuid 문자열, 빈 값=없음), **templatePageIndex**(PDF 쪽, 0부터) | 768×1024 논리 좌표. 양식만 깔린 페이지도 hasContent=true(작성함 취급·내보내기 포함) | | `DiaryTemplate` | uuid, name, kindRaw(pdf/image), data(externalStorage — PDF는 원본 통째 1개), thumbnailData(첫 쪽), pageCount | 노트 배경 양식. PDF 여러 쪽이어도 복제 없이 원본 1개 + 쪽 인덱스 참조 | -| `DiaryPageItem` | kind(photo/rectangle/ellipse/arrow/line/**text**), imageData, **text**(텍스트 상자 내용), colorHex, centerX/Y·widthRatio(비율 좌표), rotationDegrees | | +| `DiaryPageItem` | kind(photo/rectangle/ellipse/arrow/line/**text**), imageData, **text**(텍스트 상자 내용), colorHex, centerX/Y·widthRatio(비율 좌표), **heightRatio**(페이지 폭 대비 — 0=미지정→kind.defaultAspect, 사진은 항상 원본 비율), rotationDegrees, **lineWidth**(0=kind.defaultLineWidth), **filled**(사각형·원 채움), **cornerRadius**(음수=기본 10), **fontSize**(0=기본 24)·**textBold**·**textAlignRaw**(""=왼쪽/center/trailing)·**textBorder**(텍스트 전용) | 스타일 필드는 전부 기본값(2026-08-03 추가 — 구버전 데이터·CloudKit 안전) | --- @@ -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. 배치 요소: 사진·도형(사각/원/화살표/선)·**텍스트 상자**(키보드 입력, 배치 모드에서 '글 수정'으로 내용·색 편집). ⋯ 메뉴 '페이지 순서 변경'으로 노트 페이지 드래그 재배열(요약은 항상 첫 장). **실기기 필기는 펜슬 전용, 손가락은 이동/줌만**(시뮬레이터는 anyInput). 핀치줌 최대 4배, `SharpPencilCanvasView`가 줌에 맞춰 래스터 배율 유지(획 선명도). ⚠️ 배율 적용은 **뷰의 contentScaleFactor만** — 서브레이어 contentsScale+setNeedsDisplay를 직접 건드리면 PencilKit 획 타일 픽셀이 지워진 채 재렌더되지 않아 확대 시 필기가 사라진다(수정된 버그). 줄 노트(간격 4단계)·사진·도형(사각/원/화살표/선) 배치 모드(드래그·핀치 크기 조절, 저장은 비율 좌표) +- 노트 페이지: 768×1024 논리 좌표 고정 + scaleEffect. 배치 요소: 사진·도형(사각/원/화살표/선)·**텍스트 상자**(키보드 입력). **요소 편집(2026-08-03 강화)**: 배치 모드에서 드래그 이동 + **오른쪽 아래 크기 손잡이**(도형·텍스트는 가로세로 자유[heightRatio], 사진은 원본 비율 균등 — 핀치가 안 되는 맥 마우스의 유일한 크기 수단) + 핀치(균등 스케일) + **두 손가락 돌리기 회전**(rotationDegrees, 손잡이는 rotationEffect 안쪽 overlay라 로컬 좌표 드래그가 회전 상태에서도 정확). 선택 후 **'도형 수정' 시트**(색·선 굵기·채우기[사각/원]·모서리 둥글기[사각]·회전 슬라이더 — 모델 직접 바인딩이라 실시간 미리보기), 텍스트는 **'글 수정' 시트**(내용·색 + 글자 크기·굵게·정렬[왼쪽/가운데/오른쪽]·테두리[+굵기]·회전). 스타일 기본값 규칙은 §3 DiaryPageItem — 내보내기는 DiaryItemView를 그대로 재사용해 자동 일치. ⋯ 메뉴 '페이지 순서 변경'으로 노트 페이지 드래그 재배열(요약은 항상 첫 장). **실기기 필기는 펜슬 전용, 손가락은 이동/줌만**(시뮬레이터는 anyInput). 핀치줌 최대 4배, `SharpPencilCanvasView`가 줌에 맞춰 래스터 배율 유지(획 선명도). ⚠️ 배율 적용은 **뷰의 contentScaleFactor만** — 서브레이어 contentsScale+setNeedsDisplay를 직접 건드리면 PencilKit 획 타일 픽셀이 지워진 채 재렌더되지 않아 확대 시 필기가 사라진다(수정된 버그). 줄 노트(간격 4단계) 배치 모드 저장은 비율 좌표 ### 6.8 설정 탭 (`SettingsView`) - 테마(**시스템 설정 따름(기본)**/라이트/다크 — 즉시 적용, App Group 저장으로 위젯 공유. "system"은 스킴을 강제하지 않아 앱·위젯 모두 시스템 모드를 따름 — 2026-07-23 UX 감사에서 추가, 기본값도 light→system) / 언어(재시작 적용, AppleLanguages 오버라이드) @@ -351,7 +351,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f | 기록/통계 | `-historyMode timetable` `-historyWeekly` `-historyDaysAgo `(N일 전 날짜로 시작 — 지난주 등 과거 주 표시, 마케팅 촬영용) `-excludeActions "이름,이름"` `-historyShowFilter` `-statShowFilter` `-statSpan ` `-statScrollBottom` `-showExport` `-exportDump`(Documents/export-dump.png) `-recordShowEditor `(최신 기록 수정 시트) | | 설정/기타 | `-settingsScrollGoal` `-settingsScrollPremium` `-settingsScrollMeasure`(측정 섹션 — 짧은 기록 무시 푸터 검증) `-settingsShowDayStartWheel`(하루 시작 시간 휠 펼침 시작) `-premiumPreview` `-helpPreview`(+`-helpExpandAll`: 전 주제 펼침, `-helpScrollTo `: 이미지 주제로 스크롤) `-dataExportPreview`(+`-dataExportRun`이면 CSV 자동 생성 후 Documents 복사) `-dataImportPreview`(가져오기 화면 직접 표시 — -premium과 함께) `-csvImportTest`(CSV 가져오기 자가 검증 20건[파서·판별·연결·중복·멱등] → Documents/csv-import-test.txt — 인메모리 컨테이너라 실데이터 무영향) `-actionShowFavoriteOrder`(행동 탭 즐겨찾기 순서 시트 — `-startTab action`과 함께) `-showOnboarding`(온보딩 강제 표시) `-widgetPreview YES|lock|marketing`(marketing = 디버그 라벨 없는 위젯 실렌더 모아 보기 — 마케팅 스크린샷 05-widgets/06-widgets 촬영용, Marketing/README 참고) `-widgetPreviewScroll <앵커>` `-widgetRenderMode accented|vibrant`(틴트/투명 홈 렌더링 분기 검증) `cloudSync`(standard bool로 강제) `-intentSmokeTest`(시리 인텐트 10개 시나리오 실행 → Documents/intent-smoke-test.txt, -seedDemo·-premium과 함께) | | 버블 | `-radialExpanded` `-radialDoubleTapTest`(2초 뒤 FAB 더블탭 재현 — `-settings.radialDoubleTapTab <탭>`과 함께) | -| 일기 | `-diarySeed`(필기 획 2줄 포함 — 달력 화면에서 먼저 시드 후 재실행으로 열 것) `-diaryDrawMode`(맥 분기에서 '그리기' 토글 켠 채 시작) `-diaryOpenToday`(**`-startTab diary` 필수**) `-diaryPage ` `-diaryZoomScale <배>`(2.5초 뒤 fit×N 프로그램 줌 — 획 유지·선명도 확인) `-diaryShowConfig` `-diaryShowExport`(달력 화면 onAppear — `-diaryOpenToday`와 함께 쓰면 안 뜸) `-diaryExportRun pdf|image`(결과를 Documents로 복사) `-diaryShowCalendarPicker` `-diarySeedEvents` `-diaryHideFirstAction` `-diaryShowActionFilter` `-diaryHideFirstGoal` `-diaryShowGoalFilter` `-diaryShowDefaults`(기본 표시 설정 시트) `-diaryDefaultsSeed`(첫 목표·첫 행동 기본 숨김+캘린더 기본 '모든 캘린더' 주입 — 기본값 적용 검증용) `-diarySeedPhoto`(오늘=이모지+사진·어제=사진만 기분 사진 시드 — 달력 백레이어 검증) `-diarySeedTemplate`(2쪽 샘플 PDF 양식 생성) `-diaryLockScreen`(잠금 게이트 화면 강제 — 인증 없이 표시) `-diaryShowTemplates`(양식 관리 시트) `-diaryShowPageChooser`(페이지 추가 선택 시트) `-diaryApplyTemplate`(첫 양식으로 페이지 추가) / 섹션 강제: `-diary.sectionOrder "timetable,hero,goals"` `-diary.hiddenSections "mood,todos,records,bars"` | +| 일기 | `-diarySeed`(필기 획 2줄 포함 — 달력 화면에서 먼저 시드 후 재실행으로 열 것) `-diaryDrawMode`(맥 분기에서 '그리기' 토글 켠 채 시작) `-diaryOpenToday`(**`-startTab diary` 필수**) `-diaryPage ` `-diaryZoomScale <배>`(2.5초 뒤 fit×N 프로그램 줌 — 획 유지·선명도 확인) `-diaryShowConfig` `-diaryShowExport`(달력 화면 onAppear — `-diaryOpenToday`와 함께 쓰면 안 뜸) `-diaryExportRun pdf|image`(결과를 Documents로 복사) `-diaryShowCalendarPicker` `-diarySeedEvents` `-diaryHideFirstAction` `-diaryShowActionFilter` `-diaryHideFirstGoal` `-diaryShowGoalFilter` `-diaryShowDefaults`(기본 표시 설정 시트) `-diaryDefaultsSeed`(첫 목표·첫 행동 기본 숨김+캘린더 기본 '모든 캘린더' 주입 — 기본값 적용 검증용) `-diarySeedPhoto`(오늘=이모지+사진·어제=사진만 기분 사진 시드 — 달력 백레이어 검증) `-diarySeedTemplate`(2쪽 샘플 PDF 양식 생성) `-diaryLockScreen`(잠금 게이트 화면 강제 — 인증 없이 표시) `-diaryShowTemplates`(양식 관리 시트) `-diaryShowPageChooser`(페이지 추가 선택 시트) `-diaryApplyTemplate`(첫 양식으로 페이지 추가) `-diaryShapeEdit`(활성 노트 페이지 첫 도형 선택+'도형 수정' 시트 — `-diarySeed`·`-diaryOpenToday`·`-diaryPage 1`과 함께. 시드 요소들이 스타일 필드[굵기·둥글기·채움·자유 비율·회전·글자 크기·정렬·테두리]를 전부 렌더) / 섹션 강제: `-diary.sectionOrder "timetable,hero,goals"` `-diary.hiddenSections "mood,todos,records,bars"` | | 워치 | `-complicationPreview` `-complicationScroll` `-complicationScrollTo ` `-autoRunFirstAction` `-autoStopRunning`(측정 중 행 탭과 동일 경로로 대표 측정 종료 — 페어링 심 E2E용) | --- diff --git a/myApp/HaruDanim/IOS/Localizable.xcstrings b/myApp/HaruDanim/IOS/Localizable.xcstrings index 29aa000..c62345f 100644 --- a/myApp/HaruDanim/IOS/Localizable.xcstrings +++ b/myApp/HaruDanim/IOS/Localizable.xcstrings @@ -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" : { diff --git a/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift b/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift index 4e477cc..f31059a 100644 --- a/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift +++ b/myApp/HaruDanim/IOS/Views/DiaryNotePage.swift @@ -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 + 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 { + Binding(get: { item.lineWidth > 0 ? item.lineWidth : item.kind.defaultLineWidth }, + set: { item.lineWidth = $0 }) + } + + private var cornerBinding: Binding { + 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 { + Binding(get: { item.fontSize > 0 ? item.fontSize : 24 }, + set: { item.fontSize = $0 }) + } + + private var borderWidthBinding: Binding { + 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 } } } diff --git a/myApp/HaruDanim/IOS/Views/DiaryView.swift b/myApp/HaruDanim/IOS/Views/DiaryView.swift index 19e63a3..d6682de 100644 --- a/myApp/HaruDanim/IOS/Views/DiaryView.swift +++ b/myApp/HaruDanim/IOS/Views/DiaryView.swift @@ -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줄) diff --git a/myApp/HaruDanim/IOS/Views/HelpView.swift b/myApp/HaruDanim/IOS/Views/HelpView.swift index 958c967..8e9a71c 100644 --- a/myApp/HaruDanim/IOS/Views/HelpView.swift +++ b/myApp/HaruDanim/IOS/Views/HelpView.swift @@ -315,7 +315,7 @@ struct HelpView: View { HelpTopic( symbol: "applepencil", title: "펜슬 노트 페이지", - body: "페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 쓰고 그리고, '사진'·'도형'·'텍스트'(키보드로 쓰는 글 상자)를 추가한 뒤 '배치 모드'에서 드래그·핀치로 옮기고 크기를 바꿔요. 두 손가락 핀치로 페이지를 확대해 세밀하게 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 순서를 바꿀 수 있어요. 맥에서는 텍스트 입력이 기본이고, 도구 줄의 '그리기'를 켜면 마우스나 트랙패드로도 그릴 수 있어요 — 평소에는 클릭과 스크롤이 이동·버튼에 쓰여요." + body: "페이지를 넘기면 자유 노트가 이어져요. 애플 펜슬로 쓰고 그리고, '사진'·'도형'·'텍스트'(키보드로 쓰는 글 상자)를 추가한 뒤 '배치 모드'에서 꾸며요 — 드래그로 옮기고, 선택하면 나타나는 오른쪽 아래 손잡이나 핀치로 크기를 바꾸고(도형·텍스트는 가로세로를 따로), 두 손가락 돌리기로 돌릴 수 있어요. 도형을 선택하고 '도형 수정'을 누르면 색·선 굵기·채우기·모서리 둥글기·회전을, 텍스트는 '글 수정'에서 내용과 글자 색·크기·굵게·정렬·테두리를 바꿀 수 있어요. 두 손가락 핀치로 페이지를 확대해 세밀하게 쓸 수 있고, 줄 노트를 켜면 줄 간격도 고를 수 있어요. 페이지가 여러 장이면 오른쪽 위 ⋯ 메뉴의 '페이지 순서 변경'으로 순서를 바꿀 수 있어요. 맥에서는 텍스트 입력이 기본이고, 도구 줄의 '그리기'를 켜면 마우스나 트랙패드로도 그릴 수 있어요 — 평소에는 클릭과 스크롤이 이동·버튼에 쓰여요." ), HelpTopic( symbol: "doc.text.image", diff --git a/myApp/HaruDanim/Shared/DiaryModels.swift b/myApp/HaruDanim/Shared/DiaryModels.swift index 8309a6e..bba47fa 100644 --- a/myApp/HaruDanim/Shared/DiaryModels.swift +++ b/myApp/HaruDanim/Shared/DiaryModels.swift @@ -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 + } + } }