Files
sam-api/app/Services/Quote/Handlers/Tenant287/FormulaHandler.php
권혁성 b0547c425f fix:FG 수식 산출 시 제품모델/설치타입/마감타입 올바르게 적용
- parseFgCode() 추가: FG 코드에서 모델/설치타입/마감타입 파싱
- calculateTenantBom() 폴백 순서: 입력값 > FG코드 파싱 > 기본값(KSS01/벽면형/SUS)
- KQTS01 제품이 KSS01 가이드레일 규격(120*70)으로 잘못 산출되던 문제 해결

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:41:21 +09:00

1134 lines
44 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\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';
}
// =========================================================================
// 주자재(스크린) 계산
// =========================================================================
/**
* 스크린 주자재 가격 계산
*
* @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;
} 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[] = $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;
}
}