Files
sam-api/app/Services/Design/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

422 lines
14 KiB
PHP

<?php
namespace App\Services\Design;
use App\Models\Design\DesignModel;
use App\Models\Product;
use App\Models\ProductComponent;
use App\Models\Category;
use App\Services\Service;
use App\Services\ProductService;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ProductFromModelService extends Service
{
protected BomResolverService $bomResolverService;
protected ProductService $productService;
public function __construct(
BomResolverService $bomResolverService,
ProductService $productService
) {
$this->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');
}
}