이제 쿠폰은 발급까지 완료되었고 같이하는 분이 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)을 처리하는식으로 로직이 되어 있다.
내가 생각한 로직은Cart와CartItem을 생성하고
사용자별로 장바구니를 관리할 수 있게 한다.
Cart- > 사용자 |cartItem-> 장바구니에 담긴 개별 상품
일단 장바구니에 상품을 추가한다 (crd) 생성 후에 장바구니에서 주문을 누르면 장바구니 (ItemCart) -> Order로 변환해서 저장하는 로직으로 진행 해보려고 한다.
@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를 해 최종가격을 넣을 것이다!
@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를 지키려고 요새는 메소드를 최대한 쪼개서 개발하고 있다. 여러개 쓰이면 쪼개는게 맞는데 서비스 로직내에서 한번만 쓰이는 것은 어떻게 처리해야할까??
일단은 다 쪼개고 있다. 멘토님에게 여쭤봐야겠다
장바구니에 담기, 삭제하기, 아예통채로 비우기 등을 개발하였다. 장바구니에는 수정이 필요하지 않아 기능을 만들지 않았다!
추가
생각해보니 장바구니에 담을 떄 카트를 생성하지 않고 있었다
회원마다 장바구니는 기본적으로 있어야 한다 매번 생성하기보다는 카트에 담으려고 할때 없으면 생성하는 것이 메모리측면에서 효율적이라고 생각하였음.
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번이 맞는 지 확인하는 용도로 사용되었다.!