Spring Boot/구현해보기

댓글 삭제하기 구현

xowoony 2022. 11. 27. 20:38

BbsMapper.xml

가장 먼저 작성자가 작성한 댓글의 index 정보가 필요하기 때문에

select 쿼리를 이용하여 `study_bbs`.`comments` 테이블을 select 하고

index 값을 가져온다.

 

다음으로,

댓글 삭제를 위해 delete 쿼리를 이용한다.

댓글 삭제의 경우 반환 타입이 필요가 없기 때문에 resultType을 적지 않는다.

<!-- 댓삭-->
<select id="selectCommentByIndex"
        resultType="dev.xowoony.studymemberbbs.entities.bbs.CommentEntity">
    SELECT `index`         AS `index`,
           `comment_index` AS `commentIndex`,
           `user_email`    AS `userEmail`,
           `article_index` AS `articleIndex`,
           `content`       AS `content`,
           `written_on`    AS `writtenOn`
    FROM `study_bbs`.`comments`
    WHERE BINARY `index` = #{index}
    LIMIT 1
</select>

<delete id="deleteCommentByIndex">
    #반환할 것이 없기 때문에 parameterType 필요없음
    DELETE
    FROM `study_bbs`.`comments`
    WHERE `index` = #{index}
    LIMIT 1
</delete>

 

IBbsMapper.java(인터페이스)

// 댓글 삭제
int deleteCommentByIndex(@Param(value = "index") int index);
// 댓글 삭제(select 했던 부분)
CommentEntity selectCommentByIndex(@Param(value = "index") int index);

 

 

 

BbsService.java

 

1. 만약 삭제하려는 댓글이 존재하지 않는다면

NO_SUCH_COMMENT 반환

 

2. 만약 사용자가 로그인을 하지 않았거나,

사용자 이메일과 댓글 남긴 사용자의 이메일이 서로 일치하지 않을 경우(내가 남긴 댓글이 아닐 경우)

NOT_ALLOWED 반환

참고로,  이 부분에서 NullPointException 이 터지진 않는다.

(앞에서 user가 null 일 경우(로그인 안했을 경우), 뒤에는 알아볼 필요가 없어지기 때문)

 

3. 그 외의 경우 (내가 남긴 댓글의 경우)

SUCCESS 반환

 

최종적으로 comment의 index가 0보다 큰 것이 참일 경우 (0보다 큰 경우) SUCCESS 반환

0보다 큰 것이 거짓일 경우 (0일 경우) FAILURE 반환

 

// 댓글 delete
public Enum<? extends IResult> deleteComment(UserEntity user, CommentEntity comment) {
    CommentEntity existingComment = this.bbsMapper.selectCommentByIndex(comment.getIndex());
    if (existingComment == null) {
        return CommentDeleteResult.NO_SUCH_COMMENT;
    }
    if (user == null || !user.getEmail().equals(existingComment.getUserEmail())) {
        return CommentDeleteResult.NOT_ALLOWED;
    }
    return this.bbsMapper.deleteCommentByIndex(comment.getIndex()) > 0
            ? CommonResult.SUCCESS
            : CommonResult.FAILURE;
    // 성공 :
    // 실패 : deleteComment : 0  일 경우 ==> 알수 없는 이유로 실패했을 경우
    // 실패 : 로그인이 안되었고, 삭제하려는 댓글이 내 댓글이 아닌 경우
    // 실패 : 댓글이 존재하지 않을 경우
}

 

BbsController.java

★참 (주소를 동일하게 하고 방식을 달리하는 것을 레스트 라고 한다.) 고★
js에서 comment 주소의 DELETE 요청방식인 것이 실행된다

 

삭제를 하기 위해서는

1. 로그인한 사용자만 삭제를 할 수 있게 구현할 것이므로 @SessionAttribute를 이용하며 UserEntity의 user를 받아오고

2. CommentEntity가 가지는 필드 중 index가 있어야 삭제할 댓글을 알 수 있기 때문에  CommentEntity까지 받아온다 

  참고로 자바스크립트에서는 formData.append('index', commentObject['index']); 를 넘겨주고 있다.

 

