import { BusinessInfo } from '../types'; /** * 헬퍼 함수: URL에서 파일을 가져와 File 객체로 변환합니다. * CORS 문제나 Blob URL 등을 처리하기 위해 백업 프록시 로직을 포함합니다. * @param {string} url - 가져올 파일의 URL * @param {string} filename - 생성할 File 객체의 이름 * @returns {Promise} - File 객체 */ const urlToFile = async (url: string, filename: string): Promise => { try { // 백엔드 프록시를 통해 이미지 다운로드 (CORS 우회) const proxyUrl = `/api/proxy/image?url=${encodeURIComponent(url)}`; const response = await fetch(proxyUrl); if (!response.ok) { throw new Error(`Proxy failed with status: ${response.status}`); } const blob = await response.blob(); return new File([blob], filename, { type: blob.type || 'image/jpeg' }); } catch (e) { console.error(`이미지 다운로드 실패 ${url}:`, e); throw new Error(`이미지 다운로드 실패: ${url}`); } }; /** * 이미지 파일의 간단한 해시를 생성합니다 (파일 크기 + 첫 바이트). * 완벽한 중복 검사는 아니지만 빠르고 대부분의 경우를 처리합니다. */ const getImageFingerprint = async (file: File): Promise => { const size = file.size; const slice = file.slice(0, 1024); // 첫 1KB만 읽기 const buffer = await slice.arrayBuffer(); const bytes = new Uint8Array(buffer); let sum = 0; for (let i = 0; i < bytes.length; i++) { sum = (sum + bytes[i]) % 65536; } return `${size}-${sum}`; }; /** * 기존 이미지들의 fingerprint 목록을 생성합니다. */ export const getExistingFingerprints = async (existingImages: File[]): Promise> => { const fingerprints = new Set(); for (const img of existingImages) { try { const fp = await getImageFingerprint(img); fingerprints.add(fp); } catch (e) { console.warn('Fingerprint 생성 실패:', e); } } return fingerprints; }; export interface CrawlOptions { maxImages?: number; // 가져올 최대 이미지 수 (기본값: 15) existingFingerprints?: Set; // 중복 검사용 기존 이미지 fingerprints } /** * 네이버 플레이스 정보를 크롤링하는 함수입니다. * 클라이언트에서 백엔드 API를 호출하여 네이버 장소 정보를 가져옵니다. * @param {string} url - 네이버 플레이스 URL 또는 장소 ID * @param {(msg: string) => void} [onProgress] - 진행 상황을 알리는 콜백 함수 (선택 사항) * @param {CrawlOptions} [options] - 추가 옵션 (maxImages, existingFingerprints) * @returns {Promise>} - 비즈니스 정보의 부분 객체 */ export const crawlNaverPlace = async ( url: string, onProgress?: (msg: string) => void, options?: CrawlOptions ): Promise> => { const maxImages = options?.maxImages ?? 15; const existingFingerprints = options?.existingFingerprints ?? new Set(); onProgress?.("네이버 플레이스 정보 가져오는 중 (서버 요청)..."); try { // 백엔드 Express 서버의 /api/naver/crawl 엔드포인트 호출 const response = await fetch('/api/naver/crawl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) // 크롤링할 URL을 백엔드로 전송 }); if (!response.ok) { // 서버 응답이 성공적이지 않을 경우 에러 처리 const errData = await response.json().catch(() => ({})); // 에러 JSON 파싱 시도 throw new Error(errData.error || "서버 크롤링 요청 실패"); } const data = await response.json(); // 성공 시 응답 데이터 파싱 console.log(`백엔드 크롤링 결과: ${data.name}, 총 ${data.totalImages || data.images?.length}장 이미지 (셔플됨)`); onProgress?.(`'${data.name}' 정보 수신 완료. 이미지 다운로드 중... (총 ${data.totalImages || '?'}장 중 최대 ${maxImages}장)`); // 크롤링된 이미지 URL들을 File 객체로 변환 (중복 검사 포함) const imageFiles: File[] = []; const imageUrls = data.images || []; let skippedDuplicates = 0; for (let i = 0; i < imageUrls.length && imageFiles.length < maxImages; i++) { const imgUrl = imageUrls[i]; try { onProgress?.(`이미지 다운로드 중 (${imageFiles.length + 1}/${maxImages})...`); // 각 이미지 URL을 File 객체로 변환 const file = await urlToFile(imgUrl, `naver_${data.place_id}_${i}.jpg`); // 중복 검사 const fingerprint = await getImageFingerprint(file); if (existingFingerprints.has(fingerprint)) { console.log(`중복 이미지 발견, 건너뜁니다: ${imgUrl.slice(-30)}`); skippedDuplicates++; continue; } imageFiles.push(file); } catch (e) { console.warn("이미지 다운로드 실패, 건너뜁니다:", imgUrl, e); } } if (skippedDuplicates > 0) { console.log(`총 ${skippedDuplicates}장의 중복 이미지를 건너뛰었습니다.`); } // 다운로드된 이미지가 없을 경우 에러 발생 if (imageFiles.length === 0) { throw new Error("유효한 이미지를 하나도 다운로드하지 못했습니다. 다른 URL을 시도해보세요."); } onProgress?.(`${imageFiles.length}장의 이미지를 가져왔습니다.`); // 비즈니스 정보 객체 반환 return { name: data.name, description: data.description || `${data.name} - ${data.category} (${data.address})`, images: imageFiles, address: data.address, category: data.category, sourceUrl: url }; } catch (error: any) { console.error("크롤링 실패:", error); throw new Error(error.message || "네이버 플레이스 정보를 가져오는데 실패했습니다."); } };