[Spring] 페이징 + 검색 처리 기능 구현

yunSeok·2023년 10월 19일
1

사이드 프로젝트

목록 보기
13/14

사이드 프로젝트에서 공지사항 부분을 구현할때 페이징 처리를 적용해보도록 하겠습니다.

페이징 처리는 Map 객체의 사용이 중요하다고 볼 수 있는데요,
간단하게 'Map'을 사용하면 데이터페이징 정보결합하여 단일 객체로 사용할 수 있다고 생각하면 될 거 같아요.


1. 구현화면

다 구현됐을 때의 화면 먼저 보여드리겠습니다!!


2. 구현 순서

  1. Pager class
  2. DAO, Service, Controller
  3. Mapper
  4. JSP

2-1. Pager 클래스

@Data
public class Pager {
	
	private int pageNum;    // 페이지의 번호
	private int totalBoard; // 게시글의 개수
	private int pageSize;   // 페이지에 출력되는 게시글 개수 (n개씩 보기)
	private int blockSize;  // 한 블럭에 출력되는 페이지 번호 개수

	private int totalPage;  // 전체 페이지 개수
	private int startRow;   // 요청 페이지에 출력되는 게시글의 시작 행번호
	private int endRow;     // 요청 페이지에 출력되는 게시글의 종료 행번호
	private int startPage;  // 블럭에 출력되는 시작 페이지 번호
	private int endPage;    // 블럭에 출력되는 종료 페이지 번호
	private int prevPage;   // 이전 블럭에 출력되는 시작 페이지 번호
	private int nextPage;   // 다음 블럭에 출력되는 시작 페이지 번호
	
	public Pager(int pageNum, int totalBoard, int pageSize, int blockSize) {
		super();
		this.pageNum = pageNum;
		this.totalBoard = totalBoard;
		this.pageSize = pageSize;
		this.blockSize = blockSize;
		
		calcPage();
	}
	
	private void calcPage() {
		// 총 게시물 수에 페이지당 표시되는 게시물 수를 나눠 전체 페이지의 개수를 구합니다.
		totalPage=(int)Math.ceil((double)totalBoard/pageSize);
		
		// 페이지 번호가 전체 페이지 수 보다 큰 경우 페이지 번호를 1로 설정
		if(pageNum<=0 || pageNum>totalPage) {
			pageNum=1;
		}
		
		// 시작 및 끝 행 번호
		startRow=(pageNum-1)*pageSize+1;
 		endRow=pageNum*pageSize;
		if(endRow>totalBoard) {
			endRow=totalBoard;
		}
		
		// 한 블럭에 출력되는 행 개수를 기준으로 페이지 번호를 계산
		startPage=(pageNum-1)/blockSize*blockSize+1;
		endPage=startPage+blockSize-1;
		if(endPage>totalPage) {
			endPage=totalPage;
		}
		
		// 이전 및 다음 블럭의 시작 페이지 번호 계산
		prevPage=startPage-blockSize;
		nextPage=startPage+blockSize;
	}
	
}

Pager 클래스에서는 calcPage() 메소드를 사용해 페이징을 계산하는 과정들이 있는데 이 부분이 어려운 거 같습니다.. 학원 다닐 당시 class 자체는 알려주긴해서 바로 적용하는데에 오래걸리진 않았지만, 현재 스스로 프로젝트를 진행하면서 다시 봤는데 조금 어렵더라구요.. 다시 보면서 코드에 주석으로 설명을 써놓긴했는데,
사용하기 전에 어떤 방식으로 계산되어 지는지는 조금 투자해서 봐주세요!!

DTO에서 설명을 추가 하자면
위 사진을 봤을때
totalBoard은 54
pageSize은 10
blockSize은 5
로 설정되어 있다고 생각하시면 될거같아요.


2-2. DAO, Service, Controller

(1). DAO

public interface NoticeDAO {
   
    // 페이징 처리 된 전체 게시글 수 조회
    int selectNoticeCount(String selectKeyword); 
   
    // 페이징된 공지사항 목록 조회
    List<Notice> selectNoticeList(Map<String, Object> map);
	   
}

검색어 처리까지 구현하기 위해 selectKeyword 변수를 사용했습니다.

(2). DAOImpl

@Repository
@RequiredArgsConstructor
public class NoticeDAOImpl implements NoticeDAO{
	private final SqlSession sqlSession;
	
