bomResolverService = $bomResolverService; $this->productService = $productService; } /** * 모델과 매개변수로부터 제품 생성 */ public function createProductFromModel( int $modelId, array $parameters, array $productData, bool $includeComponents = true ): Product { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 모델 존재 확인 $model = DesignModel::where('tenant_id', $tenantId)->where('id', $modelId)->first(); if (!$model) { throw new NotFoundHttpException(__('error.model_not_found')); } // BOM 해석 실행 $bomResolution = $this->bomResolverService->resolveBom($modelId, $parameters); return DB::transaction(function () use ( $tenantId, $userId, $model, $parameters, $productData, $bomResolution, $includeComponents ) { // 제품 기본 정보 설정 $productPayload = array_merge([ 'tenant_id' => $tenantId, 'created_by' => $userId, 'updated_by' => $userId, 'product_type' => 'PRODUCT', 'is_active' => true, ], $productData); // 제품명에 모델 정보 포함 (명시적으로 지정되지 않은 경우) if (!isset($productData['name'])) { $productPayload['name'] = $model->name . ' (' . $this->formatParametersForName($parameters) . ')'; } // 제품 코드 자동 생성 (명시적으로 지정되지 않은 경우) if (!isset($productData['code'])) { $productPayload['code'] = $this->generateProductCode($model, $parameters); } // 제품 생성 $product = Product::create($productPayload); // BOM 구성요소를 ProductComponent로 생성 if ($includeComponents && !empty($bomResolution['resolved_bom'])) { $this->createProductComponents($product, $bomResolution['resolved_bom']); } // 모델 기반 제품임을 표시하는 메타 정보 저장 $this->saveModelMetadata($product, $model, $parameters, $bomResolution); return $product->load('components'); }); } /** * 기존 제품의 BOM 업데이트 */ public function updateProductBom(int $productId, array $newParameters): Product { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first(); if (!$product) { throw new NotFoundHttpException(__('error.product_not_found')); } // 제품이 모델 기반으로 생성된 것인지 확인 $metadata = $this->getModelMetadata($product); if (!$metadata) { throw ValidationException::withMessages([ 'product_id' => __('error.product_not_model_based') ]); } $modelId = $metadata['model_id']; // 새로운 매개변수로 BOM 해석 $bomResolution = $this->bomResolverService->resolveBom($modelId, $newParameters); return DB::transaction(function () use ($product, $userId, $newParameters, $bomResolution, $metadata) { // 기존 ProductComponent 삭제 ProductComponent::where('product_id', $product->id)->delete(); // 새로운 BOM으로 ProductComponent 생성 $this->createProductComponents($product, $bomResolution['resolved_bom']); // 메타데이터 업데이트 $this->updateModelMetadata($product, $newParameters, $bomResolution); $product->update(['updated_by' => $userId]); return $product->fresh()->load('components'); }); } /** * 모델 기반 제품 복사 (매개변수 변경) */ public function cloneProductWithParameters( int $sourceProductId, array $newParameters, array $productData = [] ): Product { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $sourceProduct = Product::where('tenant_id', $tenantId)->where('id', $sourceProductId)->first(); if (!$sourceProduct) { throw new NotFoundHttpException(__('error.product_not_found')); } // 원본 제품의 모델 메타데이터 조회 $metadata = $this->getModelMetadata($sourceProduct); if (!$metadata) { throw ValidationException::withMessages([ 'product_id' => __('error.product_not_model_based') ]); } // 원본 제품 정보를 기반으로 새 제품 데이터 구성 $newProductData = array_merge([ 'name' => $sourceProduct->name . ' (복사본)', 'description' => $sourceProduct->description, 'category_id' => $sourceProduct->category_id, 'unit' => $sourceProduct->unit, 'product_type' => $sourceProduct->product_type, ], $productData); return $this->createProductFromModel( $metadata['model_id'], $newParameters, $newProductData, true ); } /** * 모델 매개변수 변경에 따른 제품들 일괄 업데이트 */ public function updateProductsByModel(int $modelId, array $updatedParameters = null): array { $tenantId = $this->tenantId(); // 해당 모델로 생성된 제품들 조회 $modelBasedProducts = $this->getProductsByModel($modelId); $results = []; foreach ($modelBasedProducts as $product) { try { $metadata = $this->getModelMetadata($product); $parameters = $updatedParameters ?? $metadata['parameters']; $updatedProduct = $this->updateProductBom($product->id, $parameters); $results[] = [ 'product_id' => $product->id, 'product_code' => $product->code, 'status' => 'updated', 'bom_items_count' => $updatedProduct->components->count(), ]; } catch (\Exception $e) { $results[] = [ 'product_id' => $product->id, 'product_code' => $product->code, 'status' => 'failed', 'error' => $e->getMessage(), ]; } } return $results; } /** * ProductComponent 생성 */ private function createProductComponents(Product $product, array $bomItems): void { $order = 1; foreach ($bomItems as $item) { ProductComponent::create([ 'product_id' => $product->id, 'ref_type' => $item['target_type'], 'ref_id' => $item['target_id'], 'quantity' => $item['actual_quantity'] ?? $item['quantity'], 'waste_rate' => $item['waste_rate'] ?? 0, 'order' => $order, 'note' => $item['reason'] ?? null, ]); $order++; } } /** * 모델 메타데이터 저장 */ private function saveModelMetadata(Product $product, DesignModel $model, array $parameters, array $bomResolution): void { $metadata = [ 'model_id' => $model->id, 'model_code' => $model->code, 'parameters' => $parameters, 'calculated_values' => $bomResolution['calculated_values'], 'bom_resolution_summary' => $bomResolution['summary'], 'created_at' => now()->toISOString(), ]; // 실제로는 product_metadata 테이블이나 products 테이블의 metadata 컬럼에 저장 // 임시로 캐시 사용 cache()->put("product_model_metadata_{$product->id}", $metadata, 86400); // 24시간 } /** * 모델 메타데이터 업데이트 */ private function updateModelMetadata(Product $product, array $newParameters, array $bomResolution): void { $metadata = $this->getModelMetadata($product); if ($metadata) { $metadata['parameters'] = $newParameters; $metadata['calculated_values'] = $bomResolution['calculated_values']; $metadata['bom_resolution_summary'] = $bomResolution['summary']; $metadata['updated_at'] = now()->toISOString(); cache()->put("product_model_metadata_{$product->id}", $metadata, 86400); } } /** * 모델 메타데이터 조회 */ private function getModelMetadata(Product $product): ?array { return cache()->get("product_model_metadata_{$product->id}"); } /** * 매개변수를 제품명용 문자열로 포맷 */ private function formatParametersForName(array $parameters): string { $formatted = []; foreach ($parameters as $key => $value) { if (is_numeric($value)) { $formatted[] = "{$key}:{$value}"; } else { $formatted[] = "{$key}:{$value}"; } } return implode(', ', array_slice($formatted, 0, 3)); // 최대 3개 매개변수만 } /** * 제품 코드 자동 생성 */ private function generateProductCode(DesignModel $model, array $parameters): string { $tenantId = $this->tenantId(); // 모델 코드 + 매개변수 해시 $paramHash = substr(md5(json_encode($parameters)), 0, 6); $baseCode = $model->code . '-' . strtoupper($paramHash); // 중복 체크 후 순번 추가 $counter = 1; $finalCode = $baseCode; while (Product::where('tenant_id', $tenantId)->where('code', $finalCode)->exists()) { $finalCode = $baseCode . '-' . str_pad($counter, 2, '0', STR_PAD_LEFT); $counter++; } return $finalCode; } /** * 모델로 생성된 제품들 조회 */ private function getProductsByModel(int $modelId): \Illuminate\Support\Collection { $tenantId = $this->tenantId(); $products = Product::where('tenant_id', $tenantId)->get(); return $products->filter(function ($product) use ($modelId) { $metadata = $this->getModelMetadata($product); return $metadata && $metadata['model_id'] == $modelId; }); } /** * 제품의 모델 정보 조회 (API용) */ public function getProductModelInfo(int $productId): ?array { $tenantId = $this->tenantId(); $product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first(); if (!$product) { throw new NotFoundHttpException(__('error.product_not_found')); } $metadata = $this->getModelMetadata($product); if (!$metadata) { return null; } // 모델 정보 추가 조회 $model = DesignModel::where('tenant_id', $tenantId)->where('id', $metadata['model_id'])->first(); return [ 'product' => [ 'id' => $product->id, 'code' => $product->code, 'name' => $product->name, ], 'model' => [ 'id' => $model->id, 'code' => $model->code, 'name' => $model->name, ], 'parameters' => $metadata['parameters'], 'calculated_values' => $metadata['calculated_values'], 'bom_summary' => $metadata['bom_resolution_summary'], 'created_at' => $metadata['created_at'], 'updated_at' => $metadata['updated_at'] ?? null, ]; } /** * 모델 기반 제품 재생성 (모델/공식/규칙 변경 후) */ public function regenerateProduct(int $productId, bool $preserveCustomizations = false): Product { $tenantId = $this->tenantId(); $product = Product::where('tenant_id', $tenantId)->where('id', $productId)->first(); if (!$product) { throw new NotFoundHttpException(__('error.product_not_found')); } $metadata = $this->getModelMetadata($product); if (!$metadata) { throw ValidationException::withMessages([ 'product_id' => __('error.product_not_model_based') ]); } // 사용자 정의 변경사항 보존 로직 $customComponents = []; if ($preserveCustomizations) { // 기존 컴포넌트 중 규칙에서 나온 것이 아닌 수동 추가 항목들 식별 $customComponents = ProductComponent::where('product_id', $productId) ->whereNull('note') // rule_name이 없는 항목들 ->get() ->toArray(); } // 제품을 새로운 매개변수로 재생성 $regeneratedProduct = $this->updateProductBom($productId, $metadata['parameters']); // 커스텀 컴포넌트 복원 if (!empty($customComponents)) { foreach ($customComponents as $customComponent) { ProductComponent::create([ 'product_id' => $productId, 'ref_type' => $customComponent['ref_type'], 'ref_id' => $customComponent['ref_id'], 'quantity' => $customComponent['quantity'], 'waste_rate' => $customComponent['waste_rate'], 'order' => $customComponent['order'], 'note' => 'custom_preserved', ]); } } return $regeneratedProduct->fresh()->load('components'); } }