Revert "feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가"

This reverts commit bf8036a64b.
This commit is contained in:
2025-09-30 23:56:25 +09:00
parent bf8036a64b
commit 802a511aa0
81 changed files with 102 additions and 22632 deletions

View File

@@ -1,63 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\BomConditionRule;
use Illuminate\Foundation\Http\FormRequest;
class CreateBomConditionRuleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'ref_type' => ['required', 'string', 'in:MATERIAL,PRODUCT'],
'ref_id' => ['required', 'integer', 'min:1'],
'condition_expression' => ['required', 'string', 'max:1000'],
'quantity_expression' => ['nullable', 'string', 'max:500'],
'waste_rate_expression' => ['nullable', 'string', 'max:500'],
'description' => ['nullable', 'string', 'max:500'],
'priority' => ['integer', 'min:0'],
'is_active' => ['boolean'],
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'name' => __('validation.attributes.rule_name'),
'ref_type' => __('validation.attributes.ref_type'),
'ref_id' => __('validation.attributes.ref_id'),
'condition_expression' => __('validation.attributes.condition_expression'),
'quantity_expression' => __('validation.attributes.quantity_expression'),
'waste_rate_expression' => __('validation.attributes.waste_rate_expression'),
'description' => __('validation.attributes.description'),
'priority' => __('validation.attributes.priority'),
'is_active' => __('validation.attributes.is_active'),
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_active' => $this->boolean('is_active', true),
'priority' => $this->integer('priority', 0),
]);
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\BomConditionRule;
use App\Http\Requests\Api\V1\PaginateRequest;
class IndexBomConditionRuleRequest extends PaginateRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return array_merge(parent::rules(), [
'search' => ['sometimes', 'string', 'max:255'],
'ref_type' => ['sometimes', 'string', 'in:MATERIAL,PRODUCT'],
'is_active' => ['sometimes', 'boolean'],
]);
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return array_merge(parent::attributes(), [
'search' => __('validation.attributes.search'),
'ref_type' => __('validation.attributes.ref_type'),
'is_active' => __('validation.attributes.is_active'),
]);
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
parent::prepareForValidation();
if ($this->has('is_active')) {
$this->merge(['is_active' => $this->boolean('is_active')]);
}
}
}

View File

@@ -1,66 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\BomConditionRule;
use Illuminate\Foundation\Http\FormRequest;
class UpdateBomConditionRuleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:100'],
'ref_type' => ['sometimes', 'string', 'in:MATERIAL,PRODUCT'],
'ref_id' => ['sometimes', 'integer', 'min:1'],
'condition_expression' => ['sometimes', 'string', 'max:1000'],
'quantity_expression' => ['nullable', 'string', 'max:500'],
'waste_rate_expression' => ['nullable', 'string', 'max:500'],
'description' => ['nullable', 'string', 'max:500'],
'priority' => ['sometimes', 'integer', 'min:0'],
'is_active' => ['sometimes', 'boolean'],
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'name' => __('validation.attributes.rule_name'),
'ref_type' => __('validation.attributes.ref_type'),
'ref_id' => __('validation.attributes.ref_id'),
'condition_expression' => __('validation.attributes.condition_expression'),
'quantity_expression' => __('validation.attributes.quantity_expression'),
'waste_rate_expression' => __('validation.attributes.waste_rate_expression'),
'description' => __('validation.attributes.description'),
'priority' => __('validation.attributes.priority'),
'is_active' => __('validation.attributes.is_active'),
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
if ($this->has('is_active')) {
$this->merge(['is_active' => $this->boolean('is_active')]);
}
if ($this->has('priority')) {
$this->merge(['priority' => $this->integer('priority')]);
}
}
}

View File

@@ -1,89 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\BomResolver;
use Illuminate\Foundation\Http\FormRequest;
class CreateProductFromModelRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'model_id' => ['required', 'integer', 'min:1'],
'input_parameters' => ['required', 'array', 'min:1'],
'input_parameters.*' => ['required'],
'bom_template_id' => ['sometimes', 'integer', 'min:1'],
// Product data
'product_code' => ['required', 'string', 'max:50', 'regex:/^[A-Z0-9_-]+$/'],
'product_name' => ['required', 'string', 'max:100'],
'category_id' => ['sometimes', 'integer', 'min:1'],
'description' => ['nullable', 'string', 'max:1000'],
// Product attributes
'unit' => ['nullable', 'string', 'max:20'],
'min_order_qty' => ['nullable', 'numeric', 'min:0'],
'lead_time_days' => ['nullable', 'integer', 'min:0'],
'is_active' => ['boolean'],
// Additional options
'create_bom_items' => ['boolean'],
'validate_bom' => ['boolean'],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'product_code.regex' => __('validation.product.code_format'),
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'model_id' => __('validation.attributes.model_id'),
'input_parameters' => __('validation.attributes.input_parameters'),
'bom_template_id' => __('validation.attributes.bom_template_id'),
'product_code' => __('validation.attributes.product_code'),
'product_name' => __('validation.attributes.product_name'),
'category_id' => __('validation.attributes.category_id'),
'description' => __('validation.attributes.description'),
'unit' => __('validation.attributes.unit'),
'min_order_qty' => __('validation.attributes.min_order_qty'),
'lead_time_days' => __('validation.attributes.lead_time_days'),
'is_active' => __('validation.attributes.is_active'),
'create_bom_items' => __('validation.attributes.create_bom_items'),
'validate_bom' => __('validation.attributes.validate_bom'),
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_active' => $this->boolean('is_active', true),
'create_bom_items' => $this->boolean('create_bom_items', true),
'validate_bom' => $this->boolean('validate_bom', true),
]);
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\BomResolver;
use Illuminate\Foundation\Http\FormRequest;
class ResolvePreviewRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'input_parameters' => ['required', 'array', 'min:1'],
'input_parameters.*' => ['required'],
'bom_template_id' => ['sometimes', 'integer', 'min:1'],
'include_calculated_values' => ['boolean'],
'include_bom_items' => ['boolean'],
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'input_parameters' => __('validation.attributes.input_parameters'),
'bom_template_id' => __('validation.attributes.bom_template_id'),
'include_calculated_values' => __('validation.attributes.include_calculated_values'),
'include_bom_items' => __('validation.attributes.include_bom_items'),
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'include_calculated_values' => $this->boolean('include_calculated_values', true),
'include_bom_items' => $this->boolean('include_bom_items', true),
]);
}
}

View File

