BackEnd/Spring

[Spring]댓글 등록/삭제/등록

Hojung7 2024. 10. 11. 17:13

 

 REST(REpresentational State Transfer)  API

- 자원(데이터,파일)을 이름(주소)으로 
  구분(representational) 하여
  자원의 상태(State)를 주고 받는 것(Transfer)

 -> 자원의 이름(주소)를 명시하고
   HTTP Method(GET,POST,PUT,DELETE) 를 이용해
   지정된 자원에 대한 CRUD 진행

  자원의 이름(주소)는 하나만 지정 (ex. /comment)
  

POST 삽입(Create)
GET 조회(Read)
PUT 수정(Update)
DELETE 삭제(Delete)


[Comment.html]

<div id="commentArea">
	<!-- 댓글 목록 -->
	<div class="comment-list-area">

		<ul id="commentList" th:fragment="comment-list">

			<!-- 대댓글(자식)인 경우 child-comment 클래스 추가 -->
			<li class="comment-row" 
					th:each="comment : ${board.commentList} " 
					th:classappend="${comment.parentCommentNo} != 0 ? child-comment"
					th:object="${comment}"
					th:id="|c*{commentNo}|"
					th:data-comment-no="*{commentNo}">

				<th:block th:if="*{commentDelFl} == 'Y'">
					삭제된 댓글 입니다
				</th:block>

				<th:block th:if="*{commentDelFl} == 'N'">
					<p class="comment-writer">
						<!-- 프로필 이미지 없을 경우 -->
						<img th:unless="*{profileImg}" th:src="#{user.default.image}">
						<!-- 프로필 이미지 있을 경우 -->
						<img th:if="*{profileImg}" th:src="*{profileImg}">
	
						<span th:text="*{memberNickname}">닉네임</span>
						<span class="comment-date" th:text="*{commentWriteDate}">작성일</span>
					</p>
	
					<p class="comment-content" th:text="*{commentContent}">댓글 내용</p>
	
					<!-- 버튼 영역 -->
					<div class="comment-btn-area">
						<button class="child-comment-btn">답글</button>
	
						<th:block th:if="${session.loginMember != null and session.loginMember.memberNo == comment.memberNo}">
							<button class="update-comment-btn">수정</button>
							<button class="delete-comment-btn">삭제</button>
						</th:block>
						<!-- 로그인 회원과 댓글 작성자가 같은 경우 -->
	
					</div>
				</th:block>

			</li>

		</ul>
	</div>


	<!-- 댓글 작성 부분 -->
	<div class="comment-write-area">
		<textarea id="commentContent"></textarea>
		<button id="addComment">
			댓글<br>
			등록
		</button>
	</div>

</div>

 

 

1. 댓글 등록

 

[comment.js]

// 댓글 목록이 출력되는 영역(ul을 감싸는 div)
const commentListArea = document.querySelector(".comment-list-area");

/** 댓글 목록 조회 함수(ajax) */
const selectCommentList = () => {

  // boardNo : 게시글 번호(boardDetail.js 전역 변수)
  fetch("/board/commentList?boardNo=" + boardNo)
    .then(response => {
      // response.ok : HTTP 응답 상태 코드가 200번대(성공)이면 true
      if (response.ok) return response.text();
      throw new Error("댓글 목록 조회 실패")
    })
    .then(html => {
      // 매개 변수 html : 타임리프가 해석되어 완성된 html 코드
      // console.log(html); 

      // 타임리프가 해석된 html 코드를
      // .comment-list-area의 내용으로 대입 후 HTML 코드 해석
      commentListArea.innerHTML = html;

      /* [주의 사항] */
      // innerHTML로 새로 만들어진 요소에는 
      // 이벤트 리스너가 추가되어 있지 않기 때문에
      // 답글, 수정, 삭제 등이 동작하지 않는다!!!

      addEventChildComment();  // 답글 버튼에 클릭 이벤트 추가
      addEventDeleteComment(); // 삭제 버튼에 이벤트 추가
      addEventUpdateComment(); // 수정 버튼에 이벤트 추가
      
    })
    .catch(err => console.error(err));
};

// ---------------------------------------------------------

// 댓글 내용 요소
const commentContent = document.querySelector("#commentContent");

/** 댓글 등록 함수(AJAX)
 * @param parentCommentNo : 부모 댓글 번호(없음 undefined)
 */
