자주 사용되는 구조
const [params] = useSearchParams();
useEffect(() => {}, [params]);
params.pageNum ➜ 3
qs.toString() ➜ search=writer&keyword=YuJaeSeok
➫ search=writer&keyword=YuJaeSeok&pageNum=3
React 를 하면 좋은 점
react js
↙ ⭣ ↘
WebBrowser Mobile App Desktop App
SPA App android , ios window, mac os
(react native) (electron js)
<td>
<NavLink to={`/board/${item.num}`}>{item.title}</NavLink>
</td>
function BoardDetail() {
return <>
</>
}
BoardDetail 라우트 경로 추가{path:"/board/:num", element:<BoardDetail/>}
useParams 함수 추가const {num} = useParams();
useState 함수 추가const [dto, setDto] = useState({});
useEffect 함수 추가 useEffect(() => {
api.get(`/v1/board/${num}`)
.then(res => {
setDto(res.data);
})
.catch(err => console.log(err));
}, [num]); // 자세히 보여줄 댓글 번호가 변경되면 다시 로딩 되도록
detail 메소드 생성@GetMapping("/board/{num}")
public BoardDto detail(@PathVariable int num) {
return boardService.getData(num);
}
getQuery 상수 추가const getQuery = () => {
let query = "";
if(pageInfo.keyword){
query = `?search=${pageInfo.search}$keyword=${pageInfo.keyword}`;
}
};
<NavLink to={`/board/${item.num}${getQuery()}`}>{item.title}</NavLink>
useSearchParams 함수 추가const [params] = useSearchParams();
useEffect 함수에 api 코드 수정params.toString() 하면 query 문자열이 리턴된다api.get(`/v1/board/${num}?${params.toString()}`)
➜ http://localhost:5173/#/board/120?search=writer&keyword=YuJaeSeok
detail 메소드 수정@GetMapping("/board/{num}")
public BoardDto detail(@PathVariable int num, BoardDto dto) {
// dto 의 search 와 keyword 값이 null 일 수도 있고 아닐 수도 있다(검색어가 있다면 null 이 아니다)
// dto 에 글 번호도 담는다
dto.setNum(num);
// BoardDto 를 전달해서 해당글의 자세한 정보를 받아온다
return boardService.getDetail(dto);
}

