1. 넣는 법
- 아이폰 앱 저장소를 연다. 경로 이름은
day_plan_app과 같다. - 각 절의 코드를 그 파일에서 찾아 바꾼다. 없는 함수는 같은 클래스 안에 추가한다.
insert/update가 빈sync_uid를genUuid()로 채우는 줄은 그대로 둔다.DemoSeed.clear()는demo-만 지우는 함수로 둔다. 「예시 데이터 넣기」가 이 함수를 쓴다. 통삭제는clearAllExceptHolidays다.- 테스트 파일의
package:day_plan_app만 아이폰 앱pubspec의 name 으로 바꾼다. - 핫 리로드로 안 붙는다. 앱을 완전히 다시 실행한다.
| 파일 | 손대는 것 |
|---|---|
lib/models/todo.dart | repeatAnchor |
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(...) 다. 세 곳만 바꾼다.
detachPastRepeatEvent의 새 행:AppDb.childSyncUid(base.syncUid)completeRepeatOccurrence: 위 6.1_rollForwardImpl의 다음 회차: 위 6.2
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. 그대로 둘 것
- 일정 편집기 기한 칸 없음. 저장 후
dueDate는 null. - 기록 칸과 기록 반복 기준은 기한.
- 달력 헤더의 반복 보기 버튼. 기준일만 바꾸면 그 보기도 생성일 회차를 보여 준다.
calVisible = false인 반복도 반복 보기에서는 펼친다. - 「오늘 할일」이 생성일을 오늘로 덮지 않는 규칙.
anchor_created컬럼은 넣지 않는다. - 웹 목록의 반복 아이콘 위치. 앱 작업이 아니다.
insert/update의 빈sync_uid→genUuid().
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. 확인
- 생성 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일, …)가 달력 표시와 관계없이 보인다.
- 올라온 시트에서 날짜+건수 → 바로 접힘. 접힌 상태에서 다시 누르면 0.9.
- 시트 0.9 이고 아래에 항목이 남으면 목록 밑에 파란 아래 화살표. 글자와 겹치지 않는다.
- 예시 데이터 지우기 → 공휴일만 남음. 앱을 다시 실행해도 예시가 자동으로 들어가지 않음. 넣기를 누르면 다시 들어감.