const insertComment = (parentCommentNo) => {

  // 서버에 제출할 값을 저장할 객체
  const data = {};
  data.boardNo = boardNo; // 댓글이 작성된 게시글 번호
  data.commentContent = commentContent.value; // 작성된 댓글 내용

  // 매개 변수로 전달 받은 부모 댓글 번호가 있다면
  // == 답글
  if (parentCommentNo !== undefined) {
    data.parentCommentNo = parentCommentNo;

    // 답글에 작성된 내용 얻어오기
    data.commentContent =
      document.querySelector(".child-comment-content").value;
  }

  // Ajax
  fetch("/comment", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data) // JS 객체 -> JSON (문자열)
  })
    .then(response => {
      if (response.ok) return response.text();
      throw new Error("댓글 등록 실패");
    })
    .then(commentNo => {

      if (commentNo == 0) { // 등록 실패
        alert("댓글 등록 실패");
        return;
      }

      alert("댓글이 등록 되었습니다.");
      commentContent.value = ""; // textarea에 작성한 댓글 내용 삭제
      selectCommentList(); // 댓글 목록 비동기 조회 후 출력

    })
    .catch(err => console.error(err));
}

 

[Controller]

@RestController 	// @Controller + @ResponseBody
													// 비동기 요청 처리 정용 컨트롤러
													// return되는 모든 값을 있는 그대로 호출부로 반환

@RequiredArgsConstructor
@Slf4j
public class CommentController {

	private final CommentService service;
	
	/** 댓글 등록
	 * @param comment :
	 * 요청 시 body에 JSON 형태로 담겨져 제출된 데이터를
	 * HttpMessageConverter가 DTO로 변환한 객체
	 * (boardNo, commentContent, parentCommentNo)
	 * 
	 * @param loginMember : 로그인한 회원 정보
	 * @return commentNo : 삽입된 댓글 번호
	 */

	@PostMapping("comment") // POST == CREATE/INSERT의미
	public int commentInsert(
			@RequestBody Comment comment,
			@SessionAttribute("loginMember") Member loginMember) {
		
		// 로그인한 회원 번호를 comment에 세팅
		comment.setMemberNo(loginMember.getMemberNo());
		
	
		
		return service.commentInsert(comment);
	}

 

[Service]

public interface CommentService {

	/** 댓글 등록
	 * @param comment
	 * @return commentNo
	 */
	int commentInsert(Comment comment);

 

[ServiceImple]

@Service
@Transactional
@RequiredArgsConstructor
public class CommentServiceImpl implements CommentService {

	private final CommentMapper mapper;

	// 댓글 등록
	@Override
	public int commentInsert(Comment comment) {
		
		int result = mapper.commentInsert(comment);
		
		// 삽입 성공 시 댓글 번호 반환
		if(result > 0) return comment.getCommentNo();
		
		// 실패 시 0
		return 0;
	}

 

[Mapper]

@Mapper
public interface CommentMapper {

	/** 댓글 등록
	 * @param comment
	 * @return result
	 */
	int commentInsert(Comment comment);

 

[xml]

<!-- useGeneratedKeys="true"
		: DB에서 생성된 key(시퀀스) 값을
		  자바에서도 사용할 수 있게 하는 속성
	 -->

	<!-- 댓글 등록 -->
	<insert id="commentInsert" 
			parameterType="Comment"
			useGeneratedKeys="true">
	
		<selectKey order="BEFORE" resultType="_int"
				   keyProperty="commentNo">
			SELECT SEQ_COMMENT_NO.NEXTVAL FROM DUAL
		</selectKey>
		
		INSERT INTO "COMMENT"
		VALUES(
			#{commentNo}, 
		   	#{commentContent},
		   	DEFAULT,
		   	DEFAULT,
		   	#{memberNo},
		   	#{boardNo},
		   	
		   	<!--부모 댓글 -->
		   	<if test="parentCommentNo == 0">
		   	NULL
		   	</if>
		   	
		   	<!--자식 댓글 -->
		   	<if test="parentCommentNo != 0">
		   		#{parentCommentNo}
		   	</if>
		   	)
	
