[7] Order

Veloger·2023년 1월 5일
0

1. Order 기능

✨ 주문 시, 재고 부족 익셉션

-> OutOfStockException

public class OutOfStockException extends RuntimeException {
    public OutOfStockException(String message){
        super(message);
    }
}

✨ 주문시 상품 재고 감소시키는 로직

-> Item

    public void removeStock(int stockNumber){
        int restStock = this.stockNumber - stockNumber;
        if(restStock <0){
            throw new OutOfStockException("상품의 재고가 부족 합니다. (현재 재고 수량 : "+this.stockNumber+")");
        }
        this.stockNumber = restStock;
    }

✨ 주문할 상품과 주문 수량을 통해 주문상품 만드는 메소드

-> 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;
    }

✨ 생성한 주문상품으로 주문 만들기

-> Order

public void addOrderItem(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this); // Order와 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.setOrderDate(LocalDateTime.now());
        return order;
    }

    public int getTotalPrice(){
        int totalPrice = 0;
        for(OrderItem orderItem : orderItems){
            totalPrice+=orderItem.getTotalPrice();

        }
        return totalPrice;
    }

✨ 상품 상세 페이지의 주문할 상품ID와 주문 수량을 전달받을 DTO 생성

-> OrderDto

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

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

✨ 주문 로직을 서비스 클래스에 작성

-> OrderService

@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {
    private final ItemRepository itemRepository;
    private final MemberRepository memberRepository;
    private final OrderRepository orderRepository;
    private final ItemImgRepository itemImgRepository;

    /*
    
    1. orderItemList를 만든다.
    2. orderItem을 만들어서 List에 추가
    3. Order를 만들고 해당 List를 추가
    4. Order의 id 반환

     */
    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();

    }

✨ 주문 관련 요청을 처리할 주문 컨트롤러 만듬

비동기 방식으로 만들기 위해 @RequestBody, @ResponseBody 사용

-> OrderController

	 @PostMapping(value = "/order")
    public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto, BindingResult bindingResult,
                                              Principal principal){

        // 결과의 각 필드의 에러를 StringBuilder로 모은 다음, ResponseEntity로 에러메시지와, Http의 상태를 한번에 전송한다.
        if(bindingResult.hasErrors()) {
            StringBuilder sb = new StringBuilder(); // StringBuilder : 변경할 수 있는 문자열이고, append를 통해 문자열을 추가하고, toString으로 String 객체를 반환
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            for (FieldError fieldError : fieldErrors) {
                sb.append(fieldError.getDefaultMessage());
            }
            return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST); // 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스
        }

        String email = principal.getName(); // Authentication의 최상위 인터페이스로 현재 접속 유저의 정보를 가지고 있음
        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 : HttpRequest의 본문 body에 담긴 내용을 자바 객체로 전달
  • @ResponseBody : 자바 객체를 HTTPRequest의 body로 전달

✨ 주문 로직이 정상적인지 테스트 코드 작성

✨ 상품 상세 페이지에서 주문 로직 호출하기

상품 상세 뷰에 코드를 수정한다.

-> ItemDtl.html

	function order(){
      		// POST 방식은 CSRF 토큰이 필요
            var token = $("meta[name='_csrf']").attr("content");
            var header = $("meta[name='_csrf_header']").attr("content");

            var url = "/order";
      		// 전달 받은 정보를 객체로 만듬
            var paramData = {
                itemId : 

      		// 서버에 보낼 주문 정보를 JSON으로 변환
            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){

                      // http 오류 번호를 반환
                    if(jqXHR.status == '401'){
                        alert('로그인 후 이용해주세요');
                        location.href='/members/login';
                    } else{
                      // url의 full response를 반환
                        alert(jqXHR.responseText);
                    }

                }
            });

2. Order List 조회

주문 목록 및 주문 취소 기능 추가

  1. Order Repository에서 조회하는 쿼리
  2. 조회한 정보를 저장할 Order 정보 객체
  3. Order에 있는 Order_Item을 화면에 보여줄 DTO
  4. Order_Service에 Order 목록을 조회해서, 페이지 구현 객체로 반환
  5. Order 조회 메서드를 통해, 뷰에 정보 전달
  6. Order 목록 페이지 생성

✨ 조회한 주문 데이터를 화면에 보낼 DTO 생성

-> OrderItemDto

@Getter @Setter
public class OrderItemDto {

    public OrderItemDto(OrderItem orderItem, String imgUrl){
        this.itemNm = orderItem.getItem().getItemNm();
        this.count = orderItem.getCount();
        this.orderPrice = orderItem.getOrderPrice();
        this.imgUrl = imgUrl;
    }

    private String itemNm; //상품명
    private int count; //주문 수량

    private int orderPrice; //주문 금액
    private String imgUrl; //상품 이미지 경로

✨ 주문 정보를 담을 DTO 생성

-> OrderHistDto

@Getter @Setter
public class OrderHistDto {

    public OrderHistDto(Order order){
        this.orderId = order.getId();
        this.orderDate = order.getOrderDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
        this.orderStatus = order.getOrderStatus();
    }

