feat(mac): readability pass for Designed-for-iPad on Mac

- 실사용 보고: 맥에서 아이패드 앱 실행 시 버튼·정보가 전반적으로 매우 작게 보임
  (아이패드 포인트의 맥 축소 매핑 + 폭이 넓어져도 콘텐츠를 키우지 않는 레이아웃)
- DeviceLayout.isMac(isiOSAppOnMac) 신설, 맥에서만:
  ① 전역 dynamicTypeSize 최소 xLarge (사용자가 시스템에서 더 키우면 그 값 존중)
  ② 모음 탭 목표 카드 폭 430~640 / 행동 버튼 폭 200~280으로 상향
  ③ 사이드바에서 일기 탭 숨김 (AppTab.sidebarCases — 펜슬 필기 전제 기능이라
     맥에선 진입점 제거, 데이터는 보존되어 아이패드에서 계속 사용)
- 아이폰·아이패드 무영향 (isMac=false 경로 불변, 아이패드 심 회귀 스크린샷 확인)
- 검증: -forceMacLayout 인자 신설(아이패드 심에서 맥 분기 강제) — 일기 숨김·
  버튼 확대·글자 확대 확인. 맥(Designed for iPad) 대상 빌드 성공. 새 문자열 없음
- CLAUDE.md §6 맥 보정·§14 인자 기재. 맥 위젯·일기 마우스 입력은 출시 후 과제

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014CkLDi8Lr21vGqo4SjZaTp
This commit is contained in:
songyc macbook 2026-07-20 12:24:49 +09:00
parent 1283c1f799
commit 003859ee10
4 changed files with 37 additions and 4 deletions

View File

@ -169,6 +169,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
- **iPhone 기본**: 하단 탭바에 **노출 탭 1~3개**(기본 모음·행동·기록) + "더보기" 탭(나머지 목록). 설정에서 구성 - **iPhone 기본**: 하단 탭바에 **노출 탭 1~3개**(기본 모음·행동·기록) + "더보기" 탭(나머지 목록). 설정에서 구성
- **iPhone 실험 옵션**: "글래스 버블 메뉴"(`RadialNavigationView`) — 하단 중앙 FAB를 누르면 리퀴드 글라스(`glassEffect(.regular.interactive())`) 유리 구슬 탭 버튼들이 부채꼴로 펼쳐짐. 아이콘+이름이 구슬 안에 함께. 모든 유리 요소는 하나의 `GlassEffectContainer`에서 한 패스로 렌더링(성능)되고 FAB 근처에서 액체처럼 분리·합체 모핑. 2단계 연출: 삽입(FAB에서 모핑) → bloomed(스태거 스프링으로 아치 확산). ⚠️ 컨테이너 안 유리 요소에는 glassEffect 뒤 opacity/scale이 콘텐츠에 온전히 적용되지 않음 — 숨김은 계층 제거(`if`), 이동은 `offset`으로만. 순서는 설정에서 편집(기기 로컬). **두 번 눌러 바로 이동**(설정 토글+탭 선택, `settings.radialDoubleTapTab` 빈 값=꺼짐): 닫힌 FAB를 0.35초 안에 두 번 누르면 지정 탭으로 즉시 이동 — 첫 탭을 지연시키는 더블탭 제스처 대신 첫 탭은 즉시 열고 두 번째 탭에서 분기(반응성 유지, 버블이 피어나다 흡수되는 연출) - **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`로 판정) - **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`: 탭 전환 + 탭 간 전달값(모음 탭 롱프레스 → 기록/통계 탭 행동 필터 예약, 더보기 내부 경로 진입 포함) - `AppRouter`: 탭 전환 + 탭 간 전달값(모음 탭 롱프레스 → 기록/통계 탭 행동 필터 예약, 더보기 내부 경로 진입 포함)
### 6.1 모음 탭 (`MainView`) ### 6.1 모음 탭 (`MainView`)
@ -326,7 +327,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
## 14. DEBUG 런치 인자 (검증용, 모두 DEBUG 빌드 전용) ## 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 <main|action|tag|goal|history|stats|diary|settings>`, `-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 <main|action|tag|goal|history|stats|diary|settings>`, `-navStyle radial`, `-themeAutoToggle YES`, `-forceMacLayout YES`(아이패드 심에서 맥(Designed for iPad) 분기 강제 — §6 맥 보정 검증)
| 영역 | 인자 | | 영역 | 인자 |
|---|---| |---|---|

View File

@ -41,6 +41,11 @@ struct ContentView: View {
} }
.tint(AppTheme.green) .tint(AppTheme.green)
.preferredColorScheme(theme == "dark" ? .dark : .light) .preferredColorScheme(theme == "dark" ? .dark : .light)
// (Designed for iPad)
// .
.transformEnvironment(\.dynamicTypeSize) { size in
if DeviceLayout.isMac, size < .xLarge { size = .xLarge }
}
.task { .task {
#if DEBUG #if DEBUG
DebugSeed.seedIfRequested(context: context) DebugSeed.seedIfRequested(context: context)
@ -211,6 +216,13 @@ enum AppTab: String, CaseIterable, Identifiable {
allCases.filter { $0 != .diary } 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 id: String { rawValue }
var label: String { var label: String {

View File

@ -157,7 +157,12 @@ struct MainView: View {
if DeviceLayout.isPad { if DeviceLayout.isPad {
// : ( ). // : ( ).
// -lazy AdaptiveColumnsLayout ( ) // -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 ForEach(pinnedGoals) { goal in
GoalSummaryCard(goal: goal) GoalSummaryCard(goal: goal)
} }
@ -272,7 +277,12 @@ struct MainView: View {
private var grid: some View { private var grid: some View {
Group { Group {
if DeviceLayout.isPad { 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 gridCells
} }
} else { } else {

View File

@ -15,6 +15,16 @@ import UIKit
/// iPhone( ) UI . /// iPhone( ) UI .
enum DeviceLayout { enum DeviceLayout {
static let isPad = UIDevice.current.userInterfaceIdiom == .pad 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 { struct SidebarRootView: View {
@ -45,7 +55,7 @@ struct SidebarRootView: View {
NavigationSplitView { NavigationSplitView {
List(selection: sidebarSelection) { List(selection: sidebarSelection) {
Section { Section {
ForEach(AppTab.allCases) { tab in ForEach(AppTab.sidebarCases) { tab in
Label { Label {
Text(tab.label) Text(tab.label)
} icon: { } icon: {