-- ═══════════════════════════════════════════════════════════════ -- 서포터즈 사이트 주소: 병원별 서브도메인 -- ═══════════════════════════════════════════════════════════════ -- 서포터즈 사이트는 공용 도메인(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 '서브도메인을 배정한 시각. 색인 허용 전환 전에 주소를 바꾸는 편이 싸므로 그 판단에 쓴다.';