	// 전체 게시글 수 조회
	@Override
	public int selectNoticeCount(String selectKeyword) {
		return sqlSession.getMapper(NoticeMapper.class).selectNoticeCount(selectKeyword);
	}

	// 페이징된 게시글 리스트 조회
	@Override
	public List<Notice> selectNoticeList(Map<String, Object> map) {
		return sqlSession.getMapper(NoticeMapper.class).selectNoticeList(map);
	}

}

(3). Service

public interface NoticeService {
	
	// 공지사항 리스트 조회 (페이징 처리 + 검색 기능)
    Map<String, Object> getSelectNoticeList(int pageNum, int pageSize, String selectKeyword);

}

(4). ServiceImpl

@Service
@RequiredArgsConstructor
public class NoticeServiceImpl implements NoticeService{

	private final NoticeDAO noticeDAO;
	
	@Override
	public Map<String, Object> getSelectNoticeList(int pageNum, int pageSize, String selectKeyword) {
		
        // 전체 공지사항 개수 조회
		int totalBoard = noticeDAO.selectNoticeCount(selectKeyword); 
        // 페이지 블록 크기 설정
    	int blockSize = 5; 
    	
        // Pager 클래스 사용
    	Pager pager = new Pager(pageNum, totalBoard, pageSize, blockSize);
    	
    	Map<String, Object> pageMap = new HashMap<String, Object>(); 
		pageMap.put("startRow", pager.getStartRow());  
		pageMap.put("endRow", pager.getEndRow());  
		pageMap.put("totalBoard", pager.getTotalBoard());  
		pageMap.put("selectKeyword", selectKeyword);  
    	
		List<Notice> noticeList = noticeDAO.selectNoticeList(pageMap);
		
        Map<String, Object> resultMap = new HashMap<String, Object>();
        resultMap.put("noticeList", noticeList);
        resultMap.put("pager", pager);
        
        return resultMap;
		
	}

}

Pager 클래스를 담은 Map 객체noticeList에 담아서
다시 Pager와 noticeList를 Map 객체에 담아 return 해주도록 했습니다.

(5). Controller

@RestController
@RequestMapping("/notice")
@RequiredArgsConstructor
public class NoticeRestController {
	
	@Autowired
	private final NoticeService noticeService;
	
	// 공지사항 리스트
	@GetMapping("/list")
	public ResponseEntity<Map<String, Object>> noticeList(
			@RequestParam(defaultValue = "1") int pageNum
			, @RequestParam(defaultValue = "10") int pageSize
			, @RequestParam(defaultValue = "") String selectKeyword) {
		
		try {
	        return new ResponseEntity<>(noticeService.getSelectNoticeList(pageNum, pageSize, selectKeyword), HttpStatus.OK);
	    } catch (Exception e) {
	    	return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
	    }
		
	}

}

컨트롤러는
특별한건 없고 Map 객체로 전달 받도록 하겠습니다.


2-3. Mapper

(1). mapper.java

public interface NoticeMapper {
	
	// 전체 게시글 수 조회
	int selectNoticeCount(String selectKeyword); 
	   
	// 페이징된 게시글 리스트 조회
	List<Notice> selectNoticeList(Map<String, Object> map);
	   
}

(2). mapper.xml

