Skip to content

feat: 품목 등록 기능 구현#32

Merged
JoonKyoLee merged 15 commits intomainfrom
feat/create-product
Nov 20, 2025
Merged

feat: 품목 등록 기능 구현#32
JoonKyoLee merged 15 commits intomainfrom
feat/create-product

Conversation

@JoonKyoLee
Copy link
Contributor

@JoonKyoLee JoonKyoLee commented Nov 20, 2025

✨ 작업 내용

  • 품목 등록 기능 구현

📝 적용 범위

  • /product

📌 참고 사항

Summary by CodeRabbit

  • 새로운 기능

    • 품목 등록 API 추가: 이름, 코드, 단위, 중량·박스·단가 등 정보로 품목을 생성·관리할 수 있습니다.
    • 품목 단위(ML, G, BOX, EA) 지원.
  • 개선 사항

    • 성공 메시지 추가 및 응답 표준화로 등록 성공 피드백 강화.
    • 발주처 조회 실패 및 발주처 접근 권한 오류에 대해 명확한 오류 응답을 반환합니다.

✏️ Tip: You can customize this high-level summary in your review settings.

@JoonKyoLee JoonKyoLee self-assigned this Nov 20, 2025
@JoonKyoLee JoonKyoLee added the enhancement New feature or request label Nov 20, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 20, 2025

Walkthrough

상품 등록 REST 기능을 추가합니다: Product/Vendor 도메인 및 리포지토리, ProductUnit 열거형, CreateProductRequest/Response DTO, ProductController 및 ProductService(검증·권한·저장 로직)와 관련 테스트 및 글로벌 enum(에러/성공 메시지) 확장입니다.

Changes

Cohort / File(s) 변경 요약
Global 메시지/예외
src/main/java/com/almang/inventory/global/api/SuccessMessage.java, src/main/java/com/almang/inventory/global/exception/ErrorCode.java
SuccessMessageCREATE_PRODUCT_SUCCESS 추가; ErrorCodeVENDOR_NOT_FOUND, VENDOR_ACCESS_DENIED 추가
Product 도메인
src/main/java/com/almang/inventory/product/domain/Product.java, src/main/java/com/almang/inventory/product/domain/ProductUnit.java
Product JPA 엔티티 추가(다대일 Store/Vendor, 가격·중량·단위 등); ProductUnit enum(ML,G,BOX,EA) 추가
Product DTOs
src/main/java/com/almang/inventory/product/dto/request/CreateProductRequest.java, src/main/java/com/almang/inventory/product/dto/response/CreateProductResponse.java
CreateProductRequest 레코드(검증 어노테이션 포함) 추가; CreateProductResponse 레코드 및 from(Product) 정적 팩토리 추가
서비스/리포지토리
src/main/java/com/almang/inventory/product/repository/ProductRepository.java, src/main/java/com/almang/inventory/product/service/ProductService.java, src/main/java/com/almang/inventory/vendor/repository/VendorRepository.java
ProductRepository/VendorRepository 인터페이스 추가; ProductServicecreateProduct(request, userId) 추가(사용자/발주처 조회, 권한검증, 엔티티 변환 및 저장)
Vendor 도메인
src/main/java/com/almang/inventory/vendor/domain/Vendor.java, src/main/java/com/almang/inventory/vendor/domain/VendorChannel.java
Vendor JPA 엔티티 추가(Store 참조, 채널/연락처 등); VendorChannel enum 추가
컨트롤러
src/main/java/com/almang/inventory/product/controller/ProductController.java
POST /api/v1/product 엔드포인트 추가 — 인증 사용자로부터 요청 수신 후 ProductService.createProduct 호출 및 표준 ApiResponse 반환
테스트 구성 변경
src/test/java/com/almang/inventory/global/config/TestSecurityConfig.java, src/test/java/com/almang/inventory/global/security/jwt/JwtTokenProviderTest.java
테스트용 TestSecurityConfig 패키지 경로를 com.almang.inventory.global.config로 이동(패키지 선언 변경)
테스트 추가/수정
src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java, src/test/java/com/almang/inventory/product/service/ProductServiceTest.java, 기타 테스트 파일들
ProductController/Service 테스트 추가(성공·예외·검증 케이스); 기존 테스트들의 TestSecurityConfig import 경로 업데이트

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ProductController
    participant ProductService
    participant UserRepository
    participant VendorRepository
    participant ProductRepository
    participant Database

    Client->>ProductController: POST /api/v1/product (CreateProductRequest)
    ProductController->>ProductController: 인증 사용자 ID 추출
    ProductController->>ProductService: createProduct(request, userId)

    ProductService->>UserRepository: findById(userId)
    alt 사용자 없음
        UserRepository-->>ProductService: 없음
        ProductService-->>ProductController: 예외(USER_NOT_FOUND)
        ProductController-->>Client: 404
    else 사용자 존재
        UserRepository-->>ProductService: User
        ProductService->>VendorRepository: findById(vendorId)
        alt 발주처 없음
            VendorRepository-->>ProductService: 없음
            ProductService-->>ProductController: 예외(VENDOR_NOT_FOUND)
            ProductController-->>Client: 404
        else 발주처 존재
            VendorRepository-->>ProductService: Vendor
            ProductService->>ProductService: 권한검증(VENDOR_ACCESS_DENIED 가능)
            ProductService->>ProductRepository: save(product)
            ProductRepository->>Database: INSERT products
            Database-->>ProductRepository: saved Product
            ProductRepository-->>ProductService: Product
            ProductService-->>ProductController: CreateProductResponse
            ProductController-->>Client: 200 (ApiResponse with CREATE_PRODUCT_SUCCESS)
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 분

