Servlet/JSP 답변형 게시판 만들기 - 페이징

코코·2020년 8월 19일
0
post-thumbnail

페이징

구성

  1. 화면으로부터 페이지 번호와 몇 개의 게시물을 볼 것인지(기본1, 10) 데이터를 받는다.
  2. 컨트롤러는 페이징 하는 서비스 메서드를 호출한다. 화면에서 받은 페이지 데이터를 인자로 입력한다.
  3. 요청 받은 페이지의 게시물을 DB에 조회한다.
    게시물 개수가 100개가 넘고, 현재 조회하고 있는 페이지에서 '이전 페이지'버튼이 활성화 되어야 하는지 '다음 페이지'버튼이 활성화되어 있어야 하는지 연산해서 화면으로 데이터를 보낸다.
  4. 화면은 전달 받은 데이터를 띄우고, next, prev 버튼 사용 여부를 결정한다.

SQL

우선 limit을 이용해 페이징 쿼리를 만든다.
limit 0, 10은 1페이지
limit 10, 10은 2페이지
limit 20, 10은 3페이지

limit skip할 개수, 출력할 개수
limit 10, 10이라면, 앞에서부터 10개를 스킵하고 10개의 데이터만 보겠다는 뜻.

mysql.

SELECT * 
FROM (SELECT CASE WHEN LEVEL-1 > 0 then CONCAT(CONCAT(REPEAT('    ', level  - 1),'ㄴ'), t.title)
                ELSE t.title
          END AS title
    , t.bno
    , t.p_bno
    ,t.content
    ,t.id
    ,regdate
    , fnc.level
 FROM
    (SELECT fnc_hierarchy() AS id, @level AS level
       FROM (SELECT @start_with:=0, @id:=@start_with, @level:=0) 
       vars JOIN t_board
        WHERE @id IS NOT NULL) fnc
 JOIN t_board t ON fnc.id = t.bno) t
 order by bno asc limit 0, 10

DESC가 정석이긴 하다. 하지만 아직 방법을 찾지 못했다. 이 쿼리에서 desc를 먹이면 계층이 사라진다.

카운트

게시물의 총 개수를 알아야 몇 페이지까지 보여줄 것인지 연산할 수 있다. 따라서 먼저 total을 받아오는 메서드를 만든다.

테스트

    @Test
    public void getTotalTest() throws ClassNotFoundException {
    	Class.forName(DRIVER);
    	String sql = "SELECT COUNT(*) as total FROM t_board";
    	log.info(sql);
    	try(
            Connection conn = DriverManager.getConnection(URL,USER,PW);
            PreparedStatement pstmt = conn.prepareStatement(sql);
    		ResultSet rs = pstmt.executeQuery();
    		) {
    		rs.next();
    		int total = rs.getInt("total");
    		log.info("total : " + total);
    		assertNotNull(total);
    		assertTrue(total > 0);
    	} catch (Exception e) {
    		log.info(e.getMessage());
    	}
    }

PageVO

public class PageVO {
	private int page, amount;
	
	public PageVO() {
		this(0,10);
	}

	public PageVO(int page, int amount) {
		this.page = page;
		this.amount = amount;
	}
	
	//사실 상 호출하는 메서드는 getPage()가 아니라 이것이다.
	public int getSkip() {
		return (page-1)*10;
	}

	//getter , setter 생략
    	...
}

mysql은 1페이지를 조회하는 조건문이 limit 0, 10이다 따라서 기본 값을 0, 10으로 지정했다.
getSkip()은 얼마나 스킵할 것인가를 계산하기 위해 만들었다. 만약 2페이지로 이동한다면, page값으로 2를 넘겨 받는다.
mysql은 2페이지를 조회하는 조건문이 limit 2, 10이 아니라 10, 10이다.
따라서 인자로 받은 2에 1을 뺀 다음 10을 곱하는 형태로 skip할 숫자를 계산한다. 3페이지, 4페이지도 마찬가지다.

PageOper

public class PageOper {
	private int startPage, endPage;
	private boolean prev, next;
	private int total;
	private PageVO pageVO;
	
	public PageOper(PageVO vo, int total) {
		this.pageVO = vo;
		this.total = total;
		
		this.endPage = (int)(Math.ceil(pageVO.getPage() / 10.0)) * 10;
		this.startPage = this.endPage - 9;
		
		int realEnd = (int)(Math.ceil((total * 1.0)/pageVO.getAmount()));
	
		if(realEnd < this.endPage) {
			this.endPage = realEnd;
		}
		
		this.prev = this.startPage > 1;
		this.next = this.endPage < realEnd;
	}
    
    //Getter 생략..
}

