React로 게시판 만들기(2)
https://koop.tistory.com/44 React로 게시판 만들기(1)- 간략히 작성한 화면설계서를 참고해서 게시판을 만들 예정. - App.jsimport './App.css';import BoardHome from './component/BoardHome';function App() { return ( );}export
koop.tistory.com
지난 포스팅에선 게시판을 만들기 위한 개발환경 및 DB를 세팅하였고, 본격적으로 게시판의 주요 기능인 CRUD(Create, Read, Update, Delete) 구현
📢 API 구축
// src/server/server.js
// 설치한 라이브러리 변수로 받아오기
const express = require('express');
const bodyParser = require('body-parser');
const mysql = require('mysql');
const cors = require('cors');
//express 사용하기 위한 app 생성
const app = express();
//express 사용할 서버포트 설정
const PORT = process.env.PORT || 5000;
app.use(cors());
app.use(bodyParser.json());
//DB 접속
const db = mysql.createConnection({
host : 'localhost',
user: 'react',
password: 'mysql',
port:'3306',
database:'db_react'
});
// express 접속
app.listen(PORT, ()=>{
console.log(`server connecting on : http://localhost:${PORT}`);
});
//db 연결
db.connect((err)=>{
if(!err){
console.log("success");
}else{
console.log("fail");
}
});
//db에서 값을 가져오는 쿼리문
app.get('/', (req,res)=>{
res.send("React Server Connect Success!!")
});
//게시글 목록 가져오기
app.get('/list', (req, res) => {
const page = parseInt(req.query.page) || 1; // 현재 페이지
const limit = 8; // 페이지당 항목 수
const offset = (page - 1) * limit; // 시작 위치
const search = req.query.search || ''; // 검색어
const category = req.query.category || 'title'; // 검색 카테고리
// 검색 쿼리 동적으로 생성
let countSql = `SELECT COUNT(*) AS totalCount FROM sk_board WHERE ${category} LIKE ?`;
let sql = `
SELECT b.*, COUNT(c.id) AS comment_count
FROM sk_board b
LEFT JOIN comments c ON b.id = c.post_id
WHERE ${category} LIKE ?
GROUP BY b.id
ORDER BY b.id DESC
LIMIT ? OFFSET ?
`;
db.query(countSql, [`%${search}%`], (err, countData) => {
if (err) {
console.log(err);
return res.send('전송 오류');
}
const totalCount = countData[0].totalCount; // 총 게시글 수
const totalPages = Math.ceil(totalCount / limit); // 전체 페이지 수
db.query(sql, [`%${search}%`, limit, offset], (err, data) => {
if (!err) {
res.send({ data, totalPages }); // 데이터와 totalPages를 반환
} else {
console.log(err);
res.send('전송 오류');
}
});
});
});
//게시물 하나 가져오기 :id
app.get('/detail/:id', (req, res)=>{
const id = req.params.id;
const sql = `select * from sk_board where id=${id}`;
db.query(sql, (err, data)=>{
if(!err){
res.send(data);
}else{
console.log(err);
res.send('전송오류');
}
})
})
//게시물 삭제
app.post('/delete/:id', (req, res)=>{
const id = req.params.id;
const sql = `delete from sk_board where id=${id}`
db.query(sql, (err, data)=>{
if(!err){
res.send(data);
}else{
console.log(err);
res.send('전송오류');
}
})
})
//게시글 등록
app.post('/write', (req, res)=>{
const {title, writer, contents } = req.body;
const sql = `insert into sk_board(title, writer, contents) value (?,?,?)`;
db.query(sql, [title, writer, contents], (err, data)=>{
if(!err){
// res.send("OK");
res.sendStatus(200); //전송잘됨.
}else{
console.log(err);
res.send('전송오류');
}
})
})
//게시글 수정
app.get('/modify/:id', (req, res)=>{
//파라미터 가져오기
const id = req.params.id;
console.log(`/modify/${id}`);
const sql = `select * from sk_board where id=${id}`;
db.query(sql, (err, data)=>{
if(!err){
res.send(data);
}else{
console.log(err);
res.send('전송오류');
}
})
})
app.post('/modify/:id', (req, res) => {
const id = req.params.id;
const { title, writer, contents } = req.body;
console.log(`/modify/${id}`);
const sql = `update sk_board set title=?, writer=?, contents=? where id=?`;
db.query(sql, [title, writer, contents, id], (err, data) => {
if (!err) {
res.sendStatus(200); // 수정잘됨.
} else {
console.log(err);
res.send('전송오류');
}
});
});
//조회수
app.post('/views/:id', (req, res) => {
const id = req.params.id;
const sql = `update sk_board set views = views + 1 where id =${id}`;
console.log(`/views/${id}`);
db.query(sql, (err, data)=>{
if(!err){
res.send(data);
}else{
console.log(err);
res.send('전송오류');
}
})
});
//댓글
app.get('/comments/:postId', (req, res) => {
const postId = req.params.postId;
const sql = 'SELECT * FROM comments WHERE post_id = ? ORDER BY created_at DESC';
db.query(sql, [postId], (err, data) => {
if (!err) {
res.send(data);
} else {
console.log(err);
res.send('전송오류');
}
});
});
app.post('/comments', (req, res) => {
const { postId, user, content } = req.body;
const sql = 'INSERT INTO comments (post_id, user, content) VALUES (?, ?, ?)';
db.query(sql, [postId, user, content], (err, data) => {
if (!err) {
res.sendStatus(200); // 댓글 등록 성공
} else {
console.log(err);
res.send('전송오류');
}
});
});
app.delete('/comments/:id', (req, res) => {
const id = req.params.id;
const sql = 'DELETE FROM comments WHERE id = ?';
db.query(sql, [id], (err, data) => {
if (!err) {
res.sendStatus(200); // 댓글 삭제 성공
} else {
console.log(err);
res.send('전송오류');
}
});
});
🔷 Express를 사용해 서버를 생성하고, MySQL DB에 연결.
게시판 기능:
👉🏻 게시글 목록 조회: 페이지네이션, 검색기능, 게시글 목록
👉🏻 게시글 상세 조회
👉🏻 게시글 작성
👉🏻 게시글 수정
👉🏻 게시글 삭제
👉🏻 조회수 업데이트
👉🏻 댓글 기능: 조회, 작성, 삭제
📢 라우팅 설정
// src/components/BoardHome.jsx
import React from 'react';
import { BrowserRouter, Route, Routes } from "react-router-dom";
import BoardList from './BoardList';
import BoardDetail from './BoardDetail';
import BoardWrite from './BoardWrite';
import BoardModify from './BoardModify';
import shibainuImage from './shibainu.png';
const BoardHome = () => {
return (
<div className='boardHome'>
<h1>
<span>SK React Board</span>
<img src={shibainuImage} />
</h1>
<hr />
<BrowserRouter>
<Routes>
<Route path='/' element={<BoardList />} />
<Route path='/list' element={<BoardList />} />
<Route path='/detail/:id' element={<BoardDetail />} />
<Route path='/write' element={<BoardWrite />} />
<Route path='/modify/:id' element={<BoardModify />} />
</Routes>
</BrowserRouter>
</div>
);
};
export default BoardHome;
🔷 게시판 기본 구조를 설정함
📢 게시글 목록
// src/components/BoardList.jsx
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom';
const BoardList = () => {
const [boardList, setBoardList] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [pageGroup, setPageGroup] = useState(0);
const [searchTerm, setSearchTerm] = useState('');
const [searchCategory, setSearchCategory] = useState('title');
const [loading, setLoading] = useState(true);
const location = useLocation();
const getQueryParams = () => {
const params = new URLSearchParams(location.search);
return {
page: parseInt(params.get('page')) || 1,
search: params.get('search') || '',
category: params.get('category') || 'title'
};
};
const getBoardData = async (page, search = '', category = 'title') => {
setLoading(true);
try {
const response = await axios(`/list?page=${page}&search=${search}&category=${category}`);
setBoardList(response.data.data);
setTotalPages(response.data.totalPages);
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
const { page, search, category } = getQueryParams();
setCurrentPage(page);
setSearchTerm(search);
setSearchCategory(category);
getBoardData(page, search, category);
}, [location.search]);
const handleSearch = () => {
setCurrentPage(1);
getBoardData(1, searchTerm, searchCategory);
};
const navigate = useNavigate();
const trClick = async (id) => {
try {
await axios.post(`/views/${id}`);
navigate(`/detail/${id}`);
} catch (error) {
console.log(error);
}
};
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
navigate(`?page=${newPage}&search=${searchTerm}&category=${searchCategory}`);
};
const getPageButtons = () => {
const buttons = [];
const startPage = pageGroup * 10 + 1;
const endPage = Math.min(startPage + 9, totalPages);
for (let i = startPage; i <= endPage; i++) {
buttons.push(
<button
key={i}
onClick={() => handlePageChange(i)}
disabled={currentPage === i}
>
{i}
</button>
);
}
return buttons;
};
if (loading) {
return <div>Loading...</div>;
}
return (
<div className='boardList'>
<h2>📢 상호 존중하고 배려하며, 비방 금지</h2>
<div className='searchs'>
<select value={searchCategory} onChange={(e) => setSearchCategory(e.target.value)}>
<option value="title">제목</option>
<option value="contents">내용</option>
<option value="writer">작성자</option>
</select>
<input
type="text"
placeholder="검색어 입력..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button onClick={handleSearch}>검색</button>
</div>
<table>
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회</th>
</tr>
</thead>
<tbody>
{boardList.length > 0 ? (
boardList.map(l => (
<tr key={l.id}>
<td>{l.id}</td>
<td className='title' onClick={() => trClick(l.id)}>
{l.title}
<strong>
{l.comment_count > 0 && ` [${l.comment_count}]`}
</strong>
</td>
<td>{l.writer}</td>
<td>{l.reg_date.substring(0, l.reg_date.indexOf("T"))}</td>
<td>{l.views}</td>
</tr>
))
) : (
<tr>
<td colSpan="5" style={{ textAlign: 'center' }}>게시글이 없습니다.</td>
</tr>
)}
</tbody>
</table>
<div className='pagination'>
{pageGroup > 0 && <button onClick={() => setPageGroup(prev => Math.max(prev - 1, 0))}>이전</button>}
{getPageButtons()}
{pageGroup < Math.ceil(totalPages / 10) - 1 && <button onClick={() => setPageGroup(prev => prev + 1)}>다음</button>}
</div>
<div className='listBot'>
<Link to={`/write`}><button>글쓰기</button></Link>
</div>
</div>
);
};
export default BoardList;
🔷 게시글 목록 페이지 컴포넌트
👉🏻 게시글 목록, 현재 페이지, 총 페이지 수, 검색어 및 카테고리 등의 상태 관리
👉🏻 Axios를 사용, 서버에서 데이터를 비동기로 가져옴.
👉🏻 페이지, 검색어, 카테고리를 URL 쿼리 처리
👉🏻 검색어와 카테고리에 따라 게시글 목록 필터링
👉🏻 페이지 번호 버튼 생성, 페이지 변경 가능
👉🏻 제목 클릭 시 상세 페이지 이동
👉🏻 글쓰기 버튼 제공
📢 게시글 상세보기
// src/components/BoardDetail.jsx
import React, { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import axios from 'axios';
import Comment from './Comment';
const BoardDetail = () => {
const { id } = useParams();
const [board, setBoard] = useState(null);
const getBoard = async () => {
try {
const res = await axios(`/detail/${id}`);
setBoard(res.data[0]);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getBoard();
}, []);
const onDelete = async () => {
if (window.confirm('정말 삭제 하시겠습니까?')) {
try {
await axios.post(`/delete/${id}`);
window.location.href = "/list";
} catch (e) {
console.log(e);
}
}
};
if (board != null) {
return (
<div className='boardDetail'>
<h2>No.{board.id} / 게시글 상세</h2>
<div className='contents'>
<h3>{board.title}</h3>
<div className='writer'>
<span>{board.writer} </span>
<span>[{board.reg_date.substring(0, board.reg_date.indexOf("T"))}]</span>
<span>조회:{board.views}</span>
</div>
<div className='con'>{board.contents}</div>
</div>
<div className='btns'>
<Link to={`/modify/${board.id}`}><button>수정</button></Link>
<button onClick={onDelete}>삭제</button>
<Link to={`/`}><button>목록</button></Link>
</div>
<Comment postId={board.id} />
</div>
);
}
return <div>Loading...</div>; // 로딩 상태 표시
};
export default BoardDetail;
🔷 게시글 상세보기 컴포넌트
👉🏻 제목, 작성자, 작성일, 조회수, 내용 등의 상세 정보 표시.
👉🏻 useParams를 통해 URL에서 게시글 ID를 받아와 Axios로 데이터를 서버에서 가져옴
👉🏻 게시글 데이터를 저장하여 상태관리(useState)
👉🏻 삭제 버튼 누를 시, 확인 메시지 띄우고 삭제 진행. => 게시글 목록으로 리다이렉트
👉🏻 게시글 수정, 목록 버튼
👉🏻 Comment 컴포넌트로 해당 게시글에 대한 댓글 표시
📢 댓글
// src/components/Comment.jsx
import axios from 'axios';
import React, { useEffect, useState } from 'react';
const Comment = ({ postId }) => {
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState({ user: '', content: '' });
useEffect(() => {
fetchComments();
}, []);
const fetchComments = async () => {
const response = await axios.get(`/comments/${postId}`);
setComments(response.data);
};
const handleCommentSubmit = async (e) => {
e.preventDefault();
await axios.post('/comments', { postId, ...newComment });
setNewComment({ user: '', content: '' });
fetchComments();
};
const handleDeleteComment = async (commentId) => {
if (window.confirm('이 댓글을 삭제하시겠습니까?')) {
await axios.delete(`/comments/${commentId}`);
fetchComments(); // 삭제 후 댓글 목록 새로고침
}
};
return (
<div className='comment'>
<form onSubmit={handleCommentSubmit}>
<input
className='cmt-writer'
type="text"
placeholder="작성자.."
value={newComment.user}
onChange={(e) => setNewComment({ ...newComment, user: e.target.value })}
required
/>
<input
className='cmt-contents'
type='text'
placeholder="댓글 내용.."
value={newComment.content}
onChange={(e) => setNewComment({ ...newComment, content: e.target.value })}
required
/>
<button className='com-submit'>댓글등록</button>
</form>
<ul>
{comments.map(comment => (
<li key={comment.id}>
<span>{comment.user}</span><br /><strong>{comment.content} </strong>
<button onClick={() => handleDeleteComment(comment.id)}>X</button>
</li>
))}
</ul>
</div>
);
};
export default Comment;
🔷 댓글 컴포넌트
👉🏻 useEffect를 사용하여 컴포넌트가 처음 렌더링될 때 fetchComments 함수 호출하여, 댓글을 서버에서 가져옴.
👉🏻 handleCommentSubmit 함수로 새로운 댓글을 서버에 POST 요청으로 전송, 입력 필드 초기화, 댓글 목록 갱신.
👉🏻 X버튼 클릭하여 handleDeleteComment 함수 호출, 해당 댓글 삭제 후 댓글 목록 갱신.
📢 게시글 작성
// src/components/BoardWrite.jsx
import React, { useState } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';
const BoardWrite = () => {
const [board, setBoard] = useState({
title: '',
writer: '',
contents: ''
});
const { title, writer, contents } = board;
const onChange = (e) =>{
const { name, value } = e.target;
setBoard({
...board,
[name]:value
});
};
const onReset =()=>{
setBoard({
...board,
title: '',
writer: '',
contents: ''
});
};
const onCreate = async ()=>{
if(title === ''){
alert('제목을 입력해주세요.');
return;
}
if(writer === ''){
alert('작성자를 입력해주세요.');
return;
}
if(contents === ''){
alert('내용을 입력해주세요.');
return;
}
if(window.confirm('등록 하시겠습니까?')){
try {
await axios.post('/write', board);
window.location.href ="/list";
} catch (error) {
console.log(error);
}
}
}
return (
<div className='boardWrite'>
<h2>게시글 작성</h2>
<form action="" className='contents'>
<div className='wrtCon'>
<input type="text" name='title' value={title} placeholder='Title...' className='input-field' onChange={onChange} />
<input type="text" name='writer' value={writer} placeholder='Writer...' className='input-field' onChange={onChange} />
<textarea name='contents' value={contents} placeholder='Contents...' className='textarea-field' onChange={onChange} />
</div>
<div className='btns'>
<button onClick={onCreate} type='button'>제출</button>
<button onClick={onReset}>초기화</button>
<Link to={`/`}><button>목록</button></Link>
</div>
</form>
</div>
);
};
export default BoardWrite;
🔷 게시글 작성 컴포넌트
👉🏻 사용하여 제목(title), 작성자(writer), 내용(contents)을 포함하는 board 객체의 상태
👉🏻 onChange로 입력 필드에서 변경된 값을 받아 board 상태를 업데이트. (각 필드는 name 속성을 통해 어떤 값을 업데이트할지 결정)
👉🏻 onReset 함수는 입력 필드를 초기화
👉🏻 onCreate 함수는 제목, 작성자, 내용이 모두 입력되었는지 확인한 후, 확인창을 띄우고 사용자가 승인하면 axios를 통해 서버에 게시글 데이터를 POST 요청으로 전송. 전송 후에는 목록 페이지로 리다이렉트
📢 게시글 수정
// src/components/BoardModify.jsx
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
const BoardModify = () => {
const { id } = useParams();
const [ mod, setMod ] = useState(null);
const onChange = (e)=>{
const {name, value} = e.target;
setMod({
...mod,
[name]:value
});
};
const getBoard = async()=>{
try {
const res = await axios(`/modify/${id}`);
setMod(res.data[0]);
} catch (e) {
console.log(e);
}
}
useEffect(()=>{
getBoard();
},[]);
const onUpdate = async()=>{
if(mod.title === ''){
alert('title is null!!');
return;
}
if(mod.writer === ''){
alert('writer is null!!!');
return;
}
if(mod.contents === ''){
alert('contents is null!!!');
return;
}
if(window.confirm('수정 하시겠습니까?')){
try {
const res = await axios.post(`/modify/${id}`, mod);
console.log(res);
window.location.href =`/detail/${id}`;
} catch (error) {
console.log(error);
}
}
}
if(mod !==null){
return (
<div className='boardModify'>
<h2>게시글 수정</h2>
<form action="" className='contents'>
<div className='wrtCon'>
<input type="text" name='title' value={mod.title} placeholder='Title...' className='input-field' onChange={onChange} />
<input type="text" name='writer' value={mod.writer} placeholder='Writer...' className='input-field' onChange={onChange} />
<textarea name='contents' value={mod.contents} placeholder='Content...' className='textarea-field' onChange={onChange} />
</div>
<div className='btns'>
<button type='button' onClick={onUpdate}>제출</button>
<Link to={`/`}><button>목록</button></Link>
</div>
</form>
</div>
);
}
};
export default BoardModify;
🔷 게시글 수정 컴포넌트
👉🏻 수정할 게시글(mod)의 상태를 관리
👉🏻 useParams를 통해 URL에서 게시글 ID 추출.
👉🏻 getBoard 함수로 서버에 요청을 보내 게시글 정보를 가져와 상태 업데이트. useEffect로 컴포넌트가 마운트될 때 이 함수를 호출.
👉🏻 onChange 함수는 입력 필드에서 변경된 값을 받아 mod 상태를 업데이트. name 속성을 통해 어떤 값을 업데이트할지 결정.
👉🏻 onUpdate 함수는 제목, 작성자, 내용이 모두 입력되었는지 확인창을 띄우고 사용자가 승인하면 axios를 통해 수정된 데이터를 서버에 POST 요청으로 전송. 수정 후에는 상세 페이지로 리다이렉트.
📢 결과

'프론트엔드 > React' 카테고리의 다른 글
| React로 게시판 만들기(2) (1) | 2024.09.20 |
|---|---|
| React로 게시판 만들기(1) (3) | 2024.09.19 |
| useReducer로 상태 관리 (0) | 2024.09.19 |
| Axios와 useEffect를 사용하여 데이터 가져오기 (0) | 2024.09.09 |
| TODO LIST 만들기 및 useMemo() 개념 (1) | 2024.09.09 |