댓글 crud
🔑 매개변수 형태
<button>수정</button>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<div id="reply-area">
<!-- 댓글 목록 -->
<div class="reply-list-area">
<ul id="reply-list">
<c:forEach var="reply" items="${rList}">
<li class="reply-row">
<p class="reply-writer">
<c:if test="${empty reply.profileImage}">
<!-- 프로필 이미지가 없을 경우 -->
<img src="${contextPath}/resources/images/user.png">
</c:if>
<c:if test="${!empty reply.profileImage}">
<!-- 프로필 이미지가 있을 경우 -->
<img src="${contextPath}${reply.profileImage}">
</c:if>
<span>${reply.memberNickname}</span>
<span class="reply-date"> (${reply.createDate})</span>
</p>
<p class="reply-content">${reply.replyContent}</p>
<c:if test="${loginMember.memberNo == reply.memberNo}">
<div class="reply-btn-area">
<button onclick="showUpdateReply(${reply.replyNo}, this)">수정</button>
<button onclick="deleteReply(${reply.replyNo})">삭제</button>
</div>
</c:if>
</li>
</c:forEach>
</ul>
</div>
<!-- 댓글 작성 부분 -->
<div class="reply-write-area">
<textarea id="replyContent"></textarea> <!-- 스크롤 안 생기려면 딱 붙여서 쓰기!! -->
<button id="addReply">
댓글 <br>
등록
</button>
</div>
</div>
🔑 댓글 작성은 비동기(ajax)로 진행 됨
--> 따로 함수 생성해서 동적인 화면을 만들어야 한다!!
🔑 댓글 수정 시 이전 댓글의 내용을 DB에서 가져오는 것이 아닌, jsp에서 바로 가져온다
// 댓글 목록 조회(AJAX)
function selectReplyList(){
// contextPath, boardNo, memberNo 전역변수 사용
$.ajax({
url : contextPath + "/reply/selectReplyList",
data : {"boardNo" : boardNo},
type : "get",
dataType : "JSON", // JSON 형태의 문자열 응답 데이터를 JS 객체로 자동 변환
success : function(rList){
// rList : 반환받은 댓글 목록
console.log(rList);
// 화면에 출력되어 있는 댓글 목록 삭제
const replyList = document.getElementById("reply-list"); // ul 태그
replyList.innerHTML="";
// rList에 저장된 요소를 하나씩 접근
for(let reply of rList){
// 행
const replyRow = document.createElement("li");
replyRow.classList.add("reply-row");
// 작성자
const replyWriter = document.createElement("p");
replyWriter.classList.add("reply-writer");
// 프로필 이미지
const profileImage = document.createElement("img");
if(reply.profileImage != null){ // 프로필 이미지가 있는 경우
profileImage.setAttribute("src", contextPath + reply.profileImage);
} else { // 없는 경우 == 기본 이미지
profileImage.setAttribute("src", contextPath + "/resources/images/user.png");
}
// 작성자 닉네임
const memberNickname = document.createElement("span");
memberNickname.innerText = reply.memberNickname;
// 작성일
const replyDate = document.createElement("span");
replyDate.classList.add("reply-date");
replyDate.innerText = "(" + reply.createDate + ")";
// 작성자 영역(p)에 프로필, 닉네임, 작성일을 마지막 자식으로 추가
replyWriter.append(profileImage, memberNickname, replyDate);
// 댓글 내용
const replyContent = document.createElement("p");
replyContent.classList.add("reply-content");
// 왜 innerHTML? <br> 태그 인식을 위해서
replyContent.innerHTML = reply.replyContent;
// 행에 작성자, 내용 추가
replyRow.append(replyWriter, replyContent);
// 로그인한 회원 번호와 댓글 작성자의 회원번호가 같을 때만 버튼 추가
if(loginMemberNo == reply.memberNo){
// 버튼 영역
const replyBtnArea = document.createElement("div");
replyBtnArea.classList.add("reply-btn-area");
// 수정 버튼
const updateBtn = document.createElement("button");
updateBtn.innerText="수정";
// 수정 버튼에 onclick 이벤트 속성 추가
updateBtn.setAttribute("onclick", "showUpdateReply(" + reply.replyNo + ", this)");
// 삭제 버튼
const deleteBtn = document.createElement("button");
deleteBtn.innerText="삭제";
// 삭제 버튼에 onclick 이벤트 속성 추가
deleteBtn.setAttribute("onclick", "deleteReply("+ reply.replyNo +")")
// 버튼 영역 마지막 자식으로 수정/삭제 버튼 추가
replyBtnArea.append(updateBtn, deleteBtn);
// 행에 버튼 영역 추가
replyRow.append(replyBtnArea);
}
// 댓글 목록(ul)에 행(li)추가
replyList.append(replyRow);
}
},
error : function(){
console.log("오류발생");
}
});
}
//----------------------------------------------------------------------------
// 댓글 등록
const addReply = document.getElementById("addReply");
const replyContent = document.getElementById("replyContent");
addReply.addEventListener("click", function(){ // 댓글 등록 버튼이 클릭이 되었을 때
// 1) 로그인이 되어있나? -> 전역변수인 loginMemberNo 이용
if(loginMemberNo == ""){ // 로그인 X
alert("로그인 후 이용해주세요");
return;
}
// 2) 댓글 내용이 작성되어 있나?
if(replyContent.value.trim().length == 0){ // 미작성인 경우
alert("댓글을 작성한 후 버튼을 클릭해주세요");
replyContent.value=""; // 띄어쓰기, 개행문자 제거
replyContent.focus();
return;
}
// 3) AJAX를 이용해서 댓글 내용 DB에 저장(INSERT)
$.ajax({
url : contextPath + "/reply/insert",
data : {
"replyContent" : replyContent.value,
"memberNo" : loginMemberNo,
"boardNo" : boardNo
},
type : "POST",
success : function(result){
if(result > 0){ // 댓글 등록 성공
alert("댓글이 등록되었습니다");
replyContent.value="";
selectReplyList(); // 비동기 댓글 목록 조회 함수 호출
// -> 새로운 댓글 추가됨
} else { // 실패
alert("댓글 등록에 실패했습니다")
}
},
error : function(req, status, error){
console.log("댓글 등록 실패");
console.log(req.responseText);
}
});
})
//---------------------------------------------------------------------------
// 댓글 삭제
function deleteReply(replyNo){
if(confirm("정말 삭제하시겠습니까?")){
// 요청 주소 : /community/reply/delete
// 파라미터 : key : "replyNo", value : 매개변수 replyNo
// 전달방식 : "GET"
// success : 삭제 성공 시 -> " 삭제되었습니다." alert로 출력 후
// 댓글 목록 비동기 조회 함수 호출
// 삭제 실패 시 -> "삭제 실패" alert로 출력
// error : 앞 error 코드 참고
// DB에서 댓글 삭제 ==> REPLY_ST = 'Y' 변경
$.ajax({
url : contextPath + "/reply/delete",
data : {"replyNo" : replyNo},
success : function(result){
if(result>0){
alert("삭제되었습니다.");
selectReplyList();
} else {
alert("삭제 실패");
}
},
error : function(req, status, error){
console.log("댓글 삭제 실패");
console.log(req.responseText);
}
})
}
}
// --------------------------------------------------------------------------
// 댓글 수정 화면 전환
let beforeReplyRow; // 수정 전 원래 행의 상태를 저장할 변수
function showUpdateReply(replyNo, btn){
// 댓글 번호, 이벤트발생요소(수정버튼)
// ** 댓글 수정이 한 개만 열릴 수 있도록 만들기 **
const temp = document.getElementsByClassName("update-textarea");
if(temp.length>0){ // 수정이 한 개 이상 열려 있는 경우
if(confirm("다른 댓글이 수정 중입니다. 현재 댓글을 수정하시겠습니까?")){
temp[0].parentElement.innerHTML = beforeReplyRow;
// replyRow 백업한 댓글
// 백업 내용으로 덮어씌우면서 textarea가 사라짐
} else { // 취소
return;
}
}
// 1. 댓글 수정이 클릭된 행을 선택
const replyRow = btn.parentElement.parentElement; // 수정 버튼의 부모의 부모
// 2. 행 내용 삭제 전 현재 상태를 저장(백업)(문자열)
// (전역변수 이용)
beforeReplyRow = replyRow.innerHTML;
//console.log(beforeReplyRow);
// 취소 버튼 동작 코드
// replyRow.innerHTML = beforeReplyRow;
// 3. 댓글에 작성되어 있던 내용만 얻어오기 -> 새롭게 생성된 textarea 추가될 예정
console.log(replyRow);
//console.log(replyRow.children);
//console.log(replyRow.children[1].innerHTML); // <br>태그 유지를 위해 innerHTML
let beforeContent = replyRow.children[1].innerHTML;
// 이것도 가능(참고)
// let beforeContent = btn.parentElement.previousElementSibling.innerHTML;
// 4. 댓글 행 내부 내용을 모두 삭제
replyRow.innerHTML = "";
// 5. textArea 요소 생성 + 클래스 추가 + **내용추가**
const textarea = document.createElement("textarea");
textarea.classList.add("update-textarea");
// *******************************************************
// XSS 방지 처리 해제
beforeContent = beforeContent.replaceAll("&", "&");
beforeContent = beforeContent.replaceAll("<", "<");
beforeContent = beforeContent.replaceAll(">", ">");
beforeContent = beforeContent.replaceAll(""", "\"");
// 개행문자 처리 해제
beforeContent = beforeContent.replaceAll("<br>", "\n")
// *******************************************************
textarea.value = beforeContent; // 내용 추가
// 6. replyRow에 생성된 textarea 추가
replyRow.append(textarea);
// 7. 버튼 영역 + 수정/취소 버튼 생성
const replyBtnArea = document.createElement("div");
replyBtnArea.classList.add("reply-btn-area");
const updateButton = document.createElement("button");
updateButton.innerHTML="수정";
updateButton.setAttribute("onclick", "updateReply("+replyNo+", this)");
const cancelButton = document.createElement("button");
cancelButton.innerHTML="취소";
cancelButton.setAttribute("onclick", "updateCancel(this)");
// 8. 버튼 영역에 버튼 추가 후
// replyRow(행)에 버튼 영역 추가
replyBtnArea.append(updateButton, cancelButton);
replyRow.append(replyBtnArea);
}
//-----------------------------------------------------------------------------------
// 댓글 수정 취소
function updateCancel(btn){
// 매개변수 btn : 클릭된 취소 버튼
// 전역변수 beforeReplyRow : 수정 전 원래 행(댓글)을 저장할 변수
if(confirm("댓글 수정을 취소하시겠습니까?")){
btn.parentElement.parentElement.innerHTML = beforeReplyRow;
}
}
// ----------------------------------------------------------------------------------
// 댓글 수정 (AJAX)
function updateReply(replyNo, btn){
// 새로 작성된 댓글 내용 얻어오기
const replyContent = btn.parentElement.previousElementSibling.value;
console.log(replyContent)
$.ajax({
url : contextPath + "/reply/update",
data : {
"replyNo" : replyNo,
"replyContent" : replyContent,
},
type : "POST",
success : function(result){
if(result > 0) {
alert("댓글이 수정되었습니다.");
selectReplyList();
} else {
alert("댓글 수정 실패");
}
},
error : function(req, status, error){
console.log("댓글 수정 실패");
console.log(req.responseText);
}
})
}
package edu.kh.community.board.controller;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.gson.Gson;
import edu.kh.community.board.model.service.ReplyService;
import edu.kh.community.board.model.vo.Reply;
// Controller : 요청에 따라 알맞은 서비스를 호출하고
// 요청 처리 결과를 내보내줄(응답할) view를 선택
// *** Front Controller 패턴 ***
// 하나의 Servlet이 여러 요청을 받아들이고 제어하는 패턴
@WebServlet("/reply/*") // reply로 시작하는 모든 요청 받음
public class ReplyController extends HttpServlet {
// /reply/selectReplyList
// /reply/insert
// /reply/update
// /reply/delete
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// GET 방식 요청 처리
String uri = req.getRequestURI();
// /community/reply/insert
String contextPath = req.getContextPath();
// /community
String command = uri.substring( (contextPath + "/reply/").length() );
// insert
ReplyService service = new ReplyService();
try {
// 댓글 목록 조회 요청인 경우
if(command.equals("selectReplyList")) {
// 파라미터를 얻어와 정수 형태로 파싱
int boardNo = Integer.parseInt(req.getParameter("boardNo"));
// 댓글 목록 조회 service 호출 후 결과 반환 받기
List<Reply> replyList =service.selectReplyList(boardNo);
// JSON 변환 + 응답
new Gson().toJson(replyList, resp.getWriter());
}
// 댓글 등록
if(command.equals("insert")) {
// 파라미터 얻어오기
String replyContent = req.getParameter("replyContent");
int memberNo = Integer.parseInt(req.getParameter("memberNo"));
int boardNo = Integer.parseInt(req.getParameter("boardNo"));
// Reply 객체 생성해서 파라미터 담기
Reply reply = new Reply();
reply.setReplyContent(replyContent);
reply.setMemberNo(memberNo);
reply.setBoardNo(boardNo);
// 댓글 등록(insert) 서비스 호출 후 결과 반환 받기
int result = service.insertReply(reply);
// 서비스 호출 결과를 그대로 응답 데이터로 내보냄
resp.getWriter().print(result);
}
// 댓글 삭제
if(command.equals("delete")) {
int replyNo = Integer.parseInt(req.getParameter("replyNo"));
int result = service.deleteReply(replyNo);
resp.getWriter().print(result);
}
// 댓글 수정
if(command.equals("update")) {
int replyNo = Integer.parseInt(req.getParameter("replyNo"));
String replyContent = req.getParameter("replyContent");
int result = service.updateReply(replyNo, replyContent);
resp.getWriter().print(result);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// POST 방식 요청 처리
doGet(req, resp); // POST로 전달된 요청을 doGet()으로 전달하여 수행
}
}
package edu.kh.community.board.model.service;
import static edu.kh.community.common.JDBCTemplate.*;
import java.sql.Connection;
import java.util.List;
import edu.kh.community.board.model.dao.ReplyDAO;
import edu.kh.community.board.model.vo.Reply;
import edu.kh.community.common.Util;
public class ReplyService {
private ReplyDAO dao = new ReplyDAO();
/** 댓글 목록 조회 service
* @param boardNo
* @return replyList
* @throws Exception
*/
public List<Reply> selectReplyList(int boardNo) throws Exception{
Connection conn = getConnection();
List<Reply> replyList = dao.selectReplyList(conn, boardNo);
close(conn);
return replyList;
}
/** 댓글 등록 Service
* @param reply
* @return result
* @throws Exception
*/
public int insertReply(Reply reply) throws Exception {
Connection conn = getConnection();
// XSS : 관리자가 아닌 이용자가 악성 스크립트를 삽입해서 공격
// Cross Site Scripting(XSS, 크로스 사이트 스크립팅) 공격 방지 처리
reply.setReplyContent(Util.XSSHandling(reply.getReplyContent()));
// 개행문자 변경처리
// textarea에 줄바꿈 문자 입력 시 \n, \r, \r\n, \n\r 중 하나로 입력(브라우저, OS 따라 다름)
// 이 문자들을 HTML에서 줄바꿈으로 인식할 수 있도록 "<br>"태그로 변경
// reply.getReplyContent().replaceAll("정규표현식", "바꿀 문자열");
// 댓글 등록/수정
// 게시글 등록/수정에서 사용
// reply.setReplyContent(reply.getReplyContent().replaceAll("\n|\r|\r\n|\n\r", "<br>"));
// static으로 선언해둔 개행문자 변경 메소드 사용
reply.setReplyContent(Util.newLineHandling(reply.getReplyContent()));
// 내용을 util 메소드 사용해서 바꾸고 그걸 다시 내용에 세팅하겠다.
int result = dao.insertReply(conn, reply);
if(result>0) conn.commit();
else conn.rollback();
close(conn);
return result;
}
/** 댓글 삭제 service
* @param replyNo
* @return result
* @throws Exception
*/
public int deleteReply(int replyNo) throws Exception {
Connection conn = getConnection();
int result = dao.deleteReply(conn, replyNo);
if(result>0) commit(conn);
else rollback(conn);
close(conn);
return result;
}
/** 댓글 수정 service
* @param replyNo
* @param replyContent
* @return result
* @throws Exception
*/
public int updateReply(int replyNo, String replyContent) throws Exception{
Connection conn = getConnection();
// XSS 처리
replyContent = Util.XSSHandling(replyContent);
// 개행문자 처리
replyContent = Util.newLineHandling(replyContent);
int result = dao.updateReply(conn, replyNo, replyContent);
if(result>0) commit(conn);
else rollback(conn);
close(conn);
return result;
}
}
package edu.kh.community.board.model.dao;
import static edu.kh.community.common.JDBCTemplate.*;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import edu.kh.community.board.model.vo.Reply;
public class ReplyDAO {
private Statement stmt;
private PreparedStatement pstmt;
private ResultSet rs;
private Properties prop;
public ReplyDAO() {
try {
prop = new Properties();
String filePath = ReplyDAO.class.getResource("/edu/kh/community/sql/reply-sql.xml").getPath();
prop.loadFromXML(new FileInputStream(filePath));
} catch (Exception e) {
e.printStackTrace();
}
}
/** 댓글 목록 조회 DAO
* @param conn
* @param boardNo
* @return replyList
* @throws Exception
*/
public List<Reply> selectReplyList(Connection conn, int boardNo) throws Exception {
List<Reply> replyList = new ArrayList<Reply>();
try {
String sql = prop.getProperty("selectReplyList");
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, boardNo);
rs = pstmt.executeQuery();
while(rs.next()) {
Reply r = new Reply();
r.setReplyNo(rs.getInt("REPLY_NO"));
r.setReplyContent(rs.getString(2));
r.setCreateDate(rs.getString(3));
r.setBoardNo(rs.getInt(4));
r.setMemberNo(rs.getInt(5));
r.setMemberNickname(rs.getString(6));
r.setProfileImage(rs.getString(7));
replyList.add(r);
}
} finally {
close(rs);
close(pstmt);
}
return replyList;
}
/** 댓글 등록 DAO
* @param conn
* @param reply
* @return result
* @throws Exception
*/
public int insertReply(Connection conn, Reply reply) throws Exception {
int result = 0;
try {
String sql = prop.getProperty("insertReply");
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, reply.getReplyContent());
pstmt.setInt(2, reply.getMemberNo());
pstmt.setInt(3, reply.getBoardNo());
result = pstmt.executeUpdate();
} finally {
close(pstmt);
}
return result;
}
/** 댓글 삭제 DAO
* @param conn
* @param replyNo
* @return result
* @throws Exception
*/
public int deleteReply(Connection conn, int replyNo) throws Exception{
int result = 0;
try {
String sql = prop.getProperty("deleteReply");
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, replyNo);
result = pstmt.executeUpdate();
} finally {
close(pstmt);
}
return result;
}
/** 댓글 수정 DAO
* @param conn
* @param replyNo
* @param replyContent
* @return result
* @throws Exception
*/
public int updateReply(Connection conn, int replyNo, String replyContent) throws Exception {
int result = 0;
try {
String sql = prop.getProperty("updateReply");
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, replyContent);
pstmt.setInt(2, replyNo);
result = pstmt.executeUpdate();
} finally {
close(pstmt);
}
return result;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>reply-sql.xml</comment>
<!-- 댓글 목록 조회 -->
<entry key="selectReplyList">
SELECT REPLY_NO, REPLY_CONTENT,
TO_CHAR(CREATE_DT, 'YYYY.MM.DD HH24:MI:SS') CREATE_DT,
BOARD_NO, MEMBER_NO, MEMBER_NICK, PROFILE_IMG
FROM REPLY
JOIN MEMBER USING(MEMBER_NO)
WHERE REPLY_ST ='N'
AND BOARD_NO=?
ORDER BY REPLY_NO
</entry>
<!-- 댓글 등록 -->
<entry key="insertReply">
INSERT INTO REPLY VALUES(SEQ_RNO.NEXTVAL, ?, DEFAULT, DEFAULT, ?, ?)
</entry>
<!-- 댓글 삭제 -->
<entry key="deleteReply">
UPDATE REPLY
SET REPLY_ST='Y'
WHERE REPLY_NO=?
</entry>
<!-- 댓글 수정 -->
<entry key="updateReply">
UPDATE REPLY
SET REPLY_CONTENT = ?
WHERE REPLY_NO=?
</entry>
</properties>