쇼핑몰 웹사이트 만들어보기 - 검색결과 정렬 , 뷰, 카테고리 기능 추가

Shiba·2024년 8월 30일
0

프로젝트 및 일기

목록 보기
29/29

검색결과 정렬 , 뷰 , 카테고리 기능 추가

이번 시간에는 검색 결과 창을 더 사용자가 사용하기 편하도록 기능을 더 추가해보았다.

검색결과 정렬하기

검색결과를 정렬하는게 가장 쉬웠다. 프론트쪽에서 콤보박스로 정렬방식을 선택하면 해당 값을 파라미터에 삽입하여 정렬을 해주는 방식이다.

백엔드 코드 작성

검색결과 컨트롤러 수정

@GetMapping("/search")
    public String search( @RequestParam(value = "query", required = false) String keyword,
                          @RequestParam(value = "category", required = false) String category,
                          @RequestParam(value = "sellerId", required = false) String sellerId,
                          @RequestParam(value = "sort" , required = false, defaultValue = "0") int sort,
                          @RequestParam(value = "view", required = false,defaultValue = "0") int view,
                          @AuthenticationPrincipal UserDetails userDetails,
                          Model model) {
        // 여기서 query는 요청 파라미터의 이름입니다.
        // 예를 들어, /search?query=검색어 형식으로 요청이 들어온다면,
        // "검색어" 부분이 query 매개변수로 전달됩니다.

        List<Products> products = new ArrayList<>();

        if (category != null && !category.isEmpty()) {
            products = productService.searchByCategory(category);
        } else if (sellerId != null) {
            products = productService.searchBySellerId(sellerId);
        }else products = productService.searchByName(keyword);

        Comparator<Products> byReviewCount = (o1, o2) -> Long.compare(o2.getRatingCnt(), o1.getRatingCnt());
        Comparator<Products> byAverageRating = (o1, o2) -> Double.compare(o2.getAverageRating(), o1.getAverageRating());
        Comparator<Products> byPriceLowToHigh = Comparator.comparingDouble(o -> o.getPrice() * (1 - o.getDiscount() / 100));
        Comparator<Products> byPriceHighToLow = (o1, o2) -> Double.compare(o2.getPrice() * (1 - o2.getDiscount() / 100), o1.getPrice() * (1 - o1.getDiscount() / 100));
        Comparator<Products> byDiscount = (o1, o2) -> Double.compare(o2.getDiscount(), o1.getDiscount());

        if (sort == 1) {
            products.sort(byReviewCount);
        } else if (sort == 2) {
            products.sort(byAverageRating);
        } else if (sort == 3) {
            products.sort(byPriceLowToHigh);
        } else if (sort == 4) {
            products.sort(byPriceHighToLow);
        } else {  // 디폴트: 할인율 높은 순
            products.sort(byDiscount);
        }


        List<ProductDto> productDtos = new ArrayList<>();
        for (Products product:products) {
            int price = product.getPrice();
            int discountPrice = (int) (price * (1-product.getDiscount()/100));
            boolean isWishlist = false;
            int wishlistCnt = wishlistService.getAllWishlistByProductId(product.getId()).size();
            if(userDetails !=null) {
                Wishlist wishlist = wishlistService.findByTwoId(userDetails.getUsername(), product.getId());
                if (wishlist != null) isWishlist = true;
            }
            productDtos.add(new ProductDto(product,(int)product.getDiscount(),discountPrice,isWishlist,wishlistCnt));
        }

        Map<String, List<String>> categoryTree = categoryService.getCategoryTree();
        model.addAttribute("categoryTree", categoryTree);
        model.addAttribute("query", keyword);
        model.addAttribute("category", category);
        model.addAttribute("sellerId", sellerId);
        model.addAttribute("sort", sort);
        model.addAttribute("view", view);
        model.addAttribute("productDto", productDtos);
        // 검색결과를 표시할 템플릿 이름을 반환합니다.
        return "searchResult"; // search-result.html과 같은 템플릿 파일을 찾게 됩니다.
    }

밑에 추가할 기능들을 위한 파라미터를 미리 추가해두었다. 여기서 필요한 부분은

