
μ‘°ν
- μν λ±λ‘μΌ :
All / 1w / 1m / 6mκΉμ§ μ‘°ν κ°λ₯- μν νλ§€ μν :
SELL/SOLD OUT
μνλͺ λλ μν λ±λ‘μ μμ΄λ



π Querydsl μ€μ μλ£
package com.shop.dto;
import com.shop.constant.ItemSellStatus;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ItemSearchDto {
private String searchDateType; // μ‘°ν λ μ§ All / 1w / 1m / 6m
private ItemSellStatus searchSellStatus; // μν νλ§€ μν (constant) SELL/SOLD OUT
private String searchBy; // μ‘°ν μ ν createBy...
private String searchQuery = ""; // κ²μ λ¨μ΄
}
Querydslκ³ΌJPAλ₯Ό ν¨κ» μ¬μ©νκΈ° μν΄ λ§λ€μ΄μ§λ μ¬μ©μ μ μRepositoryλ λ€μ λ¨κ³λ₯Ό κ±°μ³ κ΅¬ν
1. μ¬μ©μ μ μ μΈν°νμ΄μ€ μμ±
2. μ¬μ©μ μ μ μΈν°νμ΄μ€ ꡬν
3.JPA Repositoryμμ μ¬μ©μ μ μ μΈν°νμ΄μ€ μμ
package com.shop.repository;
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);
}
package com.shop.repository;
import com.querydsl.core.QueryResults;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.shop.constant.ItemSellStatus;
import com.shop.dto.ItemSearchDto;
import com.shop.entity.Item;
import com.shop.entity.QItem;
import jakarta.persistence.EntityManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.thymeleaf.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {
// λμ 쿼리λ₯Ό μ¬μ©νκΈ° μν΄ JPAQueryFactory λ³μ μ μΈ
private JPAQueryFactory queryFactory;
// μμ±μ
public ItemRepositoryCustomImpl(EntityManager em){
// JPAQueryFactoryμ μ€μ§μ μΈ κ°μ²΄ μμ±
this.queryFactory = new JPAQueryFactory(em);
}
// BooleanExpression(λ©μλν κ°λ₯. BooleanBuilder λ λ°λ‘ μ¨μ μμ) μ ν΅ν΄ where μ μ μ μ©λ μ‘°ν 쑰건μ μμ±
// μν νλ§€ μν 쑰건
private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus){
// μν νλ§€ μ‘°κ±΄μ΄ μ μ²΄μΌ κ²½μ°, null λ¦¬ν΄ (where μ μμ μ‘°κ±΄μ΄ λ¬΄μλ¨)
// μν νλ§€ μ‘°κ±΄μ΄ νλ§€μ€ or νμ μΌ κ²½μ°, where 쑰건 νμ±ν
return searchSellStatus == null ? null : QItem.item.itemSellStatus.eq(searchSellStatus);
}
// μν λ±λ‘μΌ μ‘°κ±΄
private BooleanExpression regDtsAfter(String searchDateType){ // all, 1d, 1w, 1m, 6m
LocalDateTime dateTime = LocalDateTime.now(); // νμ¬ μκ° μΆμΆ
// BooleanExpresiion κ°μ΄ null μ΄λ©΄ ν΄λΉ μ‘°ν 쑰건μ μ¬μ©νμ§ μκ² λ€λ μλ―Έ π all
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);
}
// dateTimeμ μκ°μ λ§κ² μ€μ ν μκ°μ΄ λ§λ λ±λ‘λ μνμ΄ μ‘°ν λλλ‘ μ‘°κ±΄ κ° λ°ν
return QItem.item.regTime.after(dateTime);
}
// μνλͺ
λλ μν λ±λ‘μ μμ΄λ 쑰건
private BooleanExpression searchByLike(String searchBy, String searchQuery){
// searchBy κ°μ λ°λΌ (where 쑰건μΌλ‘) μνμ μ‘°ν νλλ‘ μ‘°κ±΄κ°μ λ°νν¨
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;
}
// queryFactory λ‘ μΏΌλ¦¬ λμ μμ±
@Override
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable) {
QueryResults<Item> results = queryFactory
.selectFrom(QItem.item)
.where(regDtsAfter(itemSearchDto.getSearchDateType()),
searchSellStatusEq(itemSearchDto.getSearchSellStatus()),
searchByLike(itemSearchDto.getSearchBy(),
itemSearchDto.getSearchQuery())) // , λ and 쑰건μ
.orderBy(QItem.item.id.desc())
.offset(pageable.getOffset()) // λ°μ΄ν°λ₯Ό κ°μ§κ³ μ¬ μμ μΈλ±μ€λ₯Ό μ§μ
.limit(pageable.getPageSize()) // ν λ²μ κ°μ§κ³ μ¬ μ΅λ κ°μ μ§μ
.fetchResults(); // QueryResults λ₯Ό λ°ν (μν λ°μ΄ν° 리μ€νΈ μ‘°ν , μν λ°μ΄ν° μ 체 κ°μ μ‘°νν¨)
List<Item> content = results.getResults();
long total = results.getTotal();
return new PageImpl<>(content, pageable, total); // μ‘°νν λ°μ΄ν°λ₯Ό Page ν΄λμ€μ ꡬνμ²΄μΈ PageImpl κ°μ²΄λ‘ λ°ν (?)
}
}
public interface ItemRepository extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item>, ItemRepositoryCustom {
...
// μ‘°ν κΈ°λ₯μ΄λ―λ‘ μ½κΈ° μ μ© μνλ‘ μ§μ
@Transactional(readOnly = true)
public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable){
return itemRepository.getAdminItemPage(itemSearchDto, pageable);
}
}
...
// μμ² URLμ΄ νμ΄μ§ λ²νΈκ° μλ κ²½μ°μ μλ κ²½μ° 2κ°μ§λ₯Ό λ§€ν
// Mapping νλΌλ―Έν°λ‘ κ°μ²΄λ₯Ό μ§μ νλ©΄(ItemSearchDto) μλμΌλ‘ new κ°μ²΄ μμ±
@GetMapping(value = {"/admin/items", "/admin/items/{page}"})
public String itemManage(ItemSearchDto itemSearchDto,
// URLμ ν΅ν΄ νμ΄μ§ λ²νΈκ° λμ΄μ€λ κ²½μ° @PathVariable λ‘ λ³μ κ° λ§€ν
@PathVariable("page") Optional<Integer> page, // nullμ΄μ΄λ λ¬Έμ β
Model model){
// PageRequest.of() λ₯Ό ν΅ν΄μ νμ΄μ§μ μ 무λ₯Ό νμΈν νμ Pageable κ°μ²΄ μμ±
// 첫 λ²μ§Έ νλΌλ―Έν°λ μ‘°νν νμ΄μ§ λ²νΈ, λ λ²μ§Έλ ν λ²μ κ°μ Έμ¬ λ°μ΄ν° μ
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 5);
// itemSearchDto: μ‘°ν 쑰건 pageable: νμ΄μ§ μ 보
Page<Item> items = itemService.getAdminItemPage(itemSearchDto, pageable);
// item: μ‘°νν μν λ°μ΄ν°
model.addAttribute("items", items);
// νμ΄μ§ μ ν μ κΈ°μ‘΄ κ²μ 쑰건μ μ μ§ν μ± μ΄λν μ μκ² λ·°μ μ λ¬
model.addAttribute("itemSearchDto", itemSearchDto);
// μ΅λ 5κ°μ μ΄λν νμ΄μ§ λ²νΈλ₯Ό 보μ¬μ€
model.addAttribute("maxPage", 5);
// μ‘°νν μν λ°μ΄ν° μ λ¬λ°λ νμ΄μ§
return "item/itemMng";
}
β
ItemRepositoryλItemRepositoryCustomλ₯Ό μμ λ°μλλ°ItemRepositoryCustomμ μμμΈItemRepositoryCustomImplμ λ©μλgetAdminItemPageλ₯Ό μ°κ³ μλ€?
νμ 건 λͺ» μ°λ κ² μλκ°??? μ΄λ»κ² μ΄κ±°μ§?? μ΄λ Έν μ΄μ λ μκ³ λμ§???π μ€νλ§μ΄ ν΄μ€λ€.
ν΄λμ€λͺ μ
Implμ λΆμΈ μ΄μ ...
μ΄λ°μμΌλ‘ μ¨κΈ°λ©΄ 보μμ΄ κ°νλλ€.

