쇼핑몰 웹사이트 만들어보기- 주문내역 검색 구현

Shiba·2024년 8월 27일
0

프로젝트 및 일기

목록 보기
26/29

주문 내역 검색 구현

이번 글에서는 주문 내역을 검색하는 페이지를 구현한 내용을 정리해볼 것이다. 주문 내역 검색은 지마켓의 주문 내역 검색을 참고해서 어떤 기능을 어떻게 구현해야할지 살펴보고 만들었다.

결제정보 테이블에서 해당 구간에 포함되는 행만 추출

리포지토리에 메소드 추가(수정?)

public List<Purchases> getPurchaseTerm(String user_id, LocalDateTime startDateTime, LocalDateTime endDateTime) {

        String jpql = "SELECT p FROM Purchases p WHERE p.users.id = :user_id AND p.created BETWEEN :startDateTime AND :endDateTime";
        TypedQuery<Purchases> query = em.createQuery(jpql, Purchases.class);
        query.setParameter("user_id", user_id);
        query.setParameter("startDateTime", startDateTime);
        query.setParameter("endDateTime", endDateTime);

        return query.getResultList();
    }

리포지토리에 원래 최근 주문 내역을 뽑아낼 때 사용했던 메소드를 다방면으로 사용할 수 있도록 수정하였다. 대신 최근 주문 내역은 컨트롤러 부분에서 LocalDateTime 객체 변수를 따로 추가해주어서 파라미터로 삽입해주어야 한다.

결제정보 테이블에서 구간에 포함 + 키워드 포함인 행

위의 코드를 살짝 고치면 키워드까지 포함하고있는 행만 추출해낼 수 있다.

리포지토리에 메소드 추가

public List<Purchases> getPurchaseTermPlusKeyword(String user_id, LocalDateTime startDateTime, LocalDateTime endDateTime, String keyword) {

        String jpql = "SELECT p FROM Purchases p WHERE p.users.id = :user_id AND p.products.product_name like :keyword AND p.created BETWEEN :startDateTime AND :endDateTime";
        TypedQuery<Purchases> query = em.createQuery(jpql, Purchases.class);
        query.setParameter("user_id", user_id);
        query.setParameter("startDateTime", startDateTime);
        query.setParameter("endDateTime", endDateTime);
        query.setParameter("keyword", "%" + keyword + "%");
        return query.getResultList();
    }

그 후에 서비스클래스에 메소드들을 추가하여 위에서 만든 메소드를 사용할 수 있도록 했다.

서비스에 해당 메소드들을 사용가능하도록 추가

 public List<Purchases> getPurchaseTerm(String user_id, LocalDateTime start, LocalDateTime end) {
        return purchasesRepository.getPurchaseTerm(user_id, start, end);
    }

    public List<Purchases> getPurchaseTermPlusKeyword(String user_id, LocalDateTime start, LocalDateTime end, String keyword) {
        return purchasesRepository.getPurchaseTermPlusKeyword(user_id, start, end, keyword);
    }

컨트롤러 코드 작성

PurchaseController 생성 및 Get요청 작성

package com.shoppingmall.controller;

import com.shoppingmall.domain.Purchases;
import com.shoppingmall.domain.Users;
import com.shoppingmall.service.PurchaseService;
import com.shoppingmall.service.UserService;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

@Controller
public class PurchaseController {

    private final PurchaseService purchaseService;
    private final UserService userService;
    public PurchaseController(PurchaseService purchaseService, UserService userService) {
        this.purchaseService = purchaseService;
        this.userService = userService;
    }