@@ -1,296 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\Design;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Models\Design\ModelParameter;
use App\Models\Product;
use App\Models\Material;
class BomConditionRuleFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Authorization handled by middleware
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$modelId = $this->route('modelId');
$ruleId = $this->route('ruleId');
$rules = [
'rule_name' => [
'required',
'string',
'max:100',
Rule::unique('bom_condition_rules')
->where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
],
'condition_expression' => ['required', 'string', 'max:1000'],
'action_type' => ['required', 'string', 'in:INCLUDE,EXCLUDE,MODIFY_QUANTITY'],
'target_type' => ['required', 'string', 'in:MATERIAL,PRODUCT'],
'target_id' => ['required', 'integer', 'min:1'],
'quantity_multiplier' => ['nullable', 'numeric', 'min:0'],
'is_active' => ['boolean'],
'priority' => ['integer', 'min:0'],
'description' => ['nullable', 'string', 'max:500'],
];
// For update requests, ignore current record in unique validation
if ($ruleId) {
$rules['rule_name'][3] = $rules['rule_name'][3]->ignore($ruleId);
}
return $rules;
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'rule_name.required' => '규칙 이름은 필수입니다.',
'rule_name.unique' => '해당 모델에 이미 동일한 규칙 이름이 존재합니다.',
'condition_expression.required' => '조건 표현식은 필수입니다.',
'condition_expression.max' => '조건 표현식은 1000자를 초과할 수 없습니다.',
'action_type.required' => '액션 타입은 필수입니다.',
'action_type.in' => '액션 타입은 INCLUDE, EXCLUDE, MODIFY_QUANTITY 중 하나여야 합니다.',
'target_type.required' => '대상 타입은 필수입니다.',
'target_type.in' => '대상 타입은 MATERIAL 또는 PRODUCT여야 합니다.',
'target_id.required' => '대상 ID는 필수입니다.',
'target_id.min' => '대상 ID는 1 이상이어야 합니다.',
'quantity_multiplier.numeric' => '수량 배수는 숫자여야 합니다.',
'quantity_multiplier.min' => '수량 배수는 0 이상이어야 합니다.',
'priority.min' => '우선순위는 0 이상이어야 합니다.',
'description.max' => '설명은 500자를 초과할 수 없습니다.',
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'rule_name' => '규칙 이름',
'condition_expression' => '조건 표현식',
'action_type' => '액션 타입',
'target_type' => '대상 타입',
'target_id' => '대상 ID',
'quantity_multiplier' => '수량 배수',
'is_active' => '활성 상태',
'priority' => '우선순위',
'description' => '설명',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_active' => $this->boolean('is_active', true),
'priority' => $this->integer('priority', 0),
]);
// Set default quantity_multiplier for actions that require it
if ($this->input('action_type') === 'MODIFY_QUANTITY' && !$this->has('quantity_multiplier')) {
$this->merge(['quantity_multiplier' => 1.0]);
}
// Clean up condition expression
if ($this->has('condition_expression')) {
$expression = preg_replace('/\s+/', ' ', trim($this->input('condition_expression')));
$this->merge(['condition_expression' => $expression]);
}
}
/**
* Configure the validator instance.
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
$this->validateConditionExpression($validator);
$this->validateTargetExists($validator);
$this->validateActionRequirements($validator);
});
}
/**
* Validate condition expression syntax and variables.
*/
private function validateConditionExpression($validator): void
{
$expression = $this->input('condition_expression');
if (!$expression) {
return;
}
// Check for potentially dangerous characters or functions
$dangerousPatterns = [
'/\b(eval|exec|system|shell_exec|passthru|file_get_contents|file_put_contents|fopen|fwrite)\b/i',
'/[;{}]/', // Semicolons and braces
'/\$[a-zA-Z_]/', // PHP variables
'/\bfunction\s*\(/i', // Function definitions
];
foreach ($dangerousPatterns as $pattern) {
if (preg_match($pattern, $expression)) {
$validator->errors()->add('condition_expression', '조건 표현식에 허용되지 않는 문자나 함수가 포함되어 있습니다.');
return;
}
}
// Validate condition expression format
if (!$this->isValidConditionExpression($expression)) {
$validator->errors()->add('condition_expression', '조건 표현식의 형식이 올바르지 않습니다.');
return;
}
// Validate variables in expression exist as parameters
$this->validateConditionVariables($validator, $expression);
}
/**
* Check if condition expression has valid syntax.
*/
private function isValidConditionExpression(string $expression): bool
{
// Allow comparison operators, logical operators, variables, numbers, strings
$patterns = [
'/^.*(==|!=|>=|<=|>|<|\sIN\s|\sNOT\sIN\s|\sAND\s|\sOR\s).*$/i',
'/^(true|false|[0-9]+)$/i', // Simple boolean or number
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $expression)) {
return true;
}
}
return false;
}
/**
* Validate that variables in condition exist as model parameters.
*/
private function validateConditionVariables($validator, string $expression): void
{
$modelId = $this->route('modelId');
// Extract variable names from expression (exclude operators and values)
preg_match_all('/\b[a-zA-Z][a-zA-Z0-9_]*\b/', $expression, $matches);
$variables = $matches[0];
// Remove logical operators and reserved words
$reservedWords = ['AND', 'OR', 'IN', 'NOT', 'TRUE', 'FALSE', 'true', 'false'];
$variables = array_diff($variables, $reservedWords);
if (empty($variables)) {
return;
}
// Get existing parameters for this model
$existingParameters = ModelParameter::where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->pluck('parameter_name')
->toArray();
// Check for undefined variables
$undefinedVariables = array_diff($variables, $existingParameters);
if (!empty($undefinedVariables)) {
$validator->errors()->add('condition_expression',
'조건식에 정의되지 않은 변수가 사용되었습니다: ' . implode(', ', $undefinedVariables)
);
}
}
/**
* Validate that the target (MATERIAL or PRODUCT) exists.
*/
private function validateTargetExists($validator): void
{
$targetType = $this->input('target_type');
$targetId = $this->input('target_id');
if (!$targetType || !$targetId) {
return;
}
$tenantId = auth()->user()?->currentTenant?->id;
switch ($targetType) {
case 'MATERIAL':
$exists = Material::where('id', $targetId)
->where('tenant_id', $tenantId)
->whereNull('deleted_at')
->exists();
if (!$exists) {
$validator->errors()->add('target_id', '지정된 자재가 존재하지 않습니다.');
}
break;
case 'PRODUCT':
$exists = Product::where('id', $targetId)
->where('tenant_id', $tenantId)
->whereNull('deleted_at')
->exists();
if (!$exists) {
$validator->errors()->add('target_id', '지정된 제품이 존재하지 않습니다.');
}
break;
}
}
/**
* Validate action-specific requirements.
*/
private function validateActionRequirements($validator): void
{
$actionType = $this->input('action_type');
$quantityMultiplier = $this->input('quantity_multiplier');
switch ($actionType) {
case 'MODIFY_QUANTITY':
// MODIFY_QUANTITY action requires quantity_multiplier
if ($quantityMultiplier === null || $quantityMultiplier === '') {
$validator->errors()->add('quantity_multiplier', 'MODIFY_QUANTITY 액션에는 수량 배수가 필요합니다.');
} elseif ($quantityMultiplier <= 0) {
$validator->errors()->add('quantity_multiplier', 'MODIFY_QUANTITY 액션의 수량 배수는 0보다 커야 합니다.');
}
break;
case 'INCLUDE':
// INCLUDE action can optionally have quantity_multiplier (default to 1)
if ($quantityMultiplier !== null && $quantityMultiplier <= 0) {
$validator->errors()->add('quantity_multiplier', 'INCLUDE 액션의 수량 배수는 0보다 커야 합니다.');
}
break;
case 'EXCLUDE':
// EXCLUDE action doesn't need quantity_multiplier
if ($quantityMultiplier !== null) {
$validator->errors()->add('quantity_multiplier', 'EXCLUDE 액션에는 수량 배수가 필요하지 않습니다.');
}
break;
}
}
}

View File

