Skip to content

feat: 발주 기반 입고 생성 기능 구현#82

Merged
JoonKyoLee merged 8 commits intomainfrom
feat/create-receipt-based-on-order
Nov 23, 2025
Merged

feat: 발주 기반 입고 생성 기능 구현#82
JoonKyoLee merged 8 commits intomainfrom
feat/create-receipt-based-on-order

Conversation

@JoonKyoLee
Copy link
Contributor

@JoonKyoLee JoonKyoLee commented Nov 23, 2025

✨ 작업 내용

  • 발주 기반 입고 생성 기능 구현

📝 적용 범위

  • /receipt

📌 참고 사항

Summary by CodeRabbit

  • 신규 기능
    • 발주 기반으로 입고를 생성하는 API와 화면에서 확인 가능한 입고(대기/확정/취소) 객체가 추가되었습니다. 발주 항목이 입고 항목으로 매핑되어 금액·수량 등 정보가 반영됩니다.
  • 버그 수정 / 검증
    • 잘못된 상태나 권한으로 입고 생성 시 적절한 오류 응답(예: 주문 없음, 권한 없음, 생성 불가)이 반환됩니다.
  • 테스트
    • 컨트롤러·서비스 수준의 포괄적인 자동화 테스트가 추가되었습니다.

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

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

coderabbitai bot commented Nov 23, 2025

Walkthrough

발주(Order)를 기반으로 새 입고(Receipt)를 생성하는 기능을 추가했습니다. 도메인(Receipt, ReceiptItem, ReceiptStatus), 저장소, 서비스, 컨트롤러, 응답 DTO와 관련 테스트 및 새로운 성공/에러 상수를 도입했습니다.

Changes

코호트 / 파일(들) 변경 요약
전역 상수
src/main/java/com/almang/inventory/global/api/SuccessMessage.java, src/main/java/com/almang/inventory/global/exception/ErrorCode.java
성공 메시지 CREATE_RECEIPT_FROM_ORDER_SUCCESS 및 에러 코드 RECEIPT_CREATION_NOT_ALLOWED_FROM_ORDER 추가.
컨트롤러
src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java
POST /api/v1/receipt/from-order/{orderId} 엔드포인트를 처리하는 ReceiptController 추가. 인증 정보에서 사용자 ID를 획득해 서비스 호출 후 표준 ApiResponse 반환.
도메인 엔티티
src/main/java/com/almang/inventory/receipt/domain/Receipt.java, src/main/java/com/almang/inventory/receipt/domain/ReceiptItem.java, src/main/java/com/almang/inventory/receipt/domain/ReceiptStatus.java
JPA 엔티티 Receipt, ReceiptItem 및 상태 열거형 ReceiptStatus 추가. 항목 연결, 상태/활성화 업데이트 메서드 포함.
응답 DTO
src/main/java/com/almang/inventory/receipt/dto/response/ReceiptResponse.java, src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java
도메인 → API 변환용 레코드 DTO 추가 및 정적 팩토리 메서드 구현.
저장소
src/main/java/com/almang/inventory/receipt/repository/ReceiptRepository.java
JpaRepository<Receipt, Long>를 확장하는 ReceiptRepository 인터페이스 추가.
비즈니스 로직
src/main/java/com/almang/inventory/receipt/service/ReceiptService.java
발주 조회·검증(권한, 상태), 발주 항목 → 입고 항목 매핑, 영속화 및 ReceiptResponse 반환하는 createReceiptFromOrder 구현(트랜잭션 포함).
단위/통합 테스트
src/test/.../receipt/controller/ReceiptControllerTest.java, src/test/.../receipt/service/ReceiptServiceTest.java
컨트롤러 MVC 테스트(성공 및 4개 오류 케이스)와 서비스 통합 스타일 테스트(성공 및 4개 오류 케이스) 추가.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller as ReceiptController
    participant Service as ReceiptService
    participant OrderRepo as OrderRepository
    participant ReceiptRepo as ReceiptRepository
    participant DB as Database

    Client->>Controller: POST /api/v1/receipt/from-order/{orderId}
    activate Controller
    Controller->>Service: createReceiptFromOrder(orderId, userId)
    deactivate Controller

    activate Service
    rect rgb(220,240,255)
        Note over Service: 사용자 & 권한 검증
        Service->>Service: findUserOrThrow(userId)
        Service->>Service: store 접근 확인
    end

    rect rgb(220,240,255)
        Note over Service: 발주 조회 및 검증
        Service->>OrderRepo: findByIdWithItems(orderId)
        OrderRepo-->>Service: Order (+items)
        Service->>Service: 권한/상태 확인 (CANCELED -> error)
    end

    rect rgb(240,255,240)
        Note over Service: 매핑(주요 변경)
        Service->>Service: Order -> Receipt
        Service->>Service: OrderItem -> ReceiptItem[]
        Service->>ReceiptRepo: save(receipt)
    end

    ReceiptRepo->>DB: INSERT receipt, receipt_items
    DB-->>ReceiptRepo: persisted entities
    ReceiptRepo-->>Service: saved Receipt
    Service-->>Controller: ReceiptResponse
    deactivate Service
    Controller-->>Client: 200 OK + ApiResponse<ReceiptResponse>
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

