From 1a8bb461375a07b38bbde0009aa6da1a8dd1681e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 4 Mar 2026 23:31:04 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[outbound]=20=EB=B0=B0=EC=B0=A8?= =?UTF-8?q?=EC=B0=A8=EB=9F=89=20=EA=B4=80=EB=A6=AC=20API=20=E2=80=94=20CRU?= =?UTF-8?q?D=20+=20options=20JSON=20=EC=A0=95=EC=B1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VehicleDispatchService: index(검색/필터/페이지네이션), stats(선불/착불/합계), show, update - VehicleDispatchController + VehicleDispatchUpdateRequest - options JSON 컬럼 추가 (dispatch_no, status, freight_cost_type, supply_amount, vat, total_amount, writer) - ShipmentService.syncDispatches에 options 필드 지원 추가 - inventory.php에 vehicle-dispatches 라우트 4개 등록 Co-Authored-By: Claude Opus 4.6 --- .../Api/V1/VehicleDispatchController.php | 74 +++++++++ .../VehicleDispatchUpdateRequest.php | 30 ++++ .../Tenants/ShipmentVehicleDispatch.php | 2 + app/Services/ShipmentService.php | 1 + app/Services/VehicleDispatchService.php | 140 ++++++++++++++++++ ...s_to_shipment_vehicle_dispatches_table.php | 23 +++ routes/api/v1/inventory.php | 9 ++ 7 files changed, 279 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/VehicleDispatchController.php create mode 100644 app/Http/Requests/VehicleDispatch/VehicleDispatchUpdateRequest.php create mode 100644 app/Services/VehicleDispatchService.php create mode 100644 database/migrations/2026_03_04_150000_add_options_to_shipment_vehicle_dispatches_table.php diff --git a/app/Http/Controllers/Api/V1/VehicleDispatchController.php b/app/Http/Controllers/Api/V1/VehicleDispatchController.php new file mode 100644 index 0000000..01e1337 --- /dev/null +++ b/app/Http/Controllers/Api/V1/VehicleDispatchController.php @@ -0,0 +1,74 @@ +only([ + 'search', + 'status', + 'start_date', + 'end_date', + 'per_page', + 'page', + ]); + + $dispatches = $this->service->index($params); + + return ApiResponse::success($dispatches, __('message.fetched')); + } + + /** + * 배차차량 통계 조회 + */ + public function stats(): JsonResponse + { + $stats = $this->service->stats(); + + return ApiResponse::success($stats, __('message.fetched')); + } + + /** + * 배차차량 상세 조회 + */ + public function show(int $id): JsonResponse + { + try { + $dispatch = $this->service->show($id); + + return ApiResponse::success($dispatch, __('message.fetched')); + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + return ApiResponse::error(__('error.not_found'), 404); + } + } + + /** + * 배차차량 수정 + */ + public function update(VehicleDispatchUpdateRequest $request, int $id): JsonResponse + { + try { + $dispatch = $this->service->update($id, $request->validated()); + + return ApiResponse::success($dispatch, __('message.updated')); + } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + return ApiResponse::error(__('error.not_found'), 404); + } + } +} diff --git a/app/Http/Requests/VehicleDispatch/VehicleDispatchUpdateRequest.php b/app/Http/Requests/VehicleDispatch/VehicleDispatchUpdateRequest.php new file mode 100644 index 0000000..9dba8f9 --- /dev/null +++ b/app/Http/Requests/VehicleDispatch/VehicleDispatchUpdateRequest.php @@ -0,0 +1,30 @@ + 'nullable|in:prepaid,collect', + 'logistics_company' => 'nullable|string|max:100', + 'arrival_datetime' => 'nullable|date', + 'tonnage' => 'nullable|string|max:20', + 'vehicle_no' => 'nullable|string|max:20', + 'driver_contact' => 'nullable|string|max:50', + 'remarks' => 'nullable|string', + 'supply_amount' => 'nullable|numeric|min:0', + 'vat' => 'nullable|numeric|min:0', + 'total_amount' => 'nullable|numeric|min:0', + 'status' => 'nullable|in:draft,completed', + ]; + } +} diff --git a/app/Models/Tenants/ShipmentVehicleDispatch.php b/app/Models/Tenants/ShipmentVehicleDispatch.php index 7db88a0..50eb385 100644 --- a/app/Models/Tenants/ShipmentVehicleDispatch.php +++ b/app/Models/Tenants/ShipmentVehicleDispatch.php @@ -22,12 +22,14 @@ class ShipmentVehicleDispatch extends Model 'vehicle_no', 'driver_contact', 'remarks', + 'options', ]; protected $casts = [ 'seq' => 'integer', 'shipment_id' => 'integer', 'arrival_datetime' => 'datetime', + 'options' => 'array', ]; /** diff --git a/app/Services/ShipmentService.php b/app/Services/ShipmentService.php index 8d42603..df8b35a 100644 --- a/app/Services/ShipmentService.php +++ b/app/Services/ShipmentService.php @@ -513,6 +513,7 @@ protected function syncDispatches(Shipment $shipment, array $dispatches, int $te 'vehicle_no' => $dispatch['vehicle_no'] ?? null, 'driver_contact' => $dispatch['driver_contact'] ?? null, 'remarks' => $dispatch['remarks'] ?? null, + 'options' => $dispatch['options'] ?? null, ]); $seq++; } diff --git a/app/Services/VehicleDispatchService.php b/app/Services/VehicleDispatchService.php new file mode 100644 index 0000000..cfae5fb --- /dev/null +++ b/app/Services/VehicleDispatchService.php @@ -0,0 +1,140 @@ +tenantId(); + + $query = ShipmentVehicleDispatch::query() + ->where('tenant_id', $tenantId) + ->with('shipment'); + + // 검색어 필터 + if (! empty($params['search'])) { + $search = $params['search']; + $query->where(function ($q) use ($search) { + $q->where('vehicle_no', 'like', "%{$search}%") + ->orWhere('options->dispatch_no', 'like', "%{$search}%") + ->orWhereHas('shipment', function ($q3) use ($search) { + $q3->where('lot_no', 'like', "%{$search}%") + ->orWhere('site_name', 'like', "%{$search}%") + ->orWhere('customer_name', 'like', "%{$search}%"); + }); + }); + } + + // 상태 필터 (options JSON) + if (! empty($params['status'])) { + $query->where('options->status', $params['status']); + } + + // 날짜 범위 필터 + if (! empty($params['start_date'])) { + $query->where('arrival_datetime', '>=', $params['start_date']); + } + if (! empty($params['end_date'])) { + $query->where('arrival_datetime', '<=', $params['end_date'].' 23:59:59'); + } + + // 정렬 + $query->orderBy('id', 'desc'); + + // 페이지네이션 + $perPage = $params['per_page'] ?? 20; + + return $query->paginate($perPage); + } + + /** + * 배차차량 통계 + */ + public function stats(): array + { + $tenantId = $this->tenantId(); + + $all = ShipmentVehicleDispatch::query() + ->where('tenant_id', $tenantId) + ->get(); + + $prepaid = 0; + $collect = 0; + $total = 0; + + foreach ($all as $dispatch) { + $opts = $dispatch->options ?? []; + $amount = (float) ($opts['total_amount'] ?? 0); + $total += $amount; + + if (($opts['freight_cost_type'] ?? '') === 'prepaid') { + $prepaid += $amount; + } + if (($opts['freight_cost_type'] ?? '') === 'collect') { + $collect += $amount; + } + } + + return [ + 'prepaid_amount' => $prepaid, + 'collect_amount' => $collect, + 'total_amount' => $total, + ]; + } + + /** + * 배차차량 상세 조회 + */ + public function show(int $id): ShipmentVehicleDispatch + { + $tenantId = $this->tenantId(); + + return ShipmentVehicleDispatch::query() + ->where('tenant_id', $tenantId) + ->with('shipment') + ->findOrFail($id); + } + + /** + * 배차차량 수정 + */ + public function update(int $id, array $data): ShipmentVehicleDispatch + { + $tenantId = $this->tenantId(); + + $dispatch = ShipmentVehicleDispatch::query() + ->where('tenant_id', $tenantId) + ->findOrFail($id); + + // options에 저장할 필드 분리 + $optionFields = ['freight_cost_type', 'supply_amount', 'vat', 'total_amount', 'status']; + $directFields = ['logistics_company', 'arrival_datetime', 'tonnage', 'vehicle_no', 'driver_contact', 'remarks']; + + // 기존 options 유지하면서 업데이트 + $options = $dispatch->options ?? []; + foreach ($optionFields as $field) { + if (array_key_exists($field, $data)) { + $options[$field] = $data[$field]; + } + } + + // 직접 컬럼 업데이트 + $updateData = ['options' => $options]; + foreach ($directFields as $field) { + if (array_key_exists($field, $data)) { + $updateData[$field] = $data[$field]; + } + } + + $dispatch->update($updateData); + + return $dispatch->load('shipment'); + } +} diff --git a/database/migrations/2026_03_04_150000_add_options_to_shipment_vehicle_dispatches_table.php b/database/migrations/2026_03_04_150000_add_options_to_shipment_vehicle_dispatches_table.php new file mode 100644 index 0000000..0bd8b28 --- /dev/null +++ b/database/migrations/2026_03_04_150000_add_options_to_shipment_vehicle_dispatches_table.php @@ -0,0 +1,23 @@ +json('options')->nullable()->after('remarks') + ->comment('추가 속성 (dispatch_no, status, freight_cost_type, supply_amount, vat, total_amount, writer)'); + }); + } + + public function down(): void + { + Schema::table('shipment_vehicle_dispatches', function (Blueprint $table) { + $table->dropColumn('options'); + }); + } +}; diff --git a/routes/api/v1/inventory.php b/routes/api/v1/inventory.php index 2246754..73b66fd 100644 --- a/routes/api/v1/inventory.php +++ b/routes/api/v1/inventory.php @@ -19,6 +19,7 @@ use App\Http\Controllers\Api\V1\ReceivingController; use App\Http\Controllers\Api\V1\ShipmentController; use App\Http\Controllers\Api\V1\StockController; +use App\Http\Controllers\Api\V1\VehicleDispatchController; use Illuminate\Support\Facades\Route; // Items API (품목 관리) @@ -123,3 +124,11 @@ Route::patch('/{id}/status', [ShipmentController::class, 'updateStatus'])->whereNumber('id')->name('v1.shipments.status'); Route::delete('/{id}', [ShipmentController::class, 'destroy'])->whereNumber('id')->name('v1.shipments.destroy'); }); + +// Vehicle Dispatch API (배차차량 관리) +Route::prefix('vehicle-dispatches')->group(function () { + Route::get('', [VehicleDispatchController::class, 'index'])->name('v1.vehicle-dispatches.index'); + Route::get('/stats', [VehicleDispatchController::class, 'stats'])->name('v1.vehicle-dispatches.stats'); + Route::get('/{id}', [VehicleDispatchController::class, 'show'])->whereNumber('id')->name('v1.vehicle-dispatches.show'); + Route::put('/{id}', [VehicleDispatchController::class, 'update'])->whereNumber('id')->name('v1.vehicle-dispatches.update'); +});