feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가
- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env) - 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget) - 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget) - 리소스 한국어화: Product, Material 모델 레이블 추가 - 대시보드: 위젯 등록 및 캐시 최적화 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
505
app/Services/ModelFormulaService.php
Normal file
505
app/Services/ModelFormulaService.php
Normal file
@@ -0,0 +1,505 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user