게시판 앱
•
프로젝트 구조
◦
클라이언트
◦
서버
•
코드
◦
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
서버 실행하기
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
복사











