쌓고 쌓다

[스프링 부트] 대댓글 작성 및 더보기 기능 - 16 본문

프로그래밍/spring

[스프링 부트] 대댓글 작성 및 더보기 기능 - 16

승민아 2023. 7. 28. 16:16

DB 테이블 변경

ALTER TABLE comment ADD COLUMN parent_comment_id int;
ALTER TABLE comment ADD COLUMN is_parent int not null;

댓글과 대댓글 구별은 컬럼 is_parent로 이뤄진다.

is_parent가 1이면 부모 댓글로 일반 댓글이며 0이면 대댓글로 구분한다.

 

부모 댓글은 자신의 id를 parent_comment_id로 가지며

대댓글은 부모 댓글의 id를 가진다.

 

기존의 DB 데이터들은 아래의 쿼리로 값을 갱신해주자.

UPDATE comment AS A
INNER JOIN comment AS B ON A.id = B.id
SET A.parent_comment_id = A.id;

UPDATE comment SET is_parent = 1;

 

CommentRepository

Page<Comment> findByPnoAndIsParent(Long pno, boolean isParent, Pageable pageable);
List<Comment> findByParentCommentIdAndIsParent(Long parentCommentId, boolean isParent,Pageable pageable);
Long countByParentCommentIdAndIsParent(Long parentCommentId, boolean isParent);

1. findByPnoAndIsParent : 게시글(pno)의 부모 댓글 또는 대댓글을 페이징 처리하여 받는다.

2. findByParentCommentIdAndIsParent : 부모 댓글의 id와 대댓글임을 판별하는 isParent=0을 통해 정렬된 대댓글을 가져옵니다.

3. countByParentCommentIdAndIsParent : 대댓글은 해당 게시글에 달린 댓글을 Count하는데 포함하지 않기위해서 작성함.

                                                                           (게시글들 목록 리스트에서 제목 옆에 댓글 수 표시를 위함)

 

 

CommentService

(1) 댓글 작성

public Long write(Comment comment) {
        comment.setRegDate(LocalDateTime.now());
        if(comment.getIsParent()) { // 부모 댓글
            posterService.incrementCommentCnt(comment.getPno());
        }

        commentRepository.save(comment); // DB에 저장할때까지 id를 알 수 없으므로

        if(comment.getIsParent()) // 부모 댓글이라면 부모Id컬럼에 자기 자신 Id를
            comment.setParentCommentId(comment.getId());

        return comment.getId();
    }

이제 댓글 작성시 부모 댓글과 대댓글에 따른 분기가 필요하다.

부모 댓글이라면 게시글 댓글 개수에 추가한다.

parentCommentId 설정시 부모의 Id가 필요한데

대댓글은 부모 댓글의 Id가 존재하므로 부모의 Id를 넣어줄 수 있다.

그런데 부모 댓글은 DB에 저장할때까지 Id는 존재하지 않는다. 영속 상태로 만들어 DB에 저장하고 Id를 생성하는 과정 후에

ParentCommentId를 넣어줘야한다.

 

(2) 대댓글 조회

    public Map<String, Object> findReply(Long parentCommentId, int page) {
        Sort sort=Sort.by(Sort.Order.desc("regDate"), Sort.Order.desc("id"));
        Pageable pageable = PageRequest.of(page, 5, sort);
        Map<String, Object> m = new HashMap<>();
        m.put("content" ,commentRepository.findByParentCommentIdAndIsParent(parentCommentId, false ,pageable));
        m.put("totalSize", commentRepository.countByParentCommentIdAndIsParent(parentCommentId, false));
        return m;
    }

페이징된 대댓글 데이터뿐만 아니라

부모 댓글의 아래 달린 대댓글의 총 개수도 totalSize로 반환해준다.

추후에 대댓글 더보기시 더보기 가능 여부를 판단하는데 사용된다.

 

CommentController

(1) 댓글 및 대댓글 삭제

    @PostMapping("/comment/delete")
    @ResponseBody
    public Comment commentDelete(Long id) {
        Comment comment = commentService.findComment(id);
        if(comment.getIsParent())
            posterService.decreaseCommentCnt(comment.getPno());
        commentService.deleteComment(id);
        return comment;
    }

