팀프로젝트/SpringBoot

스프링부트 팀플) 20240405 익명게시판 댓글/대댓

일일일코_장민기 2024. 4. 10. 16:16
728x90

 

 

올린 줄 알고 있었는데, 멍청하게 임시저장만 해놓고 올리질 않았다...



아무튼 이번에 구현한 기능을 크게 6가지
1. 댓글 출력
2. 댓글 등록
3. 댓글 삭제
4. 대댓글(답글) 출력
5. 답글 등록
6. 답글 삭제



이때 발생하는 문제는 다음과 같다.
1. 처음에 댓글을 어떻게 출력을 할 것인가?
2. 댓글 등록/삭제 후에 어떻게 출력을 할 것인가?
3. 답글을 댓글에 맞춰서 어떻게 출력을 할 것인가?
4. 어떻게 댓글에 맞춰서 답글을 등록/삭제할 것인가?

우선 전체적으로 AJAX처리를 통해 출력/등록/삭제를 진행했다.
한 마디로 게시글에 입장한 뒤에 댓글/답글과 관련된 모든 행동을 브라우저의 URL이 전혀 이동하지 않는다.
이를 통해 유저는 페이지가 전환되어 계속 스크롤을 내리는 등의 불편함을 가질 필요가 없다.


구현할 때 시간이 없었기 때문에 빠르게 구현하고자 DB의 table은 댓글과 대댓글을 나누어 만들었다.
한 곳에 몰아넣을 수 있지만, 나누는 것이 관리도 깔끔할 것이다.

 

 

---익명 게시판DB 생성
create table debugBoardDB (
 BOARDNUM Number(10) primary key,
 NICKNAME VARCHAR2(50) NOT NULL,
 PASSWORD Varchar2(50),
 TITLE VARCHAR2(200) NOT NULL,
 CATEGORY VARCHAR2(20) NOT NULL,
 CONTENT CLOB NOT NULL,
 POSTDATE VARCHAR2(30) NOT NULL,
 EDITTEDDATE VARCHAR2(30),
 VIEWCOUNT NUMBER(20) DEFAULT 0 NOT NULL,
 RECOMMENDNUM   NUMBER(20) DEFAULT 0 NOT NULL,
 DISRECOMMENDNUM  NUMBER(20) DEFAULT 0 NOT NULL
);

-- 익명 게시물 댓글DB 생성
CREATE TABLE AnonymousCommentDB (
    anonymousCommentNum NUMBER(10) PRIMARY KEY,
    commentnickname VARCHAR2(50) NOT NULL,
    commentpassword VARCHAR2(50),
    commentcontent CLOB NOT NULL,
    commentpostDate VARCHAR2(30) NOT NULL,
    commentEdittedDate VARCHAR2(30),
    boardNum NUMBER(10),
    CONSTRAINT fk_debugBoardDB
        FOREIGN KEY (boardNum) 
        REFERENCES debugBoardDB(boardNum) 
        ON DELETE CASCADE
);

-- 익명 게시물 대댓글DB 생성
CREATE TABLE AnonymousReplyDB (
    anonymousReplyNum NUMBER(10) PRIMARY KEY,
    replyNickname VARCHAR2(50) NOT NULL,
    replyPassword VARCHAR2(50),
    replyContent CLOB NOT NULL,
    replyPostDate VARCHAR2(30) NOT NULL,
    replyEdittedDate VARCHAR2(30),
    anonymousCommentNum NUMBER(10),
    CONSTRAINT fk_AnonymousCommentDB 
        FOREIGN KEY (anonymousCommentNum) 
        REFERENCES AnonymousCommentDB (anonymousCommentNum) 
        ON DELETE CASCADE
);


-- 댓글 시퀀스 생성
CREATE SEQUENCE anonymousCommentSeq
START WITH 1
INCREMENT BY 1
NOCACHE
NOCYCLE;

