개발/Project(Spring-쇼핑몰)

Project (5-2) 상품 관리 (1)

잇(IT) 2023. 8. 11. 11:32
728x90

- 상품의 상세 페이지에 진입하기 위해서 등록된 상품 번호를 직접 URL에 입력하여 상품 상세 페이지에 진입한다. 상품 번호를 모를 경우 상세 페이지로 진입할 수 없으므로 등록된 상품 리스트를 조회할 수 있는 화면을 만들어야 한다.

 

- 조회 조건

1. 상품 등록일

2. 상품 판매 상태

3. 상품명 또는 상품 등록자 아이디

 

- 복잡한 조회 조건의 경우 Querydsl을 이용해 조건에 맞는 쿼리를 동적으로 쉽게 생성할 수 있다. Querydsl을 사용하면 비슷한 쿼리를 재활용 할 수 있다는 장점이 있다. 또한 쿼리를 JAVA 문법으로 작성하기 때문에 오류를 컴파일 단계에서 찾을 수 있다는 장점이 있다.

 

- ItemSearchDto.java

@Getter @Setter
public class ItemSearchDto {

    private String searchDateType;
    //현재 시간과 상품 등록일을 비교해서 상품 데이터를 조회한다.

    private ItemSellStatus searchSellStatus;
    //상품의 판매 상태를 기준으로 상품 데이터를 조회한다.

    private String searchBy;
    //상품을 조회할 때 어떤 유형으로 조회할지 선택한다.
    1. itemNm : 상품명
    2. createdBy : 상품 등록자 아이디

    private String searchQuery = "";
    //조회할 검색어 저장할 변수다. searchBy가 itemNm일 경우 상품명을 기준으로 검색하고,
    //createdBy일 경우 상품 등록자 아이디 기준으로 검색한다.
}

- Querydsl을 Spring Data Jpa와 함께 사용하기 위해서는 사용자 정의 리포지토리를 정의해야 한다.

1. 사용자 정의 인터페이스 작성

2. 사용자 정의 인터페이스 구현

3. Spring Data Jpa 리포지토리에서 사용자 정의 인터페이스 상속

 

1. 사용자 정의 인터페이스 작성

- ItemRepositoryCustom.java

public interface ItemRepositoryCustom {

    Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable);
    //상품 조회 조건을 담고 있는 itemSearchDto 객체와 페이징 정보를 담고 있는 pageable 객체를 파라미터로 받는
    //getAdminItemPage 메소드를 정의한다. 반환 데이터로 Page<Item> 객체를 반환한다.
    }

- 파라미터로 받는 itemSearchDto의 경우상품 조회 조건을 담고 있고, pageable의 경우 페이징 정보를 담고 있다.

- 해당 메소드의 반환 데이터로 Page<Item>을 반환한다.

- Page 타입을 반환받게 되면 Spring에 의해 페이징 처리된 데이터들을 제공받을 수 있다.

 

2. 사용자 정의 인터페이스 구현

- 클래스명 끝에 "Impl"을 붙여주어야 정상적으로 동작한다.

- Querydsl에서는 BooleanExpression이라는 where절에서 사용할 수 있는 값을 지원한다.

- BooleanExpression을 반환하는 메소드를 만들고 해당 조건들을 다른 쿼리를 생성할 때 사용할 수 있기 때문에 중복 코드를 줄일 수 있다.

- ItemRepositoryCustomImpl.java

