11 KiB
Clinic Registry — Functional Specifications
Document Type: Functional Specifications Version: 1.0 Status: Design Approved — Implementation Hold Date: 2026-04-05 Author: Claude Code, directed by Haewon Kam
1. 목적 (Purpose)
잠재 고객사 성형외과의 사전 검증된 채널 마스터 DB를 구축하여, INFINITH 분석 파이프라인의 discovery 단계를 **확률적 검색(probabilistic search)**에서 **결정적 조회(deterministic lookup)**로 전환한다.
2. Scope
In-Scope
- 국내 성형외과 잠재 고객사 100곳 (MVP) → 500곳 확장
- 병원 마스터 정보 (이름, 도메인, 웹사이트, 개원연도, 위치)
- 사전 검증된 채널 핸들 (YouTube/Instagram/Facebook/Naver Blog/Naver Place/강남언니/TikTok)
- Human-in-the-loop 방식 초기 구축 (Perplexity Comet 활용)
discover-channelsEdge Function의 Registry-only 전환- 미등록 도메인에 대한 에러 UX
Out-of-Scope
- 미등록 병원 자동 fallback discovery (명시적으로 제외)
- 병원 자가 등록 포털 (추후 Sprint)
- 경쟁사 자동 업데이트 추적
- 실시간 채널 메트릭 수집 (별도
collect-channel-dataFunction의 책임)
3. 사용자 스토리 (User Stories)
US-1: 등록된 고객사 분석
As a INFINITH 운영자 I want 등록된 병원의 URL로 분석을 요청하면 So that 10초 이내에 검증된 채널 기반의 정확한 분석 결과를 받는다
Acceptance Criteria:
- URL의 도메인이
clinic_registry.domain과 매칭되면 discovery 단계를 건너뛴다 - 분석 리포트에 "✓ Registry-verified" 배지가 표시된다
- 개원연도, 위치 정보가 자동 포함된다
US-2: 미등록 병원 차단
As a INFINITH 운영자 I want 등록되지 않은 병원 URL 입력 시 명확한 에러를 받고 So that 검색 오류가 있는 부정확한 분석을 방지한다
Acceptance Criteria:
- 미등록 도메인 입력 시
404 CLINIC_NOT_REGISTERED응답 - 사용자에게 "지원하지 않는 병원" 안내 + "등록 요청" CTA 표시
- 에러는 analytics에 기록되어 향후 등록 우선순위에 반영
US-3: Registry 데이터 큐레이션
As a INFINITH 운영자 I want Perplexity Comet으로 병원 정보를 수집하고 CSV로 import하여 So that 100곳의 검증된 마스터 데이터를 30–60분 내에 구축한다
Acceptance Criteria:
- 제공된 CSV 템플릿에 맞는 데이터를 입력할 수 있다
- import 스크립트가 데이터 검증 (필수 필드, 중복 domain 체크) 후 DB 적재
- Import 결과 요약: 성공/실패/경고 카운트
4. 데이터 모델 (Data Model)
4.1 Table: clinic_registry
CREATE TABLE clinic_registry (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
-- 식별 정보
name text NOT NULL,
name_aliases text[] DEFAULT '{}',
domain text UNIQUE NOT NULL,
website_url text NOT NULL,
-- 병원 메타
founded_year int,
location_gu text,
category text DEFAULT '성형외과',
-- 채널 핸들 (사전 검증)
youtube_handle text,
youtube_channel_id text,
instagram_handle text,
facebook_handle text,
facebook_url text,
naver_blog_handle text,
naver_place_id text,
gangnam_unni_url text,
tiktok_handle text,
-- 큐레이션 메타
verified_by text NOT NULL CHECK (verified_by IN ('manual','llm','scrape')),
verified_at timestamptz DEFAULT now(),
last_checked_at timestamptz DEFAULT now(),
is_active boolean DEFAULT true,
notes text
);
CREATE UNIQUE INDEX idx_clinic_registry_domain ON clinic_registry (domain) WHERE is_active = true;
CREATE INDEX idx_clinic_registry_aliases ON clinic_registry USING gin (name_aliases);
CREATE INDEX idx_clinic_registry_last_checked ON clinic_registry (last_checked_at);
4.2 Field Dictionary
| Field | Type | Required | Notes |
|---|---|---|---|
name |
text | ✓ | 공식 병원명 (e.g., "아이디병원") |
name_aliases |
text[] | 별칭/영문명 (e.g., {"아이디성형외과","ID Hospital"}) |
|
domain |
text | ✓ | www. 제거된 도메인. Lookup key. |
website_url |
text | ✓ | 전체 URL (redirect 대응) |
founded_year |
int | 개원연도 (4-digit) | |
location_gu |
text | "강남구", "서초구" 등 | |
youtube_channel_id |
text | UC... 형식 (24자) |
|
naver_place_id |
text | Naver Place 고유 ID (동명이인 방지) | |
verified_by |
enum | ✓ | manual / llm / scrape |
is_active |
boolean | false면 lookup에서 제외 |
5. 기능 요구사항 (Functional Requirements)
FR-1: Registry Lookup
- Input:
url(string, full URL) - Process:
new URL(url).hostname추출^www\.prefix 제거clinic_registry테이블에서domain = {hostname} AND is_active = true조회
- Output (match):
{ clinicName, verifiedChannels, source: 'registry', foundedYear, location } - Output (miss):
{ error: 'CLINIC_NOT_REGISTERED', domain, status: 404 } - Latency: <100ms (single indexed query)
FR-2: VerifiedChannels 매핑
clinic_registry 레코드를 기존 VerifiedChannels 타입으로 변환:
function mapRegistryToVerifiedChannels(r: ClinicRegistry): VerifiedChannels {
return {
youtube: r.youtube_channel_id ? {
handle: r.youtube_handle, channelId: r.youtube_channel_id,
verified: true, url: `https://youtube.com/${r.youtube_handle}`
} : null,
instagram: r.instagram_handle ? [{
handle: r.instagram_handle, verified: true,
url: `https://instagram.com/${r.instagram_handle}`
}] : [],
facebook: r.facebook_handle ? {
handle: r.facebook_handle, verified: true,
url: r.facebook_url ?? `https://facebook.com/${r.facebook_handle}`
} : null,
naverBlog: r.naver_blog_handle ? {
handle: r.naver_blog_handle, verified: true,
url: `https://blog.naver.com/${r.naver_blog_handle}`
} : null,
gangnamUnni: r.gangnam_unni_url ? { handle: '', url: r.gangnam_unni_url, verified: true } : null,
tiktok: r.tiktok_handle ? {
handle: r.tiktok_handle, verified: true,
url: `https://tiktok.com/@${r.tiktok_handle}`
} : null,
};
}
FR-3: CSV Import
- Input: CSV 파일 (컬럼: FR-1 field dictionary 참조)
- Validation:
name,domain,website_url,verified_by필수domain중복 검사 (upsert on conflict)youtube_channel_id형식 체크 (^UC[a-zA-Z0-9_-]{22}$)founded_year범위 체크 (1950–현재)
- Output:
{ inserted, updated, skipped, errors: [] }
FR-4: 미등록 병원 UX
AnalysisLoadingPage.tsx에서CLINIC_NOT_REGISTERED응답 감지- 표시: 전용 페이지 (로딩 스피너 대신)
- 헤드라인: "현재 지원하지 않는 병원입니다"
- 본문: "{domain}은(는) 분석 대상 병원 목록에 포함되지 않습니다."
- CTA: "병원 등록 요청하기" 버튼 → Slack 알림 또는 간단한 폼
FR-5: Registry-verified 배지
ClinicSnapshot.tsx에서source === 'registry'일 때- 녹색 "✓ 검증된 병원" 배지
- 개원연도 + 위치 표시 (
founded_year+location_gu)
6. 비기능 요구사항 (Non-Functional Requirements)
| 항목 | 목표 | 근거 |
|---|---|---|
| Registry lookup 레이턴시 | <100ms | 인덱스된 단일 쿼리 |
| Registry 정확도 | 100% | Human-in-the-loop 큐레이션 |
| Registry 커버리지 (MVP) | 100곳 | 잠재 고객사 TOP 100 |
| CSV import 처리량 | 500행/30초 | 1회성 bulk insert |
| 재검증 주기 | 30일 | last_checked_at 기반 알림 |
7. API 계약 (API Contract)
POST /functions/v1/discover-channels (변경됨)
Request:
{ "url": "https://www.idhospital.com", "clinicName": "아이디병원" }
Response 200 (registry hit):
{
"clinicName": "아이디병원",
"verifiedChannels": { "youtube": {...}, "instagram": [...], ... },
"source": "registry",
"foundedYear": 2005,
"location": "강남구"
}
Response 404 (registry miss):
{
"error": "CLINIC_NOT_REGISTERED",
"message": "현재 지원하지 않는 병원입니다.",
"domain": "unknown-clinic.com"
}
Breaking Changes:
- 기존 5개 외부 API 호출 전부 제거
source필드 추가 (항상"registry"또는 에러)- 미등록 도메인은 404 반환 (이전에는 부정확한 결과라도 200 반환)
8. 마이그레이션 전략 (Migration Strategy)
Phase 0: 문서화 (완료)
- Plan / Functional Specs / Retrospective ✓
Phase 1: Schema & Template (30min)
supabase/migrations/20260405_clinic_registry.sql작성/적용data/clinic-registry-template.csv템플릿 배포
Phase 2: Human-in-the-loop 수집 (사용자 직접, 30–60min)
- 사용자가 Perplexity Comet으로 100곳 정보 수집
- CSV 템플릿에 기입
- 수동 QA (명백한 오류 제거)
Phase 3: Import & Integration (1.5h)
scripts/import-registry.ts작성 + 실행discover-channels/index.tsRegistry-only 전환AnalysisLoadingPage.tsx에러 UXClinicSnapshot.tsxverified 배지
Phase 4: 검증 (30min)
- 등록 도메인 테스트 (아이디병원) → registry hit + 정확한 채널
- 미등록 도메인 테스트 → 404 + 등록 요청 UX
- Edge Function 배포 + Vercel 배포
9. 검증 시나리오 (Test Scenarios)
| # | 시나리오 | Expected |
|---|---|---|
| T1 | 등록된 idhospital.com으로 분석 |
source: registry, verified 배지, <100ms discovery |
| T2 | 등록된 www.banobagi.com으로 분석 (www. 처리) |
Registry hit |
| T3 | 미등록 random-clinic.com으로 분석 |
404 + 등록 요청 UX |
| T4 | is_active=false인 병원 분석 |
404 (폐업 병원 차단) |
| T5 | 잘못된 CSV (중복 domain) import | Error 반환, insert 차단 |
| T6 | Registry 레코드의 Instagram 핸들 수정 후 재분석 | 수정된 핸들로 즉시 분석 |
10. 의존성 & 리스크 (Dependencies & Risks)
Dependencies
- Supabase PostgreSQL (기존 인프라)
- Perplexity Comet (사용자가 수집에 사용)
- 기존
collect-channel-dataFunction (변경 없음)
Risks
| Risk | Mitigation |
|---|---|
| 병원 폐업/리브랜딩 추적 실패 | last_checked_at 30일 기반 알림, is_active 플래그 |
| 채널 핸들 변경 (병원이 Instagram ID 교체) | 수동 업데이트 + 분석 시 404 응답 패턴 모니터링 |
| 고객사 확장 시 curation 병목 | Admin UI 추후 개발 (Sprint 6) |
| 미등록 URL 입력이 많아 UX 저하 | 등록 요청 CTA + 운영자 알림으로 scope 확장 |
11. Open Questions
- 미등록 병원의 "등록 요청" CTA는 어떤 채널로 알림을 보낼지? (Slack / email / DB)
- Registry 수정 권한 관리는 어떻게? (현재는 직접 SQL, 추후 Admin UI)
- 경쟁사(non-고객사)를 비교 분석용으로 registry에 추가할 것인지?
This document specifies the Registry-first architecture that replaces probabilistic channel discovery with deterministic lookup over a curated customer scope.