아까 BbsService.java에서 마지막으로 리턴 해주었던 결과(SUCCESS 또는 FAILURE)를 result에 넣어준다.

responseJson을 생성하여

그 result를 넣어주고

마지막으로 responseJson을 문자열로 리턴한다.

// delete
@RequestMapping(value = "comment",
        method = RequestMethod.DELETE,
        produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public String deleteComment(@SessionAttribute(value = "user", required = false) UserEntity user, CommentEntity comment) {
    Enum<?> result = this.bbsService.deleteComment(user, comment);
    JSONObject responseJson = new JSONObject();
    responseJson.put("result", result.name().toLowerCase());
    return responseJson.toString();
}

 

 

read.js

아까 언급해준 <js에서 comment 주소의 DELETE 요청방식인 것이 실행된다> 가 드디어 여기서 실행된다.

우선 js파일에 html 태그를 그대로 가져왔기 때문에 (여기선 html태그로 인식되지 않고 문자열로 인식됨)

아무런 의미없는 html 문자열을 찐으로 html태그로 변환시켜 준 뒤

그것을 dom에 집어 넣는 작업을 한다. (아래와 같이)

// 위에서 가져온 의미 없는 문자열을 html 태그로 만들어버리기
const domParser = new DOMParser();
const dom = domParser.parseFromString(commentHtmlText, 'text/html'); // html 로 변환후 dom에 넣는다

rel이 actionDelete인 친구(별표친 곳) 를 클릭하였을 때 alert 창을 한번 띄워주고 본격적으로 삭제작업 요청을 보낸다.

<a class="action delete" href="#" rel="actionDelete">삭제</a>

삭제작업 요청 과정

1. index 를 formData에 실어주고 xhr을 오픈한다.

2. 만약  success 값이 넘어오는 경우 정상적으로 삭제가 되도록 한다.(loadComments();)

no_such_comment 가 넘어오는 경우 alert 창을 아래에 적은 문구와 같이 띄워준다.

not_allowed 가 넘어오는 경우 alert 창을 아래에 적은 문구와 같이 띄워준다.

알 수 없는 경우 alert 창을 아래에 적은 문구와 같이 띄워준다.

이도 저도 아닌 경우 alert 창을 아래에 적은 문구와 같이 띄워준다.

마지막으로 formData를 실어 보내준다

 

// 댓글 삭제하기 눌렀을 때 댓글 삭제되게
dom.querySelector('[rel="actionDelete"]')?.addEventListener('click', e => {
    e.preventDefault();
    if (!confirm('정말로 댓글을 삭제 할건가요?')) {
        return;
    }
    const xhr = new XMLHttpRequest();
    const formData = new FormData();
    formData.append('index', commentObject['index']);

    xhr.open('DELETE', './comment');
    xhr.onreadystatechange = () => {
        if (xhr.readyState === XMLHttpRequest.DONE) {
            Cover.hide();
            if (xhr.status >= 200 && xhr.status < 300) {
                const responseObject = JSON.parse(xhr.responseText);
                switch (responseObject['result']) {
                    case 'success':
                        loadComments();
                        break;
                    case 'no_such_comment':
                        alert('삭제하려는 댓글이 더이상 존재하지 않습니다.\n이미 삭제되었을 수도 있어요.');
                        break;
                    case 'not_allowed':
                        alert('삭제할 권한이 없어요.');
                        break;
                    default:
                        alert('알 수 없는 이유로 삭제하지 못하였습니다. 다시 시도해주세요.');
                }
            } else {
                alert('서버와 통신하지 못하였습니다. 잠시후 다시 시도해주세요');
            }
        }
    };
    xhr.send(formData);
});

 

 

 

read.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title th:text="${board.getText()} + ' :: ' + ${article.getTitle()}"></title>
    <!--    <title>글 읽기</title>-->
    <script th:if="${article == null}">
        alert('존재하지 않는 게시물입니다.');
    </script>
    <th:block th:replace="~{fragments/head :: common}"></th:block>
    <link rel="stylesheet" th:href="@{/bbs/resources/stylesheets/read.css}">
    <script defer th:src="@{/bbs/resources/scripts/read.js}"></script>
</head>
<body>
<th:block th:replace="~{fragments/body :: header}"></th:block>
<th:block th:replace="~{fragments/body :: cover}"></th:block>
<main class="--main main">
    <div class="--content content">
        <h1 class="title" th:text="${board.getText()}"></h1>
        <table class="table">
            <thead>
                <tr>
                    <th>번호</th>
                    <td class="index" th:text="${article.getIndex()}"></td>
                    <th>제목</th>
                    <td colspan="3" th:text="${article.getTitle()}"></td>
                </tr>
                <tr>
                    <th>조회수</th>
                    <td th:text="${article.getView()}"></td>
                    <th>작성자</th>
                    <td th:text="${article.getUserNickname()}"></td> <!-- 놀랍게도 이메일이 아니라 닉네임이 찍혀야함 -->
                    <th>작성 일시</th>
                    <td th:text="${#dates.format(article.getWrittenOn(), 'yyyy-MM-dd HH:mm:ss')}"></td>
                </tr>
            </thead>
            <tbody>
            <tr>
                <td colspan="6">
                    <div class="content-container">
                        <div class="content" th:utext="${article.getContent()}"></div>
                    </div>
                </td>
            </tr>
            </tbody>
            <tfoot>
                <tr>
                    <td colspan="6">
                        <div class="button-container">
                            <a class="--object-button" href="#">목록</a>
                            <span class="spring"></span>
                            <a class="--object-button modify" href="#"
                               th:if="${session.user != null && session.user.getEmail().equals(article.getUserEmail())}">수정</a>
                            <a class="--object-button delete" href="#"
                               th:if="${session.user != null && session.user.getEmail().equals(article.getUserEmail())}">삭제</a>
                            <!-- session.user != null을 하지않을경우 뒤의 값에서 nullPointException이 뜬다.-->
                        </div>
                    </td>
                </tr>
                <tr class="comment-row">
                    <td colspan="6">
                        <form class="comment-form" id="commentForm">
                            <label class="label">
                                <span hidden>댓글작성</span>
                                <input class="--object-input" id="objectInput" maxlength="100" name="content"
                                       placeholder="댓글을 입력해 주세요" type="text">
                            </label>
                            <input type="hidden" th:value="${article.getIndex()}" name="aid">
                            <input class="--object-button" type="submit" value="작성">
                        </form>
                        <div class="comment-container" id="commentContainer">
                            <div class="comment liked mine">
                                <div class="head">
                                    <span class="writer">관리자</span>
                                    <span class="dt">2022-01-01 00:00:00</span>
                                    <span class="spring"></span>
                                    <span class="action-container">
                                        <a class="action reply" href="#" rel="actionReply">답글 달기</a>
                                        <a class="action modify" href="#" rel="actionModify">수정</a>
                                        <a class="action delete" href="#" rel="actionDelete">삭제</a>
                                        <a class="action cancel" href="#" rel="actionCancel">취소</a>
                                    </span>
                                </div>
                                <div class="body">
                                    <div class="content">
                                        <span class="text">댓글 내용임!</span>
                                        <div class="like">
                                            <a href="#" class="toggle">
                                                <i class="icon fa-solid fa-heart"></i>
                                            </a>
                                            <span class="count">9,999</span>
                                        </div>
                                    </div>
                                    <form class="modify-form">
                                        <label class="label">
                                            <span hidden>댓글 수정</span>
                                            <input class="--object-input" maxlength="100" name="content"
                                                   placeholder="댓글을 입력해 주세요" type="text">
                                        </label>
                                        <input class="--object-button" type="submit" value="수정">
                                    </form>
                                </div>
                            </div>

                            <form class="reply-form" id="replyForm">
                                <div class="head">
                                    <span class="to">@관리자</span>
                                    <a href="#" class="cancel">취소</a>
                                </div>
                                <div class="body">
                                    <label class="label">
                                        <span hidden>답글작성</span>
                                        <input class="--object-input" maxlength="100" name="content"
                                               placeholder="답글을 입력해 주세요" type="text">
                                    </label>
                                    <input class="--object-button" type="submit" value="작성">
                                </div>
                            </form>

                            <div class="comment sub">
                                <div class="head">
                                    <span class="writer">누구</span>
                                    <span class="dt">2022-01-01 00:00:00</span>
                                    <span class="spring"></span>
                                    <span class="action-container">
                                        <a class="action reply" href="#">답글달기</a>
                                        <a class="action delete" href="#">삭제</a>
                                </span>
                                </div>
                                <div class="body">zz</div>
                            </div>


                            <div class="comment sub mine">
                                <div class="head">
                                    <span class="writer">누구</span>
                                    <span class="dt">2022-01-01 00:00:00</span>
                                    <span class="spring"></span>
                                    <span class="action-container">
                                    <a class="action reply" href="#">답글달기</a>
                                    <a class="action delete" href="#">삭제</a>
                                    </span>
                                </div>
                                <div class="body">ss</div>
                            </div>

                        </div>
                    </td>
                </tr>
            </tfoot>
        </table>
    </div>
</main>
<th:block th:replace="~{fragments/body :: footer}"></th:block>
</body>
</html>

 

 

read.css

@charset "UTF-8";

body > .main > .content {
    margin: 5rem 0;

    align-items: stretch;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
}

body > .main > .content > .title {
    font-size: 2rem;
    font-weight: 500;
    margin-bottom: 2.5rem;
}

body > .main > .content > .table > thead td,
body > .main > .content > .table > thead th {
    padding: 0.5rem 0.75rem;
    vertical-align: middle;
}

body > .main > .content > .table > thead td {
    padding: 0.5rem;
}

body > .main > .content > .table th {
    width: 0;
    background-color: rgb(255, 255, 255);
    border-radius: 0.5rem;
    color: rgb(128, 139, 150);
    font-weight: 400;
    text-align: center;
    white-space: nowrap;
}

body > .main > .content > .table .content-container {
    background-color: rgb(255, 255, 255);
    border-radius: 0.5rem;
    margin: 0.75rem 0;
    padding: 1rem 2rem;
}

body > .main > .content > .table .button-container {
    align-items: stretch;
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
}

body > .main > .content > .table .button-container > * + * {
    margin-left: 0.5rem;
}

body > .main > .content > .table .button-container > .spring {
    flex: 1;
}

body > .main > .content > .table .button-container > a[href] {
    text-decoration: none;
}

body > .main > .content > .table .button-container > a[href].modify {
    background-color: rgb(40, 117, 225);
}

body > .main > .content > .table .button-container > a[href].delete {
    background-color: rgb(231, 76, 60);
}

#commentForm {
    margin-top: 0.5rem;

    align-items: stretch;
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
}

