게시물 상세보기 페이지 구현React+REST API 게시판 구현/FE - React2021. 10. 18. 22:11
Table of Contents
What to do?
- 게시판 페이지와 내 게시물 페이지에서 하나를 선택해서 클릭했을 때 해당 글의 상세 보기 페이지로 이동하도록 하자.
- 상세보기 페이지에는 작성자, 작성일자, 제목, 내용, 수정, 삭제 버튼이 포함되어 있다.
- 해당 글의 작성자가 로그인을 했을 때만 수정, 삭제 버튼이 보이게 하자.
- 로그인을 한 사용자의 jwt-token에서 user의 ID를 추출한 후, board(해당 글)의 작성자(user의 id)를 비교했을 때 같으면 수정, 삭제 버튼이 보이게 한다. id는 DB에 저장되어 있는 유저의 고유 번호이다.
- 게시판 상세보기의 경우는 URL이 고정되어 있지 않기 때문에 /board/:board_id 로 동적 라우팅을 설정하자.
- :board_id에서 board_id는 DB에 저장되어 있는 board의 ID이다.
- 추후에 수정 버튼을 클릭하면 해당 글을 수정할 수 있는 페이지(/board-edit/:board_id)로 이동하고 삭제 버튼을 클릭하면 정말로 해당 글을 삭제할지 물어보는 모달창을 띄우자!
- 글 작성자가 로그인을 하기 전엔 수정, 삭제 버튼이 보이지 않지만 로그인을 하면 수정 삭제 버튼이 나타난다!
게시물 상세보기 페이지 구현
- pages 디렉토리 아래 board 디렉토리를 만들고 Board.js 파일과 board.scss 파일을 만들자.
pages/board/Board.js
- 로그인한 사용자의 redux-persist에 저장된 토큰의 payload에 있는 id와 게시물을 등록한 user의 id가 같으면 수정, 삭제 버튼을 보이게 하고 삭제 버튼을 누르면 정말로 삭제할 것인지 물어보는 모달창을 띄운다.
- 수정 버튼을 누르면 수정하기 페이지 /edit-board/:board_id로 url을 변경시킨다.
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);
}}
>
예
</Button>
<Button
variant="outlined"
color="primary"
onClick={() => {
setShow(false)
}}
>
아니오
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</React.Fragment>
);
}
export default Board;
pages/board/board.scss
- keyframes를 사용해서 fade-in 효과를 주었고 기본적인 요소들의 flex를 이용해서 깔끔하게 배치했다
- 또한 미디어 쿼리를 사용해서 크로스 브라우징 처리를 했다.
@keyframes smoothAppear {
from {
opacity: 0;
transform: translateY(-10%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media all and (min-width: 1024px){
.board-wrapper{
width: 500px;
.board-body{
height: 400px;
}
}
}
@media all and (max-width: 1024px) {
.board-wrapper{
width: 450px;
.board-body{
height: 360px;
}
}
}
@media all and (max-width: 768px) {
.board-wrapper {
width: 400px;
.board-body{
height: 320px;
}
}
}
@media all and (max-width: 768px) {
.board-wrapper {
width: 360px;
.board-body{
height: 300px;
}
}
}
.edit-delete-button{
display: flex;
flex-direction: row-reverse;
margin-bottom: 0.7rem;
button{
font-size: 1.2rem;
}
.delete-button{
margin-left: 0.5rem;
}
}
.modal{
.modal-title{
font-size: 2rem;
font-weight: bold;
padding: 2rem 0;
}
.modal-button{
display: flex;
align-items: center;
justify-content: space-between;
button{
font-size: 1.4rem;
}
}
}
.board-wrapper{
transition: width 1s;
margin: 0 auto 1.5rem auto;
opacity: 0;
border-radius: 10px;
animation: smoothAppear 1.5s forwards;
animation-delay: 0.5s;
font-family: 'Noto Sans KR', sans-serif;
background-color: #fff;
padding: 4rem;
.board-header {
justify-content: space-between;
align-items: baseline;
display: flex;
.board-header-username{
font-size: 2rem;
}
.board-header-date{
font-size: 1.5rem;
vertical-align: baseline;
}
}
.board-body{
display: flex;
.board-image{
flex-shrink: 0;
margin-right: 1rem;
width: 50%;
height: 100%;
img{
width: 100%;
height: 100%;
object-fit: cover;
}
}
.board-title-content{
word-break: break-all;
margin-left: 1rem;
overflow: auto;
flex-grow: 1;
&::-webkit-scrollbar {
display: none;
}
.board-title{
font-size: 1.8rem;
font-weight: bold;
margin-bottom: 0.5rem;
color: midnightblue;
}
.board-content{
font-size: 1.2rem;
}
}
}
.board-body{
display: flex;
}
.board-footer{
}
}
라우팅 추가하기
App.js
- 동적 라우팅 추가하기. URI 파라미터를 사용!
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";
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/>}/>
</Routes>
</React.Fragment>
)
}
export default App;
'React+REST API 게시판 구현 > FE - React' 카테고리의 다른 글
댓글 보기(페이지네이션), 추가 구현 (2) | 2021.10.25 |
---|---|
게시물 수정, 삭제 구현 (0) | 2021.10.19 |
게시판/내 게시물 페이지 구현 - 페이지네이션 (0) | 2021.10.13 |
게시물 등록하기 페이지 구현 (0) | 2021.10.11 |
axios interceptor 구현하기 (0) | 2021.10.04 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!