댓글 삭제시 댓글과 대댓글의 구별이 필요하다.

대댓글 삭제는 게시글 총 댓글 개수의 카운트 감소를 하지 않는다.

 

(2) 대댓글 조회

    @GetMapping("/reply")
    @ResponseBody
    public Map<String, Object> findReply(Long parentCommentId, int page) {
        return commentService.findReply(parentCommentId, page);
    }

부모 댓글의 Id와 페이징된 대댓글의 페이지 번호를 넘겨 데이터를 받는다.

 

posterView.html

댓글 작성

let data = {
            writer: writer,
            content: content,
            pno: pno,
            isParent: 1
        };

대댓글이 아닌 댓글 작성 버튼으로 POST 요청 데이터에 부모 여부를 1로 보내게 추가함.

 

 

아래의 코드들은 commentLoad시 이뤄지는 부분들이다.

HTML 태그를 만들어 뽑아온 댓글들을 넣고 대댓글 기능들을 추가하는 과정이다.

 

(1) 대댓글 작성 및 보기

let commentUl = $("#comments");
commentUl.append(`
<li> ${comment.writer} ${comment.content} ${date + " " + time}
<button onclick="removeComment(${comment.pno}, ${comment.id})">삭제</button>

<div>
	<button class="reply-btn">답글 작성</button>
	<div class="reply-form" style="display: none">
		<input name="writer">작성자
		<input name="content">내용
		<input type="hidden" name="parent_comment_id" value="${comment.id}">
		<input type="hidden" name="pno" value="${comment.pno}">
		<button class="reply-submit">제출</button>
	</div>
	<button class="reply-see" data-parent-comment-id="${comment.id}" data-show="0">답글 보기</button>
	<ul class="reply-list" data-page="0">
	</ul>
	<button style="display: none">더보기</button>
</div>
</li>`);

댓글 목록 부분에 가져온 댓글들을 하나씩 넣을때

위의 html을 생성하여 넣는다. 답글 작성 폼은 display 속성으로 폼을 보이고숨기고 한다.

답글 작성 부분에는 "parent_comment_id"를 hidden 속성을 주어 함께 서버에 넘겨준다.

 

답글 보기는 html에서 데이터를 저장할 수 있는 data 속성을 사용했다.

답글 보기 버튼에는 부모 댓글 Id와 답글 보기를 펼쳤다 접을 수 있는 show를 작성했다.

 

reply-list는 data-page로 보여질 대댓글 페이지를 저장해 놓는다.

더보기 버튼은 더 보여줄 데이터의 여부에 따라 display 속성이 바뀐다.

 

(2) 답글 작성 버튼 클릭 이벤트

const replyButtons = document.querySelectorAll(".reply-btn");
        replyButtons.forEach(replyButton => {

            replyButton.addEventListener("click", function() {
                // 대댓글 작성 폼 토글 (보이기/숨기기)
                const replyForm = this.nextElementSibling;
                if (replyForm.style.display === "none") {
                    replyForm.style.display = "block";
                } else {
                    replyForm.style.display = "none";
                }
            });
        });

답글 보기 버튼 다음 요소는 답글 작성 폼 replyForm이다. 이 요소는 버튼의 nextElementSibling으로 선택할 수 있다.

답글 보기 버튼 클릭시 답글 작성 폼의 display 속성을 조작하여 사용자에게 표시한다.

 

 

(3) 답글 제출 버튼 클릭 이벤트