    private Long orderId; //주문아이디
    private String orderDate; //주문날짜
    private OrderStatus orderStatus; //주문 상태
    private List<OrderItemDto> orderItemDtoList = new ArrayList<>(); //주문 상품리스트

    
    public void addOrderItemDto(OrderItemDto orderItemDto){
        orderItemDtoList.add(orderItemDto);
    }

}

✨ 레포지토리에 주문 이력을 조회하는 쿼리를 작성

-> OrderRepository

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);
}

✨ 주문 서비스에 주문 목록 조회하는 로직 구현

-> OrderService

    @Transactional(readOnly = true)
    public Page<OrderHistDto> getOrderList(String email, Pageable pageable) {

        List<Order> orders = orderRepository.findOrders(email, pageable);
        Long totalCount = orderRepository.countOrder(email);

        List<OrderHistDto> orderHistDtos = new ArrayList<>();

        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);
        }

        return new PageImpl<OrderHistDto>(orderHistDtos, pageable, totalCount);
    }
  • 주문 레포지토리에서 페이징조건에 맞는 주문 목록 조회
  • 주문 목록을 for문 돌면서 주문 정보 객체로 만듬
  • 페이지 구현 객체로 생성해서 반환

✨ 구매이력을 조회하도록 컨트롤러 클래스에 해당 로직 호출

-> OrderController

    @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, 4);
        Page<OrderHistDto> ordersHistDtoList = orderService.getOrderList(principal.getName(), pageable);

        model.addAttribute("orders", ordersHistDtoList);
        model.addAttribute("page", pageable.getPageNumber());
        model.addAttribute("maxPage", 5);

        return "order/orderHist";
    }

✨ 구매 이력 페이지 생성


[ 추가 내용 ]

N+1 문제

요청이 1개인 쿼리인데 N개의 쿼리가 발생하는 현상

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);
        }

위 코드에서 getOrderItems()라는 부분에 주목하자.

회원 당 여러개의 Order을 가짐
해당 회원의 모든 Order_item을 가지고 오고 싶음.
아래 쿼리처럼 Order별로 Order_item을 다 받아와야한다.

orderitems0_.order_id = ?

Order의 갯수가 많아지면 쿼리문이 많이 발생해서 부화가 생긴다.
이를 방지하기 위해 order_id를 IN 조건을 이용해 한 번에 여러 Order_id를 실행하면 좋을 것 같다.

default_batch_fetch_size=1000으로 설정하면, order_id를 in 조건으로 묶어서 해당하는 모든 order_item들을 받아옴.
조회 쿼리 한 번으로 지정한 사이즈 만큼 한 번에 조회가 가능

예시) order_id in (Order1, Order2, Order3)

orderitems0_.order_id in (?, ?)

3. Order Cancel

상품 주문과 반대로 취소 시, Stock 수만 다시 증가 시키면 된다.

  1. 상품 재고를 다시 더해주는 메소드
  2. 주문상품 별 1번 메소드 호출
  3. 주문에서 주문 상품 별 2번의 메소드 호출
  4. 주문서비스에서 취소하기 (회원 맞는지 확인 및 DB에서 주문 불러와서 3번 실행)
  5. 주문 컨트롤러에서 4번 실행하고, 주문 번호를 담은 페이지 구현 객체 반환
  6. 주문 취소 기능을 실행하는 JS 함수 추가

✨ 상품 재고를 더해주는 메소드 추가

-> Item

    public void addStock(int stockNumber){
        this.stockNumber += stockNumber;
    }

✨ 주문 수량만큼 상품 재고를 증가시키는 메소드 구현

-> OrderItem

    public void cancel() {
        this.getItem().addStock(count);
    }

✨ 취소 시 주문 수량을 재고에 더해주는 로직, 주문상태를 취소로 바꿔주는 메소드

-> Order

    public void cancelOrder() {
        this.orderStatus = OrderStatus.CANCEL;
        for (OrderItem orderItem : orderItems) {
            orderItem.cancel();
        }
    }

✨ 주문 취소하는 로직 구현

-> 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;
    }

	// 주문 불러와서 Order의 취소 메서드 실행
    public void cancelOrder(Long orderId){
        Order order = orderRepository.findById(orderId)
                .orElseThrow(EntityNotFoundException::new);
        order.cancelOrder();
    }

✨ 주문 번호를 받아서 주문 취소 로직을 호출 메서드 생성

-> 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);
    }
  • ResponseEntity : 결과 데이터와 HTTP 상태 코드를 직접 제어할 수 있는 클래스
    결과값! 상태코드! 헤더값!을 모두 프론트에 넘겨줄 수 있고, 에러코드 또한 섬세하게 설정해서 보내줄 수 있다는 장점이 있다!
    (ResponseEntity란 무엇인가?)

✨ 주문 취소 기능을 호출하는 JS 함수 추가

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); // JSON화

            $.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);
                    }
                }
            });
        }

AJAX : JS라이브러리로, JS를 이용해서 서버에 요청 (Ajax 알아보기)
xhr : XMLHttpRequest의 약자로 AJAX를 통해 서버에 요청할 때 사용하는 객체
jqXHR : 브라우저 고유 XMLHttpRequest 객체를 대체 / 기본 XHR 기능을 시뮬레이션

0개의 댓글

관련 채용 정보