스프링

 

스프링부트 버전  3.2.1

 

 

1. 컨트롤

ItemController

    @GetMapping(value = {"/admin/items", "/admin/items/{page}"})
    public String itemManage(ItemSearchDto itemSearchDto, @PathVariable("page") Optional<Integer> page, PageMaker pageMaker,
                             Model model){
        int pageInt=page.orElse(0);
        Pageable pageable = PageRequest.of(pageInt, 10);
        Page<Item> items = itemService.getAdminItemPage(itemSearchDto, pageable);

        /// pagination html  처리
        String pagination=pageMaker.pageObject(items, pageInt, 10, 5 ,  "/admin/items/" ,"js" );

        model.addAttribute("items", items);
        model.addAttribute("itemSearchDto", itemSearchDto);
        model.addAttribute("maxPage", 5);
        model.addAttribute("pagination", pagination);
        return "item/itemMng";
    }

 

 

 

2.PageMaker

import lombok.Data;
import lombok.ToString;
import org.springframework.data.domain.Page;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

//MySQL PageMaker
@Data
@ToString
public class PageMaker {

    private int page;
    private int pageSize=10;
    private int pageStart;
    private int totalCount; //전체 개수
    private int startPage; // 시작 페이지
    private int endPage;   // 끝페이지
    private boolean prev;  // 이전 여부
    private boolean next;  // 다음 여부
    private boolean last;  //마지막 페이지 여부
    private int displayPageNum=10;  //하단 페이징  << 1 2 3 4 5 6 7 8 9 10 >>
    private int tempEndPage;
    private String searchQuery;

    Page<?>  pageObject;

    private void calcData(){
        endPage=(int)(Math.ceil(page / (double)displayPageNum)*displayPageNum);
        startPage=(endPage - displayPageNum) +1;

        if(endPage>=tempEndPage)endPage=tempEndPage;
        prev =startPage ==1 ? false :true;
        next =endPage *pageSize >=totalCount ? false :true;
    }

    /**
     *
     *
     * @param pageObject  Page<?> 반환된 리스트값
     * @param pageInt 현재 페이지
     * @param pageSize  페이지사이즈
     * @param displayPageNum  하단 페이징 기본 10설정 << 1 2 3 4 5 6 7 8 9 10 >>
     * @param pageUrl  url 주소
     * @param type   ajax, href =  자바스크립트 ,  링크
     * @return
     */
    public String pageObject(Page<?> pageObject,  Integer pageInt ,
                             Integer pageSize, Integer displayPageNum , String pageUrl, String type) {
        this.pageObject = pageObject;
        this.page=pageInt==0? 1:pageInt+1;
        if(pageSize!=null){
            this.pageSize=pageSize;
        }
        this.tempEndPage=pageObject.getTotalPages();
        if(displayPageNum!=null){
            this.displayPageNum=displayPageNum;
        }else this.displayPageNum=10;

        this.totalCount=Math.toIntExact(pageObject.getTotalElements());
        calcData();

        if(StringUtils.hasText(pageUrl)){

            if(type.equalsIgnoreCase("JS")){
                return paginationJs(pageUrl);
            }else if(type.equalsIgnoreCase("HREF")){
                return paginationHref(pageUrl);
            }else if(type.equalsIgnoreCase("PATHVARIABLE")){
                return paginationPathVariable(pageUrl);
            }

        }return null;
    }

    /**
     * javascript page 버튼 클릭  반환
     * @param url
     * @return
     */
    public String paginationJs(String url) {
        StringBuffer sBuffer = new StringBuffer();
        sBuffer.append("<ul class='pagination justify-content-center'>");
        if (prev) {
            sBuffer.append("<li class='page-item' ><a class='page-link' onclick='javascript:page(0)' >처음</a></li>");
        }

        if (prev) {
            sBuffer.append("<li class='page-item'><a  class='page-link' onclick='javascript:page("+ (startPage - 2)+")';  >&laquo;</a></li>");
        }

        String active = "";
        for (int i = startPage; i <= endPage; i++) {

            if (page==i) {
                active = "class='page-item active'";
            } else {
                active = "class='page-item'";
            }

            sBuffer.append("<li " + active + " >");
            sBuffer.append("<a class='page-link'  onclick='javascript:page("+ (i-1) +")'; >" + i + "</a></li>");
            sBuffer.append("</li>");
        }


        if (next && endPage > 0 && endPage <= tempEndPage) {
            sBuffer.append("<li class='page-item'><a  class='page-link' onclick='javascript:page("+ (endPage) +")';  >&raquo;</a></li>");
        }


        if (next && endPage > 0 && !isLast()) {
            sBuffer.append("<li class='page-item'> <a class='page-link' onclick='javascript:page("+ (tempEndPage-1) +")'; >마지막</a></li>");
        }

        sBuffer.append("</ul>");
        return sBuffer.toString();
    }




