Skip to content

feat: 품목으로 발주처 조회 및 발주처로 품목 리스트 조회 기능 구현#173

Merged
JoonKyoLee merged 10 commits intomainfrom
feat/product-vendor-relationship
Dec 4, 2025
Merged

feat: 품목으로 발주처 조회 및 발주처로 품목 리스트 조회 기능 구현#173
JoonKyoLee merged 10 commits intomainfrom
feat/product-vendor-relationship

Conversation

@JoonKyoLee
Copy link
Contributor

@JoonKyoLee JoonKyoLee commented Dec 4, 2025

✨ 작업 내용

  • 품목으로 발주처 조회 및 발주처로 품목 리스트 조회 기능 구현

📝 적용 범위

  • /vendor
  • /product

📌 참고 사항

Summary by CodeRabbit

  • 새로운 기능

    • 제품별 발주처 조회 API 엔드포인트 추가
    • 발주처별 제품 목록 조회 API 엔드포인트 추가
  • 테스트

    • 발주처-제품 관련 쿼리 기능에 대한 테스트 케이스 추가
    • 접근 권한 및 데이터 검증 시나리오 테스트 추가

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

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

coderabbitai bot commented Dec 4, 2025

Walkthrough

품목과 발주처 간의 양방향 조회 기능을 구현합니다. 특정 품목의 발주처를 조회하고, 특정 발주처의 품목 목록을 조회할 수 있는 두 가지 새로운 REST API 엔드포인트를 추가하며, 이에 필요한 서비스 로직, 저장소 메서드, 성공 메시지 상수를 함께 구현합니다.

Changes

코호트 / 파일(들) 변경 요약
API 응답 메시지
src/main/java/com/almang/inventory/global/api/SuccessMessage.java
발주처 관련 조회 기능을 위해 GET_VENDOR_PRODUCT_LIST_SUCCESS, GET_VENDOR_BY_PRODUCT_SUCCESS 두 개의 enum 상수 추가
컨트롤러 계층
src/main/java/com/almang/inventory/product/controller/ProductController.java, src/main/java/com/almang/inventory/vendor/controller/VendorController.java
ProductController에 GET /api/v1/product/{productId}/vendor 엔드포인트 추가; VendorController에 GET /{vendorId}/products 엔드포인트 추가 및 ProductService 의존성 주입
저장소 계층
src/main/java/com/almang/inventory/product/repository/ProductRepository.java
특정 매장과 발주처로 품목을 조회하는 findByStoreIdAndVendorId(Long, Long) 파생 쿼리 메서드 추가
서비스 계층
src/main/java/com/almang/inventory/product/service/ProductService.java
공개 메서드 getProductsByVendor(), getVendorByProduct() 추가; 비공개 검증 헬퍼 validateVendorAccess() 추가
테스트 코드
src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java, src/test/java/com/almang/inventory/product/service/ProductServiceTest.java, src/test/java/com/almang/inventory/vendor/controller/VendorControllerTest.java
품목으로 발주처 조회, 발주처로 품목 조회 관련 테스트 케이스 추가 (성공/실패 시나리오 포함)

Sequence Diagram

sequenceDiagram
    participant Client
    participant ProductCtrl as ProductController
    participant VendorCtrl as VendorController
    participant ProdService as ProductService
    participant ProdRepo as ProductRepository
    participant DB as Database

    rect rgb(200, 220, 240)
    Note over Client,DB: 품목으로 발주처 조회 흐름
    Client->>ProductCtrl: GET /api/v1/product/{productId}/vendor
    ProductCtrl->>ProdService: getVendorByProduct(productId, userId)
    ProdService->>ProdRepo: findById(productId)
    ProdRepo->>DB: SELECT * FROM product WHERE id = ?
    DB-->>ProdRepo: Product 객체
    ProdRepo-->>ProdService: Product
    ProdService->>ProdService: validateStoreAccess(product.store, userId)
    ProdService->>ProdService: getVendor from Product
    ProdService-->>ProductCtrl: VendorResponse
    ProductCtrl-->>Client: 200 OK + ApiResponse<VendorResponse>
    end

    rect rgb(230, 240, 200)
    Note over Client,DB: 발주처로 품목 리스트 조회 흐름
    Client->>VendorCtrl: GET /api/v1/vendor/{vendorId}/products
    VendorCtrl->>ProdService: getProductsByVendor(vendorId, userId)
    ProdService->>ProdService: validateVendorAccess(vendorId, store)
    ProdService->>ProdRepo: findByStoreIdAndVendorId(storeId, vendorId)
    ProdRepo->>DB: SELECT * FROM product WHERE store_id = ? AND vendor_id = ?
    DB-->>ProdRepo: List<Product>
    ProdRepo-->>ProdService: 제품 목록
    ProdService->>ProdService: Map to ProductResponse 리스트
    ProdService-->>VendorCtrl: List<ProductResponse>
    VendorCtrl-->>Client: 200 OK + ApiResponse<List<ProductResponse>>
    end
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20–25 minutes

