댓글, 대댓글 기능

oYJo·2025년 3월 14일

Spring

목록 보기
25/29

댓글, 대댓글 기능

1️⃣ Oracle

-- 특정 게시글 댓글 목록 조회
SELECT COMMENT_NO, COMMENT_CONTENT,
    TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
    BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM "COMMENT"
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = 3116
ORDER BY COMMENT_NO
;

SELECT * FROM BOARD
ORDER BY BOARD_NO DESC;

-- 샘플 댓글/대댓글 삽입
INSERT INTO "COMMENT"                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '부모 댓글 1', DEFAULT, DEFAULT, 2017, 1, NULL);

INSERT INTO "COMMENT"                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '부모 댓글 2', DEFAULT, DEFAULT, 2017, 1, NULL);

INSERT INTO "COMMENT"                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 1-1', DEFAULT, DEFAULT, 2017, 1, 1005);

INSERT INTO "COMMENT"                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 1-2', DEFAULT, DEFAULT, 2017, 1, 1005);

INSERT INTO "COMMENT"                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 1-3', DEFAULT, DEFAULT, 2017, 1, 1005);

INSERT INTO "COMMENT"                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 2-1', DEFAULT, DEFAULT, 2017, 1, 1006);

INSERT INTO "COMMENT"                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 2-2', DEFAULT, DEFAULT, 2017, 1, 1006);

INSERT INTO "COMMENT"                                     -- 게시글, 회원
VALUES(SEQ_COMMENT_NO.NEXTVAL, '자식 댓글 2-1-1', DEFAULT, DEFAULT, 2017, 1, 1010);

COMMIT;

-- 댓글 목록 조회(계층형 쿼리 적용)
SELECT LEVEL, C.* FROM
(SELECT COMMENT_NO, COMMENT_CONTENT,
    TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
    BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM "COMMENT"
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = 2017) C
WHERE COMMENT_DEL_FL = 'N'
START WITH PARENT_NO IS NULL
CONNECT BY PRIOR COMMENT_NO = PARENT_NO
ORDER SIBLINGS BY COMMENT_NO
;
-- 특정 게시글 댓글 목록 조회
SELECT COMMENT_NO, COMMENT_CONTENT,
    TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
    BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM "COMMENT"
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = 3116
ORDER BY COMMENT_NO
;

근데 1-1, 1-2, 1-3, 2-1, 2-1-1, 2-2으로 만들기로 함
→ 계층형 쿼리 (START WITH, CONNECT BY, ORDER SIBLINGS BY)


상위 타입과 하위 타입간의 관계를 계층식으로 표현할 수 있게 하는 질의어(SELECT)

  • START WITH : 누구부터 시작(상위 타입 지정)
    상위 타입(최상위 부모)으로 사용될 행을 지정(서브쿼리로 지정 가능)
  • CONNECT BY : 어떻게 연결(부모와 자식)
    상위 타입과 하위 타입 사이의 관계를 규정
    PRIOR(이전의) 연산자와 같이 사용하여 현재 행 이전에 상위 타입 or 하위 타입이 있을지 규정
    1) 부모 → 자식 계층 구조
    CONNECT BY PRIOR 자식 컬럼 = 부모 컬럼
    2) 자식 → 부모 계층 구조
    CONNECT BY PRIOR 부모 컬럼 = 자식 컬럼
  • ORDER SIBLINGS BY : 자식 중 누구부터 등장(순서 지정)
    계층 구조 정렬

💡 계층형 쿼리가 적용 SELECT 해석 순서
5 : SELECT
1 : FROM (+JOIN)
4 : WHERE
2 : START WITH
3 : CONNECT BY
6 : ORDER SIBLINGS BY

WHERE절이 계층형 쿼리보다 해석 순서가 늦기 때문에
먼저 조건을 반영하고 싶은 경우 FROM절 서브쿼리(인라인뷰)를 이용