검토 시 주의사항:

  • Product ↔ Store/Vendor 관계: FK 컬럼명과 fetch 전략(LAZY) 및 nullable 제약이 DB·마이그레이션과 일치하는지 확인하세요. (원인: JPA 매핑 불일치 시 N+1 또는 FK 오류 발생) — 개선: DDL/엔티티 주석과 실제 스키마 비교.
  • 권한 검증 로직: Vendor가 사용자의 Store에 속하는지 비교하는 조건과 예외 타입(VENDOR_ACCESS_DENIED) 경계 확인 필요. (원인: 잘못된 비교로 권한 우회 가능) — 개선: 테스트에 경계 케이스(동일 ID지만 다른 엔티티 인스턴스 등) 추가.
  • 입력 값 검증: 비용/가격 필드 타입(int vs BigDecimal)과 Min(0) 적용 일관성 검토. (원인: 금액 정밀도 및 음수 허용 문제) — 개선: 비즈니스 요구에 따라 BigDecimal 사용 고려 및 DTO/엔티티 정합성 확인.
  • 테스트 네임스페이스 변경 영향: 테스트 구성 패키지 이동으로 인한 클래스패스 이슈 점검.

Possibly related PRs

  • #27 — 유사한 SuccessMessage 수정: 같은 enum 파일에 상수 추가를 다루므로 병합 충돌 가능성 있음.
  • #23DELETE_USER_SUCCESS가 동일 enum에 추가된 PR으로, enum 편집 충돌 가능성 관련.
  • #8 — 전역 에러/성공 enum을 함께 변경한 PR로 네이밍·메시지 일관성 검토 대상.

Poem

새 상품이 레코드로 태어나고,
Vendor와 손 잡고 저장소로 향하네.
유효성 검사 통과하면 CREATE_PRODUCT_SUCCESS 🎉
못 찾으면 친절히 NOT_FOUND라 하고,
커밋엔 로그 한 줄, 테스트엔 웃음 한 줄 😄

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning 불필요한 범위 외 변경사항이 있습니다. TestSecurityConfig의 패키지 이동은 품목 등록 기능과 무관합니다. TestSecurityConfig 패키지 경로 변경은 별도 PR로 분리하거나, 해당 변경사항이 기본 인프라 재구성의 일부라면 명확히 문서화하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 품목 등록 기능 구현이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿의 모든 필수 섹션(작업 내용, 적용 범위, 참고 사항)을 포함하고 있습니다.
Linked Issues check ✅ Passed 모든 코드 변경사항이 이슈 #28의 품목 등록 기능 구현 요구사항을 충족합니다(엔티티, DTO, 컨트롤러, 서비스, 테스트 포함).
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/create-product

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
src/main/java/com/almang/inventory/vendor/domain/VendorChannel.java (1)

