728x90
📘 Diary Screen 기능별 학습 정리
✅ 1. 전체 개요
diary_screen.dart는 일기 조회 및 작성 기능을 가진 Flutter 화면 구성 파일이다. 이 화면에서는 사용자의 일기 목록을 보여주고, 추가, 상세보기 등의 동작이 포함된다. (추후 삭제, 수정 추가예정)
주요 구성 요소:
- StatefulWidget 기반 UI
- FutureBuilder를 활용한 비동기 데이터 처리
- Dismissible을 이용한 스와이프 삭제
- 네비게이션을 통한 상세 페이지 이동
🧱 2. 위젯 구조 및 상태관리
핵심 위젯: DiaryScreen
class DiaryScreen extends StatefulWidget {
@override
_DiaryScreenState createState() => _DiaryScreenState();
}
- StatefulWidget을 사용한 이유: 비동기 데이터를 불러오고 상태 변경에 따라 화면을 갱신하기 위함.
- 내부 상태에서 다이어리 목록 데이터를 유지하고, 삭제 등 상태 변화에 대응.
🔄 3. 비동기 데이터 로딩 (FutureBuilder)
FutureBuilder<List<Diary>>(
future: _diaryListFuture,
builder: (context, snapshot) { ... }
)
- FutureBuilder는 비동기 작업의 결과를 기반으로 UI를 렌더링.
- 상태별 처리:
- ConnectionState.waiting: 로딩 중 UI 표시
- hasError: 에러 처리
- hasData: 실제 데이터 렌더링
💡 Tip: Future를 미리 상태에 저장(_diaryListFuture)함으로써 setState를 호출해도 불필요한 재요청을 방지할 수 있음.
🗑️ 4. 스와이프 삭제 (Dismissible)
Dismissible(
key: Key(diary.id.toString()),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
setState(() {
// 리스트에서 제거
});
// DB 또는 서버에서 삭제 처리
},
background: Container(
color: Colors.red,
child: Icon(Icons.delete, color: Colors.white),
),
child: ListTile(...),
)
- Dismissible을 사용하여 오른쪽 → 왼쪽 스와이프 시 삭제.
- 삭제 UI 피드백 제공 (background).
- 삭제 후 setState로 UI 갱신.
📆 5. 일기 리스트 UI (ListView.builder)
ListView.builder(
itemCount: diaryList.length,
itemBuilder: (context, index) {
final diary = diaryList[index];
return Dismissible(...); // 위에서 설명한 삭제 기능 포함
},
)
- 리스트를 효율적으로 렌더링하는 ListView.builder.
- 각 아이템에 대해 Dismissible로 감싸 일기 항목 UI 구성.
➕ 6. 일기 추가 버튼 (FloatingActionButton)
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(...); // 작성 화면으로 이동
},
child: Icon(Icons.add),
)
- FloatingActionButton을 이용해 일기 추가 페이지로 이동.
- 작성 후 돌아올 때 setState() 또는 Navigator.pop(context, result)를 활용하면 화면 갱신 가능.
📄 7. 상세 페이지 이동
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DiaryDetailScreen(diary: diary),
),
);
}
- 일기 항목을 탭하면 상세보기 화면으로 이동.
- DiaryDetailScreen은 해당 다이어리 데이터를 받아 상세 UI를 표시.
✍️일기 화면 전체 코드
더보기
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart' show DateFormat;
import '../widgets/profile_menu_widget.dart';
// 더미데이터
// 전역에 선언된 일기 리스트 (임시 저장소)
List<Map<String, dynamic>> diary_item = [
{
'id': 0,
'title': '일기 제목 1',
'content': '오늘 하루는 정말 즐거웠어요!',
'date': DateTime(2025, 5, 1),
'emoji': '😊',
'weather': '☀️',
'isLocked': false,
'pw': '',
},
{
'id': 1,
'title': '일기 제목 2',
'content': '비가 내려서 우울했어요.',
'date': DateTime(2025, 5, 2),
'emoji': '☔️',
'weather': '🌧️',
'isLocked': false,
'pw': '',
},
{
'id': 2,
'title': '일기 제목 3',
'content': '오늘은 시험이 있었어요.',
'date': DateTime(2025, 5, 3),
'emoji': '📚',
'weather': '☁️',
'isLocked': true,
'pw': '1234',
},
{
'id': 3,
'title': '일기 제목 4',
'content': '친구와 맛집에 갔어요!',
'date': DateTime(2025, 5, 4),
'emoji': '🍔',
'weather': '☀️',
'isLocked': false,
'pw': '',
},
{
'id': 4,
'title': '일기 제목 5',
'content': '꽃이 피는 계절이 왔어요.',
'date': DateTime(2025, 5, 5),
'emoji': '🌸',
'weather': '🌸',
'isLocked': false,
'pw': '',
},
{
'id': 5,
'title': '일기 제목 6',
'content': '오늘은 긴 하루였어요.',
'date': DateTime(2025, 5, 6),
'emoji': '😅',
'weather': '🌥️',
'isLocked': false,
'pw': '',
},
{
'id': 6,
'title': '일기 제목 7',
'content': '운동하고 와서 기분 좋아요.',
'date': DateTime(2025, 5, 7),
'emoji': '💪',
'weather': '☀️',
'isLocked': false,
'pw': '',
},
{
'id': 7,
'title': '일기 제목 8',
'content': '저녁에 별이 정말 아름다웠어요.',
'date': DateTime(2025, 5, 8),
'emoji': '🌟',
'weather': '☀️',
'isLocked': false,
'pw': '',
},
{
'id': 8,
'title': '일기 제목 9',
'content': '집에서 편히 쉬었어요.',
'date': DateTime(2025, 5, 9),
'emoji': '🛋️',
'weather': '구름 많음',
'isLocked': false,
'pw': '',
},
{
'id': 9,
'title': '일기 제목 10',
'content':
'오늘은 특별한 일이 없었어요.\n\n\n\n\n\n\n\nnnnnnn\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nnn',
'date': DateTime(2025, 5, 10),
'emoji': '😴',
'weather': '🌧️',
'isLocked': false,
'pw': '',
},
];
/** // 리스트에 추가
diary_item.add({
'id': id,
'title': title,
'content': content,
'date': selectedDate,
'emoji': mainEmoji,
'weather': weatherEmoji,
'isLocked': isLocked,
'pw': pwController.text,
});
*/
/// 1. 일기 목록 화면
class DiaryListScreen extends StatefulWidget {
@override
_DiaryListScreen createState() => _DiaryListScreen();
}
class _DiaryListScreen extends State<DiaryListScreen> {
// 검색창
final TextEditingController searchController = TextEditingController();
static const int pageSize = 20;
int currentPage = 1;
List<Map<String, dynamic>> get currentItems {
final start = (currentPage - 1) * pageSize;
final end = (start + pageSize) > diary_item.length
? diary_item.length
: start + pageSize;
return diary_item.sublist(start, end);
}
int get totalPages => (diary_item.length / pageSize).ceil();
List<int> getPageNumbers() {
int startPage = currentPage - 2;
int endPage = currentPage + 2;
if (startPage < 1) {
endPage += (1 - startPage);
startPage = 1;
}
if (endPage > totalPages) {
startPage -= (endPage - totalPages);
endPage = totalPages;
}
if (startPage < 1) startPage = 1;
List<int> pages = [];
for (int i = startPage; i <= endPage; i++) {
pages.add(i);
}
return pages;
}
void goToPage(int page) {
if (page >= 1 && page <= totalPages) {
setState(() {
currentPage = page;
});
}
}
late ScrollController listScrollController;
@override
void initState() {
super.initState();
listScrollController = ScrollController();
}
@override
void dispose() {
listScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: IconButton(
icon: const Icon(Icons.home),
onPressed: () => Navigator.pushNamed(context, '/home'),
),
),
title: const Text('ઇଓ FLY 다이어리'),
centerTitle: true,
elevation: 0,
actions: [
// 여기서 프로필 이미지 클릭 메뉴 위젯 호출
ProfileMenuWidget(
profileImageUrl: 'https://path-to-your-profile-image.jpg',
),
],
),
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: Column(
children: [
Container(
// EdgeInsets.fromLTRB(left, top, right, bottom)
padding: const EdgeInsets.fromLTRB(5, 2, 5, 2),
child: TableCalendar(
focusedDay: DateTime.now(),
firstDay: DateTime(2020, 1, 1),
lastDay: DateTime(2030, 12, 31),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(
8, 10, 8, 0), // EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
DropdownButton<String>(
value: '최신순',
items: ['최신순', '오래된순', '제목순']
.map((e) =>
DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (val) {},
),
SizedBox(
width: 50,
),
Expanded(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: TextField(
controller: searchController,
decoration: const InputDecoration(
labelText: '검색 🔍',
border: OutlineInputBorder(),
),
),
),
),
ElevatedButton(
onPressed: () {
// 검색 로직
print(searchController.text);
},
child: Icon(Icons.search),
),
],
),
),
],
),
),
];
},
// 본문의 리스트와 페이징
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
// 리스트 보여주는 부분 (스크롤 가능)
Expanded(
child: ListView.builder(
controller: listScrollController,
itemCount: currentItems.length,
itemBuilder: (context, index) {
/* return ListTile(
title: Text(currentItems[index]),
);*/
final diary = currentItems[index]; // 현재 보이는 목록
return ListTile(
title: Text(
'${diary['emoji']} ${diary['title']}'), // 저장된 제목 사용
subtitle: Text(
(() {
final lines = diary['content']
.toString()
.split('\n'); // 줄바꿈 단위로 자른 배열
final firstLine = lines.first; // 첫째줄
return firstLine.length > 30
? '${firstLine.substring(0, 30)}...'
: firstLine;
})(),
),
// 내용 일부만 미리보기 첫줄 or 앞에 30자만
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DiaryDetailScreen(diary: diary), // diary 자체 전달
),
);
},
);
},
),
),
//페이징 버튼
Container(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 이전 페이지
IconButton(
icon: Icon(Icons.chevron_left),
onPressed: currentPage > 1
? () => goToPage(currentPage - 1)
: null,
),
// 페이지 번호 버튼들
...getPageNumbers().map((page) => GestureDetector(
onTap: () => goToPage(page),
child: Container(
margin: EdgeInsets.symmetric(horizontal: 4),
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: page == currentPage
? Colors.blue
: Colors.grey[200],
borderRadius: BorderRadius.circular(4),
),
child: Text(
'$page',
style: TextStyle(
color: page == currentPage
? Colors.white
: Colors.black,
),
),
),
)),
// 다음 페이지
IconButton(
icon: Icon(Icons.chevron_right),
onPressed: currentPage < totalPages
? () => goToPage(currentPage + 1)
: null,
),
],
),
),
],
),
),
),
// 플로팅 추가 버튼
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const WriteDiaryScreen(),
),
);
if (result == true) {
// 새 일기가 추가되었음을 감지하고 화면 갱신
(context as Element).markNeedsBuild(); // 또는 상태관리 적용
}
},
child: const Icon(Icons.add),
),
);
}
}
/// 2. 일기 작성 화면 (입력받고 저장 시 콘솔에 출력)
class WriteDiaryScreen extends StatefulWidget {
const WriteDiaryScreen({Key? key}) : super(key: key);
@override
State<WriteDiaryScreen> createState() => _WriteDiaryScreenState();
}
class _WriteDiaryScreenState extends State<WriteDiaryScreen> {
// 제목창
final TextEditingController titleController = TextEditingController();
// 대표 이모지, 날씨 이모지 < 버튼으로
String mainEmoji = '🪽';
String weatherEmoji = '☀️'; // 일기예보 연결하고 현재날씨로 바꾸기
// 잠금 여부 - pw + 토글
bool isLocked = false;
final TextEditingController pwController = TextEditingController();
// 기록일자
//final TextEditingController searchController = TextEditingController();
late DateTime selectedDate = DateTime.now();
// 배경
final TextEditingController bgController = TextEditingController();
// 내용
final TextEditingController contentController = TextEditingController();
// 저장 버튼 클릭 시 호출되는 함수
void _saveDiary() {
final title = titleController.text;
final content = contentController.text;
// 아이디 부여: 리스트가 비어있으면 0, 아니면 마지막 id + 1
final int id = diary_item.isEmpty ? 0 : diary_item.last['id'] + 1;
// 리스트에 추가
diary_item.add({
'id': id,
'title': title,
'content': content,
'date': selectedDate,
'emoji': mainEmoji,
'weather': weatherEmoji,
'isLocked': isLocked,
'pw': pwController.text,
});
print('제목: $title');
print('내용: $content');
// 작성 화면 종료
Navigator.pop(context, true);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: IconButton(
icon: const Icon(Icons.arrow_back),
// onPressed: () => Navigator.pop(context, true),
onPressed: () => Navigator.pushNamed(context, '/diary'),
),
),
title: const Text('ઇଓ 일기'),
centerTitle: true,
elevation: 0,
),
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverToBoxAdapter(
child: Column(
children: [
/** 작성 항목
* 제목
* 잠금 여부
* 대표 이모지, 날씨 이모지 (기본값:🪽, 현재 날씨)
* 기록일자 ( 클릭하면 달력 띄우기, 기본값 오늘 )
* 배경 이미지 ( 이미지3개 이상 중에 선택)
* 내용 ( 중간에 이미지, 링크, 줄띄우기, 폰트 변경, 글자크기, 굵게쓰기, 기울이기, 밑줄, 취소선 가능하게)
*/
// 제목 입력 필드
TextField(
controller: titleController, // 제목 컨트롤러 연결
decoration: const InputDecoration(
labelText: '제목',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 5),
// 잠금 설정 필드
// TextField(
// controller: pwController, // 잠금 설정 컨트롤러 연결
// decoration: const InputDecoration(
// labelText: '잠금 설정, 비밀번호 설정',
// border: OutlineInputBorder(),
// ),
// ),
// const SizedBox(height: 5),
Row(
children: [
Text('대표 이모지'),
DropdownButton<String>(
value: mainEmoji,
items: ['🪽', '😊', '😴', '🔥']
.map((e) =>
DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (val) => setState(() => mainEmoji = val!),
),
Text('날씨'),
DropdownButton<String>(
value: weatherEmoji,
items: ['☀️', '🌧️', '⛅', '🌩️']
.map((e) =>
DropdownMenuItem(value: e, child: Text(e)))
.toList(),
onChanged: (val) => setState(() => weatherEmoji = val!),
),
Text('잠금 여부'),
Switch(
value: isLocked,
onChanged: (val) {
setState(() {
isLocked = val;
});
},
),
if (isLocked)
Expanded(
child: TextField(
controller: pwController,
obscureText: true,
decoration: InputDecoration(labelText: '비밀번호'),
),
),
],
),
const SizedBox(height: 5),
ListTile(
title: Text(
'기록일자: ${DateFormat('yyyy-MM-dd').format(selectedDate)}'),
trailing: Icon(Icons.calendar_today),
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: selectedDate,
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
setState(() => selectedDate = picked);
}
},
),
const SizedBox(height: 5),
// 배경 선택 필드
TextField(
controller: bgController,
decoration: InputDecoration(
labelText: '배경',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 5),
TextField(
controller: contentController,
maxLines: 10,
decoration: const InputDecoration(
labelText: '내용',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _saveDiary,
child: const Text('저장'),
),
],
),
),
];
},
body: SizedBox.shrink(),
),
);
}
}
////////////////////////////////////////////////////////////////
/// 3. 일기 상세 보기 화면 (선택한 일기의 제목과 내용 표시)
class DiaryDetailScreen extends StatelessWidget {
final Map<String, dynamic> diary;
const DiaryDetailScreen({Key? key, required this.diary}) : super(key: key);
@override
Widget build(BuildContext context) {
final String formattedDate = DateFormat('yyyy-MM-dd').format(diary['date']);
return Scaffold(
appBar: AppBar(
title: Text(
'${diary['emoji'] ?? ''} ${diary['title'] ?? ''}',
style: const TextStyle(fontSize: 24),
),
actions: [
// 여기서 프로필 이미지 클릭 메뉴 위젯 호출
ProfileMenuWidget(
profileImageUrl: 'https://path-to-your-profile-image.jpg',
),
],
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 날짜 + 날씨
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
'$formattedDate ${diary['weather'] ?? ''}',
style: const TextStyle(fontSize: 18, color: Colors.grey),
),
],
),
),
// 본문 내용 (스크롤 가능하게)
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
diary['content'] ?? '',
style: const TextStyle(fontSize: 18),
),
),
),
],
),
);
}
}
'Flutter + Dart > 다이어리 프로젝트' 카테고리의 다른 글
일정화면 만들기 (1) | 2025.05.19 |
---|---|
달력, 로컬 설정 (0) | 2025.05.15 |
flutter 키, 값을 가지는 리스트 만들기 (0) | 2025.05.14 |
ListView + Column 문제 (0) | 2025.05.14 |
트러블 슈팅 - Container 위젯 내에 children 속성 사용 오류 (0) | 2025.05.13 |