Flutter ๋ค์ด์ด๋ฆฌ ์ฑ ๋ง๋ค๊ธฐ
Flutter๋ก ํ์ผ ๊ธฐ๋ฐ ์ผ๊ธฐ์ฅ ์ฑ์ ์ฒ์๋ถํฐ ๋ง๋๋ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ๊ฐ๋ฐํด๋ณด์์.
๋ชฉ์ฐจ
1. ํ๋ก์ ํธ ๊ฐ์
์ฑ ์๊ฐ
โข
์ด๋ฆ: ๋ค์ด์ด๋ฆฌ ์ฑ (Diary App)
โข
๋ชฉ์ : Flutter์ ํ์ผ I/O, ์ํ ๊ด๋ฆฌ, ํ์ด์ง ๋ผ์ฐํ
์ ์ตํ๊ธฐ ์ํ ์ค์ต ํ๋ก์ ํธ
โข
ํน์ง: DB ์์ด ํ
์คํธ ํ์ผ๋ง์ผ๋ก ์ผ๊ธฐ๋ฅผ ์ ์ฅยท๊ด๋ฆฌ
์ฃผ์ ๊ธฐ๋ฅ
๊ธฐ๋ฅ | ์ค๋ช
|
์ผ๊ธฐ ์์ฑ | ๋ ์งยท์ ๋ชฉยท๋ณธ๋ฌธ ์
๋ ฅ ํ ์ ์ฅ |
์ผ๊ธฐ ๋ชฉ๋ก | ์ต์ ์ ์ ๋ ฌ, ์ค์์ดํ ์ญ์ |
์ผ๊ธฐ ์์ธ | ๋ณด๊ธฐ / ์์ / ๋ ์ง ๋ณ๊ฒฝ |
๋ฌ๋ ฅ ๋ทฐ | ์๋ณ ๋ฌ๋ ฅ + ์ผ๊ธฐ ์๋ ๋ ๋ง์ปค ํ์ |
๊ฒ์ | ๋ ์งยท์ ๋ชฉยท๋ณธ๋ฌธ ํตํฉ ๊ฒ์ (๋๋ฐ์ด์ค ์ ์ฉ) |
๋ค์ค ์ ํ ์ญ์ | ์ฌ๋ฌ ์ผ๊ธฐ ํ ๋ฒ์ ์ญ์ |
ํ๋ก์ ํธ ๊ตฌ์กฐ
diary_app/
โโโ lib/
โ โโโ main.dart # ์ฑ ์ง์
์ ยท ํ
๋ง ์ค์
โ โโโ services/
โ โ โโโ file_service.dart # ํ์ผ I/O ์๋น์ค (๋น์ฆ๋์ค ๋ก์ง)
โ โโโ pages/
โ โโโ home_page.dart # ์ผ๊ธฐ ๋ชฉ๋ก ํ๋ฉด
โ โโโ write_page.dart # ์ผ๊ธฐ ์์ฑ ํ๋ฉด
โ โโโ detail_page.dart # ์ผ๊ธฐ ์์ธ๋ณด๊ธฐ ยท ์์ ํ๋ฉด
โ โโโ calendar_page.dart # ๋ฌ๋ ฅ ๋ทฐ ํ๋ฉด
โโโ guide/ # ๊ฐ์ ๋
ธํธ ๋ฌธ์ ํด๋
โโโ pubspec.yaml
Plain Text
๋ณต์ฌ
2. ํ๋ก์ ํธ ์ด๊ธฐ ์ค์
Flutter ํ๋ก์ ํธ ์์ฑ
flutter create diary_app
cd diary_app
Bash
๋ณต์ฌ
ํจํค์ง ์ถ๊ฐ
flutter pub add path_provider
flutter pub add calendar_date_picker2
flutter pub add table_calendar
Bash
๋ณต์ฌ
pubspec.yaml ๊ฒฐ๊ณผ
dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.0
calendar_date_picker2: ^2.0.1
table_calendar: ^3.2.0
flutter_multi_select_items: ^0.4.3
YAML
๋ณต์ฌ
ํด๋ ๊ตฌ์กฐ ์์ฑ
lib/
โโโ services/ โ FileService ๋ฐฐ์น
โโโ pages/ โ ๊ฐ ํ๋ฉด ๋ฐฐ์น
Plain Text
๋ณต์ฌ
main.dart โ ์ฑ ์ง์ ์
void main() => runApp(const DiaryApp());
class DiaryApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.amber),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
Dart
๋ณต์ฌ
useMaterial3: true + amber ์๋ ์ปฌ๋ฌ๋ก ์ ์ฒด ํ
๋ง๋ฅผ ํต์ผํฉ๋๋ค.
3. ์ฌ์ฉ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
3-1. path_provider ^2.0.0
์ญํ : ํ๋ซํผ(Android/iOS/Windows ๋ฑ)๋ณ๋ก ๋ค๋ฅธ ํ์ผ ์ ์ฅ ๊ฒฝ๋ก๋ฅผ ์๋์ผ๋ก ๋ฐํ
ํ๋ซํผ | ๊ฒฝ๋ก ์์ |
Android | /data/user/0/com.example.diary_app/app_flutter/ |
iOS | /var/mobile/.../Documents/ |
Windows | C:\\Users\\user\\Documents\\ |
import 'package:path_provider/path_provider.dart';
final dir = await getApplicationDocumentsDirectory();
// โ ์ด ๊ฒฝ๋ก ์๋์ .txt ํ์ผ ์ ์ฅ
Dart
๋ณต์ฌ
3-2. calendar_date_picker2 ^2.0.1
์ญํ : ๋ค์ด์ผ๋ก๊ทธ ํํ์ ๋ ์ง ์ ํ ๋ฌ๋ ฅ ์์ ฏ
์ ์ฉ ์์น
ํ๋ฉด | ๊ธฐ๋ฅ |
WritePage | ์ผ๊ธฐ๋ฅผ ์ธ ๋ ์ง ์ ํ (๊ธฐ๋ณธ๊ฐ: ์ค๋) |
DetailPage (edit ๋ชจ๋) | ๊ธฐ์กด ์ผ๊ธฐ์ ๋ ์ง ๋ณ๊ฒฝ |
Future<void> _pickDate() async {
final result = await showCalendarDatePicker2Dialog(
context: context,
config: CalendarDatePicker2WithActionButtonsConfig(
calendarType: CalendarDatePicker2Type.single,
selectedDayHighlightColor: Colors.amber,
okButton: const Text('ํ์ธ', style: TextStyle(color: Colors.amber)),
cancelButton: const Text('์ทจ์'),
),
dialogSize: const Size(325, 400),
borderRadius: BorderRadius.circular(15),
value: [_selectedDate],
);
if (result != null && result.isNotEmpty && result.first != null) {
setState(() => _selectedDate = result.first!);
}
}
Dart
๋ณต์ฌ
3-3. table_calendar ^3.2.0
์ญํ : ์/์ฃผ ๋ทฐ ๋ฌ๋ ฅ ์์ ฏ. ์ด๋ฒคํธ ๋ง์ปค, ๋ ์ง ์ ํ, ์ปค์คํ
์คํ์ผ ์ง์
TableCalendar<DiaryEntry>(
firstDay: DateTime(2020),
lastDay: DateTime(2100),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
eventLoader: _eventsFor, // ๋ ์ง โ ์ด๋ฒคํธ ๋ฆฌ์คํธ ํจ์
onDaySelected: _onDaySelected,
onPageChanged: (day) => setState(() => _focusedDay = day),
calendarStyle: CalendarStyle(
todayDecoration: BoxDecoration(color: Colors.amber.shade300, shape: BoxShape.circle),
selectedDecoration: BoxDecoration(color: Colors.amber, shape: BoxShape.circle),
markerDecoration: BoxDecoration(color: Colors.deepOrange, shape: BoxShape.circle),
),
headerStyle: HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
),
)
Dart
๋ณต์ฌ
์ด๋ฒคํธ ๋ง์ปค ์๋ฆฌ
Map<String, List<DiaryEntry>> _eventMap = {};
List<DiaryEntry> _eventsFor(DateTime day) {
final key = '${day.year}-${day.month.toString().padLeft(2, '0')}'
'-${day.day.toString().padLeft(2, '0')}';
return _eventMap[key] ?? [];
}
// ๋ฐํ๋ ๋ฆฌ์คํธ ํฌ๊ธฐ๋งํผ ๋ ์ง ์๋์ ์ (marker)์ด ํ์๋จ
Dart
๋ณต์ฌ
3-4. flutter_multi_select_items ^0.4.3
์ฌ์ฉํ์ง ์์ ์ด์ : ๋ด๋ถ์ ์ผ๋ก Wrap์ ์ฌ์ฉํ๋ ์์ ฏ์ด SingleChildScrollView + Expanded ์์์ ๋ฌดํ ๋์ด ๋ ์ด์์ ์ค๋ฅ ๋ฐ์
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
Plain Text
๋ณต์ฌ
๋์: Set<String> + ListView.builder๋ก ์ง์ ๊ตฌํ
Set<String> _selectedPaths = {};
onTap: () => setState(() {
if (_selectedPaths.contains(path)) {
_selectedPaths.remove(path);
} else {
_selectedPaths.add(path);
}
});
Dart
๋ณต์ฌ
4. ๋ฐ์ดํฐ ์ค๊ณ โ FileService
์์น: UI์ ๋ฐ์ดํฐ ๋ก์ง์ ๋ถ๋ฆฌํ๋ค (์๋น์ค ๋ ์ด์ด ํจํด)
4-1. ํ์ผ ์ ์ฅ ์ ๋ต
๊ณ ๋ ค ์ฌํญ | ๊ฒฐ์ |
DB ์ฌ์ฉ ์ฌ๋ถ | |
ํ๋ฃจ 1๊ฐ ์ ํ ์ฌ๋ถ | |
ํ์ผ๋ช
ํ์ | YYYY-MM-DD_HHmmss.txt |
๋ด์ฉ ์ง๋ ฌํ | ์ ๋ชฉ + \\n---DIARY---\\n + ๋ณธ๋ฌธ |
ํ์ผ๋ช
์์
2026-03-26_143022.txt
Plain Text
๋ณต์ฌ
โข
๋ ์ง(YYYY-MM-DD) + ์๊ฐ(HHmmss) ์กฐํฉ โ ํ์ผ ์ด๋ฆ ์ถฉ๋ ์์
โข
ํ์ผ ์ด๋ฆ ์์ฒด๊ฐ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๊ฒธํจ โ DB ๋ถํ์
ํ์ผ ๋ด์ฉ ํฌ๋งท
์ค๋์ ์ผ๊ธฐ ์ ๋ชฉ
---DIARY---
์ค๋์ ๋ ์จ๊ฐ ๋ง์๋ค.
Flutter ๊ณต๋ถ๋ฅผ ์ด์ฌํ ํ๋ค.
์ฌ๋ฌ ์ค๋ ๊ฐ๋ฅํ๋ค.
Plain Text
๋ณต์ฌ
4-2. DiaryEntry ๋ชจ๋ธ ํด๋์ค
class DiaryEntry {
final String path; // ํ์ผ ์ ๋ ๊ฒฝ๋ก (๊ธฐ๋ณธ ํค)
final String date; // "YYYY-MM-DD" (UI ํ์์ฉ)
final String time; // "HH:mm:ss" (UI ํ์์ฉ)
final String title; // ์ ๋ชฉ (๋ชฉ๋ก์์ ๋ฏธ๋ฆฌ ๋ก๋)
}
Dart
๋ณต์ฌ
์ค๊ณ ์ด์ : FileSystemEntity๋ฅผ ์ง์ ์ฐ๋ฉด ๋ชฉ๋ก ๋ ๋๋ง ์ ๋งค๋ฒ ํ์ผ์ ์ฝ์ด์ผ ํฉ๋๋ค.
DiaryEntry๋ก ํ ๋ฒ์ ๋ก๋ํด๋๋ฉด ์ถ๊ฐ I/O ์์ด ๋น ๋ฅธ ๋ชฉ๋ก ๊ฐฑ์ ์ด ๊ฐ๋ฅํฉ๋๋ค.
4-3. ์ฃผ์ ๋ฉ์๋ ์ ๋ฆฌ
์ง๋ ฌํ / ์ญ์ง๋ ฌํ
// ์ ์ฅ: ์ ๋ชฉ + ๊ตฌ๋ถ์ + ๋ณธ๋ฌธ โ ํ๋์ ๋ฌธ์์ด
String _serialize(String title, String content) =>
'$title\\n---DIARY---\\n$content';
// ์ฝ๊ธฐ: ์ ๋ชฉ ์ถ์ถ
String parseTitleFromRaw(String raw) {
final idx = raw.indexOf('\\n---DIARY---\\n');
if (idx == -1) return '';
return raw.substring(0, idx);
}
// ์ฝ๊ธฐ: ๋ณธ๋ฌธ ์ถ์ถ
String parseContentFromRaw(String raw) {
final sep = '\\n---DIARY---\\n';
final idx = raw.indexOf(sep);
if (idx == -1) return raw; // ๊ตฌ๋ถ์ ์์ผ๋ฉด ์ ์ฒด๊ฐ ๋ณธ๋ฌธ (๊ตฌ๋ฒ์ ํธํ)
return raw.substring(idx + sep.length);
}
Dart
๋ณต์ฌ
์ ์ผ๊ธฐ ์ ์ฅ โ saveDiaryForDate()
Future<void> saveDiaryForDate(String date, String title, String content) async {
final dirPath = await getDirPath();
final now = DateTime.now();
final t = '${now.hour.toString().padLeft(2, '0')}'
'${now.minute.toString().padLeft(2, '0')}'
'${now.second.toString().padLeft(2, '0')}';
final file = File('$dirPath/${date}_$t.txt');
await file.writeAsString(_serialize(title, content));
}
Dart
๋ณต์ฌ
โข
ํ์ฌ ์๊ฐ์ ํ์ผ๋ช
์ ํฌํจ โ ๊ฐ์ ๋ ์ฌ๋ฌ ์ผ๊ธฐ ์ ์ฅ ๊ฐ๋ฅ
โข
padLeft(2, '0'): ํ ์๋ฆฌ ์ซ์๋ฅผ ๋ ์๋ฆฌ๋ก ํจ๋ฉ (9 โ 09)
๊ธฐ์กด ์ผ๊ธฐ ์์ โ saveDiaryToPath()
Future<void> saveDiaryToPath(String path, String title, String content) async {
await File(path).writeAsString(_serialize(title, content));
}
// ๊ฒฝ๋ก๊ฐ ๊ณ ์ โ ํ์ผ๋ช
(๋ ์งยท์๊ฐ) ๋ณ๊ฒฝ ์์ด ๋ด์ฉ๋ง ๋ฎ์ด์์
Dart
๋ณต์ฌ
๋ ์ง ๋ณ๊ฒฝ โ changeDiaryDate()
Future<String> changeDiaryDate(
String oldPath, String newDate, String title, String content) async {
final dirPath = await getDirPath();
final name = oldPath.split(Platform.pathSeparator).last.replaceAll('.txt', '');
final timePart = name.contains('_') ? name.split('_').last : '';
final newPath = '$dirPath/${newDate}_$timePart.txt';
await File(newPath).writeAsString(_serialize(title, content));
await File(oldPath).delete();
return newPath;
}
Dart
๋ณต์ฌ
โข
๋ ์ง๊ฐ ๋ฐ๋๋ฉด ํ์ผ๋ช
๋ ๋ฐ๋์ด์ผ ํจ โ ์ ํ์ผ ์์ฑ ํ ๊ธฐ์กด ํ์ผ ์ญ์
โข
HHmmss ์๊ฐ ๋ถ๋ถ์ ๊ทธ๋๋ก ์ ์ง (์์ฑ ์๊ฐ ๋ณด์กด)
์ ์ฒด ๋ชฉ๋ก ์กฐํ โ getDiaryEntries()
Future<List<DiaryEntry>> getDiaryEntries() async {
final dir = Directory(await getDirPath());
final txts = dir.listSync().where((e) => e.path.endsWith('.txt')).toList();
txts.sort((a, b) => b.path.compareTo(a.path)); // ์ต์ ์ ์ ๋ ฌ
final entries = <DiaryEntry>[];
for (final f in txts) {
final raw = await File(f.path).readAsString();
entries.add(DiaryEntry(
path: f.path,
date: getDateFromPath(f.path),
time: getTimeFromPath(f.path),
title: parseTitleFromRaw(raw),
));
}
return entries;
}
Dart
๋ณต์ฌ
๊ฒฝ๋ก ํ์ฑ โ getDateFromPath() / getTimeFromPath()
// "2026-03-26_143022.txt" โ "2026-03-26"
String getDateFromPath(String path) {
final name = path.split(Platform.pathSeparator).last.replaceAll('.txt', '');
return name.contains('_') ? name.split('_').first : name;
}
// "2026-03-26_143022.txt" โ "14:30:22"
String getTimeFromPath(String path) {
final name = path.split(Platform.pathSeparator).last.replaceAll('.txt', '');
final t = name.split('_').last;
return '${t.substring(0, 2)}:${t.substring(2, 4)}:${t.substring(4, 6)}';
}
Dart
๋ณต์ฌ
Platform.pathSeparator: Windows(\\) / macOSยทLinux(/) ์๋ ๋์
๊ฒ์ โ searchEntries()
Future<List<DiaryEntry>> searchEntries(String query) async {
final all = await getDiaryEntries();
final q = query.toLowerCase();
final result = <DiaryEntry>[];
for (final entry in all) {
// 1์ฐจ: ๋ ์ง or ์ ๋ชฉ (์ด๋ฏธ ๋ก๋๋ ์ ๋ณด โ ์ถ๊ฐ I/O ์์)
if (entry.date.contains(q) || entry.title.toLowerCase().contains(q)) {
result.add(entry);
continue;
}
// 2์ฐจ: ๋ณธ๋ฌธ (ํ์ํ ๊ฒฝ์ฐ์๋ง ํ์ผ ์ถ๊ฐ ์ฝ๊ธฐ)
final raw = await readDiaryRaw(entry.path);
if (parseContentFromRaw(raw).toLowerCase().contains(q)) {
result.add(entry);
}
}
return result;
}
Dart
๋ณต์ฌ
๊ฒ์ ๋ฒ์: ๋ ์ง โ ์ ๋ชฉ โ ๋ณธ๋ฌธ ์์๋ก ํ์, ์กฐ๊ฑด ๋ง์กฑ ์ ์ฆ์ ์ถ๊ฐ
๋ค์ค ์ญ์ โ deleteMultiple()
Future<void> deleteMultiple(List<String> paths) async {
for (final p in paths) {
await deleteDiary(p); // File(path).delete()
}
}
Dart
๋ณต์ฌ
4-4. ์ ์ฒด ๋ฐ์ดํฐ ํ๋ฆ
์ฌ์ฉ์ ๋์
โ
โผ
HomePage / WritePage / DetailPage
โ ํธ์ถ
โผ
FileService (file_service.dart)
โ
โโโ ์ฐ๊ธฐ โ saveDiaryForDate / saveDiaryToPath / changeDiaryDate
โ โโโ File.writeAsString(_serialize(title, content))
โ
โโโ ์ฝ๊ธฐ โ getDiaryEntries / readDiaryRaw
โ โโโ File.readAsString โ parseTitleFromRaw / parseContentFromRaw
โ
โโโ ์ญ์ โ deleteDiary / deleteMultiple
โ โโโ File.delete()
โ
โโโ ๊ฒ์ โ searchEntries
โโโ getDiaryEntries + ๋ณธ๋ฌธ ์์ฐจ ๊ฒ์
Plain Text
๋ณต์ฌ
5. ํ๋ฉด ๊ตฌํ
5-1. HomePage โ ์ผ๊ธฐ ๋ชฉ๋ก ํ๋ฉด
๊ตฌํ ์์
1. _loadDiaries() โ getDiaryEntries() ํธ์ถ, ์ํ ์ ์ฅ
2. _buildNormalList() โ ListView.builder + Dismissible ์นด๋
3. _buildAppBar() โ ๊ฒ์ ๋ชจ๋ TextField / ์ผ๋ฐ ๋ชจ๋ ์ ๋ชฉ
4. _buildDrawer() โ ์ฌ์ด๋ ๋ฉ๋ด
5. _buildMultiSelectList() โ Set<String>์ผ๋ก ๋ค์ค ์ ํ ๊ด๋ฆฌ
6. ๊ฒ์ ๋ก์ง โ Timer ๋๋ฐ์ด์ค 500ms
Plain Text
๋ณต์ฌ
๊ฒ์ ๋๋ฐ์ด์ค ํจํด
Timer? _debounce;
void _onSearchChanged(String query) {
_debounce?.cancel(); // ์ด์ ํ์ด๋จธ ์ทจ์
_debounce = Timer(
const Duration(milliseconds: 500),
() async {
final results = await _fileService.searchEntries(query);
setState(() => _filteredList = results);
},
);
}
Dart
๋ณต์ฌ
์ด์ : ํค ์
๋ ฅ๋ง๋ค ํ์ผ ๊ฒ์์ ์คํํ๋ฉด ์ฑ๋ฅ ๋ญ๋น. 500ms ๋์ ์ถ๊ฐ ์
๋ ฅ์ด ์์ ๋๋ง ์คํ.
๋ค์ค ์ ํ ์ํ ๊ด๋ฆฌ
Set<String> _selectedPaths = {};
// ์ ์ฒด ์ ํ
_selectedPaths = _filteredList.map((e) => e.path).toSet();
// ์ ์ฒด ํด์
_selectedPaths.clear();
// ํ ๊ธ
_selectedPaths.contains(path)
? _selectedPaths.remove(path)
: _selectedPaths.add(path);
Dart
๋ณต์ฌ
5-2. WritePage โ ์ผ๊ธฐ ์์ฑ ํ๋ฉด
๊ตฌํ ์์
1. _titleController โ ์ ๋ชฉ ์
๋ ฅ ํ๋
2. _controller โ ๋ณธ๋ฌธ ์
๋ ฅ ํ๋ (expands: true๋ก ๋จ์ ๊ณต๊ฐ ์ฑ์)
3. _selectedDate โ DateTime ์ํ (๊ธฐ๋ณธ: ์ค๋)
4. _pickDate() โ calendar_date_picker2 ๋ค์ด์ผ๋ก๊ทธ
5. _save() โ ์ ํจ์ฑ ๊ฒ์ฌ โ saveDiaryForDate() โ Navigator.pop()
6. bottomSheet โ "์์ฑํ๊ธฐ" ๋ฒํผ (ํ๋ฉด ํ๋จ ๊ณ ์ )
Plain Text
๋ณต์ฌ
UI ๋ ์ด์์ ๊ตฌ์กฐ
Scaffold
โโโ AppBar (์ ์ฅ ์์ด์ฝ)
โโโ bottomSheet (์์ฑํ๊ธฐ ElevatedButton)
โโโ body (Padding fromLTRB(16, 16, 16, 80))
โโโ Column
โโโ InkWell โ ๋ ์ง ์ ํ Row
โโโ SizedBox(height: 14)
โโโ TextField (์ ๋ชฉ)
โโโ SizedBox(height: 12)
โโโ Expanded โ TextField (๋ณธ๋ฌธ, expands: true)
Plain Text
๋ณต์ฌ
5-3. DetailPage โ ์ผ๊ธฐ ์์ธ๋ณด๊ธฐ ยท ์์ ํ๋ฉด
๊ตฌํ ์์
1. _loadContent() โ readDiaryRaw() โ parseTitleFromRaw / parseContentFromRaw
2. ๋ณด๊ธฐ ๋ชจ๋ โ ์ ๋ชฉ(๊ตต๊ฒ) + Divider + ๋ณธ๋ฌธ ํ
์คํธ
3. ์์ ๋ชจ๋ ์ง์
โ _isEditing = true
4. ํธ์ง UI โ ๋ ์ง ์ ํ ํ + ์ ๋ชฉ TextField + ๋ณธ๋ฌธ TextField (Expanded)
5. _saveEdit() โ ๋ ์ง ๋ณ๊ฒฝ ์ฌ๋ถ ๋ถ๊ธฐ
โโโ ๋ ์ง ๋์ผ โ saveDiaryToPath() โ setState(_isEditing = false)
โโโ ๋ ์ง ๋ณ๊ฒฝ โ changeDiaryDate() โ Navigator.pop() (ํ์ผ๋ช
์ด ๋ฐ๋๋ฏ๋ก)
Plain Text
๋ณต์ฌ
๋ ์ง ๋ณ๊ฒฝ ์ ํ์ผ ์ฒ๋ฆฌ ์ ๋ต
if (_selectedDateStr != widget.date) {
await _fileService.changeDiaryDate(
widget.path, _selectedDateStr, title, content);
Navigator.pop(context); // ๋ชฉ๋ก ํ๋ฉด์ผ๋ก ๋์๊ฐ ์๋ ์๋ก๊ณ ์นจ
return;
}
Dart
๋ณต์ฌ
๋ ์ง๊ฐ ๋ฐ๋๋ฉด ํ์ผ๋ช
๋ ๋ฐ๋์ด์ผ ํ๋ฏ๋ก, ๊ธฐ์กด DetailPage๋ฅผ popํ๊ณ ๋ชฉ๋ก ํ๋ฉด์์ ์๋ก๊ณ ์นจํฉ๋๋ค.
5-4. CalendarPage โ ๋ฌ๋ ฅ ๋ทฐ ํ๋ฉด
๊ตฌํ ์์
1. _loadEntries() โ getDiaryEntries() โ Map<String, List<DiaryEntry>> ๋ณํ
2. _eventsFor(day) โ ๋ ์ง ํค โ ์ผ๊ธฐ ๋ชฉ๋ก ๋ฐํ (eventLoader)
3. TableCalendar โ ๋ง์ปค ํ์ + ๋ ์ง ์ ํ ์ด๋ฒคํธ
4. _onDaySelected() โ 1๊ฐ: ๋ฐ๋ก DetailPage / 2๊ฐ ์ด์: BottomSheet ์ ํ
5. _buildSelectedDayList() โ ๋ฌ๋ ฅ ์๋ ์ ํ์ผ ์ผ๊ธฐ ๋ชฉ๋ก
Plain Text
๋ณต์ฌ
๋ ์ง๋ณ Map ๊ตฌ์ฑ ๋ก์ง
final entries = await _fileService.getDiaryEntries();
final map = <String, List<DiaryEntry>>{};
for (final e in entries) {
map.putIfAbsent(e.date, () => []).add(e);
// "2026-03-26" โ [DiaryEntry, DiaryEntry, ...]
}
Dart
๋ณต์ฌ
๋ ์ง ์ ํ ์ด๋ฒคํธ ์ฒ๋ฆฌ
void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
final entries = _eventsFor(selectedDay);
if (entries.isEmpty) return;
if (entries.length == 1) {
_openDetail(entries.first); // 1๊ฐ โ ๋ฐ๋ก ์์ธ๋ณด๊ธฐ
} else {
_showPickerSheet(entries); // 2๊ฐ ์ด์ โ BottomSheet ๋ชฉ๋ก
}
}
Dart
๋ณต์ฌ
6. ์ฃผ์ UI ์ปดํฌ๋ํธ
6-1. Drawer (์ฌ์ด๋ ๋ฉ๋ด)
Scaffold์ drawer ์์ฑ์ ๋ฑ๋กํ๋ฉด AppBar ํ๋ฒ๊ฑฐ ์์ด์ฝ(โก)์ด ์๋ ์์ฑ๋ฉ๋๋ค.
Widget _buildDrawer() {
return Drawer(
child: ListView(
padding: EdgeInsets.zero, // DrawerHeader๊ฐ ์๋จ์ ๋ฑ ๋ถ๋๋ก
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.amber),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Icon(Icons.menu_book, size: 48, color: Colors.white),
SizedBox(height: 8),
Text('๋ด ์ผ๊ธฐ์ฅ', style: TextStyle(
color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)),
],
),
),
ListTile(
leading: const Icon(Icons.calendar_month),
title: const Text('๋ฌ๋ ฅ์ผ๋ก ๋ณด๊ธฐ'),
onTap: () {
Navigator.pop(context); // Drawer ๋จผ์ ๋ซ๊ธฐ
Navigator.push(context, MaterialPageRoute(
builder: (_) => const CalendarPage()));
},
),
],
),
);
}
Dart
๋ณต์ฌ
ํต์ฌ ํฌ์ธํธ
ํญ๋ชฉ | ์ค๋ช
|
padding: EdgeInsets.zero | ListView ๊ธฐ๋ณธ ์๋จ ํจ๋ฉ ์ ๊ฑฐ โ DrawerHeader๊ฐ ์๋จ์ ๋ฑ ๋ถ์ |
Navigator.pop(context) | Drawer๋ฅผ ๋จผ์ ๋ซ๊ณ ํ์ด์ง ์ด๋ํด์ผ ์์ฐ์ค๋ฌ์ด UX |
DrawerHeader | ๊ณ ์ ๋์ด(160px)์ ํค๋ ์์ญ |
ListTile | Drawer ๋ฉ๋ด์ ํ์ค ํญ๋ชฉ (leading ์์ด์ฝ + title ํ
์คํธ) |
6-2. Dismissible (์ค์์ดํ ์ญ์ )
๋ฆฌ์คํธ ํญ๋ชฉ์ ์ค์์ดํํ๋ฉด ์ฌ๋ผ์ง๊ฒ ๋ง๋๋ ์์ ฏ
Dismissible(
key: Key(entry.path), // ํ์ผ ๊ฒฝ๋ก๋ฅผ ๊ณ ์ ํค๋ก ์ฌ์ฉ
direction: DismissDirection.endToStart, // ์ผ์ชฝ ์ค์์ดํ๋ง ํ์ฉ
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 4),
decoration: BoxDecoration(
color: Colors.redAccent,
borderRadius: BorderRadius.circular(12),
),
child: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.delete, color: Colors.white, size: 28),
Text('์ญ์ ', style: TextStyle(color: Colors.white, fontSize: 12)),
],
),
),
confirmDismiss: (_) => showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('์ผ๊ธฐ ์ญ์ '),
content: Text('[$date] ์ผ๊ธฐ๋ฅผ ์ญ์ ํ์๊ฒ ์ต๋๊น?'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('์ทจ์')),
TextButton(onPressed: () => Navigator.pop(ctx, true),
child: const Text('์ญ์ ', style: TextStyle(color: Colors.red))),
],
),
),
onDismissed: (_) => _deleteSingle(entry.path),
child: Card(child: ListTile(...)),
)
Dart
๋ณต์ฌ
confirmDismiss vs onDismissed ์ฐจ์ด
์์ฑ | ์คํ ์์ | ๋ฐํ๊ฐ | ์ญํ |
confirmDismiss | ์ค์์ดํ ํ, ํญ๋ชฉ์ด ์ฌ๋ผ์ง๊ธฐ ์ | Future<bool?> | ์ญ์ ์ฌ๋ถ ์ต์ข
ํ์ธ |
onDismissed | confirmDismiss๊ฐ true ๋ฐํ ํ | ์์ | ์ค์ ์ญ์ ๋ก์ง ์คํ |
key๊ฐ ์ค์ํ ์ด์
// โ ์๋ชป๋ ์ โ ์ธ๋ฑ์ค๋ฅผ ํค๋ก ์ฌ์ฉ
key: Key(index.toString()) // ์ญ์ ํ ๋ค์ ํญ๋ชฉ์ด ๊ฐ์ ํค๋ฅผ ๊ฐ์ ธ ์ค๋์ ๊ฐ๋ฅ
// โ
์ฌ๋ฐ๋ฅธ ์ โ ๊ณ ์ ํ ์๋ณ์๋ฅผ ํค๋ก ์ฌ์ฉ
key: Key(entry.path) // ํ์ผ ๊ฒฝ๋ก๋ ํญ์ ๊ณ ์
Dart
๋ณต์ฌ
6-3. ModalBottomSheet (๋ฌ๋ ฅ ๋ค์ค ์ผ๊ธฐ ์ ํ)
๊ฐ์ ๋ ์ ์ผ๊ธฐ๊ฐ 2๊ฐ ์ด์์ผ ๋ ์ ํ ๋ชฉ๋ก์ ๋ณด์ฌ์ฃผ๋ ํจ๋
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (ctx) => Column(
mainAxisSize: MainAxisSize.min, // ๋ด์ฉ ๋์ด์๋ง ๋ง์ถค
children: [
Container(width: 40, height: 4,
decoration: BoxDecoration(color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2))),
ListView.builder(shrinkWrap: true, ...),
],
),
);
Dart
๋ณต์ฌ
์์ฑ | ์ค๋ช
|
mainAxisSize: MainAxisSize.min | Column์ด ์ปจํ
์ธ ๋์ด์๋ง ๋ง์ถฐ ์ค์ด๋ฆ |
shrinkWrap: true | ListView๊ฐ Column ์์์ ์คํฌ๋กค ์์ด ์ ์ฒด ๋์ด ์ฐจ์ง |
shape | ์๋จ ๋ชจ์๋ฆฌ ๋ฅ๊ทผ ์ฒ๋ฆฌ |
6-4. FloatingActionButton
ํ๋ฉด ์ฐํ๋จ์ ๊ณ ์ ๋ ์ํ ๋ฒํผ (์ ์ผ๊ธฐ ์ฐ๊ธฐ ์ก์
)
floatingActionButton: _isMultiSelect
? null // ๋ค์ค ์ ํ ๋ชจ๋์์๋ ์จ๊น
: FloatingActionButton(
onPressed: () async {
await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WritePage()),
);
_loadDiaries(); // ๋์์จ ํ ๋ชฉ๋ก ์๋ก๊ณ ์นจ
},
backgroundColor: Colors.amber,
foregroundColor: Colors.black,
child: const Icon(Icons.edit),
),
Dart
๋ณต์ฌ
6-5. BottomSheet (์์ฑํ๊ธฐ ๋ฒํผ)
Scaffold.bottomSheet์ ์์ ฏ์ ์ฌ๋ฆฌ๋ฉด ํ๋ฉด ํ๋จ์ ํญ์ ๊ณ ์ ๋ ์์ญ์ด ์๊น๋๋ค.
Scaffold(
bottomSheet: SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
child: SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton.icon(
onPressed: _isSaving ? null : _save,
icon: const Icon(Icons.edit_note),
label: const Text('์์ฑํ๊ธฐ'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.amber,
foregroundColor: Colors.black,
),
),
),
),
),
body: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80), // ํ๋จ 80px ํจ๋ฉ ์ถ๊ฐ
...
),
)
Dart
๋ณต์ฌ
SafeArea๋ฅผ ๊ฐ์ธ๋ ์ด์ : iOS ํ ์ธ๋์ผ์ดํฐ ๋ฑ ์์คํ
UI์ ๊ฒน์น์ง ์๋๋ก ์๋์ผ๋ก ์ฌ๋ฐฑ์ ์ถ๊ฐํฉ๋๋ค.
7. ํธ๋ฌ๋ธ์ํ
๋ฌธ์ 1: ํจํค์ง ๋ฒ์ ์ ์ฐพ์ ์ ์์
# ์๋ชป๋ ๋ฒ์ ์ง์ ์
calendar_date_picker2: ^0.9.0 # โ ์กด์ฌํ์ง ์๋ ๋ฒ์
Plain Text
๋ณต์ฌ
ํด๊ฒฐ: flutter pub add ํจํค์ง๋ช
์ฌ์ฉ โ pub.dev์์ ์ต์ ๋ฒ์ ์๋ ์ ํ
flutter pub add calendar_date_picker2 # โ ^2.0.1 ์๋ ์ค์น
Bash
๋ณต์ฌ
๋ฌธ์ 2: MultiSelectContainer ๋ ์ด์์ ์ถฉ๋
์ค๋ฅ ๋ฉ์์ง
RenderFlex children have non-zero flex but incoming height constraints are unbounded.
Plain Text
๋ณต์ฌ
์์ธ: MultiSelectContainer(๋ด๋ถ์ Wrap ์ฌ์ฉ)๋ฅผ SingleChildScrollView + Expanded ์์ ๋ฃ์ผ๋ฉด ๋์ด๊ฐ ๋ฌดํ๋๋ก ๊ณ์ฐ๋จ
ํด๊ฒฐ: ํจํค์ง ์ ๊ฑฐ ํ Set<String> + ListView.builder + ์ฒดํฌ ์์ด์ฝ์ผ๋ก ์ง์ ๊ตฌํ
๋ฌธ์ 3: ๊ธฐ์กด ํ์ผ์ ์ฝ๋๊ฐ ๋ง๋ถ์ฌ์ง
์์ธ: ํ์ผ ํธ์ง ๋๊ตฌ์ ๋งค์นญ ์คํจ ํ ํ์ผ ๋์ ๋ด์ฉ์ด append๋๋ ํ์
ํด๊ฒฐ: PowerShell Set-Content๋ก ํ์ผ ์ ์ฒด๋ฅผ ๋ฎ์ด์์
Set-Content -Path "๊ฒฝ๋ก\\ํ์ผ.dart" -Value $content -Encoding UTF8
PowerShell
๋ณต์ฌ
๋ฌธ์ 4: System Navigation Bar, BottomSheet ๋ฒํผ ๊ฒน์นจ
โข
๋ฌธ์ ๋ฐ์
โฆ
ํ๋จ ๋ฒํผ์ด ๋ค๋น๊ฒ์ด์
๋ฐ ๋ฐ์ผ๋ก ๋ค์ด๊ฐ
โข
์์ธ ์ถ์
โฆ
SafeArea ๋ฌธ์ ์ธ๊ฐ?
โฆ
BottomSheet ๊ตฌ์กฐ ๋ฌธ์ ์ธ๊ฐ?
โฆ
padding ๋ฌธ์ ์ธ๊ฐ?
โข
์์ธ ๋ฐ๊ฒฌ
โฆ
System Navigation Bar ๋์ด๋ฅผ ๊ณ ๋ ค ์ ํจ
โข
ํด๊ฒฐ
MediaQuery.of(context).padding.bottom
Plain Text
๋ณต์ฌ
โข
์ฌ๋ฐ ๋ฐฉ์ง
โฆ
ํ๋จ ๊ณ ์ ๋ฒํผ ๋ง๋ค ๋ ํญ์ MediaQuery padding ์ฌ์ฉ
โข
write_page.dart
โข
detail_page.dart
...
bottomSheet: SafeArea(
child: Padding(
// โก System Navigation Bar [ ||| ใ
< ] ์๋ก BottomSheet ๋ฒํผ ๊ฒน์น๋ ๋ฌธ์ ํด๊ฒฐ
// padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
padding: EdgeInsets.fromLTRB(16, 8, 16,MediaQuery.of(context).padding.bottom + 12, ),
...
C
๋ณต์ฌ
MediaQuery๋?
ํ์ฌ ๊ธฐ๊ธฐ์ ํ๋ฉด ์ ๋ณด ๊ฐ์ ธ์ค๋ ๊ฒ
MediaQuery.of(context)
Dart
๋ณต์ฌ
๊ฐ์ ธ์ฌ ์ ์๋ ์ ๋ณด ์:
โข
ํ๋ฉด ํฌ๊ธฐ
โข
์ํ๋ฐ ๋์ด
โข
ํค๋ณด๋ ๋์ด
โข
ํ๋จ ๋ค๋น๊ฒ์ด์
๋ฐ ๋์ด
โข
ํ๋ฉด ๋ฐฉํฅ ๋ฑ
padding.bottom ์๋ฏธ
MediaQuery.of(context).padding.bottom
Dart
๋ณต์ฌ
์์คํ
UI ๋๋ฌธ์ ์ฌ์ฉํ๋ฉด ์ ๋๋ ํ๋ฉด ์๋์ชฝ ์์ญ ๋์ด
์ฆ,
โข
์๋๋ก์ด๋ ๋ค๋น๊ฒ์ด์
๋ฐ ๋์ด
โข
์์ดํฐ ํ ์ธ๋์ผ์ดํฐ ๋์ด
โข
์ ์ค์ฒ ๋ค๋น๊ฒ์ด์
์์ญ
์ ์๋์ผ๋ก ์๋ ค์ค.
์์๋ก ๋ณด๋ฉด
๊ธฐ๊ธฐ๋ง๋ค ๊ฐ์ด ๋ค๋ฆ
๊ธฐ๊ธฐ | padding.bottom |
์๋๋ก์ด๋ ๋ฒํผ ์์ | 48 |
์๋๋ก์ด๋ ์ ์ค์ฒ | 16 |
์์ดํฐ | 34 |
PC / ์น | 0 |
๊ทธ๋์ ๊ณ ์ ๊ฐ padding ์ฃผ๋ฉด ์ ๋๊ณ MediaQuery ์จ์ผ ํจ
์ค์ ์ฌ์ฉ ์
ํ๋จ ๋ฒํผ ์ ๊ฐ๋ฆฌ๊ฒ
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom
),
child: ElevatedButton(
onPressed: () {},
child: Text("์ ์ฅ"),
),
)
Dart
๋ณต์ฌ
padding / viewInsets ์ฐจ์ด (์ค์)
Flutter์์ ์ด๊ฑฐ ์ํ ๋ฌธ์ ๊ธ์ผ๋ก ์ค์ํจ.
์ฝ๋ | ์๋ฏธ |
padding.bottom | ๋ค๋น๊ฒ์ด์
๋ฐ |
viewInsets.bottom | ํค๋ณด๋ |
viewPadding.bottom | SafeArea ํฌํจ ์์ญ |
ํค๋ณด๋ ์ฌ๋ผ์์ ๋
MediaQuery.of(context).viewInsets.bottom
Dart
๋ณต์ฌ
๋ค๋น๊ฒ์ด์ ๋ฐ
MediaQuery.of(context).padding.bottom
Dart
๋ณต์ฌ
์ค๋ฌด์์ ์ ์ผ ๋ง์ด ์ฐ๋ ์ฝ๋
ํ๋จ ๋ฒํผ + ํค๋ณด๋ ๋์๊น์ง
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom +
MediaQuery.of(context).viewInsets.bottom
)
Dart
๋ณต์ฌ
์ด๊ฑฐ ํ๋ ๊ธฐ์ตํ๋ฉด Flutter ํ๋จ UI ๋ฌธ์ ๊ฑฐ์ ๋ค ํด๊ฒฐ๋จ.
ํ ์ค ์ ๋ฆฌ
MediaQuery.of(context).padding.bottom
Dart
๋ณต์ฌ
8. ํต์ฌ Flutter ๊ฐ๋ ์์ฝ
๊ฐ๋
| ์ฌ์ฉ ์์น | ์ค๋ช
|
StatefulWidget | ๋ชจ๋ ํ์ด์ง | ์ํ๊ฐ ๋ฐ๋๋ ํ๋ฉด |
setState() | ๋ชจ๋ ํ์ด์ง | UI ์ฌ๋น๋ ํธ๋ฆฌ๊ฑฐ |
async/await | ํ์ผ I/O | ๋น๋๊ธฐ ์ฒ๋ฆฌ |
Navigator.push/pop | ํ์ด์ง ์ ํ | ์คํ ๊ธฐ๋ฐ ๋ผ์ฐํ
|
TextEditingController | ์
๋ ฅ ํ๋ | ํ
์คํธ ์ฝ๊ธฐ/์ด๊ธฐํ |
mounted ์ฒดํฌ | ๋น๋๊ธฐ ํ setState | ์์ ฏ์ด ํธ๋ฆฌ์์ ์ ๊ฑฐ๋ ๊ฒฝ์ฐ ๋ฐฉ์ง |
Timer | ๊ฒ์ ๋๋ฐ์ด์ค | ์ง์ฐ ์คํ |
Set<T> | ๋ค์ค ์ ํ | O(1) ์ค๋ณต ์๋ ๊ฒฝ๋ก ์งํฉ |
Map<K, List<V>> | ๋ฌ๋ ฅ ์ด๋ฒคํธ | ๋ ์ง๋ณ ์ผ๊ธฐ ๊ทธ๋ฃนํ |
Platform.pathSeparator | ๊ฒฝ๋ก ํ์ฑ | Windows(\\) / macOSยทLinux(/) ์๋ ๋์ |
SafeArea | bottomSheet | ์์คํ
UI ๊ฒน์นจ ๋ฐฉ์ง |
expands: true | TextField | ๋จ์ ๊ณต๊ฐ ์ ์ฒด ์ฑ์ |
9. ๊ธฐ๋ฅ ๋ฆฌ์คํธ
๊ธฐ๋ฅ | ๋ด๋น ํ์ผ |
ํ์ผ ์ ์ฅ/์ฝ๊ธฐ/์ญ์ | file_service.dart |
ํ๋ฃจ ์ฌ๋ฌ ์ผ๊ธฐ | file_service.dart (HHmmss ํ์ผ๋ช
) |
์ ๋ชฉ + ๋ณธ๋ฌธ ๋ถ๋ฆฌ ์ ์ฅ | file_service.dart (---DIARY--- ๊ตฌ๋ถ์) |
๋ ์ง ์ ํ (์์ฑ) | write_page.dart |
๋ ์ง ์ ํ (์์ ) | detail_page.dart |
๋ ์ง ๋ณ๊ฒฝ ์ ํ์ผ๋ช
๊ต์ฒด | file_service.dart changeDiaryDate() |
Drawer ์ฌ์ด๋ ๋ฉ๋ด | home_page.dart |
Dismissible ์ค์์ดํ ์ญ์ | home_page.dart |
๋ค์ค ์ ํ ์ญ์ | home_page.dart (Set<String>) |
๊ฒ์ (๋ ์งยท์ ๋ชฉยท๋ณธ๋ฌธ) | home_page.dart + file_service.dart |
๊ฒ์ ๋๋ฐ์ด์ค | home_page.dart (Timer) |
๋ฌ๋ ฅ ๋ทฐ + ์ด๋ฒคํธ ๋ง์ปค | calendar_page.dart |
๋ฌ๋ ฅ ๋ค์ค ์ผ๊ธฐ BottomSheet | calendar_page.dart |
bottomSheet ์์ฑํ๊ธฐ ๋ฒํผ | write_page.dart, detail_page.dart |















