diff --git a/src/main/java/com/sampoom/backend/api/bom/controller/BomController.java b/src/main/java/com/sampoom/backend/api/bom/controller/BomController.java index 0eb1523..2070d44 100644 --- a/src/main/java/com/sampoom/backend/api/bom/controller/BomController.java +++ b/src/main/java/com/sampoom/backend/api/bom/controller/BomController.java @@ -77,4 +77,18 @@ public ResponseEntity>> searchBoms( return ApiResponse.success(SuccessStatus.OK, bomService.searchBoms(keyword, categoryId, groupId, status, complexity, page, size)); } +// +// @Operation(summary = "모든 BOM totalCost 재계산", description = "quantity가 Double로 변경된 후 모든 BOM의 totalCost를 재계산합니다.") +// @PostMapping("/recalculate-all-costs") +// public ResponseEntity> recalculateAllBomTotalCosts() { +// bomService.recalculateAllBomTotalCosts(); +// return ApiResponse.success_only(SuccessStatus.OK); +// } +// +// @Operation(summary = "특정 BOM totalCost 재계산", description = "특정 BOM의 totalCost를 재계산합니다.") +// @PostMapping("/{bomId}/recalculate-cost") +// public ResponseEntity> recalculateBomTotalCost(@PathVariable Long bomId) { +// bomService.recalculateBomTotalCost(bomId); +// return ApiResponse.success_only(SuccessStatus.OK); +// } } diff --git a/src/main/java/com/sampoom/backend/api/bom/dto/BomDetailResponseDTO.java b/src/main/java/com/sampoom/backend/api/bom/dto/BomDetailResponseDTO.java index ab24054..8f45f47 100644 --- a/src/main/java/com/sampoom/backend/api/bom/dto/BomDetailResponseDTO.java +++ b/src/main/java/com/sampoom/backend/api/bom/dto/BomDetailResponseDTO.java @@ -32,12 +32,11 @@ public class BomDetailResponseDTO { @AllArgsConstructor @Builder public static class BomMaterialDTO { - private Long id; private Long materialId; private String materialName; private String materialCode; private String unit; - private Long quantity; + private Double quantity; private Long standardCost; // 단가 private Long total; // 단가 * 수량 } @@ -46,10 +45,9 @@ public static BomDetailResponseDTO from(Bom bom) { List materialDtos = bom.getMaterials().stream() .map(material -> { Long cost = Optional.ofNullable(material.getMaterial().getStandardCost()).orElse(0L); - Long total = cost * Optional.ofNullable(material.getQuantity()).orElse(0L); + Long total = cost * Optional.ofNullable(material.getQuantity()).orElse(0.0).longValue(); return BomMaterialDTO.builder() - .id(material.getId()) .materialId(material.getMaterial().getId()) .materialName(material.getMaterial().getName()) .materialCode(material.getMaterial().getMaterialCode()) diff --git a/src/main/java/com/sampoom/backend/api/bom/dto/BomRequestDTO.java b/src/main/java/com/sampoom/backend/api/bom/dto/BomRequestDTO.java index 1e46151..12c7a5e 100644 --- a/src/main/java/com/sampoom/backend/api/bom/dto/BomRequestDTO.java +++ b/src/main/java/com/sampoom/backend/api/bom/dto/BomRequestDTO.java @@ -23,6 +23,6 @@ public class BomRequestDTO { @Builder public static class BomMaterialDTO { private Long materialId; - private Long quantity; + private Double quantity; } } diff --git a/src/main/java/com/sampoom/backend/api/bom/dto/BomResponseDTO.java b/src/main/java/com/sampoom/backend/api/bom/dto/BomResponseDTO.java index d9d463e..f98e2ad 100644 --- a/src/main/java/com/sampoom/backend/api/bom/dto/BomResponseDTO.java +++ b/src/main/java/com/sampoom/backend/api/bom/dto/BomResponseDTO.java @@ -57,8 +57,8 @@ public static BomResponseDTO from(Bom bom) { // 실시간 계산 int componentCount = materials.size(); - long totalQuantity = materials.stream() - .mapToLong(BomMaterialResponse::getQuantity) + long totalQuantity = (long) materials.stream() + .mapToDouble(BomMaterialResponse::getQuantity) .sum(); return BomResponseDTO.builder() @@ -93,16 +93,13 @@ public static class BomMaterialResponse { private String materialName; private String materialCode; private String unit; - private Long quantity; + private Double quantity; private Long standardCost; private Long total; // 단가 * 수량 public static BomMaterialResponse from(BomMaterial bm) { Long cost = bm.getMaterial().getStandardCost() != null - ? bm.getMaterial().getStandardCost() - : 0L; - Long total = cost * bm.getQuantity(); - + ? bm.getMaterial().getStandardCost() : 0L; return BomMaterialResponse.builder() .materialId(bm.getMaterial().getId()) .materialName(bm.getMaterial().getName()) @@ -110,7 +107,7 @@ public static BomMaterialResponse from(BomMaterial bm) { .unit(bm.getMaterial().getMaterialUnit()) .quantity(bm.getQuantity()) .standardCost(cost) - .total(total) + .total(cost * bm.getQuantity().longValue()) // Double을 Long으로 변환 .build(); } } diff --git a/src/main/java/com/sampoom/backend/api/bom/entity/Bom.java b/src/main/java/com/sampoom/backend/api/bom/entity/Bom.java index 214726a..a0d239a 100644 --- a/src/main/java/com/sampoom/backend/api/bom/entity/Bom.java +++ b/src/main/java/com/sampoom/backend/api/bom/entity/Bom.java @@ -70,8 +70,8 @@ public void addMaterial(BomMaterial bomMaterial) { public void calculateTotalCost() { this.totalCost = this.materials.stream() .mapToLong(m -> { - if (m.getMaterial().getStandardCost() == null) return 0L; - return m.getMaterial().getStandardCost() * m.getQuantity(); + if (m.getMaterial().getStandardCost() == null || m.getQuantity() == null) return 0L; + return (long) (m.getMaterial().getStandardCost() * m.getQuantity()); }) .sum(); } diff --git a/src/main/java/com/sampoom/backend/api/bom/entity/BomMaterial.java b/src/main/java/com/sampoom/backend/api/bom/entity/BomMaterial.java index fdba030..7ce5ba4 100644 --- a/src/main/java/com/sampoom/backend/api/bom/entity/BomMaterial.java +++ b/src/main/java/com/sampoom/backend/api/bom/entity/BomMaterial.java @@ -27,13 +27,13 @@ public class BomMaterial { @JoinColumn(name = "material_id", nullable = false) private Material material; - private Long quantity; + private Double quantity; public void updateBom(Bom bom) { this.bom = bom; } - public void updateQuantity(Long quantity) { + public void updateQuantity(Double quantity) { this.quantity = quantity; } diff --git a/src/main/java/com/sampoom/backend/api/bom/event/dto/BomEvent.java b/src/main/java/com/sampoom/backend/api/bom/event/dto/BomEvent.java index 631336e..164df6c 100644 --- a/src/main/java/com/sampoom/backend/api/bom/event/dto/BomEvent.java +++ b/src/main/java/com/sampoom/backend/api/bom/event/dto/BomEvent.java @@ -45,7 +45,7 @@ public static class MaterialInfo { private String materialName; private String materialCode; private String unit; - private Long quantity; + private Double quantity; } } } diff --git a/src/main/java/com/sampoom/backend/api/bom/service/BomService.java b/src/main/java/com/sampoom/backend/api/bom/service/BomService.java index 6fcfd82..75de864 100644 --- a/src/main/java/com/sampoom/backend/api/bom/service/BomService.java +++ b/src/main/java/com/sampoom/backend/api/bom/service/BomService.java @@ -55,11 +55,11 @@ public BomResponseDTO createBom(BomRequestDTO requestDTO) { } // 요청 자재 중복 제거 및 수량 합산 - Map idToQty = requestDTO.getMaterials().stream() + Map idToQty = requestDTO.getMaterials().stream() .collect(Collectors.toMap( BomRequestDTO.BomMaterialDTO::getMaterialId, BomRequestDTO.BomMaterialDTO::getQuantity, - Long::sum // 중복 materialId 수량 합산 + Double::sum // 중복 materialId 수량 합산 )); // 한 번의 쿼리로 모든 자재 조회 (N+1 방지) @@ -105,7 +105,7 @@ public BomResponseDTO createBom(BomRequestDTO requestDTO) { // 5️⃣ 자재 매핑 for (Material material : materials) { - Long quantity = idToQty.get(material.getId()); + Double quantity = idToQty.get(material.getId()); BomMaterial bm = BomMaterial.builder() .bom(bom) .material(material) @@ -347,4 +347,27 @@ public void updateBomStatus(Long bomId, BomStatus newStatus) { bom.updateStatus(newStatus); bomRepository.save(bom); } + + /** + * 모든 BOM의 totalCost 재계산 (quantity가 Double로 변경된 후 실행) + */ + @Transactional + public void recalculateAllBomTotalCosts() { + List boms = bomRepository.findAll(); + for (Bom bom : boms) { + bom.calculateTotalCost(); + } + bomRepository.saveAll(boms); + } + + /** + * 특정 BOM의 totalCost 재계산 + */ + @Transactional + public void recalculateBomTotalCost(Long bomId) { + Bom bom = bomRepository.findById(bomId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.BOM_NOT_FOUND)); + bom.calculateTotalCost(); + bomRepository.save(bom); + } } diff --git a/src/main/java/com/sampoom/backend/api/part/controller/PartController.java b/src/main/java/com/sampoom/backend/api/part/controller/PartController.java index 08b3984..0396d54 100644 --- a/src/main/java/com/sampoom/backend/api/part/controller/PartController.java +++ b/src/main/java/com/sampoom/backend/api/part/controller/PartController.java @@ -107,4 +107,18 @@ public ResponseEntity> updatePart( // // return ApiResponse.success(SuccessStatus.PART_LIST_SUCCESS, partPage); // } +// +// @Operation(summary = "모든 Part standard_total_cost 재계산", description = "standard_total_cost 컬럼 추가 후 모든 Part의 비용을 재계산합니다.") +// @PostMapping("/recalculate-all-costs") +// public ResponseEntity> recalculateAllPartStandardCosts() { +// partService.recalculateAllPartStandardCosts(); +// return ApiResponse.success_only(SuccessStatus.OK); +// } +// +// @Operation(summary = "특정 Part standard_total_cost 재계산", description = "특정 Part의 standard_total_cost를 재계산합니다.") +// @PostMapping("/{partId}/recalculate-cost") +// public ResponseEntity> recalculatePartStandardCost(@PathVariable Long partId) { +// partService.recalculatePartStandardCost(partId); +// return ApiResponse.success_only(SuccessStatus.OK); +// } } diff --git a/src/main/java/com/sampoom/backend/api/part/entity/Part.java b/src/main/java/com/sampoom/backend/api/part/entity/Part.java index f788fc3..cc612ab 100644 --- a/src/main/java/com/sampoom/backend/api/part/entity/Part.java +++ b/src/main/java/com/sampoom/backend/api/part/entity/Part.java @@ -36,6 +36,8 @@ public class Part extends BaseTimeEntity { private Long standardCost; // 표준 단가 (자동 계산, 입력 X) + @Column(name = "standard_total_cost") + private Long standardTotalCost; // 표준 총비용 (BOM 비용 * 기준수량 + 공정비) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "group_id", nullable = false) @@ -99,11 +101,67 @@ public void changeCode(String newCode) { // Part의 총 비용을 BOM 비용과 Process 비용을 합쳐서 계산하는 메서드 public void calculateStandardCost(Long bomCost, Long processCost) { - this.standardCost = (bomCost != null ? bomCost : 0L) + (processCost != null ? processCost : 0L); + // standard_total_cost = bom 비용 * standard_quantity + 공정비 + Long bomTotalCost = (bomCost != null ? bomCost : 0L) * (standardQuantity != null ? standardQuantity : 1); + Long totalProcessCost = processCost != null ? processCost : 0L; + this.standardTotalCost = bomTotalCost + totalProcessCost; + + // standard_cost = standard_total_cost / standard_quantity (1000원 단위로 반올림) + Long rawStandardCost = this.standardQuantity != null && this.standardQuantity > 0 + ? this.standardTotalCost / this.standardQuantity + : this.standardTotalCost; + this.standardCost = roundToThousand(rawStandardCost); + } + + // BOM 비용과 공정비를 분리해서 계산하는 새로운 메서드 + public void calculateStandardCostAndTotal(Long bomCost, Long processCost) { + Long bomUnitCost = bomCost != null ? bomCost : 0L; + Long processUnitCost = processCost != null ? processCost : 0L; + Integer qty = standardQuantity != null ? standardQuantity : 1; + + // standard_total_cost = bom 비용 * standard_quantity + 공정비 + this.standardTotalCost = (bomUnitCost * qty) + processUnitCost; + + // standard_cost = standard_total_cost / standard_quantity (1000원 단위로 반올림) + Long rawStandardCost = qty > 0 ? this.standardTotalCost / qty : this.standardTotalCost; + this.standardCost = roundToThousand(rawStandardCost); } // standardCost 직접 설정 메서드 (필요시) public void setStandardCost(Long standardCost) { - this.standardCost = standardCost; + this.standardCost = roundToThousand(standardCost); + } + + // standardTotalCost 직접 설정 메서드 + public void setStandardTotalCost(Long standardTotalCost) { + this.standardTotalCost = standardTotalCost; + // standard_cost 재계산 (1000원 단위로 반올림) + Long rawStandardCost = this.standardQuantity != null && this.standardQuantity > 0 + ? this.standardTotalCost / this.standardQuantity + : this.standardTotalCost; + this.standardCost = roundToThousand(rawStandardCost); + } + + // standardQuantity 변경 시 비용 재계산 + public void updateStandardQuantity(Integer standardQuantity) { + this.standardQuantity = standardQuantity; + // 기존 standardTotalCost가 있다면 standardCost 재계산 (1000원 단위로 반올림) + if (this.standardTotalCost != null) { + Long rawStandardCost = standardQuantity != null && standardQuantity > 0 + ? this.standardTotalCost / standardQuantity + : this.standardTotalCost; + this.standardCost = roundToThousand(rawStandardCost); + } + } + + /** + * 1000원 단위로 반올림하는 헬퍼 메서드 + */ + private Long roundToThousand(Long amount) { + if (amount == null) { + return null; + } + // 1000으로 나누고 반올림한 후 다시 1000을 곱함 + return Math.round(amount / 1000.0) * 1000L; } } diff --git a/src/main/java/com/sampoom/backend/api/part/event/dto/PartEvent.java b/src/main/java/com/sampoom/backend/api/part/event/dto/PartEvent.java index 0d648fc..f642b8b 100644 --- a/src/main/java/com/sampoom/backend/api/part/event/dto/PartEvent.java +++ b/src/main/java/com/sampoom/backend/api/part/event/dto/PartEvent.java @@ -35,5 +35,6 @@ public static class Payload { private Long categoryId; private Long standardCost; + private Long standardTotalCost; // 표준 총비용 추가 } } diff --git a/src/main/java/com/sampoom/backend/api/part/event/service/PartEventBatchService.java b/src/main/java/com/sampoom/backend/api/part/event/service/PartEventBatchService.java index e3671cb..9970511 100644 --- a/src/main/java/com/sampoom/backend/api/part/event/service/PartEventBatchService.java +++ b/src/main/java/com/sampoom/backend/api/part/event/service/PartEventBatchService.java @@ -61,6 +61,7 @@ public void publishAllPartEvents() { .groupId(group != null ? group.getId() : null) .categoryId(category != null ? category.getId() : null) .standardCost(part.getStandardCost()) + .standardTotalCost(part.getStandardTotalCost()) .build()) .build(); diff --git a/src/main/java/com/sampoom/backend/api/part/service/PartService.java b/src/main/java/com/sampoom/backend/api/part/service/PartService.java index 1e07c00..75c0d97 100644 --- a/src/main/java/com/sampoom/backend/api/part/service/PartService.java +++ b/src/main/java/com/sampoom/backend/api/part/service/PartService.java @@ -170,6 +170,7 @@ public PartListResponseDTO createPart(PartCreateRequestDTO partCreateRequestDTO) .groupId(partGroup.getId()) .categoryId(partGroup.getCategory().getId()) .standardCost(savedPart.getStandardCost()) + .standardTotalCost(savedPart.getStandardTotalCost()) .build()) .build(); @@ -231,6 +232,7 @@ public PartListResponseDTO updatePart(Long partId, PartUpdateRequestDTO partUpda .groupId(part.getPartGroup().getId()) .categoryId(part.getPartGroup().getCategory().getId()) .standardCost(part.getStandardCost()) + .standardTotalCost(part.getStandardTotalCost()) .build()) .build(); @@ -283,6 +285,7 @@ public void deletePart(Long partId) { .groupId(part.getPartGroup().getId()) .categoryId(part.getPartGroup().getCategory().getId()) .standardCost(part.getStandardCost()) + .standardTotalCost(part.getStandardTotalCost()) .build()) .build(); @@ -430,6 +433,7 @@ public void publishPartUpdatedEvent(Part part) { .groupId(part.getPartGroup().getId()) .categoryId(part.getPartGroup().getCategory().getId()) .standardCost(part.getStandardCost()) + .standardTotalCost(part.getStandardTotalCost()) .build()) .build(); @@ -481,4 +485,33 @@ public Long getProcessCostByPartId(Long partId) { Process process = processRepository.findByPartId(partId).orElse(null); return process != null ? process.getTotalProcessCost() : 0L; } + + /** + * 모든 Part의 standard_total_cost 재계산 + */ + @Transactional + public void recalculateAllPartStandardCosts() { + List parts = partRepository.findAll(); + for (Part part : parts) { + Long bomCost = getBomCostByPartId(part.getId()); + Long processCost = getProcessCostByPartId(part.getId()); + part.calculateStandardCost(bomCost, processCost); + } + partRepository.saveAll(parts); + } + + /** + * 특정 Part의 standard_total_cost 재계산 + */ + @Transactional + public void recalculatePartStandardCost(Long partId) { + Part part = partRepository.findById(partId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.PART_NOT_FOUND)); + + Long bomCost = getBomCostByPartId(partId); + Long processCost = getProcessCostByPartId(partId); + part.calculateStandardCost(bomCost, processCost); + + partRepository.save(part); + } }