-- 댓글 목록 조회(계층형 쿼리 적용)
SELECT LEVEL, C.* FROM
(SELECT COMMENT_NO, COMMENT_CONTENT,
    TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
    BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
FROM "COMMENT"
JOIN MEMBER USING(MEMBER_NO)
WHERE BOARD_NO = 2017) C
WHERE COMMENT_DEL_FL = 'N'
START WITH PARENT_NO IS NULL
CONNECT BY PRIOR COMMENT_NO = PARENT_NO
ORDER SIBLINGS BY COMMENT_NO
;


LEVEL 지정하기


2️⃣ Visual Studio Code

✔️webapp(WEB-INF(views(board))) → comment.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"  %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<div id="commentArea">
    <!-- 댓글 목록 -->
    <div class="comment-list-area">
        
        <ul id="commentList">
            <!-- ${board.commentList}--> <!-- 확인용 -->

            <c:forEach items="${board.commentList}" var="comment">
            ${comment}
            
            
                <!-- 부모 댓글 -->
                <li class="comment-row 
                <c:if test='${comment.parentNo !=0}'>child-comment</c:if>">
                <!-- 만약 부모댓글이 있다면 자식댓글이 보이게 한다 -->
                    <p class="comment-writer">

                        <!-- 프로필 이미지 -->
                        <!-- 프로필 이미지가 없으면-->
                        <c:if test="${empty comment.profileImage}">
                            <img src="/resources/images/user.png">
                        </c:if>
                        
                        <!-- 프로필 이미지가 있으면-->
                        <c:if test="${!empty comment.profileImage}">
                            <img src="${comment.profileImage}">
                        </c:if>

                        

                        <!-- 닉네임 -->
                        <span>${comment.memberNickname}</span>
                        
                        <!-- 작성일 -->
                        <span class="comment-date">${comment.commentCreateDate}</span>
                    </p>
                    
                    <!-- 댓글 내용 -->
                    <p class="comment-content">${comment.commentContent}</p>


                        <!-- 버튼 영역 -->
                        <div class="comment-btn-area">
                        <c:if test="${!empty loginMember.memberNo}">
                            <button onclick="showInsertComment(${comment.commentNo}, this)">답글</button>
                        </c:if>
                        <!--
                        <button>답글</button>   
                        -->
                        <!-- 로그인 회원과 댓글 작성자가 같을 때는 -->  
                        <c:if test="${loginMember.memberNo == comment.memberNo}">
                            <button onclick="showUpdateComment(${comment.commentNo}, this)">수정</button>
                            <button onclick="deleteComment(${comment.commentNo})">삭제</button>
                        </c:if>
                    </div>
                </li>

            </c:forEach>
            
        </ul>
    </div>
    

    <!-- 댓글 작성 부분 -->
    <div class="comment-write-area">
        <textarea id="commentContent"></textarea>
        <button id="addComment">
            댓글<br>
            등록
        </button>
 
    </div>

</div>

부모 댓글 class=”comment-row”
자식 댓글 class=”child-comment”

부모가 0이 아닐 때

<!-- 부모 댓글 -->
<li class="comment-row 
           <c:if test='${comment.parentNo !=0}'>child-comment</c:if>">
  <!-- 만약 부모댓글이 있다면 자식댓글이 보이게 한다 -->
  <p class="comment-writer">

el이랑 jstl먼저 해석하기에 먼저 등장한다


이렇게 안에 넣어주면 된다
💡 location.search: 쿼리 스트링

✔️webapp(resources(css(board))) → comment-style.css

/*댓글*/
.comment-write-area {
	display: flex;
	min-height: 85px;
	justify-content: center;
	
	margin: 30px 0;
}

.comment-write-area > textarea{
	flex-basis: 75%;
    margin-right: 10px;
	resize: none;
}

.comment-write-area > button{
	flex-basis: 7%;
    height: 85px;

	font-weight: bold;
    border: 0;
    background-color: #455ba8;
    color: white;
    cursor: pointer;
}

#commentList{
	padding: 0;
	list-style: none;
}