-- 대댓글 시퀀스 생성
CREATE SEQUENCE anonymousReplySeq
START WITH 1
INCREMENT BY 1
NOCACHE
NOCYCLE;

 

 

 

 

 

기본적인 형태: 아래 숫자는 몇 글자까지가 한줄인지 보기 위함이다.

 

 

전체적인 댓글과 답글의 출력 양상은 위와 같다.
위치는 본문과 이전글/다음글 보기 사이이며 
출력은 유저가 게시글에 들어왔을 때 시작된다. (컨트롤러에서 넘겨주는 것이 아니다)
그 이유는 어차피 AJAX로 게시글 안에서 반복적으로 출력되어야 한다면
처음부터 게시글에 들어왔을 때 출력하도록 만드는 편이 컨트롤러의 코드를 늘리지 않아도 되기 때문이다.

댓글을 출력하는 곳의 코드는 매우 간단하다.
<!-- 댓글 표시 -->
 <div class="card">
 	<div class="card-body">
 		<h5 class="card-title">댓글</h5>
 		<div id="commentContent"></div>
 	</div>
 </div>

 

 

처음에는 출력하는 곳에서 HTML 전체를 입력했지만 답글을 넣고, 댓글목록 전체를 갱신해야 하다보니
자바스크립트에서 아예 전체를 전달해주는 것이 쉽다고 생각했다.
//페이지 로딩 시 댓글 목록 출력
		 fetchComments($("#boardNum").val());

// 대댓글 작성 폼 토글
	    $(document).on('click', '.comment-reply-btn', function() {
	        var commentNum = $(this).data('comment-num');
	        $("#replyForm-" + commentNum).toggle();
	    });

// 댓글 목록 출력 함수
	 function fetchComments(boardNum) {
	    $.ajax({
	        url: "<c:url value='/allComments'/>",
	        type: 'POST',
	        data: {
	            boardNum: boardNum
	        },
	        success: function(response) {
	            var commentsHTML = '';
	            response.forEach(function(comment) {
	                commentsHTML += "<div class='card mb-3'>";
	                commentsHTML += 	"<div class='card-body d-flex justify-content-between'>";
	                commentsHTML += 		"<div class='comment-content'>";
	                commentsHTML += 			"<p class='card-text'><strong>작성자:</strong> " + comment.commentNickname + " (" + comment.commentEdittedDate + ") ";
	                commentsHTML += 				"<span class='control-group ml-auto'>";
	                commentsHTML += 				"<a class='btn-text delete-btn' data-comment-num='" + comment.anonymousCommentNum + "'>삭제</a> ";
	                commentsHTML += 				"<a class='btn-text comment-reply-btn' href='javascript:void(0);' data-comment-num='" + comment.anonymousCommentNum + "'>답글</a>";
	                commentsHTML += 			"</span></p>";
	                commentsHTML += 			"<p class='card-text'><strong>내용:</strong> " + comment.commentContent + "</p>";

		            // 대댓글 작성 폼
	                commentsHTML += 			"<form id='replyForm-" + comment.anonymousCommentNum + "' class='mb-3' style='display: none;'>";
	                commentsHTML += 				"<input type='text' id='replyNickname-" + comment.anonymousCommentNum + "' placeholder='닉네임' required='required' pattern='^[가-힣]{1,20}$|^[a-zA-Z0-9]{1,40}$' title='한글 20글자 또는 영어+숫자 40글자 이내로 입력해주세요.''>";
	                commentsHTML += 				"<input type='password' id='replyPassword-" + comment.anonymousCommentNum + "' placeholder='패스워드' required='required' maxlength='30'> <br>";
	                commentsHTML += 				"<textarea class='form-control' id='replyContent-" + comment.anonymousCommentNum + "' rows='2' placeholder='대댓글을 입력하세요' maxlength='100'></textarea>";
	                commentsHTML += 				"<button type='button' class='btn btn-primary submit-reply-btn' data-comment-num='" + comment.anonymousCommentNum + "'>대댓글 등록</button>";
	                commentsHTML += 			"</form>";
	                
	             	// 대댓글 출력을 위한 빈 div 추가
	                commentsHTML += 			"<div class='replies' id='replies-" + comment.anonymousCommentNum + "'></div>";
	                commentsHTML += 		"</div>";
	                commentsHTML += 	"</div>";
	                commentsHTML += "</div>";
	            });
	            $("#commentContent").html(commentsHTML);
	            
	         	// 각 댓글에 대해 대댓글을 가져와서 출력
                response.forEach(function(comment) {
                    fetchReplies(comment.anonymousCommentNum);
                });
	        },
	        error: function(xhr, status, error) {
	            console.error('댓글 목록을 가져오는데 실패하였습니다:', error);
	        }
	    });
	}
    
    // 대댓글 목록을 가져오고 출력하는 함수
    function fetchReplies(commentNum) {
        $.ajax({
            url: "<c:url value='/allReplies'/>",
            type: 'POST',
            data: {
                anonymousCommentNum: commentNum
            },
            success: function(response) {
                var repliesHTML = '';
                response.forEach(function(reply) {
                    repliesHTML += "<div class='card mb-3'>";
                    repliesHTML += 		"<div class='card-body'>";
                    repliesHTML += 			"<p><strong>작성자:</strong> " + reply.replyNickname + " (" + reply.replyEdittedDate + ") ";
					repliesHTML += 			"<a class='btn-text reply-delete-btn' href='javascript:void(0);' data-reply-num='" + reply.anonymousReplyNum + "'>삭제</a></p>";
                    repliesHTML += 			"<p><strong>내용:</strong> " + reply.replyContent + "</p>";
                    repliesHTML += 		"</div>";
                    repliesHTML += "</div>";
                });
                $("#replies-" + commentNum).html(repliesHTML);
            },
            error: function(xhr, status, error) {
                console.error('대댓글 목록을 가져오는데 실패하였습니다:', error);
            }
        });
    }

 

