그라찌에 10 -> 장바구니 생성

류희수·2024년 9월 4일

이제 쿠폰은 발급까지 완료되었고 같이하는 분이 order에서 한번 맞춰본다고 해서
그 동안 장바구니를 한번 해보려고 한다.

장바구니 -> order -> payment 순이니까 한번 가보도록 하겠다.

order는 다른 분이 만들어둬서 일단 order 코드를 먼저 뜯어봤다!

  @Transactional
    public OrderGetResponseDTO createOrder(OrderCreateDTO orderCreateDTO, List<OrderItemsCreateDTO> orderItemsCreateDTOS) {
        int total = 0;

        Store store = storeRepository.findById(orderCreateDTO.getStore_id())
                .orElseThrow(() -> new EntityNotFoundException("매장을 찾을 수 없습니다."));

        Order order = new Order();

        List<Long> productIds = orderItemsCreateDTOS.stream()
                .map(OrderItemsCreateDTO::getProduct_id)
                .toList();

        List<Product> products = productRepository.findByProductIdIn(productIds);
        Map<Long, Product> productMap = products.stream()
                .collect(Collectors.toMap(Product::getProductId, product -> product));

        List<StoreProduct> storeProducts = storeProductRepository.findByStoreAndProductIn(store, productIds);
        Map<Long, StoreProduct> storeProductMap = storeProducts.stream()
                .collect(Collectors.toMap(sp -> sp.getProduct().getProductId(), sp -> sp));

        List<OrderItems> orderItemsList = new ArrayList<>();

        for (OrderItemsCreateDTO orderItemsCreateDTO : orderItemsCreateDTOS) {
            Product product = productMap.get(orderItemsCreateDTO.getProduct_id());
            if (product == null) {
                throw new EntityNotFoundException("상품을 찾을 수 없습니다.");
            }

            StoreProduct storeProduct = storeProductMap.get(product.getProductId());
            if (storeProduct == null) {
                throw new IllegalArgumentException("매장에서 판매하지 않는 상품입니다.");
            }

            if (!storeProduct.getState()) {
                throw new IllegalArgumentException("판매 중지 된 상품입니다.");
            }

            int price = orderItemsCreateDTO.getQuantity() * orderItemsCreateDTO.getProduct_price();

            OrderItems orderItems = new OrderItems();
            orderItems.setProduct(product);
            orderItems.setQuantity(orderItemsCreateDTO.getQuantity());
            orderItems.setProduct_price(orderItemsCreateDTO.getProduct_price());
            orderItems.setTotal_price(price);
            orderItems.setOrder(order);

            total += price;
            orderItemsList.add(orderItems);
        }

        // Order 생성 및 저장

        order.setStore(store);
        order.setId(orderCreateDTO.getUser_id());
        order.setCoupon_id(orderCreateDTO.getCoupon_id());
        order.setOrderItems(orderItemsList);
        order.setTotal_price(total);
        order.setDiscount_price(1000); // 추후 수정 필요
        order.setFinal_price(total - 1000);
        order.setOrder_date(orderCreateDTO.getOrder_date());
        order.setOrder_mode(orderCreateDTO.getOrder_mode());
        order.setAccept("대기");
        order.setRequirement(orderCreateDTO.getRequirement());

        // Order와 OrderItems를 함께 저장
        try {
            orderRepository.save(order);
            orderItemsRepository.saveAll(orderItemsList);
        } catch (DataIntegrityViolationException e) {
            throw new RuntimeException("데이터 무결성 위반 오류 발생 ", e);
        }

        OrderGetDTO orderGetDTO = OrderToOrderGetDTO(order);
        List<OrderItemsGetDTO> orderItemsGetDTO = OrderItemsToOrderItemsGetDTO(orderItemsList);

        return OrderGetResponseDTO
                .builder()
                .orderGetDTO(orderGetDTO)
                .orderItemsGetDTOs(orderItemsGetDTO)
                .build();
    }

코드를 보면 주문(Order)을 생성하고 관련된 주문 상품(OrderItems)을 처리하는식으로 로직이 되어 있다.

내가 생각한 로직은 CartCartItem 을 생성하고
사용자별로 장바구니를 관리할 수 있게 한다.
Cart- > 사용자 | cartItem -> 장바구니에 담긴 개별 상품

일단 장바구니에 상품을 추가한다 (crd) 생성 후에 장바구니에서 주문을 누르면 장바구니 (ItemCart) -> Order로 변환해서 저장하는 로직으로 진행 해보려고 한다.

