diff --git a/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java new file mode 100644 index 000000000..c0e4d5da9 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java @@ -0,0 +1,18 @@ +package com.loopers.application.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class BrandFacade { + private final BrandService brandService; + + public BrandInfo getBrandDetail(long id) { + Brand brand = brandService.getBrand(id); + return BrandInfo.from(brand); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java new file mode 100644 index 000000000..5a2ae251c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java @@ -0,0 +1,16 @@ +package com.loopers.application.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +public record BrandInfo(Long id, String name, String story) { + public static BrandInfo from(Brand model) { + if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "유저정보를 찾을수 없습니다."); + return new BrandInfo( + model.getId(), + model.getName(), + model.getStory() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java new file mode 100644 index 000000000..1685623a2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java @@ -0,0 +1,10 @@ +package com.loopers.application.like; + +public record LikeInfo(long likeCount, boolean isLiked) { + public static LikeInfo from(long likeCount, boolean isLiked) { + return new LikeInfo( + likeCount, + isLiked + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java new file mode 100644 index 000000000..61aa9f7e3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java @@ -0,0 +1,22 @@ +package com.loopers.application.order; + +import com.loopers.interfaces.api.order.OrderCreateV1Dto; + +import java.util.Map; +import java.util.stream.Collectors; + +public record CreateOrderCommand(Long userId, Map orderItemInfo) { + public record ItemCommand(Long productId, Map quantity) { + } + + public static CreateOrderCommand from(Long userId, OrderCreateV1Dto.OrderRequest request) { + return new CreateOrderCommand( + userId, + request.items().stream() + .collect(Collectors.toMap( + OrderCreateV1Dto.OrderItemRequest::productId, + OrderCreateV1Dto.OrderItemRequest::quantity, + Long::sum + ))); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java new file mode 100644 index 000000000..b6e1cd950 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -0,0 +1,60 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.CreateOrderService; +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderService; +import com.loopers.domain.point.PointService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.domain.product.ProductStockService; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserPointService; +import com.loopers.domain.user.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class OrderFacade { + private final UserService userService; + private final PointService pointService; + private final ProductService productService; + private final OrderService orderService; + private final CreateOrderService createOrderService; + + private final ProductStockService productStockService; + private final UserPointService userPointService; + + @Transactional(readOnly = true) + public Page getOrderList(Long userId, + String sortType, + int page, + int size) { + return orderService.getOrders(userId, sortType, page, size); + } + + @Transactional(readOnly = true) + public OrderInfo getOrderDetail(Long orderId) { + Order order = orderService.getOrder(orderId); + return OrderInfo.from(order); + } + + @Transactional + public OrderInfo createOrder(CreateOrderCommand command) { + + Map quantityMap = command.orderItemInfo(); + User user = userService.getActiveUser(command.userId()); + List productList = productService.getExistingProducts(quantityMap.keySet()); + + productStockService.deduct(productList, quantityMap); + userPointService.use(user, productList, quantityMap); + + Order savedOrder = createOrderService.save(user, productList, quantityMap); + return OrderInfo.from(savedOrder); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java new file mode 100644 index 000000000..95d20d92b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java @@ -0,0 +1,23 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +public record OrderInfo(long id, String status, BigDecimal totalPrice + , ZonedDateTime orderAt, List orderItemInfo) { + public static OrderInfo from(Order model) { + if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "주문정보를 찾을수 없습니다."); + return new OrderInfo( + model.getId(), + model.getStatus().name(), + model.getTotalPrice(), + model.getOrderAt(), + OrderItemInfo.from(model.getOrderItems()) + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java new file mode 100644 index 000000000..92a66aa2f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java @@ -0,0 +1,24 @@ +package com.loopers.application.order; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.loopers.domain.order.OrderItem; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.math.BigDecimal; +import java.util.List; + +public record OrderItemInfo(Long id, long quantity + , @JsonSerialize(using = ToStringSerializer.class) BigDecimal unitPrice, + @JsonSerialize(using = ToStringSerializer.class) BigDecimal totalPrice) { + public static List from(List model) { + if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "주문상세정보를 찾을수 없습니다."); + return model.stream().map(item -> new OrderItemInfo( + item.getId(), + item.getQuantity(), + item.getUnitPrice(), + item.getTotalPrice() + )).toList(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java index 92b3efd79..8a85275ff 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java @@ -1,10 +1,8 @@ package com.loopers.application.point; import com.loopers.domain.point.PointService; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -16,14 +14,13 @@ public class PointFacade { private final UserService userService; private final PointService pointService; - public BigDecimal getPoint(String userId) { - UserModel user = userService.getUser(userId); - return pointService.getAmount(user.getUserId()); + public BigDecimal getPoint(Long userId) { + User user = userService.getActiveUser(userId); + return user.getPoint().getAmount(); } - public BigDecimal charge(String userId, BigDecimal chargeAmt) { - UserModel user = userService.getUser(userId); - if (user == null) throw new CoreException(ErrorType.NOT_FOUND, "유저정보를 찾을수 없습니다."); + public BigDecimal charge(Long userId, BigDecimal chargeAmt) { + User user = userService.getActiveUser(userId); return pointService.charge(user, chargeAmt); } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java index ff51218d9..ca76d1644 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java @@ -1,11 +1,11 @@ package com.loopers.application.point; -import com.loopers.domain.point.PointModel; +import com.loopers.domain.point.Point; import java.math.BigDecimal; public record PointInfo(String userId, BigDecimal amount) { - public static PointInfo from(PointModel model) { + public static PointInfo from(Point model) { if (model == null) return null; return new PointInfo( model.getUser().getUserId(), diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java new file mode 100644 index 000000000..fadaa5cc8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java @@ -0,0 +1,24 @@ +package com.loopers.application.product; + +import com.loopers.application.brand.BrandInfo; +import com.loopers.application.like.LikeInfo; +import com.loopers.domain.product.Product; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.math.BigDecimal; + +public record ProductDetailInfo(Long id, String name, BigDecimal price, long stock + , BrandInfo brandInfo, LikeInfo likeInfo) { + public static ProductDetailInfo from(Product model, boolean isLiked) { + if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "상품정보를 찾을수 없습니다."); + return new ProductDetailInfo( + model.getId(), + model.getName(), + model.getPrice(), + model.getStock(), + BrandInfo.from(model.getBrand()), + LikeInfo.from(model.getLikeCount(), isLiked) + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java new file mode 100644 index 000000000..2a9a97eff --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -0,0 +1,32 @@ +package com.loopers.application.product; + +import com.loopers.domain.like.LikeService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Component +public class ProductFacade { + private final ProductService productService; + private final LikeService likeService; + + @Transactional(readOnly = true) + public Page getProductList(Long brandId, + String sortType, + int page, + int size) { + return productService.getProducts(brandId, sortType, page, size); + } + + @Transactional(readOnly = true) + public ProductDetailInfo getProductDetail(long userId, long productId) { + Product product = productService.getProduct(productId); + boolean isLiked = likeService.isLiked(userId, productId); + return ProductDetailInfo.from(product, isLiked); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index f4e3e79c3..9e7703061 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -1,6 +1,6 @@ package com.loopers.application.user; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; import com.loopers.interfaces.api.user.UserCreateV1Dto; import lombok.RequiredArgsConstructor; @@ -12,12 +12,12 @@ public class UserFacade { private final UserService userService; public UserInfo join(UserCreateV1Dto.UserRequest requestDto) { - UserModel user = userService.join(UserModel.create(requestDto.userId(), requestDto.email(), requestDto.birthday(), requestDto.gender())); + User user = userService.join(User.create(requestDto.userId(), requestDto.email(), requestDto.birthday(), requestDto.gender())); return UserInfo.from(user); } public UserInfo getUser(String userId) { - UserModel user = userService.getUser(userId); + User user = userService.getUser(userId); return UserInfo.from(user); } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java index df777bb86..16b11be7d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -1,11 +1,11 @@ package com.loopers.application.user; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; public record UserInfo(String userId, String email, String birthday, String gender) { - public static UserInfo from(UserModel model) { + public static UserInfo from(User model) { if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "유저정보를 찾을수 없습니다."); return new UserInfo( model.getUserId(), diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java new file mode 100644 index 000000000..39067ec5a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java @@ -0,0 +1,32 @@ +package com.loopers.domain.brand; + +import com.loopers.domain.BaseEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "brand") +@Getter +public class Brand extends BaseEntity { + private String name; + private String story; + + protected Brand() { + this.name = ""; + this.story = ""; + } + + private Brand(String name, String story) { + this.name = name; + this.story = story; + } + + public static Brand create(String name, String story) { + if (name.isBlank()) { + throw new IllegalArgumentException("브랜드명은 비어있을 수 없습니다."); + } + return new Brand(name, story); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java new file mode 100644 index 000000000..ceb5791f4 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java @@ -0,0 +1,10 @@ +package com.loopers.domain.brand; + +import java.util.Optional; + +public interface BrandRepository { + Optional findById(Long id); + + Brand save(Brand brand); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java new file mode 100644 index 000000000..0e05dead6 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java @@ -0,0 +1,27 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Component +public class BrandService { + + private final BrandRepository brandRepository; + + @Transactional(readOnly = true) + public Brand getBrand(Long id) { + if (id == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + return brandRepository.findById(id).orElse(null); + } + + public Brand save(Brand brand) { + return brandRepository.save(brand); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java new file mode 100644 index 000000000..7015ee53d --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java @@ -0,0 +1,43 @@ +package com.loopers.domain.like; + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.product.Product; +import com.loopers.domain.user.User; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +@Entity +@Table(name = "likes", uniqueConstraints = { + @UniqueConstraint(columnNames = {"ref_user_id", "ref_product_id"}) +}) +@Getter +public class Like extends BaseEntity { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ref_user_Id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ref_product_Id", nullable = false) + private Product product; + + protected Like() { + } + + private Like(User user, Product product) { + this.user = user; + this.product = product; + } + + public static Like create(User user, Product product) { + if (user == null || user.getId() == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "사용자ID는 비어있을 수 없습니다."); + } + if (product == null || product.getId() == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품ID는 비어있을 수 없습니다."); + } + return new Like(user, product); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java new file mode 100644 index 000000000..b0a87fb3a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java @@ -0,0 +1,21 @@ +package com.loopers.domain.like; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface LikeRepository { + + Optional findById(Long userId, Long productid); + + Like save(Like like); + + long remove(Long userId, Long productId); + + boolean isLiked(Long userId, Long productId); + + long getLikeCount(Long productId); + + Page getLikedProducts(Long userId, Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java new file mode 100644 index 000000000..01610a251 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java @@ -0,0 +1,74 @@ + +package com.loopers.domain.like; + +import com.loopers.domain.product.Product; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class LikeService { + + private final LikeRepository likeRepository; + + @Transactional + public Like save(Like like) { + Long userId = like.getUser().getId(); + Long productId = like.getProduct().getId(); + Optional existingLike = likeRepository.findById(userId, productId); + + if (existingLike.isPresent()) { + return existingLike.get(); + } + return likeRepository.save(like); + } + + + @Transactional + public long remove(Long userId, Long productId) { + return likeRepository.remove(userId, productId); + } + + @Transactional + public boolean isLiked(Long userId, Long productId) { + return likeRepository.isLiked(userId, productId); + } + + public Page getLikedProducts( + Long userId, + String sortType, + int page, + int size + ) { + if (userId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + Sort sort = this.getSortBySortType(sortType); + Pageable pageable = PageRequest.of(page, size, sort); + Page likePage = likeRepository.getLikedProducts(userId, pageable); + List likedProducts = likePage.getContent().stream().map(Like::getProduct).toList(); + return new PageImpl<>(likedProducts, pageable, likePage.getTotalElements()); + } + + private Sort getSortBySortType(String sortType) { + if (sortType == null) sortType = "latest"; + Sort latestSort = Sort.by("createdAt").descending(); + switch (sortType.toLowerCase()) { + case "latest": + return latestSort; + case "price_asc": + return Sort.by("price").ascending().and(latestSort); + case "likes_desc": + return Sort.by("likesCount").descending().and(latestSort); + default: + return latestSort; + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java new file mode 100644 index 000000000..4780d71b3 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java @@ -0,0 +1,41 @@ + +package com.loopers.domain.order; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import com.loopers.domain.user.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class CreateOrderService { + private final ProductRepository productRepository; + private final OrderRepository orderRepository; + + public Order save(User user, List productList, Map quantityMap) { + + List orderItems = new ArrayList<>(); + + BigDecimal totalPrice = productList.stream().map(product -> { + long quantity = quantityMap.get(product.getId()); + BigDecimal unitPrice = product.getPrice(); + BigDecimal itemTotalPrice = unitPrice.multiply(new BigDecimal(quantity)); + + OrderItem orderItem = OrderItem.create(product.getId(), quantity, unitPrice); + orderItems.add(orderItem); + + return itemTotalPrice; + }).reduce(BigDecimal.ZERO, BigDecimal::add); + + Order order = Order.create(user.getId(), OrderStatus.PAID, totalPrice, orderItems); + return orderRepository.save(order); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java new file mode 100644 index 000000000..157f231b7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java @@ -0,0 +1,78 @@ +package com.loopers.domain.order; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "orders") +@Getter +public class Order extends BaseEntity { + + private Long refUserId; + + private OrderStatus status; + + BigDecimal totalPrice; + + ZonedDateTime orderAt; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "ref_order_id") + private List orderItems = new ArrayList<>(); + + public void setOrderItems(List orderItems) { + if (orderItems == null || orderItems.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 상세내역이 없습니다."); + } + this.orderItems = orderItems; + } + + protected Order() { + } + + private Order(long refUserId, OrderStatus status, BigDecimal totalPrice, List orderItems) { + this.refUserId = refUserId; + this.status = status; + this.totalPrice = totalPrice; + this.orderAt = ZonedDateTime.now(); + this.orderItems = orderItems; + } + + public static Order create(long refUserId, OrderStatus status, BigDecimal totalPrice, List orderItems) { + if (status == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "상태 정보는 비어있을 수 없습니다."); + } + if (totalPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "총가격은 음수일수 없습니다."); + } + return new Order(refUserId, status, totalPrice, orderItems); + } + + public void cancel() { + if (status == OrderStatus.CANCELLED) { + throw new CoreException(ErrorType.BAD_REQUEST, "이미 취소된 주문입니다."); + } + this.status = OrderStatus.CANCELLED; + } + + public void preparing() { + if (status == OrderStatus.PENDING) { + throw new CoreException(ErrorType.BAD_REQUEST, "결재대기중 주문입니다."); + } + if (status == OrderStatus.PREPARING) { + throw new CoreException(ErrorType.BAD_REQUEST, "이미 준비중인 주문입니다."); + } + if (status.compare(OrderStatus.PREPARING) > 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품준비 완료된 주문입니다."); + } + this.status = OrderStatus.PREPARING; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java new file mode 100644 index 000000000..17c324510 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java @@ -0,0 +1,42 @@ +package com.loopers.domain.order; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "order_item") +@Getter +public class OrderItem extends BaseEntity { + + private long refProductId; + + private long quantity; + + private BigDecimal unitPrice; + + private BigDecimal totalPrice; + + protected OrderItem() { + } + + private OrderItem(long refProductId, long quantity, BigDecimal unitPrice, BigDecimal totalPrice) { + this.refProductId = refProductId; + this.quantity = quantity; + this.unitPrice = unitPrice; + this.totalPrice = totalPrice; + } + + public static OrderItem create(long refProductId, long quantity, BigDecimal unitPrice) { + if (unitPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "가격은 음수일수 없습니다."); + } + BigDecimal totalPrice = unitPrice.multiply(new BigDecimal(quantity)); + return new OrderItem(refProductId, quantity, unitPrice, totalPrice); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparer.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparer.java new file mode 100644 index 000000000..6a4a29d5a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparer.java @@ -0,0 +1,35 @@ + +package com.loopers.domain.order; + +import com.loopers.domain.point.Point; +import com.loopers.domain.product.Product; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class OrderPreparer { + public static List verifyProductStockDeductable(List products, Map quantityMap) { + products.forEach(i -> i.deductStock(quantityMap.get(i.getId()))); + return products; + } + public static List verifyProductStockAddable(List products, Map quantityMap) { + products.forEach(i -> i.addStock(quantityMap.get(i.getId()))); + return products; + } + + public static BigDecimal getTotalAmt(Point point, List products, Map quantityMap) { + return products.stream() + .map(product -> { + Long productId = product.getId(); + Long quantity = quantityMap.getOrDefault(productId, 0L); + return product.getPrice().multiply(BigDecimal.valueOf(quantity)); + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java new file mode 100644 index 000000000..4ba06bc61 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java @@ -0,0 +1,15 @@ +package com.loopers.domain.order; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface OrderRepository { + Optional findById(Long id); + + Page findByUserId(Long userId, Pageable pageable); + + Order save(Order order); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java new file mode 100644 index 000000000..e02487b7b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java @@ -0,0 +1,66 @@ + +package com.loopers.domain.order; + +import com.loopers.application.order.CreateOrderCommand; +import com.loopers.domain.product.ProductRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +@RequiredArgsConstructor +@Component +public class OrderService { + private final ProductRepository productRepository; + private final OrderRepository orderRepository; + + public Page getOrders( + Long userId, + String sortType, + int page, + int size + ) { + Sort sort = this.getSortBySortType(sortType); + Pageable pageable = PageRequest.of(page, size, sort); + Page orders = null; + if (userId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "유저 정보가 없습니다."); + } + orders = orderRepository.findByUserId(userId, pageable); + return orders; + } + + @Transactional(readOnly = true) + public Order getOrder(Long id) { + if (id == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + return orderRepository.findById(id).orElse(null); + } + + public Order save(Order order) { + return orderRepository.save(order); + } + + private Sort getSortBySortType(String sortType) { + if (sortType == null) sortType = "latest"; + Sort latestSort = Sort.by("createdAt").descending(); + switch (sortType.toLowerCase()) { + case "latest": + return latestSort; + case "price": + return Sort.by("status").descending(); + default: + return latestSort; + } + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java new file mode 100644 index 000000000..8dd95ef7e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java @@ -0,0 +1,27 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum OrderStatus { + PENDING("결제 대기", 10), + PAID("결제 완료", 20), + PREPARING("상품 준비 중", 30), + SHIPPED("배송 중", 40), + DELIVERED("배송 완료", 50), + CANCELLED("주문 취소", 90), + REFUNDED("환불 완료", 100); + + private final String description; + @Getter(AccessLevel.NONE) + private final int sequence; + + public Integer compare(OrderStatus other) { + return Integer.compare(this.sequence, other.sequence); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java similarity index 67% rename from apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java rename to apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index db3321685..84ecc6696 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -1,7 +1,7 @@ package com.loopers.domain.point; import com.loopers.domain.BaseEntity; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.*; @@ -13,33 +13,33 @@ @Entity @Table(name = "point") @Getter -public class PointModel extends BaseEntity { +public class Point extends BaseEntity { private BigDecimal amount; @OneToOne - @JoinColumn(name = "userId", unique = true, nullable = false) - private UserModel user; + @JoinColumn(name = "ref_user_Id", unique = true, nullable = false) + private User user; - public void setUser(UserModel user) { + public void setUser(User user) { this.user = user; if (user.getPoint() != this) { user.setPoint(this); } } - protected PointModel() { + protected Point() { this.amount = BigDecimal.ZERO; } - private PointModel(UserModel user, BigDecimal amount) { - Objects.requireNonNull(user, "UserModel must not be null for PointModel creation."); + private Point(User user, BigDecimal amount) { + Objects.requireNonNull(user, "유저 정보가 없습니다."); this.setUser(user); this.amount = amount; } - public static PointModel create(UserModel user, BigDecimal amount) { - return new PointModel(user, amount); + public static Point create(User user, BigDecimal amount) { + return new Point(user, amount); } public void charge(BigDecimal amountToChange) { @@ -54,7 +54,7 @@ public void use(BigDecimal amountToChange) { throw new CoreException(ErrorType.BAD_REQUEST, "차감 금액은 0보다 커야 합니다."); } if (this.amount.compareTo(amountToChange) < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "잔액이 부족합니다."); + throw new CoreException(ErrorType.INSUFFICIENT_POINT, "포인트가 부족합니다."); } this.amount = this.amount.subtract(amountToChange); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java index 35d2dc95c..0524bc7ac 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java @@ -6,12 +6,13 @@ import java.util.Optional; public interface PointRepository { + Optional findByUserId(Long userId); - Optional findByUserId(String userId); + Optional findByUserId(String userId); @Lock(LockModeType.PESSIMISTIC_WRITE) - Optional findByUserIdForUpdate(String userId); + Optional findByUserIdForUpdate(Long userId); - PointModel save(PointModel point); + Point save(Point point); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index 9554dcac0..9482ade25 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -1,7 +1,7 @@ package com.loopers.domain.point; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -18,20 +18,37 @@ public class PointService { private final PointRepository pointRepository; @Transactional(readOnly = true) - public BigDecimal getAmount(String userId) { + public Point getAvailablePoints(Long userId) { return pointRepository.findByUserId(userId) - .map(PointModel::getAmount) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "포인트 정보를 찾을 수 없습니다.")); + } + + @Transactional(readOnly = true) + public BigDecimal getAmount(Long userId) { + return pointRepository.findByUserId(userId) + .map(Point::getAmount) .orElse(null); } @Transactional - public BigDecimal charge(UserModel user, BigDecimal chargeAmt) { - Optional pointOpt = pointRepository.findByUserIdForUpdate(user.getUserId()); - if (pointOpt.isEmpty()) { + public BigDecimal charge(User user, BigDecimal chargeAmt) { + Optional pointOpt = pointRepository.findByUserIdForUpdate(user.getId()); + if (!pointOpt.isPresent()) { throw new CoreException(ErrorType.NOT_FOUND, "현재 포인트 정보를 찾을수 없습니다."); } pointOpt.get().charge(chargeAmt); pointRepository.save(pointOpt.get()); - return getAmount(user.getUserId()); + return getAmount(user.getId()); + } + + @Transactional + public BigDecimal use(User user, BigDecimal useAmt) { + Optional pointOpt = pointRepository.findByUserIdForUpdate(user.getId()); + if (!pointOpt.isPresent()) { + throw new CoreException(ErrorType.NOT_FOUND, "현재 포인트 정보를 찾을수 없습니다."); + } + pointOpt.get().use(useAmt); + pointRepository.save(pointOpt.get()); + return getAmount(user.getId()); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java new file mode 100644 index 000000000..0a536113f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java @@ -0,0 +1,73 @@ +package com.loopers.domain.product; + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.brand.Brand; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "product") +@Getter +public class Product extends BaseEntity { + private String name; + + @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 설정 (성능상 권장) + @JoinColumn(name = "ref_brand_Id") + private Brand brand; + + private BigDecimal price; + private long stock; + + @Transient + private long likeCount; + + public void setBrand(Brand brand) { + this.brand = brand; + } + + public void setLikeCount(long likeCount) { + this.likeCount = likeCount; + } + + protected Product() { + this.stock = 0; + } + + private Product(Brand brand, String name, BigDecimal price, long stock) { + this.setBrand(brand); + this.name = name; + this.price = price; + this.stock = stock; + } + + public static Product create(Brand brand, String name, BigDecimal price, long stock) { + if (price.compareTo(BigDecimal.ZERO) < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "가격은 음수 일수 없습니다."); + } + if (stock < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0보다 커야 합니다."); + } + return new Product(brand, name, price, stock); + } + + public void deductStock(long quantity) { + if (quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "수량은 0보다 커야 합니다."); + } + if (this.stock < quantity) { + throw new CoreException(ErrorType.INSUFFICIENT_STOCK, "재고가 부족합니다."); + } + this.stock -= quantity; + } + + public void addStock(long quantity) { + if (quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "수량은 0보다 커야 합니다."); + } + this.stock += quantity; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java new file mode 100644 index 000000000..ae1031751 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java @@ -0,0 +1,23 @@ +package com.loopers.domain.product; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface ProductRepository { + Optional findById(Long id); + + List findAllById(Set id); + + Page findByBrandId(Long brandId, Pageable pageable); + + Page findAll(Pageable pageable); + + Product save(Product product); + + List save(List products); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java new file mode 100644 index 000000000..15a98b75c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -0,0 +1,100 @@ + +package com.loopers.domain.product; + +import com.loopers.domain.like.LikeRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class ProductService { + + private final ProductRepository productRepository; + private final LikeRepository likeRepository; + + @Transactional(readOnly = true) + public Page getProducts( + Long brandId, + String sortType, + int page, + int size + ) { + Sort sort = this.getSortBySortType(sortType); + Pageable pageable = PageRequest.of(page, size, sort); + Page products = null; + if (brandId != null) { + products = productRepository.findByBrandId(brandId, pageable); + } else { + products = productRepository.findAll(pageable); + } + return products; + } + + @Transactional(readOnly = true) + public Product getProduct(Long id) { + if (id == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + Optional product = productRepository.findById(id); + return product.orElse(null); + } + + @Transactional(readOnly = true) + public List getExistingProducts(Set productIds) { + + if (productIds == null || productIds.isEmpty()) { + return Collections.emptyList(); + } + Set uniqueIds = new HashSet<>(productIds); + List products = productRepository.findAllById(productIds); + + if (products.size() != uniqueIds.size()) { + Set foundIds = products.stream() + .map(Product::getId) + .collect(Collectors.toSet()); + + uniqueIds.removeAll(foundIds); + throw new CoreException( + ErrorType.NOT_FOUND, + "다음 상품 ID들은 찾을 수 없습니다: " + uniqueIds.toString() + ); + } + return products; + } + + @Transactional + public Product save(Product product) { + return productRepository.save(product); + } + + @Transactional + public List save(List product) { + return productRepository.save(product); + } + + private Sort getSortBySortType(String sortType) { + if (sortType == null) sortType = "latest"; + Sort latestSort = Sort.by("createdAt").descending(); + switch (sortType.toLowerCase()) { + case "latest": + return latestSort; + case "price_asc": + return Sort.by("price").ascending().and(latestSort); + case "likes_desc": + return Sort.by("likesCount").descending().and(latestSort); + default: + return latestSort; + } + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java new file mode 100644 index 000000000..800226bbc --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java @@ -0,0 +1,27 @@ + +package com.loopers.domain.product; + +import com.loopers.domain.order.OrderPreparer; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class ProductStockService { + private final ProductRepository productService; + + public void deduct(List products, Map quantityMap) { + List deductedProducts = OrderPreparer.verifyProductStockDeductable(products, quantityMap); + productService.save(deductedProducts); + } + + public void add(List products, Map quantityMap) { + List addedProducts = OrderPreparer.verifyProductStockAddable(products, quantityMap); + productService.save(addedProducts); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java similarity index 78% rename from apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java rename to apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 500eae5ef..61c8167c0 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -1,7 +1,7 @@ package com.loopers.domain.user; import com.loopers.domain.BaseEntity; -import com.loopers.domain.point.PointModel; +import com.loopers.domain.point.Point; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.*; @@ -15,26 +15,26 @@ @Entity @Table(name = "user") @Getter -public class UserModel extends BaseEntity { +public class User extends BaseEntity { private String userId; private String email; private LocalDate birthday; private String gender; @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false) - private PointModel point; + private Point point; - public void setPoint(PointModel point) { + public void setPoint(Point point) { this.point = point; if (point.getUser() != this) { point.setUser(this); } } - protected UserModel() { + protected User() { } - private UserModel(String userId, String email, String birthday, String gender) { + private User(String userId, String email, String birthday, String gender) { if (!Pattern.compile("^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{1,10}$").matcher(userId).matches()) { throw new CoreException( @@ -67,11 +67,11 @@ private UserModel(String userId, String email, String birthday, String gender) { this.email = email; this.birthday = LocalDate.parse(birthday); this.gender = gender; - this.point = PointModel.create(this, BigDecimal.TEN); + this.point = Point.create(this, BigDecimal.TEN); } - public static UserModel create(String userId, String email, String birthday, String gender) { - return new UserModel(userId, email, birthday, gender); + public static User create(String userId, String email, String birthday, String gender) { + return new User(userId, email, birthday, gender); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java new file mode 100644 index 000000000..c29759ccf --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java @@ -0,0 +1,28 @@ + +package com.loopers.domain.user; + +import com.loopers.domain.order.OrderPreparer; +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; +import com.loopers.domain.point.PointService; +import com.loopers.domain.product.Product; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class UserPointService { + + private final PointService pointService; + + + public void use(User user, List productList, Map quantityMap) { + BigDecimal totalAmt = OrderPreparer.getTotalAmt(user.getPoint(), productList, quantityMap); + pointService.use(user, totalAmt); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java index b7f64a18f..2ac424125 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -3,9 +3,12 @@ import java.util.Optional; public interface UserRepository { - UserModel save(UserModel user); + User save(User user); boolean existsByUserId(String userId); - Optional findByUserId(String userId); + Optional findById(Long id); + + Optional findByUserId(String userId); + } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index dbb459db6..7481cf1cb 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -7,8 +7,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @RequiredArgsConstructor @Component public class UserService { @@ -16,7 +14,7 @@ public class UserService { private final UserRepository userRepository; @Transactional - public UserModel join(UserModel user) { + public User join(User user) { if (userRepository.existsByUserId(user.getUserId())) { throw new CoreException(ErrorType.BAD_REQUEST, "이미 가입된 ID 입니다."); } @@ -24,11 +22,27 @@ public UserModel join(UserModel user) { } @Transactional(readOnly = true) - public UserModel getUser(String userId) { - //Ask: userId도 UserModel 안에 넣어서 왔으면, userId Model에서 검증 가능 - if (userId == null || userId.isBlank()) { - throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + public User getUser(Long userId) { + if (userId == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ID가 없습니다."); + } + return userRepository.findById(userId).orElse(null); + } + + @Transactional(readOnly = true) + public User getUser(String userId) { + if (userId == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ID가 없습니다."); } return userRepository.findByUserId(userId).orElse(null); } + + @Transactional(readOnly = true) + public User getActiveUser(Long userId) { + if (userId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + return userRepository.findById(userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "사용자 정보를 찾을 수 없습니다.")); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java new file mode 100644 index 000000000..5ca589a5a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java @@ -0,0 +1,12 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BrandJpaRepository extends JpaRepository { + Optional findById(Long id); + + Brand save(Brand brand); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java new file mode 100644 index 000000000..60f26343a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java @@ -0,0 +1,24 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class BrandRepositoryImpl implements BrandRepository { + private final BrandJpaRepository jpaRepository; + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + + @Override + public Brand save(Brand brand) { + return jpaRepository.save(brand); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java new file mode 100644 index 000000000..1008e0adb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java @@ -0,0 +1,30 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.Like; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface LikeJpaRepository extends JpaRepository { + Optional findByUserIdAndProductId(Long userId, Long productId); + + Like save(Like like); + + long deleteByUserIdAndProductId(Long userId, Long productId); + + boolean existsByUserIdAndProductId(Long userId, Long productId); + + long countByProductId(Long productId); + + @Query( + value = "SELECT l FROM Like l JOIN FETCH l.product p WHERE l.user.id = :userId", + countQuery = "SELECT COUNT(l) FROM Like l WHERE l.user.id = :userId" + ) + Page getLikedProducts(@Param("userId") Long userId, Pageable pageable); + + //Page getLikedProducts(@Param("userId") Long userId, Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java new file mode 100644 index 000000000..74507560e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java @@ -0,0 +1,49 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.Like; +import com.loopers.domain.like.LikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +@Primary +public class LikeRepositoryImpl implements LikeRepository { + private final LikeJpaRepository jpaRepository; + + @Override + public Optional findById(Long userId, Long productId) { + return jpaRepository.findByUserIdAndProductId(userId, productId); + } + + @Override + public Like save(Like like) { + return jpaRepository.save(like); + } + + @Override + public long remove(Long userId, Long productId) { + return jpaRepository.deleteByUserIdAndProductId(userId, productId); + } + + @Override + public boolean isLiked(Long userId, Long productId) { + return jpaRepository.existsByUserIdAndProductId(userId, productId); + } + + @Override + public long getLikeCount(Long productId) { + return jpaRepository.countByProductId(productId); + } + + @Override + public Page getLikedProducts(Long userId, Pageable pageable) { + return jpaRepository.getLikedProducts(userId, pageable); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java new file mode 100644 index 000000000..2d9c05f07 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java @@ -0,0 +1,15 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface OrderJpaRepository extends JpaRepository { + Optional findById(Long id); + + Page findByRefUserId(Long userId, Pageable pageable); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java new file mode 100644 index 000000000..3e29c2a90 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java @@ -0,0 +1,32 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderRepository; +import com.loopers.domain.product.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class OrderRepositoryImpl implements OrderRepository { + private final OrderJpaRepository jpaRepository; + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + + @Override + public Page findByUserId(Long userId, Pageable pageable) { + return jpaRepository.findByRefUserId(userId, pageable); + } + + @Override + public Order save(Order order) { + return jpaRepository.save(order); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java index 2c436109d..a5a85c432 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java @@ -1,18 +1,21 @@ package com.loopers.infrastructure.point; -import com.loopers.domain.point.PointModel; +import com.loopers.domain.point.Point; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.Optional; -public interface PointJpaRepository extends JpaRepository { +public interface PointJpaRepository extends JpaRepository { boolean existsByUserUserId(String userId); - Optional findByUserUserId(String userId); + Optional findByUserUserId(String userId); + + Optional findByUserId(Long userId); + + @Query("SELECT p FROM Point p WHERE p.user.id = :userId") + Optional findByUserIdForUpdate(@Param("userId") Long userId); - @Query("SELECT p FROM PointModel p WHERE p.user.userId = :userId") - Optional findByUserUserIdForUpdate(@Param("userId") String userId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java index cce08040d..b9ef6bf0e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java @@ -1,6 +1,6 @@ package com.loopers.infrastructure.point; -import com.loopers.domain.point.PointModel; +import com.loopers.domain.point.Point; import com.loopers.domain.point.PointRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -13,18 +13,22 @@ public class PointRepositoryImpl implements PointRepository { private final PointJpaRepository jpaRepository; @Override - public PointModel save(PointModel point) { + public Point save(Point point) { return jpaRepository.save(point); } @Override - public Optional findByUserId(String userId) { - return jpaRepository.findByUserUserId(userId); + public Optional findByUserId(Long userId) { + return jpaRepository.findByUserId(userId); } @Override - public Optional findByUserIdForUpdate(String userId) { - return jpaRepository.findByUserUserIdForUpdate(userId); + public Optional findByUserId(String userId) { + return jpaRepository.findByUserUserId(userId); } + @Override + public Optional findByUserIdForUpdate(Long userId) { + return jpaRepository.findByUserIdForUpdate(userId); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java new file mode 100644 index 000000000..e2119e400 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java @@ -0,0 +1,32 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ProductJpaRepository extends JpaRepository { + Optional findById(Long id); + + List findAllById(Iterable id); + + Page findByBrandId(Long brandId, Pageable pageable); + + List save(List product); + +// @Query(value = """ +// SELECT new com.loopers.api.product.dto.Product( +// p, +// COUNT(l.id) +// ) +// FROM Product p +// LEFT JOIN Like l ON l.product = p +// GROUP BY p +// ORDER BY p.id DESC +// """, +// countQuery = "SELECT COUNT(p.id) FROM Product p") + // Page findAllWithLikeCount(Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java new file mode 100644 index 000000000..584ac2a5c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java @@ -0,0 +1,48 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@RequiredArgsConstructor +@Component +public class ProductRepositoryImpl implements ProductRepository { + private final ProductJpaRepository jpaRepository; + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + + @Override + public List findAllById(Set id) { + return jpaRepository.findAllById(id); + } + + @Override + public Page findByBrandId(Long brandId, Pageable pageable) { + return jpaRepository.findByBrandId(brandId, pageable); + } + + @Override + public Page findAll(Pageable pageable) { + return jpaRepository.findAll(pageable); + } + + @Override + public Product save(Product product) { + return jpaRepository.save(product); + } + + @Override + public List save(List products) { + return jpaRepository.saveAll(products); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java index bd867ae25..015978561 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -1,11 +1,14 @@ package com.loopers.infrastructure.user; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; -public interface UserJpaRepository extends JpaRepository { +public interface UserJpaRepository extends JpaRepository { boolean existsByUserId(String userId); - Optional findByUserId(String userId); + + Optional findById(Long id); + + Optional findByUserId(String id); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java index 3e33c3ff7..e4e85f02d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -1,6 +1,6 @@ package com.loopers.infrastructure.user; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -13,7 +13,7 @@ public class UserRepositoryImpl implements UserRepository { private final UserJpaRepository jpaRepository; @Override - public UserModel save(UserModel user) { + public User save(User user) { return jpaRepository.save(user); } @@ -23,7 +23,12 @@ public boolean existsByUserId(String userId) { } @Override - public Optional findByUserId(String userId) { + public Optional findById(Long userId) { + return jpaRepository.findById(userId); + } + + @Override + public Optional findByUserId(String userId) { return jpaRepository.findByUserId(userId); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java new file mode 100644 index 000000000..0ef09e8b2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java @@ -0,0 +1,35 @@ +package com.loopers.interfaces.api.order; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.loopers.application.order.OrderInfo; +import com.loopers.application.order.OrderItemInfo; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +public class OrderCreateV1Dto { + public record OrderItemRequest(long productId, long quantity) { + } + + public record OrderRequest(List items) { + } + + public record OrderResponse(long id, String status + , @JsonSerialize(using = ToStringSerializer.class) BigDecimal totalPrice + , @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") ZonedDateTime orderAt + , List orderItemInfo) { + public static OrderResponse from(OrderInfo info) { + if (info == null) return null; + return new OrderResponse( + info.id(), + info.status(), + info.totalPrice(), + info.orderAt(), + info.orderItemInfo() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java new file mode 100644 index 000000000..ddaf2bc04 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java @@ -0,0 +1,55 @@ +package com.loopers.interfaces.api.order; + +import com.loopers.domain.order.Order; +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; +import java.util.List; + +@Tag(name = "Order V1 API", description = "Loopers 예시 API 입니다.") +public interface OrderV1ApiSpec { + @Operation( + summary = "주문 목록 조회", + description = "사용자 ID로 주문 목록 조회합니다." + ) + @Valid + ApiResponse> getOrderList( + @Schema(name = "사용자 ID", description = "조회할 사용자의 ID") + @RequestHeader(value = "X-USER-ID", required = false) Long userId, + @RequestParam(name = "sortType", defaultValue = "latest") String sortType, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ); + + @Operation( + summary = "주문 요청", + description = "ID로 주문를 충전합니다." + ) + @Valid + ApiResponse createOrder( + @Schema(name = "사용자 ID", description = "충전할 사용자의 ID") + @RequestHeader(value = "X-USER-ID", required = false) Long userId + , @RequestBody OrderCreateV1Dto.OrderRequest request + ); + + + @Operation( + summary = "주문 상세조회", + description = "주문 ID로 주문 상세조회합니다." + ) + @Valid + ApiResponse getOrderDetail( + @Schema(name = "사용자 ID", description = "조회할 사용자의 ID") + @RequestHeader(value = "X-USER-ID", required = false) Long userId + , Long orderId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java new file mode 100644 index 000000000..b42521578 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java @@ -0,0 +1,51 @@ +package com.loopers.interfaces.api.order; + +import com.loopers.application.order.CreateOrderCommand; +import com.loopers.application.order.OrderFacade; +import com.loopers.application.order.OrderInfo; +import com.loopers.domain.order.Order; +import com.loopers.interfaces.api.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/orders") +public class OrderV1Controller implements OrderV1ApiSpec { + + private final OrderFacade orderFacade; + + @GetMapping("") + @Override + public ApiResponse> getOrderList( + @RequestHeader(value = "X-USER-ID", required = false) Long userId, + @RequestParam(name = "sortType", defaultValue = "latest") String sortType, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Page info = orderFacade.getOrderList(userId, sortType, page, size); + return ApiResponse.success(info); + } + + @PostMapping("") + @Override + public ApiResponse createOrder(@RequestHeader(value = "X-USER-ID", required = false) Long userId + , @RequestBody OrderCreateV1Dto.OrderRequest request + ) { + CreateOrderCommand command = CreateOrderCommand.from(userId, request); + OrderInfo info = orderFacade.createOrder(command); + OrderCreateV1Dto.OrderResponse response = OrderCreateV1Dto.OrderResponse.from(info); + return ApiResponse.success(response); + } + + @GetMapping("/{orderId}") + @Override + public ApiResponse getOrderDetail(@RequestHeader(value = "X-USER-ID", required = false) Long userId + , @PathVariable(value = "orderId") Long orderId + ) { + OrderInfo info = orderFacade.getOrderDetail(orderId); + OrderCreateV1Dto.OrderResponse response = OrderCreateV1Dto.OrderResponse.from(info); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java index 7e86ed59c..47b7660c9 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -19,7 +19,7 @@ public interface PointV1ApiSpec { @Valid ApiResponse getPoint( @Schema(name = "사용자 ID", description = "조회할 사용자의 ID") - @RequestHeader(value = "X-USER-ID", required = false) String userId + @RequestHeader(value = "X-USER-ID", required = false) Long userId ); @Operation( @@ -29,7 +29,7 @@ ApiResponse getPoint( @Valid ApiResponse charge( @Schema(name = "사용자 ID", description = "충전할 사용자의 ID") - @RequestHeader(value = "X-USER-ID", required = false) String userId + @RequestHeader(value = "X-USER-ID", required = false) Long userId , @RequestBody BigDecimal amount ); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java index 211efbe2a..5adeadb43 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -16,14 +16,14 @@ public class PointV1Controller implements PointV1ApiSpec { @GetMapping("") @Override - public ApiResponse getPoint(@RequestHeader(value = "X-USER-ID", required = false) String userId + public ApiResponse getPoint(@RequestHeader(value = "X-USER-ID", required = false) Long userId ) { return ApiResponse.success(pointFacade.getPoint(userId)); } @PostMapping("/charge") @Override - public ApiResponse charge(@RequestHeader(value = "X-USER-ID", required = false) String userId + public ApiResponse charge(@RequestHeader(value = "X-USER-ID", required = false) Long userId , @RequestBody BigDecimal amount ) { return ApiResponse.success(pointFacade.charge(userId, amount)); diff --git a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java index 5d142efbf..8bb2324d6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java +++ b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java @@ -7,13 +7,17 @@ @Getter @RequiredArgsConstructor public enum ErrorType { - /** 범용 에러 */ - INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."), - BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."), - NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."), - CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."); + /** + * 범용 에러 + */ + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."), + BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."), + CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."), - private final HttpStatus status; - private final String code; - private final String message; + INSUFFICIENT_STOCK(HttpStatus.BAD_REQUEST, "INSUFFICIENT_STOCK", "요청하신 상품의 재고가 부족합니다."), + INSUFFICIENT_POINT(HttpStatus.BAD_REQUEST, "INSUFFICIENT_POINT", "사용 가능한 포인트가 부족합니다."); + private final HttpStatus status; + private final String code; + private final String message; } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java new file mode 100644 index 000000000..a2121e2ef --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java @@ -0,0 +1,168 @@ +package com.loopers.application.order; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderItem; +import com.loopers.domain.order.OrderStatus; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.domain.user.User; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.order.OrderJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.interfaces.api.order.OrderCreateV1Dto; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + + +@SpringBootTest +class OrderFacadeIntegrationTest { + @Autowired + private OrderFacade orderFacade; + @MockitoSpyBean + private UserJpaRepository userJpaRepository; + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductService productService; + + @MockitoSpyBean + private OrderJpaRepository orderJpaRepository; + @Autowired + private DatabaseCleanUp databaseCleanUp; + + User savedUser; + List savedProducts; + Order savedOrder; + + @BeforeEach + void setup() { + // arrange + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + savedUser = userJpaRepository.save(user); + List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + List savedBrandList = brandList.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + List productList = List.of(Product.create(savedBrandList.get(0), "Wild Faith Rose Sweatshirt", new BigDecimal(8), 10) + , Product.create(savedBrandList.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(4), 10) + , Product.create(savedBrandList.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + ); + savedProducts = productService.save(productList); + List orderItems = new ArrayList<>(); + orderItems.add(OrderItem.create(productList.get(0).getId(), 2L, new BigDecimal(5_000))); + Order order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); + savedOrder = orderJpaRepository.save(order); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("주문목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_주문목록조회() { + // act + Page ordersPage = orderFacade.getOrderList(savedUser.getId(), "latest", 0, 20); + List orders = ordersPage.getContent(); + // assert + assertThat(orders).isNotEmpty().hasSize(1); + } + } + + @DisplayName("주문을 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 상품 ID를 주면, 해당 상품 정보를 반환한다.") + @Test + void 성공_존재하는_상품ID() { + // arrange + // act + OrderInfo result = orderFacade.getOrderDetail(savedOrder.getId()); + + // assert + assertThat(result.id()).isEqualTo(savedOrder.getId()); + assertThat(result.status()).isEqualTo(savedOrder.getStatus().toString()); + assertThat(result.totalPrice()).isEqualByComparingTo(savedOrder.getTotalPrice()); + } + + @DisplayName("존재하지 않는 상품 ID를 주면, 예외가 반환된다.") + @Test + void 실패_존재하지_않는_상품ID() { + // arrange + // act + // assert + assertThrows(CoreException.class, () -> { + orderFacade.getOrderDetail(0L); + }); + } + } + + @DisplayName("주문을 생성할 때,") + @Nested + class Post { + @DisplayName("10재고가 있는 8원의 상품 1건 주문시, 기본 포인트10으로 결재가 된다.") + @Test + void 성공_단건주문생성() { + List items = new ArrayList<>(); + items.add(new OrderCreateV1Dto.OrderItemRequest(savedProducts.get(0).getId(), 1)); + OrderCreateV1Dto.OrderRequest request = new OrderCreateV1Dto.OrderRequest(items); + CreateOrderCommand orderCommand = CreateOrderCommand.from(savedUser.getId(), request); + // act + OrderInfo savedOrder = orderFacade.createOrder(orderCommand); + // assert + assertThat(savedOrder).isNotNull(); + assertThat(savedOrder.totalPrice()).isEqualByComparingTo(savedProducts.get(0).getPrice()); + assertThat(savedOrder.orderItemInfo()).hasSize(1); + } + + @DisplayName("10재고가 있는 8원의 상품 20건 주문시, 재고 없음 오류가 발생한다.") + @Test + void 실패_재고없음오류() { + List items = new ArrayList<>(); + items.add(new OrderCreateV1Dto.OrderItemRequest(savedProducts.get(0).getId(), 20)); + OrderCreateV1Dto.OrderRequest request = new OrderCreateV1Dto.OrderRequest(items); + CreateOrderCommand orderCommand = CreateOrderCommand.from(savedUser.getId(), request); + // act + // assert + assertThrows(CoreException.class, () -> orderFacade.createOrder(orderCommand)).getErrorType().equals(ErrorType.INSUFFICIENT_STOCK); + } + + @DisplayName("10재고가 있는 4원의 상품 3건 주문시, 포인트 부족 오류가 발생한다.") + @Test + void 실패_포인트부족오류() { + long productId = savedProducts.get(1).getId(); + long quantity = 3L; + List items = new ArrayList<>(); + items.add(new OrderCreateV1Dto.OrderItemRequest(productId, quantity)); + OrderCreateV1Dto.OrderRequest request = new OrderCreateV1Dto.OrderRequest(items); + CreateOrderCommand orderCommand = CreateOrderCommand.from(savedUser.getId(), request); + // act + // assert + CoreException actualException = assertThrows(CoreException.class, + () -> orderFacade.createOrder(orderCommand)); + assertThat(actualException.getErrorType()).isEqualTo(ErrorType.INSUFFICIENT_POINT); + Product deductedProduct = productService.getProduct(productId); + assertThat(deductedProduct.getStock()).isEqualTo(10); + } + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java new file mode 100644 index 000000000..f31a3d8f2 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java @@ -0,0 +1,122 @@ +package com.loopers.application.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.domain.user.User; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.support.error.CoreException; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.product.ProductAssertions.assertProduct; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertThrows; + + +@SpringBootTest +class ProductFacadeIntegrationTest { + @Autowired + private ProductFacade productFacade; + @MockitoSpyBean + private UserJpaRepository userJpaRepository; + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductService productService; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + User savedUser; + List savedProducts; + + @BeforeEach + void setup() { + // arrange + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + savedUser = userJpaRepository.save(user); + List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + List savedBrandList = brandList.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + List productList = List.of(Product.create(savedBrandList.get(0), "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10) + , Product.create(savedBrandList.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + , Product.create(savedBrandList.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + ); + savedProducts = productService.save(productList); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_상품목록조회() { + // act + Page productsPage = productFacade.getProductList(null, "latest", 0, 20); + List products = productsPage.getContent(); + // assert + assertThat(products).isNotEmpty().hasSize(3); + } + + @DisplayName("브랜드ID 검색조건 포함시, 해당 브랜드의 상품 목록이 조회된다.") + @Test + void 성공_상품목록조회_브랜드ID() { + // arrange + // act + Page productsPage = productFacade.getProductList(savedProducts.get(0).getId(), null, 0, 20); + List productList = productsPage.getContent(); + + // assert + assertThat(productList).isNotEmpty().hasSize(2); + + assertProduct(productList.get(0), savedProducts.get(1)); + assertProduct(productList.get(1), savedProducts.get(0)); + } + } + + @DisplayName("상품을 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 상품 ID를 주면, 해당 상품 정보를 반환한다.") + @Test + void 성공_존재하는_상품ID() { + // arrange + // act + ProductDetailInfo result = productFacade.getProductDetail(savedUser.getId(), savedProducts.get(0).getId()); + + // assert + assertThat(result.name()).isEqualTo(savedProducts.get(0).getName()); + } + + @DisplayName("존재하지 않는 상품 ID를 주면, 예외가 반환된다.") + @Test + void 실패_존재하지_않는_상품ID() { + // arrange + // act + // assert + assertThrows(CoreException.class, () -> { + productFacade.getProductDetail(savedUser.getId(), (long) 100); + }); + } + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandAssertions.java new file mode 100644 index 000000000..bdf45dac1 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandAssertions.java @@ -0,0 +1,15 @@ +package com.loopers.domain.brand; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BrandAssertions { + public static void assertBrand(Brand actual, Brand expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getName()).isEqualTo(expected.getName()); + assertThat(actual.getStory()).isEqualTo(expected.getStory()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java new file mode 100644 index 000000000..57e57f3da --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java @@ -0,0 +1,49 @@ +package com.loopers.domain.brand; + +import com.loopers.domain.product.Product; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BrandModelTest { + Brand brand; + String validMsg = ""; + + @DisplayName("브랜드 모델을 생성할 때, ") + @Nested + class Create_Brand { + @DisplayName("브랜드명과 스토리가 모두 주어지면, 정상적으로 생성된다.") + @Test + void 성공_Brand_객체생성() { + //given + brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + + //assert + assertThat(brand).isNotNull(); + assertThat(brand.getName()).isEqualTo("레이브"); + assertThat(brand.getStory()).isEqualTo("레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + } + } + + @Nested + class Valid_Brand { + @BeforeEach + void setup() { + validMsg = "브랜드명은 비어있을 수 없습니다."; + } + + @Test + void 실패_이름_오류() { + assertThatThrownBy(() -> { + brand = Brand.create("", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining(validMsg); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java new file mode 100644 index 000000000..1a7346003 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java @@ -0,0 +1,76 @@ +package com.loopers.domain.brand; + +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +class BrandServiceIntegrationTest { + @Autowired + private BrandService brandService; + + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + Brand brand; + + @BeforeEach + void setup() { + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("브랜드를 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 브랜드 ID를 주면, 해당 브랜드 정보를 반환한다.") + @Test + void 성공_존재하는_브랜드ID() { + // arrange + List brands = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + List savedBrands = brands.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + // act + Brand result = brandService.getBrand(savedBrands.get(0).getId()); + + // assert + assertBrand(result, savedBrands.get(0)); + } + + @DisplayName("존재하지 않는 브랜드 ID를 주면, NOT_FOUND 예외가 발생한다.") + @Test + void 실패_존재하지_않는_브랜드ID() { + // arrange + + // act + Brand result = brandService.getBrand((long) 1); + + // assert + assertThat(result).isNull(); + } + } + + public static void assertBrand(Brand actual, Brand expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getName()).isEqualTo(expected.getName()); + assertThat(actual.getStory()).isEqualTo(expected.getStory()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/FakeLikeRepository.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/FakeLikeRepository.java new file mode 100644 index 000000000..e6e6a40ec --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/FakeLikeRepository.java @@ -0,0 +1,93 @@ +package com.loopers.domain.like; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +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.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class FakeLikeRepository implements LikeRepository { + private final Map list = new HashMap<>(); + + @Override + public Optional findById(Long userId, Long productId) { + Long id = (long) 0; + for (Long key : list.keySet()) { + id = key; + Like like = list.get(key); + if (like.getUser().getId() == userId + && like.getProduct().getId() == productId) { + return Optional.of(like); + } + } + return null; + } + + @Override + public Like save(Like entity) { + Long id = (long) 0; + for (Long key : list.keySet()) { + id = key; + Like like = list.get(key); + if (like.getUser().getId() == entity.getUser().getId() + && like.getProduct().getId() == entity.getUser().getId()) { + throw new CoreException(ErrorType.BAD_REQUEST, "중복 좋아요 요청입니다."); + } + } + id = id == (long) 0 ? 0 : id + 1; + list.put(id, entity); + return entity; + } + + @Override + public long remove(Long userId, Long productId) { + long cnt = 0; + for (Long key : list.keySet()) { + Like like = list.get(key); + if (like.getUser().getId() == userId + && like.getProduct().getId() == productId) { + list.remove(key); + cnt++; + } + } + return cnt; + } + + @Override + public boolean isLiked(Long userId, Long productId) { + for (Long key : list.keySet()) { + Like like = list.get(key); + if (like.getUser().getId() == userId + && like.getProduct().getId() == productId) { + return true; + } + } + return false; + } + + @Override + public long getLikeCount(Long productId) { + long cnt = 0; + for (Long key : list.keySet()) { + Like like = list.get(key); + if (like.getProduct().getId() == productId) { + cnt++; + } + } + return cnt; + } + + @Override + public Page getLikedProducts(Long userId, Pageable pageable) { + return new PageImpl<>(list.values().stream().filter(i -> i.getUser().getId() == userId) + .collect(Collectors.toList()), pageable, list.size()); + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java new file mode 100644 index 000000000..0c62874c5 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java @@ -0,0 +1,13 @@ +package com.loopers.domain.like; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LikeAssertions { + public static void assertLike(Like actual, Like expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getUser().getId()).isEqualTo(expected.getUser().getId()); + assertThat(actual.getProduct().getId()).isEqualTo(expected.getProduct().getId()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java new file mode 100644 index 000000000..a977dc616 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java @@ -0,0 +1,69 @@ +package com.loopers.domain.like; + +import com.loopers.domain.product.Product; +import com.loopers.domain.user.User; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LikeModelTest { + Like like; + User mockUser = mock(User.class); + Product mockProduct = mock(Product.class); + + @DisplayName("좋아요 모델을 생성할 때, ") + @Nested + class Create_Like { + @DisplayName("사용자id와 상품id가 모두 주어지면, 정상적으로 생성된다.") + @Test + void 성공_Like_객체생성() { + //given + mockUser = mock(User.class); + when(mockUser.getId()).thenReturn(1L); + mockProduct = mock(Product.class); + when(mockProduct.getId()).thenReturn(10L); + //act + like = Like.create(mockUser, mockProduct); + //assert + assertThat(like).isNotNull(); + assertThat(like.getUser().getId()).isEqualTo(mockUser.getId()); + assertThat(like.getProduct().getId()).isEqualTo(mockProduct.getId()); + } + } + + @Nested + class Valid_Like { + @BeforeEach + void setup() { + mockUser = mock(User.class); + when(mockUser.getId()).thenReturn(1L); + mockProduct = mock(Product.class); + when(mockProduct.getId()).thenReturn(10L); + } + + @Test + void 실패_사용자ID_오류() { + //given + when(mockUser.getId()).thenReturn(null); + assertThatThrownBy(() -> { + like = Like.create(mockUser, mockProduct); + }).isInstanceOf(CoreException.class).hasMessageContaining("사용자ID는 비어있을 수 없습니다."); + } + + @Test + void 실패_상품ID_오류() { + //given + when(mockProduct.getId()).thenReturn(null); + assertThatThrownBy(() -> { + like = Like.create(mockUser, mockProduct); + }).isInstanceOf(CoreException.class).hasMessageContaining("상품ID는 비어있을 수 없습니다."); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java new file mode 100644 index 000000000..ccb7fbeab --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java @@ -0,0 +1,153 @@ +package com.loopers.domain.like; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; +import com.loopers.domain.user.User; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.like.LikeJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static com.loopers.domain.like.LikeAssertions.assertLike; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@SpringBootTest +class LikeServiceIntegrationTest { + @Autowired + private LikeService likeService; + + @MockitoSpyBean + private LikeJpaRepository likeJpaRepository; + @MockitoSpyBean + private UserJpaRepository userJpaRepository; + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + User savedUser; + Brand savedBrand; + Product savedProduct; + + @TestConfiguration + static class FakeRepositoryConfig { + @Bean + public LikeRepository likeRepository() { + return new FakeLikeRepository(); + } + } + + @BeforeEach + void setup() { + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + savedUser = userJpaRepository.save(user); + Brand brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + savedBrand = brandJpaRepository.save(brand); + Product product = Product.create(savedBrand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10); + savedProduct = productJpaRepository.save(product); + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("좋아요를 조회할 때,") + @Nested + class Create { + @DisplayName("존재하는 좋아요 ID를 주면, 해당 좋아요 정보를 반환한다.") + @Test + void 성공_존재하는_좋아요ID() { + // arrange + + // act + Like result = likeService.save(Like.create(savedUser, savedProduct)); + Optional savedLike = likeJpaRepository.findById(result.getId()); + // assert + assertLike(result, savedLike.get()); + } + + @DisplayName("존재하지 않는 좋아요 ID를 주면, 예외가 발생하지 않는다.") + @Test + void 성공_이미_존재하는_좋아요ID() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + Like result2 = likeService.save(Like.create(savedUser, savedProduct)); + + // assert + assertLike(result1, result2); + } + } + + @DisplayName("좋아요를 삭제할 때,") + @Nested + class Delete { + @DisplayName("존재하는 좋아요를 삭제한다.") + @Test + void 성공_존재하는_좋아요ID() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + likeService.remove(savedUser.getId(), savedProduct.getId()); + + // assert + verify(likeJpaRepository, times(1)).deleteByUserIdAndProductId(savedUser.getId(), savedProduct.getId()); + //assertThrows(NotFoundException.class, () -> service.read(id)); + } + + @DisplayName("존재하지 않는 좋아요 ID를 삭제하면, 예외가 발생하지 않는다.") + @Test + void 성공_이미_삭제된_좋아요() { + // arrange + Like savedLike = likeService.save(Like.create(savedUser, savedProduct)); + + // act + likeService.remove(savedUser.getId(), savedProduct.getId()); + Long result1 = likeService.remove(savedUser.getId(), savedProduct.getId()); + + // assert + verify(likeJpaRepository, times(3)).deleteByUserIdAndProductId(savedUser.getId(), savedProduct.getId()); + + } + } + + @DisplayName("좋아요 상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_좋아요_상품목록조회() { + // arrange + Like savedLike = likeService.save(Like.create(savedUser, savedProduct)); + + // act + Page productsPage = likeService.getLikedProducts(savedUser.getId(), "latest", 0, 20); + List products = productsPage.getContent(); + + // assert + assertThat(products).isNotEmpty().hasSize(1); + } + + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java new file mode 100644 index 000000000..729d1b8c3 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java @@ -0,0 +1,153 @@ +package com.loopers.domain.like; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; +import com.loopers.domain.user.User; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.like.LikeAssertions.assertLike; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + + +@SpringBootTest +class LikeServiceTest { + + private final LikeService likeService; + + @Autowired + public LikeServiceTest(LikeService likeService) { + this.likeService = likeService; + } + + @TestConfiguration + static class FakeRepositoryConfig { + + @Bean + public LikeRepository likeRepository() { + return new FakeLikeRepository(); + } + } + + @MockitoSpyBean + private UserJpaRepository userJpaRepository; + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + User savedUser; + Brand savedBrand; + Product savedProduct; + + @BeforeEach + void setup() { + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + savedUser = userJpaRepository.save(user); + Brand brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + savedBrand = brandJpaRepository.save(brand); + Product product = Product.create(savedBrand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10); + savedProduct = productJpaRepository.save(product); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("좋아요를 조회할 때,") + @Nested + class Create { + @DisplayName("존재하는 좋아요 ID를 주면, 해당 좋아요 정보를 반환한다.") + @Test + void 성공_존재하는_좋아요ID() { + // arrange + + // act + Like result = likeService.save(Like.create(savedUser, savedProduct)); + + // assert + assertThat(result).isNotNull(); + assertThat(result.getUser()).isNotNull(); + } + + @DisplayName("존재하지 않는 좋아요 ID를 주면, 예외가 발생하지 않는다.") + @Test + void 성공_이미_존재하는_좋아요ID() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + Like result2 = likeService.save(Like.create(savedUser, savedProduct)); + + // assert + assertLike(result1, result2); + } + } + + @DisplayName("좋아요를 삭제할 때,") + @Nested + class Delete { + @DisplayName("존재하는 좋아요를 삭제한다.") + @Test + void 성공_존재하는_좋아요ID() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + likeService.remove(savedUser.getId(), savedProduct.getId()); + //assert + + } + + @DisplayName("존재하지 않는 좋아요 ID를 삭제하면, 예외가 발생하지 않는다.") + @Test + void 성공_이미_삭제된_좋아요() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + likeService.remove(savedUser.getId(), savedProduct.getId()); + likeService.remove(savedUser.getId(), savedProduct.getId()); + + // assert + } + } + + @DisplayName("좋아요 상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_좋아요_상품목록조회() { + // arrange + Like savedLike = likeService.save(Like.create(savedUser, savedProduct)); + + // act + Page productsPage = likeService.getLikedProducts(savedUser.getId(), "latest", 0, 20); + List products = productsPage.getContent(); + + // assert + assertThat(products).isNotEmpty().hasSize(1); + } + + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java new file mode 100644 index 000000000..65a876a15 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java @@ -0,0 +1,15 @@ +package com.loopers.domain.order; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OrderAssertions { + public static void assertOrder(Order actual, Order expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getRefUserId()).isEqualTo(expected.getRefUserId()); + assertThat(actual.getStatus()).isEqualTo(expected.getStatus()); + assertThat(actual.getTotalPrice()).isEqualByComparingTo(expected.getTotalPrice()); + assertThat(actual.getOrderAt()).isEqualTo(expected.getOrderAt()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java new file mode 100644 index 000000000..82d2f1d60 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java @@ -0,0 +1,99 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertThrows; + +class OrderModelTest { + Order order; + List orderItems; + + @DisplayName("주문 모델을 생성할 때, ") + @Nested + class Create_Order { + @DisplayName("사용자ID, 상태, 지불금액,총금액, 주문일자가 모두 주어지면, 정상적으로 생성된다.") + @Test + void 성공_Order_객체생성() { + orderItems = new ArrayList<>(); + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); + assertThat(order.getRefUserId()).isEqualTo(1); + assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING); + + } + } + + @DisplayName("주문 모델을 생성할 때, 검증") + @Nested + class Valid_Order { + + @Test + void 실패_총가격_음수오류() { + assertThrows(CoreException.class, () -> { + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(-10_000), orderItems); + }); + } + } + + @DisplayName("주문 확인후, 상품준비중 상태로 변경") + @Nested + class Valid_상품준비중 { + @Test + void 실패_주문확인_결재전오류() { + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); + assertThatThrownBy(() -> { + order.preparing(); + }).isInstanceOf(CoreException.class).hasMessageContaining("결재대기중"); + } + + @Test + void 실패_주문확인_상품준비중오류() { + order = Order.create(1, OrderStatus.PREPARING, new BigDecimal(10_000), orderItems); + assertThatThrownBy(() -> { + order.preparing(); + }).isInstanceOf(CoreException.class).hasMessageContaining("이미 준비중"); + } + + @Test + void 실패_주문확인_상품준비완료오류() { + order = Order.create(1, OrderStatus.SHIPPED, new BigDecimal(10_000), orderItems); + assertThatThrownBy(() -> { + order.preparing(); + }).isInstanceOf(CoreException.class).hasMessageContaining("상품준비 완료"); + } + + @Test + void 성공_상품준비중() { + order = Order.create(1, OrderStatus.PAID, new BigDecimal(10_000), orderItems); + order.preparing(); + assertThat(order.getStatus()).isEqualTo(OrderStatus.PREPARING); + } + } + + @DisplayName("상품 모델을 생성후, 주문취소") + @Nested + class Valid_주문취소 { + @Test + void 실패_주문취소_이미취소된주문오류() { + order = Order.create(1, OrderStatus.CANCELLED, new BigDecimal(10_000), orderItems); + assertThrows(CoreException.class, () -> { + order.preparing(); + }); + } + + @Test + void 성공_주문취소() { + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); + order.cancel(); + assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java new file mode 100644 index 000000000..a3e21b571 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java @@ -0,0 +1,120 @@ +package com.loopers.domain.order; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.brand.BrandAssertions.assertBrand; +import static com.loopers.domain.product.ProductAssertions.assertProduct; +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +@Transactional +class OrderServiceIntegrationTest { + @Autowired + private ProductService productService; + + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + List savedBrands; + List savedProducts; + + @BeforeEach + void setup() { + List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + savedBrands = brandList.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + List productList = List.of(Product.create(savedBrands.get(0), "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10) + , Product.create(savedBrands.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + , Product.create(savedBrands.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + ); + savedProducts = productList.stream().map((product) -> productJpaRepository.save(product)).toList(); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_상품목록조회() { + // arrange + + // act + Page productsPage = productService.getProducts(null, "latest", 0, 20); + List products = productsPage.getContent(); + // assert + assertThat(products).isNotEmpty().hasSize(3); + } + + @DisplayName("브랜드ID 검색조건 포함시, 해당 브랜드의 상품 목록이 조회된다.") + @Test + void 성공_상품목록조회_브랜드ID() { + // arrange + + // act + Page productsPage = productService.getProducts(savedProducts.get(0).getId(), null, 0, 20); + List productList = productsPage.getContent(); + + // assert + assertThat(productList).isNotEmpty().hasSize(2); + + assertProduct(productList.get(0), savedProducts.get(1)); + assertProduct(productList.get(1), savedProducts.get(0)); + } + } + + @DisplayName("상품을 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 상품 ID를 주면, 해당 상품 정보를 반환한다.") + @Test + void 성공_존재하는_상품ID() { + // arrange + // act + Product result = productService.getProduct(savedProducts.get(0).getId()); + + // assert + assertProduct(result, savedProducts.get(0)); + assertBrand(result.getBrand(), savedBrands.get(0)); + } + + @DisplayName("존재하지 않는 상품 ID를 주면, null이 반환된다.") + @Test + void 실패_존재하지_않는_상품ID() { + // arrange + + // act + Product result = productService.getProduct((long) 1000); + + // assert + assertThat(result).isNull(); + } + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java index ac000465c..c86ebaea9 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -1,6 +1,6 @@ package com.loopers.domain.point; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.infrastructure.point.PointJpaRepository; import com.loopers.infrastructure.user.UserJpaRepository; import com.loopers.support.error.CoreException; @@ -49,11 +49,11 @@ class Get { void 성공_존재하는_유저ID() { // arrange BigDecimal JOIN_POINT = BigDecimal.TEN; - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userJpaRepository.save(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User saved = userJpaRepository.save(user); // act - BigDecimal pointAmt = pointService.getAmount(userModel.getUserId()); + BigDecimal pointAmt = pointService.getAmount(saved.getId()); // assert(회원가입시, 기본포인트 10) assertAll( @@ -66,10 +66,10 @@ class Get { @Test void 실패_존재하지않는_유저ID() { // arrange (등록된 회원 없음) - String userId = "userId"; + Long id = 5L; // act - BigDecimal pointAmt = pointService.getAmount(userId); + BigDecimal pointAmt = pointService.getAmount(id); // assert assertThat(pointAmt).isNull(); @@ -84,11 +84,11 @@ class Charge { @Test void 실패_존재하지않는_유저ID() { // arrange (등록된 회원 없음) - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); // act, assert assertThatThrownBy(() -> { - pointService.charge(userModel, BigDecimal.TEN); + pointService.charge(user, BigDecimal.TEN); }).isInstanceOf(CoreException.class).hasMessageContaining("현재 포인트 정보를 찾을수 없습니다."); } @@ -98,11 +98,11 @@ class Charge { void 성공_존재하는_유저ID() { // arrange BigDecimal JOIN_POINT = BigDecimal.TEN; - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userJpaRepository.save(userModel); - Optional savedUserModel = userJpaRepository.findByUserId(userModel.getUserId()); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User savedUser = userJpaRepository.save(user); + // act - BigDecimal pointAmt = pointService.charge(savedUserModel.get(), BigDecimal.TEN); + BigDecimal pointAmt = pointService.charge(savedUser, BigDecimal.TEN); // assert(회원가입시, 기본포인트 10) assertAll( diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java similarity index 60% rename from apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java index 02bcd0387..29d6b647a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java @@ -1,6 +1,6 @@ package com.loopers.domain.point; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -12,13 +12,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -class PointModelTest { - UserModel userModel; - PointModel pointModel; +class PointTest { + User user; + Point point; @BeforeEach void setup() { - userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); } @DisplayName("포인트 충전") @@ -28,24 +28,24 @@ class Charge { @Test void 실패_포인트충전_0() { // arrange - pointModel = PointModel.create(userModel, BigDecimal.ZERO); + point = Point.create(user, BigDecimal.ZERO); // act, assert assertThatThrownBy(() -> { - pointModel.charge(BigDecimal.ZERO); + point.charge(BigDecimal.ZERO); }).isInstanceOf(CoreException.class).hasMessageContaining("충전 금액은 0보다 커야 합니다."); } @Test void 성공_포인트충전() { // arrange - pointModel = PointModel.create(userModel, new BigDecimal(20)); + point = Point.create(user, new BigDecimal(20)); // act - pointModel.charge(new BigDecimal(5)); + point.charge(new BigDecimal(5)); // assert - assertThat(pointModel.getAmount()).isEqualTo(new BigDecimal(25)); + assertThat(point.getAmount()).isEqualTo(new BigDecimal(25)); } } @@ -55,24 +55,24 @@ class Use { @Test void 실패_포인트사용() { // arrange - pointModel = PointModel.create(userModel, BigDecimal.ZERO); + point = Point.create(user, BigDecimal.ZERO); // act, assert assertThatThrownBy(() -> { - pointModel.use(BigDecimal.TEN); - }).isInstanceOf(CoreException.class).hasMessageContaining("잔액이 부족합니다."); + point.use(BigDecimal.TEN); + }).isInstanceOf(CoreException.class).hasMessageContaining("포인트가 부족합니다."); } @Test void 성공_포인트사용() { // arrange - pointModel = PointModel.create(userModel, new BigDecimal(20)); + point = Point.create(user, new BigDecimal(20)); // act - pointModel.use(new BigDecimal(5)); + point.use(new BigDecimal(5)); // assert - assertThat(pointModel.getAmount()).isEqualTo(new BigDecimal(15)); + assertThat(point.getAmount()).isEqualTo(new BigDecimal(15)); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductAssertions.java new file mode 100644 index 000000000..2c437632d --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductAssertions.java @@ -0,0 +1,18 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; + +import java.math.BigDecimal; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ProductAssertions { + public static void assertProduct(Product actual, Product expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getName()).isEqualTo(expected.getName()); + assertThat(actual.getPrice()).isEqualByComparingTo(expected.getPrice()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java new file mode 100644 index 000000000..cf44ba293 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java @@ -0,0 +1,87 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertThrows; + +class ProductModelTest { + Brand brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + Product product; + String validMsg = ""; + + @DisplayName("상품 모델을 생성할 때, ") + @Nested + class Create_Product { + @DisplayName("브랜드모델, 상품명, 가격, 재고, 예약재고가 모두 주어지면, 정상적으로 생성된다.") + @Test + void 성공_Product_객체생성() { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10); + assertThat(product).isNotNull(); + assertThat(product.getName()).isEqualTo("Wild Faith Rose Sweatshirt"); + assertThat(product.getPrice()).isEqualTo(new BigDecimal(80_000)); + assertThat(product.getStock()).isEqualTo(10); + } + } + + @DisplayName("상품 모델을 생성할 때, 검증") + @Nested + class Valid_Product { + @BeforeEach + void setup() { + validMsg = "형식이 유효하지 않습니다."; + } + + @Test + void 실패_가격_음수오류() { + assertThrows(CoreException.class, () -> { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(-1000), 0); + }); + } + + @Test + void 실패_재고_음수오류() { + assertThrows(CoreException.class, () -> { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), -1); + }); + } + + } + + @DisplayName("상품 모델을 생성후, 재고 예약") + @Nested + class Valid_재고차감 { + @BeforeEach + void setup() { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(1000), 10); + } + + @Test + void 실패_예약재고0_차감오류() { + assertThrows(CoreException.class, () -> { + product.deductStock(0); + }); + } + + @Test + void 실패_예약재고20_차감오류() { + assertThrows(CoreException.class, () -> { + product.deductStock(20); + }); + } + + @Test + void 성공_재고예약() { + product.deductStock(2); + assertThat(product.getStock()).isEqualTo(8); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java new file mode 100644 index 000000000..b1693262e --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java @@ -0,0 +1,118 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.brand.BrandAssertions.assertBrand; +import static com.loopers.domain.product.ProductAssertions.assertProduct; +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +@Transactional +class ProductServiceIntegrationTest { + @Autowired + private ProductService productService; + + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + List savedBrands; + List savedProducts; + + @BeforeEach + void setup() { + List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + savedBrands = brandList.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + List productList = List.of(Product.create(savedBrands.get(0), "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10) + , Product.create(savedBrands.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + , Product.create(savedBrands.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + ); + savedProducts = productList.stream().map((product) -> productJpaRepository.save(product)).toList(); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_상품목록조회() { + // arrange + + // act + Page productsPage = productService.getProducts(null, "latest", 0, 20); + List products = productsPage.getContent(); + // assert + assertThat(products).isNotEmpty().hasSize(3); + } + + @DisplayName("브랜드ID 검색조건 포함시, 해당 브랜드의 상품 목록이 조회된다.") + @Test + void 성공_상품목록조회_브랜드ID() { + // arrange + + // act + Page productsPage = productService.getProducts(savedProducts.get(0).getId(), null, 0, 20); + List productList = productsPage.getContent(); + + // assert + assertThat(productList).isNotEmpty().hasSize(2); + + assertProduct(productList.get(0), savedProducts.get(1)); + assertProduct(productList.get(1), savedProducts.get(0)); + } + } + + @DisplayName("상품을 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 상품 ID를 주면, 해당 상품 정보를 반환한다.") + @Test + void 성공_존재하는_상품ID() { + // arrange + // act + Product result = productService.getProduct(savedProducts.get(0).getId()); + + // assert + assertProduct(result, savedProducts.get(0)); + assertBrand(result.getBrand(), savedBrands.get(0)); + } + + @DisplayName("존재하지 않는 상품 ID를 주면, null이 반환된다.") + @Test + void 실패_존재하지_않는_상품ID() { + // arrange + + // act + Product result = productService.getProduct((long) 1000); + + // assert + assertThat(result).isNull(); + } + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index a3a300d5c..7cc9e69a0 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -39,15 +39,15 @@ class Join { @Test void 성공_회원가입() { // arrange - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); // act - userService.join(userModel); + userService.join(user); // assert assertAll( - () -> verify(userJpaRepository, times(1)).save(userModel), - () -> assertThrows(CoreException.class, () -> userService.join(userModel)) + () -> verify(userJpaRepository, times(1)).save(user), + () -> assertThrows(CoreException.class, () -> userService.join(user)) ); } @@ -55,15 +55,15 @@ class Join { @Test void 실패_이미_가입된ID() { // arrange - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userService.join(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + userService.join(user); // act - verify(userJpaRepository, times(1)).save(userModel); + verify(userJpaRepository, times(1)).save(user); assertThatThrownBy(() -> { - userService.join(userModel); + userService.join(user); }).isInstanceOf(CoreException.class).hasMessageContaining("이미 가입된 ID 입니다."); - verify(userJpaRepository, times(1)).save(userModel); + verify(userJpaRepository, times(1)).save(user); } } @@ -74,19 +74,19 @@ class Get { @Test void 성공_존재하는_유저ID() { // arrange - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userService.join(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + userService.join(user); // act - UserModel result = userService.getUser(userModel.getUserId()); + User result = userService.getUser(user.getUserId()); // assert assertAll( () -> assertThat(result).isNotNull(), - () -> assertThat(result.getUserId()).isEqualTo(userModel.getUserId()), - () -> assertThat(result.getEmail()).isEqualTo(userModel.getEmail()), - () -> assertThat(result.getBirthday()).isEqualTo(userModel.getBirthday()), - () -> assertThat(result.getGender()).isEqualTo(userModel.getGender()) + () -> assertThat(result.getUserId()).isEqualTo(user.getUserId()), + () -> assertThat(result.getEmail()).isEqualTo(user.getEmail()), + () -> assertThat(result.getBirthday()).isEqualTo(user.getBirthday()), + () -> assertThat(result.getGender()).isEqualTo(user.getGender()) ); } @@ -94,10 +94,10 @@ class Get { @Test void 실패_존재하지_않는_유저ID() { // arrange - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); // act - UserModel result = userService.getUser(userModel.getUserId()); + User result = userService.getUser(user.getUserId()); // assert assertThat(result).isNull(); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java similarity index 72% rename from apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java index 159dbdde4..91eb7b75f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -9,8 +9,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -class UserModelTest { - UserModel userModel; +class UserTest { + User user; String validMsg = ""; @DisplayName("회원가입-단위테스트1") @@ -24,28 +24,28 @@ void setup() { @Test void 실패_ID_숫자만() { assertThatThrownBy(() -> { - userModel = UserModel.create("12345", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("12345", "user1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_ID_문자만() { assertThatThrownBy(() -> { - userModel = UserModel.create("hello", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("hello", "user1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_ID_특수문자() { assertThatThrownBy(() -> { - userModel = UserModel.create("12345!", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("12345!", "user1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_ID_10자이상오류() { assertThatThrownBy(() -> { - userModel = UserModel.create("12345678910", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("12345678910", "user1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } } @@ -61,14 +61,14 @@ void setup() { @Test void 실패_이메일_기호없음() { assertThatThrownBy(() -> { - userModel = UserModel.create("user1", "user1test.XXX", "1999-01-01", "F"); + user = User.create("user1", "user1test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_이메일_한글포함() { assertThatThrownBy(() -> { - userModel = UserModel.create("user1", "ㄱuser1@test.XXX", "1999-01-01", "F"); + user = User.create("user1", "ㄱuser1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } } @@ -85,22 +85,22 @@ void setup() { @Test void 실패_생년월일_형식오류() { assertThatThrownBy(() -> { - userModel = UserModel.create("user1", "user1@test.XXX", "19990101", "F"); + user = User.create("user1", "user1@test.XXX", "19990101", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_생년월일_날짜오류() { assertThatThrownBy(() -> { - userModel = UserModel.create("user1", "user1@test.XXX", "1999-13-01", "F"); + user = User.create("user1", "user1@test.XXX", "1999-13-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } } @Test void 성공_User_객체생성() { - userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - assertThat(userModel).isNotNull(); - assertThat(userModel.getUserId()).isEqualTo("user1"); + user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + assertThat(user).isNotNull(); + assertThat(user.getUserId()).isEqualTo("user1"); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java index 4914412f7..d1730a77f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java @@ -1,6 +1,7 @@ package com.loopers.interfaces.api; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; import com.loopers.infrastructure.user.UserJpaRepository; import com.loopers.utils.DatabaseCleanUp; import org.junit.jupiter.api.AfterEach; @@ -22,17 +23,17 @@ public class PointV1ApiE2ETest { private final TestRestTemplate testRestTemplate; - private final UserJpaRepository userJpaRepository; + private final UserService userService; private final DatabaseCleanUp databaseCleanUp; @Autowired public PointV1ApiE2ETest( TestRestTemplate testRestTemplate, - UserJpaRepository userJpaRepository, + UserService userService, DatabaseCleanUp databaseCleanUp ) { this.testRestTemplate = testRestTemplate; - this.userJpaRepository = userJpaRepository; + this.userService = userService; this.databaseCleanUp = databaseCleanUp; } @@ -50,12 +51,12 @@ class Get { //given BigDecimal JOIN_POINT = BigDecimal.TEN; - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userJpaRepository.save(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User savedUser = userService.join(user); //when HttpHeaders headers = new HttpHeaders(); - headers.set("X-USER-ID", userModel.getUserId()); + headers.set("X-USER-ID", savedUser.getId().toString()); String url = "/api/v1/user/point"; ParameterizedTypeReference> resType = new ParameterizedTypeReference<>() { @@ -93,11 +94,11 @@ class Charge { //given BigDecimal CHARGE_POINT = BigDecimal.TEN; - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userJpaRepository.save(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User savedUser = userService.join(user); HttpHeaders headers = new HttpHeaders(); - headers.set("X-USER-ID", userModel.getUserId()); + headers.set("X-USER-ID", savedUser.getId().toString()); //when String url = "/api/v1/user/point/charge"; @@ -116,7 +117,7 @@ class Charge { void 실패_ID없음_400() { //given HttpHeaders headers = new HttpHeaders(); - headers.set("X-USER-ID", "user1"); + headers.set("X-USER-ID", "2"); //when String url = "/api/v1/user/point/charge"; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java index 6c5952457..21fcb759e 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -1,9 +1,8 @@ package com.loopers.interfaces.api; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; import com.loopers.interfaces.api.user.UserCreateV1Dto; -import com.loopers.support.error.CoreException; import com.loopers.utils.DatabaseCleanUp; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -93,11 +92,11 @@ class Get { @Test void 성공_정보조회() { //given - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userService.join(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + userService.join(user); //when - String url = ENDPOINT_GET.apply(userModel.getUserId()); + String url = ENDPOINT_GET.apply(user.getUserId()); ParameterizedTypeReference> resType = new ParameterizedTypeReference<>() { }; ResponseEntity> res = testRestTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), resType); @@ -105,7 +104,7 @@ class Get { //then assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(res.getBody().data().userId()).isNotNull(); - assertThat(res.getBody().data().userId()).isEqualTo(userModel.getUserId()); + assertThat(res.getBody().data().userId()).isEqualTo(user.getUserId()); } @DisplayName("E2E테스트2")