.comment-writer {
	display: flex;
	align-items: center;
}

.comment-writer > img{
	width: 40px;
    height: 40px;
	border-radius: 30px;
}

.comment-writer > span{
    font-weight: bold;
    margin-left : 10px;
}

.comment-date{
	font-size: 0.8em;
	color: #aaa;
}


.comment-btn-area { 
	display: flex;
	justify-content: flex-end;
}

/* 버튼 */
.comment-btn-area button{
    width: 50px;
    height: 30px;
	margin-left: 10px;

    font-weight: bold;
    border: 0;
    background-color: #455ba8;
    color : white;

    cursor: pointer;
}

.comment-btn-area button:hover{
    background-color: white;
    color : #455ba8;
    border: 2px solid #455ba8;
}

.update-textarea{
	width: 100%;
    height: 90px;
    resize: none;
}

.comment-row{
	border-top : 1px solid #ccc;
	padding : 15px 0;
}


.child-comment{
	padding-left : 50px;
	position: relative;
}

/* 
.child-comment:before {
	content: "\f0a4";
	font-family: "FontAwesome";
	
	position : absolute;
	left : 0;
	top : 35px;
} */


/* 답글 */
.commentInsertContent{
	width: 100%;
    height: 90px;
    resize: none;
    margin-top : 20px;
}

/* 댓글 내용 */
.comment-content{
	white-space:pre-wrap; /* 작성된 문자열 모양 그대로(공백 유지)*/
}

padding-left : 50px 스타일이 지정되어 있는 child-comment 클래스를 추가

✔️webapp(resources(js(board))) → comment.js(full)

// 댓글 목록 조회
function selectCommentList(){

  // REST(REpresentational State Transfer) API
  // - 자원을 이름으로 구분(Representational)하여
  //   자원의 상태(State)를 주고받는 것(Transfer)

  // -> 주소를 명시하고
  //    HTTP Method(GET, POST, PUT, DELETE)를 이용해
  //    지정된 자원에 대한 CRUD 진행

  // Create   : 생성, 삽입 (POST)
  // Read     : 조회 (GET)
  // Update   : 수정 (PUT, PETCH)
  // Delete   : 삭제 (DELETE)

  // 기본적으로 form 태그는 GET/POST만 지원

  fetch("/comment?boardNo=" + boardNo) // GET방식은 주소에 파라미터를 담아서 전달
    .then(response => response.json()) // 응답 객체 -> 파싱
    .then(cList => { // cList : 댓글 목록(객체 배열)
    console.log(cList);

    // 화면에 출력되어 있는 댓글 목록 삭제
    const commentList = document.getElementById("commentList"); // ul태그
    commentList.innerHTML = "";

    // cList에 저장된 요소를 하나씩 접근
    for(let comment of cList){

      // 행
      const commentRow = document.createElement("li");
      commentRow.classList.add("comment-row");

      // 답글일 경우 child-comment 클래스 추가
      if(comment.parentNo != 0)  commentRow.classList.add("child-comment");

      // 작성자
      const commentWriter = document.createElement("p");
      commentWriter.classList.add("comment-writer");

      // 프로필 이미지
      const profileImage = document.createElement("img");

      if( comment.profileImage != null ){ // 프로필 이미지가 있을 경우
        profileImage.setAttribute("src", comment.profileImage);
      }else{ // 없을 경우 == 기본이미지
        profileImage.setAttribute("src", "/resources/images/user.png");
      }

      // 작성자 닉네임
      const memberNickname = document.createElement("span");
      memberNickname.innerText = comment.memberNickname;

      // 작성일
      const commentDate = document.createElement("span");
      commentDate.classList.add("comment-date");
      commentDate.innerText =  "(" + comment.commentCreateDate + ")";

      // 작성자 영역(p)에 프로필,닉네임,작성일 마지막 자식으로(append) 추가
      commentWriter.append(profileImage , memberNickname , commentDate);



      // 댓글 내용
      const commentContent = document.createElement("p");
      commentContent.classList.add("comment-content");
      commentContent.innerHTML = comment.commentContent;

      // 행에 작성자, 내용 추가
      commentRow.append(commentWriter, commentContent);


      // 로그인이 되어있는 경우 답글 버튼 추가
      if(loginMemberNo != ""){
        // 버튼 영역
        const commentBtnArea = document.createElement("div");
        commentBtnArea.classList.add("comment-btn-area");

        // 답글 버튼
        const childCommentBtn = document.createElement("button");
        childCommentBtn.setAttribute("onclick", "showInsertComment("+comment.commentNo+", this)");
        childCommentBtn.innerText = "답글";

        // 버튼 영역에 답글 버튼 추가
        commentBtnArea.append(childCommentBtn);

        // 로그인한 회원번호와 댓글 작성자의 회원번호가 같을 때만 버튼 추가
        if( loginMemberNo == comment.memberNo   ){

          // 수정 버튼
          const updateBtn = document.createElement("button");
          updateBtn.innerText = "수정";

          // 수정 버튼에 onclick 이벤트 속성 추가
          updateBtn.setAttribute("onclick", "showUpdateComment("+comment.commentNo+", this)");                        

          // 삭제 버튼
          const deleteBtn = document.createElement("button");
          deleteBtn.innerText = "삭제";
          // 삭제 버튼에 onclick 이벤트 속성 추가
          deleteBtn.setAttribute("onclick", "deleteComment("+comment.commentNo+")");                       

          // 버튼 영역 마지막 자식으로 수정/삭제 버튼 추가
          commentBtnArea.append(updateBtn, deleteBtn);

        } // if 끝


        // 행에 버튼영역 추가
        commentRow.append(commentBtnArea); 
      }


      // 댓글 목록(ul)에 행(li)추가
      commentList.append(commentRow);
    }

  })
    .catch(err => console.log(err));

}

