o2o-site-AEO/backend/tests/test_schema_ddl.py
Mina Choi 6784e59ca5 최초 커밋 — 기존 코드 전체 + 문서 체계 신설
git 저장소가 없어 히스토리·협업 기반이 아예 없던 상태를 연다.
함께 문서를 재편했다. 그동안 문서가 있어도 "이 제품이 뭘 푸는가"와
"어떻게 도는가"를 담은 문서가 없어서, 목표 문장이 backend/frontend
README 두 곳에 복붙돼 있었다 — 상위 문서가 없어 아래로 샌 것이다.

신설
  README.md               레포 진입점 + 문서 지도 + 문서 규칙 4가지
  AGENTS.md               에이전트·신규 합류자용 함정 목록과 규약
                          (CLAUDE.md 는 여기로 걸린 심볼릭 링크)
  docs/PRODUCT.md         제품 정의 — 문제·사용자·원칙·**non-goals**·성공 기준
  docs/ARCHITECTURE.md    payload 경계·발행 파이프라인·서빙 결정·앱 분리 설계

이동
  backend/docs/DECISIONS.md → docs/DECISIONS.md
    백엔드만의 결정이 아니다. 게다가 코드 주석 ~25곳이 이미
    `docs/DECISIONS.md` 로 적고 있어 레포 루트 기준으로는 그게 맞다.

갱신
  docs/DEPLOY.md          서빙 결정 반영 — nginx 정적 서빙이 지금 경로(3절),
                          Azure 는 나중에 켤 때(4절)로 분리
  docs/ARCHITECTURE.md    사이트 = 한 장(2026-08-31) 구조 반영
  docs/COLLECTION_SEO_AEO_FLOW.md
                          robots.txt·sitemap.xml 은 오리진 루트에만 굽는다는 점 명시
  frontend/site/scripts/prerender.ts
                          헤더 주석의 렌더 보고서 경로가 실제(422줄)와 달라 수정

.gitignore
  ★ CLAUDE.md 를 더 이상 무시하지 않는다. 에이전트 지침은 팀과 모든
    에이전트가 공유하는 규약이라 커밋해야 한다 — 무시하면 클론한 사람이
    "배포 후 republish_all.py 필수" 같은 함정을 전달받지 못한다.
    개인용 오버라이드는 ~/.claude/CLAUDE.md 에 둔다.
2026-08-31 13:57:59 +09:00

86 lines
3.8 KiB
Python

"""init.sql ↔ ORM 모델 정합성.
스키마 정의가 두 곳(마이그레이션 SQL / SQLAlchemy 모델)에 있으므로 둘이 어긋나면
"테스트는 통과하는데 실서버에서 컬럼이 없는" 상황이 난다. DB 없이 파일만 비교해 그걸 잡는다.
"""
import re
from pathlib import Path
from common.database.model.models import MAIN_BASE
_INIT_SQL = Path(__file__).resolve().parents[2] / "postgres-init" / "init-data" / "init.sql"
_CREATE_TABLE_RE = re.compile(
r"CREATE TABLE IF NOT EXISTS\s+(?P<schema>\w+)\.(?P<table>\w+)\s*\((?P<body>.*?)\n\);",
re.S | re.I,
)
def _parse_init_sql() -> dict:
"""init.sql → {"schema.table": {컬럼명, ...}}"""
sql = _INIT_SQL.read_text(encoding="utf-8")
out = {}
for m in _CREATE_TABLE_RE.finditer(sql):
columns = set()
for line in m.group("body").splitlines():
line = line.strip()
if not line or line.startswith("--"):
continue
name = line.split()[0]
if name.upper() in ("PRIMARY", "UNIQUE", "CONSTRAINT", "FOREIGN", "CHECK"):
continue
columns.add(name)
out[f"{m.group('schema')}.{m.group('table')}"] = columns
return out
def test_init_sql_is_readable():
"""검증: init.sql 을 찾고 파싱할 수 있는지.
기대결과: 파일이 존재하고 CREATE TABLE 이 1개 이상 파싱된다."""
assert _INIT_SQL.exists(), f"init.sql 이 없다: {_INIT_SQL}"
assert _parse_init_sql(), "init.sql 에서 CREATE TABLE 을 하나도 파싱하지 못했다"
def test_every_model_table_exists_in_init_sql():
"""검증: ORM 모델의 모든 테이블이 init.sql 에도 있는지.
기대결과: 누락 없음 — 모델만 추가하고 마이그레이션을 안 쓴 경우를 잡는다."""
sql_tables = set(_parse_init_sql())
model_tables = {f"{t.schema}.{t.name}" for t in MAIN_BASE.metadata.sorted_tables}
missing = sorted(model_tables - sql_tables)
assert not missing, f"init.sql 에 없는 모델 테이블: {missing}"
def test_every_init_sql_table_has_a_model():
"""검증: init.sql 의 모든 테이블에 ORM 모델이 있는지.
기대결과: 누락 없음 — 마이그레이션만 쓰고 모델을 안 만든 경우를 잡는다."""
sql_tables = set(_parse_init_sql())
model_tables = {f"{t.schema}.{t.name}" for t in MAIN_BASE.metadata.sorted_tables}
missing = sorted(sql_tables - model_tables)
assert not missing, f"ORM 모델이 없는 init.sql 테이블: {missing}"
def test_columns_match_between_model_and_init_sql():
"""검증: 테이블마다 컬럼 집합이 양쪽에서 같은지.
기대결과: 완전 일치 — 한쪽에만 추가된 컬럼을 잡는다."""
sql_tables = _parse_init_sql()
problems = []
for table in MAIN_BASE.metadata.sorted_tables:
name = f"{table.schema}.{table.name}"
if name not in sql_tables:
continue
model_cols = {c.name for c in table.columns}
sql_cols = sql_tables[name]
if model_cols != sql_cols:
problems.append(
f"{name}: 모델에만 {sorted(model_cols - sql_cols)} / init.sql 에만 {sorted(sql_cols - model_cols)}"
)
assert not problems, "컬럼 불일치:\n" + "\n".join(problems)
def test_every_table_has_soft_delete_columns():
"""검증: 모든 테이블이 공통 컬럼(created_at·updated_at·deleted)을 갖는지.
기대결과: MainTableMixin 을 빠뜨린 모델이 없다 — 소프트 삭제 전제가 깨지면 유니크 부분 인덱스도 깨진다."""
for table in MAIN_BASE.metadata.sorted_tables:
cols = {c.name for c in table.columns}
assert {"created_at", "updated_at", "deleted"} <= cols, f"{table.schema}.{table.name}: 공통 컬럼 누락"