📌 이번 글에서는 게시글 작성 기능을 구현합니다.
- 커스텀 위젯(CustomTextFormField, CustomTextArea)을 활용하여 게시글 입력 UI를 만듭니다.
- PostWriteViewModel을 사용하여 게시글 등록 기능을 구현하고 상태를 관리합니다.
- 게시글 작성 완료 후 자동으로 입력 필드 초기화 및 피드백 메시지 표시 기능을 추가합니다.
디자인 시안
1️⃣ 게시글 목록 인증 없이 확인
게시글 목록을 인증 없이 확인할 수 있는 API 엔드포인트입니다.
📌 URI: http://localhost:8080/init/post
이 API를 사용하여 기본적인 게시글 목록을 가져올 수 있습니다.
2️⃣ Custom 위젯 만들기 (입력 필드, 텍스트 영역)
게시글 작성 화면에서 사용할 입력 필드(CustomTextFormField) 와 텍스트 영역(CustomTextArea)을 커스텀 위젯으로 만듭니다.
📌 CustomTextFormField (입력 필드)
import 'package:flutter/material.dart';
class CustomTextFormField extends StatelessWidget {
final String hint;
final bool obscureText;
final TextEditingController controller;
final String initValue;
const CustomTextFormField({
required this.hint,
this.obscureText = false,
required this.controller,
this.initValue = "",
});
@override
Widget build(BuildContext context) {
if (initValue.isNotEmpty) {
controller.text = initValue;
}
return TextFormField(
controller: controller,
obscureText: obscureText,
decoration: InputDecoration(
hintText: "Enter $hint",
enabledBorder: OutlineInputBorder(
// 3. 기본 TextFormField 디자인
borderRadius: BorderRadius.circular(20),
),
focusedBorder: OutlineInputBorder(
// 4. 손가락 터치 시 TextFormField 디자인
borderRadius: BorderRadius.circular(20),
),
errorBorder: OutlineInputBorder(
// 5. 에러 발생 시 TextFormField 디자인
borderRadius: BorderRadius.circular(20),
),
focusedErrorBorder: OutlineInputBorder(
// 5. 에러 발생 후 손가락 터치 시 TextFormField 디자인
borderRadius: BorderRadius.circular(20),
),
),
);
}
}
📌 설명
✔ TextFormField를 감싸는 커스텀 위젯입니다.
✔ hint 값을 받아 입력 필드의 힌트로 사용합니다.
✔ initValue가 있을 경우, 초기값을 설정합니다.
✔ obscureText 속성을 추가하여 비밀번호 입력도 가능하게 합니다.
✔ 입력 필드 디자인을 커스텀할 수 있도록 OutlineInputBorder를 사용합니다.
📌 CustomTextArea (텍스트 영역)
import 'package:flutter/material.dart';
class CustomTextArea extends StatelessWidget {
final String hint;
final TextEditingController controller;
final String initValue;
const CustomTextArea(
{required this.hint, required this.controller, this.initValue = ""});
@override
Widget build(BuildContext context) {
if (initValue.isNotEmpty) {
controller.text = initValue;
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: TextFormField(
controller: controller,
maxLines: 10,
decoration: InputDecoration(
hintText: "Enter $hint",
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
);
}
}
📌 설명
✔ TextFormField를 기반으로 다중 줄 입력을 지원하는 텍스트 입력 위젯입니다.
✔ maxLines: 10을 설정하여 기본적으로 10줄을 입력할 수 있습니다.
✔ initValue를 활용하여 초기값이 있을 경우 컨트롤러에 자동 설정됩니다.
✔ hint를 통해 입력 필드의 안내 텍스트를 표시합니다.
✔ 입력 필드 디자인을 통일하기 위해 OutlineInputBorder를 설정하였습니다.
3️⃣ PostWritePage 생성 (게시글 작성 화면)
게시글 작성을 위한 페이지를 생성합니다.
📌 PostWritePage (페이지)
import 'package:class_f_story/ui/pages/post/write_page/widgets/post_write_body.dart';
import 'package:flutter/material.dart';
class PostWritePage extends StatelessWidget {
const PostWritePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: PostWriteBody(),
);
}
}
📌 설명
✔ Scaffold를 사용하여 기본 페이지 구조를 만듭니다.
✔ AppBar()를 추가하여 앱 상단바를 유지합니다.
✔ PostWriteBody()를 호출하여 게시글 작성 UI를 포함하는 본문을 렌더링합니다.
📌 PostWriteBody (본문)
import 'package:class_f_story/ui/pages/post/write_page/widgets/post_write_form.dart';
import 'package:flutter/material.dart';
class PostWriteBody extends StatelessWidget {
const PostWriteBody({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// 기본적으로 사용하자
Flexible(
child: PostWriteForm(),
),
],
),
);
}
}
📌 설명
✔ Padding을 적용하여 적절한 여백을 추가합니다.
✔ PostWriteForm()을 포함하여 입력 폼을 표시합니다.
✔ Flexible을 사용하여 레이아웃을 유연하게 조정할 수 있도록 합니다.
4️⃣ PostWriteForm (게시글 작성 폼)
import 'package:class_f_story/_core/constants/size.dart';
import 'package:class_f_story/ui/widgets/custom_elevated_button.dart';
import 'package:class_f_story/ui/widgets/custom_text_area.dart';
import 'package:class_f_story/ui/widgets/custom_text_form_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PostWriteForm extends ConsumerWidget {
// 폼에 상태/ 유효성 검사 ..save()
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _contentController = TextEditingController();
PostWriteForm({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Form(
key: _formKey,
child: ListView(
shrinkWrap: true,
children: [
CustomTextFormField(
hint: 'title',
controller: _titleController,
),
const SizedBox(height: smallGap),
CustomTextArea(
hint: 'content',
controller: _contentController,
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: '글쓰기',
click: () {
// 글 작성 버튼 클릭 이벤트
},
),
],
),
);
}
}
📌 설명
✔ CustomTextFormField와 CustomTextArea를 사용하여 제목과 내용을 입력할 수 있도록 합니다.
✔ CustomElevatedButton을 추가하여 "글쓰기" 버튼을 표시합니다.
✔ GlobalKey<FormState>를 사용하여 폼의 상태를 관리합니다.
📌 PostWriteViewModel (게시글 작성 뷰모델)
게시글 작성 기능을 담당하는 뷰모델을 생성합니다.
✔ 상태 관리를 위해 Riverpod의 Notifier를 사용합니다.
✔ 게시글을 작성할 때 서버로 데이터를 전송하고 성공 여부를 관리합니다.
✔ 게시글 작성 완료 시 입력 필드를 초기화합니다.
// 글쓰기 화면 뷰 모델
// 화면 클래스에서 관리해야 하는 데이터, 기능을 여기로 옮기자.
// 그리고 상태 관리까지
import 'package:class_f_story/_core/utils/exception_handler.dart';
import 'package:class_f_story/_core/utils/my_http.dart';
import 'package:class_f_story/data/repository/user_repository.dart';
import 'package:class_f_story/main.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// dto 만들기 싫어서 배웠던 문법 -> 레코드
// 모델 POST 사용해도 되지만 --> 레코드 --> POST(모델 활용)
class PostWriteViewModel
extends Notifier<(String? title, String? content, bool isWriteCompleted)> {
// 뷰 모델에서 컨텍스트를 사용하는 방안
final mContext = navigatorkey.currentContext!;
UserRepository userRepository = const UserRepository();
// 상태값을 초기화해야 한다.
@override
(String? title, String? content, bool isWriteCompleted) build() {
// state == (String? title, String? content, bool isWriteCompleted)
return (null, null, false);
}
// 행위 - 게시글 작성
// 뷰 모델에서는 기본 데이터 타입 형태로 설계
// 0. 뷰모델에서 예외 처리를 하자.
// 1. 데이터 Map 구조로 변환 처리
// 2. 응답 --> success -- false
// 3. 응답 --> success -- true
Future<void> createPost(
{required String title, required String content}) async {
try {
// 데이터 가공 처리
final body = {"title": title, "content": content};
Response response = await dio.post('/api/post', data: body);
Map<String, dynamic> responseBody = response.data;
// 2. 서버 응답이 실패(success: false)일 경우 예외 처리
if (!responseBody['success']) {
ExceptionHandler.handleException(
responseBody['errorMessage'], StackTrace.current);
return;
}
// 시스템 키보드가 있다면 자동 닫기
FocusScope.of(mContext).unfocus();
// 게시글 작성 완료 메시지
ScaffoldMessenger.of(mContext)
.showSnackBar(SnackBar(content: Text('게시글 등록 완료')));
// 상태 갱신 처리
state = (null, null, true);
} catch (e, stackTrace) {
ExceptionHandler.handleException('게시글 등록 시 오류 발생', stackTrace);
}
}
}
// 창고 관리 만들기
final postWriteViewModelProvider = NotifierProvider<
PostWriteViewModel,
(
String? title,
String? content,
bool isWriteCompleted
)>(() => PostWriteViewModel());
📌 설명
✔ 게시글 작성 요청을 서버에 보냄 (createPost 메서드)
✔ state를 이용하여 입력값과 작성 완료 여부를 관리
✔ 게시글 작성 후 입력값 초기화 및 "게시글 등록 완료" 메시지 출력
✔ 서버 응답이 실패일 경우 예외 처리
✔ NotifierProvider를 사용하여 전역에서 뷰모델을 관리
PostWriteForm에서 뷰모델 적용
게시글 작성 폼에서 PostWriteViewModel을 활용하여 입력값을 관리하고 게시글 작성 요청을 수행합니다.
import 'package:class_f_story/_core/constants/size.dart';
import 'package:class_f_story/data/_vm/post_write_view_model.dart';
import 'package:class_f_story/ui/widgets/custom_elevated_button.dart';
import 'package:class_f_story/ui/widgets/custom_text_area.dart';
import 'package:class_f_story/ui/widgets/custom_text_form_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class PostWriteForm extends ConsumerWidget {
// 폼에 상태/ 유효성 검사 ..save()
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _contentController = TextEditingController();
PostWriteForm({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// post_write_view_model 만든 게 핵심
// 뷰 모델 상태를 구독
// (title, content, isWriteCompleted)
final data = ref.watch(postWriteViewModelProvider);
// 뷰 모델 행위를 사용해야 한다. (뷰 모델 자체 들고 오기)
final vm = ref.read(postWriteViewModelProvider.notifier);
return Form(
key: _formKey,
child: ListView(
shrinkWrap: true,
children: [
CustomTextFormField(
hint: 'title',
controller: _titleController,
),
const SizedBox(height: smallGap),
CustomTextArea(
hint: 'content',
controller: _contentController,
),
const SizedBox(height: largeGap),
CustomElevatedButton(
text: '글쓰기',
click: () {
// 게시글 작성 요청
vm.createPost(
title: _titleController.text.trim(),
content: _contentController.text.trim(),
);
// 레코드 문법 활용 가능
if (data.$3 == true) {
// 입력 필드 초기화
_titleController.clear();
_contentController.clear();
}
},
),
],
),
);
}
}
📌 설명
✔ 뷰모델의 상태를 구독하여 입력 필드의 상태를 반영합니다.
✔ vm.createPost()를 호출하여 게시글 작성 요청을 보냅니다.
✔ if (data.$3 == true) 조건을 통해 게시글이 작성 완료되면 입력 필드를 초기화합니다.
✅ 마무리: 구현된 기능 정리
✔ CustomTextFormField, CustomTextArea를 활용한 입력 UI 구성
✔ PostWritePage → PostWriteBody → PostWriteForm 구조로 페이지 설계
✔ PostWriteViewModel을 사용하여 게시글 작성 요청 관리
✔ 게시글 작성 후 입력 필드 초기화 및 성공 메시지 표시
✔ 뷰모델과 Riverpod을 활용한 상태 관리 적용
🚀 이제 게시글 작성 기능이 완성되었습니다! 🚀
다음 글에서는 pull_to_refresh 라이브러리를 사용하여 게시글 목록에서 새로고침 기능을 구현합니다. 🚀
게시글 관리와 로그아웃 기능 구현이 궁금하다면??
2025.02.09 - [Flutter/App] - [Flutter] 블로그 만들기 - 게시글 관리 및 로그아웃 기능 구현
'Flutter > App' 카테고리의 다른 글
[Flutter] 블로그 만들기 - 게시글 모델링 (0) | 2025.02.11 |
---|---|
[Flutter] 블로그 만들기 - pull_to_refresh 라이브러리 사용 (0) | 2025.02.11 |
[Flutter] 블로그 만들기 - 게시글 관리 및 로그아웃 기능 구현 (0) | 2025.02.10 |
[Flutter] 블로그 만들기 -자동 로그인 기능 구현 및 UI (0) | 2025.02.10 |
[Flutter] 블로그 만들기 - 회원가입 기능 구현 및 UI (1) | 2025.02.09 |