- bending_item_mappings 테이블 마이그레이션 - BendingCodeService: 코드 체계, 품목 매핑, LOT 일련번호 생성 - BendingController: code-map, resolve-item, generate-lot 엔드포인트 - StoreOrderRequest/UpdateOrderRequest: bending_lot validation 추가
183 lines
6.4 KiB
PHP
183 lines
6.4 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Orders\Order;
|
||
use App\Models\Production\BendingItemMapping;
|
||
|
||
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' => '케이스'],
|
||
];
|
||
|
||
// =========================================================================
|
||
// 종류 코드 + 사용 가능 제품
|
||
// =========================================================================
|
||
public const SPECS = [
|
||
['code' => 'M', 'name' => '본체', 'products' => ['R', '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', 'S', 'B', 'T']],
|
||
['code' => 'U', 'name' => 'SUS(마감)2', 'products' => ['S']],
|
||
['code' => 'E', 'name' => 'EGI(마감)', 'products' => ['R', 'S', 'B', 'T']],
|
||
['code' => 'I', 'name' => '화이바원단', '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']],
|
||
];
|
||
|
||
// =========================================================================
|
||
// 모양&길이 코드
|
||
// =========================================================================
|
||
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' => '12', 'name' => '1219'],
|
||
['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'],
|
||
];
|
||
|
||
// =========================================================================
|
||
// 제품+종류 → 원자재(재질) 매핑
|
||
// =========================================================================
|
||
public const MATERIAL_MAP = [
|
||
'G:I' => '화이바원단',
|
||
'B:S' => 'SUS 1.2T',
|
||
'B:E' => 'EGI 1.55T',
|
||
'T:S' => 'SUS 1.2T',
|
||
'T:E' => 'EGI 1.55T',
|
||
'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:E' => 'EGI 1.55T',
|
||
'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:E' => 'EGI 1.55T',
|
||
'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 테이블 품목 매핑 조회
|
||
*/
|
||
public function resolveItem(string $prodCode, string $specCode, string $lengthCode): ?array
|
||
{
|
||
$mapping = BendingItemMapping::where('tenant_id', $this->tenantId())
|
||
->where('prod_code', $prodCode)
|
||
->where('spec_code', $specCode)
|
||
->where('length_code', $lengthCode)
|
||
->where('is_active', true)
|
||
->with('item:id,code,name,specification,unit')
|
||
->first();
|
||
|
||
if (! $mapping || ! $mapping->item) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'item_id' => $mapping->item->id,
|
||
'item_code' => $mapping->item->code,
|
||
'item_name' => $mapping->item->name,
|
||
'specification' => $mapping->item->specification,
|
||
'unit' => $mapping->item->unit ?? 'EA',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* LOT 번호 생성 (일련번호 suffix 포함)
|
||
*
|
||
* base: 'GI6317-53' → 결과: 'GI6317-53-001'
|
||
*/
|
||
public function generateLotNumber(string $lotBase): string
|
||
{
|
||
$tenantId = $this->tenantId();
|
||
|
||
// 같은 base로 시작하는 기존 LOT 수 조회
|
||
$count = Order::withoutGlobalScopes()
|
||
->where('tenant_id', $tenantId)
|
||
->where('order_type_code', Order::TYPE_STOCK)
|
||
->where('options->bending_lot->lot_number', 'LIKE', $lotBase.'-%')
|
||
->count();
|
||
|
||
$seq = str_pad($count + 1, 3, '0', STR_PAD_LEFT);
|
||
|
||
return "{$lotBase}-{$seq}";
|
||
}
|
||
|
||
/**
|
||
* 날짜 → 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;
|
||
}
|
||
}
|