parameterService = new ModelParameterService(); $this->formulaService = new ModelFormulaService(); $this->conditionRuleService = new BomConditionRuleService(); } /** * 매개변수 기반 BOM 해석 (미리보기) */ public function resolveBom(int $modelId, array $inputParameters): array { $this->validateModelAccess($modelId); // 1. 매개변수 검증 및 타입 변환 $validatedParameters = $this->validateAndCastParameters($modelId, $inputParameters); // 2. 공식 계산 $calculatedValues = $this->calculateFormulas($modelId, $validatedParameters); // 3. 기본 BOM 템플릿 가져오기 $baseBomItems = $this->getBaseBomItems($modelId); // 4. 조건 규칙 적용 $resolvedBomItems = $this->applyConditionRules($modelId, $baseBomItems, $calculatedValues); // 5. 수량 및 공식 계산 적용 $finalBomItems = $this->calculateItemQuantities($resolvedBomItems, $calculatedValues); return [ 'model_id' => $modelId, 'input_parameters' => $validatedParameters, 'calculated_values' => $calculatedValues, 'bom_items' => $finalBomItems, 'summary' => $this->generateBomSummary($finalBomItems), 'resolved_at' => now()->toISOString(), ]; } /** * 실시간 미리보기 (캐시 활용) */ public function previewBom(int $modelId, array $inputParameters): array { // 캐시 키 생성 $cacheKey = $this->generateCacheKey($modelId, $inputParameters); return Cache::remember($cacheKey, 300, function () use ($modelId, $inputParameters) { return $this->resolveBom($modelId, $inputParameters); }); } /** * BOM 검증 (오류 체크) */ public function validateBom(int $modelId, array $inputParameters): array { $errors = []; $warnings = []; try { // 매개변수 검증 $parameterErrors = $this->parameterService->validateParameterValues($modelId, $inputParameters); if (!empty($parameterErrors)) { $errors['parameters'] = $parameterErrors; } // 기본 BOM 템플릿 존재 확인 if (!$this->hasBaseBomTemplate($modelId)) { $warnings[] = 'No base BOM template found for this model'; } // 공식 계산 가능 여부 확인 try { $validatedParameters = $this->validateAndCastParameters($modelId, $inputParameters); $this->calculateFormulas($modelId, $validatedParameters); } catch (\Throwable $e) { $errors['formulas'] = ['Formula calculation failed: ' . $e->getMessage()]; } // 조건 규칙 평가 가능 여부 확인 try { $calculatedValues = $this->calculateFormulas($modelId, $validatedParameters ?? []); $this->conditionRuleService->getApplicableRules($modelId, $calculatedValues); } catch (\Throwable $e) { $errors['condition_rules'] = ['Condition rule evaluation failed: ' . $e->getMessage()]; } } catch (\Throwable $e) { $errors['general'] = [$e->getMessage()]; } return [ 'valid' => empty($errors), 'errors' => $errors, 'warnings' => $warnings, ]; } /** * BOM 비교 (다른 매개변수값과 비교) */ public function compareBom(int $modelId, array $parameters1, array $parameters2): array { $bom1 = $this->resolveBom($modelId, $parameters1); $bom2 = $this->resolveBom($modelId, $parameters2); return [ 'parameters_diff' => $this->compareParameters($bom1['calculated_values'], $bom2['calculated_values']), 'bom_items_diff' => $this->compareBomItems($bom1['bom_items'], $bom2['bom_items']), 'summary_diff' => $this->compareSummary($bom1['summary'], $bom2['summary']), ]; } /** * 대량 BOM 해석 (여러 매개변수 조합) */ public function resolveBomBatch(int $modelId, array $parameterSets): array { $results = []; DB::transaction(function () use ($modelId, $parameterSets, &$results) { foreach ($parameterSets as $index => $parameters) { try { $results[$index] = $this->resolveBom($modelId, $parameters); } catch (\Throwable $e) { $results[$index] = [ 'error' => $e->getMessage(), 'parameters' => $parameters, ]; } } }); return $results; } /** * BOM 성능 최적화 제안 */ public function getOptimizationSuggestions(int $modelId, array $inputParameters): array { $resolvedBom = $this->resolveBom($modelId, $inputParameters); $suggestions = []; // 1. 불필요한 조건 규칙 탐지 $unusedRules = $this->findUnusedRules($modelId, $resolvedBom['calculated_values']); if (!empty($unusedRules)) { $suggestions[] = [ 'type' => 'unused_rules', 'message' => 'Found unused condition rules that could be removed', 'details' => $unusedRules, ]; } // 2. 복잡한 공식 탐지 $complexFormulas = $this->findComplexFormulas($modelId); if (!empty($complexFormulas)) { $suggestions[] = [ 'type' => 'complex_formulas', 'message' => 'Found complex formulas that might impact performance', 'details' => $complexFormulas, ]; } // 3. 중복 BOM 아이템 탐지 $duplicateItems = $this->findDuplicateBomItems($resolvedBom['bom_items']); if (!empty($duplicateItems)) { $suggestions[] = [ 'type' => 'duplicate_items', 'message' => 'Found duplicate BOM items that could be consolidated', 'details' => $duplicateItems, ]; } return $suggestions; } /** * 매개변수 검증 및 타입 변환 */ private function validateAndCastParameters(int $modelId, array $inputParameters): array { $errors = $this->parameterService->validateParameterValues($modelId, $inputParameters); if (!empty($errors)) { throw new \InvalidArgumentException(__('error.invalid_parameters') . ': ' . json_encode($errors)); } return $this->parameterService->castParameterValues($modelId, $inputParameters); } /** * 공식 계산 */ private function calculateFormulas(int $modelId, array $parameters): array { return $this->formulaService->calculateFormulas($modelId, $parameters); } /** * 기본 BOM 템플릿 아이템 가져오기 */ private function getBaseBomItems(int $modelId): array { $model = ModelMaster::where('tenant_id', $this->tenantId()) ->findOrFail($modelId); // 현재 활성 버전의 BOM 템플릿 가져오기 $bomTemplate = BomTemplate::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->where('is_active', true) ->orderBy('created_at', 'desc') ->first(); if (!$bomTemplate) { return []; } $bomItems = BomTemplateItem::where('tenant_id', $this->tenantId()) ->where('bom_template_id', $bomTemplate->id) ->orderBy('order') ->get() ->map(function ($item) { return [ 'id' => $item->id, 'product_id' => $item->product_id, 'material_id' => $item->material_id, 'ref_type' => $item->ref_type, 'quantity' => $item->quantity, 'quantity_formula' => $item->quantity_formula, 'waste_rate' => $item->waste_rate, 'unit' => $item->unit, 'memo' => $item->memo, 'order' => $item->order, ]; }) ->toArray(); return $bomItems; } /** * 조건 규칙 적용 */ private function applyConditionRules(int $modelId, array $bomItems, array $calculatedValues): array { return $this->conditionRuleService->applyRulesToBomItems($modelId, $bomItems, $calculatedValues); } /** * 아이템별 수량 계산 */ private function calculateItemQuantities(array $bomItems, array $calculatedValues): array { foreach ($bomItems as &$item) { // 수량 공식이 있는 경우 계산 if (!empty($item['quantity_formula'])) { $calculatedQuantity = $this->evaluateQuantityFormula($item['quantity_formula'], $calculatedValues); if ($calculatedQuantity !== null) { $item['calculated_quantity'] = $calculatedQuantity; } } else { $item['calculated_quantity'] = $item['quantity'] ?? 1; } // 낭비율 적용 if (isset($item['waste_rate']) && $item['waste_rate'] > 0) { $item['total_quantity'] = $item['calculated_quantity'] * (1 + $item['waste_rate'] / 100); } else { $item['total_quantity'] = $item['calculated_quantity']; } // 반올림 (소수점 3자리) $item['calculated_quantity'] = round($item['calculated_quantity'], 3); $item['total_quantity'] = round($item['total_quantity'], 3); } return $bomItems; } /** * 수량 공식 계산 */ private function evaluateQuantityFormula(string $formula, array $variables): ?float { try { $expression = $formula; // 변수값 치환 foreach ($variables as $name => $value) { if (is_numeric($value)) { $expression = preg_replace('/\b' . preg_quote($name, '/') . '\b/', $value, $expression); } } // 안전한 계산 실행 if (preg_match('/^[0-9+\-*\/().,\s]+$/', $expression)) { return eval("return $expression;"); } return null; } catch (\Throwable $e) { return null; } } /** * BOM 요약 정보 생성 */ private function generateBomSummary(array $bomItems): array { $summary = [ 'total_items' => count($bomItems), 'materials_count' => 0, 'products_count' => 0, 'total_cost' => 0, // 향후 가격 정보 추가 시 ]; foreach ($bomItems as $item) { if (!empty($item['material_id'])) { $summary['materials_count']++; } else { $summary['products_count']++; } } return $summary; } /** * 캐시 키 생성 */ private function generateCacheKey(int $modelId, array $parameters): string { ksort($parameters); // 매개변수 순서 정규화 $hash = md5(json_encode($parameters)); return "bom_preview_{$this->tenantId()}_{$modelId}_{$hash}"; } /** * 기본 BOM 템플릿 존재 여부 확인 */ private function hasBaseBomTemplate(int $modelId): bool { return BomTemplate::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->where('is_active', true) ->exists(); } /** * 모델 접근 권한 검증 */ private function validateModelAccess(int $modelId): void { $model = ModelMaster::where('tenant_id', $this->tenantId()) ->findOrFail($modelId); } /** * 매개변수 비교 */ private function compareParameters(array $params1, array $params2): array { $diff = []; $allKeys = array_unique(array_merge(array_keys($params1), array_keys($params2))); foreach ($allKeys as $key) { $value1 = $params1[$key] ?? null; $value2 = $params2[$key] ?? null; if ($value1 !== $value2) { $diff[$key] = [ 'set1' => $value1, 'set2' => $value2, ]; } } return $diff; } /** * BOM 아이템 비교 */ private function compareBomItems(array $items1, array $items2): array { // 간단한 비교 로직 (실제로는 더 정교한 비교 필요) return [ 'count_diff' => count($items1) - count($items2), 'items_only_in_set1' => array_diff_key($items1, $items2), 'items_only_in_set2' => array_diff_key($items2, $items1), ]; } /** * 요약 정보 비교 */ private function compareSummary(array $summary1, array $summary2): array { $diff = []; foreach ($summary1 as $key => $value) { if (isset($summary2[$key]) && $value !== $summary2[$key]) { $diff[$key] = [ 'set1' => $value, 'set2' => $summary2[$key], 'diff' => $value - $summary2[$key], ]; } } return $diff; } /** * 사용되지 않는 규칙 찾기 */ private function findUnusedRules(int $modelId, array $calculatedValues): array { $allRules = $this->conditionRuleService->getRulesByModel($modelId); $applicableRules = $this->conditionRuleService->getApplicableRules($modelId, $calculatedValues); $unusedRules = $allRules->diff($applicableRules); return $unusedRules->map(function ($rule) { return [ 'id' => $rule->id, 'name' => $rule->name, 'condition' => $rule->condition_expression, ]; })->toArray(); } /** * 복잡한 공식 찾기 */ private function findComplexFormulas(int $modelId): array { $formulas = $this->formulaService->getFormulasByModel($modelId); return $formulas->filter(function ($formula) { // 복잡성 기준 (의존성 수, 표현식 길이 등) $dependencyCount = count($formula->dependencies ?? []); $expressionLength = strlen($formula->expression); return $dependencyCount > 5 || $expressionLength > 100; })->map(function ($formula) { return [ 'id' => $formula->id, 'name' => $formula->name, 'complexity_score' => strlen($formula->expression) + count($formula->dependencies ?? []) * 10, ]; })->toArray(); } /** * 중복 BOM 아이템 찾기 */ private function findDuplicateBomItems(array $bomItems): array { $seen = []; $duplicates = []; foreach ($bomItems as $item) { $key = ($item['product_id'] ?? 'null') . '_' . ($item['material_id'] ?? 'null'); if (isset($seen[$key])) { $duplicates[] = [ 'product_id' => $item['product_id'], 'material_id' => $item['material_id'], 'occurrences' => ++$seen[$key], ]; } else { $seen[$key] = 1; } } return $duplicates; } }