
...
@GetMapping(value = "/item/{itemId}")
public String itemDtl(Model model, @PathVariable("itemId") Long itemId){
// ์ํ ์์ ํ์ด์ง์์ ์ฌ์ฉํ๋ ItemDtl ๋ฉ์๋๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ์ฌ ์ํ ์กฐํ
// ๋ฉ์๋ ์ค๋ฒ๋ก๋ฉ์ ํ๋ผ๋ฏธํฐ์ ์์๊ฐ ๋ฌ๋ผ๋ ์ ์ฉ ๋จ
ItemFormDto itemFormDto = itemService.getItemDtl(itemId);
model.addAttribute("item", itemFormDto);
return "item/itemDtl";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<head>
<!-- ์คํ๋ง ์ํ๋ฆฌํฐ๋ก ์ธํด ์๋์ผ๋ก ๋ณด๋ด์ง๋ html meta ํ๊ทธ -->
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
<title>์ํ ์์ธ ์ ๋ณด</title>
</head>
<th:block layout:fragment="script">
<script th:inline="javascript">
//totalPrice ๋ฅผ ๊ณ์ฐํด์ ์ถ๋ ฅํ๋ calculateTotalPrice() ๋ฉ์๋
//์ฒ์ ํ๋ฉด ์ถ๋ ฅํ ๋ ์ํ๋๊ณ , ๊ทธ ํ์ ์๋ ๋ณ๋ ์ ์ํ ๋จ
$(document).ready(function(){
calculateTotalPrice();
$("#count").change( function(){
calculateTotalPrice();
});
});
function calculateTotalPrice(){
var count = $("#count").val();
var price = $("#price").val();
var totalPrice = price * count;
$("#totalPrice").html(totalPrice + '์');
}
</script>
</th:block>
<!-- ์ฌ์ฉ์ CSS ์ถ๊ฐ -->
<th:block layout:fragment="css">
<style>
.mgb-15{
margin-bottom:15px;
}
.mgt-30{
margin-top:30px;
}
.mgt-50{
margin-top:50px;
}
.repImgDiv{
margin-right:15px;
height:auto;
width:50%;
}
.repImg{
width:100%;
height:400px;
}
.wd50{
height:auto;
width:50%;
}
</style>
</th:block>
<div layout:fragment="content" style="margin-left:25%; margin-right:25%">
<input type="hidden" id="itemId" th:value="${item.id}">
<div class="d-flex">
<div class="repImgDiv">
<img th:src="${item.itemImgDtoList[0].imgUrl}" class="img-fluid img-thumbnail repImg" th:alt="${item.itemNm}">
</div>
<div class="wd50">
<span th:if="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}"
class="badge bg-primary mgb-15">ํ๋งค์ค</span>
<span th:unless="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}"
class="badge bg-danger mgb-15">ํ์ </span>
<div class="h4" th:text="${item.itemNm}"></div>
<hr class="my-4">
<div class="text-right">
<div class="h4 text-danger text-left">
<input type="hidden" th:value="${item.price}" id="price" name="price">
<span th:text="${item.price}"></span>์
</div>
<div class="input-group w-50">
<div class="input-group-prepend">
<span class="input-group-text">์๋</span>
</div>
<input type="number" name="count" id="count" class="form-control" value="1" min="1">
</div>
</div>
<hr class="my-4">
<div class="text-right mgt-50">
<h5>๊ฒฐ์ ๊ธ์ก</h5>
<h3 name="totalPrice" id="totalPrice" class="font-weight-bold"></h3>
</div>
<div th:if="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="text-right">
<button type="button" class="btn btn-light border border-primary btn-lg"
onclick="addCart()">์ฅ๋ฐ๊ตฌ๋ ๋ด๊ธฐ</button>
<button type="button" class="btn btn-primary btn-lg" onclick="order()">์ฃผ๋ฌธํ๊ธฐ</button>
</div>
<div th:unless="${item.itemSellStatus == T(com.shop.constant.ItemSellStatus).SELL}" class="text-right">
<button type="button" class="btn btn-danger btn-lg">ํ์ </button>
</div>
</div>
</div>
<div class="p-3 bg-warning bg-opacity-10 border border-warning rounded">
<div class="container">
<h4 class="display-5">์ํ ์์ธ ์ค๋ช
</h4>
<hr class="my-4">
<p class="lead" th:text="${item.itemDetail}"></p>
</div>
</div>
<div th:each="itemImg : ${item.itemImgDtoList}" class="text-center">
<img th:if="${not #strings.isEmpty(itemImg.imgUrl)}" th:src="${itemImg.imgUrl}"
class="rounded mgb-15" width="800">
</div>
</div>
</html>




๐ ์์ธ ์ฒ๋ฆฌ ํ ํจํค์ง ์์ฑ
...
package com.shop.exception;
// ์ํ ์ฃผ๋ฌธ ์๋ ๋ณด๋ค ํ์ฌ ์ฌ๊ณ ์ ์๊ฐ ์ ์ ๋ ๋ฐ์ ์ํฌ Exception ์ ์
public class OutOfStockException extends RuntimeException{ // ์๋ฌ message ๋ฅผ ์ง์ ํ ์ ์๋ RuntimeException ํด๋์ค
public OutOfStockException(String message) {
super(message);
}
}
...
public void removeStock(int stockNumber){
// ๊ธฐ์กด ์ฌ๊ณ - ์ฃผ๋ฌธ ์๋ ์ฌ๊ณ
int restStock = this.stockNumber - stockNumber; // 10, 5 / 10, 20
// ์ฃผ๋ฌธ ์๋ ์ฌ๊ณ ๊ฐ ๋ ๋ง์ผ๋ฉด ์ฌ๊ตฌ ๋ถ์กฑ ์๋ฌ ๋ฐ์
if (restStock < 0){
throw new OutOfStockException("์ํ์ ์ฌ๊ณ ๊ฐ ๋ถ์กฑํฉ๋๋ค. (ํ์ฌ ์ฌ๊ณ ์๋: " + this.stockNumber + ")");
}
this.stockNumber = restStock; // 5
}
}
...
// ์ฃผ๋ฌธ ์ํ๊ณผ ์ฃผ๋ฌธ ์๋ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์๋ OrderItem Entity ์ ๊ฐ์ฒด ์์ฑ ๋ฉ์๋ ์ถ๊ฐ
// Item(์ํ) ๐ OrderItem(์ฃผ๋ฌธ ์ํ) ๋ ๊ฐ๋ ๋ค๋ฅธ ๊ฒ์
public static OrderItem createOrderItem(Item item, int count){
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setCount(count);
orderItem.setOrderPrice(item.getPrice());
item.removeStock(count);
return orderItem;
}
public int getTotalPrice(){
return orderPrice * count;
}
}
// OrderItem ๊ฐ์ฒด๋ฅผ ์ฐ๊ฒฐํ๊ณ OrderItem ๊ฐ์ฒด์ ์์ ์ ์ฐ๊ฒฐํ๋ ๋ฉ์๋
// ๋ด ์์ดํ
๋ฆฌ์คํธ์ ์ถ๊ฐํ๊ณ ์ฃผ๋ฌธ์ ๋ฆฌ์คํธ๋ฅผ ์ถ๊ฐํ๋ค.
public void addOrderItem(OrderItem orderItem){
orderItems.add(orderItem); // ์ฃผ๋ฌธ ๊ฐ์ฒด์ ์ฃผ๋ฌธ ์ํ ๊ฐ์ฒด ์ฐ๊ฒฐ
orderItem.setOrder(this); // ์ฃผ๋ฌธ ์ํ ๊ฐ์ฒด์ ์ฃผ๋ฌธ ๊ฐ์ฒด ์ฐ๊ฒฐ (์ฐ๊ด ๊ด๊ณ ์ฃผ์ธ)
}
// OrderItem ๊ฐ์ฒด๋ฅผ ์ด์ฉํ์ฌ ์ฃผ๋ฌธ ๊ฐ์ฒด๋ฅผ ๋ง๋๋ ๋ฉ์๋ ์ถ๊ฐ
public static Order createOrder(Member member, List<OrderItem> orderItemList){
// ์ฃผ๋ฌธ์ ์์ฑ
Order order = new Order();
// ํ์ฌ ๋ก๊ทธ์ธ ๋ ๋ฉค๋ฒ ์ฃผ๋ฌธ์์ ์ถ๊ฐ
order.setMember(member);
// ์ฃผ๋ฌธ ์์ดํ
๋ฆฌ์คํธ๋ฅผ ๋ฐ๋ณต๋ฌธ์ผ๋ก ์ฃผ๋ฌธ์์ ์ถ๊ฐ
for (OrderItem orderItem : orderItemList){
order.addOrderItem(orderItem);
}
order.setOrderStatus(OrderStatus.ORDER); // ์ํ๋ ORDER๋ก ์ค์ ๐ ์ฃผ๋ฌธ ์๋ฃ
order.setOrderDate(LocalDateTime.now());
return order; // ์ฃผ๋ฌธ์ ๋ฐํ
}
// ๊ฐ ์ฃผ๋ฌธ ์ํ์ TotalPrice ๋ฅผ ๊ตฌํ๋ค ๋ชจ๋ ๋ํ๋ ๋ฉ์๋ ์ถ๊ฐ
public int getTotalPrice(){
int totalPrice = 0;
// ์ฃผ๋ฌธ์์ ์๋ ์ฃผ๋ฌธ ์์ดํ
๋ฆฌ์คํธ๋ฅผ ๋ฐ๋ณต
// ์ฃผ๋ฌธ ์์ดํ
๋ง๋ค ์ด ๊ฐ๊ฒฉ์ totalPrice์ ์ถ๊ฐ
for (OrderItem orderItem : orderItems){
totalPrice += orderItem.getTotalPrice();
}
return totalPrice; // ์ฅ๋ฐ๊ตฌ๋ ์ด์ก
}
}
package com.shop.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
// ์ ํ ์์ธ ํ์ด์ง ํ๋ฉด์์ ๋ณด๋ด๋ ์ฃผ๋ฌธ ์ ๋ณด(์ํ, ์๋)๋ฅผ ์ํ Dto ๊ฐ์ฒด ์์ฑ
public class OrderDto {
@NotNull(message = "์ํ ์์ด๋๋ ํ์ ์
๋ ฅ ๊ฐ์
๋๋ค.")
private Long itemId;
@Min(value = 1, message = "์ต์ ์ฃผ๋ฌธ ์๋์ 1๊ฐ ์
๋๋ค.")
@Max(value = 999, message = "์ต๋ ์ฃผ๋ฌธ ์๋์ 999๊ฐ ์
๋๋ค.")
private int count;
}
package com.shop.service;
import com.shop.dto.OrderDto;
import com.shop.dto.OrderItemDto;
import com.shop.entity.Item;
import com.shop.entity.Member;
import com.shop.entity.Order;
import com.shop.entity.OrderItem;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderRepository;
import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
@Transactional
@RequiredArgsConstructor
// ์ํ๊ณผ ์ฃผ๋ฌธํ ๊ณ ๊ฐ์ ์กฐํ
// ์ฃผ๋ฌธ ์ํ ๊ฐ์ฒด ์์ฑ ๐ ์ฃผ๋ฌธ ๊ฐ์ฒด ์์ฑ
public class OrderService {
private final ItemRepository itemRepository; // ์ํ์ ๋ถ๋ฌ ์์ ์ฌ๊ณ ๋ฅผ ๋ณ๊ฒฝ
private final MemberRepository memberRepository; // ๋ฉค๋ฒ๋ฅผ ๋ถ๋ฌ ์์ ์ฐ๊ฒฐ
private final OrderRepository orderRepository; // ์ฃผ๋ฌธ ๊ฐ์ฒด๋ฅผ ์ ์ฅ
public Long order(OrderDto orderDto, String email){
Item item = itemRepository.findById(orderDto.getItemId())
.orElseThrow(EntityNotFoundException::new);
Member member = memberRepository.findByEmail(email);
List<OrderItem> orderItemList = new ArrayList<>();
// OrderItem.createOrderItem ๐ static ๋ฉ์๋
OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
orderItemList.add(orderItem);
// Order.createOrder ๐ static ๋ฉ์๋
Order order = Order.createOrder(member, orderItemList);
orderRepository.save(order);
return order.getId();
}
}
AJAX๋ฅผ ์ฌ์ฉํ๋ค.
package com.shop.controller;
import com.shop.dto.OrderDto;
import com.shop.service.OrderService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import java.security.Principal;
import java.util.List;
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping(value = "/order")
// ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก Json ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ์ ๐ AJAX
// @ResponseBody : ์๋ฐ ๊ฐ์ฒด๋ฅผ http ์๋ต body ๋ถ๋ถ์ผ๋ก ๋ณด๋ (ResponseEntity ์๋ฃํ)
// @RequestBody : http ์์ฒญ์ body ๋ถ๋ถ ๋ฐ์ดํฐ๋ฅผ ์๋ฐ ๊ฐ์ฒด๋ก ๋ณํํด์ ๋ฐ์
public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto,
BindingResult bindingResult,
Principal principal){
// ์
๋ ฅ๊ฐ์ ๋ฌธ์ ๊ฐ ์์ ์ ํ๋ ์๋ฌ ์ ๋ณด๋ค์ ResponseEntity ๊ฐ์ฒด์ ๋ด์์ ๋ฐํ
if (bindingResult.hasErrors()){
// String์ "ABC" + "123" ์ผ๋ก ๋ถ์ฌ "ABC123"์ผ๋ก ๋ง๋ค์ง๋ง
// StringBuilder ๋ a = append.("ABC");, b = append.("123"); ์ด๋ ๊ฒ ๋ถ์ฌ ๋ง๋ ๋ค
StringBuilder sb = new StringBuilder();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError fieldError : fieldErrors){
sb.append(fieldError.getDefaultMessage());
}
return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST);
}
// ํ์ฌ ๋ก๊ทธ์ธํ ์ ์ ์ ์ ๋ณด(principal.getName();)๋ฅผ ๋ด๊ณ ์๋ Principal ๊ฐ์ฒด์์ ์ ์ ์ email ์ถ์ถ
String email = principal.getName();
Long orderId;
try {
orderId = orderService.order(orderDto, email);
}catch (Exception e){
// OutOfStockNumber ์ด ๋ฐ๋๋ค
return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
// ์ ์์ ์ผ๋ก ๋์ ์ ์์ฑ๋ ์ฃผ๋ฌธ ๊ฐ์ฒด์ Id ์ ์ํ์ฝ๋ 200์ ๋ณด๋
// ๐ AJAX๋ก ๋ณด๋ธ๋ค
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
}
}
package com.shop.service;
import com.shop.constant.ItemSellStatus;
import com.shop.dto.OrderDto;
import com.shop.entity.Item;
import com.shop.entity.Member;
import com.shop.entity.Order;
import com.shop.entity.OrderItem;
import com.shop.repository.ItemRepository;
import com.shop.repository.MemberRepository;
import com.shop.repository.OrderRepository;
import jakarta.persistence.EntityNotFoundException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Autowired
ItemRepository itemRepository;
@Autowired
MemberRepository memberRepository;
public Item saveItem(){
Item item = new Item(); // JPA DB์ ์๋ค๊ฐ๋คํ๋ ๊ฐ์ฒด ๊ฐ์ฒด
item.setItemNm("ํ
์คํธ ์ํ");
item.setPrice(10000);
item.setItemDetail("ํ
์คํธ ์ํ ์์ธ ์ค๋ช
");
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100);
return itemRepository.save(item);
}
public Member saveMember(){
Member member = new Member();
member.setEmail("test@test.com");
return memberRepository.save(member);
}
@Test
@DisplayName("์ฃผ๋ฌธ ํ
์คํธ")
public void order(){
Item item = saveItem();
Member member = saveMember();
// ์ํ ์์ธ ํ์ด์ง ํ๋ฉด์์ ๋์ด ์ค๋ ๊ฐ ์ค์
OrderDto orderDto = new OrderDto();
orderDto.setCount(10);
orderDto.setItemId(item.getId());
// ์ฃผ๋ฌธ ๊ฐ์ฒด DB์ ์ ์ฅ
Long orderId = orderService.order(orderDto, member.getEmail());
// ์ง์ ๋ ์ฃผ๋ฌธ ๊ฐ์ฒด ์กฐํ
Order order = orderRepository.findById(orderId)
.orElseThrow(EntityNotFoundException::new);
// 1. DB์ ์ ์ฅ๋ ์ฃผ๋ฌธ ๊ฐ์ฒด์์ ์ฃผ๋ฌธ ์ํ ์ถ์ถ (1๊ฐ)
List<OrderItem> orderItems = order.getOrderItems();
// 2. ์์์ ๋ง๋ ์ฃผ๋ฌธ ์ํ ์ด ๊ฐ๊ฒฉ (1๊ฐ)
int totalPrice = orderDto.getCount() * item.getPrice();
// 1์ ๊ฐ๊ฒฉ๊ณผ 2๊ฐ ๊ฐ์์ง ํ
์คํธ
assertEquals(totalPrice, order.getTotalPrice());
System.out.println(totalPrice);
}
}
โ
create๋ก ๋ณ๊ฒฝํด์ผํจcartํ ์ด๋ธ์ ์์ง ๋ง๋ค์ง ์์์
...
// ์ฃผ๋ฌธํ๊ธฐ ๋ฒํผ์ ๋๋ฅด๋ฉด ์คํ
function order(){
// Ajax ํต์ ํ ๋, csrf ํ ํฐ ๊ฐ์ ์กฐํํด์ ์ง์ ๋ณด๋ด์ผํจ
var token = $("meta[name = '_csrf']").attr("content");
var header = $("meta[name = '_csrf_header']").attr("content");
var url = "/order";
var paramData = {
itemId : $("#itemId").val(),
count : $("#count").val()
};
// JSON ๐ String
var param = JSON.stringify(paramData);
$.ajax({
url : url,
type : "POST",
contentType : "application/json",
data : param,
beforeSend : function(xhr){
// ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ธฐ ์ ์ ํค๋์ csrf ๊ฐ์ ์ค์
xhr.setRequestHeader(header, token);
},
dataType : "json",
cache : false,
success : function(result, status){
alert("์ฃผ๋ฌธ์ด ์๋ฃ ๋์์ต๋๋ค.");
location.href = '/';
},
error : function(jqXHR, status, error){
if(jqXHR.status == '401'){
alert('๋ก๊ทธ์ธ ํ ์ด์ฉํด์ฃผ์ธ์');
location.href = '/members/login';
}
else{
alert(jqXHR.responseText);
}
}
});
}
</script>

๐ ๋ก๊ทธ์ธ ์ํ๊ณ ์ฃผ๋ฌธ

๐ ๋ฐ๋ก ๋์ด์จ๋ค



๐ ์ฌ๊ณ ๊ฐ ์์ ๋

๐ ์ฃผ๋ฌธ ์ฑ๊ณต
package com.shop.dto;
// ์ฃผ๋ฌธ ์ํ ์ ๋ณด
import com.shop.entity.OrderItem;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class OrderItemDto {
private String itemNm;
private int count;
private int orderPrice;
private String imgUrl;
public OrderItemDto(OrderItem orderItem, String imgUrl) {
this.itemNm = orderItem.getItem().getItemNm();
this.count = orderItem.getCount();
this.orderPrice = orderItem.getOrderPrice();
this.imgUrl = imgUrl;
}
}
package com.shop.dto;
import com.shop.constant.OrderStatus;
import com.shop.entity.Order;
import lombok.Getter;
import lombok.Setter;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
public class OrderHistDto {
private Long orderId;
private String orderDate;
private OrderStatus orderStatus;
private List<OrderItemDto> orderItemDtoList = new ArrayList<>();
public OrderHistDto(Order order){
this.orderId = order.getId();
this.orderDate = order.getOrderDate()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
this.orderStatus = order.getOrderStatus();
}
public void addOrderItemDto(OrderItemDto orderItemDto){
orderItemDtoList.add(orderItemDto);
}
}
package com.shop.repository;
import com.shop.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.awt.print.Pageable;
import java.util.List;
public interface OrderRepository extends JpaRepository<Order, Long> {
// ๊ตฌ๋งค ๋ด์ญ ์กฐํ
@Query("select o from Order o where o.member.email = :email order by o.orderDate desc")
List<Order> findOrders(@Param("email") String email, Pageable pageable);
@Query("select count(o) from Order o where o.member.email = :email")
Long countOrder(@Param("email") String email);
}
package com.shop.repository;
import com.shop.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
// ์ํ ์ด๋ฏธ์ง ์ ๋ณด ์ฟผ๋ฆฌ๋ฌธ ๋ ๋ฆฌ๋ Repository
public interface ItemImgRepository extends JpaRepository<ItemImg,Long> {
// ์ด๋ฏธ์ง๊ฐ ์ ์ ์ฅ๋์๋์ง ํ์ธํ๊ธฐ ์ํด ์ฟผ๋ฆฌ ์กฐํ๋ฌธ ์ถ๊ฐ
List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
// ๊ตฌ๋งค ๋ด์ญ ํ์ด์ง์์ ์ฃผ๋ฌธ ์ํ์ ๋ํ ์ด๋ฏธ์ง๋ฅผ ์ํ ์กฐํ
ItemImg findByItemIdAndRepImgYn(Long itemId, String repImgYn);
}
...
private final ItemImgRepository itemImgRepository;
...
// ์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํ ๋ฉ์๋
@Transactional(readOnly = true)
public Page<OrderHistDto> getOrderList(String email, Pageable pageable){
// ์ ์ email๊ณผ ํ์ด์ง ์กฐ๊ฑด์ ์ด์ฉํ์ฌ ์ฃผ๋ฌธ ๋ชฉ๋ก์ ์กฐํ
List<Order> orders = orderRepository.findOrders(email, pageable);
// ์ ์ ์ ์ฃผ๋ฌธ ์ด ๊ฐ์
Long totalCount = orderRepository.countOrder(email);
List<OrderHistDto> orderHistDtos = new ArrayList<>();
// ์ฃผ๋ฌธ ๋ฆฌ์คํธ๋ฅผ ์ํํ๋ฉด์ ๊ตฌ๋งค ์ด๋ ฅ ํ์ด์ง์ ์ ๋ฌํ DTO๋ฅผ ์์ฑ
for (Order order : orders){
OrderHistDto orderHistDto = new OrderHistDto(order);
List<OrderItem> orderItems = order.getOrderItems();
for (OrderItem orderItem : orderItems){
// ์ฃผ๋ฌธํ ์ํ์ ๋ํ ์ด๋ฏธ์ง๋ฅผ ์กฐํ
ItemImg itemImg = itemImgRepository.findByItemIdAndRepImgYn(orderItem.
getItem().getId(), "Y");
OrderItemDto orderItemDto = new OrderItemDto(orderItem, itemImg.getImgUrl());
orderHistDto.addOrderItemDto(orderItemDto);
}
orderHistDtos.add(orderHistDto);
}
// ํ์ด์ง ๊ตฌํ ๊ฐ์ฒด๋ฅผ ์์ฑํ์ฌ ๋ฐํ
// ๐ order, orderItem Entity ๊ฐ์ฒด๋ฅผ ๊ฐ๊ฐ OrderHistDto, OrderItemDto ๊ฐ์ฒด๋ก ๋ณํ
return new PageImpl<OrderHistDto>(orderHistDtos, pageable, totalCount);
}
}

