feat(ui): unify all time inputs as collapsible value rows

- 상시 노출 휠이 화면을 차지해 어수선하다는 실기기 피드백 → 설정 '하루 시작 시간'의 값 행+탭 펼침 패턴을 전 시각 입력으로 통일 (사용자 지시)
- 공용 CollapsibleTimeWheel 신설(값 행: 라벨+현재 값+셰브론 → 탭하면 인라인 휠 펼침, 기본 접힘) — 기록 수정·추가 4개 시트(시작/종료/기록 시각), 다짐 '마감 시각', 설정 '하루 시작 시간'(기존 개별 구현을 공용으로 교체) 총 6곳 적용
- 값 표시는 로케일 날짜 포맷(날짜+시각 또는 시·분) — 새 문자열 없음. 접힘/펼침은 표시 상태일 뿐 값 반영 경로는 검증된 인라인 휠 그대로(팝오버 금지·범위 제약 금지·분 절사 불변), 종료<시작 경고·저장 비활성도 유지
- 검증: 세션 수정(값 행 2개)·시간 기록 추가·다짐 마감 시각·설정 펼침 상태 시뮬 스크린샷, Debug/Store 빌드 성공, 카탈로그 9종 클린. CLAUDE.md §6.5 갱신

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-19 09:10:29 +09:00
parent 43be9c0aa9
commit 7baa7a424c
4 changed files with 71 additions and 43 deletions

View File

@ -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` 스냅숏 → 이미지 시트

View File

@ -163,7 +163,7 @@ struct QuestEditorView: View {
// : [ , ] ( )
Toggle("마감 시각까지만 집계", isOn: $hasDeadline.animation())
if hasDeadline {
RecordTimeWheel(
CollapsibleTimeWheel(
label: String(localized: "마감 시각"),
selection: $deadlineTime,
components: .hourAndMinute

View File

@ -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<Date>,
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("메모") {

View File

@ -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: {