Files
sam-api/app/Services/ProductFromModelService.php
kent bf8036a64b feat: DB 연결 오버라이딩 및 대시보드 통계 위젯 추가
- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env)
- 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget)
- 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget)
- 리소스 한국어화: Product, Material 모델 레이블 추가
- 대시보드: 위젯 등록 및 캐시 최적화

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-30 23:31:14 +09:00

496 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}