"""사용자 입력·교열 결과 기록 — Supabase(PostgREST) 에 직접 REST 호출. 옛 gradio 데모(`services/prj-chosun-gradio/blindtest/db.py`)의 `articles` · `pipeline_runs` 두 테이블을 그대로 쓴다(스키마는 `schema.sql`). SDK 대신 httpx 로 REST 를 부르는 것도 그때와 같다 — `supabase` 패키지가 끌고 오는 `realtime`/`websockets` 핀이 다른 의존성과 충돌했던 전력 때문. 환경 변수: SUPABASE_URL — 프로젝트 URL (https://xxx.supabase.co) SUPABASE_KEY (또는 SUPABASE_ANON_KEY) — anon 키 (RLS 정책은 schema.sql) 둘 중 하나라도 없으면 모든 쓰기는 no-op 이고 `is_configured()` 가 False 다. 기록 실패는 절대 데모를 멈추지 않는다 — `last_error()` 에 한 줄 남기고 None 을 돌려준다. """ from __future__ import annotations import os import httpx TIMEOUT_S = 10.0 _last_error: str | None = None def _url() -> str: return os.environ.get("SUPABASE_URL", "").strip().rstrip("/") def _key() -> str: # 옛 gradio 데모는 SUPABASE_KEY, 최근 .env 는 SUPABASE_ANON_KEY 를 쓴다 -- 둘 다 받는다 return (os.environ.get("SUPABASE_KEY") or os.environ.get("SUPABASE_ANON_KEY") or "").strip() def is_configured() -> bool: return bool(_url() and _key()) def last_error() -> str | None: return _last_error def _post(table: str, row: dict) -> list[dict]: headers = { "apikey": _key(), "Authorization": f"Bearer {_key()}", "Content-Type": "application/json", "Prefer": "return=representation", } with httpx.Client(timeout=TIMEOUT_S) as client: resp = client.post(f"{_url()}/rest/v1/{table}", headers=headers, json=row) if resp.status_code >= 300: raise RuntimeError(f"{resp.status_code} {resp.text[:200]}") return resp.json() if resp.content else [] def _insert(table: str, row: dict) -> int | None: global _last_error if not is_configured(): return None try: data = _post(table, row) _last_error = None return data[0]["id"] if data else None except Exception as exc: # noqa: BLE001 — 기록은 데모의 부수 기능, 어떤 실패도 삼켜서 한 줄만 남긴다 _last_error = f"{table}: {type(exc).__name__}: {exc}" return None def save_article(source_text: str) -> int | None: """사용자 입력 원문 1건. 교열 실행 **전**에 부른다 — 실행이 실패해도 입력은 남게.""" return _insert("articles", {"source_text": source_text}) def save_run( article_id: int | None, *, pipeline_key: str, prompt_key: str, model: str, output: str, processing_time_s: float, ) -> int | None: """교열 결과 1건. `article_id` 가 없으면(입력 기록 실패) 결과도 기록하지 않는다.""" if article_id is None: return None return _insert( "pipeline_runs", { "article_id": article_id, "pipeline_key": pipeline_key, "prompt_key": prompt_key, "model": model, "output": output, "processing_time_s": round(processing_time_s, 2), }, )