고객이 상품을 주문하면 현재 상품의 재고에서 주문 수량만큼 재고를 감소시켜야 합니다. 또한 주문 수량이 현재 재고 수보다 클 경우 주문이 되지 않도록 구현하겠습니다.
상품의 주문 수량보다 재고의 수가 적을 때 발생시킬 exception
을 정의하겠습니다. exception
패키지를 생성하고 하위에 RuntimeException을 상속받는 OutOfStockException 클래스를 생성합니다.
package me.jincrates.gobook.exception;
public class OutOfStockException extends RuntimeException {
public OutOfStockException(String message) {
super(message);
}
}
상품을 주문할 경우 상품의 재고를 감소시키는 로직을 작성하겠습니다.
package me.jincrates.gobook.domain.items;
//...
import me.jincrates.gobook.exception.OutOfStockException;
@Getter @ToString
@NoArgsConstructor
@Table(name = "item")
@Entity
public class Item extends BaseEntity {
//...
public void removeStock(int stockNumber) {
int restStock = this.stockNumber - stockNumber;
if (restStock < 0) {
throw new OutOfStockException("상품의 재고가 부족합니다.(현재 재고 수량: " + this.stockNumber + ")");
}
this.stockNumber = restStock;
}
}
다음으로 주문할 상품과 주문 수량을 통해 OrderItem 객체를 만드는 메소드를 작성합니다.
package me.jincrates.gobook.domain.orders;
//...
@Getter @ToString
@NoArgsConstructor
@Table(name = "order_item")
@Entity
public class OrderItem extends BaseEntity {
//...
public static OrderItem createOrderItem(Item item, int count) {
OrderItem orderItem = OrderItem.builder()
.item(item)
.count(count)
.orderPrice(item.getPrice())
.build();
item.removeStock(count);
return orderItem;
}
public int getTotalPrice() {
return orderPrice * count;
}
}
getTotalPrice()
: 주문 가격과 수량을 곱해서 해당 상품을 주문한 총 가격을 계산하는 메소드입니다.생성한 주문 상품 객체를 이용하여 주문 객체를 만드는 메소드를 작성하겠습니다.
package me.jincrates.gobook.domain.orders;
//...
@Getter @ToString
@NoArgsConstructor
@Table(name = "orders")
@Entity
public class Order extends BaseEntity {
//...
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem = OrderItem.builder()
.order(this)
.build();
}
public static Order createOrder(Member member, List<OrderItem> orderItemList) {
Order order = Order.builder()
.member(member)
.orderStatus(OrderStatus.ORDER)
.orderDate(LocalDateTime.now())
.build();
for (OrderItem orderItem : orderItemList) {
order.addOrderItem(orderItem);
}
return order;
}
public int getTotalPrice() {
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
}
addOrderItem()
: Order 엔티티와 OrderItem 엔티티가 양방향 참조 관계이므로, orderItem 객체에도 order 객체를 셋팅합니다.상품과 주문, 주문 상품 엔티티에 주문과 관련된 비즈니스 로직들을 추가했습니다. 다음으로 상품 상세 페이지에서 주문할 상품의 아이디와 주문 수량을 전달받을 OrderDto 클래스르 만들겠습니다. 주문 최소 수량은 1개, 주문 최대 수량은 999개로 제한하겠습니다.
package me.jincrates.gobook.web.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
@Data
@NoArgsConstructor
public class OrderDto {
@NotNull(message = "상품 아이디는 필수 입력 값입니다.")
private Long itemId;
@Min(value = 1, message = "최소 주문 수량은 1개 입니다.")
@Max(value = 999, message = "최대 주문 수량은 999개 입니다.")
private int count;
@Builder
public OrderDto(Long itemId, int count) {
this.itemId = itemId;
this.count = count;
}
}
package me.jincrates.gobook.service;
import lombok.RequiredArgsConstructor;
import me.jincrates.gobook.domain.items.Item;
import me.jincrates.gobook.domain.items.ItemRepository;
import me.jincrates.gobook.domain.members.Member;
import me.jincrates.gobook.domain.members.MemberRepository;
import me.jincrates.gobook.domain.orders.Order;
import me.jincrates.gobook.domain.orders.OrderItem;
import me.jincrates.gobook.domain.orders.OrderRepository;
import me.jincrates.gobook.web.dto.OrderDto;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;
@RequiredArgsConstructor
@Transactional
@Service
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 orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
orderItemList.add(orderItem);
Order order = Order.createOrder(member, orderItemList);
orderRepository.save(order);
return order.getId();
}
}
itemRepository.findById(orderDto.getItemId())
: 주문할 상품을 조회합니다.memberRepository.findByEmail(email)
: 현재 로그인한 회원의 이메일 정보를 이용해서 회원 정보를 조회합니다.OrderItem.createOrderItem(item, orderDto.getCount())
: 주문할 상품 엔티티와 주문 수량을 이용하여 주문 상품 엔티티를 생성합니다.주문 관련 요청들을 처리하기 위해 OrderController 클래스를 생성합니다. 상품 주문에서 웹 페이지의 새로고침 없이 서버에 주문을 요청하기 위해서 비동기 방식을 사용하겠습니다.
package me.jincrates.gobook.web;
import lombok.RequiredArgsConstructor;
import me.jincrates.gobook.service.OrderService;
import me.jincrates.gobook.web.dto.OrderDto;
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 javax.validation.Valid;
import java.security.Principal;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class OrderController {
private final OrderService orderService;
@PostMapping(value = "/order")
public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto
, BindingResult bindingResult, Principal principal) {
if (bindingResult.hasErrors()) {
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);
}
String email = principal.getName();
Long orderId;
try {
orderId = orderService.order(orderDto, email);
} catch (Exception e) {
return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
}
}
@RequestBody
와 @ResponseBody
어노테이션을 사용합니다.@RequestBody
: HTTP 요청의 본문 body에 담긴 내용을 자바 객체로 전달@ResponseBody
: 자바 객체를 HTTP 요청의 body로 전달package me.jincrates.gobook.service;
import me.jincrates.gobook.domain.items.Item;
import me.jincrates.gobook.domain.items.ItemRepository;
import me.jincrates.gobook.domain.items.ItemSellStatus;
import me.jincrates.gobook.domain.members.Member;
import me.jincrates.gobook.domain.members.MemberRepository;
import me.jincrates.gobook.domain.orders.Order;
import me.jincrates.gobook.domain.orders.OrderItem;
import me.jincrates.gobook.domain.orders.OrderRepository;
import me.jincrates.gobook.web.dto.OrderDto;
import org.aspectj.weaver.ast.Or;
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 javax.persistence.EntityNotFoundException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Autowired
ItemRepository itemRepository;
@Autowired
MemberRepository memberRepository;
public Item saveItem() {
Item item = Item.builder()
.itemNm("테스트 상품")
.price(10000)
.itemDetail("테스트 상품 설명")
.itemSellStatus(ItemSellStatus.SELL)
.stockNumber(100)
.build();
return itemRepository.save(item);
}
public Member saveMember() {
Member member = Member.builder()
.email("test@email.com")
.build();
return memberRepository.save(member);
}
@Test
@DisplayName("주문 테스트")
public void order() {
Item item = saveItem();
Member member = saveMember();
OrderDto orderDto = OrderDto.builder()
.itemId(item.getId())
.count(10)
.build();
Long orderId = orderService.order(orderDto, member.getEmail());
Order order = orderRepository.findById(orderId).orElseThrow(EntityNotFoundException::new);
List<OrderItem> orderItems = order.getOrderItems();
int totalPrice = orderDto.getCount() * item.getPrice();
assertEquals(totalPrice, order.getTotalPrice());
}
}
//...
<!-- 사용자 스크립트 추가 -->
<th:block layout:fragment="script">
<script th:inline="javascript">
$(document).ready(function(){
calculateTotalPrice();
$("#count").change(function() {
calculateTotalPrice();
});
});
function calculateTotalPrice() {
var price = $("#price").val();
var count = $("#count").val();
var totalPrice = price * count;
$("#totalPrice").html(totalPrice + '원');
}
function order() {
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()
};
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>
</th:block>
//...
<button type="button" class="btn btn-dark btn-lg" onclick="order()">주문하기</button>
//...
beforeSend
: 스프링 시큐리티를 사용할 경우 기본적으로 POST 방식의 데이터 전송에는 CSRF 토큰 값이 필요하므로 해당 값들을 조회합니다.order()
함수를 호출하도록 onclick 속성을 추가합니다.
주문을 했으니, 주문 내역을 조회할 수 있는 화면을 만들겠습니다. 보통 주문 이력을 조회하는 페이지에서는 주문부터 현재 상품의 배송 상태까지 보여줍니다. 상품이 출발했는지, 도착하였는지 등을 보여줍니다. 또한 반품, 교환, 취소 등의 기능들이 같이 있겠지만, 저는 주문을 취소하는 기능만 구현하겠습니다.
OrderItemDto 클래스의 생성자로 orderItem 객체와 이미지 경로를 파라미터로 받아서 멤버 변수 값을 셋팅합니다.
package me.jincrates.gobook.web.dto;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import me.jincrates.gobook.domain.orders.OrderItem;
@Data
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 me.jincrates.gobook.web.dto;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import me.jincrates.gobook.domain.orders.Order;
import me.jincrates.gobook.domain.orders.OrderStatus;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@Data
public class OrderHistoryDto {
private Long orderId; //주문아이디
private String orderDate; //주문날짜
private OrderStatus orderStatus; //주문 상태
//주문 상품 리스트
private List<OrderItemDto> orderItemDtoList = new ArrayList<>();
public OrderHistoryDto(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);
}
}
@Query
어노테이션을 이용하여 주문 이력을 조회하는 쿼리를 작성하겠습니다. 조회 조건이 복잡하지 않으면 QueryDsl을 사용하지 않고 @Query
어노테이션을 이용해서 구현하는 것도 괜찮다고 생각합니다.
package me.jincrates.gobook.domain.orders;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
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);
}
ItemImgRepository 인터페이스에서는 상품의 대표 이미지를 찾는 쿼리 메소드를 추가합니다.
package me.jincrates.gobook.domain.items;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
List<ItemImg> findByItemIdOrderByIdAsc(Long itemId);
ItemImg findByItemIdAndRepimgYn(Long itemId, String repimgYn);
}
OrderService
클래스에 주문 목록을 조회하는 로직을 구현하겠습니다.
package me.jincrates.gobook.service;
//...
import me.jincrates.gobook.web.dto.OrderDto;
import me.jincrates.gobook.web.dto.OrderHistoryDto;
import me.jincrates.gobook.web.dto.OrderItemDto;
import org.springframework.data.domain.Page;
@RequiredArgsConstructor
@Transactional
@Service
public class OrderService {
private final ItemRepository itemRepository;
private final MemberRepository memberRepository;
private final OrderRepository orderRepository;
private final ItemImgRepository itemImgRepository;
//...
@Transactional(readOnly = true)
public Page<OrderHistoryDto> getOrderList(String email, Pageable pageable) {
List<Order> orders = orderRepository.findOrders(email, pageable);
Long totalCount = orderRepository.countOrder(email);
List<OrderHistoryDto> orderHistoryDtos = new ArrayList<>();
for (Order order : orders) {
OrderHistoryDto orderHistoryDto = new OrderHistoryDto(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());
orderHistoryDto.addOrderItemDto(orderItemDto);
}
orderHistoryDtos.add(orderHistoryDto);
}
return new PageImpl<OrderHistoryDto>(orderHistoryDtos, pageable, totalCount);
}
}
countOrder(email)
: 유저의 주문 총 개수를 구합니다.구매이력을 조회할 수 있도록 OrderController에 조회 메소드를 만들겠습니다.
package me.jincrates.gobook.web;
//...
@RequiredArgsConstructor
@Controller
public class OrderController {
//...
@GetMapping(value = {"/orders", "/orders/{page}"})
public String orderHistory(@PathVariable("page")Optional<Integer> page, Principal principal, Model model) {
Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 4);
Page<OrderHistoryDto> orderHistoryDtoList = orderService.getOrderList(principal.getName(), pageable);
model.addAttribute("orders", orderHistoryDtoList);
model.addAttribute("page", pageable.getPageNumber());
model.addAttribute("maxPage", 5);
return "order/orderHistory";
}
}
이제 구매 이력을 조회하는 로직 구현이 완료됐습니다. 다음으로 구매 이력 페이지를 만들겠습니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/default}">
<head>
<meta name="_csrf" th:content="${_csrf.token}"/>
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
</head>
<!-- 사용자 CSS 추가 -->
<th:block layout:fragment="css">
<style
.repImgDiv{
margin-right:15px;
margin-left:15px;
height:auto;
}
.repImg{
height:100px;
width:100px;
}
.card{
height:100%;
padding:30px;
margin-bottom:20px;
}
.fs18{
font-size:18px
}
.fs24{
font-size:24px
}
</style>
</th:block>
<div layout:fragment="content">
<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.toString().equals('ORDER')}">
<button type="button" class="btn btn-outline-secondary py-0 px-1" th:value="${order.orderId}" onclick="cancelOrder(this.value)">주문취소</button>
</th:block>
<th:block th:unless="${order.orderStatus.toString().equals('ORDER')}">
<h4>(취소 완료)</h4>
</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>
상품의 재고를 더해주기 위해서 Item 엔티티에 addStock
메소드를 생성합니다.
package me.jincrates.gobook.domain.items;
//...
@Getter @ToString
@NoArgsConstructor
@Table(name = "item")
@Entity
public class Item extends BaseEntity {
//...
public void addStock(int stockNumber) {
this.stockNumber += stockNumber;
}
}
주문을 취소할 경우 주문 수량 만큼 상품의 재고를 증가시키는 cancel
메소드를 구현합니다.
package me.jincrates.gobook.domain.orders;
//...
@Getter @ToString
@NoArgsConstructor
@Table(name = "order_item")
@Entity
public class OrderItem extends BaseEntity {
//...
public void cancel() {
this.getItem().addStock(count);
}
}
Item 클래스에 주문 취소시 주문 수량을 상품의 재고에 더해주는 로직과 주문 상태를 취소 상태로 바꿔주는 메소드를 구현합니다.
package me.jincrates.gobook.domain.orders;
//...
@Getter @ToString
@NoArgsConstructor
@Table(name = "orders")
@Entity
public class Order extends BaseEntity {
//...
public void cancelOrder() {
this.orderStatus = OrderStatus.CANCEL;
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
}
주문을 취소하는 로직을 구현해봅시다.
package me.jincrates.gobook.service;
//...
@RequiredArgsConstructor
@Transactional
@Service
public class OrderService {
//...
@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.cancelOrder();
}
}
validateOrder()
: 현재 로그인한 사용자와 주문 데이터를 생성한 사용자가 같은지 검사를 합니다.order.cancelOrder()
: 주문 취소 상태로 변경하면 변경 감지 기능에 의해서 트랜잭션이 끝날 때 update 쿼리가 실행됩니다.OrderController 클래스에 주문번호(orderId)를 받아서 주문 취소 로직을 호출하는 메소드를 만들겠습니다. 상품을 장바구니에 담았을 때처럼 비동기 요청을 받아서 처리하겠습니다.
package me.jincrates.gobook.web;
//...
@RequiredArgsConstructor
@Controller
public class OrderController {
//...
@PostMapping("/order/{orderId}/cancel")
public @ResponseBody ResponseEntity cancelOrder(@PathVariable("orderId") Long orderId, Principal principal) {
if (!orderService.validateOrder(orderId, principal.getName())) {
return new ResponseEntity<String>("주문 취소 권한이 없습니다.", HttpStatus.FORBIDDEN);
}
orderService.cancelOrder(orderId);
return new ResponseEntity<Long>(orderId, HttpStatus.OK);
}
}
validateOrder()
: 자바스크립트에서 취소할 주문 번호는 조작이 가능하므로 다른 사람의 주문을 취소하지 못하도록 주문 취소 권한을 검사합니다.주문을 취소하는 로직 구현이 완료됐습니다. 주문 취소 로직이 제대로 동작하는지 테스트 코드를 작성하겠습니다.
package me.jincrates.gobook.service;
//...
@SpringBootTest
@Transactional
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class OrderServiceTest {
//...
@Test
@DisplayName("주문 취소 테스트")
public void cancelOrder() {
Item item = saveItem();
Member member = saveMember();
OrderDto orderDto = OrderDto.builder()
.itemId(item.getId())
.count(10)
.build();
Long orderId = orderService.order(orderDto, member.getEmail());
Order order = orderRepository.findById(orderId).orElseThrow(EntityNotFoundException::new);
orderService.cancelOrder(orderId);
assertEquals(OrderStatus.CANCEL, order.getOrderStatus());
assertEquals(100, item.getStockNumber());
}
}
cancelOrder() 함수를 가지고 있는 사용자 스크립트 영역을 추가합니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/default}">
<!-- 사용자 스크립트 추가 -->
<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>
<div layout:fragment="content">
<!-- 코드 생략 -->
<button type="button" class="btn btn-outline-secondary py-0 px-1" th:value="${order.orderId}" onclick="cancelOrder(this.value)">주문취소</button>
<!-- 코드 생략 -->
</div>
</html>