개발/Project(Spring-쇼핑몰)

Project (6-1) 주문 기능 구현

잇(IT) 2023. 8. 14. 15:01
728x90

- 고객이 상품을 주문하면 현재 상품의 재고에서 주문 수량만큼 재고를 감소시켜야 한다.

1. 주문만큼 재고를 감소

2. 주문 수량보다 재고의 수가 적을 때 발생시킬 exception(RuntimeException) 정의


- 예외 클래스 작성

- OutofStockException.java

package com.shop.exception;

public class OutOfStockException extends RuntimeException{

    // 상품의 주문 수량보다 재고의 수가 적을 때 발생시킬 exception
    public OutOfStockException(String message) {
        super(message);
    }
}

- 재고 감소 로직

- Item.java

...

//남은 재고 수량 확인
    public void removeStock(int stockNumber) {
        int restStock = this.stockNumber - stockNumber;
        //1. 상품의 재고 수량에서 주문 후 남은 재고 수량을 구한다.
        if (restStock < 0) {
            throw new OutOfStockException("상품의 재고가 부족 합니다." +
                    "(현재 재고 수량 : " + this.stockNumber + ")");
            //2. 상품의 재고가 주문 수량보다 작을 경우 재고 부족 예외를 발생시킨다.
        }
        this.stockNumber = restStock;
        //3. 주문 후 남은 재고 수량을 상품의 현재 재고 값으로 할당한다.
    }

- this.stockNumber : DB에서 DTO로 넘어온 해당 Item 엔티티의 남은 재고에서 stockNumber : 주문 수량

- Item 엔티티에서 메서드 파라미터로 넘어온 주문 수량만큼 빼주는 메서드이다.

- 주문 수량만큼 뺀 다음 해당 결과가 0보다 적으면 재고가 부족한 것이기 때문에 예외를 발생시킨다.


- 주문할 상품과 주문 수량을 통해 OrderITem 객체를 만드는 메소드를 작성한다.

- OrderItem.java

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

- 주문 제품 객체를 새롭게 생성하고 item 엔티티에 저장된 해당 제품에 대한 정보들을 저장한다.

- 또, 주문된 수량만큼 재고를 줄이는 위에서 작성한 item 클래스의 removeStock을 실행 시킨다.


- 생성한 주문 상품 객체를 이용하여 주문 객체를 만드는 메소드를 작성한다.

- Order.java

.....

public void addOrderItem(OrderItem orderItem) {
        //1. orderItems에는 주문 상품 정보들을 담아준다.
        // orderItem 객체를 order 객체의 orderItems에 추가한다.
        orderItems.add(orderItem);
        orderItem.setOrder(this);
        //2. Order 엔티티와 OrderItem 엔티티가 양방향 참조 관계 이므로,
        //orderItem 객체에도 order 객체를 세팅한다.
    }

    public static Order createOrder(Member member, List<OrderItem> orderItemList) {
        Order order = new Order();
        order.setMember(member);
        //3. 상품을 주문한 회원의 정보를 세팅한다.
        for (OrderItem orderItem : orderItemList) {
            order.addOrderItem(orderItem);
            //4. 상품 페이지에서는 1개의 상품을 주문하지만, 장바구니 페이지에서는
            //한 번에 여러 개의 상품을 주문 할 수 있다.
            //따라서 여러 개의 주문 상품을 담을 수 있도록 리스트 형태로
            //파라미터 값을 받으며 주문 객체에 orderItem 객체를 추가한다.
        }
        order.setOrderStatus(OrderStatus.ORDER);
        //5. 주문 상태를 "ORDER"로 세팅한다.
        order.setOrderDate(LocalDateTime.now());
        //6. 현재 시간을 주문 시간으로 세팅한다.
        return order;
    }

    public int getTotalPrice() {
        //7. 총 주문 금액을 구하는 메소드이다.
        int totalPrice = 0;
        for (OrderItem orderItem : orderItems) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }
