Skip to content

Conversation

@discphy
Copy link
Member

@discphy discphy commented Jan 25, 2026

📌 Summary

  • 상품 기능 작성

💬 Review Points

✅ Checklist

📎 References

hanyoung-kurly and others added 4 commits January 25, 2026 13:01
- 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>
@discphy discphy self-assigned this Jan 25, 2026
@discphy discphy requested a review from Copilot January 25, 2026 04:41
@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

Copilot AI left a 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.

Comment on lines +64 to +66
if (price == null) {
throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다.");
}
Copy link

Copilot AI Jan 25, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +72
if (stock == null) {
throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다.");
}
Copy link

Copilot AI Jan 25, 2026

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.

Copilot uses AI. Check for mistakes.

@Override
public Optional<ProductModel> find(Long id) {
return productJpaRepository.findById(id);
Copy link

Copilot AI Jan 25, 2026

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.

Suggested change
return productJpaRepository.findById(id);
return productJpaRepository.findById(id)
.filter(product -> product.getDeletedAt() == null);

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +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 이상이어야 합니다.");
}
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;
}
}
Copy link

Copilot AI Jan 25, 2026

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:

  1. Successful product creation with valid inputs
  2. Validation failures for null/blank name
  3. Validation failures for null price
  4. Validation failures for negative price
  5. Validation failures for null stock
  6. Validation failures for negative stock
  7. Successful product updates
  8. Validation failures in the update method

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +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);
}
}
Copy link

Copilot AI Jan 25, 2026

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:

  1. Creating a product and verifying it's saved correctly
  2. Retrieving an existing product by ID
  3. Handling NOT_FOUND exception when retrieving a non-existent product
  4. Updating an existing product
  5. Deleting a product (soft delete)
  6. Verifying that deleted products are properly filtered in list queries
  7. Pagination functionality in getProducts

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +27
if (price == null) {
throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0 이상이어야 합니다.");
}
Copy link

Copilot AI Jan 25, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +33
if (stock == null) {
throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0 이상이어야 합니다.");
}
Copy link

Copilot AI Jan 25, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +75
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 이상이어야 합니다.");
}
Copy link

Copilot AI Jan 25, 2026

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.

Copilot uses AI. Check for mistakes.
@discphy discphy closed this Jan 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants