-
Notifications
You must be signed in to change notification settings - Fork 9
루퍼스 깃 세션 발표 #2
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
루퍼스 깃 세션 발표 #2
Changes from all commits
b8195e7
6ffd495
79971e6
d85f9db
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 |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.loopers.application.product; | ||
|
|
||
| import com.loopers.domain.product.ProductModel; | ||
| import com.loopers.domain.product.ProductService; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import java.math.BigDecimal; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class ProductFacade { | ||
| private final ProductService productService; | ||
|
|
||
| public ProductInfo createProduct(String name, String description, BigDecimal price, Integer stock) { | ||
| ProductModel product = productService.createProduct(name, description, price, stock); | ||
| return ProductInfo.from(product); | ||
| } | ||
|
|
||
| public ProductInfo getProduct(Long id) { | ||
| ProductModel product = productService.getProduct(id); | ||
| return ProductInfo.from(product); | ||
| } | ||
|
|
||
| public Page<ProductInfo> getProducts(Pageable pageable) { | ||
| Page<ProductModel> products = productService.getProducts(pageable); | ||
| return products.map(ProductInfo::from); | ||
| } | ||
|
|
||
| public ProductInfo updateProduct(Long id, String name, String description, BigDecimal price, Integer stock) { | ||
| ProductModel product = productService.updateProduct(id, name, description, price, stock); | ||
| return ProductInfo.from(product); | ||
| } | ||
|
|
||
| public void deleteProduct(Long id) { | ||
| productService.deleteProduct(id); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.loopers.application.product; | ||
|
|
||
| import com.loopers.domain.product.ProductModel; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.ZonedDateTime; | ||
|
|
||
| public record ProductInfo(Long id, String name, String description, BigDecimal price, Integer stock, ZonedDateTime createdAt, ZonedDateTime updatedAt) { | ||
| public static ProductInfo from(ProductModel model) { | ||
| return new ProductInfo( | ||
| model.getId(), | ||
| model.getName(), | ||
| model.getDescription(), | ||
| model.getPrice(), | ||
| model.getStock(), | ||
| model.getCreatedAt(), | ||
| model.getUpdatedAt() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| package com.loopers.domain.product; | ||
|
|
||
| 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 java.math.BigDecimal; | ||
|
|
||
| @Entity | ||
| @Table(name = "product") | ||
| public class ProductModel extends BaseEntity { | ||
|
|
||
| private String name; | ||
| private String description; | ||
| private BigDecimal price; | ||
| private Integer stock; | ||
|
|
||
| protected ProductModel() {} | ||
|
|
||
| public ProductModel(String name, String description, BigDecimal price, Integer stock) { | ||
| if (name == null || name.isBlank()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "상품명은 비어있을 수 없습니다."); | ||
| } | ||
| if (price == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다."); | ||
| } | ||
| if (price.compareTo(BigDecimal.ZERO) < 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다."); | ||
| } | ||
| if (stock == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } | ||
|
Comment on lines
+31
to
+33
|
||
| if (stock < 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } | ||
|
|
||
| this.name = name; | ||
| this.description = description; | ||
| this.price = price; | ||
| this.stock = stock; | ||
| } | ||
|
|
||
| public String getName() { | ||
| return name; | ||
| } | ||
|
|
||
| public String getDescription() { | ||
| return description; | ||
| } | ||
|
|
||
| public BigDecimal getPrice() { | ||
| return price; | ||
| } | ||
|
|
||
| public Integer getStock() { | ||
| return stock; | ||
| } | ||
|
|
||
| public void update(String name, String description, BigDecimal price, Integer stock) { | ||
| if (name == null || name.isBlank()) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "상품명은 비어있을 수 없습니다."); | ||
| } | ||
| if (price == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다."); | ||
| } | ||
|
Comment on lines
+64
to
+66
|
||
| if (price.compareTo(BigDecimal.ZERO) < 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다."); | ||
| } | ||
| if (stock == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } | ||
|
Comment on lines
+70
to
+72
|
||
| if (stock < 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } | ||
|
Comment on lines
+60
to
+75
|
||
|
|
||
| this.name = name; | ||
| this.description = description; | ||
| this.price = price; | ||
| this.stock = stock; | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+82
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.loopers.domain.product; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
|
|
||
| public interface ProductRepository { | ||
| Optional<ProductModel> find(Long id); | ||
| List<ProductModel> findAll(); | ||
| Page<ProductModel> findAll(Pageable pageable); | ||
| ProductModel save(ProductModel product); | ||
| void delete(ProductModel product); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package com.loopers.domain.product; | ||
|
|
||
| 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.Pageable; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.math.BigDecimal; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Component | ||
| public class ProductService { | ||
|
|
||
| private final ProductRepository productRepository; | ||
|
|
||
| @Transactional | ||
| public ProductModel createProduct(String name, String description, BigDecimal price, Integer stock) { | ||
| ProductModel product = new ProductModel(name, description, price, stock); | ||
| return productRepository.save(product); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public ProductModel getProduct(Long id) { | ||
| return productRepository.find(id) | ||
| .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "[id = " + id + "] 상품을 찾을 수 없습니다.")); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public Page<ProductModel> getProducts(Pageable pageable) { | ||
| return productRepository.findAll(pageable); | ||
| } | ||
|
|
||
| @Transactional | ||
| public ProductModel updateProduct(Long id, String name, String description, BigDecimal price, Integer stock) { | ||
| ProductModel product = getProduct(id); | ||
| product.update(name, description, price, stock); | ||
| return product; | ||
| } | ||
|
|
||
| @Transactional | ||
| public void deleteProduct(Long id) { | ||
| ProductModel product = getProduct(id); | ||
| productRepository.delete(product); | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+48
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.loopers.infrastructure.product; | ||
|
|
||
| import com.loopers.domain.product.ProductModel; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface ProductJpaRepository extends JpaRepository<ProductModel, Long> { | ||
| Page<ProductModel> findAllByDeletedAtIsNull(Pageable pageable); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||||
| package com.loopers.infrastructure.product; | ||||||||
|
|
||||||||
| import com.loopers.domain.product.ProductModel; | ||||||||
| 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.stream.Collectors; | ||||||||
|
|
||||||||
| @RequiredArgsConstructor | ||||||||
| @Component | ||||||||
| public class ProductRepositoryImpl implements ProductRepository { | ||||||||
| private final ProductJpaRepository productJpaRepository; | ||||||||
|
|
||||||||
| @Override | ||||||||
| public Optional<ProductModel> find(Long id) { | ||||||||
| return productJpaRepository.findById(id); | ||||||||
|
||||||||
| return productJpaRepository.findById(id); | |
| return productJpaRepository.findById(id) | |
| .filter(product -> product.getDeletedAt() == null); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package com.loopers.interfaces.api.product; | ||
|
|
||
| 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; | ||
|
|
||
| @Tag(name = "Product V1 API", description = "상품 관리 API 입니다.") | ||
| public interface ProductV1ApiSpec { | ||
|
|
||
| @Operation( | ||
| summary = "상품 생성", | ||
| description = "새로운 상품을 생성합니다." | ||
| ) | ||
| ApiResponse<ProductV1Dto.ProductResponse> createProduct( | ||
| ProductV1Dto.CreateRequest request | ||
| ); | ||
|
|
||
| @Operation( | ||
| summary = "상품 조회", | ||
| description = "ID로 상품을 조회합니다." | ||
| ) | ||
| ApiResponse<ProductV1Dto.ProductResponse> getProduct( | ||
| @Schema(description = "상품 ID") | ||
| Long productId | ||
| ); | ||
|
|
||
| @Operation( | ||
| summary = "상품 목록 조회", | ||
| description = "상품 목록을 페이징하여 조회합니다." | ||
| ) | ||
| ApiResponse<ProductV1Dto.ProductListResponse> getProducts( | ||
| @Schema(description = "페이지 번호") | ||
| int page, | ||
| @Schema(description = "페이지 크기") | ||
| int size | ||
| ); | ||
|
|
||
| @Operation( | ||
| summary = "상품 수정", | ||
| description = "상품 정보를 수정합니다." | ||
| ) | ||
| ApiResponse<ProductV1Dto.ProductResponse> updateProduct( | ||
| @Schema(description = "상품 ID") | ||
| Long productId, | ||
| ProductV1Dto.UpdateRequest request | ||
| ); | ||
|
|
||
| @Operation( | ||
| summary = "상품 삭제", | ||
| description = "상품을 삭제합니다." | ||
| ) | ||
| ApiResponse<Object> deleteProduct( | ||
| @Schema(description = "상품 ID") | ||
| Long productId | ||
| ); | ||
| } |
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.
The error message is inconsistent with the validation being performed. When price is null, the message "가격은 0 이상이어야 합니다." (Price must be greater than or equal to 0) is misleading because the actual problem is that the price is null/missing, not that it's negative. The error message should be "가격은 필수입니다." or similar to indicate that the field is required.