//-------------------------------------------------------------------------------------------------

// 댓글 등록
const addComment = document.getElementById("addComment");
const commentContent = document.getElementById("commentContent");

addComment.addEventListener("click", e => { // 댓글 등록 버튼이 클릭이 되었을 때

  // 1) 로그인이 되어있나? -> 전역변수 memberNo 이용
  if(loginMemberNo == ""){ // 로그인 X
    alert("로그인 후 이용해주세요.");
    return;
  }

  // 2) 댓글 내용이 작성되어있나?
  if(commentContent.value.trim().length == 0){ // 미작성인 경우
    alert("댓글을 작성한 후 버튼을 클릭해주세요.");

    commentContent.value = ""; // 띄어쓰기, 개행문자 제거
    commentContent.focus();
    return;
  }

  // 3) AJAX를 이용해서 댓글 내용 DB에 저장(INSERT)

  const data = {"commentContent" : commentContent.value,
                "memberNo" : loginMemberNo,
                "boardNo" : boardNo}; // JS 객체

  fetch("/comment", {
    method : "POST",
    headers : {"Content-Type" : "application/json"},
    body : JSON.stringify(data) // JS 객체 -> JSON 파싱
  })
    .then(resp => resp.text())
    .then(result => {
    if(result > 0){ // 등록 성공
      alert("댓글이 등록되었습니다.");

      commentContent.value = ""; // 작성했던 댓글 삭제

      selectCommentList(); // 비동기 댓글 목록 조회 함수 호출
      // -> 새로운 댓글이 추가되어짐

    } else { // 실패
      alert("댓글 등록에 실패했습니다...");
    }
  })
    .catch(err => console.log(err));
});

// -----------------------------------------------------------------------------------
// 댓글 삭제
function deleteComment(commentNo){

  if( confirm("정말로 삭제 하시겠습니까?") ){

    fetch("/comment", {
      method : "DELETE",
      headers : {"Content-Type" : "application/json"},
      body : commentNo // 값 하나만 전달 시에는 json이 필요 없으므로 그냥 int 타입의 값 하나만 보냄
    })
      .then(resp => resp.text())
      .then(result => {
      if(result > 0){
        alert("삭제되었습니다");
        selectCommentList(); // 목록을 다시 조회해서 삭제된 글을 제거
      }else{
        alert("삭제 실패");
      }
    })
      .catch(err => console.log(err));

  }
}