@@ -1,274 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\Design;
use Illuminate\Foundation\Http\FormRequest;
use App\Models\Design\ModelParameter;
use App\Models\Design\BomTemplate;
class BomResolverFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Authorization handled by middleware
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'input_parameters' => ['required', 'array', 'min:1'],
'input_parameters.*' => ['required'],
'bom_template_id' => ['sometimes', 'integer', 'min:1'],
'include_calculated_values' => ['boolean'],
'include_bom_items' => ['boolean'],
'include_condition_rules' => ['boolean'],
'validate_before_resolve' => ['boolean'],
'calculation_precision' => ['integer', 'min:0', 'max:10'],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'input_parameters.required' => '입력 매개변수는 필수입니다.',
'input_parameters.array' => '입력 매개변수는 배열 형태여야 합니다.',
'input_parameters.min' => '최소 하나 이상의 입력 매개변수가 필요합니다.',
'input_parameters.*.required' => '모든 입력 매개변수 값은 필수입니다.',
'bom_template_id.integer' => 'BOM 템플릿 ID는 정수여야 합니다.',
'bom_template_id.min' => 'BOM 템플릿 ID는 1 이상이어야 합니다.',
'calculation_precision.integer' => '계산 정밀도는 정수여야 합니다.',
'calculation_precision.min' => '계산 정밀도는 0 이상이어야 합니다.',
'calculation_precision.max' => '계산 정밀도는 10 이하여야 합니다.',
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'input_parameters' => '입력 매개변수',
'bom_template_id' => 'BOM 템플릿 ID',
'include_calculated_values' => '계산값 포함 여부',
'include_bom_items' => 'BOM 아이템 포함 여부',
'include_condition_rules' => '조건 규칙 포함 여부',
'validate_before_resolve' => '해결 전 유효성 검사',
'calculation_precision' => '계산 정밀도',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'include_calculated_values' => $this->boolean('include_calculated_values', true),
'include_bom_items' => $this->boolean('include_bom_items', true),
'include_condition_rules' => $this->boolean('include_condition_rules', true),
'validate_before_resolve' => $this->boolean('validate_before_resolve', true),
'calculation_precision' => $this->integer('calculation_precision', 2),
]);
// Ensure input_parameters is an array
if ($this->has('input_parameters') && !is_array($this->input('input_parameters'))) {
$params = json_decode($this->input('input_parameters'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$this->merge(['input_parameters' => $params]);
}
}
}
/**
* Configure the validator instance.
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
$this->validateInputParameters($validator);
$this->validateBomTemplate($validator);
$this->validateParameterValues($validator);
});
}
/**
* Validate input parameters against model parameter definitions.
*/
private function validateInputParameters($validator): void
{
$modelId = $this->route('modelId');
$inputParameters = $this->input('input_parameters', []);
if (empty($inputParameters)) {
return;
}
// Get model's INPUT parameters
$modelParameters = ModelParameter::where('model_id', $modelId)
->where('parameter_type', 'INPUT')
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->get()
->keyBy('parameter_name');
// Check for required parameters
$requiredParams = $modelParameters->where('is_required', true)->pluck('parameter_name')->toArray();
$providedParams = array_keys($inputParameters);
$missingRequired = array_diff($requiredParams, $providedParams);
if (!empty($missingRequired)) {
$validator->errors()->add('input_parameters',
'다음 필수 매개변수가 누락되었습니다: ' . implode(', ', $missingRequired)
);
}
// Check for unknown parameters
$knownParams = $modelParameters->pluck('parameter_name')->toArray();
$unknownParams = array_diff($providedParams, $knownParams);
if (!empty($unknownParams)) {
$validator->errors()->add('input_parameters',
'알 수 없는 매개변수가 포함되어 있습니다: ' . implode(', ', $unknownParams)
);
}
}
/**
* Validate BOM template exists and belongs to the model.
*/
private function validateBomTemplate($validator): void
{
$bomTemplateId = $this->input('bom_template_id');
$modelId = $this->route('modelId');
if (!$bomTemplateId) {
return;
}
$template = BomTemplate::where('id', $bomTemplateId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->first();
if (!$template) {
$validator->errors()->add('bom_template_id', '지정된 BOM 템플릿이 존재하지 않습니다.');
return;
}
// Check if template belongs to the model (through model_version)
if ($template->modelVersion && $template->modelVersion->model_id != $modelId) {
$validator->errors()->add('bom_template_id', 'BOM 템플릿이 해당 모델에 속하지 않습니다.');
}
}
/**
* Validate parameter values against their constraints.
*/
private function validateParameterValues($validator): void
{
$modelId = $this->route('modelId');
$inputParameters = $this->input('input_parameters', []);
if (empty($inputParameters)) {
return;
}
// Get model parameter definitions
$modelParameters = ModelParameter::where('model_id', $modelId)
->where('parameter_type', 'INPUT')
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->get()
->keyBy('parameter_name');
foreach ($inputParameters as $paramName => $value) {
$parameter = $modelParameters->get($paramName);
if (!$parameter) {
continue; // Unknown parameter already handled above
}
// Validate value against parameter constraints
$this->validateParameterValue($validator, $parameter, $paramName, $value);
}
}
/**
* Validate individual parameter value.
*/
private function validateParameterValue($validator, $parameter, string $paramName, $value): void
{
// Check for null/empty required values
if ($parameter->is_required && ($value === null || $value === '')) {
$validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 필수 매개변수입니다.");
return;
}
// Validate data type
switch ($parameter->data_type ?? 'STRING') {
case 'INTEGER':
if (!is_numeric($value) || (int)$value != $value) {
$validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 정수여야 합니다.");
return;
}
$value = (int)$value;
break;
case 'DECIMAL':
if (!is_numeric($value)) {
$validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 숫자여야 합니다.");
return;
}
$value = (float)$value;
break;
case 'BOOLEAN':
if (!is_bool($value) && !in_array($value, [0, 1, '0', '1', 'true', 'false'])) {
$validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 불린 값이어야 합니다.");
return;
}
break;
case 'STRING':
if (!is_string($value) && !is_numeric($value)) {
$validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 문자열이어야 합니다.");
return;
}
$value = (string)$value;
break;
}
// Validate min/max values for numeric types
if (in_array($parameter->data_type, ['INTEGER', 'DECIMAL']) && is_numeric($value)) {
if ($parameter->min_value !== null && $value < $parameter->min_value) {
$validator->errors()->add("input_parameters.{$paramName}",
"{$paramName}은(는) {$parameter->min_value} 이상이어야 합니다."
);
}
if ($parameter->max_value !== null && $value > $parameter->max_value) {
$validator->errors()->add("input_parameters.{$paramName}",
"{$paramName}은(는) {$parameter->max_value} 이하여야 합니다."
);
}
}
// Validate options for select type parameters
if (!empty($parameter->options) && !in_array($value, $parameter->options)) {
$validOptions = implode(', ', $parameter->options);
$validator->errors()->add("input_parameters.{$paramName}",
"{$paramName}의 값은 다음 중 하나여야 합니다: {$validOptions}"
);
}
}
}

View File

