o2o-site-AEO/solution/backend/tests/test_category_schema.py
Mina Choi 5ef3e5a7de 업종 4번째를 관광체험 → 피부과·성형외과 로 바꾸고, 로그인 관문을 에디터 진입으로 되돌린다
## 업종 교체 (tour → clinic)

PlaceCategory 코드 4번의 의미를 바꾼다. 아직 배포 전이라 데이터 마이그레이션은 없다.

- category_schema: tour_activity.json → clinic.json. 체험 스키마(안전 유의사항·우천 시
  운영·준비물)를 진료 스키마(진료과목·의료진·상담료·보험 적용·야간/주말진료)로 바꿨다.
  unit 은 프로그램 → 시술이다(마취 방식·회복 기간·권장 횟수·시술 후 주의사항).
- 소개문 계열만 allow_llm 이다. 시술 효과·비용 같은 값은 LLM 이 못 쓴다 —
  이 레포의 "검증 전에는 발행 금지" 규칙이 의료 문구에서 특히 중요하다.
- jsonld: TouristAttraction → MedicalClinic. 프론트 AeoReadiness 의 같은 표도 맞췄다.
- 색 팔레트를 병원 톤(클린 블루·세이지·누드·모노)으로, 아이콘을 Compass → Stethoscope 로.
- mock_adapter 목데이터를 시술 기준으로 교체. 스키마에 없는 key 를 쓰면 수집이 죽는다.
- site_payload 의 기본 섹션표를 에디터(industryData)와 같게 맞췄다 —
  test_site_theme 이 이 둘을 대조한다.

## 로그인 관문 되돌리기 (b94daa9·d6a6c8e revert)

두 커밋이 /builder 를 통째로 RequireAuth 뒤로 옮겨 `/` 가 곧바로 로그인 화면이 됐다.
`/` 는 자기 화면 없이 /builder 로 넘기기만 하므로, 문 앞 가드는 곧 루트 가드다.
위저드를 열어 두고 에디터 진입에서 한 번 받는 969fb67 설계로 되돌린다.
d6a6c8e 가 스스로 "969fb67 과 정면으로 다른 설계"라고 적어 두었다.

## 그 밖

- test_site_theme 의 경로가 solution/front 로 남아 있었다(frontend 개명 누락).
- .dockerignore: 이 머신에 buildx 가 없어 레거시 빌더가 돌고, 그러면
  nginx/Dockerfile.dockerignore 가 무시된다. 루트 것 하나로 두 이미지를 다 커버한다.

검증: frontend·admin·site lint·build 0. 백엔드 534 passed / 4 failed —
그 4개(test_build_publish 3 · test_snapshot 1)는 이 변경 전부터 실패하던 것이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xa8ME5FQJy4VA8pPokTo1a
2026-09-02 15:42:34 +09:00

79 lines
3.9 KiB
Python

"""업종 스키마 — 4개 업종 파일이 규약대로 로드되는지.
업종 추가 = resources/ 에 JSON 1개 + PlaceCategory 코드 1줄. 이 테스트는 그 규약이 깨지면 잡는다.
"""
import pytest
from common.category_schema import CategorySchemaError, all_schemas, get_schema, is_valid_key
from common.enums import PlaceCategory
def test_all_categories_have_schema():
"""검증: PlaceCategory 의 모든 업종에 스키마 파일이 있는지.
기대결과: 4개 업종(숙박·카페·음식점·피부과·성형외과)이 모두 로드되고 code 가 1:1로 맞는다."""
schemas = all_schemas()
assert set(schemas) == {c.value for c in PlaceCategory}
for code, schema in schemas.items():
assert schema.code == code
assert schema.category == PlaceCategory(code)
assert schema.fields, f"{schema.name}: 필드가 비었다"
def test_keys_unique_within_category():
"""검증: 업종 안에서 fact key 가 유일한지.
기대결과: 중복 없음 (facts 의 (place,unit,key) 유니크가 의미를 가지려면 필수)."""
for schema in all_schemas().values():
keys = list(schema.fields)
assert len(keys) == len(set(keys)), f"{schema.name}: key 중복"
def test_place_and_unit_scopes_exist():
"""검증: 업종마다 place 스코프 필드가 있는지.
기대결과: 전 업종에 place 필드 존재. unit 필드는 업종별로 있을 수도 없을 수도 있다."""
for schema in all_schemas().values():
assert schema.keys_by_scope("place"), f"{schema.name}: place 스코프 필드가 없다"
def test_lodging_has_claim_critical_fields():
"""검증: 숙박 업종에 예약 클레임 직결 항목이 critical 로 잡혀 있는지.
기대결과: 체크인·체크아웃·취사·반려동물·취소규정이 모두 critical=True 이고 LLM 이 못 쓴다."""
schema = get_schema(PlaceCategory.LODGING)
for key in ("check_in_time", "check_out_time", "cooking_allowed", "pet_allowed", "cancel_policy"):
spec = schema.get(key)
assert spec is not None, f"숙박 스키마에 {key} 가 없다"
assert spec.critical is True, f"{key} 는 critical 이어야 한다"
assert spec.allow_llm is False, f"{key} 는 LLM 이 값을 만들면 안 된다"
def test_llm_writable_fields_are_sentences_only():
"""검증: LLM 이 값을 만들 수 있는 필드가 '문장' 필드뿐인지 (절대규칙 7 — LLM 은 사실을 만들지 않는다).
기대결과: allow_llm=True 인 필드는 전부 type=text 이고 critical 이 아니다."""
for schema in all_schemas().values():
for key in schema.llm_writable_keys():
spec = schema.get(key)
assert spec.type == "text", f"{schema.name}.{key}: LLM 이 쓸 수 있는 필드는 text 여야 한다"
assert spec.critical is False, f"{schema.name}.{key}: critical 필드를 LLM 이 쓰면 안 된다"
def test_required_fields_are_never_llm_written():
"""검증: 발행 필수 항목을 LLM 이 채우지 못하는지.
기대결과: required=True 인 필드는 전부 allow_llm=False."""
for schema in all_schemas().values():
for key in schema.required_keys():
assert schema.get(key).allow_llm is False, f"{schema.name}.{key}: 필수 항목을 LLM 이 채우면 안 된다"
def test_is_valid_key_rejects_cross_category_key():
"""검증: 업종에 없는 key 를 걸러내는지 (facts 쓰기 전 FACT_INVALID_KEY 판정).
기대결과: 숙박의 check_in_time 은 카페 스키마에서 거부된다."""
assert is_valid_key(PlaceCategory.LODGING, "check_in_time") is True
assert is_valid_key(PlaceCategory.CAFE, "check_in_time") is False
assert is_valid_key(PlaceCategory.CAFE, "break_time") is True
def test_unknown_category_raises():
"""검증: 지원하지 않는 업종 코드 조회.
기대결과: CategorySchemaError."""
with pytest.raises(CategorySchemaError):
get_schema(99)