public void addOrderItem(OrderItem orderItem) {
        //1. orderItems에는 주문 상품 정보들을 담아준다.
        // orderItem 객체를 order 객체의 orderItems에 추가한다.
        orderItems.add(orderItem);
        orderItem.setOrder(this);
        //2. Order 엔티티와 OrderItem 엔티티가 양방향 참조 관계 이므로,
        //orderItem 객체에도 order 객체를 세팅한다.
    }

- 일대다 @OneToMany 관계 있는 orderItems 리스트에 주문이 들어온 제품을 추가한다.

- 또한 양방향 관계에 있기 때문에 orderItem의 주문에도 현재 객체를 넣어 준다.

 

public static Order createOrder(Member member, List<OrderItem> orderItemList) {
        Order order = new Order();
        order.setMember(member);
        //3. 상품을 주문한 회원의 정보를 세팅한다.
        for (OrderItem orderItem : orderItemList) {
            order.addOrderItem(orderItem);
            //4. 상품 페이지에서는 1개의 상품을 주문하지만, 장바구니 페이지에서는
            //한 번에 여러 개의 상품을 주문 할 수 있다.
            //따라서 여러 개의 주문 상품을 담을 수 있도록 리스트 형태로
            //파라미터 값을 받으며 주문 객체에 orderItem 객체를 추가한다.
        }
        order.setOrderStatus(OrderStatus.ORDER);
        //5. 주문 상태를 "ORDER"로 세팅한다.
        order.setOrderDate(LocalDateTime.now());
        //6. 현재 시간을 주문 시간으로 세팅한다.
        return order;
    }

- createOrder 메서드는 주문을 생성하는 메서드이고, 어떤 회원이 시켰는지(member)를 넣고, 주문 상품 리스트를 받아오고 위에서 작성한 addOrderItem 메서드를 실행함으로서 주문 상품을 리스트에 집어넣는다.

- 주문이 들어온 것이기 때문에 주문 상태를 ORDER를 바꾼다.

- 해당 메서드가 실행된 시간을 주문한 시간으로 지정한다.

- Order 객체에 데이터를 입력한 다음 해당 객체를 반환한다.

 

public int getTotalPrice() {
        //7. 총 주문 금액을 구하는 메소드이다.
        int totalPrice = 0;
        for (OrderItem orderItem : orderItems) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }

- 주문의 총 가격 : (각 제품의 가격 * 수랑)들을 전부 더한 결과이다.

- orderItems 리스트를 돌면서 각 주문 제품의 TotalPrice를 가져오고 모든 제품에 대한 가격을 더한다.


- 상품 상세 페이지에서 주문할 상품의 아이디와 주문 수량을 전달받을 OrderDto 클래스를 생성한다.

- OrderDto.java

@Getter
@Setter
public class OrderDto {

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

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

- OrderService 클래스에 주문 로직을 작성한다.

- OrderService.java

@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);
        //1. 주문할 상품을 조회한다.
        Member member = memberRepository.findByEmail(email);
        //2. 현재 로그인한 회원의 이메일 정보를 이용해서 회원 정보를 조회한다.

        List<OrderItem> orderItemList = new ArrayList<>();
        OrderItem orderItem = OrderItem.createOrderItem(item, orderDto.getCount());
        //3. 주문할 상품 엔티티와 주문 수량을 이용하여 주문 상품 엔티티를 생성한다.
        orderItemList.add(orderItem);

        Order order = Order.createOrder(member, orderItemList);
        //4. 회원 정보와 주문할 상품 리스트 정보를 이용하여 주문 엔티티를 생성한다.
        orderRepository.save(order);
        //5. 생성한 주문 엔티티를 저장한다.

        return order.getId();
    }

- itemRepository에서 Dto로 넘어온 id값을 통해 item을 찾아온다.

- memberRepository에서 email을 기반으로 member를 찾아온다.

- 찾아온 item과 주문 수량을 통해 orderItemList에 orderItem을 엔티티를 추가한다.

- Order 엔티티의 createOrder 메서드를 통해 주문을 생성하고, orderRepository에 저장한다.

- order 메서드의 반환값으로 해당 orderId를 반환한다.


