ACHO.pk devlog

[Springboot] 주문 이력 조회 및 주문 취소하기 본문

프레임워크/Springboot

[Springboot] 주문 이력 조회 및 주문 취소하기

Acho 2023. 3. 21. 00:44

1. 주문 내역 조회하기

조회할 주문 데이터를 화면에 보낼 때 사용할 DTO 클래스를 만들어보자.

dto > OrderItemDto

package com.shop.shop.dto;

import com.shop.shop.entity.OrderItem;
import lombok.Getter;
import lombok.Setter;

@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; //상품 이미지 경로

}

▹OrderItemDto 클래스의 생성자로 orderItem 객체와 이미지 경로를 파라미터로 받아서 멤버 변수 값을 세팅한다.

public OrderItemDto(OrderItem orderItem, String imgUrl){

 

 

주문 정보를 담을 클래스를 생성한다.

dto > OrderHistDto

package com.shop.shop.dto;

import com.shop.shop.constant.OrderStatus;
import com.shop.shop.entity.Order;
import lombok.Getter;
import lombok.Setter;

import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

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

OrderHistDto 클래스의 생성자로 order 객체를 파라미터로 받아서 멤버 변수 값을 세팅한다. 주문 날짜의 경우 화면에 "yyyy-MM-dd HH:mm" 형태로 전달하기 위해서 포맷을 수정한다.

public OrderHistDto(Order order){

 

▹orderItemDto 객체를 주문 상품 리스트에 추가하는 메소드이다.

public void addOrderItemDto(OrderItemDto orderItemDto){

 

 

@Query 어노테이션을 이용하여 주문 이력을 조회하는 쿼리를 작성하자.

@Query 안에 들어가는 문법은 JPQL이다. 

repository > OrderRepository

package com.shop.shop.repository;

import com.shop.shop.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.data.domain.Pageable;
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);
}

▹현재 로그인한 사용자의 주문 데이터를 페이징 조건에 맞춰서 조회한다.

List<Order> findOrders(@Param("email") String email, Pageable pageable);

 

▹현재 로그인한 회원의 주문 개수가 몇 개인지 조회한다.

Long countOrder(@Param("email") String email);

 

 

상품의 대표 이미지를 찾는 쿼리 메소드를 추가한다. 

구매 이력 페이지에서 주문 상품의 대표 이미지를 보여주기 위함이다. 

repository > ItemImgRepository

package com.shop.shop.repository;

import com.shop.shop.entity.ItemImg;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface ItemImgRepository extends JpaRepository<ItemImg, Long> {
    List<ItemImg> findByItemIdOrderByIdAsc(Long iemId);

    ItemImg findByItemIdAndRepimgYn(Long itemId, String repimgYn);
}

 

 

주문 목록을 조회하는 로직을 구현해보자.

service > OrderService

package com.shop.shop.service;

import com.shop.shop.dto.OrderItemDto;
import com.shop.shop.entity.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.shop.shop.dto.OrderDto;
import com.shop.shop.dto.OrderHistDto;
import com.shop.shop.repository.ItemImgRepository;
import com.shop.shop.repository.ItemRepository;
import com.shop.shop.repository.MemberRepository;
import com.shop.shop.repository.OrderRepository;

import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;

import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;

@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {

    private final ItemRepository itemRepository;

    private final MemberRepository memberRepository;

    private final OrderRepository orderRepository;

    private final ItemImgRepository itemImgRepository;

    ...생략...

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

▹유저의 아디이와 페이징 조건을 이용하여 주문 목록을 조회한다.

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

 

▹유저의 주문 총 개수를 구한다.

Long totalCount = orderRepository.countOrder(email);

 

▹주문 리스트를 순회하면서 구매 이력 페이지에 전달할 DTO를 생성한다.

for (Order order : orders) {

 

▹주문한 상품의 대표 이미지를 조회한다.

ItemImg itemImg = itemImgRepository.findByItemIdAndRepimgYn
        (orderItem.getItem().getId(), "Y");

 

▹페이지 구현 객체를 생성하여 반환한다.

return new PageImpl<OrderHistDto>(orderHistDtos, pageable, totalCount);

 

 

구매이력을 조회할 수 있도록 지금까지 구현한 로직을 호출하는 메소드를 만들어보자.

controller > OrderController

package com.shop.shop.controller;

import com.shop.shop.dto.OrderDto;
import com.shop.shop.dto.OrderHistDto;
import com.shop.shop.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.security.Principal;
import java.util.List;
import java.util.Optional;

@Controller
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;
	
    ...생략...

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

▹한 번에 가지고 올 주문의 개수는 4개로 설정한다.

Pageable pageable = PageRequest.of(page.isPresent() ? page.get() : 0, 4);

 

▹현재 로그인한 회원은 이메일과 페이징 객체를 파라미터로 전달하여 화면에 전달한 주문 목록 데이터를 리턴 값으로 받는다.

Page<OrderHistDto> ordersHistDtoList = orderService.getOrderList(principal.getName(), pageable);

 

 

구매 이력 페이지를 만들어서 주문 목록 데이터를 보여주는 html 파일을 만들어보자.

templates > order > orderHist.html

아래 링크에서 코드를 가져오면 된다.

https://github.com/roadbook2/shop/blob/master/src/main/resources/templates/order/orderHist.html

 

여기까지 하면 지금까지 주문했던 상품들의 목록이 화면에 보인다.

 

 

 

2. 주문 취소하기 

상품의 재고를 더해주기 위해서 Item 클래스 addStock 메소드를 생성한다.

entity > Item

package com.shop.shop.entity;

import com.shop.shop.constant.ItemSellStatus;
import com.shop.shop.dto.ItemFormDto;
import com.shop.shop.exception.OutOfStockException;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name="item")
@Getter
@Setter
@ToString
public class Item extends BaseEntity{

    ...생략...

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

}

▹상품의 재고를 증가시키는 메소드이다. 

public void addStock(int stockNumber){

 

 

주문을 취소할 경우 주문 수량만큼 상품의 재고를 증가시키는 메소드를 구현해보자.

entity > OrderItem

package com.shop.shop.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Getter
@Setter
public class OrderItem extends BaseEntity{
    ...생략...
    
    public void cancel() {
        this.getItem().addStock(count);
    }
}

▹주문 취소 시 주문 수량만큼 상품의 재고를 더해준다.

public void cancel() {

 

 

Item 클래스에 주문 취소 시 주문 수량을 상품의 재고에 더해주는 로직과 주문 상태를 취소 상태로 바꿔주는 메소드를 구현하자.

entity > Order

package com.shop.shop.entity;

import com.shop.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order extends BaseEntity{

    ...생략...

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

 

 

주문을 취소하는 로직을 구현해보자.

service > OrderService

package com.shop.shop.service;

import com.shop.shop.dto.OrderItemDto;
import com.shop.shop.entity.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.shop.shop.dto.OrderDto;
import com.shop.shop.dto.OrderHistDto;
import com.shop.shop.repository.ItemImgRepository;
import com.shop.shop.repository.ItemRepository;
import com.shop.shop.repository.MemberRepository;
import com.shop.shop.repository.OrderRepository;

import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;

import javax.persistence.EntityNotFoundException;
import java.util.ArrayList;
import java.util.List;

@Service
@Transactional
@RequiredArgsConstructor
public class OrderService {

    private final ItemRepository itemRepository;

    private final MemberRepository memberRepository;

    private final OrderRepository orderRepository;

    private final ItemImgRepository itemImgRepository;

    ...생략...

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

▹현재 로그인한 사용자와 주문 데이터를 생성한 사용자가 같은지 검사를 한다. 같을 때는 true를 반환하고 같지 않을 경우는 false를 반환한다.

public boolean validateOrder(Long orderId, String email){

 

▹주문 취소 상태로 변경하면 변경 감지 기능에 의해서 트랜잭션이 끝날 때 update 쿼리가 실행된다.

order.cancelOrder();

 

 

주문번호를 받아서 주문 취소 로직을 호출하는 메소드를 만들어보자. 상품을 장바구니에 담았을 때처럼 비동기 요청을 받아서 처리하는 것이다. 

controller > OrderController

package com.shop.shop.controller;

import com.shop.shop.dto.OrderDto;
import com.shop.shop.dto.OrderHistDto;
import com.shop.shop.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.security.Principal;
import java.util.List;
import java.util.Optional;

@Controller
@RequiredArgsConstructor
public class OrderController {
    private final OrderService orderService;

    ...생략...
    
    @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);
    }

}

▹자바스크립트에서 취소할 주문 번호는 조작이 가능하므로 다른 사람의 주문을 취소하지 못하도록 주문 취소 권한 검사를 한다.

if(!orderService.validateOrder(orderId, principal.getName())){

 

▹주문 취소 로직을 호출한다.

orderService.cancelOrder(orderId);

 

 

2-1. 테스트 코드 작성

주문 취소 로직이 제대로 동작하는지 확인해야 한다.

package com.shop.shop.service;

import com.shop.shop.constant.ItemSellStatus;
import com.shop.shop.constant.OrderStatus;
import com.shop.shop.dto.OrderDto;
import com.shop.shop.entity.Item;
import com.shop.shop.entity.Member;
import com.shop.shop.entity.Order;
import com.shop.shop.entity.OrderItem;
import com.shop.shop.repository.ItemRepository;
import com.shop.shop.repository.MemberRepository;
import com.shop.shop.repository.OrderRepository;
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 javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@Transactional
@TestPropertySource(locations="classpath:application-test.properties")
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    ItemRepository itemRepository;

    @Autowired
    MemberRepository memberRepository;

    ...생략...

    @Test
    @DisplayName("주문 취소 테스트")
    public void cancelOrder(){
        Item item = saveItem();
        Member member = saveMember();

        OrderDto orderDto = new OrderDto();
        orderDto.setCount(10);
        orderDto.setItemId(item.getId());
        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());
    }

}

▹테스트를 위해서 상품과 회원 데이터를 생성한다. 생성한 상품의 재고는 100개다.

Item item = saveItem();
Member member = saveMember();

 

▹테스트를 위해서 주문 데이터를 생성한다. 주문 개수는 총 10개다.

Long orderId = orderService.order(orderDto, member.getEmail());

 

▹생성한 주문 엔티티를 조회한다.

Order order = orderRepository.findById(orderId)
        .orElseThrow(EntityNotFoundException::new);

 

▹해당 주문을 취소한다.

orderService.cancelOrder(orderId);

 

▹주문의 상태가 취소 상태라면 테스트가 통과한다.

assertEquals(OrderStatus.CANCEL, order.getOrderStatus());

 

▹취소 후 상품의 재고가 처음 재고 개수인 100개와 동일하다면 테스트가 통과한다.

assertEquals(100, item.getStockNumber());

테스트 실행 결과 정상적으로 작동한다.

 

 

2-3. 자바스크립트 함수 만들기

구현한 주문 취소 기능을 호출하는 자바스크립트 함수를 만들어보자. 

templates > order > orderHist.html

 

▹취소할 주문 번호를 파라미터로 넘겨준다.

var paramData = {

 

▹주문이 정상적으로 취소됐으면 현재 페이지로 다시 redirect 한다.

success  : function(result, status){

주문취소 기능 구현이 완료됐다. 버튼을 클릭하면 "취소 완료"라는 텍스트 메시지가 뜬다.

 

Comments