public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{
    //1. ItemRepositoryCustom을 상속 받는다.

    private JPAQueryFactory queryFactory;
    //2. 동적으로 쿼리를 실행하기 위해서 JPAQueryFactory 클래스를 사용한다.

    public ItemRepositoryCustomImpl(EntityManager em) {
        //3. JPAQueryFactory의 생성자로 EntityManager 객체를 넣어준다.

        this.queryFactory = new JPAQueryFactory(em);
    }

    private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus) {
        //4. 상품 판매 상태 조건이 전체(null)일 경우는 null을 리턴한다. 결과값이 null이면 where절에서 해당 조건은 무시된다.
        //상품 판매 상태 조건이 null이 아니라 판매중 or 품절 상태라면 해당 조건의 상품만 조회한다.
        return searchSellStatus == null ? null : QItem.item.itemSellStatus.eq(searchSellStatus);
    }

    private BooleanExpression regDtsAfter(String searchDateType) {
        LocalDateTime dateTime = LocalDateTime.now();
        //5. searchDateType의 값에 따라서 dateTime의 값을 이전 시간의 값으로 세팅 후 해당 시간 이후로 등록된 상품만 조회
        //ex) searchDateType 값이 "1m"인 경우 dateTime의 시간을 한 달 전으로 세팅 후 최근 한 달 동안 등록된 상품만 조회하도록 조건 값 반환

        if (StringUtils.equals("all", searchDateType) || searchDateType == null) {
            return null;
        } else if (StringUtils.equals("1d", searchDateType)) {
            dateTime = dateTime.minusDays(1);
        } else if (StringUtils.equals("1w", searchDateType)) {
            dateTime = dateTime.minusWeeks(1);
        } else if (StringUtils.equals("1m", searchDateType)) {
            dateTime = dateTime.minusMonths(1);
        } else if (StringUtils.equals("6m", searchDateType)) {
            dateTime = dateTime.minusMonths(6);
        }

        return QItem.item.regTime.after(dateTime);
    }

    private BooleanExpression searchByLike(String searchBy, String searchQuery) {
        //6. searchBy의 값에 따라서 상품명에 검색어를 포함하고 있는 상품 또는 상품 생성자 아이디에
        //검색어를 포함하고 있는 상품을 조회하도록 조건값을 반환.

        if (StringUtils.equals("itemNm", searchBy)) {
            return QItem.item.itemNm.like("%" + searchQuery + "%");
        } else if (StringUtils.equals("createdBy", searchBy)) {
            return QItem.item.createdBy.like("%" + searchQuery + "%");
        }

        return null;
    }

    @Override
    public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
        QueryResults<Item> results = queryFactory
                //7.
                .selectFrom(QItem.item)
                .where(regDtsAfter(itemSearchDto.getSearchDateType()),
                        searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
                        searchByLike(itemSearchDto.getSearchBy(),
                                itemSearchDto.getSearchQuery()))
                .orderBy(QItem.item.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        List<Item> content = results.getResults();
        long total = results.getTotal();
        return new PageImpl<>(content, pageable, total);
        //8. 조회한 데이터를 Page 클래스의 구현체인 PageImpl 객체로 반환한다.
    }

 

this.queryFactory = new JPAQueryFactory(em);
    }

- 동적으로 쿼리를 생성하기 위해서 JPAQueryFactory 클래스를 사용한다. 

 

private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus) {
        //4. 상품 판매 상태 조건이 전체(null)일 경우는 null을 리턴한다. 결과값이 null이면 where절에서 해당 조건은 무시된다.
        //상품 판매 상태 조건이 null이 아니라 판매중 or 품절 상태라면 해당 조건의 상품만 조회한다.
        return searchSellStatus == null ? null : QItem.item.itemSellStatus.eq(searchSellStatus);
    }

- searchSellStatusEq는 상품의 품절 상태는 나타내는 메서드다.

- 조건 검색에 아무것도 선택되지 않으면 null을 반환하고, where절에서 null 값을 받으면 그냥 무시가 된다.

- null이 아닐 경우 선택된 상태를 가져온다. 

 

private BooleanExpression regDtsAfter(String searchDateType) {
        LocalDateTime dateTime = LocalDateTime.now();
        //5. searchDateType의 값에 따라서 dateTime의 값을 이전 시간의 값으로 세팅 후 해당 시간 이후로 등록된 상품만 조회
        //ex) searchDateType 값이 "1m"인 경우 dateTime의 시간을 한 달 전으로 세팅 후 최근 한 달 동안 등록된 상품만 조회하도록 조건 값 반환

        if (StringUtils.equals("all", searchDateType) || searchDateType == null) {
            return null;
        } else if (StringUtils.equals("1d", searchDateType)) {
            dateTime = dateTime.minusDays(1);
        } else if (StringUtils.equals("1w", searchDateType)) {
            dateTime = dateTime.minusWeeks(1);
        } else if (StringUtils.equals("1m", searchDateType)) {
            dateTime = dateTime.minusMonths(1);
        } else if (StringUtils.equals("6m", searchDateType)) {
            dateTime = dateTime.minusMonths(6);
        }

        return QItem.item.regTime.after(dateTime);
    }

- dateTiem.minusXXX를 통해 세팅 시간된 시간 이후로 등록된 상품만 조회한다.

- Days, Weeks, Months를 통해 일, 주, 달 이전 시간으로 세팅이 가능하다.

 

