## 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>
121 lines
3.7 KiB
TypeScript
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 };
|
|
}
|