@@ -1,264 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\Design;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Models\Design\ModelParameter;
class ModelFormulaFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Authorization handled by middleware
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$modelId = $this->route('modelId');
$formulaId = $this->route('formulaId');
$rules = [
'formula_name' => [
'required',
'string',
'max:100',
'regex:/^[a-zA-Z][a-zA-Z0-9_\s]*$/',
Rule::unique('model_formulas')
->where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
],
'formula_expression' => ['required', 'string', 'max:1000'],
'unit' => ['nullable', 'string', 'max:20'],
'description' => ['nullable', 'string', 'max:500'],
'calculation_order' => ['integer', 'min:0'],
'dependencies' => ['nullable', 'array'],
'dependencies.*' => ['string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'],
];
// For update requests, ignore current record in unique validation
if ($formulaId) {
$rules['formula_name'][4] = $rules['formula_name'][4]->ignore($formulaId);
}
return $rules;
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'formula_name.required' => '공식 이름은 필수입니다.',
'formula_name.regex' => '공식 이름은 영문자로 시작하고 영문자, 숫자, 언더스코어, 공백만 사용할 수 있습니다.',
'formula_name.unique' => '해당 모델에 이미 동일한 공식 이름이 존재합니다.',
'formula_expression.required' => '공식 표현식은 필수입니다.',
'formula_expression.max' => '공식 표현식은 1000자를 초과할 수 없습니다.',
'unit.max' => '단위는 20자를 초과할 수 없습니다.',
'description.max' => '설명은 500자를 초과할 수 없습니다.',
'calculation_order.min' => '계산 순서는 0 이상이어야 합니다.',
'dependencies.array' => '의존성은 배열 형태여야 합니다.',
'dependencies.*.regex' => '의존성 변수명은 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용할 수 있습니다.',
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'formula_name' => '공식 이름',
'formula_expression' => '공식 표현식',
'unit' => '단위',
'description' => '설명',
'calculation_order' => '계산 순서',
'dependencies' => '의존성',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'calculation_order' => $this->integer('calculation_order', 0),
]);
// Convert dependencies to array if it's a string
if ($this->has('dependencies') && is_string($this->input('dependencies'))) {
$dependencies = json_decode($this->input('dependencies'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$this->merge(['dependencies' => $dependencies]);
}
}
// Clean up formula expression - remove extra whitespace
if ($this->has('formula_expression')) {
$expression = preg_replace('/\s+/', ' ', trim($this->input('formula_expression')));
$this->merge(['formula_expression' => $expression]);
}
}
/**
* Configure the validator instance.
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
$this->validateFormulaExpression($validator);
$this->validateDependencies($validator);
$this->validateNoCircularDependency($validator);
});
}
/**
* Validate formula expression syntax.
*/
private function validateFormulaExpression($validator): void
{
$expression = $this->input('formula_expression');
if (!$expression) {
return;
}
// Check for potentially dangerous characters or functions
$dangerousPatterns = [
'/\b(eval|exec|system|shell_exec|passthru|file_get_contents|file_put_contents|fopen|fwrite)\b/i',
'/[;{}]/', // Semicolons and braces
'/\$[a-zA-Z_]/', // PHP variables
'/\bfunction\s*\(/i', // Function definitions
];
foreach ($dangerousPatterns as $pattern) {
if (preg_match($pattern, $expression)) {
$validator->errors()->add('formula_expression', '공식 표현식에 허용되지 않는 문자나 함수가 포함되어 있습니다.');
return;
}
}
// Validate mathematical expression format
if (!$this->isValidMathExpression($expression)) {
$validator->errors()->add('formula_expression', '공식 표현식의 형식이 올바르지 않습니다. 수학 연산자와 변수명만 사용할 수 있습니다.');
}
// Extract variables from expression and validate they exist as parameters
$this->validateExpressionVariables($validator, $expression);
}
/**
* Check if expression contains valid mathematical operations.
*/
private function isValidMathExpression(string $expression): bool
{
// Allow: numbers, variables, basic math operators, parentheses, math functions
$allowedPattern = '/^[a-zA-Z0-9_\s\+\-\*\/\(\)\.\,]+$/';
// Allow common math functions
$mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min'];
$functionPattern = '/\b(' . implode('|', $mathFunctions) . ')\s*\(/i';
// Remove math functions for basic pattern check
$cleanExpression = preg_replace($functionPattern, '', $expression);
return preg_match($allowedPattern, $cleanExpression);
}
/**
* Validate that variables in expression exist as model parameters.
*/
private function validateExpressionVariables($validator, string $expression): void
{
$modelId = $this->route('modelId');
// Extract variable names from expression
preg_match_all('/\b[a-zA-Z][a-zA-Z0-9_]*\b/', $expression, $matches);
$variables = $matches[0];
// Remove math functions from variables
$mathFunctions = ['sin', 'cos', 'tan', 'log', 'exp', 'sqrt', 'pow', 'abs', 'ceil', 'floor', 'round', 'max', 'min'];
$variables = array_diff($variables, $mathFunctions);
if (empty($variables)) {
return;
}
// Get existing parameters for this model
$existingParameters = ModelParameter::where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->pluck('parameter_name')
->toArray();
// Check for undefined variables
$undefinedVariables = array_diff($variables, $existingParameters);
if (!empty($undefinedVariables)) {
$validator->errors()->add('formula_expression',
'공식에 정의되지 않은 변수가 사용되었습니다: ' . implode(', ', $undefinedVariables)
);
}
}
/**
* Validate dependencies array.
*/
private function validateDependencies($validator): void
{
$dependencies = $this->input('dependencies', []);
$modelId = $this->route('modelId');
if (empty($dependencies)) {
return;
}
// Get existing parameters for this model
$existingParameters = ModelParameter::where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->pluck('parameter_name')
->toArray();
// Check that all dependencies exist as parameters
$invalidDependencies = array_diff($dependencies, $existingParameters);
if (!empty($invalidDependencies)) {
$validator->errors()->add('dependencies',
'존재하지 않는 매개변수가 의존성에 포함되어 있습니다: ' . implode(', ', $invalidDependencies)
);
}
}
/**
* Validate there's no circular dependency.
*/
private function validateNoCircularDependency($validator): void
{
$dependencies = $this->input('dependencies', []);
$formulaName = $this->input('formula_name');
if (empty($dependencies) || !$formulaName) {
return;
}
// Check for direct self-reference
if (in_array($formulaName, $dependencies)) {
$validator->errors()->add('dependencies', '공식이 자기 자신을 참조할 수 없습니다.');
return;
}
// For more complex circular dependency check, this would require
// analyzing all formulas in the model - simplified version here
// In production, implement full dependency graph analysis
}
}

View File

@@ -1,172 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\Design;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class ModelParameterFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Authorization handled by middleware
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$modelId = $this->route('modelId');
$parameterId = $this->route('parameterId');
$rules = [
'parameter_name' => [
'required',
'string',
'max:50',
'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/',
Rule::unique('model_parameters')
->where('model_id', $modelId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
],
'parameter_type' => ['required', 'string', 'in:INPUT,OUTPUT'],
'is_required' => ['boolean'],
'default_value' => ['nullable', 'string', 'max:255'],
'min_value' => ['nullable', 'numeric'],
'max_value' => ['nullable', 'numeric', 'gte:min_value'],
'unit' => ['nullable', 'string', 'max:20'],
'options' => ['nullable', 'array'],
'options.*' => ['string', 'max:100'],
'description' => ['nullable', 'string', 'max:500'],
'sort_order' => ['integer', 'min:0'],
];
// For update requests, ignore current record in unique validation
if ($parameterId) {
$rules['parameter_name'][4] = $rules['parameter_name'][4]->ignore($parameterId);
}
return $rules;
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'parameter_name.required' => '매개변수 이름은 필수입니다.',
'parameter_name.regex' => '매개변수 이름은 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용할 수 있습니다.',
'parameter_name.unique' => '해당 모델에 이미 동일한 매개변수 이름이 존재합니다.',
'parameter_type.required' => '매개변수 타입은 필수입니다.',
'parameter_type.in' => '매개변수 타입은 INPUT 또는 OUTPUT이어야 합니다.',
'min_value.numeric' => '최소값은 숫자여야 합니다.',
'max_value.numeric' => '최대값은 숫자여야 합니다.',
'max_value.gte' => '최대값은 최소값보다 크거나 같아야 합니다.',
'unit.max' => '단위는 20자를 초과할 수 없습니다.',
'description.max' => '설명은 500자를 초과할 수 없습니다.',
'sort_order.min' => '정렬 순서는 0 이상이어야 합니다.',
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'parameter_name' => '매개변수 이름',
'parameter_type' => '매개변수 타입',
'is_required' => '필수 여부',
'default_value' => '기본값',
'min_value' => '최소값',
'max_value' => '최대값',
'unit' => '단위',
'options' => '옵션 목록',
'description' => '설명',
'sort_order' => '정렬 순서',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_required' => $this->boolean('is_required', false),
'sort_order' => $this->integer('sort_order', 0),
]);
// Convert options to array if it's a string
if ($this->has('options') && is_string($this->input('options'))) {
$options = json_decode($this->input('options'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$this->merge(['options' => $options]);
}
}
}
/**
* Configure the validator instance.
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
// Validate that INPUT parameters can have default values and constraints
if ($this->input('parameter_type') === 'INPUT') {
$this->validateInputParameterConstraints($validator);
}
// Validate that OUTPUT parameters don't have input-specific fields
if ($this->input('parameter_type') === 'OUTPUT') {
$this->validateOutputParameterConstraints($validator);
}
// Validate min/max value relationship
$this->validateMinMaxValues($validator);
});
}
/**
* Validate INPUT parameter specific constraints.
*/
private function validateInputParameterConstraints($validator): void
{
// INPUT parameters can have all constraints
// No additional validation needed for INPUT type
}
/**
* Validate OUTPUT parameter specific constraints.
*/
private function validateOutputParameterConstraints($validator): void
{
// OUTPUT parameters should not have certain input-specific fields
if ($this->filled('is_required') && $this->input('is_required')) {
$validator->errors()->add('is_required', 'OUTPUT 매개변수는 필수 항목이 될 수 없습니다.');
}
if ($this->filled('default_value')) {
$validator->errors()->add('default_value', 'OUTPUT 매개변수는 기본값을 가질 수 없습니다.');
}
}
/**
* Validate min/max value relationship.
*/
private function validateMinMaxValues($validator): void
{
$minValue = $this->input('min_value');
$maxValue = $this->input('max_value');
if ($minValue !== null && $maxValue !== null && $minValue > $maxValue) {
$validator->errors()->add('max_value', '최대값은 최소값보다 크거나 같아야 합니다.');
}
}
}

View File

