import { BusinessInfo } from '../types'; /** * 헬퍼 함수: URL에서 파일을 가져와 File 객체로 변환합니다. * CORS 문제를 해결하기 위해 백엔드 프록시를 사용합니다. */ const urlToFile = async (url: string, filename: string): Promise => { try { 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); 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; existingFingerprints?: Set; } /** * Instagram 프로필 URL에서 username을 추출합니다. */ export const parseInstagramUrl = (url: string): string | null => { try { // instagram.com/username 형식 const match = url.match(/instagram\.com\/([a-zA-Z0-9._]+)/); if (match && match[1] && !['p', 'reel', 'stories', 'explore', 'accounts'].includes(match[1])) { return match[1]; } return null; } catch { return null; } }; /** * 인스타그램 프로필 정보를 크롤링하는 함수입니다. * 클라이언트에서 백엔드 API를 호출하여 인스타그램 프로필 정보를 가져옵니다. * @param {string} url - 인스타그램 프로필 URL * @param {(msg: string) => void} [onProgress] - 진행 상황을 알리는 콜백 함수 * @param {CrawlOptions} [options] - 추가 옵션 (maxImages, existingFingerprints) * @returns {Promise>} - 비즈니스 정보의 부분 객체 */ export const crawlInstagramProfile = async ( url: string, onProgress?: (msg: string) => void, options?: CrawlOptions ): Promise> => { const maxImages = options?.maxImages ?? 100; const existingFingerprints = options?.existingFingerprints ?? new Set(); onProgress?.("인스타그램 프로필 정보 가져오는 중 (서버 요청)..."); try { // 백엔드 Express 서버의 /api/instagram/crawl 엔드포인트 호출 const response = await fetch('/api/instagram/crawl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); if (!response.ok) { const errData = await response.json().catch(() => ({})); 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})...`); const file = await urlToFile(imgUrl, `instagram_${data.username}_${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} - Instagram (@${data.username})`, images: imageFiles, address: '', category: 'Instagram', sourceUrl: url }; } catch (error: any) { console.error("인스타그램 크롤링 실패:", error); throw new Error(error.message || "인스타그램 프로필 정보를 가져오는데 실패했습니다."); } };