Files
sam-api/app/Services/ProductFromModelService.php

496 lines
16 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
use App\Services\Service;
use Shared\Models\Products\ModelMaster;
use Shared\Models\Products\Product;
use Shared\Models\Products\ProductComponent;
use Shared\Models\Products\BomTemplate;
use Shared\Models\Products\BomTemplateItem;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Product From Model Service
* 모델 기반 제품 생성 서비스
*/
class ProductFromModelService extends Service
{
private BomResolverService $bomResolverService;
public function __construct()
{
parent::__construct();
$this->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);
}
}