<?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="com.project.mapper.NoticeMapper">
	
	<!-- 공지사항 글 목록 개수 -->
	<select id="selectNoticeCount" resultType="int">
		SELECT COUNT(*) 
		
		FROM notice
        <!-- 검색어 조건 처리 -->
        <choose>
            <when test="selectKeyword != null and selectKeyword != ''">
                 WHERE notice_status=1 and notice_title LIKE CONCAT('%', #{selectKeyword}, '%') 
                 OR notice_status=1 and notice_content LIKE CONCAT('%', #{selectKeyword}, '%')
            </when>
            <otherwise>
                WHERE notice_status=1 <!-- 검색어가 없는 경우 status=1 데이터 검색 -->
            </otherwise>
        </choose>
	</select>
	
	<!-- 공지사항 목록 출력 -->
	<select id="selectNoticeList" resultType="com.project.dto.Notice">
		SELECT 
			notice_idx
            , notice_title
            , date_format(notice_regdate, '%y-%m-%d') as notice_regdate
            , notice_viewcnt
            , notice_status 
            , ROWNUM 
		FROM (
		
          SELECT @ROWNUM := @ROWNUM + 1 AS ROWNUM
          		 , RN.* 
          FROM (
          
          	SELECT
                  notice_idx
                  , notice_title
                  , notice_regdate
                  , notice_viewcnt
                  , notice_status
           FROM notice
	        <choose>
	            <when test="selectKeyword != null and selectKeyword != ''">
	                 WHERE (notice_title LIKE CONCAT('%', #{selectKeyword}, '%') 
	                 OR notice_content LIKE CONCAT('%', #{selectKeyword}, '%'))
	                 AND notice_status = 1
	            </when>
	            <otherwise>
	                WHERE notice_status=1 
	            </otherwise>
	        </choose>
	        ORDER BY notice_regdate DESC
          ) RN, (SELECT @ROWNUM := 0) tmp
      ) SUB 
      WHERE SUB.ROWNUM BETWEEN #{startRow} and #{endRow}
	</select>
	
</mapper>

MySQL에서 ROWNUM을 구현하기 위해 정말 많이 찾아봤는데요...
@ROWNUM을 사용해야합니다.
아래 오라클 버전도 추가도 작성하도록 하겠습니다.

Oracle.ver ROWNUM

<select id="selectNoticeList" resultType="com.project.dto.Notice">

	    SELECT * FROM (
          SELECT ROWNUM RN, BOARD.* FROM (
          	SELECT
                  notice_idx
                  , notice_title
                  , notice_content
                  , notice_regdate
                  , notice_viewcnt
                  , notice_status
           FROM notice
           WHERE notice_status = 1 AND
        <choose>
	        <when test="selectKeyword != null and selectKeyword != ''">
	            WHERE (notice_title LIKE CONCAT('%', #{selectKeyword}, '%') 
	            OR notice_content LIKE CONCAT('%', #{selectKeyword}, '%'))
	             AND notice_status = 1
	        </when>
	        <otherwise>
	            WHERE notice_status=1 
	        </otherwise>
	    </choose>
              ORDER BY notice_regdate DESC
          ) BOARD
      ) WHERE RN BETWEEN #{startRow} and #{endRow}

</select>

2-4. JSP

<!-- 게시글 수 출력 -->
<div class="totalNum">
	<span>전체 게시글 수 : <b><span id="totalBoard"></span></b></span>
</div>

<!-- n개씩 보기 출력 -->
<select id="noticeSize"></select>

<!-- 검색 기능 -->
<button id="resetButton" class="btn btn-secondary" onclick="noticeSearchCancel();">초기화</button>
<button id="searchButton" class="btn btn-secondary" onclick="noticeSearch();">검색</button>
<input type="text" class="" id="selectKeyword" placeholder="제목, 내용으로 검색해보세요!">

<!-- 테이블 생성 -->
<table border="1">
	<colgroup>
		<col width="10%" />
		<col width="25%" />
		<col width="15%" />
		<col width="20%" />
	</colgroup>
	<thead>
		<tr>
			<th>글번호</th>
			<th>제목</th>
			<th>작성일</th>
			<th>조회수</th>
		</tr>
	</thead>
	
	<tbody id="tbody" style="text-align:center;">
	</tbody>
</table>

<!-- 페이지 번호 출력 -->
<div id="pageNumDiv"></div>

위 사진 처럼 일단 테이블 뼈대를 만들어주도록 하겠습니다.

javascript

공지사항 목록

<script>
var page = 1;     // 기본 페이지 번호 설정
var size = 10;    // 기본 페이지 크기 설정
var keyword = ''; // 기본 검색어  NULL로 설정

// 페이지 랜더링시
$(document).ready(function() { 
	
    noticeListDisplay(pageNum, pageSize, selectKeyword);
  
    // n개씩 보기 값이 변경될 때
    $("#noticeSize").change(function() {
  	    var functionName =  "noticeListDisplay";
  	    pageSize = parseInt($(this).val());
        window[functionName](pageNum, pageSize, selectKeyword);
   });
 
});

// 공지 사항 목록 출력
function noticeListDisplay(pageNum, pageSize, selectKeyword) {
   
    $.ajax({
        method: "GET",
        url: "<c:url value='/notice/list'/>",
        data: {"pageNum": pageNum, "pageSize": pageSize, "selectKeyword": selectKeyword},
        dataType: "json",
        success: function(data, textStatus, xhr) {
        	if(xhr.status === 200) {
	        	// 전체 글 개수
	        	var totalBoard = data.pager.totalBoard;
	        	var pager = data.pager;
	        	
	        	// 전체 게시글 개수 출력
	        	$("#totalBoard").html(totalBoard);
	        	
	            $("#tbody").empty();
	            
	            // 공지사항 목록이 없을 때
	            if (data.noticeList.length == 0) { 
	            	$("#tbody").append("<tr><td colspan='4'>검색된 공지사항이 없습니다.</td></tr>");
	            } else {
					// 공지사항 목록 출력            	
		            for (var i = 0; i < data.noticeList.length; i++) {
		                var noticeList = data.noticeList[i];
		                var row = "<tr data-idx='" + noticeList.noticeIdx + "'>" +
		                   		  "<td>" + noticeList.rownum + "</td>" +
		                   		  "<td><a href=\"<c:url value='/notice'/>/" + noticeList.noticeIdx + "\">" + noticeList.noticeTitle + "</a></td>" +
		                   		  "<td>" + noticeList.noticeRegdate + "</td>" +
		                   		  "<td>" + noticeList.noticeViewcnt + "</td>" +
		                   		  "</tr>";
		                $("#tbody").append(row);
		            }
	            }
	            // 페이지 번호 출력
	            pageNumDisplay(data.pager);
	            // 페이지 당 출력 개수
	           noticeSizeDisplay();
        	}
        },
        error: function(xhr) {
            alert("오류가 발생했습니다. (error_code = " + xhr.status + ")");
        }
    });
}
</script>

구현 화면


페이징 번호

<script>
// 페이징 번호 출력
function pageNumDisplay(pager) {
    var html = "";
    
    // 이전 블럭이 있을 경우 이전 블럭 버튼 활성화
    if (pager.startPage > pager.blockSize) {
        html += "<a href=\"javascript:noticeListDisplay(" + pager.prevPage + ", " + pageSize + ", '" + selectKeyword + "');\" class=''><button type='button' class='btn btn-secondary' id='page-button'><</button></a>";
    } 

    // 페이지 번호
    for (var i = pager.startPage; i <= pager.endPage; i++) {
        if (pager.pageNum != i) {
            html += "<a class='page-num' id='page-num-1' href=\"javascript:noticeListDisplay(" + i + ", " + pageSize + ", '" + selectKeyword + "');\">" + i + "</a>";
        } else {
            html += "<a class='page-num' id='page-num-2' disabled>" + i + "</a>";
        }
    }

    // 다음 블럭이 있을 경우 다음 블럭 버튼 활성화
    if (pager.endPage < pager.totalPage) {
        html += "<a href=\"javascript:noticeListDisplay(" + pager.nextPage + ", " + pageSize + ", '" + selectKeyword + "');\" class=''><button type='button' class='btn btn-secondary' id='page-button'>></button></a>";
    } 

    $("#pageNumDiv").html(html);
}  
</script>

구현 화면


n개씩 보기

<script>
// 페이지 당 출력 개수
function noticeSizeDisplay() {
    // 페이지 크기 선택
    var noticeSize = $("#noticeSize");

    noticeSize.empty(); // 기존 옵션 초기화
    
    var pageSizeOptions = [10, 20, 50, 100];
    for (var i = 0; i < pageSizeOptions.length; i++) {
        var optionValue = pageSizeOptions[i];
        var optionText = optionValue + "개씩 보기";

        var option = $("<option>").val(optionValue).text(optionText);

        if (optionValue === pageSize) {
            option.prop("selected", true);
        }
        noticeSize.append(option);
    }
}  
  
</script>

구현 화면


검색 기능

<script>
// 검색
function noticeSearch() {
	selectKeyword = $("#selectKeyword").val();
    noticeListDisplay(1, pageSize, selectKeyword);
}

// 검색 초기화
function noticeSearchCancel() {
	var inputSearch = document.getElementById('selectKeyword');
	inputSearch.value = '';
    
	pageSize = 10;
	selectKeyword = '';
	noticeListDisplay(1, pageSize, selectKeyword);
}  
</script>

구현 화면

이렇게해서 페이징, 검색, n개씩보기 기능을 만들어봤습니다.
Pager 클래스와 Map 사용만 잘해주신다면 충분히 할 수 있다고 생각합니다.

전체 코드는 깃허브 링크 첨부하겠습니다.

0개의 댓글