private BooleanExpression searchByLike(String searchBy, String searchQuery) {
        //6. searchBy의 값에 따라서 상품명에 검색어를 포함하고 있는 상품 또는 상품 생성자 아이디에
        //검색어를 포함하고 있는 상품을 조회하도록 조건값을 반환.

        if (StringUtils.equals("itemNm", searchBy)) {
            return QItem.item.itemNm.like("%" + searchQuery + "%");
        } else if (StringUtils.equals("createdBy", searchBy)) {
            return QItem.item.createdBy.like("%" + searchQuery + "%");
        }

- searchByLike 메서드를 통해 상품명이나 생성자에 대한 정보를 받아온다.

- like를 사용하여 일부만 작성해도 해당 명을 포함하는 모든 데이터들을 가져온다.

 

@Override
    public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
        QueryResults<Item> results = queryFactory
                //7.
                .selectFrom(QItem.item)
                .where(regDtsAfter(itemSearchDto.getSearchDateType()),
                        searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
                        searchByLike(itemSearchDto.getSearchBy(),
                                itemSearchDto.getSearchQuery()))
                .orderBy(QItem.item.id.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();

        List<Item> content = results.getResults();
        long total = results.getTotal();
        return new PageImpl<>(content, pageable, total);
        //8. 조회한 데이터를 Page 클래스의 구현체인 PageImpl 객체로 반환한다.
    }

- queryFactory를 이용하여 JAVA 코드를 이용한 동적 쿼리를 작성할 수 있다.

- 참조 타입이 QueryResults<Item>이기 때문에 쿼리의 결과값으로 여러개의 Item 엔티티가 반환될 것이다.

- 동적 쿼리 결과로 나온 엔티티들을 List에 담는다. getTotal을 통해 조회된 데이터의 행 수를 구한다 -> 이 값은 페이징을 할 때 사용된다.

- 동적쿼리를 통해 가져온 데이터들을 PageImpl 파라미터로 넘긴다. PageImpl은 Page 클래스의 구현체이다.

메소드 기능
QueryResult<T> fetchResults() 조회 대상 리스트 및 전체 개수를 포함하는 QueryResults 반환
List<T> fetch() 조회 대상 리스트 반환
T fetchOne() 조회 대상이 1건이면 해당 타입 반환
조회 대상이 1건 이상이면 에러 발생
T fetchFirst() 조회 대상이 1건 또는 1건 이상이면 1건만 반환
long fetchCount() 해당 데이터 전체 개수 반환, count 쿼리 실행

 

- 마지막으로 ItemRepository 인터페이스에서 ItemRepositoryCustom 인터페이스를 상속한다.

- ItemRepository에서 Querydsl로 구현한 상품 관련 페이지 목록을 불러오는 getAdminItemPage() 메소드를 사용할 수 있다.

- ItemRepository.java

public interface ItemRepository extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item>,
ItemRepositoryCustom {

...

 
- ItemService 클래스에 상품 조회 조건과 페이징 정보를 파라미터로 받아서 상품 데이터를 조회하는 getAdminItemPage() 메소드를 추가한다. 데이터의 수정이 일어나지 않기 때문에 Transaction(readOnly=true) 어노테이션을 설정한다.

-ItemService.java

@Transactional(readOnly = true)
    public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
        return itemRepository.getAdminItemPage(itemSearchDto, pageable);
    }

 

- ItemController 클래스에 상품 관리 화면 이동 및 조회한 상품 데이터를 화면에 전달하는 로직을 구현한다. 아래 코드는 3개의 상품만 보여주도록 하는 코드이다.

- ItemController.java

@GetMapping(value = {"/admin/items", "/admin/items/{page}"})
    //value에 상품 관리 화면 진입 시 URL에 페이지 번호가 없는 경우와 페이지 번호가 있는 경우 2가지를 매핑한다.
    public String itemManage(ItemSearchDto itemSearchDto,
                             @PathVariable("page") Optional<Integer> page, Model model) {
        Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 3);
        //페이징을 위해서 PageRequest.of 메소드를 통해 Pageable 객체를 생성한다.
        //첫번째 파라미터로는 조회할 페이지 번호, 두 번째 파라미터로는 한 번에 가지고 올 데이터 수를 넣어준다.
        // URL 경로에 페이지 번호가 있으면 해당 페이지를 조회하도록 세팅하고, 페이지 번호가 없으면 0페이지를 조회하도록 한다.
        Page<Item> items = itemService.getAdminItemPage(itemSearchDto, pageable);
        //조회 조건과 페이징 정보를 파라미터로 넘겨서 Page<Item> 객체를 반환 받는다.
        model.addAttribute("items", items);
        //조회한 상품 데이터 및 페이징 정보를 뷰에 전달한다.
        model.addAttribute("itemSearchDto", itemSearchDto);
        //페이지 전환 시 기존 검색 조건을 유지한 채 이동할 수 있도록 뷰에 다시 전달한다.
        model.addAttribute("maxPage", 5);
        //상품 관리 메뉴 하단에 보여줄 페이지 번호의 최대 개수이다. 5로 설정했으므로 최대 5개의 이동할 페이지 번호만 보여준다.
        return "item/itemMng";
        }