#commentForm > .label {
    flex: 1;
    margin-right: 0.5rem;
}

/*댓글 전체 간격 margin-top*/
#commentContainer {
    margin-top: 0.75rem;

    align-items: stretch;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
}

/*댓글들 사이사이 간격 margin-top*/
#commentContainer > * + * {
    margin-top: 0.5rem;
}

/*댓글창 각각의 모양*/
#commentContainer .comment {
    border-radius: 0.375rem;
    box-shadow: 0 0 0.5rem 0.0625rem rgba(0, 0, 0, 10%);
    overflow: hidden;

    align-items: stretch;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
}

/*대댓글 오른쪽으로 들어가게 하기*/
#commentContainer .comment.sub {
    margin-left: 5rem;
}

/*댓글의 타이틀 모양*/
#commentContainer .comment > .head {
    background-color: rgb(128, 139, 150);
    color: rgb(255, 255, 255);
    font-size: 0.9rem;
    padding: 0.375rem 1rem;

    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
}

#commentContainer .comment.mine > .head {
    background-color: rgb(40, 117, 225);
}

/*작성일시*/
#commentContainer .comment > .head > .dt {
    color: rgb(213, 216, 220);
    margin-left: 0.5rem;
}

/*답글달기, 삭제 오른쪽으로 보내기*/
#commentContainer .comment > .head > .spring {
    flex: 1;
}

