스프링부트 버전 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)
댓글 남기기