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,
|
||||
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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user