505 lines
16 KiB
PHP
505 lines
16 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Services\Service;
|
||
|
|
use Shared\Models\Products\ModelMaster;
|
||
|
|
use Shared\Models\Products\BomTemplate;
|
||
|
|
use Shared\Models\Products\BomTemplateItem;
|
||
|
|
use Illuminate\Support\Facades\Cache;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* BOM Resolver Service
|
||
|
|
* 매개변수 기반 BOM 해석 및 생성 엔진
|
||
|
|
*/
|
||
|
|
class BomResolverService extends Service
|
||
|
|
{
|
||
|
|
private ModelParameterService $parameterService;
|
||
|
|
private ModelFormulaService $formulaService;
|
||
|
|
private BomConditionRuleService $conditionRuleService;
|
||
|
|
|
||
|
|
public function __construct()
|
||
|
|
{
|
||
|
|
parent::__construct();
|
||
|
|
$this->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;
|
||
|
|
}
|
||
|
|
}
|