o2o-site-AEO/solution/backend/services/copy_service.py
Mina Choi ef0c44c5f2 [feat] solution/backend: 수집 뒤 콘텐츠 채울 때 캐치프레이즈·순환 문구 생성 — 숙박만
가게별 첫 화면 문구를 만드는 곳이 없어 모든 숙박이 공통 문구 10개를 돌려 썼다.

- COPY 잡에 catchphrase 단계(prepare→generate→save→catchphrase→faq_fill), 숙박이 아니면 건너뜀
- prompts/catchphrase.py·gemini_text.generate_catchphrases: 캐치프레이즈 1 + 일반 20·계절 3×4·월 12·날씨 2×4.
  문구마다 길이·상호명·ground_check(근거 없는 시설·수치) 검사
- fact tagline·catchphrases(lodging 스키마, allow_llm). 순환 문구는 사장님이 줄 단위로 고치게 글로 저장
  (catchphrase_text: `봄 | ` · `9월 | ` · `비 | ` 머리)
- site_payload: narrative.tagline·catchphrases 로 넘기고 이용정보 표(facts)에서는 뺀다
- 빌더 생성 화면 단계 문구, DATA_MODEL·GENERATION_FLOW

테스트 6건 추가. 관련 20개 파일 386 passed · 실패 102건은 main 과 목록이 같다(기존 실패)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:33:17 +09:00

64 lines
2.7 KiB
Python

"""COPY 흐름."""
from common.enums import PlaceCategory
from common.logger import LOG
from services.copy_steps import (
CopyAborted, fill_faqs, generate_catchphrases, generate_copy, prepare_copy, save_catchphrases, save_copy,
)
from services.external import gemini_text
from services.llm import provider
from services.llm.errors import LlmError
from services.job_progress import JobProgress
COPY_STEPS = ("prepare", "generate", "save", "catchphrase", "faq_fill")
async def run_copy(job: dict) -> dict:
progress = JobProgress(job, COPY_STEPS)
payload = job["payload"]
async with progress.step("prepare"):
inputs = await prepare_copy(payload["place_id"], payload["owner_user_id"])
copy = None
note = None
if inputs.ungrounded or not gemini_text.is_configured():
note = "근거로 쓸 확인된 fact 가 없다" if inputs.ungrounded else f"{provider.missing_key()} 미설정"
await progress.skip("generate", "no_facts" if inputs.ungrounded else "not_configured")
if inputs.catalog is None and not inputs.ungrounded:
raise CopyAborted(note)
else:
try:
async with progress.step("generate"):
copy = await generate_copy(inputs)
except gemini_text.GeminiError as ex:
# 호출 실패는 미설정과 같은 취급이다 — fact 만으로도 편집·발행이 되고 (publish_gate.check_unique_content), 발행은 고유 콘텐츠 0건으로 막지 않는다.
note = f"생성 호출 실패: {ex}"
LOG.w(f"[copy] 생성 실패, fact 만으로 계속: {ex}")
await progress.skip("generate", "generation_failed")
async with progress.step("save"):
result = await save_copy(inputs, copy)
if inputs.place.category != PlaceCategory.LODGING.value:
await progress.skip("catchphrase", "not_lodging")
elif copy is None:
await progress.skip("catchphrase", "no_facts" if inputs.ungrounded else "not_configured")
else:
try:
async with progress.step("catchphrase"):
result["catchphrase"] = await save_catchphrases(inputs, await generate_catchphrases(inputs))
except LlmError as ex:
LOG.w(f"[copy] 첫 화면 문구 생성 실패, 공통 문구로 계속: {ex}")
await progress.skip("catchphrase", "generation_failed")
if inputs.catalog is not None:
async with progress.step("faq_fill"):
result["faq_fill"] = await fill_faqs(
inputs.place.place_id, inputs.catalog, inputs.known_fact_keys, inputs.place.phone,
)
else:
await progress.skip("faq_fill", "no_catalog")
if note:
result["note"] = note
return result