댓글 삭제하기 구현
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;
}
실행결과
로그인이 안된 경우 삭제할 권한이 없다는 알림 창이 뜨고
로그인이 되어 있는 상태에서 자신이 남긴 댓글의 경우 정상적으로 삭제됨을 확인할 수 있다.
끝