array, 'context' => array] — bending_info + dynamic_bom 생성 컨텍스트 * * @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 조립 $bendingInfo = $this->assembleBendingInfo($productInfo, $materials, $aggregated); // 7. 셔터박스 크기 추출 (dynamic_bom 컨텍스트용) $caseBom = $aggregated['bomCategories']['shutterBox_case'] ?? null; $motorBom = $aggregated['bomCategories']['motor'] ?? null; $boxSize = null; if ($caseBom) { $motorCapacity = $this->extractMotorCapacity($motorBom); $boxSize = $motorCapacity ? $this->getShutterBoxSize($motorCapacity) : null; if (! $boxSize) { $boxSize = str_replace('BD-케이스-', '', $caseBom['item_code'] ?? ''); } } return [ 'bending_info' => $bendingInfo, 'context' => [ 'productCode' => $productInfo['productCode'], 'guideType' => $productInfo['guideType'], 'finishMaterial' => $productInfo['finishMaterial'], 'materials' => $materials, 'boxSize' => $boxSize, 'hasSmokeRail' => isset($aggregated['bomCategories']['smokeBarrier_rail']), 'hasSmokeCase' => isset($aggregated['bomCategories']['smokeBarrier_case']), ], ]; } /** * 개소(work_order_item) 단위 dynamic_bom 생성 * * @param array $context build() 반환값의 'context' * @param int $width 개소의 오픈폭 (mm) * @param int $height 개소의 오픈높이 (mm) * @param int $qty 개소 수량 * @param int $tenantId 테넌트 ID (item 조회용) * @return array DynamicBomEntry::toArray() 배열 */ public function buildDynamicBomForItem(array $context, int $width, int $height, int $qty, int $tenantId = 287): array { $resolver = new PrefixResolver; $entries = []; $productCode = $context['productCode']; $guideType = $context['guideType']; $finishMaterial = $context['finishMaterial']; $materials = $context['materials']; $boxSize = $context['boxSize']; $hasExtraFinish = ! empty($materials['guideRailExtraFinish']); // ─── 1. 가이드레일 세부품목 ─── $dimGroups = [['height' => $height, 'width' => $width, 'qty' => $qty]]; $heightData = $this->heightLengthData($dimGroups); // 가이드레일은 개구부 양쪽 2개이므로 수량 ×2 foreach ($heightData as &$entry) { $entry['quantity'] *= 2; } unset($entry); $guideTypes = match ($guideType) { '혼합형' => ['wall', 'side'], '측면형' => ['side'], default => ['wall'], }; $guidePartTypes = ['finish', 'body', 'c_type', 'd_type']; if ($hasExtraFinish) { $guidePartTypes[] = 'extra_finish'; } $guidePartTypes[] = 'base'; foreach ($guideTypes as $gType) { foreach ($heightData as $ld) { foreach ($guidePartTypes as $partType) { $prefix = $resolver->resolveGuideRailPrefix($partType, $gType, $productCode); if (empty($prefix)) { continue; } $itemCode = $resolver->buildItemCode($prefix, $ld['length']); if (! $itemCode) { Log::warning('BendingInfoBuilder: lengthCode 변환 실패', ['prefix' => $prefix, 'length' => $ld['length']]); continue; } $itemId = $resolver->resolveItemId($itemCode, $tenantId); if (! $itemId) { Log::warning('BendingInfoBuilder: 미등록 품목', ['code' => $itemCode]); continue; } $entries[] = new DynamicBomEntry( child_item_id: $itemId, child_item_code: $itemCode, lot_prefix: $prefix, part_type: PrefixResolver::partTypeName($partType), category: 'guideRail', material_type: $this->resolvePartMaterial($partType, $gType, $materials), length_mm: $ld['length'], qty: $ld['quantity'], ); } } } // ─── 2. 하단마감재 세부품목 ─── [$qty3000, $qty4000] = $this->bottomBarDistribution($width); $bottomLengths = array_filter([3000 => $qty3000 * $qty, 4000 => $qty4000 * $qty]); $bottomPartTypes = ['main', 'lbar', 'reinforce']; $hasBottomExtra = ! empty($materials['bottomBarExtraFinish']) && $materials['bottomBarExtraFinish'] !== '없음'; if ($hasBottomExtra) { $bottomPartTypes[] = 'extra'; } foreach ($bottomLengths as $length => $lengthQty) { if ($lengthQty <= 0) { continue; } foreach ($bottomPartTypes as $partType) { $prefix = $resolver->resolveBottomBarPrefix($partType, $productCode, $finishMaterial); $itemCode = $resolver->buildItemCode($prefix, $length); if (! $itemCode) { continue; } $itemId = $resolver->resolveItemId($itemCode, $tenantId); if (! $itemId) { Log::warning('BendingInfoBuilder: 미등록 하단마감재', ['code' => $itemCode]); continue; } $entries[] = new DynamicBomEntry( child_item_id: $itemId, child_item_code: $itemCode, lot_prefix: $prefix, part_type: PrefixResolver::partTypeName($partType), category: 'bottomBar', material_type: $materials['bottomBarFinish'], length_mm: $length, qty: $lengthQty, ); } } // ─── 3. 셔터박스 세부품목 ─── if ($boxSize) { $dist = $this->shutterBoxDistribution($width); // 상부덮개(top_cover), 마구리(fin_cover)는 1219mm 기준으로 별도 생성 (아래 256행~) $shutterPartTypes = ['front', 'lintel', 'inspection', 'rear_corner']; // 작업일지와 동일한 순서: 파트 → 길이 foreach ($shutterPartTypes as $partType) { foreach ($dist as $length => $count) { $totalCount = $count * $qty; if ($totalCount <= 0) { continue; } $prefix = $resolver->resolveShutterBoxPrefix($partType); $itemCode = $resolver->buildItemCode($prefix, $length); if (! $itemCode) { continue; } $itemId = $resolver->resolveItemId($itemCode, $tenantId); if (! $itemId) { Log::warning('BendingInfoBuilder: 미등록 셔터박스', ['code' => $itemCode]); continue; } $entries[] = new DynamicBomEntry( child_item_id: $itemId, child_item_code: $itemCode, lot_prefix: $prefix, part_type: PrefixResolver::partTypeName($partType), category: 'shutterBox', material_type: 'EGI', length_mm: $length, qty: $totalCount, ); } } // 상부덮개 수량: ceil(width / 1219) × qty (1219mm 단위) $coverQty = (int) ceil($width / 1219) * $qty; if ($coverQty > 0) { $coverPrefix = $resolver->resolveShutterBoxPrefix('top_cover'); $coverCode = $resolver->buildItemCode($coverPrefix, 1219); if ($coverCode) { $coverId = $resolver->resolveItemId($coverCode, $tenantId); if ($coverId) { $entries[] = new DynamicBomEntry( child_item_id: $coverId, child_item_code: $coverCode, lot_prefix: $coverPrefix, part_type: PrefixResolver::partTypeName('top_cover'), category: 'shutterBox', material_type: 'EGI', length_mm: 1219, qty: $coverQty, ); } } } // 마구리 수량: qty × 2 $finQty = $qty * 2; if ($finQty > 0) { $finPrefix = $resolver->resolveShutterBoxPrefix('fin_cover'); // 마구리는 박스 높이 기준이므로 셔터박스 최소 단위 사용 $finCode = $resolver->buildItemCode($finPrefix, 1219); if ($finCode) { $finId = $resolver->resolveItemId($finCode, $tenantId); if ($finId) { $entries[] = new DynamicBomEntry( child_item_id: $finId, child_item_code: $finCode, lot_prefix: $finPrefix, part_type: PrefixResolver::partTypeName('fin_cover'), category: 'shutterBox', material_type: 'EGI', length_mm: 1219, qty: $finQty, ); } } } } // ─── 4. 연기차단재 세부품목 ─── if ($context['hasSmokeRail'] || $context['hasSmokeCase']) { $smokePrefix = $resolver->resolveSmokeBarrierPrefix(); // W50 (레일용): open_height + 250 → 표준 길이 if ($context['hasSmokeRail']) { $col24 = $height + 250; $w50Length = $this->bucketToStandardLength($col24, [2438, 3000, 3500, 4000, 4300]); if ($w50Length && $col24 <= 4300) { $w50Code = $resolver->buildItemCode($smokePrefix, $w50Length, 'w50'); if ($w50Code) { $w50Id = $resolver->resolveItemId($w50Code, $tenantId); if ($w50Id) { $entries[] = new DynamicBomEntry( child_item_id: $w50Id, child_item_code: $w50Code, lot_prefix: $smokePrefix, part_type: '연기차단재(W50)', category: 'smokeBarrier', material_type: 'GI', length_mm: $w50Length, qty: 2 * $qty, ); } } } } // W80 (케이스용): floor((width+240)*2/3000 + 1) × qty if ($context['hasSmokeCase']) { $col38 = $width + 240; $w80PerNode = (int) floor(($col38 * 2 / 3000) + 1); $w80Qty = $w80PerNode * $qty; if ($w80Qty > 0) { // W80은 3000mm 기본 (레거시 동일) $w80Code = $resolver->buildItemCode($smokePrefix, 3000, 'w80'); if ($w80Code) { $w80Id = $resolver->resolveItemId($w80Code, $tenantId); if ($w80Id) { $entries[] = new DynamicBomEntry( child_item_id: $w80Id, child_item_code: $w80Code, lot_prefix: $smokePrefix, part_type: '연기차단재(W80)', category: 'smokeBarrier', material_type: 'GI', length_mm: 3000, qty: $w80Qty, ); } } } } } return DynamicBomEntry::toArrayList($entries); } /** * 파트타입 + 가이드타입 → 실제 재질 결정 */ private function resolvePartMaterial(string $partType, string $guideType, array $materials): string { return match ($partType) { 'finish' => $materials['guideRailFinish'], 'extra_finish' => $materials['guideRailExtraFinish'], 'body', 'c_type', 'd_type' => $materials['bodyMaterial'], 'base' => 'EGI', default => '', }; } /** * 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 [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; } }