const submitButtons = document.querySelectorAll(".reply-submit");
submitButtons.forEach(submitButton => {
	submitButton.addEventListener("click", function () {
		const replyForm = this.parentElement;
		const writerInput = replyForm.querySelector('input[name="writer"]').value;
		const contentInput = replyForm.querySelector('input[name="content"]').value;
		const parentCommentIdInput = replyForm.querySelector('input[name="parent_comment_id"]').value;
		const pno = replyForm.querySelector('input[name="pno"]').value;
		reply_data = {
			writer: writerInput,
			content: contentInput,
			pno: pno,
			parentCommentId: parentCommentIdInput
		}
		$.ajax({
			type: "post",
			url: "/comment/write",
			dataType: "json",
			contentType: "application/json",
			data: JSON.stringify(reply_data),
			success: function () {
			    loadComments(pno, 0);
			},
			error: function (error) {
			    alert("error");
			}
		})
	})
})

생성된 대댓글 작성 버튼마다 클릭 이벤트를 넣었다.

댓글마다 replyForm 영역이 있는데 이 영역에서

replyForm.querySelector('input[name="writer"]')처럼 input 태그이며 name 속성을 지정하여 찾을 요소를 선택할 수 있다.

요소 선택으로 입력한 대댓글 값을 읽어 서버에 전송한다.

 

(4) 답글 보기 버튼 클릭 이벤트

const replySeeBtns = document.querySelectorAll(".reply-see");
	replySeeBtns.forEach( replySeeBtn => {
	const parentCommentId = replySeeBtn.dataset.parentCommentId;
	const replyList = replySeeBtn.nextElementSibling;
	const replyMoreBtn = replyList.nextElementSibling;

	replyMoreBtn.addEventListener("click", function() {
	    replyLoad(replyList, replyMoreBtn, parentCommentId)
	});

	replySeeBtn.addEventListener("click", function() {
        if(replySeeBtn.dataset.show === "0") {
            replySeeBtn.dataset.show = "1";
        } else {
            replyList.innerHTML="";
            replyList.dataset.page="0";
            replySeeBtn.dataset.show="0";
            replyMoreBtn.style.display = "none";
            return;
        }

            replyLoad(replyList, replyMoreBtn, parentCommentId);
	})
})

답글 보기 버튼에 data-parent-comment-id 속성으로 부모 댓글의 id가 담겨져 있다.

이 데이터를 dataset으로 가져올 수 있다.

 

답글 보기 버튼 태그 - 답글 목록 태그 - 답글 더보기 태그 순으로 HTML이 되어있다.

nextElementSibling으로 위의 요소들을 모두 선택하여 이벤트를 등록한다.

*replyLoad : 부모 댓글 parentCommentId의 댓글 목록 replyList에 대댓글 HTML을 추가

 

replyMoreBtn (더보기)

replyLoad로 대댓글 목록에 요소들을 추가함

 

replySeeBtn (답글 보기 버튼)

show가 0이라면 사용자에게 보이지 않는 상태이므로 1로 바꾸고 댓글들 로드함.

1이라면 replyList를 빈 태그로 만드는 등 초기 상태로 변경함.

그리고 replyLoad 1회 수행.

 

(5) replyLoad 함수 ( 대댓글 목록에 대댓글 채우기 )

function replyLoad(replyList, replyMoreBtn, parentCommentId) {
        page = replyList.dataset.page;
        $.ajax({
            type: "GET",
            url: `/reply?parentCommentId=${parentCommentId}&page=${page}`,
            dataType: "json",
            success: function (replys) {
                replys.content.forEach( (reply) => {
                    let Child = document.createElement("li");
                    Child.append(`${reply.writer} ${reply.content} ${reply.regDate}`);
                    replyList.appendChild(Child);
                })

                replyList.dataset.page=String(parseInt(page)+1);
                if(replys.totalSize <= (replyList.dataset.page)*5) {
                    replyMoreBtn.style.display = "none";
                } else {
                    replyMoreBtn.style.display = "block";
                }
            },
            error: function () {
                alert('error');
            }
        })
    }

replyList에 page 데이터가 있다. 이 데이터를 가지고 서버에 대댓글 요청을 하여 replyList에 붙여 넣는다.

그후 page 데이터를 1 증가시켜 저장하고 더 가져올 데이터가 남아 있지 않으면 더보기 버튼을 숨긴다.

 

구현 결과



추후에 스크립트랑 html을 분리해야겠다...

Comments