Skip to content

fix: 발주 아이템 관련 엔드포인트 구조 수정#97

Merged
JoonKyoLee merged 2 commits intomainfrom
fix/receipt-item-endpoints
Nov 24, 2025
Merged

fix: 발주 아이템 관련 엔드포인트 구조 수정#97
JoonKyoLee merged 2 commits intomainfrom
fix/receipt-item-endpoints

Conversation

@JoonKyoLee
Copy link
Contributor

@JoonKyoLee JoonKyoLee commented Nov 24, 2025

✨ 작업 내용

  • 발주 아이템 관련 엔드포인트 구조 수정

📝 적용 범위

  • /receipt

📌 참고 사항

Summary by CodeRabbit

릴리스 노트

  • 개선 사항

    • 영수증 항목 관련 API 엔드포인트 구조를 개선하여 더 명확한 계층적 관계를 반영하도록 변경했습니다.
    • 영수증 항목 조회, 수정, 삭제 시 접근 검증 로직을 강화했습니다.
  • 테스트

    • 업데이트된 API 엔드포인트 구조에 맞춰 모든 관련 테스트를 수정했습니다.

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

@JoonKyoLee JoonKyoLee self-assigned this Nov 24, 2025
@JoonKyoLee JoonKyoLee added the bug Something isn't working label Nov 24, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 24, 2025

Walkthrough

발주 아이템 관련 엔드포인트를 재구성하여 receiptId를 경로 및 서명에 추가했습니다. /{receiptId}/items/{receiptItemId} 경로 구조로 변경되었으며, 검증 로직이 Receipt 기반으로 개선되어 더 안전한 접근 제어를 구현합니다.

Changes

Cohort / File(s) 요약
컨트롤러 엔드포인트 재구성
src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java
아이템 조회, 수정, 삭제 엔드포인트 경로를 /receipt/{receiptItemId}에서 /{receiptId}/items/{receiptItemId}로 변경. 메서드 서명에 @PathVariable Long receiptId 추가하고 서비스 호출 인자 구성을 업데이트.
서비스 메서드 서명 및 검증 로직 변경
src/main/java/com/almang/inventory/receipt/service/ReceiptService.java
getReceiptItem, updateReceiptItem, deleteReceiptItem 메서드 서명에 receiptId 파라미터 추가. 접근 검증을 Receipt 기반으로 변경하고 findReceiptItemByIdAndValidateStoreAccess 헬퍼 메서드 제거. 로깅 문구 개선.
컨트롤러 테스트 업데이트
src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java
아이템 엔드포인트 경로를 새 구조로 수정하고 mockMvc 요청 구성 업데이트. 서비스 메서드 모의 객체를 3개 인자 버전으로 확장. 응답 검증에서 receiptId 포함.
서비스 테스트 업데이트
src/test/java/com/almang/inventory/receipt/service/ReceiptServiceTest.java
아이템 관련 메서드 호출을 새 서명에 맞춰 수정. 테스트 설정 단계에서 Receipt 생성 후 ID를 사용. 에러 코드 기대값을 RECEIPT_ACCESS_DENIED로 조정.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as ReceiptController
    participant Service as ReceiptService
    participant Repo as Repository
    
    rect rgb(220, 240, 250)
    Note over Client,Repo: 이전 방식 (receiptId 없음)
    Client->>Controller: GET /receipt/{receiptItemId}
    Controller->>Service: getReceiptItem(receiptItemId, userId)
    Service->>Repo: Store 기반 아이템 조회
    Repo-->>Service: ReceiptItem
    end
    
    rect rgb(240, 250, 240)
    Note over Client,Repo: 개선된 방식 (receiptId 포함)
    Client->>Controller: GET /{receiptId}/items/{receiptItemId}
    Controller->>Service: getReceiptItem(receiptId, receiptItemId, userId)
    Service->>Repo: Receipt 조회 및 접근 검증
    Repo-->>Service: Receipt
    Service->>Repo: Receipt 내 아이템 검증
    Repo-->>Service: ReceiptItem
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25분

추가 검토 필요 영역:

  • ReceiptService.java의 검증 로직: Receipt 기반 접근 제어 변경이 핵심입니다. 이전의 Store 기반 검증에서 Receipt 기반 검증으로 전환되었으므로, 모든 경로에서 올바른 계층 관계가 유지되는지 확인 필요합니다.
  • 제거된 헬퍼 메서드 (findReceiptItemByIdAndValidateStoreAccess): 다른 서비스나 테스트에서 이 메서드를 직접 호출하고 있지 않은지 확인하세요. IDE의 "Find References" 기능으로 검증하면 좋습니다.
  • 테스트 케이스 커버리지: 발주 권한이 없는 사용자가 다른 사람의 발주 아이템에 접근하는 경우 등 경계 사례(edge case)가 제대로 테스트되고 있는지 검토하세요.