@@ -1,388 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\Design;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use App\Models\Design\ModelParameter;
use App\Models\Design\BomTemplate;
use App\Models\Category;
use App\Models\Product;
class ProductFromModelFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Authorization handled by middleware
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$tenantId = auth()->user()?->currentTenant?->id;
return [
// Model and BOM configuration
'input_parameters' => ['required', 'array', 'min:1'],
'input_parameters.*' => ['required'],
'bom_template_id' => ['sometimes', 'integer', 'min:1'],
// Product basic information
'product_code' => [
'required',
'string',
'max:50',
'regex:/^[A-Z0-9_-]+$/',
Rule::unique('products')
->where('tenant_id', $tenantId)
->whereNull('deleted_at')
],
'product_name' => ['required', 'string', 'max:100'],
'description' => ['nullable', 'string', 'max:1000'],
// Product categorization
'category_id' => ['sometimes', 'integer', 'min:1'],
'product_type' => ['nullable', 'string', 'in:PRODUCT,PART,SUBASSEMBLY'],
// Product specifications
'unit' => ['nullable', 'string', 'max:20'],
'min_order_qty' => ['nullable', 'numeric', 'min:0'],
'lead_time_days' => ['nullable', 'integer', 'min:0', 'max:365'],
'is_active' => ['boolean'],
// Product creation options
'create_bom_items' => ['boolean'],
'validate_bom' => ['boolean'],
'save_parameters' => ['boolean'],
'auto_generate_variants' => ['boolean'],
// Pricing and cost
'base_cost' => ['nullable', 'numeric', 'min:0'],
'markup_percentage' => ['nullable', 'numeric', 'min:0', 'max:1000'],
// Additional attributes (dynamic based on category)
'attributes' => ['sometimes', 'array'],
'attributes.*' => ['nullable'],
// Tags and classification
'tags' => ['sometimes', 'array'],
'tags.*' => ['string', 'max:50'],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'input_parameters.required' => '입력 매개변수는 필수입니다.',
'input_parameters.array' => '입력 매개변수는 배열 형태여야 합니다.',
'input_parameters.min' => '최소 하나 이상의 입력 매개변수가 필요합니다.',
'input_parameters.*.required' => '모든 입력 매개변수 값은 필수입니다.',
'product_code.required' => '제품 코드는 필수입니다.',
'product_code.regex' => '제품 코드는 대문자, 숫자, 언더스코어, 하이픈만 사용할 수 있습니다.',
'product_code.unique' => '이미 존재하는 제품 코드입니다.',
'product_name.required' => '제품명은 필수입니다.',
'product_name.max' => '제품명은 100자를 초과할 수 없습니다.',
'description.max' => '설명은 1000자를 초과할 수 없습니다.',
'category_id.integer' => '카테고리 ID는 정수여야 합니다.',
'category_id.min' => '카테고리 ID는 1 이상이어야 합니다.',
'product_type.in' => '제품 타입은 PRODUCT, PART, SUBASSEMBLY 중 하나여야 합니다.',
'unit.max' => '단위는 20자를 초과할 수 없습니다.',
'min_order_qty.numeric' => '최소 주문 수량은 숫자여야 합니다.',
'min_order_qty.min' => '최소 주문 수량은 0 이상이어야 합니다.',
'lead_time_days.integer' => '리드타임은 정수여야 합니다.',
'lead_time_days.min' => '리드타임은 0 이상이어야 합니다.',
'lead_time_days.max' => '리드타임은 365일을 초과할 수 없습니다.',
'base_cost.numeric' => '기본 원가는 숫자여야 합니다.',
'base_cost.min' => '기본 원가는 0 이상이어야 합니다.',
'markup_percentage.numeric' => '마크업 비율은 숫자여야 합니다.',
'markup_percentage.min' => '마크업 비율은 0 이상이어야 합니다.',
'markup_percentage.max' => '마크업 비율은 1000%를 초과할 수 없습니다.',
'bom_template_id.integer' => 'BOM 템플릿 ID는 정수여야 합니다.',
'bom_template_id.min' => 'BOM 템플릿 ID는 1 이상이어야 합니다.',
'tags.array' => '태그는 배열 형태여야 합니다.',
'tags.*.max' => '각 태그는 50자를 초과할 수 없습니다.',
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'input_parameters' => '입력 매개변수',
'bom_template_id' => 'BOM 템플릿 ID',
'product_code' => '제품 코드',
'product_name' => '제품명',
'description' => '설명',
'category_id' => '카테고리 ID',
'product_type' => '제품 타입',
'unit' => '단위',
'min_order_qty' => '최소 주문 수량',
'lead_time_days' => '리드타임',
'is_active' => '활성 상태',
'create_bom_items' => 'BOM 아이템 생성',
'validate_bom' => 'BOM 유효성 검사',
'save_parameters' => '매개변수 저장',
'auto_generate_variants' => '자동 변형 생성',
'base_cost' => '기본 원가',
'markup_percentage' => '마크업 비율',
'attributes' => '추가 속성',
'tags' => '태그',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_active' => $this->boolean('is_active', true),
'create_bom_items' => $this->boolean('create_bom_items', true),
'validate_bom' => $this->boolean('validate_bom', true),
'save_parameters' => $this->boolean('save_parameters', true),
'auto_generate_variants' => $this->boolean('auto_generate_variants', false),
]);
// Ensure input_parameters is an array
if ($this->has('input_parameters') && !is_array($this->input('input_parameters'))) {
$params = json_decode($this->input('input_parameters'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$this->merge(['input_parameters' => $params]);
}
}
// Ensure attributes is an array
if ($this->has('attributes') && !is_array($this->input('attributes'))) {
$attributes = json_decode($this->input('attributes'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$this->merge(['attributes' => $attributes]);
}
}
// Ensure tags is an array
if ($this->has('tags') && !is_array($this->input('tags'))) {
$tags = json_decode($this->input('tags'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$this->merge(['tags' => $tags]);
}
}
// Set default product_type if not provided
if (!$this->has('product_type')) {
$this->merge(['product_type' => 'PRODUCT']);
}
// Convert product_code to uppercase
if ($this->has('product_code')) {
$this->merge(['product_code' => strtoupper($this->input('product_code'))]);
}
}
/**
* Configure the validator instance.
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
$this->validateInputParameters($validator);
$this->validateBomTemplate($validator);
$this->validateCategory($validator);
$this->validateParameterValues($validator);
$this->validateBusinessRules($validator);
});
}
/**
* Validate input parameters against model parameter definitions.
*/
private function validateInputParameters($validator): void
{
$modelId = $this->route('modelId');
$inputParameters = $this->input('input_parameters', []);
if (empty($inputParameters)) {
return;
}
// Get model's INPUT parameters
$modelParameters = ModelParameter::where('model_id', $modelId)
->where('parameter_type', 'INPUT')
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->get()
->keyBy('parameter_name');
// Check for required parameters
$requiredParams = $modelParameters->where('is_required', true)->pluck('parameter_name')->toArray();
$providedParams = array_keys($inputParameters);
$missingRequired = array_diff($requiredParams, $providedParams);
if (!empty($missingRequired)) {
$validator->errors()->add('input_parameters',
'다음 필수 매개변수가 누락되었습니다: ' . implode(', ', $missingRequired)
);
}
// Check for unknown parameters
$knownParams = $modelParameters->pluck('parameter_name')->toArray();
$unknownParams = array_diff($providedParams, $knownParams);
if (!empty($unknownParams)) {
$validator->errors()->add('input_parameters',
'알 수 없는 매개변수가 포함되어 있습니다: ' . implode(', ', $unknownParams)
);
}
}
/**
* Validate BOM template exists and belongs to the model.
*/
private function validateBomTemplate($validator): void
{
$bomTemplateId = $this->input('bom_template_id');
$modelId = $this->route('modelId');
if (!$bomTemplateId) {
return;
}
$template = BomTemplate::where('id', $bomTemplateId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->first();
if (!$template) {
$validator->errors()->add('bom_template_id', '지정된 BOM 템플릿이 존재하지 않습니다.');
return;
}
// Check if template belongs to the model (through model_version)
if ($template->modelVersion && $template->modelVersion->model_id != $modelId) {
$validator->errors()->add('bom_template_id', 'BOM 템플릿이 해당 모델에 속하지 않습니다.');
}
}
/**
* Validate category exists and is accessible.
*/
private function validateCategory($validator): void
{
$categoryId = $this->input('category_id');
if (!$categoryId) {
return;
}
$category = Category::where('id', $categoryId)
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->first();
if (!$category) {
$validator->errors()->add('category_id', '지정된 카테고리가 존재하지 않습니다.');
}
}
/**
* Validate parameter values against their constraints.
*/
private function validateParameterValues($validator): void
{
$modelId = $this->route('modelId');
$inputParameters = $this->input('input_parameters', []);
if (empty($inputParameters)) {
return;
}
// Get model parameter definitions
$modelParameters = ModelParameter::where('model_id', $modelId)
->where('parameter_type', 'INPUT')
->where('tenant_id', auth()->user()?->currentTenant?->id)
->whereNull('deleted_at')
->get()
->keyBy('parameter_name');
foreach ($inputParameters as $paramName => $value) {
$parameter = $modelParameters->get($paramName);
if (!$parameter) {
continue; // Unknown parameter already handled above
}
// Validate value against parameter constraints
$this->validateParameterValue($validator, $parameter, $paramName, $value);
}
}
/**
* Validate individual parameter value.
*/
private function validateParameterValue($validator, $parameter, string $paramName, $value): void
{
// Check for null/empty required values
if ($parameter->is_required && ($value === null || $value === '')) {
$validator->errors()->add("input_parameters.{$paramName}", "{$paramName}은(는) 필수 매개변수입니다.");
return;
}
// Skip validation for empty optional parameters
if (!$parameter->is_required && ($value === null || $value === '')) {
return;
}
// Use model's validation method if available
if (method_exists($parameter, 'validateValue') && !$parameter->validateValue($value)) {
$validator->errors()->add("input_parameters.{$paramName}", "{$paramName}의 값이 유효하지 않습니다.");
}
}
/**
* Validate business rules specific to product creation.
*/
private function validateBusinessRules($validator): void
{
// If validate_bom is true, ensure we have enough data for BOM validation
if ($this->input('validate_bom', true) && !$this->input('bom_template_id')) {
// Could check if model has default BOM template or condition rules
// For now, just warn
}
// If auto_generate_variants is true, validate variant generation is possible
if ($this->input('auto_generate_variants', false)) {
// Check if model has variant-generating parameters
// This would require checking parameter configurations
}
// Validate pricing logic
$baseCost = $this->input('base_cost');
$markupPercentage = $this->input('markup_percentage');
if ($baseCost !== null && $markupPercentage !== null) {
if ($baseCost == 0 && $markupPercentage > 0) {
$validator->errors()->add('markup_percentage', '기본 원가가 0인 경우 마크업을 설정할 수 없습니다.');
}
}
}
}

View File

@@ -1,67 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\ModelFormula;
use Illuminate\Foundation\Http\FormRequest;
class CreateModelFormulaRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'target_parameter' => ['required', 'string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'],
'expression' => ['required', 'string', 'max:1000'],
'description' => ['nullable', 'string', 'max:500'],
'is_active' => ['boolean'],
'execution_order' => ['integer', 'min:0'],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'target_parameter.regex' => __('validation.model_formula.target_parameter_format'),
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'name' => __('validation.attributes.formula_name'),
'target_parameter' => __('validation.attributes.target_parameter'),
'expression' => __('validation.attributes.formula_expression'),
'description' => __('validation.attributes.description'),
'is_active' => __('validation.attributes.is_active'),
'execution_order' => __('validation.attributes.execution_order'),
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_active' => $this->boolean('is_active', true),
'execution_order' => $this->integer('execution_order', 0),
]);
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\ModelFormula;
use App\Http\Requests\Api\V1\PaginateRequest;
class IndexModelFormulaRequest extends PaginateRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return array_merge(parent::rules(), [
'search' => ['sometimes', 'string', 'max:255'],
'target_parameter' => ['sometimes', 'string', 'max:50'],
]);
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return array_merge(parent::attributes(), [
'search' => __('validation.attributes.search'),
'target_parameter' => __('validation.attributes.target_parameter'),
]);
}
}