<!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 () {
// "κ²μ" λ²νΌμ΄ λ리면
$("serachBtn").on("click", function (e) {
e.preverntDefault(); // form νκ·Έμ μ μ‘μ λ§μ (νμ΄μ§ λ²νΈκ° κ·Έλλ‘ λμ΄κ° μ μμ)
// ( <form th:action="@{'/admin/items/' ... μ΄κ±°λ₯Ό λ§μ)
page(0); // νμ΄μ§ λ²νΈλ₯Ό 0μΌλ‘ μ€μ ν λ€ 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">
<!-- ${items} λ³μλ Controller μμ μ λ¬ λ°μ Page κ°μ²΄ -->
<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>
<!-- .getContent() λ©μλλ₯Ό μ΄μ©νμ¬ Page κ°μ²΄μ content λΆλΆμ μΆμΆ -->
<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>
<!-- Thymeleaf μ T("fully qualified class name") ννμμ μ¬μ©νλ€λ©΄ μ€νλ§ λ΄μ μ‘΄μ¬νλ ν΄λμ€μ μ κ·Ό κ°λ₯ -->
<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>
<!-- νμ΄μ§ κ°μ²΄μ νμ΄μ§ index λ 0λΆν° μμ, νμ΄μ§ νμ λ²νΈλ 1λΆν° μμ -->
<!-- th:with μ ν΅ν΄μ λ³μκ°μ μ μ -->
<!-- start (νμ΄μ§ μμ λ²νΈ) : ((νμ¬ νμ΄μ§λ²νΈ / 보μ¬μ€ νμ΄μ§ μ) * 보μ¬μ€ νμ΄μ§μ) + 1 -->
<!-- end (νμ΄μ§ λ λ²νΈ) : start + (보μ¬μ€ νμ΄μ§ μ - 1) -->
<!-- νμ΄μ§μ μ΄ κ°μκ° 0κ°λ©΄ end = 1, start + (보μ¬μ€ νμ΄μ§ μ - 1) λ³΄λ€ μλ€λ©΄ end = νμ΄μ§ μ΄ κ°μ -->
<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">
<!-- μ΄μ (Previous) λ²νΌ -->
<!-- 첫 νμ΄μ§λ©΄ class μλ³μλ₯Ό μΆκ°(classappend) νμ¬ disabled μ μ© -->
<!-- 첫 νμ΄μ§μμ Previous λΉνμ±ν, λ§μ§λ§ νμ΄μ§μμ Next λΉνμ±ν -->
<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>
<!-- λ±λ‘λ μνλ€ -->
<!-- νμ΄μ§ λ²νΈλ₯Ό ν΄λ¦νλ©΄ μμ μ‘΄μ¬νλ μ€ν¬λ¦½νΈμ page() ν¨μ μν -->
<!-- νμ¬ νμ΄μ§ νμ active -->
<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" style="cursor : pointer;">[[${page}]]</a>
</li>
<!-- λ€μ(Next) λ²νΌ -->
<!-- λ§μ§λ§ νμ΄μ§λ©΄ class μλ³μλ₯Ό μΆκ°(classappend) νμ¬ disabled μ μ© -->
<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>
<!-- κ²μ 쑰건 -->
<!-- th:object ${itemSearchDto} κ°μ²΄μ th:field λ§€ν -->
<div class="row gy-2 gx-3 align-items-center" th:object="${itemSearchDto}">
<select th:field="*{searchDateType}" class="form-select" 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-select" 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-select" placeholder="κ²μμ΄λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ">
<button id="searchBtn" type="submit" class="btn btn-primary">κ²μ</button>
</div>
</form>
</div>
</html>


π 26κ° μν λ±λ‘, 1 νμ΄μ§

π 2 νμ΄μ§, 2 ν΄λ¦ μ΄λ / Next, Previous μ΄λ μ λΆ μ μλ νλ€

π 6νμ΄μ§

π μνλͺ 3 κ²μ

π νμ κ²μ

π λ±λ‘μ 1 κ²μ
β μμν λ²κ·Έκ° μ’ μλ€. 2νμ΄μ§μμ κ²μ ν κ²½μ° 1νμ΄μ§λ‘ λμ΄κ°μ§ μλ κ²½μ° λ± κ³ μ³ λκ°μΌ νλ€.