# 검색/분석 파이프라인 종합 개선 계획 **작성일**: 2026-04-04 **최종 수정**: 2026-04-04 (Vision Analysis 추가) **상태**: Sprint 0 + Sprint 1 구현 중 ## Context 현재 파이프라인의 3-phase 아키텍처(discover → collect → generate)는 구조적으로는 괜찮으나, **실행 신뢰성**과 **정보 수집 범위**에 심각한 문제가 있음. ### 핵심 문제 - **Vision 분석 부재**: 텍스트만 수집 → 이미지 속 정보(개원 연도, 의료진 사진, 인증 마크, 시술 전후) 100% 누락. 전체 정보의 약 40%를 놓침 - **Silent failure 16건**: 모든 catch 블록이 에러를 삼킴 - **데이터 품질 8건**: 잘못된 URL, 동명이인 병원, 평점 스케일 혼동 - **에러 복구 0건**: 재시도 로직 없음, 새로고침 시 데이터 유실 - **API 불안정 6건**: 타임아웃, 쿼터 소진, 인증 실패 미감지 **목표**: Vision 분석 추가 + 검색 정확도 + 에러 회복력 + UX 투명성 대폭 개선 --- ## 진행 상태 체크리스트 ### Sprint 0: Vision Analysis 추가 ⭐ 최우선 (~2h) 성형외과 홈페이지의 핵심 정보가 이미지로 제공됨 (배너, 의료진 사진, 인증 마크 등). Firecrawl screenshot + Gemini Vision으로 이미지 속 정보를 추출. - [ ] **WP-V1**. 멀티페이지 스크린샷 캡처 + 저장 (45min) - 파일: `collect-channel-data/index.ts`에 추가 (Phase 2 — 검증된 URL들이 있는 시점) - 캡처 대상 (6+ 페이지): 1. **병원 메인 페이지** — 배너, 소셜 아이콘, 카카오톡 버튼 2. **의료진 페이지** — siteMap에서 `/doctor`, `/team`, `/staff` URL 자동 탐지 3. **시술 안내 페이지** — `/surgery`, `/service`, `/procedure` 탐지 4. **YouTube 채널 랜딩** — `youtube.com/@{handle}` (구독자 수, 고정 영상) 5. **Instagram 프로필** — `instagram.com/{handle}` (팔로워 수, 피드 미리보기) 6. **강남언니 페이지** — verified URL (평점, 리뷰 수, 의료진) - API: Firecrawl `formats: ["screenshot"]` + `screenshotOptions: { fullPage: false, quality: 80, viewport: { width: 1280, height: 800 } }` - 저장: Supabase Storage `screenshots/{reportId}/{channel}.png` → signed URL - DB: `channel_data.screenshots[]`에 `ScreenshotEvidence` 형태로 저장 - 프론트엔드: 이미 구현된 `ScreenshotProvider` → `EvidenceGallery` → `EvidenceLightbox` 에 바로 연결 - [ ] **WP-V2**. Gemini Vision 분석 (1h) - 파일: 새 `_shared/visionAnalysis.ts` - API: Google Gemini `gemini-2.0-flash` (GEMINI_API_KEY 이미 .env에 있음) - 각 스크린샷별 분석 프롬프트: - **메인 페이지**: 개원 연도, 소셜 아이콘, 카카오톡 버튼, 브랜드 컬러, 슬로건, 인증 마크 - **의료진 페이지**: 의사 이름 + 전문 분야 + 약력 (이미지 내 텍스트 OCR) - **시술 페이지**: 시술 카테고리, 가격 정보, 전후 사진 유무 - **YouTube 랜딩**: 구독자 수, 영상 수, 최근 업로드 제목, 고정 영상 - **Instagram 프로필**: 팔로워 수, 게시물 수, 최근 피드 미리보기, 바이오 - **강남언니**: 평점(/10), 리뷰 수, 의료진 수, 시술 종류 - 응답: 페이지별 JSON → `channel_data.visionAnalysis`에 저장 - [ ] **WP-V3**. Vision 데이터 → 리포트 + 증거 통합 (45min) - 파일: `generate-report/index.ts`, `transformReport.ts` - 변경: - 스크린샷 → `report.screenshots[]` (ScreenshotEvidence 형태) - 프론트엔드의 기존 EvidenceGallery/Lightbox에 바로 표시 - Vision 추출 의료진 → `clinicSnapshot.doctors` 보강 (이미지에서 읽은 정보) - Vision 추출 개원 연도 → `clinicSnapshot.established` 보강 - Vision 추출 YouTube/Instagram 수치 → KPI에 cross-reference - 각 채널 진단 항목에 `evidenceIds` 연결 → 해당 채널 스크린샷이 증거로 표시 - 검증: - 그랜드성형외과 분석 → "SINCE 2004" 개원 연도 추출 확인 - YouTube 분석 섹션 → 채널 랜딩 스크린샷 썸네일 표시 확인 - Instagram 분석 섹션 → 프로필 스크린샷 표시 확인 - 스크린샷 클릭 → 라이트박스 모달에서 풀사이즈 보기 확인 ### Vision Analysis 프롬프트 설계 ``` System: You are a medical clinic website visual analyst. Extract structured information from website screenshots. Respond ONLY with valid JSON. User: Analyze this Korean plastic surgery clinic homepage screenshot. Extract: 1. Founding year or operation duration (e.g., "SINCE 2004", "21년 무사고") 2. Doctor names and specialties shown in profile photos 3. Certification badges/marks (JCI, 보건복지부, medical tourism) 4. Main service categories from navigation menu 5. Social media icons/buttons visible (Instagram, YouTube, Blog, KakaoTalk, etc.) 6. Floating consultation buttons (KakaoTalk, LINE, WhatsApp) 7. Brand colors (primary, accent) from visual elements 8. Any promotional text or slogans in banners { "foundingYear": "2004", "operationYears": 21, "doctors": [{"name": "김OO", "specialty": "안면윤곽", "position": "대표원장"}], "certifications": ["JCI", "보건복지부 인증"], "serviceCategories": ["눈성형", "코성형", "가슴성형", "안면윤곽"], "socialIcons": [{"platform": "instagram", "visible": true}, ...], "floatingButtons": ["kakaotalk", "line"], "brandColors": {"primary": "#C4A882", "accent": "#FF1493"}, "slogans": ["끊임없이 의료성형 뷰티 트렌드를 연구하는 그랜드 의료진"] } ``` ### Vision Analysis에 필요한 API/리소스 | API | 용도 | 비용 | |-----|------|------| | Firecrawl `formats: ["screenshot"]` | 페이지 스크린샷 캡처 | 기존 요금에 포함 | | Gemini `gemini-2.0-flash-exp` | 이미지 분석 (Vision) | ~$0.002/이미지 | | Gemini `GEMINI_API_KEY` | 이미 설정됨 (.env) | ✅ 사용 가능 | ### Vision으로 수집 가능한 추가 정보 (현재 누락) | 정보 | 현재 수집 | Vision 추가 후 | |------|---------|-------------| | 개원 연도 | ❌ AI 추측 (자주 틀림) | ✅ 배너에서 직접 읽기 | | 의료진 수/이름 | ⚠️ 강남언니에서만 | ✅ 홈페이지 프로필에서 추출 | | 인증 마크 | ❌ 미수집 | ✅ JCI, 보건복지부 등 인식 | | 시술 카테고리 | ⚠️ Firecrawl JSON | ✅ 네비게이션 메뉴에서 확인 | | 카카오톡 상담 버튼 | ❌ JS 렌더링이라 못 잡음 | ✅ 플로팅 버튼 감지 | | 브랜드 컬러 | ⚠️ CSS 추출 (부정확) | ✅ 실제 비주얼에서 추출 | | 슬로건/태그라인 | ⚠️ 이미지 내 텍스트 누락 | ✅ 배너 텍스트 OCR | --- ### Sprint 1: 데이터 품질 Quick Wins (~2.5h) ✅ 완료 - [x] **WP-1**. YouTube Channel ID 정규식 수정 (20min) - [x] **WP-2**. Naver Place 동명이인 방지 (30min) - [x] **WP-3**. Google Maps URL 수정 (20min) - [x] **WP-4**. Naver Blog 공식 블로그 분리 (30min) - [x] **WP-5**. 강남언니 평점 정규화 (30min) - [x] **WP-6**. Perplexity 모델 상수화 (20min) - [x] **WP-7**. Apify 타임아웃 증가 (10min) **추가 완료:** - [x] 소셜 버튼 직접 추출 (Firecrawl actions + JS 렌더링 후 href 추출) --- ### Sprint 2: 에러 가시성 (~2.25h) - [ ] **WP-8**. 채널 수집 에러 추적 ⭐ 핵심 (1.5h) - 파일: `collect-channel-data/index.ts` - DB: `ALTER TABLE marketing_reports ADD COLUMN IF NOT EXISTS channel_errors JSONB DEFAULT '{}';` - 변경: - [ ] HTTP 상태 코드 체크 (429, 403, 500) - [ ] `Promise.allSettled` 결과 순회 → `channelErrors` 기록 - [ ] 부분 성공이어도 항상 DB 저장 (unconditional save) - [ ] status: `"collected"` vs `"partial"` vs `"collection_failed"` - [ ] 응답: `{ channelData, channelErrors, partialFailure }` - 검증: API 토큰 무효화 시 에러가 기록되는지 확인 - [ ] **WP-9**. Instagram/Facebook 검증 개선 (45min) - 파일: `_shared/verifyHandles.ts` - 변경: - [ ] `verified` 타입: `boolean | "unverifiable"` - [ ] Instagram: 로그인 리다이렉트 감지 - [ ] Facebook: HEAD → GET, 실패 시 `"unverifiable"` - [ ] `collect-channel-data`에서 `"unverifiable"` 포함 - 검증: Facebook 핸들이 "unverifiable"로 표시되고 수집은 시도됨 --- ### Sprint 3: 에러 회복 (~5.25h) - [ ] **WP-10**. API 재시도 유틸리티 (2h) - 파일: 새 `_shared/retry.ts` - 변경: `fetchWithRetry()` — 지수 백오프, 429 존중, AbortController 타임아웃 - 검증: 429 응답 시 자동 재시도 후 성공 - [ ] **WP-11**. 부분 실패 복구 (1h) - 파일: `collect-channel-data/index.ts` - 변경: `Promise.allSettled` 직후 무조건 중간 DB 저장 - 검증: 일부 채널 실패해도 나머지 데이터 보존 - [ ] **WP-12**. 파이프라인 이어하기 (1.5h) - 파일: `AnalysisLoadingPage.tsx`, `supabase.ts` - 변경: - [ ] `sessionStorage`에 reportId 저장 - [ ] URL에 reportId 포함 - [ ] DB status 폴링 - [ ] status별 이어하기 로직 - 검증: 분석 중 새로고침 → 이어서 진행 - [ ] **WP-13**. Enrichment 재시도 버튼 (45min) - 파일: `useEnrichment.ts`, `EmptyState.tsx` - 변경: `retry()` 함수 + "다시 시도" 버튼 (최대 2회) - 검증: Enrichment 실패 후 버튼 클릭 → 재시도 성공 --- ### Sprint 4: UX 마무리 (~50min) - [ ] **WP-14**. EmptyState 상태별 UI (20min) - 파일: `EmptyState.tsx` - 변경: `loading | error | not_found` 상태별 다른 UI - 검증: 각 상태에 맞는 아이콘/메시지/버튼 표시 - [ ] **WP-15**. Firecrawl Rate Limiting (30min) - 파일: `_shared/retry.ts`에 추가 - 변경: 도메인별 500ms 간격 강제 - 검증: Firecrawl 429 에러 발생 안 함 --- ## 핵심 파일 목록 | 파일 | Sprint | 설명 | |------|--------|------| | `supabase/functions/discover-channels/index.ts` | 0, 1 | 채널 발견 + Vision | | `supabase/functions/collect-channel-data/index.ts` | 0, 1, 2, 3 | 데이터 수집 | | `supabase/functions/_shared/visionAnalysis.ts` (신규) | 0 | Vision 분석 유틸 | | `supabase/functions/_shared/config.ts` | 1 | 공유 설정 | | `supabase/functions/_shared/verifyHandles.ts` | 1, 2 | 핸들 검증 | | `supabase/functions/_shared/retry.ts` (신규) | 3 | 재시도 유틸 | | `supabase/functions/enrich-channels/index.ts` | 1 | 레거시 enrichment | | `supabase/functions/generate-report/index.ts` | 0 | 리포트에 Vision 데이터 반영 | | `supabase/migrations/20260404_channel_errors.sql` (신규) | 2 | DB 마이그레이션 | | `src/pages/AnalysisLoadingPage.tsx` | 3 | 로딩 페이지 | | `src/hooks/useEnrichment.ts` | 3 | Enrichment 훅 | | `src/components/report/ui/EmptyState.tsx` | 4 | 빈 상태 UI | | `src/lib/supabase.ts` | 3 | API 클라이언트 | ## 총 예상 소요 시간 | Sprint | 시간 | 배포 범위 | 상태 | |--------|------|---------|------| | Sprint 0: Vision Analysis | ~2h | Edge Functions + Gemini | 🔜 다음 | | Sprint 1: Quick Wins | ~2.5h | Edge Functions × 5 | ✅ 완료 | | Sprint 2: 에러 가시성 | ~2.25h | DB + Edge Functions | 대기 | | Sprint 3: 에러 회복 | ~5.25h | Edge Functions + Frontend + Vercel | 대기 | | Sprint 4: UX 마무리 | ~50min | Frontend + Vercel | 대기 | | **합계** | **~13h** | | | --- ## Vision Analysis 아키텍처 ``` discover-channels (Stage A) ├─ A1. Firecrawl scrape (JSON + links) ← 기존 ├─ A2. Firecrawl map ← 기존 ├─ A3. Firecrawl branding ← 기존 ├─ A4. Firecrawl social buttons (JS actions) ← 방금 추가 └─ A5. Firecrawl screenshot + Gemini Vision ← Sprint 0 신규 ├─ 메인 페이지 스크린샷 캡처 ├─ Gemini Vision 분석 (개원 연도, 의료진, 인증, 소셜 아이콘) └─ 결과를 clinic 정보 + socialHandles에 병합 collect-channel-data └─ Vision 데이터를 channel_data.visionAnalysis에 저장 generate-report └─ Vision 데이터를 리포트 프롬프트에 포함 (실제 데이터 기반) ```