Search
Duplicate
moon
sun

게시판 앱

게시판 앱

•
프로젝트 구조
◦
클라이언트
◦
서버
•
코드
◦
board.dart
◦
main.dart
◦
main_screen.dart
◦
list_screen.dart
◦
read_screen.dart
◦
insert_screen.dart
◦
update_screen.dart

프로젝트 구조

•
클라이언트
•
서버

클라이언트

Flutter - board_app

📁 board_app ├── 📁 android ├── 📁 ios ├── 📁 build ├── 📁 lib │ ├── 📁 models │ │ └── 📄 board.dart │ ├── 📁 screen │ │ ├── 📁 board │ │ │ ├── 📄 insert_screen.dart │ │ │ ├── 📄 list_screen.dart │ │ │ ├── 📄 read_screen.dart │ │ │ └── 📄 update_screen.dart │ │ └── 📄 main_screen.dart │ └── 📄 main.dart ├── 📁 test ├── 📄 pubspec.yaml ├── 📄 pubspec.lock └── 📄 README.md
Dart
복사

서버

SpringBoot - board

board.war

server.zip
23570.1KB

서버 실행하기

java -jar board.war
Bash
복사

코드

•
board.dart
•
main.dart
•
main_screen.dart
•
list_screen.dart
•
read_screen.dart
•
insert_screen.dart
•
update_screen.dart

board.dart

class Board { int? no; String? title; String? writer; String? content; DateTime? regDate; DateTime? updDate; Board({ required this.no, required this.title, required this.writer, required this.content, this.regDate, this.updDate }); }
Dart
복사

main.dart

import 'package:flutter/material.dart'; import 'package:http_app/main_screen.dart'; import 'package:http_app/screen/board/insert_screen.dart'; import 'package:http_app/screen/board/list_screen.dart'; import 'package:http_app/screen/board/read_screen.dart'; import 'package:http_app/screen/board/update_screen.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), initialRoute: '/main', routes: { '/main' : (context) => const MainScreen(), '/board/list' : (context) => const ListScreen(), '/board/read' : (context) => const ReadScreen(), '/board/insert' : (context) => const InsertScreen(), '/board/update' : (context) => const UpdateScreen(), }, ); } }
Dart
복사

main_screen.dart

import 'package:flutter/material.dart'; class MainScreen extends StatefulWidget { const MainScreen({super.key}); State<MainScreen> createState() => _MainScreenState(); } class _MainScreenState extends State<MainScreen> { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("메인 화면"), ), body: Center( child: ElevatedButton( onPressed: () { Navigator.pushNamed(context, "/board/list"); }, child: Text("게시글 목록"), style: ElevatedButton.styleFrom( backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, ), ), ), ); } }
Dart
복사

list_screen.dart

