o2o-infinith-demo/docs/REGISTRY_FUNCTIONAL_SPECS.md

11 KiB
Raw Blame History

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-channels Edge Function의 Registry-only 전환
  • 미등록 도메인에 대한 에러 UX

Out-of-Scope

  • 미등록 병원 자동 fallback discovery (명시적으로 제외)
  • 병원 자가 등록 포털 (추후 Sprint)
  • 경쟁사 자동 업데이트 추적
  • 실시간 채널 메트릭 수집 (별도 collect-channel-data Function의 책임)

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곳의 검증된 마스터 데이터를 3060분 내에 구축한다

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:
    1. new URL(url).hostname 추출
    2. ^www\. prefix 제거
    3. 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)

  1. supabase/migrations/20260405_clinic_registry.sql 작성/적용
  2. data/clinic-registry-template.csv 템플릿 배포

Phase 2: Human-in-the-loop 수집 (사용자 직접, 3060min)

  1. 사용자가 Perplexity Comet으로 100곳 정보 수집
  2. CSV 템플릿에 기입
  3. 수동 QA (명백한 오류 제거)

Phase 3: Import & Integration (1.5h)

  1. scripts/import-registry.ts 작성 + 실행
  2. discover-channels/index.ts Registry-only 전환
  3. AnalysisLoadingPage.tsx 에러 UX
  4. ClinicSnapshot.tsx verified 배지

Phase 4: 검증 (30min)

  1. 등록 도메인 테스트 (아이디병원) → registry hit + 정확한 채널
  2. 미등록 도메인 테스트 → 404 + 등록 요청 UX
  3. 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-data Function (변경 없음)

Risks

Risk Mitigation
병원 폐업/리브랜딩 추적 실패 last_checked_at 30일 기반 알림, is_active 플래그
채널 핸들 변경 (병원이 Instagram ID 교체) 수동 업데이트 + 분석 시 404 응답 패턴 모니터링
고객사 확장 시 curation 병목 Admin UI 추후 개발 (Sprint 6)
미등록 URL 입력이 많아 UX 저하 등록 요청 CTA + 운영자 알림으로 scope 확장

11. Open Questions

  1. 미등록 병원의 "등록 요청" CTA는 어떤 채널로 알림을 보낼지? (Slack / email / DB)
  2. Registry 수정 권한 관리는 어떻게? (현재는 직접 SQL, 추후 Admin UI)
  3. 경쟁사(non-고객사)를 비교 분석용으로 registry에 추가할 것인지?

This document specifies the Registry-first architecture that replaces probabilistic channel discovery with deterministic lookup over a curated customer scope.