    @GetMapping("/user/orderHistory")
    public String orderHistoryPage(@RequestParam(name = "startDate") String start,
                                   @RequestParam(name = "endDate") String end,
                                   @RequestParam(name = "keyword", required = false) String keyword,
                                   @AuthenticationPrincipal UserDetails userDetails,
                                   Model model){

        Users users = userService.findById(userDetails.getUsername());
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        // 문자열을 LocalDate로 변환
        LocalDate startDate = LocalDate.parse(start, formatter);
        LocalDate endDate = LocalDate.parse(end, formatter);

        // LocalDate를 LocalDateTime으로 변환하여 하루의 시작과 끝을 나타냄
        LocalDateTime startDateTime = startDate.atStartOfDay();
        LocalDateTime endDateTime = endDate.atTime(23, 59, 59);

        List<Purchases> purchasesList = new ArrayList<>();

        if(keyword !=null && keyword.length() > 0) purchasesList = purchaseService.getPurchaseTermPlusKeyword(userDetails.getUsername(), startDateTime, endDateTime, keyword);

        else purchasesList = purchaseService.getPurchaseTerm(userDetails.getUsername(), startDateTime, endDateTime);

        // 날짜 기준으로 역순 정렬
        Collections.sort(purchasesList, new Comparator<Purchases>() {
            @Override
            public int compare(Purchases o1, Purchases o2) {
                return o2.getCreated().compareTo(o1.getCreated()); // 역순 정렬
            }
        });

        model.addAttribute("purchases",purchasesList);
        model.addAttribute("user", users);
        return "/user/orderHistory";
    }
}

파라미터로 시작 날짜, 끝 날짜, 키워드(옵션)을 받는다. 받은 날짜들을 LocalDateTime객체로 바꾸어서 결제정보의 결제날짜 타입과 같게 만들어준 후, service에서 해당 메소드를 사용한다. 그러면 우리가 원하는 값들이 리스트에 저장된다. 저장된 리스트를 역순으로 정렬하여, 사용자가 최근에 구매한 내역부터 차례대로 볼 수 있도록 했다.

프론트엔드 코드 작성

