스프링부트 버전 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)+")'; >«</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) +")'; >»</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)+"'>«</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)+"'>»</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)+"'>«</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)+"'>»</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
댓글 ( 0)
댓글 남기기