From 31c6eced2710274b2ed24f142886703bbd59b67a Mon Sep 17 00:00:00 2001 From: kent Date: Tue, 6 Jan 2026 13:28:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=AC=EC=A0=81=20BOM=20=EC=9E=90?= =?UTF-8?q?=EC=9E=AC=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FormulaEvaluatorService: 품목마스터에서 규격/단위 조회 (getItemSpecAndUnit) - QuoteService: 저장된 calculation_inputs로 BOM 자재 계산 (calculateBomMaterials) - 견적 조회 시 bom_materials 데이터 자동 포함 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Quote/FormulaEvaluatorService.php | 68 ++++++++++++++--- app/Services/Quote/QuoteService.php | 73 ++++++++++++++++++- 2 files changed, 130 insertions(+), 11 deletions(-) diff --git a/app/Services/Quote/FormulaEvaluatorService.php b/app/Services/Quote/FormulaEvaluatorService.php index 876549e..b21fae9 100644 --- a/app/Services/Quote/FormulaEvaluatorService.php +++ b/app/Services/Quote/FormulaEvaluatorService.php @@ -3,6 +3,7 @@ namespace App\Services\Quote; use App\Models\CategoryGroup; +use App\Models\Items\Item; use App\Models\Products\Price; use App\Models\Quote\QuoteFormula; use App\Services\Service; @@ -479,11 +480,16 @@ public function executeAll(Collection $formulasByCategory, array $inputVariables ? $this->evaluate($item->unit_price_formula) : $this->getItemPrice($item->item_code); + // 품목마스터에서 규격/단위 조회 (우선순위: 품목마스터 > 수식품목) + $itemMasterData = $this->getItemSpecAndUnit($item->item_code); + $specification = $itemMasterData['specification'] ?? $item->specification; + $unit = $itemMasterData['unit'] ?? $item->unit ?? 'EA'; + $items[] = [ 'item_code' => $item->item_code, 'item_name' => $item->item_name, - 'specification' => $item->specification, - 'unit' => $item->unit, + 'specification' => $specification, + 'unit' => $unit, 'quantity' => $quantity, 'unit_price' => $unitPrice, 'total_price' => $quantity * $unitPrice, @@ -729,6 +735,8 @@ public function calculateBomWithDebug( 'item_code' => $bomItem['item_code'], 'item_name' => $bomItem['item_name'], 'item_category' => $itemCategory, + 'specification' => $bomItem['specification'] ?? null, + 'unit' => $bomItem['unit'] ?? 'EA', 'quantity' => $displayQuantity, 'quantity_formula' => $quantityFormula, 'base_price' => $basePrice, @@ -1071,6 +1079,41 @@ private function getItemCategory(string $itemCode, int $tenantId): string return $category ?? '기타'; } + /** + * 품목마스터에서 규격(specification)과 단위(unit) 조회 + * + * @param string $itemCode 품목 코드 + * @return array ['specification' => string|null, 'unit' => string] + */ + private function getItemSpecAndUnit(string $itemCode): array + { + $tenantId = $this->tenantId(); + + if (! $tenantId) { + return ['specification' => null, 'unit' => 'EA']; + } + + // Items 테이블에서 기본 정보 조회 + $item = Item::where('tenant_id', $tenantId) + ->where('code', $itemCode) + ->whereNull('deleted_at') + ->with('details') + ->first(); + + if (! $item) { + return ['specification' => null, 'unit' => 'EA']; + } + + // specification은 ItemDetail에서, unit은 Item에서 가져옴 + $specification = $item->details?->specification ?? null; + $unit = $item->unit ?? 'EA'; + + return [ + 'specification' => $specification, + 'unit' => $unit, + ]; + } + /** * BOM 전개 (수량 수식 포함) */ @@ -1102,18 +1145,22 @@ private function expandBomWithFormulas(array $finishedGoods, array $variables, i $quantityFormula = $bomEntry['quantityFormula'] ?? $bomEntry['quantity'] ?? '1'; if ($childItemId) { - // ID 기반 조회 + // ID 기반 조회 (item_details 조인하여 specification 포함) $childItem = DB::table('items') - ->where('tenant_id', $tenantId) - ->where('id', $childItemId) - ->whereNull('deleted_at') + ->leftJoin('item_details', 'items.id', '=', 'item_details.item_id') + ->where('items.tenant_id', $tenantId) + ->where('items.id', $childItemId) + ->whereNull('items.deleted_at') + ->select('items.*', 'item_details.specification') ->first(); } elseif ($childItemCode) { - // 코드 기반 조회 + // 코드 기반 조회 (item_details 조인하여 specification 포함) $childItem = DB::table('items') - ->where('tenant_id', $tenantId) - ->where('code', $childItemCode) - ->whereNull('deleted_at') + ->leftJoin('item_details', 'items.id', '=', 'item_details.item_id') + ->where('items.tenant_id', $tenantId) + ->where('items.code', $childItemCode) + ->whereNull('items.deleted_at') + ->select('items.*', 'item_details.specification') ->first(); } else { continue; @@ -1127,6 +1174,7 @@ private function expandBomWithFormulas(array $finishedGoods, array $variables, i 'process_type' => $childItem->process_type, 'quantity_formula' => (string) $quantityFormula, 'unit' => $childItem->unit, + 'specification' => $childItem->specification, ]; // 재귀적 BOM 전개 (반제품인 경우) diff --git a/app/Services/Quote/QuoteService.php b/app/Services/Quote/QuoteService.php index a317062..8325f23 100644 --- a/app/Services/Quote/QuoteService.php +++ b/app/Services/Quote/QuoteService.php @@ -16,7 +16,8 @@ class QuoteService extends Service { public function __construct( - private QuoteNumberService $numberService + private QuoteNumberService $numberService, + private QuoteCalculationService $calculationService ) {} /** @@ -90,9 +91,79 @@ public function show(int $id): Quote throw new NotFoundHttpException(__('error.quote_not_found')); } + // BOM 자재 데이터 계산 및 추가 + $bomMaterials = $this->calculateBomMaterials($quote); + if (! empty($bomMaterials)) { + $quote->setAttribute('bom_materials', $bomMaterials); + } + return $quote; } + /** + * 저장된 calculation_inputs를 기반으로 BOM 자재 목록 계산 + */ + private function calculateBomMaterials(Quote $quote): array + { + $calculationInputs = $quote->calculation_inputs; + + // calculation_inputs가 없거나 items가 없으면 빈 배열 반환 + if (empty($calculationInputs) || empty($calculationInputs['items'])) { + return []; + } + + $inputItems = $calculationInputs['items']; + $allMaterials = []; + + foreach ($inputItems as $index => $input) { + // 완제품 코드 찾기 (productName에 저장됨) + $finishedGoodsCode = $input['productName'] ?? null; + if (! $finishedGoodsCode) { + continue; + } + + // BOM 계산을 위한 입력 변수 구성 + $bomInputs = [ + 'W0' => (float) ($input['openWidth'] ?? 0), + 'H0' => (float) ($input['openHeight'] ?? 0), + 'QTY' => (float) ($input['quantity'] ?? 1), + 'PC' => $input['productCategory'] ?? 'SCREEN', + 'GT' => $input['guideRailType'] ?? 'wall', + 'MP' => $input['motorPower'] ?? 'single', + 'CT' => $input['controller'] ?? 'basic', + 'WS' => (float) ($input['wingSize'] ?? 50), + 'INSP' => (float) ($input['inspectionFee'] ?? 50000), + ]; + + try { + $result = $this->calculationService->calculateBom($finishedGoodsCode, $bomInputs, false); + + if (($result['success'] ?? false) && ! empty($result['items'])) { + // 각 자재 항목에 인덱스 정보 추가 + foreach ($result['items'] as $material) { + $allMaterials[] = [ + 'item_index' => $index, + 'finished_goods_code' => $finishedGoodsCode, + 'item_code' => $material['item_code'] ?? '', + 'item_name' => $material['item_name'] ?? '', + 'specification' => $material['specification'] ?? '', + 'unit' => $material['unit'] ?? 'EA', + 'quantity' => $material['quantity'] ?? 0, + 'unit_price' => $material['unit_price'] ?? 0, + 'total_price' => $material['total_price'] ?? 0, + 'formula_category' => $material['formula_category'] ?? '', + ]; + } + } + } catch (\Throwable) { + // BOM 계산 실패 시 해당 품목은 스킵 + continue; + } + } + + return $allMaterials; + } + /** * 견적 생성 */