주의 깊게 볼 항목:

  • ReceiptService.createReceiptFromOrder: 사용자/발주 조회, 권한·상태 검사, 엔티티 매핑·영속화가 한 흐름에 몰려 있어 분리·단위화 필요. (권장: 각 책임을 private 메서드로 분리하고 단위 테스트 보강)
  • 엔티티 연관관계: Receipt ↔ ReceiptItem 양방향 설정의 cascade/orphanRemoval 및 lazy 로딩, 영속성 전이 시점 검증 필요. (권장: JPA 영속성 사이클 시나리오 확인)
  • DTO 매핑: ReceiptItemResponse.from에서 actualQuantity 매핑이 올바른지 확인 — 테스트 코멘트에 따르면 기존에 expectedQuantity로 잘못 매핑된 곳을 바로잡았는지 재확인 필요.
  • 트랜잭션 경계와 예외 변환: 서비스가 BaseException을 던지는 흐름이 컨트롤러 응답/HTTP 코드와 일관한지 확인.

추천 문서: Spring Data JPA 관계 매핑 가이드, Spring 트랜잭션 문서, DTO 매핑(예: MapStruct) 참고.

Possibly related PRs

  • #33: SuccessMessage.java에 다른 성공 상수를 추가한 PR — 동일 파일에 상수 추가 변경이 있어 충돌 가능성 있음.
  • #50: SuccessMessage 열거형을 수정한 PR — 전역 메시지 통합 일관성 검토 대상.
  • #67: Order 도메인 및 관련 에러/메시지 변경을 포함한 PR — ReceiptService가 Order 엔티티를 읽고 매핑하므로 코드 레벨 연관성이 높음.

Poem

발주가 바람 되어 입고가 오고,
엔티티 줄 맞춰 DB에 안착하네.
테스트는 빛나고 로그는 속삭여,
작은 상수 하나가 큰 흐름을 돕네. ✨📦

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 설명이 필수 항목(작업 내용, 적용 범위, 참고 사항)을 모두 포함하고 있으며 관련 이슈를 명시했습니다.
Linked Issues check ✅ Passed PR의 모든 코드 변경이 이슈 #73의 '발주 기반 입고 생성' 요구사항을 충족하고 있습니다. 엔티티, 서비스, 컨트롤러, DTO, 리포지토리 및 테스트가 완전히 구현되었습니다.
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/create-receipt-based-on-order

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66d85cd and 91fc277.

📒 Files selected for processing (1)
  • src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java

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: 2

🧹 Nitpick comments (7)
src/main/java/com/almang/inventory/receipt/domain/ReceiptItem.java (2)

36-40: 필드 타입의 일관성을 검토해주세요.

Line 36의 expectedQuantityBigDecimal이지만, Line 39의 actualQuantityInteger로 정의되어 있습니다. 두 수량 필드가 같은 단위를 나타낸다면 동일한 타입을 사용하는 것이 일관성과 정밀도 측면에서 유리합니다.

특히 무게 기반 제품이나 소수점 단위 수량을 다룰 경우, actualQuantityBigDecimal로 변경하는 것을 권장합니다. 현재 Line 89의 amount 계산 로직도 함께 조정이 필요합니다.

