Files
sam-api/app/Helpers/Legacy5130Calculator.php

510 lines
17 KiB
PHP
Raw Permalink Normal View History

<?php
namespace App\Helpers;
/**
* 5130 레거시 시스템 호환 계산기
*
* 5130 시스템의 견적 계산 로직을 SAM에서 동일하게 재현하기 위한 헬퍼 클래스입니다.
* 데이터 마이그레이션 계산 결과 동일성 검증에 사용됩니다.
*
* 주요 기능:
* - 절곡품 단가 계산 (getBendPlatePrice)
* - 모터 용량 계산 (calculateMotorSpec)
* - 두께 매핑 (normalizeThickness)
* - 면적 계산 (calculateArea)
*
* @see docs/dev_plans/5130-sam-data-migration-plan.md 섹션 4.5
*/
class Legacy5130Calculator
{
// =========================================================================
// 두께 매핑 상수
// =========================================================================
/**
* EGI(아연도금) 두께 매핑
* 5130: 1.15 1.2, 1.55 1.6
*/
public const EGI_THICKNESS_MAP = [
1.15 => 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² )
*
* 5130 방식: (length × width) / 1,000,000
*
* @param float $length 길이 (mm)
* @param float $width 너비 (mm)
* @return float 면적 ()
*/
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' => 면적()]
*/
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 면적 ()
* @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 단위 면적당 단가 (/)
* @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 단위 면적당 단가 (/)
* @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,
];
}
}