View File

@@ -1,70 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\ModelFormula;
use Illuminate\Foundation\Http\FormRequest;
class UpdateModelFormulaRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:100'],
'target_parameter' => ['sometimes', 'string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'],
'expression' => ['sometimes', 'string', 'max:1000'],
'description' => ['nullable', 'string', 'max:500'],
'is_active' => ['sometimes', 'boolean'],
'execution_order' => ['sometimes', 'integer', 'min:0'],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'target_parameter.regex' => __('validation.model_formula.target_parameter_format'),
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'name' => __('validation.attributes.formula_name'),
'target_parameter' => __('validation.attributes.target_parameter'),
'expression' => __('validation.attributes.formula_expression'),
'description' => __('validation.attributes.description'),
'is_active' => __('validation.attributes.is_active'),
'execution_order' => __('validation.attributes.execution_order'),
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
if ($this->has('is_active')) {
$this->merge(['is_active' => $this->boolean('is_active')]);
}
if ($this->has('execution_order')) {
$this->merge(['execution_order' => $this->integer('execution_order')]);
}
}
}

View File

@@ -1,83 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\ModelParameter;
use Illuminate\Foundation\Http\FormRequest;
class CreateModelParameterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'],
'label' => ['required', 'string', 'max:100'],
'type' => ['required', 'string', 'in:INPUT,OUTPUT'],
'data_type' => ['required', 'string', 'in:INTEGER,DECIMAL,STRING,BOOLEAN'],
'unit' => ['nullable', 'string', 'max:20'],
'default_value' => ['nullable', 'string', 'max:255'],
'min_value' => ['nullable', 'numeric'],
'max_value' => ['nullable', 'numeric', 'gte:min_value'],
'enum_values' => ['nullable', 'array'],
'enum_values.*' => ['string', 'max:100'],
'validation_rules' => ['nullable', 'string', 'max:500'],
'description' => ['nullable', 'string', 'max:500'],
'is_required' => ['boolean'],
'display_order' => ['integer', 'min:0'],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.regex' => __('validation.model_parameter.name_format'),
'max_value.gte' => __('validation.model_parameter.max_value_gte_min'),
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'name' => __('validation.attributes.parameter_name'),
'label' => __('validation.attributes.parameter_label'),
'type' => __('validation.attributes.parameter_type'),
'data_type' => __('validation.attributes.data_type'),
'unit' => __('validation.attributes.unit'),
'default_value' => __('validation.attributes.default_value'),
'min_value' => __('validation.attributes.min_value'),
'max_value' => __('validation.attributes.max_value'),
'enum_values' => __('validation.attributes.enum_values'),
'validation_rules' => __('validation.attributes.validation_rules'),
'description' => __('validation.attributes.description'),
'is_required' => __('validation.attributes.is_required'),
'display_order' => __('validation.attributes.display_order'),
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
$this->merge([
'is_required' => $this->boolean('is_required'),
'display_order' => $this->integer('display_order', 0),
]);
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\ModelParameter;
use App\Http\Requests\Api\V1\PaginateRequest;
class IndexModelParameterRequest extends PaginateRequest
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return array_merge(parent::rules(), [
'search' => ['sometimes', 'string', 'max:255'],
'type' => ['sometimes', 'string', 'in:INPUT,OUTPUT'],
]);
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return array_merge(parent::attributes(), [
'search' => __('validation.attributes.search'),
'type' => __('validation.attributes.parameter_type'),
]);
}
}

View File

