validateModelAccess($modelId); $query = BomConditionRule::where('model_id', $modelId) ->active() ->byPriority() ->with('model'); if ($paginate) { return $query->paginate($perPage); } return $query->get(); } /** * 조건 규칙 상세 조회 */ public function getRule(int $id): BomConditionRule { $rule = BomConditionRule::where('tenant_id', $this->tenantId()) ->findOrFail($id); $this->validateModelAccess($rule->model_id); return $rule; } /** * 조건 규칙 생성 */ public function createRule(array $data): BomConditionRule { $this->validateModelAccess($data['model_id']); // 기본값 설정 $data['tenant_id'] = $this->tenantId(); $data['created_by'] = $this->apiUserId(); // 우선순위가 지정되지 않은 경우 마지막으로 설정 if (!isset($data['priority'])) { $maxPriority = BomConditionRule::where('tenant_id', $this->tenantId()) ->where('model_id', $data['model_id']) ->max('priority') ?? 0; $data['priority'] = $maxPriority + 1; } // 규칙명 중복 체크 $this->validateRuleNameUnique($data['model_id'], $data['name']); // 조건식 검증 $rule = new BomConditionRule($data); $conditionErrors = $rule->validateConditionExpression(); if (!empty($conditionErrors)) { throw new \InvalidArgumentException(__('error.invalid_condition_expression') . ': ' . implode(', ', $conditionErrors)); } // 대상 아이템 처리 if (isset($data['target_items']) && is_string($data['target_items'])) { $data['target_items'] = json_decode($data['target_items'], true); } // 대상 아이템 검증 $this->validateTargetItems($data['target_items'] ?? [], $data['action']); $rule = BomConditionRule::create($data); return $rule->fresh(); } /** * 조건 규칙 수정 */ public function updateRule(int $id, array $data): BomConditionRule { $rule = $this->getRule($id); // 규칙명 변경 시 중복 체크 if (isset($data['name']) && $data['name'] !== $rule->name) { $this->validateRuleNameUnique($rule->model_id, $data['name'], $id); } // 조건식 변경 시 검증 if (isset($data['condition_expression'])) { $tempRule = new BomConditionRule(array_merge($rule->toArray(), $data)); $conditionErrors = $tempRule->validateConditionExpression(); if (!empty($conditionErrors)) { throw new \InvalidArgumentException(__('error.invalid_condition_expression') . ': ' . implode(', ', $conditionErrors)); } } // 대상 아이템 처리 if (isset($data['target_items']) && is_string($data['target_items'])) { $data['target_items'] = json_decode($data['target_items'], true); } // 대상 아이템 검증 if (isset($data['target_items']) || isset($data['action'])) { $action = $data['action'] ?? $rule->action; $targetItems = $data['target_items'] ?? $rule->target_items; $this->validateTargetItems($targetItems, $action); } $data['updated_by'] = $this->apiUserId(); $rule->update($data); return $rule->fresh(); } /** * 조건 규칙 삭제 */ public function deleteRule(int $id): bool { $rule = $this->getRule($id); $rule->update(['deleted_by' => $this->apiUserId()]); $rule->delete(); return true; } /** * 조건 규칙 우선순위 변경 */ public function reorderRules(int $modelId, array $orderData): bool { $this->validateModelAccess($modelId); foreach ($orderData as $item) { BomConditionRule::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->where('id', $item['id']) ->update([ 'priority' => $item['priority'], 'updated_by' => $this->apiUserId() ]); } return true; } /** * 조건 규칙 복사 (다른 모델로) */ public function copyRulesToModel(int $sourceModelId, int $targetModelId): Collection { $this->validateModelAccess($sourceModelId); $this->validateModelAccess($targetModelId); $sourceRules = $this->getRulesByModel($sourceModelId); $copiedRules = collect(); foreach ($sourceRules as $sourceRule) { $data = $sourceRule->toArray(); unset($data['id'], $data['created_at'], $data['updated_at'], $data['deleted_at']); $data['model_id'] = $targetModelId; $data['created_by'] = $this->apiUserId(); // 이름 중복 시 수정 $originalName = $data['name']; $counter = 1; while ($this->isRuleNameExists($targetModelId, $data['name'])) { $data['name'] = $originalName . '_' . $counter; $counter++; } $copiedRule = BomConditionRule::create($data); $copiedRules->push($copiedRule); } return $copiedRules; } /** * 조건 평가 및 적용할 규칙 찾기 */ public function getApplicableRules(int $modelId, array $variables): Collection { $this->validateModelAccess($modelId); $rules = $this->getRulesByModel($modelId); $applicableRules = collect(); foreach ($rules as $rule) { if ($rule->evaluateCondition($variables)) { $applicableRules->push($rule); } } return $applicableRules; } /** * 조건 규칙을 BOM 아이템에 적용 */ public function applyRulesToBomItems(int $modelId, array $bomItems, array $variables): array { $applicableRules = $this->getApplicableRules($modelId, $variables); // 우선순위 순서대로 규칙 적용 foreach ($applicableRules as $rule) { $bomItems = $rule->applyAction($bomItems); } return $bomItems; } /** * 조건식 검증 (문법 체크) */ public function validateConditionExpression(int $modelId, string $expression): array { $this->validateModelAccess($modelId); $tempRule = new BomConditionRule([ 'condition_expression' => $expression, 'model_id' => $modelId ]); return $tempRule->validateConditionExpression(); } /** * 조건식 테스트 (실제 변수값으로 평가) */ public function testConditionExpression(int $modelId, string $expression, array $variables): array { $this->validateModelAccess($modelId); $result = [ 'valid' => false, 'result' => null, 'error' => null ]; try { $tempRule = new BomConditionRule([ 'condition_expression' => $expression, 'model_id' => $modelId ]); $validationErrors = $tempRule->validateConditionExpression(); if (!empty($validationErrors)) { $result['error'] = implode(', ', $validationErrors); return $result; } $evaluationResult = $tempRule->evaluateCondition($variables); $result['valid'] = true; $result['result'] = $evaluationResult; } catch (\Throwable $e) { $result['error'] = $e->getMessage(); } return $result; } /** * 모델의 사용 가능한 변수 목록 조회 (매개변수 + 공식) */ public function getAvailableVariables(int $modelId): array { $this->validateModelAccess($modelId); $parameterService = new ModelParameterService(); $formulaService = new ModelFormulaService(); $parameters = $parameterService->getParametersByModel($modelId); $formulas = $formulaService->getFormulasByModel($modelId); $variables = []; foreach ($parameters as $parameter) { $variables[] = [ 'name' => $parameter->name, 'label' => $parameter->label, 'type' => $parameter->type, 'source' => 'parameter' ]; } foreach ($formulas as $formula) { $variables[] = [ 'name' => $formula->name, 'label' => $formula->label, 'type' => 'NUMBER', // 공식 결과는 숫자 'source' => 'formula' ]; } return $variables; } /** * 모델 접근 권한 검증 */ private function validateModelAccess(int $modelId): void { $model = ModelMaster::where('tenant_id', $this->tenantId()) ->findOrFail($modelId); } /** * 규칙명 중복 검증 */ private function validateRuleNameUnique(int $modelId, string $name, ?int $excludeId = null): void { $query = BomConditionRule::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->where('name', $name); if ($excludeId) { $query->where('id', '!=', $excludeId); } if ($query->exists()) { throw new \InvalidArgumentException(__('error.rule_name_duplicate')); } } /** * 규칙명 존재 여부 확인 */ private function isRuleNameExists(int $modelId, string $name): bool { return BomConditionRule::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->where('name', $name) ->exists(); } /** * 대상 아이템 검증 */ private function validateTargetItems(array $targetItems, string $action): void { if (empty($targetItems)) { throw new \InvalidArgumentException(__('error.target_items_required')); } foreach ($targetItems as $index => $item) { if (!isset($item['product_id']) && !isset($item['material_id'])) { throw new \InvalidArgumentException(__('error.target_item_missing_reference', ['index' => $index])); } // REPLACE 액션의 경우 replace_from 필요 if ($action === BomConditionRule::ACTION_REPLACE && !isset($item['replace_from'])) { throw new \InvalidArgumentException(__('error.replace_from_required', ['index' => $index])); } // 수량 검증 if (isset($item['quantity']) && (!is_numeric($item['quantity']) || $item['quantity'] <= 0)) { throw new \InvalidArgumentException(__('error.invalid_quantity', ['index' => $index])); } // 낭비율 검증 if (isset($item['waste_rate']) && (!is_numeric($item['waste_rate']) || $item['waste_rate'] < 0 || $item['waste_rate'] > 100)) { throw new \InvalidArgumentException(__('error.invalid_waste_rate', ['index' => $index])); } } } /** * 규칙 활성화/비활성화 */ public function toggleRuleStatus(int $id): BomConditionRule { $rule = $this->getRule($id); $rule->update([ 'is_active' => !$rule->is_active, 'updated_by' => $this->apiUserId() ]); return $rule->fresh(); } /** * 규칙 실행 로그 (디버깅용) */ public function getRuleExecutionLog(int $modelId, array $variables): array { $this->validateModelAccess($modelId); $rules = $this->getRulesByModel($modelId); $log = []; foreach ($rules as $rule) { $logEntry = [ 'rule_id' => $rule->id, 'rule_name' => $rule->name, 'condition' => $rule->condition_expression, 'priority' => $rule->priority, 'evaluated' => false, 'result' => false, 'action' => $rule->action, 'error' => null ]; try { $logEntry['evaluated'] = true; $logEntry['result'] = $rule->evaluateCondition($variables); } catch (\Throwable $e) { $logEntry['error'] = $e->getMessage(); } $log[] = $logEntry; } return $log; } }