참고: 도메인 요구사항에서 actualQuantity가 정수만 필요한 경우라면 expectedQuantity도 Integer로 통일하는 것을 고려하세요.

만약 BigDecimal로 통일한다면:

-    @Column(name = "actual_quantity")
-    private Integer actualQuantity;
+    @Column(name = "actual_quantity", precision = 8, scale = 3)
+    private BigDecimal actualQuantity;

그리고 amount 계산 로직도:

-        if (this.actualQuantity != null && this.unitPrice != null) {
-            this.amount = this.actualQuantity * this.unitPrice;
-        }
+        if (this.actualQuantity != null && this.unitPrice != null) {
+            this.amount = this.actualQuantity.multiply(BigDecimal.valueOf(this.unitPrice)).intValue();
+        }

19-19: 주석이 약간 혼란을 줄 수 있습니다.

"ERD 기준: PK 컬럼명 id"라는 주석이 있지만, 실제 코드에서는 receipt_item_id를 사용하고 있습니다. 이 주석은 ERD와 구현의 차이점을 설명하려는 의도로 보이지만, 오히려 혼란을 줄 수 있습니다.

주석을 더 명확하게 수정하거나, ERD와 구현이 일치한다면 주석을 제거하는 것을 권장합니다.

-    @Column(name = "receipt_item_id") // ERD 기준: PK 컬럼명 id
+    @Column(name = "receipt_item_id")
src/main/java/com/almang/inventory/receipt/dto/response/ReceiptResponse.java (1)

20-34: ReceiptService의 쿼리 최적화 검토 필요 - 추가 조회 시점에 fetch join 추가 고려

검증 결과, Receipt 엔티티의 Store와 Order 관계가 FetchType.LAZY로 설정되어 있고, ReceiptRepository에서는 기본 JpaRepository 메서드만 사용하고 있습니다. 현재 ReceiptResponse의 from() 메서드에서 receipt.getStore().getId()receipt.getOrder().getId()를 호출할 때 LAZY 로딩으로 인한 추가 쿼리가 발생할 수 있습니다.

현재 상황:

  • 단건 조회(createReceiptFromOrder)이므로 성능 영향은 미미
  • N+1 문제는 목록 조회 추가 시 실제 성능 저하로 나타남

개선 방안:
향후 목록 조회 API 구현 시, ReceiptRepository에 fetch join을 사용하는 메서드를 추가하세요:

@Query("SELECT r FROM Receipt r " +
       "JOIN FETCH r.store s " +
       "JOIN FETCH r.order o " +
       "WHERE r.id = :id")
Optional<Receipt> findByIdWithRelations(@Param("id") Long id);

또는 Store/Order를 eager loading으로 변경하는 것도 고려할 수 있습니다. 현재는 단건 조회이므로 즉시 적용이 필수는 아니지만, 리스트 조회 구현 시 이를 기억해두시면 좋겠습니다.

src/main/java/com/almang/inventory/receipt/domain/Receipt.java (1)

47-48: activated 필드와 컬럼명 is_activate를 일관되게 맞추는 것을 추천드립니다

현재 boolean 필드 이름은 activated인데, 컬럼명은 is_activate로 약간 어색하게 엇갈려 있어서(문법적으로도 is_activated가 더 자연스럽습니다)
쿼리 작성·리뷰 시 “이 컬럼이 뭘 의미하지?” 하는 혼선을 줄 수 있습니다.

스키마가 아직 고정되지 않았다면 아래처럼 필드 의미와 맞는 이름으로 맞춰두는 편이 유지보수에 유리합니다.

-    @Column(name = "is_activate", nullable = false)
-    private boolean activated;
+    @Column(name = "is_activated", nullable = false)
+    private boolean activated;

이미 운영 DB에 is_activate가 존재한다면 그대로 두되, DDL/ERD 상 설명을 남기거나 주석으로라도 의미를 명확히 해 두는 것을 권장드립니다. JPA 관점에서도 필드명·컬럼명을 개념적으로 최대한 맞추는 것이 베스트 프랙티스입니다.

