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 에 둔다.
86 lines
3.8 KiB
Python
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}: 공통 컬럼 누락"
|