상품 조회 조건
- 상품 등록일
- 상품 판매 상태 [SELL/SOLD OUT]
- 상품명 또는 상품 등록자 아이디
ItemSearchDTO 클래스 생성
- QDomain 클래스 생성 후 ItemSearchDTO 클래스 생성
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ItemSearchDTO {
// 상품등록일
private String searchDateType;
// 판매상태
private ItemSellStatus searchSellStatus;
// 삼품 조회 유형
private String searchBy;
// 조회할 검색어 저장할 변수
private String searchQuery;
}
Querydsl과 Spring Data Jpa를 함께 사용하기 위해서는 '사용자 정의 리파지토리'를 정의해야 함
- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스 구현
- Spring Data Jpa 리포지토리에서 사용자 정의 인터페이스 상속
public interface ItemRepositoryCustom {
Page<Item> getAdminItemPage(ItemSearchDTO itemSearchDTO, Pageable pageable);
}
- 상품 조회 조건을 담고 있는 itemSearchDTO 객체와 페이징 정보를 담고 있는 pageable 객체를 파라미터로 받는 getAdminItemPage 메소드를 정의.
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom{
private JPAQueryFactory queryFactory;
public ItemRepositoryCustomImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
- 동적으로 쿼리 생성하기 위해 JPAQueryFactory 클래스 사용
- JPAQueryFactory의 생성자로 EntityManager 객체를 넣어줌
- EntityManager를 생성자에 주입하는 것은 JPAQueryFactory의 인스턴스를 생성하고 QueryDSL을 통해 데이터베이스 쿼리를 수행하기 위한 기본적인 설정 과정
private BooleanExpression searchSellStatusEq(ItemSellStatus searchSellStatus) {
return searchSellStatus ==
null ? null : QItem.item.itemSellStatus.eq(searchSellStatus);
}
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(6);
}
return QItem.item.regDate.after(dateTime);
}
private BooleanExpression searchByLike(String searchBy, String searchQuery) {
// 상품명에 검색어를 포함하고 있는 상품 또는 상품 생성자 아이디에 검색어 포함하고 있는 상품 조회
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;
}
- BooleanExpression은 QueryDSL에서 쿼리의 조건을 표현하는 데 사용되는 클래스
- 일반적으로 SQL에서 WHERE 절에 해당하는 부분을 Java 코드에서 타입 안전하게 작성할 수 있게 해줌https://cs-ssupport.tistory.com/518
@Override
public Page<Item> getAdminItemPage(ItemSearchDTO itemSearchDTO, Pageable pageable) {
// 조건에 따라 아이템 리스트를 조회
List<Item> content = queryFactory
.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())
.fetch(); // 쿼리를 실행하여 결과 리스트를 반환
// 전체 레코드 수를 계산
Long result = queryFactory.select(Wildcard.count).from(QItem.item)
.where(regDtsAfter(itemSearchDTO.getSearchDateType()),
searchSellStatusEq(itemSearchDTO.getSearchSellStatus()),
searchByLike(itemSearchDTO.getSearchBy(), itemSearchDTO.getSearchQuery()))
.fetchOne(); // 쿼리를 실행하여 단일 결과(전체 레코드 수)를 반환
// NullPointerException을 방지
long total = (result != null) ? result : 0L;
return new PageImpl<>(content, pageable, total);
}
- queryFactory를 이용해서 쿼리를 생성
- where 조건절에서 ',' 단위로 넣어줄 경우 and 조건으로 인식
- PageImpl 클래스는 Page 인터페이스의 기본 구현체로, 페이징 처리된 결과를 담는 객체
• pageable : 페이징 정보
• total : 조건에 맞는 전체 아이템 수
• content : 실제 조회된 아이템 리스트
Querydsl 조회 결과를 반환하는 메소드
메소드 | 기능 |
QueryResults<T> fetchResults() | 조회 대상 리스트 및 전체 개수를 포함하는 QueryResults 반환 |
List<T> fetch() | 조회 대상 리스트 반환 |
T fetchOne() | 조회 대상이 1건이면 해당 타입 반환. 조회 대상이 1건 이상이면 에러 발생. |
T fetchFirst() | 조회 대상이 1건 또는 1건 이상이면 1건만 반환 |
long fetchCount() | 해당 데이터 전체 개수 반환. count 쿼리 실행 |
public interface ItemRepository extends JpaRepository<Item, Long>,
QuerydslPredicateExecutor<Item>, ItemRepositoryCustom {
...
}
- ItemRepository 인터페이스에서 ItemRepositoryCustom 인터페이스를 상속
- QuerydslPredicateExecutor는 Spring Data JPA에서 QueryDSL을 통합하여 제공하는 인터페이스
ItemService 클래스 코드 추가
- 상품 조회 조건과 페이지 정보를 파라미터로 받아서 상품 데이터를 조회하는 getAdminItemPage() 메소드 추가
- 데이터 수정이 일어나지 않으므로 @Transactional(readOnly=true) 어노테이션 설정
@Override
@Transactional(readOnly = true)
public Page<Item> getAdminItemPage(ItemSearchDTO itemSearchDTO, Pageable pageable) {
return itemRepository.getAdminItemPage(itemSearchDTO, pageable);
}
ItemController 클래스 코드 추가
- 상품 관리 화면 이동 및 조회한 상품 데이터를 화면에 전달하는 로직을 구현
@GetMapping(value= {"/admin/items", "/admin/items/{page}"})
public String itemManage(ItemSearchDTO itemSearchDTO,
@PathVariable("page")Optional<Integer> page, Model model) {
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 10);
Page<Item> items = itemService.getAdminItemPage(itemSearchDTO, pageable);
model.addAttribute("items", items);
model.addAttribute("itemSearchDTO", itemSearchDTO);
model.addAttribute("maxPage", 10);
return "item/itemMng";
}
- 페이징을 위해 PageRequest.of 메소드를 통해 Pageable 객체를 생성
- 첫번째 파라미터로 조회할 페이지 번호, 두 번째 파라미터로 한 번에 가지고 올 데이터 수 입력
- URL 경로에 페이지 번호가 있으면 해당 페이지를 조회하도록 세팅하고 페이지 번호가 없으면 0페이지를 조회하도록 함
- 조회 조건과 페이징 정보를 파라미터로 넘겨 Page<Item> 객체를 반환 받음
- 상품 관리 메뉴 하단에 보여줄 페이지 번호의 최대 개수를 10으로 설정
itemMng.html 코드 설정
1. 자바스크립트 코드
$(document).ready(function () {
$("#searchBtn").on("click", function (e) {
e.preventDefault(); // 폼 태그 전속 막아줌
page(0); // 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;
}
2. 검색 기능 코드
<!-- form 태그 -->
<form th:action="@{'/admin/items/' + ${items.number}}"
role="form" method="get" th:object="${items}">
<!-- 헤더 부분-->
<i class="bi bi-clipboard-check-fill fs-1 ms-2" style="color: #FFCFE2;"></i>
<h2 class="card-title ms-2 mt-1 mb-4 fw-bold text-muted">Item List</h2>
<!-- 검색 필터 폼 -->
<div class="form-inline justify-content-center mb-4" th:object="${itemSearchDTO}">
<select th:field="*{searchDateType}" class="form-control" style="width:auto;">
<option value="all">Total</option>
<option value="1d">1 day</option>
<option value="1w">1 week</option>
<option value="1m">1 month</option>
<option value="6m">6 months</option>
</select>
<select th:field="*{searchSellStatus}" class="form-control" style="width:auto;">
<option value="">SELL STATUS(ALL)</option>
<option value="SELL">SELL</option>
<option value="SOLD_OUT">SOLD OUT</option>
</select>
<select th:field="*{searchBy}" class="form-control" style="width:auto;">
<option value="itemNm">Product Name</option>
<option value="createdBy">Writer</option>
</select>
<input th:field="*{searchQuery}" type="text" class="form-control w-25"
placeholder="Please enter a search word">
<button id="searchBtn" type="submit" class="btn btn-dark ms-2">Search</button>
</div>
전체적인 동작 방식
1. 사용자가 검색 조건(날짜, 판매 상태, 기준, 검색어)을 설정하고, 검색 버튼을 클릭
2. 폼이 제출되면서, URL에 설정된 경로(@{'/admin/items/' + ${items.number}})로 GET 요청이 전송
3. 서버는 ItemSearchDTO 객체에 바인딩된 폼 데이터를 받아 검색 조건에 맞는 아이템 리스트를 조회
4. 조회된 결과는 페이징된 형태로 사용자에게 반환되며, 해당 페이지가 렌더링됨
3. 조회 상품 목록 코드
<!-- 테이블 구조 -->
<table class="table">
<!-- 테이블 헤더 -->
<thead>
<tr>
<th scope="col">Id</th>
<th scope="col">Product Name</th>
<th scope="col">Sell Status</th>
<th scope="col">Writer</th>
<th scope="col">Date</th>
</tr>
</thead>
<!-- 테이블 바디 -->
<tbody>
<tr th:each="item, status: ${items.getContent()}">
<th scope="col">[[${item.id}]]</th>
<td>
<a th:href="'/admin/item/'+${item.id}" th:text="${item.itemNm}"></a>
</td>
<td th:text="${item.itemSellStatus.toString()}"></td>
<td th:text="${item.createdBy}"></td>
<td>[[${#temporals.format(item.regDate, 'yyyy-MM-dd')}]]</td>
</tr>
</tbody>
</table>
전체적인 동작 방식
1. 데이터 바인딩 : items.getContent()에서 가져온 데이터 리스트를 바탕으로 각 아이템의 정보를 테이블 행(tr)으로 출력
2. 동적 데이터 렌더링 : 각 열(th, td)에 동적으로 데이터를 렌더링하여 아이템의 세부 정보를 표시
3. 클릭 가능한 링크 : Product Name 열에 아이템 상세 페이지로 이동할 수 있는 링크를 제공
4. 날짜 포맷팅 : 등록 날짜는 yyyy-MM-dd 형식으로 포맷팅되어 출력
4. 하단 페이지 번호 코드
<!-- 페이지 번호의 시작점(start)과 끝점(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” 버튼을 생성 -->
<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'>Prev</span>
</a>
</li>
<!--동적 페이지 번호 생성-->
<th:block th:each="i: ${#numbers.sequence(start, end)}">
<li th:class="${items.number == i} ? 'page-item active' : 'page-item'">
<a class="page-link" th:data-num="${i}">[[${i}]]</a>
</li>
</th:block>
<!-- “Next” 버튼 생성 -->
<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>
start | 현재 페이지 번호(items.number)를 기준으로 시작 페이지 번호를 계산. maxPage는 페이지네이션에서 보여줄 최대 페이지 수 |
end | 표시할 마지막 페이지 번호를 계산. items.totalPages는 전체 페이지 수를 나타내며, 시작 번호에서 최대 maxPage만큼 더한 값이 전체 페이지 수를 넘지 않도록 조정. |
th:classappend="${items.first}?' disabled' |
현재 페이지가 첫 번째 페이지인지(items.first) 확인하고, 첫 번째 페이지라면 "disabled" 클래스를 추가하여 버튼을 비활성화 |
th:onclick="'javascript:page(' + ${items.number - 1} + ')'" | “Previous” 버튼을 클릭하면 이전 페이지 번호를 가진 page() 함수가 호출 |
th:each="i: ${#numbers.sequence(start, end)}" | start부터 end까지의 숫자 시퀀스를 생성하여, 각 숫자에 대해 반복 |
th:class="${items.number == i} ? 'page-item active' : 'page-item' | 현재 페이지 번호(items.number)와 반복 중인 페이지 번호(i)가 일치하면 active 클래스를 추가하여 현재 페이지를 강조 |
th:classappend="${items.last}?' disabled' |
현재 페이지가 마지막 페이지인지(items.last) 확인하고, 마지막 페이지라면 "disabled" 클래스를 추가하여 버튼을 비활성화 |
th:onclick="'javascript:page(' + ${items.number + 1} + ')'" | “Next” 버튼을 클릭하면 다음 페이지 번호를 가진 page() 함수가 호출 |
'Spring & Spring Boot' 카테고리의 다른 글
Spring Boot 쇼핑몰 프로젝트 | 상품 수정 (0) | 2024.08.13 |
---|---|
Spring Boot 쇼핑몰 프로젝트 | 상품 등록 (0) | 2024.08.04 |
Spring Boot 쇼핑몰 프로젝트 | 엔티티 연관 관계 매핑, 엔티티 공통 속성 공통화 (0) | 2024.08.02 |
Spring Boot 쇼핑몰 프로젝트 | 로그인/로그아웃 화면 연동, 페이지 권한 설정 (0) | 2024.08.01 |
Spring Boot 쇼핑몰 프로젝트 | 로그인 기능 구현 (0) | 2024.08.01 |