fix(diary): calendar month drift + export date picks off-by-one at custom day start
'하루 시작 시간'이 자정보다 늦게 설정된 경우의 논리적 하루 경계 버그 2건:
- 달력: monthAnchor(자정 키)를 monthRange(containing:)에 그대로 넣어
매달 1일에 이전 달 그리드가 그려지고 헤더(현재 달)와 불일치했다.
키를 dayRange(forKey:).lowerBound로 실제 시각으로 변환해 전달
(기록·통계 탭이 쓰는 기존 패턴). monthSummary 카운트도 동일 수정.
- 내보내기: '날짜 지정'(MultiDatePicker)의 선택일(자정 Date)을 dayKey(for:)에
넣어 전날 일기가 내보내졌다. DiaryEntry.dayKey가 "달력일의 자정"이므로
선택 달력일의 startOfDay를 키로 직접 사용 — 하루 시작 시간이 몇 시든 안전.
'구간 지정'도 같은 원리로 통일(표시된 달력일 = 내보내는 일기 날짜 보장).
CLAUDE.md §4.1에 재발 방지 규칙 추가: 자정 키를 dayKey(for:)/~Range(containing:)에
재투입 금지, 달력 UI 선택일은 startOfDay를 키로 직접 사용.
검증: 하루 시작 00:00/06:00/18:00 전부 수치 검증(수정 전 06:00에서 이전 달·전날
재현 확인), Debug·Store 빌드 성공, iPad 시뮬레이터에 하루 시작 06:00 주입 후
달력(7월 헤더·그리드 일치, 마커 2건)과 내보내기 시트("일기를 쓴 날 2일") 확인.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013yKDMhuF39GVYy3FMcGHh7
This commit is contained in:
parent
3f518aa91b
commit
05b2d392c8
@ -105,6 +105,7 @@ xcrun xcstringstool sync Widgets/Localizable.xcstrings --stringsdata "${widget_f
|
||||
### 4.1 논리적 하루 (DayMath)
|
||||
- 설정 "하루 시작 시간"(`dayStartMinutes`)·"주 시작 요일"(`weekStartWeekday`, 기본 월요일)이 모든 집계의 기준
|
||||
- `dayKey(for:)`: 시각 → 그 시각이 속한 논리적 하루의 달력일 자정. 경계 이전 시각은 전날 귀속
|
||||
- ⚠️ **자정 Date(=키)를 dayKey(for:)나 ~Range(containing:)에 다시 넣지 말 것** — 하루 시작 시간이 자정보다 늦으면 전날로 밀린다(일기 달력이 매달 1일에 이전 달을 그리고, 내보내기 '날짜 지정'이 전날 일기를 담던 버그). 달력 UI에서 고른 달력일은 `calendar.startOfDay`를 키로 직접 쓰고, 키로 시각 연산이 필요하면 `dayRange(forKey:).lowerBound`로 변환해 넘긴다(기록·통계 탭 패턴)
|
||||
- 세션은 실제 시각 그대로 하나로 저장하고, **집계는 구간 겹침(overlap)으로 자동 분할** — 하루 경계를 걸친 세션이 날짜별로 나뉘어 계산됨. `Aggregator.seconds/count`가 이 규칙의 단일 구현이며 화면·위젯·내보내기 모두 이를 공유
|
||||
|
||||
### 4.2 다짐 진행률 (QuestProgress)
|
||||
|
||||
@ -59,17 +59,21 @@ struct DiaryExportSheet: View {
|
||||
LocalPrefs.orderedActions(allActionsQuery, raw: actionOrderRaw)
|
||||
}
|
||||
|
||||
/// 선택 조건에 해당하고 내용이 있는 일기 (날짜순)
|
||||
/// 선택 조건에 해당하고 내용이 있는 일기 (날짜순).
|
||||
/// 날짜 선택 UI에서 고른 달력일은 곧 일기 키의 달력일이다 — DiaryEntry.dayKey가
|
||||
/// "해당 달력일의 자정"이므로 자정(startOfDay)을 키로 직접 쓴다.
|
||||
/// ⚠️ 자정 Date를 dayKey(for:)에 넣으면 하루 시작 시간이 자정보다 늦을 때
|
||||
/// 전날로 밀려, 고른 날짜의 전날 일기가 내보내지는 버그가 있었다.
|
||||
private var targetEntries: [DiaryEntry] {
|
||||
let keys: Set<Date>
|
||||
switch mode {
|
||||
case .range:
|
||||
let startKey = math.dayKey(for: min(startDate, endDate))
|
||||
let endKey = math.dayKey(for: max(startDate, endDate))
|
||||
let startKey = math.calendar.startOfDay(for: min(startDate, endDate))
|
||||
let endKey = math.calendar.startOfDay(for: max(startDate, endDate))
|
||||
keys = Set(diaryEntries.map(\.dayKey).filter { $0 >= startKey && $0 <= endKey })
|
||||
case .picks:
|
||||
keys = Set(picks.compactMap { components in
|
||||
math.calendar.date(from: components).map { math.dayKey(for: $0) }
|
||||
math.calendar.date(from: components).map { math.calendar.startOfDay(for: $0) }
|
||||
})
|
||||
}
|
||||
return diaryEntries
|
||||
|
||||
@ -243,7 +243,9 @@ struct DiaryRootView: View {
|
||||
}
|
||||
|
||||
private var monthSummary: some View {
|
||||
let range = math.monthRange(containing: monthAnchor)
|
||||
// monthAnchor는 자정 '키'라 monthRange(containing:)에 그대로 넣으면 하루 시작 시간이
|
||||
// 자정보다 늦을 때 전날(= 매달 1일이면 이전 달)로 밀린다 — 키를 실제 시각으로 변환해 전달
|
||||
let range = math.monthRange(containing: math.dayRange(forKey: monthAnchor).lowerBound)
|
||||
let count = entriesByDay.keys.filter { range.contains(math.dayRange(forKey: $0).lowerBound) }.count
|
||||
return HStack(spacing: 6) {
|
||||
Image(systemName: "pencil.and.scribble")
|
||||
@ -419,7 +421,8 @@ private struct DiaryCalendarGrid: View {
|
||||
|
||||
/// 이 달 그리드 셀 (앞쪽 빈칸은 nil)
|
||||
private var cells: [Date?] {
|
||||
let range = math.monthRange(containing: monthAnchor)
|
||||
// monthAnchor(자정 키)를 실제 시각으로 변환해 전달 — monthSummary와 동일한 이유
|
||||
let range = math.monthRange(containing: math.dayRange(forKey: monthAnchor).lowerBound)
|
||||
let keys = math.dayKeys(in: range)
|
||||
guard let first = keys.first else { return [] }
|
||||
let firstWeekday = calendar.component(.weekday, from: first)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user