// ------------------------------------------------------------------------------------------
// 댓글 수정 화면 전환 

let beforeCommentRow; // 수정 전 원래 행의 상태를 저장할 변수

function showUpdateComment(commentNo, btn){
  // 댓글번호, 이벤트발생요소(수정버튼)

  // ** 댓글 수정이 한 개만 열릴 수 있도록 만들기 **
  // 댓글 수정을 위한 textarea를 모두 얻어옴 -> 수정이 활성화 되어 있을 경우 1개, 없으면 0개
  const temp = document.getElementsByClassName("update-textarea");  

  if(temp.length > 0){ // 수정이 한 개 이상 열려 있는 경우

    if(confirm("다른 댓글이 수정 중입니다. 현재 댓글을 수정 하시겠습니까?")){ // 확인

      temp[0].parentElement.innerHTML = beforeCommentRow;
      // comment-row                       // 백업한 댓글
      // 백업 내용으로 덮어 씌워 지면서 textarea 사라짐

    }else{ // 취소
      return;
    }
  }

  // 1. 댓글 수정이 클릭된 행을 선택
  const commentRow = btn.parentElement.parentElement; // 수정 버튼의 부모의 부모

  // 2. 행 내용 삭제 전 현재 상태를 저장(백업) (문자열)
  //    (전역변수 이용)
  beforeCommentRow = commentRow.innerHTML;

  // 3. 댓글에 작성되어 있던 내용만 얻어오기 -> 새롭게 생성된 textarea 추가될 예정

  let beforeContent = commentRow.children[1].innerHTML;

  // 이것도 가능!
  //let beforeContent = btn.parentElement.previousElementSibling.innerHTML;

  // 4. 댓글 행 내부 내용을 모두 삭제
  commentRow.innerHTML = "";

  // 5. textarea 요소 생성 + 클래스 추가  +  **내용 추가**
  const textarea = document.createElement("textarea");
  textarea.classList.add("update-textarea");

  // ******************************************
  // XSS 방지 처리 해제
  beforeContent =  beforeContent.replaceAll("&amp;", "&");
  beforeContent =  beforeContent.replaceAll("&lt;", "<");
  beforeContent =  beforeContent.replaceAll("&gt;", ">");
  beforeContent =  beforeContent.replaceAll("&quot;", "\"");

  // ******************************************
  textarea.value = beforeContent; // 내용 추가

  // 6. commentRow에 생성된 textarea 추가
  commentRow.append(textarea);

  // 7. 버튼 영역 + 수정/취소 버튼 생성
  const commentBtnArea = document.createElement("div");
  commentBtnArea.classList.add("comment-btn-area");


  const updateBtn = document.createElement("button");
  updateBtn.innerText = "수정";
  updateBtn.setAttribute("onclick", "updateComment("+commentNo+", this)");

  const cancelBtn = document.createElement("button");
  cancelBtn.innerText = "취소";
  cancelBtn.setAttribute("onclick", "updateCancel(this)");

  // 8. 버튼영역에 버튼 추가 후 
  //    commentRow(행)에 버튼영역 추가
  commentBtnArea.append(updateBtn, cancelBtn);
  commentRow.append(commentBtnArea);

}

// -----------------------------------------------------------------------------------
// 댓글 수정 취소
function updateCancel(btn){
  // 매개변수 btn : 클릭된 취소 버튼
  // 전역변수 beforeCommentRow : 수정 전 원래 행(댓글)을 저장한 변수

  if(confirm("댓글 수정을 취소하시겠습니까?")){
    btn.parentElement.parentElement.innerHTML = beforeCommentRow;
  }
}

