1. 넣는 법

  1. 아이폰 앱 저장소를 연다. 경로 이름은 day_plan_app 과 같다.
  2. 각 절의 코드를 그 파일에서 찾아 바꾼다. 없는 함수는 같은 클래스 안에 추가한다.
  3. insert / update 가 빈 sync_uid 를 genUuid() 로 채우는 줄은 그대로 둔다.
  4. DemoSeed.clear() 는 demo- 만 지우는 함수로 둔다. 「예시 데이터 넣기」가 이 함수를 쓴다. 통삭제는 clearAllExceptHolidays 다.
  5. 테스트 파일의 package:day_plan_app 만 아이폰 앱 pubspec 의 name 으로 바꾼다.
  6. 핫 리로드로 안 붙는다. 앱을 완전히 다시 실행한다.
파일손대는 것
lib/models/todo.dartrepeatAnchor
lib/logic/recurrence.dart간격, 다음 회차, pendingOccurrence
lib/widgets/todo_edit_sheet.dart_save 빈 기한
lib/db/app_db.dart회차 이동, 완료 행, 조회, childSyncUid, 통삭제
lib/screens/calendar_tab.dart칸 기준, 가상 회차, 시트 높이·화살표
lib/demo/demo_seed.dart자동 삽입 금지, 통삭제
lib/screens/settings_screen.dart지우기 문구
test/repeat_anchor_test.dart매일·매주·매월·매년

2. 반복 할일 규칙

달력 칸과 회차 일자는 생성일이다. 기한은 생성일과의 간격만 유지한다. 간격 = 기한 − 생성일. 기한이 없으면 0이다. 주기는 생성일만 민다.

주기생성 9/24 · 기한 9/25 의 다음 회차
매일생성 9/25, 기한 9/26
매주생성 10/1, 기한 10/2
매월생성 10/24, 기한 10/25
매년생성 2027-09-24, 기한 2027-09-25
예전 repeatAnchor 는 할일 기준을 dueDate 로 봤다. _rollForwardImpl 도 기한만 다음 회차로 옮기고 createdAt 은 복사했다. 그래서 칸이 기한 날에 붙었다.

할일 저장 때 기한이 비어 있으면 생성일과 같은 날(간격 0)로 채운다. 일정은 기한을 비운다. 기록의 칸과 반복 기준은 기한이다.

3. 기준일 — lib/models/todo.dart

기존 repeatAnchor getter 를 이 코드로 바꾼다. eventDay 는 그대로 둔다. occurrencesIn 은 이미 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;
}

4. 간격과 다음 회차 — lib/logic/recurrence.dart

nextOccurrenceAfter 함수가 끝난 뒤, pendingOccurrence 앞에 넣는다. pendingOccurrence 본문은 기한 대신 repeatAnchor 를 쓰게 바꾼다. _iso 와 dOnly 는 이 파일에 이미 있다.

String? _day10(String? v) {
  if (v == null || v.length < 10) return null;
  return v.substring(0, 10);
}

/// 기한 − 생성일 (일). 기한이 없으면 0. 매일·매주·매월·매년 공통.
int repeatDueGapDays(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;
}

/// 회차 생성일 [occ] 와, 원래 간격만큼 뒤인 기한.
({String createdDay, String dueDay}) repeatDatesOn(DateTime occ, int gap) {
  final created = dOnly(occ);
  return (
    createdDay: _iso(created),
    dueDay: _iso(created.add(Duration(days: gap))),
  );
}

/// 다음 회차의 생성일·기한.
/// 주기는 생성일만 민다(매일 +1, 매주 +7, 매월 같은 날, 매년 같은 날).
/// 기한은 새 생성일 + 원래 간격.
({String createdDay, String dueDay})? nextRepeatCreatedDue({
  required String createdDay,
  String? dueDate,
  required String? unit,
  int interval = 1,
  DateTime? until,
}) {
  final base = DateTime.tryParse(createdDay);
  if (base == null) return null;
  final next = nextOccurrenceAfter(base, unit, interval, until: until);
  if (next == null) return null;
  return repeatDatesOn(next, repeatDueGapDays(createdDay, dueDate));
}

