Files
sam-api/app/Services/BomConditionRuleService.php
kent bf8036a64b 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>
2025-09-30 23:31:14 +09:00

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;
}
}