From 926a7c7da6dd931ce0ea1963e1ce134e55adb261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 14 Mar 2026 14:40:04 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20[performance]=20N+1=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=203=EA=B1=B4=20=EB=B0=B0=EC=B9=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EB=A1=9C=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorkOrderService.getMaterials(): 기존 BOM 루프 내 find() x2 제거 → 루프 전 bomItemsMap/bomChildItemsMap 일괄 사전 로드 - OrderService.createWorkOrderFromOrder(): 루프 내 DB 쿼리 x2 제거 → item_code→id, process_items 사전 배치 조회 - OrderService.checkBendingStockForOrder(): 루프 내 StockService 호출 제거 → Stock 배치 조회 후 맵 참조 --- app/Services/OrderService.php | 72 +++++++++++++++++++------------ app/Services/WorkOrderService.php | 37 +++++++++++++--- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index 3138d18..f0636ca 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -1236,7 +1236,7 @@ public function createProductionOrder(int $orderId, array $data) } } - // item_code → item_id 매핑 구축 (fallback용) + // item_code → item_id 매핑 구축 (fallback용 — N+1 방지를 위해 사전 일괄 조회) $codeToIdMap = []; if (! empty($bomItemIds)) { $codeToIdRows = DB::table('items') @@ -1250,6 +1250,38 @@ public function createProductionOrder(int $orderId, array $data) } } + // order_items의 item_code로 추가 매핑 사전 구축 (루프 내 DB 조회 방지) + $orderItemCodes = $order->items->pluck('item_code')->filter()->unique()->values()->all(); + $unmappedCodes = array_diff($orderItemCodes, array_keys($codeToIdMap)); + if (! empty($unmappedCodes)) { + $extraRows = DB::table('items') + ->where('tenant_id', $tenantId) + ->whereIn('code', $unmappedCodes) + ->whereNull('deleted_at') + ->select('id', 'code') + ->get(); + foreach ($extraRows as $row) { + $codeToIdMap[$row->code] = $row->id; + } + } + + // 사전 매핑된 item_id에 대한 process_items도 일괄 조회 + $allResolvedIds = array_values(array_unique(array_merge( + array_keys($itemProcessMap), + array_values($codeToIdMap) + ))); + $unmappedProcessIds = array_diff($allResolvedIds, array_keys($itemProcessMap)); + if (! empty($unmappedProcessIds)) { + $extraProcessItems = DB::table('process_items') + ->whereIn('item_id', $unmappedProcessIds) + ->where('is_active', true) + ->select('item_id', 'process_id') + ->get(); + foreach ($extraProcessItems as $pi) { + $itemProcessMap[$pi->item_id] = $pi->process_id; + } + } + // order_items를 공정별로 그룹화 (BOM item_id → process 매핑 활용) $itemsByProcess = []; foreach ($order->items as $orderItem) { @@ -1268,31 +1300,11 @@ public function createProductionOrder(int $orderId, array $data) } } - // 3. fallback: item_code로 items 마스터 조회 → process_items 매핑 + // 3. fallback: 사전 구축된 맵에서 item_code → process 매핑 (N+1 제거) if ($processId === null && $orderItem->item_code) { $resolvedId = $codeToIdMap[$orderItem->item_code] ?? null; - if (! $resolvedId) { - $resolvedId = DB::table('items') - ->where('tenant_id', $tenantId) - ->where('code', $orderItem->item_code) - ->whereNull('deleted_at') - ->value('id'); - if ($resolvedId) { - $codeToIdMap[$orderItem->item_code] = $resolvedId; - } - } if ($resolvedId && isset($itemProcessMap[$resolvedId])) { $processId = $itemProcessMap[$resolvedId]; - } elseif ($resolvedId) { - // process_items에서도 조회 - $pi = DB::table('process_items') - ->where('item_id', $resolvedId) - ->where('is_active', true) - ->value('process_id'); - if ($pi) { - $processId = $pi; - $itemProcessMap[$resolvedId] = $pi; - } } } @@ -1893,16 +1905,22 @@ public function checkBendingStockForOrder(int $orderId): array return []; } - $stockService = app(StockService::class); + // 배치 조회로 N+1 방지 (루프 내 개별 Stock 조회 제거) + $bendingItemIds = $bendingItems->pluck('id')->all(); + $stocksMap = \App\Models\Tenants\Stock::where('tenant_id', $tenantId) + ->whereIn('item_id', $bendingItemIds) + ->get() + ->keyBy('item_id'); + $result = []; foreach ($bendingItems as $item) { $neededQty = $itemQtyMap[$item->id]; - $stockInfo = $stockService->getAvailableStock($item->id); + $stock = $stocksMap->get($item->id); - $availableQty = $stockInfo ? (float) $stockInfo['available_qty'] : 0; - $reservedQty = $stockInfo ? (float) $stockInfo['reserved_qty'] : 0; - $stockQty = $stockInfo ? (float) $stockInfo['stock_qty'] : 0; + $availableQty = $stock ? (float) $stock->available_qty : 0; + $reservedQty = $stock ? (float) $stock->reserved_qty : 0; + $stockQty = $stock ? (float) $stock->stock_qty : 0; $shortfallQty = max(0, $neededQty - $availableQty); $result[] = [ diff --git a/app/Services/WorkOrderService.php b/app/Services/WorkOrderService.php index 50717b5..2fc5f01 100644 --- a/app/Services/WorkOrderService.php +++ b/app/Services/WorkOrderService.php @@ -1486,6 +1486,35 @@ public function getMaterials(int $workOrderId): array ->keyBy('id'); } + // ── Step 1.5: 기존 BOM용 item_id 일괄 사전 로드 (N+1 방지) ── + $bomParentItemIds = $workOrder->items->pluck('item_id')->filter()->unique()->values()->all(); + $bomItemsMap = collect(); + $bomChildItemsMap = collect(); + if (! empty($bomParentItemIds)) { + $bomItemsMap = \App\Models\Items\Item::where('tenant_id', $tenantId) + ->whereIn('id', $bomParentItemIds) + ->get() + ->keyBy('id'); + + // BOM 자식 item_id 수집 및 배치 조회 + $allChildIds = []; + foreach ($bomItemsMap as $parentItem) { + if (! empty($parentItem->bom)) { + foreach ($parentItem->bom as $bomEntry) { + if (! empty($bomEntry['child_item_id'])) { + $allChildIds[] = $bomEntry['child_item_id']; + } + } + } + } + if (! empty($allChildIds)) { + $bomChildItemsMap = \App\Models\Items\Item::where('tenant_id', $tenantId) + ->whereIn('id', array_unique($allChildIds)) + ->get() + ->keyBy('id'); + } + } + // ── Step 2: 유니크 자재 목록 수집 ── // 키: dynamic_bom → "{item_id}_{woItem_id}", 기존 BOM → "{item_id}" $uniqueMaterials = []; @@ -1527,12 +1556,11 @@ public function getMaterials(int $workOrderId): array continue; // dynamic_bom이 있으면 기존 BOM fallback 건너뜀 } - // 기존 BOM 로직 (하위 호환) + // 기존 BOM 로직 (하위 호환) — 사전 로드된 맵 사용 $materialItems = []; if ($woItem->item_id) { - $item = \App\Models\Items\Item::where('tenant_id', $tenantId) - ->find($woItem->item_id); + $item = $bomItemsMap[$woItem->item_id] ?? null; if ($item && ! empty($item->bom)) { foreach ($item->bom as $bomItem) { @@ -1543,8 +1571,7 @@ public function getMaterials(int $workOrderId): array continue; } - $childItem = \App\Models\Items\Item::where('tenant_id', $tenantId) - ->find($childItemId); + $childItem = $bomChildItemsMap[$childItemId] ?? null; if (! $childItem) { continue;