diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index e454b41..b765324 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -169,6 +169,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f - **iPhone 기본**: 하단 탭바에 **노출 탭 1~3개**(기본 모음·행동·기록) + "더보기" 탭(나머지 목록). 설정에서 구성 - **iPhone 실험 옵션**: "글래스 버블 메뉴"(`RadialNavigationView`) — 하단 중앙 FAB를 누르면 리퀴드 글라스(`glassEffect(.regular.interactive())`) 유리 구슬 탭 버튼들이 부채꼴로 펼쳐짐. 아이콘+이름이 구슬 안에 함께. 모든 유리 요소는 하나의 `GlassEffectContainer`에서 한 패스로 렌더링(성능)되고 FAB 근처에서 액체처럼 분리·합체 모핑. 2단계 연출: 삽입(FAB에서 모핑) → bloomed(스태거 스프링으로 아치 확산). ⚠️ 컨테이너 안 유리 요소에는 glassEffect 뒤 opacity/scale이 콘텐츠에 온전히 적용되지 않음 — 숨김은 계층 제거(`if`), 이동은 `offset`으로만. 순서는 설정에서 편집(기기 로컬). **두 번 눌러 바로 이동**(설정 토글+탭 선택, `settings.radialDoubleTapTab` 빈 값=꺼짐): 닫힌 FAB를 0.35초 안에 두 번 누르면 지정 탭으로 즉시 이동 — 첫 탭을 지연시키는 더블탭 제스처 대신 첫 탭은 즉시 열고 두 번째 탭에서 분기(반응성 유지, 버블이 피어나다 흡수되는 연출) - **iPad·Mac**: 항상 `NavigationSplitView` 사이드바 (`SidebarRootView`, `DeviceLayout.isPad`로 판정) +- **맥(Designed for iPad) 보정** (`DeviceLayout.isMac` = isiOSAppOnMac): 아이패드 포인트가 맥 화면에 축소 매핑돼 작게 보이는 것 보정 — ①전역 dynamicTypeSize 최소 xLarge(사용자가 더 키우면 존중, ContentView) ②모음 탭 목표 카드 폭 430~640·행동 버튼 폭 200~280으로 상향(§6.1) ③**사이드바에서 일기 탭 숨김**(`AppTab.sidebarCases` — 펜슬 필기 전제 기능이라 진입점만 제거, 데이터는 보존돼 아이패드에서 계속 보임). 아이폰·아이패드는 무영향. 검증은 아이패드 심 + `-forceMacLayout`(§14). 맥 위젯·구매 흐름 실검증과 일기 마우스 입력 허용 여부는 출시 후 과제 - `AppRouter`: 탭 전환 + 탭 간 전달값(모음 탭 롱프레스 → 기록/통계 탭 행동 필터 예약, 더보기 내부 경로 진입 포함) ### 6.1 모음 탭 (`MainView`) @@ -326,7 +327,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f ## 14. DEBUG 런치 인자 (검증용, 모두 DEBUG 빌드 전용) -공통: `-seedDemo YES`(데모 데이터: 태그3·행동7·기록 7일치·목표8·자정 걸친 세션·마감 시각 다짐 1개[아침 루틴 08:00, 기대: 하루 100%·연속 1일 — 시드 기록이 고정 시각이라 오전 7:30 이전에 실행하면 오늘 기록이 미래 시각으로 연속 계산에서 빠져 연속 0일이 정상]. **시드 이름·메모는 String(localized:) — 실행 로케일을 따르므로(마케팅 스크린샷용) 이름으로 찾는 인자(-autoStart·-openStatsFor·-excludeActions·-endGoalYesterday)는 그 로케일의 이름을 넘길 것**, ko 기본 사용은 기존과 동일), `-seedBulk YES`(성능 검증용 과거 500일 약 1만 건 — 벌크 행동 4개), `-seedEmptyGoal YES`(다짐 없는 진행 중 목표 1개 추가 — '다짐 없음' 표기 검증용, 기본 시드와 분리), `-premium YES/NO`(**인자가 있는 콜드 스타트 동안만 유효** — 다음 실행에 인자를 빼면 실권한으로 복귀하므로 프리미엄 화면 검증 시 매 런치에 함께 줄 것. 지속 강제는 설정의 DEBUG 테스트 토글), `-startTab `, `-navStyle radial`, `-themeAutoToggle YES` +공통: `-seedDemo YES`(데모 데이터: 태그3·행동7·기록 7일치·목표8·자정 걸친 세션·마감 시각 다짐 1개[아침 루틴 08:00, 기대: 하루 100%·연속 1일 — 시드 기록이 고정 시각이라 오전 7:30 이전에 실행하면 오늘 기록이 미래 시각으로 연속 계산에서 빠져 연속 0일이 정상]. **시드 이름·메모는 String(localized:) — 실행 로케일을 따르므로(마케팅 스크린샷용) 이름으로 찾는 인자(-autoStart·-openStatsFor·-excludeActions·-endGoalYesterday)는 그 로케일의 이름을 넘길 것**, ko 기본 사용은 기존과 동일), `-seedBulk YES`(성능 검증용 과거 500일 약 1만 건 — 벌크 행동 4개), `-seedEmptyGoal YES`(다짐 없는 진행 중 목표 1개 추가 — '다짐 없음' 표기 검증용, 기본 시드와 분리), `-premium YES/NO`(**인자가 있는 콜드 스타트 동안만 유효** — 다음 실행에 인자를 빼면 실권한으로 복귀하므로 프리미엄 화면 검증 시 매 런치에 함께 줄 것. 지속 강제는 설정의 DEBUG 테스트 토글), `-startTab `, `-navStyle radial`, `-themeAutoToggle YES`, `-forceMacLayout YES`(아이패드 심에서 맥(Designed for iPad) 분기 강제 — §6 맥 보정 검증) | 영역 | 인자 | |---|---| diff --git a/myApp/HaruDanim/IOS/ContentView.swift b/myApp/HaruDanim/IOS/ContentView.swift index 82f5dea..30402b9 100644 --- a/myApp/HaruDanim/IOS/ContentView.swift +++ b/myApp/HaruDanim/IOS/ContentView.swift @@ -41,6 +41,11 @@ struct ContentView: View { } .tint(AppTheme.green) .preferredColorScheme(theme == "dark" ? .dark : .light) + // 맥(Designed for iPad)은 아이패드 포인트가 축소 매핑돼 글자가 작게 보임 — + // 전역 글자 크기를 한 단계 키운다. 사용자가 시스템에서 더 크게 쓰면 그 값을 존중 + .transformEnvironment(\.dynamicTypeSize) { size in + if DeviceLayout.isMac, size < .xLarge { size = .xLarge } + } .task { #if DEBUG DebugSeed.seedIfRequested(context: context) @@ -211,6 +216,13 @@ enum AppTab: String, CaseIterable, Identifiable { allCases.filter { $0 != .diary } } + /// 사이드바(iPad·Mac)에 노출되는 탭. + /// 일기는 펜슬 필기를 전제로 한 기능이라 맥(Designed for iPad)에서는 숨긴다 — + /// 데이터는 그대로 두므로 아이패드에서는 계속 보이고, 진입점만 없어진다. + static var sidebarCases: [AppTab] { + DeviceLayout.isMac ? allCases.filter { $0 != .diary } : allCases + } + var id: String { rawValue } var label: String { diff --git a/myApp/HaruDanim/IOS/Views/MainView.swift b/myApp/HaruDanim/IOS/Views/MainView.swift index 03f3abe..e697945 100644 --- a/myApp/HaruDanim/IOS/Views/MainView.swift +++ b/myApp/HaruDanim/IOS/Views/MainView.swift @@ -157,7 +157,12 @@ struct MainView: View { if DeviceLayout.isPad { // 넓은 화면: 페이징 대신 카드를 여러 장 나란히 배치 (폭도 카드 단위로 제한). // 비-lazy 레이아웃인 이유는 AdaptiveColumnsLayout 주석 참고 (스크롤 진동 버그) - AdaptiveColumnsLayout(minWidth: 340, maxWidth: 520, spacing: 10) { + // 맥은 아이패드 포인트가 축소 매핑돼 카드가 작아 보임 — 폭을 키워 보정 (§6.1) + AdaptiveColumnsLayout( + minWidth: DeviceLayout.isMac ? 430 : 340, + maxWidth: DeviceLayout.isMac ? 640 : 520, + spacing: 10 + ) { ForEach(pinnedGoals) { goal in GoalSummaryCard(goal: goal) } @@ -272,7 +277,12 @@ struct MainView: View { private var grid: some View { Group { if DeviceLayout.isPad { - AdaptiveColumnsLayout(minWidth: 150, maxWidth: 220, spacing: 12) { + // 맥은 축소 매핑 보정으로 버튼 폭을 키운다 (§6.1) + AdaptiveColumnsLayout( + minWidth: DeviceLayout.isMac ? 200 : 150, + maxWidth: DeviceLayout.isMac ? 280 : 220, + spacing: 12 + ) { gridCells } } else { diff --git a/myApp/HaruDanim/IOS/Views/SidebarRootView.swift b/myApp/HaruDanim/IOS/Views/SidebarRootView.swift index bf18ab2..2519ba4 100644 --- a/myApp/HaruDanim/IOS/Views/SidebarRootView.swift +++ b/myApp/HaruDanim/IOS/Views/SidebarRootView.swift @@ -15,6 +15,16 @@ import UIKit /// 크기 클래스 대신 기기 종류로 판정해 iPhone(가로 모드 포함)은 항상 기존 UI를 유지한다. enum DeviceLayout { static let isPad = UIDevice.current.userInterfaceIdiom == .pad + /// 맥에서 실행 중인 아이패드 앱(Designed for iPad) 여부. + /// 아이패드 포인트가 맥 화면에 축소 매핑돼 전반적으로 작게 보이므로, + /// 맥에서만 글자 크기·모음 탭 카드/버튼 폭을 키운다 (아이폰·아이패드 무영향) + static let isMac: Bool = { + #if DEBUG + // 검증용: -forceMacLayout YES → 아이패드 시뮬레이터에서 맥 분기 강제 (§14) + if UserDefaults.standard.bool(forKey: "forceMacLayout") { return true } + #endif + return ProcessInfo.processInfo.isiOSAppOnMac + }() } struct SidebarRootView: View { @@ -45,7 +55,7 @@ struct SidebarRootView: View { NavigationSplitView { List(selection: sidebarSelection) { Section { - ForEach(AppTab.allCases) { tab in + ForEach(AppTab.sidebarCases) { tab in Label { Text(tab.label) } icon: {