## 업종 교체 (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
83 lines
2.5 KiB
Python
83 lines
2.5 KiB
Python
"""Prompt contract for grounded homepage copy."""
|
|
|
|
from typing import Optional, Protocol, Sequence
|
|
|
|
from common.enums import PlaceCategory
|
|
|
|
|
|
class FactLike(Protocol):
|
|
key: str
|
|
label: str
|
|
value: str
|
|
unit: Optional[str]
|
|
|
|
|
|
RESPONSE_SCHEMA = {
|
|
"type": "object",
|
|
"properties": {
|
|
"intro": {"type": "string"},
|
|
"intro_fact_keys": {"type": "array", "items": {"type": "string"}},
|
|
"meta_description": {"type": "string"},
|
|
"faqs": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"question": {"type": "string"},
|
|
"answer": {"type": "string"},
|
|
"fact_keys": {"type": "array", "items": {"type": "string"}},
|
|
},
|
|
"required": ["question", "answer", "fact_keys"],
|
|
},
|
|
},
|
|
},
|
|
"required": ["intro", "intro_fact_keys", "meta_description", "faqs"],
|
|
}
|
|
|
|
_CATEGORY_LABEL = {
|
|
PlaceCategory.LODGING: "숙박업소",
|
|
PlaceCategory.CAFE: "카페",
|
|
PlaceCategory.RESTAURANT: "음식점",
|
|
PlaceCategory.CLINIC: "관광·체험 시설",
|
|
}
|
|
|
|
|
|
def _fact_lines(facts: Sequence[FactLike]) -> str:
|
|
return "\n".join(
|
|
f"- {fact.key} ({fact.label}) = {fact.value}" + (f" {fact.unit}" if fact.unit else "")
|
|
for fact in facts
|
|
)
|
|
|
|
|
|
def build_prompt(
|
|
place_name: str,
|
|
category: PlaceCategory,
|
|
facts: Sequence[FactLike],
|
|
max_faqs: int,
|
|
unit_facts: Optional[Sequence[FactLike]] = None,
|
|
) -> str:
|
|
sections = [
|
|
f"'{place_name}'({_CATEGORY_LABEL.get(category, '사업장')})의 공식 홈페이지 문구를 작성한다.",
|
|
"",
|
|
"확인된 사업장 사실:",
|
|
_fact_lines(facts) if facts else "- 없음",
|
|
]
|
|
if unit_facts:
|
|
sections.extend(["", "확인된 객실·메뉴 사실:", _fact_lines(unit_facts)])
|
|
sections.extend([
|
|
"",
|
|
"출력:",
|
|
"- intro: 소개문 100~250자",
|
|
"- intro_fact_keys: 소개문의 근거 key",
|
|
"- meta_description: 검색 요약 50~120자",
|
|
f"- faqs: 최대 {max_faqs}개, 각 항목에 근거 fact_keys 포함",
|
|
"",
|
|
"규칙:",
|
|
"- 위 사실에 없는 숫자·시설·지역 정보를 지어내지 마라.",
|
|
"- false·불가·없음 값을 가능하다고 표현하지 않는다.",
|
|
"- 홍보성·평가성 표현을 쓰지 않는다.",
|
|
"- 근거 없는 FAQ는 만들지 않는다.",
|
|
"- 한국어 존댓말을 사용한다.",
|
|
])
|
|
return "\n".join(sections)
|