	</insert>

 

 

/** 답글 버튼 클릭 시 
  해당 댓글에 답글 작성 영역을 추가하는 함수 
  @param btn : 클릭된 답글 버튼
*/
const showChildComment = (btn) => {

  /* 로그인이 되어 있지 않은 경우 */
  if (loginCheck === false) {
    alert("로그인 후 이용해 주세요");
    return;
  }


  // ** 답글 작성 textarea가 한 개만 열릴 수 있도록 만들기 **
  const temp = document.getElementsByClassName("child-comment-content");

  if (temp.length > 0) { // 답글 작성 textara가 이미 화면에 존재하는 경우

    if (confirm("다른 답글을 작성 중입니다. 현재 댓글에 답글을 작성 하시겠습니까?")) {
      temp[0].nextElementSibling.remove(); // 버튼 영역부터 삭제
      temp[0].remove(); // textara 삭제 (기준점은 마지막에 삭제해야 된다!)

    } else {
      return; // 함수를 종료시켜 답글이 생성되지 않게함.
    }
  }





  // 클릭된 답글 버튼이 속해있는 댓글(li) 요소 찾기
  // closest("태그") : 부모 중 가장 가까운 태그 찾기 
  const li = btn.closest("li");

  // 답글이 작성되는 댓글(부모 댓글) 번호 얻어오기
  const parentCommentNo = li.dataset.commentNo;

  // 답글을 작성할 textarea 요소 생성
  const textarea = document.createElement("textarea");
  textarea.classList.add("child-comment-content");

  li.append(textarea);

  // 답글 버튼 영역 + 등록/취소 버튼 생성 및 추가
  const commentBtnArea = document.createElement("div");
  commentBtnArea.classList.add("comment-btn-area");

  const insertBtn = document.createElement("button");
  insertBtn.innerText = "등록";

  /* 등록 버튼 클릭 시 댓글 등록 함수 호출(부모 댓글 번호 전달)  */
  insertBtn.addEventListener("click", () => insertComment(parentCommentNo));

  const cancelBtn = document.createElement("button");
  cancelBtn.innerText = "취소";

  /* 취소 버튼 클릭 시 답글 작성 화면 삭제 */
  cancelBtn.addEventListener("click", () => {

    // console.log(li.lastElementChild);
    li.lastElementChild.remove();
    li.lastElementChild.remove();
  });

  // 답글 버튼 영역의 자식으로 등록/취소 버튼 추가
  commentBtnArea.append(insertBtn, cancelBtn);

  // 답글 버튼 영역을 화면에 추가된 textarea 뒤쪽에 추가
  textarea.after(commentBtnArea);
}

 

2. 댓글 삭제

[comment.js]

/** 댓글 삭제 함수(ajax)
 * @param btn : 삭제 버튼
 */
const deleteComment = (btn) => {

  // confirm() 취소 시
  if (confirm("정말 삭제 하시겠습니까?") === false) {
    return;
  }

  // 삭제할 댓글 번호 얻어오기
  const li = btn.closest("li"); // 댓글 또는 답글
  const commentNo = li.dataset.commentNo; // 댓글 번호

  fetch("/comment", {
    method: "DELETE",
    headers: { "Content-Type": "application/json" },
    body: commentNo
  })
    .then(response => {
      if (response.ok) return response.text();
      throw new Error("댓글 삭제 실패")
    })
    .then(result => {
      if (result > 0) {
        alert("삭제 되었습니다");
        selectCommentList(); // 댓글 목록 비동기 조회 후 출력
      }
      else {
        alert("삭제 실패");
      }
    })
    .catch(err => console.error(err));
}

/* 백업된 댓글을 저장할 변수 */
let beforeCommentRow;

[commentController]

/** 댓글 삭제
 * @param commentNo : 삭제하려는 댓글 번호
 * @param loginMember : 로그인한 회원 정보
 * @return result
 */
	@DeleteMapping("comment") // DELETE == DELETE 의미
	public int commentDelete(
			@RequestBody int commentNo,
			@SessionAttribute("loginMember") Member loginMember){
		return service.commentDelete(commentNo, loginMember.getMemberNo());
	}

 

[Service]

	/** 댓글 삭제
	 * @param commentNo
	 * @param memberNo
	 * @return
	 */
	int commentDelete(int commentNo, int memberNo);

 

[ServiceImpl]

	// 댓글 삭제
	@Override
	public int commentDelete(int commentNo, int memberNo) {

		return  mapper.commentDelete(commentNo,memberNo);
	}

 

[Mapper]