DateTime? pendingOccurrence(Todo t, DateTime today) {
  final anchor = t.repeatAnchor;
  if (!t.isRepeating || anchor == null) return null;
  DateTime base;
  try {
    base = DateTime.parse(anchor);
  } catch (_) {
    return null;
  }
  final until = t.repeatUntil == null ? null : DateTime.tryParse(t.repeatUntil!);
  final occ = latestOccurrenceOnOrBefore(
      base, t.repeatUnit, t.repeatInterval, today,
      until: until);
  if (occ == null) return null;
  final ldd = t.lastDoneDate;
  if (ldd != null && ldd.compareTo(_iso(occ)) >= 0) return null;
  return occ;
}

5. 저장 — lib/widgets/todo_edit_sheet.dart _save

생성일을 채운 뒤, 「기한은 생성일보다 빠를 수 없습니다」 검사보다 앞에 둔다. 기한이 생성일과 같으면 검사는 통과한다. 일정 편집기에 기한 칸을 새로 넣지 않는다. _saveByKind 가 일정에서 dueDate = null 을 이미 해도, _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;
if (createdDay.isNotEmpty &&
    t.dueDate != null &&
    t.dueDate!.isNotEmpty &&
    t.dueDate!.compareTo(createdDay) < 0) {
  await showAppAlert(context, '기한은 생성일보다 빠를 수 없습니다.');
  return;
}

6. 회차 이동 — lib/db/app_db.dart

이 파일은 이미 ../logic/recurrence.dart 를 import 한다. _isoDate 옆에 _day10 과 dueGapDays 를 둔다.

static String? _day10(String? v) {
  if (v == null || v.length < 10) return null;
  return v.substring(0, 10);
}

static int dueGapDays(String createdDay, String? due) =>
    repeatDueGapDays(createdDay, due);

6.1 그 날짜만 완료

createdAt 은 원본 복사가 아니다. 회차 날짜 iso 다. dueDate 는 그 날 + 간격이다.

Future<void> completeRepeatOccurrence(
    Todo base, String iso, String todayIso) async {
  final createdDay = AppDb._day10(base.createdAt) ?? base.dueDate ?? iso;
  final gap = repeatDueGapDays(createdDay, base.dueDate);
  final occ = DateTime.tryParse(iso);
  final due = occ == null ? iso : repeatDatesOn(occ, gap).dueDay;
  final rec = Todo(
    content: base.content,
    isDone: true,
    isStar: base.isStar,
    isMyDay: false,
    dueDate: due,
    repeatInterval: 1,
    note: base.note,
    onCalendar: base.onCalendar,
    calVisible: true,
    isRecord: base.isRecord,
    isTodo: base.isTodo,
    doneDate: iso,
    eventTime: base.eventTime,
    createdAt: '$iso 00:00:00',
    syncUid: AppDb.childSyncUid(base.syncUid),
  );
  await insert(rec);
}

6.2 _rollForwardImpl

조회에서 due_date IS NOT NULL 을 뺀다. 완료 다음 행은 예전 created_at 을 복사하지 않는다. 일정 반복은 이 함수에 넣지 않는다.

