"""SiteOntology(o2o-site-ontology) 클라이언트 — 이 가게에 맞는 검색 키워드를 받아 온다.""" import httpx from common.logger import LOG from config.server_configs import external_api_config # 로컬 임베딩 모델이라 호출당 1~2초다. TIMEOUT_SEC = 15.0 DEFAULT_LIMIT = 40 PUBLISH_PATH = "/v1/merchants/publish" MATCH_PATH = "/v1/match" class SiteOntologyError(RuntimeError): """SiteOntology 호출 실패(네트워크·타임아웃·4xx/5xx·업체 미해석).""" def base_url() -> str: return (external_api_config.site_ontology_url or "").strip().rstrip("/") def is_configured() -> bool: return bool(base_url()) async def _post(client: httpx.AsyncClient, path: str, body: dict) -> dict: res = await client.post(f"{base_url()}{path}", json=body) if res.status_code >= 400: raise SiteOntologyError(f"{path} HTTP {res.status_code}: {res.text[:200]}") try: data = res.json() except ValueError as ex: raise SiteOntologyError(f"{path} 응답이 JSON 이 아니다: {res.text[:200]}") from ex if not isinstance(data, dict): raise SiteOntologyError(f"{path} 응답 모양이 다르다: {type(data).__name__}") return data async def match_for_merchant(merchant: dict, limit: int = DEFAULT_LIMIT) -> dict: """업체를 저장하고, 그 업체로 해석된 추천 결과(/v1/match 응답)를 돌려준다.""" body = {**merchant, "generate": False} try: async with httpx.AsyncClient(timeout=TIMEOUT_SEC) as client: try: await _post(client, PUBLISH_PATH, body) except SiteOntologyError as ex: if not body.get("regionId"): raise LOG.w(f"[site-ontology] regionId={body['regionId']} 로 저장하지 못했다 — 지역 없이 다시 보낸다: {ex}") body = {**body, "regionId": None} await _post(client, PUBLISH_PATH, body) result = await _post(client, MATCH_PATH, {"query": merchant["externalId"], "limit": limit}) except httpx.HTTPError as ex: raise SiteOntologyError(f"{type(ex).__name__}: {ex}") from ex resolved = result.get("resolved") or {} if resolved.get("externalId") != merchant["externalId"]: raise SiteOntologyError(f"업체가 해석되지 않았다 — externalId={merchant['externalId']}") return result