'use client'; import { useState, useEffect, useCallback } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; import Image from 'next/image'; import { Reorder, useDragControls } from 'framer-motion'; import api, { heroBannersApi, carmodooApi, vehicleRequestsApi, carsApi, ccApi } from '@/lib/api'; import { translateCarName } from '@/lib/i18n'; import { jsPDF } from 'jspdf'; interface CarmodooMaker { code: string; name: string; } interface CarmodooModel { code: string; name: string; type: string; } interface Grade { code: string; name: string; } interface CarmodooCarItem { id: string; car_name: string; maker_code?: string; maker_name?: string; model_code?: string; model_name?: string; car_type?: string; car_type_name?: string; grade?: string; grade_name?: string; year?: number; month?: number; mileage?: number; price?: number; fuel?: string; transmission?: string; color?: string; displacement?: number; car_number?: string; main_image?: string; images?: string[]; options?: string[]; dealer_name?: string; shop_name?: string; seize_count?: number; collateral_count?: number; check_num?: string; // 성능점검번호 car_key?: string; // 암호화된 차량 키 (딜러 설명용) } interface LocalCar { id: number; source: string; source_id: string; car_name: string; year?: number; month?: number; mileage?: number; price_krw?: number; margin_krw?: number; margin_mn?: number; final_price_krw?: number; final_price_mn?: number; is_displayed?: boolean; is_banner?: boolean; soldout?: boolean; fuel?: string; transmission?: string; color?: string; displacement?: number; car_number?: string; seize_count?: number; collateral_count?: number; dealer_name?: string; status: string; created_at: string; images?: { id: number; url: string; is_main: boolean; sort_order: number }[]; maker?: { name: string }; model?: { name: string }; } interface SearchFilters { complex_code: string; // 단지 코드 maker_code: string; model_code: string; grade: string; year_min: string; year_max: string; price_min: string; price_max: string; mileage_max: string; fuel: string; displacement_min: string; displacement_max: string; } interface CarmodooComplex { code: string; name: string; } const FUEL_TYPES = [ { value: '', label: 'All' }, { value: '가솔린', label: 'Gasoline' }, { value: '디젤', label: 'Diesel' }, { value: '하이브리드', label: 'Hybrid' }, { value: '전기', label: 'Electric' }, { value: 'LPG', label: 'LPG' }, ]; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || ''; // 이미지 URL 변환 (로컬 경로는 백엔드 URL 추가) const getImageUrl = (url: string | undefined): string => { if (!url) return ''; if (url.startsWith('http://') || url.startsWith('https://')) { return url; } return `${API_BASE_URL}${url}`; }; const YEAR_OPTIONS = Array.from({ length: 15 }, (_, i) => 2024 - i); type TabType = 'local' | 'all' | 'carmodoo'; export default function CarsAdminPage() { const [activeTab, setActiveTab] = useState('local'); // Local cars state const [localCars, setLocalCars] = useState([]); const [localLoading, setLocalLoading] = useState(false); const [localTotal, setLocalTotal] = useState(0); const [localPage, setLocalPage] = useState(1); const [selectedCar, setSelectedCar] = useState(null); const [showDetailModal, setShowDetailModal] = useState(false); const [currentImageIndex, setCurrentImageIndex] = useState(0); const [selectedLocalCars, setSelectedLocalCars] = useState>(new Set()); const [registeringLocalBanner, setRegisteringLocalBanner] = useState(false); const [togglingBanner, setTogglingBanner] = useState(null); const [bannerCarIds, setBannerCarIds] = useState([]); // 배너 순서대로 정렬된 차량 ID // 배너 관리 상태 (배치 업데이트용) const [localBannerSelections, setLocalBannerSelections] = useState>(new Set()); // 체크된 배너 차량 ID const [bannerOrderedCars, setBannerOrderedCars] = useState([]); // 순서 변경 가능한 배너 차량 목록 const [nonBannerCars, setNonBannerCars] = useState([]); // 배너가 아닌 차량 목록 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([]); const [allCarsLoading, setAllCarsLoading] = useState(false); const [allCarsTotal, setAllCarsTotal] = useState(0); const [allCarsPage, setAllCarsPage] = useState(1); const [selectedAllCars, setSelectedAllCars] = useState>(new Set()); const [addingAllCarsToRequest, setAddingAllCarsToRequest] = useState(false); // Carmodoo search state const [makers, setMakers] = useState([]); const [models, setModels] = useState([]); const [grades, setGrades] = useState([]); const [cars, setCars] = useState([]); const [selectedCars, setSelectedCars] = useState>(new Set()); const [loading, setLoading] = useState(false); const [importing, setImporting] = useState(false); const [searched, setSearched] = useState(false); const [totalCount, setTotalCount] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [filters, setFilters] = useState({ complex_code: '', maker_code: '', model_code: '', grade: '', year_min: '', year_max: '', price_min: '', price_max: '', mileage_max: '', fuel: '', displacement_min: '', displacement_max: '', }); const [complexes, setComplexes] = useState([]); const [importResult, setImportResult] = useState<{ imported: number; skipped: number; errors: number; pdfSuccess: number; pdfFailed: number; pdfDetails?: Array<{ car_id: number; car_name: string; success: boolean; 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>({}); const [regeneratingPdf, setRegeneratingPdf] = useState(null); const [registeringBanners, setRegisteringBanners] = useState(false); const [bannerResult, setBannerResult] = useState<{ registered: number; imported: number; skipped: number; errors: number; pdfSuccess: number; pdfFailed: number; pdfDetails?: Array<{ car_id: number; car_name: string; success: boolean; attempts: number; error?: string; }>; } | null>(null); // Vehicle Request mode (when coming from Vehicle Requests page) const searchParams = useSearchParams(); const router = useRouter(); const requestId = searchParams.get('requestId'); const [addingToRequest, setAddingToRequest] = useState(false); const [requestAddResult, setRequestAddResult] = useState<{ added: number; errors: number; } | null>(null); const [requestInfo, setRequestInfo] = useState<{ user_email?: string; user_name?: string; maker_name?: string; model_name?: string; grade_name?: string; year_from?: number; year_to?: number; mileage_max?: number; fuel?: string; } | null>(null); // Dealer description editing state const [showDescEditModal, setShowDescEditModal] = useState(false); const [editingCar, setEditingCar] = useState(null); const [originalDesc, setOriginalDesc] = useState(''); const [editedDesc, setEditedDesc] = useState(''); const [highlightedDesc, setHighlightedDesc] = useState(''); const [sensitiveInfo, setSensitiveInfo] = useState<{ phones: number; addresses: number; others: number; total: number } | null>(null); const [loadingDesc, setLoadingDesc] = useState(false); const [editedDescriptions, setEditedDescriptions] = useState>({}); // car_id -> edited description // Dealer comment for detail modal const [dealerComment, setDealerComment] = useState<{ ko: string | null; en: string | null; mn: string | null; ru: string | null; } | null>(null); const [loadingComment, setLoadingComment] = useState(false); const [editingComment, setEditingComment] = useState(false); const [editCommentData, setEditCommentData] = useState({ ko: '', en: '', mn: '', ru: '', }); const [savingComment, setSavingComment] = useState(false); useEffect(() => { loadLocalCars(); loadAllCars(); loadInitialData(); }, []); // 제조사 변경 시 모델 목록 로드 useEffect(() => { if (filters.maker_code) { loadModels(filters.maker_code); setGrades([]); setFilters((prev) => ({ ...prev, model_code: '', grade: '' })); } else { setModels([]); setGrades([]); setFilters((prev) => ({ ...prev, model_code: '', grade: '' })); } }, [filters.maker_code]); // 모델 변경 시 등급 목록 로드 useEffect(() => { if (filters.maker_code && filters.model_code) { loadGrades(filters.maker_code, filters.model_code); } else { setGrades([]); setFilters((prev) => ({ ...prev, grade: '' })); } }, [filters.model_code]); // Pre-fill filters from URL params (when coming from Vehicle Requests page) useEffect(() => { if (requestId) { const makerCode = searchParams.get('maker_code') || ''; const modelCode = searchParams.get('model_code') || ''; const grade = searchParams.get('grade') || ''; const yearMin = searchParams.get('year_min') || ''; const yearMax = searchParams.get('year_max') || ''; const mileageMax = searchParams.get('mileage_max') || ''; const fuel = searchParams.get('fuel') || ''; setFilters(prev => ({ ...prev, maker_code: makerCode, model_code: modelCode, grade: grade, year_min: yearMin, year_max: yearMax, mileage_max: mileageMax, fuel: fuel, })); // Switch to Carmodoo tab setActiveTab('carmodoo'); // Fetch request details for display vehicleRequestsApi.adminGetRequestDetail(parseInt(requestId)).then(data => { setRequestInfo({ user_email: data.request.user_email, user_name: data.request.user_name, maker_name: data.request.maker_name, model_name: data.request.model_name, grade_name: data.request.grade_name, year_from: data.request.year_from, year_to: data.request.year_to, mileage_max: data.request.mileage_max, fuel: data.request.fuel, }); }).catch(err => console.error('Failed to load request info:', err)); } }, [requestId]); // 배너 체크박스 토글 (로컬 상태만 변경, API 호출하지 않음) const handleToggleBannerLocal = (carId: number, e: React.MouseEvent) => { e.stopPropagation(); setLocalBannerSelections(prev => { const newSet = new Set(prev); if (newSet.has(carId)) { newSet.delete(carId); } else { newSet.add(carId); } return newSet; }); setHasBannerChanges(true); }; // 배너 업데이트 (일괄 저장) const handleUpdateBanners = async () => { setUpdatingBanners(true); try { // 현재 DB에 저장된 배너 ID와 새로 선택된 배너 ID 비교 const currentBannerIds = new Set(bannerCarIds); const newBannerIds = localBannerSelections; console.log('=== Banner Update Debug ==='); console.log('Current banner IDs (from DB):', Array.from(currentBannerIds)); console.log('New banner IDs (user selection):', Array.from(newBannerIds)); // 추가할 배너 const toAdd: number[] = []; newBannerIds.forEach(id => { if (!currentBannerIds.has(id)) { toAdd.push(id); } }); // 제거할 배너 const toRemove: number[] = []; currentBannerIds.forEach(id => { if (!newBannerIds.has(id)) { toRemove.push(id); } }); console.log('To Add:', toAdd); console.log('To Remove:', toRemove); // 배너 추가/제거 API 호출 for (const carId of toRemove) { console.log('Removing banner for car:', carId); const result = await heroBannersApi.adminToggleBanner(carId); console.log('Remove result:', result); } for (const carId of toAdd) { console.log('Adding banner for car:', carId); const result = await heroBannersApi.adminToggleBanner(carId); console.log('Add result:', result); } // 배너 순서 업데이트 (bannerOrderedCars 순서대로) const orderedIds = bannerOrderedCars .filter(car => newBannerIds.has(car.id)) .map(car => car.id); // 새로 추가된 배너는 맨 뒤에 toAdd.forEach(id => { if (!orderedIds.includes(id)) { orderedIds.push(id); } }); console.log('Final ordered IDs:', orderedIds); if (orderedIds.length > 0) { const reorderResult = await heroBannersApi.adminReorderBanners(orderedIds); console.log('Reorder result:', reorderResult); } // 목록 새로고침 console.log('Reloading cars...'); await loadLocalCars(); console.log('=== Banner Update Complete ==='); alert(`배너가 업데이트되었습니다. (추가: ${toAdd.length}, 제거: ${toRemove.length})`); } catch (err) { console.error('Failed to update banners:', err); alert('배너 업데이트에 실패했습니다.'); } finally { setUpdatingBanners(false); } }; // 드래그앤드롭으로 배너 순서 변경 const handleBannerReorder = (newOrder: LocalCar[]) => { setBannerOrderedCars(newOrder); setLocalCars([...newOrder, ...nonBannerCars]); setHasBannerChanges(true); }; // Soldout 토글 핸들러 const handleToggleSoldout = async (carId: number, currentSoldout: boolean, e: React.MouseEvent) => { e.stopPropagation(); try { if (currentSoldout) { await carsApi.markAvailable(carId); } else { await carsApi.markSoldout(carId); } // 로컬 상태 업데이트 setLocalCars(prev => prev.map(car => car.id === carId ? { ...car, soldout: !currentSoldout } : car )); } catch (err) { console.error('Failed to toggle soldout:', err); alert('상태 변경에 실패했습니다.'); } }; const loadLocalCars = async (page = 1, preserveBannerState = false, overrideFilters?: { search?: string; color?: string; yearMin?: string; yearMax?: string }) => { setLocalLoading(true); try { 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 || []; // 배너 목록도 함께 로드 (순서 정보 포함) - 실패해도 차량 목록은 표시 let orderedBannerIds: number[] = []; try { const bannerResult = await heroBannersApi.adminGetBannerCars(); orderedBannerIds = bannerResult.car_ids || []; } catch (bannerErr) { console.error('Failed to load banner cars:', bannerErr); // 배너 로드 실패 시 car.is_banner 필드 사용 orderedBannerIds = cars.filter(c => c.is_banner).map(c => c.id); } setBannerCarIds(orderedBannerIds); // 배너 차량과 비배너 차량 분리 const bannerCarsMap = new Map(); const nonBannerCarsList: LocalCar[] = []; for (const car of cars) { if (orderedBannerIds.includes(car.id)) { bannerCarsMap.set(car.id, car); } else { nonBannerCarsList.push(car); } } // 배너 차량을 display_order 순서대로 정렬 const sortedBannerCars: LocalCar[] = []; for (const carId of orderedBannerIds) { const car = bannerCarsMap.get(carId); if (car) { sortedBannerCars.push(car); } } setBannerOrderedCars(sortedBannerCars); setNonBannerCars(nonBannerCarsList); // 전체 목록 (배너 먼저, 그 다음 비배너) setLocalCars([...sortedBannerCars, ...nonBannerCarsList]); setLocalTotal(data.total || 0); setLocalPage(page); // 배너 상태 초기화 (preserveBannerState가 false일 때만) if (!preserveBannerState) { setLocalBannerSelections(new Set(orderedBannerIds)); setHasBannerChanges(false); } // Fetch PDF status for all cars const carIds = cars.map((c: LocalCar) => c.id); if (carIds.length > 0) { try { const pdfRes = await api.post('/carmodoo/pdf-status', carIds); setPdfStatus(prev => ({ ...prev, ...pdfRes.data })); } catch (err) { console.error('Failed to fetch PDF status:', err); } } } catch (err) { console.error('Failed to load local cars:', err); } finally { setLocalLoading(false); } }; const loadAllCars = async (page = 1) => { setAllCarsLoading(true); try { // Load only displayed cars (public view) const { data } = await api.get('/cars', { params: { page, page_size: 20 } }); setAllCars(data.cars || []); setAllCarsTotal(data.total || 0); setAllCarsPage(page); // Fetch PDF status for all cars const carIds = (data.cars || []).map((c: LocalCar) => c.id); if (carIds.length > 0) { try { const pdfRes = await api.post('/carmodoo/pdf-status', carIds); setPdfStatus(prev => ({ ...prev, ...pdfRes.data })); } catch (err) { console.error('Failed to fetch PDF status:', err); } } } catch (err) { console.error('Failed to load all cars:', err); } finally { setAllCarsLoading(false); } }; const handleUpdateCar = async (carId: number, updates: { margin_krw?: number; margin_mn?: number; is_displayed?: boolean }) => { try { await api.put(`/cars/${carId}`, updates); // Update local state setLocalCars(prev => prev.map(car => car.id === carId ? { ...car, ...updates, final_price_krw: updates.margin_krw !== undefined ? (car.price_krw || 0) + updates.margin_krw : car.final_price_krw, final_price_mn: updates.margin_mn !== undefined ? (car.price_krw || 0) + updates.margin_mn : car.final_price_mn } : car )); // Also update selectedCar if it's the same if (selectedCar?.id === carId) { setSelectedCar(prev => prev ? { ...prev, ...updates, final_price_krw: updates.margin_krw !== undefined ? (prev.price_krw || 0) + updates.margin_krw : prev.final_price_krw, final_price_mn: updates.margin_mn !== undefined ? (prev.price_krw || 0) + updates.margin_mn : prev.final_price_mn } : null); } } catch (err) { console.error('Failed to update car:', err); alert('Failed to update car.'); } }; const handleToggleDisplay = async (car: LocalCar, e: React.MouseEvent) => { e.stopPropagation(); await handleUpdateCar(car.id, { is_displayed: !car.is_displayed }); }; const loadInitialData = async () => { try { const [makersRes, complexesRes] = await Promise.all([ api.get('/carmodoo/makers'), api.get('/carmodoo/complexes'), ]); setMakers(makersRes.data); setComplexes(complexesRes.data); } catch (err) { console.error('Failed to load initial data:', err); } }; // Load grades when model changes const loadGrades = async (makerCode: string, modelCode: string) => { try { const { data } = await api.get(`/carmodoo/grades/${makerCode}/${modelCode}`); setGrades(data); } catch (err) { console.error('Failed to load grades:', err); setGrades([]); } }; const loadModels = async (makerCode: string) => { try { const { data } = await api.get(`/carmodoo/models/${makerCode}`); setModels(data); } catch (err) { console.error('Failed to load models:', err); setModels([]); } }; const handleFilterChange = (key: keyof SearchFilters, value: string) => { setFilters((prev) => ({ ...prev, [key]: value })); }; const handleSearch = async (page = 1) => { setLoading(true); setImportResult(null); try { const params: Record = { page, page_size: 20 }; if (filters.complex_code) params.complex_code = filters.complex_code; if (filters.maker_code) params.maker_code = filters.maker_code; if (filters.model_code) params.model_code = filters.model_code; if (filters.grade) params.grade = filters.grade; if (filters.year_min) params.year_min = parseInt(filters.year_min); if (filters.year_max) params.year_max = parseInt(filters.year_max); if (filters.price_min) params.price_min = parseInt(filters.price_min) * 10000; if (filters.price_max) params.price_max = parseInt(filters.price_max) * 10000; if (filters.mileage_max) params.mileage_max = parseInt(filters.mileage_max); if (filters.fuel) params.fuel = filters.fuel; if (filters.displacement_min) params.displacement_min = parseInt(filters.displacement_min); if (filters.displacement_max) params.displacement_max = parseInt(filters.displacement_max); const { data } = await api.get('/carmodoo/search', { params }); setCars(data.cars); setTotalCount(data.total); setCurrentPage(page); setSearched(true); setSelectedCars(new Set()); // 성능점검번호 재시도 결과 표시 if (data.check_num_retried > 0) { const msg = `${data.check_num_retried}개 차량의 성능점검번호를 재시도하여 ${data.check_num_retry_success}개 성공`; console.log('[Carmodoo]', msg); } } catch (err) { console.error('Search failed:', err); alert('Search failed. Please try again.'); } finally { setLoading(false); } }; const handleSelectCar = (carId: string) => { setSelectedCars((prev) => { const next = new Set(prev); if (next.has(carId)) { next.delete(carId); } else { next.add(carId); } return next; }); }; const [quickImporting, setQuickImporting] = useState(null); const handleQuickImport = async (car: CarmodooCarItem) => { setQuickImporting(car.id); try { const carToImport = { car_no: car.id, car_name: car.car_name || '', maker_name: car.maker_name, model_name: car.model_name, year: car.year, mileage: car.mileage, price: car.price, fuel: car.fuel, transmission: car.transmission, color: car.color, displacement: car.displacement, main_image: car.main_image, check_num: car.check_num, car_key: car.car_key, dealer_description: editedDescriptions[car.id], }; const { data } = await api.post('/carmodoo/import', { cars: [carToImport] }); if (data.summary.imported_count > 0 && data.imported?.[0]?.car_id) { const importedCarId = data.imported[0].car_id; // Fetch the imported car and show detail modal const carResponse = await api.get(`/cars/${importedCarId}?admin=true`); const localCar: LocalCar = carResponse.data; setSelectedCar(localCar); setCurrentImageIndex(0); setShowDetailModal(true); // Load dealer comments setDealerComment(null); setLoadingComment(true); try { const commentData = await carmodooApi.getCarTranslations(importedCarId); setDealerComment({ ko: commentData.dealer_description, en: commentData.translations.en, mn: commentData.translations.mn, ru: commentData.translations.ru, }); } catch { setDealerComment(null); } finally { setLoadingComment(false); } // Refresh local cars list loadLocalCars(); } else if (data.summary.skipped_count > 0) { // Already imported - find and show existing car const skipped = data.skipped?.[0]; if (skipped?.car_id) { const carResponse = await api.get(`/cars/${skipped.car_id}?admin=true`); const localCar: LocalCar = carResponse.data; setSelectedCar(localCar); setCurrentImageIndex(0); setShowDetailModal(true); setDealerComment(null); setLoadingComment(true); try { const commentData = await carmodooApi.getCarTranslations(skipped.car_id); setDealerComment({ ko: commentData.dealer_description, en: commentData.translations.en, mn: commentData.translations.mn, ru: commentData.translations.ru, }); } catch { setDealerComment(null); } finally { setLoadingComment(false); } } else { alert(`Already imported: ${skipped?.reason || 'duplicate'}`); } } else { alert('Import failed. Check car details.'); } } catch (err) { console.error('Quick import failed:', err); alert('Import failed. Please try again.'); } finally { setQuickImporting(null); } }; const handleSelectAll = () => { if (selectedCars.size === cars.length) { setSelectedCars(new Set()); } else { setSelectedCars(new Set(cars.map((c) => c.id))); } }; const handleImport = async () => { if (selectedCars.size === 0) { alert('Please select at least one car to import.'); return; } if (!confirm(`Import ${selectedCars.size} selected car(s) to local database?`)) { return; } setImporting(true); try { // 선택된 차량들의 상세 정보를 올바른 형식으로 변환 const selectedCarsList = cars.filter(car => selectedCars.has(car.id)); const carsToImport = selectedCarsList.map(car => ({ car_no: car.id, car_name: car.car_name || '', maker_name: car.maker_name, model_name: car.model_name, year: car.year, mileage: car.mileage, price: car.price, fuel: car.fuel, transmission: car.transmission, color: car.color, displacement: car.displacement, main_image: car.main_image, check_num: car.check_num, car_key: car.car_key, dealer_description: editedDescriptions[car.id], })); const { data } = await api.post('/carmodoo/import', { cars: carsToImport }); // PDF 상태 상세 정보 추출 const pdfDetails = data.imported?.map((item: any) => ({ car_id: item.car_id, car_name: item.car_name, success: item.pdf_status?.success || false, attempts: item.pdf_status?.attempts || 0, error: item.pdf_status?.error, })) || []; setImportResult({ imported: data.summary.imported_count, skipped: data.summary.skipped_count, errors: data.summary.error_count, 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 setSelectedCars(new Set()); loadLocalCars(); } catch (err) { console.error('Import failed:', err); alert('Import failed. Please try again.'); } finally { setImporting(false); } }; const handleRegeneratePdf = async (carId: number, e: React.MouseEvent) => { e.stopPropagation(); if (regeneratingPdf) return; setRegeneratingPdf(carId); try { // 먼저 fetch-check-num 시도 (check_num이 없는 경우 가져오기 + PDF 생성) const response = await api.post(`/carmodoo/car/${carId}/fetch-check-num`); if (response.data.pdf_path) { setPdfStatus(prev => ({ ...prev, [carId]: true })); alert('PDF 생성 완료!'); } else if (response.data.check_number) { // check_number는 있지만 PDF 생성 실패 alert(`성능점검번호(${response.data.check_number})를 가져왔으나 PDF 생성에 실패했습니다. 다시 시도해주세요.`); } else { alert('PDF 생성 실패: ' + (response.data.message || 'Unknown error')); } } catch (err: any) { console.error('PDF regeneration failed:', err); const errorMsg = err.response?.data?.detail || err.message; if (errorMsg.includes('Car number not available')) { alert('차량번호가 없어 성능점검번호를 가져올 수 없습니다.'); } else if (errorMsg.includes('Could not find check number')) { alert('카모두에서 성능점검번호를 찾을 수 없습니다.'); } else { alert('PDF 생성 실패: ' + errorMsg); } } finally { setRegeneratingPdf(null); } }; // 딜러 설명 미리보기 및 편집 함수 const handleEditDealerDescription = async (car: CarmodooCarItem) => { setEditingCar(car); setShowDescEditModal(true); setLoadingDesc(true); setOriginalDesc(''); setEditedDesc(''); setHighlightedDesc(''); setSensitiveInfo(null); try { const response = await fetch(`/api/carmodoo/preview-dealer-description/${car.id}?car_key=${encodeURIComponent(car.car_key || '')}`, { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, }); const data = await response.json(); if (data.found) { setOriginalDesc(data.original); setHighlightedDesc(data.highlighted_html); setSensitiveInfo(data.summary); // 이미 편집된 설명이 있으면 사용, 없으면 마스킹된 텍스트 사용 setEditedDesc(editedDescriptions[car.id] || data.masked_text); } else { setOriginalDesc(''); setEditedDesc(''); setHighlightedDesc(''); setSensitiveInfo({ phones: 0, addresses: 0, others: 0, total: 0 }); } } catch (err) { console.error('Failed to load dealer description:', err); } finally { setLoadingDesc(false); } }; const handleSaveEditedDescription = () => { if (editingCar) { setEditedDescriptions(prev => ({ ...prev, [editingCar.id]: editedDesc })); } setShowDescEditModal(false); setEditingCar(null); }; const handleApplyMasked = () => { if (editingCar) { // 마스킹된 텍스트로 설정 fetch('/api/carmodoo/check-sensitive-info', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}`, }, body: JSON.stringify({ text: originalDesc }), }) .then(res => res.json()) .then(data => { setEditedDesc(data.masked_text); }); } }; // 배너 등록 함수 const handleRegisterAsBanner = async () => { if (selectedCars.size === 0) { alert('Please select at least one car to register as banner.'); return; } if (!confirm(`${selectedCars.size}개의 차량을 Hero Banner로 등록하시겠습니까?`)) { return; } setRegisteringBanners(true); setBannerResult(null); try { const selectedCarsList = cars.filter(car => selectedCars.has(car.id)); // 1. 먼저 차량을 로컬 DB에 저장 (이미지 다운로드 포함) const carsToImport = selectedCarsList.map(car => ({ car_no: car.id, car_name: car.car_name || '', maker_name: car.maker_name, model_name: car.model_name, year: car.year, mileage: car.mileage, price: car.price, fuel: car.fuel, transmission: car.transmission, color: car.color, displacement: car.displacement, main_image: car.main_image, check_num: car.check_num, // 성능점검번호 car_key: car.car_key, // 암호화된 차량 키 (딜러 설명용) dealer_description: editedDescriptions[car.id] || undefined, // 편집된 딜러 설명 })); const importResult = await carmodooApi.importCars(carsToImport); // 2. car_id 매핑 생성 const carIdMap: Record = {}; for (const imp of importResult.imported) { carIdMap[imp.car_no] = imp.car_id; } for (const skip of importResult.skipped) { carIdMap[skip.car_no] = skip.car_id; } // 3. 기존 배너 목록 가져오기 (순서 설정용) const existingBanners = await heroBannersApi.adminGetList(); let orderStart = existingBanners.length; let successCount = 0; // 4. Banner 생성 (car_id 연결) for (const car of selectedCarsList) { const carId = carIdMap[car.id]; if (!carId) continue; const localImageUrl = `/uploads/cars/${carId}/image_0.jpg`; const bannerData = { title_ko: car.car_name || '', title_en: translateCarName(car.car_name, 'en'), title_mn: translateCarName(car.car_name, 'mn'), title_ru: translateCarName(car.car_name, 'ru'), subtitle_ko: `${car.year}년식 | ${car.mileage?.toLocaleString()}km`, subtitle_en: `${car.year} | ${car.mileage?.toLocaleString()}km`, subtitle_mn: `${car.year} | ${car.mileage?.toLocaleString()}km`, subtitle_ru: `${car.year} | ${car.mileage?.toLocaleString()}km`, image_url: localImageUrl, link_url: `/cars/${carId}`, is_active: true, display_order: orderStart++, car_id: carId, }; await heroBannersApi.adminCreate(bannerData); successCount++; } // PDF 상태 상세 정보 추출 const pdfDetails = importResult.imported?.map((item: any) => ({ car_id: item.car_id, car_name: item.car_name, success: item.pdf_status?.success || false, attempts: item.pdf_status?.attempts || 0, error: item.pdf_status?.error, })) || []; setBannerResult({ registered: successCount, imported: importResult.summary?.imported_count || 0, skipped: importResult.summary?.skipped_count || 0, errors: importResult.summary?.error_count || 0, pdfSuccess: importResult.summary?.pdf_success_count || 0, pdfFailed: importResult.summary?.pdf_failed_count || 0, pdfDetails, }); setSelectedCars(new Set()); loadLocalCars(); const pdfFailedCount = importResult.summary?.pdf_failed_count || 0; if (pdfFailedCount > 0) { alert(`${successCount}개의 차량이 Hero Banner로 등록되었습니다.\n(새로 가져온 차량: ${importResult.summary?.imported_count || 0}대)\n\n⚠️ ${pdfFailedCount}개 차량의 PDF 생성에 실패했습니다. 상세 내용을 확인하세요.`); } else { alert(`${successCount}개의 차량이 Hero Banner로 등록되었습니다.\n(새로 가져온 차량: ${importResult.summary?.imported_count || 0}대)`); } } catch (err) { console.error('Banner registration failed:', err); alert('배너 등록에 실패했습니다.'); } finally { setRegisteringBanners(false); } }; // Local Cars에서 배너 등록하는 함수 const handleRegisterLocalCarAsBanner = async () => { if (selectedLocalCars.size === 0) { alert('Please select at least one car to register as banner.'); return; } if (!confirm(`${selectedLocalCars.size}개의 차량을 Hero Banner로 등록하시겠습니까?`)) { return; } setRegisteringLocalBanner(true); try { const selectedCarsList = localCars.filter(car => selectedLocalCars.has(car.id)); const existingBanners = await heroBannersApi.adminGetList(); let orderStart = existingBanners.length; let successCount = 0; for (const car of selectedCarsList) { const localImageUrl = `/uploads/cars/${car.id}/image_0.jpg`; const bannerData = { title_ko: car.car_name || '', title_en: translateCarName(car.car_name || '', 'en'), title_mn: translateCarName(car.car_name || '', 'mn'), title_ru: translateCarName(car.car_name || '', 'ru'), subtitle_ko: `${car.year || ''}년식 | ${car.mileage ? formatMileage(car.mileage) : ''}`, subtitle_en: `${car.year || ''} | ${car.mileage ? formatMileage(car.mileage) : ''}`, subtitle_mn: `${car.year || ''} | ${car.mileage ? formatMileage(car.mileage) : ''}`, subtitle_ru: `${car.year || ''} | ${car.mileage ? formatMileage(car.mileage) : ''}`, image_url: localImageUrl, link_url: `/cars/${car.id}`, display_order: orderStart++, is_active: true, car_id: car.id, }; await heroBannersApi.adminCreate(bannerData); successCount++; } alert(`${successCount}개의 차량이 Hero Banner로 등록되었습니다.`); setSelectedLocalCars(new Set()); } catch (err) { console.error('Local banner registration failed:', err); alert('배너 등록에 실패했습니다.'); } finally { setRegisteringLocalBanner(false); } }; // Local car selection toggle const handleLocalCarSelect = (carId: number, e: React.MouseEvent) => { e.stopPropagation(); setSelectedLocalCars(prev => { const newSet = new Set(prev); if (newSet.has(carId)) { newSet.delete(carId); } else { newSet.add(carId); } return newSet; }); }; // Select all local cars const handleSelectAllLocalCars = () => { if (selectedLocalCars.size === localCars.length) { setSelectedLocalCars(new Set()); } else { setSelectedLocalCars(new Set(localCars.map(car => car.id))); } }; // 차량 추천 목록에 추가 함수 (Vehicle Request용) const handleAddToRequest = async () => { if (!requestId) return; if (selectedCars.size === 0) { alert('Please select at least one car to add to the request.'); return; } if (!confirm(`${selectedCars.size}개의 차량을 추천 목록에 추가하시겠습니까?`)) { return; } setAddingToRequest(true); setRequestAddResult(null); try { const selectedCarsList = cars.filter(car => selectedCars.has(car.id)); let addedCount = 0; let errorCount = 0; for (const car of selectedCarsList) { try { // Add vehicle to request await vehicleRequestsApi.adminAddVehicle(parseInt(requestId), { request_id: parseInt(requestId), car_data: { id: car.id, car_name: car.car_name, maker_name: car.maker_name, model_name: car.model_name, year: car.year, mileage: car.mileage, final_price: car.price, fuel: car.fuel, transmission: car.transmission, color: car.color, main_image: car.main_image, }, is_approved: true, }); addedCount++; } catch (err) { console.error(`Failed to add car ${car.id}:`, err); errorCount++; } } setRequestAddResult({ added: addedCount, errors: errorCount, }); setSelectedCars(new Set()); if (errorCount > 0) { alert(`${addedCount}개의 차량이 추천 목록에 추가되었습니다.\n(실패: ${errorCount}개)`); } else { alert(`${addedCount}개의 차량이 추천 목록에 추가되었습니다.`); } // Redirect back to vehicle requests page router.push('/admin/vehicle-requests'); } catch (err) { console.error('Add to request failed:', err); alert('추천 목록 추가에 실패했습니다.'); } finally { setAddingToRequest(false); } }; // 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_data: { id: car.id.toString(), local_car_id: car.id, // 로컬 DB에 이미 있는 차량임을 표시 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; } try { await api.delete(`/cars/${carId}`); loadLocalCars(localPage); // 삭제 후 모달 닫기 if (selectedCar?.id === carId) { setShowDetailModal(false); setSelectedCar(null); } } catch (err) { console.error('Delete failed:', err); alert('Failed to delete car.'); } }; const handleCarClick = async (car: LocalCar) => { setSelectedCar(car); setCurrentImageIndex(0); setShowDetailModal(true); setDealerComment(null); setEditingComment(false); // Fetch dealer comment translations setLoadingComment(true); try { const data = await carmodooApi.getCarTranslations(car.id); setDealerComment({ ko: data.dealer_description, en: data.translations.en, mn: data.translations.mn, ru: data.translations.ru, }); } catch (error) { console.error('Failed to load dealer comment:', error); setDealerComment(null); } finally { setLoadingComment(false); } }; const closeDetailModal = () => { setShowDetailModal(false); setSelectedCar(null); setCurrentImageIndex(0); setDealerComment(null); 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((resolve) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.readAsDataURL(blob); }); const img = new window.Image(); await new Promise((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(); const carNumber = selectedCar.car_number?.replace(/\s/g, '') || 'unknown'; pdf.save(`${carName}_${carNumber}.pdf`); } catch (error) { console.error('PDF generation failed:', error); alert('PDF generation failed. Please try again.'); } finally { setPdfDownloading(false); } }; const [perfPdfDownloading, setPerfPdfDownloading] = useState(false); const handleDownloadPerformanceCheckPdf = async () => { if (!selectedCar) return; setPerfPdfDownloading(true); try { const blob = await ccApi.downloadPerformanceCheckPdf(selectedCar.id); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const carName = selectedCar.car_name.replace(/[^a-zA-Z0-9가-힣\s]/g, '').trim(); const carNumber = selectedCar.car_number?.replace(/\s/g, '') || 'unknown'; a.download = `${carName}_${carNumber}_성능점검표.pdf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (error: any) { console.error('Performance check PDF download failed:', error); const status = error.response?.status; if (status === 404) { alert('Performance check PDF not available for this car.'); } else { alert('Failed to download performance check PDF.'); } } finally { setPerfPdfDownloading(false); } }; const handleEditComment = () => { if (dealerComment) { setEditCommentData({ ko: dealerComment.ko || '', en: dealerComment.en || '', mn: dealerComment.mn || '', ru: dealerComment.ru || '', }); setEditingComment(true); } }; const handleSaveComment = async () => { if (!selectedCar) return; setSavingComment(true); try { await carmodooApi.updateCarTranslations(selectedCar.id, { dealer_description: editCommentData.ko, dealer_description_en: editCommentData.en, dealer_description_mn: editCommentData.mn, dealer_description_ru: editCommentData.ru, }); setDealerComment({ ko: editCommentData.ko, en: editCommentData.en, mn: editCommentData.mn, ru: editCommentData.ru, }); setEditingComment(false); alert('Dealer comment saved successfully!'); } catch (error) { console.error('Failed to save dealer comment:', error); alert('Failed to save dealer comment'); } finally { setSavingComment(false); } }; const handleRegenerateTranslation = async () => { if (!selectedCar || !dealerComment?.ko) return; if (!confirm('Regenerate translations from Korean? This will overwrite existing translations.')) return; setSavingComment(true); try { const result = await carmodooApi.regenerateTranslations(selectedCar.id); setDealerComment({ ko: dealerComment.ko, en: result.translations.en, mn: result.translations.mn, ru: result.translations.ru, }); alert('Translations regenerated successfully!'); } catch (error: any) { console.error('Failed to regenerate translations:', error); alert(error.response?.data?.detail || 'Failed to regenerate translations'); } finally { setSavingComment(false); } }; const nextImage = () => { if (selectedCar?.images && selectedCar.images.length > 0) { setCurrentImageIndex((prev) => (prev + 1) % selectedCar.images!.length); } }; const prevImage = () => { if (selectedCar?.images && selectedCar.images.length > 0) { setCurrentImageIndex((prev) => (prev - 1 + selectedCar.images!.length) % selectedCar.images!.length); } }; const formatPrice = (price?: number) => { if (!price) return '-'; return `${(price / 10000).toLocaleString()}만원`; }; const formatMileage = (mileage?: number) => { if (!mileage) return '-'; return `${mileage.toLocaleString()}km`; }; const totalPages = Math.ceil(totalCount / 20); const localTotalPages = Math.ceil(localTotal / 20); const allCarsTotalPages = Math.ceil(allCarsTotal / 20); return (

Cars Management

{/* Request Mode Banner */} {requestId && (

Adding vehicles to Request #{requestId} {requestInfo?.user_email && ( ({requestInfo.user_email}) )}

{requestInfo && (
{requestInfo.maker_name && (
Maker: {requestInfo.maker_name}
)} {requestInfo.model_name && (
Model: {requestInfo.model_name}
)} {requestInfo.grade_name && (
Grade: {requestInfo.grade_name}
)} {(requestInfo.year_from || requestInfo.year_to) && (
Year: {requestInfo.year_from || '-'} ~ {requestInfo.year_to || '-'}
)} {requestInfo.mileage_max && (
Mileage: ~{Math.round(requestInfo.mileage_max / 10000)}만km
)} {requestInfo.fuel && (
Fuel: {requestInfo.fuel}
)}
)}
)} {/* Tabs */}
{/* Local Cars Tab */} {activeTab === 'local' && (
{/* Filter Bar */}
{ e.preventDefault(); setLocalPage(1); loadLocalCars(1); }} className="flex flex-wrap gap-3 items-end">
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" />
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" />
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" />
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" />
{(localFilterSearch || localFilterColor || localFilterYearMin || localFilterYearMax) && ( )}

Imported Cars ({localTotal} total) {selectedLocalCars.size > 0 && ( ({selectedLocalCars.size} selected) )}

{/* 배너 업데이트 버튼 */} {hasBannerChanges && ( )} {/* 배너 상태 표시 */} Banner: {localBannerSelections.size}
{localLoading ? (
) : localCars.length === 0 ? (
📦

No cars imported yet

Go to the "Search Carmodoo" tab to find and import cars.

) : ( <> {/* 배너 차량 드래그앤드롭 섹션 */} {bannerOrderedCars.length > 0 && (

🎯 Banner Cars ({bannerOrderedCars.length}) - Drag to reorder

{bannerOrderedCars.map((car, index) => ( {index + 1}
{car.images?.[0]?.url && ( {car.car_name )}
{translateCarName(car.car_name || '', 'en')}
{car.year} | {car.mileage?.toLocaleString()}km
))}
)}
{localCars.map((car, index) => { const mainImage = getImageUrl(car.images?.find((img) => img.is_main)?.url || car.images?.[0]?.url); const isSoldout = car.soldout || false; const isCheckedBanner = localBannerSelections.has(car.id); const isBannerCar = bannerCarIds.includes(car.id); return ( handleCarClick(car)} > {/* Banner 체크박스 */} {/* Display 토글 */} {/* Status (Soldout) */} ); })}
Banner Display Image Car Name Year Mileage Original Margin Final Price Fuel PDF Status Actions
e.stopPropagation()}>
{/* 드래그 핸들 (배너 차량만) */} {isBannerCar && ( )}
{mainImage ? ( {car.car_name { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : (
)}
{car.car_name}
{car.maker?.name} {car.model?.name}
{car.year}년 {car.month}월 {formatMileage(car.mileage)} {formatPrice(car.price_krw)} {car.margin_krw ? `+${formatPrice(car.margin_krw)}` : '-'} {formatPrice(car.final_price_krw || car.price_krw)} {car.fuel} {pdfStatus[car.id] ? ( ) : ( )} e.stopPropagation()}> {isSoldout ? ( ) : ( )}
{/* Pagination */} {localTotalPages > 1 && (
Page {localPage} of {localTotalPages}
)} )}
)} {/* All Cars Tab (Public View) */} {activeTab === 'all' && (

All Displayed Cars ({allCarsTotal} total)

This shows the cars that are visible to users (is_displayed = true). This is the same view as the public /cars page.

{/* Selection Controls for Request Mode */} {requestId && allCars.length > 0 && (
{selectedAllCars.size} selected
)} {allCarsLoading ? (
) : allCars.length === 0 ? (
📦

No cars displayed to users

Go to the "Local Cars" tab and toggle the display switch to show cars.

) : ( <>
{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 (
handleCarClick(car)} >
{/* Checkbox for Request Mode */} {requestId && (
e.stopPropagation()} > toggleAllCarSelection(car.id)} className="w-5 h-5 text-blue-600 rounded cursor-pointer" />
)} {mainImage ? ( {car.car_name { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : (
)} {/* PDF Status Badge */}
{pdfStatus[car.id] ? ( PDF ) : ( )}

{car.car_name}

{car.year}년 | {formatMileage(car.mileage)}

{car.fuel} | {car.transmission}

{formatPrice(car.final_price_krw || car.price_krw)}

); })}
{/* Pagination */} {allCarsTotalPages > 1 && (
Page {allCarsPage} of {allCarsTotalPages}
)} )}
)} {/* Carmodoo Search Tab */} {activeTab === 'carmodoo' && ( <> {/* Search Filters */}

Search from Carmodoo

{/* First Row: Complex, Maker, Model, Grade */}
{/* Complex (단지선택) */}
{/* Maker (제조사) */}
{/* Model (모델) */}
{/* Grade (등급) */}
{/* Second Row: Year, Price */}
{/* Year Range */}
{/* Price Range (만원 단위) */}
handleFilterChange('price_min', e.target.value)} placeholder="Min" className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500" />
handleFilterChange('price_max', e.target.value)} placeholder="Max" className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500" />
{/* Third Row: Mileage, Fuel, Displacement */}
{/* Mileage */}
handleFilterChange('mileage_max', e.target.value)} placeholder="e.g. 100000" className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-primary-500 focus:border-primary-500" />
{/* Fuel Type */}
{/* Displacement Range */}
{/* Search Button */}
{/* Import Result */} {importResult && (
0 ? 'bg-red-50 border-red-200' : importResult.pdfFailed > 0 ? 'bg-amber-50 border-amber-200' : 'bg-green-50 border-green-200' }`}>

0 ? 'text-red-800' : importResult.pdfFailed > 0 ? 'text-amber-800' : 'text-green-800' }`}> Import Complete