Future<void> _rollForwardImpl(String todayIso) async {
  final d = await db;
  final u = await currentUser();
  final today = DateTime.tryParse(todayIso);
  if (today == null) return;
  final rows = await d.query('tb_todo',
      where:
          "user_id = ? AND is_todo = 1 AND repeat_unit IS NOT NULL AND repeat_unit != '' "
          "AND is_deleted = 0 AND (created_at IS NOT NULL OR due_date IS NOT NULL)",
      whereArgs: [u]);
  for (final m in rows) {
    final t = Todo.fromMap(m);
    final createdDay = AppDb._day10(t.createdAt) ?? t.dueDate;
    if (createdDay == null || createdDay.isEmpty) continue;
    final base = DateTime.tryParse(createdDay);
    if (base == null) continue;
    final gap = repeatDueGapDays(createdDay, t.dueDate);
    final until =
        t.repeatUntil == null ? null : DateTime.tryParse(t.repeatUntil!);
    if (t.isDone) {
      await d.update(
        'tb_todo',
        {
          'repeat_unit': null,
          'cal_visible': 1,
          'done_date': createdDay,
          'updated_at': nowUtcStamp(),
        },
        where: 'id = ? AND user_id = ?',
        whereArgs: [t.id, u],
      );
      final nxt = nextRepeatCreatedDue(
        createdDay: createdDay,
        dueDate: t.dueDate,
        unit: t.repeatUnit,
        interval: t.repeatInterval,
        until: until,
      );
      if (nxt != null) {
        final map = t.toMap()
          ..remove('id')
          ..['created_at'] = '${nxt.createdDay} 00:00:00'
          ..['due_date'] = nxt.dueDay
          ..['is_done'] = 0
          ..['done_date'] = null
          ..['last_done_date'] = null
          ..['user_id'] = u
          ..['sync_uid'] = AppDb.childSyncUid(t.syncUid)
          ..['updated_at'] = nowUtcStamp()
          ..['is_deleted'] = 0;
        await d.insert('tb_todo', map);
      }
      continue;
    }
    final target = latestOccurrenceOnOrBefore(
        base, t.repeatUnit, t.repeatInterval, today,
        until: until);
    if (target == null) continue;
    final shifted = repeatDatesOn(target, gap);
    if (shifted.createdDay == createdDay) continue;
    await d.update(
      'tb_todo',
      {
        'created_at': '${shifted.createdDay} 00:00:00',
        'due_date': shifted.dueDay,
        'updated_at': nowUtcStamp(),
      },
      where: 'id = ? AND user_id = ?',
      whereArgs: [t.id, u],
    );
  }
}

7. 조회 — lib/db/app_db.dart

7.1 listByDate 의 where

반복 줄은 기한이 아니라 생성일 칸이다. 생성일이 없을 때만 기한으로 폴백한다.

where:
    "user_id = ? AND is_todo = 1 AND is_deleted = 0 AND ("
    "((repeat_unit IS NULL OR repeat_unit = '') AND ("
    "substr(created_at, 1, 10) = ? OR ("
    "(created_at IS NULL OR created_at = '') AND due_date = ?))) OR ("
    "repeat_unit IS NOT NULL AND repeat_unit != '' AND ("
    "substr(created_at, 1, 10) = ? OR ("
    "(created_at IS NULL OR created_at = '') AND due_date = ?))))",
whereArgs: [u, iso, iso, iso, iso],

7.2 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))",
whereArgs: [u],

7.3 doneTodosInRange

완료 범위는 완료일, 없으면 생성일, 없으면 기한 순이다.

where:
    "user_id = ? AND is_done = 1 AND is_todo = 1 "
    "AND (repeat_unit IS NULL OR repeat_unit = '') AND is_deleted = 0 "
    "AND COALESCE(NULLIF(done_date, ''), substr(created_at, 1, 10), due_date) >= ? "
    "AND COALESCE(NULLIF(done_date, ''), substr(created_at, 1, 10), due_date) <= ?",
whereArgs: [u, startIso, endIso],

8. 달력 — lib/screens/calendar_tab.dart

8.1 이미 있는 회차를 건너뛸 때 생성일 칸

void markOcc(Todo t) {
  final created = t.createdAt;
  final home = (created != null && created.length >= 10)
      ? created.substring(0, 10)
      : t.dueDate;
  if (home == null || home.isEmpty) return;
  occTodo.putIfAbsent(t.content, () => <String>{}).add(home);
}

8.2 시트에 넣을 가상 회차

dueDate = 선택일 을 넣지 않는다. 간격을 잃지 않기 위해서다.

for (final t in occ) {
  if (baseIds.contains(t.id)) continue;
  if (occContents.contains(t.content)) continue;
  base.add(t.copy()..isRecurring = true);
}

