# backend/app/services/settings_service.py # app_setting(영속) ↔ runtime_config(인메모리 오버레이) 동기화. from sqlmodel import Session, select from .. import runtime_config from ..models import AppSetting, now def _upsert(session: Session, key: str, value: str | None) -> None: """value=None/빈 → 행 삭제(기본으로 복귀). 그 외 → upsert.""" row = session.get(AppSetting, key) if value is None or (isinstance(value, str) and value.strip() == ""): if row: session.delete(row) return if row: row.value = str(value) row.updated_at = now() session.add(row) else: session.add(AppSetting(key=key, value=str(value))) def update_llm( session: Session, *, provider: str | None = None, model: str | None = None, host: str | None = None, timeout: float | None = None, api_key: str | None = None, ) -> None: """LLM 설정을 영속(app_setting)하고 런타임 오버레이에 즉시 반영. provider 는 ollama|openai|gemini. None 인 필드는 건드리지 않는다(부분 갱신). api_key 는 암호화 저장(평문 DB 금지) — 빈 문자열이면 삭제(키 제거).""" from ..crypto import encrypt_str fields: dict[str, object | None] = {} if provider is not None: if provider.strip() == "": fields["llm.provider"] = None # 빈 값 → 기본(env)으로 되돌리기 elif provider not in runtime_config._PROVIDERS: raise ValueError(f"provider must be one of {runtime_config._PROVIDERS}") else: fields["llm.provider"] = provider if model is not None: fields["llm.model"] = model if host is not None: fields["llm.host"] = host if timeout is not None: # timeout<=0 은 '기본으로 되돌리기' 신호(프론트 리셋용). fields["llm.timeout"] = None if timeout <= 0 else timeout if api_key is not None: # 빈 문자열 → 삭제(None), 그 외 → 암호화 후 저장. fields["llm.api_key"] = None if api_key.strip() == "" else encrypt_str(api_key) for key, val in fields.items(): _upsert(session, key, None if val is None else str(val)) session.commit() # 영속 후 DB → 오버레이 재적재(부분 삭제까지 일관되게 반영) runtime_config.load_from_db(session) def overlay_active(session: Session) -> bool: """LLM 오버레이 행이 하나라도 있으면 True.""" rows = session.exec(select(AppSetting)).all() return any(r.key in runtime_config.LLM_KEYS for r in rows)