Entity 생성

@Getter
@Setter
@Entity
public class Cart {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToOne
    private User user;

    @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<CartItem> cartItems = new ArrayList<>();
    
}
@Getter
@Setter
@Entity
public class CartItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    private Cart cart;

    @ManyToOne
    private Product product;

    private int quantity;

    private int product_price;

}

하나의 상품은 여러 장바구니 항목에 포함될 수 있음
-> ManyToOne

한 장바구니에는 여러 장바구니 항목이 포함될 수 있습니다
-> ManyToOne

한 사용자는 하나의 장바구니를 가진다 -> OneToOne

한 장바구니는 여러 장바구니 항목을 가질 수 있음
-> OneToMany

cartItem은 말 그대로 장바구니에 담기는 item이므로 최종가격을 위한 price를 따로 생성하였다.
product에서 가격을 가져와 * quantity를 해 최종가격을 넣을 것이다!


CartService

@Service
@RequiredArgsConstructor
public class CartService {
   private final CartRepository cartRepository;
   private final CartItemRepository cartItemRepository;
   private final ProductRepository productRepository;
   private final UserRepository userRepository;

   @Transactional
   public void addProductToCart(Long userId, Long productId, int quantity) {
       User user = findUser(userId);
       Cart cart = findCartByUser(user);
       Product product = findProductById(productId);

       CartItem cartItem = createCartItem(cart, product, quantity);

       cart.getCartItems().add(cartItem);
       cartItemRepository.save(cartItem);
       cartRepository.save(cart);
   }

   @Transactional(readOnly = true)
   public List<CartItem> readCartItem(Long userId) {
       User user = findUser(userId);
       Cart cart = findCartByUser(user);

       return cart.getCartItems();
   }

   @Transactional
   public void deleteCartItem(Long userId, Long cartItemId) {
       User user = findUser(userId);
       Cart cart = findCartByUser(user);
       CartItem cartItem = findCartItemByItem(cartItemId);

       if (cart.getCartItems().contains(cartItem)) {
           cart.getCartItems().remove(cartItem);
           cartItemRepository.delete(cartItem);
       }
   }

   @Transactional
   public void deleteAllCartItems(Long userId) {
       User user = findUser(userId);
       Cart cart = findCartByUser(user);

       List<CartItem> cartItems = new ArrayList<>(cart.getCartItems());
       cart.getCartItems().clear();

       for (CartItem cartItem : cartItems) {
           cartItemRepository.delete(cartItem);
       }
       cartRepository.save(cart);
   }


   private CartItem createCartItem(Cart cart, Product product, int quantity) {
       CartItem cartItem = new CartItem();
       cartItem.setCart(cart);
       cartItem.setProduct(product);
       cartItem.setQuantity(quantity);
       cartItem.setProduct_price(product.getPrice() * quantity);
       return cartItem;
   }

   private User findUser(Long userId) {
       return  userRepository.findById(userId)
               .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다."));
   }

   private Cart findCartByUser(User user) {
       return cartRepository.findByUser(user)
               .orElseThrow(() -> new EntityNotFoundException("해당 사용자의 장바구니가 없습니다."));
   }

   private Product findProductById(Long productId) {
       return  productRepository.findById(productId)
               .orElseThrow(() -> new EntityNotFoundException("해당 상품을 찾을 수 없습니다."));
   }

   private CartItem findCartItemByItem(Long cartItemId) {
       return cartItemRepository.findById(cartItemId)
               .orElseThrow(() -> new EntityNotFoundException("장바구니 항목을 찾을 수 없습니다."));
   }

}

만드는 김에 메소드를 SRP를 지키려고 요새는 메소드를 최대한 쪼개서 개발하고 있다. 여러개 쓰이면 쪼개는게 맞는데 서비스 로직내에서 한번만 쓰이는 것은 어떻게 처리해야할까??
일단은 다 쪼개고 있다. 멘토님에게 여쭤봐야겠다

장바구니에 담기, 삭제하기, 아예통채로 비우기 등을 개발하였다. 장바구니에는 수정이 필요하지 않아 기능을 만들지 않았다!

추가
생각해보니 장바구니에 담을 떄 카트를 생성하지 않고 있었다
회원마다 장바구니는 기본적으로 있어야 한다 매번 생성하기보다는 카트에 담으려고 할때 없으면 생성하는 것이 메모리측면에서 효율적이라고 생각하였음.


CartServiceTest

public class CartServiceTest {

