개발/[Spring] 블로그 만들기

[코드로 배우는 스프링 웹 프로젝트] 17-3. Ajax 댓글 처리 (페이징처리/DB인덱스생성..)

ee2ee2 2022. 5. 2. 23:49
728x90
반응형

ReplyService ReplyService 해당 프로젝트는 코드로 배우는 스프링 웹 프로젝트(개정판)을 기반으로 진행됩니다.


앞서 작성된 예제들을 해당 게시물의 전체 댓글을 가져와서 화면에 출력한다. 댓글이 몇 개 없을 때는 괜찮지만, 만약 1000개 라면? DB에서 모두 조회해야하며 끝없는 스크롤을 내리게 될 것 이다. (성능 문제는 물론)

이러한 문제를 페이징 처리를 이용하여 처리해볼 것 이다.

 

17.6 댓글의 페이징 처리

17.6.1 데이터베이스의 인덱스 설계

댓글에 대해 우선적으로 고려해야할 부분은 tbl_reply 테이블을 접근할 때 댓글의 번호(rno)가 아니라, 게시물의 번호(bno)가 중심이 되어야한다. tbl_reply의 PK는 rno이므로, bno에 대해 인덱스를 생성해주자.

create index idx_reply on tbl_reply (bno desc, rno asc);

17.6.2 인덱스를 이용한 페이징 처리

인덱스를 이용하는 이유 중에 하나는 정렬을 피할 수 있기 때문이다. 특정한 게시물의 rno의 순번대로 데이터를 조회하고 싶다면 아래와 같이 작성하면 된다.

테이블에 접근해서 결과를 만들 때 생성되는 ROWNUM은 가장 낮은 rno 값을 가지는 데이터가 1번이 된다. (인덱스 생성을 해놓았기 때문!)

이제 댓글 조회시 페이징 처리가 될 수 있도록 적용해보자.

