feat: [작업지시/작업자화면] items.item 관계 로드, 부서 필터 개선, BD 재질 자동 매칭, 전개도 폭 매칭

This commit is contained in:
김보곤
2026-03-21 07:59:53 +09:00
parent 41177f8f6c
commit da1ea6d3b4
3 changed files with 189 additions and 31 deletions

View File

@@ -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)