Files
sam-api/app/Services/Production/BendingInfoBuilder.php
권혁성 3ab4f24bb4 fix(WEB): 철재 모터용량/셔터박스 계산 레거시 일치 수정
- FormulaHandler: 철재 면적 공식 W1×(H1+550) → W1×H1 (레거시 Slat_updateCol12and13 동일)
- FormulaHandler: 샤프트 인치 자동계산 추가 (레거시 Slat_updateCol22 동일)
- BendingInfoBuilder: 셔터박스 크기를 모터용량→브라켓→박스 매핑으로 결정
  (BOM 원자재 코드 BD-케이스-500*380 대신 조립 크기 650*550 등 사용)

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

867 lines
30 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\Production;
use App\Models\Orders\Order;
use App\Models\Process;
use Illuminate\Support\Collection;
/**
* 수주 → 생산지시 시 절곡 공정용 bending_info JSON 자동 생성
*
* 입력: Order (rootNodes eager loaded) + processId
* 출력: BendingInfoExtended 구조의 array (work_orders.options.bending_info에 저장)
*
* @see react/src/components/production/WorkOrders/documents/bending/types.ts
*/
class BendingInfoBuilder
{
// 표준 원자재 길이 버킷 (5130 레거시 write_form.php 기준)
private const GUIDE_RAIL_LENGTHS = [2438, 3000, 3500, 4000, 4300];
private const SHUTTER_BOX_LENGTHS = [1219, 2438, 3000, 3500, 4000, 4150];
/**
* 수주의 노드/BOM 데이터로 bending_info JSON 생성
*/
/**
* @param array|null $nodeIds 특정 노드만 집계 (null이면 전체)
*/
public function build(Order $order, int $processId, ?array $nodeIds = null): ?array
{
// 1. 절곡 공정인지 확인
$process = Process::find($processId);
if (! $process || $process->process_name !== '절곡') {
return null;
}
// 2. 루트 노드 확인 (nodeIds가 있으면 해당 노드만 필터)
$nodes = $order->rootNodes;
if ($nodeIds !== null) {
$nodes = $nodes->whereIn('id', $nodeIds);
}
if ($nodes->isEmpty()) {
return null;
}
// 3. 첫 번째 루트 노드에서 product_code 파싱
$firstNode = $nodes->first();
$productCode = $firstNode->options['product_code'] ?? '';
if (empty($productCode)) {
return null;
}
$productInfo = $this->parseProductCode($productCode);
if (empty($productInfo['productCode'])) {
return null;
}
// 4. 재질 매핑
$materials = $this->getMaterialMapping(
$productInfo['productCode'],
$productInfo['finishMaterial']
);
// 5. 노드 집계 (치수별 그룹핑 + BOM 카테고리 분류)
$aggregated = $this->aggregateNodes($nodes);
// 6. bending_info 조립
return $this->assembleBendingInfo($productInfo, $materials, $aggregated);
}
/**
* product_code 파싱
* "FG-KSS02-벽면형-SUS" → productCode, guideType, finishMaterial
*/
private function parseProductCode(string $fullCode): array
{
$parts = explode('-', $fullCode);
// FG 접두사 제거
if (($parts[0] ?? '') === 'FG') {
array_shift($parts);
}
$finish = $parts[2] ?? 'EGI';
return [
'productCode' => $parts[0] ?? '',
'guideType' => $parts[1] ?? '벽면형',
'finishMaterial' => $finish === 'SUS' ? 'SUS마감' : 'EGI마감',
];
}
/**
* BOM 아이템 카테고리 분류 (item_code/item_name 패턴 매칭)
*/
private function categorizeBomItem(array $bomItem): ?string
{
$code = $bomItem['item_code'] ?? '';
$name = $bomItem['item_name'] ?? '';
if (str_starts_with($code, 'BD-가이드레일')) {
return 'guideRail';
}
if (str_starts_with($code, 'BD-케이스')) {
return 'shutterBox_case';
}
if (str_starts_with($code, 'BD-마구리')) {
return 'shutterBox_finCover';
}
if (str_contains($name, '하장바')) {
return 'bottomBar';
}
if ($code === 'EST-SMOKE-레일용') {
return 'smokeBarrier_rail';
}
if ($code === 'EST-SMOKE-케이스용') {
return 'smokeBarrier_case';
}
if (str_starts_with($code, 'BD-L-BAR')) {
return 'detail_lbar';
}
if (str_starts_with($code, 'BD-보강평철')) {
return 'detail_reinforce';
}
if (str_starts_with($code, 'EST-MOTOR-')) {
return 'motor';
}
return null;
}
/**
* 노드 집계: 치수별 그룹핑 + BOM 카테고리 분류
*/
private function aggregateNodes(Collection $nodes): array
{
$dimensionGroups = [];
$totalNodeQty = 0;
$bomCategories = [];
foreach ($nodes as $node) {
$opts = $node->options ?? [];
// 절곡 원자재 길이는 오픈 사이즈 기준 (제조치수는 +offset 포함으로 부적합)
$width = (int) ($opts['open_width'] ?? $opts['width'] ?? 0);
$height = (int) ($opts['open_height'] ?? $opts['height'] ?? 0);
$nodeQty = $node->quantity ?? 1;
$totalNodeQty += $nodeQty;
// 치수별 그룹핑
$dimKey = "{$height}_{$width}";
if (! isset($dimensionGroups[$dimKey])) {
$dimensionGroups[$dimKey] = [
'height' => (int) $height,
'width' => (int) $width,
'qty' => 0,
];
}
$dimensionGroups[$dimKey]['qty'] += $nodeQty;
// BOM 아이템 카테고리 분류 (첫 노드에서 메타데이터 추출)
$bomItems = $opts['bom_result']['items'] ?? [];
foreach ($bomItems as $bom) {
$cat = $this->categorizeBomItem($bom);
if ($cat && ! isset($bomCategories[$cat])) {
$bomCategories[$cat] = $bom;
}
}
}
return [
'dimensionGroups' => array_values($dimensionGroups),
'totalNodeQty' => $totalNodeQty,
'bomCategories' => $bomCategories,
];
}
/**
* bending_info JSON 조립 (BendingInfoExtended 구조)
*/
private function assembleBendingInfo(array $productInfo, array $materials, array $agg): array
{
$guideRailBom = $agg['bomCategories']['guideRail'] ?? null;
$caseBom = $agg['bomCategories']['shutterBox_case'] ?? null;
$finCoverBom = $agg['bomCategories']['shutterBox_finCover'] ?? null;
$lbarBom = $agg['bomCategories']['detail_lbar'] ?? null;
$motorBom = $agg['bomCategories']['motor'] ?? null;
$dimGroups = $agg['dimensionGroups'];
// 가이드레일 baseSize 추출: BD-가이드레일-KSS01-SUS-120*70 → "120*70"
$baseSize = $guideRailBom ? $this->extractBaseSize($guideRailBom['item_code'] ?? '') : '';
return [
'productCode' => $productInfo['productCode'],
'finishMaterial' => $productInfo['finishMaterial'],
'common' => $this->buildCommon($productInfo['guideType'], $baseSize, $dimGroups),
'detailParts' => $this->buildDetailParts($guideRailBom, $lbarBom, $materials),
'guideRail' => $this->buildGuideRail($productInfo['guideType'], $baseSize, $materials, $dimGroups, $productInfo['productCode']),
'bottomBar' => $this->buildBottomBar($materials, $dimGroups),
'shutterBox' => $this->buildShutterBox($caseBom, $finCoverBom, $motorBom, $baseSize, $dimGroups),
'smokeBarrier' => $this->buildSmokeBarrier($dimGroups),
];
}
// ─────────────────────────────────────────────────
// common 섹션
// ─────────────────────────────────────────────────
private function buildCommon(string $guideType, string $baseSize, array $dimGroups): array
{
return [
'kind' => $guideType.' '.str_replace('*', 'X', $baseSize),
'type' => $guideType.'('.$baseSize.')',
'lengthQuantities' => $this->heightLengthQuantities($dimGroups),
];
}
// ─────────────────────────────────────────────────
// detailParts 섹션
// ─────────────────────────────────────────────────
private function buildDetailParts(?array $guideRailBom, ?array $lbarBom, array $materials): array
{
$parts = [];
if ($guideRailBom) {
$baseSize = $this->extractBaseSize($guideRailBom['item_code'] ?? '');
$parts[] = [
'partName' => '가이드레일',
'material' => $materials['guideRailFinish'],
'barcyInfo' => $baseSize,
];
}
if ($lbarBom) {
// BD-L-BAR-KSS01-17*60 → "17*60"
$lbarSize = $this->extractLastSize($lbarBom['item_code'] ?? '');
$parts[] = [
'partName' => 'L-BAR',
'material' => $this->extractFinishFromCode($lbarBom['item_code'] ?? '', $materials),
'barcyInfo' => $lbarSize,
];
}
return $parts;
}
// ─────────────────────────────────────────────────
// guideRail 섹션
// ─────────────────────────────────────────────────
private function buildGuideRail(string $guideType, string $baseSize, array $materials, array $dimGroups, string $productCode): array
{
$heightData = $this->heightLengthData($dimGroups);
// 가이드레일은 개구부 양쪽 2개이므로 수량 ×2
foreach ($heightData as &$entry) {
$entry['quantity'] *= 2;
}
unset($entry);
$baseDims = $this->getBaseDimensions($productCode);
if ($guideType === '혼합형') {
// 혼합형: 벽면+측면 둘 다
return [
'wall' => [
'baseSize' => $baseSize,
'baseDimension' => $baseDims['wall'],
'lengthData' => $heightData,
],
'side' => [
'baseSize' => $baseSize,
'baseDimension' => $baseDims['side'],
'lengthData' => $heightData,
],
];
}
if ($guideType === '측면형') {
return [
'wall' => null,
'side' => [
'baseSize' => $baseSize,
'baseDimension' => $baseDims['side'],
'lengthData' => $heightData,
],
];
}
// 벽면형 (기본)
return [
'wall' => [
'baseSize' => $baseSize,
'baseDimension' => $baseDims['wall'],
'lengthData' => $heightData,
],
'side' => null,
];
}
// ─────────────────────────────────────────────────
// bottomBar 섹션
// ─────────────────────────────────────────────────
private function buildBottomBar(array $materials, array $dimGroups): array
{
$length3000Qty = 0;
$length4000Qty = 0;
// 레거시 Slat_updateCol49to51() 동일 로직: 오픈폭 범위별 3000/4000 조합 배분
foreach ($dimGroups as $group) {
$width = $group['width'];
$qty = $group['qty'];
[$qty3000, $qty4000] = $this->bottomBarDistribution($width);
$length3000Qty += $qty3000 * $qty;
$length4000Qty += $qty4000 * $qty;
}
return [
'material' => $materials['bottomBarFinish'],
'extraFinish' => $materials['bottomBarExtraFinish'],
'length3000Qty' => $length3000Qty,
'length4000Qty' => $length4000Qty,
];
}
// ─────────────────────────────────────────────────
// shutterBox 섹션
// ─────────────────────────────────────────────────
private function buildShutterBox(?array $caseBom, ?array $finCoverBom, ?array $motorBom, string $guideBaseSize, array $dimGroups): array
{
if (! $caseBom) {
return [];
}
// 셔터박스 크기: 모터용량 → 브라켓 → 박스 매핑 (레거시 Slat_updateCol37)
$motorCapacity = $this->extractMotorCapacity($motorBom);
$boxSize = $motorCapacity ? $this->getShutterBoxSize($motorCapacity) : null;
if (! $boxSize) {
// fallback: BOM 케이스 item_code에서 추출 (BD-케이스-500*380 → "500*380")
$boxSize = str_replace('BD-케이스-', '', $caseBom['item_code'] ?? '');
}
$boxParts = explode('*', $boxSize);
$boxWidth = (int) ($boxParts[0] ?? 0);
$boxHeight = (int) ($boxParts[1] ?? 0);
// railWidth: 가이드레일 baseSize에서 첫 번째 숫자 (120*70 → 120)
$guideParts = explode('*', $guideBaseSize);
$railWidth = (int) ($guideParts[0] ?? 0);
// 상부덮개 수량: ceil(openWidth / 1219) × nodeQty (레거시 Slat_updateCol45)
$coverQty = 0;
// 마구리 수량: nodeQty × 2 (레거시 Slat_updateCol46: col15 * 2)
$finCoverQty = 0;
foreach ($dimGroups as $group) {
$coverQty += (int) ceil($group['width'] / 1219) * $group['qty'];
$finCoverQty += $group['qty'] * 2;
}
// lengthData: 표준 원자재 조합 배분 (레거시 Slat_updateCol39to43)
$widthData = $this->shutterBoxCombinedLengthData($dimGroups);
return [
[
'size' => $boxSize,
'direction' => '양면',
'railWidth' => $railWidth,
'frontBottom' => $boxHeight,
'coverQty' => $coverQty,
'finCoverQty' => $finCoverQty,
'lengthData' => $widthData,
],
];
}
/**
* dimGroups를 조합 배분하여 셔터박스 lengthData 생성
*/
private function shutterBoxCombinedLengthData(array $dimGroups): array
{
$combined = [];
foreach ($dimGroups as $group) {
$dist = $this->shutterBoxDistribution($group['width']);
foreach ($dist as $length => $count) {
if ($count > 0) {
$combined[$length] = ($combined[$length] ?? 0) + $count * $group['qty'];
}
}
}
$result = [];
ksort($combined);
foreach ($combined as $length => $qty) {
$result[] = ['length' => $length, 'quantity' => $qty];
}
return $result;
}
/**
* 오픈폭에 따른 셔터박스 표준 원자재 조합 배분
* 레거시 5130/estimate/common/common_addrowJS.php Slat_updateCol39to43() 동일
*
* @return array<int,int> [1219 => qty, 2438 => qty, 3000 => qty, 3500 => qty, 4000 => qty, 4150 => qty]
*/
private function shutterBoxDistribution(int $openWidth): array
{
$d = [1219 => 0, 2438 => 0, 3000 => 0, 3500 => 0, 4000 => 0, 4150 => 0];
// col39 (1219mm)
if ($openWidth <= 1219) {
$d[1219] = 1;
} elseif ($openWidth > 4150 && $openWidth <= 4219) {
$d[1219] = 1;
} elseif ($openWidth > 4219 && $openWidth <= 4719) {
$d[1219] = 1;
} elseif ($openWidth > 4876 && $openWidth <= 5219) {
$d[1219] = 1;
} elseif ($openWidth > 5219 && $openWidth <= 5369) {
$d[1219] = 1;
} elseif ($openWidth > 9026 && $openWidth <= 9219) {
$d[1219] = 1;
}
// col40 (2438mm)
if ($openWidth > 1219 && $openWidth <= 2438) {
$d[2438] = 1;
} elseif ($openWidth > 4719 && $openWidth <= 4876) {
$d[2438] = 2;
} elseif ($openWidth > 5369 && $openWidth <= 5938) {
$d[2438] = 1;
} elseif ($openWidth > 6000 && $openWidth <= 6438) {
$d[2438] = 1;
} elseif ($openWidth > 6500 && $openWidth <= 6588) {
$d[2438] = 1;
} elseif ($openWidth > 8300 && $openWidth <= 8376) {
$d[2438] = 2;
} elseif ($openWidth > 8376 && $openWidth <= 8438) {
$d[2438] = 1;
} elseif ($openWidth > 8438 && $openWidth <= 8876) {
$d[2438] = 2;
} elseif ($openWidth > 9000 && $openWidth <= 9026) {
$d[2438] = 2;
} elseif ($openWidth > 9219 && $openWidth <= 9438) {
$d[2438] = 1;
} elseif ($openWidth > 10150 && $openWidth <= 10738) {
$d[2438] = 1;
}
// col41 (3000mm)
if ($openWidth > 2438 && $openWidth <= 3000) {
$d[3000] = 1;
} elseif ($openWidth > 4150 && $openWidth <= 4219) {
$d[3000] = 1;
} elseif ($openWidth > 5369 && $openWidth <= 5438) {
$d[3000] = 1;
} elseif ($openWidth > 5938 && $openWidth <= 6000) {
$d[3000] = 2;
} elseif ($openWidth > 6438 && $openWidth <= 6500) {
$d[3000] = 1;
} elseif ($openWidth > 7000 && $openWidth <= 7150) {
$d[3000] = 1;
} elseif ($openWidth > 8376 && $openWidth <= 8438) {
$d[3000] = 2;
} elseif ($openWidth > 8876 && $openWidth <= 9000) {
$d[3000] = 3;
} elseif ($openWidth > 9438 && $openWidth <= 10150) {
$d[3000] = 2;
} elseif ($openWidth > 10738 && $openWidth <= 11000) {
$d[3000] = 1;
}
// col42 (3500mm)
if ($openWidth > 3000 && $openWidth <= 3500) {
$d[3500] = 1;
} elseif ($openWidth > 4219 && $openWidth <= 4719) {
$d[3500] = 1;
} elseif ($openWidth > 5438 && $openWidth <= 5938) {
$d[3500] = 1;
} elseif ($openWidth > 6438 && $openWidth <= 6500) {
$d[3500] = 1;
} elseif ($openWidth > 6588 && $openWidth <= 7000) {
$d[3500] = 2;
} elseif ($openWidth > 7150 && $openWidth <= 7650) {
$d[3500] = 1;
} elseif ($openWidth > 8300 && $openWidth <= 8376) {
$d[3500] = 1;
} elseif ($openWidth > 9219 && $openWidth <= 9438) {
$d[3500] = 2;
} elseif ($openWidth > 9438 && $openWidth <= 9500) {
$d[3500] = 1;
}
// col43 (4000mm)
if ($openWidth > 3500 && $openWidth <= 4000) {
$d[4000] = 1;
} elseif ($openWidth > 4876 && $openWidth <= 5219) {
$d[4000] = 1;
} elseif ($openWidth > 6000 && $openWidth <= 6438) {
$d[4000] = 1;
} elseif ($openWidth > 7150 && $openWidth <= 7500) {
$d[4000] = 1;
} elseif ($openWidth > 7650 && $openWidth <= 8000) {
$d[4000] = 2;
} elseif ($openWidth > 8000 && $openWidth <= 8150) {
$d[4000] = 1;
} elseif ($openWidth > 8438 && $openWidth <= 8876) {
$d[4000] = 1;
} elseif ($openWidth > 9026 && $openWidth <= 9219) {
$d[4000] = 2;
} elseif ($openWidth > 9500 && $openWidth <= 10000) {
$d[4000] = 1;
} elseif ($openWidth > 10150 && $openWidth <= 10438) {
$d[4000] = 2;
} elseif ($openWidth > 10738 && $openWidth <= 11000) {
$d[4000] = 2;
}
// col44 (4150mm)
if ($openWidth > 4000 && $openWidth <= 4150) {
$d[4150] = 1;
} elseif ($openWidth > 5219 && $openWidth <= 5369) {
$d[4150] = 1;
} elseif ($openWidth > 6500 && $openWidth <= 6588) {
$d[4150] = 1;
} elseif ($openWidth > 7000 && $openWidth <= 7150) {
$d[4150] = 1;
} elseif ($openWidth > 7500 && $openWidth <= 7650) {
$d[4150] = 1;
} elseif ($openWidth > 8000 && $openWidth <= 8150) {
$d[4150] = 1;
} elseif ($openWidth > 8150 && $openWidth <= 8300) {
$d[4150] = 2;
} elseif ($openWidth > 9000 && $openWidth <= 9026) {
$d[4150] = 1;
} elseif ($openWidth > 10000 && $openWidth <= 10150) {
$d[4150] = 1;
} elseif ($openWidth > 10438 && $openWidth <= 10738) {
$d[4150] = 2;
}
return $d;
}
// ─────────────────────────────────────────────────
// smokeBarrier 섹션
// ─────────────────────────────────────────────────
/**
* 연기차단재 섹션 (레거시 Slat_updateCol24~29, Slat_updateCol48 동일)
*
* W50 (레일용): col24 = open_height + 250 → 범위별 표준 길이, qty = 2 × 셔터수
* W80 (케이스용): col38 = open_width + 240, col48 = floor(col38*2/3000 + 1) × 셔터수
*/
private function buildSmokeBarrier(array $dimGroups): array
{
$w50Combined = [];
$w80Qty = 0;
foreach ($dimGroups as $group) {
$height = $group['height'];
$width = $group['width'];
$nodeQty = $group['qty'];
// W50: col24 = open_height + 250 → 범위별 표준 길이
$col24 = $height + 250;
$w50Length = null;
if ($col24 <= 2438) {
$w50Length = 2438;
} elseif ($col24 <= 3000) {
$w50Length = 3000;
} elseif ($col24 <= 3500) {
$w50Length = 3500;
} elseif ($col24 <= 4000) {
$w50Length = 4000;
} elseif ($col24 <= 4300) {
$w50Length = 4300;
}
// > 4300: W50 없음
if ($w50Length !== null) {
$w50Combined[$w50Length] = ($w50Combined[$w50Length] ?? 0) + 2 * $nodeQty;
}
// W80: col38 = open_width + 240, col48 = floor(col38*2/3000 + 1) × 셔터수
$col38 = $width + 240;
$w80PerNode = (int) floor(($col38 * 2 / 3000) + 1);
$w80Qty += $w80PerNode * $nodeQty;
}
// W50 결과 조립
$w50Result = [];
ksort($w50Combined);
foreach ($w50Combined as $length => $qty) {
if ($qty > 0) {
$w50Result[] = ['length' => $length, 'quantity' => $qty];
}
}
return [
'w50' => $w50Result,
'w80Qty' => $w80Qty,
];
}
// ─────────────────────────────────────────────────
// 재질 매핑 (프론트 getMaterialMapping 동일 로직)
// ─────────────────────────────────────────────────
/**
* @see react/src/components/production/WorkOrders/documents/bending/utils.ts:77
*/
private function getMaterialMapping(string $productCode, string $finishMaterial): array
{
// Group 1: SUS 전용 (KQTS01, KSS01, KSS02)
if (in_array($productCode, ['KQTS01', 'KSS01', 'KSS02'])) {
return [
'guideRailFinish' => 'SUS 1.2T',
'bodyMaterial' => 'EGI 1.55T',
'guideRailExtraFinish' => '',
'bottomBarFinish' => 'SUS 1.5T',
'bottomBarExtraFinish' => '없음',
];
}
// Group 2: KTE01 (마감유형 분기)
if ($productCode === 'KTE01') {
$isSUS = $finishMaterial === 'SUS마감';
return [
'guideRailFinish' => 'EGI 1.55T',
'bodyMaterial' => 'EGI 1.55T',
'guideRailExtraFinish' => $isSUS ? 'SUS 1.2T' : '',
'bottomBarFinish' => 'EGI 1.55T',
'bottomBarExtraFinish' => $isSUS ? 'SUS 1.2T' : '없음',
];
}
// Group 3: 기타 (KSE01, KWE01 등)
$isSUS = str_contains($finishMaterial, 'SUS');
return [
'guideRailFinish' => 'EGI 1.55T',
'bodyMaterial' => 'EGI 1.55T',
'guideRailExtraFinish' => $isSUS ? 'SUS 1.2T' : '',
'bottomBarFinish' => 'EGI 1.55T',
'bottomBarExtraFinish' => $isSUS ? 'SUS 1.2T' : '없음',
];
}
// ─────────────────────────────────────────────────
// 유틸리티 메서드
// ─────────────────────────────────────────────────
/**
* 모터 BOM item_code에서 용량 추출
* EST-MOTOR-220V-500K → "500K"
*/
private function extractMotorCapacity(?array $motorBom): ?string
{
if (! $motorBom) {
return null;
}
$code = $motorBom['item_code'] ?? '';
if (preg_match('/(\d+K)$/', $code, $matches)) {
return $matches[1];
}
return null;
}
/**
* 모터용량 → 셔터박스 조립 크기 매핑
* 레거시 체인: 모터용량(col20) → 브라켓(col21) → 박스크기(col37)
*
* @see 5130/estimate/common/common_addrowJS.php Slat_updateCol21(), Slat_updateCol37()
*/
private function getShutterBoxSize(string $motorCapacity): string
{
return match ($motorCapacity) {
'300K', '400K' => '650*550',
'500K', '600K' => '700*600',
'800K', '1000K' => '780*650',
default => '650*550',
};
}
/**
* 가이드레일 item_code에서 baseSize 추출
* BD-가이드레일-KSS01-SUS-120*70 → "120*70"
*/
private function extractBaseSize(string $itemCode): string
{
$parts = explode('-', $itemCode);
$last = end($parts);
// 마지막 세그먼트가 "숫자*숫자" 패턴인지 확인
if (preg_match('/^\d+\*\d+$/', $last)) {
return $last;
}
return '';
}
/**
* item_code 마지막 사이즈 세그먼트 추출
* BD-L-BAR-KSS01-17*60 → "17*60"
*/
private function extractLastSize(string $itemCode): string
{
return $this->extractBaseSize($itemCode);
}
/**
* item_code에서 마감재 정보 추출 (L-BAR 등)
*/
private function extractFinishFromCode(string $itemCode, array $materials): string
{
if (str_contains($itemCode, '-SUS-')) {
return 'SUS';
}
if (str_contains($itemCode, '-EGI-')) {
return 'EGI';
}
// BOM 코드에 마감재 표시 없으면 재질 매핑에서 추출
return str_contains($materials['guideRailFinish'], 'SUS') ? 'SUS' : 'EGI';
}
/**
* height 기준 lengthQuantities (common 섹션용)
*/
private function heightLengthQuantities(array $dimGroups): array
{
$result = [];
$grouped = [];
foreach ($dimGroups as $group) {
$h = $group['height'];
$grouped[$h] = ($grouped[$h] ?? 0) + $group['qty'];
}
foreach ($grouped as $length => $qty) {
$result[] = ['length' => $length, 'quantity' => $qty];
}
return $result;
}
/**
* height 기준 lengthData (guideRail, smokeBarrier 섹션용)
* 오픈높이를 표준 원자재 길이로 버킷팅
*/
private function heightLengthData(array $dimGroups): array
{
return $this->bucketedLengthData($dimGroups, 'height', self::GUIDE_RAIL_LENGTHS);
}
/**
* width 기준 lengthData (shutterBox 섹션용)
* 오픈폭을 표준 원자재 길이로 버킷팅
*/
private function widthLengthData(array $dimGroups): array
{
return $this->bucketedLengthData($dimGroups, 'width', self::SHUTTER_BOX_LENGTHS);
}
/**
* 치수를 표준 원자재 길이로 버킷팅하여 그룹핑
*/
private function bucketedLengthData(array $dimGroups, string $dimKey, array $buckets): array
{
$result = [];
$grouped = [];
foreach ($dimGroups as $group) {
$raw = (int) $group[$dimKey];
$bucketed = $this->bucketToStandardLength($raw, $buckets);
$grouped[$bucketed] = ($grouped[$bucketed] ?? 0) + $group['qty'];
}
foreach ($grouped as $length => $qty) {
$result[] = ['length' => $length, 'quantity' => $qty];
}
return $result;
}
/**
* 오픈폭에 따른 하단마감재(하장바) 3000/4000 수량 배분
* 레거시 5130/estimate/common/common_addrowJS.php Slat_updateCol49to51() 동일
*
* @return array{0: int, 1: int} [3000mm수량, 4000mm수량]
*/
private function bottomBarDistribution(int $openWidth): array
{
// [3000mm 기본수, 4000mm 기본수]
if ($openWidth <= 3000) {
return [1, 0];
}
if ($openWidth <= 4000) {
return [0, 1];
}
if ($openWidth <= 6000) {
return [2, 0];
}
if ($openWidth <= 7000) {
return [1, 1];
}
if ($openWidth <= 8000) {
return [0, 2];
}
if ($openWidth <= 9000) {
return [3, 0];
}
if ($openWidth <= 10000) {
return [2, 1];
}
if ($openWidth <= 11000) {
return [1, 2];
}
if ($openWidth <= 12000) {
return [0, 3];
}
// 12000 초과: 4000mm 기준 올림
return [0, (int) ceil($openWidth / 4000)];
}
/**
* 제품코드별 실제 하부BASE 물리 치수 반환
* 대형 프로파일 (KQTS01, KTE01): 벽면 BASE = 135*130
* 소형 프로파일 (KSS01, KSS02 등): 벽면 BASE = 135*80
* 측면형 BASE는 항상 135*130
*/
private function getBaseDimensions(string $productCode): array
{
// 레거시: 벽면형 BASE = 135*80 (line 519), 측면형 BASE = 135*130 (line 626)
// 제품코드와 무관하게 고정
return [
'wall' => '135*80',
'side' => '135*130',
];
}
/**
* 치수를 표준 원자재 길이로 변환 (올림 버킷팅)
* dimension 이상인 최소 표준 길이 반환, 초과 시 원본 반환
*/
private function bucketToStandardLength(int $dimension, array $buckets): int
{
foreach ($buckets as $bucket) {
if ($bucket >= $dimension) {
return $bucket;
}
}
return $dimension;
}
}