ReplyMapper.xml

	<!-- 특정 게시글의 댓글 조회 -->
	<select id ="getListWithPaging" resultType="org.zerock.domain.ReplyVO">
		<![CDATA[
			select rno, bno, reply, replyer, replyDate, updateDate
			from 
			(
				select /*+INDEX(tbl_reply idx_reply) */
						rownum rn, rno, bno, reply, replyer, replyDate, updateDate
				from tbl_reply 
				where bno = #{bno}
						and rno > 0
						and rownum <= #{cri.pageNum} * #{cri.amount}
			) where rn > (#{cri.pageNum}-1) * #{cri.amount}
		]]>
	</select>

 아래의 테스트 코드로 페이징 처리된 결과를 확인한다.

ReplyMapperTests.java의 일부

	@Test
	public void testList2() {
		Criteria cri = new Criteria(2, 3);
		
		List<ReplyVO> replies = mapper.getListWithPaging(cri, 1202122L);
			
		replies.forEach(reply -> log.info("############### " + reply));
	}

 

실행 결과 - 3개씩 출력할 때, 2번째 페이지 조회 테스트

실행 결과

 

http://localhost:8080/replies/pages/1202122/1.json -> 1202122번 게시글의 댓글 목록 조회


17.6.3 댓글의 숫자 파악

댓글의 페이징 처리를 하기 위해서는 해당 게시물의 전체 댓글의 숫자를 파악해서 화면에 보여줘여 한다. 특정 게시글의 댓글의 개수를 가져오는 getCountByBno()를 추가하자.

ReplyMapper 인터페이스

	<!-- 댓글 개수 조회 -->
	<select id ="getCountByBno" resultType="Integer">
		<![CDATA[
			select count(rno) from tbl_reply where bno = #{bno}
		]]>
	</select>

17.6.4 ReplyServiceImpl에서 댓글과 댓글 수 처리

ReplyService 인터페이스와 구현 클래스인 ReplyServiceImpl 클래스는 List<ReplyVO>와 같이 댓글의 수를 같이 전달할 수 있도록 변경한다.

 

먼저, org.zerock.domain 패키지에 두 가지 정보를 담을 수 있는 ReplyPageDTO 클래스를 정의해보자.

ReplyPageDTO.java

package org.zerock.domain;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;

@Data
@AllArgsConstructor
@Getter
public class ReplyPageDTO {

	private int replyCnt;
	private List<ReplyVO> list;
	
}

ReplyService 인터페이스와 ReplyServiceImpl 클래스에는 ReplyPageSTO를 반환하는 메소드를 추가한다.

 

ReplyService 인터페이스 일부

public interface ReplyService {

	...생략..
	
	public ReplyPageDTO getListPage(Criteria cri, Long bno);
}

 

ReplyServiceImpl 클래스의 일부

	@Override
	public ReplyPageDTO getListPage(Criteria cri, Long bno) {
		return new ReplyPageDTO(mapper.getCountByBno(bno), mapper.getListWithPaging(cri, bno));
	}

17.6.5 ReplyController 수정

ReplyController에서는 ReplyService에 새롭게 추가된 getListPage()를 호출하고 데이터를 전송하는 형태로 수정한다.

ReplyController 일부

	//특정 게시물의 댓글 목록 확인 함수
	@GetMapping(value="/pages/{bno}/{page}", 
			produces = {MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_UTF8_VALUE})
	//public ResponseEntity<List<ReplyVO>> getList(@PathVariable("page") int page, @PathVariable("bno") Long bno) {
	public ResponseEntity<ReplyPageDTO> getList(@PathVariable("page") int page, @PathVariable("bno") Long bno) {
		log.info("Get LIST ...................");
		
		Criteria cri = new Criteria(page, 10);
		log.info("Criteria : " + cri);
		
		//return new ResponseEntity<>(service.getList(cri, bno), HttpStatus.OK);
		return new ResponseEntity<>(service.getListPage(cri, bno), HttpStatus.OK);
	}

기존과 동일하게 JSON 타입의 데이터를 전송하지만 ReplyPageDTO 객체를 JSON으로 전송해야하므로, 특정 게시물의 댓글 목록을 조회하면 'replyCnt'와 'list'라는 이름의 속성을 가지는 JSON 문자열이 전송된다.

실행 결과

http://localhost:8080/replies/pages/1202122/1.json

 

반응형

17.7 댓글 페이지의 화면 처리

댓글의 화면 처리는 아래와 같다.

- 게시물을 조회 하는 페이지에 들어오면 기본적으로 가장 오래된 댓글을 가져와서 1페이지에 보여준다.
- 1페이지 게시물을 가져올 때 해당 게시물의 댓글의 숫자를 파악해서 댓글의 페이지 번호를 출력한다.
- 댓글이 추가되면 댓글의 숫자만을 가져와서 최종 페이지를 찾아 이동한다.
- 댓글의 수정과 삭제 후에는 다시 동일 페이지를 호출한다.

 

<게시물 조회 시 댓글 1페이지>

<신규 댓글 작성>

 

<마지막 페이지로 이동>


 

17.7.1 댓글 페이지 계산과 출력

Ajax로 가져로는 데이터가 replyCnt와 list라는 데이터로 구성되므로 이를 처리하는 reply.js 를 수정해준다.

reply.js 일부

 

	function getList(param, callback, error) {
		var bno = param.bno;
		var page = param.page || 1;
		
		$.getJSON("/replies/pages/" + bno + "/" + page + ".json",
			function(data) {
				if(callback) {
					//callback(data); //댓글 목록만 가져오는 경우
					callback(data.replyCnt, data.list); //댓글 개수와 목록을 가져오는 경우
				}
			}).fail(function(xhr, status, err) {
				if(error) {
					error();
				}
			});
	}

화면에 대한 처리는 get.jsp에서이뤄진다.

reply.js를 이용해서 댓글의 페이지를 호출하는 부분은 showList 함수이므로 페이지 번호를 출력하도록 수정한다.

get.jsp 일부

function showList(page) {
	console.log("show list " + page);
	
	//해당 게시물의 모든 댓글을 가져오는지 확인
	replyService.getList(
		{bno:bnoValue, page: page || 1}, 
		function(replyCnt, list){
			console.log("replyCnt : " + replyCnt);
			console.log("list : " + list);
			console.log(list);

			//마지막 페이지를 찾아 다시 호출 (새로운 댓글 등록시)
			if(page == -1){
				pageNum = Math.ceil(replyCnt/10.0);
				showList(pageNum);
				return;
			}

			var str = "";

			if(list==null || list.length ==0) {
				//replyUL.html("");
				return;
			}

			for(var i=0, len = list.length||0; i<len; i++){
				str += "<li class='left clearfix' data-rno='" + list[i].rno + "'>";
				str += "<div> <div class='header'> <strong class='primary-font'> " + list[i].replyer + "</strong>";
				str += "<small class='pull-right text-muted'> " + replyService.displayTime(list[i].replyDate) + "</small></div>";
				str += "<p>" + list[i].reply + "</p></div></li>";
			}

			replyUL.html(str);

			showReplyPage(replyCnt);
		}
	); //end function
}	//end showList

댓글의 페이지 번호는 화면상에서 댓글의 오른쪽 아래서 추가한다.

get.jsp 일부

: <div class="panel-footer"> 추가

	            		<!-- ./ panel-heading -->
	            		<div class="panel-body">
	            			<ul class="chat">

	            			</ul>
	            			<!-- end ul -->
	            		</div>
	            		<!-- ./ panel .chat-panel -->
	            		
	            		<div class="panel-footer">
	            		
	            		</div>
	            	</div>
	            </div>
	            <!-- ./ end row -->
	            
            </div>
            <!-- /.row -->
            
       ... 생략
            
       <script type="text/javascript">
		$(document).ready(function(){     
            
            //댓글의 페이지 번호 출력 함수
            var pageNum = 1;
            var replyPageFooter = $(".panel-footer");

            function showReplyPage(replyCnt){
                var endNum = Math.ceil(pageNum/ 10.0) *10;
                var startNum = endNum - 9;

                var prev = startNum != 1;
                var next = false;

                if(endNum * 10 >= replyCnt){
                    endNum = Math.ceil(replyCnt/10.0);
                }

                if(endNum*10 < replyCnt){
                    enxt=true;
                }

                var str = "<ul class='pagination pull-right'>";

                if(prev){
                    str += "<li class='page-item'> <a class='page-link' href='" +(startNum -1)+"'>Previous</a></li>";
                }

                for(var i= startNum; i <= endNum; i++){
                    var active = pageNum == i? "active":"";

                    str+="<li class='page-item " + active + "'><a class='page-link' href = '" + i + "'>" + i + "</a></li>";
                }

                if(next){
                    str += "<li class='page-item'> <a class='page-link' href='" +(endNum + 1)+"'>Next</a></li>";
                }

                str += "</ul></div>";

                console.log(str);

                replyPageFooter.html(str);
            }
            
            
... 생략

showReplyPage()는 기존에 JAVA로 작성되는 PageMaker의 JS 버전에 해당된다. 댓글 페이지를 문자열로 구성할 후 <div>의 innerHTML로 추가한다. showList()의 마지막에 페이지를 출력하도록 수정한다.

						if(list==null || list.length ==0) {
							//replyUL.html("");
							return;
						}
						
						for(var i=0, len = list.length||0; i<len; i++){

						}
						
						replyUL.html(str);
						
						showReplyPage(replyCnt);
					}
			); //end function
		}	//end showList

