bomResolverService = new BomResolverService(); } /** * 모델에서 제품 생성 */ public function createProductFromModel(int $modelId, array $inputParameters, array $productData = []): Product { $this->validateModelAccess($modelId); return DB::transaction(function () use ($modelId, $inputParameters, $productData) { // 1. BOM 해석 $resolvedBom = $this->bomResolverService->resolveBom($modelId, $inputParameters); // 2. 제품 기본 정보 생성 $product = $this->createProduct($modelId, $inputParameters, $resolvedBom, $productData); // 3. BOM 스냅샷 저장 $this->saveBomSnapshot($product->id, $resolvedBom); // 4. 매개변수 및 계산값 저장 $this->saveParameterSnapshot($product->id, $resolvedBom); return $product->fresh(['components']); }); } /** * 제품 코드 생성 미리보기 */ public function previewProductCode(int $modelId, array $inputParameters): string { $this->validateModelAccess($modelId); $model = ModelMaster::where('tenant_id', $this->tenantId()) ->findOrFail($modelId); // BOM 해석하여 계산값 가져오기 $resolvedBom = $this->bomResolverService->resolveBom($modelId, $inputParameters); return $this->generateProductCode($model, $resolvedBom['calculated_values']); } /** * 제품 사양서 생성 */ public function generateProductSpecification(int $modelId, array $inputParameters): array { $this->validateModelAccess($modelId); $model = ModelMaster::where('tenant_id', $this->tenantId()) ->findOrFail($modelId); $resolvedBom = $this->bomResolverService->resolveBom($modelId, $inputParameters); return [ 'model' => [ 'id' => $model->id, 'code' => $model->code, 'name' => $model->name, 'version' => $model->current_version, ], 'parameters' => $this->formatParameters($resolvedBom['input_parameters']), 'calculated_values' => $this->formatCalculatedValues($resolvedBom['calculated_values']), 'bom_summary' => $resolvedBom['summary'], 'specifications' => $this->generateSpecifications($model, $resolvedBom), 'generated_at' => now()->toISOString(), ]; } /** * 대량 제품 생성 */ public function createProductsBatch(int $modelId, array $parameterSets, array $baseProductData = []): array { $this->validateModelAccess($modelId); $results = []; DB::transaction(function () use ($modelId, $parameterSets, $baseProductData, &$results) { foreach ($parameterSets as $index => $parameters) { try { $productData = array_merge($baseProductData, [ 'name' => ($baseProductData['name'] ?? '') . ' #' . ($index + 1), ]); $product = $this->createProductFromModel($modelId, $parameters, $productData); $results[$index] = [ 'success' => true, 'product_id' => $product->id, 'product_code' => $product->code, 'parameters' => $parameters, ]; } catch (\Throwable $e) { $results[$index] = [ 'success' => false, 'error' => $e->getMessage(), 'parameters' => $parameters, ]; } } }); return $results; } /** * 기존 제품의 BOM 업데이트 */ public function updateProductBom(int $productId, array $newParameters): Product { $product = Product::where('tenant_id', $this->tenantId()) ->findOrFail($productId); if (!$product->source_model_id) { throw new \InvalidArgumentException(__('error.product_not_from_model')); } return DB::transaction(function () use ($product, $newParameters) { // 1. 새 BOM 해석 $resolvedBom = $this->bomResolverService->resolveBom($product->source_model_id, $newParameters); // 2. 기존 BOM 컴포넌트 삭제 ProductComponent::where('product_id', $product->id)->delete(); // 3. 새 BOM 스냅샷 저장 $this->saveBomSnapshot($product->id, $resolvedBom); // 4. 매개변수 업데이트 $this->saveParameterSnapshot($product->id, $resolvedBom, true); // 5. 제품 정보 업데이트 $product->update([ 'updated_by' => $this->apiUserId(), ]); return $product->fresh(['components']); }); } /** * 제품 버전 생성 (기존 제품의 파라미터 변경) */ public function createProductVersion(int $productId, array $newParameters, string $versionNote = ''): Product { $originalProduct = Product::where('tenant_id', $this->tenantId()) ->findOrFail($productId); if (!$originalProduct->source_model_id) { throw new \InvalidArgumentException(__('error.product_not_from_model')); } return DB::transaction(function () use ($originalProduct, $newParameters, $versionNote) { // 원본 제품 복제 $productData = $originalProduct->toArray(); unset($productData['id'], $productData['created_at'], $productData['updated_at'], $productData['deleted_at']); // 버전 정보 추가 $productData['name'] = $originalProduct->name . ' (v' . now()->format('YmdHis') . ')'; $productData['parent_product_id'] = $originalProduct->id; $productData['version_note'] = $versionNote; $newProduct = $this->createProductFromModel( $originalProduct->source_model_id, $newParameters, $productData ); return $newProduct; }); } /** * 제품 생성 */ private function createProduct(int $modelId, array $inputParameters, array $resolvedBom, array $productData): Product { $model = ModelMaster::where('tenant_id', $this->tenantId()) ->findOrFail($modelId); // 기본 제품 데이터 설정 $defaultData = [ 'tenant_id' => $this->tenantId(), 'code' => $this->generateProductCode($model, $resolvedBom['calculated_values']), 'name' => $productData['name'] ?? $this->generateProductName($model, $resolvedBom['calculated_values']), 'type' => 'PRODUCT', 'source_model_id' => $modelId, 'model_version' => $model->current_version, 'is_active' => true, 'created_by' => $this->apiUserId(), ]; $finalData = array_merge($defaultData, $productData); return Product::create($finalData); } /** * BOM 스냅샷 저장 */ private function saveBomSnapshot(int $productId, array $resolvedBom): void { foreach ($resolvedBom['bom_items'] as $index => $item) { ProductComponent::create([ 'tenant_id' => $this->tenantId(), 'product_id' => $productId, 'ref_type' => $item['ref_type'] ?? ($item['material_id'] ? 'MATERIAL' : 'PRODUCT'), 'product_id_ref' => $item['product_id'] ?? null, 'material_id' => $item['material_id'] ?? null, 'quantity' => $item['total_quantity'], 'base_quantity' => $item['calculated_quantity'], 'waste_rate' => $item['waste_rate'] ?? 0, 'unit' => $item['unit'] ?? 'ea', 'memo' => $item['memo'] ?? null, 'order' => $item['order'] ?? $index + 1, 'created_by' => $this->apiUserId(), ]); } } /** * 매개변수 스냅샷 저장 */ private function saveParameterSnapshot(int $productId, array $resolvedBom, bool $update = false): void { $parameterData = [ 'input_parameters' => $resolvedBom['input_parameters'], 'calculated_values' => $resolvedBom['calculated_values'], 'bom_summary' => $resolvedBom['summary'], 'resolved_at' => $resolvedBom['resolved_at'], ]; $product = Product::findOrFail($productId); if ($update) { $existingSnapshot = $product->parameter_snapshot ?? []; $parameterData = array_merge($existingSnapshot, $parameterData); } $product->update([ 'parameter_snapshot' => $parameterData, 'updated_by' => $this->apiUserId(), ]); } /** * 제품 코드 생성 */ private function generateProductCode(ModelMaster $model, array $calculatedValues): string { // 모델 코드 + 주요 치수값으로 코드 생성 $code = $model->code; // 주요 치수값 추가 (W1, H1 등) $keyDimensions = ['W1', 'H1', 'W0', 'H0']; $dimensionParts = []; foreach ($keyDimensions as $dim) { if (isset($calculatedValues[$dim]) && is_numeric($calculatedValues[$dim])) { $dimensionParts[] = $dim . (int) $calculatedValues[$dim]; } } if (!empty($dimensionParts)) { $code .= '_' . implode('_', $dimensionParts); } // 고유성 보장을 위한 접미사 $suffix = Str::upper(Str::random(4)); $code .= '_' . $suffix; return $code; } /** * 제품명 생성 */ private function generateProductName(ModelMaster $model, array $calculatedValues): string { $name = $model->name; // 주요 치수값 추가 $keyDimensions = ['W1', 'H1']; $dimensionParts = []; foreach ($keyDimensions as $dim) { if (isset($calculatedValues[$dim]) && is_numeric($calculatedValues[$dim])) { $dimensionParts[] = (int) $calculatedValues[$dim]; } } if (!empty($dimensionParts)) { $name .= ' (' . implode('×', $dimensionParts) . ')'; } return $name; } /** * 매개변수 포맷팅 */ private function formatParameters(array $parameters): array { $formatted = []; foreach ($parameters as $name => $value) { $formatted[] = [ 'name' => $name, 'value' => $value, 'type' => is_numeric($value) ? 'number' : (is_bool($value) ? 'boolean' : 'text'), ]; } return $formatted; } /** * 계산값 포맷팅 */ private function formatCalculatedValues(array $calculatedValues): array { $formatted = []; foreach ($calculatedValues as $name => $value) { if (is_numeric($value)) { $formatted[] = [ 'name' => $name, 'value' => round((float) $value, 3), 'type' => 'calculated', ]; } } return $formatted; } /** * 사양서 생성 */ private function generateSpecifications(ModelMaster $model, array $resolvedBom): array { $specs = []; // 기본 치수 사양 $dimensionSpecs = $this->generateDimensionSpecs($resolvedBom['calculated_values']); if (!empty($dimensionSpecs)) { $specs['dimensions'] = $dimensionSpecs; } // 무게/면적 사양 $physicalSpecs = $this->generatePhysicalSpecs($resolvedBom['calculated_values']); if (!empty($physicalSpecs)) { $specs['physical'] = $physicalSpecs; } // BOM 요약 $bomSpecs = $this->generateBomSpecs($resolvedBom['bom_items']); if (!empty($bomSpecs)) { $specs['components'] = $bomSpecs; } return $specs; } /** * 치수 사양 생성 */ private function generateDimensionSpecs(array $calculatedValues): array { $specs = []; $dimensionKeys = ['W0', 'H0', 'W1', 'H1', 'D1', 'L1']; foreach ($dimensionKeys as $key) { if (isset($calculatedValues[$key]) && is_numeric($calculatedValues[$key])) { $specs[$key] = [ 'value' => round((float) $calculatedValues[$key], 1), 'unit' => 'mm', 'label' => $this->getDimensionLabel($key), ]; } } return $specs; } /** * 물리적 사양 생성 */ private function generatePhysicalSpecs(array $calculatedValues): array { $specs = []; if (isset($calculatedValues['area']) && is_numeric($calculatedValues['area'])) { $specs['area'] = [ 'value' => round((float) $calculatedValues['area'], 3), 'unit' => 'm²', 'label' => '면적', ]; } if (isset($calculatedValues['weight']) && is_numeric($calculatedValues['weight'])) { $specs['weight'] = [ 'value' => round((float) $calculatedValues['weight'], 2), 'unit' => 'kg', 'label' => '중량', ]; } return $specs; } /** * BOM 사양 생성 */ private function generateBomSpecs(array $bomItems): array { $specs = [ 'total_components' => count($bomItems), 'materials' => 0, 'products' => 0, 'major_components' => [], ]; foreach ($bomItems as $item) { if (!empty($item['material_id'])) { $specs['materials']++; } else { $specs['products']++; } // 주요 구성품 (수량이 많거나 중요한 것들) if (($item['total_quantity'] ?? 0) >= 10 || !empty($item['memo'])) { $specs['major_components'][] = [ 'type' => !empty($item['material_id']) ? 'material' : 'product', 'id' => $item['material_id'] ?? $item['product_id'], 'quantity' => $item['total_quantity'] ?? 0, 'unit' => $item['unit'] ?? 'ea', 'memo' => $item['memo'] ?? '', ]; } } return $specs; } /** * 치수 라벨 가져오기 */ private function getDimensionLabel(string $key): string { $labels = [ 'W0' => '입력 폭', 'H0' => '입력 높이', 'W1' => '실제 폭', 'H1' => '실제 높이', 'D1' => '깊이', 'L1' => '길이', ]; return $labels[$key] ?? $key; } /** * 모델 접근 권한 검증 */ private function validateModelAccess(int $modelId): void { $model = ModelMaster::where('tenant_id', $this->tenantId()) ->findOrFail($modelId); } }