3-8: 발주처 채널 열거형이 깔끔하게 정의되었습니다.

현재 구현은 충분히 명확합니다. 향후 API 문서화나 클라이언트 연동 시 각 채널에 대한 한글 설명이나 메타데이터를 추가하는 것을 고려해보세요 (예: @JsonValue, 설명 필드 추가 등).

선택적 개선 예시:

public enum VendorChannel {
    KAKAO("카카오톡"),
    EMAIL("이메일"),
    WEB("웹"),
    MESSAGE("문자메시지");
    
    private final String description;
    
    VendorChannel(String description) {
        this.description = description;
    }
}
src/main/java/com/almang/inventory/product/domain/ProductUnit.java (1)

3-5: 품목 단위 열거형이 잘 정의되었습니다.

ML, G, BOX, EA 모두 일반적으로 이해 가능한 단위입니다. VendorChannel과 유사하게, 사용자 인터페이스에 표시할 때를 대비해 한글 설명을 추가하는 것을 추후 고려해보세요.

선택적 개선 예시:

public enum ProductUnit {
    ML("ml"),
    G("g"),
    BOX("박스"),
    EA("개");
    
    private final String displayName;
    
    ProductUnit(String displayName) {
        this.displayName = displayName;
    }
}
src/main/java/com/almang/inventory/vendor/repository/VendorRepository.java (1)

1-7: Vendor용 기본 리포지토리 정의 무난합니다

현재 스펙(품목 등록 시 거래처 존재 여부/조회)만 보면, 별도 커스텀 메서드 없이 기본 CRUD 리포지토리만 정의해 둔 선택이 과하지 않고 적절해 보입니다. 이후 “상점별 거래처 목록 조회”, “이름/채널 기준 검색” 같은 요구가 생기면, 이 인터페이스에 메서드 시그니처만 추가해 나가면 되어 확장도 용이하겠습니다.

src/main/java/com/almang/inventory/vendor/domain/Vendor.java (1)

1-40: Vendor 엔티티 설계가 전체 도메인과 잘 맞습니다

  • Store와의 연관관계를 LAZY로 두고, 이름/채널/연락처/활성화 여부 등 최소 필수 정보만 명확히 컬럼 제약으로 잡은 구조가 깨끗합니다.
  • Product 엔티티 쪽 vendor 필드와도 매핑이 자연스러워서, 품목 등록/조회 시 양쪽 모델이 어색하지 않게 연결될 것 같습니다.

추가로, 나중에 거래처 검색이나 관리 화면이 생기면

  • 자주 검색할 컬럼(name, channel, isActivate 등)에 인덱스가 필요한지,
  • 도메인 규칙(예: 이름/연락처 길이, channel별 필수값)이 더 세분화되어야 하는지
    를 한 번 더 점검해보시면 장기적인 성능/유효성 측면에서 도움이 될 것 같습니다. (엔티티 레벨보다는 DB 설계/요구사항 논의 쪽 이슈에 가깝습니다.)
src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java (1)

163-190: 유효성 검증 테스트가 잘 작성되었습니다.

Bean Validation이 제대로 동작하는지 확인하는 테스트입니다. 다만, 비즈니스 로직 관점에서 추가 검증이 필요할 수 있습니다:

  • unitPerBox가 0인 경우: 박스당 개수가 0이면 논리적으로 문제가 될 수 있습니다.
  • costPrice, retailPrice, wholesalePrice가 0인 경우: 가격이 0원인 상품이 비즈니스적으로 허용되는지 검토가 필요합니다.

CreateProductRequest@Min(1) 같은 추가 검증 어노테이션을 고려해보세요.

