댓글 보기(페이지네이션), 추가 구현React+REST API 게시판 구현/FE - React2021. 10. 25. 11:22
Table of Contents
What to do?
- 게시판 상세 보기 페이지(/board/:board_id)에 댓글 기능을 구현해보자.
- 댓글 기능은 게시판 상세보기 페이지 안에 구현할 것이므로 별도의 라우팅으로 처리하지 않고 컴포넌트(Comments.js)를 만들어서 게시판 상세 보기 컴포넌트(Board.js)에 추가한다.
- 게시판 상세 보기 컴포넌트에서 댓글 기능 컴포넌트에 해당 게시판(board)의 id를 넘겨줘서 댓글 기능 컴포넌트에서 해당 board의 id를 가지고 DB에서 해당 게시판의 댓글을 가져와서 렌더링하거나 추가 기능을 수행할 수 있도록 하자.
- 댓글 목록은 props로 해당 게시판의 id를 받아서 DB에서 조회한 후에 화면에 렌더링된다.
- 댓글을 추가하려면 로그인을 해야하기 때문에 로그인이 되어있지 않아서 jwt-token 정보가 없는 경우에 댓글을 쓰는 textarea를 클릭하면 로그인 페이지로 이동할 것인지 물어보는 모달창이 화면에 나타난다. 만약 로그인에 완료되면 기존의 게시판 상세 보기 페이지로 돌아온다.
- 댓글을 등록해서 DB에 반영함과 동시에 화면에 나타난다!
- 댓글을 보여줄 때, 버튼을 누르면 페이지수만큼 서버에서 데이터를 가져온다 -> 버튼을 이용한 페이지네이션?
게시판 상세보기 컴포넌트에 댓글 컴포넌트 추가
pages/board/Board.js
- 댓글 기능 컴포넌트인 <Comments/> 컴포넌트를 추가하고 props로 board의 id를 넘겨서 board의 댓글을 가져오거나 추가하도록 한다
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";
import Comments from "../../components/Comments";
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">
<Comments board_id={board_id}/>
</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;
댓글 기능 구현
- 별도의 페이지 기능이 없으므로 components 폴더 아래 Comments.js 파일과 comments.scss 파일을 생성하자
댓글 추가 기능
- 게시물 상세보기 페이지에서 댓글을 입력하는 textarea를 클릭할 때, 로그인하지 않은 사용자면 redirectUrl을 넘겨서 로그인 페이지로 넘어가게한 후 로그인이 완료되면 다시 해당 게시물로 돌아오게 한다.
- 로그인에 성공해서 댓글을 작성하면 완료 알람을 띄우고 컴포넌트를 다시 마운트해서 최신의 서버 상태를 받아오기 위해 새로고침을 한다.
댓글 보기 - 페이지네이션
- 맨 처음에 1페이지에 해당하는 댓글을 가져오고 댓글 더 보기 버튼을 다음 페이지에 해당하는 댓글을 가져온다
- 서버와 약속을 해야 구현할 수 있음. 나는 페이지가 1부터 시작하고 한 페이지당 댓글은 5개로 구현!
- 서버에서 받은 pageCount(전체 페이지)와 현재 page가 같으면 더이상 댓글을 가져오는 api를 호출하지 않기 위해 댓글 더보기 버튼을 사라지게한다!
components/Comments.js
- page와 pageCount 상태에 주의하자!
- page(현재 페이지)와 pageCount(총 페이지 갯수)가 같으면 서버에서 모든 댓글을 가져온 상태이므로 댓글 더보기 버튼이 보이지 않게 한다.
- page의 초기 상태가 1이기 때문에 컴포넌트가 마운트 된 후 첫페이지를 가져온다. 만약 pageCount가 5이고 현재 page가 4라면 버튼을 누르는 순간 page가 5가되어 마지막 페이지의 데이터를 가져온다.
import React, {useCallback, useEffect, useState} from "react";
import axios from "axios";
import moment from 'moment';
import {Button, Dialog, DialogContent, IconButton, TextField} from "@mui/material";
import {useSelector} from "react-redux";
import {jwtUtils} from "../utils/jwtUtils";
import api from "../utils/api";
import {useLocation, useNavigate} from "react-router-dom";
import DisabledByDefaultOutlinedIcon from "@mui/icons-material/DisabledByDefaultOutlined";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import "./comments.scss";
const Comments = ({board_id}) => {
// 로그인 후 현재 경로로 돌아오기 위해 useLocation 사용
const location = useLocation();
const navigate = useNavigate();
const [commentList, setCommentList] = useState([]);
// 입력한 댓글 내용
const [content, setContent] = useState("");
const token = useSelector(state => state.Auth.token);
// 현재 페이지, 전체 페이지 갯수
const [page, setPage] = useState(1);
const [pageCount, setPageCount] = useState(0);
// modal이 보이는 여부 상태
const [show, setShow] = useState(false);
// 페이지에 해당하는 댓글 목록은 page 상태가 변경될 때마다 가져옴
// 맨 처음 페이지가 1이므로 처음엔 1페이지에 해당하는 댓글을 가져온다
useEffect(() => {
const getCommentList = async () => {
const {data} = await axios.get(`/api/comment/list?board_id=${board_id}&page_number=${page}&page_size=${5}`);
return data;
}
// 기존 commentList에 데이터를 덧붙임
getCommentList().then((result) => setCommentList([...commentList, ...result]));
}, [page])
// 페이지 카운트는 컴포넌트가 마운트되고 딱 한번만 가져오면됨
useEffect(() => {
// 댓글 전체 갯수 구하기
const getTotalBoard = async () => {
const {data} = await axios.get(`/api/comment/count?board_id=${board_id}`);
return data.total;
}
// 페이지 카운트 구하기: (전체 comment 갯수) / (한 페이지 갯수) 결과 올림
getTotalBoard().then((result) => setPageCount(Math.ceil(result / 5)));
}, []);
// 댓글 추가하기, 댓글 추가하는 API는 인증 미들웨어가 설정되어 있으므로
// HTTP HEADER에 jwt-token 정보를 보내는 interceptor 사용
const submit = useCallback(async () => {
const comment = {
board_id: board_id,
// DB에 엔터가 먹힌 상태로 들어가므로 제대로 화면에 띄우기 위해 <br>로 치환
content: content,
user_id: jwtUtils.getId(token)
}
// axios interceptor 사용 : 로그인한 사용자만 쓸 수 있다!
await api.post('/api/comment', comment);
alert("댓글 등록 완료");
window.location.reload();
}, [content]);
console.log(commentList)
/*modal 관련 코드*/
// 로그인 후 돌아올 수 있게 현재 경로 세팅
const goLogin = () => {
setShow(false);
navigate(`/login?redirectUrl=${location.pathname}`);
}
// 로그인을 하지 않은 상태에서 댓글 입력 창을 클릭하면 Modal이 열림.
const isLogin = () => {
if (!jwtUtils.isAuth(token)) {
setShow(true);
}
}
return (
<div className="comments-wrapper">
<div className="comments-header">
<TextField
className="comments-header-textarea"
maxRows={3}
onClick={isLogin}
onChange={(e) => {
setContent(e.target.value)
}}
multiline placeholder="댓글을 입력해주세요✏️"
/>
{content !== "" ? (
<Button variant="outlined" onClick={submit}>등록하기</Button>
) : (
<Button variant="outlined" disabled={true}>
등록하기
</Button>
)}
</div>
<div className="comments-body">
{commentList.map((item, index) => (
<div key={index} className="comments-comment">
<div className="comment-username-date">
<div className="comment-date">{moment(item.created).add(9, "hour").format('YYYY-MM-DD HH:mm:ss')}</div>
</div>
<div className="comment-content">{item.content}</div>
<div className="comment-username">{item.user.username}</div>
<hr/>
</div>
))}
</div>
{
/*
page(현재 페이지)와 pageCount(총 페이지 갯수)가 같으면 서버에서
모든 댓글을 가져온 상태이므로 댓글 더보기 버튼이 보이지 않게 한다.
page의 초기 상태가 1이기 때문에 컴포넌트가 마운트 된 후 첫페이지를 가져오고 만약 pageCount가 5이고
현재 page가 4라면 버튼을 누르는 순간 page가 5가되어 마지막 페이지의 데이터를 가져온다.
*/
page < pageCount && (
<div className="comments-footer"
onClick={() => {
setPage(page + 1);
}}
>
댓글 더보기
<KeyboardArrowDownIcon/>
</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-content">로그인 페이지로 이동하시겠습니까?</div>
<div className="modal-button">
<Button
variant="outlined" color="error"
onClick={goLogin}
>
예
</Button>
<Button
variant="outlined" color="primary"
onClick={() => {
setShow(false)
}}
>
아니오
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
);
}
export default Comments;
components/comments.scss
.comments-wrapper {
.comments-header {
margin: 1rem 0;
display: flex;
.comments-header-textarea {
flex: 1;
&::-webkit-scrollbar {
display: none;
}
}
button {
margin-left: 0.5rem;
font-size: 1.1rem;
}
}
.comments-body {
.comments-comment {
margin: 0.5rem 0;
.comment-username-date {
display: flex;
align-items: baseline;
justify-content: space-between;
.comment-date {
color: lightgray;
}
}
.comment-content {
// \n(엔터)를 태그 안의 텍스트에 적용
white-space: pre-wrap;
word-break: break-all;
margin: 0.4rem auto;
}
}
.comment-username {
display: flex;
flex-direction: row-reverse;
align-items: baseline;
font-weight: 600;
}
}
.comments-footer{
display: flex;
justify-content: center;
cursor: pointer;
align-items: center;
}
}
'React+REST API 게시판 구현 > FE - React' 카테고리의 다른 글
기타 추가 사항 (0) | 2022.03.18 |
---|---|
게시물 수정, 삭제 구현 (0) | 2021.10.19 |
게시물 상세보기 페이지 구현 (0) | 2021.10.18 |
게시판/내 게시물 페이지 구현 - 페이지네이션 (0) | 2021.10.13 |
게시물 등록하기 페이지 구현 (0) | 2021.10.11 |
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!