src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java (1)

53-101: 컨트롤러 테스트가 매핑 버그를 잡기에는 범위가 살짝 아쉽습니다

지금 성공 케이스 테스트는 다음을 잘 검증하고 있어서 API 레벨 계약은 안정적입니다. 굿입니다 👍

  • HTTP status / 메시지
  • receiptId, storeId, orderId, status, activated
  • 첫 번째 아이템의 receiptItemId, productId, amount

다만 몇 가지 한계가 있습니다.

  1. 아이템 세부 필드 매핑 검증이 없음

    expectedQuantity, actualQuantity, measuredWeight, errorRate, note 등은 전혀 검증하지 않아서,
    이번 PR의 ReceiptItemResponse.actualQuantity 매핑 버그 같은 문제는 컨트롤러 테스트로는 발견이 어렵습니다.

  2. 서비스를 DTO 레벨로 stub 하고 있어 매핑 로직이 테스트 범위 밖

    when(receiptService.createReceiptFromOrder(...)).thenReturn(response); 식으로
    이미 완성된 ReceiptResponse를 반환하도록 stub 하고 있어서,

    • ReceiptService 내 Order → Receipt/ReceiptItem 변환,
    • ReceiptItemResponse.from(ReceiptItem) 매핑

    은 이 테스트의 범위 밖입니다.

개선 제안

  • 이 파일에서는 HTTP 계약 위주 검증 그대로 두되,
  • 별도의 서비스/DTO 단위 테스트에서 매핑 로직을 강하게 검증하는 방향을 추천드립니다.

예를 들어 ReceiptServiceTestReceiptItemResponseTest에서:

  • expectedQuantity, actualQuantity, amount, note 등의 값이
    OrderItem/ReceiptItem에서 기대한 대로 내려오는지 검증

을 추가해 두면, 매핑에 손을 댈 때 바로 깨져서 버그를 빠르게 인지할 수 있습니다.

지금 구조도 나쁘지 않지만, 위처럼 계층별로 테스트 책임을 분리하면 장기적으로 훨씬 튼튼해집니다.

src/main/java/com/almang/inventory/receipt/service/ReceiptService.java (2)

78-90: 금액 계산(amount)에 대한 타입·연산 방식을 한 번 정리해 두면 좋겠습니다

toReceiptItemEntity에서:

.amount(orderItem.getQuantity() * orderItem.getUnitPrice())

int * int 연산 결과를 그대로 amount에 넣고 있는데,

  • 수량·단가가 커질 수 있는 도메인이라면 int 곱셈은 오버플로우 위험이 있고,
  • 금액 도메인은 보통 long(분 단위, 센트 단위) 또는 BigDecimal을 쓰는 것이 정석입니다.

지금 당장 문제가 되지는 않을 수 있지만, 도메인 결정이 늦어질수록 전체 엔티티·DB 스키마를 한꺼번에 바꿔야 해서 비용이 커집니다.

제안

  • OrderItem.unitPrice/ReceiptItem.amount 타입을 어떻게 가져갈지(원 단위 Integer vs 분 단위 Long vs BigDecimal)를 지금 시점에 한 번 명확히 결정해 두시고,
  • 그에 맞게 여기 연산도 맞춰 두시면 이후 회계/정산 기능 추가 시 리스크를 많이 줄일 수 있습니다. (Java 금액 계산 관련해서는 “돈은 BigDecimal로 다루라”는 JDK/스프링 진영 공통 관례를 참고하시면 좋습니다.)

코드는 그대로 두되, TODO나 간단한 주석으로라도 “금액 타입 리팩터링 검토”를 남겨두면 나중에 놓치지 않고 처리하기 좋습니다.


107-111: 입고 생성 허용 상태를 명시적인 허용 집합으로 관리하면 안전합니다

현재 상태 검증은:

private void validateOrderStatusForReceipt(Order order) {
    if (order.getStatus() == OrderStatus.CANCELED) {
        throw new BaseException(ErrorCode.RECEIPT_CREATION_NOT_ALLOWED_FROM_ORDER);
    }
}