- 주문 관련 요청들을 처리하기 위해 OrderController 클래스를 생성한다.

- 상품 주문에서 웹 페이지의 새로 고침 없이 서버에 주문을 요청하기 위해서 비동기 방식을 이용한다.

- OrderController.java

@Controller
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    @PostMapping(value = "/order")
    public @ResponseBody ResponseEntity order(@RequestBody @Valid OrderDto orderDto,
                                              BindingResult bindingResult, Principal principal) {
        //1. 스프링에서 비동기 처리를 할 때 @RequestBody와 @ResponseBody 어노테이션을 사용한다.
        //@RequestBody : HTTP 요청의 본문 body에 담긴 내용을 자바 객체로 전달
        //@ResponseBody : 자바 객체를 HTTP 요청의 body로 전달
        if (bindingResult.hasErrors()) {
            //2. 주문 정보를 받는 orderDto 객체에 데이터 바인딩 시 에러가 있는지 검사한다.
            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);
            //3. 에러 정보를 ResponseEntity 객체에 담아서 반환한다.
        }

        String email = principal.getName();
        //4. 현재 로그인 유저의 정보를 얻기 위해서 @Controller 어노테이션이 선언된 클래스에서
        // 메소드 인자로 principal 객체를 넘겨 줄 경우 해당 객체에 직접 접근할 수 있다.
        // principal 객체에서 현재 로그인한 회원의 이메일 정보를 조회한다.
        Long orderId;

        try {
            orderId = orderService.order(orderDto, email);
            //5. 화면으로부터 넘어오는 주문 정보와 회원의 이메일 정보를 이용하여 주문 로직을 호출한다.
        } catch (Exception e) {
            return new ResponseEntity<String>(e.getMessage(),
                    HttpStatus.BAD_REQUEST);
        }

        return new ResponseEntity<Long>(orderId, HttpStatus.OK);
        //6. 결과값으로 생성된 주문 번호와 요청이 성공했다는 HTTP 응답 상태 코드를 반환한다.
    }

- @PostMapping임과 동시에 @ResponseBody ResponseEntity를 통해 body로 넘어온 데이터를 받을 수 있고, 또한 HTTP 응답의 상태 코드, 헤더 및 본문 데이터를 정의하는데 사용된다.

- Principal

1. 현재 인증된 사용자의 정보를 나타낸다.

2. Spring Security와 함께 Spring Web 요청에서 해당 인자를 포함시키면 자동으로 현재 인증된 사용자 정보가 주입된다.

- bindingResult를 통해 에러가 발생하면 Builder 패턴을 통해 에러에 대한 객체를 생성한다.

- orderService의 order에서드는 Dto, email을 기반으로 orderitem을 찾아서 order를 생성하고 해당 orderId를 반환한다.

- 각 상황에 맞게 ResponseEntity 객체에 HTTP 상태 코드를 주입해서 객체를 생성해준다.


- 상품 상세 페이지에서 구현한 주문 로직을 호출하는 코드를 작성해야 한다.

- form 태그를 사용하여 submit 방식으로 서버에 요청하게 되면 페이지가 새로 고침 된다는 단점이 있다.

- Ajax를 이용하여 주문 로직을 비동기 방식으로 호출한다. 비동기 방식을 사용하면 웹 페이지의 새로 고침 없이 필요한 부분만 불러와 사용할 수 있다는 장점이 있다.

- itemDtl.html