    public String makeSearch(int page){
        UriComponents uriComponents=
                UriComponentsBuilder.newInstance()
                        .queryParam("searchQuery", searchQuery)
                        .queryParam("page", page)
                        .build();
        return uriComponents.toUriString();
    }

    /**
     * 링크 파리미터 반환
     * @param url
     * @return
     */
    public String paginationHref(String url){
        StringBuffer sBuffer=new StringBuffer();
        sBuffer.append("<ul class='pagination justify-content-center'>");
        if(prev){
            sBuffer.append("<li class='page-item'><a  class='page-link' href='"+url+makeSearch(1)+"'>처음</a></li>");
        }

        if(prev){
            sBuffer.append("<li class='page-item'><a  class='page-link' href='"+url+makeSearch(startPage-2)+"'>&laquo;</a></li>");
        }

        String active="";
        for(int i=startPage; i <=endPage; i++){

            if (page==i) {
                active = "class='page-item active'";
                sBuffer.append("<li " +active+"  > ");
                sBuffer.append("<a class='page-link' href='javascript:void(0)'>"+i+"</a></li>");
                sBuffer.append("</li>");

            } else {
                active = "class='page-item'";
                sBuffer.append("<li " +active+"  > ");
                sBuffer.append("<a class='page-link' href='"+url+makeSearch(i-1)+"'>"+i+"</a></li>");
                sBuffer.append("</li>");
            }

        }

        if(next && endPage>0  && endPage <= tempEndPage){
            sBuffer.append("<li class='page-item'><a class='page-link' href='"+url+makeSearch(endPage)+"'>&raquo;</a></li>");
        }

        if (next && endPage > 0 && !isLast()) {
            sBuffer.append("<li class='page-item'><a  class='page-link' href='"+url+makeSearch(tempEndPage-1)+"'>마지막</a></li>");
        }

        sBuffer.append("</ul>");
        return sBuffer.toString();
    }




    public String paginationPathVariable(String url){
        StringBuffer sBuffer=new StringBuffer();
        sBuffer.append("<ul class='pagination justify-content-center'>");
        if(prev){
            sBuffer.append("<li class='page-item'><a  class='page-link' href='"+url+(1)+"'>처음</a></li>");
        }

        if(prev){
            sBuffer.append("<li class='page-item'><a  class='page-link' href='"+url+(startPage-2)+"'>&laquo;</a></li>");
        }

        String active="";
        for(int i=startPage; i <=endPage; i++){

            if (page==i) {
                active = "class='page-item active'";
                sBuffer.append("<li " +active+"  > ");
                sBuffer.append("<a class='page-link' href='javascript:void(0)'>"+i+"</a></li>");
                sBuffer.append("</li>");

            } else {
                active = "class='page-item'";
                sBuffer.append("<li " +active+"  > ");
                sBuffer.append("<a class='page-link' href='"+url+(i-1)+"'>"+i+"</a></li>");
                sBuffer.append("</li>");
            }

        }

        if(next && endPage>0  && endPage <= tempEndPage){
            sBuffer.append("<li class='page-item'><a class='page-link' href='"+url+(endPage)+"'>&raquo;</a></li>");
        }

        if (next && endPage > 0 && !isLast()) {
            sBuffer.append("<li class='page-item'><a  class='page-link' href='"+url+(tempEndPage-1)+"'>마지막</a></li>");
        }

        sBuffer.append("</ul>");
        return sBuffer.toString();
    }

}

 

 

 

3.서비스

ItemService

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

 

 

 

 

3.Repository

1)ItemRepositoryCustom 

import com.shop.dto.ItemSearchDto;
import com.shop.entity.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;



public interface ItemRepositoryCustom {
  Page<Item>  getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) ;

}

 

 

2)ItemRepositoryCustomImpl

package com.shop.repository;

import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemSearchDto;
import com.shop.dto.MainItemDto;
import com.shop.dto.QMainItemDto;
import com.shop.entity.Item;
import com.shop.entity.QItem;
import com.shop.entity.QItemImg;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.thymeleaf.util.StringUtils;

import java.time.LocalDateTime;
import java.util.List;

import static com.shop.entity.QItem.item;

