feat(WEB): 절곡 공정 BendingInfoBuilder 추가
- 수주→작업지시 시 bending_info JSON 자동 생성 서비스 - 가이드레일: qty×2 적용, baseDimension 벽면형 135*80 / 측면형 135*130 - 하단마감재: 범위별 3000/4000mm 배분 로직 - 셔터박스: coverQty/finCoverQty 계산, 조합배분 로직 - 연기차단재: W50(open_height+250 범위별), W80(floor 공식) - 레거시(viewBendingWork_slat.php) 수식 기반 구현 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
822
app/Services/Production/BendingInfoBuilder.php
Normal file
822
app/Services/Production/BendingInfoBuilder.php
Normal file
@@ -0,0 +1,822 @@
|
||||
<?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';
|
||||
}
|
||||
|
||||
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;
|
||||
$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, $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, string $guideBaseSize, array $dimGroups): array
|
||||
{
|
||||
if (! $caseBom) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// BD-케이스-500*380 → "500*380"
|
||||
$caseSize = str_replace('BD-케이스-', '', $caseBom['item_code'] ?? '');
|
||||
$caseParts = explode('*', $caseSize);
|
||||
$caseWidth = (int) ($caseParts[0] ?? 0);
|
||||
$caseHeight = (int) ($caseParts[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' => $caseSize,
|
||||
'direction' => '양면',
|
||||
'railWidth' => $railWidth,
|
||||
'frontBottom' => $caseHeight,
|
||||
'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' : '없음',
|
||||
];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// 유틸리티 메서드
|
||||
// ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 가이드레일 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user