로 “취소 상태만 막고, 나머지는 전부 허용”하는 형태입니다.
지금 enum 값이 적을 때는 괜찮지만, 나중에 COMPLETED, CLOSED, RETURNED 같은 상태가 추가되면:

  • 새 상태에 대해 입고 생성이 허용되는지/안 되는지 고민하기 전에,
  • 기본값으로 “그냥 허용”되어 버리는 위험이 있습니다.

제안

도메인 관점에서 “입고 생성이 가능한 상태”를 화이트리스트로 정의하는 편이 더 안전합니다. 예를 들면:

private static final EnumSet<OrderStatus> RECEIPT_CREATABLE_STATUSES =
        EnumSet.of(OrderStatus.ORDERED, OrderStatus.APPROVED); // 예시

private void validateOrderStatusForReceipt(Order order) {
    if (!RECEIPT_CREATABLE_STATUSES.contains(order.getStatus())) {
        throw new BaseException(ErrorCode.RECEIPT_CREATION_NOT_ALLOWED_FROM_ORDER);
    }
}

이렇게 하면:

  • 새로운 OrderStatus가 추가될 때마다 이 집합을 업데이트해야 해서,
  • 상태 모델이 바뀔 때 도메인 규칙을 빠뜨리지 않고 점검할 수 있습니다.

실제 허용 상태 목록은 PO/기획과 한 번 합의가 필요하니, 그에 맞춰 상수만 정리해 두시면 좋겠습니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2654f87 and 66d85cd.

📒 Files selected for processing (12)
  • 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/receipt/controller/ReceiptController.java (1 hunks)
  • src/main/java/com/almang/inventory/receipt/domain/Receipt.java (1 hunks)
  • src/main/java/com/almang/inventory/receipt/domain/ReceiptItem.java (1 hunks)
  • src/main/java/com/almang/inventory/receipt/domain/ReceiptStatus.java (1 hunks)
  • src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java (1 hunks)
  • src/main/java/com/almang/inventory/receipt/dto/response/ReceiptResponse.java (1 hunks)
  • src/main/java/com/almang/inventory/receipt/repository/ReceiptRepository.java (1 hunks)
  • src/main/java/com/almang/inventory/receipt/service/ReceiptService.java (1 hunks)
  • src/test/java/com/almang/inventory/receipt/controller/ReceiptControllerTest.java (1 hunks)
  • src/test/java/com/almang/inventory/receipt/service/ReceiptServiceTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/main/java/com/almang/inventory/receipt/domain/ReceiptItem.java
🧬 Code graph analysis (1)
src/main/java/com/almang/inventory/receipt/domain/ReceiptItem.java (1)
src/main/java/com/almang/inventory/receipt/domain/Receipt.java (1)
  • Entity (13-77)
🔇 Additional comments (8)
src/main/java/com/almang/inventory/receipt/domain/ReceiptStatus.java (1)

3-7: 깔끔한 enum 정의입니다!

입고의 라이프사이클을 명확하게 표현하는 상태 값들이 잘 정의되어 있습니다. PENDING → CONFIRMED → CANCELED 흐름이 직관적입니다.

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

50-51: 적절한 위치에 성공 메시지가 추가되었습니다!

RECEIPT 섹션을 새로 만들고 발주 기반 입고 생성 성공 메시지를 추가한 것이 좋습니다. 향후 입고 관련 메시지 확장에도 용이한 구조입니다.

src/main/java/com/almang/inventory/receipt/repository/ReceiptRepository.java (1)

6-7: 표준 JPA 레포지토리 패턴이 잘 적용되었습니다!

기본 CRUD 작업으로 충분한 현재 요구사항에 적합한 구조입니다.

src/main/java/com/almang/inventory/global/exception/ErrorCode.java (1)

48-49: 비즈니스 규칙을 명확히 표현하는 에러 코드입니다!

발주 상태에 따른 입고 생성 제약을 적절한 HTTP 상태 코드(BAD_REQUEST)와 함께 정의했습니다. 에러 메시지도 사용자가 이해하기 쉽게 작성되었습니다.

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

139-165: 성공 시나리오에 대한 테스트가 꼼꼼하게 작성되었습니다!

