validateModelAccess($modelId); $query = ModelFormula::where('model_id', $modelId) ->active() ->ordered() ->with('model'); if ($paginate) { return $query->paginate($perPage); } return $query->get(); } /** * 공식 상세 조회 */ public function getFormula(int $id): ModelFormula { $formula = ModelFormula::where('tenant_id', $this->tenantId()) ->findOrFail($id); $this->validateModelAccess($formula->model_id); return $formula; } /** * 공식 생성 */ public function createFormula(array $data): ModelFormula { $this->validateModelAccess($data['model_id']); // 기본값 설정 $data['tenant_id'] = $this->tenantId(); $data['created_by'] = $this->apiUserId(); // 순서가 지정되지 않은 경우 마지막으로 설정 if (!isset($data['order'])) { $maxOrder = ModelFormula::where('tenant_id', $this->tenantId()) ->where('model_id', $data['model_id']) ->max('order') ?? 0; $data['order'] = $maxOrder + 1; } // 공식명 중복 체크 $this->validateFormulaNameUnique($data['model_id'], $data['name']); // 공식 검증 및 의존성 추출 $formula = new ModelFormula($data); $expressionErrors = $formula->validateExpression(); if (!empty($expressionErrors)) { throw new \InvalidArgumentException(__('error.invalid_formula_expression') . ': ' . implode(', ', $expressionErrors)); } // 의존성 검증 $dependencies = $formula->extractVariables(); $this->validateDependencies($data['model_id'], $dependencies); $data['dependencies'] = $dependencies; // 순환 의존성 체크 $this->validateCircularDependency($data['model_id'], $data['name'], $dependencies); $formula = ModelFormula::create($data); // 계산 순서 재정렬 $this->recalculateOrder($data['model_id']); return $formula->fresh(); } /** * 공식 수정 */ public function updateFormula(int $id, array $data): ModelFormula { $formula = $this->getFormula($id); // 공식명 변경 시 중복 체크 if (isset($data['name']) && $data['name'] !== $formula->name) { $this->validateFormulaNameUnique($formula->model_id, $data['name'], $id); } // 공식 표현식 변경 시 검증 if (isset($data['expression'])) { $tempFormula = new ModelFormula(array_merge($formula->toArray(), $data)); $expressionErrors = $tempFormula->validateExpression(); if (!empty($expressionErrors)) { throw new \InvalidArgumentException(__('error.invalid_formula_expression') . ': ' . implode(', ', $expressionErrors)); } // 의존성 검증 $dependencies = $tempFormula->extractVariables(); $this->validateDependencies($formula->model_id, $dependencies); $data['dependencies'] = $dependencies; // 순환 의존성 체크 (기존 공식 제외) $this->validateCircularDependency($formula->model_id, $data['name'] ?? $formula->name, $dependencies, $id); } $data['updated_by'] = $this->apiUserId(); $formula->update($data); // 의존성이 변경된 경우 계산 순서 재정렬 if (isset($data['expression'])) { $this->recalculateOrder($formula->model_id); } return $formula->fresh(); } /** * 공식 삭제 */ public function deleteFormula(int $id): bool { $formula = $this->getFormula($id); // 다른 공식에서 사용 중인지 확인 $this->validateFormulaNotInUse($formula->model_id, $formula->name); $formula->update(['deleted_by' => $this->apiUserId()]); $formula->delete(); // 계산 순서 재정렬 $this->recalculateOrder($formula->model_id); return true; } /** * 공식 복사 (다른 모델로) */ public function copyFormulasToModel(int $sourceModelId, int $targetModelId): Collection { $this->validateModelAccess($sourceModelId); $this->validateModelAccess($targetModelId); $sourceFormulas = $this->getFormulasByModel($sourceModelId); $copiedFormulas = collect(); // 의존성 순서대로 복사 $orderedFormulas = $this->sortFormulasByDependency($sourceFormulas); foreach ($orderedFormulas as $sourceFormula) { $data = $sourceFormula->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->isFormulaNameExists($targetModelId, $data['name'])) { $data['name'] = $originalName . '_' . $counter; $counter++; } // 대상 모델의 매개변수/공식에 맞게 의존성 재검증 $dependencies = $this->extractVariablesFromExpression($data['expression']); $validDependencies = $this->getValidDependencies($targetModelId, $dependencies); $data['dependencies'] = $validDependencies; $copiedFormula = ModelFormula::create($data); $copiedFormulas->push($copiedFormula); } // 복사 완료 후 계산 순서 재정렬 $this->recalculateOrder($targetModelId); return $copiedFormulas; } /** * 공식 계산 실행 */ public function calculateFormulas(int $modelId, array $inputValues): array { $this->validateModelAccess($modelId); $formulas = $this->getFormulasByModel($modelId); $results = $inputValues; // 매개변수 값으로 시작 // 의존성 순서대로 계산 $orderedFormulas = $this->sortFormulasByDependency($formulas); foreach ($orderedFormulas as $formula) { try { $result = $formula->calculate($results); if ($result !== null) { $results[$formula->name] = $result; } } catch (\Throwable $e) { // 계산 실패 시 null로 설정 $results[$formula->name] = null; } } return $results; } /** * 공식 검증 (문법 및 의존성) */ public function validateFormula(int $modelId, string $name, string $expression): array { $this->validateModelAccess($modelId); $errors = []; // 임시 공식 객체로 문법 검증 $tempFormula = new ModelFormula([ 'name' => $name, 'expression' => $expression, 'model_id' => $modelId ]); $expressionErrors = $tempFormula->validateExpression(); if (!empty($expressionErrors)) { $errors['expression'] = $expressionErrors; } // 의존성 검증 $dependencies = $tempFormula->extractVariables(); $dependencyErrors = $this->validateDependencies($modelId, $dependencies, false); if (!empty($dependencyErrors)) { $errors['dependencies'] = $dependencyErrors; } // 순환 의존성 체크 try { $this->validateCircularDependency($modelId, $name, $dependencies); } catch (\InvalidArgumentException $e) { $errors['circular_dependency'] = [$e->getMessage()]; } return $errors; } /** * 의존성 순서대로 공식 정렬 */ public function sortFormulasByDependency(Collection $formulas): Collection { $sorted = collect(); $remaining = $formulas->keyBy('name'); $processed = []; while ($remaining->count() > 0) { $progress = false; foreach ($remaining as $formula) { $dependencies = $formula->dependencies ?? []; $canProcess = true; // 의존성이 모두 처리되었는지 확인 foreach ($dependencies as $dep) { if (!in_array($dep, $processed) && $remaining->has($dep)) { $canProcess = false; break; } } if ($canProcess) { $sorted->push($formula); $processed[] = $formula->name; $remaining->forget($formula->name); $progress = true; } } // 진행이 없으면 순환 의존성 if (!$progress && $remaining->count() > 0) { break; } } // 순환 의존성으로 처리되지 않은 공식들도 추가 return $sorted->concat($remaining->values()); } /** * 모델 접근 권한 검증 */ private function validateModelAccess(int $modelId): void { $model = ModelMaster::where('tenant_id', $this->tenantId()) ->findOrFail($modelId); } /** * 공식명 중복 검증 */ private function validateFormulaNameUnique(int $modelId, string $name, ?int $excludeId = null): void { $query = ModelFormula::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.formula_name_duplicate')); } // 매개변수명과도 중복되지 않아야 함 $parameterExists = ModelParameter::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->where('name', $name) ->exists(); if ($parameterExists) { throw new \InvalidArgumentException(__('error.formula_name_conflicts_with_parameter')); } } /** * 공식명 존재 여부 확인 */ private function isFormulaNameExists(int $modelId, string $name): bool { return ModelFormula::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->where('name', $name) ->exists(); } /** * 의존성 검증 */ private function validateDependencies(int $modelId, array $dependencies, bool $throwException = true): array { $errors = []; // 매개변수 목록 가져오기 $parameters = ModelParameter::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->active() ->pluck('name') ->toArray(); // 기존 공식 목록 가져오기 $formulas = ModelFormula::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->active() ->pluck('name') ->toArray(); $validNames = array_merge($parameters, $formulas); foreach ($dependencies as $dep) { if (!in_array($dep, $validNames)) { $errors[] = "Dependency '{$dep}' not found in model parameters or formulas"; } } if (!empty($errors) && $throwException) { throw new \InvalidArgumentException(__('error.invalid_formula_dependencies') . ': ' . implode(', ', $errors)); } return $errors; } /** * 순환 의존성 검증 */ private function validateCircularDependency(int $modelId, string $formulaName, array $dependencies, ?int $excludeId = null): void { $allFormulas = ModelFormula::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->active(); if ($excludeId) { $allFormulas->where('id', '!=', $excludeId); } $allFormulas = $allFormulas->get(); // 현재 공식을 임시로 추가하여 순환 의존성 검사 $tempFormula = new ModelFormula([ 'name' => $formulaName, 'dependencies' => $dependencies ]); $allFormulas->push($tempFormula); if ($this->hasCircularDependency($tempFormula, $allFormulas->toArray())) { throw new \InvalidArgumentException(__('error.circular_dependency_detected')); } } /** * 순환 의존성 검사 */ private function hasCircularDependency(ModelFormula $formula, array $allFormulas, array $visited = []): bool { if (in_array($formula->name, $visited)) { return true; } $visited[] = $formula->name; foreach ($formula->dependencies ?? [] as $dep) { foreach ($allFormulas as $depFormula) { if ($depFormula->name === $dep) { if ($this->hasCircularDependency($depFormula, $allFormulas, $visited)) { return true; } break; } } } return false; } /** * 공식이 다른 공식에서 사용 중인지 확인 */ private function validateFormulaNotInUse(int $modelId, string $formulaName): void { $usageCount = ModelFormula::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->whereJsonContains('dependencies', $formulaName) ->count(); if ($usageCount > 0) { throw new \InvalidArgumentException(__('error.formula_in_use')); } } /** * 계산 순서 재정렬 */ private function recalculateOrder(int $modelId): void { $formulas = $this->getFormulasByModel($modelId); $orderedFormulas = $this->sortFormulasByDependency($formulas); foreach ($orderedFormulas as $index => $formula) { $formula->update([ 'order' => $index + 1, 'updated_by' => $this->apiUserId() ]); } } /** * 표현식에서 변수 추출 */ private function extractVariablesFromExpression(string $expression): array { $tempFormula = new ModelFormula(['expression' => $expression]); return $tempFormula->extractVariables(); } /** * 유효한 의존성만 필터링 */ private function getValidDependencies(int $modelId, array $dependencies): array { $parameters = ModelParameter::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->active() ->pluck('name') ->toArray(); $formulas = ModelFormula::where('tenant_id', $this->tenantId()) ->where('model_id', $modelId) ->active() ->pluck('name') ->toArray(); $validNames = array_merge($parameters, $formulas); return array_intersect($dependencies, $validNames); } }