본문 바로가기
프로그래밍/스프링 & 스프링 부트

스프링 게시판 jQuery 페이징 처리

by 밍구몬 2019. 5. 27.

jQuery를 이용한 페이징 처리 예제이고, 디비는 OracleDB를 사용하였다.

 

SQL

//게시판 테이블
CREATE TABLE boardTest(
	idx NUMBER(10,0),
	writer VARCHAR2(20) NOT NULL,
	title VARCHAR2(30) NOT NULL,
	content LONG NOT NULL,
	reg_date DATE NOT NULL,
	cnt int default 0
);

//시퀀스 생성
CREATE SEQUENCE tmp_seq ;

//제약조건 추가
alter table boardTest add constraint pk_board primary key (idx);

INSERT INTO boardtest(idx, title,writer,content,reg_date) VALUES(tmp_seq.NEXTVAL, tmp_seq.currval||'번째 글', 'test', tmp_seq.currval||'번째 글입니다.', (select sysdate from dual));

이전에 만들었던 테이블은 지우고 새로 만들었다.

글의 고유한 값을 가지게 하기 위하여 idx를 사용하였고 pk_board라는 제약조건을 걸어주었다.

pk_board라는 이름으로 제약조건을 건 이유는 나중에 빠르게 검색하기 위하여 오라클 힌트를 사용하기 때문이다.

맨 마지막 insert문은 테스트 insert문으로 쉽게 데이터를 넣어 확인을 하라고 넣어 두었다.

나는 Oracle SQL Developer를 이용하여 테스트 데이터를 삽입해 두었다.

오라클은 데이터를 삽입하거나 테이블을 만들고 나서 commit을 꼭 해주어야 한다 !!

 

PagingCriteria

vo를 만들어 준곳에 PagingCriteria.java를 생성해 준다.

package com.wipia.study.domain;

public class PagingCriteria {
	
	private int pageNum=1;	//페이지 번호
	private int amount=10;	//페이지당 데이터 갯수

	public int getPageNum() {
		return pageNum;
	}

	public void setPageNum(int pageNum) {
		this.pageNum = pageNum;
	}

	public int getAmount() {
		return amount;
	}

	public void setAmount(int amount) {
		this.amount = amount;
	}

	@Override
	public String toString() {
		return "PagingCriteria [pageNum=" + pageNum + ", amount=" + amount + "]";
	}
	
}

페이지 번호를 알 수있는 pageNum과 페이지당 몇개의 데이터를 보여줄지 정하는 amount를 만들고 getter와 setter를 만들어 준다.

PageMaker

package com.wipia.study.domain;

public class PageMaker {
	
	private int startPage;
	private int endPage;
	private boolean prev;
	private boolean next;
	
	private PagingCriteria cri;
	
	public PageMaker(PagingCriteria cri,int total){
		this.cri=cri;
		int realEnd = (int)(Math.ceil((total * 1.0) / cri.getAmount()));
		this.endPage = (int)(Math.ceil(cri.getPageNum() / 10.0) * 10);
		this.startPage = getEndPage()-9;
		
		if(realEnd < this.endPage) {
			this.endPage=realEnd;
		}
		
		this.next = getEndPage() < realEnd;
		this.prev = getStartPage()>1;
		
	}
	
	public PagingCriteria getCri() {
		return cri;
	}

	public void setCri(PagingCriteria cri) {
		this.cri = cri;
	}

	public int getStartPage() {
		return startPage;
	}

	public void setStartPage(int startPage) {
		this.startPage = startPage;
	}

	public int getEndPage() {
		return endPage;
	}

	public void setEndPage(int endPage) {
		this.endPage = endPage;
	}

	public boolean isPrev() {
		return prev;
	}

	public void setPrev(boolean prev) {
		this.prev = prev;
	}

	public boolean isNext() {
		return next;
	}

	public void setNext(boolean next) {
		this.next = next;
	}

	@Override
	public String toString() {
		return "PageMaker [startPage=" + startPage + ", endPage=" + endPage + ", prev=" + prev + ", next=" + next + "]";
	}
}

그 다음 같은 위치에 PageMaker를 만들어 준다.

페이지메이커에서는 PagingCriteria와 total을 받아준다.

*Math.ceil : 올림

endPage는 페이징의 끝 번호를 나타내며, (페이지번호 /10)을 올림한 값에 10을 곱해준다.

