Files
sam-api/app/Services/Quote/Handlers/KyungdongFormulaHandler.php
권혁성 f4a902fceb fix: FormulaEvaluatorService 슬랫 통합 및 면적/중량 공식 수정
- FormulaEvaluatorService: 슬랫 면적 공식 분리 (W0×(H0+50) vs W1×(H1+550))
- FormulaEvaluatorService: MOTOR_CAPACITY/BRACKET_SIZE 입력값 우선 처리
- KyungdongFormulaHandler: calculateDynamicItems 면적/중량 제품타입별 분기
- KyungdongFormulaHandler: normalizeGuideType() 추가 (벽면↔벽면형 호환)
- KyungdongFormulaHandler: guide_rail_spec 파라미터 별칭 지원
- 검증: 스크린/슬랫 5치수×3수량 전체 5130 정합성 확인 (±1원 이내)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:39:53 +09:00

1029 lines
38 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\Services\Quote\EstimatePriceService;
/**
* 경동기업 전용 견적 계산 핸들러
*
* 5130 레거시 시스템의 견적 로직을 SAM에 구현
* tenant_id = 287 전용
*/
class KyungdongFormulaHandler
{
private const TENANT_ID = 287;
private EstimatePriceService $priceService;
public function __construct(?EstimatePriceService $priceService = null)
{
$this->priceService = $priceService ?? new EstimatePriceService(self::TENANT_ID);
}
// =========================================================================
// 모터 용량 계산
// =========================================================================
/**
* 모터 용량 계산 (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
{
// 면적 계산: W0 × (H0 + 550) / 1,000,000
// 5130 공식: col10 × (col11 + 550) / 1,000,000
$calculateHeight = $height + 550;
$area = ($width * $calculateHeight) / 1000000;
// 원자재 단가 조회 (실리카/스크린)
$unitPrice = $this->getRawMaterialPrice('실리카');
// 5130 동일: round(area, 2) 후 단가 곱셈
$roundedArea = round($area, 2);
return [
'unit_price' => $unitPrice,
'area' => $roundedArea,
'total_price' => round($unitPrice * $roundedArea),
];
}
/**
* 슬랫(철재) 주자재 가격 계산
* 5130 공식: W0 × (H0 + 50) / 1,000,000 × 단가
*
* @return array [unit_price, area, total_price]
*/
public function calculateSlatPrice(float $width, float $height): array
{
$calculateHeight = $height + 50;
$area = ($width * $calculateHeight) / 1000000;
// 원자재 단가 조회 (방화/슬랫)
$unitPrice = $this->getRawMaterialPrice('방화');
$roundedArea = round($area, 2);
return [
'unit_price' => $unitPrice,
'area' => $roundedArea,
'total_price' => round($unitPrice * $roundedArea),
];
}
// =========================================================================
// 단가 조회 메서드 (EstimatePriceService 사용)
// =========================================================================
/**
* 원자재 단가 조회
*/
public function getRawMaterialPrice(string $materialName): float
{
return $this->priceService->getRawMaterialPrice($materialName);
}
/**
* 모터 단가 조회
*/
public function getMotorPrice(string $motorCapacity): float
{
return $this->priceService->getMotorPrice($motorCapacity);
}
/**
* 제어기 단가 조회
*/
public function getControllerPrice(string $controllerType): float
{
return $this->priceService->getControllerPrice($controllerType);
}
/**
* 샤프트 단가 조회
*/
public function getShaftPrice(string $size, float $length): float
{
return $this->priceService->getShaftPrice($size, $length);
}
/**
* 5130 고정 샤프트 제품 규격 매핑
* col59~65: 3인치 300, 4인치 3000/4500/6000, 5인치 6000/7000/8200
*
* @param string $size 인치 (3, 4, 5)
* @param float $lengthMm W0 올림값 (mm)
* @return float 매핑된 길이 (m 단위), 0이면 매핑 불가
*/
private function mapShaftToFixedProduct(string $size, float $lengthMm): float
{
$products = match ($size) {
'3' => [300],
'4' => [3000, 4500, 6000],
'5' => [6000, 7000, 8200],
default => [6000, 7000, 8200], // 기본 5인치
};
// 올림값 이상인 제품 중 가장 작은 것 선택
foreach ($products as $productMm) {
if ($lengthMm <= $productMm) {
return $productMm / 1000; // mm → m
}
}
return 0; // 매핑 불가 (초과)
}
/**
* 파이프 단가 조회
*/
public function getPipePrice(string $thickness, int $length): float
{
return $this->priceService->getPipePrice($thickness, $length);
}
/**
* 모터 받침용 앵글 단가 조회
*
* @param string $searchOption 검색옵션 (스크린용, 철제300K 등)
*/
public function getAnglePrice(string $searchOption): float
{
return $this->priceService->getAnglePrice($searchOption);
}
/**
* 부자재용 앵글 단가 조회
*
* @param string $angleType 앵글타입 (앵글3T, 앵글4T)
* @param string $size 길이 (2.5, 10)
*/
public function getMainAnglePrice(string $angleType, string $size): float
{
return $this->priceService->getMainAnglePrice($angleType, $size);
}
// =========================================================================
// 절곡품 계산 (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);
$productType = $params['product_type'] ?? 'screen';
$modelName = $params['model_name'] ?? $params['product_model'] ?? 'KSS01';
$rawFinish = $params['finishing_type'] ?? 'SUS';
// DB에는 'SUS', 'EGI'로 저장 → 'SUS마감' → 'SUS' 변환
$finishingType = str_replace('마감', '', $rawFinish);
// 절곡품 관련 파라미터
$caseSpec = $params['case_spec'] ?? '500*380';
$caseLength = (float) ($params['case_length'] ?? ($width + 220)); // mm 단위 (레거시: W0+220)
$guideType = $this->normalizeGuideType($params['guide_type'] ?? '벽면형');
$guideSpec = $params['guide_spec'] ?? $params['guide_rail_spec'] ?? '120*70';
$guideLength = (float) ($params['guide_length'] ?? ($height + 250)) / 1000; // m 단위 (레거시: H0+250)
$bottomBarLength = (float) ($params['bottombar_length'] ?? $width) / 1000; // m 단위 (레거시: W0)
$lbarLength = (float) ($params['lbar_length'] ?? ($width + 220)) / 1000; // m 단위 (레거시: W0+220)
$flatBarLength = (float) ($params['flatbar_length'] ?? ($width + 220)) / 1000; // m 단위 (레거시: W0+220)
$weightPlateQty = (int) ($params['weight_plate_qty'] ?? 0); // 무게평철 수량
// 환봉 수량: 5130 자동계산 (col10=폭 기준)
// ≤3000→1, ≤6000→2, ≤9000→3, ≤12000→4 (× 수량)
$roundBarQty = (int) ($params['round_bar_qty'] ?? -1);
if ($roundBarQty < 0) {
if ($width <= 3000) {
$roundBarQty = 1 * $quantity;
} elseif ($width <= 6000) {
$roundBarQty = 2 * $quantity;
} elseif ($width <= 9000) {
$roundBarQty = 3 * $quantity;
} elseif ($width <= 12000) {
$roundBarQty = 4 * $quantity;
} else {
$roundBarQty = 0;
}
}
// 1. 케이스 (단가/1000 × 길이mm × 수량)
$casePrice = $this->priceService->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 = $this->priceService->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. 케이스 마구리 (단가 × 수량)
// 마구리 규격 = 케이스 규격 각 치수 + 5mm (레거시 updateCol45 공식)
$caseCapSpec = $this->convertToCaseCapSpec($caseSpec);
$caseCapPrice = $this->priceService->getCaseCapPrice($caseCapSpec);
if ($caseCapPrice > 0) {
$capQty = $quantity; // 5130: maguriPrices × $su (수량)
$items[] = [
'category' => 'steel',
'item_name' => '케이스 마구리',
'specification' => $caseCapSpec,
'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 × 수량 (좌우)
// 슬랫: 단가 × 길이m × 수량 (×2 없음)
$railSmokePrice = $this->priceService->getRailSmokeBlockPrice();
if ($railSmokePrice > 0 && $guideLength > 0) {
$railSmokeMultiplier = ($productType === 'slat') ? 1 : 2;
$railSmokeQty = $railSmokeMultiplier * $quantity;
$items[] = [
'category' => 'steel',
'item_name' => '레일용 연기차단재',
'specification' => ($railSmokeMultiplier > 1) ? "{$guideLength}m × 2" : "{$guideLength}m",
'unit' => 'm',
'quantity' => $guideLength * $railSmokeQty,
'unit_price' => $railSmokePrice,
'total_price' => round($railSmokePrice * $guideLength * $railSmokeQty),
];
}
// 6. 하장바 (단가 × 길이m × 수량)
$bottomBarPrice = $this->priceService->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 = ($productType !== 'slat') ? $this->priceService->getLBarPrice($modelName) : 0;
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 = ($productType !== 'slat') ? $this->priceService->getFlatBarPrice() : 0;
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 && $productType !== 'slat') {
$roundBarPrice = 2000;
$items[] = [
'category' => 'steel',
'item_name' => '환봉',
'specification' => '',
'unit' => 'EA',
'quantity' => $roundBarQty,
'unit_price' => $roundBarPrice,
'total_price' => $roundBarPrice * $roundBarQty,
];
}
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 가이드레일 항목 배열
*/
/**
* 모델별 가이드레일 규격 매핑
*
* BDmodels 테이블 기준:
* KSS01/02, KSE01, KWE01 → 120*70 / 120*120
* KTE01, KQTS01 → 130*75 / 130*125
* KDSS01 → 150*150 / 150*212
*/
private function getGuideRailSpecs(string $modelName): array
{
return match ($modelName) {
'KTE01', 'KQTS01' => ['wall' => '130*75', 'side' => '130*125'],
'KDSS01' => ['wall' => '150*150', 'side' => '150*212'],
default => ['wall' => '120*70', 'side' => '120*120'],
};
}
private function calculateGuideRails(
string $modelName,
string $finishingType,
string $guideType,
string $guideSpec,
float $guideLength,
int $quantity
): array {
$items = [];
if ($guideLength <= 0) {
return $items;
}
$specs = $this->getGuideRailSpecs($modelName);
$wallSpec = $specs['wall'];
$sideSpec = $specs['side'];
switch ($guideType) {
case '벽면형':
$price = $this->priceService->getGuideRailPrice($modelName, $finishingType, $wallSpec);
if ($price > 0) {
$guideQty = 2 * $quantity;
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} {$wallSpec} {$guideLength}m × 2",
'unit' => 'm',
'quantity' => $guideLength * $guideQty,
'unit_price' => $price,
'total_price' => round($price * $guideLength * $guideQty),
];
}
break;
case '측면형':
$price = $this->priceService->getGuideRailPrice($modelName, $finishingType, $sideSpec);
if ($price > 0) {
$guideQty = 2 * $quantity;
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} {$sideSpec} {$guideLength}m × 2",
'unit' => 'm',
'quantity' => $guideLength * $guideQty,
'unit_price' => $price,
'total_price' => round($price * $guideLength * $guideQty),
];
}
break;
case '혼합형':
$priceWall = $this->priceService->getGuideRailPrice($modelName, $finishingType, $wallSpec);
$priceSide = $this->priceService->getGuideRailPrice($modelName, $finishingType, $sideSpec);
if ($priceWall > 0) {
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} {$wallSpec} {$guideLength}m",
'unit' => 'm',
'quantity' => $guideLength * $quantity,
'unit_price' => $priceWall,
'total_price' => round($priceWall * $guideLength * $quantity),
];
}
if ($priceSide > 0) {
$items[] = [
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} {$sideSpec} {$guideLength}m",
'unit' => 'm',
'quantity' => $guideLength * $quantity,
'unit_price' => $priceSide,
'total_price' => round($priceSide * $guideLength * $quantity),
];
}
break;
}
return $items;
}
/**
* 가이드타입 정규화 (5130 ↔ SAM 호환)
*
* 5130: '벽면', '측면', '혼합' (col6 필드)
* SAM: '벽면형', '측면형', '혼합형' (switch case)
*/
private function normalizeGuideType(string $type): string
{
return match ($type) {
'벽면', '벽면형' => '벽면형',
'측면', '측면형' => '측면형',
'혼합', '혼합형' => '혼합형',
default => $type,
};
}
// =========================================================================
// 부자재 계산 (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. 감기샤프트 (5130: col59~65 고정 제품)
// 5130 고정 규격: 3인치→0.3m, 4인치→3/4.5/6m, 5인치→6/7/8.2m
$shaftSize = $bracketInch;
$shaftLengthMm = ceil($width / 1000) * 1000; // W0 → 올림 (mm)
$shaftLength = $this->mapShaftToFixedProduct($shaftSize, $shaftLengthMm);
$shaftPrice = $shaftLength > 0 ? $this->getShaftPrice($shaftSize, $shaftLength) : 0;
if ($shaftPrice > 0) {
$items[] = [
'category' => 'parts',
'item_name' => "감기샤프트 {$shaftSize}인치",
'specification' => "{$shaftLength}m",
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $shaftPrice,
'total_price' => $shaftPrice * $quantity,
];
}
// 2. 각파이프 (5130: col67 = col37 + 3000 × col66, col68/col69 자동계산)
$pipeThickness = '1.4';
$caseLength = (float) ($params['case_length'] ?? ($width + 220)); // col37 (mm)
$connectionCount = (int) ($params['connection_count'] ?? 0); // col66 (연결 수)
$pipeBaseLength = $caseLength + 3000 * $connectionCount; // col67
// 5130 자동계산 공식: col67 기준
$pipe3000Qty = (int) ($params['pipe_3000_qty'] ?? 0);
$pipe6000Qty = (int) ($params['pipe_6000_qty'] ?? 0);
if ($pipe3000Qty === 0 && $pipe6000Qty === 0) {
// col68: 3000mm 파이프 수량
if ($pipeBaseLength <= 9000) {
$pipe3000Qty = 3 * $quantity;
} elseif ($pipeBaseLength <= 12000) {
$pipe3000Qty = 4 * $quantity;
} elseif ($pipeBaseLength <= 15000) {
$pipe3000Qty = 5 * $quantity;
} elseif ($pipeBaseLength <= 18000) {
$pipe3000Qty = 6 * $quantity;
}
// col69: 6000mm 파이프 수량 (18000 초과 시)
if ($pipeBaseLength > 18000 && $pipeBaseLength <= 24000) {
$pipe6000Qty = 4 * $quantity;
} elseif ($pipeBaseLength > 24000 && $pipeBaseLength <= 30000) {
$pipe6000Qty = 5 * $quantity;
} elseif ($pipeBaseLength > 30000 && $pipeBaseLength <= 36000) {
$pipe6000Qty = 6 * $quantity;
} elseif ($pipeBaseLength > 36000 && $pipeBaseLength <= 42000) {
$pipe6000Qty = 7 * $quantity;
} elseif ($pipeBaseLength > 42000 && $pipeBaseLength <= 48000) {
$pipe6000Qty = 8 * $quantity;
}
}
if ($pipe3000Qty > 0) {
$pipe3000Price = $this->getPipePrice($pipeThickness, 3000);
if ($pipe3000Price > 0) {
$items[] = [
'category' => 'parts',
'item_name' => '각파이프',
'specification' => "{$pipeThickness}T 3000mm",
'unit' => 'EA',
'quantity' => $pipe3000Qty,
'unit_price' => $pipe3000Price,
'total_price' => $pipe3000Price * $pipe3000Qty,
];
}
}
if ($pipe6000Qty > 0) {
$pipe6000Price = $this->getPipePrice($pipeThickness, 6000);
if ($pipe6000Price > 0) {
$items[] = [
'category' => 'parts',
'item_name' => '각파이프',
'specification' => "{$pipeThickness}T 6000mm",
'unit' => 'EA',
'quantity' => $pipe6000Qty,
'unit_price' => $pipe6000Price,
'total_price' => $pipe6000Price * $pipe6000Qty,
];
}
}
// 3. 모터 받침용 앵글 (bracket angle)
// 5130: calculateAngle(qty, itemList, '스크린용') → col2 검색, qty × $su × 4
// 5130 슬랫: col23(앵글사이즈) 비어있으면 생략
$motorCapacity = $params['MOTOR_CAPACITY'] ?? '300K';
$bracketAngleEnabled = (bool) ($params['bracket_angle_enabled'] ?? ($productType !== 'slat'));
if ($productType === 'screen') {
$angleSearchOption = '스크린용';
} else {
// 철재/슬랫: bracketSize로 매핑
$angleSearchOption = match ($bracketSize) {
'530*320' => '철제300K',
'600*350' => '철제400K',
'690*390' => '철제800K',
default => '철제300K',
};
}
$anglePrice = $bracketAngleEnabled ? $this->getAnglePrice($angleSearchOption) : 0;
if ($anglePrice > 0) {
$angleQty = 4 * $quantity; // 5130: $su * 4
$items[] = [
'category' => 'parts',
'item_name' => '모터 받침용 앵글',
'specification' => $angleSearchOption,
'unit' => 'EA',
'quantity' => $angleQty,
'unit_price' => $anglePrice,
'total_price' => $anglePrice * $angleQty,
];
}
// 4. 부자재 앵글 (main angle)
// 스크린 5130: calculateMainAngle(1, $itemList, '앵글3T', '2.5') × col71
// 슬랫 5130: calculateMainAngle(1, $itemList, '앵글4T', '2.5') × col77
$mainAngleType = ($productType === 'slat') ? '앵글4T' : ($bracketSize === '690*390' ? '앵글4T' : '앵글3T');
$mainAngleSize = '2.5';
$mainAngleQty = (int) ($params['main_angle_qty'] ?? 2); // col71/col77, default 2 (좌우)
$mainAnglePrice = $this->getMainAnglePrice($mainAngleType, $mainAngleSize);
if ($mainAnglePrice > 0 && $mainAngleQty > 0) {
$items[] = [
'category' => 'parts',
'item_name' => "앵글 {$mainAngleType}",
'specification' => "{$mainAngleSize}m",
'unit' => 'EA',
'quantity' => $mainAngleQty,
'unit_price' => $mainAnglePrice,
'total_price' => $mainAnglePrice * $mainAngleQty,
];
}
// 5. 조인트바 (슬랫 전용, 5130: price × col76, QTY 미적용)
if ($productType === 'slat') {
$jointBarQty = (int) ($params['joint_bar_qty'] ?? 0);
if ($jointBarQty > 0) {
$jointBarPrice = $this->getRawMaterialPrice('조인트바');
if ($jointBarPrice > 0) {
$items[] = [
'category' => 'parts',
'item_name' => '조인트바',
'specification' => '',
'unit' => 'EA',
'quantity' => $jointBarQty,
'unit_price' => $jointBarPrice,
'total_price' => round($jointBarPrice * $jointBarQty),
];
}
}
}
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 로직) - 제품타입별 면적/중량 공식
if ($productType === 'slat') {
// 슬랫: W0 × (H0 + 50) / 1M, 중량 = 면적 × 25
$area = ($width * ($height + 50)) / 1000000;
$weight = $area * 25;
} else {
// 스크린/철재: W1 × (H1 + 550) / 1M
$W1 = $width + 160;
$H1 = $height + 350;
$area = ($W1 * ($H1 + 550)) / 1000000;
if ($productType === 'steel') {
$weight = $area * 25;
} else {
$weight = $area * 2 + ($width / 1000) * 14.17;
}
}
// 모터 용량/브라켓 크기 계산 (입력값 우선, 없으면 자동계산)
$motorCapacity = $inputs['MOTOR_CAPACITY'] ?? $this->calculateMotorCapacity($productType, $weight, $bracketInch);
$bracketSize = $inputs['BRACKET_SIZE'] ?? $this->calculateBracketSize($weight, $bracketInch);
// 입력값에 계산된 값 추가 (부자재 계산용)
$inputs['WEIGHT'] = $weight;
$inputs['MOTOR_CAPACITY'] = $motorCapacity;
$inputs['BRACKET_SIZE'] = $bracketSize;
// 0. 검사비 (5130: inspectionFee × col14, 기본 50,000원)
$inspectionFee = (int) ($inputs['inspection_fee'] ?? 50000);
if ($inspectionFee > 0) {
$items[] = [
'category' => 'inspection',
'item_code' => 'KD-INSPECTION',
'item_name' => '검사비',
'specification' => '',
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $inspectionFee,
'total_price' => $inspectionFee * $quantity,
];
}
// 1. 주자재 (스크린 또는 슬랫)
if ($productType === 'slat') {
$materialResult = $this->calculateSlatPrice($width, $height);
$materialName = '주자재(슬랫)';
$materialCode = 'KD-SLAT';
} else {
$materialResult = $this->calculateScreenPrice($width, $height);
$materialName = '주자재(스크린)';
$materialCode = 'KD-SCREEN';
}
$items[] = [
'category' => 'material',
'item_code' => $materialCode,
'item_name' => $materialName,
'specification' => "면적 {$materialResult['area']}",
'unit' => '㎡',
'quantity' => $materialResult['area'] * $quantity,
'unit_price' => $materialResult['unit_price'],
'total_price' => $materialResult['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. 제어기 (5130: 매립형×col15 + 노출형×col16 + 뒷박스×col17)
// 5130: 제어기 = price_매립 × col15 + price_노출 × col16 + price_뒷박스 × col17
// col15/col16/col17은 고정 수량 (QTY와 무관, $su를 곱하지 않음)
$controllerType = $inputs['controller_type'] ?? '매립형';
$controllerQty = (int) ($inputs['controller_qty'] ?? 1);
$controllerPrice = $this->getControllerPrice($controllerType);
if ($controllerPrice > 0 && $controllerQty > 0) {
$items[] = [
'category' => 'controller',
'item_code' => 'KD-CTRL-'.strtoupper($controllerType),
'item_name' => "제어기 {$controllerType}",
'specification' => $controllerType,
'unit' => 'EA',
'quantity' => $controllerQty,
'unit_price' => $controllerPrice,
'total_price' => $controllerPrice * $controllerQty,
];
}
// 뒷박스 (5130: col17 수량, QTY와 무관)
$backboxQty = (int) ($inputs['backbox_qty'] ?? 1);
if ($backboxQty > 0) {
$backboxPrice = $this->getControllerPrice('뒷박스');
if ($backboxPrice > 0) {
$items[] = [
'category' => 'controller',
'item_code' => 'KD-CTRL-BACKBOX',
'item_name' => '뒷박스',
'specification' => '',
'unit' => 'EA',
'quantity' => $backboxQty,
'unit_price' => $backboxPrice,
'total_price' => $backboxPrice * $backboxQty,
];
}
}
// 4. 절곡품
// installation_type → guide_type 매핑 (calculateSteelItems는 guide_type 사용)
if (isset($inputs['installation_type']) && ! isset($inputs['guide_type'])) {
$inputs['guide_type'] = $this->normalizeGuideType($inputs['installation_type']);
}
$steelItems = $this->calculateSteelItems($inputs);
$items = array_merge($items, $steelItems);
// 5. 부자재
$partItems = $this->calculatePartItems($inputs);
$items = array_merge($items, $partItems);
return $items;
}
/**
* 케이스 규격 → 마구리 규격 변환
*
* 레거시 updateCol45/Slat_updateCol46 공식:
* 마구리 규격 = (케이스 가로 + 5) × (케이스 세로 + 5)
* 예: 500*380 → 505*385
*/
private function convertToCaseCapSpec(string $caseSpec): string
{
if (str_contains($caseSpec, '*')) {
$parts = explode('*', $caseSpec);
$width = (int) trim($parts[0]) + 5;
$height = (int) trim($parts[1]) + 5;
return "{$width}*{$height}";
}
return $caseSpec;
}
}