본문 바로가기
Flutter for Beginners

Flutter 게시판 with Hive

by Records that rule memory 2025. 4. 23.
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