From 9d8360d6340f8b2cee4cb405412d4dcf52485b76 Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Mon, 14 Sep 2026 11:09:52 +0900 Subject: [PATCH] =?UTF-8?q?feat(db):=20clinic=5Fregistry=20=EC=97=90=20?= =?UTF-8?q?=EC=84=9C=EB=B8=8C=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=C2=B7=EC=9C=A0=EB=8B=88=ED=81=AC=C2=B7=EC=98=88?= =?UTF-8?q?=EC=95=BD=EC=96=B4=20=EC=A0=9C=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서포터즈 사이트를 공용 도메인(webforai.kr)의 병원별 서브도메인에 두기로 했다. 수백 곳으로 늘 때 이름을 코드가 지으면 "뷰"·"연세"처럼 여러 병원이 쓰는 이름에서 충돌이 나므로, 이 표를 서브도메인의 단일 원본으로 둔다. 컬럼 - subdomain: 공용 도메인 앞에 붙는 라벨 (viewpl → viewpl.webforai.kr) - clinic_slug: 워커 --clinic 값. 빌드 폴더·Vercel 프로젝트 이름에 쓰여 서브도메인과 다를 수 있다 - subdomain_assigned_at: 배정 시각. 색인 전환 전에 주소를 바꾸는 편이 싸므로 판단에 쓴다 제약 - subdomain·clinic_slug 각각 대소문자 무시 유니크 - 형식: 소문자·숫자·붙임표, 2~40자, 붙임표로 시작·끝 금지, 연속 붙임표 금지 - 예약어 70여개 차단. 메일·인증(mx·dmarc·autodiscover)을 병원에 내주면 메일 수신과 도메인 소유 확인이 깨지고, 회수하는 시점에는 이미 색인된 주소가 깨진다 배정: 뷰 viewpl · 원진 wonjin · 오라클 oracle. 오라클은 이 레지스트리가 성형외과 73곳으로 만들어져 등록돼 있지 않아 함께 넣었다. 운영 DB 적용 중 확인한 것: 마이그레이션 이력에 10개 중 3개만 기록돼 있고 나머지는 SQL 편집기로 직접 만든 상태다. 그래서 db push 대신 이 파일만 따로 적용했다. 20260406_clinic_registry.sql 에 있는 verified_by·verified_at·is_active 는 실제 표에 없다. Co-Authored-By: Claude Opus 5 (1M context) --- .../migrations/20260914_clinic_subdomain.sql | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 supabase/migrations/20260914_clinic_subdomain.sql 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 + '서브도메인을 배정한 시각. 색인 허용 전환 전에 주소를 바꾸는 편이 싸므로 그 판단에 쓴다.';