'R', 'name' => '가이드레일(벽면형)'], ['code' => 'S', 'name' => '가이드레일(측면형)'], ['code' => 'G', 'name' => '연기차단재'], ['code' => 'B', 'name' => '하단마감재(스크린)'], ['code' => 'T', 'name' => '하단마감재(철재)'], ['code' => 'L', 'name' => 'L-Bar'], ['code' => 'C', 'name' => '케이스'], ]; // ========================================================================= // 종류 코드 + 사용 가능 제품 // 경동기업 재공품 LOT 채번 규칙 기준 (2026-03 최신) // ========================================================================= public const SPECS = [ ['code' => 'M', 'name' => '본체', 'products' => ['R']], ['code' => 'M', 'name' => '본체디딤', 'products' => ['S']], ['code' => 'T', 'name' => '본체(철재)', 'products' => ['R', 'S']], ['code' => 'C', 'name' => 'C형', 'products' => ['R', 'S']], ['code' => 'D', 'name' => 'D형', 'products' => ['R', 'S']], ['code' => 'S', 'name' => 'SUS마감재', 'products' => ['R', 'B', 'T']], ['code' => 'S', 'name' => 'SUS마감재(3)', 'products' => ['S']], ['code' => 'U', 'name' => 'SUS마감재(3)', 'products' => ['S']], ['code' => 'W', 'name' => '본체(L120)', 'products' => ['R', 'S']], ['code' => 'F', 'name' => 'SUS마감재(L120)', 'products' => ['R', 'S']], ['code' => 'E', 'name' => 'EGI', 'products' => ['B', 'T']], ['code' => 'I', 'name' => '화이바원단(W50)', 'products' => ['G']], ['code' => 'H', 'name' => '화이바원단(W80)', 'products' => ['G']], ['code' => 'A', 'name' => '스크린용', 'products' => ['L']], ['code' => 'F', 'name' => '전면부', 'products' => ['C']], ['code' => 'P', 'name' => '점검구', 'products' => ['C']], ['code' => 'L', 'name' => '린텔부', 'products' => ['C']], ['code' => 'B', 'name' => '후면코너부', 'products' => ['C']], ]; // ========================================================================= // 모양&길이 코드 // 연기차단재(G)는 SMOKE_BARRIER, 그 외는 GENERAL 사용 // 신규 길이 발생 시 코드 추가 가능 (확장형) // ========================================================================= public const LENGTHS_SMOKE_BARRIER = [ ['code' => '53', 'name' => 'W50 × 3000'], ['code' => '54', 'name' => 'W50 × 4000'], ['code' => '83', 'name' => 'W80 × 3000'], ['code' => '84', 'name' => 'W80 × 4000'], ]; public const LENGTHS_GENERAL = [ ['code' => '06', 'name' => '610'], ['code' => '12', 'name' => '1219'], ['code' => '17', 'name' => '1750'], ['code' => '20', 'name' => '2000'], ['code' => '24', 'name' => '2438'], ['code' => '30', 'name' => '3000'], ['code' => '35', 'name' => '3500'], ['code' => '40', 'name' => '4000'], ['code' => '41', 'name' => '4150'], ['code' => '42', 'name' => '4200'], ['code' => '43', 'name' => '4300'], ['code' => '45', 'name' => '4500'], ]; // ========================================================================= // 제품+종류 → 원자재(재질) 매핑 // ========================================================================= public const MATERIAL_MAP = [ // 연기차단재 'G:I' => '화이바원단', 'G:H' => '화이바원단', // 하단마감재(스크린) 'B:S' => 'SUS 1.2T', 'B:E' => 'EGI 1.55T', // 하단마감재(철재) 'T:S' => 'SUS 1.2T', 'T:E' => 'EGI 1.55T', // L-Bar 'L:A' => 'EGI 1.55T', // 가이드레일(벽면형) 'R:M' => 'EGI 1.55T', 'R:T' => 'EGI 1.55T', 'R:C' => 'EGI 1.55T', 'R:D' => 'EGI 1.55T', 'R:S' => 'SUS 1.2T', 'R:W' => 'EGI 1.55T', 'R:F' => 'SUS 1.2T', // 가이드레일(측면형) 'S:M' => 'EGI 1.55T', 'S:T' => 'EGI 1.55T', 'S:C' => 'EGI 1.55T', 'S:D' => 'EGI 1.55T', 'S:S' => 'SUS 1.2T', 'S:U' => 'SUS 1.2T', 'S:W' => 'EGI 1.55T', 'S:F' => 'SUS 1.2T', // 케이스 'C:F' => 'EGI 1.55T', 'C:P' => 'EGI 1.55T', 'C:L' => 'EGI 1.55T', 'C:B' => 'EGI 1.55T', ]; /** * 코드맵 전체 반환 (프론트엔드 드롭다운 구성용) */ public function getCodeMap(): array { return [ 'products' => self::PRODUCTS, 'specs' => self::SPECS, 'lengths' => [ 'smoke_barrier' => self::LENGTHS_SMOKE_BARRIER, 'general' => self::LENGTHS_GENERAL, ], 'material_map' => self::MATERIAL_MAP, ]; } /** * 드롭다운 선택 조합 → 품목(items) 매핑 조회 * * 품목코드 패턴: BD-{prod}{spec}-{length} (예: BD-RC-24) */ public function resolveItem(string $prodCode, string $specCode, string $lengthCode): ?array { // 1차: lot_no 앞 2자리(prod+spec) + length_code로 조회 $item = BendingItem::where('tenant_id', $this->tenantId()) ->where('lot_no', 'like', "{$prodCode}{$specCode}%") ->where('length_code', $lengthCode) ->where('is_active', true) ->first(); // 2차: legacy_code 폴백 if (! $item) { $legacyCode = "BD-{$prodCode}{$specCode}-{$lengthCode}"; $item = BendingItem::where('tenant_id', $this->tenantId()) ->where('legacy_code', $legacyCode) ->where('is_active', true) ->first(); } if (! $item) { return null; } return [ 'item_id' => $item->id, 'item_code' => $item->lot_no ?? $item->code, 'item_name' => $item->item_name, 'specification' => $item->item_spec, 'unit' => 'EA', ]; } /** * LOT 번호 생성 (일련번호 없음 — 같은 날 같은 조합은 동일 LOT) * * 예: prod='C', spec='L', length='30', date='2026-03-18' → 'CL6318-30' */ public function generateLotNumber(string $prodCode, string $specCode, string $lengthCode, string $date): string { $dateCode = self::generateDateCode($date); return "{$prodCode}{$specCode}{$dateCode}-{$lengthCode}"; } /** * 날짜 → 4자리 날짜코드 * * 2026-03-17 → '6317' * 2026-10-05 → '6A05' */ public static function generateDateCode(string $date): string { $dt = \Carbon\Carbon::parse($date); $year = $dt->year % 10; $month = $dt->month; $day = $dt->day; $monthCode = $month >= 10 ? chr(55 + $month) // 10=A, 11=B, 12=C : (string) $month; return $year.$monthCode.str_pad($day, 2, '0', STR_PAD_LEFT); } /** * 제품+종류 → 원자재(재질) 반환 */ public static function getMaterial(string $prodCode, string $specCode): ?string { return self::MATERIAL_MAP["{$prodCode}:{$specCode}"] ?? null; } /** * 품목 코드(BD-XX-YY) → 매칭되는 bending_item의 전개 폭(width_sum) 반환 * * 매칭 로직: * BD-{prod}{spec}-{length} 파싱 * → PRODUCTS/SPECS에서 item_bending, item_sep, 키워드 추출 * → bending_items 검색 → bending_data 마지막 sum = 전개 폭 */ public function getBendingWidthByItemCode(string $itemCode): ?float { if (! preg_match('/^BD-([A-Z])([A-Z])-(\d+)$/', $itemCode, $m)) { return null; } $prodCode = $m[1]; $specCode = $m[2]; // 제품명 → item_bending 추출 (가이드레일(벽면형) → 가이드레일) $productName = null; foreach (self::PRODUCTS as $p) { if ($p['code'] === $prodCode) { $productName = $p['name']; break; } } if (! $productName) { return null; } // 종류명 추출 $specName = null; foreach (self::SPECS as $s) { if ($s['code'] === $specCode && in_array($prodCode, $s['products'])) { $specName = $s['name']; break; } } if (! $specName) { return null; } // item_bending: 괄호 제거 (가이드레일(벽면형) → 가이드레일) $itemBending = preg_replace('/\(.*\)/', '', $productName); // item_sep 판단: 종류명 또는 제품명에 '철재' → 철재, 아니면 스크린 $itemSep = (str_contains($specName, '철재') || str_contains($productName, '철재')) ? '철재' : '스크린'; // bending_items 검색 $query = \App\Models\BendingItem::query() ->where('tenant_id', $this->tenantId()) ->where('item_bending', $itemBending) ->where('item_sep', $itemSep) ->whereNotNull('bending_data'); // 가이드레일: 벽면형/측면형 구분 (item_name 키워드 매칭) if (str_contains($productName, '벽면형')) { $query->where('item_name', 'LIKE', '%벽면형%'); } elseif (str_contains($productName, '측면형')) { $query->where('item_name', 'LIKE', '%측면형%'); } // 종류 키워드 매칭 (본체, C형, D형, 전면, 점검구, 린텔 등) $specKeyword = preg_replace('/\(.*\)/', '', $specName); // 본체(철재) → 본체 $query->where('item_name', 'LIKE', "%{$specKeyword}%"); // 최신 코드 우선 $bendingItem = $query->orderByDesc('code')->first(); if (! $bendingItem) { return null; } // bending_data 마지막 항목의 sum = 전개 폭 $data = $bendingItem->bending_data; if (empty($data)) { return null; } $last = end($data); return isset($last['sum']) ? (float) $last['sum'] : null; } }