diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index 1a794ab..eb1e1fb 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -16,7 +16,7 @@ | 번들 ID | `com.yechan.HaruDanim` / App Group `group.com.yechan.HaruDanim` / CloudKit `iCloud.com.yechan.HaruDanim` | | 수익 모델 | 부분 유료화. 무료 한도(행동 10·목표 2·목표당 다짐 3) + 프리미엄 결제 시 해제 | | 결제 상품 | `com.yechan.HaruDanim.premium.monthly`(월 5,000원) / `.yearly`(년 50,000원) / `.lifetime`(80,000원) — 임시 가격, App Store Connect 등록은 아직 안 됨 | -| 현재 상태 (2026-07-21) | **기능 동결(release freeze)** — 첫 배포 후보(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, 행동 많아질 때 혼잡도 문제의 1차 대응 — 사용자 승인 소기능, 시뮬레이션 기반 대안 A~F 중 A만 채택)·**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 즐겨찾기 줄 반영해 아이폰·아이패드 01-main과 help-main ko/en/ja 재촬영·합성판 재생성). **사업자 등록·통신판매업 신고 완료(2026-07-22)** — 서류 절차 끝, ASC 등록·유료 계약 진행 가능(순서는 Marketing/README 체크리스트). 사용자는 계속 실사용 테스트 중이며 버그 보고 시: 최소 변경 + 시뮬레이터 재검증 + l10n 루틴(§2.3) + Debug/Store(워치 파일이면 워치까지) 빌드 + 커밋·푸시 | +| 현재 상태 (2026-07-21) | **기능 동결(release freeze)** — 첫 배포 후보(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-22)** — 서류 절차 끝, ASC 등록·유료 계약 진행 가능(순서는 Marketing/README 체크리스트). 사용자는 계속 실사용 테스트 중이며 버그 보고 시: 최소 변경 + 시뮬레이터 재검증 + l10n 루틴(§2.3) + Debug/Store(워치 파일이면 워치까지) 빌드 + 커밋·푸시 | | 남은 배포 작업 | App Store Connect 상품 3종 등록(구독 그룹 + 비소모성, 유료 앱 계약), **CloudKit 프로덕션 스키마 배포**(대시보드 Deploy Schema to Production — DiaryTemplate 등 새 레코드 타입/필드는 개발 환경에만 자동 생성되므로 출시 전 필수), 실기기 검증(펜슬 필기감·캘린더 권한), App Store 배포. 코드 측 잔여 없음 — `ITSAppUsesNonExemptEncryption=NO` 등록 완료, **개인정보 매니페스트(PrivacyInfo.xcprivacy) 4개 번들 완비**(UserDefaults CA92.1·1C8F.1, 수집·트래킹 없음 — 위젯·워치위젯 폴더 것은 앱·워치 앱 타깃에서 membershipException으로 중복 복사 제외), 결제 화면 약관 링크 완료(개인정보처리방침 `Docs/PRIVACY.md` ko/en/ja는 `github.com/notalentprogrammer/HaruDanim` main의 PRIVACY.md로 게시됨, 이용약관은 Apple 표준 EULA URL). ASC 등록 시 앱 설명에도 두 링크 기재 + '앱 개인정보 보호'는 "데이터 수집 안 함" 선택 | --- @@ -151,7 +151,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f ### 5.2 동기화되는 데이터 vs 기기 로컬 설정 (`Shared/LocalPrefs.swift`) - **데이터**(행동·목표·기록·일기)는 SwiftData(+CloudKit), **"이 기기에서 어떻게 보여줄지"는 App Group UserDefaults**: - - `local.actionOrder`(모음 탭 배치 — 위젯·워치·인텐트 나열 순서도 이걸 따름), `local.pinnedGoals`(모음 탭 노출 목표), `local.collapsedGoals`, `local.notePromptActions`(기록 시 메모 창), `local.watchGoals`(워치 컴플리케이션에 노출할 목표 — 기본 비어 있음(옵트인), 워치는 이 아이폰과 페어링되므로 기기 로컬이 자연스러움) + - `local.actionOrder`(모음 탭 배치 — 위젯·워치·인텐트 나열 순서도 이걸 따름), `local.pinnedGoals`(모음 탭 노출 목표), `local.collapsedGoals`, `local.notePromptActions`(기록 시 메모 창), `local.watchGoals`(워치 컴플리케이션에 노출할 목표 — 기본 비어 있음(옵트인), 워치는 이 아이폰과 페어링되므로 기기 로컬이 자연스러움), `local.mainOthersCollapsed`(모음 탭 '나머지 행동' 섹션 접힘 — Bool) - 대응하는 @Model 레거시 필드는 스키마 안정성 위해 남겨두고 1회 이관(`adoptLegacyModelValuesIfNeeded`) - 설정 키는 `Shared/Settings.swift` — 집계 기준·테마·대표시간 등은 App Group defaults(위젯 공유), navStyle·visibleTabs·radialTabOrder·diary.section* 은 standard(기기 로컬) @@ -175,11 +175,11 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f ### 6.1 모음 탭 (`MainView`) - 상단: 노출 목표 진행 현황 카드(1개=단독, 2개+=스냅 페이징 가로 스크롤 + 페이지 점, iPad=그리드). 카드 스타일 설정: 다짐별 각각(perQuest, 접힘 시 3개+더보기) / 전체 합산(combined) - "현재 진행 중" 영역: 측정 중 세션 나열 + 실시간 타이머 + 종료 버튼 -- **즐겨찾기 빠른 실행 영역**(`favoritesArea`, `runningArea` 아래·전체 그리드 위, `!favoriteActions.isEmpty && !isEditing`일 때만): `Action.isFavorite`(행동 탭과 동일 필드, §6.2) 켠 행동을 기기별 배치 순서(`orderedActions` 기준) 그대로 가로 스크롤 한 줄로 노출 — 행동이 많아져도 즐겨찾기는 스크롤 없이 바로 접근 가능하게 하는 목적(2026-07-21, 실사용 크라우딩 피드백 대응, §15 하단 이력 참고). **추가만 하고 전체 그리드에서 제외하지 않음**(즐겨찾기 아닌 행동을 찾기 더 어려워지는 트레이드오프 방지가 의도된 설계 — 그리드에서 숨기는 방식은 검토 후 기각됨). 셀은 `ActionButtonCell(compact: true)` 고정 폭(108pt, 맥은 축소 매핑 보정으로 150pt — 그리드 버튼과 같은 비율) 재사용, 탭 동작(`handleTap`)과 롱프레스 메뉴(`actionContextMenu(for:)` — 기록 확인/통계 보기/기록 직접 입력·수정/**즐겨찾기 지정·해제**/행동 설정 수정/삭제)는 전체 그리드 셀과 완전히 공유. 즐겨찾기 지정·해제는 이 메뉴 또는 행동 탭에서 가능(§6.2) +- **즐겨찾기·나머지 섹션**(`favoritesSection`/`othersSection`, `!favoriteActions.isEmpty && !isEditing`일 때 — 없으면 기존 통짜 그리드): `Action.isFavorite`(행동 탭과 동일 필드, §6.2) 켠 행동이 **그리드의 첫 섹션**(별 헤더)으로 올라오고, 나머지는 "나머지 행동 · N개" 섹션으로 나뉜다. 두 섹션 모두 통짜 그리드와 같은 `actionGrid(_:)` 빌더를 써서 셀 크기·열 규칙·맥 보정이 항상 동일(시각적으로 하나의 그리드). **v1(별도 카드+컴팩트 셀 가로 스크롤 줄, 2026-07-21)은 그리드와 이질적이고 같은 행동이 두 번 보여 "따로 노는 느낌"이라는 실사용 피드백(2026-07-22)으로 v2로 교체**. **'나머지 행동' 헤더를 탭하면 접힘/펼침**(기기별 `local.mainOthersCollapsed`, 기본 펼침, 셰브론 회전) — 행동이 많아도 접으면 즐겨찾기만 남는다. **숨기는 행동은 없음**(전부 같은 화면, 위치만 승격 — 그리드에서 완전히 숨기는 방식은 기각 유지). 편집(배치) 중에는 배치 순서가 하나뿐이므로 항상 통짜 그리드로 전환. 각 섹션 순서는 기기별 배치 순서(`orderedActions`)를 그대로 따름. 탭 동작(`handleTap`)과 롱프레스 메뉴(`actionContextMenu(for:)` — 기록 확인/통계 보기/기록 직접 입력·수정/**즐겨찾기 지정·해제**/행동 설정 수정/삭제)는 전 섹션 공유. 즐겨찾기 지정·해제는 이 메뉴 또는 행동 탭에서 가능(§6.2) - 행동 버튼 그리드: 꼬리표 색 그라데이션 배경 + 아이콘 + 이름 + 오늘 누적(시간형) 또는 오늘 횟수(횟수형, 숫자 전환 애니메이션). 측정 중이면 노란 테두리 + record 점. 탭 = 시작/종료 토글 또는 +1 (스프링 눌림 + 햅틱) - 한 줄 개수: iPhone 2~6개 설정(5개 이상은 compact 셀), iPad는 화면 폭 자동 채움 — **iPad의 목표 카드·행동 그리드는 LazyVGrid가 아니라 비-lazy `AdaptiveColumnsLayout`**(MainView 내 커스텀 Layout, LazyVGrid(.adaptive)와 같은 열 계산). lazy 높이 추정이 높이 제각각인 목표 카드 + 1초 타이머 갱신과 결합해 맨 아래 스크롤 시 화면이 무한 진동(터치 불능)하는 실기기 버그의 수정 — iPhone(고정 열·페이징 높이 고정)은 LazyVGrid 유지. iPad 모음 그리드를 다시 lazy로 되돌리지 말 것. **행동 셀 높이 통일 규칙**: 시간형·횟수형 푸터 높이가 달라(횟수형 27pt 숫자) 셀 자연 높이가 달랐음 — 푸터에 숫자 크기의 hidden 템플릿(Text "0")을 깔아 두 유형의 자연 높이를 맞추고, AdaptiveColumnsLayout.place가 LazyVGrid처럼 행 높이를 제안해 행 안 셀도 균일(측정 패스는 불변이라 진동 버그와 무관, 고정 높이 목표 카드 무영향) - 롱프레스 메뉴: 기록 확인(기록 탭 필터 이동) / 통계 보기 / 기록 직접 입력·수정 / **즐겨찾기 지정·해제** / 행동 설정 수정 / 삭제 -- **행동이 많아질 때의 혼잡도 문제**(2026-07-21 실사용 피드백): 22개 시드로 iPhone(3열·6열)·iPad·맥 4가지를 직접 시뮬레이션해 6가지 대안(A~F)을 검토 — 이번에 구현한 즐겨찾기 빠른 실행(A)에 더해, 향후 후보로 ⓑ꼬리표별 접기(출시 후), ⓒ검색/필터(출시 후), ⓓ한 줄 개수 늘릴 때 경고 문구(미착수, 저위험)가 있음. **명시적으로 기각**: 즐겨찾기 등을 전체 그리드에서 숨기는 옵트인 방식(행동이 눌러도 안 보이게 될 위험 > 이득) — 다시 제안하지 말 것 +- **행동이 많아질 때의 혼잡도 문제**(2026-07-21 실사용 피드백): 22개 시드로 iPhone(3열·6열)·iPad·맥 4가지를 직접 시뮬레이션해 6가지 대안(A~F)을 검토 — 구현된 즐겨찾기 모아 보기(A, 현재는 v2 섹션형+나머지 접기)에 더해, 향후 후보로 ⓑ꼬리표별 접기(출시 후), ⓒ검색/필터(출시 후), ⓓ한 줄 개수 늘릴 때 경고 문구(미착수, 저위험)가 있음. **명시적으로 기각**: 즐겨찾기 등을 전체 그리드에서 숨기는 옵트인 방식(행동이 눌러도 안 보이게 될 위험 > 이득) — 다시 제안하지 말 것 - 배치 편집: 지글 애니메이션 + 드래그 재정렬 → `local.actionOrder` 저장 (CloudKit 모델에 쓰지 않음) - "짧은 기록 무시"(minSessionSeconds): 설정보다 짧은 세션은 종료 시 삭제. 메모 창 옵션 켠 행동은 종료/+1 시 메모 시트(건너뛰기 가능) - 툴바: 새로고침(좌) / 배치 편집(우) @@ -329,11 +329,11 @@ 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`, `-forceMacLayout YES`(아이패드 심에서 맥(Designed for iPad) 분기 강제 — §6 맥 보정 검증) +공통: `-seedDemo YES`(데모 데이터: 태그3·행동7[즐겨찾기 3: 독서·달리기·물 마시기]·기록 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 맥 보정 검증) | 영역 | 인자 | |---|---| -| 모음 | `-startEditing` `-expandGoalCard` `-pinGoals ` `-openStatsFor "이름"` `-openHistoryFor "이름"` `-autoStart "이름"` `-recordAddFor "이름"`(기록 직접 입력 시트) | +| 모음 | `-startEditing` `-expandGoalCard` `-pinGoals ` `-openStatsFor "이름"` `-openHistoryFor "이름"` `-autoStart "이름"` `-recordAddFor "이름"`(기록 직접 입력 시트) `-mainOthersCollapsed`('나머지 행동' 섹션 접힘 강제 — 표시 전용 오버라이드, 실 설정 무변경) | | 측정 알림 | `-settings.longSessionAlertHours `(설정 주입) `-longSessionAlertTestSeconds `(시간 대신 N초 발화, provisional 권한 자동) `-alertDump`(예약 상태 → Documents/session-alerts.txt) | | 목표 | `-goalShowEditor` `-goalShowFinished` `-goalReorder` `-goalScrollBottom` `-questShowEditor`(다짐 편집기 표시 — 마감 시각 다짐 우선, 없으면 첫 다짐) `-endGoalYesterday "제목"`(종료일을 어제로 — 실행 시점 자동 판정 검증) `-watchGoals `(생성순 목표 N개를 '애플워치에서 보기'로 설정, 0=전체 해제 — 설정 후 스냅숏 즉시 푸시, 컴플리케이션 필터 검증용) `-streakDump`(모든 다짐의 연속 계산 결과 → Documents/streak-dump.txt) `-progressSelfTest`(spanProgress 수치 37건 자가 검증[환산 캡·수행일 집계·마감·atMost·custom] → Documents/progress-self-test.txt — 인메모리 컨테이너·설정 고정이라 실데이터/기기 설정 무영향) | | 기록/통계 | `-historyMode timetable` `-historyWeekly` `-excludeActions "이름,이름"` `-historyShowFilter` `-statShowFilter` `-statSpan ` `-statScrollBottom` `-showExport` `-exportDump`(Documents/export-dump.png) `-recordShowEditor `(최신 기록 수정 시트) | diff --git a/myApp/HaruDanim/IOS/Core/DebugSeed.swift b/myApp/HaruDanim/IOS/Core/DebugSeed.swift index b7cf9ac..5a00fe2 100644 --- a/myApp/HaruDanim/IOS/Core/DebugSeed.swift +++ b/myApp/HaruDanim/IOS/Core/DebugSeed.swift @@ -500,11 +500,13 @@ enum DebugSeed { english.tags = [study] let running = Action(name: String(localized: "달리기"), symbolName: "figure.run", trackingType: .time, sortOrder: 2) running.tags = [workout] + running.isFavorite = true let pushup = Action(name: String(localized: "팔굽혀펴기"), symbolName: "dumbbell.fill", trackingType: .count, sortOrder: 3) pushup.tags = [workout] let water = Action(name: String(localized: "물 마시기"), symbolName: "drop.fill", trackingType: .count, sortOrder: 4) water.tags = [life] water.promptsForNote = true + water.isFavorite = true // '이하 유지' 다짐 검증용 (하루 1시간 이하 목표, 일부러 초과하는 날 포함) let video = Action(name: String(localized: "영상 시청"), symbolName: "play.rectangle.fill", trackingType: .time, sortOrder: 5) video.tags = [life] diff --git a/myApp/HaruDanim/IOS/Localizable.xcstrings b/myApp/HaruDanim/IOS/Localizable.xcstrings index 414c5db..fc11c22 100644 --- a/myApp/HaruDanim/IOS/Localizable.xcstrings +++ b/myApp/HaruDanim/IOS/Localizable.xcstrings @@ -2720,6 +2720,54 @@ } } }, + "나머지 행동" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Other actions" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他のアクション" + } + } + } + }, + "나머지 행동 접기" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hide other actions" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他のアクションを折りたたむ" + } + } + } + }, + "나머지 행동 펼치기" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Show other actions" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "その他のアクションを表示" + } + } + } + }, "날짜" : { "localizations" : { "en" : { @@ -9741,18 +9789,18 @@ } } }, - "즐겨찾기 빠른 실행" : { + "즐겨찾기 모아 보기" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Favorites quick access" + "value" : "Favorites at the top" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "お気に入りクイック実行" + "value" : "お気に入りをまとめて表示" } } } @@ -12507,18 +12555,18 @@ } } }, - "행동을 즐겨찾기로 지정하면 모음 탭 상단에 '즐겨찾기' 줄이 생겨 가로로 훑어 바로 실행할 수 있어요. 행동이 많아져 아래 전체 목록을 찾기 번거로울 때 자주 쓰는 행동만 모아 두는 용도예요 — 즐겨찾기해도 전체 목록에서는 사라지지 않아요. 지정은 행동 버튼을 길게 눌러 '즐겨찾기'를 선택하거나, 행동 탭에서 행동을 열어 별 아이콘을 눌러도 돼요." : { + "행동을 즐겨찾기로 지정하면 모음 탭 행동 목록이 '즐겨찾기'와 '나머지 행동' 두 부분으로 나뉘고, 즐겨찾기가 항상 맨 위에 와요. '나머지 행동' 제목을 누르면 그 아래를 접어 둘 수 있어서 행동이 많아져도 자주 쓰는 것만 남길 수 있어요(접힘은 기기마다 저장). 지정은 행동 버튼을 길게 눌러 '즐겨찾기'를 선택하거나, 행동 탭에서 행동을 열어 별 아이콘을 눌러도 돼요." : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Mark an action as a favorite and a 'Favorites' row appears at the top of the Home tab, letting you swipe through and run it right away. It's meant for keeping your most-used actions handy once the full list below gets long to scroll through — favoriting doesn't remove the action from the full list. Long-press an action button and choose 'Favorite', or open it from the Actions tab and tap the star icon." + "value" : "Mark actions as favorites and the Home tab list splits into two parts — 'Favorites' and 'Other actions' — with favorites always at the top. Tap the 'Other actions' title to collapse everything below it, so even with lots of actions only the ones you use most stay in view (the collapsed state is saved per device). To mark a favorite, long-press an action button and choose 'Favorites', or open the action in the Actions tab and tap the star icon." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "行動をお気に入りに指定すると、モーム(集約)タブの上部に「お気に入り」の行が表示され、横にスワイプしてすぐに実行できます。下の全リストが長くなって探しにくいときに、よく使う行動だけをまとめておくためのものです — お気に入りにしても全リストから消えることはありません。指定は行動ボタンを長押しして「お気に入り」を選ぶか、行動タブで行動を開いて星アイコンをタップしても行えます。" + "value" : "アクションをお気に入りに指定すると、ホームタブの一覧が「お気に入り」と「その他のアクション」の2つに分かれ、お気に入りが常に一番上に表示されます。「その他のアクション」のタイトルをタップすると下を折りたたむことができ、アクションが増えてもよく使うものだけを残せます(折りたたみ状態は端末ごとに保存)。指定はアクションボタンを長押しして「お気に入り」を選ぶか、アクションタブでアクションを開いて星アイコンをタップしてもできます。" } } } diff --git a/myApp/HaruDanim/IOS/Views/HelpView.swift b/myApp/HaruDanim/IOS/Views/HelpView.swift index 0abfcbd..bf095e5 100644 --- a/myApp/HaruDanim/IOS/Views/HelpView.swift +++ b/myApp/HaruDanim/IOS/Views/HelpView.swift @@ -188,8 +188,8 @@ struct HelpView: View { ), HelpTopic( symbol: "star.fill", - title: "즐겨찾기 빠른 실행", - body: "행동을 즐겨찾기로 지정하면 모음 탭 상단에 '즐겨찾기' 줄이 생겨 가로로 훑어 바로 실행할 수 있어요. 행동이 많아져 아래 전체 목록을 찾기 번거로울 때 자주 쓰는 행동만 모아 두는 용도예요 — 즐겨찾기해도 전체 목록에서는 사라지지 않아요. 지정은 행동 버튼을 길게 눌러 '즐겨찾기'를 선택하거나, 행동 탭에서 행동을 열어 별 아이콘을 눌러도 돼요." + title: "즐겨찾기 모아 보기", + body: "행동을 즐겨찾기로 지정하면 모음 탭 행동 목록이 '즐겨찾기'와 '나머지 행동' 두 부분으로 나뉘고, 즐겨찾기가 항상 맨 위에 와요. '나머지 행동' 제목을 누르면 그 아래를 접어 둘 수 있어서 행동이 많아져도 자주 쓰는 것만 남길 수 있어요(접힘은 기기마다 저장). 지정은 행동 버튼을 길게 눌러 '즐겨찾기'를 선택하거나, 행동 탭에서 행동을 열어 별 아이콘을 눌러도 돼요." ), HelpTopic( symbol: "hand.tap.fill", diff --git a/myApp/HaruDanim/IOS/Views/MainView.swift b/myApp/HaruDanim/IOS/Views/MainView.swift index cda9718..ef9eeb0 100644 --- a/myApp/HaruDanim/IOS/Views/MainView.swift +++ b/myApp/HaruDanim/IOS/Views/MainView.swift @@ -20,18 +20,26 @@ struct MainView: View { // 배치 순서·노출 목표는 기기별 로컬 설정 (CloudKit 동기화 제외, LocalPrefs 참고) @AppStorage(LocalPrefsKeys.actionOrder, store: AppGroup.defaults) private var actionOrderRaw = "" @AppStorage(LocalPrefsKeys.pinnedGoals, store: AppGroup.defaults) private var pinnedGoalsRaw = "" + /// '나머지 행동' 섹션 접힘(기기별) — 즐겨찾기가 있을 때만 섹션이 생기므로 그때만 의미 있음 + @AppStorage(LocalPrefsKeys.mainOthersCollapsed, store: AppGroup.defaults) private var othersCollapsed = false private var orderedActions: [Action] { LocalPrefs.orderedActions(actions, raw: actionOrderRaw) } - /// 즐겨찾기 빠른 실행 줄에 노출할 행동 (기기별 배치 순서 유지). 전체 그리드에서 제외하지 않고 - /// 추가로만 보여준다 — 행동이 많아져도 즐겨찾기는 항상 스크롤 없이 접근 가능하게 하는 목적 - /// (전체 그리드에서 숨기면 즐겨찾기 아닌 행동을 찾기 더 어려워지는 트레이드오프 방지, CLAUDE.md §6.1) + /// 즐겨찾기 행동 (기기별 배치 순서 유지). 별도 카드가 아니라 그리드의 첫 섹션으로 노출 — + /// v1(가로 스크롤 컴팩트 카드 줄)은 그리드와 셀 크기·배경이 달라 "따로 노는 느낌"이라는 + /// 실사용 피드백으로 교체됨. 숨기는 행동 없이 위치만 맨 위로 승격되고, 아래 '나머지 행동' + /// 섹션은 접어 둘 수 있어 행동이 많아도 자주 쓰는 것만 남는다 (CLAUDE.md §6.1) private var favoriteActions: [Action] { orderedActions.filter(\.isFavorite) } + /// 즐겨찾기 밖 행동 — 즐겨찾기가 하나라도 있을 때만 별도 섹션(접기 가능)으로 나뉜다 + private var otherActions: [Action] { + orderedActions.filter { !$0.isFavorite } + } + private var pinnedGoals: [Goal] { allGoals.filter { LocalPrefs.contains($0.uuid, in: pinnedGoalsRaw) } } @@ -70,16 +78,19 @@ struct MainView: View { if anyRunning && !isEditing { runningArea } - if !favoriteActions.isEmpty && !isEditing { - favoritesArea - } if isEditing { layoutControl } if actions.isEmpty { emptyState - } else { + } else if isEditing || favoriteActions.isEmpty { + // 편집 중엔 순서가 하나(전체 배치)이므로 항상 통짜 그리드 grid + } else { + favoritesSection + if !otherActions.isEmpty { + othersSection + } } } .padding() @@ -251,30 +262,58 @@ struct MainView: View { .background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 18, style: .continuous)) } - // MARK: 즐겨찾기 빠른 실행 영역 (행동이 많아져도 항상 가로 스크롤 한 줄로 접근) + // MARK: 즐겨찾기·나머지 섹션 (행동이 많아도 자주 쓰는 것만 위에 남기기) - private var favoritesArea: some View { - VStack(alignment: .leading, spacing: 8) { - Label("즐겨찾기", systemImage: "star.fill") - .font(.subheadline.weight(.semibold)) - .foregroundStyle(AppTheme.green) - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 10) { - ForEach(favoriteActions) { action in - ActionButtonCell(action: action, isEditing: false, compact: true) { - handleTap(action) - } - // 맥은 축소 매핑 보정으로 폭을 키운다 — 그리드 버튼(150→200)과 같은 비율 (§6.1) - .frame(width: DeviceLayout.isMac ? 150 : 108) - .contextMenu { - actionContextMenu(for: action) - } - } + /// 접힘 상태 — DEBUG 스크린샷 검증용 오버라이드(-mainOthersCollapsed, 실 설정 무변경) 포함 + private var effectiveOthersCollapsed: Bool { + #if DEBUG + if UserDefaults.standard.bool(forKey: "mainOthersCollapsed") { return true } + #endif + return othersCollapsed + } + + private var favoritesSection: some View { + VStack(alignment: .leading, spacing: 10) { + Label { + Text("즐겨찾기") + } icon: { + Image(systemName: "star.fill") + .foregroundStyle(AppTheme.yellow) + } + .font(.subheadline.weight(.semibold)) + .foregroundStyle(AppTheme.green) + .padding(.leading, 2) + actionGrid(favoriteActions) + } + } + + private var othersSection: some View { + VStack(alignment: .leading, spacing: 10) { + Button { + withAnimation(.smooth(duration: 0.25)) { othersCollapsed.toggle() } + } label: { + HStack(spacing: 6) { + Text("나머지 행동") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(AppTheme.green) + Text("\(otherActions.count)개") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + Spacer(minLength: 0) + Image(systemName: "chevron.down") + .font(.caption.weight(.bold)) + .foregroundStyle(.secondary) + .rotationEffect(.degrees(effectiveOthersCollapsed ? -90 : 0)) } + .padding(.leading, 2) + .contentShape(.rect) + } + .buttonStyle(.plain) + .accessibilityLabel(effectiveOthersCollapsed ? Text("나머지 행동 펼치기") : Text("나머지 행동 접기")) + if !effectiveOthersCollapsed { + actionGrid(otherActions) } } - .padding(14) - .background(AppTheme.surface, in: RoundedRectangle(cornerRadius: 18, style: .continuous)) } // MARK: 배치 편집 컨트롤 (한 줄 개수 선택) @@ -309,8 +348,14 @@ struct MainView: View { Array(repeating: GridItem(.flexible(), spacing: 12), count: gridColumns) } - /// iPad는 목표 카드 영역과 같은 비-lazy 적응형 레이아웃 (통일 + 스크롤 진동 방지) + /// 편집 중·즐겨찾기 없음일 때의 통짜 그리드 (기존 동작 그대로) private var grid: some View { + actionGrid(orderedActions) + } + + /// iPad는 목표 카드 영역과 같은 비-lazy 적응형 레이아웃 (통일 + 스크롤 진동 방지). + /// 즐겨찾기·나머지 섹션이 같은 빌더를 쓰므로 셀 크기·열 규칙이 항상 그리드와 동일하다 + private func actionGrid(_ list: [Action]) -> some View { Group { if DeviceLayout.isPad { // 맥은 축소 매핑 보정으로 버튼 폭을 키운다 (§6.1) @@ -319,24 +364,22 @@ struct MainView: View { maxWidth: DeviceLayout.isMac ? 280 : 220, spacing: 12 ) { - gridCells + ForEach(list) { action in + cell(for: action) + } } } else { LazyVGrid(columns: gridLayout, spacing: 12) { - gridCells + ForEach(list) { action in + cell(for: action) + } } } } - .animation(.default, value: orderedActions.map(\.persistentModelID)) + .animation(.default, value: list.map(\.persistentModelID)) .animation(.default, value: gridColumns) } - private var gridCells: some View { - ForEach(orderedActions) { action in - cell(for: action) - } - } - @ViewBuilder private func cell(for action: Action) -> some View { let base = ActionButtonCell( @@ -370,7 +413,7 @@ struct MainView: View { } } - /// 행동 셀 롱프레스 메뉴 — 모음 탭 전체 그리드와 즐겨찾기 빠른 실행 줄이 공유 + /// 행동 셀 롱프레스 메뉴 — 모음 탭 즐겨찾기·나머지 섹션과 통짜 그리드가 공유 @ViewBuilder private func actionContextMenu(for action: Action) -> some View { Button { diff --git a/myApp/HaruDanim/Shared/LocalPrefs.swift b/myApp/HaruDanim/Shared/LocalPrefs.swift index d86da00..f509600 100644 --- a/myApp/HaruDanim/Shared/LocalPrefs.swift +++ b/myApp/HaruDanim/Shared/LocalPrefs.swift @@ -31,6 +31,8 @@ nonisolated enum LocalPrefsKeys { /// 기본은 비어 있음(모두 꺼짐) — 목표·다짐이 많아지면 컴플리케이션 목록이 폭증하므로 옵트인. /// 워치는 이 아이폰과 페어링되므로 기기 로컬이 하드웨어 현실과 일치한다. static let watchGoals = "local.watchGoals" + /// 모음 탭 '나머지 행동' 섹션(즐겨찾기가 있을 때만 생김) 접힘 여부 — Bool + static let mainOthersCollapsed = "local.mainOthersCollapsed" } nonisolated enum LocalPrefs {