Flutter · 웹 → 폰앱 이식
이 세션에서 웹(하루계획)에 적용한 변경을 폰앱(auto_schedule_app · day_plan_app)에 통일하고, 동일 명세를 macOS 프로젝트로 옮기기 위한 문서입니다. 실제 코드 변경은 A. 알림 설정 열기, B. ‘오늘 할일’ 라벨, C. 기록 날짜 이동, D. 기록 종류(템플릿) + 서버 동기화, 그리고 실기기 피드백 반영인 E. 폰앱 UI 개선(버튼 가시성·빠른 기록 다이얼로그·완료 점 위치)입니다.
앱은 이미 flutter_local_notifications로 알림 권한을 요청하지만, 거부/OS 차단 시 앱 안에서 OS 알림 설정으로 이동할 경로가 없었습니다. 설정 탭 맨 아래에 「알림 설정 열기」 버튼을 추가했습니다.
icalendar_parser: ^2.1.0 app_settings: ^6.1.1 # OS 알림 설정 화면 열기
+ 추가 후 flutter pub get
import 'package:flutter/material.dart'; import 'package:app_settings/app_settings.dart'; import '../services/gcal.dart';
child: const Text('저장'), ), const Divider(height: 40), // ---------- 알림 설정 ---------- const Text('알림', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)), const SizedBox(height: 6), const Text( '미리 알림이 울리지 않으면 이 앱의 알림 권한이 꺼져 있을 수 있습니다. ' '아래 버튼으로 이 앱의 OS 알림 설정을 열어 알림을 켜 주세요.', style: TextStyle(color: Colors.grey, fontSize: 12.5), ), const SizedBox(height: 12), FilledButton.icon( onPressed: () => AppSettings.openAppSettings( type: AppSettingsType.notification), icon: const Icon(Icons.notifications_active_outlined), label: const Text('알림 설정 열기'), style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(50)), ), ],
핵심 · AppSettings.openAppSettings(type: AppSettingsType.notification)
할일 입력의 ‘나의 하루에 추가’를 ‘오늘 할일’(☀아이콘 제거 + 진한 파란색)로 바꿨습니다. 웹은 ‘오늘’, 폰앱은 ‘오늘 할일’로 표기.
📄 lib/widgets/todo_edit_sheet.dart — SwitchListTile value: t.isMyDay,
onChanged: (v) => setState(() => t.isMyDay = v),
- secondary:
- const Icon(Icons.wb_sunny_outlined, color: Color(0xFFF5A623)),
- title: const Text('나의 하루에 추가'),
+ title: const Text('오늘 할일',
+ style: TextStyle(
+ color: Color(0xFF12448A), fontWeight: FontWeight.w600)),
dense: true,
),
기록은 기한 입력이 없어 선택 날짜에 귀속되므로, 폰앱에서 만든 기록은 다른 날짜로 옮길 수 없었습니다. 기록 편집기에 ‘날짜’ 이동 컨트롤을 추가했습니다 — 웹과 동일하게 과거 O · 미래 X(기록 미래 불가).
📄 lib/widgets/todo_edit_sheet.dart — _buildEventRecord (kind == 'record')if (kind == 'event') _rowDue(), if (kind == 'record') _rowRecordDate(), // 기록: 날짜 이동(과거 O, 미래 X) _rowEventTime(),
+ 날짜 선택기(미래 차단) & 행 위젯 추가:
Future<void> _pickRecordDate() async {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day); // 기록 미래 불가
final existing = DateTime.tryParse(t.dueDate ?? '');
final init =
(existing != null && !existing.isAfter(today)) ? existing : today;
final d = await showDatePicker(
context: context,
initialDate: init,
firstDate: DateTime(2020, 1, 1),
lastDate: today, // 미래 선택 차단
);
if (d != null) setState(() => t.dueDate = isoOf(d));
}
Widget _rowRecordDate() {
final iso = t.dueDate ?? isoOf(DateTime.now());
return InkWell(
onTap: _pickRecordDate,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Row(children: [
const Icon(Icons.event, color: Color(0xFF7A4FBB)),
const SizedBox(width: 14),
Expanded(
child: Text('날짜 · ${_fmtDue(iso)}',
style: const TextStyle(fontSize: 15, color: Color(0xFF7A4FBB))),
),
const Icon(Icons.chevron_right, size: 20, color: Colors.grey),
]),
),
);
}
+ 저장 시 서버 방어(동기화된 미래 기록도 오늘로) — _save():
// 기록: 미래 불가 → 미래 날짜면 오늘로(과거는 유지). 웹과 동일.
if (t.isRecord &&
t.dueDate != null &&
t.dueDate!.compareTo(todayIso) > 0) {
t.dueDate = todayIso;
}
설정에서 기록 항목명 + 옵션(예: 수면관리 → 기상/취침)을 만들어 두고, 달력에서 📝 빠른 기록으로 옵션·시간을 골라 기록 1건을 바로 저장합니다. 저장 형식은 웹과 동일 — content=옵션(예: ‘기상’), event_time=시간.
동기화: auto는 서버(modam_doc)와 sync_uid 기준 병합(LWW·툼스톤). day_plan은 로컬 전용(SyncService 미참조, bumpData()만).
웹: 좌측 메뉴 ⚙ 기록 설정 → 페이지 제목 기록 설정 종류. 폰: 설정 탭 → 기록 종류 관리 버튼. 달력 빠른 입력 버튼은 웹·폰 모두 📝 빠른 기록.
path, - version: 8, + version: 9, onConfigure: ... // onCreate + onUpgrade(oldV < 9): await d.execute( 'CREATE TABLE tb_record_template (id INTEGER PRIMARY KEY AUTOINCREMENT, ' 'name TEXT NOT NULL, options TEXT, is_fixed INTEGER NOT NULL DEFAULT 0, ' 'sync_uid TEXT, updated_at TEXT, is_deleted INTEGER NOT NULL DEFAULT 0)'); await d.execute('CREATE UNIQUE INDEX ux_rectpl_uid ON tb_record_template(sync_uid)');
+ DB 메서드: list/save/delete(소프트) · allRecordTemplatesForSync · applyRecordTemplates(sync_uid LWW)
lib/screens/record_templates_screen.dart // 기록 종류 관리(항목명·옵션 행·고정·목록)
lib/widgets/record_quick_sheet.dart // showRecordQuickSheet(dateIso) 바텀시트
설정 진입 · 달력 버튼:
// settings_screen.dart — 알림 섹션 뒤 FilledButton.icon( onPressed: () => Navigator.of(context).push(MaterialPageRoute( builder: (_) => const RecordTemplatesScreen())), icon: const Icon(Icons.playlist_add), label: const Text('기록 종류 관리'), ...), // calendar_tab.dart — 하단 시트 날짜 헤더 아래 if (!_holidaysOnly) OutlinedButton.icon( onPressed: () async { final saved = await showRecordQuickSheet(context, dateIso: selIso); if (saved && mounted) _load(); }, icon: const Text('📝'), label: const Text('빠른 기록')),
final tpls = await AppDb.instance.allRecordTemplatesForSync();
// POST body 에 'record_templates': tpls 추가, 응답 스냅샷 적용:
final rtList = (data['record_templates'] as List?) ?? const [];
final rtN = await AppDb.instance.applyRecordTemplates(rtList);
서버(modam_doc)도 tb_record_template에 sync_uid·updated_at·is_deleted 추가 + api_todos_sync 확장(하위호환). day_plan은 sync.dart 없음 → 이 단계 생략.
실기기(삼성 Z Fold5)에서 확인된 UI 문제를 폰앱 전용으로 손봤습니다. 웹·macOS 이식 대상이 아니라 폰앱 고유 항목입니다(참고용).
기본 FilledButton이 일부 기기에서 배경과 거의 같은 색으로 흐릿하게 나와 저장/수정/추가 버튼이 안 보이던 문제. 배경·글자색을 명시(파란/보라 배경 + 흰 굵은 글씨 + ✓ 아이콘 + 전체폭)로 교체.
// record_templates_screen.dart · todo_edit_sheet.dart - FilledButton(child: Text('수정')) // 흐릿 + FilledButton.icon( + icon: const Icon(Icons.check, size: 20), + label: Text('수정 저장', style: bold 16), + style: FilledButton.styleFrom( + backgroundColor: 0xFF2F6AA8, foregroundColor: Colors.white, + minimumSize: Size.fromHeight(52)))
달력 날짜 시트의 「📝 빠른 기록」(모든 모드 노출)을 제거. 신규 기록 작성 시 상단 유형칩(할일/일정/기록) 행의 「기록」 오른쪽에만 버튼 배치. (‘기록’ 라벨 행은 삭제, 수정 화면엔 버튼 없음.) 버튼 클릭 → 다이얼로그에서 템플릿 옵션 선택 → 제목·시간 자동 입력 → 검증된 기존 「추가」 저장 경로 사용.
// calendar_tab.dart 날짜시트: 빠른 기록 버튼 제거(+ record_quick_sheet import 제거) // todo_edit_sheet.dart _typeSelector() Row([ chip('할일'), chip('일정'), chip('기록'), if (kind=='record') [ Spacer(), _quickRecordButton() ] ]) // _quickRecordButton → _openQuickRecordDialog(): // showDialog → 템플릿별 옵션 ActionChip → 선택 시 Navigator.pop(opt) // _applyQuickOption(opt): _title.text = opt; 시간 비면 현재시각
기록 생성은 편집기의 _saveByKind()(isTodo=false·onCalendar=false·isRecord=true) 그대로 → 확실히 저장됨.
※ 기존 widgets/record_quick_sheet.dart(별도 바텀시트)는 미사용(사장).
예: 기상, 2행 예: 취침.증상: 한 템플릿의 고정을 끄면 모든 템플릿이 비고정이 됨. 원인: 웹에서 만든 템플릿은 updated_at이 서버 실시각인데, 폰 시계가 뒤처지면 폰 수정의 updated_at이 더 작아 LWW에서 서버의 옛 값(0)이 이겨 되돌려짐(저장 시 전체 동기화라 전부 0으로 보임). 서버 병합 자체는 정상(행별 is_fixed)임을 서버에서 직접 테스트로 확인.
// app_db.dart — 로컬 수정용 updated_at 을 기존값보다 항상 크게(단조 증가) String _laterStamp(String? existing) { final now = nowUtcStamp(); if (existing == null || now.compareTo(existing) > 0) return now; return existing + 1초; // 폰 시계가 뒤처져도 폰 수정이 LWW 에서 이김 } // saveRecordTemplate(수정)·deleteRecordTemplate 에서 사용
※ 같은 원리가 다른 웹 생성 항목(할일/일정)에도 잠재. 필요 시 update() 등으로 확장 적용 예정.
둥근 카드형 날짜칸 → 진짜 표 격자. TableCalendar의 calendarStyle.tableBorder(안팎 가로·세로선)로 단일 격자선, cellMargin/cellPadding = 0. 셀 자체 테두리 제거, 선택 칸만 파란 강조. 선 색은 #9AA5B1(진회색).
할일 완료 표시 점을 날짜칸 하단 중앙 → 우측 상단으로 이동. 셀을 Stack으로 감싸 Positioned 배치(구글 일정 파란 점은 하단 중앙 유지).
child: Stack(children: [
Column(... 날짜/제목 ...),
if (doneCnt > 0 && ...) Positioned(top: 4, right: 4, child: _dot(Colors.black)),
])
달력 오늘 날짜 시트는 미완료('오늘 할 일')만 표시가 정상 — 오늘 완료 항목은 ‘완료됨’ 뷰에서 조회. (한때 완료 항목을 시트에 추가했다가 되돌림.)
애플 버전(iOS)에서 확인·수정한 3건을 두 안드로이드 앱(auto·day_plan)에 동일 이식. 모두 공통 Dart lib/ 변경이며 네이티브 수정 없음. 두 앱 flutter analyze 통과.
구글 일정을 [할일/일정/기록]으로 편입하면 원본이 gcal_hidden(tb_setting)에 영구 숨김됨. 편입본을 삭제해도 숨김이 남아 원본이 안 보이던 문제. 완전 삭제 시 날짜·제목으로 매칭해 숨김 해제.
// gcal.dart — 키: '$iso|$time|$title', 시각 무시하고 날짜+제목 매칭 static Future<bool> unhideByContent(String iso, String title) async { await _loadHidden(); final before = _hidden!.length; _hidden!.removeWhere((k) => k.startsWith('$iso|') && k.endsWith('|$title')); if (_hidden!.length == before) return false; await _saveHidden(); return true; } // todo_edit_sheet.dart — 완전 삭제 2곳(_delete 단일 / _deleteWithChoice 둘다) 뒤: await GcalService.unhideByContent(t.dueDate ?? '', t.content); // 부분 삭제(setPresence, 일정만/할일만)는 제외
한계: 같은 날 같은 제목 다른 시각이 여럿이면 함께 풀릴 수 있음. 기존 숨김 잔재는 소급 안 됨(앞으로의 삭제부터).
별표가 2차 정렬 기준이라 완료·나의하루 그룹 안에서만 위로 가던 것을, 상태와 무관하게 항상 최상단으로. DB orderBy는 유지, 앱단에서 안정 분할.
// calendar_tab.dart List<Todo> _starFirst(List<Todo> items) => [...items.where((t) => t.isStar), ...items.where((t) => !t.isStar)]; _selItems = _starFirst(sel); // (기존: _selItems = sel;)
// home_screen.dart — 달력 탭 앱바만 변경(다른 탭·런처 이름 그대로) static const _titles = ['모담 하루계획', '할 일', '일정', '기록']; // 기존 '하루계획'
파일: services/gcal.dart · widgets/todo_edit_sheet.dart · screens/calendar_tab.dart · screens/home_screen.dart. 문서: docs/2026-09-03/ 3건(편입삭제·별표상단·제목변경).
이 세션의 웹 변경을 폰앱 기준으로 분류했습니다. 실제 코드 수정이 필요했던 건 위 A·B뿐이고, 나머지는 이미 처리돼 있거나 웹 전용이라 해당이 없습니다.
| 웹 변경 | 폰 상태 | 메모 |
|---|---|---|
| ‘나의 하루에 추가’ → ‘오늘 할일’ | ✓ 적용 | B · todo_edit_sheet.dart (웹 ‘오늘’ / 폰 ‘오늘 할일’) |
| 알림 설정 열기(app_settings) | ✓ 적용 | A · settings_screen.dart |
| 미리알림 ‘안함’ 시 시간 숨김 | 이미 처리 | 알림 없을 때 전일/당일·시간 숨김(has 게이트) |
| 과거 기록/일정 수정 시 날짜 유지 | 이미 처리 | 과거→오늘 보정이 순수 할일(isTodo)만 적용 |
| 할일 날짜 이동 | 이미 처리 | 편집 시트 기한 선택(오늘/내일/다음주/날짜선택), firstDate=오늘로 과거 이동 차단(웹과 동일) |
| 기록 날짜 이동 (미래 불가) | ✓ 적용 | C · 기록 편집기에 ‘날짜’ 컨트롤 추가, lastDate=오늘로 미래 차단 + 저장 시 클램프 |
| 기록 종류(템플릿) + 서버 동기화 | ✓ 적용 | D · DB v9 + 설정 관리 + 📝 빠른 기록 + 동기화(auto) / 로컬(day_plan) |
| 폰앱 UI 개선 묶음(실기기 피드백) | ✓ 적용 | E · 버튼 가시성·빠른 기록(신규 유형칩 옆 버튼→다이얼로그)·완료 점 우측 상단·기록종류 목록 개별 고정 체크박스·달력 표 격자·고정 동기화 LWW 시계오차 수정 |
| 상세폼 헤더(× + 라벨) 제거 | 웹 전용 | 폰은 바텀시트 편집기 — 해당 UI 없음 |
| 클릭 눌림/플래시 효과 | 웹 전용 | 폰은 Material 리플 기본 제공 |
| 기록 마커 원→네모(달력) | 해당 없음 | 폰 달력은 기록을 보라색 텍스트로 표시(마커 없음) |
| 설정 페이지 여백(‘전체 기록’과 정렬) | 웹 전용 | 설정 콘텐츠를 .sch-listwrap로 감싸 다른 목록 페이지와 동일한 상단 22px·좌우 24px 여백 적용(폰은 별도 화면 — 해당 없음) |
| 상단 날짜 이동 · 사이드바 순서 · 메모높이 · 제목폰트 | 웹 전용 | 웹 상세폼/사이드바 레이아웃 고유 |
Changed 1 dependency).No issues found.app_settings: ^6.1.1 추가 → flutter pub get.SwitchListTile 아이콘 제거 + ‘오늘 할일’(진한 파랑) 교체(B), ② 기록 편집기에 _rowRecordDate()/_pickRecordDate() 추가 + _save() 기록 미래 클램프(C).app_db.dart DB v9 테이블+메서드, 신규 파일 2개(record_templates_screen.dart·record_quick_sheet.dart), 설정 진입 + 달력 📝 버튼. 서버 동기화 쓰면 sync.dart에 record_templates 병합 추가(로컬 전용이면 생략).flutter analyze 후 flutter run -d macos 로 동작 확인.같은 코드지만 플랫폼 동작이 다를 수 있습니다. macOS에서 AppSettingsType.notification은 시스템 설정 › 알림을 엽니다(앱별 항목 위치는 macOS 버전에 따라 다를 수 있음). 또 macOS에서 실제 알림 수신에는 데스크톱 알림 권한/엔터틀먼트가 별도로 필요합니다 — 이 버튼은 “설정 이동”만 담당합니다. 빌드 후 실동작을 꼭 확인하세요.