시작 페이지와 마지막 페이지를 연산해서 '다음'이 필요한지, 혹은 '이전'버튼이 필요한지 나타내는 클래스다.
그리고 실제로 끝나야 하는 페이지까지 계산한다. 만약 345개의 게시물이 있다면, 게시판은 40페이지가 아닌 35페이지에서 끝나야 한다.

Test

    @Test
    public void getListPaging() throws ClassNotFoundException {
    	
    	PageVO vo = new PageVO();

    	Class.forName(DRIVER);
    	String sql = "SELECT *\r\n" + 
    			"FROM \r\n" + 
    			"	(SELECT CASE WHEN LEVEL-1 > 0 then CONCAT(CONCAT(REPEAT('    ', level  - 1),' '), t.title)\r\n" + 
    			"                ELSE t.title\r\n" + 
    			"          END AS title\r\n" + 
    			"    , t.bno\r\n" + 
    			"    , t.p_bno\r\n" + 
    			"    ,t.content\r\n" + 
    			"    ,t.id\r\n" + 
    			"    ,regdate\r\n" + 
    			"    , fnc.level\r\n" + 
    			" FROM\r\n" + 
    			"    (SELECT fnc_hierarchy() AS id, @level AS level\r\n" + 
    			"       FROM (SELECT @start_with:=0, @id:=@start_with, @level:=0) \r\n" + 
    			"       vars JOIN t_board\r\n" + 
    			"        WHERE @id IS NOT NULL) fnc\r\n" + 
    			" JOIN t_board t ON fnc.id = t.bno) t\r\n" + 
    			" ORDER BY bno limit ?, ?" + 
    			";";
    	log.info(sql);
    	try(
            Connection conn = DriverManager.getConnection(URL,USER,PW);
            PreparedStatement pstmt = conn.prepareStatement(sql);
    		) {
    		
    		vo.setPage(1);	//1페이지
    		vo.setAmount(10);	//10개의 게시물
    		
    		pstmt.setInt(1, vo.getSkip());
    		pstmt.setInt(2, vo.getAmount());
    		
    		ResultSet rs = pstmt.executeQuery();
    		
    		assertNotNull(rs);
    		
    		while (rs.next()) {
    			int bno = rs.getInt("bno");
    			String title = rs.getString("title");
    			String content = rs.getString("content");
    			String id = rs.getString("id");

    			log.info("bno : " + bno);
    			log.info("title : " + title);
    			log.info("content : " + content);
    			log.info("id : " + id);
    		}
    		
    		//임의의 수
    		int total = 515;
    		
    		log.info("page info : " + new PageOper(vo, total));
    		
    	} catch (Exception e) {
    		log.info(e.getMessage());
    	}
    }

페이징 처리 프로젝트 적용

DAO


//전체 게시물 수를 조회하는 메서드 PageOper 사용할 때 필요
	public int getTotal() {
    	String sql = "SELECT COUNT(*) as total FROM t_board";
    	log.info(sql);
    	try(
            Connection conn = ds.getConnection();
            PreparedStatement pstmt = conn.prepareStatement(sql);
    		ResultSet rs = pstmt.executeQuery();
    			) {
    		rs.next();
    		
    		return rs.getInt("total");
    		
    	} catch (Exception e) {
    		log.info(e.getMessage());
    	}
    	
    	return -1;
	}

