feat: 경동기업 전용 견적 계산 로직 구현 (Phase 4 완료)

- KdPriceTable 모델: 경동기업 단가 테이블 (motor, shaft, pipe, angle, raw_material, bdmodels)
- KyungdongFormulaHandler: 모터 용량, 브라켓 크기, 절곡품(10종), 부자재(3종) 계산
- FormulaEvaluatorService: tenant_id=287 라우팅 추가
- kd_price_tables 마이그레이션 및 시더 (47건 단가 데이터)

테스트 결과: W0=3000, H0=2500 입력 시 16개 항목, 합계 751,200원 정상 계산

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 01:10:42 +09:00
parent e9894fef61
commit 3fce54b7d4
6 changed files with 2013 additions and 0 deletions

View File

@@ -6,6 +6,7 @@
use App\Models\Items\Item;
use App\Models\Products\Price;
use App\Models\Quote\QuoteFormula;
use App\Services\Quote\Handlers\KyungdongFormulaHandler;
use App\Services\Service;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
@@ -28,6 +29,11 @@ class FormulaEvaluatorService extends Service
'SUM', 'ROUND', 'CEIL', 'FLOOR', 'ABS', 'MIN', 'MAX', 'IF', 'AND', 'OR', 'NOT',
];
/**
* 경동기업 테넌트 ID
*/
private const KYUNGDONG_TENANT_ID = 287;
private array $variables = [];
private array $errors = [];
@@ -599,6 +605,11 @@ public function calculateBomWithDebug(
];
}
// 경동기업(tenant_id=287) 전용 계산 로직 분기
if ($tenantId === self::KYUNGDONG_TENANT_ID) {
return $this->calculateKyungdongBom($finishedGoodsCode, $inputVariables, $tenantId);
}
// Step 1: 입력값 수집 (React 동기화 변수 포함)
$this->addDebugStep(1, '입력값수집', [
'W0' => $inputVariables['W0'] ?? null,
@@ -1538,4 +1549,219 @@ private function mergeLeafMaterials(array $leafMaterials, int $tenantId): array
return array_values($result);
}
// =========================================================================
// 경동기업 전용 계산 (tenant_id = 287)
// =========================================================================
/**
* 경동기업 전용 BOM 계산
*
* 5130 레거시 시스템의 견적 로직을 구현한 KyungdongFormulaHandler 사용
* - 3차원 조건 모터 용량 계산 (제품타입 × 인치 × 중량)
* - 브라켓 크기 결정
* - 10종 절곡품 계산
* - 3종 부자재 계산
*
* @param string $finishedGoodsCode 완제품 코드
* @param array $inputVariables 입력 변수 (W0, H0, QTY 등)
* @param int $tenantId 테넌트 ID
* @return array 계산 결과
*/
private function calculateKyungdongBom(
string $finishedGoodsCode,
array $inputVariables,
int $tenantId
): array {
$this->addDebugStep(0, '경동전용계산', [
'tenant_id' => $tenantId,
'handler' => 'KyungdongFormulaHandler',
'finished_goods' => $finishedGoodsCode,
]);
// Step 1: 입력값 수집
$this->addDebugStep(1, '입력값수집', [
'W0' => $inputVariables['W0'] ?? null,
'H0' => $inputVariables['H0'] ?? null,
'QTY' => $inputVariables['QTY'] ?? 1,
'bracket_inch' => $inputVariables['bracket_inch'] ?? '5',
'product_type' => $inputVariables['product_type'] ?? 'screen',
'finishing_type' => $inputVariables['finishing_type'] ?? 'SUS',
'finished_goods' => $finishedGoodsCode,
]);
// Step 2: 완제품 조회
$finishedGoods = $this->getItemDetails($finishedGoodsCode, $tenantId);
if (! $finishedGoods) {
$this->addDebugStep(2, '완제품선택', [
'code' => $finishedGoodsCode,
'error' => '완제품을 찾을 수 없습니다.',
]);
return [
'success' => false,
'error' => __('error.finished_goods_not_found', ['code' => $finishedGoodsCode]),
'debug_steps' => $this->debugSteps,
];
}
$this->addDebugStep(2, '완제품선택', [
'code' => $finishedGoods['code'],
'name' => $finishedGoods['name'],
'item_category' => $finishedGoods['item_category'] ?? 'N/A',
]);
// KyungdongFormulaHandler 인스턴스 생성
$handler = new KyungdongFormulaHandler;
// Step 3: 경동 전용 변수 계산
$W0 = (float) ($inputVariables['W0'] ?? 0);
$H0 = (float) ($inputVariables['H0'] ?? 0);
$QTY = (int) ($inputVariables['QTY'] ?? 1);
$bracketInch = $inputVariables['bracket_inch'] ?? '5';
$productType = $inputVariables['product_type'] ?? 'screen';
// 중량 계산 (5130 로직)
$area = ($W0 * ($H0 + 550)) / 1000000;
$weight = $area * ($productType === 'steel' ? 25 : 2) + ($W0 / 1000) * 14.17;
// 모터 용량 결정
$motorCapacity = $handler->calculateMotorCapacity($productType, $weight, $bracketInch);
// 브라켓 크기 결정
$bracketSize = $handler->calculateBracketSize($weight, $bracketInch);
$calculatedVariables = array_merge($inputVariables, [
'W0' => $W0,
'H0' => $H0,
'QTY' => $QTY,
'W1' => $W0 + 140,
'H1' => $H0 + 350,
'AREA' => round($area, 4),
'WEIGHT' => round($weight, 2),
'MOTOR_CAPACITY' => $motorCapacity,
'BRACKET_SIZE' => $bracketSize,
'bracket_inch' => $bracketInch,
'product_type' => $productType,
]);
$this->addDebugStep(3, '변수계산', [
'W0' => $W0,
'H0' => $H0,
'area' => round($area, 4),
'weight' => round($weight, 2),
'motor_capacity' => $motorCapacity,
'bracket_size' => $bracketSize,
'calculation_type' => '경동기업 전용 공식',
]);
// Step 4-7: 동적 항목 계산 (KyungdongFormulaHandler 사용)
$dynamicItems = $handler->calculateDynamicItems($calculatedVariables);
$this->addDebugStep(4, 'BOM전개', [
'total_items' => count($dynamicItems),
'item_categories' => array_unique(array_column($dynamicItems, 'category')),
]);
// Step 5-7: 단가 계산 (각 항목별)
$calculatedItems = [];
foreach ($dynamicItems as $item) {
$this->addDebugStep(6, '수량계산', [
'item_name' => $item['item_name'],
'quantity' => $item['quantity'],
]);
$this->addDebugStep(7, '금액계산', [
'item_name' => $item['item_name'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'total_price' => $item['total_price'],
]);
$calculatedItems[] = [
'item_code' => $item['item_code'] ?? '',
'item_name' => $item['item_name'],
'item_category' => $item['category'],
'specification' => $item['specification'] ?? '',
'unit' => $item['unit'],
'quantity' => $item['quantity'],
'unit_price' => $item['unit_price'],
'total_price' => $item['total_price'],
'category_group' => $item['category'],
'calculation_note' => '경동기업 전용 계산',
];
}
// Step 8: 카테고리별 그룹화
$groupedItems = [];
foreach ($calculatedItems as $item) {
$category = $item['category_group'];
if (! isset($groupedItems[$category])) {
$groupedItems[$category] = [
'name' => $this->getKyungdongCategoryName($category),
'items' => [],
'subtotal' => 0,
];
}
$groupedItems[$category]['items'][] = $item;
$groupedItems[$category]['subtotal'] += $item['total_price'];
}
$this->addDebugStep(8, '카테고리그룹화', [
'groups' => array_map(fn ($g) => [
'name' => $g['name'],
'count' => count($g['items']),
'subtotal' => $g['subtotal'],
], $groupedItems),
]);
// Step 9: 소계 계산
$subtotals = [];
foreach ($groupedItems as $category => $group) {
$subtotals[$category] = [
'name' => $group['name'],
'count' => count($group['items']),
'subtotal' => $group['subtotal'],
];
}
$this->addDebugStep(9, '소계계산', $subtotals);
// Step 10: 최종 합계
$grandTotal = array_sum(array_column($calculatedItems, 'total_price'));
$this->addDebugStep(10, '최종합계', [
'item_count' => count($calculatedItems),
'grand_total' => $grandTotal,
'formatted' => number_format($grandTotal).'원',
]);
return [
'success' => true,
'finished_goods' => $finishedGoods,
'variables' => $calculatedVariables,
'items' => $calculatedItems,
'grouped_items' => $groupedItems,
'subtotals' => $subtotals,
'grand_total' => $grandTotal,
'debug_steps' => $this->debugSteps,
'calculation_type' => 'kyungdong',
];
}
/**
* 경동기업 카테고리명 반환
*/
private function getKyungdongCategoryName(string $category): string
{
return match ($category) {
'material' => '주자재',
'motor' => '모터',
'controller' => '제어기',
'steel' => '절곡품',
'parts' => '부자재',
default => $category,
};
}
}