From 407afe38e484875131b8562f9b8c74109578ff50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 16 Mar 2026 21:27:13 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20[order]=20=EC=9E=AC=EA=B3=A0?= =?UTF-8?q?=EC=83=9D=EC=82=B0=EA=B4=80=EB=A6=AC(STOCK)=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Order 모델에 TYPE_STOCK = 'STOCK' 상수 추가 - StoreOrderRequest/UpdateOrderRequest에 STOCK 타입 validation 추가 - options에 production_reason, target_stock_qty 필드 추가 - 재고생산 채번: STK{YYYYMMDD}{NNNN} 형식 - stats()에 order_type 필터 파라미터 추가 - STOCK 타입 확정 시 매출 자동 생성 스킵 --- .../Controllers/Api/V1/OrderController.php | 6 +-- app/Http/Requests/Order/StoreOrderRequest.php | 4 +- .../Requests/Order/UpdateOrderRequest.php | 4 +- app/Models/Orders/Order.php | 2 + app/Services/OrderService.php | 45 ++++++++++++++++--- 5 files changed, 49 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/Api/V1/OrderController.php b/app/Http/Controllers/Api/V1/OrderController.php index d7b4b2ca..acf7001f 100644 --- a/app/Http/Controllers/Api/V1/OrderController.php +++ b/app/Http/Controllers/Api/V1/OrderController.php @@ -30,10 +30,10 @@ public function index(Request $request) /** * 통계 조회 */ - public function stats() + public function stats(Request $request) { - return ApiResponse::handle(function () { - return $this->service->stats(); + return ApiResponse::handle(function () use ($request) { + return $this->service->stats($request->input('order_type')); }, __('message.order.fetched')); } diff --git a/app/Http/Requests/Order/StoreOrderRequest.php b/app/Http/Requests/Order/StoreOrderRequest.php index aed252d1..272f924c 100644 --- a/app/Http/Requests/Order/StoreOrderRequest.php +++ b/app/Http/Requests/Order/StoreOrderRequest.php @@ -18,7 +18,7 @@ public function rules(): array return [ // 기본 정보 'quote_id' => 'nullable|integer|exists:quotes,id', - 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])], + 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE, Order::TYPE_STOCK])], 'status_code' => ['nullable', Rule::in([ Order::STATUS_DRAFT, Order::STATUS_CONFIRMED, @@ -55,6 +55,8 @@ public function rules(): array 'options.shipping_address' => 'nullable|string|max:500', 'options.shipping_address_detail' => 'nullable|string|max:500', 'options.manager_name' => 'nullable|string|max:100', + 'options.production_reason' => 'nullable|string|max:500', + 'options.target_stock_qty' => 'nullable|numeric|min:0', // 품목 배열 'items' => 'nullable|array', diff --git a/app/Http/Requests/Order/UpdateOrderRequest.php b/app/Http/Requests/Order/UpdateOrderRequest.php index 59a25181..53cd3168 100644 --- a/app/Http/Requests/Order/UpdateOrderRequest.php +++ b/app/Http/Requests/Order/UpdateOrderRequest.php @@ -17,7 +17,7 @@ public function rules(): array { return [ // 기본 정보 (order_no는 수정 불가) - 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])], + 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE, Order::TYPE_STOCK])], 'category_code' => 'nullable|string|max:50', // 거래처 정보 @@ -49,6 +49,8 @@ public function rules(): array 'options.shipping_address' => 'nullable|string|max:500', 'options.shipping_address_detail' => 'nullable|string|max:500', 'options.manager_name' => 'nullable|string|max:100', + 'options.production_reason' => 'nullable|string|max:500', + 'options.target_stock_qty' => 'nullable|numeric|min:0', // 품목 배열 (전체 교체) 'items' => 'nullable|array', diff --git a/app/Models/Orders/Order.php b/app/Models/Orders/Order.php index 426aece9..792ab592 100644 --- a/app/Models/Orders/Order.php +++ b/app/Models/Orders/Order.php @@ -78,6 +78,8 @@ class Order extends Model public const TYPE_PURCHASE = 'PURCHASE'; // 발주 + public const TYPE_STOCK = 'STOCK'; // 재고생산 + // 매출 인식 시점 public const SALES_ON_ORDER_CONFIRM = 'on_order_confirm'; // 수주확정 시 diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index f0636ca0..dd1ab494 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -109,17 +109,22 @@ public function index(array $params) /** * 통계 조회 */ - public function stats(): array + public function stats(?string $orderType = null): array { $tenantId = $this->tenantId(); - $counts = Order::where('tenant_id', $tenantId) + $baseQuery = Order::where('tenant_id', $tenantId); + if ($orderType !== null) { + $baseQuery->where('order_type_code', $orderType); + } + + $counts = (clone $baseQuery) ->select('status_code', DB::raw('count(*) as count')) ->groupBy('status_code') ->pluck('count', 'status_code') ->toArray(); - $amounts = Order::where('tenant_id', $tenantId) + $amounts = (clone $baseQuery) ->select('status_code', DB::raw('sum(total_amount) as total')) ->groupBy('status_code') ->pluck('total', 'status_code') @@ -162,10 +167,13 @@ public function store(array $data) $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { - // 수주번호 자동 생성 + // 수주번호 자동 생성 (재고생산은 STK 접두사) $pairCode = $data['pair_code'] ?? null; unset($data['pair_code']); - $data['order_no'] = $this->generateOrderNo($tenantId, $pairCode); + $isStock = ($data['order_type_code'] ?? null) === Order::TYPE_STOCK; + $data['order_no'] = $isStock + ? $this->generateStockOrderNo($tenantId) + : $this->generateOrderNo($tenantId, $pairCode); $data['tenant_id'] = $tenantId; $data['created_by'] = $userId; $data['updated_by'] = $userId; @@ -629,8 +637,8 @@ public function updateStatus(int $id, string $status) $createdSale = null; $previousStatus = $order->status_code; - // 수주확정 시 매출 자동 생성 (sales_recognition = on_order_confirm인 경우) - if ($status === Order::STATUS_CONFIRMED && $order->shouldCreateSaleOnConfirm()) { + // 수주확정 시 매출 자동 생성 (재고생산은 매출 생성 불필요) + if ($status === Order::STATUS_CONFIRMED && $order->order_type_code !== Order::TYPE_STOCK && $order->shouldCreateSaleOnConfirm()) { $createdSale = $this->createSaleFromOrder($order, $userId); $order->sale_id = $createdSale->id; } @@ -776,6 +784,29 @@ private function generateOrderNoLegacy(int $tenantId): string return sprintf('%s%s%04d', $prefix, $date, $seq); } + /** + * 재고생산 번호 생성 (STK{YYYYMMDD}{NNNN}) + */ + private function generateStockOrderNo(int $tenantId): string + { + $prefix = 'STK'; + $date = now()->format('Ymd'); + + $lastNo = Order::withoutGlobalScopes() + ->where('tenant_id', $tenantId) + ->where('order_no', 'like', "{$prefix}{$date}%") + ->orderByDesc('order_no') + ->value('order_no'); + + if ($lastNo) { + $seq = (int) substr($lastNo, -4) + 1; + } else { + $seq = 1; + } + + return sprintf('%s%s%04d', $prefix, $date, $seq); + } + /** * 견적에서 수주 생성 */ From ae73275cf9305680794e2174b620a8da2ba7104a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 16 Mar 2026 22:24:15 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20[order]=20=EC=9E=AC=EA=B3=A0?= =?UTF-8?q?=EC=83=9D=EC=82=B0=20=EC=83=9D=EC=82=B0=EC=A7=80=EC=8B=9C=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STOCK 타입 store() 시 site_name='재고생산' 자동 설정 - createProductionOrder() STOCK 분기 추가: - project_name='재고생산' 고정 - 절곡 공정 자동 선택 (BOM 매칭 스킵) - scheduled_date=now() 자동 설정 - 절곡 공정 미존재 시 에러 메시지 추가 --- app/Services/OrderService.php | 238 +++++++++++++++++++--------------- lang/en/error.php | 1 + lang/ko/error.php | 1 + 3 files changed, 134 insertions(+), 106 deletions(-) diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index dd1ab494..0ed30b65 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -182,6 +182,11 @@ public function store(array $data) $data['status_code'] = $data['status_code'] ?? Order::STATUS_DRAFT; $data['order_type_code'] = $data['order_type_code'] ?? Order::TYPE_ORDER; + // 재고생산: 현장명 자동 설정 + if ($isStock) { + $data['site_name'] = '재고생산'; + } + $items = $data['items'] ?? []; unset($data['items']); @@ -1233,121 +1238,142 @@ public function createProductionOrder(int $orderId, array $data) throw new BadRequestHttpException(__('error.order.production_order_already_exists')); } - // order_nodes의 BOM 결과를 기반으로 공정별 자동 분류 - $bomItemIds = []; - $nodesBomMap = []; // node_id => [item_name => bom_item] + // 재고생산(STOCK): 절곡 공정에 모든 품목 직접 배정 (BOM 매칭 스킵) + $isStock = $order->order_type_code === Order::TYPE_STOCK; + $nodesBomMap = []; - foreach ($order->rootNodes as $node) { - $bomResult = $node->options['bom_result'] ?? []; - $bomItems = $bomResult['items'] ?? []; - - foreach ($bomItems as $bomItem) { - if (! empty($bomItem['item_id'])) { - $bomItemIds[] = $bomItem['item_id']; - $nodesBomMap[$node->id][$bomItem['item_name']] = $bomItem; - } - } - } - - $bomItemIds = array_unique($bomItemIds); - - // process_items 테이블에서 item_id → process_id 매핑 조회 - $itemProcessMap = []; - if (! empty($bomItemIds)) { - $processItems = DB::table('process_items as pi') - ->join('processes as p', 'pi.process_id', '=', 'p.id') - ->where('p.tenant_id', $tenantId) - ->whereIn('pi.item_id', $bomItemIds) - ->where('pi.is_active', true) - ->select('pi.item_id', 'pi.process_id') - ->get(); - - foreach ($processItems as $pi) { - $itemProcessMap[$pi->item_id] = $pi->process_id; - } - } - - // item_code → item_id 매핑 구축 (fallback용 — N+1 방지를 위해 사전 일괄 조회) - $codeToIdMap = []; - if (! empty($bomItemIds)) { - $codeToIdRows = DB::table('items') - ->where('tenant_id', $tenantId) - ->whereIn('id', $bomItemIds) - ->whereNull('deleted_at') - ->select('id', 'code') - ->get(); - foreach ($codeToIdRows as $row) { - $codeToIdMap[$row->code] = $row->id; - } - } - - // 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) + if ($isStock) { + $bendingProcess = \App\Models\Process::where('tenant_id', $tenantId) + ->where('process_name', '절곡') ->where('is_active', true) - ->select('item_id', 'process_id') - ->get(); - foreach ($extraProcessItems as $pi) { - $itemProcessMap[$pi->item_id] = $pi->process_id; - } - } + ->first(); - // order_items를 공정별로 그룹화 (BOM item_id → process 매핑 활용) - $itemsByProcess = []; - foreach ($order->items as $orderItem) { - $processId = null; - - // 1. order_item의 item_id가 있으면 직접 매핑 - if ($orderItem->item_id && isset($itemProcessMap[$orderItem->item_id])) { - $processId = $itemProcessMap[$orderItem->item_id]; + if (! $bendingProcess) { + throw new BadRequestHttpException(__('error.order.bending_process_not_found')); } - // 2. item_id가 없으면 노드의 BOM에서 item_name으로 찾기 - elseif ($orderItem->order_node_id && isset($nodesBomMap[$orderItem->order_node_id])) { - $nodeBom = $nodesBomMap[$orderItem->order_node_id]; - $bomItem = $nodeBom[$orderItem->item_name] ?? null; - if ($bomItem && ! empty($bomItem['item_id']) && isset($itemProcessMap[$bomItem['item_id']])) { - $processId = $itemProcessMap[$bomItem['item_id']]; + + $itemsByProcess = [ + $bendingProcess->id => [ + 'process_id' => $bendingProcess->id, + 'items' => $order->items->all(), + ], + ]; + } else { + // 기존 로직: order_nodes의 BOM 결과를 기반으로 공정별 자동 분류 + $bomItemIds = []; + + foreach ($order->rootNodes as $node) { + $bomResult = $node->options['bom_result'] ?? []; + $bomItems = $bomResult['items'] ?? []; + + foreach ($bomItems as $bomItem) { + if (! empty($bomItem['item_id'])) { + $bomItemIds[] = $bomItem['item_id']; + $nodesBomMap[$node->id][$bomItem['item_name']] = $bomItem; + } } } - // 3. fallback: 사전 구축된 맵에서 item_code → process 매핑 (N+1 제거) - if ($processId === null && $orderItem->item_code) { - $resolvedId = $codeToIdMap[$orderItem->item_code] ?? null; - if ($resolvedId && isset($itemProcessMap[$resolvedId])) { - $processId = $itemProcessMap[$resolvedId]; + $bomItemIds = array_unique($bomItemIds); + + // process_items 테이블에서 item_id → process_id 매핑 조회 + $itemProcessMap = []; + if (! empty($bomItemIds)) { + $processItems = DB::table('process_items as pi') + ->join('processes as p', 'pi.process_id', '=', 'p.id') + ->where('p.tenant_id', $tenantId) + ->whereIn('pi.item_id', $bomItemIds) + ->where('pi.is_active', true) + ->select('pi.item_id', 'pi.process_id') + ->get(); + + foreach ($processItems as $pi) { + $itemProcessMap[$pi->item_id] = $pi->process_id; } } - $key = $processId ?? 'none'; - - if (! isset($itemsByProcess[$key])) { - $itemsByProcess[$key] = [ - 'process_id' => $processId, - 'items' => [], - ]; + // item_code → item_id 매핑 구축 (fallback용 — N+1 방지를 위해 사전 일괄 조회) + $codeToIdMap = []; + if (! empty($bomItemIds)) { + $codeToIdRows = DB::table('items') + ->where('tenant_id', $tenantId) + ->whereIn('id', $bomItemIds) + ->whereNull('deleted_at') + ->select('id', 'code') + ->get(); + foreach ($codeToIdRows as $row) { + $codeToIdMap[$row->code] = $row->id; + } + } + + // 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) { + $processId = null; + + // 1. order_item의 item_id가 있으면 직접 매핑 + if ($orderItem->item_id && isset($itemProcessMap[$orderItem->item_id])) { + $processId = $itemProcessMap[$orderItem->item_id]; + } + // 2. item_id가 없으면 노드의 BOM에서 item_name으로 찾기 + elseif ($orderItem->order_node_id && isset($nodesBomMap[$orderItem->order_node_id])) { + $nodeBom = $nodesBomMap[$orderItem->order_node_id]; + $bomItem = $nodeBom[$orderItem->item_name] ?? null; + if ($bomItem && ! empty($bomItem['item_id']) && isset($itemProcessMap[$bomItem['item_id']])) { + $processId = $itemProcessMap[$bomItem['item_id']]; + } + } + + // 3. fallback: 사전 구축된 맵에서 item_code → process 매핑 (N+1 제거) + if ($processId === null && $orderItem->item_code) { + $resolvedId = $codeToIdMap[$orderItem->item_code] ?? null; + if ($resolvedId && isset($itemProcessMap[$resolvedId])) { + $processId = $itemProcessMap[$resolvedId]; + } + } + + $key = $processId ?? 'none'; + + if (! isset($itemsByProcess[$key])) { + $itemsByProcess[$key] = [ + 'process_id' => $processId, + 'items' => [], + ]; + } + $itemsByProcess[$key]['items'][] = $orderItem; } - $itemsByProcess[$key]['items'][] = $orderItem; } return DB::transaction(function () use ($order, $data, $tenantId, $userId, $itemsByProcess, $nodesBomMap) { @@ -1409,13 +1435,13 @@ public function createProductionOrder(int $orderId, array $data) 'tenant_id' => $tenantId, 'work_order_no' => $workOrderNo, 'sales_order_id' => $order->id, - 'project_name' => $order->site_name ?? $order->client_name, + 'project_name' => $isStock ? '재고생산' : ($order->site_name ?? $order->client_name), 'process_id' => $processId, 'status' => (! empty($assigneeIds) || $teamId) ? WorkOrder::STATUS_WAITING : WorkOrder::STATUS_UNASSIGNED, 'priority' => $priority, 'assignee_id' => $primaryAssigneeId, 'team_id' => $teamId, - 'scheduled_date' => $data['scheduled_date'] ?? $order->delivery_date, + 'scheduled_date' => $data['scheduled_date'] ?? ($isStock ? now()->toDateString() : $order->delivery_date), 'memo' => $data['memo'] ?? null, 'options' => $workOrderOptions, 'is_active' => true, diff --git a/lang/en/error.php b/lang/en/error.php index cea9ee2c..01aa5b42 100644 --- a/lang/en/error.php +++ b/lang/en/error.php @@ -122,6 +122,7 @@ 'not_found' => 'Work order not found.', 'cannot_delete_in_progress' => 'Cannot delete in progress or completed work order.', 'not_bending_process' => 'This is not a bending process.', + 'bending_process_not_found' => 'Bending process not found. Please check process settings.', 'invalid_transition' => "Cannot transition status from ':from' to ':to'. Allowed statuses: :allowed", ], diff --git a/lang/ko/error.php b/lang/ko/error.php index 29d03c1d..68027f33 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -428,6 +428,7 @@ 'already_created_from_quote' => '이미 해당 견적에서 수주가 생성되었습니다.', 'must_be_confirmed_for_production' => '확정 상태의 수주만 생산지시를 생성할 수 있습니다.', 'production_order_already_exists' => '이미 생산지시가 존재합니다.', + 'bending_process_not_found' => '절곡 공정을 찾을 수 없습니다. 공정 설정을 확인해주세요.', 'cannot_revert_completed' => '완료된 수주의 생산지시는 되돌릴 수 없습니다.', 'cannot_revert_not_confirmed' => '수주확정 상태에서만 되돌리기가 가능합니다.', 'cannot_revert_work_order_completed' => '완료 또는 출하된 작업지시는 취소할 수 없습니다.', From 750776d5c88aecca5df3ec68876526d5fa6739e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Tue, 17 Mar 2026 10:25:15 +0900 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20[exception]=20BadRequestHttpExceptio?= =?UTF-8?q?n=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EB=A9=94=EC=8B=9C=EC=A7=80?= =?UTF-8?q?=20=EC=A0=84=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 하드코딩된 '잘못된 요청' 대신 예외 메시지 우선 사용 - 메시지 없을 경우 기존 기본값 유지 Co-Authored-By: Claude Opus 4.6 (1M context) --- app/Exceptions/Handler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 03e127a8..3928ffed 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -95,7 +95,7 @@ public function render($request, Throwable $exception) if ($exception instanceof BadRequestHttpException) { return response()->json([ 'success' => false, - 'message' => '잘못된 요청', + 'message' => $exception->getMessage() ?: '잘못된 요청', 'data' => null, ], 400); } From 053323c1440fa52ed8fc8620fcbb8742969ca4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 17 Mar 2026 10:46:29 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20[production]=20=EC=83=9D=EC=82=B0?= =?UTF-8?q?=EC=A7=80=EC=8B=9C=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20$isStock?= =?UTF-8?q?=20=EB=AF=B8=EC=A0=95=EC=9D=98=20=EC=98=A4=EB=A5=98=20=EB=B0=8F?= =?UTF-8?q?=20=EC=88=98=EB=9F=89=20=EC=A0=95=EC=88=98=20=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB::transaction 클로저 use절에 $isStock 변수 추가 - work_order_items 수량을 정수로 캐스팅 --- app/Services/OrderService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index 0ed30b65..f181d481 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -1376,7 +1376,7 @@ public function createProductionOrder(int $orderId, array $data) } } - return DB::transaction(function () use ($order, $data, $tenantId, $userId, $itemsByProcess, $nodesBomMap) { + return DB::transaction(function () use ($order, $data, $tenantId, $userId, $itemsByProcess, $nodesBomMap, $isStock) { $workOrders = []; // 담당자 ID 배열 처리 (assignee_ids 우선, fallback으로 assignee_id) @@ -1530,7 +1530,7 @@ public function createProductionOrder(int $orderId, array $data) 'item_id' => $itemId, 'item_name' => $orderItem->item_name, 'specification' => $orderItem->specification, - 'quantity' => $orderItem->quantity, + 'quantity' => (int) $orderItem->quantity, 'unit' => $orderItem->unit, 'sort_order' => $sortOrder++, 'status' => 'pending', From 8404f29bca36568d71072a01037ca04360b4a5b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 17 Mar 2026 11:00:06 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20[account]=20=EA=B3=84=EC=A0=95?= =?UTF-8?q?=EA=B3=BC=EB=AA=A9=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=98=81=EB=AC=B8=20=ED=86=B5=EC=9D=BC=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 한글 카테고리(자산/부채/자본/수익/비용)를 영문(asset/liability/capital/revenue/expense)으로 변환 - API 표준에 맞춰 전체 테넌트 통일 --- ...nify_account_codes_category_to_english.php | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 database/migrations/2026_03_17_105822_unify_account_codes_category_to_english.php diff --git a/database/migrations/2026_03_17_105822_unify_account_codes_category_to_english.php b/database/migrations/2026_03_17_105822_unify_account_codes_category_to_english.php new file mode 100644 index 00000000..74e2ef91 --- /dev/null +++ b/database/migrations/2026_03_17_105822_unify_account_codes_category_to_english.php @@ -0,0 +1,52 @@ + 'asset', + '부채' => 'liability', + '자본' => 'capital', + '수익' => 'revenue', + '비용' => 'expense', + ]; + + foreach ($mapping as $korean => $english) { + DB::table('account_codes') + ->where('category', $korean) + ->update(['category' => $english]); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $mapping = [ + 'asset' => '자산', + 'liability' => '부채', + 'capital' => '자본', + 'revenue' => '수익', + 'expense' => '비용', + ]; + + // tenant_id=1만 원복 (다른 테넌트는 원래 영문) + foreach ($mapping as $english => $korean) { + DB::table('account_codes') + ->where('tenant_id', 1) + ->where('category', $english) + ->update(['category' => $korean]); + } + } +}; From 921f1ecba78478613ebd139229a46637e2d7c6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 17 Mar 2026 11:03:37 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20[production]=20=EC=83=9D=EC=82=B0?= =?UTF-8?q?=EC=A7=80=EC=8B=9C=20=EC=83=9D=EC=84=B1=20=EC=8B=9C=20$process?= =?UTF-8?q?=20=EB=AF=B8=EC=A0=95=EC=9D=98=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - $process 변수를 if 블록 밖에서 null로 초기화 --- app/Services/OrderService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php index f181d481..f66ac5ee 100644 --- a/app/Services/OrderService.php +++ b/app/Services/OrderService.php @@ -1396,6 +1396,7 @@ public function createProductionOrder(int $orderId, array $data) // 공정 옵션 초기화 (보조 공정 플래그 포함) $workOrderOptions = null; + $process = null; if ($processId) { $process = \App\Models\Process::find($processId); if ($process && ! empty($process->options['is_auxiliary'])) { From ead546e268c5766038fa734b0a3e8f6db652be30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 17 Mar 2026 11:13:51 +0900 Subject: [PATCH 7/7] =?UTF-8?q?feat:=20[account]=20tenant=5Fid=3D1=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=EA=B3=BC=EB=AA=A9=EC=9D=84=20KIS=205?= =?UTF-8?q?=EC=9E=90=EB=A6=AC=20=ED=91=9C=EC=A4=80=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=99=84=EC=A0=84=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존 3자리 코드 체계 삭제 - 서비스 표준(5자리 KIS) 458개 코드를 활성 상태로 복사 - 기존 전표 account_code 매핑은 수동 진행 예정 --- ...enant1_account_codes_with_kis_standard.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 database/migrations/2026_03_17_111249_replace_tenant1_account_codes_with_kis_standard.php diff --git a/database/migrations/2026_03_17_111249_replace_tenant1_account_codes_with_kis_standard.php b/database/migrations/2026_03_17_111249_replace_tenant1_account_codes_with_kis_standard.php new file mode 100644 index 00000000..43bd5d63 --- /dev/null +++ b/database/migrations/2026_03_17_111249_replace_tenant1_account_codes_with_kis_standard.php @@ -0,0 +1,68 @@ +where('tenant_id', '!=', 1) + ->whereRaw('LENGTH(code) = 5') + ->value('tenant_id'); + + if (! $sourceTenantId) { + // 소스 테넌트가 없으면 스킵 + return; + } + + // 1. tenant_id=1 기존 데이터 전체 삭제 + DB::table('account_codes')->where('tenant_id', 1)->delete(); + + // 2. 소스 테넌트의 5자리 코드를 tenant_id=1로 복사 (활성 상태) + $sourceCodes = DB::table('account_codes') + ->where('tenant_id', $sourceTenantId) + ->whereRaw('LENGTH(code) = 5') + ->get(); + + $now = now(); + $inserts = $sourceCodes->map(fn ($row) => [ + 'tenant_id' => 1, + 'code' => $row->code, + 'name' => $row->name, + 'category' => $row->category, + 'sub_category' => $row->sub_category, + 'parent_code' => $row->parent_code, + 'depth' => $row->depth, + 'department_type' => $row->department_type, + 'description' => $row->description, + 'sort_order' => $row->sort_order, + 'is_active' => true, + 'created_at' => $now, + 'updated_at' => $now, + ])->toArray(); + + // chunk insert (한 번에 100건씩) + foreach (array_chunk($inserts, 100) as $chunk) { + DB::table('account_codes')->insert($chunk); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // 롤백 불가 (기존 3자리 데이터 복원 불가능) + // 필요 시 백업에서 복원 + } +};