실행 결과

마지막 처리는 페이지의 번호를 클릭했을 때 새로운 댓글을 가져오도록 하는 부분이다.

get.jsp 일부

		//댓글 페이지 번호 클릭했을 때, 새로운 댓글 가져오는 부분
		replyPageFooter.on("click", "li a", function(e){
				e.preventDefault();
				console.log("Reply pageBtn Click");
				
				var targetPageNum = $(this).attr("href");
				
				console.log("targetPageNum : " + targetPageNum);
				
				pageNum = targetPageNum;
				showList(pageNum);
			});

17.7.2 댓글의 수정과 삭제

get.jsp 일부

		//댓글 수정 처리
		modalModBtn.on("click", function(e){
			var reply = {
					rno : modal.data("rno"),
					reply : modalInputReply.val()
			};
			
			replyService.modify(
					reply,
					function(result){
						alert("RESELT : " + result);
						modal.modal("hide");
						//showList(1);
						showList(pageNum);
					}
				);
		});
		
		//댓글 삭제 처리
		modalRemoveBtn.on("click", function(e){
			var rno = modal.data("rno");
			
			replyService.remove(
					rno,
					function(result){
						alert("RESELT : " + result);
						modal.modal("hide");
						//showList(1);
						showList(pageNum);
					}
				);
		});

