해당 프로젝트는 2023/01/25 ~ 2023/03/12 내에 진행되는
아카데미 내 수강생들끼리 팀을 나누어 진행한 모의 프로젝트입니다.
팀원은 5명이었으며, 프로젝트 리더를 맡았습니다.
이전 글 목록
1) 주어진 RFP를 바탕으로 주제 선정 - Spring Project(OTT 서비스)
2) ERD 설계 - Spring Project(OTT 서비스)
3) 회원 가입 기능 구현 - Spring Project (OTT 서비스)
4) 로그인, 로그아웃 기능 구현 - Spring Project (OTT 서비스)
5) 상세 페이지 및 회원 정보 수정 - Spring Project (OTT 서비스)
6) CRUD를 한번에 → 게시판 만들기(QNA게시판) - Spring Project(Mybatis) (OTT 서비스)
페이징 처리
전제 조건
1. GET방식을 이용하여 게시판 리스트를 호출하였다.
2. 페이지당 게시글은 10개로 설정하였으며, ( 전체 게시글의 수 / 한 페이지 당 게시글 수 )보다 페이지 수가 많으면 안된다. 예를들어 총 게시글 수가 132개라면 페이지 수는 14개여야 한다는 뜻이다.
3. 이전, 다음버튼이 존재하며, 한 페이지당 게시글 수를 10개로 설정하였기 때문에, 10개씩 호출되어야 한다. 이전, 다음버튼이 없다면 페이지 수가 많을수록 페이지 번호가 무수히 많이 한 번에 View되어야 할 것이다.
4. 검색 조건과 키워드를 option, keyword로 두었으며, 검색 시에도 페이징처리가 유지되어 있어야 할 것이다.
qna_SQL.xml
<select id="q_list" resultType="com.test.test1.board.qna.dto.QnaDto">
select QUESTION_ID, ID, Q_SUBJECT, Q_CONTENT, ANSWER, Q_CREATE_DATE, PASSWORD, ISLOGINED
from QNA
where <if test="option == 'ID'"> ID like CONCAT('%',#{keyword},'%')</if>
<if test="option == 'SUBJECT'"> Q_SUBJECT like CONCAT('%',#{keyword},'%')</if>
<if test="option == 'CONTENT'"> Q_CONTENT like CONCAT('%',#{keyword},'%')</if>
<if test="option == 'SUBJECT,CONTENT'"> Q_SUBJECT like CONCAT('%',#{keyword},'%')
or Q_CONTENT like CONCAT('%',#{keyword},'%')</if>
<if test="option == null or option == ''">1=1</if>
order by QUESTION_ID desc
limit #{pageStart}, #{perPageNum}
</select>
<select id="listCount" resultType="int">
select count(QUESTION_ID)
from QNA
where <if test="option == 'ID'"> ID like CONCAT('%',#{keyword},'%')</if>
<if test="option == 'SUBJECT'"> Q_SUBJECT like CONCAT('%',#{keyword},'%')</if>
<if test="option == 'CONTENT'"> Q_CONTENT like CONCAT('%',#{keyword},'%')</if>
<if test="option == 'SUBJECT,CONTENT'"> Q_SUBEJECT like CONCAT('%',#{keyword},'%')
or Q_CONTENT like CONCAT('%',#{keyword},'%')</if>
<if test="option == null or option == ''">1=1</if>
</select>
limit을 이용하여 페이징 처리 시 한 페이지당 view될 리스트 개수를 설정했다. pageStart는 해당 페이지의 첫번 째 게시글 idx이며, perPageNum은 한 페이지당 내가 설정한 게시글 개수이다. 10개가 될 것이다.
또한 페이징 처리를 위해 게시물 수를 카운팅 해주는 쿼리를 작성한다. 해당 쿼리는 검색 진행 상황까지 고려하여 작성하였다.
스프링부트를 이용했을 때는 JPA 라이브러리의 Page와 Pageable클래스를 통해 리파지토리에 메서드를 생성하여 쉽게 처리했었다. 이번 프로젝트에서는 JPA를 사용하지 않기 때문에 직접 페이징 처리에 관련된 클래스들을 만들어서 사용해야 했다. 조금 난해했으나 공식화 된 것들이라고 한다.
Criteria.java
해당 패키지 안에 페이징 처리를 위한 DTO나 VO라고 생각하고 사용하였다.
package com.test.test1.board.qna.dto;
public class Criteria {
private int page; //현재 페이지 번호
private int perPageNum; //한 페이지 당 게시물 수
private String keyword, option; //검색 시 페이징 처리 유지를 위한 검색조건들
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public String getOption() {
return option;
}
public void setOption(String option) {
this.option = option;
}
//현재 페이지 게시글 시작 번호
public int getPageStart() {
return (this.page-1)*perPageNum;
}
//첫 접근 시 현재 페이지는 1, 게시물 개수 10개
public Criteria() {
this.page = 1;
this.perPageNum = 10;
}
public int getPage() {
return page;
}
//음수가 될 경우는 시작 페이지를 리턴
public void setPage(int page) {
if(page <= 0) {
this.page = 1;
} else {
this.page = page;
}
}
public int getPerPageNum() {
return perPageNum;
}
//페이지 당 게시글 수가 변하지 않아야 한다
public void setPerPageNum(int pageCount) {
int cnt = this.perPageNum;
if(pageCount != cnt) {
this.perPageNum = cnt;
} else {
this.perPageNum = pageCount;
}
}
@Override
public String toString() {
return "Criteria[page = " + page + ", perPageNum = " + perPageNum + "]";
}
}
PageMaker.java
페이징 버튼을 만들기 위한 계산을 담당하는 클래스이다.
package com.test.test1.board.qna.dto;
public class PageMaker {
private Criteria cri;
private int totalCount;
private int startPage;
private int endPage;
private boolean prev;
private boolean next;
private int displayPageNum = 5; //페이지 버튼 개수
public Criteria getCri() {
return cri;
}
public void setCri(Criteria cri) {
this.cri = cri;
}
public int getTotalCount() {
return totalCount;
}
public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
calcData();
}
private void calcData() {
endPage = (int) (Math.ceil(cri.getPage() / (double) displayPageNum) * displayPageNum);
//시작 페이지 번호 = (끝 페이지 번호 - 화면에 보여질 페이지 번호의 갯수) + 1
startPage = (endPage - displayPageNum) + 1;
if(startPage <= 0) startPage = 1;
//마지막 페이지 번호 = 총 게시글 수 / 한 페이지당 보여줄 게시글 개수
int tempEndPage = (int) (Math.ceil(totalCount / (double) cri.getPerPageNum()));
if (endPage > tempEndPage) {
endPage = tempEndPage;
}
//이전, 다음버튼 생성 여부
prev = startPage == 1 ? false : true;
next = endPage * cri.getPerPageNum() < totalCount ? true : false;
}
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;
}
public int getDisplayPageNum() {
return displayPageNum;
}
public void setDisplayPageNum(int displayPageNum) {
this.displayPageNum = displayPageNum;
}
@Override
public String toString() {
return "PageMaker[totalCount="+totalCount+", startPage="+startPage+", endPage="+endPage+
", prev="+prev+", next="+next+", displayPageNum="+displayPageNum+"]";
}
}
Controller
@RequestMapping("list")
public ModelAndView qnaList(ModelAndView mv, Criteria cri) throws Exception {
PageMaker pageMaker = new PageMaker();
pageMaker.setCri(cri); //page, perpagenum 셋팅
pageMaker.setTotalCount(qnaService.listCount(cri)); //총 게시글 수 셋팅
//View에 페이징 처리를 위한 조건 및 그에 맞는 게시판 리스트 전송
mv.addObject("pageMaker", pageMaker);
mv.addObject("data", qnaService.list(cri));
mv.setViewName("board/qna/qna_list");
return mv;
}
pageMaker 객체에 page와 perpagenum을 셋팅 하고, pageMaker에 총 게시글 수를 셋팅 해 준다.
listCount 조회 시 총 게시물 수는, 조건에 따라 유동적으로 변할 것이다. (검색 진행, 미진행 상황)
조회한 게시물 수와 조건에 맞는 게시물들을 mv에 담은 뒤 view로 리턴 해 주었다.
Service,Impl,DAO
//Service
List<QnaDto> list(Criteria cri) throws Exception;
public int listCount(Criteria cri) throws Exception;
//Service End
//ServiceImpl
@Override
public List<QnaDto> list(Criteria cri) throws Exception {
return qnaDao.list(cri);
}
@Override
public int listCount(Criteria cri) throws Exception {
return qnaDao.listCount(cri);
}
//ServiceImpl End
//DAO
public List<QnaDto> list(Criteria cri) throws Exception {
return ss.selectList("qna.q_list", cri);
}
public int listCount(Criteria cri) {
return ss.selectOne("qna.listCount", cri);
}
//DAO End
jsp
<!-- 검색과 페이징 버튼 이벤트를 서로 다른 폼에 넣어 구현했다. -->
<!-- 검색 버튼 클릭 시 form1에 검색 option, keyword을 담아 Controller에 전송 -->
<form name="form1">
<div class="inner-form">
<div class="input-field first-wrap">
<div class="input-select">
<select id="selectBox" name="option" data-trigger="">
<option value="ID">ID</option>
<option value="SUBJECT">제목</option>
<option value="CONTENT">내용</option>
<option value="SUBJECT,CONTENT">제목+내용</option>
</select>
</div>
</div>
<div class="input-field second-wrap">
<input id="searchh" type="text" placeholder="검색" name="keyword">
</div>
<div class="input-field third-wrap">
<button class="btn-search" type="button" id="searchBtn">
<svg class="svg-inline--fa fa-search fa-w-16" aria-hidden="true" data-prefix="fas" data-icon="search" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<path fill="currentColor" d="M505 442.7L405.3 343c-4.5-4.5-10.6-7-17-7H372c27.6-35.3 44-79.7 44-128C416 93.1 322.9 0 208 0S0 93.1 0 208s93.1 208 208 208c48.3 0 92.7-16.4 128-44v16.3c0 6.4 2.5 12.5 7 17l99.7 99.7c9.4 9.4 24.6 9.4 33.9 0l28.3-28.3c9.4-9.4 9.4-24.6.1-34zM208 336c-70.7 0-128-57.2-128-128 0-70.7 57.2-128 128-128 70.7 0 128 57.2 128 128 0 70.7-57.2 128-128 128z"></path>
</svg>
</button>
</div>
<input type="hidden" name="page" value="1">
</div>
</form>
<!-- 페이징 -->
<!-- form2에 페이징 버튼 클릭 시 현재 페이지 정보와 검색 상황일 때 페이징 유지를 위한 조건을 담아 보냈다 -->
<div>
<form name="form2">
<div id="pagination">
<ul id="pageUL" class="btn-group">
<c:if test="${pageMaker.prev}">
<li class="left">
<a class="left" href='<c:url value="/qna/list?page=${pageMaker.startPage-1}"/>'><i class="fa fa-chevron-left"></i></a>
</li>
</c:if>
<c:forEach begin="${pageMaker.startPage}" end="${pageMaker.endPage}" var="pageNum">
<li class="curPage">
<a href='<c:url value="/qna/list?page=${pageNum}"/>'><i class="fa">${pageNum}</i></a>
</li>
</c:forEach>
<c:if test="${pageMaker.next && pageMaker.endPage >0}">
<li class="right">
<a class="right" href='<c:url value="/qna/list?page=${pageMaker.endPage+1}"/>'><i class="fa fa-chevron-right"></i></a>
</li>
</c:if>
</ul>
</div>
<input id="pageH" type="hidden" name="page" value="${pageMaker.cri.page}">
<input id="keywordH" type="hidden" name="keyword" value="${pageMaker.cri.keyword}">
<input id="optionH" type="hidden" name="option" value="${pageMaker.cri.option}">
</form>
</div>
JS
//selectbox 선택한 값만 검색 - 02.07
$('#searchBtn').click(function(){
if($('input[type=text]').val() == 'undefined' || $('input[type=text]').val() == ''){
alert("검색어를 입력하세요");
return;
}
document.form1.submit();
});
//검색 시 페이징처리 처리 - 02.14
//1) 페이지 버튼 클릭 시 pageNum값을 가지고 form태그로 이동하도록 처리
//2) 화면에 검색키워드가 미리 남겨지도록 처리.
var pagination = document.querySelector("#pagination");
var pageUL = document.querySelector('#pageUL');
pagination.onclick = function() {
event.preventDefault();
if((event.target.className).indexOf("right") != -1){
document.form2.page.value = Number(document.form2.page.value) +5;
document.form2.submit();
return;
}else if((event.target.className).indexOf("left") != -1){
document.form2.page.value = Number(document.form2.page.value) -5;
document.form2.submit();
return;
}else if(event.target.tagName == 'A' || event.target.tagName == 'I'){
document.form2.page.value = event.target.textContent;
document.form2.submit();
}
else return;
}
검색 시 selectbox에 담긴 option의 값만 검색되도록 하였다.
검색 시에 "<", ">" 버튼 클릭 시 에러가 발생하여 임의로 페이지 자체를 강제로 +5, -5처리 해 주었다.
공식처럼 잘 맞물려서 돌아가야 할텐데 처음 하다보니 여기서 실수가 발생했던 것 같다.
해당 버튼 클릭 시의 상황만 좀 더 맞물려서 잘 돌아간다면 페이징처리를 더 완벽하게 할 수 있을 것 같다.
얼른 공부해서 수정할 수 있도록 하자.
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!