/*답글달기, 삭제*/
#commentContainer .comment > .head > .action-container {
    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
}

#commentContainer .comment > .head > .action-container > .action {
    color: inherit;
    text-decoration: none;
}

#commentContainer .comment > .head > .action-container > .action.cancel {
    disply: none;
}


#commentContainer .comment.modifying > .head > .action-container > .action.cancel {
    display: inline-block;
}

/*답글달기, 삭제 글자 사이 간격*/
#commentContainer .comment > .head > .action-container > .action + .action {
    margin-left: 0.5rem;
}


#commentContainer .comment.modifying > .head > .action-container > .action.reply,
#commentContainer .comment.modifying > .head > .action-container > .action.modify,
#commentContainer .comment.modifying > .head > .action-container > .action.delete {
    display: none;
}

/*답글달기, 삭제 글자 hover시 */
#commentContainer .comment > .head > .action-container > .action:hover {
    text-decoration: underline;
}

/*댓글입력칸 공간 크기 조절*/
#commentContainer .comment > .body {
    background-color: rgb(255, 255, 255);
    font-size: 1rem;
    padding: 0.75rem 1rem;

    align-items: stretch;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
}

#commentContainer .comment > .body > .content {
    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
}

#commentContainer .comment.modifying > .body > .content {
    display: none;
}

