- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env) - 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget) - 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget) - 리소스 한국어화: Product, Material 모델 레이블 추가 - 대시보드: 위젯 등록 및 캐시 최적화 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
436 lines
13 KiB
PHP
436 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Services\Service;
|
|
use Shared\Models\Products\BomConditionRule;
|
|
use Shared\Models\Products\ModelMaster;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
|
|
/**
|
|
* BOM Condition Rule Service
|
|
* BOM 조건 규칙 관리 서비스
|
|
*/
|
|
class BomConditionRuleService extends Service
|
|
{
|
|
/**
|
|
* 모델별 조건 규칙 목록 조회
|
|
*/
|
|
public function getRulesByModel(int $modelId, bool $paginate = false, int $perPage = 15): Collection|LengthAwarePaginator
|
|
{
|
|
$this->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;
|
|
}
|
|
} |