feat: Add search/filter bar to admin cars page
- Backend: Add search (car name/plate number), color, year filters to GET /api/cars - Frontend: Add filter bar with car name/plate, color, year range inputs - Clear button resets all filters Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,8 @@ def get_cars(
|
|||||||
price_max: Optional[int] = None,
|
price_max: Optional[int] = None,
|
||||||
mileage_max: Optional[int] = None,
|
mileage_max: Optional[int] = None,
|
||||||
fuel: Optional[str] = None,
|
fuel: Optional[str] = None,
|
||||||
|
color: Optional[str] = None,
|
||||||
|
search: Optional[str] = None,
|
||||||
status: Optional[str] = None,
|
status: Optional[str] = None,
|
||||||
is_displayed: Optional[bool] = None,
|
is_displayed: Optional[bool] = None,
|
||||||
admin: bool = Query(False, description="Admin mode - show all cars"),
|
admin: bool = Query(False, description="Admin mode - show all cars"),
|
||||||
@@ -85,6 +87,14 @@ def get_cars(
|
|||||||
if is_displayed is not None and admin:
|
if is_displayed is not None and admin:
|
||||||
base_query = base_query.filter(Car.is_displayed == is_displayed)
|
base_query = base_query.filter(Car.is_displayed == is_displayed)
|
||||||
|
|
||||||
|
# 통합 검색 (차량번호, 차량명)
|
||||||
|
if search:
|
||||||
|
search_term = f"%{search}%"
|
||||||
|
base_query = base_query.filter(
|
||||||
|
(Car.car_number.ilike(search_term)) |
|
||||||
|
(Car.car_name.ilike(search_term))
|
||||||
|
)
|
||||||
|
|
||||||
if maker_id:
|
if maker_id:
|
||||||
base_query = base_query.filter(Car.maker_id == maker_id)
|
base_query = base_query.filter(Car.maker_id == maker_id)
|
||||||
if model_id:
|
if model_id:
|
||||||
@@ -101,6 +111,8 @@ def get_cars(
|
|||||||
base_query = base_query.filter(Car.mileage <= mileage_max)
|
base_query = base_query.filter(Car.mileage <= mileage_max)
|
||||||
if fuel:
|
if fuel:
|
||||||
base_query = base_query.filter(Car.fuel == fuel)
|
base_query = base_query.filter(Car.fuel == fuel)
|
||||||
|
if color:
|
||||||
|
base_query = base_query.filter(Car.color.ilike(f"%{color}%"))
|
||||||
|
|
||||||
total = base_query.count()
|
total = base_query.count()
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,12 @@ export default function CarsAdminPage() {
|
|||||||
const [hasBannerChanges, setHasBannerChanges] = useState(false); // 변경사항 있는지
|
const [hasBannerChanges, setHasBannerChanges] = useState(false); // 변경사항 있는지
|
||||||
const [updatingBanners, setUpdatingBanners] = useState(false); // 업데이트 중
|
const [updatingBanners, setUpdatingBanners] = useState(false); // 업데이트 중
|
||||||
|
|
||||||
|
// Local cars filter state
|
||||||
|
const [localFilterSearch, setLocalFilterSearch] = useState('');
|
||||||
|
const [localFilterColor, setLocalFilterColor] = useState('');
|
||||||
|
const [localFilterYearMin, setLocalFilterYearMin] = useState('');
|
||||||
|
const [localFilterYearMax, setLocalFilterYearMax] = useState('');
|
||||||
|
|
||||||
// All Cars (public view) state
|
// All Cars (public view) state
|
||||||
const [allCars, setAllCars] = useState<LocalCar[]>([]);
|
const [allCars, setAllCars] = useState<LocalCar[]>([]);
|
||||||
const [allCarsLoading, setAllCarsLoading] = useState(false);
|
const [allCarsLoading, setAllCarsLoading] = useState(false);
|
||||||
@@ -464,10 +470,19 @@ export default function CarsAdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadLocalCars = async (page = 1, preserveBannerState = false) => {
|
const loadLocalCars = async (page = 1, preserveBannerState = false, overrideFilters?: { search?: string; color?: string; yearMin?: string; yearMax?: string }) => {
|
||||||
setLocalLoading(true);
|
setLocalLoading(true);
|
||||||
try {
|
try {
|
||||||
const { data } = await api.get('/cars', { params: { page, page_size: 100, admin: true } });
|
const filterParams: any = { page, page_size: 100, admin: true };
|
||||||
|
const s = overrideFilters?.search ?? localFilterSearch;
|
||||||
|
const c = overrideFilters?.color ?? localFilterColor;
|
||||||
|
const yMin = overrideFilters?.yearMin ?? localFilterYearMin;
|
||||||
|
const yMax = overrideFilters?.yearMax ?? localFilterYearMax;
|
||||||
|
if (s) filterParams.search = s;
|
||||||
|
if (c) filterParams.color = c;
|
||||||
|
if (yMin) filterParams.year_min = parseInt(yMin);
|
||||||
|
if (yMax) filterParams.year_max = parseInt(yMax);
|
||||||
|
const { data } = await api.get('/cars', { params: filterParams });
|
||||||
const cars: LocalCar[] = data.cars || [];
|
const cars: LocalCar[] = data.cars || [];
|
||||||
|
|
||||||
// 배너 목록도 함께 로드 (순서 정보 포함) - 실패해도 차량 목록은 표시
|
// 배너 목록도 함께 로드 (순서 정보 포함) - 실패해도 차량 목록은 표시
|
||||||
@@ -1562,6 +1577,74 @@ export default function CarsAdminPage() {
|
|||||||
{/* Local Cars Tab */}
|
{/* Local Cars Tab */}
|
||||||
{activeTab === 'local' && (
|
{activeTab === 'local' && (
|
||||||
<div className="bg-white rounded-xl shadow-sm p-6">
|
<div className="bg-white rounded-xl shadow-sm p-6">
|
||||||
|
{/* Filter Bar */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4 mb-4">
|
||||||
|
<form onSubmit={(e) => { e.preventDefault(); setLocalPage(1); loadLocalCars(1); }} className="flex flex-wrap gap-3 items-end">
|
||||||
|
<div className="flex-1 min-w-[200px]">
|
||||||
|
<label className="block text-xs font-medium text-gray-500 mb-1">Car Name / Plate Number</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={localFilterSearch}
|
||||||
|
onChange={(e) => setLocalFilterSearch(e.target.value)}
|
||||||
|
placeholder="NX, 286소9799..."
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-32">
|
||||||
|
<label className="block text-xs font-medium text-gray-500 mb-1">Color</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={localFilterColor}
|
||||||
|
onChange={(e) => setLocalFilterColor(e.target.value)}
|
||||||
|
placeholder="검정, 흰색..."
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-24">
|
||||||
|
<label className="block text-xs font-medium text-gray-500 mb-1">Year From</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={localFilterYearMin}
|
||||||
|
onChange={(e) => setLocalFilterYearMin(e.target.value)}
|
||||||
|
placeholder="2020"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-24">
|
||||||
|
<label className="block text-xs font-medium text-gray-500 mb-1">Year To</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={localFilterYearMax}
|
||||||
|
onChange={(e) => setLocalFilterYearMax(e.target.value)}
|
||||||
|
placeholder="2025"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 text-sm font-medium"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
{(localFilterSearch || localFilterColor || localFilterYearMin || localFilterYearMax) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setLocalFilterSearch('');
|
||||||
|
setLocalFilterColor('');
|
||||||
|
setLocalFilterYearMin('');
|
||||||
|
setLocalFilterYearMax('');
|
||||||
|
setLocalPage(1);
|
||||||
|
loadLocalCars(1, false, { search: '', color: '', yearMin: '', yearMax: '' });
|
||||||
|
}}
|
||||||
|
className="px-4 py-2 border border-gray-300 text-gray-600 rounded-lg hover:bg-gray-100 text-sm"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-lg font-semibold text-gray-800">
|
<h2 className="text-lg font-semibold text-gray-800">
|
||||||
Imported Cars ({localTotal} total)
|
Imported Cars ({localTotal} total)
|
||||||
|
|||||||
Reference in New Issue
Block a user