...
// ๊ตฌ๋งค ๋ด์ญ ์กฐํ
@GetMapping(value = {"/orders", "/orders/{page}"})
public String orderHist(@PathVariable("page")Optional<Integer> page, Principal principal, Model model){
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 5);
// ํ์ฌ ๋ก๊ทธ์ธํ ํ์์ ์ด๋ฉ์ผ๊ณผ ํ์ด์ง ๊ฐ์ฒด๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ์ ๋ฌํ์ฌ ํ๋ฉด์ ์ ๋ฌํ ์ฃผ๋ฌธ ๋ชฉ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฐํ
Page<OrderHistDto> orderHistDtoList = orderService.getOrderList(principal.getName(), pageable);
model.addAttribute("orders", orderHistDtoList);
model.addAttribute("page", pageable.getPageNumber());
model.addAttribute("maxPage", 5);
return "/order/orderHist";
}
}

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
<th:block layout:fragment="css">
<style>
.content-mg{
margin-left:30%;
margin-right:30%;
margin-top:2%;
margin-bottom:100px;
}
.repImgDiv{
margin-right:15px;
margin-left:15px;
height:auto;
}
.repImg{
height:100px;
width:100px;
}
.card{
width:750px;
height:100%;
padding:30px;
margin-bottom:20px;
}
.fs18{
font-size:18px
}
.fs24{
font-size:24px
}
</style>
</th:block>
<div layout:fragment="content" class="content-mg p-3 bg-info bg-opacity-10 border border-info border rounded">
<h2 class="mb-4">์ฃผ๋ฌธ ๋ด์ญ</h2>
<div th:each="order : ${orders.getContent()}">
<div class="d-flex mb-3 align-self-center">
<h4 th:text="${order.orderDate} + ' ์ฃผ๋ฌธ'"></h4>
<div class="ms-3">
<th:block th:if="${order.orderStatus == T(com.shop.constant.OrderStatus).ORDER}">
<button type="button" class="btn btn-outline-success"
th:value="${order.orderId}" onclick="cancelOrder(this.value)">์ฃผ๋ฌธ์ทจ์</button>
</th:block>
<th:block th:unless="${order.orderStatus == T(com.shop.constant.OrderStatus).ORDER}">
<h6 style="margin-top : 6px">(์ทจ์ ์๋ฃ)</h6>
</th:block>
</div>
</div>
<div class="card d-flex">
<div th:each="orderItem : ${order.orderItemDtoList}" class="d-flex mb-3">
<div class="repImgDiv">
<img th:src="${orderItem.imgUrl}" class = "rounded repImg" th:alt="${orderItem.itemNm}">
</div>
<div class="align-self-center w-75">
<span th:text="${orderItem.itemNm}" class="fs24 font-weight-bold"></span>
<div class="fs18 font-weight-light">
<span th:text="${orderItem.orderPrice} +'์'"></span>
<span th:text="${orderItem.count} +'๊ฐ'"></span>
</div>
</div>
</div>
</div>
</div>
<div th:with="start=${(orders.number/maxPage)*maxPage + 1}, end=(${(orders.totalPages == 0) ? 1 : (start + (maxPage - 1) < orders.totalPages ? start + (maxPage - 1) : orders.totalPages)})" >
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${orders.number eq 0}?'disabled':''">
<a th:href="@{'/orders/' + ${orders.number-1}}" aria-label='Previous' class="page-link">
<span aria-hidden='true'>Previous</span>
</a>
</li>
<li class="page-item" th:each="page: ${#numbers.sequence(start, end)}" th:classappend="${orders.number eq page-1}?'active':''">
<a th:href="@{'/orders/' + ${page-1}}" th:inline="text" class="page-link">[[${page}]]</a>
</li>
<li class="page-item" th:classappend="${orders.number+1 ge orders.totalPages}?'disabled':''">
<a th:href="@{'/orders/' + ${orders.number+1}}" aria-label='Next' class="page-link">
<span aria-hidden='true'>Next</span>
</a>
</li>
</ul>
</div>
</div>
</html>

