ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Camel][Spring] 게시판 만들기 #7-3. 댓글처리 구현 (게시물 적용)
    Spring/게시판 만들기 2020. 6. 28. 00:12

    [Camel][Spring] 게시판 만들기 #7-3. 댓글처리 구현 (게시물 적용)

     

    본 게시판 만들기 프로젝트는 더블에스 Devlog Spring-MVC 를 참조하여 작성했음을 알려드립니다. 또한 개인적인 학습을 목적으로한 포스팅이기 때문에 완벽하지 않을 수 있음을 알려드립니다. 문제점이나 궁금한점은 댓글로 남겨주시면 감사하겠습니다. 프로젝트 생성에 앞서 이번 게시판 만들기 프로젝트는 이클립스를 사용하여 구현하였습니다. 

     

    이번 포스팅에서는 이전 포스팅에서 다뤘던 댓글처리 구현을 바탕으로 게시물에 적용시켜 보도록 하겠습니다.

     

    1. 게시글 조회 화면 수정

    게시글 목록 화면에서는 댓글을 달 수 없는 것이 일반적이기에 게시글 조회화면으로 만들었던 read.jsp 파일을 수정하도록하겠습니다. 

     

    1-1. 댓글 입력 영역

     우선 댓글입력 영역을 추가해야하는데 댓글입력 영역은 reply_test.jsp 페이지의 댓글입력 영역(아래 코드)을 참고해서 작성하겠 주겠습니다. 

    <div class="card-body">
      <form class="form-horizontal">
        <div class="row">
          <div class="form-group col-sm-8">
            <input class="form-control input-sm" id="newReplyText" type="text" placeholder="댓글 입력...">
          </div>
          <div class="form-group col-sm-2">
            <input class="form-control input-sm" id="newReplyWriter" type="text" placeholder="작성자">
          </div>
          <div class="form-group col-sm-2">
            <button type="button" class="btn btn-primary btn-sm btn-block replyAddBtn">
            <i class="fa fa-save"></i> 저장
          </button>
          </div>
        </div>	
      </form>	
    </div>

     

    1-2. 댓글 목록 페이징 영역

     댓글입력 영역을 추가해줬으면 다음으로는 페이징 처리가 댓글 목록 영역을 출력해야합니다. 댓글목록 출력 영역도 reply_test.jsp 페이지의 댓글출력 영역(아래 코드)을 참고해서 작성하겠습니다. 

    <div class="card card-primary card-outline">
      <%--댓글 유무 / 댓글 갯수 / 댓글 펼치기, 접기--%>
      <div class="card-header">
      <a href="" class="link-black text-lg"><i class="fas fa-comments margin-r-5 replyCount"></i></a>
        <div class="card-tools">
          <button type="button" class="btn primary"  data-widget="collapse">
              <i class="fa fa-plus"></i>
          </button>
        </div>
      </div>
      <%--댓글 목록--%>
      <div class="card-body repliesDiv">
      
      </div>
      <%--댓글 페이징--%>
      <div class="card-footer">
        <nav aria-label="Contacts Page Navigation">
        <ul class="pagination pagination-sm no-margin justify-content-center m-0">
    
        </ul>
        </nav>
      </div>
    </div>

     

     

    2. Handlebars를 이용한 JS템플릿 적용

    댓글 기능을 구현하는데 있어서 가장 중요한 부분은 댓글이 추가되거나 수정, 삭제되더라도 지속적으로 댓글 목록이 갱신되어 화면에 출력 되어야하는 것입니다. 목록의 출력은 <div>태그가 반복적으로 구성되고, 하나의 <div> 안의 댓글의 정보들이 채워지는 방식으로 동작하게 됩니다. 이러한 작업은 문자열로 이루어지기 때문에 상당히 번거롭고, 지저분한 코드가 만들어지게 됩니다. 하지만 자바스크립트 템플릿을 통해 좀더 깔끔한 코드를 작성할 수 있고 가독성 향상에도 도움이 됩니다. 자바스크립트 템플릿 종류는 다양하지만 이 예제에서는 Handlebars를 사용하겠습니다.

     

    Handlebars의 구체적인 사용법을 알고 싶다면 아래를 통해 참조하시면 됩니다.

    https://handlebarsjs.com/

     

    Handlebars

     

    handlebarsjs.com

    Handlebars를 사용하기 위해서는 Handlebar라이브러리를 추가해줘야하기에 WEB-INF/view/include/plugin_js.jsp에 Handlebar라이브러리를 추가해줍니다.

    <!-- REQUIRED SCRIPTS -->
    
    <!-- jQuery -->
    <script src="${path}/plugins/jquery/jquery.min.js"></script>
    <!-- Bootstrap 4 -->
    <script src="${path}/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
    <!-- AdminLTE App -->
    <script src="${path}/dist/js/adminlte.min.js"></script>
    <!-- HandleBars JS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js"></script>

     

    2-1. Template 코드 작성

    댓글 목록을 출력하기 위해 Template 코드를 아래와 같이 작성하겠습니다. 

    <script id="replyTemplate" type="text/x-handlebars-template">
        {{#each.}}
        <div class="post replyDiv" data-reply_no={{reply_no}}>
            <div class="user-block">
                <img class="img-circle img-bordered-sm" src="${path}/dist/img/user1-128x128.jpg" alt="user image">
                <span class="username">
                    <a href="#">{{reply_writer}}</a>
                    <a href="#" class="float-right btn-box-tool replyDelBtn" data-toggle="modal" data-target="#delModal">
                        <i class="fa fa-times"> 삭제</i>
                    </a>
                    <a href="#" class="float-right btn-box-tool replyModBtn" data-toggle="modal" data-target="#modModal">
                        <i class="fa fa-edit"> 수정</i>
                    </a>
                </span>
                <span class="description"></span>
            </div>
            <div class="oldReplyText">{{reply_text}}</div>
            <br/>
        </div>
        {{/each}}
    </script>

     

    3. JS 코드 작성

    아래의 댓글 등록, 목록 출력, 수정, 삭제, 페이징 처리에 해당하는 JS 코드를 추가하면 댓글 처리 관련 구현이 완료됩니다. 함수별로 주석에 해당 함수의 목적 및 기능을 설명해 두었습니다. 

    $(document).ready(function () {
    
        var formObj = $("form[role='form']");
        console.log(formObj);
    
        $(".modBtn").on("click", function () {
            formObj.attr("action", "${path}/article/paging/search/modify");
            formObj.attr("method", "get");
            formObj.submit();
        });
    
        $(".delBtn").on("click", function () {
            formObj.attr("action", "${path}/article/paging/search/remove");
            formObj.submit();
        });
    
        $(".listBtn").on("click", function () {
            formObj.attr("action", "${path}/article/paging/search/list");
            formObj.attr("method", "get");
            formObj.submit();
        });
        
        var article_no = "${article.article_no}";  // 현재 게시글 번호
        var replyPageNum = 1; // 댓글 페이지 번호 초기화
    
        // 댓글 내용 : 줄바꿈/공백처리
        Handlebars.registerHelper("escape", function (reply_text) {
            var text = Handlebars.Utils.escapeExpression(reply_text);
            text = text.replace(/(\r\n|\n|\r)/gm, "<br/>");
            text = text.replace(/( )/gm, "&nbsp;");
            return new Handlebars.SafeString(text);
        });
    
        // 댓글 등록일자 : 날짜/시간 2자리로 맞추기
        Handlebars.registerHelper("prettifyDate", function (timeValue) {
            var dateObj = new Date(timeValue);
            var year = dateObj.getFullYear();
            var month = dateObj.getMonth() + 1;
            var date = dateObj.getDate();
            var hours = dateObj.getHours();
            var minutes = dateObj.getMinutes();
            // 2자리 숫자로 변환
            month < 10 ? month = '0' + month : month;
            date < 10 ? date = '0' + date : date;
            hours < 10 ? hours = '0' + hours : hours;
            minutes < 10 ? minutes = '0' + minutes : minutes;
            return year + "-" + month + "-" + date ;
        });
    
        // 댓글 목록 함수 호출
        getReplies("${path}/replies/" + article_no + "/" + replyPageNum);
    
        // 댓글 목록 함수
        function getReplies(repliesUri) {
            $.getJSON(repliesUri, function (data) {
                printReplyCount(data.pageMaker.totalCount);
                printReplies(data.replies, $(".repliesDiv"), $("#replyTemplate"));
                printReplyPaging(data.pageMaker, $(".pagination"));
            });
        }
    
        // 댓글 갯수 출력 함수
        function printReplyCount(totalCount) {
    
            var replyCount = $(".replyCount");
            var collapsedBox = $(".collapsed-box");
    
            // 댓글이 없으면
            if (totalCount === 0) {
                replyCount.html(" 댓글이 없습니다. 의견을 남겨주세요");
                collapsedBox.find(".btn-box-tool").remove();
                return;
            }
    
            // 댓글이 존재하면
            replyCount.html(" 댓글목록 (" + totalCount + ")");
            collapsedBox.find(".box-tools").html(
                "<button type='button' class='btn btn-box-tool' data-widget='collapse'>"
                + "<i class='fa fa-plus'></i>"
                + "</button>"
            );
    
        }
    
        // 댓글 목록 출력 함수
        function printReplies(replyArr, targetArea, templateObj) {
            var replyTemplate = Handlebars.compile(templateObj.html());
            var html = replyTemplate(replyArr);
            $(".replyDiv").remove();
            targetArea.html(html);
        }
    
        // 댓글 페이징 출력 함수
        function printReplyPaging(pageMaker, targetArea) {
        	var str = "";
    
    	    // 이전 버튼 활성화
    	    if (pageMaker.prev) {
    	        str += "<li class=\"page-item\"><a class=\"page-link\" href='"+(pageMaker.startPage-1)+"'>이전</a></li>";
    	    }
    
    	    // 페이지 번호
    	    for (var i = pageMaker.startPage, len = pageMaker.endPage; i <= len; i++) {
    	        var strCalss = pageMaker.criteria.page == i ? 'class=active' : '';
    	        str += "<li class=\"page-item\" "+strCalss+"><a class=\"page-link\" href='"+i+"'>"+i+"</a></li>";
    	    }
    
    	    // 다음 버튼 활성화
    	    if (pageMaker.next) {
    	        str += "<li class=\"page-item\"><a class=\"page-link\" href='"+(pageMaker.endPage + 1)+"'>다음</a></li>";
    	    }
            targetArea.html(str);
        }
    
        // 댓글 페이지 번호 클릭 이벤트
        $(".pagination").on("click", "li a", function (event) {
            event.preventDefault();
            replyPageNum = $(this).attr("href");
            getReplies("${path}/replies/" + article_no + "/" + replyPageNum);
        });
    	
     	// 댓글 저장 버튼 클릭 이벤트
        $(".replyAddBtn").on("click", function () {
    
            // 입력 form 선택자
            var reply_writerObj = $("#newReplyWriter");
            var reply_textObj = $("#newReplyText");
            var reply_writer = reply_writerObj.val();
            var reply_text = reply_textObj.val();
    
            // 댓글 입력처리 수행
            $.ajax({
                type : "post",
                url : "${path}/replies/",
                headers : {
                    "Content-Type" : "application/json",
                    "X-HTTP-Method-Override" : "POST"
                },
                dataType : "text",
                data : JSON.stringify({
                    article_no : article_no,
                    reply_writer : reply_writer,
                    reply_text : reply_text
                }),
                success: function (result) {
                    console.log("result : " + result);
                    if (result === "regSuccess") {
                        alert("댓글이 등록되었습니다.");
                        replyPageNum = 1;  // 페이지 1로 초기화
                        getReplies("${path}/replies/" + article_no + "/" + replyPageNum); // 댓글 목록 호출
                        reply_textObj.val("");   // 댓글 입력창 공백처리
                        reply_writerObj.val("");   // 댓글 입력창 공백처리
                    }
                }
            });
        });
     	
     	// 댓글 수정을 위해 modal창에 선택한 댓글의 값들을 세팅
        $(".repliesDiv").on("click", ".replyDiv", function (event) {
            var reply = $(this);
            $(".reply_no").val(reply.attr("data-reply_no"));
            $("#reply_text").val(reply.find(".oldReplyText").text());
        });
    
        // modal 창의 댓글 수정버튼 클릭 이벤트
        $(".modalModBtn").on("click", function () {
            var reply_no = $(".reply_no").val();
            var reply_text = $("#reply_text").val();
            $.ajax({
                type : "put",
                url : "${path}/replies/" + reply_no,
                headers : {
                    "Content-Type" : "application/json",
                    "X-HTTP-Method-Override" : "PUT"
                },
                dataType : "text",
                data: JSON.stringify({
                	reply_text : reply_text
                }),
                success: function (result) {
                    console.log("result : " + result);
                    if (result === "modSuccess") {
                        alert("댓글이 수정되었습니다.");
                        getReplies("${path}/replies/" + article_no + "/" + replyPageNum); // 댓글 목록 호출
                        $("#modModal").modal("hide"); // modal 창 닫기
                    }
                }
            })
        });
    
        // modal 창의 댓글 삭제버튼 클릭 이벤트
        $(".modalDelBtn").on("click", function () {
            var reply_no = $(".reply_no").val();
            $.ajax({
                type: "delete",
                url: "${path}/replies/" + reply_no,
                headers: {
                    "Content-Type" : "application/json",
                    "X-HTTP-Method-Override" : "DELETE"
                },
                dataType: "text",
                success: function (result) {
                    console.log("result : " + result);
                    if (result === "delSuccess") {
                        alert("댓글이 삭제되었습니다.");
                        getReplies("${path}/replies/" + article_no + "/" + replyPageNum); // 댓글 목록 호출
                        $("#delModal").modal("hide"); // modal 창 닫기
                    }
                }
            });
        });
    });

     

     

     

    4. 포스팅을 마치며

    마침내 댓글 처리 관련 포스팅이 끝났습니다. 하지만 댓글 삭제 및 수정을 아무나 할수 있는 게시판은 이 세상에 없습니다. 이러한 부분은 추후 로그인 처리를 구현한 후에 다시 수정하도록 하겠습니다. 다음 포스팅에서는 게시글 목록에서 게시글 조회 수와 댓글 수의 출력을 구현해보도록 하겠습니다. 

     

    다음 포스팅 

    2020/06/28 - [Spring/게시판 만들기] - [Camel][Spring] 게시판 만들기 #8. 게시글 조회수 증가 및 트랜잭션 처리

     

    [Camel][Spring] 게시판 만들기 #8. 게시글 조회수 증가 및 트랜잭션 처리

    [Camel][Spring] 게시판 만들기 #8. 게시글 조회수 증가 및 트랜잭션 처리 본 게시판 만들기 프로젝트는 더블에스 Devlog Spring-MVC 를 참조하여 작성했음을 알려드립니다. 또한 개인적인 학습을

    cameldev.tistory.com

     

    글을 읽으시면서 잘못된 부분이나 궁금하신 사항은 댓글 달아주시면 빠른 시일내에 수정 및 답변하도록 하겠습니다. 

    댓글

Camel`s Tistory.