// -----------------------------------------------------------------------------------
// 댓글 수정(AJAX)
function updateComment(commentNo, btn){

  // 새로 작성된 댓글 내용 얻어오기
  const commentContent = btn.parentElement.previousElementSibling.value;

  const data = {
    "commentNo" : commentNo,
    "commentContent" : commentContent
  }

  fetch("/comment", {
    method : "PUT",
    headers : {"Content-Type" : "application/json"},
    body : JSON.stringify(data)
  })
    .then(resp => resp.text())
    .then(result => {
    if(result > 0){
      alert("댓글이 수정되었습니다.");
      selectCommentList();
    }else{
      alert("댓글 수정 실패");
    }
  })
    .catch(err => console.log(err));

}

// -------------------------------------------------------------------------------
// -------------------------------------------------------------------------------

// 답글 작성 화면 추가 
// -> 답글 작성 화면은 전체 화면에 1개만 존재 해야한다!

function showInsertComment(parentNo, btn){
  // 부모 댓글 번호, 클릭한 답글 버튼

  // ** 답글 작성 textarea가 한 개만 열릴 수 있도록 만들기 **
  const temp = document.getElementsByClassName("commentInsertContent");

  if(temp.length > 0){ // 답글 작성 textara가 이미 화면에 존재하는 경우

    if(confirm("다른 답글을 작성 중입니다. 현재 댓글에 답글을 작성 하시겠습니까?")){
      temp[0].nextElementSibling.remove(); // 버튼 영역부터 삭제
      temp[0].remove(); // textara 삭제 (기준점은 마지막에 삭제해야 된다!)

    } else{
      return; // 함수를 종료시켜 답글이 생성되지 않게함.
    }
  }

  // 답글을 작성할 textarea 요소 생성
  const textarea = document.createElement("textarea");
  textarea.classList.add("commentInsertContent");

  // 답글 버튼의 부모의 뒤쪽에 textarea 추가
  // after(요소) : 뒤쪽에 추가
  btn.parentElement.after(textarea);

  // 답글 버튼 영역 + 등록/취소 버튼 생성 및 추가
  const commentBtnArea = document.createElement("div");
  commentBtnArea.classList.add("comment-btn-area");

  const insertBtn = document.createElement("button");
  insertBtn.innerText = "등록";
  insertBtn.setAttribute("onclick", "insertChildComment("+parentNo+", this)");

  const cancelBtn = document.createElement("button");
  cancelBtn.innerText = "취소";
  cancelBtn.setAttribute("onclick", "insertCancel(this)");

  // 답글 버튼 영역의 자식으로 등록/취소 버튼 추가
  commentBtnArea.append(insertBtn, cancelBtn);

  // 답글 버튼 영역을 화면에 추가된 textarea 뒤쪽에 추가
  textarea.after(commentBtnArea);

}

// 답글 취소
function insertCancel(btn){
  // 취소
  btn.parentElement.previousElementSibling.remove(); // 취소의 부모의 이전 요소(textarea) 제거
  btn.parentElement.remove(); // 취소의 부모 요소(comment-btn-area) 제거
}