//요청 받은 페이지 데이터를 조회하는 메서드.(기존 getList() 약간 수정한 것임)
	public List<BoardVO> getList(PageVO page) {
		List<BoardVO> list = new ArrayList<>();
		
		String sql = "SELECT CASE WHEN LEVEL-1 > 0 then CONCAT(CONCAT(REPEAT('    ', level  - 1),''), t.title)\r\n" + 
				"                 ELSE t.title\r\n" + 
				"           END AS title\r\n" + 
				"     , t.bno\r\n" + 
				"     , t.p_bno\r\n" + 
				"     ,t.content\r\n" + 
				"     ,t.id\r\n" + 
				"     ,regdate\r\n" + 
				"     , fnc.level\r\n" + 
				"  FROM\r\n" + 
				"     (SELECT fnc_hierarchy() AS id, @level AS level\r\n" + 
				"        FROM (SELECT @start_with:=0, @id:=@start_with, @level:=0) vars\r\n" + 
				"          JOIN t_board\r\n" + 
				"         WHERE @id IS NOT NULL) fnc\r\n" + 
				"  JOIN t_board t ON fnc.id = t.bno " +
				"limit ?, ?";
		
		log.info(sql);
		
		try(	//try with resources
			Connection conn = ds.getConnection();
			PreparedStatement pstmt = conn.prepareCall(sql);
			) {

			pstmt.setInt(1, page.getSkip());
			pstmt.setInt(2, page.getAmount());
			
			ResultSet rs = pstmt.executeQuery();
			
			while(rs.next()) {
				int level = rs.getInt("level");
				int bno = rs.getInt("bno");
				int p_bno = rs.getInt("p_bno");
				String title = rs.getString("title");
				String content = rs.getString("content");
				String id = rs.getString("id");
				Date regdate = rs.getDate("regdate");

				BoardVO vo = new BoardVO(bno, p_bno, level, title, content, id, regdate);
				list.add(vo);

			} // while
			
			
			return list;
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return null;
	} //getList()

크게 달라진 것은 없다. getList()가 매개변수로 PageVO를 받는다는 것이 달라졌다.

service

	@Override
	public int getTotal() {
		return dao.getTotal();
	}
    
    	@Override
	public List<BoardVO> getList(PageVO page) {
		return dao.getList(page);
	}

controller

if(action == null || action.equals("/list")) {
	//화면으로부터 페이지 데이터를 받아서 PageVO를 초기화한다.
	int pageNum = request.getParameter("page") != null || Integer.parseInt(request.getParameter("page")) > 0 ? 
		Integer.parseInt(request.getParameter("page")) : 1;
	
    	int pageAmount = request.getParameter("amount") != null || Integer.parseInt(request.getParameter("amount")) > 0 ?
		Integer.parseInt(request.getParameter("amount")) : 10;
					
					
	PageVO page = new PageVO(pageNum, pageAmount);
								
			
	//전체 게시물의 개수를 조회한다.
	int total = boardService.getTotal();
			
	//요청받은 페이지 데이터를 화면으로 전달한다.
	request.setAttribute("boardList", boardService.getList(page));
			
	//다음 페이지 또는 이전 페이지, 어떤 것을 나타내야 하는지 연산한 결과를 화면으로 보낸다.
	request.setAttribute("page", new PageOper(page, total));
	nextPage="/WEB-INF/board/list.jsp";
}

list페이지 요청을 처리하던 조건문이다. 자세한 건 주석에 적어두었다.

view

list.jsp 수정

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판</title>
<style>
.cls1 {
	text-decoration:none;
}

.cls2 {
	text-align:center;
	font-size:30px;
}
</style>
</head>
<body>
<h1 align='center'>자유 게시판</h1>
<table align='center' border='1' width='80%'>
	<tr height='10' align='center' bgcolor='lightgreen'>
		<td>번호</td>
		<td>작성자</td>
		<td>제목</td>
		<td>작성일</td>
	</tr>
	<c:choose>
		<c:when test='${boardList == null}'>
			<td colspan="4">
				<p align="center">
					<b><span style='font-size=9pt;'>등록된 글이 없습니다.</span></b>
				</p>
			</td>
		</c:when>
		
		<c:when test="${boardList != null }">
			<c:forEach var="post" items="${boardList }" varStatus="postNum">
			<tr align="center">
				<td width="5%">${post.bno}</td>
				<td width="10%">${post.id }</td>
				<td align='left' width="35%">
				<span style='padding-right:30px'></span>
			<c:choose>
				<c:when test="${post.level > 1 }">
					<c:forEach begin="1" end="${post.level }">
						<span style='padding-left:20px'></span>
					</c:forEach>
					<span style='font-size:12px;'>[답변]</span>
					<a class="cls1" href='/board/post?no=${post.bno }'>${post.title }</a>
				</c:when>
				
				<c:otherwise>
					<a class='cls1' href='/board/post?no=${post.bno }'>${post.title }</a>
				</c:otherwise>
			</c:choose>
			</td>
			<td width="10%">
				<fmt:formatDate value="${post.regdate }"/>
			</td>
			</tr>
			</c:forEach>
		</c:when>
	</c:choose>
  
  <!-- 페이징 처리 부분 -->
	<td colspan="4">
	<p align="center"><b><span style='font-size=9pt;'>
	<c:if test="${page.prev }">
		<a href="/board/list?page=${page.startPage -1 }">이전 페이지</a>
	</c:if>
&nbsp;<c:forEach var="num" begin="${page.startPage }" end="${page.endPage }">
			<a href="/board/list?page=${num }">${num }</a> &nbsp;
	</c:forEach>
	<c:if test="${page.next }">
		<a href="/board/list?page=${page.endPage + 1 }">다음 페이지</a>
	</c:if>
	</span></b>
	</p>
	</td>
<!--End Paging -->
  </table>
  
<a class='cls1' href='/board/write'><p class='cls2'>글쓰기</a>

<script>
	window.onload = () => {
		const removeResult = '${result }'
		if(removeResult == 'success') {
			alert('삭제 되었습니다.')
		} else if(removeResult=='fail') {
			alert('다시 시도하세요');
			return;
		}
	}
</script>
</body>
</html>

첫 페이지와 마지막 페이지

0개의 댓글