fix: Show skip/error reasons in car import results
- Backend: Include car_name in skipped response - Frontend: Display skip details (car name + reason like "Already imported (ID: 123)") - Frontend: Display error details with error message - Add import process description during loading Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1423,6 +1423,7 @@ async def import_cars_from_carmodoo(
|
||||
skipped.append({
|
||||
"car_no": car_data.car_no,
|
||||
"car_id": existing.id,
|
||||
"car_name": car_data.car_name or existing.name or car_data.car_no,
|
||||
"reason": "already exists"
|
||||
})
|
||||
continue
|
||||
|
||||
@@ -199,6 +199,16 @@ export default function CarsAdminPage() {
|
||||
attempts: number;
|
||||
error?: string;
|
||||
}>;
|
||||
skipDetails?: Array<{
|
||||
car_no: string;
|
||||
car_id: number;
|
||||
car_name: string;
|
||||
reason: string;
|
||||
}>;
|
||||
errorDetails?: Array<{
|
||||
car_no: string;
|
||||
error: string;
|
||||
}>;
|
||||
} | null>(null);
|
||||
const [pdfStatus, setPdfStatus] = useState<Record<number, boolean>>({});
|
||||
const [regeneratingPdf, setRegeneratingPdf] = useState<number | null>(null);
|
||||
@@ -738,6 +748,8 @@ export default function CarsAdminPage() {
|
||||
pdfSuccess: data.summary.pdf_success_count || 0,
|
||||
pdfFailed: data.summary.pdf_failed_count || 0,
|
||||
pdfDetails,
|
||||
skipDetails: data.skipped || [],
|
||||
errorDetails: data.errors || [],
|
||||
});
|
||||
|
||||
// Remove imported cars from selection and reload local cars
|
||||
@@ -2207,41 +2219,53 @@ export default function CarsAdminPage() {
|
||||
{/* Import Result */}
|
||||
{importResult && (
|
||||
<div className={`border rounded-xl p-4 mb-6 ${
|
||||
importResult.pdfFailed > 0
|
||||
? 'bg-amber-50 border-amber-200'
|
||||
: 'bg-green-50 border-green-200'
|
||||
importResult.errors > 0
|
||||
? 'bg-red-50 border-red-200'
|
||||
: importResult.pdfFailed > 0
|
||||
? 'bg-amber-50 border-amber-200'
|
||||
: 'bg-green-50 border-green-200'
|
||||
}`}>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<h3 className={`font-semibold mb-2 ${
|
||||
importResult.pdfFailed > 0 ? 'text-amber-800' : 'text-green-800'
|
||||
importResult.errors > 0 ? 'text-red-800' : importResult.pdfFailed > 0 ? 'text-amber-800' : 'text-green-800'
|
||||
}`}>
|
||||
Import Complete
|
||||
</h3>
|
||||
<div className="text-gray-700 space-y-1">
|
||||
<div>
|
||||
<span className="mr-4">Imported: {importResult.imported}</span>
|
||||
<span className="mr-4">Skipped: {importResult.skipped}</span>
|
||||
{importResult.errors > 0 && (
|
||||
<span className="text-red-600">Errors: {importResult.errors}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
<span className="flex items-center gap-1">
|
||||
<svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
PDF Success: {importResult.pdfSuccess}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-green-100 text-green-800 rounded text-sm font-medium">
|
||||
Imported: {importResult.imported}
|
||||
</span>
|
||||
{importResult.pdfFailed > 0 && (
|
||||
<span className="flex items-center gap-1 text-amber-700">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
PDF Failed: {importResult.pdfFailed}
|
||||
{importResult.skipped > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-yellow-100 text-yellow-800 rounded text-sm font-medium">
|
||||
Skipped: {importResult.skipped}
|
||||
</span>
|
||||
)}
|
||||
{importResult.errors > 0 && (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-red-100 text-red-800 rounded text-sm font-medium">
|
||||
Errors: {importResult.errors}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{importResult.imported > 0 && (
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
<span className="flex items-center gap-1 text-sm">
|
||||
<svg className="w-4 h-4 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
PDF Success: {importResult.pdfSuccess}
|
||||
</span>
|
||||
{importResult.pdfFailed > 0 && (
|
||||
<span className="flex items-center gap-1 text-amber-700 text-sm">
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
PDF Failed: {importResult.pdfFailed}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -2254,28 +2278,60 @@ export default function CarsAdminPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Skipped 상세 */}
|
||||
{importResult.skipped > 0 && importResult.skipDetails && importResult.skipDetails.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-yellow-200">
|
||||
<h4 className="text-sm font-medium text-yellow-800 mb-1.5">Skipped:</h4>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{importResult.skipDetails.map((s, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between text-sm bg-white rounded px-2 py-1">
|
||||
<span className="text-gray-700 truncate mr-2">{s.car_name || s.car_no}</span>
|
||||
<span className="text-yellow-700 text-xs whitespace-nowrap">
|
||||
{s.reason === 'already exists' ? `Already imported (ID: ${s.car_id})` : s.reason}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error 상세 */}
|
||||
{importResult.errors > 0 && importResult.errorDetails && importResult.errorDetails.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-red-200">
|
||||
<h4 className="text-sm font-medium text-red-800 mb-1.5">Errors:</h4>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{importResult.errorDetails.map((e, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between text-sm bg-white rounded px-2 py-1">
|
||||
<span className="text-gray-700">{e.car_no}</span>
|
||||
<span className="text-red-600 text-xs truncate ml-2">{e.error}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PDF 실패 상세 정보 */}
|
||||
{importResult.pdfFailed > 0 && importResult.pdfDetails && (
|
||||
<div className="mt-4 pt-4 border-t border-amber-200">
|
||||
<h4 className="text-sm font-medium text-amber-800 mb-2">
|
||||
PDF 생성 실패 차량 (3회 재시도 완료):
|
||||
<div className="mt-3 pt-3 border-t border-amber-200">
|
||||
<h4 className="text-sm font-medium text-amber-800 mb-1.5">
|
||||
PDF Failed (3 retries attempted):
|
||||
</h4>
|
||||
<div className="space-y-2 max-h-40 overflow-y-auto">
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{importResult.pdfDetails
|
||||
.filter(d => !d.success)
|
||||
.map((detail, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between text-sm bg-white rounded p-2">
|
||||
<div key={idx} className="flex items-center justify-between text-sm bg-white rounded px-2 py-1">
|
||||
<span className="text-gray-700">
|
||||
[{detail.car_id}] {detail.car_name}
|
||||
</span>
|
||||
<span className="text-amber-600 text-xs">
|
||||
{detail.attempts}회 시도 - {detail.error || 'Unknown error'}
|
||||
{detail.attempts} attempts - {detail.error || 'Unknown error'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-amber-700">
|
||||
⚠️ Local Cars 탭에서 해당 차량의 "PDF" 버튼을 클릭하여 수동으로 재시도할 수 있습니다.
|
||||
<p className="mt-2 text-xs text-amber-700">
|
||||
Retry from Local Cars tab using the "PDF" button.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -2391,6 +2447,11 @@ export default function CarsAdminPage() {
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{importing && (
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Duplicate check → Images → Description → Translation → PDF → Specs
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={handleRegisterAsBanner}
|
||||
disabled={importing || registeringBanners || addingToRequest || selectedCars.size === 0}
|
||||
|
||||
Reference in New Issue
Block a user