Files
sam-api/app/Helpers/Legacy5130Calculator.php
권혁성 f9cd219f67 feat: [품질관리] 품질관리서/실적신고/검사 API
- QualityDocument CRUD + 수주 연결 + 개소별 데이터 저장
- PerformanceReport 실적신고 확인/메모 API
- Inspection 검사 설정 + product_code 전파 수정
- 수주선택 API에 client_name 필드 추가
- 절곡 검사 프로파일 분리 (S1/S2/S3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 02:57:48 +09:00

510 lines
17 KiB
PHP
Raw Permalink 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\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² → 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,
];
}
}