// 답글 등록
function insertChildComment(parentNo, btn){
  // 부모 댓글 번호, 답글 등록 버튼

  // 누가?                loginMemberNo(로그인한 회원의 memberNo )(전역변수)
  // 어떤 내용?           textarea에 작성된 내용
  // 몇번 게시글?         현재 게시글 boardNo (전역변수)
  // 부모 댓글은 누구?    parentNo (매개변수)

  // 답글 내용
  const commentContent = btn.parentElement.previousElementSibling.value;

  // 답글 내용이 작성되지 않은 경우
  if(commentContent.trim().length == 0){
    alert("답글 작성 후 등록 버튼을 클릭해주세요.");
    btn.parentElement.previousElementSibling.value = "";
    btn.parentElement.previousElementSibling.focus();
    return;
  }

  const data = {"commentContent" : commentContent,
                "memberNo" : loginMemberNo,
                "boardNo" : boardNo,
                "parentNo" : parentNo
               };

  fetch("/comment", {
    method : "POST",
    headers : {"Content-Type" : "application/json"},
    body : JSON.stringify(data)
  })
    .then(resp => resp.text())
    .then(result => {
    if(result > 0){ // 등록 성공
      alert("답글이 등록되었습니다.");
      selectCommentList(); // 비동기 댓글 목록 조회 함수 호출

    } else { // 실패
      alert("답글 등록에 실패했습니다...");
    }
  })
    .catch(err => console.log(err));

}
// 댓글 목록 조회
function selectCommentList(){
    // REST(REpresentational State Transfer) API
    // - 자원을 이름으로 구분(Representational)하여
    //   자원의 상태(State)를 주고받는 것(Transfer)
    // -> 주소를 명시하고
    //    HTTP Method(GET, POST, PUT, DELETE)를 이용해
    //    지정된 자원에 대한 CRUD 진행
    // Create   : 생성, 삽입 (POST)
    // Read     : 조회 (GET)
    // Update   : 수정 (PUT, PETCH)
    // Delete   : 삭제 (DELETE)
    // 기본적으로 form 태그는 GET/POST만 지원


선언했으니깐 그냥 boardNo로 사용할 수 있다

답글 등록
comment.commentNo, this

const data = {
  "memberNo" : loginMemberNo,
  **"commentContent" : commentContent,**
  "boardNo" : boardNo,
  "parentNo" : parentNo
}; // 자바스크립트 객체(K:V)

댓글 수정
comment.commentNo, this

const data = {
  "commentNo" : commentNo,
  **"commentContent" : commentContent**
};

댓글 삭제
comment.commentNo

body : commentNo // 값 하나 전달 시에는 JSON 필요 X
// commentContent만 넘기면 되기에 JSON.stringify(data)으로 안 써도 된다
})


참고로 이때, commentContent.value가 아닌 이유
이미 위에 const commentContent = btn.parentElement.previousElementSibling.value;
들어가있기에


3️⃣ Spring

✔️resources(mappers) → board-mapper.xml