import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http_app/models/board.dart'; import 'package:http/http.dart' as http; class ListScreen extends StatefulWidget { const ListScreen({super.key}); State<ListScreen> createState() => _ListScreenState(); } class _ListScreenState extends State<ListScreen> { List<Board> _boardList = []; void initState() { super.initState(); getBoardList().then((result) { setState(() { _boardList = result; }); }); } // 🌞 게시글 목록 데이터 요청 Future<List<Board>> getBoardList() async { var url = "http://10.0.2.2:8080/board"; List<Board> list = []; try { var response = await http.get(Uri.parse(url)); print("::::: response - body :::::"); print(response.body); // UTF-8 디코딩 var utf8Decoded = utf8.decode(response.bodyBytes); // JSON 디코딩 var boardList = jsonDecode(utf8Decoded); for (var i = 0; i < boardList.length; i++) { list.add(Board( no: boardList[i]['no'], title: boardList[i]['title'], writer: boardList[i]['writer'], content: boardList[i]['content'], )); } print(list); } catch (e) { print(e); } return list; } final List<PopupMenuEntry<String>> _popupMenuItems = [ const PopupMenuItem<String>( value: 'update', child: Row( children: [ Icon(Icons.edit, color: Colors.black), // 아이콘 SizedBox(width: 8), // 아이콘과 텍스트 사이에 간격 추가 Text('수정하기'), // 텍스트 ], ), ), const PopupMenuItem<String>( value: 'delete', child: Row( children: [ Icon(Icons.delete, color: Colors.black), // 아이콘 SizedBox(width: 8), // 아이콘과 텍스트 사이에 간격 추가 Text('삭제하기'), // 텍스트 ], ), ), ]; Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("게시글 목록")), body: Container( padding: const EdgeInsets.fromLTRB(5, 0, 5, 10), child: ListView.builder( itemCount: _boardList.length, itemBuilder: (context, index) { return GestureDetector( child: Card( child: ListTile( leading: Text(_boardList[index].no.toString() ?? '0'), title: Text(_boardList[index].title ?? "제목없음"), subtitle: Text(_boardList[index].writer ?? '-'), trailing: PopupMenuButton<String>( itemBuilder: (BuildContext context) { return _popupMenuItems; }, onSelected: (String value) async { if (value == 'update') { Navigator.pushNamed( context, "/board/update", arguments: _boardList[index].no, ); } else if (value == 'delete') { bool check = await _showDeleteConfirmDialog(); if (check) { deleteBoard(_boardList[index].no).then((result) { if (result) { setState(() { _boardList.removeAt(index); }); } }); } } }, ), ), ), onTap: () { Navigator.pushNamed( context, "/board/read", arguments: _boardList[index].no, ); }, ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { Navigator.pushReplacementNamed(context, "/board/insert"); }, child: const Icon(Icons.create), ), ); } /// 게시글 삭제 요청 Future<bool> deleteBoard(int? no) async { var url = "http://10.0.2.2:8080/board/$no"; try { var response = await http.delete(Uri.parse(url)); print("::::: response - statusCode :::::"); print(response.statusCode); if (response.statusCode == 200 || response.statusCode == 204) { // 성공적으로 삭제됨 print("게시글 삭제 성공"); return true; } else { // 실패 시 오류 메시지 throw Exception('Failed to delete board. Status code: ${response.statusCode}'); } } catch (e) { print(e); return false; } } /// 삭제 확인 다이얼로그 표시 Future<bool> _showDeleteConfirmDialog() async { bool result = false; await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('삭제 확인'), content: const Text('정말로 이 게시글을 삭제하시겠습니까?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); // 취소를 클릭하면 false 반환 }, child: const Text('취소'), ), TextButton( onPressed: () { Navigator.of(context).pop(true); // 삭제를 클릭하면 true 반환 }, child: const Text('삭제'), ), ], ); }, ).then((value) { result = value ?? false; }); return result; } }
Dart
복사

read_screen.dart

import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:http_app/models/board.dart'; class ReadScreen extends StatefulWidget { const ReadScreen({super.key}); State<ReadScreen> createState() => _ReadScreenState(); } class _ReadScreenState extends State<ReadScreen> { late int no; late Future<Board> _board; final List<PopupMenuEntry<String>> _popupMenuItems = [ const PopupMenuItem<String>( value: 'update', child: Row( children: [ Icon(Icons.edit, color: Colors.black), // 아이콘 SizedBox(width: 8), // 아이콘과 텍스트 사이에 간격 추가 Text('수정하기'), // 텍스트 ], ), ), const PopupMenuItem<String>( value: 'delete', child: Row( children: [ Icon(Icons.delete, color: Colors.black), // 아이콘 SizedBox(width: 8), // 아이콘과 텍스트 사이에 간격 추가 Text('삭제하기'), // 텍스트 ], ), ), ]; void didChangeDependencies() { super.didChangeDependencies(); final arguments = ModalRoute.of(context)!.settings.arguments; if (arguments != null) { no = arguments as int; _board = getBoard(no); } else { // 기본값 설정 또는 예외 처리 no = 0; _board = Future.error('No board number provided'); } } /// /// 👩‍💻 게시글 조회 요청 /// Future<Board> getBoard(int no) async { var url = "http://10.0.2.2:8080/board/$no"; try { var response = await http.get(Uri.parse(url)); print("::::: response - body :::::"); print(response.body); // UTF-8 디코딩 var utf8Decoded = utf8.decode(response.bodyBytes); // JSON 디코딩 var boardJson = jsonDecode(utf8Decoded); print(boardJson); return Board( no: boardJson['no'], title: boardJson['title'], writer: boardJson['writer'], content: boardJson['content'], ); } catch (e) { print(e); throw Exception('Failed to load board'); } } /// 게시글 삭제 요청 Future<bool> deleteBoard(int no) async { var url = "http://10.0.2.2:8080/board/$no"; try { var response = await http.delete(Uri.parse(url)); print("::::: response - statusCode :::::"); print(response.statusCode); if (response.statusCode == 200 || response.statusCode == 204) { // 성공적으로 삭제됨 print("게시글 삭제 성공"); return true; } else { // 실패 시 오류 메시지 throw Exception('Failed to delete board. Status code: ${response.statusCode}'); } } catch (e) { print(e); return false; } } /// /// ❓ 삭제 확인 /// Future<bool> _showDeleteConfirmDialog() async { bool result = false; await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('삭제 확인'), content: Text('정말로 이 게시글을 삭제하시겠습니까?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); // 취소를 클릭하면 false 반환 }, child: Text('취소'), ), TextButton( onPressed: () { Navigator.of(context).pop(true); // 삭제를 클릭하면 true 반환 }, child: Text('삭제'), ), ], ); }, ).then((value) { // 다이얼로그 결과를 result에 저장 result = value ?? false; }); return result; } Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("게시글 조회"), actions: [ PopupMenuButton( itemBuilder: (BuildContext context) { return _popupMenuItems; }, icon: const Icon(Icons.more_vert), onSelected: (String value) async { if (value == 'update') { Navigator.pushReplacementNamed(context, "/board/update", arguments: no,); } else if (value == 'delete') { bool check = await _showDeleteConfirmDialog(); if( check ) { deleteBoard(no).then((result) { if( result ) { Navigator.pop(context); Navigator.pushReplacementNamed(context, "/board/list"); } }); } } }, ) ], ), body: FutureBuilder<Board>( future: _board, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}')); } else if (!snapshot.hasData) { return const Center(child: Text('No data found')); } else { var board = snapshot.data!; return Padding( padding: const EdgeInsets.fromLTRB(5, 0, 5, 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( child: ListTile( leading: const Icon(Icons.article), title: Text(board.title ?? '제목'), ), ), Card( child: ListTile( leading: const Icon(Icons.person), title: Text(board.writer ?? '작성자'), ), ), const SizedBox(height: 10.0,), Container( margin: const EdgeInsets.symmetric(horizontal: 4.0), padding: const EdgeInsets.all(12.0), width: double.infinity, height: 320.0, decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), // 그림자의 색상과 투명도 spreadRadius: 2, // 그림자의 확산 정도 blurRadius: 8, // 그림자의 흐림 정도 offset: const Offset(4, 4), // 그림자의 위치 (x, y) ), ], borderRadius: BorderRadius.circular(8), // 옵션: 모서리 둥글기 ), child: SingleChildScrollView( child: Text(board.content ?? '내용') ) ), ] ), ); } }, ), ); } }
Dart
복사

