감 잃지말고 개발하기

[JSP] [MVC] [AJAX] [JS] [JSON] AJAX를 이용한 비동기 통신으로 댓글/대댓글 구현하기 #4 대댓글 Create 본문

JSP/MVC

[JSP] [MVC] [AJAX] [JS] [JSON] AJAX를 이용한 비동기 통신으로 댓글/대댓글 구현하기 #4 대댓글 Create

persii 2023. 4. 13. 04:34

MVC패턴을 지키면서 특정 페이지에서 ajax를 이용한 비동기 통신으로 댓글을 작성하고 모든 댓글을 출력하는 로직을 기록하고자 한다.

지난 포스팅에는 댓글을 화면에 출력하는 로직을 기록했다. 이번 포스팅에는 대댓글을 작성하고 저장하는 로직을 기록해 보기로 한다.

 

목표

 JSP에서 MVC 패턴을 지키면서 AJAX를 통해 대댓글 저장 로직을 구현할 수 있다.

♠ DB에서 댓글 테이블에 필요한 그룹 칼럼, 깊이 칼럼, 출력 순서 칼럼을 이해할 수 있다. 

 

 

로직 흐름

  1. 도서 상세 페이지의 댓글이 출력되는 영역(왼쪽)에서 "댓글" 버튼을 클릭한 후, 댓글을 작성하는 영역(오른쪽)에서 대댓글을 작성하고 "등록하기" 버튼을 누르면 ajax로 Form의 데이터가 서버에 전송된다(bookView.jsp).
  2. 해당 컨트롤러에서 해당 URL을 받아 대댓글 작성을 처리하는 클래스를 호출한다(ReviewFrontController.java).
  3. Action 클래스에서 비즈니스 로직을 처리하는 클래스를 호출하여 대댓글을 DB에 저장시키고, DB처리 결과에 따라 응답 처리를 한다(BookCommentRegistProAction.java).
  4. Service 클래스에서 DAO클래스를 호출한다(BookCommentRegistProService.java).
  5. DAO 클래스에서 MySQL8.0 DB에 접근해 데이터를 가져온다(CommentDAO).
  6. 다시 도서 상세 페이지로 돌아와 적절한 응답을 한다.

 

 

로직 흐름 코드 및 실행화면

1. 도서 상세 페이지(bookView.jsp)

1-1.  도서 상세 페이지 대댓글 작성 영역

"댓글" 버튼을 누르면 스크립트 함수가 실행되고, 오른쪽 영역의 Form 속성이 달라진다.대댓글을 입력하고 "등록하기" 버튼을 누르면 스크립트 함수가 실행된다. 

대댓글 작성

 

1-2.  도서 상세 페이지 "댓글" 버튼 클릭 JS

지난 포스팅에서 모든 댓글이 화면에 출력될 때 "댓글" 버튼도 출력되는 것을 확인했다. 이 "댓글" 버튼을 클릭하면 스크립트의 replyForm() 함수가 실행된다.

 

replyForm() 함수의 중요한 역할은

대댓글을 다는 부모 댓글(무조건 원글만을 의미하지 않는다)의 그룹번호(c_ref), 깊이(c_lev), 출력순서(c_seq)를 대댓글을 다는 Form 영역에 값으로 추가 설정해 주는 것에 있다. 

JQuery 문법으로 Form 태그 하위에 <input type="hidden"> 태그 3개를 추가해 각각의 인자를 value로 설정해 주었다. 

이 값들은 나중에 서버로 넘어간 대댓글의 ref, lev, seq를 설정하는 기준이 된다.

아래 CommentDAO 클래스를 설명할 때 설명하겠다.

지금은 대댓글 다는 Form에 부모 댓글의 ref, lev, seq 값이 저장되도록 설정했다는 것만 알고있자.

<script type="text/javascript">

    // "댓글" 버튼을 누를 때 호출되는 함수
    function replyForm(c_ref, c_lev, c_seq) {
        alert(c_ref + c_lev + c_seq);

        // 대댓글 다는 Form 요소 설정하기
        $("#commentForm").append($('<input/>', {type: 'hidden', name: 'c_ref'}));
        $("#commentForm").append($('<input/>', {type: 'hidden', name: 'c_lev'}));
        $("#commentForm").append($('<input/>', {type: 'hidden', name: 'c_seq'}));
        $('input[name="c_ref"]').val(c_ref);
        $('input[name="c_lev"]').val(c_lev);
        $('input[name="c_seq"]').val(c_seq);

        $(".review_box").children("h4").html("댓글을 달아주세요.");
        $("#c_title").removeAttr("readonly");
        $("#c_text").removeAttr("readonly");
        $("#c_title").val("").focus();
        $("#c_text").val("");
        $("#c_title").attr("placeholder", "댓글 제목을 적어주세요.");
        $("#c_text").attr("placeholder", "댓글을 달아주세요.");
    }