@RequiredArgsConstructor
@Log4j2
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {


  private final JPAQueryFactory queryFactory;


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


  /**
   * searchDateType 의 값에 따라서 dateTime 의 값을 이전 시간의 값으로 세팅 후
   * 해당 시간 이후로 등록된 상품만 조회합니다. ddateTime 의 시간을 한달 전으로
   * 세팅 후 최근 한달 동안 등록된 상품만 조회하도록 조건값을 반환합니다.
   *
   * @param searchDateType
   * @return
   */
  private BooleanExpression regDtsAfter(String searchDateType) {
    LocalDateTime dateTime = LocalDateTime.now();

    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(1);
    }

    return item.regTime.after(dateTime);
  }


  /**
   * searchBy 의 값에 따라서 상품명에 검색어를 포함하고 있는 상품 또는 상품
   * 생성자의 아이디에 검색어를 포하하고 있는 상품을 조회하도록 조건값을 반환합니다.
   *
   * @param searchBy
   * @param searchQuery
   * @return
   */
    private BooleanExpression searchByLike(String searchBy, String searchQuery) {
      if (StringUtils.equals("itemNm", searchBy)) {
        return item.itemNm.like("%" + searchQuery + "%");

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


  @Override
  public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
    /**
     * where 조건절 :  BooleanExpression 반환하는 조건문을 넣어 줍니다.
     * "," 단위로 넣어줄 경우 and 조건으로 인식합니다.
     */
    log.info("***** itemSearchDto  : {}", itemSearchDto.toString());

    List<Item> content = queryFactory.selectFrom(item)
        .where(
            regDtsAfter(itemSearchDto.getSearchDateType()),
            searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
            searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery())
        )
        .orderBy(item.id.desc())
        .offset(pageable.getOffset())  //데이터를 가지고 올 시작 인텍스를 지정
        .limit(pageable.getPageSize()) //한번에 가지고 올 최대 개수를 지정
        .fetch();

    //fetchResults() : 조회한 리스트 및 전체 개수를 포함하는 QueryResult 를 반환
    //상품 데이터 리스트 조회 및 상품 데이터 전체 개수를 조회하는 2번의 쿼리문이 실행된다.

    JPAQuery<Long> countQuery = queryFactory.
        select(item.count())
        .from(item)
        .where(
            regDtsAfter(itemSearchDto.getSearchDateType()),
            searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
            searchByLike(itemSearchDto.getSearchBy(), itemSearchDto.getSearchQuery())
        );

    return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
  }


  

  /**
   * 검색어가 null 이 아니면 상품명에 해당 검색어가 포함되는 상품을 조회하는 조건을 반환합니다.
   * @param searchQuery
   * @return
   */
    private BooleanExpression itemNmLike(String searchQuery){
       return StringUtils.isEmpty(searchQuery) ? null :  item.itemNm.like("%" +searchQuery +"%");
    }



    @Override
    public Page<MainItemDto> getMainItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {

      QItem item= QItem.item;
      QItemImg itemImg=QItemImg.itemImg;

      List<MainItemDto> content = queryFactory.select(
              new QMainItemDto(
                  item.id,
                  item.itemNm,
                  item.itemDetail,
                  itemImg.imgUrl,
                  item.price
              )
          ).from(itemImg)
          .join(itemImg.item, item)  //itemImg 와  item 을 내부 조인
          .where(itemImg.repimgYn.eq("Y"))  //상품 이미지의 경우 대표 상품 이미지만 불러온다.
          .where(itemNmLike(itemSearchDto.getSearchQuery()))
          .orderBy(item.id.desc())
          .offset(pageable.getOffset())
          .limit((pageable.getPageSize()))
          .fetch();

      JPAQuery<Long> countQuery = queryFactory.select(
              itemImg.count()
          ).from(itemImg)
          .join(itemImg.item, item)  //itemImg 와  item 을 내부 조인
          .where(itemImg.repimgYn.eq("Y"))  //상품 이미지의 경우 대표 상품 이미지만 불러온다.
          .where(itemNmLike(itemSearchDto.getSearchQuery()));


      return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
    }


}

 

 

 

3)ItemRepository

import com.shop.entity.Item;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.query.Param;

import java.util.List;

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

}

 

 

 

 

 

 

 

4. thymeleaf 뷰 처리

 

<!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();
                page(0);
            });
        });

        function 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">


  <div class="form-inline justify-content-center mb-5" 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>


  <div class="mt-3">
  <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'>이전</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'>다음</span>
          </a>
        </li>

      </ul>
    </div>


    <div th:utext="${pagination}"></div>



  </form>

  </div>