Possibly related PRs

Poem

🔐 경로는 정확히, 접근은 안전히
Receipt를 먼저 확인하고 Item을 검증하네
/receipt/receipt의 중복은 이제 안녕 👋
계층 구조 명확하니 버그는 자취를 감추고
깔끔한 엔드포인트로 API는 한층 더 우아하리✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
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 (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 요약합니다. 발주 아이템 엔드포인트 구조 수정이라는 핵심 변경사항을 정확히 전달하고 있습니다.
Description check ✅ Passed PR 설명이 요구되는 템플릿 섹션(작업 내용, 적용 범위, 참고 사항)을 모두 포함하고 있으며, 관련 이슈 #96을 명시하고 있습니다.
Linked Issues check ✅ Passed 변경사항이 이슈 #96의 요구사항을 충족합니다. 잘못된 /receipt/receipt 중복 경로가 /{receiptId}/items/{receiptItemId}로 올바르게 수정되었습니다.
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 fix/receipt-item-endpoints

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 (7)
src/main/java/com/almang/inventory/receipt/service/ReceiptService.java (2)

162-175: 입고 아이템 단건 조회/수정/삭제가 Receipt 기반으로 잘 재설계되었습니다.

  • 기존 버그 원인(아이템을 receiptId 없이 itemId만으로 조회/조작 가능하고 /receipt/receipt... 같은 애매한 경로 구조)을, findReceiptByIdAndValidateAccess(receiptId, store)findReceiptItemByIdAndValidateAccess(itemId, receipt) 순서로 풀어서 명확히 차단한 구조입니다.
  • 사용자 → Store → Receipt → ReceiptItem 순서로 권한을 검증하므로,
    • 다른 상점의 receiptId: RECEIPT_ACCESS_DENIED
    • 같은 상점이지만 다른 Receipt의 아이템: RECEIPT_ITEM_ACCESS_DENIED
    • 존재하지 않는 아이템: RECEIPT_ITEM_NOT_FOUND
      로 케이스가 잘 분리됩니다. 서비스 테스트와도 일관적입니다.

개선 제안(선택 사항):

  • 세 메서드(get/update/delete)의 공통 패턴(유저 조회 + store 추출 + receipt 검증 + item 검증 + 로그)이 거의 동일하니, 나중에 executeReceiptItemAction(...) 같은 private 템플릿 메서드로 묶으면 중복 제거와 테스트 유지보수에 도움이 됩니다.
    (예: 공통 pre-check + 람다로 아이템 조작 로직 전달).

Also applies to: 178-203, 205-220


339-354: updateReceiptItems에서도 Receipt 단위 접근 검증을 재사용한 점이 좋습니다.

  • 루프 안에서 findReceiptItemByIdAndValidateAccess(receiptItemRequest.receiptItemId(), receipt)를 쓰도록 바뀌어, 단건 API와 동일하게 “해당 Receipt 소속 아이템만 수정 가능”이라는 규칙이 유지됩니다.
  • findReceiptItemByIdAndValidateAccess가 Receipt로 한 번 더 교차 검증을 하기 때문에, 잘못된 receiptItemId가 섞여 들어와도 RECEIPT_ITEM_ACCESS_DENIED로 안전하게 막힙니다.

개선 제안(추천 수준):

  1. DTO의 receiptId 활용 여부 정리

    • UpdateReceiptItemRequestreceiptId 필드가 있지만, 서비스에서는 사용하지 않고 receiptItemId + Receipt 엔티티만 신뢰하고 있습니다.
    • 유지보수자의 혼동을 줄이려면 아래 둘 중 하나를 고려해볼 수 있습니다.
      • (A) 이 메서드에서 receiptItemRequest.receiptId()receipt.getId()가 일치하는지 검증해 일종의 이중 방어를 추가.
      • (B) API 설계상 필요 없다면 receiptId 필드를 deprecate 후 제거(다음 버전에서)하고, 식별은 path + receiptItemId에만 의존.
  2. 쿼리 최적화(필요시)

    • 현재는 receiptItemRepository.findByIdgetReceipt().getId()를 다시 접근하는 구조라 최소 1번, lazy인 경우 2번의 쿼리가 발생할 수 있습니다.
    • 성능이 이 경로에서 민감하다면 Spring Data JPA의 파생 쿼리 메서드(findByIdAndReceiptId(...))를 리포지토리에 추가해 한 번에 검증하는 것도 좋습니다.

Also applies to: 356-364

src/test/java/com/almang/inventory/receipt/service/ReceiptServiceTest.java (2)

983-1064: 입고 아이템 조회 테스트가 새 시그니처와 예외 케이스를 잘 커버하고 있습니다.

  • 버그 원인이었던 “itemId만으로 조회 + /receipt/receipt... 구조”를 반영해,
    • 정상 조회: getReceiptItem(savedReceipt.getId(), targetItem.getId(), userId)
    • USER_NOT_FOUND, RECEIPT_ITEM_NOT_FOUND, RECEIPT_ACCESS_DENIED
      각 케이스를 명확히 분리한 점이 좋습니다.
  • 특히 입고_아이템_조회시_아이템이_존재하지_않으면_예외에서 실제 Receipt 엔티티를 생성해 유효한 receiptId를 넘긴 뒤, itemId만 잘못 주는 패턴은 서비스의 실제 동작과 잘 맞습니다.

개선 제안(선택 사항):

  • 현재 조회 테스트에서는 “같은 상점이지만 다른 Receipt에 속한 아이템” 시나리오는 없고, 이 케이스는 수정/삭제 테스트에서만 다룹니다.
    • 조회에도 같은 패턴의 테스트를 하나 추가하면(RECEIPT_ITEM_ACCESS_DENIED 기대), 단건 API 3종(GET/PATCH/DELETE)의 접근 제어 행태를 완전히 대칭적으로 검증할 수 있습니다.

1107-1430: 입고 아이템 수정/삭제 테스트가 경계 조건을 촘촘하게 잡고 있습니다.

  • 수정 성공/실패 케이스에서 새 시그니처 updateReceiptItem(receiptId, itemId, req, userId)를 모두 반영했고,
    • 다른 Receipt의 아이템 수정 시 RECEIPT_ITEM_ACCESS_DENIED,
    • 잘못된 receiptId(존재하지 않는 ID) 사용 시 RECEIPT_NOT_FOUND
      등 실제 서비스 로직과 예외 타입을 정확히 매칭하고 있습니다.
  • 삭제 쪽도
    • 성공 시 boxCount 재계산 검증,
    • USER_NOT_FOUND / RECEIPT_ITEM_NOT_FOUND / RECEIPT_ACCESS_DENIED
      케이스를 별도로 가져가서 회귀 가능성을 잘 줄였습니다.

개선 제안(선택 사항):

  • 테스트 전반에 걸쳐 Receipt/Order/Item fixture 생성 코드가 거의 동일하게 반복되는데, JUnit helper 메서드나 별도 TestFixture 클래스로 묶어두면 이후 시그니처 변화 시 수정 범위를 줄이는 데 도움이 됩니다.
src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java (2)

68-81: ReceiptItemResponse 생성자 사용은 맞지만, 장기적으로는 빌더/팩토리 패턴이 더 안전해 보입니다.

  • 현재 테스트가 DTO 생성자 시그니처 변경(예: receiptItemId, receiptId, productId, ...)을 정확히 반영하고 있고, 다른 테스트들과 인자 순서도 일관적입니다. 동기화 자체는 잘 되어 있습니다.
  • 다만 파라미터 수가 많다 보니, 향후 필드 추가/순서 변경 시 테스트가 “컴파일은 되지만 잘못된 값”을 줄 가능성이 큽니다.

개선 제안:

  • DTO에도 적용 가능하다면 ReceiptItemResponse.of(...) 정적 팩토리나 빌더를 도입하고, 테스트에서는 의미 있는 파라미터명을 가진 팩토리/빌더를 사용하면 가독성과 안전성이 크게 올라갈 것입니다.
    • 예: ReceiptItemResponse.of(receiptItemId, receiptId, productId, boxCount, ...)
    • 혹은 Lombok @builder 등을 사용해 ReceiptItemResponse.builder().receiptId(...).productId(...).build() 형태로 작성.

787-821: 입고 아이템 컨트롤러 테스트가 새 경로와 시그니처를 잘 검증하고 있습니다. 다만 에러 코드 시나리오 네이밍은 살짝 정리가 필요해 보입니다.

좋은 점:

  • 모든 아이템 관련 엔드포인트가 /api/v1/receipt/{receiptId}/items/{receiptItemId} 형태로 바뀐 것을 테스트에서 명확히 검증하고 있습니다.
    • 성공 케이스에서 jsonPath("$.data.receiptId").value(receiptId)까지 확인하는 부분이 특히 좋습니다.
  • 서비스 mock 역시 새 시그니처
    • getReceiptItem(anyLong(), anyLong(), anyLong())
    • updateReceiptItem(anyLong(), anyLong(), any(UpdateReceiptItemRequest.class), anyLong())
    • deleteReceiptItem(anyLong(), anyLong(), anyLong())
      로 모두 통일되어 있어 컨트롤러와 서비스 인터페이스가 잘 맞습니다.
  • 각 예외 케이스(USER_NOT_FOUND, RECEIPT_NOT_FOUND, RECEIPT_ACCESS_DENIED, RECEIPT_ITEM_NOT_FOUND, RECEIPT_ITEM_ACCESS_DENIED)에 대한 HTTP status 및 응답 바디 구조를 세밀하게 검증하고 있어, 글로벌 예외 핸들러 변경 시에도 회귀를 잘 잡아줄 수 있습니다.

개선 제안(추천):

  1. “다른 상점” 시나리오의 ErrorCode 정합성

    • 서비스 레벨 테스트에서는
      • “다른 상점의 아이템” → RECEIPT_ACCESS_DENIED
        로 검증하고 있는 반면, 이 컨트롤러 테스트에서는 같은 한글 설명을 쓰면서 mock이 RECEIPT_ITEM_ACCESS_DENIED를 던지는 경우가 있습니다(조회/삭제 모두).
    • 실제 동작은 ReceiptService 쪽 구현을 따를 것이므로, 다음 중 하나로 정리하면 읽는 사람이 덜 헷갈립니다.
      • (A) 이 컨트롤러 테스트의 mock도 RECEIPT_ACCESS_DENIED로 맞추기.
      • (B) 테스트 메서드 이름을 “아이템 접근 권한 없음” 등으로 일반화해서 ErrorCode와 시나리오를 1:1로 묶지 않기.
  2. 중복 상수 정리(선택 사항)

    • Long receiptId = 1L; Long receiptItemId = 1000L; 패턴이 여러 테스트에서 반복되는데, 클래스 상단에 상수로 빼거나 helper 메서드에서 공통으로 만드는 식으로 정리하면 테스트가 더 간결해집니다.

관련 개념은 Spring MVC의 @PathVariable 활용과 REST 리소스 계층 구조(컬렉션/하위 리소스) 설계 가이드를 한번쯤 같이 참고하시면 좋겠습니다.

Also applies to: 823-878, 881-931, 933-1096, 1098-1150

src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java (1)

141-155: 입고 아이템 관련 엔드포인트가 REST스럽게 정리되었고, 서비스 호출도 일관적입니다.

  • 버그 원인이었던 /receipt/receipt 스타일 중복/모호 경로 대신,
    • 조회: GET /api/v1/receipt/{receiptId}/items/{receiptItemId}
    • 수정: PATCH /api/v1/receipt/{receiptId}/items/{receiptItemId}
    • 삭제: DELETE /api/v1/receipt/{receiptId}/items/{receiptItemId}
      형태로 Receipt → Item 계층 구조가 명확해졌습니다.
  • 각 메서드가 공통적으로
    • userPrincipal에서 userId 추출
    • 로그에 userId, receiptId, receiptItemId 모두 포함
    • receiptService.*ReceiptItem(receiptId, receiptItemId, userId/req) 호출
      로 작성되어 있어, 서비스 레이어 변경사항과도 정확히 맞습니다.

개선 제안(추천):

  • 현재 API에서는 path variable로 receiptId를 받고, body UpdateReceiptItemRequest에도 receiptId 필드가 중복으로 존재합니다.
    • 실제 서비스 로직은 path의 receiptId만 신뢰하고 body 쪽은 사용하지 않기 때문에, Swagger/OpenAPI 문서를 볼 때 “둘 중 무엇이 기준인지” 혼란이 생길 수 있습니다.
    • 향후 버전에서
      • (A) body의 receiptId를 요청 스펙에서 제거하거나(deprecate → remove), 또는
      • (B) controller에서 path의 receiptId와 body의 request.receiptId()가 일치하는지 간단히 검증해 주는
        방향을 고려하면 API 일관성이 더 좋아질 것 같습니다.

Also applies to: 156-171, 173-190, 192-207

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f0bbe2 and 6570007.

📒 Files selected for processing (4)
  • src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java (2 hunks)
  • src/main/java/com/almang/inventory/receipt/service/ReceiptService.java (3 hunks)
  • src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java (21 hunks)
  • src/test/java/com/almang/inventory/receipt/service/ReceiptServiceTest.java (13 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-22T18:12:13.161Z
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.161Z
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/receipt/controller/ReceiptControllerTest.java
  • src/main/java/com/almang/inventory/receipt/service/ReceiptService.java
📚 Learning: 2025-11-20T10:43:47.489Z
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.489Z
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/receipt/service/ReceiptServiceTest.java

@JoonKyoLee JoonKyoLee merged commit 1064237 into main Nov 24, 2025
1 check passed
@JoonKyoLee JoonKyoLee deleted the fix/receipt-item-endpoints branch November 25, 2025 06:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] 발주 아이템 관련 엔드포인트 수정

1 participant