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:
parent
43be9c0aa9
commit
7baa7a424c
@ -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` 스냅숏 → 이미지 시트
|
||||
|
||||
@ -163,7 +163,7 @@ struct QuestEditorView: View {
|
||||
// 마감 시각: 그날의 집계 창을 [하루 시작, 마감]으로 줄인다 (하루 단위 전용)
|
||||
Toggle("마감 시각까지만 집계", isOn: $hasDeadline.animation())
|
||||
if hasDeadline {
|
||||
RecordTimeWheel(
|
||||
CollapsibleTimeWheel(
|
||||
label: String(localized: "마감 시각"),
|
||||
selection: $deadlineTime,
|
||||
components: .hourAndMinute
|
||||
|
||||
@ -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("메모") {
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user