15 KiB
15 KiB
검색/분석 파이프라인 종합 개선 계획
작성일: 2026-04-04 최종 수정: 2026-04-07 (파이프라인 전수 감사 + P0 버그 수정) 상태: Sprint 0 ✅ 구현 완료 | Sprint 1 ✅ 완료 | Sprint 2 🔜 진행 중
2026-04-07 전수 감사 결과 (Full Pipeline Audit)
URL → DB → Channel Discovery → Firecrawl → Report 흐름 검증
전체 4단계 파이프라인을 코드 수준으로 감사한 결과:
✅ 이미 구현 완료된 것들
| 항목 | 파일 | 상태 |
|---|---|---|
| Registry-first domain lookup | discover-channels/index.ts |
✅ 완전 구현 |
| Vision Analysis (screenshot + Gemini) | _shared/visionAnalysis.ts, collect-channel-data |
✅ 완전 구현 |
wrapChannelTask 에러 격리 |
_shared/retry.ts |
✅ Promise.all 안전 |
fetchWithRetry + 지수 백오프 |
_shared/retry.ts |
✅ Firecrawl/Apify에 적용 |
| channel_errors 추적 + unconditional DB save | collect-channel-data/index.ts |
✅ 구현 |
| 강남언니 Firecrawl JSON scrape | collect-channel-data/index.ts |
✅ 구현 (rating 버그 수정됨) |
| Vision data → report 강제 주입 harness | generate-report/index.ts |
✅ 구현 |
| Founding year 3단계 fallback chain | collect → generate 양쪽 | ✅ 구현 |
| V3 dual-write (clinics + analysis_runs) | discover + collect + generate | ✅ 구현 |
🔧 이번 세션에서 수정한 P0 버그
| 버그 | 위치 | 수정 내용 |
|---|---|---|
| 강남언니 rating 0-10 스케일 오변환 | collect-channel-data:323 |
rating ≤ 5 → ×2 로직 제거. Firecrawl 프롬프트가 이미 0-10 지시 → 직접 신뢰 |
| Perplexity 단일 fetch (재시도 없음) | generate-report:115 |
fetchWithRetry(maxRetries:2, backoffMs:[5000,15000], timeoutMs:90s) 로 교체 |
🚧 남은 Gap (우선순위순)
| 우선순위 | Gap | 세부 내용 |
|---|---|---|
| P1 | Health score 미계산 | channel_snapshots.health_score 컬럼은 있지만 항상 NULL. 수집된 followers/rating/reviews 기반으로 계산 필요 |
| P1 | 네이버 블로그 공식 컨텐츠 미수집 | 현재: Naver Search API로 3rd-party 언급 수집. 필요: 등록된 공식 블로그 URL을 Firecrawl로 직접 스크랩 |
| P1 | Firecrawl 스크린샷 URL 만료 | GCS URL 7일 후 만료 → Supabase Storage로 아카이빙 필요 |
| P2 | Delta/트렌드 비교 없음 | channel_snapshots에 시계열 데이터 있지만 이전 run 대비 delta 계산 미구현 |
| P2 | V3 dual-write silent error | 에러 발생 시 console만 출력, analysis_runs.error_message에 기록 안 됨 |
| P3 | 강남언니 rating > 5 엣지케이스 | 수정 후에도 Firecrawl이 0-5 반환 시 그대로 저장. 추후 rawRating과 비교하는 정규화 로직 고려 |
Context
현재 파이프라인의 3-phase 아키텍처(discover → collect → generate)는 구조적으로는 괜찮으나, 실행 신뢰성과 정보 수집 범위에 심각한 문제가 있음.
핵심 문제
- Vision 분석 부재: 텍스트만 수집 → 이미지 속 정보(개원 연도, 의료진 사진, 인증 마크, 시술 전후) 100% 누락. 전체 정보의 약 40%를 놓침
- Silent failure 16건: 모든 catch 블록이 에러를 삼킴
- 데이터 품질 8건: 잘못된 URL, 동명이인 병원, 평점 스케일 혼동
- 에러 복구 0건: 재시도 로직 없음, 새로고침 시 데이터 유실
- API 불안정 6건: 타임아웃, 쿼터 소진, 인증 실패 미감지
목표: Vision 분석 추가 + 검색 정확도 + 에러 회복력 + UX 투명성 대폭 개선
진행 상태 체크리스트
Sprint 0: Vision Analysis 추가 ✅ 완료
성형외과 홈페이지의 핵심 정보가 이미지로 제공됨 (배너, 의료진 사진, 인증 마크 등). Firecrawl screenshot + Gemini Vision으로 이미지 속 정보를 추출.
-
WP-V1. 멀티페이지 스크린샷 캡처 + 저장 (45min)
- 파일:
collect-channel-data/index.ts에 추가 (Phase 2 — 검증된 URL들이 있는 시점) - 캡처 대상 (6+ 페이지):
- 병원 메인 페이지 — 배너, 소셜 아이콘, 카카오톡 버튼
- 의료진 페이지 — siteMap에서
/doctor,/team,/staffURL 자동 탐지 - 시술 안내 페이지 —
/surgery,/service,/procedure탐지 - YouTube 채널 랜딩 —
youtube.com/@{handle}(구독자 수, 고정 영상) - Instagram 프로필 —
instagram.com/{handle}(팔로워 수, 피드 미리보기) - 강남언니 페이지 — 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) ✅ 완료
- WP-1. YouTube Channel ID 정규식 수정 (20min)
- WP-2. Naver Place 동명이인 방지 (30min)
- WP-3. Google Maps URL 수정 (20min)
- WP-4. Naver Blog 공식 블로그 분리 (30min)
- WP-5. 강남언니 평점 정규화 (30min)
- WP-6. Perplexity 모델 상수화 (20min)
- WP-7. Apify 타임아웃 증가 (10min)
추가 완료:
- 소셜 버튼 직접 추출 (Firecrawl actions + JS 렌더링 후 href 추출)
Sprint 2: 에러 가시성 ✅ 완료
-
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 데이터를 리포트 프롬프트에 포함 (실제 데이터 기반)