From b17840ef75723433d51f6d6bd10a2ad8a5d1bc2e Mon Sep 17 00:00:00 2001 From: AutonetSellCar Deploy Date: Thu, 2 Apr 2026 14:54:01 +0900 Subject: [PATCH] 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) --- backend/app/api/cars.py | 12 ++++ frontend/src/app/admin/cars/page.tsx | 87 +++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/backend/app/api/cars.py b/backend/app/api/cars.py index 8080986..929fc45 100644 --- a/backend/app/api/cars.py +++ b/backend/app/api/cars.py @@ -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() diff --git a/frontend/src/app/admin/cars/page.tsx b/frontend/src/app/admin/cars/page.tsx index b7eefa0..23f2a9c 100644 --- a/frontend/src/app/admin/cars/page.tsx +++ b/frontend/src/app/admin/cars/page.tsx @@ -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([]); 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' && (
+ {/* Filter Bar */} +
+
{ e.preventDefault(); setLocalPage(1); loadLocalCars(1); }} className="flex flex-wrap gap-3 items-end"> +
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + 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" + /> +
+ + {(localFilterSearch || localFilterColor || localFilterYearMin || localFilterYearMax) && ( + + )} +
+
+

Imported Cars ({localTotal} total)