입고 생성 후 응답 검증뿐만 아니라 실제로 DB에 저장된 Receipt 엔티티까지 조회하여 검증하는 것이 훌륭합니다. 연관관계(Store, Order, Items) 매핑도 함께 확인하여 신뢰도 높은 테스트입니다.


167-223: 예외 시나리오에 대한 테스트 커버리지가 우수합니다!

사용자 미존재, 발주 미존재, 권한 없음, 취소된 발주 등 핵심 예외 상황을 모두 테스트하고 있습니다. 특히 Line 216에서 updateStatus(CANCELED)를 사용해 취소된 발주 시나리오를 명시적으로 재현한 것이 좋습니다.


48-137: 테스트 헬퍼 메서드들이 재사용성 있게 잘 구성되었습니다!

newStore, newUser, newVendor 등 헬퍼 메서드들이 테스트 데이터 생성을 명확하고 간결하게 만들어줍니다. 특히 newOrderWithItems는 Order와 OrderItem의 양방향 연관관계 설정까지 처리하여 실수 가능성을 줄였습니다.

src/main/java/com/almang/inventory/receipt/domain/ReceiptItem.java (1)

58-91: update 메서드가 잘 구현되었습니다!

조건부 null 체크를 통한 선택적 업데이트 패턴이 적절하게 적용되었습니다. Line 88-90의 amount 재계산 로직도 actualQuantity와 unitPrice가 모두 존재할 때만 수행되어 방어적입니다.

Based on learnings, 서비스 계층에서 파라미터 검증을 수행하는 패턴을 따르고 있어 별도의 null 체크가 필요하지 않습니다.

Comment on lines +27 to +39
@PostMapping("/from-order/{orderId}")
public ResponseEntity<ApiResponse<ReceiptResponse>> createReceiptFromOrder(
@PathVariable Long orderId,
@AuthenticationPrincipal CustomUserPrincipal userPrincipal
) {
Long userId = userPrincipal.getId();
log.info("[ReceiptController] 발주 기반 입고 생성 요청 - userId: {}, orderId: {}", userId, orderId);
ReceiptResponse response = receiptService.createReceiptFromOrder(orderId, userId);

return ResponseEntity.ok(
ApiResponse.success(SuccessMessage.CREATE_RECEIPT_FROM_ORDER_SUCCESS.getMessage(), response)
);
}
Copy link

@coderabbitai coderabbitai bot Nov 23, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

컨트롤러 구조가 깔끔하게 구현되었습니다!

RESTful 엔드포인트 설계, 인증 처리, 서비스 레이어로의 위임이 잘 구현되어 있습니다. 로깅도 적절히 추가되어 운영 시 추적이 용이합니다.

다만, Line 32에서 userPrincipal.getId()를 호출하기 전에 userPrincipal이 null이 아님을 보장하는지 확인이 필요합니다. Spring Security 설정에서 인증되지 않은 요청을 필터링한다면 문제없지만, 방어적 코딩 관점에서 null 체크를 고려할 수 있습니다.

다음 스크립트로 Spring Security 설정을 확인하여 /api/v1/receipt 경로가 인증을 요구하는지 검증하세요:


🏁 Script executed:

#!/bin/bash
# Spring Security 설정 파일에서 receipt 경로 인증 요구사항 확인
rg -n "receipt|/api/v1" --type=java -g "**/*Security*.java" -C 3

Length of output: 1883


이 리뷰 코멘트를 검증하기 위해 추가 정보를 수집하겠습니다.

스크립트 결과를 분석하니 중요한 발견이 있습니다:

SecurityConfig의 인증 설정 분석:

  • 명시적으로 인증이 필요한 경로: /api/v1/auth/logout only
  • 나머지 모든 요청: .anyRequest().permitAll() (라인 44)

이는 /api/v1/receipt 경로가 인증 보호를 받지 않음을 의미합니다.

Receipt 컨트롤러의 메서드별 보안 설정과 더 정확한 상황을 확인하기 위해 추가 검증이 필요합니다:

추가 검증을 진행했습니다. 중요한 발견이 있습니다:

보안 설정 검증 결과:

