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:
AutonetSellCar Deploy
2026-04-02 14:54:01 +09:00
parent 234f91a14a
commit b17840ef75
2 changed files with 97 additions and 2 deletions

View File

@@ -64,6 +64,8 @@ def get_cars(
price_max: Optional[int] = None,
mileage_max: Optional[int] = None,
fuel: Optional[str] = None,
color: Optional[str] = None,
search: Optional[str] = None,
status: Optional[str] = None,
is_displayed: Optional[bool] = None,
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:
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:
base_query = base_query.filter(Car.maker_id == maker_id)
if model_id:
@@ -101,6 +111,8 @@ def get_cars(
base_query = base_query.filter(Car.mileage <= mileage_max)
if 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()

View File

@@ -153,6 +153,12 @@ export default function CarsAdminPage() {
const [hasBannerChanges, setHasBannerChanges] = 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
const [allCars, setAllCars] = useState<LocalCar[]>([]);
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);
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 || [];
// 배너 목록도 함께 로드 (순서 정보 포함) - 실패해도 차량 목록은 표시
@@ -1562,6 +1577,74 @@ export default function CarsAdminPage() {
{/* Local Cars Tab */}
{activeTab === 'local' && (
<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">
<h2 className="text-lg font-semibold text-gray-800">
Imported Cars ({localTotal} total)