@@ -1,86 +0,0 @@
<?php
namespace App\Http\Requests\Api\V1\ModelParameter;
use Illuminate\Foundation\Http\FormRequest;
class UpdateModelParameterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => ['sometimes', 'string', 'max:50', 'regex:/^[a-zA-Z][a-zA-Z0-9_]*$/'],
'label' => ['sometimes', 'string', 'max:100'],
'type' => ['sometimes', 'string', 'in:INPUT,OUTPUT'],
'data_type' => ['sometimes', 'string', 'in:INTEGER,DECIMAL,STRING,BOOLEAN'],
'unit' => ['nullable', 'string', 'max:20'],
'default_value' => ['nullable', 'string', 'max:255'],
'min_value' => ['nullable', 'numeric'],
'max_value' => ['nullable', 'numeric', 'gte:min_value'],
'enum_values' => ['nullable', 'array'],
'enum_values.*' => ['string', 'max:100'],
'validation_rules' => ['nullable', 'string', 'max:500'],
'description' => ['nullable', 'string', 'max:500'],
'is_required' => ['sometimes', 'boolean'],
'display_order' => ['sometimes', 'integer', 'min:0'],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.regex' => __('validation.model_parameter.name_format'),
'max_value.gte' => __('validation.model_parameter.max_value_gte_min'),
];
}
/**
* Get custom attribute names for validator errors.
*/
public function attributes(): array
{
return [
'name' => __('validation.attributes.parameter_name'),
'label' => __('validation.attributes.parameter_label'),
'type' => __('validation.attributes.parameter_type'),
'data_type' => __('validation.attributes.data_type'),
'unit' => __('validation.attributes.unit'),
'default_value' => __('validation.attributes.default_value'),
'min_value' => __('validation.attributes.min_value'),
'max_value' => __('validation.attributes.max_value'),
'enum_values' => __('validation.attributes.enum_values'),
'validation_rules' => __('validation.attributes.validation_rules'),
'description' => __('validation.attributes.description'),
'is_required' => __('validation.attributes.is_required'),
'display_order' => __('validation.attributes.display_order'),
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
if ($this->has('is_required')) {
$this->merge(['is_required' => $this->boolean('is_required')]);
}
if ($this->has('display_order')) {
$this->merge(['display_order' => $this->integer('display_order')]);
}
}
}

View File

@@ -1,169 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Shared\Models\Products\BomConditionRule;
class BomConditionRuleRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'model_id' => 'required|integer|exists:models,id',
'name' => 'required|string|max:100',
'condition_expression' => 'required|string|max:1000',
'action' => 'required|string|in:' . implode(',', BomConditionRule::ACTIONS),
'target_items' => 'required|array|min:1',
'target_items.*.product_id' => 'nullable|integer|exists:products,id',
'target_items.*.material_id' => 'nullable|integer|exists:materials,id',
'target_items.*.quantity' => 'nullable|numeric|min:0',
'target_items.*.waste_rate' => 'nullable|numeric|min:0|max:100',
'target_items.*.unit' => 'nullable|string|max:20',
'target_items.*.memo' => 'nullable|string|max:200',
'priority' => 'nullable|integer|min:1',
'description' => 'nullable|string|max:500',
'is_active' => 'boolean',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'condition_expression.required' => __('error.condition_expression_required'),
'target_items.required' => __('error.target_items_required'),
'target_items.min' => __('error.target_items_required'),
'target_items.*.quantity.min' => __('error.quantity_must_be_positive'),
'target_items.*.waste_rate.max' => __('error.waste_rate_too_high'),
];
}
/**
* Configure the validator instance.
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
$this->validateRuleNameUnique($validator);
$this->validateConditionExpression($validator);
$this->validateTargetItems($validator);
});
}
/**
* 규칙명 중복 검증
*/
private function validateRuleNameUnique($validator): void
{
if (!$this->input('model_id') || !$this->input('name')) {
return;
}
$query = BomConditionRule::where('model_id', $this->input('model_id'))
->where('name', $this->input('name'));
// 수정 시 자기 자신 제외
if ($this->route('id')) {
$query->where('id', '!=', $this->route('id'));
}
if ($query->exists()) {
$validator->errors()->add('name', __('error.rule_name_duplicate'));
}
}
/**
* 조건식 검증
*/
private function validateConditionExpression($validator): void
{
if (!$this->input('condition_expression')) {
return;
}
$tempRule = new BomConditionRule([
'condition_expression' => $this->input('condition_expression'),
'model_id' => $this->input('model_id'),
]);
$conditionErrors = $tempRule->validateConditionExpression();
if (!empty($conditionErrors)) {
foreach ($conditionErrors as $error) {
$validator->errors()->add('condition_expression', $error);
}
}
}
/**
* 대상 아이템 검증
*/
private function validateTargetItems($validator): void
{
$targetItems = $this->input('target_items', []);
$action = $this->input('action');
foreach ($targetItems as $index => $item) {
// 제품 또는 자재 참조 필수
if (empty($item['product_id']) && empty($item['material_id'])) {
$validator->errors()->add(
"target_items.{$index}",
__('error.target_item_missing_reference')
);
}
// 제품과 자재 동시 참조 불가
if (!empty($item['product_id']) && !empty($item['material_id'])) {
$validator->errors()->add(
"target_items.{$index}",
__('error.target_item_multiple_reference')
);
}
// REPLACE 액션의 경우 replace_from 필수
if ($action === BomConditionRule::ACTION_REPLACE && empty($item['replace_from'])) {
$validator->errors()->add(
"target_items.{$index}.replace_from",
__('error.replace_from_required')
);
}
// replace_from 검증
if (!empty($item['replace_from'])) {
if (empty($item['replace_from']['product_id']) && empty($item['replace_from']['material_id'])) {
$validator->errors()->add(
"target_items.{$index}.replace_from",
__('error.replace_from_missing_reference')
);
}
}
}
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// JSON 문자열인 경우 배열로 변환
if ($this->has('target_items') && is_string($this->input('target_items'))) {
$this->merge([
'target_items' => json_decode($this->input('target_items'), true) ?? []
]);
}
}
}

View File

@@ -1,81 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class BomResolveRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'model_id' => 'required|integer|exists:models,id',
'parameters' => 'required|array',
'parameters.*' => 'required',
'preview_only' => 'boolean',
'use_cache' => 'boolean',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'model_id.required' => __('error.model_id_required'),
'model_id.exists' => __('error.model_not_found'),
'parameters.required' => __('error.parameters_required'),
'parameters.array' => __('error.parameters_must_be_array'),
];
}
/**
* Configure the validator instance.
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
$this->validateParameters($validator);
});
}
/**
* 매개변수 검증
*/
private function validateParameters($validator): void
{
if (!$this->input('model_id') || !$this->input('parameters')) {
return;
}
try {
$parameterService = new \App\Services\ModelParameterService();
$errors = $parameterService->validateParameterValues(
$this->input('model_id'),
$this->input('parameters')
);
if (!empty($errors)) {
foreach ($errors as $paramName => $paramErrors) {
foreach ($paramErrors as $error) {
$validator->errors()->add("parameters.{$paramName}", $error);
}
}
}
} catch (\Throwable $e) {
$validator->errors()->add('parameters', __('error.parameter_validation_failed'));
}
}
}

View File

