parameterService = $parameterService; $this->formulaService = $formulaService; $this->ruleService = $ruleService; } /** * 매개변수 기반 BOM 해석 (전체 프로세스) */ public function resolveBom(int $modelId, array $inputParameters, ?int $templateId = null): array { $tenantId = $this->tenantId(); // 1. 모델 존재 확인 $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); if (!$model) { throw new NotFoundHttpException(__('error.model_not_found')); } // 2. 매개변수 검증 및 기본값 적용 $validatedParameters = $this->parameterService->validateParameters($modelId, $inputParameters); // 3. 공식 계산 실행 $calculatedValues = $this->formulaService->calculateFormulas($modelId, $validatedParameters); // 4. 조건 규칙 평가 $ruleResults = $this->ruleService->evaluateRules($modelId, $calculatedValues); // 5. 기본 BOM 템플릿 조회 (지정된 템플릿 또는 최신 버전) $baseBom = $this->getBaseBomTemplate($modelId, $templateId); // 6. 조건 규칙 적용으로 BOM 변환 $resolvedBom = $this->applyRulesToBom($baseBom, $ruleResults['bom_actions'], $calculatedValues); // 7. BOM 아이템 정보 보강 (재료/제품 세부정보) $enrichedBom = $this->enrichBomItems($resolvedBom); return [ 'model' => [ 'id' => $model->id, 'code' => $model->code, 'name' => $model->name, ], 'input_parameters' => $validatedParameters, 'calculated_values' => $calculatedValues, 'matched_rules' => $ruleResults['matched_rules'], 'base_bom_template_id' => $baseBom['template_id'] ?? null, 'resolved_bom' => $enrichedBom, 'summary' => $this->generateBomSummary($enrichedBom), ]; } /** * BOM 해석 미리보기 (저장하지 않음) */ public function previewBom(int $modelId, array $inputParameters, ?int $templateId = null): array { return $this->resolveBom($modelId, $inputParameters, $templateId); } /** * 매개변수 변경에 따른 BOM 차이 분석 */ public function compareBomByParameters(int $modelId, array $parameters1, array $parameters2, ?int $templateId = null): array { // 두 매개변수 세트로 각각 BOM 해석 $bom1 = $this->resolveBom($modelId, $parameters1, $templateId); $bom2 = $this->resolveBom($modelId, $parameters2, $templateId); return [ 'parameters_diff' => [ 'set1' => $parameters1, 'set2' => $parameters2, 'changed' => array_diff_assoc($parameters2, $parameters1), ], 'calculated_values_diff' => [ 'set1' => $bom1['calculated_values'], 'set2' => $bom2['calculated_values'], 'changed' => array_diff_assoc($bom2['calculated_values'], $bom1['calculated_values']), ], 'bom_diff' => $this->compareBomItems($bom1['resolved_bom'], $bom2['resolved_bom']), 'summary_diff' => [ 'set1' => $bom1['summary'], 'set2' => $bom2['summary'], ], ]; } /** * 기본 BOM 템플릿 조회 */ private function getBaseBomTemplate(int $modelId, ?int $templateId = null): array { $tenantId = $this->tenantId(); if ($templateId) { // 지정된 템플릿 사용 $template = BomTemplate::where('tenant_id', $tenantId) ->where('id', $templateId) ->first(); } else { // 해당 모델의 최신 버전에서 BOM 템플릿 찾기 $template = BomTemplate::query() ->where('tenant_id', $tenantId) ->whereHas('modelVersion', function ($q) use ($modelId) { $q->where('model_id', $modelId) ->where('status', 'RELEASED'); }) ->orderByDesc('created_at') ->first(); } if (!$template) { // 기본 템플릿이 없으면 빈 BOM 반환 return [ 'template_id' => null, 'items' => [], ]; } $items = BomTemplateItem::query() ->where('bom_template_id', $template->id) ->orderBy('order') ->get() ->map(function ($item) { return [ 'target_type' => $item->ref_type, 'target_id' => $item->ref_id, 'quantity' => $item->quantity, 'waste_rate' => $item->waste_rate, 'reason' => 'base_template', ]; }) ->toArray(); return [ 'template_id' => $template->id, 'items' => $items, ]; } /** * 조건 규칙을 BOM에 적용 */ private function applyRulesToBom(array $baseBom, array $bomActions, array $calculatedValues): array { $currentBom = $baseBom['items']; foreach ($bomActions as $action) { switch ($action['action_type']) { case 'INCLUDE': // 새 아이템 추가 (중복 체크) $exists = collect($currentBom)->contains(function ($item) use ($action) { return $item['target_type'] === $action['target_type'] && $item['target_id'] === $action['target_id']; }); if (!$exists) { $currentBom[] = [ 'target_type' => $action['target_type'], 'target_id' => $action['target_id'], 'quantity' => $action['quantity_multiplier'] ?? 1, 'waste_rate' => 0, 'reason' => $action['rule_name'], ]; } break; case 'EXCLUDE': // 아이템 제외 $currentBom = array_filter($currentBom, function ($item) use ($action) { return !($item['target_type'] === $action['target_type'] && $item['target_id'] === $action['target_id']); }); break; case 'MODIFY_QUANTITY': // 수량 변경 foreach ($currentBom as &$item) { if ($item['target_type'] === $action['target_type'] && $item['target_id'] === $action['target_id']) { $multiplier = $action['quantity_multiplier'] ?? 1; // 공식으로 계산된 값이 있으면 그것을 사용 if (isset($calculatedValues['quantity_' . $action['target_id']])) { $item['quantity'] = $calculatedValues['quantity_' . $action['target_id']]; } else { $item['quantity'] = $item['quantity'] * $multiplier; } $item['reason'] = $action['rule_name']; } } break; } } return array_values($currentBom); // 인덱스 재정렬 } /** * BOM 아이템 정보 보강 */ private function enrichBomItems(array $bomItems): array { $tenantId = $this->tenantId(); $enriched = []; foreach ($bomItems as $item) { $enrichedItem = $item; if ($item['target_type'] === 'MATERIAL') { $material = Material::where('tenant_id', $tenantId) ->where('id', $item['target_id']) ->first(); if ($material) { $enrichedItem['target_info'] = [ 'id' => $material->id, 'code' => $material->code, 'name' => $material->name, 'unit' => $material->unit, 'type' => 'material', ]; } } elseif ($item['target_type'] === 'PRODUCT') { $product = Product::where('tenant_id', $tenantId) ->where('id', $item['target_id']) ->first(); if ($product) { $enrichedItem['target_info'] = [ 'id' => $product->id, 'code' => $product->code, 'name' => $product->name, 'unit' => $product->unit, 'type' => 'product', ]; } } // 실제 필요 수량 계산 (폐기율 적용) $baseQuantity = $enrichedItem['quantity']; $wasteRate = $enrichedItem['waste_rate'] ?? 0; $enrichedItem['actual_quantity'] = $baseQuantity * (1 + $wasteRate / 100); $enriched[] = $enrichedItem; } return $enriched; } /** * BOM 요약 정보 생성 */ private function generateBomSummary(array $bomItems): array { $totalItems = count($bomItems); $materialCount = 0; $productCount = 0; $totalValue = 0; // 나중에 가격 정보 추가 시 사용 foreach ($bomItems as $item) { if ($item['target_type'] === 'MATERIAL') { $materialCount++; } elseif ($item['target_type'] === 'PRODUCT') { $productCount++; } } return [ 'total_items' => $totalItems, 'material_count' => $materialCount, 'product_count' => $productCount, 'total_estimated_value' => $totalValue, 'generated_at' => now()->toISOString(), ]; } /** * BOM 아이템 비교 */ private function compareBomItems(array $bom1, array $bom2): array { $added = []; $removed = []; $modified = []; // BOM1에 있던 아이템들 체크 foreach ($bom1 as $item1) { $key = $item1['target_type'] . '_' . $item1['target_id']; $found = false; foreach ($bom2 as $item2) { if ($item2['target_type'] === $item1['target_type'] && $item2['target_id'] === $item1['target_id']) { $found = true; // 수량이 변경되었는지 체크 if ($item1['quantity'] != $item2['quantity']) { $modified[] = [ 'target_type' => $item1['target_type'], 'target_id' => $item1['target_id'], 'target_info' => $item1['target_info'] ?? null, 'old_quantity' => $item1['quantity'], 'new_quantity' => $item2['quantity'], 'change' => $item2['quantity'] - $item1['quantity'], ]; } break; } } if (!$found) { $removed[] = $item1; } } // BOM2에 새로 추가된 아이템들 foreach ($bom2 as $item2) { $found = false; foreach ($bom1 as $item1) { if ($item1['target_type'] === $item2['target_type'] && $item1['target_id'] === $item2['target_id']) { $found = true; break; } } if (!$found) { $added[] = $item2; } } return [ 'added' => $added, 'removed' => $removed, 'modified' => $modified, 'summary' => [ 'added_count' => count($added), 'removed_count' => count($removed), 'modified_count' => count($modified), ], ]; } /** * BOM 해석 결과 저장 (향후 주문/견적 연계용) */ public function saveBomResolution(int $modelId, array $inputParameters, array $bomResolution, string $purpose = 'ESTIMATION'): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($tenantId, $userId, $modelId, $inputParameters, $bomResolution, $purpose) { // BOM 해석 결과를 데이터베이스에 저장 // 향후 order_items, quotation_items 등과 연계할 수 있도록 구조 준비 $resolutionRecord = [ 'tenant_id' => $tenantId, 'model_id' => $modelId, 'input_parameters' => json_encode($inputParameters), 'calculated_values' => json_encode($bomResolution['calculated_values']), 'resolved_bom' => json_encode($bomResolution['resolved_bom']), 'matched_rules' => json_encode($bomResolution['matched_rules']), 'summary' => json_encode($bomResolution['summary']), 'purpose' => $purpose, 'created_by' => $userId, 'created_at' => now(), ]; // 실제 테이블이 있다면 저장, 없으면 파일이나 캐시에 임시 저장 $resolutionId = md5(json_encode($resolutionRecord)); // 임시로 캐시에 저장 (1시간) cache()->put("bom_resolution_{$resolutionId}", $resolutionRecord, 3600); return [ 'resolution_id' => $resolutionId, 'saved_at' => now()->toISOString(), 'purpose' => $purpose, ]; }); } /** * 저장된 BOM 해석 결과 조회 */ public function getBomResolution(string $resolutionId): ?array { return cache()->get("bom_resolution_{$resolutionId}"); } /** * KSS01 시나리오 테스트용 빠른 실행 */ public function resolveKSS01(array $parameters): array { // KSS01 모델이 있다고 가정하고 하드코딩된 로직 $defaults = [ 'W0' => 800, 'H0' => 600, 'screen_type' => 'FABRIC', 'install_type' => 'WALL', ]; $params = array_merge($defaults, $parameters); // 공식 계산 시뮬레이션 $calculated = [ 'W1' => $params['W0'] + 100, 'H1' => $params['H0'] + 100, ]; $calculated['area'] = ($calculated['W1'] * $calculated['H1']) / 1000000; // 조건 규칙 시뮬레이션 $bom = []; // 스크린 타입에 따른 자재 if ($params['screen_type'] === 'FABRIC') { $bom[] = ['target_type' => 'MATERIAL', 'target_id' => 1, 'quantity' => $calculated['area'], 'target_info' => ['name' => '패브릭 스크린']]; } else { $bom[] = ['target_type' => 'MATERIAL', 'target_id' => 2, 'quantity' => $calculated['area'], 'target_info' => ['name' => '스틸 스크린']]; } // 브라켓 개수 (폭에 따라) $bracketCount = $calculated['W1'] > 1000 ? 3 : 2; $bom[] = ['target_type' => 'PRODUCT', 'target_id' => 10, 'quantity' => $bracketCount, 'target_info' => ['name' => '브라켓']]; // 가이드레일 $railLength = ($calculated['W1'] + $calculated['H1']) * 2 / 1000; // m 단위 $bom[] = ['target_type' => 'MATERIAL', 'target_id' => 3, 'quantity' => $railLength, 'target_info' => ['name' => '가이드레일']]; return [ 'model' => ['code' => 'KSS01', 'name' => '기본 스크린 시스템'], 'input_parameters' => $params, 'calculated_values' => $calculated, 'resolved_bom' => $bom, 'summary' => ['total_items' => count($bom)], ]; } }