o2o-infinith-demo/supabase/functions/_shared/urlClassifier.ts
Haewon Kam d5f7f24e0a feat: clinic registry DB + pipeline audit P0 fixes
## Clinic Registry
- data/clinic-registry/clinic_registry_working.csv — 91개 병원 채널 마스터 DB
- data/clinic-registry/INFINITH_Outbound_List.csv — BD팀 아웃바운드 리스트 (17컬럼)
- data/clinic-registry/update_csv.py — 안전 CSV 업데이트 스크립트 (빈 필드만 채움)
- data/clinic-registry/extract_place_ids.py — 네이버 플레이스 ID 추출기
- scripts/import-registry.ts — CSV → Supabase clinic_registry 테이블 임포트
- supabase/migrations/20260406_clinic_registry.sql — clinic_registry 테이블 스키마

## Pipeline P0 Bug Fixes (전수 감사 후)
- fix(collect-channel-data): 강남언니 rating 0-10 스케일 오변환 제거
  - 기존: rating ≤ 5이면 ×2 → 4.8/10을 9.6/10으로 잘못 변환
  - 수정: Firecrawl 프롬프트가 이미 0-10 지시 → rawValue 직접 신뢰
- fix(generate-report): Perplexity 단일 fetch → fetchWithRetry 교체
  - maxRetries:2, backoffMs:[5000,15000], timeoutMs:90s
  - 기존: 타임아웃/429 시 리포트 생성 전체 실패
  - 수정: 자동 재시도로 일시적 API 오류 극복

## Docs
- docs/PIPELINE_IMPROVEMENT_PLAN.md — Sprint 0/1/2 완료 표시 + 전수 감사 결과 추가
- docs/REGISTRY_FUNCTIONAL_SPECS.md, DB_SCHEMA_V3.md 외 기획 문서 다수 추가

## New Components & Features
- supabase/functions/generate-content-plan, adjust-strategy — 콘텐츠 플랜/전략 조정
- src/components/plan/EditEntryModal, StrategyAdjustmentSection — 플랜 편집 UI
- supabase/functions/_shared/dataQuality, foundingYearExtractor, urlClassifier — 데이터 품질 유틸

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:33:25 +09:00

121 lines
3.7 KiB
TypeScript

/**
* URL Classification Guard — Harness 1
*
* Separates URL classification into a testable pure function with
* an embedded test corpus. Prevents misclassification bugs like
* `/about/meet-our-doctors.php` being tagged as an "about" page.
*
* Priority: doctor > surgery > about (specific → general)
*/
// ─── Pattern Definitions ───
const PAGE_PATTERNS = {
doctor: {
positive: [
"/doctor", "/doctors", "/team", "/staff", "/specialist", "/professor",
"/의료진", "/원장", "meet-our-doctor", "/physicians", "/surgeon",
"our-team", "our-doctors",
],
},
surgery: {
positive: [
"/surgery", "/service", "/procedure", "/treatment", "/시술", "/수술",
"/procedures", "/treatments", "/services", "/진료",
],
},
about: {
positive: [
"/about", "/intro", "/소개", "/greeting", "/인사말", "/history", "/연혁",
"/company", "/clinic-info",
],
negative: [
"/doctor", "/doctors", "/procedure", "/surgery", "/service",
"/treatment", "meet-our", "/team", "/staff", "/specialist",
"/의료진", "/원장", "/시술", "/수술", "/진료",
"our-team", "our-doctors",
],
},
} as const;
// ─── Classification Function ───
/**
* Classify a URL into one of: 'doctor' | 'surgery' | 'about' | null.
* Uses priority ordering (doctor > surgery > about) so that
* compound URLs like `/about/meet-our-doctors.php` correctly resolve
* to the more specific category.
*/
export function classifyPageUrl(url: string): "doctor" | "surgery" | "about" | null {
const lower = url.toLowerCase();
// Priority 1: Doctor pages
if (PAGE_PATTERNS.doctor.positive.some((p) => lower.includes(p))) {
return "doctor";
}
// Priority 2: Surgery/service pages
if (PAGE_PATTERNS.surgery.positive.some((p) => lower.includes(p))) {
return "surgery";
}
// Priority 3: About pages (with negative exclusion)
if (PAGE_PATTERNS.about.positive.some((p) => lower.includes(p))) {
if (PAGE_PATTERNS.about.negative.some((p) => lower.includes(p))) {
return null; // Contains about + doctor/surgery keyword → ambiguous, skip
}
return "about";
}
return null;
}
// ─── Test Corpus ───
/**
* Known failure cases and expected classifications.
* Used by validateClassifier() for regression prevention.
*/
export const CLASSIFICATION_TEST_CORPUS: ReadonlyArray<readonly [string, "doctor" | "surgery" | "about" | null]> = [
// Bug cases (historically misclassified)
["/about/meet-our-doctors.php", "doctor"], // 그랜드성형외과 bug
["/about/procedures.php", "surgery"], // 그랜드성형외과
// Standard doctor pages
["/doctors", "doctor"],
["/의료진", "doctor"],
["/team/professor-kim", "doctor"],
["/about/our-team", "doctor"],
// Standard surgery pages
["/surgery/rhinoplasty", "surgery"],
["/시술안내", "surgery"],
["/services/breast", "surgery"],
// Standard about pages
["/about/", "about"],
["/about/greeting.php", "about"],
["/about/intro", "about"],
["/about/history", "about"],
["/소개", "about"],
["/인사말", "about"],
// Null cases
["/gallery", null],
["/contact", null],
["/blog/post-123", null],
];
// ─── Self-Test ───
/**
* Run classification against the test corpus.
* Call during Edge Function cold-start for automatic regression detection.
*/
export function validateClassifier(): { pass: boolean; failures: string[] } {
const failures: string[] = [];
for (const [url, expected] of CLASSIFICATION_TEST_CORPUS) {
const result = classifyPageUrl(url);
if (result !== expected) {
failures.push(`${url}: expected '${expected}', got '${result}'`);
}
}
return { pass: failures.length === 0, failures };
}