1.2, 1.55 => 1.6, ]; /** * SUS(스테인리스) 두께 매핑 * 5130: 1.15 → 1.2, 1.55 → 1.5 */ public const SUS_THICKNESS_MAP = [ 1.15 => 1.2, 1.55 => 1.5, ]; // ========================================================================= // 모터 용량 상수 // ========================================================================= /** * 스크린 모터 용량 테이블 * [min_weight, max_weight, min_bracket_inch, max_bracket_inch] => capacity */ public const SCREEN_MOTOR_CAPACITY = [ // 150K: 중량 ≤20, 브라켓인치 ≤6 ['weight_max' => 20, 'bracket_max' => 6, 'capacity' => '150K'], // 200K: 중량 ≤35, 브라켓인치 ≤8 ['weight_max' => 35, 'bracket_max' => 8, 'capacity' => '200K'], // 300K: 중량 ≤50, 브라켓인치 ≤10 ['weight_max' => 50, 'bracket_max' => 10, 'capacity' => '300K'], // 400K: 중량 ≤70, 브라켓인치 ≤12 ['weight_max' => 70, 'bracket_max' => 12, 'capacity' => '400K'], // 500K: 중량 ≤90, 브라켓인치 ≤14 ['weight_max' => 90, 'bracket_max' => 14, 'capacity' => '500K'], // 600K: 그 이상 ['weight_max' => PHP_INT_MAX, 'bracket_max' => PHP_INT_MAX, 'capacity' => '600K'], ]; /** * 철재 모터 용량 테이블 */ public const STEEL_MOTOR_CAPACITY = [ // 300K: 중량 ≤40, 브라켓인치 ≤8 ['weight_max' => 40, 'bracket_max' => 8, 'capacity' => '300K'], // 400K: 중량 ≤60, 브라켓인치 ≤10 ['weight_max' => 60, 'bracket_max' => 10, 'capacity' => '400K'], // 500K: 중량 ≤80, 브라켓인치 ≤12 ['weight_max' => 80, 'bracket_max' => 12, 'capacity' => '500K'], // 600K: 중량 ≤100, 브라켓인치 ≤14 ['weight_max' => 100, 'bracket_max' => 14, 'capacity' => '600K'], // 800K: 중량 ≤150, 브라켓인치 ≤16 ['weight_max' => 150, 'bracket_max' => 16, 'capacity' => '800K'], // 1000K: 그 이상 ['weight_max' => PHP_INT_MAX, 'bracket_max' => PHP_INT_MAX, 'capacity' => '1000K'], ]; /** * 브라켓 사이즈 매핑 (용량 → 사이즈) */ public const BRACKET_SIZE_MAP = [ '150K' => ['width' => 450, 'height' => 280], '200K' => ['width' => 480, 'height' => 300], '300K' => ['width' => 530, 'height' => 320], '400K' => ['width' => 530, 'height' => 320], '500K' => ['width' => 600, 'height' => 350], '600K' => ['width' => 600, 'height' => 350], '800K' => ['width' => 690, 'height' => 390], '1000K' => ['width' => 690, 'height' => 390], ]; // ========================================================================= // 두께 정규화 // ========================================================================= /** * 두께 정규화 (5130 방식) * * @param string $material 재질 (EGI, SUS) * @param float $thickness 입력 두께 * @return float 정규화된 두께 */ public static function normalizeThickness(string $material, float $thickness): float { $material = strtoupper($material); if ($material === 'EGI' && isset(self::EGI_THICKNESS_MAP[$thickness])) { return self::EGI_THICKNESS_MAP[$thickness]; } if ($material === 'SUS' && isset(self::SUS_THICKNESS_MAP[$thickness])) { return self::SUS_THICKNESS_MAP[$thickness]; } return $thickness; } // ========================================================================= // 면적 계산 // ========================================================================= /** * 면적 계산 (mm² → m²) * * 5130 방식: (length × width) / 1,000,000 * * @param float $length 길이 (mm) * @param float $width 너비 (mm) * @return float 면적 (m²) */ public static function calculateArea(float $length, float $width): float { return ($length * $width) / 1000000; } /** * 면적 계산 (개구부 → 제작 사이즈) * * @param float $W0 개구부 폭 (mm) * @param float $H0 개구부 높이 (mm) * @param string $productType 제품 유형 (screen, steel) * @return array ['W1' => 제작폭, 'H1' => 제작높이, 'area' => 면적(m²)] */ public static function calculateManufacturingSize(float $W0, float $H0, string $productType = 'screen'): array { $productType = strtolower($productType); // 마진 값 결정 if ($productType === 'steel') { $marginW = 110; $marginH = 350; } else { // screen (기본값) $marginW = 140; $marginH = 350; } $W1 = $W0 + $marginW; $H1 = $H0 + $marginH; $area = self::calculateArea($W1, $H1); return [ 'W1' => $W1, 'H1' => $H1, 'area' => $area, ]; } // ========================================================================= // 중량 계산 // ========================================================================= /** * 중량 계산 (5130 방식) * * @param float $W0 개구부 폭 (mm) * @param float $area 면적 (m²) * @param string $productType 제품 유형 (screen, steel) * @return float 중량 (kg) */ public static function calculateWeight(float $W0, float $area, string $productType = 'screen'): float { $productType = strtolower($productType); if ($productType === 'steel') { // 철재: 면적 × 25 kg/m² return $area * 25; } // 스크린: (면적 × 2) + (폭(m) × 14.17) return ($area * 2) + (($W0 / 1000) * 14.17); } // ========================================================================= // 절곡품 단가 계산 // ========================================================================= /** * 절곡품 단가 계산 (5130 getBendPlatePrice 호환) * * @param string $material 재질 (EGI, SUS) * @param float $thickness 두께 (mm) * @param float $length 길이 (mm) * @param float $width 너비 (mm) * @param int $qty 수량 * @param float $unitPricePerM2 단위 면적당 단가 (원/m²) * @return array ['area' => 면적, 'total' => 총액] */ public static function getBendPlatePrice( string $material, float $thickness, float $length, float $width, int $qty, float $unitPricePerM2 ): array { // 1. 두께 정규화 $normalizedThickness = self::normalizeThickness($material, $thickness); // 2. 면적 계산 (mm² → m²) $areaM2 = self::calculateArea($length, $width); // 3. 총액 계산 (절삭 - Math.floor 호환) $total = floor($unitPricePerM2 * $areaM2 * $qty); return [ 'material' => $material, 'original_thickness' => $thickness, 'normalized_thickness' => $normalizedThickness, 'length' => $length, 'width' => $width, 'area_m2' => $areaM2, 'qty' => $qty, 'unit_price_per_m2' => $unitPricePerM2, 'total' => $total, ]; } // ========================================================================= // 모터 용량 계산 // ========================================================================= /** * 모터 용량 계산 (5130 calculateMotorSpec 호환) * * @param float $weight 중량 (kg) * @param float $bracketInch 브라켓 인치 * @param string $productType 제품 유형 (screen, steel) * @return array ['capacity' => 용량, 'bracket_size' => 브라켓 사이즈] */ public static function calculateMotorSpec(float $weight, float $bracketInch, string $productType = 'screen'): array { $productType = strtolower($productType); // 용량 테이블 선택 $capacityTable = ($productType === 'steel') ? self::STEEL_MOTOR_CAPACITY : self::SCREEN_MOTOR_CAPACITY; // 용량 결정 (경계값은 상위 용량 적용) $capacity = null; foreach ($capacityTable as $entry) { if ($weight <= $entry['weight_max'] && $bracketInch <= $entry['bracket_max']) { $capacity = $entry['capacity']; break; } } // 기본값 (마지막 항목) if ($capacity === null) { $lastEntry = end($capacityTable); $capacity = $lastEntry['capacity']; } // 브라켓 사이즈 조회 $bracketSize = self::BRACKET_SIZE_MAP[$capacity] ?? ['width' => 530, 'height' => 320]; return [ 'product_type' => $productType, 'weight' => $weight, 'bracket_inch' => $bracketInch, 'capacity' => $capacity, 'bracket_size' => $bracketSize, 'bracket_dimensions' => "{$bracketSize['width']}×{$bracketSize['height']}", ]; } /** * 품목 코드로 제품 유형 판별 (5130 방식) * * @param string $itemCode 품목 코드 (예: KS-001, ST-002) * @return string 제품 유형 (screen, steel) */ public static function detectProductType(string $itemCode): string { $prefix = strtoupper(substr($itemCode, 0, 2)); // KS, KW로 시작하면 스크린 if (in_array($prefix, ['KS', 'KW'])) { return 'screen'; } // 그 외는 철재 return 'steel'; } // ========================================================================= // 가이드레일/샤프트/파이프 계산 // ========================================================================= /** * 가이드레일 수량 계산 (5130 calculateGuidrail 호환) * * @param float $height 높이 (mm) * @param float $standardLength 기본 길이 (mm, 기본값 3490) * @return int 가이드레일 수량 */ public static function calculateGuiderailQty(float $height, float $standardLength = 3490): int { if ($standardLength <= 0) { return 1; } return (int) ceil($height / $standardLength); } // ========================================================================= // 비인정 자재 단가 계산 // ========================================================================= /** * 비인정 스크린 단가 계산 * * @param float $width 너비 (mm) * @param float $height 높이 (mm) * @param int $qty 수량 * @param float $unitPricePerM2 단위 면적당 단가 (원/m²) * @return array ['area' => 면적, 'total' => 총액] */ public static function calculateUnapprovedScreenPrice( float $width, float $height, int $qty, float $unitPricePerM2 ): array { $areaM2 = self::calculateArea($width, $height); $total = floor($unitPricePerM2 * $areaM2 * $qty); return [ 'width' => $width, 'height' => $height, 'area_m2' => $areaM2, 'qty' => $qty, 'unit_price_per_m2' => $unitPricePerM2, 'total' => $total, ]; } /** * 철재 스라트 비인정 단가 계산 * * @param string $type 유형 (방화셔터, 방범셔터, 단열셔터, 이중파이프, 조인트바) * @param float $width 너비 (mm) * @param float $height 높이 (mm) * @param int $qty 수량 * @param float $unitPrice 단가 * @return array ['surang' => 수량, 'total' => 총액] */ public static function calculateUnapprovedSteelSlatPrice( string $type, float $width, float $height, int $qty, float $unitPrice ): array { // 면적 기준 유형 $areaBased = ['방화셔터', '방범셔터', '단열셔터', '이중파이프']; if (in_array($type, $areaBased)) { // 면적 × 수량 $areaM2 = self::calculateArea($width, $height); $surang = $areaM2 * $qty; } else { // 수량 기준 (조인트바 등) $surang = $qty; } $total = floor($unitPrice * $surang); return [ 'type' => $type, 'width' => $width, 'height' => $height, 'qty' => $qty, 'surang' => $surang, 'unit_price' => $unitPrice, 'total' => $total, ]; } // ========================================================================= // 전체 견적 계산 (통합) // ========================================================================= /** * 전체 견적 계산 (5130 호환 모드) * * 5130의 계산 로직을 그대로 재현하여 동일한 결과를 반환합니다. * * @param float $W0 개구부 폭 (mm) * @param float $H0 개구부 높이 (mm) * @param int $qty 수량 * @param string $productType 제품 유형 (screen, steel) * @return array 계산 결과 */ public static function calculateEstimate( float $W0, float $H0, int $qty, string $productType = 'screen' ): array { // 1. 제작 사이즈 계산 $size = self::calculateManufacturingSize($W0, $H0, $productType); $W1 = $size['W1']; $H1 = $size['H1']; $area = $size['area']; // 2. 중량 계산 $weight = self::calculateWeight($W0, $area, $productType); // 3. 브라켓 인치 계산 (폭 기준, 25.4mm = 1inch) $bracketInch = ceil($W1 / 25.4); // 4. 모터 용량 계산 $motorSpec = self::calculateMotorSpec($weight, $bracketInch, $productType); return [ 'input' => [ 'W0' => $W0, 'H0' => $H0, 'qty' => $qty, 'product_type' => $productType, ], 'calculated' => [ 'W1' => $W1, 'H1' => $H1, 'area_m2' => round($area, 4), 'weight_kg' => round($weight, 2), 'bracket_inch' => $bracketInch, ], 'motor' => $motorSpec, ]; } // ========================================================================= // 검증 유틸리티 // ========================================================================= /** * 5130 계산 결과와 비교 검증 * * @param array $samResult SAM 계산 결과 * @param array $legacy5130Result 5130 계산 결과 * @param float $tolerance 허용 오차 (기본 0.01 = 1%) * @return array ['match' => bool, 'differences' => array] */ public static function validateAgainstLegacy( array $samResult, array $legacy5130Result, float $tolerance = 0.01 ): array { $differences = []; // 비교할 필드 목록 $compareFields = ['W1', 'H1', 'area_m2', 'weight_kg', 'total']; foreach ($compareFields as $field) { $samValue = $samResult[$field] ?? $samResult['calculated'][$field] ?? null; $legacyValue = $legacy5130Result[$field] ?? null; if ($samValue === null || $legacyValue === null) { continue; } $diff = abs($samValue - $legacyValue); $percentDiff = $legacyValue != 0 ? ($diff / abs($legacyValue)) : ($diff > 0 ? 1 : 0); if ($percentDiff > $tolerance) { $differences[$field] = [ 'sam' => $samValue, 'legacy' => $legacyValue, 'diff' => $diff, 'percent_diff' => round($percentDiff * 100, 2).'%', ]; } } return [ 'match' => empty($differences), 'differences' => $differences, ]; } }