startPage는 페이징의 시작 번호를 나타내며, (endPage - (amount-1)) 의 값이 시작 페이지 번호이다.

realEnd는 (전체 게시글 수 / 페이지당 글의 갯수)로 계산하며, endPage의 값을 바로잡아? 주기 위하여 사용된다.

전체 데이터의 수가 100개 미만 60개로 가정하면 endPage는 ceil(6 / 10)*10=10이 되므로 6으로 바로잡아주기 위하여 사용된다.

 

BoardController

package com.wipia.study.controller;

import java.io.IOException;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.wipia.study.domain.BoardVO;
import com.wipia.study.domain.PageMaker;
import com.wipia.study.domain.PagingCriteria;
import com.wipia.study.service.BoardService;

@Controller
@SessionAttributes("board")
public class BoardController {
	
	@Autowired
	private BoardService boardService;
	
	//글 목록
	@RequestMapping("/getBoardList.do")
	public String getBoardList(PagingCriteria cri, Model model) {
		
		List<BoardVO> boardList = boardService.getBoardList(cri);
		
		int total = boardService.totalCnt();
		
		// Model 정보 저장
		model.addAttribute("boardList",boardList);
		model.addAttribute("paging",new PageMaker(cri,total));
		return "boardList"; // View 이름 리턴
	}
	
	// 글 상세 조회
	@RequestMapping("/getContent.do")
	public String getBoard(BoardVO vo, Model model, @ModelAttribute("cri") PagingCriteria cri) {
		model.addAttribute("board", boardService.getContent(vo)); // Model 정보 저장
		return "content"; // View 이름 리턴
	}
	
	// 글 쓰기
	@RequestMapping(value="/insertBoard.do", method=RequestMethod.POST) 
	public String insertBoard(BoardVO vo) throws IOException {
			
			boardService.insertBoard(vo); 
			return "redirect:getBoardList.do"; 
	}
	
	// 글 쓰기 페이지 이동
	@RequestMapping("/writeBoard.do") 
	public String moveInsertBoard()throws Exception{
		return "insertBoard";
	}
	 

	// 글 수정
	@RequestMapping("/updateBoard.do")
	public String updateBoard(@ModelAttribute("board") BoardVO vo, @ModelAttribute("cri") PagingCriteria cri,RedirectAttributes rttr) {
		
		if(boardService.updateBoard(vo)) {
			rttr.addFlashAttribute("result","success");
		}
		
		rttr.addAttribute("amount",cri.getAmount());
		rttr.addAttribute("pageNum",cri.getPageNum());
		
		return "redirect:getBoardList.do";
	}

	// 글 삭제
	@RequestMapping("/deleteBoard.do")
	public String deleteBoard(BoardVO vo) {
		boardService.deleteBoard(vo);
		return "redirect:getBoardList.do";
	}
	
}

boardController에서 볼 내용은 글 목록과 글 수정이다.

글 목록에서 jsp의 Criteria를 받아온다.

boardService의 totalCnt를 호출하여 전체 게시글 수(total)를 받아와 pageMaker에 Criteria와 함께 보내 계산하여 model에 paging정보를 저장한다.

 

글 수정에서는 RedirectAttributes를 이용하여 만약 100번째 글을 수정하였을 경우 첫 번째 페이지가 아닌 100번째 글이 위치한 페이지를 보여주기위해 수정하였다.

이 기능이 필요 없다면, 글 수정페이지는 따라하지 않아도 된다.

글 수정에서도 cri의 정보를 받아와 redirect되는 페이지에 받아온 cri의 값을 넘겨주었다.

 

BoardService

package com.wipia.study.service;

import java.util.List;

import com.wipia.study.domain.BoardVO;
import com.wipia.study.domain.PagingCriteria;

public interface BoardService {
	
	// 글 목록 조회
	List<BoardVO> getBoardList(PagingCriteria paging);
	
	// 글 상세 조회
	BoardVO getContent(BoardVO vo);
	
	// 글 등록
	void insertBoard(BoardVO vo);

	// 글 수정
	boolean updateBoard(BoardVO vo);

	// 글 삭제
	void deleteBoard(BoardVO vo);
	
	//글 갯수
	int totalCnt();
	
}

BoardServiceImpl

package com.wipia.study.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.wipia.study.dao.BoardDAO;
import com.wipia.study.domain.BoardVO;
import com.wipia.study.domain.PagingCriteria;
import com.wipia.study.service.BoardService;

