feat: [작업지시/작업자화면] items.item 관계 로드, 부서 필터 개선, BD 재질 자동 매칭, 전개도 폭 매칭
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 논리적 데이터베이스 관계 문서
|
||||
|
||||
> **자동 생성**: 2026-03-20 11:23:51
|
||||
> **자동 생성**: 2026-03-20 16:30:28
|
||||
> **소스**: Eloquent 모델 관계 분석
|
||||
|
||||
## 📊 모델별 관계 현황
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\BendingItem;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Items\Item;
|
||||
|
||||
class BendingCodeService extends Service
|
||||
{
|
||||
@@ -128,28 +127,19 @@ public function getCodeMap(): array
|
||||
}
|
||||
|
||||
/**
|
||||
* 드롭다운 선택 조합 → bending_items 품목 매핑 조회
|
||||
* 드롭다운 선택 조합 → 품목(items) 매핑 조회
|
||||
*
|
||||
* legacy_code 패턴: BD-{prod}{spec}-{length} (예: BD-CP-30)
|
||||
* 품목코드 패턴: BD-{prod}{spec}-{length} (예: BD-RC-24)
|
||||
*/
|
||||
public function resolveItem(string $prodCode, string $specCode, string $lengthCode): ?array
|
||||
{
|
||||
// 1차: code + length_code로 조회 (신규 LOT 체계)
|
||||
$item = BendingItem::where('tenant_id', $this->tenantId())
|
||||
->where('code', 'like', "{$prodCode}{$specCode}%")
|
||||
->where('length_code', $lengthCode)
|
||||
$itemCode = "BD-{$prodCode}{$specCode}-{$lengthCode}";
|
||||
|
||||
$item = Item::where('tenant_id', $this->tenantId())
|
||||
->where('code', $itemCode)
|
||||
->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;
|
||||
}
|
||||
@@ -157,9 +147,9 @@ public function resolveItem(string $prodCode, string $specCode, string $lengthCo
|
||||
return [
|
||||
'item_id' => $item->id,
|
||||
'item_code' => $item->code,
|
||||
'item_name' => $item->item_name,
|
||||
'specification' => $item->item_spec,
|
||||
'unit' => 'EA',
|
||||
'item_name' => $item->name,
|
||||
'specification' => $item->getOption('item_spec'),
|
||||
'unit' => $item->unit ?? 'EA',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -202,4 +192,87 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Documents\Document;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Models\Documents\DocumentTemplate;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\Process;
|
||||
@@ -18,6 +17,7 @@
|
||||
use App\Models\Tenants\StockTransaction;
|
||||
use App\Services\Audit\AuditLogger;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
@@ -62,6 +62,7 @@ public function index(array $params)
|
||||
'salesOrder.client:id,name',
|
||||
'process:id,process_name,process_code,department,options',
|
||||
'items:id,work_order_id,item_id,item_name,specification,quantity,unit,status,options,sort_order,source_order_item_id',
|
||||
'items.item:id,code',
|
||||
'items.sourceOrderItem:id,order_node_id,floor_code,symbol_code',
|
||||
'items.sourceOrderItem.node:id,name,code',
|
||||
'items.materialInputs:id,work_order_id,work_order_item_id,stock_lot_id,item_id,qty,input_by,input_at',
|
||||
@@ -123,14 +124,21 @@ public function index(array $params)
|
||||
->orWhereHas('assignees', fn ($aq) => $aq->where('user_id', $userId));
|
||||
});
|
||||
} else {
|
||||
// 2차: 사용자 소속 부서의 작업지시 필터
|
||||
// 2차: 사용자 소속 부서 + 상위 부서의 작업지시 필터
|
||||
$departmentIds = DB::table('department_user')
|
||||
->where('user_id', $userId)
|
||||
->where('tenant_id', $tenantId)
|
||||
->pluck('department_id');
|
||||
|
||||
if ($departmentIds->isNotEmpty()) {
|
||||
$query->whereIn('team_id', $departmentIds);
|
||||
// 소속 부서의 상위 부서도 포함 (부서 계층 지원)
|
||||
$parentIds = DB::table('departments')
|
||||
->whereIn('id', $departmentIds)
|
||||
->whereNotNull('parent_id')
|
||||
->pluck('parent_id');
|
||||
|
||||
$allDeptIds = $departmentIds->merge($parentIds)->unique();
|
||||
$query->whereIn('team_id', $allDeptIds);
|
||||
}
|
||||
// 3차: 부서도 없으면 필터 없이 전체 노출
|
||||
}
|
||||
@@ -151,7 +159,37 @@ public function index(array $params)
|
||||
|
||||
$query->orderByDesc('created_at');
|
||||
|
||||
return $query->paginate($size, ['*'], 'page', $page);
|
||||
$result = $query->paginate($size, ['*'], 'page', $page);
|
||||
|
||||
// 작업자 화면: BENDING 카테고리 품목에 전개도 폭(bending_width) 추가
|
||||
if ($workerScreen) {
|
||||
$this->appendBendingWidths($result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* BENDING 카테고리 품목에 전개도 폭 추가
|
||||
*/
|
||||
private function appendBendingWidths($paginator): void
|
||||
{
|
||||
$bendingService = app(BendingCodeService::class);
|
||||
|
||||
foreach ($paginator->items() as $workOrder) {
|
||||
foreach ($workOrder->items as $item) {
|
||||
$itemCode = $item->item?->code;
|
||||
if (! $itemCode || ! str_starts_with($itemCode, 'BD-')) {
|
||||
continue;
|
||||
}
|
||||
$width = $bendingService->getBendingWidthByItemCode($itemCode);
|
||||
if ($width !== null) {
|
||||
$options = $item->options ?? [];
|
||||
$options['bending_width'] = $width;
|
||||
$item->setAttribute('options', $options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,6 +253,7 @@ public function show(int $id)
|
||||
'salesOrder.writer:id,name',
|
||||
'process:id,process_name,process_code,work_steps,department,options',
|
||||
'process.steps' => fn ($q) => $q->where('is_active', true)->orderBy('sort_order'),
|
||||
'items.item:id,code',
|
||||
'items.sourceOrderItem:id,order_node_id,floor_code,symbol_code',
|
||||
'items.sourceOrderItem.node:id,name,code',
|
||||
'items.materialInputs:id,work_order_id,work_order_item_id,stock_lot_id,item_id,qty,input_by,input_at',
|
||||
@@ -3806,13 +3845,59 @@ public function getMaterialsForItem(int $workOrderId, int $itemId): array
|
||||
}
|
||||
}
|
||||
|
||||
// BOM이 없으면 품목 자체를 자재로 사용
|
||||
// BOM이 없으면 BD 품목의 재질 정보로 원자재 자동 매칭
|
||||
if (empty($materialItems) && $woItem->item_id && $woItem->item) {
|
||||
$materialItems[] = [
|
||||
'item' => $woItem->item,
|
||||
'bom_qty' => 1,
|
||||
'required_qty' => $woItem->quantity ?? 1,
|
||||
];
|
||||
$itemOptions = $woItem->item->options ?? [];
|
||||
$material = $itemOptions['material'] ?? null;
|
||||
|
||||
$matchedRawItems = [];
|
||||
if ($material && preg_match('/^(\w+)\s*(\d+\.?\d*)/i', $material, $matMatch)) {
|
||||
$matType = $matMatch[1];
|
||||
$matThickness = (float) $matMatch[2];
|
||||
|
||||
// 품목명에서 제품 길이 추출 (예: 1750mm)
|
||||
$productLength = 0;
|
||||
if (preg_match('/(\d{3,5})mm/', $woItem->item->name, $lenMatch)) {
|
||||
$productLength = (int) $lenMatch[1];
|
||||
}
|
||||
|
||||
// 원자재 검색: material_type + thickness 매칭, length >= 제품길이
|
||||
$rawItems = \App\Models\Items\Item::where('tenant_id', $tenantId)
|
||||
->where('item_type', 'RM')
|
||||
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(options, '$.attributes.material_type')) = ?", [$matType])
|
||||
->whereRaw('CAST(JSON_EXTRACT(options, \'$.attributes.thickness\') AS DECIMAL(10,2)) = ?', [$matThickness])
|
||||
->get();
|
||||
|
||||
foreach ($rawItems as $rawItem) {
|
||||
$rawAttrs = $rawItem->options['attributes'] ?? [];
|
||||
$rawLength = $rawAttrs['length'] ?? null;
|
||||
|
||||
// 길이 조건: 원자재 길이 >= 제품 길이 (길이 미정이면 통과)
|
||||
if ($rawLength !== null && $productLength > 0 && $rawLength < $productLength) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchedRawItems[] = $rawItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($matchedRawItems)) {
|
||||
// 매칭된 원자재를 자재 목록으로 추가
|
||||
foreach ($matchedRawItems as $rawItem) {
|
||||
$materialItems[] = [
|
||||
'item' => $rawItem,
|
||||
'bom_qty' => 1,
|
||||
'required_qty' => $woItem->quantity ?? 1,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// 매칭 실패 시 기존 동작 유지 (품목 자체를 자재로 표시)
|
||||
$materialItems[] = [
|
||||
'item' => $woItem->item,
|
||||
'bom_qty' => 1,
|
||||
'required_qty' => $woItem->quantity ?? 1,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 이미 투입된 수량 조회 (item_id별 SUM)
|
||||
|
||||
Reference in New Issue
Block a user