feat:차량정비이력 검색 버튼 및 새로고침 버튼 추가

- 검색 버튼 클릭 또는 엔터키로 검색 실행
- 새로고침 버튼 추가 (로딩 시 회전 애니메이션)
- 필터 변경 시 자동 검색 제거

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-03 20:17:30 +09:00
parent 018590313e
commit 76b8d9795f

View File

@@ -44,6 +44,7 @@
const DollarSign = createIcon('dollar-sign');
const Fuel = createIcon('fuel');
const Car = createIcon('car');
const RefreshCw = createIcon('refresh-cw');
function VehicleMaintenanceManagement() {
const [loading, setLoading] = useState(true);
@@ -75,6 +76,7 @@ function VehicleMaintenanceManagement() {
};
const loadMaintenances = async () => {
setLoading(true);
try {
const params = new URLSearchParams({
start_date: dateRange.start,
@@ -95,16 +97,11 @@ function VehicleMaintenanceManagement() {
}
};
// 초기 로드
useEffect(() => {
loadVehicles();
loadVehicles().then(() => loadMaintenances());
}, []);
useEffect(() => {
if (vehicles.length > 0 || !loading) {
loadMaintenances();
}
}, [dateRange, filterCategory, filterVehicle]);
const [searchTerm, setSearchTerm] = useState('');
const [filterCategory, setFilterCategory] = useState('all');
const [filterVehicle, setFilterVehicle] = useState('all');
@@ -119,6 +116,18 @@ function VehicleMaintenanceManagement() {
const categories = ['주유', '정비', '보험', '세차', '주차', '통행료', '검사', '기타'];
// 검색 실행
const handleSearch = () => {
loadMaintenances();
};
// 엔터키 핸들러
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
handleSearch();
}
};
// 차량 표시용 헬퍼
const getVehicleDisplay = (vehicleId) => {
const v = vehicles.find(v => v.id === vehicleId);
@@ -145,18 +154,9 @@ function VehicleMaintenanceManagement() {
};
const parseInputCurrency = (value) => String(value).replace(/[^\d]/g, '');
// API에서 이미 필터링된 데이터를 받으므로 클라이언트 측에서는 검색어만 필터링
const filteredMaintenances = maintenances.filter(item => {
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);
const fuelAmount = filteredMaintenances.filter(m => m.category === '주유').reduce((sum, item) => sum + item.amount, 0);
const maintenanceAmount = filteredMaintenances.filter(m => m.category === '정비').reduce((sum, item) => sum + item.amount, 0);
const totalAmount = maintenances.reduce((sum, item) => sum + item.amount, 0);
const fuelAmount = maintenances.filter(m => m.category === '주유').reduce((sum, item) => sum + item.amount, 0);
const maintenanceAmount = maintenances.filter(m => m.category === '정비').reduce((sum, item) => sum + item.amount, 0);
const otherAmount = totalAmount - fuelAmount - maintenanceAmount;
// 유지비 등록/수정
@@ -192,7 +192,6 @@ function VehicleMaintenanceManagement() {
vendor: formData.vendor,
memo: formData.memo
};
console.log('저장 요청:', payload);
const url = modalMode === 'add' ? '/finance/vehicle-maintenance' : `/finance/vehicle-maintenance/${editingItem.id}`;
const method = modalMode === 'add' ? 'POST' : 'PUT';
const response = await fetch(url, {
@@ -200,9 +199,7 @@ function VehicleMaintenanceManagement() {
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content },
body: JSON.stringify(payload)
});
console.log('응답 상태:', response.status);
const result = await response.json();
console.log('응답 데이터:', result);
if (result.success) {
await loadMaintenances();
setShowModal(false);
@@ -239,7 +236,7 @@ function VehicleMaintenanceManagement() {
const handleDownload = () => {
const rows = [['차량 유지비', `${dateRange.start} ~ ${dateRange.end}`], [], ['날짜', '차량', '구분', '내용', '금액', '주행거리', '업체'],
...filteredMaintenances.map(item => [item.date, getVehicleDisplay(item.vehicleId), item.category, item.description, item.amount, item.mileage, item.vendor])];
...maintenances.map(item => [item.date, getVehicleDisplay(item.vehicleId), item.category, item.description, item.amount, item.mileage, item.vendor])];
const csvContent = rows.map(row => row.join(',')).join('\n');
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `차량유지비_${dateRange.start}_${dateRange.end}.csv`; link.click();
@@ -270,7 +267,7 @@ function VehicleMaintenanceManagement() {
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center justify-between mb-2"><span className="text-sm text-gray-500"> 유지비</span><DollarSign className="w-5 h-5 text-gray-400" /></div>
<p className="text-2xl font-bold text-gray-900">{formatCurrency(totalAmount)}</p>
<p className="text-xs text-gray-400 mt-1">{filteredMaintenances.length}</p>
<p className="text-xs text-gray-400 mt-1">{maintenances.length}</p>
</div>
<div className="bg-white rounded-xl border border-amber-200 p-6 bg-amber-50/30">
<div className="flex items-center justify-between mb-2"><span className="text-sm text-amber-700">주유비</span><Fuel className="w-5 h-5 text-amber-500" /></div>
@@ -288,21 +285,45 @@ function VehicleMaintenanceManagement() {
{/* 필터 */}
<div className="bg-white rounded-xl border border-gray-200 p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="relative">
<div className="flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-[200px]">
<Search className="w-5 h-5 text-gray-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input type="text" placeholder="내용, 업체 검색..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-500" />
<input
type="text"
placeholder="내용, 업체 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={handleKeyDown}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-500"
/>
</div>
<select value={filterVehicle} onChange={(e) => setFilterVehicle(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
<option value="all">전체 차량</option>
{vehicles.map(v => <option key={v.id} value={v.id}>{v.plateNumber} ({v.model})</option>)}
</select>
<select value={filterCategory} onChange={(e) => setFilterCategory(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg"><option value="all">전체 구분</option>{categories.map(c => <option key={c} value={c}>{c}</option>)}</select>
<div className="flex items-center gap-2 md:col-span-2">
<input type="date" value={dateRange.start} onChange={(e) => setDateRange(prev => ({ ...prev, start: e.target.value }))} className="flex-1 px-2 py-2 border border-gray-300 rounded-lg text-sm" />
<span>~</span>
<input type="date" value={dateRange.end} onChange={(e) => setDateRange(prev => ({ ...prev, end: e.target.value }))} className="flex-1 px-2 py-2 border border-gray-300 rounded-lg text-sm" />
</div>
<select value={filterCategory} onChange={(e) => setFilterCategory(e.target.value)} className="px-3 py-2 border border-gray-300 rounded-lg">
<option value="all">전체 구분</option>
{categories.map(c => <option key={c} value={c}>{c}</option>)}
</select>
<input type="date" value={dateRange.start} onChange={(e) => setDateRange(prev => ({ ...prev, start: e.target.value }))} className="px-3 py-2 border border-gray-300 rounded-lg text-sm" />
<span className="text-gray-400">~</span>
<input type="date" value={dateRange.end} onChange={(e) => setDateRange(prev => ({ ...prev, end: e.target.value }))} className="px-3 py-2 border border-gray-300 rounded-lg text-sm" />
<button
onClick={handleSearch}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-amber-500 hover:bg-amber-600 text-white rounded-lg disabled:opacity-50"
>
<Search className="w-4 h-4" />
<span className="text-sm font-medium">검색</span>
</button>
<button
onClick={handleSearch}
disabled={loading}
className="flex items-center gap-2 px-3 py-2 border border-gray-300 text-gray-700 hover:bg-gray-100 rounded-lg disabled:opacity-50"
title="새로고침"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</button>
</div>
</div>
@@ -321,9 +342,11 @@ function VehicleMaintenanceManagement() {
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{filteredMaintenances.length === 0 ? (
{loading ? (
<tr><td colSpan="7" className="px-6 py-12 text-center text-gray-400">로딩 ...</td></tr>
) : maintenances.length === 0 ? (
<tr><td colSpan="7" className="px-6 py-12 text-center text-gray-400">데이터가 없습니다.</td></tr>
) : filteredMaintenances.map(item => {
) : maintenances.map(item => {
const vehicle = vehicles.find(v => v.id === item.vehicleId);
return (
<tr key={item.id} className="hover:bg-gray-50 cursor-pointer" onClick={() => handleEdit(item)}>