댓글 및 답글 출력 코드이다.
쉽게 말해서 부트스트랩과 답글 작성폼까지 모두 들어간 HTML코드를 AJAX 성공 시 불러오게 만들었다.
답글 작성폼은 토글 형태로 댓글의 '답글' 버튼을 클릭하면 출력되도록 만들었다.
또한 this키를 사용하여 '답글' 버튼을 누른 댓글의 답글 작성폼이 열리도록 만들었다.
따라서 뷰랜더링/AJAX -> 댓글 출력 -> 답글 출력 순서대로 자동으로 작동된다.

이전에 메일 시스템 만들었던 .append를 반복한 것과 유사하다.

 

 

댓글 등록창

 

출력이 가장 어려웠고 그 다음부터는 비교적 쉽다.
댓글 등록은 폼 / 등록 / 갱신이 3가지만 신경쓰면 된다.

 

<!-- 댓글 작성 폼 -->
<form id="commentForm">
    <div class="mb-3">
        <label for="commentContent" class="form-label">댓글 작성</label>
        <input 	type="text" id="newCommentNickname" placeholder="닉네임" required="required" 
				pattern="^[가-힣]{1,20}$|^[a-zA-Z0-9]{1,40}$" title="한글 20글자 또는 영어+숫자 40글자 이내로 입력해주세요."> 
        <input 	type="password" id="newCommentPassword" placeholder="패스워드" required="required" maxlength="30"> <br>
        <textarea class="form-control" id="newCommentContent" rows="3" placeholder="댓글을 입력하세요" maxlength="100"></textarea>
    </div>
    <button type="submit" class="btn btn-primary">댓글 등록</button>
</form>

 

