이번 글에서는 주문 내역을 검색하는 페이지를 구현한 내용을 정리해볼 것이다. 주문 내역 검색은 지마켓의 주문 내역 검색을 참고해서 어떤 기능을 어떻게 구현해야할지 살펴보고 만들었다.
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);
}
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를 사용하면 된다고 한다.
실험을 위해서 다음과 같이 데이터베이스를 추가하였다.
버튼과 날짜 옵션 선택 테스트
키워드 테스트
테스트가 정상적으로 작동됨을 확인했다!!