feat:차량일지 기능 구현
- VehicleLogController: CRUD 및 통계 API 추가 - VehicleLog 모델: 구분/분류 코드 정의 추가 - vehicle-logs.blade.php: React 기반 운행기록부 UI - routes/web.php: vehicles, summary 엔드포인트 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,6 @@
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class VehicleLogController extends Controller
|
||||
{
|
||||
@@ -21,82 +18,112 @@ public function index(Request $request): View|Response
|
||||
return response('', 200)->header('HX-Redirect', route('finance.vehicle-logs'));
|
||||
}
|
||||
|
||||
return view('finance.vehicle-logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 목록 조회
|
||||
*/
|
||||
public function vehicles(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('tenant_id', 1);
|
||||
|
||||
$vehicles = CorporateVehicle::where('tenant_id', $tenantId)
|
||||
->where('status', 'active')
|
||||
->orderBy('plate_number')
|
||||
->get();
|
||||
|
||||
return view('finance.vehicle-logs', [
|
||||
'vehicles' => $vehicles,
|
||||
'tripTypes' => VehicleLog::tripTypeLabels(),
|
||||
'locationTypes' => VehicleLog::locationTypeLabels(),
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $vehicles,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 운행기록 목록 조회
|
||||
*/
|
||||
public function list(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('tenant_id', 1);
|
||||
|
||||
$request->validate([
|
||||
'vehicle_id' => 'required|integer',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
]);
|
||||
$query = VehicleLog::with('vehicle')
|
||||
->where('tenant_id', $tenantId);
|
||||
|
||||
$vehicleId = $request->vehicle_id;
|
||||
$startDate = $request->start_date;
|
||||
$endDate = $request->end_date;
|
||||
// 차량 필터
|
||||
if ($request->filled('vehicle_id') && $request->vehicle_id !== 'all') {
|
||||
$query->where('vehicle_id', $request->vehicle_id);
|
||||
}
|
||||
|
||||
// 차량 정보
|
||||
$vehicle = CorporateVehicle::where('tenant_id', $tenantId)
|
||||
->findOrFail($vehicleId);
|
||||
// 년/월 필터
|
||||
if ($request->filled('year')) {
|
||||
$query->whereYear('log_date', $request->year);
|
||||
}
|
||||
if ($request->filled('month')) {
|
||||
$query->whereMonth('log_date', $request->month);
|
||||
}
|
||||
|
||||
// 전체 운행기록 수 (해당 차량)
|
||||
$totalCount = VehicleLog::where('tenant_id', $tenantId)
|
||||
->where('vehicle_id', $vehicleId)
|
||||
->count();
|
||||
// 구분 필터
|
||||
if ($request->filled('trip_type') && $request->trip_type !== 'all') {
|
||||
$query->where('trip_type', $request->trip_type);
|
||||
}
|
||||
|
||||
$logs = VehicleLog::where('tenant_id', $tenantId)
|
||||
->where('vehicle_id', $vehicleId)
|
||||
->whereBetween('log_date', [$startDate, $endDate])
|
||||
->orderBy('log_date', 'desc')
|
||||
// 검색어
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('driver_name', 'like', "%{$search}%")
|
||||
->orWhere('department', 'like', "%{$search}%")
|
||||
->orWhere('departure_name', 'like', "%{$search}%")
|
||||
->orWhere('arrival_name', 'like', "%{$search}%")
|
||||
->orWhere('note', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
$logs = $query->orderBy('log_date', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->get();
|
||||
|
||||
// 월별 합계
|
||||
$totals = [
|
||||
'business_km' => $logs->whereIn('trip_type', ['commute_to', 'commute_from', 'business', 'commute_round', 'business_round'])->sum('distance_km'),
|
||||
'personal_km' => $logs->whereIn('trip_type', ['personal', 'personal_round'])->sum('distance_km'),
|
||||
'total_km' => $logs->sum('distance_km'),
|
||||
];
|
||||
// 응답 포맷팅
|
||||
$data = $logs->map(function ($log) {
|
||||
return [
|
||||
'id' => $log->id,
|
||||
'logDate' => $log->log_date->format('Y-m-d'),
|
||||
'vehicleId' => $log->vehicle_id,
|
||||
'plateNumber' => $log->vehicle?->plate_number,
|
||||
'model' => $log->vehicle?->model,
|
||||
'department' => $log->department,
|
||||
'driverName' => $log->driver_name,
|
||||
'tripType' => $log->trip_type,
|
||||
'departureType' => $log->departure_type,
|
||||
'departureName' => $log->departure_name,
|
||||
'departureAddress' => $log->departure_address,
|
||||
'arrivalType' => $log->arrival_type,
|
||||
'arrivalName' => $log->arrival_name,
|
||||
'arrivalAddress' => $log->arrival_address,
|
||||
'distanceKm' => $log->distance_km,
|
||||
'note' => $log->note,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'vehicle' => $vehicle,
|
||||
'logs' => $logs,
|
||||
'totals' => $totals,
|
||||
'totalCount' => $totalCount,
|
||||
],
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 운행기록 등록
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('tenant_id', 1);
|
||||
|
||||
$request->validate([
|
||||
'vehicle_id' => 'required|integer|exists:corporate_vehicles,id',
|
||||
'vehicle_id' => 'required|exists:corporate_vehicles,id',
|
||||
'log_date' => 'required|date',
|
||||
'driver_name' => 'required|string|max:50',
|
||||
'trip_type' => 'required|in:commute_to,commute_from,business,personal,commute_round,business_round,personal_round',
|
||||
'trip_type' => 'required|in:commute_to,commute_from,business,personal',
|
||||
'distance_km' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
// 해당 차량이 현재 테넌트의 것인지 확인
|
||||
CorporateVehicle::where('tenant_id', $tenantId)
|
||||
->findOrFail($request->vehicle_id);
|
||||
$tenantId = session('tenant_id', 1);
|
||||
|
||||
$log = VehicleLog::create([
|
||||
'tenant_id' => $tenantId,
|
||||
@@ -122,6 +149,9 @@ public function store(Request $request): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 운행기록 수정
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('tenant_id', 1);
|
||||
@@ -129,13 +159,15 @@ public function update(Request $request, int $id): JsonResponse
|
||||
$log = VehicleLog::where('tenant_id', $tenantId)->findOrFail($id);
|
||||
|
||||
$request->validate([
|
||||
'vehicle_id' => 'required|exists:corporate_vehicles,id',
|
||||
'log_date' => 'required|date',
|
||||
'driver_name' => 'required|string|max:50',
|
||||
'trip_type' => 'required|in:commute_to,commute_from,business,personal,commute_round,business_round,personal_round',
|
||||
'trip_type' => 'required|in:commute_to,commute_from,business,personal',
|
||||
'distance_km' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$log->update([
|
||||
'vehicle_id' => $request->vehicle_id,
|
||||
'log_date' => $request->log_date,
|
||||
'department' => $request->department,
|
||||
'driver_name' => $request->driver_name,
|
||||
@@ -157,6 +189,9 @@ public function update(Request $request, int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 운행기록 삭제
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$tenantId = session('tenant_id', 1);
|
||||
@@ -170,96 +205,65 @@ public function destroy(int $id): JsonResponse
|
||||
]);
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse
|
||||
/**
|
||||
* 월간 통계 조회
|
||||
*/
|
||||
public function summary(Request $request): JsonResponse
|
||||
{
|
||||
$tenantId = session('tenant_id', 1);
|
||||
|
||||
$request->validate([
|
||||
'vehicle_id' => 'required|integer',
|
||||
'start_date' => 'required|date',
|
||||
'end_date' => 'required|date|after_or_equal:start_date',
|
||||
]);
|
||||
$query = VehicleLog::where('tenant_id', $tenantId);
|
||||
|
||||
$vehicleId = $request->vehicle_id;
|
||||
$startDate = $request->start_date;
|
||||
$endDate = $request->end_date;
|
||||
|
||||
$vehicle = CorporateVehicle::where('tenant_id', $tenantId)
|
||||
->findOrFail($vehicleId);
|
||||
|
||||
$logs = VehicleLog::where('tenant_id', $tenantId)
|
||||
->where('vehicle_id', $vehicleId)
|
||||
->whereBetween('log_date', [$startDate, $endDate])
|
||||
->orderBy('log_date')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('운행기록부');
|
||||
|
||||
// 기본 정보
|
||||
$sheet->setCellValue('A1', '업무용승용차 운행기록부');
|
||||
$sheet->setCellValue('A3', '차량번호');
|
||||
$sheet->setCellValue('B3', $vehicle->plate_number);
|
||||
$sheet->setCellValue('C3', '차종');
|
||||
$sheet->setCellValue('D3', $vehicle->model);
|
||||
$sheet->setCellValue('E3', '구분');
|
||||
$sheet->setCellValue('F3', $this->getOwnershipTypeLabel($vehicle->ownership_type));
|
||||
$sheet->setCellValue('A4', '조회기간');
|
||||
$sheet->setCellValue('B4', sprintf('%s ~ %s', $startDate, $endDate));
|
||||
|
||||
// 헤더
|
||||
$headers = ['일자', '부서', '성명', '구분', '출발지', '도착지', '주행km', '비고'];
|
||||
$col = 'A';
|
||||
foreach ($headers as $header) {
|
||||
$sheet->setCellValue($col . '6', $header);
|
||||
$col++;
|
||||
// 차량 필터
|
||||
if ($request->filled('vehicle_id') && $request->vehicle_id !== 'all') {
|
||||
$query->where('vehicle_id', $request->vehicle_id);
|
||||
}
|
||||
|
||||
// 데이터
|
||||
$row = 7;
|
||||
$tripTypeLabels = VehicleLog::tripTypeLabels();
|
||||
|
||||
foreach ($logs as $log) {
|
||||
$sheet->setCellValue('A' . $row, $log->log_date->format('Y-m-d'));
|
||||
$sheet->setCellValue('B' . $row, $log->department ?? '');
|
||||
$sheet->setCellValue('C' . $row, $log->driver_name);
|
||||
$sheet->setCellValue('D' . $row, $tripTypeLabels[$log->trip_type] ?? $log->trip_type);
|
||||
$sheet->setCellValue('E' . $row, $log->departure_name ?? '');
|
||||
$sheet->setCellValue('F' . $row, $log->arrival_name ?? '');
|
||||
$sheet->setCellValue('G' . $row, $log->distance_km);
|
||||
$sheet->setCellValue('H' . $row, $log->note ?? '');
|
||||
$row++;
|
||||
// 년/월 필터
|
||||
if ($request->filled('year')) {
|
||||
$query->whereYear('log_date', $request->year);
|
||||
}
|
||||
if ($request->filled('month')) {
|
||||
$query->whereMonth('log_date', $request->month);
|
||||
}
|
||||
|
||||
// 합계
|
||||
$businessKm = $logs->whereIn('trip_type', ['commute_to', 'commute_from', 'business', 'commute_round', 'business_round'])->sum('distance_km');
|
||||
$personalKm = $logs->whereIn('trip_type', ['personal', 'personal_round'])->sum('distance_km');
|
||||
$totalKm = $logs->sum('distance_km');
|
||||
// 구분별 주행거리 합계
|
||||
$summary = $query->selectRaw('
|
||||
trip_type,
|
||||
COUNT(*) as count,
|
||||
SUM(distance_km) as total_distance
|
||||
')
|
||||
->groupBy('trip_type')
|
||||
->get()
|
||||
->keyBy('trip_type');
|
||||
|
||||
$sheet->setCellValue('A' . $row, '합계');
|
||||
$sheet->setCellValue('F' . $row, '업무용: ' . number_format($businessKm) . 'km');
|
||||
$sheet->setCellValue('G' . $row, number_format($totalKm));
|
||||
$sheet->setCellValue('H' . $row, '비업무: ' . number_format($personalKm) . 'km');
|
||||
$tripTypes = VehicleLog::getTripTypes();
|
||||
$result = [];
|
||||
$totalCount = 0;
|
||||
$totalDistance = 0;
|
||||
|
||||
$filename = sprintf('운행기록부_%s_%s_%s.xlsx', $vehicle->plate_number, $startDate, $endDate);
|
||||
foreach ($tripTypes as $type => $label) {
|
||||
$data = $summary->get($type);
|
||||
$count = $data ? $data->count : 0;
|
||||
$distance = $data ? $data->total_distance : 0;
|
||||
$result[$type] = [
|
||||
'label' => $label,
|
||||
'count' => $count,
|
||||
'distance' => $distance,
|
||||
];
|
||||
$totalCount += $count;
|
||||
$totalDistance += $distance;
|
||||
}
|
||||
|
||||
return response()->streamDownload(function () use ($spreadsheet) {
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$writer->save('php://output');
|
||||
}, $filename, [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'byType' => $result,
|
||||
'total' => [
|
||||
'count' => $totalCount,
|
||||
'distance' => $totalDistance,
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function getOwnershipTypeLabel(string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'corporate' => '회사',
|
||||
'rent' => '렌트',
|
||||
'lease' => '리스',
|
||||
default => $type,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class VehicleLog extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
@@ -28,65 +29,63 @@ class VehicleLog extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'log_date' => 'date:Y-m-d',
|
||||
'log_date' => 'date',
|
||||
'distance_km' => 'integer',
|
||||
];
|
||||
|
||||
// trip_type 상수
|
||||
public const TRIP_TYPE_COMMUTE_TO = 'commute_to';
|
||||
public const TRIP_TYPE_COMMUTE_FROM = 'commute_from';
|
||||
public const TRIP_TYPE_BUSINESS = 'business';
|
||||
public const TRIP_TYPE_PERSONAL = 'personal';
|
||||
public const TRIP_TYPE_COMMUTE_ROUND = 'commute_round';
|
||||
public const TRIP_TYPE_BUSINESS_ROUND = 'business_round';
|
||||
public const TRIP_TYPE_PERSONAL_ROUND = 'personal_round';
|
||||
|
||||
// location_type 상수
|
||||
public const LOCATION_TYPE_HOME = 'home';
|
||||
public const LOCATION_TYPE_OFFICE = 'office';
|
||||
public const LOCATION_TYPE_CLIENT = 'client';
|
||||
public const LOCATION_TYPE_OTHER = 'other';
|
||||
|
||||
public static function tripTypeLabels(): array
|
||||
{
|
||||
return [
|
||||
self::TRIP_TYPE_COMMUTE_TO => '출근용',
|
||||
self::TRIP_TYPE_COMMUTE_FROM => '퇴근용',
|
||||
self::TRIP_TYPE_BUSINESS => '업무용',
|
||||
self::TRIP_TYPE_PERSONAL => '비업무용(개인)',
|
||||
self::TRIP_TYPE_COMMUTE_ROUND => '출퇴근용(왕복)',
|
||||
self::TRIP_TYPE_BUSINESS_ROUND => '업무용(왕복)',
|
||||
self::TRIP_TYPE_PERSONAL_ROUND => '비업무용(왕복)',
|
||||
];
|
||||
}
|
||||
|
||||
public static function locationTypeLabels(): array
|
||||
{
|
||||
return [
|
||||
self::LOCATION_TYPE_HOME => '자택',
|
||||
self::LOCATION_TYPE_OFFICE => '회사',
|
||||
self::LOCATION_TYPE_CLIENT => '거래처',
|
||||
self::LOCATION_TYPE_OTHER => '기타',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 차량 관계
|
||||
*/
|
||||
public function vehicle(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CorporateVehicle::class, 'vehicle_id');
|
||||
}
|
||||
|
||||
public function getTripTypeLabelAttribute(): string
|
||||
/**
|
||||
* 테넌트 관계
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return self::tripTypeLabels()[$this->trip_type] ?? $this->trip_type;
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function getDepartureTypeLabelAttribute(): string
|
||||
/**
|
||||
* 구분(trip_type) 목록
|
||||
*/
|
||||
public static function getTripTypes(): array
|
||||
{
|
||||
return self::locationTypeLabels()[$this->departure_type] ?? ($this->departure_type ?? '');
|
||||
return [
|
||||
'commute_to' => '출근용',
|
||||
'commute_from' => '퇴근용',
|
||||
'business' => '업무용',
|
||||
'personal' => '비업무',
|
||||
];
|
||||
}
|
||||
|
||||
public function getArrivalTypeLabelAttribute(): string
|
||||
/**
|
||||
* 분류(location_type) 목록
|
||||
*/
|
||||
public static function getLocationTypes(): array
|
||||
{
|
||||
return self::locationTypeLabels()[$this->arrival_type] ?? ($this->arrival_type ?? '');
|
||||
return [
|
||||
'home' => '자택',
|
||||
'office' => '회사',
|
||||
'client' => '거래처',
|
||||
'other' => '기타',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 비고 목록
|
||||
*/
|
||||
public static function getNoteOptions(): array
|
||||
{
|
||||
return [
|
||||
'거래처방문',
|
||||
'제조시설등',
|
||||
'회의참석',
|
||||
'판촉활동',
|
||||
'교육등',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -828,11 +828,12 @@
|
||||
|
||||
// 차량일지 (운행기록부)
|
||||
Route::get('/vehicle-logs', [\App\Http\Controllers\Finance\VehicleLogController::class, 'index'])->name('vehicle-logs');
|
||||
Route::get('/vehicle-logs/vehicles', [\App\Http\Controllers\Finance\VehicleLogController::class, 'vehicles'])->name('vehicle-logs.vehicles');
|
||||
Route::get('/vehicle-logs/list', [\App\Http\Controllers\Finance\VehicleLogController::class, 'list'])->name('vehicle-logs.list');
|
||||
Route::get('/vehicle-logs/summary', [\App\Http\Controllers\Finance\VehicleLogController::class, 'summary'])->name('vehicle-logs.summary');
|
||||
Route::post('/vehicle-logs', [\App\Http\Controllers\Finance\VehicleLogController::class, 'store'])->name('vehicle-logs.store');
|
||||
Route::put('/vehicle-logs/{id}', [\App\Http\Controllers\Finance\VehicleLogController::class, 'update'])->name('vehicle-logs.update');
|
||||
Route::delete('/vehicle-logs/{id}', [\App\Http\Controllers\Finance\VehicleLogController::class, 'destroy'])->name('vehicle-logs.destroy');
|
||||
Route::get('/vehicle-logs/export', [\App\Http\Controllers\Finance\VehicleLogController::class, 'export'])->name('vehicle-logs.export');
|
||||
|
||||
// 차량정비이력
|
||||
Route::get('/vehicle-maintenance', [\App\Http\Controllers\Finance\VehicleMaintenanceController::class, 'index'])->name('vehicle-maintenance');
|
||||
|
||||
Reference in New Issue
Block a user