@Service
public class BoardServiceImpl implements BoardService{

	@Autowired
	private BoardDAO boardDAO;
	
	@Override
	public List<BoardVO> getBoardList(PagingCriteria paging) {
		return boardDAO.getBoardList(paging);
	}
	
	@Override
	public BoardVO getContent(BoardVO vo) {
		return boardDAO.getContent(vo);
	}

	@Override
	public void insertBoard(BoardVO vo) {
		boardDAO.insertBoard(vo);
	}

	@Override
	public boolean updateBoard(BoardVO vo) {
		return boardDAO.updateBoard(vo);
	}

	@Override
	public void deleteBoard(BoardVO vo) {
		boardDAO.deleteBoard(vo);
	}

	@Override
	public int totalCnt() {
		return boardDAO.totalCnt();
	}
	

}

boardService에서 변경된 내용은 getBoardList의 파라미터, 추가된 내용은 totalCnt다.

cri정보를 DAO로 넘겨 필요한 만큼 데이터를 받아온다.

 

BoardDAO

package com.wipia.study.dao;

import java.util.List;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.wipia.study.domain.BoardVO;
import com.wipia.study.domain.PagingCriteria;

@Repository
public class BoardDAO{
	
	@Autowired
	private SqlSessionTemplate mybatis;

	public void insertBoard(BoardVO vo) {
		System.out.println("===> Mybatis로 insertBoard() 기능 처리");
		mybatis.insert("BoardMapper.insertBoard", vo);
	}

	public boolean updateBoard(BoardVO vo) {
		System.out.println("===> Mybatis로 updateBoard() 기능 처리");
		return mybatis.update("BoardMapper.updateBoard", vo) == 1;
	}

	public void deleteBoard(BoardVO vo) {
		System.out.println("===> Mybatis로 deleteBoard() 기능 처리");
		mybatis.delete("BoardMapper.deleteBoard", vo);
	}

	public BoardVO getContent(BoardVO vo) {
		System.out.println("===> Mybatis로 getContent() 기능 처리");
		return (BoardVO) mybatis.selectOne("BoardMapper.getContent", vo);
	}

	public List<BoardVO> getBoardList(PagingCriteria paging) {
		System.out.println("===> Mybatis로 getBoardList() 기능 처리");
		return mybatis.selectList("BoardMapper.getBoardList",paging);
	}
	
	public int totalCnt() {
		System.out.println("===> Mybatis로 totalCnt");
		return mybatis.selectOne("BoardMapper.getTotalCnt");
	}
	
}

DAO는 Mapper로 값을 받아와 리턴해 주기때문에 딱히 설명이 필요없을 것 같아서 생략한다.

 