	/** 댓글 삭제
	 * @param commentNo
	 * @param memberNo
	 * @return
	 */
	int commentDelete(@Param("commentNo") int commentNo,
															@Param("memberNo") int memberNo);

 

[xml]

	<!-- 댓글 삭제(상태 값 변경)  -->
	<update id="commentDelete">
	UPDATE "COMMENT"
	SET
		COMMENT_DEL_FL = 'Y'
		WHERE 
		COMMENT_NO = #{commentNo}
	AND
		 MEMBER_NO = #{memberNo}
	</update>

 

3. 댓글 수정

 

[comment.js]

/* 백업된 댓글을 저장할 변수 */
let beforeCommentRow;

/** 댓글 수정 화면으로 전환
 * @param btn : 수정 버튼
 */
const showUpdateComment = (btn) => {

  /* 댓글 수정 화면이 1개만 열려 있을 수 있게 하기 */
  // == 이미 열려있는 수정 화면이 있으면 닫아버리기
  const temp = document.querySelector(".update-textarea");

  if(temp != null){ // 이미 열려있는 수정 화면이 있을 경우

    if(confirm("수정 중인 댓글이 있습니다. " 
              + "현재 댓글을 수정 하시겠습니까?") === true){
                
      const commentRow = temp.parentElement; // 열려있는 댓글 행
      commentRow.after(beforeCommentRow); // 백업본을 다음 요소로 추가
      commentRow.remove(); // 열려있던 행 삭제
      
      // 백업본 버튼에 이벤트 추가

      const childeCommentBtn = beforeCommentRow.querySelector(".child-comment-btn");
      const updateCommentBtn = beforeCommentRow.querySelector(".update-comment-btn");
      const deleteCommentBtn = beforeCommentRow.querySelector(".delete-comment-btn");

      childeCommentBtn.addEventListener("click", () => showChildComment(childeCommentBtn));
      updateCommentBtn.addEventListener("click", () => showUpdateComment(updateCommentBtn));
      deleteCommentBtn.addEventListener("click", () => deleteComment(deleteCommentBtn));

    }else{
      return;
    }

  }



  // 1. 수정하려는 댓글(li) 요소 얻어오기
  const commentRow = btn.closest("li");
  const commentNo = commentRow.dataset.commentNo; // 댓글 번호

  // 2. 취소 버튼 동작에 대비하여
  //    현재 댓글(commentRow)의 요소를 복제해서 백업
  beforeCommentRow = commentRow.cloneNode(true);

  /* 요소.cloneNode(true);
    - 요소 복제하여 반환
    - 매개 변수 true : 복제하려는 요소의 하위 요소들도 복제
   */

  // 3. 기존 댓글에 작성된 내용만 얻어오기
  let beforeContent = commentRow.children[1].innerText;

  // 4. 댓글 행 내부를 모두 삭제
  commentRow.innerHTML = "";

  // 5. textarea 생성 + 클래스 추가 + 내용 추가
  const textarea = document.createElement("textarea");
  textarea.classList.add("update-textarea");
  textarea.value = beforeContent;

  // 6. 댓글 행에 textarea 추가
  commentRow.append(textarea);

  // 7. 버튼 영역 생성
  const commentBtnArea = document.createElement("div");
  commentBtnArea.classList.add("comment-btn-area");

  // 8. 수정 버튼 생성
  const updateBtn = document.createElement("button");
  updateBtn.innerText = "수정";

  // 수정 버튼 클릭 시 댓글 수정(ajax)
  updateBtn.addEventListener("click", () => {
    const data = {
      "commentNo" : commentNo,
      "commentContent" : textarea.value

    }
    fetch("/comment",{
      method : "PUT",
      headers : {"Content-Type" : "application/json"},
      body : JSON.stringify(data)

    })
    .then(response => {
      if(response.ok) return response.text();
      throw new Error("댓글 수정 실패");
    })
    .then(result=> {
      if(result > 0){
        alert("댓글이 수정되었습니다");
        selectCommentList();
      }else{
        alert("댓글 수정 실패")
      }
    })
    .catch(err=> console.error(err));
  })


  // 9. 취소 버튼 생성
  const cancelBtn = document.createElement("button");
  cancelBtn.innerText = "취소";

  cancelBtn.addEventListener("click", () => {

    // 취소 안함 -> 수정 계속 진행
    if(confirm("취소 하시겠습니까?") === false) return;

    // 현재 댓글 행 다음 위치에 백업한 원본 댓글 추가
    commentRow.after(beforeCommentRow);
    commentRow.remove(); // 수정 화면으로 변환된 행 삭제

    /* 원상 복구된 댓글의 버튼에 이벤트 추가하기 */
    const childCommentBtn 
      = beforeCommentRow.querySelector(".child-comment-btn");

    childCommentBtn.addEventListener("click", () => {
      showChildComment(childCommentBtn);
    });


    const updateCommentBtn 
      = beforeCommentRow.querySelector(".update-comment-btn");

    updateCommentBtn.addEventListener("click", () => {
      showUpdateComment(updateCommentBtn);
    });


    const deleteCommentBtn 
      = beforeCommentRow.querySelector(".delete-comment-btn");

    deleteCommentBtn.addEventListener("click", () => {
      deleteComment(deleteCommentBtn);
    })


  })


  // 10. 버튼 영역에 수정/취소 버튼 추가 후
  //     댓글 행에 버튼 영역 추가
  commentBtnArea.append(updateBtn, cancelBtn);
  commentRow.append(commentBtnArea);

}

 

[Controller]

/** 댓글 수정
 * @return
 */
	@PutMapping("comment") // PUT == UPDATE 의미
	public int commentUpdate(
		@RequestBody Comment comment,
		@SessionAttribute("loginMember")Member loginMember) {
		comment.setMemberNo(loginMember.getMemberNo());
		 return service.commentUpdate(comment);
	}

 

[Service]

