505 lines
16 KiB
PHP
505 lines
16 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Services\Service;
|
||
|
|
use Shared\Models\Products\ModelFormula;
|
||
|
|
use Shared\Models\Products\ModelParameter;
|
||
|
|
use Shared\Models\Products\ModelMaster;
|
||
|
|
use Illuminate\Database\Eloquent\Collection;
|
||
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Model Formula Service
|
||
|
|
* 모델 공식 관리 서비스
|
||
|
|
*/
|
||
|
|
class ModelFormulaService extends Service
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* 모델별 공식 목록 조회
|
||
|
|
*/
|
||
|
|
public function getFormulasByModel(int $modelId, bool $paginate = false, int $perPage = 15): Collection|LengthAwarePaginator
|
||
|
|
{
|
||
|
|
$this->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);
|
||
|
|
}
|
||
|
|
}
|