From 26e33fdc1332eab848ca9c37a7ad4d6037ce2c48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 18 Mar 2026 09:22:41 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20[production-orders,order]=20=EC=83=9D?= =?UTF-8?q?=EC=82=B0=EC=A7=80=EC=8B=9C=20=EB=AA=A9=EB=A1=9D=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94=20+=20=EC=A7=84=ED=96=89=EB=A5=A0=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=88=98=EC=A0=95=20+=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=EC=B2=B4=ED=81=AC=20=EC=B7=A8=EC=86=8C=20=EA=B1=B4=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Services/OrderService.php | 3 +- app/Services/ProductionOrderService.php | 87 +++++++++++++++++-------- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index d182f206..79411dc2 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -1286,9 +1286,10 @@ public function createProductionOrder(int $orderId, array $data) throw new BadRequestHttpException(__('error.order.must_be_confirmed_for_production')); } - // 이미 생산지시가 존재하는지 확인 + // 이미 활성 생산지시가 존재하는지 확인 (취소된 건 제외) $existingWorkOrder = WorkOrder::where('tenant_id', $tenantId) ->where('sales_order_id', $orderId) + ->where('status', '!=', WorkOrder::STATUS_CANCELLED) ->first(); if ($existingWorkOrder) { diff --git a/app/Services/ProductionOrderService.php b/app/Services/ProductionOrderService.php index 638ca005..6f23f6ac 100644 --- a/app/Services/ProductionOrderService.php +++ b/app/Services/ProductionOrderService.php @@ -28,13 +28,14 @@ public function index(array $params): LengthAwarePaginator $query = Order::query() ->where('tenant_id', $tenantId) ->whereIn('status_code', self::PRODUCTION_STATUSES) - ->with(['client', 'workOrders.process', 'workOrders.assignees.user']) + ->with(['client']) ->withCount([ 'workOrders' => fn ($q) => $q->whereNotNull('process_id') ->where(fn ($q2) => $q2->whereNull('options->is_auxiliary') ->orWhere('options->is_auxiliary', false)), 'nodes', - ]); + ]) + ->withMin('workOrders', 'created_at'); // 검색어 필터 if (! empty($params['search'])) { @@ -74,24 +75,23 @@ public function index(array $params): LengthAwarePaginator $perPage = $params['per_page'] ?? 20; $result = $query->paginate($perPage); + // 작업지시 진행률을 일괄 서브쿼리로 조회 (N+1 방지) + $orderIds = $result->getCollection()->pluck('id')->toArray(); + $woProgress = $this->getWorkOrderProgressBatch($orderIds); + // 가공 필드 추가 - $result->getCollection()->transform(function (Order $order) { - $minCreatedAt = $order->workOrders->min('created_at'); + $result->getCollection()->transform(function (Order $order) use ($woProgress) { + $minCreatedAt = $order->work_orders_min_created_at; $order->production_ordered_at = $minCreatedAt - ? $minCreatedAt->format('Y-m-d') + ? \Carbon\Carbon::parse($minCreatedAt)->format('Y-m-d') : null; // 개소수 (order_nodes 수) $order->node_count = $order->nodes_count ?? 0; - // 주요 생산 공정 WO만 (구매품 + 보조 공정 제외) - $productionWOs = $this->filterMainProductionWOs($order->workOrders); - $order->work_order_progress = [ - 'total' => $productionWOs->count(), - 'completed' => $productionWOs->where('status', 'completed')->count() - + $productionWOs->where('status', 'shipped')->count(), - 'in_progress' => $productionWOs->where('status', 'in_progress')->count(), - ]; + // 주요 생산 공정 WO 진행률 (서브쿼리 결과 사용) + $progress = $woProgress[$order->id] ?? ['total' => 0, 'completed' => 0, 'in_progress' => 0]; + $order->work_order_progress = $progress; // 프론트 탭용 production_status 매핑 $order->production_status = $this->mapProductionStatus($order->status_code); @@ -109,21 +109,17 @@ public function stats(): array { $tenantId = $this->tenantId(); - $waiting = Order::where('tenant_id', $tenantId) - ->where('status_code', Order::STATUS_IN_PROGRESS) - ->count(); + $counts = Order::where('tenant_id', $tenantId) + ->whereIn('status_code', self::PRODUCTION_STATUSES) + ->selectRaw('status_code, COUNT(*) as cnt') + ->groupBy('status_code') + ->pluck('cnt', 'status_code'); - $inProduction = Order::where('tenant_id', $tenantId) - ->where('status_code', Order::STATUS_IN_PRODUCTION) - ->count(); - - $completed = Order::where('tenant_id', $tenantId) - ->whereIn('status_code', [ - Order::STATUS_PRODUCED, - Order::STATUS_SHIPPING, - Order::STATUS_SHIPPED, - ]) - ->count(); + $waiting = $counts->get(Order::STATUS_IN_PROGRESS, 0); + $inProduction = $counts->get(Order::STATUS_IN_PRODUCTION, 0); + $completed = $counts->get(Order::STATUS_PRODUCED, 0) + + $counts->get(Order::STATUS_SHIPPING, 0) + + $counts->get(Order::STATUS_SHIPPED, 0); return [ 'total' => $waiting + $inProduction + $completed, @@ -264,6 +260,43 @@ private function extractBomProcessGroups($nodes): array return array_values($groups); } + /** + * 주문 ID 목록에 대해 작업지시 진행률을 일괄 조회 (단일 쿼리) + */ + private function getWorkOrderProgressBatch(array $orderIds): array + { + if (empty($orderIds)) { + return []; + } + + $rows = \DB::table('work_orders') + ->selectRaw('sales_order_id, status, COUNT(*) as cnt') + ->whereIn('sales_order_id', $orderIds) + ->whereNotNull('process_id') + ->where(function ($q) { + $q->whereNull('options->is_auxiliary') + ->orWhere('options->is_auxiliary', false); + }) + ->whereNull('deleted_at') + ->groupBy('sales_order_id', 'status') + ->get(); + + $result = []; + foreach ($rows as $row) { + if (! isset($result[$row->sales_order_id])) { + $result[$row->sales_order_id] = ['total' => 0, 'completed' => 0, 'in_progress' => 0]; + } + $result[$row->sales_order_id]['total'] += $row->cnt; + if (in_array($row->status, ['completed', 'shipped'])) { + $result[$row->sales_order_id]['completed'] += $row->cnt; + } elseif ($row->status === 'in_progress') { + $result[$row->sales_order_id]['in_progress'] += $row->cnt; + } + } + + return $result; + } + /** * 주요 생산 공정 WO만 필터 (구매품/서비스 + 보조 공정 제외) *