feat: Add Request functionality to All Cars tab
- Add checkbox selection for cars in All Cars tab when requestId is present - Add "Add to Request" button with selection count - Add select all/deselect all functionality - Highlight selected cars with blue ring Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -151,6 +151,8 @@ export default function CarsAdminPage() {
|
||||
const [allCarsLoading, setAllCarsLoading] = useState(false);
|
||||
const [allCarsTotal, setAllCarsTotal] = useState(0);
|
||||
const [allCarsPage, setAllCarsPage] = useState(1);
|
||||
const [selectedAllCars, setSelectedAllCars] = useState<Set<number>>(new Set());
|
||||
const [addingAllCarsToRequest, setAddingAllCarsToRequest] = useState(false);
|
||||
|
||||
// Carmodoo search state
|
||||
const [makers, setMakers] = useState<CarmodooMaker[]>([]);
|
||||
@@ -1093,6 +1095,92 @@ export default function CarsAdminPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// All Cars 탭에서 Request에 추가하는 함수
|
||||
const handleAddAllCarsToRequest = async () => {
|
||||
if (!requestId) return;
|
||||
if (selectedAllCars.size === 0) {
|
||||
alert('Please select at least one car to add to the request.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`${selectedAllCars.size}개의 차량을 추천 목록에 추가하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAddingAllCarsToRequest(true);
|
||||
|
||||
try {
|
||||
const selectedCarsList = allCars.filter(car => selectedAllCars.has(car.id));
|
||||
let addedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const car of selectedCarsList) {
|
||||
try {
|
||||
const mainImage = car.images?.find((img) => img.is_main)?.url || car.images?.[0]?.url;
|
||||
await vehicleRequestsApi.adminAddVehicle(parseInt(requestId), {
|
||||
request_id: parseInt(requestId),
|
||||
car_id: car.id, // 이미 로컬 DB에 있는 차량이므로 car_id 사용
|
||||
car_data: {
|
||||
id: car.id.toString(),
|
||||
car_name: car.car_name,
|
||||
maker_name: car.maker?.name,
|
||||
model_name: car.model?.name,
|
||||
year: car.year,
|
||||
mileage: car.mileage,
|
||||
final_price: car.final_price_krw || car.price_krw,
|
||||
fuel: car.fuel,
|
||||
transmission: car.transmission,
|
||||
color: car.color,
|
||||
main_image: mainImage,
|
||||
},
|
||||
is_approved: true,
|
||||
});
|
||||
addedCount++;
|
||||
} catch (err) {
|
||||
console.error(`Failed to add car ${car.id}:`, err);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedAllCars(new Set());
|
||||
|
||||
if (errorCount > 0) {
|
||||
alert(`${addedCount}개의 차량이 추천 목록에 추가되었습니다.\n(실패: ${errorCount}개)`);
|
||||
} else {
|
||||
alert(`${addedCount}개의 차량이 추천 목록에 추가되었습니다.`);
|
||||
}
|
||||
|
||||
router.push('/admin/vehicle-requests');
|
||||
} catch (err) {
|
||||
console.error('Add to request failed:', err);
|
||||
alert('추천 목록 추가에 실패했습니다.');
|
||||
} finally {
|
||||
setAddingAllCarsToRequest(false);
|
||||
}
|
||||
};
|
||||
|
||||
// All Cars 선택 토글
|
||||
const toggleAllCarSelection = (carId: number) => {
|
||||
setSelectedAllCars(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(carId)) {
|
||||
newSet.delete(carId);
|
||||
} else {
|
||||
newSet.add(carId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// All Cars 전체 선택/해제
|
||||
const handleSelectAllAllCars = () => {
|
||||
if (selectedAllCars.size === allCars.length) {
|
||||
setSelectedAllCars(new Set());
|
||||
} else {
|
||||
setSelectedAllCars(new Set(allCars.map(car => car.id)));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCar = async (carId: number) => {
|
||||
if (!confirm('Are you sure you want to delete this car?')) {
|
||||
return;
|
||||
@@ -1700,6 +1788,45 @@ export default function CarsAdminPage() {
|
||||
This shows the cars that are visible to users (is_displayed = true). This is the same view as the public /cars page.
|
||||
</p>
|
||||
|
||||
{/* Selection Controls for Request Mode */}
|
||||
{requestId && allCars.length > 0 && (
|
||||
<div className="flex items-center justify-between mb-4 p-3 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAllCars.size === allCars.length && allCars.length > 0}
|
||||
onChange={handleSelectAllAllCars}
|
||||
className="w-5 h-5 text-primary-600 rounded"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700">Select All</span>
|
||||
</label>
|
||||
<span className="text-sm text-blue-600 font-medium">
|
||||
{selectedAllCars.size} selected
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddAllCarsToRequest}
|
||||
disabled={addingAllCarsToRequest || selectedAllCars.size === 0}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{addingAllCarsToRequest ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>Adding...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span>Add to Request #{requestId}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allCarsLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-8 h-8 border-4 border-primary-600 border-t-transparent rounded-full animate-spin"></div>
|
||||
@@ -1717,13 +1844,30 @@ export default function CarsAdminPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{allCars.map((car) => {
|
||||
const mainImage = getImageUrl(car.images?.find((img) => img.is_main)?.url || car.images?.[0]?.url);
|
||||
const isSelected = selectedAllCars.has(car.id);
|
||||
return (
|
||||
<div
|
||||
key={car.id}
|
||||
className="border rounded-lg overflow-hidden hover:shadow-md transition cursor-pointer"
|
||||
className={`border rounded-lg overflow-hidden hover:shadow-md transition cursor-pointer ${
|
||||
isSelected ? 'ring-2 ring-blue-500 border-blue-500' : ''
|
||||
}`}
|
||||
onClick={() => handleCarClick(car)}
|
||||
>
|
||||
<div className="relative h-40 bg-gray-100">
|
||||
{/* Checkbox for Request Mode */}
|
||||
{requestId && (
|
||||
<div
|
||||
className="absolute top-2 left-2 z-10"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleAllCarSelection(car.id)}
|
||||
className="w-5 h-5 text-blue-600 rounded cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mainImage ? (
|
||||
<img
|
||||
src={mainImage}
|
||||
|
||||
Reference in New Issue
Block a user