728x90
로컬 데이터베이스 Hive를 활용하며, 깔끔한 UI와 객체지향 구조를 갖춘 경량 블로그 앱을 만들어 봅니다.
기능 설명
1. 글 목록 화면 (PostListPage)
- Hive에 저장된 글을 불러와 목록으로 표시
- 각 글은 카드 형태로 표시되며:
- 제목, 내용 일부, 댓글 개수 표시
- 홀/짝수 행마다 배경색 다르게 적용
- 각 글을 탭하면 상세 페이지로 이동
- 플로팅 버튼(➕) 클릭 시 글쓰기 화면으로 이동
2. 글 작성 화면 (PostWritePage)
- 제목과 내용 입력 후 저장 가능
- 저장 시 Hive에 새 Post 객체로 추가
- 저장 완료 후 글 목록 화면으로 이동
3. 글 상세 화면 (PostDetailPage)
- 선택한 글의 제목 + 전체 내용 표시
- 글 삭제 버튼 → Hive에서 해당 글 제거 후 목록으로 이동
- 목록으로 버튼 → 이전 화면으로 이동
- 스크롤 지원: 글 내용이 길면 화면 전체 스크롤 가능
- 댓글 타이틀 위에 구분선(Divider) 추가
- UI는 다크 테마에 최적화 (블랙 배경 + 흰 텍스트)
4. 댓글 기능 (CommentList, CommentPopup)
- 댓글은 List<String> 형태로 Post 객체 안에 저장
- 댓글 목록:
- 카드 스타일
- 홀/짝수 댓글 구분 배경
- 댓글 오른쪽에 수정/삭제 아이콘 있음
- 댓글 작성:
- 플로팅 버튼 클릭 시 팝업 입력창 표시
- 팝업에서 작성 후 등록하면 곧바로 반영
5. 구조적 특징
- 전 화면 다크 테마 적용
- 객체지향 리팩토링 완료:
- 각 UI 구성 요소는 개별 위젯 클래스로 분리됨
- 디렉토리 구조
lib/
├── main.dart
├── models/
├── screens/
├── widgets/
기술적 요소
항목 | 사용 |
상태 저장 | Hive (로컬 DB) |
상태 반영 | ValueListenableBuilder |
네비게이션 | Navigator.push / pop / pushAndRemoveUntil |
디자인 | Card + Dark Theme + Divider + Padding |
데이터 구조 | Post(title, content, comments[], timestamp) |
플러그인 및 권한 설정
pubspec.yaml
dependencies:
flutter:
sdk: flutter
hive: ^2.2.3
hive_flutter: ^1.1.0
path_provider: ^2.1.1
dev_dependencies:
flutter_test:
sdk: flutter
hive_generator: ^2.0.1
build_runner: ^2.4.6
app/src/main/AndroidManifest.xml
<!-- 인터넷 권한 -->
<uses-permission android:name="android.permission.INTERNET"/>
- 네트워크 권한만 추가해주면됩니다.
lib/ 디렉토리 구조
lib/
├── main.dart
├── models/
│ ├── post.dart
│ └── post.g.dart
├── screens/
│ ├── post_detail_page.dart
│ ├── post_list_page.dart
│ └── post_write_page.dart
└── widgets/
├── comment_list.dart
├── comment_popup.dart
├── comment_title.dart
├── post_action_buttons.dart
├── post_card.dart
├── post_content.dart
└── post_list.dart
- models/: 데이터 모델 (Post)
- screens/: 주요 화면 구성 (목록, 상세, 작성)
- widgets/: 분리된 재사용 가능한 UI 컴포넌트
728x90
블로그 앱 코드
models/post.dart
// Hive 패키지를 사용하여 로컬 NoSQL DB 기능 제공
import 'package:hive/hive.dart';
// 코드 생성 도구를 위한 part 파일 (build_runner로 생성됨)
// 이 파일은 Hive가 객체를 직렬화/역직렬화할 수 있게 도와줌
part 'post.g.dart';
// 이 클래스를 Hive에서 저장 가능한 객체로 정의하며,
// typeId는 고유 식별자로 Hive 내부에서 Post 타입을 구분하는 용도
@HiveType(typeId: 0)
class Post extends HiveObject {
// 글 제목 (Hive에서 필드 순번 0번)
@HiveField(0)
String title;
// 글 본문 내용 (필드 순번 1번)
@HiveField(1)
String content;
// 글 작성 시간 (필드 순번 2번)
@HiveField(2)
DateTime timestamp;
// 댓글 목록 (단순 문자열 리스트로 구성됨, 필드 순번 3번)
@HiveField(3)
List<String> comments;
// 생성자: title, content, timestamp는 필수이며,
// comments는 기본값으로 빈 리스트를 사용
Post({
required this.title,
required this.content,
required this.timestamp,
this.comments = const [],
});
}
요소 | 설명 |
@HiveType | Hive가 객체를 저장할 수 있도록 식별 타입 부여 |
@HiveField(n) | Hive가 각 필드를 인덱스로 저장 (변경 시 순서 유지 필수) |
HiveObject 상속 | Hive에서 .save(), .delete() 같은 메서드 사용 가능 |
post.g.dart | build_runner로 자동 생성되는 직렬화 코드 포함 파일 flutter pub run build_runner build |
widgets/comment_list.dart
Hive 기반의 댓글 목록 UI로, 댓글 표시, 수정, 삭제 기능을 모두 포함하고 있습니다.
// Flutter UI 및 Hive 연결
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/post.dart';
// 댓글 목록을 보여주는 위젯
class CommentList extends StatelessWidget {
final dynamic postKey; // 게시글 키 (Hive의 key 타입은 dynamic 가능)
const CommentList({super.key, required this.postKey});
@override
Widget build(BuildContext context) {
final box = Hive.box<Post>('posts'); // Hive에서 Post 박스 열기
return ValueListenableBuilder(
valueListenable: box.listenable(), // Hive의 데이터 변경을 실시간 감지
builder: (context, Box<Post> box, _) {
final updatedPost = box.get(postKey); // 현재 글 객체 가져오기
if (updatedPost == null) return const SizedBox.shrink(); // 없으면 빈 공간 반환
return ListView.separated(
shrinkWrap: true, // Column 내부에서 쓸 수 있게 wrap
physics: const NeverScrollableScrollPhysics(), // 중첩 스크롤 방지
itemCount: updatedPost.comments.length, // 댓글 개수
padding: const EdgeInsets.symmetric(horizontal: 16),
separatorBuilder: (_, __) => const SizedBox(height: 8), // 댓글 사이 여백
itemBuilder: (context, index) {
final comment = updatedPost.comments[index]; // 댓글 하나씩
return Container(
decoration: BoxDecoration(
color: index % 2 == 0 ? Colors.grey[850] : Colors.grey[800], // 홀/짝 배경색
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey.shade700),
),
child: ListTile(
title: Text(
comment,
style: const TextStyle(color: Colors.white), // 댓글 내용
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.tealAccent, size: 20),
onPressed: () =>
_showEditDialog(context, postKey, index, comment), // 수정
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.redAccent, size: 20),
onPressed: () => _deleteComment(postKey, index), // 삭제
),
],
),
),
);
},
);
},
);
}
// 댓글 삭제: 댓글 인덱스를 기준으로 제거하고 Hive에 다시 저장
void _deleteComment(dynamic postKey, int index) {
final box = Hive.box<Post>('posts');
final post = box.get(postKey);
if (post != null) {
final updatedComments = List<String>.from(post.comments)..removeAt(index);
final updated = Post(
title: post.title,
content: post.content,
timestamp: post.timestamp,
comments: updatedComments,
);
box.put(postKey, updated); // 업데이트된 Post 객체로 덮어쓰기
}
}
// 댓글 수정 다이얼로그: 팝업에서 댓글 수정 후 저장
void _showEditDialog(
BuildContext context, dynamic postKey, int index, String oldComment) {
final controller = TextEditingController(text: oldComment); // 초기값 설정
showDialog(
context: context,
builder: (context) {
return AlertDialog(
backgroundColor: Colors.grey[900],
title: const Text('댓글 수정', style: TextStyle(color: Colors.white)),
content: TextField(
controller: controller,
maxLines: 3,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
hintText: '댓글을 수정하세요',
hintStyle: TextStyle(color: Colors.white38),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.white30),
),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.tealAccent),
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('취소', style: TextStyle(color: Colors.white60)),
),
ElevatedButton(
onPressed: () {
final newComment = controller.text.trim();
if (newComment.isEmpty) return;
final box = Hive.box<Post>('posts');
final post = box.get(postKey);
if (post != null) {
final updatedComments = List<String>.from(post.comments);
updatedComments[index] = newComment;
final updated = Post(
title: post.title,
content: post.content,
timestamp: post.timestamp,
comments: updatedComments,
);
box.put(postKey, updated); // 수정된 댓글 반영
}
Navigator.pop(context); // 팝업 닫기
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.tealAccent.shade700,
foregroundColor: Colors.black,
),
child: const Text('저장'),
),
],
);
},
);
}
}
기능 | 구현 방식 |
댓글 표시 | ListView.separated로 스타일 적용된 카드 리스트 |
댓글 수정 | 다이얼로그 팝업 → TextField로 수정 → Hive 업데이트 |
댓글 삭제 | 인덱스로 삭제 후 Hive.put() |
스타일 | 다크 테마 대응 카드 스타일 (홀/짝 배경, 둥근 테두리 등) |
widgets/comment_popup.dart
이 코드는 댓글 작성 팝업으로, 사용자가 댓글을 입력하고 저장할 수 있는 기능을 제공합니다.
// Flutter UI와 Hive 로컬 DB 사용
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import '../models/post.dart';
// 댓글 작성용 팝업을 띄우는 StatefulWidget
class CommentPopup extends StatefulWidget {
final dynamic postKey; // 댓글을 추가할 대상 게시글의 key
const CommentPopup({super.key, required this.postKey});
@override
State<CommentPopup> createState() => _CommentPopupState();
}
class _CommentPopupState extends State<CommentPopup> {
final _controller = TextEditingController(); // 댓글 입력 컨트롤러
// 댓글을 추가하고 Hive에 저장하는 비동기 함수
Future<void> _addComment() async {
final text = _controller.text.trim(); // 입력된 텍스트 정리
if (text.isEmpty) return; // 공백이면 무시
final box = Hive.box<Post>('posts'); // Hive에서 Post 박스 열기
final post = box.get(widget.postKey); // 현재 게시글 가져오기
if (post != null) {
// 기존 댓글 목록에 새 댓글 추가하여 새로운 Post 객체 생성
final updated = Post(
title: post.title,
content: post.content,
timestamp: post.timestamp,
comments: [...post.comments, text], // 댓글 추가
);
await box.put(widget.postKey, updated); // 덮어쓰기
}
Navigator.pop(context); // 팝업 창 닫기
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('댓글 작성'),
content: TextField(
controller: _controller,
maxLines: 3,
decoration: const InputDecoration(
hintText: '댓글을 입력하세요',
),
),
actions: [
// 취소 버튼 → 팝업 닫기
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('취소'),
),
// 등록 버튼 → _addComment 호출
ElevatedButton(
onPressed: _addComment,
child: const Text('등록'),
),
],
);
}
}
항목 | 내용 |
팝업 형태 | AlertDialog 사용 |
입력 | TextField + TextEditingController |
저장 | 댓글을 기존 목록에 추가 후 Hive.put()으로 저장 |
닫기 | Navigator.pop() |
widgets/comment_title.dart
이 위젯은 댓글 목록 위에 표시되는 섹션 제목 역할을 합니다.
// Flutter UI 라이브러리
import 'package:flutter/material.dart';
// 댓글 섹션의 타이틀(헤더)을 나타내는 StatelessWidget
class CommentTitle extends StatelessWidget {
const CommentTitle({super.key}); // const 생성자로 불변성 유지
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 16), // 좌우 여백 설정
child: Align(
alignment: Alignment.centerLeft, // 텍스트를 왼쪽 정렬
child: Text(
'댓글', // 실제 표시되는 타이틀 텍스트
style: TextStyle(
fontWeight: FontWeight.bold, // 굵은 글씨
fontSize: 16, // 폰트 크기
color: Colors.white, // 다크 테마에 어울리는 밝은 색상
),
),
),
);
}
}
항목 | 설명 |
역할 | 댓글 리스트 섹션 상단에 "댓글"이라는 타이틀 표시 |
구성 | Padding + Align + Text로 구성 |
정렬 | 왼쪽 정렬 (Alignment.centerLeft) |
디자인 | 흰색 굵은 글자, 크기 16 (다크 테마 기준) |
widgets/post_action_buttons.dart
이 위젯은 게시글 상세 화면 하단에서 글을 삭제하거나 목록으로 이동할 수 있는 버튼들을 담당합니다.
// Flutter UI 및 Hive 로컬 DB, 페이지 전환 관련 import
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/post.dart';
import '../screens/post_list_page.dart';
// 게시글 상세 화면에서 보여질 액션 버튼 영역
class PostActionButtons extends StatelessWidget {
final dynamic postKey; // 삭제할 글의 Hive key
const PostActionButtons({super.key, required this.postKey});
@override
Widget build(BuildContext context) {
final box = Hive.box<Post>('posts'); // Hive 박스 가져오기
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, // 가운데 정렬
children: [
// 🔴 삭제 버튼
ElevatedButton.icon(
onPressed: () {
box.delete(postKey); // Hive에서 해당 글 삭제
// 삭제 후 비동기적으로 목록 페이지로 강제 이동
Future.microtask(() {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const PostListPage()),
(route) => false, // 기존 화면 스택 모두 제거
);
});
},
icon: const Icon(Icons.delete_outline), // 휴지통 아이콘
label: const Text('삭제'), // 버튼 텍스트
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent, // 버튼 배경색
foregroundColor: Colors.white, // 버튼 글자색
),
),
const SizedBox(width: 12), // 버튼 간 여백
// 🔵 목록으로 돌아가는 버튼
ElevatedButton.icon(
onPressed: () => Navigator.pop(context), // 이전 페이지로 단순 이동
icon: const Icon(Icons.list), // 목록 아이콘
label: const Text('목록'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // 파란 배경
foregroundColor: Colors.white, // 흰 글씨
),
),
],
),
);
}
}
버튼 | 기능 |
🟥 삭제 | Hive.delete()로 글 제거 후 목록 화면으로 이동 (스택 제거) |
🔵 목록 | 단순히 pop()으로 이전 화면으로 돌아감 |
스타일 | ElevatedButton.icon을 사용한 아이콘+텍스트 버튼 |
정렬 | 가운데 정렬 (Row + mainAxisAlignment: center) |
widgets/post_card.dart
이 위젯은 글 목록에서 하나의 글 카드를 구성하는 컴포넌트로, 제목, 내용 일부, 댓글 수, 탭 시 상세 화면으로 이동하는 기능을 담당합니다.
// Flutter UI 관련 import
import 'package:flutter/material.dart';
// 모델 및 상세 페이지 import
import '../models/post.dart';
import '../screens/post_detail_page.dart';
// 글 목록에서 하나의 글을 카드 형태로 보여주는 위젯
class PostCard extends StatelessWidget {
final Post post; // 표시할 글 객체
final Color backgroundColor; // 카드 배경색 (홀/짝 줄마다 다르게)
final int commentCount; // 댓글 수
const PostCard({
super.key,
required this.post,
required this.backgroundColor,
required this.commentCount,
});
@override
Widget build(BuildContext context) {
return Card(
color: backgroundColor, // 카드 배경 색상 지정
margin: const EdgeInsets.symmetric(vertical: 6), // 카드 위아래 간격
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), // 둥근 모서리
),
elevation: 2, // 살짝 떠오르는 그림자 효과
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
// 카드를 탭하면 상세 페이지로 이동
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => PostDetailPage(postKey: post.key), // post.key는 HiveObject의 고유 키
),
);
},
child: Padding(
padding: const EdgeInsets.all(16), // 내부 여백
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // 왼쪽 정렬
children: [
// 글 제목
Text(
post.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
// 글 내용 일부 (2줄까지만 표시)
Text(
post.content,
maxLines: 2,
overflow: TextOverflow.ellipsis, // 줄임표로 처리
style: const TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
const SizedBox(height: 12),
// 댓글 개수 표시
Text(
'댓글 $commentCount개',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade400,
),
),
],
),
),
),
);
}
}
항목 | 설명 |
UI | 카드 형태로 하나의 글 표시 |
클릭 동작 | 상세 페이지(PostDetailPage)로 이동 |
스타일 | 다크 테마에 맞는 밝은 텍스트와 배경, 둥근 테두리, 그림자 |
내용 제한 | 제목(한 줄), 본문(2줄까지), 댓글 수 표시 |
widgets/post_content.dart
이 위젯은 게시글 상세 화면에서 글의 제목과 본문 내용을 표시하는 역할을 합니다.
// Flutter UI 라이브러리 import
import 'package:flutter/material.dart';
// 게시글의 제목과 본문 내용을 보여주는 위젯
class PostContent extends StatelessWidget {
final String title; // 게시글 제목
final String content; // 게시글 본문
const PostContent({
super.key,
required this.title,
required this.content,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), // 외부 여백
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // 왼쪽 정렬
children: [
// 제목 텍스트
Text(
title,
style: const TextStyle(
fontSize: 22, // 큰 폰트
fontWeight: FontWeight.bold, // 굵은 텍스트
color: Colors.white, // 다크 테마 대비용 흰색
),
),
const SizedBox(height: 12), // 제목과 본문 사이 간격
// 본문 내용 텍스트
Text(
content,
style: const TextStyle(
fontSize: 16,
color: Colors.white70, // 회색톤 텍스트로 대비 조절
),
),
],
),
);
}
}
요소 | 설명 |
title | 게시글 제목 (크게, 굵게, 흰색) |
content | 게시글 본문 (중간 크기, 회색 텍스트) |
Padding | 글 주변에 적당한 여백 확보 |
Column | 제목과 본문을 수직 배치, 왼쪽 정렬 |
widgets/post_list.dart
이 위젯은 글 목록 전체를 렌더링하는 리스트 뷰이며, 각 아이템은 PostCard로 구성되어 있습니다.
// 글 카드 위젯(PostCard) 및 기본 Flutter UI import
import 'package:andrew_blog/widgets/post_card.dart';
import 'package:flutter/material.dart';
import '../models/post.dart';
// 글 목록 전체를 보여주는 리스트 뷰 위젯
class PostList extends StatelessWidget {
final List<Post> posts; // 표시할 전체 글 목록
const PostList({super.key, required this.posts});
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.all(12), // 전체 리스트 여백
itemCount: posts.length, // 글 개수
itemBuilder: (context, index) {
final post = posts[index]; // 하나의 글 객체 가져오기
final commentCount = post.comments.length; // 댓글 수 계산
// 홀수/짝수 배경색 설정 (가독성 향상용)
final Color backgroundColor =
index % 2 == 0 ? const Color(0xFF1E1E1E) : const Color(0xFF2A2A2A);
// PostCard 위젯으로 구성된 한 줄 반환
return PostCard(
post: post,
backgroundColor: backgroundColor,
commentCount: commentCount,
);
},
);
}
}
항목 | 설명 |
UI 구성 | ListView.builder를 사용하여 스크롤 가능한 목록 생성 |
아이템 | 각 글은 PostCard 위젯으로 렌더링 |
색상 | 짝수/홀수 줄마다 배경색 다르게 지정 |
데이터 | List<Post>를 전달받아 리스트로 구성 |
screens/post_list_page.dart
이 위젯은 블로그 앱의 메인 화면(글 목록 페이지)로서 Hive에 저장된 게시글을 실시간으로 표시하고, 글쓰기 화면으로 이동할 수 있는 기능을 담당합니다.
// Flutter UI 및 Hive 상태관리 기능 import
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/post.dart'; // Post 데이터 모델
import '../widgets/post_list.dart'; // 글 목록을 구성하는 PostList 위젯
import 'post_write_page.dart'; // 글 작성 화면
// 메인 글 목록 페이지
class PostListPage extends StatelessWidget {
const PostListPage({super.key});
@override
Widget build(BuildContext context) {
// Hive에서 posts라는 이름의 박스를 열어 데이터 접근
final box = Hive.box<Post>('posts');
return Scaffold(
appBar: AppBar(
title: const Text('기억을 지배하는 기록'), // 앱 타이틀
),
body: ValueListenableBuilder(
valueListenable: box.listenable(), // Hive 데이터 변경 감지
builder: (context, Box<Post> box, _) {
// 모든 게시글(Post)을 리스트로 가져옴
final posts = box.values.toList().cast<Post>();
// PostList 위젯에 전달하여 목록 화면 구성
return PostList(posts: posts);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add), // 글쓰기 아이콘 (➕)
onPressed: () {
// 글쓰기 화면(PostWritePage)으로 이동
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const PostWritePage()),
);
},
),
);
}
}
항목 | 설명 |
상태 감지 | ValueListenableBuilder를 사용해 Hive 변경 실시간 반영 |
목록 구성 | PostList 위젯을 통해 리스트 렌더링 |
글쓰기 이동 | 플로팅 버튼 클릭 시 PostWritePage로 이동 |
데이터 | Hive<Box<Post>>에서 toList()로 변환 후 사용 |
screens/post_write_page.dart
이 위젯은 새 글을 작성하고 Hive에 저장하는 폼 화면입니다.
// Flutter UI 및 Hive 데이터베이스 사용
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import '../models/post.dart'; // Post 모델 import
// 글 작성 화면 - StatefulWidget으로 상태 관리 필요 (텍스트 입력 필드)
class PostWritePage extends StatefulWidget {
const PostWritePage({super.key});
@override
State<PostWritePage> createState() => _PostWritePageState();
}
class _PostWritePageState extends State<PostWritePage> {
// 제목, 내용 입력 필드 컨트롤러
final _titleController = TextEditingController();
final _contentController = TextEditingController();
// 글 저장 함수
Future<void> _submitPost() async {
final title = _titleController.text.trim(); // 공백 제거
final content = _contentController.text.trim(); // 공백 제거
if (title.isEmpty || content.isEmpty) return; // 제목이나 내용이 비었으면 종료
final post = Post(
title: title,
content: content,
timestamp: DateTime.now(), // 작성 시간은 현재 시각
comments: [], // 초기 댓글은 비어 있음
);
final box = Hive.box<Post>('posts'); // Hive Box 열기
await box.add(post); // 새 글 저장
Navigator.pop(context); // 작성 후 목록 화면으로 되돌아감
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('글 작성')), // 상단 제목
body: Padding(
padding: const EdgeInsets.all(16.0), // 전체 여백
child: Column(
children: [
// 제목 입력 필드
TextField(
controller: _titleController,
decoration: const InputDecoration(labelText: '제목'),
),
const SizedBox(height: 12), // 제목/내용 간 간격
// 내용 입력 필드
TextField(
controller: _contentController,
decoration: const InputDecoration(labelText: '내용'),
maxLines: 5, // 5줄까지 입력 가능
),
const SizedBox(height: 20), // 입력 필드와 버튼 간 간격
// 등록 버튼
ElevatedButton.icon(
onPressed: _submitPost, // 저장 실행
icon: const Icon(Icons.save), // 저장 아이콘
label: const Text('등록'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // 파란색 배경
foregroundColor: Colors.white, // 흰색 텍스트
),
),
],
),
),
);
}
}
항목 | 설명 |
입력 | 제목 / 내용 필드 입력 (TextEditingController) |
저장 | Hive.add()로 새 글 저장 |
이동 | 저장 후 Navigator.pop(context) 로 목록으로 돌아감 |
UI | TextField + ElevatedButton.icon 구성 |
screens/post_detail_page.dart
이 페이지는 블로그 글 상세 보기 화면으로, 글 내용, 댓글 목록, 댓글 추가 및 삭제/수정, 글 삭제 기능을 모두 포함하고 있습니다.
// 필요한 위젯 및 모델 import
import 'package:andrew_blog/widgets/comment_list.dart';
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/post.dart'; // Post 데이터 모델
import '../widgets/comment_popup.dart'; // 댓글 작성 팝업
import '../widgets/comment_title.dart'; // 댓글 타이틀 위젯
import '../widgets/post_action_buttons.dart'; // 글 삭제/목록 버튼
import '../widgets/post_content.dart'; // 글 제목 및 본문 표시
// 글 상세 화면 위젯
class PostDetailPage extends StatelessWidget {
final dynamic postKey; // Hive에서 조회할 게시글의 key
const PostDetailPage({super.key, required this.postKey});
@override
Widget build(BuildContext context) {
final box = Hive.box<Post>('posts'); // Post 데이터 박스 열기
final post = box.get(postKey); // 현재 게시글 정보 가져오기
// 글을 찾지 못했을 경우 예외 처리
if (post == null) {
return const Scaffold(
body: Center(
child: Text(
'해당 게시글을 찾을 수 없습니다.',
style: TextStyle(color: Colors.white),
),
),
);
}
return Scaffold(
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(bottom: 80), // FAB 공간 확보
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 글 제목과 내용 표시
PostContent(title: post.title, content: post.content),
// 글 삭제/목록 이동 버튼
PostActionButtons(postKey: postKey),
// 댓글 구역을 구분하는 선
const Divider(
height: 32,
thickness: 1,
color: Colors.grey,
indent: 16,
endIndent: 16,
),
// 댓글 섹션 제목
const CommentTitle(),
const SizedBox(height: 12),
// 댓글 리스트 표시 (수정/삭제 포함)
CommentList(postKey: postKey),
],
),
),
),
// 플로팅 버튼 → 댓글 작성 팝업 열기
floatingActionButton: FloatingActionButton(
onPressed: () {
showDialog(
context: context,
builder: (_) => CommentPopup(postKey: postKey), // 댓글 작성 팝업
);
},
backgroundColor: Colors.teal,
child: const Icon(Icons.comment), // 💬 아이콘
),
);
}
}
✅ 구성 요소 요약
위젯 역할
위젯 | 역할 |
PostContent | 글 제목과 본문 표시 |
PostActionButtons | 삭제 / 목록으로 이동 버튼 |
Divider | 댓글과 본문을 구분하는 선 |
CommentTitle | "댓글"이라는 타이틀 텍스트 |
CommentList | 댓글 목록 표시 (수정/삭제 포함) |
CommentPopup | 댓글 작성 다이얼로그 |
main.dart
이 코드는 앱의 진입점으로, Hive 초기화, 다크 테마 적용, 첫 화면으로 PostListPage를 설정하는 역할을 합니다.
// Flutter UI 및 Hive 플러그인 import
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'models/post.dart'; // Post 모델 클래스
import 'screens/post_list_page.dart'; // 앱 첫 화면: 글 목록 페이지
// 앱 실행 전 필수 설정을 위한 비동기 main 함수
void main() async {
WidgetsFlutterBinding.ensureInitialized(); // Flutter 비동기 초기화
await Hive.initFlutter(); // Hive DB 초기화 (Flutter 환경용)
Hive.registerAdapter(PostAdapter()); // Post 모델 등록 (직렬화용)
await Hive.openBox<Post>('posts'); // posts 박스 열기 (없으면 생성됨)
runApp(const BlogApp()); // 앱 실행 시작
}
// 전체 앱을 구성하는 루트 위젯
class BlogApp extends StatelessWidget {
const BlogApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Andrew blog', // 앱 제목
// 다크 테마 설정
theme: ThemeData.dark().copyWith(
colorScheme: ColorScheme.dark(
primary: Colors.blueGrey.shade200, // 주요 색상
secondary: Colors.tealAccent.shade100, // 보조 색상
),
scaffoldBackgroundColor: Colors.black, // 화면 배경
cardColor: Colors.grey[900], // 카드 배경색
dialogBackgroundColor: Colors.grey[850], // 팝업 배경색
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: Colors.teal, // 플로팅 버튼 색상
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.black, // AppBar 배경
foregroundColor: Colors.white, // AppBar 글자색
),
),
home: const PostListPage(), // 앱 첫 화면으로 글 목록 페이지 지정
debugShowCheckedModeBanner: false, // 우측 상단 디버그 배너 숨김
);
}
}
항목 | 설명 |
Hive 초기화 | Hive.initFlutter() + registerAdapter() |
DB 열기 | Hive.openBox<Post>('posts') |
테마 | 다크 테마 전반 적용 |
첫 화면 | PostListPage()로 지정 |
구조 | 전체 앱의 루트 위젯은 BlogApp |
728x90
'Flutter for Beginners' 카테고리의 다른 글
Flutter Gemini 앱 (0) | 2025.04.22 |
---|---|
Flutter 포토 스티커 (0) | 2025.04.21 |
Flutter Google Map (0) | 2025.04.18 |
Flutter 영상통화 (0) | 2025.04.15 |
Flutter 비디오 플레이어 (0) | 2025.04.10 |