Files
sam-api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php

1164 lines
45 KiB
PHP
Raw Normal View History

<?php
namespace App\Services\Quote\Handlers\Tenant287;
use App\Services\Quote\Contracts\TenantFormulaHandler;
use App\Services\Quote\EstimatePriceService;
/**
* 경동기업 (tenant_id: 287) 전용 견적 계산 핸들러
*
* 5130 레거시 시스템의 견적 로직을 SAM에 구현.
* FormulaHandlerFactory가 class_exists() 자동 발견.
*/
class FormulaHandler implements TenantFormulaHandler
{
private const TENANT_ID = 287;
private EstimatePriceService $priceService;
public function __construct(?EstimatePriceService $priceService = null)
{
$this->priceService = $priceService ?? new EstimatePriceService(self::TENANT_ID);
}
// =========================================================================
// 아이템 매핑 헬퍼 메서드
// =========================================================================
/**
* items master에서 코드로 아이템 조회 (캐싱 적용, id + name)
* seeder 재실행 ID가 변경될 있으므로 항상 DB에서 동적 조회
*
* @param string $code 아이템 코드
* @return array{id: int|null, name: string|null}
*/
private function lookupItem(string $code): array
{
static $cache = [];
if (isset($cache[$code])) {
return $cache[$code];
}
$item = \App\Models\Items\Item::where('tenant_id', self::TENANT_ID)
->where('code', $code)
->first(['id', 'name']);
if ($item === null) {
\Illuminate\Support\Facades\Log::warning('FormulaHandler: 미등록 품목 참조', [
'tenant_id' => self::TENANT_ID,
'code' => $code,
]);
}
$cache[$code] = ['id' => $item?->id, 'name' => $item?->name];
return $cache[$code];
}
/**
* items master에서 코드로 아이템 ID 조회 (캐싱 적용)
*
* @param string $code 아이템 코드
* @return int|null 아이템 ID (없으면 null)
*/
private function lookupItemId(string $code): ?int
{
return $this->lookupItem($code)['id'];
}
/**
* 아이템 배열에 item_code/item_id 매핑 추가 + 마스터 품목명 적용
*
* items 마스터에 등록된 품목이면 마스터의 name을 item_name으로 사용하고,
* 미등록 품목이면 하드코딩된 item_name을 그대로 유지
*
* @param array $item 아이템 배열
* @param string $code 아이템 코드
* @return array 매핑이 추가된 아이템 배열
*/
private function withItemMapping(array $item, string $code): array
{
$looked = $this->lookupItem($code);
$merged = array_merge($item, [
'item_code' => $code,
'item_id' => $looked['id'],
]);
// 마스터에 등록된 품목이면 마스터 name 사용
if ($looked['name']) {
$merged['item_name'] = $looked['name'];
}
return $merged;
}
/**
* 모터 용량에 따른 기본 전압 결정
* 800K 이상은 380V, 외는 220V
*
* @param string $motorCapacity 모터 용량 (: '300K', '800K')
* @return string 전압 (220V 또는 380V)
*/
private function getMotorVoltage(string $motorCapacity): string
{
$capacity = (int) str_replace(['K', '(S)'], '', $motorCapacity);
return $capacity >= 800 ? '380V' : '220V';
}
// =========================================================================
// 모터 용량 계산
// =========================================================================
/**
* 모터 용량 계산 (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';
}
/**
* 샤프트(브라켓) 인치 자동 계산
* 레거시 5130 Slat_updateCol22() 동일: W1(셔터기장) 중량으로 결정
*
* @param float $W1 제조폭 (mm)
* @param float $weight 중량 (kg)
* @return int 샤프트 인치 (4, 5, 6, 8)
*/
private function calculateShaftInch(float $W1, float $weight): int
{
if ($W1 <= 4500) {
return $weight <= 400 ? 4 : 5;
}
if ($W1 <= 5600) {
return $weight <= 600 ? 5 : 6;
}
if ($W1 <= 7800) {
return $weight <= 800 ? 6 : 8;
}
return 8;
}
// =========================================================================
// 주자재(스크린) 계산
// =========================================================================
/**
* 스크린 주자재 가격 계산
*
* @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종)
// =========================================================================
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) {
// 5130: round($shutter_price * $total_length * 1000) * $su → 단건 반올림 후 × QTY
$perUnitPrice = round(($casePrice / 1000) * $caseLength);
$itemCode = "BD-케이스-{$caseSpec}";
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '케이스',
'specification' => "{$caseSpec} {$caseLength}mm",
'unit' => 'm',
'quantity' => $caseLength / 1000 * $quantity,
'unit_price' => $casePrice,
'total_price' => $perUnitPrice * $quantity,
], $itemCode);
}
// 2. 케이스용 연기차단재 - 5130: round(단가 × 길이m) × QTY
$caseSmokePrice = $this->priceService->getCaseSmokeBlockPrice();
if ($caseSmokePrice > 0 && $caseLength > 0) {
$lengthM = $caseLength / 1000;
$perUnitSmoke = round($caseSmokePrice * $lengthM);
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '케이스용 연기차단재',
'specification' => "{$lengthM}m",
'unit' => 'm',
'quantity' => $lengthM * $quantity,
'unit_price' => $caseSmokePrice,
'total_price' => $perUnitSmoke * $quantity,
], 'BD-케이스용 연기차단재');
}
// 3. 케이스 마구리 - 5130: round(단가 × QTY)
$caseCapSpec = $this->convertToCaseCapSpec($caseSpec);
$caseCapPrice = $this->priceService->getCaseCapPrice($caseCapSpec);
if ($caseCapPrice > 0) {
$capQty = $quantity;
$itemCode = "BD-마구리-{$caseCapSpec}";
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '케이스 마구리',
'specification' => $caseCapSpec,
'unit' => 'EA',
'quantity' => $capQty,
'unit_price' => $caseCapPrice,
'total_price' => round($caseCapPrice * $capQty),
], $itemCode);
}
// 4. 가이드레일 (단가 × 길이m × 수량) - 타입별 처리
$guideItems = $this->calculateGuideRails($modelName, $finishingType, $guideType, $guideSpec, $guideLength, $quantity);
$items = array_merge($items, $guideItems);
// 5. 레일용 연기차단재 - 5130: round(단가 × 길이m) × multiplier × QTY
$railSmokePrice = $this->priceService->getRailSmokeBlockPrice();
if ($railSmokePrice > 0 && $guideLength > 0) {
$railSmokeMultiplier = ($productType === 'slat') ? 1 : 2;
$railSmokeQty = $railSmokeMultiplier * $quantity;
$perUnitRailSmoke = round($railSmokePrice * $guideLength);
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '레일용 연기차단재',
'specification' => ($railSmokeMultiplier > 1) ? "{$guideLength}m × 2" : "{$guideLength}m",
'unit' => 'm',
'quantity' => $guideLength * $railSmokeQty,
'unit_price' => $railSmokePrice,
'total_price' => $perUnitRailSmoke * $railSmokeQty,
], 'BD-가이드레일용 연기차단재');
}
// 6. 하장바 (단가 × 길이m × 수량)
$bottomBarPrice = $this->priceService->getBottomBarPrice($modelName, $finishingType);
if ($bottomBarPrice > 0 && $bottomBarLength > 0) {
// 하장바 코드: SUS→00035, EGI→00036
$bottomBarCode = ($finishingType === 'EGI') ? '00036' : '00035';
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '하장바',
'specification' => "{$modelName} {$finishingType} {$bottomBarLength}m",
'unit' => 'm',
'quantity' => $bottomBarLength * $quantity,
'unit_price' => $bottomBarPrice,
'total_price' => round($bottomBarPrice * $bottomBarLength * $quantity),
], $bottomBarCode);
}
// 7. L바 (단가 × 길이m × 수량) - 스크린 전용, 슬랫 미사용
$lbarPrice = ($productType !== 'slat') ? $this->priceService->getLBarPrice($modelName) : 0;
if ($lbarPrice > 0 && $lbarLength > 0) {
// L바 코드: BD-L-BAR-{모델}-{규격} (예: BD-L-BAR-KSS01-17*60)
// L바 규격은 모델별로 다르지만 대부분 17*60 또는 17*100
$lbarSpec = (str_contains($modelName, 'KDSS') || str_contains($modelName, 'KQT')) ? '17*100' : '17*60';
$itemCode = "BD-L-BAR-{$modelName}-{$lbarSpec}";
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => 'L바',
'specification' => "{$modelName} {$lbarLength}m",
'unit' => 'm',
'quantity' => $lbarLength * $quantity,
'unit_price' => $lbarPrice,
'total_price' => round($lbarPrice * $lbarLength * $quantity),
], $itemCode);
}
// 8. 보강평철 (단가 × 길이m × 수량) - 스크린 전용, 슬랫 미사용
$flatBarPrice = ($productType !== 'slat') ? $this->priceService->getFlatBarPrice() : 0;
if ($flatBarPrice > 0 && $flatBarLength > 0) {
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '보강평철',
'specification' => "{$flatBarLength}m",
'unit' => 'm',
'quantity' => $flatBarLength * $quantity,
'unit_price' => $flatBarPrice,
'total_price' => round($flatBarPrice * $flatBarLength * $quantity),
], 'BD-보강평철-50');
}
// 9. 무게평철12T (고정 12,000원 × 수량)
if ($weightPlateQty > 0) {
$weightPlatePrice = 12000;
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '무게평철12T',
'specification' => '12T',
'unit' => 'EA',
'quantity' => $weightPlateQty * $quantity,
'unit_price' => $weightPlatePrice,
'total_price' => $weightPlatePrice * $weightPlateQty * $quantity,
], '00021');
}
// 10. 환봉 (고정 2,000원 × 수량) - 스크린 전용, 슬랫 미사용
if ($roundBarQty > 0 && $productType !== 'slat') {
$roundBarPrice = 2000;
// 환봉 코드: 파이 규격에 따라 분기 (기본 30파이)
$roundBarPhi = (int) ($params['round_bar_phi'] ?? 30);
$roundBarCode = match ($roundBarPhi) {
35 => '90202',
45 => '90203',
50 => '90204',
default => '90201', // 30파이
};
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '환봉',
'specification' => "{$roundBarPhi}파이",
'unit' => 'EA',
'quantity' => $roundBarQty,
'unit_price' => $roundBarPrice,
'total_price' => $roundBarPrice * $roundBarQty,
], $roundBarCode);
}
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'];
// 5130: 세트가격(단가×2 또는 wall+side) → round(세트가격 × 길이m) × QTY
switch ($guideType) {
case '벽면형':
$price = $this->priceService->getGuideRailPrice($modelName, $finishingType, $wallSpec);
if ($price > 0) {
$setPrice = $price * 2; // 5130: 2개 세트 가격
$perSetTotal = round($setPrice * $guideLength);
$itemCode = "BD-가이드레일-{$modelName}-{$finishingType}-{$wallSpec}";
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} {$wallSpec} {$guideLength}m × 2",
'unit' => 'm',
'quantity' => $guideLength * 2 * $quantity,
'unit_price' => $price,
'total_price' => $perSetTotal * $quantity,
], $itemCode);
}
break;
case '측면형':
$price = $this->priceService->getGuideRailPrice($modelName, $finishingType, $sideSpec);
if ($price > 0) {
$setPrice = $price * 2; // 5130: 2개 세트 가격
$perSetTotal = round($setPrice * $guideLength);
$itemCode = "BD-가이드레일-{$modelName}-{$finishingType}-{$sideSpec}";
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => "{$modelName} {$finishingType} {$sideSpec} {$guideLength}m × 2",
'unit' => 'm',
'quantity' => $guideLength * 2 * $quantity,
'unit_price' => $price,
'total_price' => $perSetTotal * $quantity,
], $itemCode);
}
break;
case '혼합형':
$priceWall = $this->priceService->getGuideRailPrice($modelName, $finishingType, $wallSpec);
$priceSide = $this->priceService->getGuideRailPrice($modelName, $finishingType, $sideSpec);
// 5130: (wallPrice + sidePrice) → round(합산가격 × 길이m) × QTY (단일 항목)
$setPrice = ($priceWall ?: 0) + ($priceSide ?: 0);
if ($setPrice > 0) {
$perSetTotal = round($setPrice * $guideLength);
$spec = "{$modelName} {$finishingType} {$wallSpec}/{$sideSpec} {$guideLength}m";
// 혼합형은 벽면형 코드 사용 (주 가이드레일)
$itemCode = "BD-가이드레일-{$modelName}-{$finishingType}-{$wallSpec}";
$items[] = $this->withItemMapping([
'category' => 'steel',
'item_name' => '가이드레일',
'specification' => $spec,
'unit' => 'm',
'quantity' => $guideLength * 2 * $quantity,
'unit_price' => $setPrice,
'total_price' => $perSetTotal * $quantity,
], $itemCode);
}
break;
}
return $items;
}
/**
* 가이드타입 정규화 (5130 SAM 호환)
*
* 5130: '벽면', '측면', '혼합' (col6 필드)
* SAM: '벽면형', '측면형', '혼합형' (switch case)
*/
private function normalizeGuideType(string $type): string
{
return match ($type) {
'벽면', '벽면형' => '벽면형',
'측면', '측면형' => '측면형',
'혼합', '혼합형' => '혼합형',
default => $type,
};
}
// =========================================================================
// 부자재 계산 (3종)
// =========================================================================
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) {
$itemCode = "EST-SHAFT-{$shaftSize}-{$shaftLength}";
$items[] = $this->withItemMapping([
'category' => 'parts',
'item_name' => "감기샤프트 {$shaftSize}인치",
'specification' => "{$shaftLength}m",
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $shaftPrice,
'total_price' => $shaftPrice * $quantity,
], $itemCode);
}
// 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[] = $this->withItemMapping([
'category' => 'parts',
'item_name' => '각파이프',
'specification' => "{$pipeThickness}T 3000mm",
'unit' => 'EA',
'quantity' => $pipe3000Qty,
'unit_price' => $pipe3000Price,
'total_price' => $pipe3000Price * $pipe3000Qty,
], 'EST-PIPE-1.4-3000');
}
}
if ($pipe6000Qty > 0) {
$pipe6000Price = $this->getPipePrice($pipeThickness, 6000);
if ($pipe6000Price > 0) {
$items[] = $this->withItemMapping([
'category' => 'parts',
'item_name' => '각파이프',
'specification' => "{$pipeThickness}T 6000mm",
'unit' => 'EA',
'quantity' => $pipe6000Qty,
'unit_price' => $pipe6000Price,
'total_price' => $pipe6000Price * $pipe6000Qty,
], 'EST-PIPE-1.4-6000');
}
}
// 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
$itemCode = "EST-ANGLE-BRACKET-{$angleSearchOption}";
$items[] = $this->withItemMapping([
'category' => 'parts',
'item_name' => '모터 받침용 앵글',
'specification' => $angleSearchOption,
'unit' => 'EA',
'quantity' => $angleQty,
'unit_price' => $anglePrice,
'total_price' => $anglePrice * $angleQty,
], $itemCode);
}
// 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) {
// 앵글 코드: EST-ANGLE-MAIN-{타입}-{길이} (예: EST-ANGLE-MAIN-앵글3T-2.5)
$itemCode = "EST-ANGLE-MAIN-{$mainAngleType}-{$mainAngleSize}";
$items[] = $this->withItemMapping([
'category' => 'parts',
'item_name' => "앵글 {$mainAngleType}",
'specification' => "{$mainAngleSize}m",
'unit' => 'EA',
'quantity' => $mainAngleQty,
'unit_price' => $mainAnglePrice,
'total_price' => $mainAnglePrice * $mainAngleQty,
], $itemCode);
}
// 5. 조인트바 (슬랫/철재 공통, 5130: price × col76, QTY 미적용)
// 5130 레거시: 철재(KQTS01)도 슬랫 공정에서 조인트바 사용
if (in_array($productType, ['slat', 'steel'])) {
$jointBarQty = (int) ($params['joint_bar_qty'] ?? 0);
// 프론트에서 미전달 시 레거시 5130 자동 계산 공식 적용
// 5130/estimate/common/common_addrowJS.php Slat_updateCo76():
// col76 = (2 + floor((제작가로 - 500) / 1000)) * 셔터수량
if ($jointBarQty <= 0) {
$width = (float) ($params['W0'] ?? 0);
$quantity = (int) ($params['QTY'] ?? 1);
if ($width > 0) {
$jointBarQty = (2 + (int) floor(($width - 500) / 1000)) * $quantity;
}
}
if ($jointBarQty > 0) {
$jointBarPrice = $this->getRawMaterialPrice('조인트바');
if ($jointBarPrice > 0) {
$items[] = $this->withItemMapping([
'category' => 'parts',
'item_name' => '조인트바',
'specification' => '',
'unit' => 'EA',
'quantity' => $jointBarQty,
'unit_price' => $jointBarPrice,
'total_price' => round($jointBarPrice * $jointBarQty),
], '800361');
}
}
}
return $items;
}
// =========================================================================
// 전체 동적 항목 계산
// =========================================================================
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;
} elseif ($productType === 'steel') {
// 철재: W1 × H1 / 1M (레거시 Slat_updateCol12and13 동일)
$W1 = $width + 110;
$H1 = $height + 350;
$area = ($W1 * $H1) / 1000000;
$weight = $area * 25;
} else {
// 스크린: W1 × (H1 + 550) / 1M
$W1 = $width + 160;
$H1 = $height + 350;
$area = ($W1 * ($H1 + 550)) / 1000000;
$weight = $area * 2 + ($width / 1000) * 14.17;
}
// 샤프트 인치: 철재는 W1/중량 기반 자동계산 (레거시 Slat_updateCol22 동일)
if ($productType === 'steel' && ! isset($inputs['bracket_inch'])) {
$bracketInch = (string) $this->calculateShaftInch($W1 ?? ($width + 110), $weight);
}
// 모터 용량/브라켓 크기 계산 (입력값 우선, 없으면 자동계산)
$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[] = $this->withItemMapping([
'category' => 'inspection',
'item_name' => '검사비',
'specification' => '',
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $inspectionFee,
'total_price' => $inspectionFee * $quantity,
], 'EST-INSPECTION');
}
// 1. 주자재 (스크린 = 실리카, 철재/슬랫 = EGI 코일 슬랫)
// 5130: KQTS01(철재)도 슬랫 공정에서 EGI 코일로 슬랫 생산 (viewSlatWork.php 참조)
if ($productType === 'screen') {
$materialResult = $this->calculateScreenPrice($width, $height);
$materialName = '주자재(스크린)';
$screenType = $inputs['screen_type'] ?? '실리카';
$materialCode = "EST-RAW-스크린-{$screenType}";
} else {
// steel, slat 모두 슬랫(EGI 코일) 사용
$materialResult = $this->calculateSlatPrice($width, $height);
$materialName = '주자재(슬랫)';
$slatType = $inputs['slat_type'] ?? '방화';
$materialCode = "EST-RAW-슬랫-{$slatType}";
}
$items[] = $this->withItemMapping([
'category' => 'material',
'item_name' => $materialName,
'specification' => "면적 {$materialResult['area']}",
'unit' => '㎡',
'quantity' => $materialResult['area'] * $quantity,
'unit_price' => $materialResult['unit_price'],
'total_price' => $materialResult['total_price'] * $quantity,
], $materialCode);
// 2. 모터
$motorPrice = $this->getMotorPrice($motorCapacity);
// 모터 전압 (기본: 220V, 대용량은 380V)
$motorVoltage = $inputs['motor_voltage'] ?? $this->getMotorVoltage($motorCapacity);
// 모터 코드: 150K는 150K(S)만 존재
$motorCapacityCode = ($motorCapacity === '150K') ? '150K(S)' : $motorCapacity;
$motorCode = "EST-MOTOR-{$motorVoltage}-{$motorCapacityCode}";
$items[] = $this->withItemMapping([
'category' => 'motor',
'item_name' => "모터 {$motorCapacity}",
'specification' => $motorCapacity,
'unit' => 'EA',
'quantity' => $quantity,
'unit_price' => $motorPrice,
'total_price' => $motorPrice * $quantity,
], $motorCode);
// 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) {
$ctrlCode = "EST-CTRL-{$controllerType}";
$items[] = $this->withItemMapping([
'category' => 'controller',
'item_name' => "제어기 {$controllerType}",
'specification' => $controllerType,
'unit' => 'EA',
'quantity' => $controllerQty,
'unit_price' => $controllerPrice,
'total_price' => $controllerPrice * $controllerQty,
], $ctrlCode);
}
// 뒷박스 (5130: col17 수량, QTY와 무관)
$backboxQty = (int) ($inputs['backbox_qty'] ?? 1);
if ($backboxQty > 0) {
$backboxPrice = $this->getControllerPrice('뒷박스');
if ($backboxPrice > 0) {
$items[] = $this->withItemMapping([
'category' => 'controller',
'item_name' => '뒷박스',
'specification' => '',
'unit' => 'EA',
'quantity' => $backboxQty,
'unit_price' => $backboxPrice,
'total_price' => $backboxPrice * $backboxQty,
], 'EST-CTRL-뒷박스');
}
}
// 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;
}
}