"/v1/board/**" 추가.requestMatchers(HttpMethod.GET, "/v1/board", "/v1/board/**").permitAll()
useState 함수 추가const [commentList, SetCommentList] = useState([]);
➤ 1번 게시글 요청 경로
GET /baord/1
➤ 1번 게시글에 달린 댓글 목록 요청 경로
GET ?
/board/1/comments/board/2/comments/myboard/3/comments/yourboard/4/comments➜ @GetMapping("/{category}/{num}/comments
useEffect 함수 안에 댓글 관련 api 코드 추가.get(`/v1/board/${num}/comments`)
.then(res => SetCommentList(res.data))
.catch(err => console.log(err));
commentList 메소드 생성@GetMapping("/board/{num}/comments")
public List<CommentDto> commentList(@PathVariable int num){
return commentService.getComments(num);
}
<h1>게시글 상세보기</h1>
<p className="alert alert-success p-3"></p>
{params.get("search") &&
<p className="alert alert-info px-3 py-2 rounded-3 shadow-sm">
<i className="bi bi-search me-2"></i>
<strong>{params.get("search")}</strong> 조건
<strong>{params.get("keyword")}</strong> 검색 결과 입니다
</p>
}
useNavigate 함수 추가const navigate = useNavigate();
handleMove 함수 추가const handleMove = () => {
};
<div className="btn-group mb-2">
<button className="btn btn-outline-secondary btn-sm">
<i className="bi bi-arrow-left"></i>
Prev
</button>
<button className="btn btn-outline-secondary btn-sm">
<i className="bi bi-arrow-right"></i>
Next
</button>
</div>
disabled 속성 추가<button onClick={movePrev}
disabled={dto.prevNum === 0}
className="btn btn-outline-secondary btn-sm">
<i className="bi bi-arrow-left"></i>
Prev
</button>
<button onClick={moveNext}
disabled={dto.nextNum === 0}
className="btn btn-outline-secondary btn-sm">
<i className="bi bi-arrow-right"></i>
Next
</button>
handelMove 함수 안에 navigate 코드 추가const handleMove = (targetNum) => {
navigate(`/board/${targetNum}${params.get("search") ? "?"+params.toString() : ""}`);
};
onClick 이벤트 추가<button onClick={()=>handleMove(dto.prevNum)}
disabled={dto.prevNum === 0}
className="btn btn-outline-secondary btn-sm">
<i className="bi bi-arrow-left"></i>
Prev
</button>
<button onClick={()=>handleMove(dto.nextNum)}
disabled={dto.nextNum === 0}
className="btn btn-outline-secondary btn-sm">
<i className="bi bi-arrow-right"></i>
Next
</button>
<table className="table table-striped">
<colgroup>
<col className="col-2" />
<col className="col" />
</colgroup>
<tr>
<th>글번호</th>
<td>{dto.num}</td>
</tr>
<tr>
<th>작성자</th>
<td></td>
</tr>
<tr>
<th>제목</th>
<td></td>
</tr>
<tr>
<th>조회수</th>
<td></td>
</tr>
<tr>
<th>작성일</th>
<td></td>
</tr>
</table>
<div className="card mt-4">
<div className="card-header bg-light">
<strong>본문 내용</strong>
</div>
<div className="card-body p-1">
</div>
</div>
<td>
{ !dto.profileImage && <i style={{fontSize:"100px"}} className="bi bi-person-circle"></i>}
{ dto.profileImage &&
<img src={`/upload/${dto.profileImage}`}
style={{width:"100px", height:"100px", borderRadius:"50%"}}/>
}
</td>
<tr>
<th>제목</th>
<td>{dto.title}</td>
</tr>
<tr>
<th>조회수</th>
<td>{dto.viewCount}</td>
</tr>
<tr>
<th>작성일</th>
<td>{dto.createdAt}</td>
</tr>
<div className="card mt-4">
<div className="card-header bg-light">
<strong>본문 내용</strong>
</div>
<div className="card-body p-1" dangerouslySetInnerHTML = {{__html:dto.content}}>
</div>
</div>
getByNum & getByDto 의 createdAt 코드 수정b.createdAt
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="comment">
<select id="selectList" parameterType="int" resultType="commentDto">
<![CDATA[
SELECT c.num, c.writer, c.targetWriter, c.content, c.deleted, c.groupNum, u.profileImage,
CASE
WHEN SYSDATE - c.createdAt < 1/1440 THEN '1분 전'
WHEN SYSDATE - c.createdAt < 10/1440 THEN '10분 전'
WHEN SYSDATE - c.createdAt < 365 THEN
TO_CHAR(TRUNC(SYSDATE - c.createdAt)) || '일 전'
WHEN SYSDATE - c.createdAt < 730 THEN '1년 전'
ELSE '2년 이상'
END AS createdAt,
(SELECT COUNT(*)
FROM comments
WHERE groupNum = c.num AND num != groupNum) AS replyCount
FROM comments c
INNER JOIN users u ON c.writer = u.userName
WHERE c.parentNum = #{num}
ORDER BY c.groupNum ASC, c.num ASC
]]>
</select>
<update id="delete" parameterType="int">
UPDATE comments
SET deleted = 'yes'
WHERE num=#{num}
</update>
<update id="update" parameterType="commentDto">
UPDATE comments
SET content = #{content}
WHERE num=#{num}
</update>
<insert id="insert" parameterType="commentDto">
INSERT INTO comments
(num, writer, targetWriter, content, parentNum, groupNum)
VALUES(#{num}, #{writer}, #{targetWriter}, #{content}, #{parentNum}, #{groupNum})
</insert>
<select id="getSequence" resultType="int">
SELECT comments_seq.NEXTVAL AS num FROM DUAL
</select>
<select id="getByNum" parameterType="int" resultType="commentDto">
SELECT num, writer, targetWriter, content, parentNum, groupNum, createdAt
FROM comments
WHERE num=#{num}
</select>
</mapper>
function Comment() {
return <>
</>
}
Comment 추가{ }(중괄호) 없이 " "(따옴표)만 해도 된다 ➜ 단순히 문자열만 전달할 때 가능<Comment category="board" parentNum={dto.num} parentWriter={dto.writer} list={commentList}/>

props 구조 분해 할당 코드 추가function Comment({category, parentNum, parentWriter, list}) {
return <>
</>
}
<div className="card my-3">
<div className="card-header bg-primary-subtle text-white">
댓글을 입력해 주세요
</div>
<div className="card-body">
<form action="/v1/comments" method="post">
<input type="hidden" name="parentNum" value={parentNum}/>
<input type="hidden" name="targetWriter" value={parentWriter}/>
<div className="mb-3">
<label htmlFor="commentContent" className="form-label">댓글 내용</label>
<textarea id="commentContent" name="content" rows="5" className="form-control" placeholder="댓글을 입력하세요"></textarea>
</div>
<button type="submit" className="btn bg-primary-subtle text-white">등록</button>
</form>
</div>
</div>
import cn from 'classnames';
map 함수을 이용한 div 코드 추가<div className="comments">
{list.map(item =>
<div key={item.num className={cn("card", "mb-3", {"ms-5 re-re d-none" : item.num !== item.groupNum})}>
</div>
)}
</div>
{ item.deleted === 'yes' &&
<div className="card-body bg-light text-muted rounded">삭제된 댓글입니다</div>
}
{ item.deleted !== 'yes' &&
<div className="card-body d-flex flex-column flex-sm-row position-relative">
</div>
}
{ item.replyCount !== 0 && item.num === item.groupNum &&
<button className="dropdown-btn btn btn-outline-secondary btn-sm position-absolute"
style={{bottom:"16px", right:"16px"}}>
<i class="bi bi-caret-down"></i>
답글 {item.replyCount}</button>
}
{ item.num !== item.groupNum &&
<i className="bi bi-arrow-return-right position-absolute"
style={{top:"0", left:"-30px"}}></i>
}
useSelector 함수 추가const userInfo = useSelector(state => state.userInfo);
{ item.writer === userInfo.userName &&
<button data-num={item.num}
className="btn-close position-absolute top-0 end-0 m-1"></button>
}
{ item.profileImage === null ?
<i style={{fontSize:"50px"}}
className="bi bi-person-circle me-3 align-self-center"></i>
:
<img className="rounded-circle me-3 align-self-center"
src={`/upload/${item.profileImage}`}
alt="프로필 이미지"
style={{width:"50px", height:"50px"}}/>
}
<div className="flex-grow-1">
<div className="d-flex justify-content-between">
<div>
<strong>{item.writer}</strong>
<span>@{item.targetWriter}</span>
<small className="text-muted">{item.createdAt}</small>
</div>
</div>
</div>
pre 요소 추가<pre>{item.content}</pre>
{ item.writer === userInfo.userName &&
<>
<button className="btn btn-sm bg-primary-subtle btn-outline-secondary edit-btn">수정</button> <div class="d-none form-div">
<form action="" method="post">
<input type="hidden" name="num" value={item.num}/>
<input type="hidden" name="parentNum" value={parentNum}/>
<textarea name="content" className="form-control mb-2" rows="2">{item.content}</textarea>
<button type="submit" className="btn btn-sm btn-success">수정 완료</button>
<button type="reset" className="btn btn-sm btn-secondary cancel-edit-btn">취소</button>
</form>
</div>
</>
}
{ item.writer !== userInfo.userName &&
<>
<button className="btn btn-sm btn-outline-primary show-reply-btn">댓글</button>
<div className="d-none form-div">
<form action="" method="post">
<input type="hidden" name="parentNum" value={parentNum}/>
<input type="hidden" name="targetWriter" value={item.writer}/>
<input type="hidden" name="groupNum" value={item.groupNum}/>
<textarea name="content" className="form-control mb-2" rows="2"
placeholder="댓글을 입력하세요..."></textarea>
<button type="submit" class="btn btn-sm btn-success">등록</button>
<button type="reset" class="btn btn-sm btn-secondary cancel-reply-btn">취소</button>
</form>
</div>
</>
}
userInfo 관련 코드 수정 및 추가const ➝ letlet userInfo = useSelector(state => state.userInfo);
// 만일 로그인하지 않았으면 null 이기 때문에 빈 object 를 넣어준다
if(!userInfo) userInfo={
};
Comment.jsx 전체 코드
// src/components/Comment.jsx
//클래스를 쉽게 제어하기 위한 유틸리티
import cn from 'classnames';
import { useSelector } from 'react-redux';
function Comment({category, parentNum, parentWriter, list}) {
// redux store 로 부터 로그인 정보를 얻어낸다
let userInfo = useSelector(state=>state.userInfo);
// 만일 로그인을 하지 않았으면 null 이기 때문에 빈 object 를 넣어준다
if(!userInfo)userInfo={};
return <>
<div className="card my-3">
<div className="card-header bg-primary text-white">
댓글을 입력해 주세요
</div>
<div className="card-body">
<form action="/v1/comments" method="post">
<input type="hidden" name="parentNum" value={parentNum} />
<input type="hidden" name="targetWriter" value={parentWriter} />
<div className="mb-3">
<label htmlFor="commentContent" className="form-label">댓글 내용</label>
<textarea id="commentContent" name="content" rows="5" className="form-control" placeholder="댓글을 입력하세요"></textarea>
</div>
<button type="submit" className="btn btn-success">등록</button>
</form>
</div>
</div>
<div className="comments">
{list.map(item=>
<div key={item.num} className={cn("card","mb-3", {"ms-5 re-re d-none": item.num !== item.groupNum})}>
{ item.deleted === 'yes' &&
<div className="card-body bg-light text-muted rounded">삭제된 댓글 입니다</div>
}
{ item.deleted !== 'yes' &&
<div className="card-body d-flex flex-column flex-sm-row position-relative">
{ item.replyCount !== 0 && item.num === item.groupNum &&
<button className="dropdown-btn btn btn-outline-secondary btn-sm position-absolute"
style={{bottom:"16px", right:"16px"}}>
<i className="bi bi-caret-down"></i>
답글 {item.replyCount} 개
</button>
}
{ item.num !== item.groupNum &&
<i className="bi bi-arrow-return-right position-absolute"
style={{top:0, left:"-30px"}}></i>
}
{ item.writer === userInfo.userName &&
<button data-num={item.num}
className="btn-close position-absolute top-0 end-0 m-1"></button>
}
{ item.profileImage === null ?
<i style={{fontSize:"50px"}} className="bi bi-person-circle me-3 align-self-center"></i>
:
<img className="rounded-circle me-3 align-self-center"
src={`/upload/${item.profileImage}`}
alt="프로필 이미지"
style={{width:"50px", height:"50px"}} />
}
<div className="flex-grow-1">
<div className="d-flex justify-content-between">
<div>
<strong>{item.writer}</strong>
<span>@{item.targetWriter}</span>
<small className="text-muted">{item.createdAt}</small>
</div>
</div>
<pre>{item.content}</pre>
{ item.writer === userInfo.userName &&
<>
<button className="btn btn-sm btn-outline-secondary edit-btn">수정</button>
<div className="d-none form-div">
<form action="" method="post">
<input type="hidden" name="num" value={item.num}/>
<input type="hidden" name="parentNum" value={parentNum}/>
<textarea name="content" className="form-control mb-2" rows="2" >{item.content}</textarea>
<button type="submit" className="btn btn-sm btn-success">수정 완료</button>
<button type="reset" className="btn btn-sm btn-secondary cancel-edit-btn">취소</button>
</form>
</div>
</>
}
{ item.writer !== userInfo.userName &&
<>
<button className="btn btn-sm btn-outline-primary show-reply-btn">댓글</button>
<div className="d-none form-div">
<form action="" method="post">
<input type="hidden" name="parentNum" value={parentNum}/>
<input type="hidden" name="targetWriter" value={item.writer}/>
<input type="hidden" name="groupNum" value={item.groupNum}/>
<textarea name="content" className="form-control mb-2" rows="2"
placeholder="댓글을 입력하세요..."></textarea>
<button type="submit" className="btn btn-sm btn-success">등록</button>
<button type="reset" className="btn btn-sm btn-secondary cancel-reply-btn">취소</button>
</form>
</div>
</>
}
</div>
</div>
}
</div>
)}
</div>
</>
}
export default Comment;