From a36b7a2514376787798b85ed0dae2698f5899448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EB=B3=91=EC=B2=A0?= Date: Fri, 13 Mar 2026 11:38:18 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[vehicle]=20=EB=B2=95=EC=9D=B8=EC=B0=A8?= =?UTF-8?q?=EB=9F=89=20=EA=B4=80=EB=A6=AC=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 법인차량 CRUD (CorporateVehicle) - 차량 운행일지 CRUD (VehicleLog) - 차량 정비이력 CRUD (VehicleMaintenance) - 모델, 서비스, 컨트롤러, 라우트 구성 Co-Authored-By: Claude Opus 4.6 --- .../V1/Vehicle/CorporateVehicleController.php | 64 +++++++++ .../V1/Vehicle/VehicleLogController.php | 66 +++++++++ .../Vehicle/VehicleMaintenanceController.php | 56 ++++++++ app/Models/Tenants/CorporateVehicle.php | 61 +++++++++ app/Models/Tenants/VehicleLog.php | 40 ++++++ app/Models/Tenants/VehicleMaintenance.php | 36 +++++ .../Vehicle/CorporateVehicleService.php | 97 +++++++++++++ app/Services/Vehicle/VehicleLogService.php | 129 ++++++++++++++++++ .../Vehicle/VehicleMaintenanceService.php | 93 +++++++++++++ routes/api.php | 1 + routes/api/v1/vehicle.php | 35 +++++ 11 files changed, 678 insertions(+) create mode 100644 app/Http/Controllers/V1/Vehicle/CorporateVehicleController.php create mode 100644 app/Http/Controllers/V1/Vehicle/VehicleLogController.php create mode 100644 app/Http/Controllers/V1/Vehicle/VehicleMaintenanceController.php create mode 100644 app/Models/Tenants/CorporateVehicle.php create mode 100644 app/Models/Tenants/VehicleLog.php create mode 100644 app/Models/Tenants/VehicleMaintenance.php create mode 100644 app/Services/Vehicle/CorporateVehicleService.php create mode 100644 app/Services/Vehicle/VehicleLogService.php create mode 100644 app/Services/Vehicle/VehicleMaintenanceService.php create mode 100644 routes/api/v1/vehicle.php diff --git a/app/Http/Controllers/V1/Vehicle/CorporateVehicleController.php b/app/Http/Controllers/V1/Vehicle/CorporateVehicleController.php new file mode 100644 index 0000000..79adce1 --- /dev/null +++ b/app/Http/Controllers/V1/Vehicle/CorporateVehicleController.php @@ -0,0 +1,64 @@ + $this->service->index($request->only([ + 'search', 'ownership_type', 'status', 'per_page', + ])), + __('message.fetched') + ); + } + + public function show(int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->show($id), + __('message.fetched') + ); + } + + public function store(Request $request): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->store($request->all()), + __('message.created') + ); + } + + public function update(Request $request, int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->update($id, $request->all()), + __('message.updated') + ); + } + + public function destroy(int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->destroy($id), + __('message.deleted') + ); + } + + public function dropdown(): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->dropdown(), + __('message.fetched') + ); + } +} diff --git a/app/Http/Controllers/V1/Vehicle/VehicleLogController.php b/app/Http/Controllers/V1/Vehicle/VehicleLogController.php new file mode 100644 index 0000000..926a35b --- /dev/null +++ b/app/Http/Controllers/V1/Vehicle/VehicleLogController.php @@ -0,0 +1,66 @@ + $this->service->index($request->only([ + 'search', 'vehicle_id', 'year', 'month', 'trip_type', 'per_page', + ])), + __('message.fetched') + ); + } + + public function show(int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->show($id), + __('message.fetched') + ); + } + + public function store(Request $request): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->store($request->all()), + __('message.created') + ); + } + + public function update(Request $request, int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->update($id, $request->all()), + __('message.updated') + ); + } + + public function destroy(int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->destroy($id), + __('message.deleted') + ); + } + + public function summary(Request $request): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->summary($request->only([ + 'vehicle_id', 'year', 'month', + ])), + __('message.fetched') + ); + } +} diff --git a/app/Http/Controllers/V1/Vehicle/VehicleMaintenanceController.php b/app/Http/Controllers/V1/Vehicle/VehicleMaintenanceController.php new file mode 100644 index 0000000..21b60dd --- /dev/null +++ b/app/Http/Controllers/V1/Vehicle/VehicleMaintenanceController.php @@ -0,0 +1,56 @@ + $this->service->index($request->only([ + 'search', 'vehicle_id', 'category', 'start_date', 'end_date', 'per_page', + ])), + __('message.fetched') + ); + } + + public function show(int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->show($id), + __('message.fetched') + ); + } + + public function store(Request $request): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->store($request->all()), + __('message.created') + ); + } + + public function update(Request $request, int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->update($id, $request->all()), + __('message.updated') + ); + } + + public function destroy(int $id): JsonResponse + { + return ApiResponse::handle( + fn () => $this->service->destroy($id), + __('message.deleted') + ); + } +} diff --git a/app/Models/Tenants/CorporateVehicle.php b/app/Models/Tenants/CorporateVehicle.php new file mode 100644 index 0000000..4930e61 --- /dev/null +++ b/app/Models/Tenants/CorporateVehicle.php @@ -0,0 +1,61 @@ + 'integer', + 'mileage' => 'integer', + 'purchase_price' => 'integer', + 'vehicle_price' => 'integer', + 'residual_value' => 'integer', + 'deposit' => 'integer', + 'monthly_rent' => 'integer', + 'monthly_rent_tax' => 'integer', + ]; + + public function logs(): HasMany + { + return $this->hasMany(VehicleLog::class, 'vehicle_id'); + } + + public function maintenances(): HasMany + { + return $this->hasMany(VehicleMaintenance::class, 'vehicle_id'); + } +} diff --git a/app/Models/Tenants/VehicleLog.php b/app/Models/Tenants/VehicleLog.php new file mode 100644 index 0000000..2d06448 --- /dev/null +++ b/app/Models/Tenants/VehicleLog.php @@ -0,0 +1,40 @@ + 'integer', + 'distance_km' => 'integer', + ]; + + public function vehicle(): BelongsTo + { + return $this->belongsTo(CorporateVehicle::class, 'vehicle_id'); + } +} diff --git a/app/Models/Tenants/VehicleMaintenance.php b/app/Models/Tenants/VehicleMaintenance.php new file mode 100644 index 0000000..6d92bf0 --- /dev/null +++ b/app/Models/Tenants/VehicleMaintenance.php @@ -0,0 +1,36 @@ + 'integer', + 'amount' => 'integer', + 'mileage' => 'integer', + ]; + + public function vehicle(): BelongsTo + { + return $this->belongsTo(CorporateVehicle::class, 'vehicle_id'); + } +} diff --git a/app/Services/Vehicle/CorporateVehicleService.php b/app/Services/Vehicle/CorporateVehicleService.php new file mode 100644 index 0000000..c45bfa6 --- /dev/null +++ b/app/Services/Vehicle/CorporateVehicleService.php @@ -0,0 +1,97 @@ +where(function ($q) use ($search) { + $q->where('plate_number', 'like', "%{$search}%") + ->orWhere('model', 'like', "%{$search}%") + ->orWhere('driver', 'like', "%{$search}%"); + }); + } + + if (! empty($filters['ownership_type'])) { + $query->where('ownership_type', $filters['ownership_type']); + } + + if (! empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + $query->orderByDesc('id'); + + return $query->paginate($filters['per_page'] ?? 20); + } + + public function show(int $id): CorporateVehicle + { + $vehicle = CorporateVehicle::find($id); + + if (! $vehicle) { + throw new NotFoundHttpException('차량을 찾을 수 없습니다.'); + } + + return $vehicle; + } + + public function store(array $data): CorporateVehicle + { + return DB::transaction(function () use ($data) { + $data['tenant_id'] = $this->tenantId(); + + return CorporateVehicle::create($data); + }); + } + + public function update(int $id, array $data): CorporateVehicle + { + return DB::transaction(function () use ($id, $data) { + $vehicle = CorporateVehicle::find($id); + + if (! $vehicle) { + throw new NotFoundHttpException('차량을 찾을 수 없습니다.'); + } + + $vehicle->update($data); + + return $vehicle->fresh(); + }); + } + + public function destroy(int $id): bool + { + return DB::transaction(function () use ($id) { + $vehicle = CorporateVehicle::find($id); + + if (! $vehicle) { + throw new NotFoundHttpException('차량을 찾을 수 없습니다.'); + } + + return $vehicle->delete(); + }); + } + + /** + * 드롭다운 목록 (차량일지, 정비이력에서 사용) + */ + public function dropdown(): array + { + return CorporateVehicle::where('status', '!=', 'disposed') + ->orderBy('plate_number') + ->get(['id', 'plate_number', 'model']) + ->toArray(); + } +} diff --git a/app/Services/Vehicle/VehicleLogService.php b/app/Services/Vehicle/VehicleLogService.php new file mode 100644 index 0000000..fcf77fc --- /dev/null +++ b/app/Services/Vehicle/VehicleLogService.php @@ -0,0 +1,129 @@ +with(['vehicle:id,plate_number,model']); + + if (! empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('driver_name', 'like', "%{$search}%") + ->orWhere('departure_name', 'like', "%{$search}%") + ->orWhere('arrival_name', 'like', "%{$search}%"); + }); + } + + if (! empty($filters['vehicle_id'])) { + $query->where('vehicle_id', $filters['vehicle_id']); + } + + if (! empty($filters['year']) && ! empty($filters['month'])) { + $query->whereYear('log_date', $filters['year']) + ->whereMonth('log_date', $filters['month']); + } + + if (! empty($filters['trip_type'])) { + $query->where('trip_type', $filters['trip_type']); + } + + $query->orderByDesc('log_date')->orderByDesc('id'); + + return $query->paginate($filters['per_page'] ?? 20); + } + + public function show(int $id): VehicleLog + { + $log = VehicleLog::with('vehicle:id,plate_number,model')->find($id); + + if (! $log) { + throw new NotFoundHttpException('운행기록을 찾을 수 없습니다.'); + } + + return $log; + } + + public function store(array $data): VehicleLog + { + return DB::transaction(function () use ($data) { + $data['tenant_id'] = $this->tenantId(); + + return VehicleLog::create($data); + }); + } + + public function update(int $id, array $data): VehicleLog + { + return DB::transaction(function () use ($id, $data) { + $log = VehicleLog::find($id); + + if (! $log) { + throw new NotFoundHttpException('운행기록을 찾을 수 없습니다.'); + } + + $log->update($data); + + return $log->fresh(['vehicle:id,plate_number,model']); + }); + } + + public function destroy(int $id): bool + { + return DB::transaction(function () use ($id) { + $log = VehicleLog::find($id); + + if (! $log) { + throw new NotFoundHttpException('운행기록을 찾을 수 없습니다.'); + } + + return $log->delete(); + }); + } + + /** + * 월별 통계 + */ + public function summary(array $filters = []): array + { + $query = VehicleLog::query(); + + if (! empty($filters['vehicle_id'])) { + $query->where('vehicle_id', $filters['vehicle_id']); + } + + if (! empty($filters['year']) && ! empty($filters['month'])) { + $query->whereYear('log_date', $filters['year']) + ->whereMonth('log_date', $filters['month']); + } + + $totalDistance = (clone $query)->sum('distance_km'); + $totalCount = (clone $query)->count(); + + $commuteToQuery = (clone $query)->whereIn('trip_type', ['commute_to', 'commute_round']); + $commuteFromQuery = (clone $query)->whereIn('trip_type', ['commute_from', 'commute_round']); + $businessQuery = (clone $query)->whereIn('trip_type', ['business', 'business_round']); + $personalQuery = (clone $query)->whereIn('trip_type', ['personal', 'personal_round']); + + return [ + 'total_distance' => (int) $totalDistance, + 'total_count' => $totalCount, + 'commute_to_distance' => (int) $commuteToQuery->sum('distance_km'), + 'commute_to_count' => $commuteToQuery->count(), + 'commute_from_distance' => (int) $commuteFromQuery->sum('distance_km'), + 'commute_from_count' => $commuteFromQuery->count(), + 'business_distance' => (int) $businessQuery->sum('distance_km'), + 'business_count' => $businessQuery->count(), + 'personal_distance' => (int) $personalQuery->sum('distance_km'), + 'personal_count' => $personalQuery->count(), + ]; + } +} diff --git a/app/Services/Vehicle/VehicleMaintenanceService.php b/app/Services/Vehicle/VehicleMaintenanceService.php new file mode 100644 index 0000000..6170e72 --- /dev/null +++ b/app/Services/Vehicle/VehicleMaintenanceService.php @@ -0,0 +1,93 @@ +with(['vehicle:id,plate_number,model']); + + if (! empty($filters['search'])) { + $search = $filters['search']; + $query->where(function ($q) use ($search) { + $q->where('description', 'like', "%{$search}%") + ->orWhere('vendor', 'like', "%{$search}%"); + }); + } + + if (! empty($filters['vehicle_id'])) { + $query->where('vehicle_id', $filters['vehicle_id']); + } + + if (! empty($filters['category'])) { + $query->where('category', $filters['category']); + } + + if (! empty($filters['start_date'])) { + $query->where('date', '>=', $filters['start_date']); + } + + if (! empty($filters['end_date'])) { + $query->where('date', '<=', $filters['end_date']); + } + + $query->orderByDesc('date')->orderByDesc('id'); + + return $query->paginate($filters['per_page'] ?? 20); + } + + public function show(int $id): VehicleMaintenance + { + $item = VehicleMaintenance::with('vehicle:id,plate_number,model')->find($id); + + if (! $item) { + throw new NotFoundHttpException('정비이력을 찾을 수 없습니다.'); + } + + return $item; + } + + public function store(array $data): VehicleMaintenance + { + return DB::transaction(function () use ($data) { + $data['tenant_id'] = $this->tenantId(); + + return VehicleMaintenance::create($data); + }); + } + + public function update(int $id, array $data): VehicleMaintenance + { + return DB::transaction(function () use ($id, $data) { + $item = VehicleMaintenance::find($id); + + if (! $item) { + throw new NotFoundHttpException('정비이력을 찾을 수 없습니다.'); + } + + $item->update($data); + + return $item->fresh(['vehicle:id,plate_number,model']); + }); + } + + public function destroy(int $id): bool + { + return DB::transaction(function () use ($id) { + $item = VehicleMaintenance::find($id); + + if (! $item) { + throw new NotFoundHttpException('정비이력을 찾을 수 없습니다.'); + } + + return $item->delete(); + }); + } +} diff --git a/routes/api.php b/routes/api.php index e82f5e5..0a4b6ae 100644 --- a/routes/api.php +++ b/routes/api.php @@ -43,6 +43,7 @@ require __DIR__.'/api/v1/esign.php'; require __DIR__.'/api/v1/quality.php'; require __DIR__.'/api/v1/equipment.php'; + require __DIR__.'/api/v1/vehicle.php'; // 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖) Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download'); diff --git a/routes/api/v1/vehicle.php b/routes/api/v1/vehicle.php new file mode 100644 index 0000000..857cd59 --- /dev/null +++ b/routes/api/v1/vehicle.php @@ -0,0 +1,35 @@ +group(function () { + Route::get('', [CorporateVehicleController::class, 'index'])->name('v1.corporate-vehicles.index'); + Route::get('/dropdown', [CorporateVehicleController::class, 'dropdown'])->name('v1.corporate-vehicles.dropdown'); + Route::post('', [CorporateVehicleController::class, 'store'])->name('v1.corporate-vehicles.store'); + Route::get('/{id}', [CorporateVehicleController::class, 'show'])->whereNumber('id')->name('v1.corporate-vehicles.show'); + Route::put('/{id}', [CorporateVehicleController::class, 'update'])->whereNumber('id')->name('v1.corporate-vehicles.update'); + Route::delete('/{id}', [CorporateVehicleController::class, 'destroy'])->whereNumber('id')->name('v1.corporate-vehicles.destroy'); +}); + +// 차량일지 +Route::prefix('vehicle-logs')->group(function () { + Route::get('', [VehicleLogController::class, 'index'])->name('v1.vehicle-logs.index'); + Route::get('/summary', [VehicleLogController::class, 'summary'])->name('v1.vehicle-logs.summary'); + Route::post('', [VehicleLogController::class, 'store'])->name('v1.vehicle-logs.store'); + Route::get('/{id}', [VehicleLogController::class, 'show'])->whereNumber('id')->name('v1.vehicle-logs.show'); + Route::put('/{id}', [VehicleLogController::class, 'update'])->whereNumber('id')->name('v1.vehicle-logs.update'); + Route::delete('/{id}', [VehicleLogController::class, 'destroy'])->whereNumber('id')->name('v1.vehicle-logs.destroy'); +}); + +// 정비이력 +Route::prefix('vehicle-maintenances')->group(function () { + Route::get('', [VehicleMaintenanceController::class, 'index'])->name('v1.vehicle-maintenances.index'); + Route::post('', [VehicleMaintenanceController::class, 'store'])->name('v1.vehicle-maintenances.store'); + Route::get('/{id}', [VehicleMaintenanceController::class, 'show'])->whereNumber('id')->name('v1.vehicle-maintenances.show'); + Route::put('/{id}', [VehicleMaintenanceController::class, 'update'])->whereNumber('id')->name('v1.vehicle-maintenances.update'); + Route::delete('/{id}', [VehicleMaintenanceController::class, 'destroy'])->whereNumber('id')->name('v1.vehicle-maintenances.destroy'); +});