예시:

public record CreateProductRequest(
        @NotNull Long vendorId,
        @NotBlank String name,
        @NotBlank String code,
        @NotNull ProductUnit unit,
        BigDecimal boxWeightG,
        @Min(1) int unitPerBox,  // 최소 1개 이상
        BigDecimal unitWeightG,
        @Min(0) int costPrice,   // 음수 방지
        @Min(0) int retailPrice,
        @Min(0) int wholesalePrice
) {}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d71f67a and f75e965.

📒 Files selected for processing (21)
  • src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1 hunks)
  • src/main/java/com/almang/inventory/global/exception/ErrorCode.java (1 hunks)
  • src/main/java/com/almang/inventory/product/controller/ProductController.java (1 hunks)
  • src/main/java/com/almang/inventory/product/domain/Product.java (1 hunks)
  • src/main/java/com/almang/inventory/product/domain/ProductUnit.java (1 hunks)
  • src/main/java/com/almang/inventory/product/dto/request/CreateProductRequest.java (1 hunks)
  • src/main/java/com/almang/inventory/product/dto/response/CreateProductResponse.java (1 hunks)
  • src/main/java/com/almang/inventory/product/repository/ProductRepository.java (1 hunks)
  • src/main/java/com/almang/inventory/product/service/ProductService.java (1 hunks)
  • src/main/java/com/almang/inventory/vendor/domain/Vendor.java (1 hunks)
  • src/main/java/com/almang/inventory/vendor/domain/VendorChannel.java (1 hunks)
  • src/main/java/com/almang/inventory/vendor/repository/VendorRepository.java (1 hunks)
  • src/test/java/com/almang/inventory/admin/controller/AdminControllerTest.java (1 hunks)
  • src/test/java/com/almang/inventory/global/config/TestSecurityConfig.java (1 hunks)
  • src/test/java/com/almang/inventory/global/security/jwt/JwtTokenProviderTest.java (1 hunks)
  • src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java (1 hunks)
  • src/test/java/com/almang/inventory/product/service/ProductServiceTest.java (1 hunks)
  • src/test/java/com/almang/inventory/store/admin/controller/StoreAdminControllerTest.java (1 hunks)
  • src/test/java/com/almang/inventory/store/controller/StoreControllerTest.java (1 hunks)
  • src/test/java/com/almang/inventory/user/auth/controller/AuthControllerTest.java (1 hunks)
  • src/test/java/com/almang/inventory/user/controller/UserControllerTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/com/almang/inventory/vendor/domain/Vendor.java (1)
src/main/java/com/almang/inventory/product/domain/Product.java (1)
  • Entity (11-62)
src/main/java/com/almang/inventory/product/domain/Product.java (1)
src/main/java/com/almang/inventory/vendor/domain/Vendor.java (1)
  • Entity (8-40)
🔇 Additional comments (18)
src/main/java/com/almang/inventory/global/exception/ErrorCode.java (1)

27-29: LGTM! 발주처 에러 코드가 깔끔하게 추가되었습니다.

기존 패턴과 일관되게 작성되었고, 메시지도 명확합니다.

src/test/java/com/almang/inventory/user/auth/controller/AuthControllerTest.java (1)

15-15: 패키지 리팩토링이 올바르게 적용되었습니다.

테스트 설정을 글로벌 스코프로 이동하는 것은 좋은 구조 개선입니다.

src/test/java/com/almang/inventory/global/security/jwt/JwtTokenProviderTest.java (1)

1-1: 패키지 구조 정리가 잘 되었습니다.

글로벌 컴포넌트에서 "store" 프리픽스를 제거한 것이 더 명확한 구조입니다.

src/test/java/com/almang/inventory/admin/controller/AdminControllerTest.java (1)

13-13: 일관된 패키지 구조 개선입니다.

다른 테스트 파일들과 동일하게 적용되어 통일성이 좋습니다.

src/test/java/com/almang/inventory/global/config/TestSecurityConfig.java (1)

