-
Notifications
You must be signed in to change notification settings - Fork 43
feat: User API 구현 (회원가입, 내 정보 조회, 비밀번호 변경) #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5da86c1
0da135e
ebe81df
7fee320
e18ddab
7cde2b9
fafa33d
4488138
b4517ba
e7fdc40
5c84614
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,3 +38,4 @@ out/ | |
|
|
||
| ### Kotlin ### | ||
| .kotlin | ||
| CLAUDE.md | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.loopers.application.brand; | ||
|
|
||
| import com.loopers.domain.brand.Brand; | ||
| import com.loopers.domain.brand.BrandService; | ||
| import com.loopers.domain.product.ProductService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class BrandFacade { | ||
|
|
||
| private final BrandService brandService; | ||
| private final ProductService productService; | ||
|
|
||
| public BrandInfo register(String name, String description, String imageUrl) { | ||
| Brand brand = brandService.register(name, description, imageUrl); | ||
| return BrandInfo.from(brand); | ||
| } | ||
|
|
||
| public BrandInfo getBrand(Long id) { | ||
| Brand brand = brandService.getBrand(id); | ||
| return BrandInfo.from(brand); | ||
| } | ||
|
|
||
| public List<BrandInfo> getAllBrands() { | ||
| return brandService.getAllBrands().stream() | ||
| .map(BrandInfo::from) | ||
| .toList(); | ||
| } | ||
|
|
||
| public BrandInfo update(Long id, String name, String description, String imageUrl) { | ||
| Brand brand = brandService.update(id, name, description, imageUrl); | ||
| return BrandInfo.from(brand); | ||
| } | ||
|
|
||
| public void delete(Long id) { | ||
| brandService.delete(id); | ||
| productService.deleteAllByBrandId(id); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.loopers.application.brand; | ||
|
|
||
| import com.loopers.domain.brand.Brand; | ||
|
|
||
| public record BrandInfo( | ||
| Long brandId, | ||
| String name, | ||
| String description, | ||
| String imageUrl | ||
| ) { | ||
| public static BrandInfo from(Brand brand) { | ||
| return new BrandInfo( | ||
| brand.getId(), | ||
| brand.getName(), | ||
| brand.getDescription(), | ||
| brand.getImageUrl() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| package com.loopers.application.like; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.loopers.application.product.ProductInfo; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.loopers.domain.brand.Brand; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.loopers.domain.brand.BrandService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.loopers.domain.like.ProductLike; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.loopers.domain.like.ProductLikeService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.loopers.domain.product.Product; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.loopers.domain.product.ProductService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.loopers.support.error.CoreException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Component; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Objects; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Component | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class ProductLikeFacade { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final ProductService productService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final ProductLikeService productLikeService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final BrandService brandService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void like(Long userId, Long productId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| productService.getProduct(productId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| productLikeService.like(userId, productId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public void unlike(Long userId, Long productId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| productLikeService.unlike(userId, productId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
수정안: 추가 테스트: 존재하지 않는 🔧 수정 예시 public void unlike(Long userId, Long productId) {
+ productService.getProduct(productId);
productLikeService.unlike(userId, productId);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public ProductLikeInfo getLikeInfo(Long userId, Long productId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| long likeCount = productLikeService.getLikeCount(productId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| boolean liked = productLikeService.isLiked(userId, productId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ProductLikeInfo.of(productId, likeCount, liked); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 존재하지 않는 상품에 대해 좋아요 정보가 정상 응답으로 반환될 수 있다
🔧 수정 예시 public ProductLikeInfo getLikeInfo(Long userId, Long productId) {
+ productService.getProduct(productId);
long likeCount = productLikeService.getLikeCount(productId);
boolean liked = productLikeService.isLiked(userId, productId);
return ProductLikeInfo.of(productId, likeCount, liked);
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| public List<ProductInfo> getLikedProducts(Long userId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| List<ProductLike> likes = productLikeService.getLikedProducts(userId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return likes.stream() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(like -> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Product product = productService.getProduct(like.getProductId()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Brand brand = brandService.getBrand(product.getBrandId()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| long likeCount = productLikeService.getLikeCount(product.getId()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ProductInfo.of(product, brand, likeCount, true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (CoreException e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+44
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
수정안: 예외 발생 시 🔧 수정 예시+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class ProductLikeFacade {
+ private static final Logger log = LoggerFactory.getLogger(ProductLikeFacade.class);
...
} catch (CoreException e) {
+ log.warn("좋아요 상품 조회 실패 - productId={}", like.getProductId(), e);
return null;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(Objects::nonNull) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
좋아요 항목 수(N)만큼 수정안:
추가 테스트: 좋아요 수가 많을 때 쿼리 수가 일정(O(1))한지 검증하는 통합 테스트를 추가한다. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.loopers.application.like; | ||
|
|
||
| public record ProductLikeInfo( | ||
| Long productId, | ||
| long likeCount, | ||
| boolean liked | ||
| ) { | ||
| public static ProductLikeInfo of(Long productId, long likeCount, boolean liked) { | ||
| return new ProductLikeInfo(productId, likeCount, liked); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| package com.loopers.application.order; | ||
|
|
||
| public record OrderCreateItem(Long productId, int quantity) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| package com.loopers.application.order; | ||
|
|
||
| import com.loopers.domain.brand.Brand; | ||
| import com.loopers.domain.brand.BrandService; | ||
| import com.loopers.domain.order.Order; | ||
| import com.loopers.domain.order.OrderItem; | ||
| import com.loopers.domain.order.OrderService; | ||
| import com.loopers.domain.product.Product; | ||
| import com.loopers.domain.product.ProductService; | ||
| import com.loopers.support.error.CoreException; | ||
| import com.loopers.support.error.ErrorType; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class OrderFacade { | ||
|
|
||
| private final ProductService productService; | ||
| private final BrandService brandService; | ||
| private final OrderService orderService; | ||
|
|
||
| public OrderInfo createOrder(Long userId, List<OrderCreateItem> items) { | ||
| List<OrderItem> orderItems = new ArrayList<>(); | ||
|
|
||
| for (OrderCreateItem createItem : items) { | ||
| Product product = productService.getProduct(createItem.productId()); | ||
|
|
||
| if (!product.hasEnoughStock(createItem.quantity())) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다."); | ||
| } | ||
|
|
||
| productService.decreaseStock(createItem.productId(), createItem.quantity()); | ||
|
|
||
| Brand brand = brandService.getBrand(product.getBrandId()); | ||
|
|
||
| OrderItem orderItem = OrderItem.createSnapshot( | ||
| product.getId(), | ||
| brand.getName(), | ||
| product.getName(), | ||
| product.getPrice(), | ||
| createItem.quantity() | ||
| ); | ||
| orderItems.add(orderItem); | ||
| } | ||
|
|
||
| Order order = orderService.createOrder(userId, orderItems); | ||
| List<OrderItem> savedItems = orderService.getOrderItems(order.getId()); | ||
| List<OrderItemInfo> itemInfos = savedItems.stream() | ||
| .map(OrderItemInfo::from) | ||
| .toList(); | ||
|
|
||
| return OrderInfo.of(order, itemInfos); | ||
| } | ||
|
Comment on lines
+26
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 주문 생성 전체를 단일 트랜잭션으로 묶어야 한다. 🛡️ 제안 수정안(예시)+import org.springframework.transaction.annotation.Transactional;
public class OrderFacade {
+ `@Transactional`
public OrderInfo createOrder(Long userId, List<OrderCreateItem> items) {
List<OrderItem> orderItems = new ArrayList<>();🤖 Prompt for AI Agents |
||
|
|
||
| public OrderInfo getOrder(Long orderId) { | ||
| Order order = orderService.getOrder(orderId); | ||
| List<OrderItem> items = orderService.getOrderItems(orderId); | ||
| List<OrderItemInfo> itemInfos = items.stream() | ||
| .map(OrderItemInfo::from) | ||
| .toList(); | ||
| return OrderInfo.of(order, itemInfos); | ||
| } | ||
|
|
||
| public List<OrderInfo> getOrdersByUserId(Long userId) { | ||
| List<Order> orders = orderService.getOrdersByUserId(userId); | ||
| return orders.stream() | ||
| .map(order -> { | ||
| List<OrderItem> items = orderService.getOrderItems(order.getId()); | ||
| List<OrderItemInfo> itemInfos = items.stream() | ||
| .map(OrderItemInfo::from) | ||
| .toList(); | ||
| return OrderInfo.of(order, itemInfos); | ||
| }) | ||
| .toList(); | ||
| } | ||
|
|
||
| public List<OrderInfo> getAllOrders() { | ||
| List<Order> orders = orderService.getAllOrders(); | ||
| return orders.stream() | ||
| .map(order -> { | ||
| List<OrderItem> items = orderService.getOrderItems(order.getId()); | ||
| List<OrderItemInfo> itemInfos = items.stream() | ||
| .map(OrderItemInfo::from) | ||
| .toList(); | ||
| return OrderInfo.of(order, itemInfos); | ||
| }) | ||
| .toList(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.loopers.application.order; | ||
|
|
||
| import com.loopers.domain.order.Order; | ||
|
|
||
| import java.time.ZonedDateTime; | ||
| import java.util.List; | ||
|
|
||
| public record OrderInfo( | ||
| Long orderId, | ||
| Long userId, | ||
| int totalAmount, | ||
| List<OrderItemInfo> items, | ||
| ZonedDateTime createdAt | ||
| ) { | ||
| public static OrderInfo of(Order order, List<OrderItemInfo> items) { | ||
| return new OrderInfo( | ||
| order.getId(), | ||
| order.getUserId(), | ||
| order.getTotalAmount(), | ||
| items, | ||
| order.getCreatedAt() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package com.loopers.application.order; | ||
|
|
||
| import com.loopers.domain.order.OrderItem; | ||
|
|
||
| public record OrderItemInfo( | ||
| Long orderItemId, | ||
| Long productId, | ||
| String brandName, | ||
| String productName, | ||
| int price, | ||
| int quantity | ||
| ) { | ||
| public static OrderItemInfo from(OrderItem item) { | ||
| return new OrderItemInfo( | ||
| item.getId(), | ||
| item.getProductId(), | ||
| item.getBrandName(), | ||
| item.getProductName(), | ||
| item.getPrice(), | ||
| item.getQuantity() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package com.loopers.application.product; | ||
|
|
||
| import com.loopers.domain.brand.Brand; | ||
| import com.loopers.domain.brand.BrandService; | ||
| import com.loopers.domain.like.ProductLikeService; | ||
| import com.loopers.domain.product.Product; | ||
| import com.loopers.domain.product.ProductService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class ProductFacade { | ||
|
|
||
| private final ProductService productService; | ||
| private final BrandService brandService; | ||
| private final ProductLikeService productLikeService; | ||
|
|
||
| public ProductInfo getProductDetail(Long productId, Long userId) { | ||
| Product product = productService.getProduct(productId); | ||
| Brand brand = brandService.getBrand(product.getBrandId()); | ||
| long likeCount = productLikeService.getLikeCount(productId); | ||
| boolean liked = userId != null && productLikeService.isLiked(userId, productId); | ||
| return ProductInfo.of(product, brand, likeCount, liked); | ||
| } | ||
|
|
||
| public List<ProductInfo> getProducts(Long brandId) { | ||
| List<Product> products; | ||
| if (brandId != null) { | ||
| products = productService.getProductsByBrandId(brandId); | ||
| } else { | ||
| products = productService.getAllProducts(); | ||
| } | ||
| return products.stream() | ||
| .map(product -> { | ||
| Brand brand = brandService.getBrand(product.getBrandId()); | ||
| long likeCount = productLikeService.getLikeCount(product.getId()); | ||
| return ProductInfo.of(product, brand, likeCount, false); | ||
| }) | ||
| .toList(); | ||
|
Comment on lines
+36
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
상품 수(N)만큼 수정안: 상품의 추가 테스트: 상품이 다수일 때 실행 쿼리 수가 N에 비례하지 않음을 검증하는 통합 테스트를 추가한다. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| public ProductInfo registerProduct(Long brandId, String name, String description, int price, int stock, String imageUrl) { | ||
| brandService.getBrand(brandId); | ||
| Product product = productService.register(brandId, name, description, price, stock, imageUrl); | ||
| Brand brand = brandService.getBrand(product.getBrandId()); | ||
| return ProductInfo.of(product, brand, 0L, false); | ||
| } | ||
|
|
||
| public ProductInfo updateProduct(Long productId, String name, String description, int price, int stock, String imageUrl) { | ||
| Product product = productService.update(productId, name, description, price, stock, imageUrl); | ||
| Brand brand = brandService.getBrand(product.getBrandId()); | ||
| long likeCount = productLikeService.getLikeCount(productId); | ||
| return ProductInfo.of(product, brand, likeCount, false); | ||
| } | ||
|
|
||
| public void deleteProduct(Long productId) { | ||
| productService.delete(productId); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.loopers.application.product; | ||
|
|
||
| import com.loopers.domain.brand.Brand; | ||
| import com.loopers.domain.product.Product; | ||
|
|
||
| public record ProductInfo( | ||
| Long productId, | ||
| String productName, | ||
| String description, | ||
| int price, | ||
| int stock, | ||
| String imageUrl, | ||
| Long brandId, | ||
| String brandName, | ||
| long likeCount, | ||
| boolean liked | ||
| ) { | ||
| public static ProductInfo of(Product product, Brand brand, long likeCount, boolean liked) { | ||
| return new ProductInfo( | ||
| product.getId(), | ||
| product.getName(), | ||
| product.getDescription(), | ||
| product.getPrice(), | ||
| product.getStock(), | ||
| product.getImageUrl(), | ||
| brand.getId(), | ||
| brand.getName(), | ||
| likeCount, | ||
| liked | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
브랜드 삭제와 상품 삭제가 원자적으로 처리되지 않는다
delete메서드에@Transactional을 부여하거나BrandService.delete내부에서productService.deleteAllByBrandId를 호출해 단일 트랜잭션으로 묶는다.productService.deleteAllByBrandId가 예외를 던질 때 브랜드 삭제가 롤백되는지 통합 테스트를 추가한다.🔧 수정 예시
📝 Committable suggestion
🤖 Prompt for AI Agents