feat: Add PDF download button for car images in admin car detail modal

- Download all car images as a single PDF (A4 landscape, one image per page)
- Button shows image count and displays in modal header for easy access
- Uses jsPDF library for client-side PDF generation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2026-03-27 17:41:00 +09:00
parent b5d4b8b5bd
commit a8aced66a8
3 changed files with 327 additions and 9 deletions

View File

@@ -6,6 +6,7 @@ import Image from 'next/image';
import { Reorder, useDragControls } from 'framer-motion';
import api, { heroBannersApi, carmodooApi, vehicleRequestsApi, carsApi } from '@/lib/api';
import { translateCarName } from '@/lib/i18n';
import { jsPDF } from 'jspdf';
interface CarmodooMaker {
code: string;
@@ -1258,6 +1259,73 @@ export default function CarsAdminPage() {
setEditingComment(false);
};
const [pdfDownloading, setPdfDownloading] = useState(false);
const handleDownloadImagesPdf = async () => {
if (!selectedCar?.images || selectedCar.images.length === 0) return;
setPdfDownloading(true);
try {
const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
const pageWidth = pdf.internal.pageSize.getWidth();
const pageHeight = pdf.internal.pageSize.getHeight();
const images = selectedCar.images;
for (let i = 0; i < images.length; i++) {
if (i > 0) pdf.addPage();
const imgUrl = getImageUrl(images[i].url);
try {
const response = await fetch(imgUrl);
const blob = await response.blob();
const dataUrl = await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
const img = new window.Image();
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error(`Failed to load image ${i + 1}`));
img.src = dataUrl;
});
const imgRatio = img.width / img.height;
const pageRatio = pageWidth / pageHeight;
let drawWidth: number, drawHeight: number, x: number, y: number;
if (imgRatio > pageRatio) {
drawWidth = pageWidth;
drawHeight = pageWidth / imgRatio;
x = 0;
y = (pageHeight - drawHeight) / 2;
} else {
drawHeight = pageHeight;
drawWidth = pageHeight * imgRatio;
x = (pageWidth - drawWidth) / 2;
y = 0;
}
pdf.addImage(dataUrl, 'JPEG', x, y, drawWidth, drawHeight);
} catch (imgError) {
console.error(`Failed to load image ${i + 1}:`, imgError);
pdf.setFontSize(14);
pdf.text(`Image ${i + 1} - Failed to load`, pageWidth / 2, pageHeight / 2, { align: 'center' });
}
}
const carName = selectedCar.car_name.replace(/[^a-zA-Z0-9가-힣\s]/g, '').trim();
pdf.save(`${carName}_images.pdf`);
} catch (error) {
console.error('PDF generation failed:', error);
alert('PDF generation failed. Please try again.');
} finally {
setPdfDownloading(false);
}
};
const handleEditComment = () => {
if (dealerComment) {
setEditCommentData({
@@ -2667,16 +2735,39 @@ export default function CarsAdminPage() {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
{/* Modal Header */}
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center">
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex justify-between items-center z-10">
<h2 className="text-xl font-bold text-gray-800">{selectedCar.car_name}</h2>
<button
onClick={closeDetailModal}
className="text-gray-500 hover:text-gray-700"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="flex items-center gap-3">
{selectedCar.images && selectedCar.images.length > 0 && (
<button
onClick={handleDownloadImagesPdf}
disabled={pdfDownloading}
className="flex items-center gap-2 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 disabled:opacity-50 transition font-medium text-sm"
>
{pdfDownloading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>Generating...</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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span>PDF ({selectedCar.images.length})</span>
</>
)}
</button>
)}
<button
onClick={closeDetailModal}
className="text-gray-500 hover:text-gray-700"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Modal Content */}