๐ 1ํ์ด์ง

๐ 2ํ์ด์ง
ํฌํธํด๋ฆฌ์ค์ ๋ค์ด๋ฒ ๊ธ์ฐ๊ธฐ ์ฒ๋ผ ์ด๋ฏธ์ง๋ฅผ ์ฒจ๋ถํ๋ฉด ๊ธ ์์ ๋ค์ด๊ฐ์ ์์ฑํ๋ ๊ฒ์ ๋ง๋ค์ด ๋ณด๋ฉด ์ข๊ฒ ๋ค.
...
public void addStock(int stockNumber){
// ์ฃผ๋ฌธ ์ทจ์ ์ ์ํ์ ์ฌ๊ณ ๋ฅผ ์ํ ์ฃผ๋ฌธ ์๋ ๋งํผ ๋ค์ ๋ํจ
this.stockNumber += stockNumber;
}
}
...
public void cancel(){
// ์ฃผ๋ฌธ ์ํ ์๋์ ํ๋ผ๋ฏธํฐ๋ก ๋๊น
this.getItem().addStock(count);
}
}
...
public void cacncelOrder(){
// ์ฃผ๋ฌธ ์ํ๋ฅผ CANCEL ๋ก ๋ณ๊ฒฝ
this.orderStatus = OrderStatus.CANCEL;
// ๋ชจ๋ OrderItem ๊ฐ์ฒด๊ฐ cancel() ๋ฉ์๋ ์คํ
for (OrderItem orderItem : orderItems){
orderItem.cancel();
}
}
}
...
@Transactional(readOnly = true)
// ์ํ์ ์ฃผ๋ฌธํ ์ ์ ์ ์ฃผ๋ฌธ ์ทจ์๋ฅผ ์์ฒญํ ์ ์ ๊ฐ ๋์ผํ์ง ๊ฒ์ฆ
public boolean validateOrder(Long orderId, String email){
Member curMember = memberRepository.findByEmail(email);
Order order = orderRepository.findById(orderId).orElseThrow(EntityNotFoundException::new);
Member savedMember = order.getMember();
// ํ์ฌ ๋ก๊ทธ์ธ๋ ๋ฉค๋ฒ์ ์ธ์ด๋ธ๋ ๋ฉค๋ฒ์ ๋น๊ต
if(!StringUtils.equals(curMember.getEmail(), savedMember.getEmail())){
return false;
}
return true;
}
// ์ฃผ๋ฌธ ์ทจ์ ๋ฉ์๋ (๋ณ๊ฒฝ ๊ฐ์ง)
public void cancelOrder(Long orderId){
Order order = orderRepository.findById(orderId).orElseThrow(EntityNotFoundException::new);
order.cacncelOrder();
}
}
...
@PostMapping("/order/{orderId}/cancel")
// AJAX ํํ!!!!
public @ResponseBody ResponseEntity cancelOrder(@PathVariable("orderId")
Long orderId, Principal principal){
// ์ํ์ ์ฃผ๋ฌธํ ์ ์ ๊ฐ ๋ง๋ ์ง ๊ฒ์ฆ ํ orderService.cancelOrder() ๋ฉ์๋ ์คํ
if (!orderService.validateOrder(orderId, principal.getName())){
return new ResponseEntity<String>("์ฃผ๋ฌธ ์ทจ์ ๊ถํ์ด ์์ต๋๋ค.", HttpStatus.FORBIDDEN);
}
orderService.cancelOrder(orderId);
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
}
}
<th:block layout:fragment="script">
<script th:inline="javascript">
function cancelOrder(orderId) {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
var url = "/order/" + orderId + "/cancel";
var paramData = {
orderId : orderId,
};
var param = JSON.stringify(paramData);
$.ajax({
url : url,
type : "POST",
contentType : "application/json",
data : param,
beforeSend : function(xhr){
/* ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ธฐ ์ ์ ํค๋์ csrf๊ฐ์ ์ค์ */
xhr.setRequestHeader(header, token);
},
dataType : "json",
cache : false,
success : function(result, status){
alert("์ฃผ๋ฌธ์ด ์ทจ์ ๋์์ต๋๋ค.");
location.href='/orders/' + [[${page}]];
},
error : function(jqXHR, status, error){
if(jqXHR.status == '401'){
alert('๋ก๊ทธ์ธ ํ ์ด์ฉํด์ฃผ์ธ์');
location.href='/members/login';
} else{
alert(jqXHR.responseText);
}
}
});
}
</script>
</th:block>

๐ ํ์ธ ๋ฒํผ ํด๋ฆญ

๐ AJAX ๋ก ๋ฐ๋๋ค!


๐ ์ฌ๊ณ ๋ ์๋๋๋ก ๋์์๋ค
๋์ค์ ์ฅ๋ฐ๊ตฌ๋๋ก ์ฃผ๋ฌธ ํ์ ๋ ์ฃผ๋ฌธ ์ทจ์ํ๋ฉด ์ ๋ถ ์ทจ์๊ฐ ๋๋๋ฐ,
check box๋ก ์ ํํ์ฌ ์ทจ์๊ฐ ๋๊ฒ๋ ๋ง๋ค์.