8.3 완료 토글은 선택일을 넘긴다

onToggleDone: t.isRecurring
    ? () => AppDb.instance.completeRepeatOccurrence(
        t,
        isoOf(_selected),
        isoOf(DateTime.now()))
    : null,

9. 달력 시트 — lib/screens/calendar_tab.dart

날짜칸을 누르면 내용 높이 + 여유 24 + 머리 48. 최소 0.6, 잘릴 때만 0.9. 날짜+건수는 「이미 올라왔는가」로 판정한다. 0.6·0.63·0.9 는 바로 접고, 접혀 있으면 0.9 로 올린다. 화살표는 0.9 이고 목록 아래가 8px 넘게 남을 때만, Expanded 목록의 다음 자식으로 둔다. 글자 위에 겹치지 않는다. 목록 아래 패딩은 8이다.

const double kCalendarSheetRaised = 0.6;
const double kCalendarSheetMax = 0.9;
const double kCalendarSheetHeaderH = 48;
const double kCalendarSheetContentSlack = 24;

double calendarSheetSizeOnDayTap({
  required double contentHeight,
  required double bodyHeight,
}) {
  if (bodyHeight <= 0 || contentHeight <= 0) return kCalendarSheetRaised;
  final needed = contentHeight +
      kCalendarSheetContentSlack +
      kCalendarSheetHeaderH;
  final size = needed / bodyHeight;
  if (size <= kCalendarSheetRaised) return kCalendarSheetRaised;
  if (size >= kCalendarSheetMax) return kCalendarSheetMax;
  return size;
}

double calendarSheetSizeOnCountTap({
  required double current,
  required double collapsed,
}) {
  final raised = current > collapsed + 0.12;
  return raised ? collapsed : kCalendarSheetMax;
}

bool calendarSheetShowsMoreBelow({
  required double sheetSize,
  required double scrollOffset,
  required double maxScrollExtent,
}) {
  final atMax = sheetSize >= kCalendarSheetMax - 0.02;
  if (!atMax) return false;
  return maxScrollExtent - scrollOffset > 8;
}

시트 본문 구조. 목록 스크롤은 _listCtl, 시트 높이는 _sheetCtl 로 나눈다. 둘을 한 컨트롤러로 쓰면 목록을 스크롤할 때 시트 높이가 바뀐다.

Expanded(
  child: ListView(
    controller: _listCtl,
    primary: false,
    padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
    children: [ /* 기존 목록 */ ],
  ),
),
_SheetMoreHint(sheet: _sheetCtl, list: _listCtl),
class _SheetMoreHint extends StatefulWidget {
  const _SheetMoreHint({required this.sheet, required this.list});
  final DraggableScrollableController sheet;
  final ScrollController list;
  @override
  State<_SheetMoreHint> createState() => _SheetMoreHintState();
}

class _SheetMoreHintState extends State<_SheetMoreHint> {
  @override
  void initState() {
    super.initState();
    widget.sheet.addListener(_onChange);
    widget.list.addListener(_onChange);
    WidgetsBinding.instance.addPostFrameCallback((_) => _onChange());
  }

  @override
  void didUpdateWidget(covariant _SheetMoreHint oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.sheet != widget.sheet) {
      oldWidget.sheet.removeListener(_onChange);
      widget.sheet.addListener(_onChange);
    }
    if (oldWidget.list != widget.list) {
      oldWidget.list.removeListener(_onChange);
      widget.list.addListener(_onChange);
    }
  }

  @override
  void dispose() {
    widget.sheet.removeListener(_onChange);
    widget.list.removeListener(_onChange);
    super.dispose();
  }

  void _onChange() {
    if (mounted) setState(() {});
  }

  bool get _visible {
    if (!widget.sheet.isAttached || !widget.list.hasClients) return false;
    final pos = widget.list.position;
    if (!pos.hasContentDimensions) return false;
    return calendarSheetShowsMoreBelow(
      sheetSize: widget.sheet.size,
      scrollOffset: pos.pixels,
      maxScrollExtent: pos.maxScrollExtent,
    );
  }

  @override
  Widget build(BuildContext context) {
    if (!_visible) return const SizedBox.shrink();
    return const ColoredBox(
      color: Color(0xFFF7F9FC),
      child: SizedBox(
        width: double.infinity,
        child: Padding(
          padding: EdgeInsets.only(bottom: 4),
          child: Icon(
            Icons.keyboard_arrow_down,
            size: 22,
            color: Color(0xFF3F6FB0),
          ),
        ),
      ),
    );
  }
}

