게시물 수정, 삭제 구현React+REST API 게시판 구현/FE - React2021. 10. 19. 23:32
Table of Contents
What to do?
게시물 수정
- 게시판 작성자가 로그인을 해서 해당 게시판의 상세보기 페이지에 접속해서 수정, 삭제를 하려고 하는 경우를 구현하자.
- 게시판 상세 보기 페이지에서 수정 버튼을 클릭하면 게시판 수정 페이지(/edit-board/:board_id)로 이동해서 해당 게시판을 수정할 수 있게 하고 수정이 완료되면 해당 게시물 상세보기 페이지(/board/:board_id)로 이동시키자.
- 게시판 수정 페이지는 로그인한 사용자만 이용할 수 있으므로 PrivateRoute로 처리하고 HTTP 요청을 미리 구현해 둔 http 헤더에 jwt-token을 보내는 api interceptor를 사용하자.
게시물 삭제
- 게시판 상세보기 페이지에서 삭제 버튼을 클릭하면 해당 글을 삭제하는지 물어보는 모달창을 띄우고 모달창의 확인 버튼을 클릭하면 해당 글을 삭제하고 내 게시물 보기 페이지(/myboard-list)로 이동시키자.
- 게시판 삭제는 별도의 컴포넌트 없이 게시판 상세 보기 페이지에서 로그인한 사용자만 이용할 수 있으므로 HTTP 요청을 미리 구현해 둔 http 헤더에 jwt-token을 보내는 api interceptor를 사용한다.
게시물 수정하기 구현
- pages 디렉토리 아래 edit-board 디렉토리를 만들고 EditBoard.js 파일을 생성하자. AddBoard 컴포넌트와 같은 스타일을 사용할 것이므로 따로 scss 파일을 만들지 않아도 된다!
- 해당 글의 작성자가 로그인을 했을 경우에만 수정 버튼이 보이고, 수정 버튼을 클릭하면 글을 수정하는 페이지로 이동한다!
- 글을 수정하고 해당 게시물 상세보기 페이지로 이동하면 잘 반영된 모습을 볼 수 있다!
pages/edit-board/EditBoard.js
- useEffect 훅을 사용해서 사용자가 이전에 등록했던 게시물의 상태를 그대로 보여줘서 UX적인 요소를 가미하고 만약 이미지를 선택하지 않고 수정 버튼을 클릭해도 서버에 저장된 이미지를 그대로 사용할 것이므로 제목과 내용만 빈칸이 아니면 된다.
→ API를 그렇게 작성했기 때문!
import {useSelector} from "react-redux";
import {useNavigate, useParams} from "react-router-dom";
import {useCallback, useEffect, useState} from "react";
import ImageUploader from "../../components/ImageUploader";
import api from "../../utils/api";
import TextArea from "../../components/TextArea";
import {Button} from "@mui/material";
import "../add-board/addBoard.scss";
import axios from "axios";
import {toast} from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
const EditBoard = () => {
const token = useSelector(state => state.Auth.token);
const navigate = useNavigate();
// URI 파라미터 가져오기
const {board_id} = useParams();
// 게시판 제목, 내용, 사진
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [image, setImage] = useState({
image_file: "",
preview_URL: "image/default_image.png",
});
// 사용자가 직전에 등록한 게시물의 상태를 그대로 보여주기 위해
// 컴포넌트가 마운트되고 URI 파라미터에 해당하는 board를 가져와서
// title, content, image의 상태를 바꿔줌
useEffect(() => {
const getBoard = async () => {
const {data} = await axios.get(`/api/board/${board_id}`);
return data;
}
getBoard().then((result) => {
setTitle(result.title);
setContent(result.content);
// 이미지는 파일을 불러올 필요가 없이 미리보기 url만 가져온다.
// 이미지를 선택하지 않고 올리면 db에 저장되어 있는 이미지를 그대로 사용!
setImage({...image, preview_URL: `/api/image/view/${board_id}`})
});
}, [])
const canSubmit = useCallback(() => {
return content !== "" && title !== "";
}, [image, title, content]);
const handleSubmit = useCallback(async () => {
try {
const formData = new FormData();
formData.append("title", title);
formData.append("content", content);
// 이미지를 선택했을 때만 formdata에 넣음
formData.append("file", image.image_file);
// 수정할 땐 board_id를 보내자
formData.append("id", board_id);
await api.put("/api/board", formData);
window.alert("😎수정이 완료되었습니다😎");
// 이전 페이지로 돌아가기
window.location.href = `/board/${board_id}`;
} catch (e) {
// 서버에서 받은 에러 메시지 출력
toast.error("오류발생! 이모지를 사용하면 오류가 발생할 수 있습니다" + "😭", {
position: "top-center",
});
}
}, [canSubmit]);
return (
<div className="addBoard-wrapper">
<div className="addBoard-header">
게시물 수정하기 🖊️
</div>
<div className="submitButton">
{canSubmit() ? (
<Button
onClick={handleSubmit}
className="success-button"
variant="outlined"
>
수정하기😃
</Button>
) : (
<Button
className="disable-button"
variant="outlined"
size="large"
>
제목과 내용을 모두 입력하세요😭
</Button>
)}
</div>
<div className="addBoard-body">
<ImageUploader setImage={setImage} preview_URL={image.preview_URL}/>
<TextArea setTitle={setTitle} setContent={setContent} title={title} content={content}/>
</div>
</div>
);
}
export default EditBoard;
App.js
- 게시물 수정 페이지(/edit-board/:board_id)의 동적 라우팅을 추가하고 로그인한 사용자만 이용할 수 있으므로 PrivateRoute로 처리했다.
import React from "react";
import {Routes, Route, useLocation} from "react-router-dom";
import Header from "./components/Header";
import Home from "./pages/home/Home";
import SignUp from "./pages/sign-up/SignUp";
import Login from "./pages/login/Login";
import PrivateRoute from "./routes/PrivateRoute";
import AddBoard from "./pages/add-board/AddBoard";
import BoardList from "./pages/board-list/BoardList";
import MyBoardList from "./pages/myboard-list/MyBoardList";
import Board from "./pages/board/Board";
import EditBoard from "./pages/edit-board/EditBoard";
const App = () => {
const location = useLocation();
return (
<React.Fragment>
<Header/>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/sign-up" element={<SignUp/>}/>
<Route path="/login" element={<Login/>}/>
<Route
path="/add-board"
element={
<PrivateRoute path="/add-board" component={AddBoard}/>
}
/>
<Route path="/board-list" element={<BoardList/>}/>
<Route
path="/myboard-list"
element={
// 쿼리 파라미터가 존재하므로 전체 url을 PrivateRoute에 넘겨준다
<PrivateRoute path={`${location.pathname}`} component={MyBoardList}/>
}
/>
<Route path="/board/:board_id" element={<Board/>}/>
<Route
path="/edit-board/:board_id"
element={
// URI 파라미터가 존재하므로 전체 url을 PrivateRoute에 넘겨준다
<PrivateRoute path={`${location.pathname}`} component={EditBoard}/>
}
/>
</Routes>
</React.Fragment>
)
}
export default App;
게시물 삭제 구현
- 게시물 상세보기 페이지에서 해당 글의 작성자가 로그인을 했을 경우에만 삭제 버튼이 보이고, 삭제 버튼을 클릭하면 글을 삭제하겠는지 물어보는 모달창이 나타난다.
- 게시물이 삭제되면 게시판과 내 게시물에서 사라진 모습을 볼 수 있다!
- 게시물 삭제는 별도의 라우팅 처리로 페이지 역할을 하지 않으므로 별도의 컴포넌트 없이 게시판 상세 보기 페이지에서 삭제 버튼을 클릭해서 나오는 모달창에서 OK 버튼을 클릭하면 삭제하는 API를 호출한다. (axios intercetpor 사용)
게시물 상세보기 페이지 수정
pages/board/Board.js (게시물 상세보기 페이지)
- 삭제 버튼을 클릭하면 모달창이 나타나고 모달창의 예 버튼을 클릭하면 게시물 삭제 API를 호출하고 내 게시물(/myboard-list)로 이동한다.
import React, {useEffect, useState} from "react";
import axios from "axios";
import {useNavigate, useParams} from "react-router-dom";
import "./board.scss";
import {jwtUtils} from "../../utils/jwtUtils";
import {Button, Dialog, DialogContent, IconButton} from "@mui/material";
import {useSelector} from "react-redux";
import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined';
import DeleteForeverOutlinedIcon from '@mui/icons-material/DeleteForeverOutlined';
import DisabledByDefaultOutlinedIcon from "@mui/icons-material/DisabledByDefaultOutlined";
import api from "../../utils/api";
import moment from "moment";
const Board = () => {
// URL 파라미터 받기 - board의 id
const {board_id} = useParams();
const [board, setBoard] = useState({});
const [isLoaded, setIsLoaded] = useState(false);
const token = useSelector(state => state.Auth.token);
const navigate = useNavigate();
// modal이 보이는 여부 상태
const [show, setShow] = useState(false);
// board 가져오기
useEffect(() => {
const getBoard = async () => {
const {data} = await axios.get(`/api/board/${board_id}`);
return data;
}
getBoard().then(result => setBoard(result)).then(() => setIsLoaded(true));
}, [])
return (
<React.Fragment>
{isLoaded && (
<div className="board-wrapper">
{
/*
해당 글의 작성자가 로그인을 했을 때만 수정, 삭제 버튼이 보이게 하자.
로그인을 한 사용자의 jwt-token에서 user의 ID를 추출한 후,
board(해당 글)의 user의 ID를 비교했을 때 같으면 수정, 삭제 버튼이 보이게 한다.
ID는 DB에 저장되어 있는 유저의 고유 번호이다.
*/
jwtUtils.isAuth(token) && jwtUtils.getId(token) === board.user.id &&
<div className="edit-delete-button">
<Button
variant="outlined" color="error" endIcon={<DeleteForeverOutlinedIcon/>}
className="delete-button"
onClick={() => {
setShow(true)
}}
>
삭제
</Button>
<Button
variant="outlined" endIcon={<BuildOutlinedIcon/>}
onClick={() => {
navigate(`/edit-board/${board_id}`)
}}
>
수정
</Button>
</div>
}
<div className="board-header">
<div className="board-header-username">{board.user.username}</div>
<div className="board-header-date">{moment(board.created).add(9,"hour").format('YYYY-MM-DD')}</div>
</div>
<hr/>
<div className="board-body">
<div className="board-image">
<img src={`/api/image/view/${board_id}`}/>
</div>
<div className="board-title-content">
<div className="board-title">{board.title}</div>
<div className="board-content">{board.content}</div>
</div>
</div>
<hr/>
<div className="board-footer"></div>
</div>
)}
{/*modal*/}
<Dialog open={show}>
<DialogContent style={{position: "relative"}}>
<IconButton
style={{position: "absolute", top: "0", right: "0"}}
onClick={() => setShow(false)}
>
<DisabledByDefaultOutlinedIcon/>
</IconButton>
<div className="modal">
<div className="modal-title"> 정말 삭제하시겠습니까 ?</div>
<div className="modal-button">
<Button
variant="outlined"
color="error"
onClick={async () => {
setShow(false);
// 모달의 예 버튼 클릭시 게시물 삭제
await api.delete(`/api/board/${board_id}`);
alert("게시물이 삭제되었습니다😎");
window.location.href = "/myboard-list";
}}
>
예
</Button>
<Button
variant="outlined"
color="primary"
onClick={() => {
setShow(false)
}}
>
아니오
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</React.Fragment>
);
}
export default Board;
'React+REST API 게시판 구현 > FE - React' 카테고리의 다른 글
기타 추가 사항 (0) | 2022.03.18 |
---|---|
댓글 보기(페이지네이션), 추가 구현 (2) | 2021.10.25 |
게시물 상세보기 페이지 구현 (0) | 2021.10.18 |
게시판/내 게시물 페이지 구현 - 페이지네이션 (0) | 2021.10.13 |
게시물 등록하기 페이지 구현 (0) | 2021.10.11 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!