Page 인터페이스는 다음과 같은 주요 메서드와 속성을 가지고 있습니다:

getContent(): 현재 페이지에 포함된 데이터를 리스트로 반환합니다.
getNumber(): 현재 페이지의 인덱스(0부터 시작)를 반환합니다.
getSize(): 페이지당 데이터 개수를 반환합니다.
getTotalElements(): 전체 데이터의 개수를 반환합니다.
getTotalPages(): 전체 페이지 수를 반환합니다.
hasNext(): 다음 페이지가 있는지 여부를 반환합니다.
hasPrevious(): 이전 페이지가 있는지 여부를 반환합니다.

 

- itemMng.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
    <script th:inline="javascript">

        $(document).ready(function(){
            $("#searchBtn").on("click",function(e) {
                e.preventDefault();
                //<검색> 버튼 클릭 시 form 태그의 전송을 막아준다.
                page(0);
                //<검색> 버튼을 클릭할 페이지 번호로 0번째 페이지를 조회하는 page 함수를 호출한다.
            });
        });

        function page(page){
            //page 함수는 이동할 페이지 값을 받아서 현재 조회조건으로 설정된 상품 등록 기간, 판매 상태, 조회 유형, 검색어를
            //파라미터로 설정 후 상품 데이터를 조회한다.
            var searchDateType = $("#searchDateType").val();
            var searchSellStatus = $("#searchSellStatus").val();
            var searchBy = $("#searchBy").val();
            var searchQuery = $("#searchQuery").val();

            location.href="/admin/items/" + page + "?searchDateType=" + searchDateType
                + "&searchSellStatus=" + searchSellStatus
                + "&searchBy=" + searchBy
                + "&searchQuery=" + searchQuery;
        }

    </script>
</th:block>

<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
    <style>
        select{
            margin-right:10px;
        }
    </style>
</th:block>

<div layout:fragment="content">

    <form th:action="@{'/admin/items/' + ${items.number}}" role="form" method="get" th:object="${items}">
        <table class="table">
            <thead>
            <tr>
                <td>상품아이디</td>
                <td>상품명</td>
                <td>상태</td>
                <td>등록자</td>
                <td>등록일</td>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item, status: ${items.getContent()}">
                <td th:text="${item.id}"></td>
                <td>
                    <a th:href="'/admin/item/'+${item.id}" th:text="${item.itemNm}"></a>
                </td>
                <td th:text="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL} ? '판매중' : '품절'"></td>
                <td th:text="${item.createdBy}"></td>
                <td th:text="${item.regTime}"></td>
            </tr>
            </tbody>
        </table>

        <div th:with="start=${(items.number/maxPage)*maxPage + 1}, end=(${(items.totalPages == 0) ? 1 : (start + (maxPage - 1) < items.totalPages ? start + (maxPage - 1) : items.totalPages)})" >
            <ul class="pagination justify-content-center">

                <li class="page-item" th:classappend="${items.first}?'disabled'">
                    <a th:onclick="'javascript:page(' + ${items.number - 1} + ')'" aria-label='Previous' class="page-link">
                        <span aria-hidden='true'>Previous</span>
                    </a>
                </li>

                <li class="page-item" th:each="page: ${#numbers.sequence(start, end)}" th:classappend="${items.number eq page-1}?'active':''">
                    <a th:onclick="'javascript:page(' + ${page - 1} + ')'" th:inline="text" class="page-link">[[${page}]]</a>
                </li>

                <li class="page-item" th:classappend="${items.last}?'disabled'">
                    <a th:onclick="'javascript:page(' + ${items.number + 1} + ')'" aria-label='Next' class="page-link">
                        <span aria-hidden='true'>Next</span>
                    </a>
                </li>

            </ul>
        </div>

        <div class="form-inline justify-content-center" th:object="${itemSearchDto}">
            <select th:field="*{searchDateType}" class="form-control" style="width:auto;">
                <option value="all">전체기간</option>
                <option value="1d">1일</option>
                <option value="1w">1주</option>
                <option value="1m">1개월</option>
                <option value="6m">6개월</option>
            </select>
            <select th:field="*{searchSellStatus}" class="form-control" style="width:auto;">
                <option value="">판매상태(전체)</option>
                <option value="SELL">판매</option>
                <option value="SOLD_OUT">품절</option>
            </select>
            <select th:field="*{searchBy}" class="form-control" style="width:auto;">
                <option value="itemNm">상품명</option>
                <option value="createdBy">등록자</option>
            </select>
            <input th:field="*{searchQuery}" type="text" class="form-control" placeholder="검색어를 입력해주세요">
            <button id="searchBtn" type="submit" class="btn btn-primary">검색</button>
        </div>
    </form>