</div>

</html>

 

 

 

Item

import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemFormDto;
import com.shop.entity.base.BaseEntity;
import com.shop.exception.OutOfStockException;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Entity
@Table(name = "item")
@Getter
@Setter
@ToString
public class Item extends BaseEntity {

    @Id
    @Column(name = "item_id")
    @GeneratedValue(strategy =GenerationType.IDENTITY )
    private Long id; //상품코드

    @Column(nullable = false, length = 50)
    private String itemNm; //상품명

    @Column(name="price", nullable = false)
    private int price; //가격

    @Column(nullable= false)
    private int stockNumber; //재고수량

    @Lob
    @Column(nullable = false)
    private String itemDetail; //상품 상세 설명

    @Enumerated(EnumType.STRING)
    private ItemSellStatus itemSellStatus; //상품 판매 상태

    public void updateItem(ItemFormDto itemFormDto){
        this.itemNm = itemFormDto.getItemNm();
        this.price = itemFormDto.getPrice();
        this.stockNumber = itemFormDto.getStockNumber();
        this.itemDetail = itemFormDto.getItemDetail();
        this.itemSellStatus = itemFormDto.getItemSellStatus();
    }

    public void removeStock(int stockNumber){
        int restStock = this.stockNumber - stockNumber;
        if(restStock < 0){
            throw new OutOfStockException("상품의 재고가 부족합니다. (현재 재고 수량 : " + this.stockNumber + ")");
        }
        this.stockNumber = restStock;
    }

    public void addStock(int stockNumber){
        this.stockNumber += stockNumber;
    }

}

 

 

ItemSearchDto

import com.shop.constant.ItemSellStatus;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class ItemSearchDto {

  /**
   * 현재 시간과 상품 등록일을 비교해서 상품 데이터를 조회합니다. 조회시간 기준은 아래와 같습니다.
   * all: 상품 등록일 전체
   * 1d: 최근 하루 동안 등록된 상품
   * 1w: 최근 일주일 동안 등록된 상품
   * 1m: 최근 한달 동안 등록된 상품
   * 6m: 최근 6개월 동안 등록된 상품
   *
   */
  private String searchDateType;

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


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

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




}

 

 

 

 

 

EX )

컨트롤

    @GetMapping(value = {"/", ""})
    public String main(ItemSearchDto itemSearchDto, @PathVariable(value ="page") Optional<Integer> page, PageMaker pageMaker, Model model){
        int pageInt=page.orElse(0);
        if(pageInt==0&&pageMaker.getPage()!=0)pageInt=pageMaker.getPage();

        Pageable pageable = PageRequest.of(pageInt , 6);
        Page<MainItemDto> items = itemService.getMainItemPage(itemSearchDto, pageable);

        String pagination = pageMaker.pageObject(items, pageInt, 6, 5, "/" , "href");
        log.info("************** pagination  {}:", pageMaker.toString());

        model.addAttribute("items", items);
        model.addAttribute("itemSearchDto", itemSearchDto);
        model.addAttribute("pagination", pagination);
        return "main";
    }

 

 

 

 

 

 

 

  <input type="hidden" name="searchQuery" th:value="${itemSearchDto.searchQuery}">

  <div th:if="${not #strings.isEmpty(itemSearchDto.searchQuery)}" class="center">
    <p class="h3 font-weight-bold" th:text="${itemSearchDto.searchQuery}+ '검색 결과'"></p>
  </div>


  <div class="row mt-5">
    <th:block th:each="item, status:${items.getContent()}">
          <div class="col-md-4 margin">
            <div class="card">
               <a th:href="'/item/'+${item.id}" class="text-dark">
                 <img th:src="${item.imgUrl}" class="card-img-top" th:alt="${item.itemNm}" height="400">
                 <div class="card-body">
                    <h4 class="card-title">[[${item.itemNm}]]</h4>
                    <p class="card-text">[[${item.itemDetail}]]</p>
                    <h3 class="card-title text-danger">[[${item.price}]]원</h3>
                 </div>
               </a>
            </div>
          </div>
    </th:block>
  </div>



  <div class="mt-5">
      <div th:utext="${pagination}"></div>
  </div>

 

 

 

 

 

 

 

 

 

 

 

 

 

소스 :

https://github.dev/braverokmc79/jpa-shop-test2

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

All great song has been sincere song. (진실하지 않은 노래는 생명이 짧다.)

댓글 ( 0)

댓글 남기기

작성