날짜+건수 탭은 calendarSheetSizeOnCountTap 을 호출한다. maxChildSize 와 드래그 상한은 kCalendarSheetMax(0.9)다.

10. 예시 데이터 지우기

버튼은 공휴일 이름·메모와 구글 iCal 주소만 남기고, 현재 사용자의 할일·일정·기록과 기록 종류를 하드 삭제한다. 직접 만든 항목도 지운다. 지운 뒤 demo_skip_autoload=1 이라 할일이 0이어도 다음 실행에 예시가 다시 들어가지 않는다. 「예시 데이터 넣기」 끝에서 그 값을 0 으로 푼다.

10.1 lib/db/app_db.dart — 갈라진 회차도 demo-

클래스 안, localUser 옆에 둔다. genUuid 는 ../util/sync_meta.dart 에 있다.

static const demoUidPrefix = 'demo-';

static String childSyncUid(String? parentUid) {
  if (parentUid != null && parentUid.startsWith(demoUidPrefix)) {
    return '$demoUidPrefix${genUuid()}';
  }
  return genUuid();
}

새 줄의 sync_uid 는 genUuid() 가 아니라 AppDb.childSyncUid(...) 다. 세 곳만 바꾼다.

Future<int> deleteAllExceptHolidays() async {
  final d = await db;
  final u = await currentUser();
  var n = 0;
  n += await d.delete('tb_todo', where: 'user_id = ?', whereArgs: [u]);
  n += await d.delete('tb_record_template');
  return n;
}

10.2 lib/demo/demo_seed.dart

파일 위에 import '../services/notify.dart'; 를 추가한다. clear() 는 sync_uid LIKE 'demo-%' 만 지우는 기존 함수로 둔다.

static const _skipAutoloadKey = 'demo_skip_autoload';

static Future<void> installIfNoTodos() async {
  final db = AppDb.instance;
  if (await db.getSetting(_skipAutoloadKey) == '1') {
    await _fixCreatedAfterDue();
    return;
  }
  // 이하 기존: 할일 0건이면 install(), 이어서 _fixCreatedAfterDue()
}

static Future<void> clearAllExceptHolidays() async {
  final db = AppDb.instance;
  await db.deleteAllExceptHolidays();
  await db.setSetting(_skipAutoloadKey, '1');
  await NotifyService.instance.syncFromDb();
  bumpData();
}

install() 은 bumpData() 직전에 자동 넣기를 다시 켠다.

await db.setSetting(_skipAutoloadKey, '0');
bumpData();

10.3 lib/screens/settings_screen.dart

Future<void> _clearDemo() async {
  final ok = await showAppConfirm(
    context,
    '예시 데이터를 지울까요?\n공휴일은 남고, 할일·일정·기록은 모두 지워집니다.',
  );
  if (!ok) return;
  await DemoSeed.clearAllExceptHolidays();
  if (!mounted) return;
  await showAppAlert(context, '예시 데이터를 지웠습니다. 공휴일만 남습니다.');
}

안내 문장과 버튼 글자:

const Text(
  '할일·일정·기록을 오늘 날짜 기준으로 넣습니다. '
  '「예시 데이터 지우기」는 공휴일은 남기고 할일·일정·기록을 모두 지웁니다.',
);
// 버튼 label
const Text('예시 데이터 지우기')
로컬 하드 삭제다. 로그인 기기에서 서버에 같은 항목이 있으면 다음 동기화 때 다시 내려올 수 있다. 제목으로 지우지 않는다.

