From 14b18bc22af797d102e7906f2bb073bcc39847ff Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Wed, 22 Jul 2026 21:17:38 +0900 Subject: [PATCH] =?UTF-8?q?fix(review):=20=EC=A0=84=EC=B2=B4=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=EB=B7=B0=202=EC=B0=A8=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=E2=80=94=20=ED=9A=9F=EC=88=98=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=20en=20=EB=8B=A8=EB=B3=B5=EC=88=98=20+=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1차(c18e3c7)에 이어 나머지 전 영역을 세션 본체가 직접 정독 완료: 위젯 10파일·인텐트, 앱 셸(ContentView/Settings/Help/Radial/SymbolCatalog), 워치 전체(페이로드·동기화·앱·컴플리케이션), 일기 전체(6파일 ~4.2k줄), 내보내기 뷰부·CSV, MainView·ActionViews 잔여 — 추가 결함 1건만 발견. [L] 모음 탭 횟수 셀: 큰 숫자와 단위("회")를 분리 렌더링하는 유일한 지점이라 en에서 1일 때 "1 times"가 됨 — Format.countUnit(단복수 분기, 명시 키 count.unit.one/other)로 수정. 단독 "회" 키는 사용처 소멸로 정리 (ja 「回」는 단복수 무관) 문서(CLAUDE.md): 이번 리뷰로 확정된 동작 규칙 4건을 명세에 반영 — 세션 편집 원본 초 보존·0길이 차단(§6.5), 타임테이블 HH:mm 라벨(§6.5), 필터 활성 실재 행동 기준(§6.5), 통계 동명 구분 Format.disambiguated(§6.6) 정독 제외(사유 명시): DebugSeed·WidgetPreviewScreen·ComplicationPreview (DEBUG 전용), 워치 엔트리 2파일(41줄), HelpView 본문 문구부(카탈로그 검증으로 커버), ExportImageView PhotoSaver(~40줄) 검증: Debug/Store 빌드, 카탈로그 missing/stale 0 Co-Authored-By: Claude Fable 5 --- myApp/HaruDanim/CLAUDE.md | 8 +-- myApp/HaruDanim/IOS/Localizable.xcstrings | 62 +++++++++++++++++------ myApp/HaruDanim/IOS/Views/MainView.swift | 2 +- myApp/HaruDanim/Shared/Formatters.swift | 10 ++++ 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index 584f6d7..4df4b99 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -204,14 +204,14 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f ### 6.5 기록 탭 (`HistoryView`, `RecordEditors`) - **범위 조회**: 기록은 전량 로드하지 않고 선택 날짜가 속한 **주 범위**만 @Query로 가져온다(`HistoryRecordsView` — SwiftData 동적 쿼리 패턴: 날짜가 바뀌면 새 조건으로 재생성). 하루 경계 걸침·진행 중 세션은 "시작 < 범위끝 AND (종료 없음 OR 종료 > 범위시작)" 겹침 조건으로 포함 - 날짜 헤더(화살표 이동, 날짜 탭=달력 팝오버, "오늘" 버튼) + 목록/타임테이블 세그먼트 -- 목록: 시간 기록(시작~종료·총 시간·메모)·횟수 기록(시각·+n·메모), 탭하면 수정 시트. **수정·추가 시트의 시각 입력은 상시 노출 휠 다이얼**(`RecordEditors.swift` RecordTimeWheel) — 컴팩트 팝오버는 다이얼 감속 중 닫히면 정착값이 유실되는 실기기 보고가 있어 폐기. `in:` 범위 제약도 하한 근처 선택을 소리 없이 되감아 금지(검증은 종료<시작 경고 문구+저장 비활성). 편집 상태는 분 단위 절사(flooredToMinute) — 실측정 기록의 초 찌꺼기(14:32:47)가 남으면 다이얼로 맞춘 1시간이 59분으로 표시되는 어긋남 방지. **앱의 모든 시각 입력은 `CollapsibleTimeWheel`**(RecordEditors.swift) — 평소엔 값 행(라벨+현재 값+셰브론), 탭하면 인라인 휠(RecordTimeWheel) 펼침. 상시 노출 휠이 어수선하다는 실기기 피드백으로 기록 편집 4시트·다짐 '마감 시각'·설정 '하루 시작 시간' 전부 통일(기본 접힘). **펼침/접힘은 조건부 삽입(if)이 아니라 높이 0↔216 프레임 보간**(.smooth 0.3s) — 삽입 방식은 List 행 높이가 스냅되어 '확' 열리는 느낌(실기기 피드백). 휠은 Color.clear의 overlay로 띄워 UIDatePicker 고유 높이가 레이아웃에 개입하지 않게 한다(접힘 상태 빈 공간 버그 방지 — 이 구조를 유지할 것). 팝오버 금지 이유(감속 중 닫힘 시 정착값 유실)와 범위 제약 금지·분 절사 규칙은 그대로 -- 타임테이블: 24시간 축(하루 시작 시간 기준), 하루/일주일 전환(주간 보기에서 화살표는 7일씩 점프). 시간=색 블록, 횟수=색 점. 블록/점 탭=수정 -- 필터: `RecordFilterSheet`(공용) — 꼬리표 아래 행동이 그룹된 트리, **"제외한 행동 ID" 집합**으로 관리해 기본이 전체 선택. 활성 시 초록 칩 표시 +- 목록: 시간 기록(시작~종료·총 시간·메모)·횟수 기록(시각·+n·메모), 탭하면 수정 시트. **수정·추가 시트의 시각 입력은 상시 노출 휠 다이얼**(`RecordEditors.swift` RecordTimeWheel) — 컴팩트 팝오버는 다이얼 감속 중 닫히면 정착값이 유실되는 실기기 보고가 있어 폐기. `in:` 범위 제약도 하한 근처 선택을 소리 없이 되감아 금지(검증은 종료<시작 경고 문구+저장 비활성). 편집 상태는 분 단위 절사(flooredToMinute) — 실측정 기록의 초 찌꺼기(14:32:47)가 남으면 다이얼로 맞춘 1시간이 59분으로 표시되는 어긋남 방지. **저장은 다이얼을 움직인 필드만 절사 값으로 쓰고, 안 움직인 필드는 원본 시각(초 포함)을 보존**(effectiveStart/End — 메모만 고쳐도 1분 미만 세션이 0초로 파괴되던 결함의 수정, 2026-07-22 전체 리뷰). 시작=종료(0길이) 저장은 경고 문구+비활성으로 차단(목록·타임테이블·내보내기 어디에도 안 보이는 유령 기록 방지). **앱의 모든 시각 입력은 `CollapsibleTimeWheel`**(RecordEditors.swift) — 평소엔 값 행(라벨+현재 값+셰브론), 탭하면 인라인 휠(RecordTimeWheel) 펼침. 상시 노출 휠이 어수선하다는 실기기 피드백으로 기록 편집 4시트·다짐 '마감 시각'·설정 '하루 시작 시간' 전부 통일(기본 접힘). **펼침/접힘은 조건부 삽입(if)이 아니라 높이 0↔216 프레임 보간**(.smooth 0.3s) — 삽입 방식은 List 행 높이가 스냅되어 '확' 열리는 느낌(실기기 피드백). 휠은 Color.clear의 overlay로 띄워 UIDatePicker 고유 높이가 레이아웃에 개입하지 않게 한다(접힘 상태 빈 공간 버그 방지 — 이 구조를 유지할 것). 팝오버 금지 이유(감속 중 닫힘 시 정착값 유실)와 범위 제약 금지·분 절사 규칙은 그대로 +- 타임테이블: 24시간 축(하루 시작 시간 기준), 하루/일주일 전환(주간 보기에서 화살표는 7일씩 점프). 시간=색 블록, 횟수=색 점. 블록/점 탭=수정. **하루 시작이 정시가 아니면(예 06:30) 축 라벨을 HH:mm으로 표기**(행 경계가 06:30~07:30이라 시만 쓰면 최대 59분 어긋남 — 화면·내보내기 동일 규칙, 2026-07-22) +- 필터: `RecordFilterSheet`(공용) — 꼬리표 아래 행동이 그룹된 트리, **"제외한 행동 ID" 집합**으로 관리해 기본이 전체 선택. 활성 시 초록 칩 표시. 활성 판정(isFiltering)은 **실재 행동 기준** — 제외했던 행동을 삭제하면 잔존 ID를 무시해 칩·내보내기 필터 문구가 허위로 남지 않는다(기록·통계 공통, 2026-07-22) - 내보내기: 현재 날짜/모드/필터 그대로 `ExportBuilder.history` 스냅숏 → 이미지 시트 ### 6.6 통계 탭 (`StatsTabView`) — 기록과 분리된 독립 탭 - **범위 조회 + 1회 버킷 집계**(`StatsChartsView`): 보고 있는 기간 범위만 @Query로 가져오고, 조회된 기록을 한 번만 순회해 행동별·하루별 버킷(`StatsData`)을 만들어 모든 카드가 공유 — 카드마다 Aggregator로 전체 기록을 재순회하지 않음. 하루 분할은 `splitByDay`(기존과 동일 수치). 대량 검증은 `-seedBulk` -- 하루: 꼬리표별 시간/횟수 가로 막대. 주간: 행동별 일별 꺾은선(행동 색+범례)+합계 막대+합계·평균 표. 월간: 주차별+일별 꺾은선+합계+표 +- 하루: 꼬리표별 시간/횟수 가로 막대. 주간: 행동별 일별 꺾은선(행동 색+범례)+합계 막대+합계·평균 표. 월간: 주차별+일별 꺾은선+합계+표. **동명 행동/꼬리표는 표시 이름을 "이름 (2)"로 구분**(`Format.disambiguated` — 이름이 곧 시리즈 키·범례·Identifiable id라 동명이 한 선으로 합쳐지고 id가 충돌하던 결함의 수정. 통계 탭·통계 내보내기·⑤ 위젯 3표면 공통, 꼬리표 집계는 identity 키. 2026-07-22) - 평균 분모는 "이미 시작된" 날/주만 (미래로 희석 방지). 필터·내보내기는 기록 탭과 동일 패턴 ### 6.7 일기 탭 (iPad 전용, 프리미엄 — `DiaryView`, `DiaryNotePage`, `DiaryCalendarEvents`, `DiaryExportSheet`) diff --git a/myApp/HaruDanim/IOS/Localizable.xcstrings b/myApp/HaruDanim/IOS/Localizable.xcstrings index 337a49c..d5c46d9 100644 --- a/myApp/HaruDanim/IOS/Localizable.xcstrings +++ b/myApp/HaruDanim/IOS/Localizable.xcstrings @@ -1232,6 +1232,52 @@ } } }, + "count.unit.one" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "time" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "回" + } + }, + "ko" : { + "stringUnit" : { + "state" : "new", + "value" : "회" + } + } + } + }, + "count.unit.other" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "times" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "回" + } + }, + "ko" : { + "stringUnit" : { + "state" : "new", + "value" : "회" + } + } + } + }, "iCloud 동기화는 네트워크 상황에 따라 몇 초에서 몇 분까지 걸릴 수 있어요. 다른 기기의 기록이 안 보이면 모음 탭 왼쪽 위 새로고침 버튼을 눌러 주세요. 그래도 안 보이면 잠시 후 다시 시도하거나 두 기기 모두 인터넷 연결을 확인해 주세요." : { "localizations" : { "en" : { @@ -12828,22 +12874,6 @@ } } }, - "회" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "times" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "回" - } - } - } - }, "횟수" : { "localizations" : { "en" : { diff --git a/myApp/HaruDanim/IOS/Views/MainView.swift b/myApp/HaruDanim/IOS/Views/MainView.swift index f9a8caf..c5d5f53 100644 --- a/myApp/HaruDanim/IOS/Views/MainView.swift +++ b/myApp/HaruDanim/IOS/Views/MainView.swift @@ -1286,7 +1286,7 @@ struct ActionButtonCell: View { .monospacedDigit() .foregroundStyle(.white) .contentTransition(.numericText(value: Double(todayCount))) - Text("회") + Text(Format.countUnit(todayCount)) .font((compact ? Font.caption2 : Font.caption).weight(.semibold)) .foregroundStyle(.white.opacity(0.85)) } diff --git a/myApp/HaruDanim/Shared/Formatters.swift b/myApp/HaruDanim/Shared/Formatters.swift index fb62707..89fc9cf 100644 --- a/myApp/HaruDanim/Shared/Formatters.swift +++ b/myApp/HaruDanim/Shared/Formatters.swift @@ -68,6 +68,16 @@ enum Format { comment: "횟수 평균 (소수 1자리)") } + /// 숫자와 분리 렌더링되는 횟수 단위 라벨("회") — 모음 탭 셀처럼 숫자를 크게, 단위를 + /// 작게 따로 그리는 곳 전용. 단독 "회" 키(en "times")는 1일 때 "1 times"가 되므로 + /// 영어 단복수(time/times)를 나눈다. 명시 키를 쓰는 이유: ko 원문이 둘 다 "회"라 + /// 리터럴 키로는 두 변형을 가질 수 없음 + static func countUnit(_ count: Int) -> String { + count == 1 + ? String(localized: "count.unit.one", defaultValue: "회") + : String(localized: "count.unit.other", defaultValue: "회") + } + /// 동명 항목 구분 표시 이름 — 이름이 곧 차트 시리즈 키·범례·Identifiable id가 되는 곳 /// (통계 탭·통계 내보내기·⑤ 행동 통계 위젯)에서 같은 이름의 행동/꼬리표가 한 시리즈로 /// 합쳐지거나 id가 충돌하는 것을 막는다. 첫 항목은 이름 그대로, 이후 중복만