diff --git a/app/Http/Controllers/Finance/VehicleMaintenanceController.php b/app/Http/Controllers/Finance/VehicleMaintenanceController.php new file mode 100644 index 00000000..12eb8802 --- /dev/null +++ b/app/Http/Controllers/Finance/VehicleMaintenanceController.php @@ -0,0 +1,200 @@ +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' => '정비 이력이 삭제되었습니다.', + ]); + } +} diff --git a/app/Models/VehicleMaintenance.php b/app/Models/VehicleMaintenance.php new file mode 100644 index 00000000..48dc64bd --- /dev/null +++ b/app/Models/VehicleMaintenance.php @@ -0,0 +1,55 @@ + '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 ['주유', '정비', '보험', '세차', '주차', '통행료', '검사', '기타']; + } +} diff --git a/resources/views/finance/vehicle-maintenance.blade.php b/resources/views/finance/vehicle-maintenance.blade.php index ed0af9ef..b48fc91f 100644 --- a/resources/views/finance/vehicle-maintenance.blade.php +++ b/resources/views/finance/vehicle-maintenance.blade.php @@ -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('삭제 중 오류가 발생했습니다.'); } }; diff --git a/routes/web.php b/routes/web.php index ac6fe633..26f0f329 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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 () {