[Spring] Thymeleaf를 통한 페이지네이션 구현
매장 목록 조회 구현
@Transactional(readOnly = true)
public MultiResponseDto<StoreResponseDto> getAllStoresByPaging(int page, int size) {
Page<Store> stores = storeRepository.findAll(PageRequest.of(page, size));
List<StoreResponseDto> responses = stores.getContent()
.stream()
.map(StoreResponseDto::from)
.toList();
return new MultiResponseDto<>(responses, PageInfo.from(stores));
}
page, size 2가지의 변수를 받아 Pagable 객체를 생성하여 Repository에서 검색한 뒤
임의로 생성한 DTO에 Response body와 Page 정보를 담아 리턴하도록 했다.
@GetMapping("/stores")
public String searchStoresForm(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size, Model model) {
MultiResponseDto<StoreResponseDto> responses = storeService.getAllStoresByPaging(page - 1, size);
model.addAttribute("storeList", responses.data());
model.addAttribute("pageInfo", responses.pageInfo());
return "search-stores";
}
리턴받은 정보는 model에 담아서 전달했다.
매장 목록 조회 템플릿
<div class="container" layout:fragment="content">
<table style="table-layout: fixed">
<thead class="table-light">
<tr>
<th scope="col">매장 이름</th>
<th scope="col">매장 주소</th>
</tr>
</thead>
<tbody>
<tr th:each="store: ${storeList}">
<td th:text="${store.storeName}"></td>
<td th:text="${store.address}"></td>
</tr>
</tbody>
</table>
<nav th:replace="~{fragments/pageNavBar::pageNavBar(${pageInfo}, 'stores')}"></nav>
</div>
간단하게 매장 이름과 주소만 전달 받아서 테이블 형식으로 출력하도록 구성했다.
하단에는 페이지네이션에 대한 네비게이션 바를 공통으로 사용할 수 있게 fragment로 구성했다.
<!-- pageNavBar.html -->
<nav style="text-align: center" th:fragment="pageNavBar (pageInfo, path)">
<!-- maxPage : 네비게이션바에 나타나는 페이지 수
start : 해당 네비게이션 바의 시작 페이지
end : 해당 네비게이션 바의 끝 페이지-->
<ul th:with="url=|/search/${path}|,
maxPage=5,
start=(${(pageInfo.page() % maxPage == 0) ?
((pageInfo.page() - 1) / maxPage) * maxPage + 1:
(pageInfo.page() / maxPage) * maxPage + 1}),
end=(${(pageInfo.totalPages() == 0) ? 1 :
(start + (maxPage - 1) < pageInfo.totalPages ? start + (maxPage - 1) : pageInfo.totalPages)})">
<li th:if="${start > 1}">
<a th:href="|${url}?page=1&size=${pageInfo.size()}|" th:text="'<<'"></a>
</li>
<li th:if="${start > 1}">
<a th:href="|${url}?page=${start - maxPage}&size=${pageInfo.size()}|" th:text="'<'"></a>
</li>
<li th:each="pageNum: ${#numbers.sequence(start, end)}">
<a th:text="${pageNum}" th:href="|${url}?page=${pageNum}&size=${pageInfo.size()}|"></a>
</li>
<li th:if="${end < pageInfo.totalPages()}">
<a th:href="|${url}?page=${start + maxPage}&size=${pageInfo.size()}|" th:text="'>'"></a>
</li>
<li th:if="${end < pageInfo.totalPages()}">
<a th:href="|${url}?page=${pageInfo.totalPages()}&size=${pageInfo.size()}|" th:text="'>>'"></a>
</li>
</ul>
</nav>
해당 부분을 나눠서 보면
<nav style="text-align: center" th:fragment="pageNavBar (pageInfo, path)">
fragment를 통해 공통으로 사용할 수 있도록 하고, 필요한 변수를 전달받았다.
<ul th:with="url=|/search/${path}|,
maxPage=5,
start=(${(pageInfo.page() % maxPage == 0) ?
((pageInfo.page() - 1) / maxPage) * maxPage + 1:
(pageInfo.page() / maxPage) * maxPage + 1}),
end=(${(pageInfo.totalPages() == 0) ? 1 :
(start + (maxPage - 1) < pageInfo.totalPages ? start + (maxPage - 1) : pageInfo.totalPages)})">
url은 pathVariable이 아닌 경로 자체를 변수로 받아서 사용하기 위해 설정한 값이다.
maxPage는 한 네비게이션 바에 몇 개의 목록이 나열될지 최대 수를 정한 것이다.
start는 네비게이션 바의 시작점
end는 네비게이션 바의 끝점이다.
현재 maxPage=5이므로 1 2 3 4 5 > 다음은 6 7 8 이라고 가정하자.
1 2 3 4 5에서 start는 1, end는 5이고, 6 7 8에서 start는 6, end는 8이다.
현재 페이지 정보(pageInfo.page())를 전달 받을 때, 0부터 시작하면 start는 아래와 같이 간단하게 설정이 가능하다.
<ul th:with="start=${pageInfo.page() / maxPage) * maxPage + 1}">
나는 아무 생각 없이 1부터 시작하도록 했더니 maxPage의 배수인 경우에 이상한 오류가 발생해서 저렇게 조건을 붙였다.
앞으로 프론트엔드에게 데이터를 전달할 땐 0부터 시작하도록 하는게 좋은지 물어보도록 해야겠다.
end는 데이터가 없을 때 1이 표시되도록 하고, 6 7 8 처럼 마지막 페이지 수가 maxPage 갯수에 모자랄 경우에 대한 설정을 해줬다.
<li th:if="${start > 1}">
<a th:href="|${url}?page=1&size=${pageInfo.size()}|" th:text="'<<'"></a>
</li>
<li th:if="${start > 1}">
<a th:href="|${url}?page=${start - maxPage}&size=${pageInfo.size()}|" th:text="'<'"></a>
</li>
시작 번호가 1보다 클 때 (첫 페이지가 아닐 때) 처음으로 이동하거나 이전 페이지로 이동하는 버튼을 만들어 주었고, 마찬가지로 반대에도 다음 페이지로 이동하는 버튼을 만들어 주었다.
<li th:each="pageNum: ${#numbers.sequence(start, end)}">
<a th:text="${pageNum}" th:href="|${url}?page=${pageNum}&size=${pageInfo.size()}|"></a>
</li>
해당 버튼 사이에는 start에서 end까지 해당 페이지로 이동하는 페이지 번호를 남겨주었다.
결과 페이지
추후 개선 사항
pagination할 때 지금은 page, size 정보만 받지만 이후에 발전되면 재고순, 거리순 등 상점을 검색하는 조건들을 Enum으로 만든 뒤, 해당 조건을 파라미터로 받아 CustomPageRequest를 구성해서 정렬 기준을 추가해보면 좋을 것 같다.
사용하면서 공부한 기술 정리
[Thymeleaf] 변수를 통해 경로 지정하기 (PathVariable 아님)
더 자세한 코드는 여기에서 확인이 가능합니다.
'Spring' 카테고리의 다른 글
[Spring] 로그인 시 Cors 에러 해결 (0) | 2023.11.14 |
---|---|
[Thymeleaf] th:href 경로에 변수를 사용한 경로 설정 (0) | 2023.09.07 |
[Thymeleaf] layout fragment로 변수 넘겨주기 (0) | 2023.09.07 |
[Spring/Thymeleaf] 세션 방식 로그인 및 템플릿 구현 (0) | 2023.08.29 |
[Spring/Error] WebSecurityCustomizer를 통해 정적 자원에 대한 Ignore가 안될때 (0) | 2023.08.28 |