</script>

 

1-3.  도서 상세 페이지 대댓글 작성 부분 HTML

위의 JS 코드로 인해 각 요소의 설정이 변했음을 확인할 수 있다.아래에 <input type="hidden"> 태그 3개가 추가된 것을 확인할 수 있다. 

대댓글 작성 HTML

 

1-4.  "등록하기" 버튼 클릭 JS 코드

댓글 작성 Form에서 "등록하기" 버튼을 클릭하면 실행된다. 

 

jQuery 문법으로 ajax를 사용한다.

  • ajax로 호출할 URL : "/bookCommentRegistPro.re"
  • ajax로 보낼 데이터 통신타입 : POST
  • ajax로 서버에 요청 시 보낼 매개변수 : queryString
    • serialize()를 이용해 form 태그 안의 모든 값들의 name값을 키값으로 만들어 보내준다.
  • 응답받을 데이터 타입 : TEXT
    • 서버에서 DB insert에 대한 결과물(문자열)을 response에 담아 보낼 것이므로 dataType을 TEXT로 설정한다.
  • 정상적으로 요청/응답된 경우(success) : 서버에서 보낸 text 인자 값이 "success"일 때 alert창을 띄우고, getCommentList() 함수를 호출해 댓글이 재출력되도록 처리한다.
<script type="text/javascript">

    // 댓글 작성
    $(".registCommentBtn").click(function() {

        var queryString = $("form[name=commentForm]").serialize();

        $.ajax({
            url: "/bookCommentRegistPro.re",
            type: "POST",
            data: queryString,
            dataType: "TEXT",	// 서버에서 DB insert에 대한 결과물을 받을 것
            success: function(text) {
                if(text === "success") {
                    alert("등록되었습니다.");
                    $("#c_title").attr("readonly", "readonly");
                    $("#c_text").attr("readonly", "readonly");
                    getCommentList(${book.b_id});
                } else {
                    alert("등록되지 못했습니다.");
                    return false;
                }
            }, 
            error: function(request, status, error) {
                console.log("code: "+request.status+"\n"+"message : "+
                			request.responseText+"\n"+"error: "+error);
            } 
        });
    });
</script>

 

2. 컨트롤러(ReviewFrontController.java)

웹 브라우저에서 요청한 URL인 /bookCommentRegistPro.re에 해당하는 if문으로 들어오고 BookCommentRegistProAction 클래스의 execute() 메서드가 호출된다. 

package controller;

import java.io.IOException;
import java.rmi.ServerException;

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 action.BookReviewListAction;
import action.BookReviewRegistProAction;
import svc.AjaxAction;

/** AJAX로 데이터 통신
 * ActionForward 클래스 필요 X */
@WebServlet("*.re")
public class ReviewFrontController extends HttpServlet{
	
