o2o-site-AEO/solution/backend/services/publish_gate.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

84 lines
3.2 KiB
Python

"""발행 검수 게이트 — 사이트가 나가기 전에 반드시 통과해야 하는 검사."""
from dataclasses import dataclass, field
from common.category_schema import get_schema
from common.enums import PUBLISHABLE_FACT_STATUSES, FactStatus, PlaceCategory, PublishRejectReason
@dataclass
class GateResult:
"""검수 결과."""
passed: bool
reason: PublishRejectReason | None = None
detail: dict = field(default_factory=dict)
def as_log(self) -> dict:
return {"reason": self.reason.name if self.reason else None, **self.detail}
def check_facts_verified(facts: list) -> GateResult:
"""규칙 1 — 노출 대상 fact 가 전부 검증됐는가."""
bad = [
{"key": f["key"] if isinstance(f, dict) else f.key,
"status": FactStatus(f["status"] if isinstance(f, dict) else f.status).name}
for f in facts
if FactStatus(f["status"] if isinstance(f, dict) else f.status) not in PUBLISHABLE_FACT_STATUSES
]
if bad:
return GateResult(False, PublishRejectReason.UNVERIFIED_FACT, {"unverified": bad[:20], "count": len(bad)})
return GateResult(True)
def check_required_fields(category: PlaceCategory, facts: list) -> GateResult:
"""업종 스키마의 required 필드가 다 있는가."""
schema = get_schema(category)
required = set(schema.required_keys("place"))
have = {
(f["key"] if isinstance(f, dict) else f.key)
for f in facts
if str(f["value"] if isinstance(f, dict) else f.value or "").strip()
}
missing = sorted(required - have)
if missing:
return GateResult(
False, PublishRejectReason.REQUIRED_FACT_MISSING,
{"missing": missing, "labels": [schema.get(k).label for k in missing if schema.get(k)]},
)
return GateResult(True)
def check_unique_content(unique_content_count: int | None) -> GateResult:
"""렌더러가 '고유 콘텐츠 0건' 으로 거부했을 때 사유 코드를 붙이는 자리."""
if unique_content_count is None:
return GateResult(True)
if unique_content_count <= 0:
return GateResult(False, PublishRejectReason.NO_UNIQUE_CONTENT, {"unique_content_count": unique_content_count})
return GateResult(True)
def check_jsonld_matches(mismatches: list) -> GateResult:
"""규칙 3 — 구조화 데이터 값이 화면 값과 같은가."""
if mismatches:
return GateResult(False, PublishRejectReason.JSONLD_MISMATCH, {"mismatches": list(mismatches)[:20]})
return GateResult(True)
def evaluate(
category: PlaceCategory, facts: list, unique_content_count: int | None, mismatches: list
) -> GateResult:
"""게이트 전체."""
for result in (
check_facts_verified(facts),
check_jsonld_matches(mismatches),
):
if not result.passed:
return result
# 통과했지만 비어 있는 필수 항목은 경고로 실어 보낸다(발행은 진행된다).
required = check_required_fields(category, facts)
if not required.passed:
return GateResult(True, None, {"warning": PublishRejectReason.REQUIRED_FACT_MISSING.name,
**(required.detail or {})})
return GateResult(True)