Revert "feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가"
This reverts commit bf8036a64b.
This commit is contained in:
@@ -1,492 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Design;
|
||||
|
||||
use App\Models\Design\BomConditionRule;
|
||||
use App\Models\Design\DesignModel;
|
||||
use App\Models\Product;
|
||||
use App\Models\Material;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class BomConditionRuleService extends Service
|
||||
{
|
||||
/**
|
||||
* 모델의 조건 규칙 목록 조회
|
||||
*/
|
||||
public function listByModel(int $modelId, string $q = '', int $page = 1, int $size = 20): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 모델 존재 확인
|
||||
$model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first();
|
||||
if (!$model) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$query = BomConditionRule::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId);
|
||||
|
||||
if ($q !== '') {
|
||||
$query->where(function ($w) use ($q) {
|
||||
$w->where('rule_name', 'like', "%{$q}%")
|
||||
->orWhere('condition_expression', 'like', "%{$q}%")
|
||||
->orWhere('description', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
return $query->orderBy('priority')->orderBy('id')->paginate($size, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙 조회
|
||||
*/
|
||||
public function show(int $ruleId): BomConditionRule
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first();
|
||||
if (!$rule) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 연관된 타겟 정보도 함께 조회
|
||||
$rule->load(['designModel']);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙 생성
|
||||
*/
|
||||
public function create(array $data): BomConditionRule
|
||||
{
|
||||
$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'));
|
||||
}
|
||||
|
||||
// 타겟 존재 확인
|
||||
$this->validateTarget($data['target_type'], $data['target_id']);
|
||||
|
||||
// 같은 모델 내에서 규칙명 중복 체크
|
||||
$exists = BomConditionRule::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $data['model_id'])
|
||||
->where('rule_name', $data['rule_name'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw ValidationException::withMessages(['rule_name' => __('error.duplicate')]);
|
||||
}
|
||||
|
||||
// 조건식 문법 검증
|
||||
$this->validateConditionExpression($data['condition_expression']);
|
||||
|
||||
return DB::transaction(function () use ($tenantId, $userId, $data) {
|
||||
// priority가 없으면 자동 설정
|
||||
if (!isset($data['priority'])) {
|
||||
$maxPriority = BomConditionRule::where('tenant_id', $tenantId)
|
||||
->where('model_id', $data['model_id'])
|
||||
->max('priority') ?? 0;
|
||||
$data['priority'] = $maxPriority + 1;
|
||||
}
|
||||
|
||||
$payload = array_merge($data, [
|
||||
'tenant_id' => $tenantId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return BomConditionRule::create($payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙 수정
|
||||
*/
|
||||
public function update(int $ruleId, array $data): BomConditionRule
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first();
|
||||
if (!$rule) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 규칙명 변경 시 중복 체크
|
||||
if (isset($data['rule_name']) && $data['rule_name'] !== $rule->rule_name) {
|
||||
$exists = BomConditionRule::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $rule->model_id)
|
||||
->where('rule_name', $data['rule_name'])
|
||||
->where('id', '!=', $ruleId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw ValidationException::withMessages(['rule_name' => __('error.duplicate')]);
|
||||
}
|
||||
}
|
||||
|
||||
// 타겟 변경 시 존재 확인
|
||||
if (isset($data['target_type']) || isset($data['target_id'])) {
|
||||
$targetType = $data['target_type'] ?? $rule->target_type;
|
||||
$targetId = $data['target_id'] ?? $rule->target_id;
|
||||
$this->validateTarget($targetType, $targetId);
|
||||
}
|
||||
|
||||
// 조건식 변경 시 문법 검증
|
||||
if (isset($data['condition_expression'])) {
|
||||
$this->validateConditionExpression($data['condition_expression']);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($rule, $userId, $data) {
|
||||
$payload = array_merge($data, ['updated_by' => $userId]);
|
||||
$rule->update($payload);
|
||||
|
||||
return $rule->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙 삭제
|
||||
*/
|
||||
public function delete(int $ruleId): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first();
|
||||
if (!$rule) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($rule, $userId) {
|
||||
$rule->update(['deleted_by' => $userId]);
|
||||
return $rule->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙 활성화/비활성화
|
||||
*/
|
||||
public function toggle(int $ruleId): BomConditionRule
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first();
|
||||
if (!$rule) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($rule, $userId) {
|
||||
$rule->update([
|
||||
'is_active' => !$rule->is_active,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return $rule->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙 우선순위 변경
|
||||
*/
|
||||
public function reorder(int $modelId, array $ruleIds): 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, $ruleIds) {
|
||||
$priority = 1;
|
||||
$updated = [];
|
||||
|
||||
foreach ($ruleIds as $ruleId) {
|
||||
$rule = BomConditionRule::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->where('id', $ruleId)
|
||||
->first();
|
||||
|
||||
if ($rule) {
|
||||
$rule->update([
|
||||
'priority' => $priority,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
$updated[] = $rule->fresh();
|
||||
$priority++;
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙 대량 저장 (upsert)
|
||||
*/
|
||||
public function bulkUpsert(int $modelId, array $rules): 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, $rules) {
|
||||
$result = [];
|
||||
|
||||
foreach ($rules as $index => $ruleData) {
|
||||
$ruleData['model_id'] = $modelId;
|
||||
|
||||
// 타겟 및 조건식 검증
|
||||
if (isset($ruleData['target_type']) && isset($ruleData['target_id'])) {
|
||||
$this->validateTarget($ruleData['target_type'], $ruleData['target_id']);
|
||||
}
|
||||
|
||||
if (isset($ruleData['condition_expression'])) {
|
||||
$this->validateConditionExpression($ruleData['condition_expression']);
|
||||
}
|
||||
|
||||
// ID가 있으면 업데이트, 없으면 생성
|
||||
if (isset($ruleData['id']) && $ruleData['id']) {
|
||||
$rule = BomConditionRule::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->where('id', $ruleData['id'])
|
||||
->first();
|
||||
|
||||
if ($rule) {
|
||||
$rule->update(array_merge($ruleData, ['updated_by' => $userId]));
|
||||
$result[] = $rule->fresh();
|
||||
}
|
||||
} else {
|
||||
// 새로운 규칙 생성
|
||||
$exists = BomConditionRule::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->where('rule_name', $ruleData['rule_name'])
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
if (!isset($ruleData['priority'])) {
|
||||
$ruleData['priority'] = $index + 1;
|
||||
}
|
||||
|
||||
$payload = array_merge($ruleData, [
|
||||
'tenant_id' => $tenantId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
$result[] = BomConditionRule::create($payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙 평가 실행
|
||||
*/
|
||||
public function evaluateRules(int $modelId, array $parameters): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 활성 조건 규칙들을 우선순위 순으로 조회
|
||||
$rules = BomConditionRule::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->where('is_active', true)
|
||||
->orderBy('priority')
|
||||
->get();
|
||||
|
||||
$matchedRules = [];
|
||||
$bomActions = [];
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
try {
|
||||
if ($rule->evaluateCondition($parameters)) {
|
||||
$matchedRules[] = [
|
||||
'rule_id' => $rule->id,
|
||||
'rule_name' => $rule->rule_name,
|
||||
'action_type' => $rule->action_type,
|
||||
'target_type' => $rule->target_type,
|
||||
'target_id' => $rule->target_id,
|
||||
'quantity_multiplier' => $rule->quantity_multiplier,
|
||||
];
|
||||
|
||||
$bomActions[] = [
|
||||
'action_type' => $rule->action_type,
|
||||
'target_type' => $rule->target_type,
|
||||
'target_id' => $rule->target_id,
|
||||
'quantity_multiplier' => $rule->quantity_multiplier,
|
||||
'rule_name' => $rule->rule_name,
|
||||
];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 조건 평가 실패 시 로그 남기고 건너뜀
|
||||
\Log::warning("Rule evaluation failed: {$rule->rule_name}", [
|
||||
'error' => $e->getMessage(),
|
||||
'parameters' => $parameters,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'matched_rules' => $matchedRules,
|
||||
'bom_actions' => $bomActions,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건식 테스트
|
||||
*/
|
||||
public function testCondition(int $ruleId, array $parameters): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$rule = BomConditionRule::where('tenant_id', $tenantId)->where('id', $ruleId)->first();
|
||||
if (!$rule) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $rule->evaluateCondition($parameters);
|
||||
|
||||
return [
|
||||
'rule_name' => $rule->rule_name,
|
||||
'condition_expression' => $rule->condition_expression,
|
||||
'parameters' => $parameters,
|
||||
'result' => $result,
|
||||
'action_type' => $rule->action_type,
|
||||
'target_type' => $rule->target_type,
|
||||
'target_id' => $rule->target_id,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
throw ValidationException::withMessages([
|
||||
'condition_expression' => __('error.condition_evaluation_failed', ['error' => $e->getMessage()])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 타겟 유효성 검증
|
||||
*/
|
||||
private function validateTarget(string $targetType, int $targetId): void
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
switch ($targetType) {
|
||||
case 'MATERIAL':
|
||||
$exists = Material::where('tenant_id', $tenantId)->where('id', $targetId)->exists();
|
||||
if (!$exists) {
|
||||
throw ValidationException::withMessages(['target_id' => __('error.material_not_found')]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'PRODUCT':
|
||||
$exists = Product::where('tenant_id', $tenantId)->where('id', $targetId)->exists();
|
||||
if (!$exists) {
|
||||
throw ValidationException::withMessages(['target_id' => __('error.product_not_found')]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw ValidationException::withMessages(['target_type' => __('error.invalid_target_type')]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건식 문법 검증
|
||||
*/
|
||||
private function validateConditionExpression(string $expression): void
|
||||
{
|
||||
// 기본적인 문법 검증
|
||||
$expression = trim($expression);
|
||||
|
||||
if (empty($expression)) {
|
||||
throw ValidationException::withMessages(['condition_expression' => __('error.condition_expression_required')]);
|
||||
}
|
||||
|
||||
// 허용된 패턴들 검증
|
||||
$allowedPatterns = [
|
||||
'/^.+\s*(==|!=|>=|<=|>|<)\s*.+$/', // 비교 연산자
|
||||
'/^.+\s+IN\s+\(.+\)$/i', // IN 연산자
|
||||
'/^.+\s+NOT\s+IN\s+\(.+\)$/i', // NOT IN 연산자
|
||||
'/^(true|false|1|0)$/i', // 불린 값
|
||||
];
|
||||
|
||||
$isValid = false;
|
||||
foreach ($allowedPatterns as $pattern) {
|
||||
if (preg_match($pattern, $expression)) {
|
||||
$isValid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isValid) {
|
||||
throw ValidationException::withMessages([
|
||||
'condition_expression' => __('error.invalid_condition_expression')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델의 규칙 템플릿 조회 (자주 사용되는 패턴들)
|
||||
*/
|
||||
public function getRuleTemplates(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => '크기별 브라켓 개수',
|
||||
'description' => '폭/높이에 따른 브라켓 개수 결정',
|
||||
'condition_example' => 'W1 > 1000',
|
||||
'action_type' => 'INCLUDE',
|
||||
'target_type' => 'MATERIAL',
|
||||
],
|
||||
[
|
||||
'name' => '스크린 타입별 자재',
|
||||
'description' => '스크린 종류에 따른 자재 선택',
|
||||
'condition_example' => "screen_type == 'STEEL'",
|
||||
'action_type' => 'INCLUDE',
|
||||
'target_type' => 'MATERIAL',
|
||||
],
|
||||
[
|
||||
'name' => '설치 방식별 부품',
|
||||
'description' => '설치 타입에 따른 추가 부품',
|
||||
'condition_example' => "install_type IN ('CEILING', 'WALL')",
|
||||
'action_type' => 'INCLUDE',
|
||||
'target_type' => 'PRODUCT',
|
||||
],
|
||||
[
|
||||
'name' => '면적별 수량 배수',
|
||||
'description' => '면적에 비례하는 자재 수량',
|
||||
'condition_example' => 'area > 10',
|
||||
'action_type' => 'MODIFY_QUANTITY',
|
||||
'target_type' => 'MATERIAL',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Design;
|
||||
|
||||
use App\Models\Design\DesignModel;
|
||||
use App\Models\Design\ModelParameter;
|
||||
use App\Models\Design\ModelFormula;
|
||||
use App\Models\Design\BomConditionRule;
|
||||
use App\Models\Design\BomTemplate;
|
||||
use App\Models\Design\BomTemplateItem;
|
||||
use App\Models\Product;
|
||||
use App\Models\Material;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class BomResolverService extends Service
|
||||
{
|
||||
protected ModelParameterService $parameterService;
|
||||
protected ModelFormulaService $formulaService;
|
||||
protected BomConditionRuleService $ruleService;
|
||||
|
||||
public function __construct(
|
||||
ModelParameterService $parameterService,
|
||||
ModelFormulaService $formulaService,
|
||||
BomConditionRuleService $ruleService
|
||||
) {
|
||||
$this->parameterService = $parameterService;
|
||||
$this->formulaService = $formulaService;
|
||||
$this->ruleService = $ruleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 기반 BOM 해석 (전체 프로세스)
|
||||
*/
|
||||
public function resolveBom(int $modelId, array $inputParameters, ?int $templateId = null): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 1. 모델 존재 확인
|
||||
$model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first();
|
||||
if (!$model) {
|
||||
throw new NotFoundHttpException(__('error.model_not_found'));
|
||||
}
|
||||
|
||||
// 2. 매개변수 검증 및 기본값 적용
|
||||
$validatedParameters = $this->parameterService->validateParameters($modelId, $inputParameters);
|
||||
|
||||
// 3. 공식 계산 실행
|
||||
$calculatedValues = $this->formulaService->calculateFormulas($modelId, $validatedParameters);
|
||||
|
||||
// 4. 조건 규칙 평가
|
||||
$ruleResults = $this->ruleService->evaluateRules($modelId, $calculatedValues);
|
||||
|
||||
// 5. 기본 BOM 템플릿 조회 (지정된 템플릿 또는 최신 버전)
|
||||
$baseBom = $this->getBaseBomTemplate($modelId, $templateId);
|
||||
|
||||
// 6. 조건 규칙 적용으로 BOM 변환
|
||||
$resolvedBom = $this->applyRulesToBom($baseBom, $ruleResults['bom_actions'], $calculatedValues);
|
||||
|
||||
// 7. BOM 아이템 정보 보강 (재료/제품 세부정보)
|
||||
$enrichedBom = $this->enrichBomItems($resolvedBom);
|
||||
|
||||
return [
|
||||
'model' => [
|
||||
'id' => $model->id,
|
||||
'code' => $model->code,
|
||||
'name' => $model->name,
|
||||
],
|
||||
'input_parameters' => $validatedParameters,
|
||||
'calculated_values' => $calculatedValues,
|
||||
'matched_rules' => $ruleResults['matched_rules'],
|
||||
'base_bom_template_id' => $baseBom['template_id'] ?? null,
|
||||
'resolved_bom' => $enrichedBom,
|
||||
'summary' => $this->generateBomSummary($enrichedBom),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 해석 미리보기 (저장하지 않음)
|
||||
*/
|
||||
public function previewBom(int $modelId, array $inputParameters, ?int $templateId = null): array
|
||||
{
|
||||
return $this->resolveBom($modelId, $inputParameters, $templateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 변경에 따른 BOM 차이 분석
|
||||
*/
|
||||
public function compareBomByParameters(int $modelId, array $parameters1, array $parameters2, ?int $templateId = null): array
|
||||
{
|
||||
// 두 매개변수 세트로 각각 BOM 해석
|
||||
$bom1 = $this->resolveBom($modelId, $parameters1, $templateId);
|
||||
$bom2 = $this->resolveBom($modelId, $parameters2, $templateId);
|
||||
|
||||
return [
|
||||
'parameters_diff' => [
|
||||
'set1' => $parameters1,
|
||||
'set2' => $parameters2,
|
||||
'changed' => array_diff_assoc($parameters2, $parameters1),
|
||||
],
|
||||
'calculated_values_diff' => [
|
||||
'set1' => $bom1['calculated_values'],
|
||||
'set2' => $bom2['calculated_values'],
|
||||
'changed' => array_diff_assoc($bom2['calculated_values'], $bom1['calculated_values']),
|
||||
],
|
||||
'bom_diff' => $this->compareBomItems($bom1['resolved_bom'], $bom2['resolved_bom']),
|
||||
'summary_diff' => [
|
||||
'set1' => $bom1['summary'],
|
||||
'set2' => $bom2['summary'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 BOM 템플릿 조회
|
||||
*/
|
||||
private function getBaseBomTemplate(int $modelId, ?int $templateId = null): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
if ($templateId) {
|
||||
// 지정된 템플릿 사용
|
||||
$template = BomTemplate::where('tenant_id', $tenantId)
|
||||
->where('id', $templateId)
|
||||
->first();
|
||||
} else {
|
||||
// 해당 모델의 최신 버전에서 BOM 템플릿 찾기
|
||||
$template = BomTemplate::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereHas('modelVersion', function ($q) use ($modelId) {
|
||||
$q->where('model_id', $modelId)
|
||||
->where('status', 'RELEASED');
|
||||
})
|
||||
->orderByDesc('created_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$template) {
|
||||
// 기본 템플릿이 없으면 빈 BOM 반환
|
||||
return [
|
||||
'template_id' => null,
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$items = BomTemplateItem::query()
|
||||
->where('bom_template_id', $template->id)
|
||||
->orderBy('order')
|
||||
->get()
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'target_type' => $item->ref_type,
|
||||
'target_id' => $item->ref_id,
|
||||
'quantity' => $item->quantity,
|
||||
'waste_rate' => $item->waste_rate,
|
||||
'reason' => 'base_template',
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'template_id' => $template->id,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 규칙을 BOM에 적용
|
||||
*/
|
||||
private function applyRulesToBom(array $baseBom, array $bomActions, array $calculatedValues): array
|
||||
{
|
||||
$currentBom = $baseBom['items'];
|
||||
|
||||
foreach ($bomActions as $action) {
|
||||
switch ($action['action_type']) {
|
||||
case 'INCLUDE':
|
||||
// 새 아이템 추가 (중복 체크)
|
||||
$exists = collect($currentBom)->contains(function ($item) use ($action) {
|
||||
return $item['target_type'] === $action['target_type'] &&
|
||||
$item['target_id'] === $action['target_id'];
|
||||
});
|
||||
|
||||
if (!$exists) {
|
||||
$currentBom[] = [
|
||||
'target_type' => $action['target_type'],
|
||||
'target_id' => $action['target_id'],
|
||||
'quantity' => $action['quantity_multiplier'] ?? 1,
|
||||
'waste_rate' => 0,
|
||||
'reason' => $action['rule_name'],
|
||||
];
|
||||
}
|
||||
break;
|
||||
|
||||
case 'EXCLUDE':
|
||||
// 아이템 제외
|
||||
$currentBom = array_filter($currentBom, function ($item) use ($action) {
|
||||
return !($item['target_type'] === $action['target_type'] &&
|
||||
$item['target_id'] === $action['target_id']);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'MODIFY_QUANTITY':
|
||||
// 수량 변경
|
||||
foreach ($currentBom as &$item) {
|
||||
if ($item['target_type'] === $action['target_type'] &&
|
||||
$item['target_id'] === $action['target_id']) {
|
||||
|
||||
$multiplier = $action['quantity_multiplier'] ?? 1;
|
||||
|
||||
// 공식으로 계산된 값이 있으면 그것을 사용
|
||||
if (isset($calculatedValues['quantity_' . $action['target_id']])) {
|
||||
$item['quantity'] = $calculatedValues['quantity_' . $action['target_id']];
|
||||
} else {
|
||||
$item['quantity'] = $item['quantity'] * $multiplier;
|
||||
}
|
||||
|
||||
$item['reason'] = $action['rule_name'];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($currentBom); // 인덱스 재정렬
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 아이템 정보 보강
|
||||
*/
|
||||
private function enrichBomItems(array $bomItems): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$enriched = [];
|
||||
|
||||
foreach ($bomItems as $item) {
|
||||
$enrichedItem = $item;
|
||||
|
||||
if ($item['target_type'] === 'MATERIAL') {
|
||||
$material = Material::where('tenant_id', $tenantId)
|
||||
->where('id', $item['target_id'])
|
||||
->first();
|
||||
|
||||
if ($material) {
|
||||
$enrichedItem['target_info'] = [
|
||||
'id' => $material->id,
|
||||
'code' => $material->code,
|
||||
'name' => $material->name,
|
||||
'unit' => $material->unit,
|
||||
'type' => 'material',
|
||||
];
|
||||
}
|
||||
} elseif ($item['target_type'] === 'PRODUCT') {
|
||||
$product = Product::where('tenant_id', $tenantId)
|
||||
->where('id', $item['target_id'])
|
||||
->first();
|
||||
|
||||
if ($product) {
|
||||
$enrichedItem['target_info'] = [
|
||||
'id' => $product->id,
|
||||
'code' => $product->code,
|
||||
'name' => $product->name,
|
||||
'unit' => $product->unit,
|
||||
'type' => 'product',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 실제 필요 수량 계산 (폐기율 적용)
|
||||
$baseQuantity = $enrichedItem['quantity'];
|
||||
$wasteRate = $enrichedItem['waste_rate'] ?? 0;
|
||||
$enrichedItem['actual_quantity'] = $baseQuantity * (1 + $wasteRate / 100);
|
||||
|
||||
$enriched[] = $enrichedItem;
|
||||
}
|
||||
|
||||
return $enriched;
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 요약 정보 생성
|
||||
*/
|
||||
private function generateBomSummary(array $bomItems): array
|
||||
{
|
||||
$totalItems = count($bomItems);
|
||||
$materialCount = 0;
|
||||
$productCount = 0;
|
||||
$totalValue = 0; // 나중에 가격 정보 추가 시 사용
|
||||
|
||||
foreach ($bomItems as $item) {
|
||||
if ($item['target_type'] === 'MATERIAL') {
|
||||
$materialCount++;
|
||||
} elseif ($item['target_type'] === 'PRODUCT') {
|
||||
$productCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'total_items' => $totalItems,
|
||||
'material_count' => $materialCount,
|
||||
'product_count' => $productCount,
|
||||
'total_estimated_value' => $totalValue,
|
||||
'generated_at' => now()->toISOString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 아이템 비교
|
||||
*/
|
||||
private function compareBomItems(array $bom1, array $bom2): array
|
||||
{
|
||||
$added = [];
|
||||
$removed = [];
|
||||
$modified = [];
|
||||
|
||||
// BOM1에 있던 아이템들 체크
|
||||
foreach ($bom1 as $item1) {
|
||||
$key = $item1['target_type'] . '_' . $item1['target_id'];
|
||||
$found = false;
|
||||
|
||||
foreach ($bom2 as $item2) {
|
||||
if ($item2['target_type'] === $item1['target_type'] &&
|
||||
$item2['target_id'] === $item1['target_id']) {
|
||||
$found = true;
|
||||
|
||||
// 수량이 변경되었는지 체크
|
||||
if ($item1['quantity'] != $item2['quantity']) {
|
||||
$modified[] = [
|
||||
'target_type' => $item1['target_type'],
|
||||
'target_id' => $item1['target_id'],
|
||||
'target_info' => $item1['target_info'] ?? null,
|
||||
'old_quantity' => $item1['quantity'],
|
||||
'new_quantity' => $item2['quantity'],
|
||||
'change' => $item2['quantity'] - $item1['quantity'],
|
||||
];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$removed[] = $item1;
|
||||
}
|
||||
}
|
||||
|
||||
// BOM2에 새로 추가된 아이템들
|
||||
foreach ($bom2 as $item2) {
|
||||
$found = false;
|
||||
|
||||
foreach ($bom1 as $item1) {
|
||||
if ($item1['target_type'] === $item2['target_type'] &&
|
||||
$item1['target_id'] === $item2['target_id']) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$added[] = $item2;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'added' => $added,
|
||||
'removed' => $removed,
|
||||
'modified' => $modified,
|
||||
'summary' => [
|
||||
'added_count' => count($added),
|
||||
'removed_count' => count($removed),
|
||||
'modified_count' => count($modified),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* BOM 해석 결과 저장 (향후 주문/견적 연계용)
|
||||
*/
|
||||
public function saveBomResolution(int $modelId, array $inputParameters, array $bomResolution, string $purpose = 'ESTIMATION'): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
return DB::transaction(function () use ($tenantId, $userId, $modelId, $inputParameters, $bomResolution, $purpose) {
|
||||
// BOM 해석 결과를 데이터베이스에 저장
|
||||
// 향후 order_items, quotation_items 등과 연계할 수 있도록 구조 준비
|
||||
|
||||
$resolutionRecord = [
|
||||
'tenant_id' => $tenantId,
|
||||
'model_id' => $modelId,
|
||||
'input_parameters' => json_encode($inputParameters),
|
||||
'calculated_values' => json_encode($bomResolution['calculated_values']),
|
||||
'resolved_bom' => json_encode($bomResolution['resolved_bom']),
|
||||
'matched_rules' => json_encode($bomResolution['matched_rules']),
|
||||
'summary' => json_encode($bomResolution['summary']),
|
||||
'purpose' => $purpose,
|
||||
'created_by' => $userId,
|
||||
'created_at' => now(),
|
||||
];
|
||||
|
||||
// 실제 테이블이 있다면 저장, 없으면 파일이나 캐시에 임시 저장
|
||||
$resolutionId = md5(json_encode($resolutionRecord));
|
||||
|
||||
// 임시로 캐시에 저장 (1시간)
|
||||
cache()->put("bom_resolution_{$resolutionId}", $resolutionRecord, 3600);
|
||||
|
||||
return [
|
||||
'resolution_id' => $resolutionId,
|
||||
'saved_at' => now()->toISOString(),
|
||||
'purpose' => $purpose,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장된 BOM 해석 결과 조회
|
||||
*/
|
||||
public function getBomResolution(string $resolutionId): ?array
|
||||
{
|
||||
return cache()->get("bom_resolution_{$resolutionId}");
|
||||
}
|
||||
|
||||
/**
|
||||
* KSS01 시나리오 테스트용 빠른 실행
|
||||
*/
|
||||
public function resolveKSS01(array $parameters): array
|
||||
{
|
||||
// KSS01 모델이 있다고 가정하고 하드코딩된 로직
|
||||
$defaults = [
|
||||
'W0' => 800,
|
||||
'H0' => 600,
|
||||
'screen_type' => 'FABRIC',
|
||||
'install_type' => 'WALL',
|
||||
];
|
||||
|
||||
$params = array_merge($defaults, $parameters);
|
||||
|
||||
// 공식 계산 시뮬레이션
|
||||
$calculated = [
|
||||
'W1' => $params['W0'] + 100,
|
||||
'H1' => $params['H0'] + 100,
|
||||
];
|
||||
$calculated['area'] = ($calculated['W1'] * $calculated['H1']) / 1000000;
|
||||
|
||||
// 조건 규칙 시뮬레이션
|
||||
$bom = [];
|
||||
|
||||
// 스크린 타입에 따른 자재
|
||||
if ($params['screen_type'] === 'FABRIC') {
|
||||
$bom[] = ['target_type' => 'MATERIAL', 'target_id' => 1, 'quantity' => $calculated['area'], 'target_info' => ['name' => '패브릭 스크린']];
|
||||
} else {
|
||||
$bom[] = ['target_type' => 'MATERIAL', 'target_id' => 2, 'quantity' => $calculated['area'], 'target_info' => ['name' => '스틸 스크린']];
|
||||
}
|
||||
|
||||
// 브라켓 개수 (폭에 따라)
|
||||
$bracketCount = $calculated['W1'] > 1000 ? 3 : 2;
|
||||
$bom[] = ['target_type' => 'PRODUCT', 'target_id' => 10, 'quantity' => $bracketCount, 'target_info' => ['name' => '브라켓']];
|
||||
|
||||
// 가이드레일
|
||||
$railLength = ($calculated['W1'] + $calculated['H1']) * 2 / 1000; // m 단위
|
||||
$bom[] = ['target_type' => 'MATERIAL', 'target_id' => 3, 'quantity' => $railLength, 'target_info' => ['name' => '가이드레일']];
|
||||
|
||||
return [
|
||||
'model' => ['code' => 'KSS01', 'name' => '기본 스크린 시스템'],
|
||||
'input_parameters' => $params,
|
||||
'calculated_values' => $calculated,
|
||||
'resolved_bom' => $bom,
|
||||
'summary' => ['total_items' => count($bom)],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,461 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Design;
|
||||
|
||||
use App\Models\Design\ModelFormula;
|
||||
use App\Models\Design\DesignModel;
|
||||
use App\Models\Design\ModelParameter;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ModelFormulaService extends Service
|
||||
{
|
||||
/**
|
||||
* 모델의 공식 목록 조회
|
||||
*/
|
||||
public function listByModel(int $modelId, string $q = '', int $page = 1, int $size = 20): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->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;
|
||||
}
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Design;
|
||||
|
||||
use App\Models\Design\ModelParameter;
|
||||
use App\Models\Design\DesignModel;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ModelParameterService extends Service
|
||||
{
|
||||
/**
|
||||
* 모델의 매개변수 목록 조회
|
||||
*/
|
||||
public function listByModel(int $modelId, string $q = '', int $page = 1, int $size = 20): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 모델 존재 확인
|
||||
$model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first();
|
||||
if (!$model) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$query = ModelParameter::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId);
|
||||
|
||||
if ($q !== '') {
|
||||
$query->where(function ($w) use ($q) {
|
||||
$w->where('parameter_name', 'like', "%{$q}%")
|
||||
->orWhere('description', 'like', "%{$q}%");
|
||||
});
|
||||
}
|
||||
|
||||
return $query->orderBy('sort_order')->orderBy('id')->paginate($size, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 조회
|
||||
*/
|
||||
public function show(int $parameterId): ModelParameter
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$parameter = ModelParameter::where('tenant_id', $tenantId)->where('id', $parameterId)->first();
|
||||
if (!$parameter) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return $parameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 생성
|
||||
*/
|
||||
public function create(array $data): ModelParameter
|
||||
{
|
||||
$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 = ModelParameter::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $data['model_id'])
|
||||
->where('parameter_name', $data['parameter_name'])
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw ValidationException::withMessages(['parameter_name' => __('error.duplicate')]);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($tenantId, $userId, $data) {
|
||||
// sort_order가 없으면 자동 설정
|
||||
if (!isset($data['sort_order'])) {
|
||||
$maxOrder = ModelParameter::where('tenant_id', $tenantId)
|
||||
->where('model_id', $data['model_id'])
|
||||
->max('sort_order') ?? 0;
|
||||
$data['sort_order'] = $maxOrder + 1;
|
||||
}
|
||||
|
||||
$payload = array_merge($data, [
|
||||
'tenant_id' => $tenantId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
return ModelParameter::create($payload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 수정
|
||||
*/
|
||||
public function update(int $parameterId, array $data): ModelParameter
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$parameter = ModelParameter::where('tenant_id', $tenantId)->where('id', $parameterId)->first();
|
||||
if (!$parameter) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
// 매개변수명 변경 시 중복 체크
|
||||
if (isset($data['parameter_name']) && $data['parameter_name'] !== $parameter->parameter_name) {
|
||||
$exists = ModelParameter::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $parameter->model_id)
|
||||
->where('parameter_name', $data['parameter_name'])
|
||||
->where('id', '!=', $parameterId)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
throw ValidationException::withMessages(['parameter_name' => __('error.duplicate')]);
|
||||
}
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($parameter, $userId, $data) {
|
||||
$payload = array_merge($data, ['updated_by' => $userId]);
|
||||
$parameter->update($payload);
|
||||
|
||||
return $parameter->fresh();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 삭제
|
||||
*/
|
||||
public function delete(int $parameterId): bool
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$parameter = ModelParameter::where('tenant_id', $tenantId)->where('id', $parameterId)->first();
|
||||
if (!$parameter) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($parameter, $userId) {
|
||||
$parameter->update(['deleted_by' => $userId]);
|
||||
return $parameter->delete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 순서 변경
|
||||
*/
|
||||
public function reorder(int $modelId, array $parameterIds): 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, $parameterIds) {
|
||||
$order = 1;
|
||||
$updated = [];
|
||||
|
||||
foreach ($parameterIds as $parameterId) {
|
||||
$parameter = ModelParameter::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->where('id', $parameterId)
|
||||
->first();
|
||||
|
||||
if ($parameter) {
|
||||
$parameter->update([
|
||||
'sort_order' => $order,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
$updated[] = $parameter->fresh();
|
||||
$order++;
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 대량 저장 (upsert)
|
||||
*/
|
||||
public function bulkUpsert(int $modelId, array $parameters): 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, $parameters) {
|
||||
$result = [];
|
||||
|
||||
foreach ($parameters as $index => $paramData) {
|
||||
$paramData['model_id'] = $modelId;
|
||||
|
||||
// ID가 있으면 업데이트, 없으면 생성
|
||||
if (isset($paramData['id']) && $paramData['id']) {
|
||||
$parameter = ModelParameter::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->where('id', $paramData['id'])
|
||||
->first();
|
||||
|
||||
if ($parameter) {
|
||||
$parameter->update(array_merge($paramData, ['updated_by' => $userId]));
|
||||
$result[] = $parameter->fresh();
|
||||
}
|
||||
} else {
|
||||
// 새로운 매개변수 생성
|
||||
$exists = ModelParameter::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->where('parameter_name', $paramData['parameter_name'])
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
if (!isset($paramData['sort_order'])) {
|
||||
$paramData['sort_order'] = $index + 1;
|
||||
}
|
||||
|
||||
$payload = array_merge($paramData, [
|
||||
'tenant_id' => $tenantId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
]);
|
||||
|
||||
$result[] = ModelParameter::create($payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수 값 검증
|
||||
*/
|
||||
public function validateParameters(int $modelId, array $values): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 모델의 모든 매개변수 조회
|
||||
$parameters = ModelParameter::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
$errors = [];
|
||||
$validated = [];
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
$paramName = $parameter->parameter_name;
|
||||
$value = $values[$paramName] ?? null;
|
||||
|
||||
// 필수 매개변수 체크
|
||||
if ($parameter->is_required && ($value === null || $value === '')) {
|
||||
$errors[$paramName] = __('error.required');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 값이 없고 필수가 아니면 기본값 사용
|
||||
if ($value === null || $value === '') {
|
||||
$validated[$paramName] = $parameter->default_value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 타입별 검증
|
||||
if (!$parameter->validateValue($value)) {
|
||||
$errors[$paramName] = __('error.invalid_value');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 형변환 후 저장
|
||||
$validated[$paramName] = $parameter->castValue($value);
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw ValidationException::withMessages($errors);
|
||||
}
|
||||
|
||||
return $validated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델의 매개변수 스키마 조회 (API용)
|
||||
*/
|
||||
public function getParameterSchema(int $modelId): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 모델 존재 확인
|
||||
$model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first();
|
||||
if (!$model) {
|
||||
throw new NotFoundHttpException(__('error.not_found'));
|
||||
}
|
||||
|
||||
$parameters = ModelParameter::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('model_id', $modelId)
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'model' => [
|
||||
'id' => $model->id,
|
||||
'code' => $model->code,
|
||||
'name' => $model->name,
|
||||
],
|
||||
'parameters' => $parameters->map(function ($param) {
|
||||
return [
|
||||
'name' => $param->parameter_name,
|
||||
'type' => $param->parameter_type,
|
||||
'required' => $param->is_required,
|
||||
'default' => $param->default_value,
|
||||
'min' => $param->min_value,
|
||||
'max' => $param->max_value,
|
||||
'unit' => $param->unit,
|
||||
'options' => $param->options,
|
||||
'description' => $param->description,
|
||||
];
|
||||
})->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Design;
|
||||
|
||||
use App\Models\Design\DesignModel;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductComponent;
|
||||
use App\Models\Category;
|
||||
use App\Services\Service;
|
||||
use App\Services\ProductService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class ProductFromModelService extends Service
|
||||
{
|
||||
protected BomResolverService $bomResolverService;
|
||||
protected ProductService $productService;
|
||||
|
||||
public function __construct(
|
||||
BomResolverService $bomResolverService,
|
||||
ProductService $productService
|
||||
) {
|
||||
$this->bomResolverService = $bomResolverService;
|
||||
$this->productService = $productService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델과 매개변수로부터 제품 생성
|
||||
*/
|
||||
public function createProductFromModel(
|
||||
int $modelId,
|
||||
array $parameters,
|
||||
array $productData,
|
||||
bool $includeComponents = true
|
||||
): Product {
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
// 모델 존재 확인
|
||||
$model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first();
|
||||
if (!$model) {
|
||||
throw new NotFoundHttpException(__('error.model_not_found'));
|
||||
}
|
||||
|
||||
// BOM 해석 실행
|
||||
$bomResolution = $this->bomResolverService->resolveBom($modelId, $parameters);
|
||||
|
||||
return DB::transaction(function () use (
|
||||
$tenantId,
|
||||
$userId,
|
||||
$model,
|
||||
$parameters,
|
||||
$productData,
|
||||
$bomResolution,
|
||||
$includeComponents
|
||||
) {
|
||||
// 제품 기본 정보 설정
|
||||
$productPayload = array_merge([
|
||||
'tenant_id' => $tenantId,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId,
|
||||
'product_type' => 'PRODUCT',
|
||||
'is_active' => true,
|
||||
], $productData);
|
||||
|
||||
// 제품명에 모델 정보 포함 (명시적으로 지정되지 않은 경우)
|
||||
if (!isset($productData['name'])) {
|
||||
$productPayload['name'] = $model->name . ' (' . $this->formatParametersForName($parameters) . ')';
|
||||
}
|
||||
|
||||
// 제품 코드 자동 생성 (명시적으로 지정되지 않은 경우)
|
||||
if (!isset($productData['code'])) {
|
||||
$productPayload['code'] = $this->generateProductCode($model, $parameters);
|
||||
}
|
||||
|
||||
// 제품 생성
|
||||
$product = Product::create($productPayload);
|
||||
|
||||
// BOM 구성요소를 ProductComponent로 생성
|
||||
if ($includeComponents && !empty($bomResolution['resolved_bom'])) {
|
||||
$this->createProductComponents($product, $bomResolution['resolved_bom']);
|
||||
}
|
||||
|
||||
// 모델 기반 제품임을 표시하는 메타 정보 저장
|
||||
$this->saveModelMetadata($product, $model, $parameters, $bomResolution);
|
||||
|
||||
return $product->load('components');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 기존 제품의 BOM 업데이트
|
||||
*/
|
||||
public function updateProductBom(int $productId, array $newParameters): Product
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first();
|
||||
if (!$product) {
|
||||
throw new NotFoundHttpException(__('error.product_not_found'));
|
||||
}
|
||||
|
||||
// 제품이 모델 기반으로 생성된 것인지 확인
|
||||
$metadata = $this->getModelMetadata($product);
|
||||
if (!$metadata) {
|
||||
throw ValidationException::withMessages([
|
||||
'product_id' => __('error.product_not_model_based')
|
||||
]);
|
||||
}
|
||||
|
||||
$modelId = $metadata['model_id'];
|
||||
|
||||
// 새로운 매개변수로 BOM 해석
|
||||
$bomResolution = $this->bomResolverService->resolveBom($modelId, $newParameters);
|
||||
|
||||
return DB::transaction(function () use ($product, $userId, $newParameters, $bomResolution, $metadata) {
|
||||
// 기존 ProductComponent 삭제
|
||||
ProductComponent::where('product_id', $product->id)->delete();
|
||||
|
||||
// 새로운 BOM으로 ProductComponent 생성
|
||||
$this->createProductComponents($product, $bomResolution['resolved_bom']);
|
||||
|
||||
// 메타데이터 업데이트
|
||||
$this->updateModelMetadata($product, $newParameters, $bomResolution);
|
||||
|
||||
$product->update(['updated_by' => $userId]);
|
||||
|
||||
return $product->fresh()->load('components');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 기반 제품 복사 (매개변수 변경)
|
||||
*/
|
||||
public function cloneProductWithParameters(
|
||||
int $sourceProductId,
|
||||
array $newParameters,
|
||||
array $productData = []
|
||||
): Product {
|
||||
$tenantId = $this->tenantId();
|
||||
$userId = $this->apiUserId();
|
||||
|
||||
$sourceProduct = Product::where('tenant_id', $tenantId)->where('id', $sourceProductId)->first();
|
||||
if (!$sourceProduct) {
|
||||
throw new NotFoundHttpException(__('error.product_not_found'));
|
||||
}
|
||||
|
||||
// 원본 제품의 모델 메타데이터 조회
|
||||
$metadata = $this->getModelMetadata($sourceProduct);
|
||||
if (!$metadata) {
|
||||
throw ValidationException::withMessages([
|
||||
'product_id' => __('error.product_not_model_based')
|
||||
]);
|
||||
}
|
||||
|
||||
// 원본 제품 정보를 기반으로 새 제품 데이터 구성
|
||||
$newProductData = array_merge([
|
||||
'name' => $sourceProduct->name . ' (복사본)',
|
||||
'description' => $sourceProduct->description,
|
||||
'category_id' => $sourceProduct->category_id,
|
||||
'unit' => $sourceProduct->unit,
|
||||
'product_type' => $sourceProduct->product_type,
|
||||
], $productData);
|
||||
|
||||
return $this->createProductFromModel(
|
||||
$metadata['model_id'],
|
||||
$newParameters,
|
||||
$newProductData,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 매개변수 변경에 따른 제품들 일괄 업데이트
|
||||
*/
|
||||
public function updateProductsByModel(int $modelId, array $updatedParameters = null): array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 해당 모델로 생성된 제품들 조회
|
||||
$modelBasedProducts = $this->getProductsByModel($modelId);
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($modelBasedProducts as $product) {
|
||||
try {
|
||||
$metadata = $this->getModelMetadata($product);
|
||||
$parameters = $updatedParameters ?? $metadata['parameters'];
|
||||
|
||||
$updatedProduct = $this->updateProductBom($product->id, $parameters);
|
||||
|
||||
$results[] = [
|
||||
'product_id' => $product->id,
|
||||
'product_code' => $product->code,
|
||||
'status' => 'updated',
|
||||
'bom_items_count' => $updatedProduct->components->count(),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$results[] = [
|
||||
'product_id' => $product->id,
|
||||
'product_code' => $product->code,
|
||||
'status' => 'failed',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* ProductComponent 생성
|
||||
*/
|
||||
private function createProductComponents(Product $product, array $bomItems): void
|
||||
{
|
||||
$order = 1;
|
||||
|
||||
foreach ($bomItems as $item) {
|
||||
ProductComponent::create([
|
||||
'product_id' => $product->id,
|
||||
'ref_type' => $item['target_type'],
|
||||
'ref_id' => $item['target_id'],
|
||||
'quantity' => $item['actual_quantity'] ?? $item['quantity'],
|
||||
'waste_rate' => $item['waste_rate'] ?? 0,
|
||||
'order' => $order,
|
||||
'note' => $item['reason'] ?? null,
|
||||
]);
|
||||
|
||||
$order++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 메타데이터 저장
|
||||
*/
|
||||
private function saveModelMetadata(Product $product, DesignModel $model, array $parameters, array $bomResolution): void
|
||||
{
|
||||
$metadata = [
|
||||
'model_id' => $model->id,
|
||||
'model_code' => $model->code,
|
||||
'parameters' => $parameters,
|
||||
'calculated_values' => $bomResolution['calculated_values'],
|
||||
'bom_resolution_summary' => $bomResolution['summary'],
|
||||
'created_at' => now()->toISOString(),
|
||||
];
|
||||
|
||||
// 실제로는 product_metadata 테이블이나 products 테이블의 metadata 컬럼에 저장
|
||||
// 임시로 캐시 사용
|
||||
cache()->put("product_model_metadata_{$product->id}", $metadata, 86400); // 24시간
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 메타데이터 업데이트
|
||||
*/
|
||||
private function updateModelMetadata(Product $product, array $newParameters, array $bomResolution): void
|
||||
{
|
||||
$metadata = $this->getModelMetadata($product);
|
||||
if ($metadata) {
|
||||
$metadata['parameters'] = $newParameters;
|
||||
$metadata['calculated_values'] = $bomResolution['calculated_values'];
|
||||
$metadata['bom_resolution_summary'] = $bomResolution['summary'];
|
||||
$metadata['updated_at'] = now()->toISOString();
|
||||
|
||||
cache()->put("product_model_metadata_{$product->id}", $metadata, 86400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 메타데이터 조회
|
||||
*/
|
||||
private function getModelMetadata(Product $product): ?array
|
||||
{
|
||||
return cache()->get("product_model_metadata_{$product->id}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 매개변수를 제품명용 문자열로 포맷
|
||||
*/
|
||||
private function formatParametersForName(array $parameters): string
|
||||
{
|
||||
$formatted = [];
|
||||
|
||||
foreach ($parameters as $key => $value) {
|
||||
if (is_numeric($value)) {
|
||||
$formatted[] = "{$key}:{$value}";
|
||||
} else {
|
||||
$formatted[] = "{$key}:{$value}";
|
||||
}
|
||||
}
|
||||
|
||||
return implode(', ', array_slice($formatted, 0, 3)); // 최대 3개 매개변수만
|
||||
}
|
||||
|
||||
/**
|
||||
* 제품 코드 자동 생성
|
||||
*/
|
||||
private function generateProductCode(DesignModel $model, array $parameters): string
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
// 모델 코드 + 매개변수 해시
|
||||
$paramHash = substr(md5(json_encode($parameters)), 0, 6);
|
||||
$baseCode = $model->code . '-' . strtoupper($paramHash);
|
||||
|
||||
// 중복 체크 후 순번 추가
|
||||
$counter = 1;
|
||||
$finalCode = $baseCode;
|
||||
|
||||
while (Product::where('tenant_id', $tenantId)->where('code', $finalCode)->exists()) {
|
||||
$finalCode = $baseCode . '-' . str_pad($counter, 2, '0', STR_PAD_LEFT);
|
||||
$counter++;
|
||||
}
|
||||
|
||||
return $finalCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델로 생성된 제품들 조회
|
||||
*/
|
||||
private function getProductsByModel(int $modelId): \Illuminate\Support\Collection
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$products = Product::where('tenant_id', $tenantId)->get();
|
||||
|
||||
return $products->filter(function ($product) use ($modelId) {
|
||||
$metadata = $this->getModelMetadata($product);
|
||||
return $metadata && $metadata['model_id'] == $modelId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 제품의 모델 정보 조회 (API용)
|
||||
*/
|
||||
public function getProductModelInfo(int $productId): ?array
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first();
|
||||
if (!$product) {
|
||||
throw new NotFoundHttpException(__('error.product_not_found'));
|
||||
}
|
||||
|
||||
$metadata = $this->getModelMetadata($product);
|
||||
if (!$metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 모델 정보 추가 조회
|
||||
$model = DesignModel::where('tenant_id', $tenantId)->where('id', $metadata['model_id'])->first();
|
||||
|
||||
return [
|
||||
'product' => [
|
||||
'id' => $product->id,
|
||||
'code' => $product->code,
|
||||
'name' => $product->name,
|
||||
],
|
||||
'model' => [
|
||||
'id' => $model->id,
|
||||
'code' => $model->code,
|
||||
'name' => $model->name,
|
||||
],
|
||||
'parameters' => $metadata['parameters'],
|
||||
'calculated_values' => $metadata['calculated_values'],
|
||||
'bom_summary' => $metadata['bom_resolution_summary'],
|
||||
'created_at' => $metadata['created_at'],
|
||||
'updated_at' => $metadata['updated_at'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모델 기반 제품 재생성 (모델/공식/규칙 변경 후)
|
||||
*/
|
||||
public function regenerateProduct(int $productId, bool $preserveCustomizations = false): Product
|
||||
{
|
||||
$tenantId = $this->tenantId();
|
||||
|
||||
$product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first();
|
||||
if (!$product) {
|
||||
throw new NotFoundHttpException(__('error.product_not_found'));
|
||||
}
|
||||
|
||||
$metadata = $this->getModelMetadata($product);
|
||||
if (!$metadata) {
|
||||
throw ValidationException::withMessages([
|
||||
'product_id' => __('error.product_not_model_based')
|
||||
]);
|
||||
}
|
||||
|
||||
// 사용자 정의 변경사항 보존 로직
|
||||
$customComponents = [];
|
||||
if ($preserveCustomizations) {
|
||||
// 기존 컴포넌트 중 규칙에서 나온 것이 아닌 수동 추가 항목들 식별
|
||||
$customComponents = ProductComponent::where('product_id', $productId)
|
||||
->whereNull('note') // rule_name이 없는 항목들
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
// 제품을 새로운 매개변수로 재생성
|
||||
$regeneratedProduct = $this->updateProductBom($productId, $metadata['parameters']);
|
||||
|
||||
// 커스텀 컴포넌트 복원
|
||||
if (!empty($customComponents)) {
|
||||
foreach ($customComponents as $customComponent) {
|
||||
ProductComponent::create([
|
||||
'product_id' => $productId,
|
||||
'ref_type' => $customComponent['ref_type'],
|
||||
'ref_id' => $customComponent['ref_id'],
|
||||
'quantity' => $customComponent['quantity'],
|
||||
'waste_rate' => $customComponent['waste_rate'],
|
||||
'order' => $customComponent['order'],
|
||||
'note' => 'custom_preserved',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $regeneratedProduct->fresh()->load('components');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user