Comparator<Products> byReviewCount = (o1, o2) -> Long.compare(o2.getRatingCnt(), o1.getRatingCnt());
        Comparator<Products> byAverageRating = (o1, o2) -> Double.compare(o2.getAverageRating(), o1.getAverageRating());
        Comparator<Products> byPriceLowToHigh = Comparator.comparingDouble(o -> o.getPrice() * (1 - o.getDiscount() / 100));
        Comparator<Products> byPriceHighToLow = (o1, o2) -> Double.compare(o2.getPrice() * (1 - o2.getDiscount() / 100), o1.getPrice() * (1 - o1.getDiscount() / 100));
        Comparator<Products> byDiscount = (o1, o2) -> Double.compare(o2.getDiscount(), o1.getDiscount());

        if (sort == 1) {
            products.sort(byReviewCount);
        } else if (sort == 2) {
            products.sort(byAverageRating);
        } else if (sort == 3) {
            products.sort(byPriceLowToHigh);
        } else if (sort == 4) {
            products.sort(byPriceHighToLow);
        } else {  // 디폴트: 할인율 높은 순
            products.sort(byDiscount);
        }

정렬을 하는 이 부분이다. 일단은 가장 기본적인 정렬방식으로 추가를 해주었다.

프론트엔드 코드 작성

검색결과 페이지 html,js수정

<div id="sort_list" th:data-view="${view != null ? view:'0'}" th:data-sort="${sort != null ? sort:'0'}">
    <select id="sort" name="sort">
        <option value="0" th:selected="${sort == 0}">할인율 높은순</option>
        <option value="1" th:selected="${sort == 1}">리뷰 많은순</option>
        <option value="2" th:selected="${sort == 2}">리뷰 좋은순</option>
        <option value="3" th:selected="${sort == 3}">낮은 가격순</option>
        <option value="4" th:selected="${sort == 4}">높은 가격순</option>
    </select>
  </div>
document.getElementById('sort').addEventListener('change', function () {
        const query = document.getElementById('search_input').dataset.query;
        const category = document.getElementById('category_list').dataset.category;
        const sortSelected = document.getElementById('sort').value;
        const view = document.getElementById('sort_list').dataset.view;
        let url = query === 'default'? "/search?category="+encodeURIComponent(category) : "/search?query="+encodeURIComponent(query);
        url += "&sort=" + encodeURIComponent(sortSelected);
        if(view != 'default') url += "&view=" + encodeURIComponent(view);
        window.location.href = url;
    });

정렬을 위해서 다음 태그와 메소드를 추가해주었다. view는 바로 밑에 view설정에서 사용할 것이다. 방식은 앞에서 말한대로 sort값을 받아와서 이를 url의 파라미터로 집어넣어 서버로 보낸다. 서버가 sort값을 받았다면 그에 맞는 정렬을 해주고 다시 클라이언트로 보내주는 방식이다.

검색결과 뷰 설정하기

뷰는 더 간단하다! view라는 파라미터를 그냥 주고받기만 하면된다!

백엔드 코드는 위에서 보이듯이 그냥 view라는 파라미터를 받아서 다시 모델에 넣어주는 작업만 한다.

프론트엔드 코드 작성

<div id="view_control">
        <button id="vertical_button" class="vertical_button">
                <span th:style="${view} == 0? 'background:#3180d1;':'background:black'"></span>
                <span th:style="${view} == 0? 'background:#3180d1;':'background:black'"></span>
                <span th:style="${view} == 0? 'background:#3180d1;':'background:black'"></span>
        </button>
        <button id="card_button" class="card_button">
                <span th:style="${view} != 0? 'background:#3180d1;':'background:black'"></span>
                <span th:style="${view} != 0? 'background:#3180d1;':'background:black'"></span>
                <span th:style="${view} != 0? 'background:#3180d1;':'background:black'"></span>
                <span th:style="${view} != 0? 'background:#3180d1;':'background:black'"></span>
        </button>
    </div>

html코드를 보면 의아할 것이다. span태그로 버튼을 꾸몄다.
(지마켓의 버튼을 참고했다) th:style을 통해서 해당방법으로 뷰를 설정했다면 아이콘이 파랗게 변한다.

#view_control{
    display: flex;
    margin-left: 24px;
}
.vertical_button {
    display: flex;
    flex-wrap: wrap; /* 자식 요소들이 줄바꿈 되도록 설정 */
    align-items: center;
}

.vertical_button span {
    display: block;
    width: 22px;
    height: 3px;
    background-color: black;
}
.card_button, .vertical_button{
    width: 30px;
    height: 30px;
}
.card_button {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    align-content: center;
}
.card_button span {
    display: inline-block;
    width: 7px;
    height: 6px;
    background-color: black;
    margin-top: 2px;
}

css를 다음과 같이 작성해주면 리스트형과 카드형 버튼이 생성된다.