testMapper

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="BoardMapper">
	
	<select id="getBoardList" resultType="BoardVO">
		<![CDATA[
			SELECT idx,title,writer,reg_date,cnt
			FROM	(SELECT /*+INDEX_DESC(boardTest pk_board) */
						rownum rn, idx, title, writer,reg_date,cnt 
					from boardTest where rownum <= #{pageNum} * #{amount})
			where rn > (#{pageNum}-1) * #{amount}
			ORDER BY idx DESC
		]]>
	</select>
	
	<select id="getContent" resultType="BoardVO">
		<![CDATA[
			SELECT *
			FROM boardtest
			WHERE idx = #{idx}
		]]>
	</select>
	
	<select id="insertBoard">
		<![CDATA[
		INSERT INTO boardtest(idx, title,writer,content,reg_date)
		VALUES(tmp_seq.NEXTVAL, #{title}, #{writer}, #{content}, (select sysdate from dual))
		]]>
	</select>
	
	<update id="updateBoard">
		<![CDATA[
		UPDATE boardtest SET
		title = #{title},
		content = #{content}
		WHERE idx = #{idx}
		]]>
	</update>

	<delete id="deleteBoard">
		<![CDATA[
		DELETE FROM boardtest
		WHERE idx = #{idx}
		]]>
	</delete>
	
	<select id="getTotalCnt" resultType="int">
		SELECT count(*)
		FROM boardTest
	</select>
</mapper>

getBoardList를 보면 FROM절에 sql hint를 추가하였다.

sql hint를 잘 모르겠다면 검색을 하여 더 좋은 설명을 보는것을 추천한다.

hint를 이용하여 최신글부터 보이도록 INDEX_DESC를 해주었다. where절에 보면 rownum은 (페이지 수 * 페이지당 게시글 수)로 계산하였는데 필요한 갯수만큼 가져와 아래의 where절에서 페이지에 맞는 데이터를 뽑아주도록 만들어 졌다.

 

getTotalCnt는 그냥 전체 글 수를 센다.

 

boardList.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jstl/core_rt" prefix="c"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>글 목록</title>
	<script src="resources/jQuery/jquery-3.4.1.min.js"></script>
</head>
<body>
	<a href="/">메인</a>
	<table border="1" >
		<tr>
			<th bgcolor="" width="50">no</th>
			<th bgcolor="" width="200">제목</th>
			<th bgcolor="" width="150">작성자</th>
			<th bgcolor="" width="150">작성일</th>
			<th bgcolor="" width="100">조회수</th>
		</tr>
		<c:choose>
			<c:when test="${!empty boardList}">
				<c:forEach items="${boardList }" var="board">
					<tr>
						<td>${board.idx }</td>
						<td align="left"><a href="${board.idx }">
								${board.title }</a></td>
						<td>${board.writer }</td>
						<td><fmt:formatDate value="${board.regDate }" pattern="yyyy-MM-dd"/></td>
						<td>${board.cnt }</td>
					</tr>
				</c:forEach>
			</c:when>
			<c:otherwise>
				<tr>
					<td colspan="5">등록된 글이 없습니다.</td>
				</tr>
			</c:otherwise>
		</c:choose>
	</table>
	<br>
	<a href="writeBoard.do">글 쓰기</a>
	<div id="pagingDiv">
			<c:if test="${paging.prev}">
				<a href="${paging.startPage - 1 }">이전</a>
			</c:if>
			<c:forEach var="num" begin="${paging.startPage}" end="${paging.endPage }">
				&nbsp;<a href="${num }">${num }</a>&nbsp;
			</c:forEach>
			<c:if test="${paging.next}">
				<a id="next" href="${paging.endPage + 1 }">다음</a>
			</c:if>
	</div>
	
	<form id="pagingFrm" name="pagingForm" action="getBoardList.do" method="get">
		<input type="hidden" id="pageNum" name="pageNum" value="${paging.cri.pageNum }">
		<input type="hidden" id="amount" name="amount" value="${paging.cri.amount }">
	</form>
</body>

<script type="text/javascript">
	$(document).ready(function(){
		
		//페이지 번호 이동
		$('#pagingDiv a').click(function(e){
			e.preventDefault();
			$('#pageNum').val($(this).attr("href"));
			pagingForm.submit();
			
		});
		
		//게시글에 pageNum넘기기
		$('table a').click(function(e){
			e.preventDefault();
			var html = "<input type='hidden' name='idx' value='"+$(this).attr("href")+"'>";
			$('#pagingFrm').append(html);
			$('#pagingFrm').attr("action","getContent.do");
			$('#pagingFrm').submit();
		});
	});
</script>

</html>

위는 boardList의 전체 소스이고, 추가된 내용은 jstl을 이용한 페이징 처리와 jQuery이다.

	<div id="pagingDiv">
			<c:if test="${paging.prev}">
				<a href="${paging.startPage - 1 }">이전</a>
			</c:if>
			<c:forEach var="num" begin="${paging.startPage}" end="${paging.endPage }">
				&nbsp;<a href="${num }">${num }</a>&nbsp;
			</c:forEach>
			<c:if test="${paging.next}">
				<a id="next" href="${paging.endPage + 1 }">다음</a>
			</c:if>
	</div>
	
	<form id="pagingFrm" name="pagingForm" action="getBoardList.do" method="get">
		<input type="hidden" id="pageNum" name="pageNum" value="${paging.cri.pageNum }">
		<input type="hidden" id="amount" name="amount" value="${paging.cri.amount }">
	</form>

이전을 누르게 되면 startPage -1 다음을 누르게 되면 endPage +1을 해준다.

forEach문을 통하여 시작페이지와 끝페이지만큼 반복하여 번호를 출력해 준다.

 

form의 데이터는 pageNum과 amout값을 넘기기 위하여 넣어주었다.

<script type="text/javascript">
	$(document).ready(function(){
		
		//페이지 번호 이동
		$('#pagingDiv a').click(function(e){
			e.preventDefault();
			$('#pageNum').val($(this).attr("href"));
			pagingForm.submit();
			
		});
		
		//게시글에 pageNum넘기기
		$('table a').click(function(e){
			e.preventDefault();
			var html = "<input type='hidden' name='idx' value='"+$(this).attr("href")+"'>";
			$('#pagingFrm').append(html);
			$('#pagingFrm').attr("action","getContent.do");
			$('#pagingFrm').submit();
		});
	});
</script>

페이지 번호를 누르게 되면, 이벤트를 취소하고 pageNum의  value를 입력받은 값으로 변경한뒤 submit시킨다.

아래의 게시글에 pageNum을 넘기는 내용은 게시글을 보고나서 목록을 눌렀을 경우 1페이지가 아닌 이전 페이지로 돌아가기 위하여 만들었다. 필요없다면 패스..

content.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jstl/core_rt" prefix="c"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring" %>
<%@taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<title>글 상세</title>
	<script src="resources/jQuery/jquery-3.4.1.min.js"></script>
</head>
<body>
	<h1>글 상세</h1>
	<hr>
	<form id="frm" action="updateBoard.do" method="post">
		<input name="seq" type="hidden" value="${board.idx}" />
		<table border="1">
			<tr>
				<td bgcolor="orange" width="70">제목</td>
				<td align="left"><input id="title" name="title" type="text"
					value="${board.title }" /></td>
			</tr>
			<tr>
				<td bgcolor="orange">작성자</td>
				<td align="left">${board.writer }</td>
			</tr>
			<tr>
				<td bgcolor="orange">내용</td>
				<td align="left"><textarea id="content" name="content" cols="40" rows="10">${board.content }</textarea></td>
			</tr>
			<tr>
				<td bgcolor="orange">등록일</td>
				<td align="left"><fmt:formatDate value="${board.regDate }" pattern="yyyy-MM-dd"/></td>
			</tr>
			<tr>
				<td bgcolor="orange">조회수</td>
				<td align="left">${board.cnt }</td>
			</tr>
			<tr>
				<td colspan="2" align="center"><input type="button" id="modify" value="글 수정" /></td>
			</tr>
		</table>
	</form>
	<hr>
	<a href="writeBoard.do">글 쓰기</a>&nbsp;&nbsp;&nbsp; 
	<a href="deleteBoard.do?idx=${board.idx }">글 삭제</a>&nbsp;&nbsp;&nbsp;
	<a id="list" href="getBoardList.do">글 목록</a>
</body>
<script type="text/javascript">
	$(document).ready(function (e){
		
		//수정하기 버튼
		$('#modify').click(function(){
			var frmArr = ["title","content"];

			$('#frm').append("<input type='hidden' name='pageNum' value='<c:out value='${cri.pageNum }'/>'>");
			$('#frm').append("<input type='hidden' name='amount' value='<c:out value='${cri.amount }'/>'>");
			
			//입력 값 널 체크
			for(var i=0;i<frmArr.length;i++){
				//alert(arr[i]);
				if($.trim($('#'+frmArr[i]).val()) == ''){
					alert('빈 칸을 모두 입력해 주세요. -'+frmArr[i]);
					$('#'+frmArr[i]).focus();
					return false;
				}
			}
			//전송
			$('#frm').submit();
		});
		
		//글 목록
		$('#list').click(function(e){
			e.preventDefault();
			var $form = $('<form></form>');
			$form.attr('action','getBoardList.do');
			$form.attr('method','get');
			$form.appendTo('body');
			
			$form.append("<input type='hidden' name='pageNum' value='<c:out value='${cri.pageNum}'/>'>");
			$form.append("<input type='hidden' name='amount' value='<c:out value='${cri.amount}'/>'>");
			$form.submit();
		});
	});
</script>
</html>

내용을 보고 수정할 수 있는 content페이지에서는 수정하거나 목록 버튼을 눌렀을 시 이전 페이지로 돌아가도록 변경해 주었다.

수정하기 버튼을 눌렀을 경우 frm폼에 이전 페이지에서 받아온 pageNum과 amount값을 추가해 수정 후 이전에 있던 페이지로 이동하도록 만들어 주었고,

글 목록 버튼을 눌렀을 경우 새로운 폼을 생성해 이전에 있던 페이지로 보내주었다.

 

 

소스코드는 https://github.com/ming9mon/spring/tree/master/Study 에서 다운받을 수 있다.