-
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
Conversation
- Add ProductModel entity with name, description, price, stock fields - Add ProductRepository interface for domain abstraction - Add ProductService with CRUD business logic Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ProductJpaRepository extending Spring Data JPA - Add ProductRepositoryImpl implementing domain repository - Support soft delete with findAllByDeletedAtIsNull Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ProductInfo record for application DTO - Add ProductFacade for coordinating service calls Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ProductV1Dto with Create/Update requests and responses
- Add ProductV1ApiSpec with OpenAPI annotations
- Add ProductV1Controller with CRUD endpoints:
- POST /api/v1/products (create)
- GET /api/v1/products/{id} (read single)
- GET /api/v1/products (read list with pagination)
- PUT /api/v1/products/{id} (update)
- DELETE /api/v1/products/{id} (soft delete)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Pull request overview
This PR implements a complete product management feature for a commerce API, including CRUD operations with soft-delete support and pagination.
Changes:
- Added Product domain model with validation for name, price, and stock
- Created REST API endpoints for creating, reading, updating, and deleting products
- Implemented pagination support for product listing
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| ProductV1Dto.java | Defines request/response DTOs with validation constraints for product API endpoints |
| ProductV1Controller.java | REST controller implementing CRUD operations for products |
| ProductV1ApiSpec.java | OpenAPI/Swagger specifications for product API documentation |
| ProductRepositoryImpl.java | Repository implementation with soft-delete filtering for paginated queries |
| ProductJpaRepository.java | JPA repository interface with custom query for soft-deleted products |
| ProductService.java | Domain service containing business logic for product operations |
| ProductRepository.java | Domain repository interface defining data access methods |
| ProductModel.java | Domain model entity with validation logic and soft-delete support |
| ProductInfo.java | Application layer DTO for transferring product data between layers |
| ProductFacade.java | Application facade orchestrating product operations |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (price == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다."); | ||
| } |
Copilot
AI
Jan 25, 2026
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.
| if (stock == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } |
Copilot
AI
Jan 25, 2026
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 stock is null, the message "재고는 0 이상이어야 합니다." (Stock must be greater than or equal to 0) is misleading because the actual problem is that the stock is null/missing, not that it's negative. The error message should be "재고는 필수입니다." or similar to indicate that the field is required.
|
|
||
| @Override | ||
| public Optional<ProductModel> find(Long id) { | ||
| return productJpaRepository.findById(id); |
Copilot
AI
Jan 25, 2026
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 find method does not filter out soft-deleted products, which could allow operations on deleted products. For example, calling getProduct, updateProduct, or deleteProduct on a deleted product ID would succeed, which is likely unintended behavior. Consider filtering soft-deleted products in the find method by returning an empty Optional if the product's deletedAt is not null, or add a dedicated method like findByIdAndDeletedAtIsNull in ProductJpaRepository.
| return productJpaRepository.findById(id); | |
| return productJpaRepository.findById(id) | |
| .filter(product -> product.getDeletedAt() == null); |
| 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 이상이어야 합니다."); | ||
| } | ||
| 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 이상이어야 합니다."); | ||
| } | ||
| if (price.compareTo(BigDecimal.ZERO) < 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다."); | ||
| } | ||
| if (stock == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } | ||
| if (stock < 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } | ||
|
|
||
| this.name = name; | ||
| this.description = description; | ||
| this.price = price; | ||
| this.stock = stock; | ||
| } | ||
| } |
Copilot
AI
Jan 25, 2026
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 ProductModel class lacks test coverage. Based on the test patterns in ExampleModelTest.java, this class should have unit tests covering:
- Successful product creation with valid inputs
- Validation failures for null/blank name
- Validation failures for null price
- Validation failures for negative price
- Validation failures for null stock
- Validation failures for negative stock
- Successful product updates
- Validation failures in the update method
| 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); | ||
| } | ||
| } |
Copilot
AI
Jan 25, 2026
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 ProductService class lacks integration test coverage. Based on the test patterns in ExampleServiceIntegrationTest.java, this class should have integration tests covering:
- Creating a product and verifying it's saved correctly
- Retrieving an existing product by ID
- Handling NOT_FOUND exception when retrieving a non-existent product
- Updating an existing product
- Deleting a product (soft delete)
- Verifying that deleted products are properly filtered in list queries
- Pagination functionality in getProducts
| if (price == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다."); | ||
| } |
Copilot
AI
Jan 25, 2026
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.
| if (stock == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } |
Copilot
AI
Jan 25, 2026
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 stock is null, the message "재고는 0 이상이어야 합니다." (Stock must be greater than or equal to 0) is misleading because the actual problem is that the stock is null/missing, not that it's negative. The error message should be "재고는 필수입니다." or similar to indicate that the field is required.
| 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 이상이어야 합니다."); | ||
| } | ||
| if (price.compareTo(BigDecimal.ZERO) < 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다."); | ||
| } | ||
| if (stock == null) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } | ||
| if (stock < 0) { | ||
| throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다."); | ||
| } |
Copilot
AI
Jan 25, 2026
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 validation logic in the constructor is duplicated in the update method (lines 60-75). This violates the DRY (Don't Repeat Yourself) principle and makes the code harder to maintain. Consider extracting the validation logic into a private method that both the constructor and update method can call.
📌 Summary
💬 Review Points
✅ Checklist
📎 References