회차 메모 · 회차 날짜 · 날짜칸 — 앱 반영

2026-09-24 11:05 (KST). 대상은 auto_schedule_app. 아래 코드는 반영 직후 저장소 상태다. 웹 코드와 아이폰 day_plan_appl_app 은 수정하지 않았다.

1. 요약

작업내용
반복 회차 메모 처음 메모는 모든 회차. 이후 수정은 그 회차만 occ_notes
다른 회차 편집 화면 생성일·기한을 그 회차 날로 보여 주고, 저장 시 원본 생성일은 유지. 명세 20260924_1028_회차날짜_상단오늘_앱이식.html
날짜에 연도 2026년 9월 24일 (목)
편집·탭 상단 오늘 할일·일정·기록 이름 옆에 오늘 아이콘과 오늘 날짜. 달력 탭에는 없음
날짜칸 공휴일 날짜 숫자 오른쪽. 칸을 넘기면 …
날짜칸 높이 기록·일정·할일이 겹쳐 아래 칸을 침범하면 그 달 행을 키움

2. 반복 회차 메모는 개별 저장

반복 할일·일정은 DB에 지금 회차 행 하나다. 다른 칸은 그 행을 펼친 복사본이라, 메모를 고치면 모든 회차가 같이 바뀌었다.

규칙:

2.1 모델 — lib/models/todo.dart

/// 회차별 메모. JSON {"yyyy-MM-dd":"메모"}. 키가 있으면 그 회차만 이 값.
/// 빈 문자열은 그 회차 메모 없음(공통 메모로 되돌리지 않음).
String? occNotes;

/// 이 객체가 가리키는 반복 회차 날짜. 전개 복사본에만 채운다.
String? occIso;

/// 그 회차에 보이는 메모. 개별 값이 있으면 그것, 없으면 만들 때 넣은 공통 메모.
String? noteFor(String day) { ... }

/// 이 회차만 메모를 덮어쓴다. 공통 [note] 는 바꾸지 않는다.
void setOccNote(String day, String text) { ... }

DB 컬럼 occ_notes TEXT (스키마 버전 12). 달력 전개 복사본에 occIso 를 붙인다.

2.2 저장 — lib/widgets/todo_edit_sheet.dart

void _writeNote() {
  final text = _note.text.trim();
  final wasRepeating = widget.todo?.isRepeating ?? false;
  if (isEdit && wasRepeating && t.isRepeating) {
    final day = t.memoDay;
    if (day != null && day.isNotEmpty) {
      t.setOccNote(day, text);
      return;
    }
  }
  t.note = text.isEmpty ? null : text;
  if (!t.isRepeating) t.occNotes = null;
}

3. 다른 회차를 열면 그 날로 계산

10월 1일 칸은 원본 9월 24일 행을 펼친 것이다. 편집 화면이 원본 날짜를 그대로 읽으면 생성일·기한이 9월 24일로 남고, 연도가 다른 두 날이 9월 24일 로만 보여 D-365 처럼 보인다.

연 회차화면 생성일화면 기한저장되는 원본
이번 회차 9월 24일 2026-09-24 2027-09-24 그대로
다음 회차 10월 1일 2026-10-01 2027-10-01 생성일 9월 24일 유지. 기한을 고치면 간격만 반영

일정은 기한이 없다. 다른 회차의 생성일만 그 날로 보여 주고 저장 시 기한은 비운다. 기록은 이 계산에 넣지 않는다.

3.1 계산 — lib/logic/recurrence.dart

({String createdDay, String dueDay}) occurrenceCreatedDue({
  required String seriesCreated,
  required String? seriesDue,
  required String occDay,
}) {
  final gap = repeatDueGapDays(seriesCreated, seriesDue);
  final occ = DateTime.tryParse(occDay);
  if (occ == null) {
    return (createdDay: occDay, dueDay: seriesDue ?? occDay);
  }
  return repeatDatesOn(occ, gap);
}

3.2 편집기 — 화면만 바꾸고 저장 때 되돌림

String? _seriesCreatedAt;
String? _seriesDueDate;

void _showOccurrenceDates() {
  final shifted = _occurrenceDates();
  if (shifted == null) return;
  t.createdAt = _createdStamp(shifted.createdDay);
  if (kind == 'event') {
    t.dueDate = null;
    return;
  }
  t.dueDate = shifted.dueDay;
}

void _foldOccurrenceDatesBack() {
  final series = _dateOnly(_seriesCreatedAt);
  final shifted = _occurrenceDates();
  if (shifted == null || series.isEmpty) return;
  final shownCreated = _dateOnly(t.createdAt);
  final gap = shownCreated.isEmpty
      ? repeatDueGapDays(series, _seriesDueDate)
      : repeatDueGapDays(shownCreated, t.dueDate);
  t.createdAt = _seriesCreatedAt;
  if (kind == 'event') {
    t.dueDate = null;
    return;
  }
  final base = DateTime.tryParse(series);
  if (base == null || t.dueDate == null || t.dueDate!.isEmpty) {
    t.dueDate = _seriesDueDate;
    return;
  }
  t.dueDate = repeatDatesOn(base, gap).dueDay;
}

initState 수정 분기에서 원본을 기억한 뒤 화면 날짜를 바꾼다. _save 에서는 메모를 쓴 직후 원본으로 되돌린다.

} else {
  _seriesCreatedAt = t.createdAt;
  _seriesDueDate = t.dueDate;
  _showOccurrenceDates();
  final created = _dateOnly(t.createdAt);
  _anchorCreatedIso = created.isNotEmpty ? created : t.dueDate;
}