1-1: 테스트 보안 설정의 위치가 적절하게 변경되었습니다.

글로벌 설정으로 이동하여 재사용성이 향상되었고, 모든 의존 파일들도 함께 업데이트되었습니다.

src/test/java/com/almang/inventory/user/controller/UserControllerTest.java (1)

16-16: 패키지 리팩토링이 완료되었습니다.

모든 테스트 파일들이 일관되게 업데이트되어 프로젝트 구조가 깔끔해졌습니다. 👍

src/test/java/com/almang/inventory/store/admin/controller/StoreAdminControllerTest.java (1)

14-27: 테스트 시큐리티 설정 경로 정리 좋습니다 👌

테스트용 시큐리티 설정을 global.config로 통합해서 가져오는 방향이어서, 다른 테스트들과 일관성이 생겨 유지보수에 유리해 보입니다. 이 파일 기준으로는 추가 수정 필요 없어 보입니다.

src/test/java/com/almang/inventory/store/controller/StoreControllerTest.java (1)

17-35: StoreController 테스트의 시큐리티 설정 정리 👍

TestSecurityConfig를 공용 패키지에서 import하도록 정리해서, 시큐리티 관련 테스트 설정이 한 곳에서 관리되도록 한 점이 좋습니다. @Import(TestSecurityConfig.class)와도 잘 맞물려서 구조적으로 깔끔합니다.

src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1)

22-26: 상품 등록용 성공 메시지 enum 추가 구성 괜찮습니다

USER 섹션 뒤에 // PRODUCT 섹션을 분리해서 CREATE_PRODUCT_SUCCESS를 추가한 구조가 도메인별로 읽기 좋아서 유지보수 측면에서 좋습니다. 컨트롤러 응답에서 이 enum을 그대로 사용하면 메시지 관리 일원화에도 도움이 되니, 현재 ProductController에서도 이 상수를 사용하고 있는지만 한 번만 같이 맞춰보시면 좋겠습니다.

src/main/java/com/almang/inventory/product/repository/ProductRepository.java (1)

6-7: 깔끔한 리포지토리 구현입니다! 👍

Spring Data JPA의 표준 패턴을 잘 따르고 있습니다. 현재 단계에서는 기본 CRUD 작업만으로 충분하며, 향후 복잡한 쿼리가 필요할 때 커스텀 메서드를 추가하면 됩니다.

src/main/java/com/almang/inventory/product/controller/ProductController.java (1)

30-43: 컨트롤러 구현이 깔끔합니다! 🎯

관심사의 분리가 잘 되어 있고, 유효성 검증, 인증, 로깅이 적절하게 적용되었습니다.

Line 36에서 userPrincipal.getId()를 호출하는데, Spring Security 설정에서 인증되지 않은 요청을 차단하고 있다면 문제없지만, 방어적 코딩 관점에서 null 체크를 고려할 수 있습니다. 다만 현재 보안 설정이 이를 보장한다면 현재 코드로 충분합니다.

src/main/java/com/almang/inventory/product/service/ProductService.java (2)

27-37: 트랜잭션 처리와 로깅이 적절합니다! 👍

@Transactional을 통한 트랜잭션 관리와 생성 전후 로깅이 잘 구현되어 있습니다. 생성된 품목의 ID를 로깅하는 것도 추적성 측면에서 좋습니다.


58-66: 예외 처리 패턴이 일관되고 명확합니다.

orElseThrow를 사용한 간결한 예외 처리 패턴이 코드 가독성을 높이고 있습니다.

src/test/java/com/almang/inventory/product/service/ProductServiceTest.java (2)

39-72: 테스트 헬퍼 메서드가 잘 구성되어 있습니다.

newStore(), newVendor(), newUser() 헬퍼 메서드들이 테스트 데이터 생성을 깔끔하게 추상화하고 있습니다. 테스트 코드의 가독성과 유지보수성이 좋습니다.


74-110: 성공 케이스 테스트가 포괄적입니다! ✅

모든 필드를 세밀하게 검증하고 있으며, BigDecimal 비교에 isEqualByComparingTo를 사용한 것이 정확합니다.

