Files
sam-api/app/Services/Calculation/CalculationEngine.php
hskwon bd678dfea9 feat: 업체별 동적 BOM 계산 시스템 구현
- 데이터베이스 스키마 확장: BOM 테이블에 계산 관련 필드 추가
- 계산 엔진 구현: CalculationEngine, FormulaParser, ParameterValidator
- API 구현: 견적 파라미터 추출, 실시간 BOM 계산, 업체별 산출식 관리
- FormRequest 검증: 모든 입력 데이터 검증 및 한국어 에러 메시지
- 라우트 등록: 5개 BOM 계산 API 엔드포인트 추가

주요 기능:
• BOM에서 필요한 조건만 동적 추출하여 견적 화면에 표시
• 경동기업 하드코딩 산출식을 동적 시스템으로 전환
• 업체별 산출식 버전 관리 및 실시간 테스트 지원

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 22:09:42 +09:00

268 lines
9.3 KiB
PHP

<?php
namespace App\Services\Calculation;
use App\Models\Design\BomTemplate;
use App\Models\Design\BomTemplateItem;
use App\Models\Calculation\CalculationConfig;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class CalculationEngine
{
protected FormulaParser $parser;
protected ParameterValidator $validator;
public function __construct(FormulaParser $parser, ParameterValidator $validator)
{
$this->parser = $parser;
$this->validator = $validator;
}
/**
* BOM 계산 실행
* @param int $bomTemplateId BOM 템플릿 ID
* @param array $parameters 입력 파라미터
* @param string|null $companyName 업체명 (null시 기본값 사용)
* @return array 계산 결과
*/
public function calculateBOM(int $bomTemplateId, array $parameters, ?string $companyName = null): array
{
try {
// BOM 템플릿 조회
$bomTemplate = BomTemplate::with(['items', 'modelVersion.model'])
->findOrFail($bomTemplateId);
// 파라미터 검증
$this->validateParameters($bomTemplate, $parameters);
// 중간 계산값 도출 (W1, H1, 면적, 중량 등)
$calculatedValues = $this->calculateIntermediateValues($bomTemplate, $parameters, $companyName);
// BOM 아이템별 수량 계산
$bomItems = $this->calculateBomItems($bomTemplate->items, $calculatedValues, $parameters, $companyName);
return [
'success' => true,
'bom_template' => [
'id' => $bomTemplate->id,
'name' => $bomTemplate->name,
'company_type' => $bomTemplate->company_type,
'formula_version' => $bomTemplate->formula_version
],
'input_parameters' => $parameters,
'calculated_values' => $calculatedValues,
'bom_items' => $bomItems,
'calculation_timestamp' => now()
];
} catch (\Exception $e) {
Log::error('BOM 계산 실패', [
'bom_template_id' => $bomTemplateId,
'parameters' => $parameters,
'error' => $e->getMessage()
]);
return [
'success' => false,
'error' => $e->getMessage(),
'bom_template_id' => $bomTemplateId
];
}
}
/**
* 견적시 필요한 파라미터 스키마 추출
* @param int $bomTemplateId BOM 템플릿 ID
* @return array 파라미터 스키마
*/
public function getRequiredParameters(int $bomTemplateId): array
{
$bomTemplate = BomTemplate::findOrFail($bomTemplateId);
$schema = $bomTemplate->calculation_schema;
if (!$schema) {
return $this->getDefaultParameterSchema($bomTemplate);
}
return $schema;
}
/**
* 중간 계산값 도출 (W1, H1, 면적, 중량 등)
*/
protected function calculateIntermediateValues(BomTemplate $bomTemplate, array $parameters, ?string $companyName): array
{
$company = $companyName ?: $bomTemplate->company_type;
$calculated = [];
// 기본 파라미터에서 추출
$W0 = $parameters['W0'] ?? 0;
$H0 = $parameters['H0'] ?? 0;
$productType = $parameters['product_type'] ?? 'screen';
// 업체별 제작사이즈 계산
$sizeFormula = $this->getCalculationConfig($company, 'manufacturing_size');
if ($sizeFormula) {
$calculated = array_merge($calculated, $this->parser->execute($sizeFormula->formula_expression, [
'W0' => $W0,
'H0' => $H0,
'product_type' => $productType
]));
} else {
// 기본 공식 (경동기업 기준)
if ($productType === 'screen') {
$calculated['W1'] = $W0 + 160;
$calculated['H1'] = $H0 + 350;
} else {
$calculated['W1'] = $W0 + 110;
$calculated['H1'] = $H0 + 350;
}
}
// 면적 계산
$calculated['area'] = ($calculated['W1'] * $calculated['H1']) / 1000000;
// 중량 계산
$weightFormula = $this->getCalculationConfig($company, 'weight_calculation');
if ($weightFormula) {
$weightResult = $this->parser->execute($weightFormula->formula_expression, array_merge($parameters, $calculated));
$calculated['weight'] = $weightResult['weight'] ?? 0;
} else {
// 기본 중량 공식 (스크린)
if ($productType === 'screen') {
$calculated['weight'] = ($calculated['area'] * 2) + ($W0 / 1000 * 14.17);
} else {
$calculated['weight'] = $calculated['area'] * 40;
}
}
return $calculated;
}
/**
* BOM 아이템별 수량 계산
*/
protected function calculateBomItems(Collection $bomItems, array $calculatedValues, array $parameters, ?string $companyName): array
{
$results = [];
foreach ($bomItems as $item) {
if (!$item->is_calculated) {
// 고정 수량 아이템
$results[] = [
'item_id' => $item->id,
'ref_type' => $item->ref_type,
'ref_id' => $item->ref_id,
'original_qty' => $item->qty,
'calculated_qty' => $item->qty,
'is_calculated' => false,
'calculation_formula' => null
];
continue;
}
// 계산식 적용 아이템
try {
$allParameters = array_merge($parameters, $calculatedValues);
$calculatedQty = $this->parser->execute($item->calculation_formula, $allParameters);
$results[] = [
'item_id' => $item->id,
'ref_type' => $item->ref_type,
'ref_id' => $item->ref_id,
'original_qty' => $item->qty,
'calculated_qty' => $calculatedQty['result'] ?? $item->qty,
'is_calculated' => true,
'calculation_formula' => $item->calculation_formula,
'depends_on' => $item->depends_on
];
} catch (\Exception $e) {
Log::warning('BOM 아이템 계산 실패', [
'item_id' => $item->id,
'formula' => $item->calculation_formula,
'error' => $e->getMessage()
]);
// 계산 실패시 원래 수량 사용
$results[] = [
'item_id' => $item->id,
'ref_type' => $item->ref_type,
'ref_id' => $item->ref_id,
'original_qty' => $item->qty,
'calculated_qty' => $item->qty,
'is_calculated' => false,
'calculation_error' => $e->getMessage()
];
}
}
return $results;
}
/**
* 파라미터 검증
*/
protected function validateParameters(BomTemplate $bomTemplate, array $parameters): void
{
$schema = $bomTemplate->calculation_schema;
if (!$schema) return;
$this->validator->validate($schema, $parameters);
}
/**
* 업체별 산출식 설정 조회
*/
protected function getCalculationConfig(string $companyName, string $formulaType): ?CalculationConfig
{
return CalculationConfig::where('company_name', $companyName)
->where('formula_type', $formulaType)
->where('is_active', true)
->latest('version')
->first();
}
/**
* 기본 파라미터 스키마 생성
*/
protected function getDefaultParameterSchema(BomTemplate $bomTemplate): array
{
return [
'required_parameters' => [
[
'key' => 'W0',
'label' => '오픈사이즈 가로(mm)',
'type' => 'integer',
'required' => true,
'min' => 500,
'max' => 15000
],
[
'key' => 'H0',
'label' => '오픈사이즈 세로(mm)',
'type' => 'integer',
'required' => true,
'min' => 500,
'max' => 5000
],
[
'key' => 'product_type',
'label' => '제품타입',
'type' => 'select',
'options' => ['screen' => '스크린', 'steel' => '철재'],
'required' => true
],
[
'key' => 'installation_type',
'label' => '설치방식',
'type' => 'select',
'options' => ['wall' => '벽면형', 'side' => '측면형', 'mixed' => '혼합형'],
'required' => false
]
],
'company_type' => $bomTemplate->company_type ?: 'default',
'formula_version' => $bomTemplate->formula_version ?: 'v1.0'
];
}
}