From de19ac97aa10fe52eb5850a80b3b562dc85ce13d Mon Sep 17 00:00:00 2001 From: kent Date: Thu, 8 Jan 2026 11:11:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=88=98=EC=A3=BC=EA=B4=80=EB=A6=AC(Or?= =?UTF-8?q?der=20Management)=20API=20Phase=201.1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OrderService 구현 (index, stats, show, store, update, destroy, updateStatus) - OrderController 구현 (7개 API 엔드포인트) - FormRequest 클래스 3개 생성 (Store, Update, UpdateStatus) - 상태 전환 규칙 검증 (DRAFT → CONFIRMED → IN_PROGRESS → COMPLETED/CANCELLED) - 수주번호 자동 생성 (ORD{YYYYMMDD}{0001} 형식) - Swagger API 문서 작성 (OrderApi.php) - i18n 메시지 키 추가 (ko/en) Co-Authored-By: Claude Opus 4.5 --- CURRENT_WORKS.md | 44 +++ .../Controllers/Api/V1/OrderController.php | 88 +++++ app/Http/Requests/Order/StoreOrderRequest.php | 71 ++++ .../Requests/Order/UpdateOrderRequest.php | 66 ++++ .../Order/UpdateOrderStatusRequest.php | 36 ++ app/Services/OrderService.php | 322 +++++++++++++++ app/Swagger/v1/OrderApi.php | 367 ++++++++++++++++++ lang/en/error.php | 7 + lang/en/message.php | 9 + lang/ko/error.php | 7 + lang/ko/message.php | 9 + routes/api.php | 15 + 12 files changed, 1041 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/OrderController.php create mode 100644 app/Http/Requests/Order/StoreOrderRequest.php create mode 100644 app/Http/Requests/Order/UpdateOrderRequest.php create mode 100644 app/Http/Requests/Order/UpdateOrderStatusRequest.php create mode 100644 app/Services/OrderService.php create mode 100644 app/Swagger/v1/OrderApi.php diff --git a/CURRENT_WORKS.md b/CURRENT_WORKS.md index 8ab210b..0aa816e 100644 --- a/CURRENT_WORKS.md +++ b/CURRENT_WORKS.md @@ -1,5 +1,49 @@ # SAM API 작업 현황 +## 2026-01-08 (수) - Order Management API Phase 1.1 구현 + +### 작업 목표 +- 수주관리(Order Management) API 기본 CRUD 및 상태 관리 기능 구현 +- WorkOrderService/Controller 패턴을 참고하여 SAM API 규칙 준수 + +### 생성된 파일 +| 파일명 | 설명 | +|--------|------| +| `app/Services/OrderService.php` | 수주 비즈니스 로직 서비스 | +| `app/Http/Controllers/Api/V1/OrderController.php` | 수주 API 컨트롤러 | +| `app/Http/Requests/Order/StoreOrderRequest.php` | 생성 요청 검증 | +| `app/Http/Requests/Order/UpdateOrderRequest.php` | 수정 요청 검증 | +| `app/Http/Requests/Order/UpdateOrderStatusRequest.php` | 상태 변경 요청 검증 | +| `app/Swagger/v1/OrderApi.php` | Swagger API 문서 | + +### 수정된 파일 +| 파일명 | 설명 | +|--------|------| +| `routes/api.php` | OrderController import 및 7개 라우트 추가 | +| `lang/ko/message.php` | 수주 관련 메시지 키 추가 | +| `lang/en/message.php` | 수주 관련 메시지 키 추가 | +| `lang/ko/error.php` | 수주 에러 메시지 키 추가 | +| `lang/en/error.php` | 수주 에러 메시지 키 추가 | + +### 주요 구현 내용 +1. **OrderService 메서드**: index, stats, show, store, update, destroy, updateStatus +2. **상태 전환 규칙**: DRAFT → CONFIRMED → IN_PROGRESS → COMPLETED/CANCELLED +3. **수주번호 자동생성**: ORD{YYYYMMDD}{0001} 형식 +4. **품목 금액 계산**: 공급가, 세액, 합계 자동 계산 +5. **Swagger 스키마**: Order, OrderItem, OrderPagination, OrderStats 등 + +### 검증 완료 +- [x] Pint 코드 스타일 (6개 파일 자동 수정) +- [x] Swagger 문서 생성 +- [x] Service-First 아키텍처 준수 +- [x] i18n 메시지 키 사용 + +### 관련 문서 +- 계획: `docs/plans/order-management-plan.md` +- 변경 요약: `docs/changes/20250108_order_management_phase1.md` + +--- + ## 2026-01-02 (목) - 채권현황 동적월 지원 및 버그 수정 ### 작업 목표 diff --git a/app/Http/Controllers/Api/V1/OrderController.php b/app/Http/Controllers/Api/V1/OrderController.php new file mode 100644 index 0000000..03198db --- /dev/null +++ b/app/Http/Controllers/Api/V1/OrderController.php @@ -0,0 +1,88 @@ +service->index($request->all()); + }, __('message.order.fetched')); + } + + /** + * 통계 조회 + */ + public function stats() + { + return ApiResponse::handle(function () { + return $this->service->stats(); + }, __('message.order.fetched')); + } + + /** + * 단건 조회 + */ + public function show(int $id) + { + return ApiResponse::handle(function () use ($id) { + return $this->service->show($id); + }, __('message.order.fetched')); + } + + /** + * 생성 + */ + public function store(StoreOrderRequest $request) + { + return ApiResponse::handle(function () use ($request) { + return $this->service->store($request->validated()); + }, __('message.order.created')); + } + + /** + * 수정 + */ + public function update(UpdateOrderRequest $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + return $this->service->update($id, $request->validated()); + }, __('message.order.updated')); + } + + /** + * 삭제 + */ + public function destroy(int $id) + { + return ApiResponse::handle(function () use ($id) { + $this->service->destroy($id); + + return 'success'; + }, __('message.order.deleted')); + } + + /** + * 상태 변경 + */ + public function updateStatus(UpdateOrderStatusRequest $request, int $id) + { + return ApiResponse::handle(function () use ($request, $id) { + return $this->service->updateStatus($id, $request->validated()['status']); + }, __('message.order.status_updated')); + } +} diff --git a/app/Http/Requests/Order/StoreOrderRequest.php b/app/Http/Requests/Order/StoreOrderRequest.php new file mode 100644 index 0000000..471805d --- /dev/null +++ b/app/Http/Requests/Order/StoreOrderRequest.php @@ -0,0 +1,71 @@ + 'nullable|integer|exists:quotes,id', + 'order_type_code' => ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])], + 'status_code' => ['nullable', Rule::in([ + Order::STATUS_DRAFT, + Order::STATUS_CONFIRMED, + ])], + 'category_code' => 'nullable|string|max:50', + + // 거래처 정보 + 'client_id' => 'nullable|integer|exists:clients,id', + 'client_name' => 'nullable|string|max:200', + 'client_contact' => 'nullable|string|max:100', + 'site_name' => 'nullable|string|max:200', + + // 금액 정보 + 'supply_amount' => 'nullable|numeric|min:0', + 'tax_amount' => 'nullable|numeric|min:0', + 'total_amount' => 'nullable|numeric|min:0', + 'discount_rate' => 'nullable|numeric|min:0|max:100', + 'discount_amount' => 'nullable|numeric|min:0', + + // 배송/기타 + 'delivery_date' => 'nullable|date', + 'delivery_method_code' => 'nullable|string|max:50', + 'received_at' => 'nullable|date', + 'memo' => 'nullable|string', + 'remarks' => 'nullable|string', + 'note' => 'nullable|string', + + // 품목 배열 + 'items' => 'nullable|array', + 'items.*.item_id' => 'nullable|integer|exists:items,id', + 'items.*.item_name' => 'required|string|max:200', + 'items.*.specification' => 'nullable|string|max:500', + 'items.*.quantity' => 'required|numeric|min:0', + 'items.*.unit' => 'nullable|string|max:20', + 'items.*.unit_price' => 'required|numeric|min:0', + 'items.*.supply_amount' => 'nullable|numeric|min:0', + 'items.*.tax_amount' => 'nullable|numeric|min:0', + 'items.*.total_amount' => 'nullable|numeric|min:0', + ]; + } + + public function messages(): array + { + return [ + 'items.*.item_name.required' => __('validation.required', ['attribute' => '품목명']), + 'items.*.quantity.required' => __('validation.required', ['attribute' => '수량']), + 'items.*.unit_price.required' => __('validation.required', ['attribute' => '단가']), + ]; + } +} diff --git a/app/Http/Requests/Order/UpdateOrderRequest.php b/app/Http/Requests/Order/UpdateOrderRequest.php new file mode 100644 index 0000000..fd731b8 --- /dev/null +++ b/app/Http/Requests/Order/UpdateOrderRequest.php @@ -0,0 +1,66 @@ + ['nullable', Rule::in([Order::TYPE_ORDER, Order::TYPE_PURCHASE])], + 'category_code' => 'nullable|string|max:50', + + // 거래처 정보 + 'client_id' => 'nullable|integer|exists:clients,id', + 'client_name' => 'nullable|string|max:200', + 'client_contact' => 'nullable|string|max:100', + 'site_name' => 'nullable|string|max:200', + + // 금액 정보 + 'supply_amount' => 'nullable|numeric|min:0', + 'tax_amount' => 'nullable|numeric|min:0', + 'total_amount' => 'nullable|numeric|min:0', + 'discount_rate' => 'nullable|numeric|min:0|max:100', + 'discount_amount' => 'nullable|numeric|min:0', + + // 배송/기타 + 'delivery_date' => 'nullable|date', + 'delivery_method_code' => 'nullable|string|max:50', + 'received_at' => 'nullable|date', + 'memo' => 'nullable|string', + 'remarks' => 'nullable|string', + 'note' => 'nullable|string', + + // 품목 배열 (전체 교체) + 'items' => 'nullable|array', + 'items.*.item_id' => 'nullable|integer|exists:items,id', + 'items.*.item_name' => 'required|string|max:200', + 'items.*.specification' => 'nullable|string|max:500', + 'items.*.quantity' => 'required|numeric|min:0', + 'items.*.unit' => 'nullable|string|max:20', + 'items.*.unit_price' => 'required|numeric|min:0', + 'items.*.supply_amount' => 'nullable|numeric|min:0', + 'items.*.tax_amount' => 'nullable|numeric|min:0', + 'items.*.total_amount' => 'nullable|numeric|min:0', + ]; + } + + public function messages(): array + { + return [ + 'items.*.item_name.required' => __('validation.required', ['attribute' => '품목명']), + 'items.*.quantity.required' => __('validation.required', ['attribute' => '수량']), + 'items.*.unit_price.required' => __('validation.required', ['attribute' => '단가']), + ]; + } +} diff --git a/app/Http/Requests/Order/UpdateOrderStatusRequest.php b/app/Http/Requests/Order/UpdateOrderStatusRequest.php new file mode 100644 index 0000000..c5297a4 --- /dev/null +++ b/app/Http/Requests/Order/UpdateOrderStatusRequest.php @@ -0,0 +1,36 @@ + ['required', Rule::in([ + Order::STATUS_DRAFT, + Order::STATUS_CONFIRMED, + Order::STATUS_IN_PROGRESS, + Order::STATUS_COMPLETED, + Order::STATUS_CANCELLED, + ])], + ]; + } + + public function messages(): array + { + return [ + 'status.required' => __('validation.required', ['attribute' => '상태']), + 'status.in' => __('validation.in', ['attribute' => '상태']), + ]; + } +} diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php new file mode 100644 index 0000000..124102e --- /dev/null +++ b/app/Services/OrderService.php @@ -0,0 +1,322 @@ +tenantId(); + + $page = (int) ($params['page'] ?? 1); + $size = (int) ($params['size'] ?? 20); + $q = trim((string) ($params['q'] ?? '')); + $status = $params['status'] ?? null; + $orderType = $params['order_type'] ?? null; + $clientId = $params['client_id'] ?? null; + $dateFrom = $params['date_from'] ?? null; + $dateTo = $params['date_to'] ?? null; + + $query = Order::query() + ->where('tenant_id', $tenantId) + ->with(['client:id,name', 'items']); + + // 검색어 (수주번호, 현장명, 거래처명) + if ($q !== '') { + $query->where(function ($qq) use ($q) { + $qq->where('order_no', 'like', "%{$q}%") + ->orWhere('site_name', 'like', "%{$q}%") + ->orWhere('client_name', 'like', "%{$q}%"); + }); + } + + // 상태 필터 + if ($status !== null) { + $query->where('status_code', $status); + } + + // 주문유형 필터 (ORDER/PURCHASE) + if ($orderType !== null) { + $query->where('order_type_code', $orderType); + } + + // 거래처 필터 + if ($clientId !== null) { + $query->where('client_id', $clientId); + } + + // 날짜 범위 (수주일 기준) + if ($dateFrom !== null) { + $query->where('received_at', '>=', $dateFrom); + } + if ($dateTo !== null) { + $query->where('received_at', '<=', $dateTo); + } + + $query->orderByDesc('created_at'); + + return $query->paginate($size, ['*'], 'page', $page); + } + + /** + * 통계 조회 + */ + public function stats(): array + { + $tenantId = $this->tenantId(); + + $counts = Order::where('tenant_id', $tenantId) + ->select('status_code', DB::raw('count(*) as count')) + ->groupBy('status_code') + ->pluck('count', 'status_code') + ->toArray(); + + $amounts = Order::where('tenant_id', $tenantId) + ->select('status_code', DB::raw('sum(total_amount) as total')) + ->groupBy('status_code') + ->pluck('total', 'status_code') + ->toArray(); + + return [ + 'total' => array_sum($counts), + 'draft' => $counts[Order::STATUS_DRAFT] ?? 0, + 'confirmed' => $counts[Order::STATUS_CONFIRMED] ?? 0, + 'in_progress' => $counts[Order::STATUS_IN_PROGRESS] ?? 0, + 'completed' => $counts[Order::STATUS_COMPLETED] ?? 0, + 'cancelled' => $counts[Order::STATUS_CANCELLED] ?? 0, + 'total_amount' => array_sum($amounts), + 'confirmed_amount' => ($amounts[Order::STATUS_CONFIRMED] ?? 0) + ($amounts[Order::STATUS_IN_PROGRESS] ?? 0), + ]; + } + + /** + * 단건 조회 + */ + public function show(int $id) + { + $tenantId = $this->tenantId(); + + $order = Order::where('tenant_id', $tenantId) + ->with([ + 'client:id,name,business_no,representative,phone,email', + 'items' => fn ($q) => $q->orderBy('sort_order'), + 'quote:id,quote_no,site_name', + ]) + ->find($id); + + if (! $order) { + throw new NotFoundHttpException(__('error.not_found')); + } + + return $order; + } + + /** + * 생성 + */ + public function store(array $data) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + return DB::transaction(function () use ($data, $tenantId, $userId) { + // 수주번호 자동 생성 + $data['order_no'] = $this->generateOrderNo($tenantId); + $data['tenant_id'] = $tenantId; + $data['created_by'] = $userId; + $data['updated_by'] = $userId; + + // 기본 상태 설정 + $data['status_code'] = $data['status_code'] ?? Order::STATUS_DRAFT; + $data['order_type_code'] = $data['order_type_code'] ?? Order::TYPE_ORDER; + + $items = $data['items'] ?? []; + unset($data['items']); + + $order = Order::create($data); + + // 품목 저장 + foreach ($items as $index => $item) { + $item['sort_order'] = $index; + $this->calculateItemAmounts($item); + $order->items()->create($item); + } + + // 합계 재계산 + $order->refresh(); + $order->recalculateTotals()->save(); + + return $order->load(['client:id,name', 'items']); + }); + } + + /** + * 수정 + */ + public function update(int $id, array $data) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $order = Order::where('tenant_id', $tenantId)->find($id); + if (! $order) { + throw new NotFoundHttpException(__('error.not_found')); + } + + // 완료/취소 상태에서는 수정 불가 + if (in_array($order->status_code, [Order::STATUS_COMPLETED, Order::STATUS_CANCELLED])) { + throw new BadRequestHttpException(__('error.order.cannot_update_completed')); + } + + return DB::transaction(function () use ($order, $data, $userId) { + $data['updated_by'] = $userId; + + $items = $data['items'] ?? null; + unset($data['items'], $data['order_no']); // 번호 변경 불가 + + $order->update($data); + + // 품목 교체 (있는 경우) + if ($items !== null) { + $order->items()->delete(); + foreach ($items as $index => $item) { + $item['sort_order'] = $index; + $this->calculateItemAmounts($item); + $order->items()->create($item); + } + + // 합계 재계산 + $order->refresh(); + $order->recalculateTotals()->save(); + } + + return $order->load(['client:id,name', 'items']); + }); + } + + /** + * 삭제 + */ + public function destroy(int $id) + { + $tenantId = $this->tenantId(); + + $order = Order::where('tenant_id', $tenantId)->find($id); + if (! $order) { + throw new NotFoundHttpException(__('error.not_found')); + } + + // 진행 중이거나 완료된 수주는 삭제 불가 + if (in_array($order->status_code, [ + Order::STATUS_IN_PROGRESS, + Order::STATUS_COMPLETED, + ])) { + throw new BadRequestHttpException(__('error.order.cannot_delete_in_progress')); + } + + $order->deleted_by = $this->apiUserId(); + $order->save(); + $order->delete(); + + return 'success'; + } + + /** + * 상태 변경 + */ + public function updateStatus(int $id, string $status) + { + $tenantId = $this->tenantId(); + $userId = $this->apiUserId(); + + $order = Order::where('tenant_id', $tenantId)->find($id); + if (! $order) { + throw new NotFoundHttpException(__('error.not_found')); + } + + // 상태 유효성 검증 + $validStatuses = [ + Order::STATUS_DRAFT, + Order::STATUS_CONFIRMED, + Order::STATUS_IN_PROGRESS, + Order::STATUS_COMPLETED, + Order::STATUS_CANCELLED, + ]; + + if (! in_array($status, $validStatuses)) { + throw new BadRequestHttpException(__('error.invalid_status')); + } + + // 상태 전환 규칙 검증 + $this->validateStatusTransition($order->status_code, $status); + + $order->status_code = $status; + $order->updated_by = $userId; + $order->save(); + + return $order->load(['client:id,name', 'items']); + } + + /** + * 상태 전환 규칙 검증 + */ + private function validateStatusTransition(string $from, string $to): void + { + $allowedTransitions = [ + Order::STATUS_DRAFT => [Order::STATUS_CONFIRMED, Order::STATUS_CANCELLED], + Order::STATUS_CONFIRMED => [Order::STATUS_IN_PROGRESS, Order::STATUS_CANCELLED], + Order::STATUS_IN_PROGRESS => [Order::STATUS_COMPLETED, Order::STATUS_CANCELLED], + Order::STATUS_COMPLETED => [], // 완료 상태에서는 변경 불가 + Order::STATUS_CANCELLED => [Order::STATUS_DRAFT], // 취소에서 임시저장으로만 복구 가능 + ]; + + if (! in_array($to, $allowedTransitions[$from] ?? [])) { + throw new BadRequestHttpException(__('error.order.invalid_status_transition')); + } + } + + /** + * 품목 금액 계산 + */ + private function calculateItemAmounts(array &$item): void + { + $quantity = (float) ($item['quantity'] ?? 0); + $unitPrice = (float) ($item['unit_price'] ?? 0); + + $item['supply_amount'] = $quantity * $unitPrice; + $item['tax_amount'] = round($item['supply_amount'] * 0.1, 2); + $item['total_amount'] = $item['supply_amount'] + $item['tax_amount']; + } + + /** + * 수주번호 자동 생성 + */ + private function generateOrderNo(int $tenantId): string + { + $prefix = 'ORD'; + $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); + } +} diff --git a/app/Swagger/v1/OrderApi.php b/app/Swagger/v1/OrderApi.php new file mode 100644 index 0000000..6f6a0de --- /dev/null +++ b/app/Swagger/v1/OrderApi.php @@ -0,0 +1,367 @@ + 'Not in a shippable state.', ], + // Order related + 'order' => [ + 'cannot_update_completed' => 'Cannot update completed or cancelled order.', + 'cannot_delete_in_progress' => 'Cannot delete in progress or completed order.', + 'invalid_status_transition' => 'Invalid status transition.', + ], + ]; diff --git a/lang/en/message.php b/lang/en/message.php index 2cdb173..fea4fa5 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -93,4 +93,13 @@ 'deleted' => 'File has been deleted.', 'fetched' => 'File list retrieved successfully.', ], + + // Order Management + 'order' => [ + 'fetched' => 'Order retrieved successfully.', + 'created' => 'Order has been created.', + 'updated' => 'Order has been updated.', + 'deleted' => 'Order has been deleted.', + 'status_updated' => 'Order status has been updated.', + ], ]; diff --git a/lang/ko/error.php b/lang/ko/error.php index c23691e..1b053b4 100644 --- a/lang/ko/error.php +++ b/lang/ko/error.php @@ -356,4 +356,11 @@ 'send_failed' => 'FCM 발송 중 오류가 발생했습니다.', 'token_not_found' => 'FCM 토큰을 찾을 수 없습니다.', ], + + // 수주 관련 + 'order' => [ + 'cannot_update_completed' => '완료 또는 취소된 수주는 수정할 수 없습니다.', + 'cannot_delete_in_progress' => '진행 중이거나 완료된 수주는 삭제할 수 없습니다.', + 'invalid_status_transition' => '유효하지 않은 상태 전환입니다.', + ], ]; diff --git a/lang/ko/message.php b/lang/ko/message.php index f6b63ce..2f97da3 100644 --- a/lang/ko/message.php +++ b/lang/ko/message.php @@ -429,4 +429,13 @@ 'inspection_toggled' => '검사 상태가 변경되었습니다.', 'packaging_toggled' => '포장 상태가 변경되었습니다.', ], + + // 수주관리 + 'order' => [ + 'fetched' => '수주를 조회했습니다.', + 'created' => '수주가 등록되었습니다.', + 'updated' => '수주가 수정되었습니다.', + 'deleted' => '수주가 삭제되었습니다.', + 'status_updated' => '수주 상태가 변경되었습니다.', + ], ]; diff --git a/routes/api.php b/routes/api.php index 01dc329..8988d2f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -103,6 +103,7 @@ use App\Http\Controllers\Api\V1\UserInvitationController; use App\Http\Controllers\Api\V1\UserRoleController; use App\Http\Controllers\Api\V1\WithdrawalController; +use App\Http\Controllers\Api\V1\OrderController; use App\Http\Controllers\Api\V1\WorkOrderController; use App\Http\Controllers\Api\V1\WorkResultController; use App\Http\Controllers\Api\V1\WorkSettingController; @@ -1070,6 +1071,20 @@ Route::patch('/{id}/toggle', [ProcessController::class, 'toggleActive'])->whereNumber('id')->name('v1.processes.toggle'); }); + // 수주관리 API (Sales) + Route::prefix('orders')->group(function () { + // 기본 CRUD + Route::get('', [OrderController::class, 'index'])->name('v1.orders.index'); // 목록 + Route::get('/stats', [OrderController::class, 'stats'])->name('v1.orders.stats'); // 통계 + Route::post('', [OrderController::class, 'store'])->name('v1.orders.store'); // 생성 + Route::get('/{id}', [OrderController::class, 'show'])->whereNumber('id')->name('v1.orders.show'); // 상세 + Route::put('/{id}', [OrderController::class, 'update'])->whereNumber('id')->name('v1.orders.update'); // 수정 + Route::delete('/{id}', [OrderController::class, 'destroy'])->whereNumber('id')->name('v1.orders.destroy'); // 삭제 + + // 상태 관리 + Route::patch('/{id}/status', [OrderController::class, 'updateStatus'])->whereNumber('id')->name('v1.orders.status'); // 상태 변경 + }); + // 작업지시 관리 API (Production) Route::prefix('work-orders')->group(function () { // 기본 CRUD