게시판 - 댓글과 대댓글 기능 구현 (23.08.30)

·2023년 8월 30일
0

Spring

목록 보기
25/36
post-thumbnail

🌷 댓글/대댓글


이번 포스팅에서는 댓글/대댓글 작성, 수정, 삭제 기능을 구현해 볼 것이다.
이 기능을 구현하려면 우선 계층형 쿼리REST API 대한 이해가 필요하다.

이전 포스팅에서 개념 알기
계층형 쿼리, REST API (23.08.30)


👀 코드로 살펴보기

🌼 Oracle DBMS

위와 같은 형태로 댓글/대댓글을 삽입 후 계층형 쿼리로 조회해 보자.

-- 댓글 목록 조회
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
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
;

COMMENT 테이블에 위와 같은 형태로 댓글이 삽입된 것을 볼 수 있다.
이것을 계층형 쿼리를 적용한 SELECT문으로 조회해 보자.

계층형 쿼리를 실행하자 PARENT_NO를 기준으로 부모 댓글과 그에 따른 자식 댓글 순으로 조회되었다.
이런 식으로 SELECT를 할 수 있다는 게 그저 놀라울 따름... 🤭


🌼 Spring

이전에 작성해 놓았던 board-mapper.xml특정 게시글 댓글 조회 select문도 계층형 쿼리 형태로 수정해 보자.

🌱 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>
...

🌱 CommentController.java

package edu.kh.project.board.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import edu.kh.project.board.model.dto.Comment;
import edu.kh.project.board.model.service.CommentService;

// @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);
	}
	
}

🌱 CommentService.java

package edu.kh.project.board.model.service;

import java.util.List;

import edu.kh.project.board.model.dto.Comment;

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);

}

🌱 CommentServiceImpl.java

package edu.kh.project.board.model.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import edu.kh.project.board.model.dao.CommentDAO;
import edu.kh.project.board.model.dto.Comment;
import edu.kh.project.common.utility.Util;

@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);
	}
	
}

🌱 CommentDAO.java

package edu.kh.project.board.model.dao;

import java.util.List;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import edu.kh.project.board.model.dto.Comment;

@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);
	}
	
}

🌱 comment-mapper.xml

<?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="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>

🌼 VS Code

댓글과 대댓글을 구분하기 위해 대댓글인 경우에는 padding-left : 50px 스타일이 지정되어 있는 child-comment 클래스를 추가해 줄 것이다.

🌱 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"  %>

<div id="commentArea">
    <!-- 댓글 목록 -->
    <div class="comment-list-area">
        
        <ul id="commentList">

            <c:forEach items="${board.commentList}" var="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">
                        <button onclick="showInsertComment(${comment.commentNo}, this)">답글</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>

🌱 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
 }
 
 .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.js

// 댓글 목록 조회
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));


}
profile
풀스택 개발자 기록집 📁

0개의 댓글