11. 그대로 둘 것

12. 테스트 — test/repeat_anchor_test.dart

아이폰 앱이면 import 의 패키지 이름만 pubspec.yaml 의 name 으로 바꾼다.

import 'package:day_plan_app/db/app_db.dart';
import 'package:day_plan_app/logic/recurrence.dart';
import 'package:day_plan_app/models/todo.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('반복 할일 기준일은 생성일 — 매일·매주·매월·매년', () {
    for (final unit in ['day', 'week', 'month', 'year']) {
      final t = Todo(
        isTodo: true,
        createdAt: '2026-09-24 00:00:00',
        dueDate: '2026-09-25',
        repeatUnit: unit,
      );
      expect(t.repeatAnchor, '2026-09-24', reason: unit);
    }
    expect(
      Todo(
        isTodo: false,
        onCalendar: true,
        createdAt: '2026-09-24 00:00:00',
        repeatUnit: 'month',
      ).repeatAnchor,
      '2026-09-24',
    );
    expect(
      Todo(
        isTodo: false,
        isRecord: true,
        createdAt: '2026-09-20 00:00:00',
        dueDate: '2026-09-24',
        repeatUnit: 'year',
      ).repeatAnchor,
      '2026-09-24',
    );
  });

  test('다음 회차: 생성일은 주기만큼, 기한은 간격 유지', () {
    const created = '2026-09-24';
    const due = '2026-09-25';
    expect(
      nextRepeatCreatedDue(createdDay: created, dueDate: due, unit: 'day'),
      (createdDay: '2026-09-25', dueDay: '2026-09-26'),
    );
    expect(
      nextRepeatCreatedDue(createdDay: created, dueDate: due, unit: 'week'),
      (createdDay: '2026-10-01', dueDay: '2026-10-02'),
    );
    expect(
      nextRepeatCreatedDue(createdDay: created, dueDate: due, unit: 'month'),
      (createdDay: '2026-10-24', dueDay: '2026-10-25'),
    );
    expect(
      nextRepeatCreatedDue(createdDay: created, dueDate: due, unit: 'year'),
      (createdDay: '2027-09-24', dueDay: '2027-09-25'),
    );
  });

  test('기한이 비면 간격 0, 있으면 일수 차이', () {
    expect(AppDb.dueGapDays('2026-09-24', null), 0);
    expect(AppDb.dueGapDays('2026-09-24', ''), 0);
    expect(AppDb.dueGapDays('2026-09-24', '2026-09-25'), 1);
    expect(AppDb.dueGapDays('2026-09-24', '2026-10-01'), 7);
  });
}

13. 확인

  1. 생성 9/24 할일, 기한 비움 → 저장 후 기한 9/24, 칸은 24일.
  2. 그 할일을 매주로 하고 기한을 9/25 로 저장 → 칸은 24일. 25일 칸에는 없다.
  3. 오늘이 10월 3일이면 생성일은 10월 1일, 기한은 10월 2일로 옮겨 있다.
  4. 그 회차를 완료하면 10월 1일에 완료로 남고, 다음 행은 생성 10월 8일, 기한 10월 9일.
  5. 매일·매월·매년도 생성일만 주기만큼, 기한은 같은 간격.
  6. 일정은 기한 칸이 없고, 저장 후에도 dueDate 가 비어 있다. 칸은 생성일.
  7. 반복 보기에서 그 달의 생성일 회차(24일, 10월 1일, …)가 달력 표시와 관계없이 보인다.
  8. 올라온 시트에서 날짜+건수 → 바로 접힘. 접힌 상태에서 다시 누르면 0.9.
  9. 시트 0.9 이고 아래에 항목이 남으면 목록 밑에 파란 아래 화살표. 글자와 겹치지 않는다.
  10. 예시 데이터 지우기 → 공휴일만 남음. 앱을 다시 실행해도 예시가 자동으로 들어가지 않음. 넣기를 누르면 다시 들어감.