드디어 17강이 끝이 났다!

get.jsp의 코드 양이 많아 아래에 전체 소스코드를 첨부하였습니다. 참고하세요!

더보기

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>

<%@include file="../includes/header.jsp" %>

            <div class="row">
                <div class="col-lg-12">
                    <h1 class="page-header">Board Register</h1>
                </div>
                <!-- /.col-lg-12 -->
            </div>
            <!-- /.row -->
            <div class="row">
                <div class="col-lg-12">
                    <div class="panel panel-default">
                        <div class="panel-heading"> Board Register </div>
                        <!-- /.panel-heading -->
                        <div class="panel-body">                        
                         <div class="form-group">
                            <label>Bno</label> <input class="form-control" name="bno" value='<c:out value="${board.bno }"/>' readonly="readonly">
                            </div>
                           
                            <div class="form-group">
                            <label>Title</label> <input class="form-control" name="title" value='<c:out value="${board.title }"/>' readonly="readonly">
                            </div>
                            
                            <div class="form-group">
                            <label>Text area</label> <textarea class="form-control" rows="3" name="content" readonly="readonly"><c:out value="${board.content }"/> </textarea>
                            </div>
                            
                            <div class="form-group">
                             <label>Writer</label> <input class="form-control" name="writer" value='<c:out value="${board.writer }"/>' readonly="readonly">
                            </div>
                            
                            <!--  
                             <button data-oper='modify' class="btn btn-default" onclick="location.href='/board/modify?bno=<c:out value="${board.bno }"/>'">Modify</button>
                             <button data-oper='list' class="btn btn-info" onclick="location.href='/board/list'">List</button>
-->

<button data-oper='modify' class="btn btn-default">Modify</button>
                            <button data-oper='list' class="btn btn-info">List</button>

<form id='operForm' action="/board/modify" method="get">
<input type="hidden" id='bno' name='bno' value='<c:out value="${board.bno }"/>'>
<input type="hidden" id='pageNum' name='pageNum' value='<c:out value="${cri.pageNum }"/>'>
<input type="hidden" id='amount' name='amount' value='<c:out value="${cri.amount }"/>'>
<input type="hidden" id='type' name='type' value='<c:out value="${cri.type }"/>'>
<input type="hidden" id='keyword' name='keyword' value='<c:out value="${cri.keyword }"/>'>
</form>
                            <!-- /.table-responsive -->
                        </div>
                        <!-- /.end panel-body -->
                    </div>
                    <!-- /.end panel -->
                </div>
                <!-- /.col-lg-12 -->
                
                <!-- 댓글 목록 -->
            <div class='row'>
             <div class="col-lg-12">
            
             <!-- /. panel -->
             <div class="panel panel-default">
             <div class="panel-heading">
             <i class="fa fa-comments fa-fw"></i> Reply
             <button id='addReplyBtn' class='btn btn-primary btn-xs pull-right'> New Reply </button>
             </div>
             </div>
            
             <!-- ./ panel-heading -->
             <div class="panel-body">
             <ul class="chat">
             <!-- start reply -->
             <li class="left clearfix" data-rno='12'>
             <div>
             <div class="header">
             <strong class="primary-font">user00</strong>
             <small class="pull-right text-muted">2022-04-02 22:40</small>
             </div>
             <p> Good Job!</p>
             </div>
             </li>
             <!-- end reply -->
             </ul>
             <!-- end ul -->
             </div>
             <!-- ./ panel .chat-panel -->
            
             <div class="panel-footer">
            
             </div>
             </div>
            </div>
            <!-- ./ end row -->
            
            </div>
            <!-- /.row -->
<%@include file="../includes/footer.jsp" %>


<!-- 댓글 Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                <h4 class="modal-title" id="myModalLabel">REPLY MODAL</h4>
            </div>
                        
