관심쟁이 영호

[#19] Spring Boot ㅣ 완성 코드 분석 ㅣ 주문 취소 살펴보기 본문

Bank-End/인프런- Spring 공부

[#19] Spring Boot ㅣ 완성 코드 분석 ㅣ 주문 취소 살펴보기

관심쟁이 영호 2021. 5. 3. 01:52
반응형

이번에는 주문취소에 대해서 살펴볼 예정이다.

주문취소를 보는 이유는

 

DB에서 값을 가져와서 데이터를 변경해주는 작업이기 때문이다.

여기서 문제가 있다.

 

만약, 물품이 10개가 있다.

A라는 사람이 1개의 물품을 주문함과 동시에

B라는 사람이 10개의 물품을 주문할 수도 있다.

이렇게 된다면, DB에도 문제가 생기고

결제서비스와 같은 직접 돈과 연관되어 있는 문제면 엄청난 파장이 생길 것이다.

 

어쨌든 살펴보자.

홈화면에서 "주문 내역"을 눌러준다.

 

 

그러면 위와같은 페이지가 나온다.

페이지에 해당하는 매핑 컨트롤러로 가보자!

페이지는 "http://localhost:8080/orders" 이다.

 

// OrderController.class

@Controller
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;
    private final MemberService memberService;
    private final ItemService itemService;
    
    @GetMapping(value = "/orders")
    public String orderList(@ModelAttribute("orderSearch") OrderSearch
                                    orderSearch, Model model) {
        List<Order> orders = orderService.findOrders(orderSearch);
        model.addAttribute("orders", orders);
        return "order/orderList";
    }

   
}

@GetMapping으로 구성되었다.

 

먼저 DB에서 모든 order을 가지고 온다.

 

- 이전 블로그에 다루지 않았던, db를 들고오는 것을 한번 짚고가자.

OrderService에서 Repository의 메소드를 실행한다.

영속성 컨텍스트를 다루는 Repository를 접근하는 것은 당연할 것이다.

 

// OrderRepository.class

@Repository
@RequiredArgsConstructor
public class OrderRepository {

    private final EntityManager em;

    public Order findOne(Long id) {
        return em.find(Order.class, id);
    }

    // public List<Order> findAll(OrderSearch ordersearch){}
    public List<Order> findAllByCriteria(OrderSearch orderSearch) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Order> cq = cb.createQuery(Order.class);
        Root<Order> o = cq.from(Order.class);
        Join<Order, Member> m = o.join("member", JoinType.INNER); //회원과 조인
        List<Predicate> criteria = new ArrayList<>();
        //주문 상태 검색
        if (orderSearch.getOrderStatus() != null) {
            Predicate status = cb.equal(o.get("status"),
                    orderSearch.getOrderStatus());
            criteria.add(status);
        }
        //회원 이름 검색
        if (StringUtils.hasText(orderSearch.getMemberName())) {
            Predicate name =
                    cb.like(m.<String>get("name"), "%" +
                            orderSearch.getMemberName() + "%");
            criteria.add(name);
        }
        cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
        TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000); //최대1000건
        return query.getResultList();


    }
}

findOne, findAll 두개의 메소드가 있다.

1개를 찾아오는 것, 전부를 찾아오는 것이 있다.

 

우리는 List에 담을 모든 order을 불러와야 하기때문에, findall을 살펴볼 예정이다.

 

해당하는 부분에 Criteria 방식으로 db를 조회하는 것이다.

근데, Criteria라는 방식에 대해서 정확한 설명을 듣지못해서 확실하게 기록할 수가 없다.

이유는, 실무에서는 다른방식을 사용하신다고 하셔서 제대로 안들었다..ㅋㅋㅋ

 

어쨋든 여기에서 검색 text가 입력되어있다면 해당하는 정보만 조회,

없다면 모든 정보를 조회할 수 있도록 만들어놓은 것이다.

 

cb에 em.getCritiriaBuilder를 넣어주고,

이것에다가 만드는 느낌이다.

 

다시 돌아가면,

이런식으로 join하고 조회를 해와서 해당값을 리턴해주고, 그것을 리스트에 담아준다.

 

  • 취소는 어떻게 되는건가?

이건 먼저 html문서를 봐야한다.

이유는 버튼 클릭 액션이 일어나고, 거기서 해당하는 요청을 해야하기 때문이다.

 

html을 살펴보자.