Imported: {importResult.imported} {importResult.skipped > 0 && ( Skipped: {importResult.skipped} )} {importResult.errors > 0 && ( Errors: {importResult.errors} )}
{importResult.imported > 0 && (
PDF Success: {importResult.pdfSuccess} {importResult.pdfFailed > 0 && ( PDF Failed: {importResult.pdfFailed} )}
)}
{/* Skipped 상세 */} {importResult.skipped > 0 && importResult.skipDetails && importResult.skipDetails.length > 0 && (

Skipped:

{importResult.skipDetails.map((s, idx) => (
{s.car_name || s.car_no} {s.reason === 'already exists' ? `Already imported (ID: ${s.car_id})` : s.reason}
))}
)} {/* Error 상세 */} {importResult.errors > 0 && importResult.errorDetails && importResult.errorDetails.length > 0 && (

Errors:

{importResult.errorDetails.map((e, idx) => (
{e.car_no} {e.error}
))}
)} {/* PDF 실패 상세 정보 */} {importResult.pdfFailed > 0 && importResult.pdfDetails && (

PDF Failed (3 retries attempted):

{importResult.pdfDetails .filter(d => !d.success) .map((detail, idx) => (
[{detail.car_id}] {detail.car_name} {detail.attempts} attempts - {detail.error || 'Unknown error'}
))}

Retry from Local Cars tab using the "PDF" button.

)}
)} {/* Banner Registration Result */} {bannerResult && (
0 ? 'bg-amber-50 border-amber-200' : 'bg-purple-50 border-purple-200' }`}>

0 ? 'text-amber-800' : 'text-purple-800' }`}> Banner Registration Complete

