국비 75 - 댓글 CRUD

냐아암·2023년 8월 9일
0

국비

목록 보기
102/114

댓글 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("&amp;", "&");
    beforeContent = beforeContent.replaceAll("&lt;", "<");
    beforeContent = beforeContent.replaceAll("&gt;", ">");
    beforeContent = beforeContent.replaceAll("&quot;", "\"");

    // 개행문자 처리 해제
    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);
        }

    })

}

Servlet

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()으로 전달하여 수행
	}

}

Service

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;
	}

}

DAO

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;
	}

	
}

sql

<?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>
profile
개발 일지

0개의 댓글