#commentContainer .comment > .body > .content > .text {
    flex: 1;
}

#commentContainer .comment > .body > .content > .like {
    align-items: center;
    display: flex;
    flex-direction: column;
    justify-content: center;
}

#commentContainer .comment > .body > .content > .like > .count {
    color: rgb(128, 139, 150);
    font-size: 0.8rem;
    line-height: 100%;

}

#commentContainer .comment > .body > .modify-form {
    align-items: stretch;
    display: none;
    flex-direction: row;
    justify-content: flex-start;
}

#commentContainer .comment.modifying > .body > .modify-form {
    display: flex;
}

#commentContainer .comment > .body > .modify-form > .label {
    flex: 1;
    margin-right: 0.5rem;
}


#commentContainer .comment > .body > .content > .like > .toggle {
    width: 1.75rem;
    height: 1.75rem;
    border: 0.125rem solid rgb(171, 178, 185);
    border-radius: 0.375rem;
    color: rgb(171, 178, 185);
    margin-bottom: 0.5rem;
    text-decoration: none;

    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: center;
}

#commentContainer .comment > .body > .content > .like > .toggle:hover {
    border: 0.125rem solid rgb(128, 139, 150);
    color: rgb(128, 139, 150);
}

#commentContainer .comment.liked > .body > .content > .like > .toggle {
    background-color: rgb(231, 76, 60);
    border: 0.125rem solid rgb(241, 62, 22);
    color: rgb(220, 213, 215);
}

#commentContainer .comment.liked > .body > .content > .like > .toggle:hover {
    background-color: rgb(203, 67, 53);
    border: 0.125rem solid rgb(203, 67, 53);
}


/*대댓글창 크기*/
#commentContainer .reply-form {
    border-radius: 0.375rem;
    box-shadow: 0 0 0.5rem 0.0625rem rgba(0, 0, 0, 10%);
    overflow: hidden;
    margin-left: 5rem;


    align-items: stretch;
    display: none;
    flex-direction: row;
    justify-content: flex-start;
}

#commentContainer .reply-form.visible {
    display: inline-block;
}

/*대댓글창 머리부분*/
#commentContainer .reply-form > .head {
    background-color: rgb(13, 77, 168);
    color: rgb(255, 255, 255);
    font-size: 0.9rem;
    padding: 0.375rem 1rem;

    align-items: center;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
}

#commentContainer .reply-form > .body {
    background-color: rgb(255, 255, 255);
    padding: 0.75rem 1rem;

    align-items: stretch;
    display: flex;
    flex-direction: row;
    justify-content: flex-start;
}

#commentContainer .reply-form > .body > .label {
    flex: 1;
    margin-right: 0.5rem;
}

#commentContainer .reply-form > .head > .cancel {
    color: inherit;
    text-decoration: none;
}

#commentContainer .reply-form > .head > .cancel:hover {
    text-decoration: underline;
}

#commentContainer .reply-form > .label {
    flex: 1;
    margin-right: 0.5rem;
}

 

 

실행결과

로그인이 안된 경우 삭제할 권한이 없다는 알림 창이 뜨고

로그인이 되어 있는 상태에서 자신이 남긴 댓글의 경우 정상적으로 삭제됨을 확인할 수 있다.