스크립트와 웹 검색 결과에 따르면:

  1. Receipt 엔드포인트는 인증 보호를 받지 않습니다

    • SecurityConfig.java 라인 43: /api/v1/auth/logout.authenticated() 지정
    • 라인 44: .anyRequest().permitAll() - 모든 다른 요청 허용
  2. @AuthenticationPrincipal의 동작
    @AuthenticationPrincipal은 사용자가 인증되면 principal을 주입하거나, 인증되지 않으면 null을 주입합니다.

  3. 현재 코드의 실제 문제

    • 엔드포인트가 인증을 요구하지 않으므로 userPrincipal이 null일 수 있습니다
    • 라인 32에서 userPrincipal.getId()를 호출하면 NullPointerException 발생 가능

내 관찰:
원본 리뷰는 null 체크 필요성을 올바르게 지적했지만, 엔드포인트가 이미 인증되어 있다고 잘못 가정했습니다. 실제로는 더 심각한 상황입니다 — 엔드포인트 자체가 인증을 요구해야 할 것으로 보입니다.

검증이 필요한 사항:
Receipt 생성 엔드포인트가 인증된 사용자만 접근해야 하는 기능인지 확인하겠습니다.


🏁 Script executed:

#!/bin/bash
# 1. Check if ReceiptController has @PreAuthorize or security annotations
rg -n "@PreAuthorize|@Secured|@RolesAllowed" src/main/java/com/almang/inventory/receipt/ --type=java -C 2

# 2. Search for any additional matchers in SecurityConfig that might protect receipt paths
rg -n "requestMatchers|antMatchers|receipt" src/main/java/com/almang/inventory/global/config/security/SecurityConfig.java --type=java -C 2

Length of output: 326


🏁 Script executed:

#!/bin/bash
# 3. Verify if Spring Security version supports authenticated @AuthenticationPrincipal handling
# Check pom.xml for Spring Security version
fd pom.xml | xargs grep -A 2 -B 2 "spring-security"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# 다른 인증이 필요한 엔드포인트의 패턴 확인 (비교 목적)
rg -n "PostMapping|GetMapping|PutMapping|DeleteMapping" src/main/java/com/almang/inventory --type=java -A 2 | grep -E "(PostMapping|@RequestMapping|@PreAuthorize)" | head -20

Length of output: 2392


라인 32에서 NullPointerException 발생 위험이 있습니다.

검증 결과, Authentication이 null이면 @AuthenticationPrincipal도 null을 반환합니다. SecurityConfig에서 /api/v1/receipt 경로는 명시적 인증 보호가 없고 .anyRequest().permitAll()에 포함되어 있으므로, 인증되지 않은 사용자도 접근할 수 있습니다. 따라서 userPrincipal이 null일 수 있고, userPrincipal.getId() 호출 시 NullPointerException이 발생합니다.

개선 방안:

  1. 권장: 엔드포인트 보호 추가 (SecurityConfig에서)
.requestMatchers("/api/v1/receipt/**").authenticated()
  1. 또는: null 체크 추가 (방어적 코딩)
if (userPrincipal == null) {
    throw new UnauthorizedException("User must be authenticated");
}
Long userId = userPrincipal.getId();

Spring Security 공식 문서에서는 인증 요청인 엔드포인트는 먼저 인증을 강제하는 것이 권장됩니다. 현재 코드는 인증된 사용자의 ID를 필요로 하므로, 엔드포인트 수준에서 인증을 요구하는 것이 설계 의도에 부합합니다.

🤖 Prompt for AI Agents
In src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java
around lines 27 to 39, calling userPrincipal.getId() can throw a
NullPointerException because @AuthenticationPrincipal may be null for
unauthenticated requests; fix by either (preferred) protecting the endpoint in
SecurityConfig so this controller is only reachable by authenticated users
(e.g., add requestMatchers("/api/v1/receipt/**").authenticated() or equivalent
for this route), or (if you must allow anonymous access) add a defensive null
check in this method that throws an UnauthorizedException (or returns an
appropriate 401 response) when userPrincipal is null before calling getId().

Copy link
Contributor Author

@JoonKyoLee JoonKyoLee Nov 23, 2025

Choose a reason for hiding this comment

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

개발 중이라 엔드포인트 보호를 하지 않은 상태입니다. 추후 변경 예정입니다.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

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