</div>

</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/layout1}">

<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
    <script th:inline="javascript">

        $(document).ready(function(){
            $("#searchBtn").on("click",function(e) {
                e.preventDefault();
                //<검색> 버튼 클릭 시 form 태그의 전송을 막아준다.
                page(0);
                //<검색> 버튼을 클릭할 페이지 번호로 0번째 페이지를 조회하는 page 함수를 호출한다.
            });
        });

        function page(page){
            //page 함수는 이동할 페이지 값을 받아서 현재 조회조건으로 설정된 상품 등록 기간, 판매 상태, 조회 유형, 검색어를
            //파라미터로 설정 후 상품 데이터를 조회한다.
            var searchDateType = $("#searchDateType").val();
            var searchSellStatus = $("#searchSellStatus").val();
            var searchBy = $("#searchBy").val();
            var searchQuery = $("#searchQuery").val();

            location.href="/admin/items/" + page + "?searchDateType=" + searchDateType
                + "&searchSellStatus=" + searchSellStatus
                + "&searchBy=" + searchBy
                + "&searchQuery=" + searchQuery;
        }

    </script>
</th:block>

- e.preventDefault() : 버튼 클릭 시 form 태그의 전송을 막아준다.

- 위의 코드는 버튼 클릭 시 page(0) 즉, 0번째 페이지를 조회하는 page 함수를 호출한다.

- 검색 버튼 이벤트가 발생하게 되면 page(0)의 함수가 실행되고, 그 아래 itemSearchDto에 입력된 데이터들이 location.herf를 통해 url을 통해 get 방식으로 해당 url에 요청을 보낸다.

 

<form th:action="@{'/admin/items/' + ${items.number}}" role="form" method="get" th:object="${items}">
        <table class="table">
            <thead>
            <tr>
                <td>상품아이디</td>
                <td>상품명</td>
                <td>상태</td>
                <td>등록자</td>
                <td>등록일</td>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item, status: ${items.getContent()}">
                <td th:text="${item.id}"></td>
                <td>
                    <a th:href="'/admin/item/'+${item.id}" th:text="${item.itemNm}"></a>
                </td>
                <td th:text="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL} ? '판매중' : '품절'"></td>
                <td th:text="${item.createdBy}"></td>
                <td th:text="${item.regTime}"></td>
            </tr>
            </tbody>
        </table>
<form th:action="@{'/admin/items/' + ${items.number}}"

 

@GetMapping(value = {"/admin/items", "/admin/items/{page}"})
    //value에 상품 관리 화면 진입 시 URL에 페이지 번호가 없는 경우와 페이지 번호가 있는 경우 2가지를 매핑한다.
    public String itemManage(ItemSearchDto itemSearchDto,
                             @PathVariable("page") Optional<Integer> page, Model model) {
        Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 3);

- 위 코드들에 의해 페이지 번호가 없다면 0을 페이지 번호가 있다면 해당 페이지 번호부터 시작해서 총 3개의 데이터를 불러온다. 페이지가 없으면 0을 페이지가 있다면 해당 페이지 번호를 넘기고 items.number가 그 값을 받는다.

- 상품 관리를 클릭해서 최초로 상품 관리 페이지에 진입하게 되면 최초 {page}의 값은 없기 때문에 0이 items.number로 바인딩 된다.

 

<tr th:each="item, status: ${items.getContent()}">

