2023.12.05 - [백엔드(Back-End)/Spring Boot] - [sts4-Spring Boot] 11. Alert Message 메시지 전달
저번 글부터는 게시글에서 기능을 조금씩 추가하고 있다.
이번에는 하단에 페이지 블록을 띄우고, 페이징 기능을 구현하려고 한다. 고고~
페이징
사용자가 어떤 데이터를 필요로 할 때, 전체 데이터 중의 일부를 보여주는 방식
등록된 게시글을 모두 출력 -> 페이지 로딩 속도↓, 불편
~> 페이징과 (다음글에서) 검색 기능 ㅇ
효율성, 관리를 위해 BoardDTO 클래스와 같이 공통으로 사용할 수 있는 클래스로 처리
- currentPageNo : 현재 페이지 번호
- recordsPerPage : 페이지당 출력할 게시글 수
- pageSize : 화면 하단에 출력할 페이지 사이즈 1~10
- searchKeyword : 검색 키워드
- searchType : 검색 유형, 전체/제목/내용/작성자
공통 페이징 파라미터 처리를 위해 클래스를 만들자.
메인/자바> com.board > paging 패키지를 만들고 Criteria 클래스를 만든다.
Criteria
조건이나 기준을 의미
보통 검색이나 데이터 필터링, 페이징과 관련된 로직에서 사용
ex) 데이터 검색, 쿼리 실행 시 조건 정의.
페이지번호, 페이지 당 표시할 레코드 수, 검색 키워드, 정렬 방식 등
package com.board.paging;
public class Criteria {
private int currentPageNo;
private int recordsPerPage;
private int pageSize;
private String searchKeyword;
private String searchType;
public Criteria() {
// 기본값으로 현재 페이지 번호는 1
this.currentPageNo = 1;
// 페이지당 출력할 게시글 수
this.recordsPerPage = 10;
// 하단에 출력할 페이지 단위 수
this.pageSize = 10;
}
// MySQL에서 LIMIT 구문의 앞부분에 사용되는 메서드
public int getStartPage() {
return (currentPageNo -1) * recordsPerPage;
}
//Getter, Setter, toString() Generate
// ...
}
getter, setter, toString은 일일이 입력하지 말고,
오른쪽 클릭 > Source > Getter and Setter & toString 으로 Generate 하자.
getStartPage는
예를 들어 현재 페이지 번호가 3페이지, 한 페이지당 10개의 글이 보여지면
2 * 10 = 20번째 글부터 게시글이 보여져야 한다.
계산한 페이지 번호를 쿼리에 넘길 것이므로
매퍼 인터페이스에 파라미터로 Criteria 클래스를 추가하자
package com.board.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.board.domain.BoardDTO;
@Mapper
public interface BoardMapper {
public int insertBoard(BoardDTO params);
public BoardDTO selectBoardDetail(Long idx);
public int updateBoard(BoardDTO params);
public int deleteBoard(Long idx);
public List<BoardDTO> selectBoardList(Criteria criteria);
public int selectBoardTotalCount(Criteria criteria);
}
인터페이스를 바꿨으니 매퍼 XML의 selectBoardList와 selectBoardTotalCount의 SQL 쿼리도 변경해주자.
parameter 타입과 LIMIT 에 변경 사항이 있다.
<select id="selectBoardList" parameterType="Criteria" resultType="BoardDTO">
SELECT
<include refid="boardColumns" />
FROM
board
WHERE
delete_yn = 'N'
ORDER BY
notice_yn ASC,
idx DESC,
insert_time DESC
<!-- 데이터를 원하는 만큼 가져오고 싶을 때 활용 -->
<!-- 시작할 행의 수부터, 반환할 행의 수 -->
LIMIT
#{startPage}, #{recordsPerPage}
</select>
<select id="selectBoardTotalCount" parameterType="Criteria" resultType="int">
SELECT
COUNT(*)
FROM
board
WHERE
delete_yn = 'N'
</select>
Criteria 클래스에서 getStartPage의 결과값을 startPage로 받아온다.
#{getStartPage}가 아니라 #{startPage}로 받는 이유?
- Mybatis에서는 #{ }를 이용하여 파라미터나 속성 값을 바인딩할 때, 해당 속성의 이름!을 사용한다.
So, #{startPage} ~> 객체의 getStartPage() 메서드를 호출하여 그 결과를 사용
- 반환하는 속성 이름은 Java의 빈 규칙(JavaBeans)에 따라 결정
일반적으로, 속성 이름 : 'get'이나 'is'로 시작하는 메서드 이름에서 해당 단어를 제외한 부분
자바빈의 네이밍 컨벤션에 따라 파싱되고, MyBatis도 이 네이밍 규칙을 따른다.
매퍼의 메서드 파라미터를 변경했으니 서비스에서도 적용을 해줘야겠지~
package com.board.service;
import java.util.List;
import com.board.pagin.Criteria;
import com.board.domain.BoardDTO;
public interface BoardService {
public boolean registerBoard(BoardDTO params);
public BoardDTO getBoardDetail(Long idx);
public boolean deleteBoard(Long idx);
public List<BoardDTO> getBoardList(Criteria criteria);
}
인터페이스의 변경 사항에 맞춰 구현 클래스도 수정을 해주자.
getBoardList의 파라미터로 Criteria를 추가하고
selectBoardTotalCount와 selectBoardLst 메서드 인자를 criteria로 지정한다. (매퍼 메서드)
@Override
public List<BoardDTO> getBoardList(Criteria criteria) {
List<BoardDTO> boardList = Collections.emptyList();
int boardTotalCount = boardMapper.selectBoardTotalCount(criteria);
if (boardTotalCount > 0) {
boardList = boardMapper.selectBoardList(criteria);
}
return boardList;
}
서비스도 바꿨겠다. 이제 컨트롤러를 변경해보자.
GET방식으로 list.do가 매핑됐을 때, getBoradList의 인자를 criteria 클래스로 넣어주면 되겠다.
@GetMapping(value = "/board/list.do")
public String openBoardList(@ModelAttribute("criteria") Criteria criteria, Model model) {
List<BoardDTO> boardList = boardService.getBoardList(params);
model.addAttribute("boardList", boardList);
return "board/list";
}
@RequestParam
- HTTP 요청에서 파라미터 값을 가져올 때 사용
- 주로 쿼리스트링, POST 요청의 파라미터 값 가져올 때 & 단일 값(1:1)
-> 메서드 내에서 model.addAttribute(key, value)로 직접 모델에 속성을 추가
@ModelAttribute
- 해당 메서드의 반환값을 자동으로 모델에 추가 = 자동으로 뷰까지 전달
- 요청 파라미터를 객체로 변환하여 전달 가능
i.e. 요청 파라미터들을 객체로 묶어 컨트롤러 메서드에 전달
여태는 domain 패키지(DTO 클래스)에 있는 객체의 변수들만 사용했지만,
paging 패키지(Criteria 클래스)에 있는 page 관련 변수를 사용할 것이기 때문에
DBConfiguration에서 setTypeAliasesPackage를 com.board.domain에서 com.board.*로 바꿔주자.
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception{
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.setMapperLocations(applicationContext.getResources("classpath:/mappers/**/*Mapper.xml"));
factoryBean.setTypeAliasesPackage("com.board.*");
factoryBean.setConfiguration(mybatisConfg());
return factoryBean.getObject();
}
이제 페이지 정보를 계산하는 클래스를 생성하자.
전체 글 수, 총 페이지 수, 첫 페이지, 마지막 페이지, 이전 페이지, 다음 페이지 등등
package com.board.paging;
public class PaginationInfo {
private Criteria criteria;
private int totalRecordCount;
private int totalPageCount;
private int firstPage;
private int lastPage;
private int firstRecordIndex;
private int lastRecordIndex;
private boolean hasPreviousPage;
private boolean hasNextPage;
public PaginationInfo(Criteria criteria) {
if (criteria.getCurrentPageNo() < 1) {
criteria.setCurrentPageNo(1);
}
if (criteria.getRecordsPerPage() < 1 || criteria.getRecordsPerPage() > 100) {
criteria.setRecordsPerPage(10);
}
if (criteria.getPageSize() < 5 || criteria.getPageSize() > 20) {
criteria.setPageSize(10);
}
this.criteria = criteria;
}
public void setTotalRecordCount(int totalRecordCount) {
this.totalRecordCount = totalRecordCount;
if (totalRecordCount > 0) {
calculation();
}
}
private void calculation() {
// 현재 페이지 번호 > 전체 페이지 수 => 현재 페이지 번호에 전체 페이지 수를 저장
totalPageCount = ((totalRecordCount - 1) / criteria.getRecordsPerPage() + 1);
if (criteria.getCurrentPageNo() > totalPageCount) {
criteria.setCurrentPageNo(totalPageCount);
}
// 페이지 리스트의 첫 페이지 번호
firstPage = ((criteria.getCurrentPageNo() - 1) / criteria.getPageSize()) * criteria.getPageSize() + 1;
// 마지막 페이지가 전체 페이지 수보다 크면 마지막 페이지에 전체 페이지 수 저장
lastPage = firstPage + criteria.getPageSize() - 1;
if (lastPage > totalPageCount) {
lastPage = totalPageCount;
}
//SQL의 조건절에 사용되는 첫 RNUM
firstRecordIndex = (criteria.getCurrentPageNo() - 1) * criteria.getRecordsPerPage();
// 마지막 RNUM
lastRecordIndex = criteria.getCurrentPageNo() * criteria.getRecordsPerPage();
// 이전 페이지 존재 여부
hasPreviousPage = firstPage != 1;
// 다음 페이지 존재 여부
hasNextPage = (lastPage * criteria.getRecordsPerPage()) < totalRecordCount;
}
// Getter, Setter, toString() 추가
// ...
}
criteria
- 페이지 계산에 Criteria 클래스의 멤버 변수 사용(페이지 범위, 현재 페이지, 페이지 당 게시글 수)
firstPage
- 현재 페이지가 10~20 페이지 범위에 속하면, 10 페이지를 의미
lastPage
- 현재 페이지가 10~20 페이지 범위에 속하면, 20 페이지를 의미
firstRecordIndex
- 특정 페이지의 첫 데이터
- Criteria 클래스의 getStartPage 메서드를 대신 LIMIT 구문 첫 번째 값에 사용
lastRecordIndex
- 특정 페이지의 마지막 데이터
- 오라클과 같이 LIMIT 구문 존재 안 하고, 인라인 뷰 (FROM 절 서브 쿼리)를 사용해야 하는 DB에서의 활용 위한 변수
- 현재 프로젝트는 MYSQL 기반이어서 사용x
hasPreviousPage, hasNextPage
- 이전, 다음 페이지의 존재 여부
PaginationInfo 메서드
- 현재 페이지, 페이지 범위, 페이지 당 글 수에 잘못된 값이 들어왔을 때, 기본값 지정
setTotalRecordCount 메서드
- 파라미터로 넘어온 전체 데이터 개수를 현재 클래스의 전체 데이터 개수에 저장
- 게시글 수가 1개 이상이면 calculation 메서드 실행
calculation 메서드
- 총 페이지 수 계산 totalPageCount
ex) 전체 글의 수 = 45개, 페이지 당 10개 글 => 총 5페이지 필요
$$ 전체\,페이지\,개수 = \frac{전체\,글\,수\, -1}{페이지당\,글\,수} + 1 $$ - 페이지 범위의 첫 페이지 firstPage
ex) 현재 15페이지, 페이지 당 글 10개 => 11~20에 속하므로 11이 첫 페이지
$$ \frac{현재\,페이지\,번호\,-1}{페이지\,범위\,단위} \times 페이지\,범위\,단위 + 1$$ - 페이지 범위의 마지막 페이지 lastPage
ex) 동일 조건 => 20이 마지막 페이지
=> 해당 범위의 첫 페이지인 10에서 단위를 곱하고 1 빼면 됨
$$ (첫\,페이지\,번호 + 페이지\,범위\,단위) - 1$$
- 페이지의 첫 글 firstRecordIndex
ex) 현재 3페이지, 페이지 당 10개 글 => 20번째 글
$$ (현재\,페이지\,-1) \times 페이지당\,글\,수$$ - 페이지의 마지막 글 lastRecordIndex
ex) 동일 조건 => 29번째 글
$$ 현재\,페이지 \times 페이지당\,글\,수$$ - 이전 페이지 존재 여부 hasPreviousPage
특정(현재 페이지가 속한)페이지 범위의 첫 페이지가 1이면 = 이전 페이지 x - 다음 페이지 존재 여부 hasNextPage
ex) 특정 페이지 범위의 마지막 페이지까지의 글 수보다 전체 글 수가 더 크면 = 이후 페이지 o
서비스와 매퍼를 바꿨으니 MVC 영역을 바꿔주자.
먼저 domian 패키지에 CommonDTO 클래스를 생성한다.
Criteria를 상속받고
package com.board.domain;
import java.time.LocalDateTime;
import com.board.paging.Criteria;
import com.board.paging.PaginationInfo;
public class CommonDTO extends Criteria {
private PaginationInfo paginationInfo; // 페이징 정보
private String deleteYn;
private LocalDateTime insertTime;
private LocalDateTime updateTime;
private LocalDateTime deleteTime;
//Getter, Setter, toString()
// ...
}
보드DTO는 CommonDTO를 상속받도록 한다.
package com.board.domain;
public class BoardDTO extends CommonDTO {
private Long idx;
private String title;
private String content;
private String writer;
private int viewCnt;
private String noticeYn;
private String secretYn;
//Getter, Setter, toString() Gernerate
// ...
}
보드매퍼XML의 매개변수를 변경하고, Limit도 수정한다.
<select id="selectBoardList" parameterType="BoardDTO" resultType="BoardDTO">
SELECT
<include refid="boardColumns" />
FROM
board
WHERE
delete_yn = 'N'
<include refid="CommonMapper.search" />
ORDER BY
notice_yn ASC,
idx DESC,
insert_time DESC
LIMIT
#{paginationInfo.firstRecordIndex}, #{recordsPerPage}
</select>
<select id="selectBoardTotalCount" parameterType="BoardDTO" resultType="int">
SELECT
COUNT(*)
FROM
board
WHERE
delete_yn = 'N'
</select>
보드서비스의 인터페이스도 매개변수를 BoardDTO로
public interface BoardService {
public boolean registerBoard(BoardDTO params);
public BoardDTO getBoardDetail(Long idx);
public boolean deleteBoard(Long idx);
public List<BoardDTO> getBoardList(BoardDTO params);
}
그럼 구현 클래스도 변경
@Override
public List<BoardDTO> getBoardList(BoardDTO params) {
List<BoardDTO> boardList = Collections.emptyList();
int boardTotalCount = boardMapper.selectBoardTotalCount(params);
PaginationInfo paginationInfo = new PaginationInfo(params);
paginationInfo.setTotalRecordCount(boardTotalCount);
params.setPaginationInfo(paginationInfo);
if (boardTotalCount > 0) {
boardList = boardMapper.selectBoardList(params);
}
return boardList;
}
그럼 컨트롤러도 변경
@GetMapping(value = "/board/list.do")
public String openBoardList(@ModelAttribute("params") BoardDTO params, HttpServletRequest request, Model model) {
List<BoardDTO> boardList = boardService.getBoardList(params);
model.addAttribute("requestURI", request.getRequestURI());
model.addAttribute("boardList", boardList);
return "board/list";
}
Criteria클래스에 페이지 관련 정보를 쿼리스트링으로 전달하기 위해
makeQueryString 메서드를 만들어주자.
public String makeQueryString(int pageNo) {
UriComponents uriComponents = UriComponentsBuilder.newInstance()
.queryParam("currentPageNo", pageNo)
.queryParam("recordsPerPage", recordsPerPage)
.queryParam("pageSize", pageSize)
.queryParam("searchType", searchType)
.queryParam("searchKeyword", searchKeyword)
.build()
.encode();
return uriComponents.toUriString();
}
메인/리소스의 템플릿.보드.fragments 폴더에 common.html 을 만들자.
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<nav th:fragment="pagination" th:if="${params != null and params.paginationInfo.totalRecordCount > 0}" th:object="${params.paginationInfo}" th:with="info=${params.paginationInfo}" aria-label="Page navigation" class="text-center">
<ul class="pagination">
<li th:if="*{hasPreviousPage == true}" th:onclick="movePage([[ ${requestURI} ]], [[ ${params.makeQueryString(1)} ]])">
<a href="javascript:void(0)" aria-label="Previous"><span aria-hidden="true">«</span></a>
</li>
<li th:if="*{hasPreviousPage == true}" th:onclick="movePage([[ ${requestURI} ]], [[ ${params.makeQueryString(info.firstPage - 1)} ]])">
<a href="javascript:void(0)" aria-label="Previous"><span aria-hidden="true">‹</span></a>
</li>
<li th:each="pageNo : *{#numbers.sequence( firstPage, lastPage )}" th:class="${pageNo == params.currentPageNo} ? 'active'">
<a href="javascript:void(0)" th:text="${pageNo}" th:onclick="movePage([[ ${requestURI} ]], [[ ${params.makeQueryString(pageNo)} ]])"></a>
</li>
<li th:if="*{hasNextPage == true}" th:onclick="movePage([[ ${requestURI} ]], [[ ${params.makeQueryString(info.lastPage + 1)} ]])">
<a href="javascript:void(0)" aria-label="Next"><span aria-hidden="true">›</span></a>
</li>
<li th:if="*{hasNextPage == true}" th:onclick="movePage([[ ${requestURI} ]], [[ ${params.makeQueryString(info.totalPageCount)} ]])">
<a href="javascript:void(0)" aria-label="Next"><span aria-hidden="true">»</span></a>
</li>
</ul>
</nav>
</html>
페이징 기능을 만들었으니 페이지를 보여줄 list.html도 변경해줘야겠다.
기존에 기능 없이 페이지를 보여줬던 부분을 pagination 프래그먼트 인클루드하고
replace문을 원래 " "였는데 버전이 바뀌면서 "~{ }" 문법을 추천한다.
스크립트를 추가한다.
<th:block layout:fragment="paging">
<nav th:replace="~{board/fragments/common :: pagination}"></nav>
</th:block>
<th:block layout:fragment="script">
<script th:inline="javascript">
/* <![CDATA[ */
function movePage(uri, queryString) {
location.href = uri + queryString;
}
/* ]]> */
</script>
</th:block>
그럼 해당 페이지로 잘 넘어간다~!!!!!!!!!!!
'백엔드(Back-End) > Spring Boot' 카테고리의 다른 글
[sts4-Spring Boot] 14. 이전 페이지 정보 유지하기 (0) | 2023.12.07 |
---|---|
[Spring Boot] 13. Searching 검색 기능(1) (0) | 2023.12.06 |
[sts4-Spring Boot] 11. Alert Message 메시지 전달 (1) | 2023.12.05 |
[sts4-Spring Boot] 10. 게시글 삭제하기 (0) | 2023.12.05 |
[sts4-Spring Boot] 09. 게시글 읽기/수정하기 (0) | 2023.12.05 |