Banners Created: {bannerResult.registered} Cars Imported: {bannerResult.imported} Skipped: {bannerResult.skipped} {bannerResult.errors > 0 && ( Errors: {bannerResult.errors} )}
PDF Success: {bannerResult.pdfSuccess} {bannerResult.pdfFailed > 0 && ( PDF Failed: {bannerResult.pdfFailed} )}
{/* PDF 실패 상세 정보 */} {bannerResult.pdfFailed > 0 && bannerResult.pdfDetails && (

PDF 생성 실패 차량 (3회 재시도 완료):

{bannerResult.pdfDetails .filter(d => !d.success) .map((detail, idx) => (
[{detail.car_id}] {detail.car_name} {detail.attempts}회 시도 - {detail.error || 'Unknown error'}
))}

⚠️ Local Cars 탭에서 해당 차량의 "PDF" 버튼을 클릭하여 수동으로 재시도할 수 있습니다.

)}
)} {/* Search Results */} {searched && (

Search Results ({totalCount} cars found)

{selectedCars.size} selected {importing && (
Duplicate check → Images → Description → Translation → PDF → Specs
)} {requestId && ( )}
{/* Table */}
{cars.map((car) => ( handleQuickImport(car)} > ))}
0 && selectedCars.size === cars.length} onChange={handleSelectAll} className="rounded border-gray-300 text-primary-600 focus:ring-primary-500" /> Image Car Name Type/Grade Year Mileage Price Fuel Status Description
handleSelectCar(car.id)} onClick={(e) => e.stopPropagation()} className="rounded border-gray-300 text-primary-600 focus:ring-primary-500" />
{car.main_image ? ( {car.car_name} { (e.target as HTMLImageElement).style.display = 'none'; }} /> ) : (
)}
{car.car_name}
{car.car_number}
{car.car_type_name || car.car_type || '-'} {car.grade_name || car.grade || '-'}
{car.year}년 {car.month}월 {formatMileage(car.mileage)} {formatPrice(car.price)} {car.fuel} {(car.seize_count || 0) > 0 || (car.collateral_count || 0) > 0 ? ( Issues ) : ( Clean )}
{/* Pagination */} {totalPages > 1 && (
Page {currentPage} of {totalPages}
)} {/* Empty State */} {cars.length === 0 && (
No cars found matching your criteria.
)}
)} {/* Initial State */} {!searched && (
🔍

Search for Cars from Carmodoo

Use the filters above to search for cars, then select and import them to your local database.

)} )} {/* Car Detail Modal */} {showDetailModal && selectedCar && (
{/* Modal Header */}

{selectedCar.car_name}

{selectedCar.images && selectedCar.images.length > 0 && ( )}
{/* Modal Content */}
{/* Image Gallery */}
{selectedCar.images && selectedCar.images.length > 0 ? ( <> {`${selectedCar.car_name} { (e.target as HTMLImageElement).style.display = 'none'; }} /> {/* Navigation Arrows */} {selectedCar.images.length > 1 && ( <> {/* Image Counter */}
{currentImageIndex + 1} / {selectedCar.images.length}
)} ) : (

No images available

)}
{/* Thumbnail Strip */} {selectedCar.images && selectedCar.images.length > 1 && (
{selectedCar.images.map((img, idx) => ( ))}
)}
{/* Car Details */}
{/* Basic Info */}

Basic Information

Maker {selectedCar.maker?.name || '-'}
Model {selectedCar.model?.name || '-'}
Year {selectedCar.year}년 {selectedCar.month}월
Mileage {formatMileage(selectedCar.mileage)}
{/* Pricing Info */}

Pricing

Original Price {formatPrice(selectedCar.price_krw)}
Korea Margin (한국 마진)
{ const marginValue = parseInt(e.target.value || '0') * 10000; if (marginValue !== (selectedCar.margin_krw || 0)) { handleUpdateCar(selectedCar.id, { margin_krw: marginValue }); } }} className="w-24 px-2 py-1 border border-gray-300 rounded text-right" min="0" onClick={(e) => e.stopPropagation()} /> 만원
Mongolia Margin (몽골 마진)
{ const marginValue = parseInt(e.target.value || '0') * 10000; if (marginValue !== (selectedCar.margin_mn || 0)) { handleUpdateCar(selectedCar.id, { margin_mn: marginValue }); } }} className="w-24 px-2 py-1 border border-gray-300 rounded text-right" min="0" onClick={(e) => e.stopPropagation()} /> 만원
Final Price (Korea) {formatPrice(selectedCar.final_price_krw || selectedCar.price_krw)}
Final Price (Mongolia) {formatPrice(selectedCar.final_price_mn || selectedCar.price_krw)}
Display to Users
{/* Technical Info */}

Technical Details

Fuel {selectedCar.fuel || '-'}
Transmission {selectedCar.transmission || '-'}
Color {selectedCar.color || '-'}
Displacement {selectedCar.displacement ? `${selectedCar.displacement}cc` : '-'}
Car Number {selectedCar.car_number || '-'}
{/* Status Info */}

Status

Status {selectedCar.status}
Sold Out {selectedCar.soldout ? 'SOLD OUT' : 'Available'}
Seize Count 0 ? 'text-red-600' : ''}`}> {selectedCar.seize_count || 0}
Collateral Count 0 ? 'text-red-600' : ''}`}> {selectedCar.collateral_count || 0}
Created At {new Date(selectedCar.created_at).toLocaleDateString()}
{/* Source Info */}

Source Info

Source {selectedCar.source}
Source ID {selectedCar.source_id}
Dealer {selectedCar.dealer_name || '-'}
{/* Dealer's Comment Section */}

Dealer's Comment

{!editingComment && dealerComment && (
{dealerComment.ko && ( )}
)}
{loadingComment ? (
) : editingComment ? (