여기서도 기본적인 틀은 챗지피티를 통해 받아내서 작성했다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/css/statusTemplate.css">
    <link rel="stylesheet" href="/css/status.css">
    <title>주문 내역 조회</title>
    <style>
        .order-history {
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            max-width: 800px;
            margin: 0 auto;
        }
        .order-history h1 {
            font-size: 24px;
            margin-bottom: 20px;
        }
        .period-buttons {
            margin-bottom: 20px;
        }
        .period-buttons button {
            padding: 10px 15px;
            margin-right: 10px;
            border: none;
            border-radius: 5px;
            background-color: #e0e0e0;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s;
        }
        .period-buttons button:hover {
            background-color: #d5d5d5;
        }
        .date-selectors {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
        }
        .date-selectors select {
            padding: 10px;
            font-size: 14px;
            border: 1px solid #cccccc;
            border-radius: 5px;
            margin-right: 10px;
            appearance: none;
            background-color: #ffffff;
            cursor: pointer;
        }
        .search-section {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .search-section input {
            flex: 1;
            padding: 10px;
            font-size: 14px;
            border: 1px solid #cccccc;
            border-radius: 5px;
        }
        .search-section button {
            padding: 10px 20px;
            font-size: 14px;
            border: none;
            border-radius: 5px;
            background-color: #28a745;
            color: #ffffff;
            cursor: pointer;
            transition: background-color 0.2s;
        }
        .search-section button:hover {
            background-color: #218838;
        }
    </style>
</head>
<body>
<div class="header">
    <a href="/" id="home_logo">
        <img src="/images/icons/logo.png" />
    </a>
</div>
<div id="body_container">
    <div class="aside">
        <div id="profile">
            <div style="display: grid">
                <label th:text="${user.id}" style="padding-bottom: 12px"></label>
                <label th:text="${user.role} + ' 등급'" style="padding-bottom: 12px"></label>
            </div>
            <button id="status" onclick="info()">회원정보</button>
            <form th:action="@{/logout}" method="post">
                <button id="logout" type="submit">로그아웃</button>
            </form>
        </div>
        <div id="link_box">
            <button class="link" id="coupon" onclick="myCoupon()">쿠폰함</button>
            <button class="link" id="order" onclick="orderHistory()">주문내역</button>
            <button class="link" id="wishlist">내가 찜한 상품</button>
            <button class="link" id="quit" onclick="quit()">회원탈퇴</button>
        </div>
    </div>
    <div id="content" style="border-left: #e0e6eb solid 1px">
        <div class="order-history">
            <h1>주문 내역</h1>
            <div class="period-buttons" id="periodButtons">
                <!-- 기간 버튼들이 동적으로 생성됩니다 -->
            </div>
            <div class="date-selectors">
                <select id="startDateSelect"></select>
                <span>~</span>
                <select id="endDateSelect"></select>
            </div>
            <div class="search-section">
                <input type="text" id="searchInput" placeholder="주문 상품의 정보를 입력하세요">
                <button onclick="searchOrders()">조회하기</button>
            </div>
        </div>

        <ul>
            <li th:each="purchase : ${purchases}">
                <h3 th:text="${#temporals.format(purchase.created, 'yyyy-MM-dd')}"></h3>
                <div class="order_box" style="display: flex">
                    <div class="photo">
                        <a th:href="'/products/'+${purchase.product_id}">
                            <img id="image" th:src="@{'/images/uploads/' + ${purchase.products.photo}}" alt="Product Image" width="150" height="150">
                        </a>
                    </div>
                    <div class="product_name">
                        <a  class="product_name" th:href="'/products/'+${purchase.product_id}" th:text="${purchase.products.product_name}"></a>
                    </div>
                    <div class="product_info">
                        <span th:text="'결제가격: ' + ${purchase.price} + '원'"></span>
                        <span th:text="${purchase.product_cnt} + '개'"></span>
                    </div>
                </div>
            </li>
        </ul>
    </div>
</div>
<script>
    document.addEventListener("DOMContentLoaded", function() {
        initializeDateSelectors();
        initializePeriodButtons();
        setDefaultDatesFromURL();
    });

    // 년도와 월을 하나의 콤보박스로 초기화 (현재 월을 기준으로 역순 정렬)
    function initializeDateSelectors() {
        const startDateSelect = document.getElementById('startDateSelect');
        const endDateSelect = document.getElementById('endDateSelect');

        const currentYear = new Date().getFullYear();
        const currentMonth = new Date().getMonth() + 1;

        // 현재 달부터 과거 5년 전까지의 월을 역순으로 추가
        for (let year = currentYear; year >= currentYear - 5; year--) {
            for (let month = (year === currentYear ? currentMonth : 12); month >= 1; month--) {
                const optionValue = `${year}-${month.toString().padStart(2, '0')}`;
                const optionText = `${year}년 ${month}월`;

                const startOption = document.createElement('option');
                startOption.value = optionValue;
                startOption.text = optionText;
                startDateSelect.appendChild(startOption);

                const endOption = document.createElement('option');
                endOption.value = optionValue;
                endOption.text = optionText;
                endDateSelect.appendChild(endOption);
            }
        }

        startDateSelect.value = `${currentYear}-${currentMonth.toString().padStart(2, '0')}`;
        endDateSelect.value = `${currentYear}-${currentMonth.toString().padStart(2, '0')}`;
    }

    // 기간 버튼 초기화
    function initializePeriodButtons() {
        const periodButtonsContainer = document.getElementById('periodButtons');
        const currentDate = new Date();
        const currentYear = currentDate.getFullYear();
        const currentMonth = currentDate.getMonth() + 1;

        // 최대(5년) 버튼 생성
        const maxButton = document.createElement('button');
        maxButton.textContent = '최대(5년)';
        maxButton.onclick = function() {
            setPeriod('5y');
            searchOrders();
        };
        periodButtonsContainer.appendChild(maxButton);

        // 최근 3개월 버튼 생성
        for (let i = 1; i <= 3; i++) {
            const date = new Date(currentYear, currentMonth - i, 1);
            const year = date.getFullYear();
            const month = date.getMonth() + 1; // 1월 = 0이므로 +1
            const button = document.createElement('button');
            button.textContent = month + '월';
            button.onclick = function() {
                setPeriodMonth(year, month);
                searchOrders();
            };
            periodButtonsContainer.appendChild(button);
        }
    }

    // URL에서 전달된 시작 날짜와 종료 날짜로 기본값 설정
    function setDefaultDatesFromURL() {
        const urlParams = new URLSearchParams(window.location.search);
        const startDateParam = urlParams.get('startDate');
        const endDateParam = urlParams.get('endDate');

        if (startDateParam && endDateParam) {
            document.getElementById('startDateSelect').value = startDateParam.slice(0, 7);
            document.getElementById('endDateSelect').value = endDateParam.slice(0, 7);
        }
    }

    // 기간 설정 함수: 최대(5년)
    function setPeriod(period) {
        if (period === '5y') {
            const currentDate = new Date();
            const endYear = currentDate.getFullYear();
            const endMonth = currentDate.getMonth() + 1;

            const startYear = endYear - 5;
            const startMonth = endMonth;

            document.getElementById('startDateSelect').value = `${startYear}-${startMonth.toString().padStart(2, '0')}`;
            document.getElementById('endDateSelect').value = `${endYear}-${endMonth.toString().padStart(2, '0')}`;
        }
    }

    // 기간 설정 함수: 특정 월
    function setPeriodMonth(year, month) {
        const dateValue = `${year}-${month.toString().padStart(2, '0')}`;
        document.getElementById('startDateSelect').value = dateValue;
        document.getElementById('endDateSelect').value = dateValue;
    }

    // 검색 함수
    function searchOrders() {
        const startDate = document.getElementById('startDateSelect').value + "-01";
        const endDate = document.getElementById('endDateSelect').value + "-" + getLastDayOfMonth(startDate);

        const searchInput = document.getElementById('searchInput').value;

        // 예시로 쿼리 스트링을 생성하여 GET 요청을 보낼 수 있습니다.
        const queryString = `?startDate=${startDate}&endDate=${endDate}&keyword=${encodeURIComponent(searchInput)}`;
        window.location.href = '/user/orderHistory' + queryString;
    }

    // 특정 년월의 마지막 날 계산
    function getLastDayOfMonth(date) {
        const [year, month] = date.split('-');
        return new Date(year, month, 0).getDate();
    }

    function orderHistory() {
        // 현재 날짜
        const today = new Date();

        // 5년 전 날짜
        const fiveYearsAgo = new Date();
        fiveYearsAgo.setFullYear(today.getFullYear() - 5);

        // 포맷된 날짜
        const startDate = formatDate(fiveYearsAgo);
        const endDate = formatDate(today);

        // 예시로 쿼리 스트링을 생성하여 GET 요청을 보낼 수 있습니다.
        const queryString = `?startDate=${startDate}&endDate=${endDate}`;
        window.location.href = '/user/orderHistory' + queryString;
    }
    // 날짜를 YYYY-MM-DD 포맷으로 변환하는 함수
    function formatDate(date) {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        return `${year}-${month}-${day}`;
    }
    function info() {
        window.location.href = "/user/info";
    }
    function myCoupon() {
        window.location.href = "/myCoupon";
    }

    function quit() {
        window.location.href = "/user/quit";
    }
</script>

</body>
</html>

함수가 좀 많은데 간단히 말하면 날짜를 변환하는 함수들이 있고, 변환한 날짜를 이용하여 버튼과 옵션을 만든다. 또 그 날짜들을 변환해서 쿼리를 날리면 다시 리스트를 추출해내고 역순으로 정렬하여 보여주는 형태이다.

여기서 새로 배운건

${#temporals.format(purchase.created, 'yyyy-MM-dd')}

인데, 타임리프를 이용하여 LocalDateTime 타입으로 오는 값을 yyyy-MM-dd의 형식으로 포맷해서 보여준다.
Date타입을 사용했다면 #dates를 사용하면 된다고 한다.

테스트 결과

실험을 위해서 다음과 같이 데이터베이스를 추가하였다.

버튼과 날짜 옵션 선택 테스트

키워드 테스트

테스트가 정상적으로 작동됨을 확인했다!!

profile
모르는 것 정리하기

0개의 댓글