<div class="modal-body">
<div class="form-group">
<label>Reply</label>
                    <input class="form-control" name='reply' value='New Reply!!'>
                </div>
                        
                <div class="form-group">
                    <label>Replyer</label>
                    <input class="form-control" name='replyer' value='New Replyer'>
                </div>
                        
                <div class="form-group">
                    <label>Date</label>
                    <input class="form-control" name='replyDate' value=''>
                </div>    
</div>
                        
<div class="modal-footer">
<button id='modalModBtn' type="button" class="btn btn-warning">Modify</button>
                <button id="modalRemoveBtn" type="button" class="btn btn-danger">Remove</button>
                <button id="modalRegisterBtn" type="button" class="btn btn-danger">Register</button>
                <button id="modalCloseBtn" type="button" class="btn btn-default">Close</button>
            </div>
</div>
<!-- /.modal-content -->
</div>
    <!-- /.modal-dialog -->
</div>
<!-- /.modal -->

<script type="text/javascript" src="/resources/js/reply.js"> </script>

<script type="text/javascript">
$(document).ready(function(){

//console.log("===============================");
//console.log("============JS TEST============");

var bnoValue = '<c:out value="${board.bno}"/>';
var replyUL = $(".chat");

showList(1);

/*  function showList(page) {
//해당 게시물의 모든 댓글을 가져오는지 확인
replyService.getList(
{bno:bnoValue, page: page || 1}, 
function(list){
var str = "";

if(list==null || list.length ==0) {
replyUL.html("");
return;
}

for(var i=0, len = list.length||0; i<len; i++){
str += "<li class='left clearfix' data-rno='" + list[i].rno + "'>";
str += "<div> <div class='header'> <strong class='primary-font'> " + list[i].replyer + "</strong>";
str += "<small class='pull-right text-muted'> " + replyService.displayTime(list[i].replyDate) + "</small></div>";
str += "<p>" + list[i].reply + "</p></div></li>";
}

replyUL.html(str);
}
); //end function
} //end showList */

function showList(page) {
console.log("show list " + page);

//해당 게시물의 모든 댓글을 가져오는지 확인
replyService.getList(
{bno:bnoValue, page: page || 1}, 
function(replyCnt, list){
console.log("replyCnt : " + replyCnt);
console.log("list : " + list);
console.log(list);

//마지막 페이지를 찾아 다시 호출 (새로운 댓글 등록시)
if(page == -1){
pageNum = Math.ceil(replyCnt/10.0);
showList(pageNum);
return;
}

var str = "";

if(list==null || list.length ==0) {
//replyUL.html("");
return;
}

for(var i=0, len = list.length||0; i<len; i++){
str += "<li class='left clearfix' data-rno='" + list[i].rno + "'>";
str += "<div> <div class='header'> <strong class='primary-font'> " + list[i].replyer + "</strong>";
str += "<small class='pull-right text-muted'> " + replyService.displayTime(list[i].replyDate) + "</small></div>";
str += "<p>" + list[i].reply + "</p></div></li>";
}

replyUL.html(str);

showReplyPage(replyCnt);
}
); //end function
} //end showList

//댓글의 페이지 번호 출력 함수
var pageNum = 1;
var replyPageFooter = $(".panel-footer");

function showReplyPage(replyCnt){
var endNum = Math.ceil(pageNum/ 10.0) *10;
var startNum = endNum - 9;

var prev = startNum != 1;
var next = false;

if(endNum * 10 >= replyCnt){
endNum = Math.ceil(replyCnt/10.0);
}

if(endNum*10 < replyCnt){
enxt=true;
}

var str = "<ul class='pagination pull-right'>";

if(prev){
str += "<li class='page-item'> <a class='page-link' href='" +(startNum -1)+"'>Previous</a></li>";
}

for(var i= startNum; i <= endNum; i++){
var active = pageNum == i? "active":"";

str+="<li class='page-item " + active + "'><a class='page-link' href = '" + i + "'>" + i + "</a></li>";
}

if(next){
str += "<li class='page-item'> <a class='page-link' href='" +(endNum + 1)+"'>Next</a></li>";
}

str += "</ul></div>";

console.log(str);

replyPageFooter.html(str);
}

var modal = $(".modal"); //class로 가져올 때 .
var modalInputReply = modal.find("input[name='reply']");
var modalInputReplyer = modal.find("input[name='replyer']");
var modalInputReplyDate = modal.find("input[name='replyDate']");

var modalModBtn = $("#modalModBtn"); //id로 가져올 때 #
var modalRemoveBtn = $("#modalRemoveBtn");
var modalRegisterBtn = $("#modalRegisterBtn");
var modalCloseBtn = $("modalCloseBtn");

//댓글 입력 버튼 눌렀을 때,
$("#addReplyBtn").on("click", function(e){
modal.find("input").val("");
modalInputReplyDate.closest("div").hide();
modal.find("button[id != 'modalCloseBtn']").hide();

modalRegisterBtn.show();

$(".modal").modal("show")
});

  //새로운 댓글 추가 처리 
modalRegisterBtn.on("click", function(e){
var reply = {
reply : modalInputReply.val(),
replyer : modalInputReplyer.val(),
bno : bnoValue
};

replyService.add(
reply,
function(result){
alert("RESELT : " + result);
modal.find("input").val("");
modal.modal("hide");
}
);

//showList(1);
showList(-1); //마지막 페이지로 이동
}); 

//댓글 수정 처리
modalModBtn.on("click", function(e){
var reply = {
rno : modal.data("rno"),
reply : modalInputReply.val()
};

replyService.modify(
reply,
function(result){
alert("RESELT : " + result);
modal.modal("hide");
//showList(1);
showList(pageNum);
}
);
});

//댓글 삭제 처리
modalRemoveBtn.on("click", function(e){
var rno = modal.data("rno");

replyService.remove(
rno,
function(result){
alert("RESELT : " + result);
modal.modal("hide");
//showList(1);
showList(pageNum);
}
);
});


//댓글 페이지 번호 클릭했을 때, 새로운 댓글 가져오는 부분
replyPageFooter.on("click", "li a", function(e){
e.preventDefault();
console.log("Reply pageBtn Click");

var targetPageNum = $(this).attr("href");

console.log("targetPageNum : " + targetPageNum);

pageNum = targetPageNum;
showList(pageNum);
});


//댓글마다 이벤트 걸기
$(".chat").on("click", "li", function(e) {
var rno = $(this).data("rno");
console.log("현재 선택한 댓글 번호 : " + rno);

replyService.get(rno, 
function(reply){
modalInputReply.val(reply.reply);
modalInputReplyer.val(reply.replyer);
modalInputReplyDate.val(replyService.displayTime(reply.replyDate)).attr("readonly", "readonly");
modal.data("rno", reply.rno);

modal.find("button[id != 'modalCloseBtn']").hide();
modalModBtn.show();
modalRemoveBtn.show();

$(".modal").modal("show");
}
);
});



/*
//해당 게시물의 모든 댓글을 가져오는지 확인
replyService.getList(
{bno:bnoValue, page:1}, 
function(list){
for(var i=0, len = list.length||0; i<len; i++){
console.log(list[i]);
}
}
);
*/

/*
//댓글 삭제 테스트
replyService.remove(23, 
function(count){
console.log(count);
if(count == "success"){
alert("REMOVED SUCCESS");
}
}, function(err) {
alert("ERROR,,,");
}
);
*/

/*
//댓글 수정 테스트
replyService.modify(
{rno : 22, bno:bnoValue, reply : "Modify Reply...."},
function(result){
alert("댓글 수정 완료");
}
);
*/

/*
//댓글 조회 테스트
replyService.get(22, 
function(data){
console.log(data + "조회 성공");
}
);
*/

/*
//for replyServuce add test
replyService.add(
{reply : "JS TEST", replyer :"tester", bno:bnoValue},
function(result){
alert("RESELT : " + result);
}
);
*/


var operForm=  $("#operForm");

$("button[data-oper='modify']").on("click", function(e) {
operForm.attr("action", "/board/modify");
operForm.submit();
});

$("button[data-oper='list']").on("click", function(e) {
operForm.find("#bno").remove();
operForm.attr("action", "/board/list");
operForm.submit();
});
});

</script>