Jin's Dev Story

[스프링 부트 쇼핑몰 프로젝트 with JPA] 11. 상품 주문 본문

Web & Android/스프링 부트 쇼핑몰 프로젝트 with JPA

[스프링 부트 쇼핑몰 프로젝트 with JPA] 11. 상품 주문

woojin._. 2023. 10. 16. 19:17
이 내용은 스프링 부트 쇼핑몰 프로젝트 with JPA 책을 학습한 내용입니다.

1. 상품 재고 부족 Exception

  • 상품 주문 수량보다 현재 재고의 수가 적을 때 발생시킬 Exception 정의
  • 에러 메시지를 지정할 수 있는 RuntimeException 클래스 구현
// 상품 주문 수량보다 현재 재고의 수가 적을 때 발생시킬 Exception
public class OutOfStockException extends RuntimeException {

    public OutOfStockException(String message) {
        super(message);
    }
}

2. 상품 재고 변경

  • (기존 재고 - 주문 수량 재고) 로 stockNumber 수정
  • 만약 0 보다 작다면 재고가 부족한 것이므로 Exception 발생
// item 클래스
// 상품 재고 변경
    public void removeStock(int stockNumber) {
        // (기존 재고 - 주문 수량 재고)
        int restStock = this.stockNumber - stockNumber;

        // 만약 0보다 작다면 재고가 부족한 것이므로 Exception 발생
        if(restStock < 0) {
            throw new OutOfStockException("상품의 재고가 부족합니다. (현재 재고 수량 : " + this.stockNumber + ")");
        }
        this.stockNumber = restStock;
    }

3. OrderItem 객체

  • 주문 상품과 주문 수량 정보를 가지고 있는 OrderItem Entity 에 객체 생성 메소드 추가
// 주문 상품과 주문 수량 정보를 가지고 있는 메소드
    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;
    }

4. Order 객체

  • OrderItem 객체를 연결하고 OrderItem 객체에 자신을 연결하는 메소드 추가
  • OrderItem 객체를 이용하여 주문 객체를 만드는 메소드 추가
  • 각 주문 상품의 TotalPrice를 구한뒤 모두 더하는 메소드 추가
// OrderItem 객체 연결 및 자신을 연결하는 메소드
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem); // 주문 객체에 주문 상품 객체 연결
        orderItem.setOrder(this);  // 주문 상품 객체에 주문 객체 연결(연관 관계 주인)
    }

    // 주문 객체를 만드는 메소드
    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.setOrderDate(LocalDateTime.now());
        return order;
    }

    // 각 주문 상품의 TotalPrice를 구한 뒤 모두 더하는 메소드
    public int getTotalPrice() {
        int totalPrice = 0;

        // 각 상품마다 TotalPrice를 구하고 모두 더함
        for(OrderItem orderItem : orderItems) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }

5. 주문 정보 Dto

  • 제품 상세 페이지 화면에서 보내는 주문 정보 (상품, 수량)를 위한 DTO 객체 생성
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class OrderDto {

    @NotNull(message = "상품 아이디는 필수 입력 값입니다.")
    private Long itemId;

    @Min(value = 1, message = "최소 주문 수량은 1개입니다.")
    @Max(value = 999, message = "최대 주문 수량은 999개입니다.")
    private int count;

}

6. OrderService

  • 상품과 주문한 고객을 조회
  • 주문 상품 객체 생성 -> 주문 객체 생성
@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {

    private final ItemRepository itemRepository;   // 상품을 불러와서 재고 변경해야 함
    private final MemberRepository memberRepository; // 멤버를 불러와서 연결해야 함
    private final OrderRepository orderRepository;  // 주문 객체를 저장해야 함
    private final ItemImgRepository itemImgRepository;  // 상품 대표 이미지를 출력해야함

    // 상품과 주문한 고객 조회
    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();
    }

7. OrderController

  • 비동기 방식으로 Json 데이터를 주고 받음
    • @RequestBody : http 요청의 body 부분 데이터를 자바 객체로 변환해서 받음
    • @ResponseBody : 자바 객체를 http 응답 body 부분으로 보냄 (ResponseEntity 자료형)
  • 입력값에 문제가 있을 시 필드 에러 정보들을 ResponseEntity 객체에 담아서 반환
  • 현재 로그인한 유저의 정보를 담고 있는 Principal 객체에서 유저의 email 추출
  • 정상적으로 동작 시 생성된 주문 객체의 Id 와 상태코드 200을 보냄
@Controller
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @PostMapping("/order")
    @ResponseBody
    public 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);
        }

        // 현재 로그인한 유저의 정보를 담고 있는 Principal 객체에서 유저의 email 추출
        String email = principal.getName();
        Long orderId;

        try {
            orderId = orderService.order(orderDto, email);
        } catch (Exception e) {
            return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST);
        }

        // 정상 작동 시, 생성된 주문 객체의 id와 상태코드 200 보냄
        return new ResponseEntity<Long>(orderId, HttpStatus.OK);
    }

8. 주문 상세 페이지 수정

  • 비동기 통신 방법인 Ajax 를 이용하여 주문 요청 및 응답
  • "주문하기" 버튼을 누르면 order() 함수 스크립트를 수행하도록 지정
<button type="button" class="btn btn-primary btn-lg" onclick="order()">주문하기</button>
  • order() 함수 Ajax 코드
<!-- itemDetail.html -->
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);
          }

        }
      });
    }
  • 스프링 시큐리티를 사용할 경우 CSRF 토큰 값을 자동으로 주고 받지만, Ajax 통신할 때는 직접 코드를 구현해서 CSRF 토큰을 보내야함
<meta name="_csrf" th:content="${_csrf.token}"/>
  <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
  • 주문 성공