	/** 댓글 수정
	 * @param comment
	 * @return
	 */
	int commentUpdate(Comment comment);

 

[ServiceImpl]

	// 댓글 수정
	@Override
	public int commentUpdate(Comment comment) {
		// TODO Auto-generated method stub
		return mapper.commentUpdate(comment);
	}

 

[mapper]

	/** 댓글 수정
	 * @param comment
	 * @return
	 */
	int commentUpdate(Comment comment);

 

[xml]

<!-- 댓글 수정 -->
<update id="commentUpdate">
UPDATE "COMMENT"
 SET
  COMMENT_CONTENT = #{commentContent}
  WHERE 
  COMMENT_NO = #{commentNo}
  AND
  MEMBER_NO = #{memberNo}

</update>

 

 


 

이벤트 추가 구문

/* 댓글 등록 버튼 클릭 동작 추가 */
const addComment = document.querySelector("#addComment");
addComment.addEventListener("click", () => {

  // 1) 로그인 여부 검사(boardDetail.html의 loginCheck 전역변수)
  if (loginCheck === false) {
    alert("로그인 후 이용해 주세요");
    return;
  }

  // 2) 댓글 작성 여부 검사
  if (commentContent.value.trim().length === 0) {
    alert("내용 작성 후 등록 버튼을 클릭해 주세요");
    return;
  }

  // 3) 1,2 통과 시 댓글 등록 함수 호출
  insertComment();
})


/* 화면에 존재 하는 답글 버튼을 모두 찾아 이벤트 리스너 추가 */
const addEventChildComment = () => {
  const btns = document.querySelectorAll(".child-comment-btn");

  btns.forEach(btn => {
    btn.addEventListener("click", () => {
      showChildComment(btn); // 답글 작성 화면 출력 함수 호출
    });
  })
}

/** 화면에 존재하는 모든 댓글 삭제 버튼에 
 *  이벤트 리스너 추가하는 함수
 */
const addEventDeleteComment = () => {
  const btns = document.querySelectorAll(".delete-comment-btn");

  btns.forEach(btn => {
    btn.addEventListener("click", () => {
      deleteComment(btn); // 클릭 시 deleteComment() 함수 호출
    })
  });
}


/** 화면에 존재하는 댓글 수정 버튼에 이벤트 리스너 추가 
 */
const addEventUpdateComment = () => {
  const btns = document.querySelectorAll(".update-comment-btn");

  btns.forEach(btn => {
    btn.addEventListener("click", () => {
      showUpdateComment(btn);
      // 수정 버튼 클릭 시 showUpdateComment() 호출
    })
  })
}





/* 화면 코드 해석 완료 후*/
document.addEventListener("DOMContentLoaded", () => {
  addEventChildComment();  // 답글 버튼에 이벤트 추가
  addEventDeleteComment(); // 삭제 버튼에 이벤트 추가
  addEventUpdateComment(); // 수정 버튼에 이벤트 추가
});