_writeNote();
_foldOccurrenceDatesBack();

3.3 일자에 연도

String _fmtDue(String iso) {
  final d = DateTime.tryParse(iso);
  if (d == null) return iso;
  return '${d.year}년 ${d.month}월 ${d.day}일 (${_wd[d.weekday]})';
}

생성일, 기한, 반복 기한, 완료일, 기록 날짜가 이 함수를 쓴다.

5. 날짜칸 — 공휴일은 숫자 오른쪽

스샷의 24일 칸은 날짜 아래 줄에 추석 전날 이 있고, 그 아래 기록·일정·할일이 겹쳐 다음 주 1일 칸까지 글자가 내려갔다.

공휴일 이름은 날짜 숫자 오른쪽에 둔다. 칸 폭(완료 점 여백 포함)을 넘기면 TextOverflow.ellipsis 로 추석 전… 처럼 자른다. 아래 칸으로 넘기지 않는다.

return Container(
  width: double.infinity,
  clipBehavior: Clip.hardEdge,
  child: Stack(
    clipBehavior: Clip.hardEdge,
    children: [
      Column(children: [
        Padding(
          padding: EdgeInsets.fromLTRB(2, 3, showDone ? 12 : 2, 0),
          child: Row(children: [
            Container( /* 날짜 숫자 24×24 */ ),
            if (showHoli) ...[
              const SizedBox(width: 2),
              Expanded(
                child: Text(
                  holi!,
                  maxLines: 1,
                  softWrap: false,
                  overflow: TextOverflow.ellipsis,
                  style: const TextStyle(
                    fontSize: 12, color: Color(0xFFD94C4C)),
                ),
              ),
            ],
          ]),
        ),
        // 기록 → 일정 → 할일 → 구글 점
      ]),
      if (showDone)
        Positioned(top: 4, right: 4, child: _dot(Colors.black)),
    ],
  ),
);

clipBehavior: Clip.hardEdge 는 행 높이 추정이 1px 짧아도 아래 칸으로 그려지지 않게 하는 안전장치다.

6. 내용이 많으면 행 높이를 키운다

공휴일을 숫자 옆으로 옮겨도, 기록·일정·할일이 한 칸에 겹치면 고정 fitRow 보다 내용이 길다. 잘라 숨기지 않고 TableCalendar.rowHeight 를 필요한 높이로 올린다. 표는 행마다 같은 높이라, 그 달에서 가장 내용이 많은 칸을 기준으로 모든 행이 커진다.

double _titleLineH(List<String>? items) {
  if (items == null || items.isEmpty) return 0;
  return items.length <= 1 ? 16 : 18;
}

double _neededCellHeight(DateTime day, {required bool outside}) {
  const dateRow = 27.0; // 위 패딩 3 + 숫자 24
  const bottom = 4.0;
  if (outside) return dateRow + bottom;
  final iso = isoOf(day);
  var h = dateRow;
  // 기록 / 일정 / 할일 각 한 줄 + 구글 점 6
  return h + bottom;
}

double _maxNeededRowHeight() {
  var maxH = 31.0;
  final last = DateTime(_focused.year, _focused.month + 1, 0).day;
  for (var d = 1; d <= last; d++) {
    final need = _neededCellHeight(
      DateTime(_focused.year, _focused.month, d),
      outside: false,
    );
    if (need > maxH) maxH = need;
  }
  return maxH;
}
var row = fitRow;
if (!row.isFinite || row <= 0) row = 80.0;
final need = _maxNeededRowHeight();
if (need > row) row = need;
// TableCalendar(rowHeight: row)
return row > fitRow
    ? SingleChildScrollView(child: cal)
    : cal;

row > fitRow 이면 달력 영역만 세로 스크롤한다. 예전 주석 「fitRow 로 채우되 내용 많으면 그 이상」이 이제 실제로 동작한다.

7. 손본 파일

파일내용
lib/models/todo.dart occNotes, occIso, noteFor, setOccNote
lib/db/app_db.dart occ_notes 컬럼(버전 12), 완료 회차에 그 날 메모
lib/logic/recurrence.dart occurrenceCreatedDue, 전개 복사본 occIso
lib/widgets/todo_edit_sheet.dart 회차 메모, 회차 날짜 접기, 연도, _kindHead
lib/screens/home_screen.dart 탭 제목 옆 오늘 아이콘·연도
lib/screens/calendar_tab.dart 공휴일 위치, 말줄임, 행 높이
test/occ_note_test.dart 공통 메모 vs 회차 메모
test/repeat_anchor_test.dart 다른 회차 생성일·기한 간격

확인

  1. 매주 할일을 만들고 한 회차만 메모를 바꾼다. 다른 주는 처음 메모가 남는다.
  2. 생성 2026-09-24, 기한 2027-09-24, 매주. 10월 1일 회차를 열면 생성 2026년 10월 1일, 기한 2027년 10월 1일. 저장해도 원본 생성일은 9월 24일이다.
  3. 할일·일정·기록 편집 상단에 오늘 아이콘과 오늘 날짜가 있다.
  4. 하단 할일·일정·기록 탭에만 오늘 아이콘이 있다. 달력 탭에는 없다.
  5. 9월 24일 칸: 24 옆에 추석 전날. 길면 …. 기록·일정·할일이 있어도 1일 칸으로 글자가 내려가지 않고, 그 주 행이 높아진다.

핫 리로드 대신 앱을 완전히 다시 실행한다.