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 [allCarsLoading, setAllCarsLoading] = useState(false);
|
||||||
const [allCarsTotal, setAllCarsTotal] = useState(0);
|
const [allCarsTotal, setAllCarsTotal] = useState(0);
|
||||||
const [allCarsPage, setAllCarsPage] = useState(1);
|
const [allCarsPage, setAllCarsPage] = useState(1);
|
||||||
|
const [selectedAllCars, setSelectedAllCars] = useState<Set<number>>(new Set());
|
||||||
|
const [addingAllCarsToRequest, setAddingAllCarsToRequest] = useState(false);
|
||||||
|
|
||||||
// Carmodoo search state
|
// Carmodoo search state
|
||||||
const [makers, setMakers] = useState<CarmodooMaker[]>([]);
|
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) => {
|
const handleDeleteCar = async (carId: number) => {
|
||||||
if (!confirm('Are you sure you want to delete this car?')) {
|
if (!confirm('Are you sure you want to delete this car?')) {
|
||||||
return;
|
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.
|
This shows the cars that are visible to users (is_displayed = true). This is the same view as the public /cars page.
|
||||||
</p>
|
</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 ? (
|
{allCarsLoading ? (
|
||||||
<div className="flex justify-center py-12">
|
<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>
|
<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">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
{allCars.map((car) => {
|
{allCars.map((car) => {
|
||||||
const mainImage = getImageUrl(car.images?.find((img) => img.is_main)?.url || car.images?.[0]?.url);
|
const mainImage = getImageUrl(car.images?.find((img) => img.is_main)?.url || car.images?.[0]?.url);
|
||||||
|
const isSelected = selectedAllCars.has(car.id);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={car.id}
|
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)}
|
onClick={() => handleCarClick(car)}
|
||||||
>
|
>
|
||||||
<div className="relative h-40 bg-gray-100">
|
<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 ? (
|
{mainImage ? (
|
||||||
<img
|
<img
|
||||||
src={mainImage}
|
src={mainImage}
|
||||||
|
|||||||
Reference in New Issue
Block a user