특별히 검토할 영역:

  • validateVendorAccess() 헬퍼 메서드 — 발주처 존재 여부 및 매장 소유권 검증 로직이 정확히 구현되었는지 확인 필요 (VENDOR_NOT_FOUND, VENDOR_ACCESS_DENIED 예외 처리)
  • ProductRepository.findByStoreIdAndVendorId() — 쿼리 메서드의 스프링 데이터 파생 쿼리 문법이 정확하고 성능상 N+1 쿼리 문제가 없는지 검토
  • 크로스 도메인 의존성 — ProductService가 VendorController에 주입되고 있는데, 계층 간 책임 분리가 명확하고 순환 의존성이 없는지 확인
  • 테스트 커버리지 — 성공/실패 케이스, 권한 체크, 스토어 격리 등 엣지 케이스가 모두 포함되었는지 검증

Possibly Related PRs

Poem

🎯 품목에서 발주처를, 발주처에서 품목을,
양방향 조회의 다리를 놓다.
검증과 쿼리, 한 걸음 한 걸음,
기능의 정원을 가꾸네요. 🌱

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 구현된 핵심 기능(품목으로 발주처 조회 및 발주처로 품목 리스트 조회)을 명확하고 구체적으로 설명하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 모든 필수 섹션(✨ 작업 내용, 📝 적용 범위, 📌 참고 사항)을 포함하고 있습니다.
Linked Issues check ✅ Passed 코드 변경사항이 #172의 요구사항을 완전히 충족합니다: (1) 품목으로 발주처 조회(ProductController.getVendorByProduct) (2) 발주처로 품목 리스트 조회(VendorController.getProductsByVendor) 모두 구현.
Out of Scope Changes check ✅ Passed 모든 변경사항이 두 가지 핵심 기능(품목→발주처 조회, 발주처→품목 조회) 구현에만 집중되어 있으며, 범위를 벗어난 변경은 없습니다.
✨ 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/product-vendor-relationship

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

@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

🧹 Nitpick comments (2)
src/main/java/com/almang/inventory/product/repository/ProductRepository.java (1)

31-32: 파생 쿼리 메서드가 올바르게 작성되었습니다.

storeIdvendorId 조합으로 조회하는 것은 멀티테넌트 격리를 위한 좋은 설계입니다.

성능 고려사항: 이 쿼리가 자주 호출된다면 (store_id, vendor_id) 복합 인덱스 추가를 고려해보세요. 데이터 볼륨이 증가할수록 조회 성능에 영향을 줄 수 있습니다.

CREATE INDEX idx_product_store_vendor ON product(store_id, vendor_id);
src/test/java/com/almang/inventory/vendor/controller/VendorControllerTest.java (1)

731-749: 기본적인 엔드포인트 테스트가 구현되어 있습니다.

현재 빈 리스트(List.of())로 테스트하고 있는데, 실제 데이터가 포함된 케이스도 추가하면 응답 구조 검증이 더 완벽해집니다.

-        List<ProductResponse> response = List.of();
+        List<ProductResponse> response = List.of(
+                new ProductResponse(1L, "테스트 품목", "P-001", ProductUnit.G, true, 1000, 1500, 1200, 1L, vendorId)
+        );

         when(productService.getProductsByVendor(anyLong(), anyLong()))
                 .thenReturn(response);

         // when & then
         mockMvc.perform(get("/api/v1/vendor/{vendorId}/products", vendorId)
                         .with(authentication(auth()))
                         .contentType(MediaType.APPLICATION_JSON))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.status").value(200))
                 .andExpect(jsonPath("$.message").value(SuccessMessage.GET_VENDOR_PRODUCT_LIST_SUCCESS.getMessage()))
-                .andExpect(jsonPath("$.data").isArray());
+                .andExpect(jsonPath("$.data").isArray())
+                .andExpect(jsonPath("$.data.length()").value(1))
+                .andExpect(jsonPath("$.data[0].vendorId").value(vendorId));

