Files
invyone/frontend/lib/tenant/subdomain.ts
T
hjjeong 383b837a60 dev *.localhost 테넌트 라우팅 + direnv/Java 21 dev 환경 정비
SubdomainResolverFilter.extractSubdomain() 가 2파트 {sub}.localhost 호스트도
첫 파트로 파싱 (RFC 6761). 운영 3파트 경로 무변경. 단위 테스트 8건 추가.

frontend/lib/api/client.ts 에 *.localhost (bare 제외) 직접 호출 분기 1-b 추가.
8081 로 직결해 Host 헤더 보존. subdomain.ts 도 동일 규칙 적용.

application.yml CORS 디폴트에 http://*.localhost:[*] 패턴 추가.
docs/MULTI_TENANCY_ARCHITECTURE.md §4.2 (실행 모드 A/B) + §6 (1-b 분기) 갱신.
.gitignore 에 .envrc/.direnv 추가, .java-version=21 명시 (jenv 호환).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:52:30 +09:00

60 lines
1.6 KiB
TypeScript

/**
* 테넌트 서브도메인 헬퍼.
* 메인 사이트(solution, www, admin 등 예약어) 와 베이스 도메인은 null 을 리턴해서
* TenantGuard 가 체크를 스킵하게 한다.
*
* 백엔드 SubdomainResolverFilter.extractSubdomain() 와 동일한 규칙을 따라야 함:
* - bare localhost / IP / 베이스 도메인 → null (메타)
* - {sub}.localhost (dev) → 첫 파트 (예약어 제외)
* - {sub}.invyone.com (운영) → 첫 파트 (예약어 제외)
*
* 백엔드 provisioning 의 RESERVED_SUBDOMAINS 와 같은 값을 유지할 것.
*/
const RESERVED_MAIN = new Set([
"solution",
"www",
"admin",
"api",
"app",
"static",
"assets",
"main",
"mail",
"blog",
"test",
"staging",
"prod",
"console",
]);
const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
export function extractTenantSubdomain(host: string): string | null {
if (!host) return null;
const cleanHost = host.split(":")[0].toLowerCase();
if (!cleanHost) return null;
if (cleanHost === "localhost") return null;
if (IPV4.test(cleanHost)) return null;
const parts = cleanHost.split(".");
// 2파트 — "{sub}.localhost" 만 허용 (dev). invyone.com 같은 베이스 도메인은 null.
if (parts.length === 2) {
if (parts[1] !== "localhost") return null;
const first = parts[0];
if (!first) return null;
if (RESERVED_MAIN.has(first)) return null;
return first;
}
// 3파트 이상 (운영 *.invyone.com 등) — 첫 파트가 서브도메인
if (parts.length < 3) return null;
const first = parts[0];
if (!first) return null;
if (RESERVED_MAIN.has(first)) return null;
return first;
}