diff --git a/supabase/migrations/20260914_clinic_subdomain.sql b/supabase/migrations/20260914_clinic_subdomain.sql new file mode 100644 index 0000000..81de07a --- /dev/null +++ b/supabase/migrations/20260914_clinic_subdomain.sql @@ -0,0 +1,65 @@ +-- ═══════════════════════════════════════════════════════════════ +-- 서포터즈 사이트 주소: 병원별 서브도메인 +-- ═══════════════════════════════════════════════════════════════ +-- 서포터즈 사이트는 공용 도메인(webforai.kr)의 병원별 서브도메인에 둔다. +-- 병원마다 도메인을 사면 병원 수만큼 갱신 대상이 늘고, 한 건만 놓쳐도 그 병원 사이트가 멈춘다. +-- 근거: docs/business/INFINITH_서포터즈_사이트주소_방식비교_2026-09-14.docx +-- +-- 이 표가 서브도메인의 단일 원본이다. 워커·배포 스크립트가 이름을 매번 짓지 않고 여기서 읽는다. +-- 이름을 코드가 지으면 "뷰"·"연세"·"그랜드"처럼 여러 병원이 쓰는 이름에서 충돌이 난다. + +ALTER TABLE clinic_registry + ADD COLUMN IF NOT EXISTS subdomain TEXT, + -- 워커가 쓰는 병원 id(--clinic). 서브도메인과 다를 수 있어 따로 둔다. + ADD COLUMN IF NOT EXISTS clinic_slug TEXT, + -- 주소를 언제 붙였는지. 색인 전환 판단에 쓴다. + ADD COLUMN IF NOT EXISTS subdomain_assigned_at TIMESTAMPTZ; + +-- 한 서브도메인은 한 병원에만. 대소문자 구분 없이 막는다. +CREATE UNIQUE INDEX IF NOT EXISTS clinic_registry_subdomain_key + ON clinic_registry (lower(subdomain)) + WHERE subdomain IS NOT NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS clinic_registry_clinic_slug_key + ON clinic_registry (lower(clinic_slug)) + WHERE clinic_slug IS NOT NULL; + +-- 형식: 소문자·숫자·붙임표만. 2~40자(1자는 실수일 가능성이 높고 짧은 이름을 병원에 내주게 된다). 붙임표로 시작하거나 끝날 수 없고 연속 붙임표도 막는다. +-- 대문자·밑줄·점을 넣으면 DNS 레이블로 쓸 수 없거나 브라우저가 다르게 해석한다. +ALTER TABLE clinic_registry + DROP CONSTRAINT IF EXISTS clinic_registry_subdomain_format; +ALTER TABLE clinic_registry + ADD CONSTRAINT clinic_registry_subdomain_format + CHECK (subdomain IS NULL OR (subdomain ~ '^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$' AND subdomain !~ '--')); + +-- 예약어. 서비스가 쓰거나 쓸 수 있는 이름은 병원에 주지 않는다. +-- 여기 있는 이름을 병원에 내주면 나중에 메일·API·관리 화면을 붙일 때 주소를 회수해야 한다. +-- 회수하면 그 시점에 이미 색인된 주소가 깨진다. +ALTER TABLE clinic_registry + DROP CONSTRAINT IF EXISTS clinic_registry_subdomain_reserved; +ALTER TABLE clinic_registry + ADD CONSTRAINT clinic_registry_subdomain_reserved + CHECK (subdomain IS NULL OR subdomain NOT IN ( + -- 웹·서비스 기본 + 'www', 'web', 'app', 'apps', 'api', 'admin', 'dashboard', 'console', 'portal', + 'static', 'assets', 'cdn', 'img', 'images', 'media', 'files', 'download', 'downloads', + -- 메일·인증 (잘못 내주면 메일 수신과 도메인 소유 확인이 깨진다) + 'mail', 'email', 'smtp', 'imap', 'pop', 'mx', 'ns', 'ns1', 'ns2', 'dns', + 'autodiscover', 'autoconfig', 'mta-sts', 'dmarc', 'spf', 'dkim', + -- 환경·배포 + 'dev', 'test', 'stage', 'staging', 'preview', 'demo', 'sandbox', 'beta', 'alpha', 'local', + 'status', 'health', 'metrics', 'log', 'logs', 'monitor', + -- 제품·조직 + 'infinith', 'webforai', 'supporters', 'clinic', 'clinics', 'hospital', 'report', 'reports', + 'discovery', 'plan', 'planner', 'blog', 'news', 'newsroom', 'help', 'support', 'docs', + 'about', 'contact', 'login', 'auth', 'account', 'billing', 'pay', 'payment', + -- 오인 방지 + 'en', 'ko', 'ja', 'zh', 'kr', 'go', 'or', 'co', 'ne', 're' + )); + +COMMENT ON COLUMN clinic_registry.subdomain IS + '서포터즈 사이트 주소의 서브도메인 라벨. 공용 도메인 앞에 붙는다(예: viewpl → viewpl.webforai.kr). 소문자·숫자·붙임표만.'; +COMMENT ON COLUMN clinic_registry.clinic_slug IS + '워커 --clinic 에 넘기는 병원 id. 빌드 폴더·Vercel 프로젝트 이름에 쓰인다. 서브도메인과 다를 수 있다.'; +COMMENT ON COLUMN clinic_registry.subdomain_assigned_at IS + '서브도메인을 배정한 시각. 색인 허용 전환 전에 주소를 바꾸는 편이 싸므로 그 판단에 쓴다.';