<!-- OrderList.html -->

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader"/>
    <div>
        <div>
            <form th:object="${orderSearch}" class="form-inline">
                <div class="form-group mb-2">
                    <input type="text" th:field="*{memberName}" class="formcontrol" placeholder="회원명"/>
                </div>
                <div class="form-group mx-sm-1 mb-2">
                    <select th:field="*{orderStatus}" class="form-control">
                        <option value="">주문상태</option>
                        <option th:each=
                                        "status : ${T(jpashop.jpashop.domain.OrderStatus).values()}"
                                th:value="${status}"
                                th:text="${status}">option
                        </option>
                    </select>
                </div>
                <button type="submit" class="btn btn-primary mb-2">검색</button>
            </form>
        </div>
        <table class="table table-striped">
            <thead>
            <tr>
                <th>#</th>
                <th>회원명</th>
                <th>대표상품 이름</th>
                <th>대표상품 주문가격</th>
                <th>대표상품 주문수량</th>
                <th>상태</th>
                <th>일시</th>
                <th></th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item : ${orders}">
                <td th:text="${item.id}"></td>
                <td th:text="${item.member.name}"></td>
                <td th:text="${item.orderItems[0].item.name}"></td>
                <td th:text="${item.orderItems[0].orderPrice}"></td>
                <td th:text="${item.orderItems[0].count}"></td>
                <td th:text="${item.status}"></td>
                <td th:text="${item.orderDate}"></td>
                <td>
                    <a th:if="${item.status.name() == 'ORDER'}" href="#"
                       th:href="'javascript:cancel('+${item.id}+')'"
                       class="btn btn-danger">CANCEL</a>
                </td>
            </tr>
            </tbody>
        </table>
    </div>
    <div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
<script>
 function cancel(id) {
 var form = document.createElement("form");
 form.setAttribute("method", "post");
 form.setAttribute("action", "/orders/" + id + "/cancel");
 document.body.appendChild(form);
 form.submit();
 }
</script>
</html>

여기서 cancel하면 해당하는 주문이 사라져야해서, 자바스크립트가 실행된 것을 볼 수 있다.

자바스크립트 공부가 아니니, 다음에 짚어보자!

 

그럼 코드에서 해당하는 주문의 영속성 컨텍스트를 삭제해주어야 한다.

코드가 어떻게 움직일까?

 

여기도 form이 있고, action과 post방식으로 넘겨주고,

해당하는 url을 보내는 것을 볼 수 있다.

 

"/orders/" + id + "/cancel" <- 이렇게 표시되어 있는 곳과 매핑되어있는 곳을 보자.

 

// OrderController

 @PostMapping(value = "/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable("orderId") Long orderId) {
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }

위와같다. {}중괄호는 자바스크립트 쪽인거 같으니까 다음에 짚어보고,

해당하는 값이 id와 같이 넘어온다.

 

그리고 Service.cancelOrder 메서드를 불러온다.

orderService 클래스를 보자.

 

// OrderService.class

//취소
    @Transactional
    public void cancelOrder(Long orderId){

        // 주문 엔티티 조회
        Order order = orderRepository.findOne(orderId);

        // 주문 취소
        order.cancel();


    }

해당하는 id를 DB와 직접연결된 Repository 클래스에 넘겨준다.

그리고 Repository에서 찾아온 값을 cancel 메서드를 호출하여 삭제해준다.

 

public void cancel(){
        if(delivery.getStatus() == DeliveryState.COMP){ //COMP 는 이미 배송이 완료된 상품이다.
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }

        this.setStatus(OrderStatus.CANCEL);

        for(OrderItem orderItem: orderItems){
            //주문에 담은 각 상품들에 cancel을 날려주어 상품도메인에 각 상품에 갯수를 + 해주어야 한다.
            orderItem.cancel();
        }
    }

cancel 메소드이다.

먼저 배송이 완료되었는지, 확인을 해준다.

배송이 완료되었으면 취소불가하다.

배송이 완료되지 않았으면 상품주문을 취소하고 해당상품의 재고를 1개 늘려주어야 한다.

그래서 orderItem.cancel을 불러준다.

 

그럼 orderitem.cancel을 살펴보자.

public void cancel() { //재고를 늘려주는게 목표이다,
        getItem().addStock(count);
    }

 

주문 취소를 했으니, 당연히 재고는 늘어날 것이다.

getItem은 해당 item 객체를 받아오는 것이고, 해당 객체의 addStock 메서드는 다음과 같다.

 

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

수량이 늘어난 것을 볼 수 있다.

 

이렇게하면 영속성 컨텍스트에 포함되어 있는 객체의 값이 변경될 것이고,

JPA는 영속성 컨텍스트의 데이터 변경감지를 알아차리고

DB테이블과 매칭시켜 데이터를 바꾸어줄 것이다.

 

300x250
Comments