insert_screen.dart

import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; class InsertScreen extends StatefulWidget { const InsertScreen({super.key}); State<InsertScreen> createState() => _InsertScreenState(); } class _InsertScreenState extends State<InsertScreen> { final _formKey = GlobalKey<FormState>(); final TextEditingController _titleController = TextEditingController(); final TextEditingController _writerController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); Future<void> insert() async { if (_formKey.currentState!.validate()) { var url = "http://10.0.2.2:8080/board"; try { var response = await http.post( Uri.parse(url), headers: {"Content-Type": "application/json"}, body: jsonEncode({ 'title': _titleController.text, 'writer': _writerController.text, 'content': _contentController.text, }), ); print("::::: response - body :::::"); print(response.body); if (response.statusCode == 200 || response.statusCode == 201) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('게시글 등록 성공!'), backgroundColor: Colors.blueAccent, ), ); Navigator.pushReplacementNamed(context, "/board/list"); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('게시글 등록 실패...'), backgroundColor: Colors.redAccent, ), ); } } catch (e) { print(e); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('에러: $e')), ); } } } Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("게시글 작성"), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( controller: _titleController, decoration: const InputDecoration(labelText: '제목'), validator: (value) { if (value == null || value.isEmpty) { return '제목을 입력하세요'; } return null; }, ), TextFormField( controller: _writerController, decoration: const InputDecoration(labelText: '작성자'), validator: (value) { if (value == null || value.isEmpty) { return '작성자를 입력하세요'; } return null; }, ), TextFormField( controller: _contentController, decoration: const InputDecoration(labelText: '내용'), maxLines: 5, validator: (value) { if (value == null || value.isEmpty) { return '내용을 입력하세요'; } return null; }, ), ], ), ), ), bottomSheet: Container( height: 60, color: Colors.white, child: Center( child: ElevatedButton( onPressed: () { insert(); }, style: ElevatedButton.styleFrom( minimumSize: const Size(double.infinity, 50), // 가로 100% 버튼 backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.zero, // 테두리를 둥글지 않게 설정 ), ), child: const Text('등록하기'), ), ), ), ); } }
Dart
복사

update_screen.dart

import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; class UpdateScreen extends StatefulWidget { const UpdateScreen({super.key}); State<UpdateScreen> createState() => _UpdateScreenState(); } class _UpdateScreenState extends State<UpdateScreen> { final _formKey = GlobalKey<FormState>(); final TextEditingController _titleController = TextEditingController(); final TextEditingController _writerController = TextEditingController(); final TextEditingController _contentController = TextEditingController(); late int no; final List<PopupMenuEntry<String>> _popupMenuItems = [ const PopupMenuItem<String>( value: 'delete', child: Row( children: [ Icon(Icons.delete, color: Colors.black), // 아이콘 SizedBox(width: 8), // 아이콘과 텍스트 사이에 간격 추가 Text('삭제하기'), // 텍스트 ], ), ), ]; void didChangeDependencies() { super.didChangeDependencies(); final arguments = ModalRoute.of(context)!.settings.arguments; if (arguments != null) { no = arguments as int; getBoard(no); // 이름 변경된 메서드 호출 } } /// /// 👩‍💻 게시글 조회 요청 /// Future<void> getBoard(int no) async { var url = "http://10.0.2.2:8080/board/$no"; try { var response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { var utf8Decoded = utf8.decode(response.bodyBytes); var boardJson = jsonDecode(utf8Decoded); _titleController.text = boardJson['title']; _writerController.text = boardJson['writer']; _contentController.text = boardJson['content']; } else { throw Exception('Failed to load board details'); } } catch (e) { print(e); } } /// 게시글 수정 요청 Future<void> updateBoard() async { if (_formKey.currentState!.validate()) { var url = "http://10.0.2.2:8080/board"; try { var response = await http.put( Uri.parse(url), headers: {"Content-Type": "application/json"}, body: jsonEncode({ 'no' : no, 'title': _titleController.text, 'writer': _writerController.text, 'content': _contentController.text, }), ); if (response.statusCode == 200 || response.statusCode == 204) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('게시글 수정 성공!'), backgroundColor: Colors.blueAccent, ), ); Navigator.pushReplacementNamed(context, "/board/list"); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('게시글 수정 실패...'), backgroundColor: Colors.redAccent, ), ); } } catch (e) { print(e); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('에러: $e')), ); } } } /// 게시글 삭제 요청 Future<bool> deleteBoard(int no) async { var url = "http://10.0.2.2:8080/board/$no"; try { var response = await http.delete(Uri.parse(url)); print("::::: response - statusCode :::::"); print(response.statusCode); if (response.statusCode == 200 || response.statusCode == 204) { // 성공적으로 삭제됨 print("게시글 삭제 성공"); return true; } else { // 실패 시 오류 메시지 return false; } } catch (e) { print(e); return false; } } Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("게시글 수정"), actions: [ PopupMenuButton( itemBuilder: (BuildContext context) { return _popupMenuItems; }, icon: const Icon(Icons.more_vert), onSelected: (String value) async { if (value == 'delete') { bool check = await _showDeleteConfirmDialog(); if( check ) { deleteBoard(no).then((result) { if( result ) { Navigator.pop(context); Navigator.pushReplacementNamed(context, "/board/list"); } }); } } }, ) ], ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( controller: _titleController, decoration: const InputDecoration(labelText: '제목'), validator: (value) { if (value == null || value.isEmpty) { return '제목을 입력하세요'; } return null; }, ), TextFormField( controller: _writerController, decoration: const InputDecoration(labelText: '작성자'), validator: (value) { if (value == null || value.isEmpty) { return '작성자를 입력하세요'; } return null; }, ), TextFormField( controller: _contentController, decoration: const InputDecoration(labelText: '내용'), maxLines: 5, validator: (value) { if (value == null || value.isEmpty) { return '내용을 입력하세요'; } return null; }, ), ], ), ), ), bottomSheet: Container( height: 60, color: Colors.white, child: Center( child: ElevatedButton( onPressed: () { updateBoard(); }, style: ElevatedButton.styleFrom( minimumSize: const Size(double.infinity, 50), // 가로 100% 버튼 backgroundColor: Colors.blueAccent, foregroundColor: Colors.white, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.zero, // 테두리를 둥글지 않게 설정 ), ), child: const Text('수정하기'), ), ), ), ); } /// 삭제 확인 다이얼로그 표시 메서드 Future<bool> _showDeleteConfirmDialog() async { bool result = false; await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('삭제 확인'), content: Text('정말로 이 게시글을 삭제하시겠습니까?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(false); // 취소를 클릭하면 false 반환 }, child: Text('취소'), ), TextButton( onPressed: () { Navigator.of(context).pop(true); // 삭제를 클릭하면 true 반환 }, child: Text('삭제'), ), ], ); }, ).then((value) { result = value ?? false; }); return result; } }
Dart
복사