feat:차량정비이력 검색 버튼 및 새로고침 버튼 추가
- 검색 버튼 클릭 또는 엔터키로 검색 실행 - 새로고침 버튼 추가 (로딩 시 회전 애니메이션) - 필터 변경 시 자동 검색 제거 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)}>
|
||||
|
||||
Reference in New Issue
Block a user