- items는 참조형은 Page이기 때문에 해당 페이지 범위에 해당하는 데이터만 가지고 있고 getContent 메서드를 통해 해당 페이지에 속하는 데이터들을 가져올 수 있고, each를 통해 해당 데이터들을 순서대로 조회하는 코드다.

 

<tr th:each="item, status: ${items.getContent()}">
                <td th:text="${item.id}"></td>
                <td>
                    <a th:href="'/admin/item/'+${item.id}" th:text="${item.itemNm}"></a>
                </td>
                <td th:text="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL} ? '판매중' : '품절'"></td>
                <td th:text="${item.createdBy}"></td>
                <td th:text="${item.regTime}"></td>
            </tr>

- 페이지에 해당하는 데이터를 하나씩 가져오면서 해당 아이템의 상세 정보들을 불러온다.

- itemSellStatus의 경우 판매 상태가 SELL이면 판매 중으로 같지 않으면 품절로 보여준다.

 

<div th:with="start=${(items.number/maxPage)*maxPage + 1}, end=(${(items.totalPages == 0) ? 1
        : (start + (maxPage - 1) < items.totalPages ? start + (maxPage - 1) : items.totalPages)})" >
        <!--1. th:with는 변수값을 정의할 때 사용한다. 페이지 시작 번호(start)와 페이지 끝 페이지 번호(end)를
            구해서 저장한다. 시작 페이지와 끝과 페이지 번호를 구하는 방법은 다음과 같다.
            - start = (현재 페이지 번호/보여줄 페이지 수) + 1
            - end = start + (보여줄 페이지 수 - 1) -->
            <ul class="pagination justify-content-center">

                <li class="page-item" th:classappend="${items.first}?'disabled'">
                <!--2. 첫 번째 페이지면 이전 페이지로 이동하는 <Previous> 버튼을 선택 불가능하도록 disabled
                    클래스를 추가한다.-->
                    <a th:onclick="'javascript:page(' + ${items.number - 1} + ')'" aria-label='Previous' class="page-link">
                        <!--3. <Previous> 버튼 클릭 시 현재 페이지에서 이전 페이지로 이동하도록 page 함수를 호출한다.-->
                        <span aria-hidden='true'>Previous</span>
                    </a>
                </li>

                <li class="page-item" th:each="page: ${#numbers.sequence(start, end)}" th:classappend="${items.number eq page-1}?'active':''">
                    <!--4. 현재 페이지면 active 클래스를 추가한다.-->
                    <a th:onclick="'javascript:page(' + ${page - 1} + ')'" th:inline="text" class="page-link">[[${page}]]</a>
                    <!--5. 페이지 번호 클릭 시 해당 페이지로 이동하도록 page 함수를 호출한다.-->

                </li>

                <li class="page-item" th:classappend="${items.last}?'disabled'">
                <!--6. 마지막 페이지일 경우 다음 페이지로 이동하는 <Next> 버튼을 선택 불가능하도록 disabled
                    클래스를 추가한다.-->
                    <a th:onclick="'javascript:page(' + ${items.number + 1} + ')'" aria-label='Next' class="page-link">
                        <!--7. <Next> 버튼 클릭 시 현재 페이지에서 다음 페이지로 이동하도록 page 함수를 호출한다.-->
                        <span aria-hidden='true'>Next</span>
                    </a>
                </li>

            </ul>
        </div>

1. th:with는 변수값을 정의할 때 사용한다. 페이지 시작 번호(start)와 페이지 끝 페이지 번호(end)를 구해서 저장한다. 시작 페이지와 끝과 페이지 번호를 구하는 방법은 다음과 같다.
- start = (현재 페이지 번호/보여줄 페이지 수) + 1
- end = start + (보여줄 페이지 수 - 1)

2. 첫 번째 페이지면 이전 페이지로 이동하는 <Previous> 버튼을 선택 불가능하도록 disabled 클래스를 추가한다.

3. <Previous> 버튼 클릭 시 현재 페이지에서 이전 페이지로 이동하도록 page 함수를 호출한다.

4. 현재 페이지면 active 클래스를 추가한다.

5. 페이지 번호 클릭 시 해당 페이지로 이동하도록 page 함수를 호출한다.

6. 마지막 페이지일 경우 다음 페이지로 이동하는 <Next> 버튼을 선택 불가능하도록 disabled 클래스를 추가한다.

7. <Next> 버튼 클릭 시 현재 페이지에서 다음 페이지로 이동하도록 page 함수를 호출한다.

 

 

728x90