1. 왜 웹을 고쳐야 하는가
HTTP 경로와 JSON 봉투는 그대로다. 컬럼을 새로 만든 것도 아니다. 바뀐 것은 같은 컬럼의 의미다.
| 지금 웹·서버 | 지금 앱 | |
|---|---|---|
| 할일 달력 칸 | due_date |
created_at 앞 10자 |
| 일정 달력 칸 | due_date |
created_at 앞 10자. 기한 없음 |
| 기록 달력 칸 | due_date |
due_date (그대로) |
| 오늘 목록 | 기한 ≤ 오늘 + 나의하루 + 별표 미래 | 기한 ≤ 오늘 + 오늘 할일 + 생성일=오늘. 별표만으로는 안 넣음 |
동기화 created_at |
DB에는 있으나 병합·스냅샷에 없음 | 값이 있으면 보낸다. 스냅샷에 없으면 로컬 값을 유지 |
due_date = null 이 서버에 올라간다.
웹 달력은 아직 기한으로 일정을 찾으므로 웹에서 일정이 사라진다.
반대로 웹이 기한 칸에 둔 일정을 앱은 생성일 칸에 두므로
같은 항목이 다른 날에 보인다.
앱 규칙 원문:
auto_schedule_app/docs/2026-09-23/할일일정기록_날짜규칙.html
(같은 파일이 day_plan_app/docs/2026-09-23/ 에도 있다).
2. 손대지 않는 것
- 엔드포인트:
POST /schedule/api/login/,POST /schedule/api/todos/sync/,GET|POST /schedule/api/todos/ - JSON 봉투:
todos·holiday_memos·record_templates·token - 식별:
sync_uid/ 병합: 더 최근updated_at승 / 삭제:is_deleted묘비 - 테이블 컬럼 추가·삭제 없음.
tb_todo.created_at은 이미 있다 - 기록의 칸은 계속
due_date - 반복 할일의 회차 앵커는 계속
due_date
3. 맞출 날짜 의미
한 레코드가 할일, 일정, 기록 중 하나다. 비교는 yyyy-MM-dd, 오늘은 서버 로컬 날짜(timezone.localdate()).
| 필드 | 할일 | 일정 | 기록 |
|---|---|---|---|
created_at |
달력·목록에 놓는 날. 고른 칸. | 일정 그 자체인 날. 기한을 대신한다. | 만든 시각. 칸에 쓰지 않는다. |
due_date |
마감. 비어 있을 수 있다. 칸 위치가 아니다. | 없음. 저장·동기화 때 비운다. | 기록한 날. 칸 위치다. |
done_date |
완료로 바꾼 날. | 쓰지 않는다. 지난 날은 날짜 비교로 완료다. | 해당 없음. |
할일
- 비반복 칸은 생성일이다. 예: 생성 9/25 · 기한 9/27 → 9/25에만 보인다.
- 생성일이 비어 있는 옛 행만 기한 날로 폴백한다.
- 반복 할일은 회차일(
due_date)에 펼친다. 반복 기준일을 생성일로 바꾸지 않는다. - “오늘 할일”을 켜면 생성일을 오늘로 이동한다. 끄면 고른 칸으로 되돌린다.
- 지연 = 미완료이고 기한이 오늘보다 전. 기한 없으면 생성일이 지나도 지연이 아니다. 기한=오늘은 지연이 아니다.
- 오늘 상태 = 미완료·미지연이고 (생성일=오늘 또는 오늘 할일 또는 기한=오늘).
일정
- 지연 없음. 지난 날=완료, 오늘=오늘, 미래=예정.
- 입력에서 기한 줄을 보여 주지 않는다. 저장 때
due_date를 비운다. - 달력 칸, 일정 목록, 반복 시작일, 미리알림 기준일은 모두 생성일.
- 지난 일정은 알림을 오늘로 넘기지 않는다.
오늘 목록 (할일만)
- 기한 ≤ 오늘 (지연이거나 기한이 오늘)
- “오늘 할일”(
is_my_day)이 켜져 있다 - 반복이 아니고 생성일 날짜가 오늘이다
별표는 오늘에 넣는 이유가 아니다. 지금 웹 _today_where()의
is_star = 1 AND due_date >= today 를 뺀다.
정렬 · 칸
할일 탭·일정 탭 묶음 순서(오름/내림과 무관): 오늘 → 예정 → 지연 → 완료. 일정에는 지연 묶음이 없다. 한 묶음 안에서만 생성일 → 기한 → 완료일.
달력 칸 줄: 날짜 → 공휴일 → 기록 → 일정 → 할일 → 구글 점.
4. 반드시 이 순서로 배포
created_at 을 먼저 넣으면, 서버 기본값(넣은 시각)이
폰의 생성일을 덮어 쓴다.
일정 날이 오늘로 바뀐다. 아래 1 → 2 → 3 → 4 를 지키면 된다.
- 병합 쓰기만
created_at을 받는다._content_vals/_CONTENT_ORDER/ INSERT·UPDATE. 스냅샷(_SYNC_COLS)은 아직 열지 않는다. - 앱이 한 번 이상 동기화한다. 폰의 생성일이 서버에 저장된다. (이미 기한이 지워진 일정도 생성일이 서버에 남는다.)
- 그다음 스냅샷에
created_at을 넣고fetch_all_sync에서_stamp()한다. - 서버에서 일정 기한 마이그레이션을 돌리고, 조회 SQL·화면을 날짜 규칙에 맞춘다.
1만 배포한 동안 구버전 앱은 created_at 을 안 보내도 된다.
서버는 기존처럼 DEFAULT 로 채운다. 하위 호환이다.
5. 동기화 필드 — sql_statement.py
파일: app_schedule/pkg_sql_statement/sql_statement.py
지금 (스냅샷에 created_at 없음)
_SYNC_COLS = (
"sync_uid, updated_at, is_deleted, content, is_done, is_star, is_my_day, "
"due_date, remind_time, remind_day, event_time, repeat_unit, repeat_interval, repeat_until, note, "
"on_calendar, cal_visible, is_record, is_todo, last_done_date, done_date"
)
_CONTENT_ORDER = (
'content', 'is_done', 'is_star', 'is_my_day', 'due_date', 'remind_time',
'remind_day', 'event_time', 'repeat_unit', 'repeat_interval', 'repeat_until', 'note',
'on_calendar', 'cal_visible', 'is_record', 'is_todo', 'last_done_date',
'done_date', 'my_day_date', 'is_deleted',
)
바꿀 점
_content_vals에'created_at': _stamp(t.get('created_at')) or None. 빈 값이면None→ INSERT 때 DB DEFAULT._CONTENT_ORDER끝에'created_at'.- 단계 3 이후
_SYNC_COLS끝에created_at. fetch_all_sync에서updated_at처럼r['created_at'] = _stamp(r.get('created_at')).- 강제 Push(
api_todosPOST)는 이미_CONTENT_ORDER를 쓰므로 같이 따라온다.
앱이 보내는 형식: 'yyyy-MM-dd HH:mm:ss' (로컬). 예: 2026-09-25 00:00:00.
MariaDB DATETIME 에 그대로 넣는다. JSON 으로 내릴 때도 같은 문자열.
my_day_date 는 서버 전용 파생값이다. 앱은 안 보낸다.
지금처럼 is_my_day 이면 기한(없으면 생성일 날짜)으로 채워도 된다.
“오늘 할일”이 생성일을 오늘로 옮기면 기한이 비어도 생성일=오늘이라 오늘 목록에 들어간다.
6. 기존 일정 마이그레이션
앱 AppDb.dropEventDueDates() 와 같다.
순수 일정만 (on_calendar=1 AND is_todo=0 AND is_record=0).
할일·기록의 기한은 지우지 않는다.
-- 1) 생성일이 없거나 기한보다 늦으면 생성일을 기한 날로 당긴다.
UPDATE tb_todo
SET created_at = CONCAT(due_date, ' 00:00:00')
WHERE on_calendar = 1 AND is_todo = 0 AND is_record = 0 AND is_deleted = 0
AND due_date IS NOT NULL AND due_date != ''
AND (created_at IS NULL
OR DATE(created_at) > due_date);
-- 2) 일정의 기한을 모두 지운다. 칸은 생성일이다.
UPDATE tb_todo
SET due_date = NULL
WHERE on_calendar = 1 AND is_todo = 0 AND is_record = 0 AND is_deleted = 0
AND due_date IS NOT NULL;
- 생성일이 기한보다 이른 일정: 생성일 유지, 기한만 삭제. 칸은 생성일. 예: 생성 9/20 · 기한 9/25 → 9/20 칸. 웹·앱이 같이 옮긴다.
updated_at은 건드리지 않는다. 내용 의미 정규화이지 사용자 수정이 아니다. 다음에 어느 쪽이 저장하면 그때 갱신된다.- 앱은 시작 때마다 같은 SQL을 로컬에 돌린다. 서버도 배포 시 한 번(또는
init_schema보정)이면 된다.
넣을 곳: init_schema() 끝, 또는 배포 스크립트 1회.
7. 조회 SQL 을 칸 의미에 맞추기
헬퍼를 쓰면 반복이 줄어든다.
-- 생성일 날짜 (DATETIME / 문자열 모두)
# DATE(created_at) 또는 LEFT(created_at, 10)
-- 할일 비반복 칸: 생성일, 없으면 기한
# COALESCE(NULLIF(DATE(created_at), '0000-00-00'), due_date)
# 앱: substr(created_at,1,10) 없으면 due_date
일정 — 기한 대신 생성일
| 함수 | 지금 | 이후 |
|---|---|---|
list_events_only |
due_date = %s |
DATE(created_at) = %s |
cal_events_in_range |
due_date 구간 |
DATE(created_at) 구간. 반환 키도 생성일 날짜.
뷰가 due_date 로 칸을 묶으면 생성일 별칭을 쓰거나 뷰를 고친다. |
events_in_range_full |
due_date 구간 |
생성일 구간. _month_events 의 e['date'] 도 생성일. |
list_repeating |
due_date IS NOT NULL 만 |
할일: 기한 있음. 일정: 생성일 있음.
반복 전개 기준일은 일정=DATE(created_at), 할일=due_date. |
reminders_for |
due_date IS NOT NULL |
할일 기준일=기한. 일정 기준일=생성일.
(due_date IS NOT NULL AND due_date != '') OR (on_calendar=1 AND is_todo=0 AND created_at IS NOT NULL) |
할일 — 칸은 생성일, 오늘 목록은 규칙 5
| 함수 | 지금 | 이후 |
|---|---|---|
list_todos |
due_date = %s |
비반복: 생성일(없으면 기한)=그 날. 반복: due_date = 그 날 |
cal_todos_in_range |
due_date 구간 |
비반복 생성일 구간(없으면 기한). 반복은 뷰에서 전개 |
_today_where / todos_by_view('today') |
기한≤오늘 OR 나의하루 OR 별표 미래 | 기한≤오늘 OR is_my_day=1 OR
(비반복 AND DATE(created_at)=오늘).
별표 조건 삭제 |
| 오늘 배지 카운트 | 위와 같은 별표 포함 | 오늘 WHERE 와 동일 |
기록 조회(cal_records_in_range, recordsOnDate 해당)는 due_date 유지.
뷰에서 손볼 곳
파일: app_schedule/pkg_views/views_schedule.py
- 달력 셀 묶기: 일정·할일을
due_date로setdefault하는 부분 → 생성일 날짜. _month_events:'date': r['due_date']→ 생성일 날짜. 지난/오늘은 그 날짜로 비교.- 반복 전개 시작일: 일정은 생성일, 할일은 기한. (앱
Todo.repeatAnchor) - 목록 정렬: 오늘 → 예정 → 지연 → 완료. 지금 일부 뷰는 지연이 오늘보다 앞선다.
- 저장(
todo_update/ insert): 일정이면due_date=None, 생성일은 고른 칸. 할일이면 생성일 입력(또는 칸), 기한은 생성일 이후만.
8. 웹 화면
파일: app_schedule/templates/app_schedule/
_todo_fields.html— 일정 입력의 hiddendue_date를 넣지 않는다(빈 값). 고른 칸은 생성일로 저장. 기록은 지금처럼 hiddendue_date= 그 날.- 할일 폼 — 생성일을 고를 수 있게. 기한 선택 시작일=생성일. 생성일 > 기한 저장 거부. 문구: “기한은 생성일보다 빠를 수 없습니다.”
- “오늘 할일” 켜면 생성일을 오늘로 이동. 기한이 그 칸과 같으면 기한을 비워 복사처럼 안 남게.
- 일정 타일 — “기한 yyyy-MM-dd” 를 일정 날(생성일)로 바꾸거나 기한 줄을 뺀다. 지난 일정은 완료 태그·취소선.
- 달력 칸 제목 순서: 기록 → 일정 → 할일. (지금 웹이 다르면 맞춘다.)
- 필터 칩 이름은 “중요”가 아니라 별표. 개수가 있을 때만.
- 할일·일정·기록 화면 제목 옆에 오늘 날짜. 예:
할 일 9월 23일 (수). 달력 제목에는 안 붙인다.
9. 이식 체크
동기화
- 앱에서 생성 9/25 · 기한 9/27 할일을 저장 → 동기화 → 웹 달력 9/25에만 있다. 9/27에는 없다.
- 앱에서 내일 칸 일정 저장 → 서버
due_date가 NULL,DATE(created_at)가 내일. 웹 내일 칸에 보인다. 오늘 칸에는 없다. - 웹에서 내일 일정 저장 → 앱 내일 칸에 보인다. 기한 필드 없음.
- 같은 항목을 웹에서 제목만 고친 뒤 동기화 → 폰의 생성일이 오늘로 바뀌지 않는다.
- 강제 Pull 후에도 일정이 생성일 칸에 있다.
규칙
- 기한을 생성일보다 이르게 저장할 수 없다.
- “오늘 할일”을 켜면 원래 칸에서 사라지고 오늘 칸으로 이동한다.
- 기한 없고 생성일만 지난 할일은 지연이 아니다. 오늘 목록에도 없다(생성일이 오늘이 아니면).
- 기한이 어제이고 미완료면 지연이며 오늘 목록에 있다. 기한이 오늘이면 지연이 아니다.
- 별표만 있는 미래 할일은 오늘 목록에 없다.
- 어제 일정은 완료, 내일 일정은 예정. 일정 목록 내림차순이어도 묶음은 오늘 → 예정 → 완료.
- 할일 목록 오름차순이어도 묶음은 오늘 → 예정 → 지연 → 완료.
10. 작업 파일 목록
| 파일 | 할 일 |
|---|---|
app_schedule/pkg_sql_statement/sql_statement.py |
병합 필드, 스냅샷 stamp, 마이그레이션, 조회 SQL, 오늘 WHERE, 반복/알림 |
app_schedule/pkg_views/views_schedule.py |
달력 묶기, 이달 일정 날짜, 정렬, 저장 시 일정 기한 비우기·생성일 저장 |
app_schedule/templates/app_schedule/_todo_fields.html |
일정 기한 hidden 제거, 할일 생성일, 오늘 할일 이동 |
app_schedule/templates/app_schedule/schedule.html |
칸 순서, 일정 타일 날짜 줄, 제목 옆 오늘 날짜, 별표 칩 |
app_schedule/templates/app_schedule/_todo_item.html |
기한 라벨, 별은 제목 뒤 |
반복 모듈(뷰에서 due_date 를 시작일로 쓰는 곳) |
일정은 생성일 앵커 |
한줄
- API·컬럼은 그대로.
created_at을 병합에 넣고, 일정 칸은 생성일, 할일 칸도 생성일, 기록만 기한. - 스냅샷보다 병합 쓰기를 먼저 배포한다.
- 일정 기한은 서버에서 지운다. 별표만으로 오늘 목록에 넣지 않는다.