2026-03-17 13:06:29 +09:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
2026-03-21 07:59:53 +09:00
|
|
|
|
use App\Models\Items\Item;
|
2026-03-17 13:06:29 +09:00
|
|
|
|
|
|
|
|
|
|
class BendingCodeService extends Service
|
|
|
|
|
|
{
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
// 제품 코드 (7종)
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
public const PRODUCTS = [
|
|
|
|
|
|
['code' => 'R', 'name' => '가이드레일(벽면형)'],
|
|
|
|
|
|
['code' => 'S', 'name' => '가이드레일(측면형)'],
|
|
|
|
|
|
['code' => 'G', 'name' => '연기차단재'],
|
|
|
|
|
|
['code' => 'B', 'name' => '하단마감재(스크린)'],
|
|
|
|
|
|
['code' => 'T', 'name' => '하단마감재(철재)'],
|
|
|
|
|
|
['code' => 'L', 'name' => 'L-Bar'],
|
|
|
|
|
|
['code' => 'C', 'name' => '케이스'],
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
// 종류 코드 + 사용 가능 제품
|
2026-03-18 19:31:30 +09:00
|
|
|
|
// 경동기업 재공품 LOT 채번 규칙 기준 (2026-03 최신)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
// =========================================================================
|
|
|
|
|
|
public const SPECS = [
|
2026-03-18 19:31:30 +09:00
|
|
|
|
['code' => 'M', 'name' => '본체', 'products' => ['R']],
|
|
|
|
|
|
['code' => 'M', 'name' => '본체디딤', 'products' => ['S']],
|
2026-03-17 13:06:29 +09:00
|
|
|
|
['code' => 'T', 'name' => '본체(철재)', 'products' => ['R', 'S']],
|
|
|
|
|
|
['code' => 'C', 'name' => 'C형', 'products' => ['R', 'S']],
|
|
|
|
|
|
['code' => 'D', 'name' => 'D형', 'products' => ['R', 'S']],
|
2026-03-18 19:31:30 +09:00
|
|
|
|
['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']],
|
2026-03-17 13:06:29 +09:00
|
|
|
|
['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']],
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
// 모양&길이 코드
|
2026-03-18 19:31:30 +09:00
|
|
|
|
// 연기차단재(G)는 SMOKE_BARRIER, 그 외는 GENERAL 사용
|
|
|
|
|
|
// 신규 길이 발생 시 코드 추가 가능 (확장형)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
// =========================================================================
|
|
|
|
|
|
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 = [
|
2026-03-18 19:31:30 +09:00
|
|
|
|
['code' => '06', 'name' => '610'],
|
2026-03-17 13:06:29 +09:00
|
|
|
|
['code' => '12', 'name' => '1219'],
|
2026-03-18 19:31:30 +09:00
|
|
|
|
['code' => '17', 'name' => '1750'],
|
|
|
|
|
|
['code' => '20', 'name' => '2000'],
|
2026-03-17 13:06:29 +09:00
|
|
|
|
['code' => '24', 'name' => '2438'],
|
|
|
|
|
|
['code' => '30', 'name' => '3000'],
|
|
|
|
|
|
['code' => '35', 'name' => '3500'],
|
|
|
|
|
|
['code' => '40', 'name' => '4000'],
|
|
|
|
|
|
['code' => '41', 'name' => '4150'],
|
2026-03-18 19:43:15 +09:00
|
|
|
|
['code' => '42', 'name' => '4200'],
|
2026-03-17 13:06:29 +09:00
|
|
|
|
['code' => '43', 'name' => '4300'],
|
2026-03-18 19:31:30 +09:00
|
|
|
|
['code' => '45', 'name' => '4500'],
|
2026-03-17 13:06:29 +09:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
// 제품+종류 → 원자재(재질) 매핑
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
public const MATERIAL_MAP = [
|
2026-03-18 19:31:30 +09:00
|
|
|
|
// 연기차단재
|
2026-03-17 13:06:29 +09:00
|
|
|
|
'G:I' => '화이바원단',
|
2026-03-18 19:31:30 +09:00
|
|
|
|
'G:H' => '화이바원단',
|
|
|
|
|
|
// 하단마감재(스크린)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
'B:S' => 'SUS 1.2T',
|
|
|
|
|
|
'B:E' => 'EGI 1.55T',
|
2026-03-18 19:31:30 +09:00
|
|
|
|
// 하단마감재(철재)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
'T:S' => 'SUS 1.2T',
|
|
|
|
|
|
'T:E' => 'EGI 1.55T',
|
2026-03-18 19:31:30 +09:00
|
|
|
|
// L-Bar
|
2026-03-17 13:06:29 +09:00
|
|
|
|
'L:A' => 'EGI 1.55T',
|
2026-03-18 19:31:30 +09:00
|
|
|
|
// 가이드레일(벽면형)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
'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',
|
2026-03-18 19:31:30 +09:00
|
|
|
|
'R:W' => 'EGI 1.55T',
|
|
|
|
|
|
'R:F' => 'SUS 1.2T',
|
|
|
|
|
|
// 가이드레일(측면형)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
'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',
|
2026-03-18 19:31:30 +09:00
|
|
|
|
'S:W' => 'EGI 1.55T',
|
|
|
|
|
|
'S:F' => 'SUS 1.2T',
|
|
|
|
|
|
// 케이스
|
2026-03-17 13:06:29 +09:00
|
|
|
|
'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,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-21 07:59:53 +09:00
|
|
|
|
* 드롭다운 선택 조합 → 품목(items) 매핑 조회
|
2026-03-19 19:54:23 +09:00
|
|
|
|
*
|
2026-03-21 07:59:53 +09:00
|
|
|
|
* 품목코드 패턴: BD-{prod}{spec}-{length} (예: BD-RC-24)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
*/
|
|
|
|
|
|
public function resolveItem(string $prodCode, string $specCode, string $lengthCode): ?array
|
|
|
|
|
|
{
|
2026-03-21 07:59:53 +09:00
|
|
|
|
$itemCode = "BD-{$prodCode}{$specCode}-{$lengthCode}";
|
|
|
|
|
|
|
|
|
|
|
|
$item = Item::where('tenant_id', $this->tenantId())
|
|
|
|
|
|
->where('code', $itemCode)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
->where('is_active', true)
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
2026-03-19 19:54:23 +09:00
|
|
|
|
if (! $item) {
|
2026-03-17 13:06:29 +09:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
2026-03-19 19:54:23 +09:00
|
|
|
|
'item_id' => $item->id,
|
|
|
|
|
|
'item_code' => $item->code,
|
2026-03-21 07:59:53 +09:00
|
|
|
|
'item_name' => $item->name,
|
|
|
|
|
|
'specification' => $item->getOption('item_spec'),
|
|
|
|
|
|
'unit' => $item->unit ?? 'EA',
|
2026-03-17 13:06:29 +09:00
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-18 20:14:19 +09:00
|
|
|
|
* LOT 번호 생성 (일련번호 없음 — 같은 날 같은 조합은 동일 LOT)
|
2026-03-17 13:06:29 +09:00
|
|
|
|
*
|
2026-03-18 20:14:19 +09:00
|
|
|
|
* 예: prod='C', spec='L', length='30', date='2026-03-18' → 'CL6318-30'
|
2026-03-17 13:06:29 +09:00
|
|
|
|
*/
|
2026-03-18 20:14:19 +09:00
|
|
|
|
public function generateLotNumber(string $prodCode, string $specCode, string $lengthCode, string $date): string
|
2026-03-17 13:06:29 +09:00
|
|
|
|
{
|
2026-03-18 20:14:19 +09:00
|
|
|
|
$dateCode = self::generateDateCode($date);
|
2026-03-17 13:06:29 +09:00
|
|
|
|
|
2026-03-18 20:14:19 +09:00
|
|
|
|
return "{$prodCode}{$specCode}{$dateCode}-{$lengthCode}";
|
2026-03-17 13:06:29 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 날짜 → 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;
|
|
|
|
|
|
}
|
2026-03-21 07:59:53 +09:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 품목 코드(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;
|
|
|
|
|
|
}
|
2026-03-17 13:06:29 +09:00
|
|
|
|
}
|