    @Mock
    private CartRepository cartRepository;

    @Mock
    private CartItemRepository cartItemRepository;

    @Mock
    private ProductRepository productRepository;

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private CartService cartService;

    private User user;
    private Cart cart;
    private Product product;
    private CartItem cartItem;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);

        user = new User();
        user.setId(1L);

        cart = new Cart();
        cart.setId(1L);
        cart.setUser(user);
        cart.setCartItems(new ArrayList<>());

        product = new Product();
        product.setProductId(1L);
        product.setPrice(10000);

        cartItem = new CartItem();
        cartItem.setCart(cart);
        cartItem.setProduct(product);
        cartItem.setQuantity(2);
        cartItem.setProduct_price(20000);

        cart.getCartItems().add(cartItem);
    }

    @Test
    @DisplayName("장바구니에 상품 추가하기")
    void addProductToCart() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        when(cartRepository.findByUser(user)).thenReturn(Optional.of(cart));
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));

        cartService.addProductToCart(1L, 1L, 2);

        verify(cartItemRepository, times(1)).save(any(CartItem.class));
        verify(cartRepository, times(1)).save(cart);
    }

    @Test
    @DisplayName("장바구니 상품 조회하기")
    void readCartItem() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        when(cartRepository.findByUser(user)).thenReturn(Optional.of(cart));

        List<CartItem> items = cartService.readCartItem(1L);

        assertEquals(1, items.size()); // 상품 1개 넣었으니
        assertEquals(cartItem, items.get(0));  // 0번째랑 비교
    }

    @Test
    @DisplayName("특정 상품 삭제하기")
    void deleteCartItem() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        when(cartRepository.findByUser(user)).thenReturn(Optional.of(cart));
        when(cartItemRepository.findById(1L)).thenReturn(Optional.of(cartItem));

        cartService.deleteCartItem(1L, 1L);

        assertTrue(cart.getCartItems().isEmpty());
        verify(cartItemRepository, times(1)).delete(cartItem);
    }

    @Test
    @DisplayName("장바구니 비우기")
    void deleteAllCartItem() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        when(cartRepository.findByUser(user)).thenReturn(Optional.of(cart));

        cartService.deleteAllCartItems(1L);

        assertTrue(cart.getCartItems().isEmpty());
        verify(cartItemRepository, times(1)).delete(cartItem);
        verify(cartRepository, times(1)).save(cart);
    }


    @Test
    @DisplayName("사용자 못 찾았을 때")
    void findUserNotFound() {
        when(userRepository.findById(1L)).thenReturn(Optional.empty());

        Exception exception = assertThrows(EntityNotFoundException.class, () -> {
            cartService.readCartItem(1L);
        });

        assertEquals("사용자를 찾을 수 없습니다.", exception.getMessage());
    }

    @Test
    @DisplayName("장바구니 못 찾았을 때")
    void findCartNotFound() {
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        when(cartRepository.findByUser(user)).thenReturn(Optional.empty());

        Exception exception = assertThrows(EntityNotFoundException.class, () -> {
            cartService.readCartItem(1L);
        });

        assertEquals("해당 사용자의 장바구니가 없습니다.", exception.getMessage());
    }
}

마지막 2개는 이미 앞에서 충분히 검증해서 굳이 만들지 않아도 되지만 메소드를 쪼개기도 하고 테스트에 익숙해질겸 만들었다!

아직 테스트 코드에 익숙하지 않아 여러가지 찾아보고 정리하였다 .
주로 예시와 내가 사용한 간단한 예시로 정리하면 더 기억에 남을 것 같다!

assertEquals
용도: 두 값이 같은지 비교할 때 사용한다 주로 실제 결과와 예상 결과가 일치하는지를 검증할 때 사용된다.
여기서는 assertEquals(1, items.size());
아이템 사이즈와 , 1을 비교하였음)

assertTrue
용도: 주어진 조건식이 참인지 거짓인지 검증할 때 사용된다
여기서는 assertTrue(cart.getCartItems().isEmpty());
장바구니가 비어 있는지 확인할 때 사용하였음!

verify
용도 : Mockito를 사용할 때, 모의 객체의 메서드가 호출되었는지, 호출 횟수는 얼마인지 등을 검증할 때 사용된다.
여기서는 verify(cartItemRepository,times(1)).delete(cartItem); 리포지토리에서 카트 아이템이 지워졌는지 호출횟수는 1번이 맞는 지 확인하는 용도로 사용되었다.!


profile
자바를자바

0개의 댓글