tenantId(); // 모델 존재 확인 $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); if (!$model) { throw new NotFoundHttpException(__('error.not_found')); } $query = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $modelId); if ($q !== '') { $query->where(function ($w) use ($q) { $w->where('formula_name', 'like', "%{$q}%") ->orWhere('formula_expression', 'like', "%{$q}%") ->orWhere('description', 'like', "%{$q}%"); }); } return $query->orderBy('calculation_order')->orderBy('id')->paginate($size, ['*'], 'page', $page); } /** * 공식 조회 */ public function show(int $formulaId): ModelFormula { $tenantId = $this->tenantId(); $formula = ModelFormula::where('tenant_id', $tenantId)->where('id', $formulaId)->first(); if (!$formula) { throw new NotFoundHttpException(__('error.not_found')); } return $formula; } /** * 공식 생성 */ public function create(array $data): ModelFormula { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 모델 존재 확인 $model = DesignModel::where('tenant_id', $tenantId)->where('id', $data['model_id'])->first(); if (!$model) { throw new NotFoundHttpException(__('error.not_found')); } // 같은 모델 내에서 공식명 중복 체크 $exists = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $data['model_id']) ->where('formula_name', $data['formula_name']) ->exists(); if ($exists) { throw ValidationException::withMessages(['formula_name' => __('error.duplicate')]); } return DB::transaction(function () use ($tenantId, $userId, $data) { // calculation_order가 없으면 자동 설정 if (!isset($data['calculation_order'])) { $maxOrder = ModelFormula::where('tenant_id', $tenantId) ->where('model_id', $data['model_id']) ->max('calculation_order') ?? 0; $data['calculation_order'] = $maxOrder + 1; } // 공식에서 변수 추출 및 의존성 설정 $tempFormula = new ModelFormula(['formula_expression' => $data['formula_expression']]); $variables = $tempFormula->extractVariables(); $data['dependencies'] = $variables; // 의존성 순환 체크 $this->validateNoDependencyLoop($data['model_id'], $data['formula_name'], $variables); $payload = array_merge($data, [ 'tenant_id' => $tenantId, 'created_by' => $userId, 'updated_by' => $userId, ]); return ModelFormula::create($payload); }); } /** * 공식 수정 */ public function update(int $formulaId, array $data): ModelFormula { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $formula = ModelFormula::where('tenant_id', $tenantId)->where('id', $formulaId)->first(); if (!$formula) { throw new NotFoundHttpException(__('error.not_found')); } // 공식명 변경 시 중복 체크 if (isset($data['formula_name']) && $data['formula_name'] !== $formula->formula_name) { $exists = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $formula->model_id) ->where('formula_name', $data['formula_name']) ->where('id', '!=', $formulaId) ->exists(); if ($exists) { throw ValidationException::withMessages(['formula_name' => __('error.duplicate')]); } } return DB::transaction(function () use ($formula, $userId, $data) { // 공식 표현식이 변경되면 의존성 재계산 if (isset($data['formula_expression'])) { $tempFormula = new ModelFormula(['formula_expression' => $data['formula_expression']]); $variables = $tempFormula->extractVariables(); $data['dependencies'] = $variables; // 의존성 순환 체크 (자기 자신 제외) $formulaName = $data['formula_name'] ?? $formula->formula_name; $this->validateNoDependencyLoop($formula->model_id, $formulaName, $variables, $formula->id); } $payload = array_merge($data, ['updated_by' => $userId]); $formula->update($payload); return $formula->fresh(); }); } /** * 공식 삭제 */ public function delete(int $formulaId): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $formula = ModelFormula::where('tenant_id', $tenantId)->where('id', $formulaId)->first(); if (!$formula) { throw new NotFoundHttpException(__('error.not_found')); } // 다른 공식에서 이 공식을 의존하는지 체크 $dependentFormulas = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $formula->model_id) ->where('id', '!=', $formulaId) ->get() ->filter(function ($f) use ($formula) { return in_array($formula->formula_name, $f->dependencies ?? []); }); if ($dependentFormulas->isNotEmpty()) { $dependentNames = $dependentFormulas->pluck('formula_name')->implode(', '); throw ValidationException::withMessages([ 'formula_name' => __('error.formula_in_use', ['formulas' => $dependentNames]) ]); } return DB::transaction(function () use ($formula, $userId) { $formula->update(['deleted_by' => $userId]); return $formula->delete(); }); } /** * 공식 계산 순서 변경 */ public function reorder(int $modelId, array $formulaIds): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 모델 존재 확인 $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); if (!$model) { throw new NotFoundHttpException(__('error.not_found')); } return DB::transaction(function () use ($tenantId, $userId, $modelId, $formulaIds) { $order = 1; $updated = []; foreach ($formulaIds as $formulaId) { $formula = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $modelId) ->where('id', $formulaId) ->first(); if ($formula) { $formula->update([ 'calculation_order' => $order, 'updated_by' => $userId, ]); $updated[] = $formula->fresh(); $order++; } } return $updated; }); } /** * 공식 대량 저장 (upsert) */ public function bulkUpsert(int $modelId, array $formulas): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 모델 존재 확인 $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); if (!$model) { throw new NotFoundHttpException(__('error.not_found')); } return DB::transaction(function () use ($tenantId, $userId, $modelId, $formulas) { $result = []; foreach ($formulas as $index => $formulaData) { $formulaData['model_id'] = $modelId; // 공식에서 의존성 추출 if (isset($formulaData['formula_expression'])) { $tempFormula = new ModelFormula(['formula_expression' => $formulaData['formula_expression']]); $formulaData['dependencies'] = $tempFormula->extractVariables(); } // ID가 있으면 업데이트, 없으면 생성 if (isset($formulaData['id']) && $formulaData['id']) { $formula = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $modelId) ->where('id', $formulaData['id']) ->first(); if ($formula) { $formula->update(array_merge($formulaData, ['updated_by' => $userId])); $result[] = $formula->fresh(); } } else { // 새로운 공식 생성 $exists = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $modelId) ->where('formula_name', $formulaData['formula_name']) ->exists(); if (!$exists) { if (!isset($formulaData['calculation_order'])) { $formulaData['calculation_order'] = $index + 1; } $payload = array_merge($formulaData, [ 'tenant_id' => $tenantId, 'created_by' => $userId, 'updated_by' => $userId, ]); $result[] = ModelFormula::create($payload); } } } return $result; }); } /** * 공식 계산 실행 */ public function calculateFormulas(int $modelId, array $inputValues): array { $tenantId = $this->tenantId(); // 모델의 모든 공식을 계산 순서대로 조회 $formulas = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $modelId) ->orderBy('calculation_order') ->get(); $results = $inputValues; // 입력값을 결과에 포함 $errors = []; foreach ($formulas as $formula) { try { // 의존하는 변수들이 모두 준비되었는지 확인 $dependencies = $formula->dependencies ?? []; $hasAllDependencies = true; foreach ($dependencies as $dependency) { if (!array_key_exists($dependency, $results)) { $hasAllDependencies = false; break; } } if (!$hasAllDependencies) { $errors[$formula->formula_name] = __('error.missing_dependencies'); continue; } // 공식 계산 실행 $calculatedValue = $formula->calculate($results); $results[$formula->formula_name] = $calculatedValue; } catch (\Exception $e) { $errors[$formula->formula_name] = $e->getMessage(); } } if (!empty($errors)) { throw ValidationException::withMessages($errors); } return $results; } /** * 의존성 순환 검증 */ private function validateNoDependencyLoop(int $modelId, string $formulaName, array $dependencies, ?int $excludeFormulaId = null): void { $tenantId = $this->tenantId(); // 해당 모델의 모든 공식 조회 (수정 중인 공식 제외) $query = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $modelId); if ($excludeFormulaId) { $query->where('id', '!=', $excludeFormulaId); } $allFormulas = $query->get()->toArray(); // 새로운 공식을 임시로 추가 $allFormulas[] = [ 'formula_name' => $formulaName, 'dependencies' => $dependencies, ]; // 각 의존성에 대해 순환 검사 foreach ($dependencies as $dependency) { if ($this->hasCircularDependency($formulaName, $dependency, $allFormulas, [])) { throw ValidationException::withMessages([ 'formula_expression' => __('error.circular_dependency', ['dependency' => $dependency]) ]); } } } /** * 순환 의존성 검사 (DFS) */ private function hasCircularDependency(string $startFormula, string $currentFormula, array $allFormulas, array $visited): bool { if ($currentFormula === $startFormula) { return true; // 순환 발견 } if (in_array($currentFormula, $visited)) { return false; // 이미 방문한 노드 } $visited[] = $currentFormula; // 현재 공식의 의존성들을 확인 $currentFormulaData = collect($allFormulas)->firstWhere('formula_name', $currentFormula); if (!$currentFormulaData || empty($currentFormulaData['dependencies'])) { return false; } foreach ($currentFormulaData['dependencies'] as $dependency) { if ($this->hasCircularDependency($startFormula, $dependency, $allFormulas, $visited)) { return true; } } return false; } /** * 모델의 공식 의존성 그래프 조회 */ public function getDependencyGraph(int $modelId): array { $tenantId = $this->tenantId(); // 모델 존재 확인 $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); if (!$model) { throw new NotFoundHttpException(__('error.not_found')); } $formulas = ModelFormula::query() ->where('tenant_id', $tenantId) ->where('model_id', $modelId) ->orderBy('calculation_order') ->get(); $graph = [ 'nodes' => [], 'edges' => [], ]; // 노드 생성 (공식들) foreach ($formulas as $formula) { $graph['nodes'][] = [ 'id' => $formula->formula_name, 'label' => $formula->formula_name, 'expression' => $formula->formula_expression, 'order' => $formula->calculation_order, ]; } // 엣지 생성 (의존성들) foreach ($formulas as $formula) { if (!empty($formula->dependencies)) { foreach ($formula->dependencies as $dependency) { $graph['edges'][] = [ 'from' => $dependency, 'to' => $formula->formula_name, ]; } } } return $graph; } }