매장 목록 조회 구현

@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까지 해당 페이지로 이동하는 페이지 번호를 남겨주었다.

 

 

결과 페이지

CSS는 없습니다.. 많이 누추하시죠.. 죄송합니다..

 

추후 개선 사항

pagination할 때 지금은 page, size 정보만 받지만 이후에 발전되면 재고순, 거리순 등 상점을 검색하는 조건들을 Enum으로 만든 뒤, 해당 조건을 파라미터로 받아 CustomPageRequest를 구성해서 정렬 기준을 추가해보면 좋을 것 같다.

 

 

 

사용하면서 공부한 기술 정리

[Thymeleaf] fragment로 변수 넘겨주기

 

[Thymeleaf] 변수를 통해 경로 지정하기 (PathVariable 아님)

 

 

 

 

더 자세한 코드는 여기에서 확인이 가능합니다.

+ Recent posts