...
<!-- 특정 게시글 댓글 조회 -->
<select id="selectCommentList" resultMap="comment_rm">
  SELECT LEVEL, C.* FROM
  (SELECT COMMENT_NO, COMMENT_CONTENT,
  TO_CHAR(C_CREATE_DATE, 'YYYY"년" MM"월" DD"일" HH24"시" MI"분" SS"초"') C_CREATE_DATE,
  BOARD_NO, MEMBER_NO, MEMBER_NICKNAME, PROFILE_IMG, PARENT_NO, COMMENT_DEL_FL
  FROM "COMMENT"
  JOIN MEMBER USING(MEMBER_NO)
  WHERE BOARD_NO = #{boardNo}) C
  WHERE COMMENT_DEL_FL = 'N'
  START WITH PARENT_NO IS NULL
  CONNECT BY PRIOR COMMENT_NO = PARENT_NO
  ORDER SIBLINGS BY COMMENT_NO
</select>
...

✔️porject(board(controller)) → CommentController.java

// @Controller + @ResponseBody
@RestController // 요청/응답 처리(단, 모든 요청 응답은 비동기)
				// -> REST API를 구축하기 위한 Controller
public class CommentController {
	
	@Autowired
	private CommentService service;

	// 댓글 목록 조회
	@GetMapping(value = "/comment", produces = "application/json; charset=UTF-8")
	public List<Comment> select(int boardNo) {
		return service.select(boardNo); // HttpMessageConverter가 List -> JSON 변환
	}
	
	// 댓글 삽입
	@PostMapping("/comment")
	public int insert(@RequestBody Comment comment) {
		// 요청 데이터(JSON)를
		// HttpMessageConverter가 해석해서 Java 객체(comment)에 대입
		
		return service.insert(comment);
	}
	
	// 댓글 삭제
	@DeleteMapping("/comment")
	public int delete(@RequestBody int commentNo) {
				// ajax 요청 시 body에 담겨 있는 하나밖에 없는 데이터는
				// 매개변수 int commentNo에 담기게 된다!
			
		return service.delete(commentNo);
	}
	
	@PutMapping("/comment")
	public int update(@RequestBody Comment comment) {
		return service.update(comment);
	}
	
}

✔️project(board(model(service))) → CommentService.java


public interface CommentService {

	/** 댓글 목록 조회
	 * @param boardNo
	 * @return cList
	 */
	List<Comment> select(int boardNo);

	/** 댓글 삽입
	 * @param comment
	 * @return result
	 */
	int insert(Comment comment);

	/** 댓글 삭제
	 * @param commentNo
	 * @return result
	 */
	int delete(int commentNo);

	/** 댓글 수정
	 * @param comment
	 * @return result
	 */
	int update(Comment comment);

}

✔️project(board(model(service))) → CommentServiceImpl.java

@Service
public class CommentServiceImpl implements CommentService {

	@Autowired
	private CommentDAO dao;

	// 댓글 목록 조회
	@Override
	public List<Comment> select(int boardNo) {
		return dao.select(boardNo);
	}

	// 댓글 삽입
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int insert(Comment comment) {
		
		// XSS 방지 처리
		comment.setCommentContent( Util.XSSHandling( comment.getCommentContent() ) );
		
		return dao.insert(comment);
	}

	// 댓글 삭제
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int delete(int commentNo) {
		return dao.delete(commentNo);
	}

	// 댓글 수정
	@Transactional(rollbackFor = Exception.class)
	@Override
	public int update(Comment comment) {
		
		// XSS 방지 처리
		comment.setCommentContent( Util.XSSHandling( comment.getCommentContent() ) );
		
		return dao.update(comment);
	}
	
}

✔️project(board(model(dao))) → CommentDAO.java

@Repository // DB 관련 + bean 등록 (IOC)
public class CommentDAO {

	@Autowired
	private SqlSessionTemplate sqlSession;

	/** 댓글 목록 조회
	 * @param boardNo
	 * @return cList
	 */
	public List<Comment> select(int boardNo) {
							// board-mapper.xml에 작성된 select 이용
		return sqlSession.selectList("boardMapper.selectCommentList", boardNo);
	}

	/** 댓글 삽입
	 * @param comment
	 * @return result
	 */
	public int insert(Comment comment) {
		return sqlSession.insert("commentMapper.insert", comment);
	}

	/** 댓글 삭제
	 * @param commentNo
	 * @return result
	 */
	public int delete(int commentNo) {
		return sqlSession.update("commentMapper.delete", commentNo);
	}

	/** 댓글 수정
	 * @param comment
	 * @return result
	 */
	public int update(Comment comment) {
		return sqlSession.update("commentMapper.update", comment);
	}
	
}

✔️resources(mappers) → comment-mapper.xml

<!-- 댓글 삽입 -->
<insert id="insert">
  INSERT INTO "COMMENT"
  VALUES(SEQ_COMMENT_NO.NEXTVAL,
  #{commentContent},
  DEFAULT, DEFAULT, 
  #{boardNo}, #{memberNo}, 

  <!-- 동적 SQL : if문 -->

  <!-- 부모 댓글 -->
  <if test="parentNo == 0">NULL</if>

  <!-- 자식 댓글 -->
  <if test="parentNo != 0">#{parentNo}</if>
  )
</insert>

<!-- 댓글 삭제 -->
<update id="delete">
  UPDATE "COMMENT" SET
  COMMENT_DEL_FL = 'Y'
  WHERE COMMENT_NO = #{commentNo}
</update>

<!-- 댓글 수정 -->
<update id="update">
  UPDATE "COMMENT" SET
  COMMENT_CONTENT = #{commentContent}
  WHERE COMMENT_NO = #{commentNo}
</update>
</mapper>
profile
Hello! My Name is oYJo

0개의 댓글