📝 TIL
[TIL] JSP 댓글 기능 구현 (Comment)
오늘 ONEUL
2022. 6. 21. 21:27
✍ Today I Learned
- 댓글을 달 때마다 페이지가 리로딩된다면? 트래픽 양을 감당하기 힘들 것이다. 페이지를 이동하지 않고 자바스크립트 비동기 통신을 이용하여 댓글 기능을 구현해보자.
- 순서는 VO → mapper → DAO → Service → Controller → 화면단(view)
- 먼저 프로젝트를 분리하고 톰캣에 올라가 있는 프로젝트를 remove 해준다.
[CommentVO.java]
- comment 테이블의 pk인 cno와 board 테이블의 pk인 bno는 long 타입으로 선언한다.
- 비동기 통신에서는 register 대신 post라는 단어를 주로 사용한다.
- post, list, modify 상황별 생성자를 만들고 getter, setter를 생성한다. (list의 상황에서는 전체 컬럼을 select 할 것이기 때문에 굳이 생성자가 필요 없지만 주고받는 데이터를 분명히 하기 위해 생성한다.)
- 댓글에는 detail의 상황이 따로 없다.
[commentMapper.xml]
- 다른 mapper.xml 파일과 마찬가지로 dtd를 먼저 작성한다.
- namespace를 CommentMapper로 지정하고, <insert, select, update, delete> 상황에 맞는 쿼리문을 작성한다.
- selectList의 경우, '어떤' 게시글의 댓글인지 식별하기 위해 bno를 파라미터로 설정한다. 따라서 parameterType은 long으로 작성한다.
- 게시글이 삭제되었을 경우, 해당 게시글의 전체 댓글이 삭제되어야 하기 때문에 deleteAll의 상황을 담은 쿼리문을 작성한다.
<?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="CommentMapper">
<insert id="add" parameterType="cvo">
insert into comment (bno, writer, content)
values (#{bno}, #{writer}, #{content})
</insert>
<select id="list" resultType="cvo" parameterType="long">
select * from comment where bno = #{bno}
</select>
<update id="mod" parameterType="cvo">
update comment set content = #{content}, mod_at = now()
where cno = #{cno}
</update>
<delete id="del" parameterType="long">
delete from comment where cno = #{cno}
</delete>
<delete id="delAll" parameterType="long">
delete from comment where bno = #{bno}
</delete>
</mapper>
[MybatisConfig.xml]
- mapper를 정의하고, typeAliases를 cvo로 설정한다.
- 이후 DAO → Service 순서로 구현한다.
- 여기까지는 여태까지 구현한 방식과 크게 다르지 않다.
[CommentCtrl.java]
- Controller는 servlet으로 생성한다. URL mappings는 /cmt/*로 설정하고,
doPost()
,doGet()
,service()
메서드를 오버라이딩 한다. service()
메서드에 모든 로직을 작성한다.- request와 response 객체는 이전과 같이 UTF-8로 인코딩 하지만, ContentType은 따로 지정하지 않는다. 주고받는 데이터가 json이기 때문이다.
- String 타입으로 전달된 json 형태의 데이터를 직접 분리하려면 많은 시간이 소요될 것이다. json 데이터를 쉽게 처리할 수 있는 여러 가지 오픈소스 라이브러리 중 google에서 제공하는 json-simple 라이브러리를 이용할 것이다.
- 자바스크립트에서 어떤 방식으로 json 데이터를 보내는지 잘 기억해두자.
// 서버로 부터 데이터를 받을 때
const obj = JSON.parse(text);
// 서버로 데이터를 보낼 때
const myJSON = JSON.stringify(obj);
post(insert)
- 기본적으로 모든 switch문에 각각 try/catch를 설정한다.
- 멀티 스레드 환경에서 보다 안전하게 문자열을 받을 수 있는 StringBuffer 자료형에 전달받은 데이터를 담을 것이다.
StringBuffer 자료형은append()
메서드를 사용하여 문자열을 추가할 수 있고,toString()
메서드를 이용하여 String 자료형으로 변경할 수 있다. (참고자료) - 먼저 StringBuffer 객체를 생성하고, request에 있는 모든 데이터를
getReader()
로 받아와 BufferedReader에 담는다. - 이후 json-simple 라이브러리를 이용하여 키값을 추출할 수 있는 형태로 파싱 한다.
- 만들어둔 Service 객체의 post 메서드에 추출한 데이터로 만든 VO 객체를 전달한다.
response.getWriter()
를 호출하여 응답으로 내보낼 출력 스트림을 얻어낸 후,out.print()
로 스트림에 데이터를 기록한다. (자바스크립트에서 fetch API의 then 메서드로 전달받는 게 바로 이 print 객체이다.)
StringBuffer sb = new StringBuffer();
String line = null;
BufferedReader br = req.getReader();
while ((line = br.readLine()) != null) {
sb.append(line);
}
log.info(">>> sb : {}", sb.toString());
// 여기서부터 json simple 라이브러리 이용
JSONParser parser = new JSONParser();
JSONObject jsonObj = (JSONObject) parser.parse(sb.toString());
isUp = csv.post(new CommentVO(
Long.parseLong(jsonObj.get("bno").toString()),
jsonObj.get("writer").toString(),
jsonObj.get("content").toString()));
PrintWriter out = res.getWriter();
out.print(isUp);
[board.detail.jsp]
- 부트스트랩 템플릿에서 댓글창으로 사용할 적당한 부분(input form + list form)을 가져와 붙여 넣고 input 태그와 button 태그에 각각 cmtText, cmtAddBtn id를 부여한다.
- 여기서 form 태그를 이용해 request객체로 데이터를 전달하는 것이 아니라 자바스크립트의 fetch API를 이용하여 비동기 통신을 해볼 것이다.
- bno 값을 <c:out> 태그로 받아와 bnoVal로 정의하고, detail.jsp에 외부 js 파일을 정의한다. (절대 경로)
<script>
const bnoVal = '<c:out value="${bvo.bno}"/>';
</script>
<script src="/resources/js/board.detail.js"></script>
- Eclips와 VSCode를 연동하여 board.detil.js를 작성한다. 주의할 점은 VSCode에서 변경된 내용을 Eclipse도 알 수 있도록 관련 파일들을 매번 Refresh(단축키 F5키) 해주어야 한다.
[board.detail.js]
- comment insert에 필요한 bno, writer, content를 객체로 만들어 서버로 전달하는 로직을 작성한다.
// comment 데이터를 서버로 전달
async function postCommentToServer(cmtData){
try {
const url = "/cmt/post";
const config = {
method : 'POST',
headers : {
'Content-Type' : 'application/json; charset=utf-8'
},
body : JSON.stringify(cmtData)
};
const resp = await fetch(url, config);
const result = await resp.text();
return result;
}catch(error){
console.log(error);
}
}
// 댓글 등록 버튼을 눌렀을 때
document.getElementById('cmtAddBtn').addEventListener('click', () => {
const cmtText = document.getElementById('cmtText').value;
if(cmtText == null || cmtText == ''){
alert('댓글 내용을 입력해 주세요!'); // 입력창이 빈칸이라면 alert 띄우기
return false;
}else {
let cmtData = { // comment 데이터를 객체로 만들기
bno : bnoVal,
writer : document.getElementById('cmtWriter').innerText,
content : cmtText
};
postCommentToServer(cmtData).then(result => {
if(result > 0) {
alert('댓글 등록 성공!');
document.getElementById('cmtText').value = ""; // 댓글 등록 후 input창 비우기
}
printCommentList(cmtData.bno); // 댓글 리스트 출력
});
}
});
[CommentCtrl.java Path 경로 재설정]
- /cmt/list/bno 형태로 path를 재설정하고, bno값을 변수에 담을 수 있도록 path 구하는 로직을 수정한다.
String uri = req.getRequestURI(); // /cmt/list/10
String path = uri.substring("/cmt/".length()); // list/10
log.info(">>> path : {}", path);
String pathVar = "";
if(path.contains("/")) {
pathVar = path.substring(path.lastIndexOf("/")+1); // 10
path = path.substring(0, path.lastIndexOf("/")); // list
}
getList(selectList)
- pathVar를 이용해 DB에서 받아온 모든 Comment 데이터를 List에 담는다.
- json-simple 라이브러리를 이용해 데이터를 담을 공간을 만들고, for문으로 데이터를 담는다.
- PrintWriter로 스트림에 데이터를 기록한다.
List<CommentVO> list = csv.getList(Long.parseLong(pathVar));
JSONObject[] jsonObjArr = new JSONObject[list.size()];
JSONArray jsonObjList = new JSONArray();
for (int i = 0; i < list.size(); i++) {
jsonObjArr[i] = new JSONObject(); // key:value
jsonObjArr[i].put("cno", list.get(i).getCno());
jsonObjArr[i].put("bno", list.get(i).getBno());
jsonObjArr[i].put("writer", list.get(i).getWriter());
jsonObjArr[i].put("content", list.get(i).getContent());
jsonObjArr[i].put("mod_at", list.get(i).getMod_at());
jsonObjList.add(jsonObjArr[i]);
}
String jsonData = jsonObjList.toJSONString();
PrintWriter out = res.getWriter();
out.print(jsonData);
- 화면에 댓글 목록을 출력할 함수를 detail.jsp에서 호출한다.
<script>
printCommentList(bnoVal);
</script>
- board.detail.js에서 서버로부터 받아온 comment 데이터를 출력하는 로직을 작성한다.
// 서버로부터 comment 데이터 가져오기
async function getCommentListFromServer(bno){
try {
const resp = await fetch("/cmt/list/" + bno);
const cmtList = await resp.json();
return await cmtList;
} catch (error) {
console.log(error);
}
}
// 가져온 데이터로 html 만들기
function spreadCommentList(cmtArr) {
let div = document.getElementById('accordionExample');
div.innerHTML = '';
for (let i = 0; i < cmtArr.length; i++) {
let html = `<div class="accordion-item">
<h2 class="accordion-header" id="heading${i}">
<button class="accordion-button" type="button"
data-bs-toggle="collapse" data-bs-target="#collapse${i}"
aria-expanded="true" aria-controls="collapse${i}">${cmtArr[i].cno}, ${cmtArr[i].bno}, ${cmtArr[i].writer}</button>
</h2>
<div id="collapse${i}" class="accordion-collapse collapse show"
aria-labelledby="heading${i}" data-bs-parent="#accordionExample">
<div class="accordion-body">
<button type="button" data-cno="${cmtArr[i].cno}" class="btn btn-sm btn-outline-warning cmtModBtn">%</button>
<button type="button" data-cno="${cmtArr[i].cno}" class="btn btn-sm btn-outline-danger cmtDelBtn">X</button>
<input type="text" class="form-control" id="cmtText" value="${cmtArr[i].content}">
${cmtArr[i].content}, ${cmtArr[i].mod_at}
</div>
</div>
</div>`;
div.innerHTML += html;
}
}
// comment 데이터를 화면에 출력
function printCommentList(bno){
getCommentListFromServer(bno).then(result => {
console.log(result);
if(result.length > 0) {
spreadCommentList(result);
}else {
let div = document.getElementById('accordionExample');
div.innerHTML = '더이상의 Comment가 존재하지 않습니다.';
}
});
}
modify(update)
- update에 필요한 cno와 cno에 해당하는 content 값을 얻는 방법은 다양하다.
- cno값은 spreadCommentList 함수에서 미리 만들어놓은 비표준 속성을 이용해 얻을 수 있다.
비표준 속성(Non-Standard Attrilbute)이란? html 태그의 속성이 제한적이므로 개발자가 용도에 맞게 사용하도록 별도로 예약된 속성이다. data-* 로 시작하고, dataset 프로퍼티를 사용하여 이 속성에 접근할 수 있다. - content값은 가장 가까운 조상 태그 중 div 태그를 찾는
closest('div')
를 이용하여 얻을 수 있다. - cno값과 content값을 body에 객체 형식으로 담아 보낸다.
- post와 마찬가지로 StringBuffer로 전달받은 값을 json-simple 라이브러리를 통해 파싱 하고 Service 객체로 보낸다.
- DB를 거쳐 해당 comment 데이터를 update 한 후, 성공 여부를 전달한다.
let cnoVal = e.target.dataset.cno;
let div = e.target.closest('div');
let cmtText = div.querySelector('#cmtText').value;
remove(delete)
- update와 거의 비슷하나, cno값만 전달하면 되기 때문에 headers는 제외하고, url에 실어 보낸다.
- Controller에서 전달받은 url의 pathVar 값으로 해당 comment를 삭제하고, 성공 여부를 전달한다.