- DB 연결: 로컬/Docker 환경 오버라이딩 설정 (.env) - 테넌트 위젯: redirect 버그 수정 (TenantSelectorWidget) - 통계 위젯: 사용자/제품/자재/주문 카드 추가 (StatsOverviewWidget) - 리소스 한국어화: Product, Material 모델 레이블 추가 - 대시보드: 위젯 등록 및 캐시 최적화 🤖 Generated with [Claude Code](https://claude.ai/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
422 lines
14 KiB
PHP
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');
|
|
}
|
|
} |