댓글 폼은 기본적으로 브라우저에 출력된 상태이기 때문에 평범하게 만들면 된다.
// 댓글 등록
	$('#commentForm').submit(function(event) {

			event.preventDefault();
		    var commentContent = $("#newCommentContent").val();
		    var commentNickname = $("#newCommentNickname").val()
		    var commentPassword = $("#newCommentPassword").val()
		    
		    $.ajax({
		        type: 'POST',  
		        url: "<c:url value='/addComment'/>",  
		        data: {
		        	commentContent: commentContent,
		        	boardNum: boardNum,
		        	commentNickname: commentNickname,
		        	commentPassword: commentPassword
		        }, 
		        success: function(response) {  
		        	fetchComments(boardNum);
		            $('#newCommentContent').val('');
		        },
		        error: function(xhr, status, error) {
		            console.error(xhr.responseText);
		        }
		    });
		});
		
	})

 

댓글 등록도 평범한 AJAX이다.
한 가지 주의할 점은 success 후에 입력한 내용 비우기랑 댓글 목록 갱신하기 이다.
이것이 되지 않으면 사용자가 사용하기 불편해진다. 특히 목록이 갱신되지 않으면 ajax의 의미가 없다.

답글을 눌렀을 때 출력되는 대댓글 등록창

 

상술된 댓글 출력 js코드에 있는 답글 등록창 출력화면이다.
기본적인 처리가 위에서 끝났기 때문에 답글 등록은 간단한 ajax만 만들어주면 된다.
//대댓글 등록
	$(document).on('click', '.submit-reply-btn', function(event) {
	    event.preventDefault();
	    var commentNum = $(this).data('comment-num');
	    var replyNickname = $("#replyNickname-" + commentNum).val();
	    var replyPassword = $("#replyPassword-" + commentNum).val();
	    var replyContent = $("#replyContent-" + commentNum).val();
	    
	    $.ajax({
	        type: 'POST',
	        url: "<c:url value='/addReply'/>",
	        data: {
	            anonymousCommentNum: commentNum,
	            replyNickname: replyNickname,
	            replyPassword: replyPassword,
	            replyContent: replyContent
	        },
	        success: function(response) {
	            // 대댓글 폼 초기화
	            $("#replyContent-" + commentNum).val("");
	            // 대댓글 폼 숨기기
	            $("#replyForm-" + commentNum).hide();
	            fetchComments(boardNum);
	        },
	        error: function(xhr, status, error) {
	            console.error('대댓글 등록에 실패하였습니다:', error);
	        }
	    });
	});

 

 

 

 

 

 

댓글 삭제를 클릭하면 출력되는 모달창
삭제된 상태

 

댓글 삭제 기능은 처음에 팝오버를 사용해서 구현할 생각이었는데
아무리해도 팝오버의 html코드가 기능하질 않았다.
마무리까지 시간이 얼마 남지 않아서 간단히 모달창으로 구현하는 것으로 변경했다.

 

<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-labelledby="confirmDeleteModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="confirmDeleteModalLabel">댓글 삭제 확인</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <p>댓글을 삭제하시려면 패스워드를 입력하세요:</p>
        <input type="password" class="form-control" id="commentPasswordToDelete" placeholder="패스워드" maxlength="30">
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
        <button type="button" class="btn btn-primary" id="confirmDeleteBtn">확인</button>
      </div>
    </div>
  </div>
</div>
// 댓글 삭제 버튼에 클릭 이벤트로 삭제 기능 부여
	    $(document).on('click', '.delete-btn', function() {
	        var anonymousCommentNum = $(this).data('comment-num');
	        $('#commentPasswordToDelete').val("");
	        $('#confirmDeleteModal').modal('show');
	        $('#confirmDeleteBtn').data('comment-num', anonymousCommentNum);
	    });
        
  // 댓글 삭제 함수
		$('#confirmDeleteBtn').click(function() {
		    var anonymousCommentNum = $(this).data('comment-num');
		    var commentPassword = $('#commentPasswordToDelete').val();
		    
		    $.ajax({
		        url: "<c:url value='/deleteComment'/>",
		        type: "DELETE",
		        data: {
		            anonymousCommentNum: anonymousCommentNum,
		            commentPassword: commentPassword
		        },
		        success: function(response) {
		        	if(response == false){
		        		alert("비밀번호가 다릅니다")
		        	}
		            fetchComments(boardNum);
		            $('#confirmDeleteModal').modal('hide'); 
		        },
		        error: function(xhr, status, error) {
		            console.error('댓글 삭제에 실패하였습니다:', error);
		            $('#confirmDeleteModal').modal('hide');
		        }
		    });
		});

 