document.getElementById("vertical_button").addEventListener("click", function () {
        const query = document.getElementById('search_input').dataset.query;
        const category = document.getElementById('category_list').dataset.category;
        const sort = document.getElementById('sort_list').dataset.sort;
        let url = query === 'default'? "/search?category="+encodeURIComponent(category) : "/search?query="+encodeURIComponent(query);
        if(sort != '0') url += "&sort=" + sort;
        url += "&view=" + encodeURIComponent(0);
        window.location.href = url;
    });
    document.getElementById("card_button").addEventListener("click", function () {
        const query = document.getElementById('search_input').dataset.query;
        const category = document.getElementById('category_list').dataset.category;
        const sort = document.getElementById('sort_list').dataset.sort;
        let url = query === 'default'? "/search?category="+encodeURIComponent(category) : "/search?query="+encodeURIComponent(query);
        if(sort != '0') url += "&sort=" + sort;
        url += "&view=" + encodeURIComponent(1);
        window.location.href = url;
    });

js코드는 정렬과 거의 같다고 보면된다. 파라미터에 view값을 넣어서 서버로 보내고 다시 클라이언트가 이를 받아서 뷰 방식을 설정한다.

테스트 결과


테스트 결과를 보면 알듯이 계속 url을 바꾸어가면서 보여지는 형태와 상품의 순서도 달라지게 됨을 알 수 있다.

검색결과 창에 카테고리 추가

카테고리를 넣는 것은 챗지피티의 도움을 조금 받았다. 카테고리는 '의류/상의' 와 같이 마치 폴더의 경로처럼 구성이되어있기때문에 이를 잘라내서 저장하는 기술이 필요했다. 여기서 챗지피티의 힘을 빌렸다.

백엔드 코드 작성

카테고리 리포지토리와 서비스 코드 추가

카테고리 리포지토리 코드 작성

public List<Category> findAll() {
        String jpql = "SELECT c FROM Category c";
        TypedQuery<Category> query = em.createQuery(jpql, Category.class);
        return query.getResultList();
    }

카테고리 서비스 코드 작성

public Map<String, List<String>> getCategoryTree() {
        List<Category> categories = categoryRepository.findAll();
        Map<String, List<String>> categoryTree = new HashMap<>();

        for (Category category : categories) {
            String[] parts = category.getTitle().split("/");

            if (parts.length == 2) {
                String parentCategory = parts[0];
                String subCategory = parts[1];

                // 부모 카테고리에 하위 카테고리 리스트 추가
                categoryTree
                        .computeIfAbsent(parentCategory, k -> new ArrayList<>())
                        .add(subCategory);
            }
        }

    return categoryTree;
}

서비스 코드에서 위 코드에서처럼 경로를 파싱해서 맵으로 저장해버린다. 맵으로 트리형태를 만드는 것이다. (여기서 궁금한 점이 생겼다. 지금이야 상위/하위 카테고리 밖에없어서 위와 같이 파싱하면 되었지만, 세부카테고리가 아주 깊게 구성되어있다면 어떻게 이를 다 잘라서 클라이언트로 보내주는 것일까?)

프론트엔드 코드 수정

<div class="aside">
        <ul class="category_list" id="category_list" th:data-category="${category != null ? category : 'default'}">
            <!-- 상위 카테고리 순회 -->
            <li th:each="parentCategory : ${categoryTree.keySet()}">
                <a  class="parentCategory" th:href="@{/search(category=${parentCategory})}">
                    <span class="parentCategory"
                          th:text="${parentCategory}">상위 카테고리</span>
                </a>
                <ul>
                    <!-- 하위 카테고리 순회 -->
                    <li th:each="subCategory : ${categoryTree.get(parentCategory)}">
                        <a class="subCategory" th:href="@{/search(category=${subCategory})}">
                            <span class="subCategory"
                                  th:text="${subCategory}">하위 카테고리</span>
                        </a>
                    </li>
                </ul>
            </li>
        </ul>
    </div>

카테고리를 위와 같이 작성해주면 화면 왼쪽에 카테고리 칸이 생기게된다! 누르면 a태그로 구성했기때문에 바로 해당 카테고리로 검색을 해버릴 수 있다!

테스트 결과

테스트 결과 원하는대로 작동이 됨을 확인했다!!


이제 기능을 생각나는건 다 추가해본 것같다. (이벤트 슬라이드는 프론트엔드쪽이기 때문에 일단 생략했다.)

profile
모르는 것 정리하기

0개의 댓글