diff --git a/myApp/HaruDanim/CLAUDE.md b/myApp/HaruDanim/CLAUDE.md index f3f04fe..c4c020f 100644 --- a/myApp/HaruDanim/CLAUDE.md +++ b/myApp/HaruDanim/CLAUDE.md @@ -201,7 +201,7 @@ 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분으로 표시되는 어긋남 방지. 다짐 '마감 시각'·설정 '하루 시작 시간'도 같은 RecordTimeWheel(components: .hourAndMinute)로 통일 — 앱의 시각 입력 다이얼은 전부 인라인 휠(팝오버 금지). 설정 '하루 시작 시간'은 상시 노출이 거슬린다는 실기기 피드백으로 **평소엔 값 행(라벨+현재 시각+셰브론), 탭하면 휠 펼침** — 마감 시각·기록 편집기는 일시적 시트라 상시 노출 유지 +- 목록: 시간 기록(시작~종료·총 시간·메모)·횟수 기록(시각·+n·메모), 탭하면 수정 시트. **수정·추가 시트의 시각 입력은 상시 노출 휠 다이얼**(`RecordEditors.swift` RecordTimeWheel) — 컴팩트 팝오버는 다이얼 감속 중 닫히면 정착값이 유실되는 실기기 보고가 있어 폐기. `in:` 범위 제약도 하한 근처 선택을 소리 없이 되감아 금지(검증은 종료<시작 경고 문구+저장 비활성). 편집 상태는 분 단위 절사(flooredToMinute) — 실측정 기록의 초 찌꺼기(14:32:47)가 남으면 다이얼로 맞춘 1시간이 59분으로 표시되는 어긋남 방지. **앱의 모든 시각 입력은 `CollapsibleTimeWheel`**(RecordEditors.swift) — 평소엔 값 행(라벨+현재 값+셰브론), 탭하면 인라인 휠(RecordTimeWheel) 펼침. 상시 노출 휠이 어수선하다는 실기기 피드백으로 기록 편집 4시트·다짐 '마감 시각'·설정 '하루 시작 시간' 전부 통일(기본 접힘). 팝오버 금지 이유(감속 중 닫힘 시 정착값 유실)와 범위 제약 금지·분 절사 규칙은 그대로 - 타임테이블: 24시간 축(하루 시작 시간 기준), 하루/일주일 전환(주간 보기에서 화살표는 7일씩 점프). 시간=색 블록, 횟수=색 점. 블록/점 탭=수정 - 필터: `RecordFilterSheet`(공용) — 꼬리표 아래 행동이 그룹된 트리, **"제외한 행동 ID" 집합**으로 관리해 기본이 전체 선택. 활성 시 초록 칩 표시 - 내보내기: 현재 날짜/모드/필터 그대로 `ExportBuilder.history` 스냅숏 → 이미지 시트 diff --git a/myApp/HaruDanim/IOS/Views/QuestEditorView.swift b/myApp/HaruDanim/IOS/Views/QuestEditorView.swift index 9e166d2..53f263f 100644 --- a/myApp/HaruDanim/IOS/Views/QuestEditorView.swift +++ b/myApp/HaruDanim/IOS/Views/QuestEditorView.swift @@ -163,7 +163,7 @@ struct QuestEditorView: View { // 마감 시각: 그날의 집계 창을 [하루 시작, 마감]으로 줄인다 (하루 단위 전용) Toggle("마감 시각까지만 집계", isOn: $hasDeadline.animation()) if hasDeadline { - RecordTimeWheel( + CollapsibleTimeWheel( label: String(localized: "마감 시각"), selection: $deadlineTime, components: .hourAndMinute diff --git a/myApp/HaruDanim/IOS/Views/RecordEditors.swift b/myApp/HaruDanim/IOS/Views/RecordEditors.swift index fb44f28..cc32331 100644 --- a/myApp/HaruDanim/IOS/Views/RecordEditors.swift +++ b/myApp/HaruDanim/IOS/Views/RecordEditors.swift @@ -21,7 +21,6 @@ private func flooredToMinute(_ date: Date) -> Date { /// 반영되지 않는 경우가 있어(사용자 실기기 보고), 다이얼 자체가 곧 표시가 /// 되도록 휠을 인라인으로 둔다. 범위 제약(in:)은 하한 근처 선택을 소리 없이 /// 되감아 버리므로 쓰지 않고, 검증은 호출부의 경고·저장 비활성으로 한다. -/// 기록 편집 외에 다짐 '마감 시각'·설정 '하루 시작 시간'(시·분 전용)도 공용. struct RecordTimeWheel: View { let label: String @Binding var selection: Date @@ -36,6 +35,62 @@ struct RecordTimeWheel: View { } } +/// 시각 입력의 표준 컨트롤: 평소엔 값 행(라벨+현재 값+셰브론)만 보이고 +/// 탭하면 인라인 휠(RecordTimeWheel)이 펼쳐진다 — 상시 노출 휠이 화면을 +/// 계속 차지해 어수선하다는 실기기 피드백으로 전 시각 입력을 이 패턴으로 통일. +/// 접힘/펼침은 표시 상태일 뿐 값 반영 경로는 인라인 휠 그대로 (§6.5). +struct CollapsibleTimeWheel: View { + let label: String + @Binding var selection: Date + var components: DatePickerComponents = [.date, .hourAndMinute] + @State private var isExpanded: Bool + + init(label: String, selection: Binding, + components: DatePickerComponents = [.date, .hourAndMinute], + initiallyExpanded: Bool = false) { + self.label = label + self._selection = selection + self.components = components + self._isExpanded = State(initialValue: initiallyExpanded) + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Button { + withAnimation(.snappy(duration: 0.25)) { + isExpanded.toggle() + } + } label: { + HStack { + Text(label) + .foregroundStyle(.primary) + Spacer() + Text(selection, format: valueFormat) + .foregroundStyle(isExpanded ? AnyShapeStyle(AppTheme.green) : AnyShapeStyle(.secondary)) + Image(systemName: "chevron.down") + .font(.caption2.weight(.semibold)) + .foregroundStyle(.tertiary) + .rotationEffect(.degrees(isExpanded ? 180 : 0)) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(Text(label)) + .accessibilityValue(Text(selection, format: valueFormat)) + if isExpanded { + RecordTimeWheel(label: label, selection: $selection, components: components) + } + } + } + + /// 시·분 전용이면 시각만, 날짜 포함이면 날짜(요일)+시각으로 현재 값 표시 + private var valueFormat: Date.FormatStyle { + components == .hourAndMinute + ? Date.FormatStyle().hour().minute() + : Date.FormatStyle().month().day().weekday(.abbreviated).hour().minute() + } +} + // MARK: - 행동별 기록 관리 시트 (롱프레스 메뉴에서 진입) struct ActionRecordsSheet: View { @@ -218,12 +273,12 @@ struct SessionEditorView: View { NavigationStack { Form { Section("시작") { - RecordTimeWheel(label: String(localized: "시작 시각"), selection: $startAt) + CollapsibleTimeWheel(label: String(localized: "시작 시각"), selection: $startAt) } Section("종료") { Toggle("종료됨", isOn: $isFinished.animation()) if isFinished { - RecordTimeWheel(label: String(localized: "종료 시각"), selection: $endAt) + CollapsibleTimeWheel(label: String(localized: "종료 시각"), selection: $endAt) if endAt < startAt { Text("종료 시각이 시작 시각보다 빨라요.") .font(.caption) @@ -296,10 +351,10 @@ struct SessionAddView: View { NavigationStack { Form { Section("시작") { - RecordTimeWheel(label: String(localized: "시작 시각"), selection: $startAt) + CollapsibleTimeWheel(label: String(localized: "시작 시각"), selection: $startAt) } Section("종료") { - RecordTimeWheel(label: String(localized: "종료 시각"), selection: $endAt) + CollapsibleTimeWheel(label: String(localized: "종료 시각"), selection: $endAt) if endAt < startAt { Text("종료 시각이 시작 시각보다 빨라요.") .font(.caption) @@ -347,7 +402,7 @@ struct CountEntryEditorView: View { NavigationStack { Form { Section("기록 시각") { - RecordTimeWheel(label: String(localized: "기록 시각"), selection: $timestamp) + CollapsibleTimeWheel(label: String(localized: "기록 시각"), selection: $timestamp) } Stepper(value: $amount, in: 1...9999) { HStack { @@ -422,7 +477,7 @@ struct CountAddView: View { } .pickerStyle(.segmented) if !useNow { - RecordTimeWheel(label: String(localized: "기록 시각"), selection: $timestamp) + CollapsibleTimeWheel(label: String(localized: "기록 시각"), selection: $timestamp) } } Section("메모") { diff --git a/myApp/HaruDanim/IOS/Views/SettingsView.swift b/myApp/HaruDanim/IOS/Views/SettingsView.swift index 11bdbbf..a2438d4 100644 --- a/myApp/HaruDanim/IOS/Views/SettingsView.swift +++ b/myApp/HaruDanim/IOS/Views/SettingsView.swift @@ -155,40 +155,13 @@ struct SettingsView: View { Text("\(Format.weekdayShort(weekday))요일").tag(weekday) } } - // 평소엔 현재 값만 보이는 행, 탭하면 인라인 휠 펼침 — - // 상시 노출 휠이 설정 탭을 계속 차지한다는 실기기 피드백 반영. - // 팝오버로 되돌리지 않는 이유: 팝오버 다이얼은 감속 중 닫히면 - // 정착값이 유실되는 문제가 있어 휠 인라인 유지가 안전 (§6.5) - VStack(alignment: .leading, spacing: 0) { - Button { - withAnimation(.snappy(duration: 0.25)) { - showsDayStartWheel.toggle() - } - } label: { - HStack { - Text("하루 시작 시간") - .foregroundStyle(.primary) - Spacer() - Text(dayStartDate.wrappedValue, format: .dateTime.hour().minute()) - .foregroundStyle(showsDayStartWheel ? AppTheme.green : .secondary) - Image(systemName: "chevron.down") - .font(.caption2.weight(.semibold)) - .foregroundStyle(.tertiary) - .rotationEffect(.degrees(showsDayStartWheel ? 180 : 0)) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - .accessibilityLabel(Text("하루 시작 시간")) - .accessibilityValue(Text(dayStartDate.wrappedValue, format: .dateTime.hour().minute())) - if showsDayStartWheel { - RecordTimeWheel( - label: String(localized: "하루 시작 시간"), - selection: dayStartDate, - components: .hourAndMinute - ) - } - } + // 값 행 + 탭 펼침 — 전 시각 입력 공통 컨트롤 (CollapsibleTimeWheel, §6.5) + CollapsibleTimeWheel( + label: String(localized: "하루 시작 시간"), + selection: dayStartDate, + components: .hourAndMinute, + initiallyExpanded: showsDayStartWheel + ) } header: { Text("날짜 기준") } footer: {