상술된 댓글 출력 목록의 버튼에 삭제 기능을 부여+모달창 출력하는 함수와 삭제 AJAX이다.
a태그를 사용해서 삭제 버튼을 만들고 싶지 않았지만
span을 사용하지 않으면서 class를 사용할 수 있는 태그가 마땅히 떠오르지 않았다.
지금 생각해보니 class를 쓸 필요는 딱히 없었으니 차후 개선되어야 할 부분

 

답글의 삭제를 누르면 출력되는 모달창
답글 삭제

 

<!-- 대댓글 삭제 모달 창 -->
<div class="modal fade" id="confirmDeleteReplyModal" tabindex="-1" aria-labelledby="confirmDeleteReplyModalLabel" aria-hidden="true">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="confirmDeleteReplyModalLabel">대댓글 삭제 확인</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
      </div>
      <div class="modal-body">
        <p>대댓글을 삭제하시려면 패스워드를 입력하세요:</p>
        <input type="password" class="form-control" id="replyPasswordToDelete" placeholder="패스워드" maxlength="30">
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
        <button type="button" class="btn btn-primary" id="replyDeleteBtn">확인</button>
      </div>
    </div>
  </div>
</div>
<hr>
// 대댓글 삭제 버튼에 클릭 이벤트로 삭제 기능 부여
	    $(document).on('click', '.reply-delete-btn', function() {
            var anonymousReplyNum = $(this).data('reply-num');
            $('#replyPasswordToDelete').val(""); 
            $('#confirmDeleteReplyModal').modal('show'); 
            $('#replyDeleteBtn').data('reply-num', anonymousReplyNum); 
        });
        
 // 대댓글 삭제 함수
	$('#replyDeleteBtn').click(function() {
         var anonymousReplyNum = $(this).data('reply-num');
         var replyPassword = $('#replyPasswordToDelete').val();
         
         $.ajax({
             url: "<c:url value='/deleteReply'/>",
             type: "DELETE",
             data: {
                 anonymousReplyNum: anonymousReplyNum,
                 replyPassword: replyPassword
             },
             success: function(response) {
                 if (response == false) {
                     alert("비밀번호가 다릅니다");
                 } else {
                     fetchComments(boardNum); 
                 }
                 $('#confirmDeleteReplyModal').modal('hide');
             },
             error: function(xhr, status, error) {
                 console.error('대댓글 삭제에 실패하였습니다:', error);
                 $('#confirmDeleteReplyModal').modal('hide'); 
             }
         });
     });
답글도 같은 방식으로 구현했다.






이것으로 기본적인 익명게시판을 전반적으로 구현했다.
아직 이미지 포스팅이 안 되는 부분은 아쉽지만 나중에 시간이 나면 구현해야 할 부분이다.
게시판은 기본적으로 쉽고 간단하게 쓸 수 있는 것이 가장 중요하다.

그 과정에서 유저가 한 눈에 보고 쓸 수 있게 구현해야 한다는 점,
유저의 사용 피로도를 최소화해야 한다는 점을 고려하면서 구현했고,

부족하지만 어느정도는 이런 점을 반영하면서 구현된 것 같다.


시간이 나면 보완하고 싶은 점은 다음과 같다.

1. 이미지/영상 포스팅
2. 댓글/답글 수정 기능
3. 댓글/답글 버튼의 효율성 증대

등등


다음은 스프링시큐리티의 적용에 대해 포스팅하겠다.