이렇게 하면 응답 데이터 구조까지 검증할 수 있습니다. 단, 현재 구현도 기능적으로 문제없습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7915a5b and 45fd41a.

📒 Files selected for processing (8)
  • src/main/java/com/almang/inventory/global/api/SuccessMessage.java (1 hunks)
  • src/main/java/com/almang/inventory/product/controller/ProductController.java (2 hunks)
  • src/main/java/com/almang/inventory/product/repository/ProductRepository.java (2 hunks)
  • src/main/java/com/almang/inventory/product/service/ProductService.java (3 hunks)
  • src/main/java/com/almang/inventory/vendor/controller/VendorController.java (3 hunks)
  • src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java (2 hunks)
  • src/test/java/com/almang/inventory/product/service/ProductServiceTest.java (2 hunks)
  • src/test/java/com/almang/inventory/vendor/controller/VendorControllerTest.java (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-20T10:43:47.502Z
Learnt from: JoonKyoLee
Repo: almang2/inventory-server PR: 33
File: src/main/java/com/almang/inventory/product/domain/Product.java:62-66
Timestamp: 2025-11-20T10:43:47.502Z
Learning: In the almang2/inventory-server repository, Product entity update methods (e.g., updateVendor in src/main/java/com/almang/inventory/product/domain/Product.java) do not require null checks on vendor parameters because ProductService validates vendor existence via findVendorByIdAndValidateAccess before calling update methods, and the vendor field has nullable=false constraint ensuring this.vendor is never null for persisted entities.

Applied to files:

  • src/test/java/com/almang/inventory/product/service/ProductServiceTest.java
  • src/test/java/com/almang/inventory/vendor/controller/VendorControllerTest.java
  • src/main/java/com/almang/inventory/product/service/ProductService.java
  • src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java
  • src/main/java/com/almang/inventory/product/controller/ProductController.java
  • src/main/java/com/almang/inventory/vendor/controller/VendorController.java
  • src/main/java/com/almang/inventory/product/repository/ProductRepository.java
📚 Learning: 2025-11-22T18:12:13.172Z
Learnt from: JoonKyoLee
Repo: almang2/inventory-server PR: 66
File: src/main/java/com/almang/inventory/order/domain/OrderItem.java:41-53
Timestamp: 2025-11-22T18:12:13.172Z
Learning: In the almang2/inventory-server repository, OrderItem entity update methods (updateQuantity, updatePrice in src/main/java/com/almang/inventory/order/domain/OrderItem.java) do not require null checks because OrderService will validate parameters before calling these update methods, following the same pattern as Product entity updates.
<!--

Applied to files:

  • src/test/java/com/almang/inventory/product/service/ProductServiceTest.java
  • src/test/java/com/almang/inventory/vendor/controller/VendorControllerTest.java
  • src/main/java/com/almang/inventory/product/service/ProductService.java
  • src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java
  • src/main/java/com/almang/inventory/vendor/controller/VendorController.java
🔇 Additional comments (14)
src/test/java/com/almang/inventory/product/service/ProductServiceTest.java (4)

1252-1325: 테스트 커버리지가 잘 구성되어 있습니다! 👍

발주처별 품목 목록 조회 테스트가 다음을 잘 검증하고 있습니다:

  • 두 개의 발주처(vendor1, vendor2)를 생성하여 격리 테스트
  • vendorIdname 필드 검증을 통한 정확한 데이터 매핑 확인

1327-1353: 예외 케이스 테스트가 깔끔합니다.

존재하지 않는 발주처와 다른 상점의 발주처 접근에 대한 예외 처리 테스트가 적절히 구현되어 있습니다. VENDOR_NOT_FOUNDVENDOR_ACCESS_DENIED 에러 코드를 검증하는 방식이 일관성 있습니다.


1355-1390: 품목 → 발주처 조회 테스트 LGTM!

VendorResponse의 주요 필드들(vendorId, name, channel, storeId, activated)을 모두 검증하고 있어 데이터 무결성 확인이 충분합니다.


1392-1446: 접근 제어 테스트가 완벽합니다.

존재하지 않는 품목(PRODUCT_NOT_FOUND)과 다른 상점 품목 접근(STORE_ACCESS_DENIED)에 대한 예외 처리가 잘 검증되어 있습니다. 멀티테넌트 환경에서 중요한 보안 테스트입니다.

src/test/java/com/almang/inventory/product/controller/ProductControllerTest.java (2)

616-650: 컨트롤러 테스트가 체계적으로 구성되어 있습니다.

성공 케이스에서 HTTP 상태, 메시지, 응답 데이터 필드(vendorId, name, channel, phoneNumber, storeId, activated)를 모두 검증하고 있어 API 스펙 확인이 충분합니다.


652-686: 에러 핸들링 테스트가 일관성 있게 작성되었습니다.

PRODUCT_NOT_FOUND(404)와 STORE_ACCESS_DENIED(403) 케이스가 기존 패턴과 동일하게 테스트되어 있습니다. $.data가 존재하지 않음을 검증하는 것도 좋은 관행입니다.

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

39-40: 성공 메시지 상수가 일관성 있게 추가되었습니다.

기존 네이밍 컨벤션(GET_*_SUCCESS)을 잘 따르고 있으며, VENDOR 섹션에 적절히 배치되었습니다.

src/test/java/com/almang/inventory/vendor/controller/VendorControllerTest.java (1)

751-785: 에러 시나리오 테스트가 완벽합니다.

VENDOR_NOT_FOUNDVENDOR_ACCESS_DENIED 예외 처리가 기존 패턴과 일관성 있게 테스트되어 있습니다.

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

119-133: 새 엔드포인트가 기존 패턴과 일관성 있게 구현되었습니다! 🎉

  • 로깅에 userIdproductId 컨텍스트 포함
  • Swagger @Operation 어노테이션 적용
  • ApiResponse<VendorResponse> 반환 타입이 명확

RESTful 리소스 설계 관점에서 /product/{productId}/vendor 경로는 "품목의 발주처"라는 관계를 잘 표현하고 있습니다.

src/main/java/com/almang/inventory/vendor/controller/VendorController.java (2)

8-9: 의존성 주입이 깔끔합니다! 👍

VendorControllerProductService를 의존하는 크로스 도메인 의존성이지만, RESTful 관점에서 vendor의 하위 리소스(products)를 조회하는 엔드포인트이므로 적절한 설계 선택입니다. 생성자 주입을 통해 불변성도 보장되고 있네요.

Also applies to: 42-42


157-170: 엔드포인트 구현이 일관성 있게 잘 되어 있습니다!

기존 패턴을 정확히 따르고 있으며, 인증/로깅/응답 래핑이 모두 적절합니다. RESTful 설계 원칙에 따라 /vendor/{vendorId}/products로 하위 리소스를 표현한 점도 좋습니다.

참고: REST API 설계에서 리소스 간 관계를 표현할 때는 이렇게 경로를 중첩시키는 것이 표준 관례입니다. (RFC 3986 참고)

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

122-135: 보안 검증 플로우가 훌륭합니다!

발주처 접근 권한을 먼저 검증한 후 데이터를 조회하는 순서가 정확합니다. 이렇게 하면 권한이 없는 사용자가 쿼리 실행 전에 차단되어 불필요한 DB 부하를 방지할 수 있습니다.

추가 포인트:

  • @Transactional(readOnly = true)는 읽기 전용 힌트를 제공하여 성능 최적화에 도움을 줍니다
  • Stream API를 활용한 매핑이 깔끔하고 가독성이 좋네요

137-149: 깔끔한 구현입니다!

품목 검증 → Store 접근 권한 검증 → 발주처 반환의 흐름이 논리적이고 명확합니다. product.getVendor()에서 null 체크가 없는 것은 DB 스키마의 nullable=false 제약 조건 덕분에 안전하며, 기존 학습 내용과도 일치합니다.


194-201: 코드 중복을 제거한 좋은 리팩토링입니다!

기존의 findVendorByIdAndValidateAccess(Long, User) 메서드와 유사한 로직을 Store를 직접 받는 형태로 추출하여 getProductsByVendor에서 재사용할 수 있게 만들었네요. 단일 책임 원칙(SRP)을 잘 따르고 있습니다.

참고: 이런 식으로 검증 로직을 분리하면 테스트 작성도 더 쉬워지고, 향후 검증 규칙 변경 시 한 곳만 수정하면 됩니다.

@JoonKyoLee JoonKyoLee merged commit cf96790 into main Dec 4, 2025
1 check passed
@JoonKyoLee JoonKyoLee deleted the feat/product-vendor-relationship branch December 5, 2025 07:41
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