@@ -1,214 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Shared\Models\Products\ModelFormula;
class ModelFormulaRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'model_id' => 'required|integer|exists:models,id',
'name' => 'required|string|max:50|regex:/^[a-zA-Z][a-zA-Z0-9_]*$/',
'label' => 'required|string|max:100',
'expression' => 'required|string|max:1000',
'unit' => 'nullable|string|max:20',
'order' => 'nullable|integer|min:1',
'description' => 'nullable|string|max:500',
'is_active' => 'boolean',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.regex' => __('error.formula_name_format'),
'expression.required' => __('error.formula_expression_required'),
];
}
/**
* Configure the validator instance.
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
$this->validateFormulaNameUnique($validator);
$this->validateFormulaExpression($validator);
$this->validateDependencies($validator);
});
}
/**
* 공식명 중복 검증
*/
private function validateFormulaNameUnique($validator): void
{
if (!$this->input('model_id') || !$this->input('name')) {
return;
}
$query = ModelFormula::where('model_id', $this->input('model_id'))
->where('name', $this->input('name'));
// 수정 시 자기 자신 제외
if ($this->route('id')) {
$query->where('id', '!=', $this->route('id'));
}
if ($query->exists()) {
$validator->errors()->add('name', __('error.formula_name_duplicate'));
}
// 매개변수명과 중복 검증
$parameterExists = \Shared\Models\Products\ModelParameter::where('model_id', $this->input('model_id'))
->where('name', $this->input('name'))
->exists();
if ($parameterExists) {
$validator->errors()->add('name', __('error.formula_name_conflicts_with_parameter'));
}
}
/**
* 공식 표현식 검증
*/
private function validateFormulaExpression($validator): void
{
if (!$this->input('expression')) {
return;
}
$tempFormula = new ModelFormula([
'expression' => $this->input('expression'),
'model_id' => $this->input('model_id'),
]);
$expressionErrors = $tempFormula->validateExpression();
if (!empty($expressionErrors)) {
foreach ($expressionErrors as $error) {
$validator->errors()->add('expression', $error);
}
}
}
/**
* 의존성 검증
*/
private function validateDependencies($validator): void
{
if (!$this->input('model_id') || !$this->input('expression')) {
return;
}
$tempFormula = new ModelFormula([
'expression' => $this->input('expression'),
'model_id' => $this->input('model_id'),
]);
$dependencies = $tempFormula->extractVariables();
if (empty($dependencies)) {
return;
}
// 매개변수 목록 가져오기
$parameters = \Shared\Models\Products\ModelParameter::where('model_id', $this->input('model_id'))
->active()
->pluck('name')
->toArray();
// 기존 공식 목록 가져오기 (자기 자신 제외)
$formulasQuery = ModelFormula::where('model_id', $this->input('model_id'))
->active();
if ($this->route('id')) {
$formulasQuery->where('id', '!=', $this->route('id'));
}
$formulas = $formulasQuery->pluck('name')->toArray();
$validNames = array_merge($parameters, $formulas);
foreach ($dependencies as $dep) {
if (!in_array($dep, $validNames)) {
$validator->errors()->add('expression', __('error.dependency_not_found', ['name' => $dep]));
}
}
// 순환 의존성 검증
$this->validateCircularDependency($validator, $dependencies);
}
/**
* 순환 의존성 검증
*/
private function validateCircularDependency($validator, array $dependencies): void
{
if (!$this->input('model_id') || !$this->input('name')) {
return;
}
$allFormulasQuery = ModelFormula::where('model_id', $this->input('model_id'))
->active();
if ($this->route('id')) {
$allFormulasQuery->where('id', '!=', $this->route('id'));
}
$allFormulas = $allFormulasQuery->get();
// 현재 공식을 임시로 추가
$tempFormula = new ModelFormula([
'name' => $this->input('name'),
'dependencies' => $dependencies,
]);
$allFormulas->push($tempFormula);
if ($this->hasCircularDependency($tempFormula, $allFormulas->toArray())) {
$validator->errors()->add('expression', __('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;
}
}

View File

@@ -1,127 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Shared\Models\Products\ModelParameter;
class ModelParameterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'model_id' => 'required|integer|exists:models,id',
'name' => 'required|string|max:50|regex:/^[a-zA-Z][a-zA-Z0-9_]*$/',
'label' => 'required|string|max:100',
'type' => 'required|string|in:' . implode(',', ModelParameter::TYPES),
'unit' => 'nullable|string|max:20',
'validation_rules' => 'nullable|array',
'options' => 'nullable|array',
'default_value' => 'nullable',
'order' => 'nullable|integer|min:1',
'description' => 'nullable|string|max:500',
'is_required' => 'boolean',
'is_active' => 'boolean',
];
// 타입별 세부 검증
if ($this->input('type') === ModelParameter::TYPE_NUMBER) {
$rules['validation_rules.min'] = 'nullable|numeric';
$rules['validation_rules.max'] = 'nullable|numeric|gte:validation_rules.min';
$rules['default_value'] = 'nullable|numeric';
}
if ($this->input('type') === ModelParameter::TYPE_SELECT) {
$rules['options'] = 'required|array|min:1';
$rules['options.*'] = 'required|string|max:100';
$rules['default_value'] = 'nullable|string|in_array:options';
}
if ($this->input('type') === ModelParameter::TYPE_BOOLEAN) {
$rules['default_value'] = 'nullable|boolean';
}
if ($this->input('type') === ModelParameter::TYPE_TEXT) {
$rules['validation_rules.max_length'] = 'nullable|integer|min:1|max:1000';
$rules['validation_rules.pattern'] = 'nullable|string|max:200';
$rules['default_value'] = 'nullable|string';
}
return $rules;
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.regex' => __('error.parameter_name_format'),
'validation_rules.max.gte' => __('error.max_must_be_greater_than_min'),
'options.required' => __('error.select_type_requires_options'),
'options.min' => __('error.select_type_requires_options'),
'default_value.in_array' => __('error.default_value_not_in_options'),
];
}
/**
* Configure the validator instance.
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
// 추가 검증 로직
$this->validateParameterNameUnique($validator);
$this->validateValidationRules($validator);
});
}
/**
* 매개변수명 중복 검증
*/
private function validateParameterNameUnique($validator): void
{
if (!$this->input('model_id') || !$this->input('name')) {
return;
}
$query = ModelParameter::where('model_id', $this->input('model_id'))
->where('name', $this->input('name'));
// 수정 시 자기 자신 제외
if ($this->route('id')) {
$query->where('id', '!=', $this->route('id'));
}
if ($query->exists()) {
$validator->errors()->add('name', __('error.parameter_name_duplicate'));
}
}
/**
* 검증 규칙 유효성 검증
*/
private function validateValidationRules($validator): void
{
$type = $this->input('type');
$validationRules = $this->input('validation_rules', []);
if ($type === ModelParameter::TYPE_TEXT && isset($validationRules['pattern'])) {
// 정규식 패턴 검증
if (@preg_match($validationRules['pattern'], '') === false) {
$validator->errors()->add('validation_rules.pattern', __('error.invalid_regex_pattern'));
}
}
}
}

View File

@@ -1,138 +0,0 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProductFromModelRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'model_id' => 'required|integer|exists:models,id',
'parameters' => 'required|array',
'parameters.*' => 'required',
'product_data' => 'nullable|array',
'product_data.name' => 'nullable|string|max:200',
'product_data.code' => 'nullable|string|max:100|unique:products,code',
'product_data.description' => 'nullable|string|max:1000',
'product_data.category_id' => 'nullable|integer|exists:categories,id',
'product_data.memo' => 'nullable|string|max:500',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'model_id.required' => __('error.model_id_required'),
'model_id.exists' => __('error.model_not_found'),
'parameters.required' => __('error.parameters_required'),
'parameters.array' => __('error.parameters_must_be_array'),
'product_data.code.unique' => __('error.product_code_duplicate'),
];
}
/**
* Configure the validator instance.
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
$this->validateParameters($validator);
$this->validateProductData($validator);
});
}
/**
* 매개변수 검증
*/
private function validateParameters($validator): void
{
if (!$this->input('model_id') || !$this->input('parameters')) {
return;
}
try {
$parameterService = new \App\Services\ModelParameterService();
$errors = $parameterService->validateParameterValues(
$this->input('model_id'),
$this->input('parameters')
);
if (!empty($errors)) {
foreach ($errors as $paramName => $paramErrors) {
foreach ($paramErrors as $error) {
$validator->errors()->add("parameters.{$paramName}", $error);
}
}
}
} catch (\Throwable $e) {
$validator->errors()->add('parameters', __('error.parameter_validation_failed'));
}
}
/**
* 제품 데이터 검증
*/
private function validateProductData($validator): void
{
$productData = $this->input('product_data', []);
// 제품 코드 중복 검증 (수정 시 제외)
if (!empty($productData['code'])) {
$query = \Shared\Models\Products\Product::where('code', $productData['code']);
if ($this->route('id')) {
$query->where('id', '!=', $this->route('id'));
}
if ($query->exists()) {
$validator->errors()->add('product_data.code', __('error.product_code_duplicate'));
}
}
// 카테고리 존재 확인
if (!empty($productData['category_id'])) {
$categoryExists = \Shared\Models\Products\Category::where('id', $productData['category_id'])
->where('is_active', true)
->exists();
if (!$categoryExists) {
$validator->errors()->add('product_data.category_id', __('error.category_not_found'));
}
}
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// JSON 문자열인 경우 배열로 변환
if ($this->has('parameters') && is_string($this->input('parameters'))) {
$this->merge([
'parameters' => json_decode($this->input('parameters'), true) ?? []
]);
}
if ($this->has('product_data') && is_string($this->input('product_data'))) {
$this->merge([
'product_data' => json_decode($this->input('product_data'), true) ?? []
]);
}
}
}