488 lines
17 KiB
PHP
488 lines
17 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services\ModelSet;
|
||
|
|
|
||
|
|
use App\Models\Commons\Category;
|
||
|
|
use App\Models\Commons\CategoryField;
|
||
|
|
use App\Models\Design\Model;
|
||
|
|
use App\Models\Design\ModelVersion;
|
||
|
|
use App\Models\Design\BomTemplate;
|
||
|
|
use App\Models\Products\Product;
|
||
|
|
use App\Services\Service;
|
||
|
|
use App\Services\Calculation\CalculationEngine;
|
||
|
|
use Illuminate\Support\Collection;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
class ModelSetService extends Service
|
||
|
|
{
|
||
|
|
protected CalculationEngine $calculationEngine;
|
||
|
|
|
||
|
|
public function __construct(CalculationEngine $calculationEngine)
|
||
|
|
{
|
||
|
|
parent::__construct();
|
||
|
|
$this->calculationEngine = $calculationEngine;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모델셋 목록 조회 (카테고리 기반)
|
||
|
|
*/
|
||
|
|
public function getModelSets(array $filters = []): Collection
|
||
|
|
{
|
||
|
|
$query = Category::with(['fields', 'children'])
|
||
|
|
->where('tenant_id', $this->tenantId())
|
||
|
|
->where('code_group', 'estimate')
|
||
|
|
->where('level', '>=', 2); // 루트 카테고리 제외
|
||
|
|
|
||
|
|
if (!empty($filters['category_type'])) {
|
||
|
|
$query->where('code', $filters['category_type']);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!empty($filters['is_active'])) {
|
||
|
|
$query->where('is_active', $filters['is_active']);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $query->orderBy('sort_order')->get();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모델셋 상세 조회
|
||
|
|
*/
|
||
|
|
public function getModelSetDetail($categoryId): array
|
||
|
|
{
|
||
|
|
$category = Category::with(['fields', 'parent', 'children'])
|
||
|
|
->where('tenant_id', $this->tenantId())
|
||
|
|
->findOrFail($categoryId);
|
||
|
|
|
||
|
|
// 해당 카테고리의 제품들
|
||
|
|
$products = Product::where('tenant_id', $this->tenantId())
|
||
|
|
->where('category_id', $categoryId)
|
||
|
|
->with(['components'])
|
||
|
|
->get();
|
||
|
|
|
||
|
|
// 해당 카테고리의 모델 및 BOM 템플릿들
|
||
|
|
$models = $this->getRelatedModels($categoryId);
|
||
|
|
|
||
|
|
return [
|
||
|
|
'category' => $category,
|
||
|
|
'products' => $products,
|
||
|
|
'models' => $models,
|
||
|
|
'field_schema' => $this->generateFieldSchema($category->fields),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 새로운 모델셋 생성
|
||
|
|
*/
|
||
|
|
public function createModelSet(array $data): array
|
||
|
|
{
|
||
|
|
return DB::transaction(function () use ($data) {
|
||
|
|
// 1. 카테고리 생성
|
||
|
|
$category = Category::create([
|
||
|
|
'tenant_id' => $this->tenantId(),
|
||
|
|
'parent_id' => $data['parent_id'] ?? null,
|
||
|
|
'code_group' => 'estimate',
|
||
|
|
'code' => $data['code'],
|
||
|
|
'name' => $data['name'],
|
||
|
|
'description' => $data['description'] ?? '',
|
||
|
|
'level' => $data['level'] ?? 2,
|
||
|
|
'sort_order' => $data['sort_order'] ?? 999,
|
||
|
|
'profile_code' => $data['profile_code'] ?? 'custom_category',
|
||
|
|
'is_active' => $data['is_active'] ?? true,
|
||
|
|
'created_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 2. 동적 필드 생성
|
||
|
|
if (!empty($data['fields'])) {
|
||
|
|
foreach ($data['fields'] as $fieldData) {
|
||
|
|
CategoryField::create([
|
||
|
|
'tenant_id' => $this->tenantId(),
|
||
|
|
'category_id' => $category->id,
|
||
|
|
'field_key' => $fieldData['key'],
|
||
|
|
'field_name' => $fieldData['name'],
|
||
|
|
'field_type' => $fieldData['type'],
|
||
|
|
'is_required' => $fieldData['required'] ?? false,
|
||
|
|
'sort_order' => $fieldData['order'] ?? 999,
|
||
|
|
'default_value' => $fieldData['default'] ?? null,
|
||
|
|
'options' => $fieldData['options'] ?? null,
|
||
|
|
'description' => $fieldData['description'] ?? '',
|
||
|
|
'created_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. 모델 및 BOM 템플릿 생성 (선택사항)
|
||
|
|
if (!empty($data['create_model'])) {
|
||
|
|
$this->createDefaultModel($category, $data['model_data'] ?? []);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->getModelSetDetail($category->id);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모델셋 수정
|
||
|
|
*/
|
||
|
|
public function updateModelSet($categoryId, array $data): array
|
||
|
|
{
|
||
|
|
return DB::transaction(function () use ($categoryId, $data) {
|
||
|
|
$category = Category::where('tenant_id', $this->tenantId())
|
||
|
|
->findOrFail($categoryId);
|
||
|
|
|
||
|
|
// 카테고리 정보 업데이트
|
||
|
|
$category->update([
|
||
|
|
'name' => $data['name'] ?? $category->name,
|
||
|
|
'description' => $data['description'] ?? $category->description,
|
||
|
|
'sort_order' => $data['sort_order'] ?? $category->sort_order,
|
||
|
|
'is_active' => $data['is_active'] ?? $category->is_active,
|
||
|
|
'updated_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 필드 업데이트 (기존 필드 삭제 후 재생성)
|
||
|
|
if (isset($data['fields'])) {
|
||
|
|
CategoryField::where('category_id', $categoryId)->delete();
|
||
|
|
|
||
|
|
foreach ($data['fields'] as $fieldData) {
|
||
|
|
CategoryField::create([
|
||
|
|
'tenant_id' => $this->tenantId(),
|
||
|
|
'category_id' => $categoryId,
|
||
|
|
'field_key' => $fieldData['key'],
|
||
|
|
'field_name' => $fieldData['name'],
|
||
|
|
'field_type' => $fieldData['type'],
|
||
|
|
'is_required' => $fieldData['required'] ?? false,
|
||
|
|
'sort_order' => $fieldData['order'] ?? 999,
|
||
|
|
'default_value' => $fieldData['default'] ?? null,
|
||
|
|
'options' => $fieldData['options'] ?? null,
|
||
|
|
'description' => $fieldData['description'] ?? '',
|
||
|
|
'created_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->getModelSetDetail($categoryId);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모델셋 삭제
|
||
|
|
*/
|
||
|
|
public function deleteModelSet($categoryId): void
|
||
|
|
{
|
||
|
|
DB::transaction(function () use ($categoryId) {
|
||
|
|
$category = Category::where('tenant_id', $this->tenantId())
|
||
|
|
->findOrFail($categoryId);
|
||
|
|
|
||
|
|
// 연관된 데이터들 확인
|
||
|
|
$hasProducts = Product::where('category_id', $categoryId)->exists();
|
||
|
|
$hasChildren = Category::where('parent_id', $categoryId)->exists();
|
||
|
|
|
||
|
|
if ($hasProducts || $hasChildren) {
|
||
|
|
throw new \Exception(__('error.modelset.has_dependencies'));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 필드 삭제
|
||
|
|
CategoryField::where('category_id', $categoryId)->delete();
|
||
|
|
|
||
|
|
// 카테고리 삭제 (소프트 삭제)
|
||
|
|
$category->update(['deleted_by' => $this->apiUserId()]);
|
||
|
|
$category->delete();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모델셋 복제
|
||
|
|
*/
|
||
|
|
public function cloneModelSet($categoryId, array $data): array
|
||
|
|
{
|
||
|
|
return DB::transaction(function () use ($categoryId, $data) {
|
||
|
|
$originalCategory = Category::with('fields')
|
||
|
|
->where('tenant_id', $this->tenantId())
|
||
|
|
->findOrFail($categoryId);
|
||
|
|
|
||
|
|
// 새로운 카테고리 생성
|
||
|
|
$newCategory = Category::create([
|
||
|
|
'tenant_id' => $this->tenantId(),
|
||
|
|
'parent_id' => $originalCategory->parent_id,
|
||
|
|
'code_group' => $originalCategory->code_group,
|
||
|
|
'code' => $data['code'],
|
||
|
|
'name' => $data['name'],
|
||
|
|
'description' => $data['description'] ?? $originalCategory->description,
|
||
|
|
'level' => $originalCategory->level,
|
||
|
|
'sort_order' => $data['sort_order'] ?? 999,
|
||
|
|
'profile_code' => $originalCategory->profile_code,
|
||
|
|
'is_active' => $data['is_active'] ?? true,
|
||
|
|
'created_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
// 필드 복제
|
||
|
|
foreach ($originalCategory->fields as $field) {
|
||
|
|
CategoryField::create([
|
||
|
|
'tenant_id' => $this->tenantId(),
|
||
|
|
'category_id' => $newCategory->id,
|
||
|
|
'field_key' => $field->field_key,
|
||
|
|
'field_name' => $field->field_name,
|
||
|
|
'field_type' => $field->field_type,
|
||
|
|
'is_required' => $field->is_required,
|
||
|
|
'sort_order' => $field->sort_order,
|
||
|
|
'default_value' => $field->default_value,
|
||
|
|
'options' => $field->options,
|
||
|
|
'description' => $field->description,
|
||
|
|
'created_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->getModelSetDetail($newCategory->id);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모델셋의 카테고리 필드 구조 조회
|
||
|
|
*/
|
||
|
|
public function getModelSetCategoryFields($categoryId): array
|
||
|
|
{
|
||
|
|
$category = Category::with('fields')
|
||
|
|
->where('tenant_id', $this->tenantId())
|
||
|
|
->findOrFail($categoryId);
|
||
|
|
|
||
|
|
return $this->generateFieldSchema($category->fields);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모델셋의 BOM 템플릿 목록
|
||
|
|
*/
|
||
|
|
public function getModelSetBomTemplates($categoryId): Collection
|
||
|
|
{
|
||
|
|
// 해당 카테고리와 연관된 모델들의 BOM 템플릿들
|
||
|
|
$models = $this->getRelatedModels($categoryId);
|
||
|
|
|
||
|
|
$bomTemplates = collect();
|
||
|
|
foreach ($models as $model) {
|
||
|
|
foreach ($model['versions'] as $version) {
|
||
|
|
$bomTemplates = $bomTemplates->merge($version['bom_templates']);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $bomTemplates;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 견적 파라미터 조회 (동적 필드 기반)
|
||
|
|
*/
|
||
|
|
public function getEstimateParameters($categoryId, array $filters = []): array
|
||
|
|
{
|
||
|
|
$category = Category::with('fields')
|
||
|
|
->where('tenant_id', $this->tenantId())
|
||
|
|
->findOrFail($categoryId);
|
||
|
|
|
||
|
|
// 입력 파라미터 (사용자가 입력해야 하는 필드들)
|
||
|
|
$inputFields = $category->fields
|
||
|
|
->filter(function ($field) {
|
||
|
|
return in_array($field->field_key, [
|
||
|
|
'open_width', 'open_height', 'quantity',
|
||
|
|
'model_name', 'guide_rail_type', 'shutter_box'
|
||
|
|
]);
|
||
|
|
})
|
||
|
|
->map(function ($field) {
|
||
|
|
return [
|
||
|
|
'key' => $field->field_key,
|
||
|
|
'name' => $field->field_name,
|
||
|
|
'type' => $field->field_type,
|
||
|
|
'required' => $field->is_required,
|
||
|
|
'default' => $field->default_value,
|
||
|
|
'options' => $field->options,
|
||
|
|
'description' => $field->description,
|
||
|
|
];
|
||
|
|
});
|
||
|
|
|
||
|
|
// 계산 결과 필드 (자동으로 계산되는 필드들)
|
||
|
|
$calculatedFields = $category->fields
|
||
|
|
->filter(function ($field) {
|
||
|
|
return in_array($field->field_key, [
|
||
|
|
'make_width', 'make_height', 'calculated_weight',
|
||
|
|
'calculated_area', 'motor_bracket_size', 'motor_capacity'
|
||
|
|
]);
|
||
|
|
})
|
||
|
|
->map(function ($field) {
|
||
|
|
return [
|
||
|
|
'key' => $field->field_key,
|
||
|
|
'name' => $field->field_name,
|
||
|
|
'type' => $field->field_type,
|
||
|
|
'description' => $field->description,
|
||
|
|
];
|
||
|
|
});
|
||
|
|
|
||
|
|
return [
|
||
|
|
'category' => [
|
||
|
|
'id' => $category->id,
|
||
|
|
'name' => $category->name,
|
||
|
|
'code' => $category->code,
|
||
|
|
],
|
||
|
|
'input_fields' => $inputFields->values(),
|
||
|
|
'calculated_fields' => $calculatedFields->values(),
|
||
|
|
'calculation_schema' => $this->getCalculationSchema($category->code),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 모델셋 기반 BOM 계산
|
||
|
|
*/
|
||
|
|
public function calculateModelSetBom($categoryId, array $parameters): array
|
||
|
|
{
|
||
|
|
$category = Category::where('tenant_id', $this->tenantId())
|
||
|
|
->findOrFail($categoryId);
|
||
|
|
|
||
|
|
// BOM 템플릿 찾기 (기본 템플릿 사용)
|
||
|
|
$bomTemplate = $this->findDefaultBomTemplate($categoryId, $parameters);
|
||
|
|
|
||
|
|
if (!$bomTemplate) {
|
||
|
|
throw new \Exception(__('error.bom_template.not_found'));
|
||
|
|
}
|
||
|
|
|
||
|
|
// 기존 BOM 계산 엔진 사용
|
||
|
|
return $this->calculationEngine->calculateBOM(
|
||
|
|
$bomTemplate->id,
|
||
|
|
$parameters,
|
||
|
|
$this->getCompanyName($category)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 카테고리와 연관된 모델들 조회
|
||
|
|
*/
|
||
|
|
protected function getRelatedModels($categoryId): Collection
|
||
|
|
{
|
||
|
|
// 카테고리 코드 기반으로 모델 찾기
|
||
|
|
$category = Category::findOrFail($categoryId);
|
||
|
|
|
||
|
|
return Model::with(['versions.bomTemplates'])
|
||
|
|
->where('tenant_id', $this->tenantId())
|
||
|
|
->where('code', 'like', $category->code . '%')
|
||
|
|
->get()
|
||
|
|
->map(function ($model) {
|
||
|
|
return [
|
||
|
|
'id' => $model->id,
|
||
|
|
'code' => $model->code,
|
||
|
|
'name' => $model->name,
|
||
|
|
'versions' => $model->versions->map(function ($version) {
|
||
|
|
return [
|
||
|
|
'id' => $version->id,
|
||
|
|
'version_no' => $version->version_no,
|
||
|
|
'status' => $version->status,
|
||
|
|
'bom_templates' => $version->bomTemplates,
|
||
|
|
];
|
||
|
|
}),
|
||
|
|
];
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 필드 스키마 생성
|
||
|
|
*/
|
||
|
|
protected function generateFieldSchema(Collection $fields): array
|
||
|
|
{
|
||
|
|
return $fields->map(function ($field) {
|
||
|
|
return [
|
||
|
|
'key' => $field->field_key,
|
||
|
|
'name' => $field->field_name,
|
||
|
|
'type' => $field->field_type,
|
||
|
|
'required' => $field->is_required,
|
||
|
|
'order' => $field->sort_order,
|
||
|
|
'default' => $field->default_value,
|
||
|
|
'options' => $field->options,
|
||
|
|
'description' => $field->description,
|
||
|
|
];
|
||
|
|
})->sortBy('order')->values()->toArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 기본 모델 생성
|
||
|
|
*/
|
||
|
|
protected function createDefaultModel(Category $category, array $modelData): void
|
||
|
|
{
|
||
|
|
$model = Model::create([
|
||
|
|
'tenant_id' => $this->tenantId(),
|
||
|
|
'code' => $modelData['code'] ?? $category->code . '_MODEL',
|
||
|
|
'name' => $modelData['name'] ?? $category->name . ' 기본 모델',
|
||
|
|
'description' => $modelData['description'] ?? '',
|
||
|
|
'status' => 'DRAFT',
|
||
|
|
'created_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
$version = ModelVersion::create([
|
||
|
|
'tenant_id' => $this->tenantId(),
|
||
|
|
'model_id' => $model->id,
|
||
|
|
'version_no' => 'v1.0',
|
||
|
|
'status' => 'DRAFT',
|
||
|
|
'created_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
|
||
|
|
BomTemplate::create([
|
||
|
|
'tenant_id' => $this->tenantId(),
|
||
|
|
'model_version_id' => $version->id,
|
||
|
|
'name' => $category->name . ' 기본 BOM',
|
||
|
|
'company_type' => $this->getCompanyName($category),
|
||
|
|
'formula_version' => 'v1.0',
|
||
|
|
'calculation_schema' => $this->getDefaultCalculationSchema($category),
|
||
|
|
'created_by' => $this->apiUserId(),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 계산 스키마 조회
|
||
|
|
*/
|
||
|
|
protected function getCalculationSchema(string $categoryCode): array
|
||
|
|
{
|
||
|
|
if ($categoryCode === 'screen_product') {
|
||
|
|
return [
|
||
|
|
'size_calculation' => 'kyungdong_screen_size',
|
||
|
|
'weight_calculation' => 'screen_weight_calculation',
|
||
|
|
'bracket_calculation' => 'motor_bracket_size',
|
||
|
|
];
|
||
|
|
} elseif ($categoryCode === 'steel_product') {
|
||
|
|
return [
|
||
|
|
'size_calculation' => 'kyungdong_steel_size',
|
||
|
|
'bracket_calculation' => 'motor_bracket_size',
|
||
|
|
'round_bar_calculation' => 'round_bar_quantity',
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 기본 BOM 템플릿 찾기
|
||
|
|
*/
|
||
|
|
protected function findDefaultBomTemplate($categoryId, array $parameters): ?BomTemplate
|
||
|
|
{
|
||
|
|
$models = $this->getRelatedModels($categoryId);
|
||
|
|
|
||
|
|
foreach ($models as $model) {
|
||
|
|
foreach ($model['versions'] as $version) {
|
||
|
|
if ($version['status'] === 'RELEASED' && $version['bom_templates']->isNotEmpty()) {
|
||
|
|
return $version['bom_templates']->first();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 업체명 조회
|
||
|
|
*/
|
||
|
|
protected function getCompanyName(Category $category): string
|
||
|
|
{
|
||
|
|
// 테넌트 정보에서 업체명 조회하거나 기본값 사용
|
||
|
|
return '경동기업'; // 임시 하드코딩
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 기본 계산 스키마 생성
|
||
|
|
*/
|
||
|
|
protected function getDefaultCalculationSchema(Category $category): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'calculation_type' => $category->code,
|
||
|
|
'formulas' => $this->getCalculationSchema($category->code),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|