πŸ’· πŸ”₯μƒν’ˆ 관리 νŽ˜μ΄μ§€ λ§Œλ“€κΈ°πŸ”₯

gdhiΒ·2023λ…„ 12μ›” 13일
post-thumbnail

πŸ’·μƒν’ˆ 관리 νŽ˜μ΄μ§€

쑰회

  • μƒν’ˆ 등둝일 : All / 1w / 1m / 6m κΉŒμ§€ 쑰회 κ°€λŠ₯
  • μƒν’ˆ 판맀 μƒνƒœ : SELL/SOLD OUT
    μƒν’ˆλͺ… λ˜λŠ” μƒν’ˆ λ“±λ‘μž 아이디

πŸ“ŒQuerydsl

πŸ‘‰ Querydsl μ„€μ • μ™„λ£Œ



πŸ“ŒItemSearchDto 클래슀 생성

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 = ""; // 검색 단어

}



πŸ“Œ1. ItemRepositoryCustom μΈν„°νŽ˜μ΄μŠ€ 생성

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



πŸ“2. ItemRepositoryCustomImpl 클래슀 생성

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 객체둜 λ°˜ν™˜ (?)
    }

}




πŸ“3. ItemRepository 클래슀 상속 λ°›κΈ°

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



πŸ“ŒItemService 클래슀 μˆ˜μ •

...

    // 쑰회 κΈ°λŠ₯μ΄λ―€λ‘œ 읽기 μ „μš© μƒνƒœλ‘œ μ§€μ •
    @Transactional(readOnly = true)
    public Page<Item> getAdminItemPage(ItemSearchDto itemSearchDto, Pageable pageable){
        return itemRepository.getAdminItemPage(itemSearchDto, pageable);
    }

}



πŸ“ŒItemController 클래슀 μˆ˜μ •

...

// μš”μ²­ 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을 뢙인 이유...
μ΄λŸ°μ‹μœΌλ‘œ 숨기면 λ³΄μ•ˆμ΄ κ°•ν™”λœλ‹€.



πŸ“ŒitemMng.html 생성

<!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νŽ˜μ΄μ§€λ‘œ λ„˜μ–΄κ°€μ§€ μ•ŠλŠ” 경우 λ“± 고쳐 λ‚˜κ°€μ•Ό ν•œλ‹€.



❓어떀 ꡬ쑰둜 μ‹€ν–‰?

0개의 λŒ“κΈ€