src/main/java/com/almang/inventory/product/domain/Product.java (2)

24-30: 엔티티 관계 설정이 적절합니다.

StoreVendor에 대한 ManyToOne 관계가 지연 로딩(LAZY)으로 잘 설정되어 있고, nullable = false로 필수 관계임을 명시했습니다. 이는 성능과 데이터 무결성 측면에서 좋은 선택입니다.

참고로, 향후 Vendor에서 Store 소유권을 검증하는 로직이 ProductService에 추가되어야 합니다. (별도 코멘트 참조)


54-61: 가격 필드들이 int 타입으로 선언되었습니다.

가격을 정수형으로 저장하는 것은 소수점이 필요 없는 원화 기준으로는 적절합니다. 다만 향후 다른 통화를 지원하거나 할인율 계산 등이 필요하면 BigDecimal을 고려할 수 있습니다.

현재 요구사항에는 적합해 보입니다.

src/main/java/com/almang/inventory/product/dto/response/CreateProductResponse.java (1)

21-36: 코드 검증 완료 - 현재 구현은 안전합니다.

검증 결과, 원본 리뷰 코멘트의 우려사항은 이론상 타당하지만 현재 구현에서는 문제가 없습니다:

  • StoreVendor는 실제로 LAZY 로딩으로 설정됨
  • CreateProductResponse.from()ProductService.createProduct() 내에서만 호출됨
  • createProduct() 메서드는 @Transactional 컨텍스트 내에서 실행
  • Product 객체가 저장되어 세션에 연결되어 있으므로, 지연 로딩된 연관관계(Store, Vendor)에 접근 가능

현재 코드는 LazyInitializationException이 발생할 위험이 없습니다. 리뷰의 원본 의견처럼 "현재는 문제없으나"라는 평가가 정확하며, 향후 유지보수를 위해 문서화하거나 필요시 fetch join을 도입하는 것은 선택적 개선사항입니다.

Comment on lines +45 to +46
@Column(name = "unit_per_box")
private Integer unitPerBox;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

타입 불일치 발견: unitPerBox의 타입 불일치가 있습니다.

Line 46에서 unitPerBoxInteger(nullable)로 선언되었지만, CreateProductRequest(line 14)와 CreateProductResponse(line 13)에서는 int(primitive)로 선언되어 있습니다.

문제점:

  • 엔티티는 null을 허용하지만, DTO는 기본값 0을 가집니다.
  • 비즈니스 로직에서 "값이 없음"과 "값이 0"을 구분해야 한다면 Integer가 맞고, 항상 값이 있어야 한다면 int가 맞습니다.

권장사항:
비즈니스 요구사항에 따라 통일하세요:

  1. 박스당 개수가 필수라면: 엔티티를 int로 변경하고 nullable = false 추가
  2. 선택 사항이라면: DTO도 Integer로 변경

예시 (필수 필드로 통일):

-    @Column(name = "unit_per_box")
-    private Integer unitPerBox;
+    @Column(name = "unit_per_box", nullable = false)
+    private int unitPerBox;
🤖 Prompt for AI Agents
In src/main/java/com/almang/inventory/product/domain/Product.java around lines
45-46, unitPerBox is declared as Integer but DTOs use primitive int causing a
type/nullability mismatch; decide which semantics you need and make the types
consistent: if unitPerBox is required change the entity to int and add nullable
= false on the @Column, update any constructors/mappers to use primitive int; if
optional change CreateProductRequest/CreateProductResponse to Integer and adjust
validation/mappers to handle null. Also update any tests and mapping code to
reflect the chosen type.