	protected void doProcess(HttpServletRequest req, HttpServletResponse resp) 
			throws ServerException, IOException, ServletException {
		req.setCharacterEncoding("UTF-8");
		
		/* 1. 요청 주소 파악 */
		String requestURI = req.getRequestURI();
		String contextPath = req.getContextPath();	
		String command = requestURI.substring(contextPath.length());

		/* 2. 각 요청 주소의 매핑 처리 */
		AjaxAction action = null;
		
		// 코멘트 등록 처리 요청
		if (command.equals("/bookCommentRegistPro.re")) {
			action = new BookCommentRegistProAction();
			try {
				action.execute(req, resp);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
			throws ServerException, IOException, ServletException {
		doProcess(req, resp);
	}

	protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
			throws ServerException, IOException, ServletException {
		doProcess(req, resp);
	}
}

 

2. Action 클래스(BookCommentRegistProAction.java)

원글과 대댓글일 경우의 처리를 다르게 해 주기 위해 execute() 메서드의 내용을 아래와 같이 수정했다.

  1. ajax로 건너온 대댓글 데이터와 부모 댓글의 ref, lev, seq를 저장한다.
    • 원글일 경우 p_lev 값은 넘어오지 않으므로(#2 댓글 Create 포스팅 참고) p_lev 변수에는 -1이 저장될 것이다.
  2. p_lev의 값에 따라 BookCommentRegistProService 클래스의 insertComment() 메서드를 호출한다.
    • 원글일 경우 Comment 객체만 인자로 보낸다.
    • 대댓글일 경우 Comment 객체와 부모 댓글의 ref, lev, seq 변수를 인자로 보낸다.
  3. DB 처리 결과에 따른 결과값을 response에 담아 보낸다. 
package action;

import java.io.PrintWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import svc.AjaxAction;
import svc.BookCommentRegistProService;
import util.Util;
import vo.Comment;

/** 도서 아이디에 따른 댓글을 처리하는 Action 클래스 */
public class BookCommentRegistProAction implements AjaxAction {

	@Override
	public void execute(HttpServletRequest req, HttpServletResponse resp) throws Exception {
		
                // 부모 댓글의 ref, lev, seq를 저장하는 변수
		int p_ref = -1;
		int p_lev = -1;
		int p_seq = -1;
		
		/* 대댓글의 경우 부모 댓글의 c_ref, c_lev, c_seq를 가져온다. */
		if(req.getParameter("c_lev") != null) {
			p_lev = Integer.parseInt(req.getParameter("c_lev"));
			p_ref = Integer.parseInt(req.getParameter("c_ref"));
			p_seq = Integer.parseInt(req.getParameter("c_seq"));
		}
		
		System.out.println(" B.CommentRegistPro.A : "+p_ref+p_lev+p_seq);
		
		/* ajax로 넘어온 데이터를 Comment 객체에 저장*/
		Comment comment = new Comment();
		comment.setC_b_id(Integer.parseInt(req.getParameter("c_b_id")));
		comment.setC_m_id(req.getParameter("c_m_id"));
		comment.setC_title(req.getParameter("c_title"));
		comment.setC_text(req.getParameter("c_text"));
		
		/* DB에 접근해 데이터 등록 
		 * 원글일 경우와 대댓글일 경우 분리하여 메서드 호출 */
		BookCommentRegistProService service = new BookCommentRegistProService();
		boolean isRegistSuccess;
		if(p_lev == -1) {
			// 원글일 경우
			isRegistSuccess = service.insertComment(comment);
		} else {
			// 댓글일 경우
			isRegistSuccess = service.insertComment(comment, p_ref, p_lev, p_seq);
		}

		/* DB 처리에 따른 결과값 리턴 */
		PrintWriter out = resp.getWriter();
		if(isRegistSuccess) {
			out.print("success");
		} else {
			out.print("failed");
		}
	}
}

 

4. Service 클래스(BookCommentRegistProService.java)

  1. insertComment(comment) 메서드는 원글을 저장하고 처리결과를 리턴한다. 
  2. insertComment(comment, p_ref, p_lev, p_seq) 메서드는 대댓글을 저장하고 처리결과를 리턴한다.
package svc;

import static db.JdbcUtil.close;
import static db.JdbcUtil.commit;
import static db.JdbcUtil.getConnection;
import static db.JdbcUtil.rollback;

import java.sql.Connection;

import dao.CommentDAO;
import vo.Comment;

/** 댓글 등록을 처리하는 Service 클래스 */
public class BookCommentRegistProService {

	/** 댓글(원글)을 등록하는 메서드 */
	public boolean insertComment(Comment comment) {

		boolean isRegistSuccess = false;
		
		/* DB 작업 */
		Connection conn = getConnection();
		CommentDAO commentDAO = CommentDAO.getInstance();
		commentDAO.setConnection(conn);
		int insertCount = commentDAO.insertComment(comment);
		
		if(insertCount > 0) {
			// 성공적으로 삽입된 경우. 트랜잭션 영구반영
			commit(conn);
			isRegistSuccess = true;
		} else {
			rollback(conn);
		}
		close(conn);
		return isRegistSuccess;
	}

	/** 대댓글을 등록하는 메서드 */
	public boolean insertComment(Comment comment, int p_ref, int p_lev, int p_seq) {
		
		boolean isRegistSuccess = false;
		
		/* DB 작업 */
		Connection conn = getConnection();
		CommentDAO commentDAO = CommentDAO.getInstance();
		commentDAO.setConnection(conn);
		int insertCount = commentDAO.insertComment(comment, p_ref, p_lev, p_seq);
		
		if(insertCount > 0) {
			// 성공적으로 삽입된 경우. 트랜잭션 영구반영
			commit(conn);
			isRegistSuccess = true;
		} else {
			rollback(conn);
		}	
		close(conn);
		return isRegistSuccess;
	}
}

 

5. DAO 클래스(CommentDAO.java)

원글을 DB에 저장하는 코드는 #2 댓글 Create 포스팅에 작성한 코드와 동일하므로 대댓글을 저장하는 코드만 가져왔다.

 

  1. 같은 그룹 번호(c_ref)를 가진 댓글 및 대댓글의 출력순서(c_seq) 값을 재설정한다. 
    • UPDATE jspbookshop.comment SET c_seq = c_seq+1 WHERE c_ref=? AND c_seq>?
      pstmt.setInt(1, p_ref);
      pstmt.setInt(2, p_seq);
    • WHERE c_ref = p_ref
      • c_ref의 값이 원글의 c_ref값(원글 밑에 달리는 모든 대댓글의 c_ref은 원글과 동일하다)인 댓글, 즉 같은 그룹 내의 모든 댓글을 대상으로 한다.
    • AND c_seq > p_seq
      • c_seq의 값이 부모 글(대댓글을 다는 댓글을 의미)의 c_seq값보다 큰 모든 대댓글을 대상으로 한다. 
    • SET c_seq = c_seq + 1
      • 부모 댓글의 c_seq보다 큰, 같은 c_ref  내의 모든 댓글의 c_seq를 1씩 증가시킨다.
      • 새로 저장될 대댓글이 부모 댓글 바로 아래에 출력될 수 있도록 출력 순서 자리를 비워주는 sql문이라고 생각하면 된다.
      • 대댓글을 저장하는 INSERT문에서 새로 저장될 대댓글의 c_seq값을 부모 댓글의 seq값(p_seq)에 1을 더한 값으로 저장해 위에서 만든 자리를 메꾼다. 
  2. 새로 저장할 대댓글 Comment 객체의 깊이(c_lev)와 출력 순서(c_seq) 값을 부모 댓글보다 1씩 더해 저장한다. 이때 관련그룹(c_ref)은 부모와 같이 묶어준다. 
    • Iint c_seq = p_seq + 1;
      int c_lev = p_lev + 1;
      INSERT INTO jspbookshop.comment VALUES(NULL,?,?,?,?,now(),?,?,?,?)
      • pstmt.setInt(6, p_ref);
        pstmt.setInt(7, c_lev);
        pstmt.setInt(8, c_seq);
  3. 삽입된 결과를 리턴한다. 
package dao;

import static db.JdbcUtil.*;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;

import vo.Comment;

public class CommentDAO {

	Connection conn;
	private static CommentDAO commentDAO;
	
	/** 대댓글을 등록하는 메서드 */
	@SuppressWarnings("resource")
	public int insertComment(Comment comment, int p_ref, int p_lev, int p_seq) {
		
		PreparedStatement pstmt = null;
		int insertCount = 0;
		
		/* 대댓글 처리 방법(ref, lev, seq) 
		 * 1. 같은 그룹(ref)의 댓글들의 출력순서(seq)를 1씩 증가시킨다.
		 * 2. 새로 등록할 대댓글의 출력순서(seq)와 깊이(lev)를 1씩 증가시켜 insert한다.
		 * */
		String sql = "update jspbookshop.comment "
				+ "set c_seq = c_seq+1 "
				+ "where c_ref=? and c_seq>?";
		
		try {
			pstmt = conn.prepareStatement(sql);
			pstmt.setInt(1, p_ref);
			pstmt.setInt(2, p_seq);
			int updateCount = pstmt.executeUpdate();
			
			if(updateCount > 0) {
				commit(conn);
			}
			
			int c_seq = p_seq + 1;
			int c_lev = p_lev + 1;
			sql = "insert into jspbookshop.comment values(NULL,?,?,?,?,now(),?,?,?,?)";
			pstmt = conn.prepareStatement(sql);
			pstmt.setInt(1, comment.getC_b_id());
			pstmt.setString(2, comment.getC_m_id());
			pstmt.setString(3, comment.getC_title());
			pstmt.setString(4, comment.getC_text());
			pstmt.setInt(5, comment.getC_empathy());
			pstmt.setInt(6, p_ref);
			pstmt.setInt(7, c_lev);
			pstmt.setInt(8, c_seq);
			
			insertCount = pstmt.executeUpdate();
		} catch (Exception e) {
			System.out.println(" C.DAO : insertComment() ERROR : "+e);
		} finally {
			close(pstmt);
		}
		return insertCount;
	}
}

 

6. 도서 상세 페이지(bookView.jsp)

ajax로 보낸 요청이 정상적으로 처리되고, 서버로부터 정상적인 응답이 이루어졌다면  DB에 댓글이 저장되고, 화면에 alert창이 뜬 후 다시 모든 댓글이 출력된다.

댓글 저장 alert창
댓글 출력

 


 

댓글을 많이 달아보면 아래와 같이 출력된다.

화면에 출력되는 부분과 c_id ASC로 정렬해서 조회한 DB 결과물을 비교해 보면서 이해해 보자!

댓글 DB

 

이렇게 댓글/대댓글 CRUD에서 Create과 Read 로직이 끝났다!

 

 

끝.