-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 발주 기반 입고 생성 기능 구현 #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
6d321a1
feat: Receipt, ReceiptItem 엔티티 추가
JoonKyoLee ae9584a
feat: ReceiptRepository 생성
JoonKyoLee 41c1ddf
feat: 입고 및 입고 아이템 응답 DTO 추가
JoonKyoLee 3a4a650
feat: 발주 기반 입고 생성 서비스 로직 추가
JoonKyoLee e3e3e99
feat: 발주 기반 입고 생성 API 추가
JoonKyoLee 37c40ce
test: 발주 기반 입고 생성 서비스 로직 테스트 추가
JoonKyoLee 66d85cd
test: 발주 기반 입고 생성 API 테스트 추가
JoonKyoLee 91fc277
fix: 도메인 타입에 맞게 필드 타입 변경
JoonKyoLee File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
src/main/java/com/almang/inventory/receipt/controller/ReceiptController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.almang.inventory.receipt.controller; | ||
|
|
||
| import com.almang.inventory.global.api.ApiResponse; | ||
| import com.almang.inventory.global.api.SuccessMessage; | ||
| import com.almang.inventory.global.security.principal.CustomUserPrincipal; | ||
| import com.almang.inventory.receipt.dto.response.ReceiptResponse; | ||
| import com.almang.inventory.receipt.service.ReceiptService; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @Slf4j | ||
| @RestController | ||
| @RequestMapping("/api/v1/receipt") | ||
| @RequiredArgsConstructor | ||
| @Tag(name = "Receipt", description = "입고 관련 API") | ||
| public class ReceiptController { | ||
|
|
||
| private final ReceiptService receiptService; | ||
|
|
||
| @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) | ||
| ); | ||
| } | ||
| } | ||
77 changes: 77 additions & 0 deletions
77
src/main/java/com/almang/inventory/receipt/domain/Receipt.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| package com.almang.inventory.receipt.domain; | ||
|
|
||
| import com.almang.inventory.global.entity.BaseTimeEntity; | ||
| import com.almang.inventory.order.domain.Order; | ||
| import com.almang.inventory.store.domain.Store; | ||
| import jakarta.persistence.*; | ||
| import java.math.BigDecimal; | ||
| import java.time.LocalDate; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import lombok.*; | ||
|
|
||
| @Entity | ||
| @Table(name = "receipts") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class Receipt extends BaseTimeEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| @Column(name = "receipt_id") | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "store_id", nullable = false) | ||
| private Store store; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "order_id", nullable = false) | ||
| private Order order; | ||
|
|
||
| @Column(name = "receipt_date", nullable = false) | ||
| private LocalDate receiptDate; | ||
|
|
||
| @Column(name = "total_box_count", nullable = false) | ||
| private Integer totalBoxCount; | ||
|
|
||
| @Column(name = "total_weight_g", precision = 8, scale = 3) | ||
| private BigDecimal totalWeightG; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "status", nullable = false) | ||
| private ReceiptStatus status; | ||
|
|
||
| @Column(name = "is_activate", nullable = false) | ||
| private boolean activated; | ||
|
|
||
| @OneToMany(mappedBy = "receipt", cascade = CascadeType.ALL, orphanRemoval = true) | ||
| @Builder.Default | ||
| private List<ReceiptItem> items = new ArrayList<>(); | ||
|
|
||
| public void addItem(ReceiptItem item) { | ||
| items.add(item); | ||
| item.setReceipt(this); | ||
| } | ||
|
|
||
| public void updateTotalBoxCount(Integer totalBoxCount) { | ||
| if (totalBoxCount != null) { | ||
| this.totalBoxCount = totalBoxCount; | ||
| } | ||
| } | ||
|
|
||
| public void updateTotalWeightG(BigDecimal totalWeightG) { | ||
| this.totalWeightG = totalWeightG; | ||
| } | ||
|
|
||
| public void updateReceiptStatus(ReceiptStatus status) { | ||
| this.status = status; | ||
| } | ||
|
|
||
| public void deactivate() { | ||
| this.activated = false; | ||
| this.status = ReceiptStatus.CANCELED; | ||
| } | ||
| } |
92 changes: 92 additions & 0 deletions
92
src/main/java/com/almang/inventory/receipt/domain/ReceiptItem.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| package com.almang.inventory.receipt.domain; | ||
|
|
||
| import com.almang.inventory.global.entity.BaseTimeEntity; | ||
| import com.almang.inventory.product.domain.Product; | ||
| import jakarta.persistence.*; | ||
| import java.math.BigDecimal; | ||
| import lombok.*; | ||
|
|
||
| @Entity | ||
| @Table(name = "receipt_items") | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @AllArgsConstructor | ||
| @Builder | ||
| public class ReceiptItem extends BaseTimeEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| @Column(name = "receipt_item_id") // ERD 기준: PK 컬럼명 id | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "receipt_id", nullable = false) | ||
| private Receipt receipt; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "product_id", nullable = false) | ||
| private Product product; | ||
|
|
||
| @Column(name = "box_count") | ||
| private Integer boxCount; | ||
|
|
||
| @Column(name = "measured_weight", precision = 8, scale = 3) | ||
| private BigDecimal measuredWeight; | ||
|
|
||
| @Column(name = "expected_quantity", precision = 8, scale = 3) | ||
| private BigDecimal expectedQuantity; | ||
|
|
||
| @Column(name = "actual_quantity") | ||
| private Integer actualQuantity; | ||
|
|
||
| @Column(name = "unit_price") | ||
| private Integer unitPrice; | ||
|
|
||
| @Column(name = "amount") | ||
| private Integer amount; | ||
|
|
||
| @Column(name = "error_rate", precision = 6, scale = 3) | ||
| private BigDecimal errorRate; | ||
|
|
||
| @Column(name = "note", columnDefinition = "TEXT") | ||
| private String note; | ||
|
|
||
| public void setReceipt(Receipt receipt) { | ||
| this.receipt = receipt; | ||
| } | ||
|
|
||
| public void update( | ||
| Integer boxCount, | ||
| BigDecimal measuredWeight, | ||
| BigDecimal expectedQuantity, | ||
| Integer actualQuantity, | ||
| Integer unitPrice, | ||
| BigDecimal errorRate, | ||
| String note | ||
| ) { | ||
| if (boxCount != null) { | ||
| this.boxCount = boxCount; | ||
| } | ||
| if (measuredWeight != null) { | ||
| this.measuredWeight = measuredWeight; | ||
| } | ||
| if (expectedQuantity != null) { | ||
| this.expectedQuantity = expectedQuantity; | ||
| } | ||
| if (actualQuantity != null) { | ||
| this.actualQuantity = actualQuantity; | ||
| } | ||
| if (unitPrice != null) { | ||
| this.unitPrice = unitPrice; | ||
| } | ||
| if (errorRate != null) { | ||
| this.errorRate = errorRate; | ||
| } | ||
| if (note != null) { | ||
| this.note = note; | ||
| } | ||
| if (this.actualQuantity != null && this.unitPrice != null) { | ||
| this.amount = this.actualQuantity * this.unitPrice; | ||
| } | ||
| } | ||
| } |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/almang/inventory/receipt/domain/ReceiptStatus.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.almang.inventory.receipt.domain; | ||
|
|
||
| public enum ReceiptStatus { | ||
| PENDING, | ||
| CONFIRMED, | ||
| CANCELED | ||
| } |
32 changes: 32 additions & 0 deletions
32
src/main/java/com/almang/inventory/receipt/dto/response/ReceiptItemResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.almang.inventory.receipt.dto.response; | ||
|
|
||
| import com.almang.inventory.receipt.domain.ReceiptItem; | ||
| import java.math.BigDecimal; | ||
|
|
||
| public record ReceiptItemResponse( | ||
| Long receiptItemId, | ||
| Long receiptId, | ||
| Long productId, | ||
| Integer boxCount, | ||
| BigDecimal measuredWeight, | ||
| BigDecimal expectedQuantity, | ||
| Integer actualQuantity, | ||
| Integer amount, | ||
| BigDecimal errorRate, | ||
| String note | ||
| ) { | ||
| public static ReceiptItemResponse from(ReceiptItem receiptItem) { | ||
| return new ReceiptItemResponse( | ||
| receiptItem.getId(), | ||
| receiptItem.getReceipt().getId(), | ||
| receiptItem.getProduct().getId(), | ||
| receiptItem.getBoxCount(), | ||
| receiptItem.getMeasuredWeight(), | ||
| receiptItem.getExpectedQuantity(), | ||
| receiptItem.getActualQuantity(), | ||
| receiptItem.getAmount(), | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| receiptItem.getErrorRate(), | ||
| receiptItem.getNote() | ||
| ); | ||
| } | ||
| } | ||
35 changes: 35 additions & 0 deletions
35
src/main/java/com/almang/inventory/receipt/dto/response/ReceiptResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| package com.almang.inventory.receipt.dto.response; | ||
|
|
||
| import com.almang.inventory.receipt.domain.Receipt; | ||
| import com.almang.inventory.receipt.domain.ReceiptStatus; | ||
| import java.math.BigDecimal; | ||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| public record ReceiptResponse( | ||
| Long receiptId, | ||
| Long storeId, | ||
| Long orderId, | ||
| LocalDate receiptDate, | ||
| Integer totalBoxCount, | ||
| BigDecimal totalWeightG, | ||
| ReceiptStatus status, | ||
| boolean activated, | ||
| List<ReceiptItemResponse> receiptItems | ||
| ) { | ||
| public static ReceiptResponse from(Receipt receipt) { | ||
| return new ReceiptResponse( | ||
| receipt.getId(), | ||
| receipt.getStore().getId(), | ||
| receipt.getOrder().getId(), | ||
| receipt.getReceiptDate(), | ||
| receipt.getTotalBoxCount(), | ||
| receipt.getTotalWeightG(), | ||
| receipt.getStatus(), | ||
| receipt.isActivated(), | ||
| receipt.getItems().stream() | ||
| .map(ReceiptItemResponse::from) | ||
| .toList() | ||
| ); | ||
| } | ||
| } |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/almang/inventory/receipt/repository/ReceiptRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.almang.inventory.receipt.repository; | ||
|
|
||
| import com.almang.inventory.receipt.domain.Receipt; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface ReceiptRepository extends JpaRepository<Receipt, Long> { | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
컨트롤러 구조가 깔끔하게 구현되었습니다!
RESTful 엔드포인트 설계, 인증 처리, 서비스 레이어로의 위임이 잘 구현되어 있습니다. 로깅도 적절히 추가되어 운영 시 추적이 용이합니다.
다만, Line 32에서
userPrincipal.getId()를 호출하기 전에 userPrincipal이 null이 아님을 보장하는지 확인이 필요합니다. Spring Security 설정에서 인증되지 않은 요청을 필터링한다면 문제없지만, 방어적 코딩 관점에서 null 체크를 고려할 수 있습니다.다음 스크립트로 Spring Security 설정을 확인하여 /api/v1/receipt 경로가 인증을 요구하는지 검증하세요:
🏁 Script executed:
Length of output: 1883
이 리뷰 코멘트를 검증하기 위해 추가 정보를 수집하겠습니다.
스크립트 결과를 분석하니 중요한 발견이 있습니다:
SecurityConfig의 인증 설정 분석:
/api/v1/auth/logoutonly.anyRequest().permitAll()(라인 44)이는
/api/v1/receipt경로가 인증 보호를 받지 않음을 의미합니다.Receipt 컨트롤러의 메서드별 보안 설정과 더 정확한 상황을 확인하기 위해 추가 검증이 필요합니다:
추가 검증을 진행했습니다. 중요한 발견이 있습니다:
보안 설정 검증 결과:
스크립트와 웹 검색 결과에 따르면:
Receipt 엔드포인트는 인증 보호를 받지 않습니다
/api/v1/auth/logout만.authenticated()지정.anyRequest().permitAll()- 모든 다른 요청 허용@AuthenticationPrincipal의 동작
@AuthenticationPrincipal은 사용자가 인증되면 principal을 주입하거나, 인증되지 않으면 null을 주입합니다.
현재 코드의 실제 문제
userPrincipal이 null일 수 있습니다userPrincipal.getId()를 호출하면 NullPointerException 발생 가능내 관찰:
원본 리뷰는 null 체크 필요성을 올바르게 지적했지만, 엔드포인트가 이미 인증되어 있다고 잘못 가정했습니다. 실제로는 더 심각한 상황입니다 — 엔드포인트 자체가 인증을 요구해야 할 것으로 보입니다.
검증이 필요한 사항:
Receipt 생성 엔드포인트가 인증된 사용자만 접근해야 하는 기능인지 확인하겠습니다.
🏁 Script executed:
Length of output: 326
🏁 Script executed:
Length of output: 0
🏁 Script executed:
Length of output: 2392
라인 32에서
NullPointerException발생 위험이 있습니다.검증 결과, Authentication이 null이면 @AuthenticationPrincipal도 null을 반환합니다. SecurityConfig에서
/api/v1/receipt경로는 명시적 인증 보호가 없고.anyRequest().permitAll()에 포함되어 있으므로, 인증되지 않은 사용자도 접근할 수 있습니다. 따라서userPrincipal이 null일 수 있고,userPrincipal.getId()호출 시 NullPointerException이 발생합니다.개선 방안:
Spring Security 공식 문서에서는 인증 요청인 엔드포인트는 먼저 인증을 강제하는 것이 권장됩니다. 현재 코드는 인증된 사용자의 ID를 필요로 하므로, 엔드포인트 수준에서 인증을 요구하는 것이 설계 의도에 부합합니다.
🤖 Prompt for AI Agents
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
개발 중이라 엔드포인트 보호를 하지 않은 상태입니다. 추후 변경 예정입니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.