[게시판 프로젝트] 댓글 및 대댓글

J_Eddy·2021년 12월 27일
4
post-thumbnail

📌 댓글

🏁 댓글 테이블

먼저 댓글을 처음 구현할 때 시행착오가 한번 있었다. 일반 댓글을 구현할 때에는 문제가 없었지만 대댓글 까지 구현하려니 댓글테이블대댓글테이블을 따로 만들어야 하나 라는 고민에 빠졌다. 둘다 만들자니 성격이 비슷한 테이블이 두개 만들어 지는거 같아 불 필요하다고 느꼈고, 계층구조를 이용하여서 하나의 테이블로 구현하기로 결정했다.

아래 테이블과 같이 컬럼을 구성하였다. CID는 댓글의 ID, Board_ID는 Board(해당 게시물)의 아이디로 조인이 되어있는 상태이다. Comment Writer역시 Member테이블과 조인된 상태로 작성자를 의미한다. 아래 빨간 네모 친 부분을 추가함으로서 계층 구조로 테이블을 구현한 것인데, CDEPTH는 댓글의 깊이를 나타내며 일반 댓글은 0, 대댓글은 1로 구현된다. default값은 0이다. CGROUP는 대댓글일 경우 모댓글의 CID값을 저장하여 누구의 대댓글인지 확인 할 수 있게 구현하였다.

🏁 댓글 목록 불러오기