Comment on lines +139 to +164
@Test
void 발주처가_존재하지_않으면_품목_생성시_예외가_발생한다() {
// given
Store store = newStore();
User user = newUser(store);

Long notExistVendorId = 9999L;

CreateProductRequest request = new CreateProductRequest(
notExistVendorId,
"고체치약",
"P-001",
ProductUnit.G,
BigDecimal.valueOf(1000.0),
10,
BigDecimal.valueOf(100.0),
1000,
1500,
1200
);

// when & then
assertThatThrownBy(() -> productService.createProduct(request, user.getId()))
.isInstanceOf(BaseException.class)
.hasMessageContaining(ErrorCode.VENDOR_NOT_FOUND.getMessage());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

테스트 커버리지 추가 필요: 발주처 소유권 검증 테스트가 누락되었습니다.

현재 테스트는 존재하지 않는 발주처(9999L)에 대한 검증만 수행합니다. 하지만 다른 상점의 발주처를 사용하는 경우에 대한 테스트가 없습니다.

ProductService에서 발주처 소유권 검증을 추가한다면, 해당 검증을 테스트하는 케이스를 추가해야 합니다.

추가할 테스트 예시:

@Test
void 다른_상점의_발주처로_품목_생성시_예외가_발생한다() {
    // given
    Store store1 = newStore();
    Store store2 = storeRepository.save(
            Store.builder()
                    .name("다른 상점")
                    .isActivate(true)
                    .defaultCountCheckThreshold(BigDecimal.valueOf(0.2))
                    .build()
    );
    
    Vendor vendorOfStore2 = newVendor(store2); // 상점2의 발주처
    User userOfStore1 = newUser(store1);        // 상점1의 사용자

    CreateProductRequest request = new CreateProductRequest(
            vendorOfStore2.getId(),  // 다른 상점의 발주처!
            "고체치약",
            "P-001",
            ProductUnit.G,
            BigDecimal.valueOf(1000.0),
            10,
            BigDecimal.valueOf(100.0),
            1000,
            1500,
            1200
    );

    // when & then
    assertThatThrownBy(() -> productService.createProduct(request, userOfStore1.getId()))
            .isInstanceOf(BaseException.class)
            .hasMessageContaining(ErrorCode.VENDOR_NOT_FOUND.getMessage());
}
🤖 Prompt for AI Agents
In src/test/java/com/almang/inventory/product/service/ProductServiceTest.java
around lines 139 to 164, add a new test that verifies vendor ownership
validation: create a second Store (persisted), create a Vendor tied to that
second store, create a User for the original store, build a CreateProductRequest
that uses the other-store vendor's ID, then call
productService.createProduct(request, userOfOriginalStore.getId()) and assert it
throws BaseException with ErrorCode.VENDOR_NOT_FOUND.getMessage(); use existing
helpers (newStore/newVendor/newUser) or repository.save() to persist entities as
in other tests.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/com/almang/inventory/product/dto/request/CreateProductRequest.java (1)

10-21: 무게(BigDecimal) 필드에 대한 최소값 검증을 추가하면 더 안전합니다

unitPerBox와 가격 필드에 검증을 추가하신 건 아주 좋습니다. 다만 boxWeightG, unitWeightG는 여전히 음수 값이 들어갈 수 있어서, 도메인 의미상 방어가 조금 아쉬운 상태입니다.

실제 업무에서 “상자 무게가 -100g” 같은 값은 의미가 없으므로, 다음과 같이 0 이상 제약을 걸어두면 API 레벨에서 조기에 잘못된 입력을 걸러낼 수 있습니다.

-import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.Min;
@@
-        @NotNull ProductUnit unit,
-        BigDecimal boxWeightG,
+        @NotNull ProductUnit unit,
+        @DecimalMin("0") BigDecimal boxWeightG,
         @Positive int unitPerBox,
-        BigDecimal unitWeightG,
+        @DecimalMin("0") BigDecimal unitWeightG,

정리하면, “무게는 0 이상”이라는 도메인 규칙을 DTO 레벨에서 명시해두면 추후 서비스/엔티티 검증 로직도 한결 단순해집니다 🙂

🧹 Nitpick comments (2)
src/main/java/com/almang/inventory/global/exception/ErrorCode.java (1)

28-30: VENDOR_ACCESS_DENIED의 HttpStatus를 FORBIDDEN(403)으로 맞추는 것 검토 제안

VENDOR_ACCESS_DENIED는 "해당 상점의 발주처가 아닙니다."라는 권한/소유권 문제라서, 클라이언트 요청 형식 오류(400)보다는 보통 HttpStatus.FORBIDDEN(403) 쪽이 REST 관례에 더 가깝습니다.

API 일관성과 의미 전달을 위해 아래와 같이 변경을 한 번 검토해 보시면 좋겠습니다.

-    VENDOR_ACCESS_DENIED(HttpStatus.BAD_REQUEST, "해당 상점의 발주처가 아닙니다."),
+    VENDOR_ACCESS_DENIED(HttpStatus.FORBIDDEN, "해당 상점의 발주처가 아닙니다."),

짧게 말하면, "요청이 이상한 것(400)"이 아니라 "너는 이 발주처를 쓸 수 없음(403)"에 더 가깝습니다 🙂

src/test/java/com/almang/inventory/product/service/ProductServiceTest.java (1)

74-110: CreateProductRequest 생성 로직 중복 제거 제안 (테스트 유지보수성 향상)

테스트 시나리오는 네 가지 모두 잘 잡혀 있어서 서비스 동작을 꽤 탄탄하게 검증하고 있습니다. 특히 마지막 다른_상점의_발주처로_품목_생성시_예외가_발생한다까지 있어서 권한 이슈도 잘 커버됐어요. 굿입니다 😄

다만 CreateProductRequest 생성 코드가 각 테스트마다 거의 동일하게 반복되고 있어, 필드가 추가/변경되면 네 군데를 모두 수정해야 하는 구조입니다.

예를 들어 아래와 같이 헬퍼 메서드를 하나 두면 중복을 줄이고, 나중에 필드 스펙이 바뀌어도 한 곳만 수정하면 되어 유지보수성이 좋아집니다.

private CreateProductRequest newProductRequest(Long vendorId) {
    return new CreateProductRequest(
            vendorId,
            "고체치약",
            "P-001",
            ProductUnit.G,
            BigDecimal.valueOf(1000.0),
            10,
            BigDecimal.valueOf(100.0),
            1000,
            1500,
            1200
    );
}

그리고 각 테스트에서는 다음처럼 간결하게 사용할 수 있습니다.

CreateProductRequest request = newProductRequest(vendor.getId());

테스트 코드도 “DRY” 원칙을 지켜두면, 기능 확장 시 가장 먼저 도움을 주는 친구가 됩니다 🙂

Also applies to: 112-137, 139-164, 166-198

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f75e965 and 8b74f24.

📒 Files selected for processing (4)
  • src/main/java/com/almang/inventory/global/exception/ErrorCode.java (1 hunks)
  • src/main/java/com/almang/inventory/product/dto/request/CreateProductRequest.java (1 hunks)
  • src/main/java/com/almang/inventory/product/service/ProductService.java (1 hunks)
  • src/test/java/com/almang/inventory/product/service/ProductServiceTest.java (1 hunks)
🔇 Additional comments (1)
src/main/java/com/almang/inventory/product/service/ProductService.java (1)

27-37: 발주처 소유권 검증까지 포함된 createProduct 플로우가 잘 설계되었습니다

createProducttoEntityfindUserById / findVendorById의 흐름이 깔끔하게 분리되어 있고, 특히 아래 두 부분이 좋습니다.

  • findVendorById에서 VENDOR_NOT_FOUND로 예외를 감싸 도메인 의미를 분명히 한 점
  • toEntity에서 vendor.getStore().getId().equals(user.getStore().getId())발주처 소유권을 서비스 레이어에서 한 번 더 방어하는 점 (권한 상승 방지)

이 구조 덕분에 컨트롤러는 “정상 플로우”에만 집중하고, 예외 케이스는 서비스에서 책임지는 형태가 되어 책임 분리가 잘 되어 있습니다.
현재 요구사항 범위 내에서는 추가적인 리팩터링 없이 그대로 가셔도 충분해 보입니다 👌

Also applies to: 39-60, 62-70

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 품목 등록 기능 구현

1 participant