Files
sam-api/app/Services/Quote/Handlers/KyungdongFormulaHandler.php
권혁성 3fce54b7d4 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>
2026-01-29 01:10:42 +09:00

774 lines
26 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\Quote\Handlers;
use App\Models\Kyungdong\KdPriceTable;
/**
* 경동기업 전용 견적 계산 핸들러
*
* 5130 레거시 시스템의 견적 로직을 SAM에 구현
* tenant_id = 287 전용
*/
class KyungdongFormulaHandler
{
private const TENANT_ID = 287;
// =========================================================================
// 모터 용량 계산
// =========================================================================
/**
* 모터 용량 계산 (3차원 조건: 제품타입 × 인치 × 중량)
*
* @param string $productType 제품 타입 (screen, steel)
* @param float $weight 중량 (kg)
* @param string $bracketInch 브라켓 인치 (4, 5, 6, 8)
* @return string 모터 용량 (150K, 300K, 400K, 500K, 600K, 800K, 1000K)
*/
public function calculateMotorCapacity(string $productType, float $weight, string $bracketInch): string
{
$inch = (int) $bracketInch;
if ($productType === 'screen') {
return $this->calculateScreenMotor($weight, $inch);
}
return $this->calculateSteelMotor($weight, $inch);
}
/**
* 스크린 모터 용량 계산
*/
private function calculateScreenMotor(float $weight, int $inch): string
{
if ($inch === 4) {
if ($weight <= 150) {
return '150K';
}
if ($weight <= 300) {
return '300K';
}
return '400K';
}
if ($inch === 5) {
if ($weight <= 123) {
return '150K';
}
if ($weight <= 246) {
return '300K';
}
if ($weight <= 327) {
return '400K';
}
if ($weight <= 500) {
return '500K';
}
return '600K';
}
if ($inch === 6) {
if ($weight <= 104) {
return '150K';
}
if ($weight <= 208) {
return '300K';
}
if ($weight <= 300) {
return '400K';
}
if ($weight <= 424) {
return '500K';
}
return '600K';
}
// 기본값
return '300K';
}
/**
* 철재 모터 용량 계산
*/
private function calculateSteelMotor(float $weight, int $inch): string
{
if ($inch === 4) {
if ($weight <= 300) {
return '300K';
}
return '400K';
}
if ($inch === 5) {
if ($weight <= 246) {
return '300K';
}
if ($weight <= 327) {
return '400K';
}
if ($weight <= 500) {
return '500K';
}
return '600K';
}
if ($inch === 6) {
if ($weight <= 208) {
return '300K';
}
if ($weight <= 277) {
return '400K';
}
if ($weight <= 424) {
return '500K';
}
if ($weight <= 508) {
return '600K';
}
if ($weight <= 800) {
return '800K';
}
return '1000K';
}
if ($inch === 8) {
if ($weight <= 324) {
return '500K';
}
if ($weight <= 388) {
return '600K';
}
if ($weight <= 611) {
return '800K';
}
return '1000K';
}
// 기본값
return '300K';
}
// =========================================================================
// 브라켓 크기 계산
// =========================================================================
/**
* 브라켓 크기 결정
*
* @param float $weight 중량 (kg)
* @param string|null $bracketInch 브라켓 인치 (선택)
* @return string 브라켓 크기 (530*320, 600*350, 690*390)
*/
public function calculateBracketSize(float $weight, ?string $bracketInch = null): string
{
$motorCapacity = $this->getMotorCapacityByWeight($weight, $bracketInch);
return match ($motorCapacity) {
'300K', '400K' => '530*320',
'500K', '600K' => '600*350',
'800K', '1000K' => '690*390',
default => '530*320',
};
}
/**
* 중량으로 모터 용량 판단 (인치 없을 때)
*/
private function getMotorCapacityByWeight(float $weight, ?string $bracketInch = null): string
{
if ($bracketInch) {
// 인치가 있으면 철재 기준으로 계산
return $this->calculateSteelMotor($weight, (int) $bracketInch);
}
// 인치 없으면 중량만으로 판단
if ($weight <= 300) {
return '300K';
}
if ($weight <= 400) {
return '400K';
}
if ($weight <= 500) {
return '500K';
}
if ($weight <= 600) {
return '600K';
}
if ($weight <= 800) {
return '800K';
}
return '1000K';
}
// =========================================================================
// 주자재(스크린) 계산
// =========================================================================
/**
* 스크린 주자재 가격 계산
*
* @param float $width 폭 (mm)
* @param float $height 높이 (mm)
* @return array [unit_price, area, total_price]
*/
public function calculateScreenPrice(float $width, float $height): array
{
// 면적 계산: W × (H + 550) / 1,000,000
$calculateHeight = $height + 550;
$area = ($width * $calculateHeight) / 1000000;
// 원자재 단가 조회 (실리카/스크린)
$unitPrice = $this->getRawMaterialPrice('실리카');
return [
'unit_price' => $unitPrice,
'area' => round($area, 2),
'total_price' => round($unitPrice * $area),
];
}
// =========================================================================
// 단가 조회 메서드 (KdPriceTable 사용)
// =========================================================================
/**
* BDmodels 테이블에서 단가 조회
*
* @param string $modelName 모델코드 (KSS01, KWS01 등)
* @param string $secondItem 부품분류 (케이스, 가이드레일, 하단마감재, L-BAR 등)
* @param string|null $finishingType 마감재질 (SUS, EGI)
* @param string|null $spec 규격 (120*70, 650*550 등)
* @return float 단가
*/
public function getBDModelPrice(
string $modelName,
string $secondItem,
?string $finishingType = null,
?string $spec = null
): float {
// BDmodels는 복잡한 구조이므로 items 테이블의 기존 데이터 활용
// TODO: 필요시 kd_price_tables TYPE_BDMODELS 추가
return 0.0;
}
/**
* price_* 테이블에서 단가 조회 (모터, 샤프트, 파이프, 앵글)
*
* @param string $tableName 테이블명 (motor, shaft, pipe, angle)
* @param array $conditions 조회 조건
* @return float 단가
*/
public function getPriceFromTable(string $tableName, array $conditions): float
{
$query = KdPriceTable::where('table_type', $tableName)->active();
foreach ($conditions as $field => $value) {
$query->where($field, $value);
}
$record = $query->first();
return (float) ($record?->unit_price ?? 0);
}
/**
* 원자재 단가 조회
*
* @param string $materialName 원자재명 (실리카, 스크린 등)
* @return float 단가
*/
public function getRawMaterialPrice(string $materialName): float
{
return KdPriceTable::getRawMaterialPrice($materialName);
}
/**
* 모터 단가 조회
*
* @param string $motorCapacity 모터 용량 (150K, 300K 등)
* @return float 단가
*/
public function getMotorPrice(string $motorCapacity): float
{
return KdPriceTable::getMotorPrice($motorCapacity);
}
/**
* 제어기 단가 조회
*
* @param string $controllerType 제어기 타입 (매립형, 노출형, 뒷박스)
* @return float 단가
*/
public function getControllerPrice(string $controllerType): float
{
return KdPriceTable::getControllerPrice($controllerType);
}
/**
* 샤프트 단가 조회
*
* @param string $size 사이즈 (3, 4, 5인치)
* @param float $length 길이 (m 단위)
* @return float 단가
*/
public function getShaftPrice(string $size, float $length): float
{
return KdPriceTable::getShaftPrice($size, $length);
}
/**
* 파이프 단가 조회
*
* @param string $thickness 두께 (1.4 등)
* @param int $length 길이 (3000, 6000)
* @return float 단가
*/
public function getPipePrice(string $thickness, int $length): float
{
return KdPriceTable::getPipePrice($thickness, $length);
}
/**
* 앵글 단가 조회
*
* @param string $type 타입 (스크린용, 철재용)
* @param string $bracketSize 브라켓크기 (530*320, 600*350, 690*390)
* @param string $angleType 앵글타입 (앵글3T, 앵글4T)
* @return float 단가
*/
public function getAnglePrice(string $type, string $bracketSize, string $angleType): float
{
return KdPriceTable::getAnglePrice($type, $bracketSize, $angleType);
}
// =========================================================================
// 절곡품 계산 (10종)
// =========================================================================
/**
* 절곡품 항목 계산 (10종)
*
* 케이스, 케이스용 연기차단재, 케이스 마구리, 가이드레일,
* 레일용 연기차단재, 하장바, L바, 보강평철, 무게평철12T, 환봉
*
* @param array $params 입력 파라미터
* @return array 절곡품 항목 배열
*/
public function calculateSteelItems(array $params): array
{
$items = [];
// 기본 파라미터
$width = (float) ($params['W0'] ?? 0);
$height = (float) ($params['H0'] ?? 0);
$quantity = (int) ($params['QTY'] ?? 1);
$modelName = $params['model_name'] ?? 'KSS01';
$finishingType = $params['finishing_type'] ?? 'SUS';
// 절곡품 관련 파라미터
$caseSpec = $params['case_spec'] ?? '500*380';
$caseLength = (float) ($params['case_length'] ?? $width); // mm 단위
$guideType = $params['guide_type'] ?? '벽면형'; // 벽면형, 측면형, 혼합형
$guideSpec = $params['guide_spec'] ?? '120*70'; // 120*70, 120*100
$guideLength = (float) ($params['guide_length'] ?? ($height + 550)) / 1000; // m 단위
$bottomBarLength = (float) ($params['bottombar_length'] ?? $width) / 1000; // m 단위
$lbarLength = (float) ($params['lbar_length'] ?? $width) / 1000; // m 단위
$flatBarLength = (float) ($params['flatbar_length'] ?? $width) / 1000; // m 단위
$weightPlateQty = (int) ($params['weight_plate_qty'] ?? 0); // 무게평철 수량
$roundBarQty = (int) ($params['round_bar_qty'] ?? 0); // 환봉 수량
// 1. 케이스 (단가/1000 × 길이mm × 수량)
$casePrice = KdPriceTable::getCasePrice($caseSpec);
if ($casePrice > 0 && $caseLength > 0) {
$totalPrice = ($casePrice / 1000) * $caseLength * $quantity;
$items[] = [
'category' => 'steel',
'item_name' => '케이스',
'specification' => "{$caseSpec} {$caseLength}mm",
'unit' => 'm',
'quantity' => $caseLength / 1000 * $quantity,
'unit_price' => $casePrice,
'total_price' => round($totalPrice),
];
}
// 2. 케이스용 연기차단재 (단가 × 길이m × 수량)
$caseSmokePrice = KdPriceTable::getCaseSmokeBlockPrice();
if ($caseSmokePrice > 0 && $caseLength > 0) {
$lengthM = $caseLength / 1000;
$items[] = [
'category' => 'steel',
'item_name' => '케이스용 연기차단재',
'specification' => "{$lengthM}m",
'unit' => 'm',
'quantity' => $lengthM * $quantity,
'unit_price' => $caseSmokePrice,
'total_price' => round($caseSmokePrice * $lengthM * $quantity),
];
}
// 3. 케이스 마구리 (단가 × 수량)
$caseCapPrice = KdPriceTable::getCaseCapPrice($caseSpec);
if ($caseCapPrice > 0) {
$capQty = 2 * $quantity; // 좌우 2개
$items[] = [
'category' => 'steel',
'item_name' => '케이스 마구리',
'specification' => $caseSpec,
'unit' => 'EA',
'quantity' => $capQty,
'unit_price' => $caseCapPrice,
'total_price' => round($caseCapPrice * $capQty),
];
}
// 4. 가이드레일 (단가 × 길이m × 수량) - 타입별 처리
$guideItems = $this->calculateGuideRails($modelName, $finishingType, $guideType, $guideSpec, $guideLength, $quantity);
$items = array_merge($items, $guideItems);
// 5. 레일용 연기차단재 (단가 × 길이m × 2 × 수량)
$railSmokePrice = KdPriceTable::getRailSmokeBlockPrice();
if ($railSmokePrice > 0 && $guideLength > 0) {
$railSmokeQty = 2 * $quantity; // 좌우 2개
$items[] = [
'category' => 'steel',
'item_name' => '레일용 연기차단재',
'specification' => "{$guideLength}m × 2",
'unit' => 'm',
'quantity' => $guideLength * $railSmokeQty,
'unit_price' => $railSmokePrice,
'total_price' => round($railSmokePrice * $guideLength * $railSmokeQty),
];
}
// 6. 하장바 (단가 × 길이m × 수량)
$bottomBarPrice = KdPriceTable::getBottomBarPrice($modelName, $finishingType);
if ($bottomBarPrice > 0 && $bottomBarLength > 0) {
$items[] = [
'category' => 'steel',
'item_name' => '하장바',
'specification' => "{$modelName} {$finishingType} {$bottomBarLength}m",
'unit' => 'm',
'quantity' => $bottomBarLength * $quantity,
'unit_price' => $bottomBarPrice,
'total_price' => round($bottomBarPrice * $bottomBarLength * $quantity),
];
}
// 7. L바 (단가 × 길이m × 수량)
$lbarPrice = KdPriceTable::getLBarPrice($modelName);
if ($lbarPrice > 0 && $lbarLength > 0) {
$items[] = [
'category' => 'steel',
'item_name' => 'L바',
'specification' => "{$modelName} {$lbarLength}m",
'unit' => 'm',
'quantity' => $lbarLength * $quantity,
'unit_price' => $lbarPrice,
'total_price' => round($lbarPrice * $lbarLength * $quantity),
];
}
// 8. 보강평철 (단가 × 길이m × 수량)
$flatBarPrice = KdPriceTable::getFlatBarPrice();
if ($flatBarPrice > 0 && $flatBarLength > 0) {
$items[] = [
'category' => 'steel',
'item_name' => '보강평철',
'specification' => "{$flatBarLength}m",
'unit' => 'm',
'quantity' => $flatBarLength * $quantity,
'unit_price' => $flatBarPrice,
'total_price' => round($flatBarPrice * $flatBarLength * $quantity),
];
}
// 9. 무게평철12T (고정 12,000원 × 수량)
if ($weightPlateQty > 0) {
$weightPlatePrice = 12000;
$items[] = [
'category' => 'steel',
'item_name' => '무게평철12T',
'specification' => '12T',
'unit' => 'EA',
'quantity' => $weightPlateQty * $quantity,
'unit_price' => $weightPlatePrice,
'total_price' => $weightPlatePrice * $weightPlateQty * $quantity,
];
}
// 10. 환봉 (고정 2,000원 × 수량)
if ($roundBarQty > 0) {
$roundBarPrice = 2000;
$items[] = [
'category' => 'steel',
'item_name' => '환봉',
'specification' => '',
'unit' => 'EA',
'quantity' => $roundBarQty * $quantity,
'unit_price' => $roundBarPrice,
'total_price' => $roundBarPrice * $roundBarQty * $quantity,
];
}
return $items;
}
/**
* 가이드레일 계산 (타입별 처리)
*
* @param string $modelName 모델코드
* @param string $finishingType 마감재질
* @param string $guideType 가이드레일 타입 (벽면형, 측면형, 혼합형)
* @param string $guideSpec 가이드레일 규격 (120*70, 120*100)
* @param float $guideLength 가이드레일 길이 (m)
* @param int $quantity 수량
* @return array 가이드레일 항목 배열
*/
private function calculateGuideRails(
string $modelName,
string $finishingType,
string $guideType,
string $guideSpec,
float $guideLength,
int $quantity
): array {
$items = [];
if ($guideLength <= 0) {
return $items;
}
switch ($guideType) {
case '벽면형':
// 120*70 × 2개
$price = KdPriceTable::getGuideRailPrice($modelName, $finishingType, '120*70');
if ($price > 0) {
$guideQty = 2 * $quantity;
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} 120*70 {$guideLength}m × 2",
'unit' => 'm',
'quantity' => $guideLength * $guideQty,
'unit_price' => $price,
'total_price' => round($price * $guideLength * $guideQty),
];
}
break;
case '측면형':
// 120*100 × 2개
$price = KdPriceTable::getGuideRailPrice($modelName, $finishingType, '120*100');
if ($price > 0) {
$guideQty = 2 * $quantity;
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} 120*100 {$guideLength}m × 2",
'unit' => 'm',
'quantity' => $guideLength * $guideQty,
'unit_price' => $price,
'total_price' => round($price * $guideLength * $guideQty),
];
}
break;
case '혼합형':
// 120*70 × 1개 + 120*100 × 1개
$price70 = KdPriceTable::getGuideRailPrice($modelName, $finishingType, '120*70');
$price100 = KdPriceTable::getGuideRailPrice($modelName, $finishingType, '120*100');
if ($price70 > 0) {
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} 120*70 {$guideLength}m",
'unit' => 'm',
'quantity' => $guideLength * $quantity,
'unit_price' => $price70,
'total_price' => round($price70 * $guideLength * $quantity),
];
}
if ($price100 > 0) {
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} 120*100 {$guideLength}m",
'unit' => 'm',
'quantity' => $guideLength * $quantity,
'unit_price' => $price100,
'total_price' => round($price100 * $guideLength * $quantity),
];
}
break;
}
return $items;
}
// =========================================================================
// 부자재 계산 (3종)
// =========================================================================
/**
* 부자재 항목 계산
*
* @param array $params 입력 파라미터
* @return array 부자재 항목 배열
*/
public function calculatePartItems(array $params): array
{
$items = [];
$width = (float) ($params['W0'] ?? 0);
$bracketInch = $params['bracket_inch'] ?? '5';
$bracketSize = $params['BRACKET_SIZE'] ?? $this->calculateBracketSize(100, $bracketInch);
$productType = $params['product_type'] ?? 'screen';
$quantity = (int) ($params['QTY'] ?? 1);
// 1. 감기샤프트
$shaftSize = $bracketInch;
$shaftLength = ceil($width / 1000); // mm → m 변환 후 올림
$shaftPrice = $this->getShaftPrice($shaftSize, $shaftLength);
if ($shaftPrice > 0) {
$items[] = [
'category' => 'parts',
'item_name' => "감기샤프트 {$shaftSize}인치",
'specification' => "{$shaftLength}m",
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $shaftPrice,
'total_price' => $shaftPrice * $quantity,
];
}
// 2. 각파이프
$pipeThickness = '1.4';
$pipeLength = $width > 3000 ? 6000 : 3000;
$pipePrice = $this->getPipePrice($pipeThickness, $pipeLength);
if ($pipePrice > 0) {
$items[] = [
'category' => 'parts',
'item_name' => '각파이프',
'specification' => "{$pipeThickness}T {$pipeLength}mm",
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $pipePrice,
'total_price' => $pipePrice * $quantity,
];
}
// 3. 앵글
$angleType = $productType === 'steel' ? '철재용' : '스크린용';
$angleSpec = $bracketSize === '690*390' ? '앵글4T' : '앵글3T';
$anglePrice = $this->getAnglePrice($angleType, $bracketSize, $angleSpec);
if ($anglePrice > 0) {
$items[] = [
'category' => 'parts',
'item_name' => "앵글 {$angleSpec}",
'specification' => "{$angleType} {$bracketSize}",
'unit' => 'EA',
'quantity' => 2 * $quantity, // 좌우 2개
'unit_price' => $anglePrice,
'total_price' => $anglePrice * 2 * $quantity,
];
}
return $items;
}
// =========================================================================
// 전체 동적 항목 계산
// =========================================================================
/**
* 동적 항목 전체 계산
*
* @param array $inputs 입력 파라미터
* @return array 계산된 항목 배열
*/
public function calculateDynamicItems(array $inputs): array
{
$items = [];
$width = (float) ($inputs['W0'] ?? 0);
$height = (float) ($inputs['H0'] ?? 0);
$quantity = (int) ($inputs['QTY'] ?? 1);
$bracketInch = $inputs['bracket_inch'] ?? '5';
$productType = $inputs['product_type'] ?? 'screen';
// 중량 계산 (5130 로직)
$area = ($width * ($height + 550)) / 1000000;
$weight = $area * ($productType === 'steel' ? 25 : 2) + ($width / 1000) * 14.17;
// 모터 용량/브라켓 크기 계산
$motorCapacity = $this->calculateMotorCapacity($productType, $weight, $bracketInch);
$bracketSize = $this->calculateBracketSize($weight, $bracketInch);
// 입력값에 계산된 값 추가 (부자재 계산용)
$inputs['WEIGHT'] = $weight;
$inputs['MOTOR_CAPACITY'] = $motorCapacity;
$inputs['BRACKET_SIZE'] = $bracketSize;
// 1. 주자재 (스크린)
$screenResult = $this->calculateScreenPrice($width, $height);
$items[] = [
'category' => 'material',
'item_code' => 'KD-SCREEN',
'item_name' => '주자재(스크린)',
'specification' => "면적 {$screenResult['area']}",
'unit' => '㎡',
'quantity' => $screenResult['area'] * $quantity,
'unit_price' => $screenResult['unit_price'],
'total_price' => $screenResult['total_price'] * $quantity,
];
// 2. 모터
$motorPrice = $this->getMotorPrice($motorCapacity);
$items[] = [
'category' => 'motor',
'item_code' => "KD-MOTOR-{$motorCapacity}",
'item_name' => "모터 {$motorCapacity}",
'specification' => $motorCapacity,
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $motorPrice,
'total_price' => $motorPrice * $quantity,
];
// 3. 제어기
$controllerType = $inputs['controller_type'] ?? '매립형';
$controllerPrice = $this->getControllerPrice($controllerType);
$items[] = [
'category' => 'controller',
'item_code' => 'KD-CTRL-'.strtoupper($controllerType),
'item_name' => "제어기 {$controllerType}",
'specification' => $controllerType,
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $controllerPrice,
'total_price' => $controllerPrice * $quantity,
];
// 4. 절곡품
$steelItems = $this->calculateSteelItems($inputs);
$items = array_merge($items, $steelItems);
// 5. 부자재
$partItems = $this->calculatePartItems($inputs);
$items = array_merge($items, $partItems);
return $items;
}
}