feat:차량정비이력 실제 DB 연동 구현

This commit is contained in:
김보곤
2026-02-03 19:56:44 +09:00
parent e7e2c1d515
commit fe15cecbdb
4 changed files with 448 additions and 49 deletions

View File

@@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers\Finance;
use App\Http\Controllers\Controller;
use App\Models\CorporateVehicle;
use App\Models\VehicleMaintenance;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
class VehicleMaintenanceController extends Controller
{
public function index(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('finance.vehicle-maintenance'));
}
return view('finance.vehicle-maintenance');
}
/**
* 차량 목록 조회
*/
public function vehicles(Request $request): JsonResponse
{
$tenantId = session('tenant_id', 1);
$vehicles = CorporateVehicle::where('tenant_id', $tenantId)
->orderBy('plate_number')
->get();
return response()->json([
'success' => true,
'data' => $vehicles,
]);
}
/**
* 정비 이력 목록 조회
*/
public function list(Request $request): JsonResponse
{
$tenantId = session('tenant_id', 1);
$query = VehicleMaintenance::with('vehicle')
->where('tenant_id', $tenantId);
// 차량 필터
if ($request->filled('vehicle_id') && $request->vehicle_id !== 'all') {
$query->where('vehicle_id', $request->vehicle_id);
}
// 카테고리 필터
if ($request->filled('category') && $request->category !== 'all') {
$query->where('category', $request->category);
}
// 날짜 범위 필터
if ($request->filled('start_date')) {
$query->whereDate('date', '>=', $request->start_date);
}
if ($request->filled('end_date')) {
$query->whereDate('date', '<=', $request->end_date);
}
// 검색어
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('description', 'like', "%{$search}%")
->orWhere('vendor', 'like', "%{$search}%")
->orWhere('memo', 'like', "%{$search}%");
});
}
$maintenances = $query->orderBy('date', 'desc')->get();
// 응답 포맷팅
$data = $maintenances->map(function ($m) {
return [
'id' => $m->id,
'date' => $m->date->format('Y-m-d'),
'vehicleId' => $m->vehicle_id,
'plateNumber' => $m->vehicle?->plate_number,
'model' => $m->vehicle?->model,
'category' => $m->category,
'description' => $m->description,
'amount' => $m->amount,
'mileage' => $m->mileage,
'vendor' => $m->vendor,
'memo' => $m->memo,
];
});
return response()->json([
'success' => true,
'data' => $data,
]);
}
/**
* 정비 이력 등록
*/
public function store(Request $request): JsonResponse
{
$request->validate([
'vehicle_id' => 'required|exists:corporate_vehicles,id',
'date' => 'required|date',
'category' => 'required|string|max:20',
'amount' => 'required|numeric|min:0',
]);
$tenantId = session('tenant_id', 1);
$maintenance = VehicleMaintenance::create([
'tenant_id' => $tenantId,
'vehicle_id' => $request->vehicle_id,
'date' => $request->date,
'category' => $request->category,
'description' => $request->description,
'amount' => $request->amount ?? 0,
'mileage' => $request->mileage,
'vendor' => $request->vendor,
'memo' => $request->memo,
]);
// 차량 주행거리 업데이트
if ($request->filled('mileage')) {
CorporateVehicle::where('id', $request->vehicle_id)
->where('tenant_id', $tenantId)
->update(['mileage' => $request->mileage]);
}
return response()->json([
'success' => true,
'message' => '정비 이력이 등록되었습니다.',
'data' => $maintenance,
]);
}
/**
* 정비 이력 수정
*/
public function update(Request $request, int $id): JsonResponse
{
$tenantId = session('tenant_id', 1);
$maintenance = VehicleMaintenance::where('tenant_id', $tenantId)->findOrFail($id);
$request->validate([
'vehicle_id' => 'required|exists:corporate_vehicles,id',
'date' => 'required|date',
'category' => 'required|string|max:20',
'amount' => 'required|numeric|min:0',
]);
$maintenance->update([
'vehicle_id' => $request->vehicle_id,
'date' => $request->date,
'category' => $request->category,
'description' => $request->description,
'amount' => $request->amount ?? 0,
'mileage' => $request->mileage,
'vendor' => $request->vendor,
'memo' => $request->memo,
]);
// 차량 주행거리 업데이트
if ($request->filled('mileage')) {
CorporateVehicle::where('id', $request->vehicle_id)
->where('tenant_id', $tenantId)
->update(['mileage' => $request->mileage]);
}
return response()->json([
'success' => true,
'message' => '정비 이력이 수정되었습니다.',
'data' => $maintenance,
]);
}
/**
* 정비 이력 삭제
*/
public function destroy(int $id): JsonResponse
{
$tenantId = session('tenant_id', 1);
$maintenance = VehicleMaintenance::where('tenant_id', $tenantId)->findOrFail($id);
$maintenance->delete();
return response()->json([
'success' => true,
'message' => '정비 이력이 삭제되었습니다.',
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class VehicleMaintenance extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'tenant_id',
'vehicle_id',
'date',
'category',
'description',
'amount',
'mileage',
'vendor',
'memo',
];
protected $casts = [
'date' => 'date',
'amount' => 'integer',
'mileage' => 'integer',
];
/**
* 차량 관계
*/
public function vehicle(): BelongsTo
{
return $this->belongsTo(CorporateVehicle::class, 'vehicle_id');
}
/**
* 테넌트 관계
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
/**
* 카테고리 목록
*/
public static function getCategories(): array
{
return ['주유', '정비', '보험', '세차', '주차', '통행료', '검사', '기타'];
}
}

View File

@@ -48,23 +48,64 @@
function VehicleMaintenanceManagement() {
// 탭 상태
const [activeTab, setActiveTab] = useState('maintenance');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// 차량 목록 (동적 관리)
const [vehicles, setVehicles] = useState([
{ id: 1, plateNumber: '12가 3456', model: '제네시스 G80', ownershipType: 'corporate', purchaseDate: '2023-05-15', purchasePrice: '', currentMileage: 15000 },
{ id: 2, plateNumber: '34나 5678', model: '현대 스타렉스', ownershipType: 'corporate', purchaseDate: '2022-03-10', purchasePrice: '', currentMileage: 48000 },
{ id: 3, plateNumber: '56다 7890', model: '기아 레이', ownershipType: 'rent', contractDate: '2024-01-01', rentCompany: '롯데렌터카', rentCompanyTel: '1588-1234', rentPeriod: '36개월', agreedMileage: '30000', vehiclePrice: '25000000', residualValue: '15000000', deposit: '3000000', monthlyRent: '450000', monthlyRentTax: '45000', insuranceCompany: '삼성화재', insuranceCompanyTel: '1588-5114', currentMileage: 62000 },
{ id: 4, plateNumber: '78라 1234', model: '포터2', ownershipType: 'lease', contractDate: '2023-06-01', rentCompany: '현대캐피탈', rentCompanyTel: '1588-1234', rentPeriod: '48개월', agreedMileage: '60000', vehiclePrice: '32000000', residualValue: '12000000', deposit: '5000000', monthlyRent: '520000', monthlyRentTax: '52000', insuranceCompany: 'DB손해보험', insuranceCompanyTel: '1588-0100', currentMileage: 95000 },
]);
// 차량 목록 (API에서 로드)
const [vehicles, setVehicles] = useState([]);
const [maintenances, setMaintenances] = useState([
{ id: 1, date: '2026-01-20', vehicleId: 1, category: '주유', description: '휘발유', amount: 120000, mileage: 15000, vendor: 'GS칼텍스', memo: '' },
{ id: 2, date: '2026-01-18', vehicleId: 2, category: '주유', description: '경유', amount: 95000, mileage: 48000, vendor: 'SK에너지', memo: '' },
{ id: 3, date: '2026-01-15', vehicleId: 4, category: '정비', description: '엔진오일 교환', amount: 150000, mileage: 95000, vendor: '현대오토', memo: '정기 점검' },
{ id: 4, date: '2026-01-10', vehicleId: 3, category: '보험', description: '자동차보험 연납', amount: 850000, mileage: 62000, vendor: '삼성화재', memo: '2025.01~2026.01' },
{ id: 5, date: '2026-01-05', vehicleId: 1, category: '세차', description: '세차 및 실내크리닝', amount: 50000, mileage: 14500, vendor: '카와시', memo: '' },
{ id: 6, date: '2025-12-20', vehicleId: 2, category: '정비', description: '타이어 교체', amount: 480000, mileage: 45000, vendor: '한국타이어', memo: '4개 교체' },
]);
// 정비 이력 (API에서 로드)
const [maintenances, setMaintenances] = useState([]);
// 데이터 로드
const loadVehicles = async () => {
try {
const response = await fetch('/finance/vehicle-maintenance/vehicles');
const result = await response.json();
if (result.success) {
setVehicles(result.data.map(v => ({
id: v.id,
plateNumber: v.plate_number,
model: v.model,
ownershipType: v.ownership_type,
currentMileage: v.mileage || 0
})));
}
} catch (error) {
console.error('차량 로드 실패:', error);
}
};
const loadMaintenances = async () => {
try {
const params = new URLSearchParams({
start_date: dateRange.start,
end_date: dateRange.end,
category: filterCategory,
vehicle_id: filterVehicle,
search: searchTerm
});
const response = await fetch(`/finance/vehicle-maintenance/list?${params}`);
const result = await response.json();
if (result.success) {
setMaintenances(result.data);
}
} catch (error) {
console.error('정비 이력 로드 실패:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadVehicles();
}, []);
useEffect(() => {
if (vehicles.length > 0 || !loading) {
loadMaintenances();
}
}, [dateRange, filterCategory, filterVehicle]);
const [searchTerm, setSearchTerm] = useState('');
const [filterCategory, setFilterCategory] = useState('all');
@@ -96,6 +137,14 @@ function VehicleMaintenanceManagement() {
return v ? `${v.plateNumber} (${v.model})` : '-';
};
// API 응답에서 차량 정보 표시 (plateNumber가 직접 포함된 경우)
const getVehicleDisplayFromItem = (item) => {
if (item.plateNumber && item.model) {
return `${item.plateNumber} (${item.model})`;
}
return getVehicleDisplay(item.vehicleId);
};
const getOwnershipLabel = (type) => {
const found = ownershipTypes.find(t => t.value === type);
return found ? found.label : type;
@@ -154,13 +203,13 @@ function VehicleMaintenanceManagement() {
};
const parseInputCurrency = (value) => String(value).replace(/[^\d]/g, '');
// API에서 이미 필터링된 데이터를 받으므로 클라이언트 측에서는 검색어만 필터링
const filteredMaintenances = maintenances.filter(item => {
const matchesSearch = item.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
item.vendor.toLowerCase().includes(searchTerm.toLowerCase());
const matchesCategory = filterCategory === 'all' || item.category === filterCategory;
const matchesVehicle = filterVehicle === 'all' || item.vehicleId === parseInt(filterVehicle);
const matchesDate = item.date >= dateRange.start && item.date <= dateRange.end;
return matchesSearch && matchesCategory && matchesVehicle && matchesDate;
if (!searchTerm) return true;
const search = searchTerm.toLowerCase();
return (item.description || '').toLowerCase().includes(search) ||
(item.vendor || '').toLowerCase().includes(search) ||
(item.plateNumber || '').toLowerCase().includes(search);
});
const totalAmount = filteredMaintenances.reduce((sum, item) => sum + item.amount, 0);
@@ -171,36 +220,131 @@ function VehicleMaintenanceManagement() {
// 유지비 등록/수정
const handleAdd = () => { setModalMode('add'); setFormData({...initialFormState, vehicleId: vehicles[0]?.id || ''}); setShowModal(true); };
const handleEdit = (item) => { setModalMode('edit'); setEditingItem(item); setFormData({ ...item }); setShowModal(true); };
const handleSave = () => {
const handleSave = async () => {
if (!formData.description || !formData.amount) { alert('필수 항목을 입력해주세요.'); return; }
const amount = parseInt(formData.amount) || 0;
const mileage = parseInt(formData.mileage) || 0;
if (modalMode === 'add') {
setMaintenances(prev => [{ id: Date.now(), ...formData, vehicleId: parseInt(formData.vehicleId), amount, mileage }, ...prev]);
} else {
setMaintenances(prev => prev.map(item => item.id === editingItem.id ? { ...item, ...formData, vehicleId: parseInt(formData.vehicleId), amount, mileage } : item));
setSaving(true);
try {
const payload = {
vehicle_id: parseInt(formData.vehicleId),
date: formData.date,
category: formData.category,
description: formData.description,
amount: parseInt(String(formData.amount).replace(/[^\d]/g, '')) || 0,
mileage: parseInt(String(formData.mileage).replace(/[^\d]/g, '')) || null,
vendor: formData.vendor,
memo: formData.memo
};
const url = modalMode === 'add' ? '/finance/vehicle-maintenance' : `/finance/vehicle-maintenance/${editingItem.id}`;
const method = modalMode === 'add' ? 'POST' : 'PUT';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.success) {
await loadMaintenances();
setShowModal(false);
setEditingItem(null);
} else {
alert(result.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('저장 오류:', error);
alert('저장 중 오류가 발생했습니다.');
} finally {
setSaving(false);
}
};
const handleDelete = async (id) => {
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const response = await fetch(`/finance/vehicle-maintenance/${id}`, {
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
});
const result = await response.json();
if (result.success) {
await loadMaintenances();
setShowModal(false);
} else {
alert(result.message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('삭제 오류:', error);
alert('삭제 중 오류가 발생했습니다.');
}
setShowModal(false); setEditingItem(null);
};
const handleDelete = (id) => { if (confirm('정말 삭제하시겠습니까?')) { setMaintenances(prev => prev.filter(item => item.id !== id)); setShowModal(false); } };
// 차량 등록/수정
// 차량 등록/수정 - corporate-vehicles API 사용
const handleAddVehicle = () => { setVehicleModalMode('add'); setVehicleFormData(initialVehicleFormState); setShowVehicleModal(true); };
const handleEditVehicle = (vehicle) => { setVehicleModalMode('edit'); setEditingVehicle(vehicle); setVehicleFormData({ ...vehicle }); setShowVehicleModal(true); };
const handleSaveVehicle = () => {
const handleSaveVehicle = async () => {
if (!vehicleFormData.plateNumber || !vehicleFormData.model) { alert('차량번호와 모델명은 필수입니다.'); return; }
if (vehicleModalMode === 'add') {
setVehicles(prev => [{ id: Date.now(), ...vehicleFormData }, ...prev]);
} else {
setVehicles(prev => prev.map(v => v.id === editingVehicle.id ? { ...v, ...vehicleFormData } : v));
setSaving(true);
try {
const payload = {
plate_number: vehicleFormData.plateNumber,
model: vehicleFormData.model,
vehicle_type: '승용차',
ownership_type: vehicleFormData.ownershipType,
year: new Date().getFullYear(),
mileage: parseInt(String(vehicleFormData.currentMileage).replace(/[^\d]/g, '')) || 0,
purchase_date: vehicleFormData.purchaseDate,
purchase_price: parseInt(String(vehicleFormData.purchasePrice).replace(/[^\d]/g, '')) || 0,
contract_date: vehicleFormData.contractDate,
rent_company: vehicleFormData.rentCompany,
rent_company_tel: vehicleFormData.rentCompanyTel,
rent_period: vehicleFormData.rentPeriod,
agreed_mileage: vehicleFormData.agreedMileage,
vehicle_price: parseInt(String(vehicleFormData.vehiclePrice).replace(/[^\d]/g, '')) || 0,
residual_value: parseInt(String(vehicleFormData.residualValue).replace(/[^\d]/g, '')) || 0,
deposit: parseInt(String(vehicleFormData.deposit).replace(/[^\d]/g, '')) || 0,
monthly_rent: parseInt(String(vehicleFormData.monthlyRent).replace(/[^\d]/g, '')) || 0,
monthly_rent_tax: parseInt(String(vehicleFormData.monthlyRentTax).replace(/[^\d]/g, '')) || 0,
insurance_company: vehicleFormData.insuranceCompany,
insurance_company_tel: vehicleFormData.insuranceCompanyTel
};
const url = vehicleModalMode === 'add' ? '/finance/corporate-vehicles' : `/finance/corporate-vehicles/${editingVehicle.id}`;
const method = vehicleModalMode === 'add' ? 'POST' : 'PUT';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.success) {
await loadVehicles();
setShowVehicleModal(false);
setEditingVehicle(null);
} else {
alert(result.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('저장 오류:', error);
alert('저장 중 오류가 발생했습니다.');
} finally {
setSaving(false);
}
setShowVehicleModal(false); setEditingVehicle(null);
};
const handleDeleteVehicle = (id) => {
if (confirm('차량을 삭제하시겠습니까? 관련 유지비 기록도 함께 삭제됩니다.')) {
setVehicles(prev => prev.filter(v => v.id !== id));
setMaintenances(prev => prev.filter(m => m.vehicleId !== id));
setShowVehicleModal(false);
const handleDeleteVehicle = async (id) => {
if (!confirm('차량을 삭제하시겠습니까? 관련 유지비 기록도 함께 삭제됩니다.')) return;
try {
const response = await fetch(`/finance/corporate-vehicles/${id}`, {
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }
});
const result = await response.json();
if (result.success) {
await loadVehicles();
await loadMaintenances();
setShowVehicleModal(false);
} else {
alert(result.message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('삭제 오류:', error);
alert('삭제 중 오류가 발생했습니다.');
}
};

View File

@@ -834,13 +834,13 @@
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', function () {
if (request()->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('finance.vehicle-maintenance'));
}
return view('finance.vehicle-maintenance');
})->name('vehicle-maintenance');
// 차량정비이력
Route::get('/vehicle-maintenance', [\App\Http\Controllers\Finance\VehicleMaintenanceController::class, 'index'])->name('vehicle-maintenance');
Route::get('/vehicle-maintenance/vehicles', [\App\Http\Controllers\Finance\VehicleMaintenanceController::class, 'vehicles'])->name('vehicle-maintenance.vehicles');
Route::get('/vehicle-maintenance/list', [\App\Http\Controllers\Finance\VehicleMaintenanceController::class, 'list'])->name('vehicle-maintenance.list');
Route::post('/vehicle-maintenance', [\App\Http\Controllers\Finance\VehicleMaintenanceController::class, 'store'])->name('vehicle-maintenance.store');
Route::put('/vehicle-maintenance/{id}', [\App\Http\Controllers\Finance\VehicleMaintenanceController::class, 'update'])->name('vehicle-maintenance.update');
Route::delete('/vehicle-maintenance/{id}', [\App\Http\Controllers\Finance\VehicleMaintenanceController::class, 'destroy'])->name('vehicle-maintenance.destroy');
// 거래처관리
Route::get('/customers', function () {