1. 규칙
반복 할일의 달력 칸과 회차 일자는 기한이 아니라 생성일이다. 기한은 생성일과의 간격만 유지한다.
| 상태 | 생성일 | 기한 | 칸 |
|---|---|---|---|
| 이번 주. 생성 9월 24일, 기한 9월 25일, 매주 | 2026-09-24 | 2026-09-25 | 9월 24일 |
| 다음 주 | 2026-10-01 (생성일 +7) | 2026-10-02 (새 생성일 +1일) | 10월 1일 |
| 그다음 주 | 2026-10-08 | 2026-10-09 | 10월 8일 |
| 할일 저장 때 기한이 비어 있음 | 고른 생성일 | 생성일과 같은 날 (간격 0) | 생성일 |
간격 = 기한 날짜 − 생성일 날짜. 기한이 없으면 0이다. 매일이면 +1일, 매주면 +7일, 매달·매년도 같은 방식으로 생성일을 밀고 기한은 새 생성일 + 간격이다.
Todo.repeatAnchor 는 할일의 기준일을 dueDate 로 본다.
rollForwardRepeatTodos 도 기한만 다음 회차로 옮기고 createdAt 은 복사만 한다.
그래서 생성일 9월 24일, 기한 9월 25일, 매주면 칸이 25일에 붙고, 다음 주도 기한만 +7 된다.
2. 기준일을 생성일로
파일 lib/models/todo.dart 의 repeatAnchor 를 바꾼다. 순수 일정은 지금처럼 생성일. 반복 할일도 생성일. 기록은 기한.
/// 반복 기준일. 순수 일정과 반복 할일은 생성일. 기록은 기한.
String? get repeatAnchor {
final created = createdAt;
final createdDay = (created != null && created.length >= 10)
? created.substring(0, 10)
: null;
final pureEvent = onCalendar && !isTodo && !isRecord;
if (pureEvent) return eventDay;
if (isTodo && !isRecord && isRepeating && createdDay != null) {
return createdDay;
}
if (dueDate != null && dueDate!.isNotEmpty) return dueDate;
return createdDay;
}
occurrencesIn 은 이미 repeatAnchor 를 쓴다. 여기를 바꾸면 일반 달력과 반복 보기의 칸이 생성일로 따라간다. 반복 보기 버튼은 앱에 이미 있다. 지우지 않는다.
3. 할일 저장 — 빈 기한은 생성일
파일 lib/widgets/todo_edit_sheet.dart 의 _save. 생성일을 채운 직후, 할일이고 기한이 비어 있으면 생성일 날짜를 기한에 넣는다. 일정은 기한을 비운 채로 둔다. 일정 편집기에 기한 칸을 넣지 않는다.
final createdDay = _dateOnly(t.createdAt);
if (kind == 'todo' &&
(t.dueDate == null || t.dueDate!.isEmpty) &&
createdDay.isNotEmpty) {
t.dueDate = createdDay;
}
if (kind == 'event') t.dueDate = null;
이 대입은 지금 있는 「기한은 생성일보다 빠를 수 없습니다」 검사보다 앞에 둔다. 기한이 생성일과 같으면 그 검사는 통과한다.
할일을 먼저 저장한 뒤 매주만 켜도 기한이 생성일과 같아서, 반복 조회에서 빠지지 않는다. 칸은 생성일이다.
4. 회차 이동 — 생성일과 기한을 함께
파일 lib/db/app_db.dart 의 _rollForwardImpl. 기준은 dueDate 가 아니라 생성일이다. 완료 회차는 그 생성일에 남기고, 다음 행은 생성일 + 주기, 기한은 새 생성일 + 간격이다. 미완료는 오늘 이하 최근 생성일 회차로 생성일과 기한을 함께 옮긴다.
조회 조건에서 due_date IS NOT NULL 을 빼도 된다. 생성일이 있는 반복 할일이 대상이다. 일정 반복은 지금처럼 가상 표시라 이 함수에 넣지 않는다.
String? _day10(String? v) {
if (v == null || v.length < 10) return null;
return v.substring(0, 10);
}
int _gapDays(String createdDay, String? due) {
final d = _day10(due);
if (d == null) return 0;
final a = DateTime.tryParse(createdDay);
final b = DateTime.tryParse(d);
if (a == null || b == null) return 0;
return DateTime(b.year, b.month, b.day)
.difference(DateTime(a.year, a.month, a.day))
.inDays;
}
String _plusDays(DateTime day, int gap) {
final n = DateTime(day.year, day.month, day.day).add(Duration(days: gap));
return _isoDate(n);
}
각 행의 처리:
createdDay = _day10(t.createdAt) ?? t.dueDate. 없으면 건너뛴다.base = DateTime.parse(createdDay),gap = _gapDays(createdDay, t.dueDate).- 완료면
done_date = createdDay,repeat_unit = null,cal_visible = 1. 다음 회차next가 있으면 새 행의created_at = '${_isoDate(next)} 00:00:00',due_date = _plusDays(next, gap). 예전 생성일을 복사하지 않는다. - 미완료면
latestOccurrenceOnOrBefore(base, ...). 결과가base와 같으면 그대로 둔다. 다르면created_at과due_date를 함께 갱신한다. 기한만 고치지 않는다.
그 날짜만 완료(completeRepeatOccurrence)도 회차 날짜를 생성일로 둔다. createdAt 은 원본을 복사하지 않고 '$iso 00:00:00', doneDate 는 iso, dueDate 는 그 날 + 간격이다.
5. 반복 조회
listRepeating 이 due_date IS NOT NULL 만 가져오면, 기한이 비어 있는 반복 할일과 기한이 없는 일정이 빠진다. 조건을 생성일 기준으로 바꾼다.
where:
"user_id = ? AND repeat_unit IS NOT NULL AND repeat_unit != '' "
"AND is_deleted = 0 AND ("
"(NOT (on_calendar = 1 AND is_todo = 0 AND is_record = 0) "
" AND (created_at IS NOT NULL OR due_date IS NOT NULL)) "
"OR (on_calendar = 1 AND is_todo = 0 AND is_record = 0 "
" AND created_at IS NOT NULL))"
달력에 숨김(calVisible = false)인 반복도 반복 보기에서는 회차를 모두 펼친다. 이 동작은 앱에 이미 있다. 유지한다.
6. 그대로 둘 것
- 일정 편집기에는 기한 칸이 없다. 제목, 생성일, 일정 시간, 미리 알림, 반복, 메모.
dueDate = null유지. - 기록 칸은 기한이다. 반복 기준도 기한이다.
- 달력 헤더의 반복 버튼은 유지한다. 기준일만 생성일로 바뀌면 그 보기도 생성일 회차를 보여 준다.
- 「오늘 할일」은 생성일을 오늘로 덮지 않는다. 칸만 오늘로 간다. 그 규칙은
20260923_2023_오늘체크_생성일유지_앱이식.html이다.anchor_created컬럼은 앱에 넣지 않는다. - 웹 목록에서 반복 아이콘을 제목 오른쪽으로 옮긴 것은 웹 화면이다. 앱 작업이 아니다.
created_at 과 due_date 가 함께 밀려 있다.
앱이 할일 반복을 기한에 두면, 같은 항목이 폰에서는 기한 칸에 보인다.
7. 확인
- 9월 24일을 생성일로 할일을 만들고 기한을 비운 채 저장한다. 기한이 9월 24일로 채워지고, 칸은 24일이다.
- 그 할일을 매주로 하고 기한을 9월 25일로 저장한다. 칸은 24일이다. 25일 칸에는 없다.
- 오늘이 10월 3일이면 생성일은 10월 1일, 기한은 10월 2일로 한 칸 옮겨 있다.
- 그 회차를 완료하면 10월 1일에 완료로 남고, 다음 행은 생성일 10월 8일, 기한 10월 9일이다.
- 일정은 기한 칸이 없고, 저장 후에도
dueDate가 비어 있다. 칸은 생성일이다. - 반복 보기에서는 그 달의 생성일 회차(24일, 10월 1일, …)가 달력 표시와 관계없이 보인다.