function order(){
            var token = $("meta[name='_csrf']").attr("content");
            var header = $("meta[name='_csrf_header']").attr("content");
            //1,2. 스프링 시큐리티를 사용할 경우 기본적으로 POST 방식의 데이터 전송에는
            //CSRF 토큰 값이 필요하므로 해당 값들을 조회한다.

            var url = "/order";
            var paramData = {
                //3. 주문할 상품의 아이디와 주문 수량 데이터를 전달할 객체를 생성한다.
                itemId : $("#itemId").val(),
                count : $("#count").val()
            };

            var param = JSON.stringify(paramData);
            //4. 서버에 보낼 주문 데이터를 json으로 변경한다.

            $.ajax({
                url      : url,
                type     : "POST",
                contentType : "application/json",
                //5. 서버에 데이터를 보낼 형식을 json으로 지정한다.
                data     : param,
                beforeSend : function(xhr){
                    /* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
                    xhr.setRequestHeader(header, token);
                },
                dataType : "json",
                //6. 서버에서 결과값으로 받을 데이터의 타입을 json으로 설정한다.
                cache   : false,
                success  : function(result, status){
                    //7. 주문 로직 호출이 성공하면 "주문이 완료되었습니다"라는
                    //메시지를 보여주고 메인 페이지로 이동
                    alert("주문이 완료 되었습니다.");
                    location.href='/';
                },
                error : function(jqXHR, status, error){

                    if(jqXHR.status == '401'){
                        //8. 현재 로그인 상태가 아니라면, "로그인 후 이용해주세요"
                        //라는 메시지를 보여주고 로그인 페이지로 이동
                        alert('로그인 후 이용해주세요');
                        location.href='/members/login';
                    } else{
                        alert(jqXHR.responseText);
                        //9. 주문 시 에러가 발생하면 해당 메시지를 보여준다.
                    }

                }
            });
        }
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");

- 스프링 시큐리티를 사용할 경우 기본적으로 POST 방식의 데이터 전송에는 CSRF 토큰 값이 필요하므로 해당 값들을 조회한다.

var url = "/order";
            var paramData = {
                //3. 주문할 상품의 아이디와 주문 수량 데이터를 전달할 객체를 생성한다.
                itemId : $("#itemId").val(),
                count : $("#count").val()
            };
            
            ...
            
    <input type="hidden" id="itemId" th:value="${item.id}">
    ...
<input type="number" name="count" id="count" class="form-control" value="1" min="1">

- id가 각 itemId, count에 해당하는 값들을 가져온다.

var param = JSON.stringify(paramData);
            //4. 서버에 보낼 주문 데이터를 json으로 변경한다.

- Post 방식으로 body에 json 방식으로 보내기 위해 var 변수에 작성된 데이터들을 json 방식으로 변환시켜준다.

$.ajax({
                url      : url,
                type     : "POST",
                contentType : "application/json",
                //5. 서버에 데이터를 보낼 형식을 json으로 지정한다.
                data     : param,
                beforeSend : function(xhr){
                    /* 데이터를 전송하기 전에 헤더에 csrf값을 설정 */
                    xhr.setRequestHeader(header, token);
                },
                dataType : "json",
                //6. 서버에서 결과값으로 받을 데이터의 타입을 json으로 설정한다.
                cache   : false,
                success  : function(result, status){
                    //7. 주문 로직 호출이 성공하면 "주문이 완료되었습니다"라는
                    //메시지를 보여주고 메인 페이지로 이동
                    alert("주문이 완료 되었습니다.");
                    location.href='/';
                },
                error : function(jqXHR, status, error){

                    if(jqXHR.status == '401'){
                        //8. 현재 로그인 상태가 아니라면, "로그인 후 이용해주세요"
                        //라는 메시지를 보여주고 로그인 페이지로 이동
                        alert('로그인 후 이용해주세요');
                        location.href='/members/login';
                    } else{
                        alert(jqXHR.responseText);
                        //9. 주문 시 에러가 발생하면 해당 메시지를 보여준다.
                    }

                }
            });

- 비동기 방식을 이용하기 위해 ajax를 이용한다.

- ajax는 XMLHttpRequest객체를 통해 서버에 request한다. 위의 코드와 같이 헤더에 입력해야 하는 정보들을 입력하여 필요한 정보만 서버에 보내고 받을 수 있다.

- url, type, contentType 등 정보를 입력하고, 성공과 실패시 어떤 함수를 호출할지에 대한 코드들을 작성했다.

- 위 코드의 경우에는 성공 시 /(메인 페이지)로 이동하고 실패하게 되면 (로그인을 하지 않은 상태) 로그인 페이지로 이동 시킨다.

728x90