o2o-site-AEO/solution/backend/services/site_slug.py
Mina Choi 11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00

68 lines
3.5 KiB
Python

"""사이트 주소(네임스페이스) 규칙 — 확인(check)과 저장(POST)이 함께 쓰는 단 하나의 정의."""
import re
from typing import Optional
# 3~50자.
SLUG_PATTERN = re.compile(r"^[a-z0-9](?:[a-z0-9-]{1,48}[a-z0-9])$")
SLUG_MIN_LEN = 3
SLUG_MAX_LEN = 50
# 불가 사유 코드.
REASON_LENGTH = "INVALID_LENGTH" # 3~50자를 벗어남
REASON_FORMAT = "INVALID_FORMAT" # 한글·대문자·언더스코어·연속 하이픈·양끝 하이픈
REASON_RESERVED = "RESERVED" # 서비스가 선점한 이름
REASON_TAKEN = "TAKEN" # 다른 사업장이 이미 쓰는 주소
REASON_LOCKED = "LOCKED" # 이미 발행됨 — 색인된 주소는 바꾸지 않는다
# 예약어.
RESERVED_SLUGS = frozenset({
# 서비스 경로 / 인프라 서브도메인
"s", "site", "sites", "api", "app", "admin", "www", "www2", "static", "assets", "cdn", "media",
"img", "images", "css", "js", "files", "download", "downloads", "public", "dev", "stage", "staging",
"test", "demo", "local", "localhost", "mail", "smtp", "imap", "pop", "ftp", "ns", "ns1", "ns2", "mx",
"webmail", "vpn", "proxy", "gateway", "internal", "console", "dashboard", "manage", "management",
# 서버가 실제로 서비스하는 엔드포인트
"health", "healthz", "healthcheck", "status", "metrics", "robots", "sitemap", "llms", "favicon",
"manifest", "feed", "rss", "opensearch", "well-known", "v1", "v2", "graphql", "openapi", "docs", "swagger",
# 계정 / 정책 페이지
"login", "logout", "signin", "signup", "register", "join", "auth", "oauth", "account", "accounts",
"user", "users", "me", "my", "owner", "root", "system", "support", "help", "contact", "about",
"terms", "privacy", "policy", "legal", "security", "billing", "payment", "pay", "order", "orders",
# 도메인 리소스 이름 — 관리 API 경로와 같아 헷갈린다
"place", "places", "fact", "facts", "faq", "faqs", "job", "jobs", "report", "reports", "unit", "units",
"search", "index", "home", "main", "new", "edit", "delete", "create", "update",
# 값이 비었을 때 프론트가 문자열로 흘려보내는 것들 — 주소로 들어오면 버그의 흔적이다
"null", "undefined", "none", "nan", "true", "false",
# 손으로 만든 목업이 쓰는 주소(solution/site/out/s, mockup/README).
"stay", "stay2", "stay3", "stay4", "stay5", "stay6",
})
def validate_slug(slug: Optional[str]) -> Optional[str]:
"""형식·예약어만 본다(중복은 DB 를 봐야 하므로 서비스가 판단한다)."""
value = (slug or "").strip()
if len(value) < SLUG_MIN_LEN or len(value) > SLUG_MAX_LEN:
return REASON_LENGTH
# 하이픈 연속은 정규식으로 막지 않는다(가운데 문자 집합이라 통과한다) — 여기서 따로 거른다.
if "--" in value:
return REASON_FORMAT
if not SLUG_PATTERN.fullmatch(value):
return REASON_FORMAT
if value in RESERVED_SLUGS:
return REASON_RESERVED
return None
def suggestions(slug: Optional[str], count: int = 9) -> list[str]:
"""`-2`, `-3` … 을 붙인 대안 후보."""
value = (slug or "").strip()
result: list[str] = []
for n in range(2, 2 + max(count, 0)):
suffix = f"-{n}"
# 50자 상한을 넘기지 않도록 앞을 자른다.
base = value[: SLUG_MAX_LEN - len(suffix)].rstrip("-")
candidate = f"{base}{suffix}"
if validate_slug(candidate) is None:
result.append(candidate)
return result