게시글 하단에 댓글들이 나타나게 구현하였다. 댓글들을 불러오는 방식은 ajax를 이용하여 해당 게시물의 ID를 Board_Id로 가지고 있는 댓글들을 모두 가져오는 방식을 이용하였다. 불러올 때 해당 대댓글들의 갯수도 count해 map형식으로 담아 보냈다.

 $.ajax({
        type: "get",
        url: "/api/comment/list",
        data: {"bid": $("#bid").val()},
        success: function (data) {
            let html = "";
             const count = data.list.length;
          ...
public HashMap<String, Object> getCommentList(Long bid) {
        HashMap<String, Object> map = new HashMap<>();
        Optional<BoardEntity> boardEntity = boardRepository.findById(bid);
        List<BoardCommentEntity> boardCommentEntityList = boardCommentQueryRepository.findByBoardId(boardEntity.get());

        List<BoardCommentDto> boardCommentDtoList = new ArrayList<>(); // 댓글 리스트
        List<Long> ccCountList = new ArrayList<>(); // 대댓글 갯수 카운트

        for (int i = 0; i < boardCommentEntityList.size(); i++) {
            boardCommentDtoList.add(boardCommentEntityList.get(i).toDto()); //댓글 리스트
            ccCountList.add(boardCommentQueryRepository.findReCommentCnt(boardCommentEntityList.get(i).getId())); //대댓글 갯수 카운트
        }
        map.put("list", boardCommentDtoList);
        map.put("commentCnt", ccCountList);

        return map;
    }

이후 불러온 값들을 이용하여 JS에서 댓글 리스트들을 그렸다. 해당 댓글들이 존재할 때 (data.list.length >0)값을 그리기 시작하였다. Html태그들을 이용하여 그렸는데 처음에 할때에는 id값을 동일하게 그렸다. 하지만 코드리뷰 당시 'id값은 왠만하면 다르게 해야한다. 지금은 정상적으로 작동할지 몰라도 나중에 문제가 될수 있다.' 라고 말씀 하셔서 해당 id값을 commentId_해당 CID값으로 설정 하였다. 이후 작성자, 내용, 날짜 등도 위와 같은 id형식을 이용하여 작성하였다. 아래 코드 중 ccCount라는 id를 가진 span태그가 존재한다. 이는 대댓글의 숫자를 보여주도록 하는 태그이다.

추가적으로 답글 달기버튼은 default값으로 display를, 답글 닫기 버튼은 default값으로 display none을 설정해두었다.

/*댓글 목록*/
function getCommentList() {

   ...
        success: function (data) {
            let html = "";
             const count = data.list.length;

            if (data.list.length > 0) {
                for (let i = 0; i < data.list.length; i++) {
                    html += "<div class='mb-2'>";
                    html += "<input type='hidden' id='commentId_"+ data.list[i].id +"' value='" + data.list[i].id + "'>"
                    html += "<b id='commentWriter_" + data.list[i].id + "'>" + data.list[i].writer + "</b>";
                    html += "<span style='float:right;' align='right' id='commentDate_"+ data.list[i].id +"'> " + displayTime(data.list[i].updateDate) + " </span>";
                    html += "<div class='mb-1 comment_container' >"
                    html += "<h5 id='commentText_" + data.list[i].id + "' style='display: inline'>" + data.list[i].comment +"</h5>";
                    html += "<span id='ccCount_" + data.list[i].id + "' style='color: red'> ["+data.commentCnt[i]+"]</span>"
                    html += "</div>"
                    html += "<span style='cursor: pointer; color: blue' class='reCommentBtn' id='reCommentBtn_"+ data.list[i].id +"'>답글 달기 </span>";
                    html += "<span style='display:none; cursor: pointer; color: blue' class='reCommentCloseBtn' id='reCommentCloseBtn_"+ data.list[i].id +"'>답글 닫기 </span>";
                  
                  ...
                    
                    html += "<hr>";
                    html += "<div class='mx-4 reCommentDiv' id='reCommentDiv_" + data.list[i].id + "'></div></div>";
                }
            } else {
                html += "<div class='mb-2'>";
                html += "<h6><strong>등록된 댓글이 없습니다.</strong></h6>";
                html += "</div>";
            }
            $("#count").html(count);
            $("#commentList").html(html);
        },
        error: function (request, status, error) {
            alert("code: " + request.status + "\n"  + "error: " + error);
        }
    });
}

이후 만약 댓글이 존재 하지 않으면 '등록된 댓글이 없습니다'라는 문구를 띄웠다.

이후 만약 댓글의 작성자와 현재 로그인 한 사람의 세션 정보과 동일할 경우 아래와 같이 삭제, 수정이 이루어질수 있도록 구현하였다. 이 때 대댓글이 존재할 시 삭제가 불가능 하게 설정하였다.

 if (data.list[i].writer === $("#sessionNickname").val()) {
                        html += "<span style='cursor: pointer; color: blue' class='commentMod' data-toggle='modal' data-target='#modifyModal'>수정 </span>";

                        html += "<span style='cursor: pointer; color: blue' class='commentDel'>삭제</span>";
                    }  else if($("#sessionRole").val() === "ROLE_ADMIN"){
                        html += "<span style='cursor: pointer; color: blue' class='commentDel'>삭제</span>";
                    }

구현된 모습은 아래와 같다.

🏁 댓글 작성

댓글 작성 버튼은 해당게시물의 id값과 input창의 값을 얻어 ajax를 통해 이루어졌다. ajax를 이용하여 해당 컨트롤러에 매핑될 시 cDepth와 cGroup은 default값으로 0을 세팅한다. 이후 해당 값을 통해 테이블에 저장한다. 이때 WriterMember테이블과 조인되어있으므로 해당 값을 찾아 저장해준다. 이후 성공시 success라는 값을 받아 리턴하고 ajax에서는 페이지를 리로드 하는 방식으로구현하였다. 원래는 리로드 없이 바로 보여주려 했지만 이 부분은 조금 더 공부가 필요한 부분이다.

/*댓글 등록*/
function commentPost() {
    $.ajax({
        type: "post",
        url: "/api/comment/post",
        data: {"comment": $("#comment").val(), "bid": $("#bid").val()},
        success: function (data) {
            if (data.result == "success") {
                location.reload();
            }
        },
        error: function (request, status, error) {
            alert("code: " + request.status + "\n" + "error: " + error);
        }

    });
}

컨트롤러

 @ResponseBody
    @PostMapping("/api/comment/post")
    public HashMap<String, Object> commentPost(@RequestParam Long bid,
                              @RequestParam String comment,
                              Authentication authentication) throws Exception {
        int cDepth = 0;
        Long cGroup = 0L;

        return commentService.commentPost(bid, comment, cDepth, cGroup, authentication.getName());
    }

서비스

/*댓글 등록*/
    public HashMap<String, Object> commentPost(Long bid, String comment, int cDepth, Long cGroup, String username) throws Exception {
        HashMap<String, Object> map = new HashMap<>();

        BoardCommentDto boardCommentDto = new BoardCommentDto();

        boardCommentDto.setComment(comment);
        boardCommentDto.setCDepth(cDepth);
        boardCommentDto.setCGroup(cGroup);
        boardCommentDto.setCreateDate(LocalDateTime.now());
        boardCommentDto.setUpdateDate(LocalDateTime.now());

        Optional<MemberEntity> memberEntity = memberRepository.findByUsername(username);
        Optional<BoardEntity> boardEntity = boardRepository.findById(bid);

        BoardCommentEntity boardCommentEntity = boardCommentDto.toEntity();

        boardCommentEntity.setCommentBoardId(boardEntity.get());
        boardCommentEntity.setCommentWriter(memberEntity.get());

        boardCommentRepository.save(boardCommentEntity);
        map.put("result","success");
        return map;
    }

🏁 댓글 수정 - modal

댓글 수정 부분은 모달을 이용하여 수정하였다. 굳이 모달을 사용하지 않고 일반 input창에서 수정을 구현 할 수도 있었지만 보다 다양한 방식으로 접근해 보고 싶어 모달을 이용하였다.

먼저 모달을 사용하려고 html에 기본세팅을 해주었다. 방식의 차이는 있지만 나는 댓글내용과 작성자 정도만 보이도록 하였다. 이후 댓글 수정 버튼 클릭시 셀렉터를 통해서 내가 클릭한 댓글의 id값과 내용, 작성자를 받아오고 해당 값을 modal에 띄우도록 구현하였다.

/*댓글 수정버튼 클릭*/
$(document).on("click", ".commentMod", function () {

    const comment_id = $(this).siblings('input').val();
    const comment_text = $(this).siblings('.comment_container').children('h5').text();
    const comment_writer = $(this).siblings('b').text();

    $("#comment_id").val(comment_id);
    $("#comment_text").val(comment_text);
    $("#comment_writer").val(comment_writer);

});

이후 수정완료 버튼 클릭 시 ajax통신을 이용해서 QueryDsl을 통해 업데이트 시키게 하였다.

/*모달창 수정 버튼*/
$(".modalModBtn").on("click", function () {
    const comment_id = $("#comment_id").val();
    const comment_text = $("#comment_text").val();
    if (!confirm("댓글을 수정하시겠습니까?")) {
        return false;
    } else {
        $.ajax({
            type: 'put',
            url: "/api/comment/modify",
            data: {"cid": comment_id, "comment": comment_text},
            success: function (result) {
                if (result == "success") {
                    alert("댓글을 수정하였습니다.");
                } else {
                    alert("오류가 발생하였습니다. 다시 시도해주세요")
                }
                location.reload();
            },
            error: function (request, status, error) {
                alert("code: " + request.status + "\n" + "error: " + error);
            }
        });
    }
});
@Transactional
    public void updateComment(Long cid, String comment) {
        queryFactory.update(QBoardCommentEntity.boardCommentEntity)
                .set(QBoardCommentEntity.boardCommentEntity.comment, comment)
                .set(QBoardCommentEntity.boardCommentEntity.updateDate, LocalDateTime.now())
                .where(QBoardCommentEntity.boardCommentEntity.id.eq(cid))
                .execute();
    }

📌 대댓글

🏁 대댓글 목록

대댓글 목록은 해당 댓글에서 대댓글 버튼을 눌렀을 때 클릭 이벤트와 셀렉터를 이용하여 해당 모댓글의 CID값을 통하여 목록을 나타내었다. 이후 대댓글보기버튼은 숨기고 창닫기버튼을 활성화 시켰다.

/*대댓글 버튼 클릭*/
$(document).on("click",".reCommentBtn",function (){

    const _this = $(this);
    //const cid = reComment.find("#commentId").val();
    const cid = $(this).siblings('input').val();

    _this.siblings('.reCommentDiv').show();
    _this.hide();
    _this.siblings('.reCommentCloseBtn').show();

    $.ajax({
        type: "get",
        url: "/api/comment/reComment/list",
        data: {"cid": cid},
        success: function (data) {
            let html = "";

            if (data.list.length > 0) {
                for (let i = 0; i < data.list.length; i++) {
                    html += "<div class='mb-2'>";
                    html += "<input type='hidden' id='commentId_"+ data.list[i].id +"' value='" + data.list[i].id + "'>"
                    html += "<b id='commentWriter_" + data.list[i].id + "' >" + data.list[i].writer + "</b>";
                    html += "<span style='float:right;' align='right' id='commentDate'> " + displayTime(data.list[i].updateDate) + " </span>";
                    html += "<h5 id='commentText_"+ data.list[i].id +"'>" + data.list[i].comment + "</h5>";
                    if (data.list[i].writer === $("#sessionNickname").val()) {
                        html += "<span style='cursor: pointer; color: blue' class='commentMod' data-toggle='modal' data-target='#modifyModal' >수정 </span>";
                        html += "<span style='cursor: pointer; color: blue' class='commentDel'>삭제</span>";
                    } else if($("#sessionRole").val() === "ROLE_ADMIN"){
                        html += "<span style='cursor: pointer; color: blue' class='commentDel'>삭제</span>";
                    }
                    html += "<hr></div>";
                }
            } else {
                html += "<div class='mb-2'>";
                html += "<h6><strong>등록된 댓글이 없습니다.</strong></h6>";
                html += "</div>";
            }
            html += "<input style='width: 90%' id='reComment_"+cid+"' class='reComment' name='reComment' placeholder='댓글을 입력해 주세요'>";
            html += "<button type='button' class='btn btn-primary mx-2 reCommentSubmit'>등록</button>";

            _this.siblings(".reCommentDiv").html(html);

            /*$("#reComment_"+cid+"").emojiInit({
                fontSize: 14
            });*/
        },
        error: function (request, status, error) {
            alert("code: " + request.status + "\n" + "error: " + error);
        }
    });
  ...

🏁 대댓글 작성

대댓글 작성 부부은 댓글 작성과 크게 다를것이 없지만 cDepthcGroup의 설정이 다르다. 대댓글을 작성할 때 모댓글의 id인 Cid값을 같이 넘겨주어 cGroup에 저장하고, cDepth는 1로 설정하여 누구의 대댓글인지 확인할 수 있게 설정하였다.

🏁 대댓글 수정

대댓글 수정 부분은 댓글 수정과 동일하다.

📌 관리자 권한

댓글과 대댓글은 다음과 같은 조건이 있다.

  1. 본인이 작성한 댓글, 대댓글만 삭제가 가능하다.
  2. 대댓글이 존재할 경우 댓글의 삭제가 불가능하다.

하지만 관리자는 위 두가지를 모두 제어할수있는 권한이 있다. 그럼 한가지 의문이 생긴다. 관리자가 대댓글이 있는 댓글을 삭제해버리면 무결성참조에 위배가 되지않나? 라는 생각이 들 수있다. 나도 처음에 단순 삭제를 구현했을 때 위와같은 문제점을 마주했다. 이를 해결하기 위해 차례대로 삭제하는 과정을 거쳤다. 이는 이전에 회원탈퇴 때 사용했던 로직과 같은 로직을 보인다.

/*댓글 삭제*/
    public Long deleteComment(Long cid, Object roleSession) {
        Optional<BoardCommentEntity> boardCommentEntity = boardCommentRepository.findById(cid);
        int depth = boardCommentEntity.get().getCDepth();
        Long reCommentCnt = boardCommentQueryRepository.deleteCheck(cid);

        if (depth == 0) {
            if (roleSession.equals(Role.ROLE_ADMIN)) {
                /*모댓글의 대댓글 까지 전부 삭제*/
                boardCommentQueryRepository.deleteByCid(cid);
                return 0L;
            }
            if (boardCommentQueryRepository.deleteCheck(cid) == 0) {
                boardCommentRepository.deleteById(cid);
            }
        } else if (depth == 1) {
            boardCommentRepository.deleteById(cid);
        }

        return reCommentCnt;
    }

최종 구현 화면은 아래와 같다.

profile
논리적으로 사고하고 해결하는 것을 좋아하는 개발자입니다.

2개의 댓글

comment-user-thumbnail
2022년 11월 24일

깃헙주소 알 수 있을까요?mapper코드도 보고 싶어서요!

답글 달기
comment-user-thumbnail
2023년 8월 14일

깃헙 링크 알 수 있을까여?

답글 달기