📝 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를 삭제하고, 성공 여부를 전달한다.