"""환경설정 (env -> 타입드 Settings). 마스터플랜 §1.1 원칙: 엔진/DB/음성 엔드포인트는 전부 env 주입(이미지에 굽지 않음 = 이식성). Docker Compose secrets/.env 로 주입, 코드에 하드코딩 금지. """ from __future__ import annotations from functools import lru_cache from typing import Literal from urllib.parse import urlsplit from pydantic import Field, SecretStr, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from .contracts.engine_gateway import EngineProvider # 엔진 어댑터 provider 플래그 (마스터플랜 §0, R1: claude -p 과금누수 회피) # claude_api = Anthropic Messages API 직결 (기본) # claude_cli = 로컬 claude -p 상주풀 (stream-json, 옵션/시연용) # codex_cli = 로컬 Codex CLI (app-server 모델 탐색 + exec 생성) # agy_cli = 로컬 Agy/Google Antigravity CLI # openai = OpenAI 호환 (폴백/평가 보조) # solar = 국내 모델 라우팅 (PII 민감구간 inference_geo:kr) EngineMode = EngineProvider def _is_local_url(value: str) -> bool: parsed = urlsplit(value) host = (parsed.hostname or "").lower() return host in {"localhost", "127.0.0.1", "::1"} def _is_allowed_local_dev_cors_origin(value: str) -> bool: parsed = urlsplit(value) host = (parsed.hostname or "").lower() return ( parsed.scheme == "http" and host in {"localhost", "127.0.0.1"} and parsed.port in range(5170, 5181) and not parsed.path and not parsed.query and not parsed.fragment ) def _is_local_or_forbidden_non_dev_origin(value: str) -> bool: return _is_local_url(value) and not _is_allowed_local_dev_cors_origin(value) class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", case_sensitive=False, populate_by_name=True, ) # ── 앱 ─────────────────────────────────────────────── app_name: str = "vignette-api" environment: Literal["dev", "staging", "prod"] = "dev" debug: bool = False # ── DB (NAS PostgreSQL 16 + pgvector, 단일 SoR) ───── # 예: postgresql://user:pass@nas:5432/vignette database_url: str = Field( default="postgresql://user:pass@localhost:5432/vignette", validation_alias="DATABASE_URL", ) db_pool_min_size: int = 2 db_pool_max_size: int = 10 db_command_timeout: float = 30.0 # ── 엔진 게이트웨이 (람다 소유, 이 앱은 HTTP 호출만) ── # engine_gateway/ 서비스 베이스 URL. 게이트웨이가 provider 라우팅을 흡수. engine_url: str = Field( default="http://engine:8100", validation_alias="ENGINE_URL", ) engine_gateway_shared_secret: SecretStr = Field( default=SecretStr(""), validation_alias="ENGINE_GATEWAY_SHARED_SECRET", ) engine_mode: EngineMode = Field( default="claude_api", validation_alias="ENGINE_MODE", ) # 실시간 내담자 응답은 평가/저작 provider와 분리할 수 있다. Agy/Codex 같은 # 범용 agent CLI의 도구 prompt와 프로세스 기동 지연을 상담 왕복에 강제하지 않는다. live_client_provider: EngineMode | None = Field( default=None, validation_alias="VIGNETTE_LIVE_CLIENT_PROVIDER", ) engine_timeout: float = 120.0 # SSE 롱리브드 (50분 상담 대비, 스트림은 무제한 별도) engine_connect_timeout: float = 10.0 admin_usage_budget_usd: float = Field( default=0.0, validation_alias="ADMIN_USAGE_BUDGET_USD", ) evaluator_fast_model: str = Field( default="", validation_alias="EVALUATOR_FAST_MODEL", ) evaluator_deep_model: str = Field( default="", validation_alias="EVALUATOR_DEEP_MODEL", ) evaluator_semantic_cache_enabled: bool = Field( default=True, validation_alias="EVALUATOR_SEMANTIC_CACHE_ENABLED", ) evaluator_semantic_cache_ttl_seconds: int = Field( default=900, validation_alias="EVALUATOR_SEMANTIC_CACHE_TTL_SECONDS", ) evaluator_semantic_cache_max_entries: int = Field( default=256, validation_alias="EVALUATOR_SEMANTIC_CACHE_MAX_ENTRIES", ) session_evaluation_timeout: float = Field( default=0.0, validation_alias="SESSION_EVALUATION_TIMEOUT", ) session_evaluation_recovery_limit: int = Field( default=5, validation_alias="SESSION_EVALUATION_RECOVERY_LIMIT", ) session_digest_worker_enabled: bool = Field( default=False, validation_alias="SESSION_DIGEST_WORKER_ENABLED", ) session_digest_worker_model: str = Field( default="", validation_alias="SESSION_DIGEST_WORKER_MODEL", ) # ── 외부 LLM 키 (게이트웨이가 못 받을 때 직접 폴백, PII 마스킹 후만) ── anthropic_api_key: str = Field(default="", validation_alias="ANTHROPIC_API_KEY") openai_api_key: str = Field(default="", validation_alias="OPENAI_API_KEY") openai_base_url: str = Field( default="https://api.openai.com/v1", validation_alias="OPENAI_BASE_URL", ) # `local_whisper`는 노트북 상주 faster-whisper 사이드카(scripts/local-whisper-stt-server.py)를 # 쓴다. 오디오가 호스트를 벗어나지 않고 외부 STT 키가 필요 없다. voice_stt_provider: Literal["openai", "deepgram", "local_whisper"] = Field( default="openai", validation_alias="VIGNETTE_VOICE_STT_PROVIDER", ) local_whisper_stt_url: str = Field( default="ws://127.0.0.1:9882/v1/listen", validation_alias="VIGNETTE_LOCAL_WHISPER_STT_URL", ) local_whisper_stt_model: str = Field( default="small", validation_alias="VIGNETTE_LOCAL_WHISPER_STT_MODEL", ) local_whisper_stt_language: str = Field( default="ko", validation_alias="VIGNETTE_LOCAL_WHISPER_STT_LANGUAGE", ) local_whisper_endpointing_ms: int = Field( default=300, ge=10, le=5000, validation_alias="VIGNETTE_LOCAL_WHISPER_ENDPOINTING_MS", ) local_whisper_utterance_end_ms: int = Field( default=1200, ge=1000, le=10000, validation_alias="VIGNETTE_LOCAL_WHISPER_UTTERANCE_END_MS", ) local_whisper_finalize_timeout_seconds: float = Field( default=20.0, ge=1.0, le=120.0, validation_alias="VIGNETTE_LOCAL_WHISPER_FINALIZE_TIMEOUT_SECONDS", ) deepgram_api_key: SecretStr = Field( default=SecretStr(""), validation_alias="DEEPGRAM_API_KEY", ) deepgram_stt_url: str = Field( default="wss://api.deepgram.com/v1/listen", validation_alias="DEEPGRAM_STT_URL", ) deepgram_stt_model: str = Field( default="nova-3", validation_alias="DEEPGRAM_STT_MODEL", ) deepgram_stt_language: str = Field( default="ko", validation_alias="DEEPGRAM_STT_LANGUAGE", ) deepgram_endpointing_ms: int = Field( default=300, ge=10, le=5000, validation_alias="DEEPGRAM_ENDPOINTING_MS", ) deepgram_utterance_end_ms: int = Field( default=1200, ge=1000, le=10000, validation_alias="DEEPGRAM_UTTERANCE_END_MS", ) deepgram_keepalive_seconds: float = Field( default=4.0, ge=1.0, le=8.0, validation_alias="DEEPGRAM_KEEPALIVE_SECONDS", ) deepgram_finalize_timeout_seconds: float = Field( default=15.0, ge=1.0, le=60.0, validation_alias="DEEPGRAM_FINALIZE_TIMEOUT_SECONDS", ) deepgram_mip_opt_out: bool = Field( default=True, validation_alias="DEEPGRAM_MIP_OPT_OUT", ) voice_poc_sample_tts_enabled: bool = Field( default=False, validation_alias="VIGNETTE_VOICE_POC_SAMPLE_TTS", ) voice_poc_sample_tts_dir: str = Field( default="", validation_alias="VIGNETTE_VOICE_POC_SAMPLE_TTS_DIR", ) # `melotts`는 MIT 라이선스 사전학습 한국어 다화자 모델이라 상업 사용 제약도, # 실존 인물 reference 문제도 없다. Higgs 와 달리 환경·프리셋 제한이 필요 없다. melotts_tts_url: str = Field( default="http://127.0.0.1:9883", validation_alias="VIGNETTE_MELOTTS_TTS_URL", ) melotts_tts_timeout_seconds: float = Field( default=120.0, ge=1.0, le=600.0, validation_alias="VIGNETTE_MELOTTS_TTS_TIMEOUT_SECONDS", ) voice_tts_provider: Literal["openai", "higgs", "melotts"] = Field( default="openai", validation_alias="VIGNETTE_VOICE_TTS_PROVIDER", ) higgs_tts_url: str = Field( default="http://127.0.0.1:9881", validation_alias="VIGNETTE_HIGGS_TTS_URL", ) higgs_tts_timeout_seconds: float = Field( default=300.0, validation_alias="VIGNETTE_HIGGS_TTS_TIMEOUT_SECONDS", ) # ── 세션/인증 (BFF OAuth 2.1, 토큰 서버 보관) ──────── session_secret: str = Field( default="dev-insecure-change-me", validation_alias="SESSION_SECRET", ) # G3 evaluator ingestion is a machine-to-machine boundary. An empty value # does not fall back to a development secret; the endpoints fail closed. rupture_internal_token: SecretStr = Field( default=SecretStr(""), validation_alias="VIGNETTE_RUPTURE_INTERNAL_TOKEN", ) # G4 evaluator prescription ingestion is independently revocable from G3. # Missing or short configuration disables the endpoint instead of falling # back to a shared development secret. practice_internal_token: SecretStr = Field( default=SecretStr(""), validation_alias="VIGNETTE_PRACTICE_INTERNAL_TOKEN", ) # G5 evaluator calibration/transfer ingestion has its own independently # revocable credential and never receives an implicit development fallback. calibration_transfer_internal_token: SecretStr = Field( default=SecretStr(""), validation_alias="VIGNETTE_CALIBRATION_TRANSFER_INTERNAL_TOKEN", ) # G6 supervision/research ingestion separates supervisor and research AI # views behind one independently revocable machine credential. supervision_research_internal_token: SecretStr = Field( default=SecretStr(""), validation_alias="VIGNETTE_SUPERVISION_RESEARCH_INTERNAL_TOKEN", ) supervision_research_producer_enabled: bool = Field( default=True, validation_alias="VIGNETTE_SUPERVISION_RESEARCH_PRODUCER_ENABLED", ) supervision_research_producer_interval_seconds: float = Field( default=3600.0, ge=60.0, validation_alias="VIGNETTE_SUPERVISION_RESEARCH_PRODUCER_INTERVAL_SECONDS", ) supervision_research_producer_startup_delay_seconds: float = Field( default=15.0, ge=0.0, le=300.0, validation_alias="VIGNETTE_SUPERVISION_RESEARCH_PRODUCER_STARTUP_DELAY_SECONDS", ) # G8 automation qualification is research-only and independently revocable; # no development token is synthesized when the setting is absent. continuous_improvement_internal_token: SecretStr = Field( default=SecretStr(""), validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_INTERNAL_TOKEN", ) # G8 scheduled generation is opt-in because each queued item performs # multiple model calls. Schema readiness is checked separately in lifespan. continuous_improvement_producer_enabled: bool = Field( default=False, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENABLED", ) continuous_improvement_producer_interval_seconds: float = Field( default=3600.0, ge=30.0, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_INTERVAL_SECONDS", ) continuous_improvement_producer_startup_delay_seconds: float = Field( default=30.0, ge=0.0, le=300.0, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_STARTUP_DELAY_SECONDS", ) continuous_improvement_producer_retry_delay_seconds: float = Field( default=300.0, ge=30.0, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_RETRY_DELAY_SECONDS", ) continuous_improvement_producer_lease_timeout_seconds: float = Field( default=1800.0, ge=60.0, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_LEASE_TIMEOUT_SECONDS", ) continuous_improvement_producer_engine_timeout_seconds: float = Field( default=300.0, ge=60.0, le=600.0, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_ENGINE_TIMEOUT_SECONDS", ) continuous_improvement_producer_batch_size: int = Field( default=1, ge=1, le=10, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_PRODUCER_BATCH_SIZE", ) # Operational measurement drift is a separate fail-closed ingestion gate. # Enabling the producer alone must not start consuming benchmark ledgers. continuous_improvement_drift_trigger_enabled: bool = Field( default=False, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_DRIFT_TRIGGER_ENABLED", ) # Human authorization and rollback execution are separate boundaries. NAS # deployments must opt in explicitly and point at an authenticated control plane. continuous_improvement_rollback_executor_enabled: bool = Field( default=False, validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_ENABLED", ) continuous_improvement_rollback_executor_endpoint: str = Field( default="", validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_ENDPOINT", ) continuous_improvement_rollback_executor_token: SecretStr = Field( default=SecretStr(""), validation_alias="VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_TOKEN", ) continuous_improvement_rollback_executor_timeout_seconds: float = Field( default=30.0, ge=2.0, le=120.0, validation_alias=( "VIGNETTE_CONTINUOUS_IMPROVEMENT_ROLLBACK_EXECUTOR_TIMEOUT_SECONDS" ), ) # G7 consent-bound evaluator ingestion is isolated from every other # machine credential and fails closed when unconfigured. multimodal_alliance_internal_token: SecretStr = Field( default=SecretStr(""), validation_alias="VIGNETTE_MULTIMODAL_ALLIANCE_INTERNAL_TOKEN", ) # __Host- 쿠키 정책: prod 에선 secure=True 강제 cookie_name: str = "__Host-vignette_sid" session_ttl_seconds: int = 60 * 60 * 8 # 8h # Google OIDC (1차, 한신대 SSO 는 2차 — R11) oauth_google_client_id: str = Field(default="", validation_alias="OAUTH_GOOGLE_CLIENT_ID") oauth_google_client_secret: str = Field( default="", validation_alias="OAUTH_GOOGLE_CLIENT_SECRET" ) oauth_redirect_uri: str = Field( default="https://api-vignette.chanpaca.net/auth/callback", validation_alias="OAUTH_REDIRECT_URI", ) # 로컬 dev-login과 SAML 조직 정책용 allowlist다. Google OIDC는 provider가 # 검증한 이메일이면 도메인·사전등록 여부와 무관하게 로그인시킨다. auth_allowed_email_domains: list[str] = Field( default=["hs.ac.kr", "twentyoz.kr"], validation_alias="AUTH_ALLOWED_EMAIL_DOMAINS", ) auth_teacher_emails: list[str] = Field( default=[], validation_alias="AUTH_TEACHER_EMAILS", ) auth_admin_emails: list[str] = Field( default=[], validation_alias="AUTH_ADMIN_EMAILS", ) auth_super_admin_emails: list[str] = Field( default=["yunchan@twentyoz.kr", "hoonjungkoo@hs.ac.kr"], validation_alias="AUTH_SUPER_ADMIN_EMAILS", ) auth_approved_emails: list[str] = Field( default=[], validation_alias="AUTH_APPROVED_EMAILS", ) auth_new_user_default_status: Literal["pending", "approved"] = Field( default="pending", validation_alias="AUTH_NEW_USER_DEFAULT_STATUS", ) auth_email_cohort_map: dict[str, str] = Field( default_factory=dict, validation_alias="AUTH_EMAIL_COHORT_MAP", ) auth_domain_cohort_map: dict[str, str] = Field( default_factory=dict, validation_alias="AUTH_DOMAIN_COHORT_MAP", ) auth_dev_login_enabled: bool = Field( default=False, validation_alias="AUTH_DEV_LOGIN_ENABLED", ) auth_saml_enabled: bool = Field( default=False, validation_alias="AUTH_SAML_ENABLED", ) saml_sp_entity_id: str = Field( default="", validation_alias="SAML_SP_ENTITY_ID", ) saml_sso_url: str = Field( default="", validation_alias="SAML_SSO_URL", ) saml_x509_cert_fingerprint: str = Field( default="", validation_alias="SAML_X509_CERT_FINGERPRINT", ) default_affiliation: str = Field( default="", validation_alias="DEFAULT_AFFILIATION", ) frontend_base_url: str = Field( default="http://localhost:5173", validation_alias="FRONTEND_BASE_URL", ) frontend_origin_map: dict[str, str] = Field( default_factory=lambda: { "api-vignette.chanpaca.net": "https://vignette.chanpaca.net", "api-vnet.18ka.net": "https://vnet.18ka.net", }, validation_alias="FRONTEND_ORIGIN_MAP", ) user_upload_dir: str = Field( default="uploads", validation_alias="USER_UPLOAD_DIR", ) # ── 운영 메일 알림 ───────────────────────────────────── notification_email_provider: Literal["disabled", "smtp"] = Field( default="disabled", validation_alias="NOTIFICATION_EMAIL_PROVIDER", ) notification_email_max_attempts: int = Field( default=3, validation_alias="NOTIFICATION_EMAIL_MAX_ATTEMPTS", ) notification_email_retry_seconds: int = Field( default=900, validation_alias="NOTIFICATION_EMAIL_RETRY_SECONDS", ) smtp_host: str = Field(default="", validation_alias="SMTP_HOST") smtp_port: int = Field(default=587, validation_alias="SMTP_PORT") smtp_username: str = Field(default="", validation_alias="SMTP_USERNAME") smtp_password: str = Field(default="", validation_alias="SMTP_PASSWORD") smtp_from_email: str = Field(default="", validation_alias="SMTP_FROM_EMAIL") smtp_from_name: str = Field(default="Vignette", validation_alias="SMTP_FROM_NAME") smtp_starttls: bool = Field(default=True, validation_alias="SMTP_STARTTLS") smtp_ssl: bool = Field(default=False, validation_alias="SMTP_SSL") # ── CORS (정적 프론트 + SSE 분리경로) ──────────────── cors_origins: list[str] = Field( default=[ "https://vignette.chanpaca.net", "https://vnet.18ka.net", "https://vignette-b1q.pages.dev", ], validation_alias="CORS_ORIGINS", ) auth_dev_login_extra_origins: list[str] = Field( default=[], validation_alias="AUTH_DEV_LOGIN_EXTRA_ORIGINS", ) # Built-in personas are developer/bootstrap fixtures, not runtime truth. # Production should use approved rows from app.persona_card only. auto_seed_personas: bool = Field( default=False, validation_alias="AUTO_SEED_PERSONAS", ) allow_seed_persona_fallback: bool = Field( default=False, validation_alias="ALLOW_SEED_PERSONA_FALLBACK", ) # ── 회기 시간 제한 (2026-07-13 한신대 회의: 시간 기반 종료 전환) ── # 임상 근거: 한 회기에 4단계가 모두 이뤄지지 않는 게 정상 — 종료는 단계 완수가 아니라 # 시간이 결정한다. 목표 달성 후에도 시간 내에는 계속 진행할 수 있다. session_duration_minutes: int = Field( default=60, validation_alias="SESSION_DURATION_MINUTES", ) session_warning_minutes: int = Field( default=10, validation_alias="SESSION_WARNING_MINUTES", ) # 시간 만료 후에도 마무리 인사를 나눌 수 있는 유예. 유예까지 지나면 새 턴을 거부한다. session_overtime_grace_minutes: int = Field( default=10, validation_alias="SESSION_OVERTIME_GRACE_MINUTES", ) # ── SSE 스트리밍 ───────────────────────────────────── sse_heartbeat_seconds: int = 30 # Cloudflare 100초 timeout 회피 (R2) @property def is_prod(self) -> bool: return self.environment == "prod" @model_validator(mode="after") def validate_non_dev_runtime_flags(self) -> "Settings": gateway_secret = self.engine_gateway_shared_secret.get_secret_value().strip() if gateway_secret and ( len(gateway_secret) < 32 or gateway_secret.lower().startswith( ("change-me", "replace-with", "dummy", "example") ) ): raise ValueError( "ENGINE_GATEWAY_SHARED_SECRET must be a non-placeholder value " "containing at least 32 characters" ) if self.continuous_improvement_rollback_executor_enabled: endpoint = self.continuous_improvement_rollback_executor_endpoint.strip() parsed_endpoint = urlsplit(endpoint) rollback_token = ( self.continuous_improvement_rollback_executor_token.get_secret_value().strip() ) local_dev_http = ( self.environment == "dev" and parsed_endpoint.scheme == "http" and _is_local_url(endpoint) ) if not endpoint or not parsed_endpoint.hostname: raise ValueError( "rollback executor endpoint is required when executor is enabled" ) if parsed_endpoint.scheme != "https" and not local_dev_http: raise ValueError( "rollback executor endpoint must use HTTPS outside local development" ) if ( parsed_endpoint.username or parsed_endpoint.password or parsed_endpoint.query or parsed_endpoint.fragment ): raise ValueError( "rollback executor endpoint cannot contain credentials, query, or fragment" ) if len(rollback_token) < 32 or rollback_token.lower().startswith( ("change-me", "replace-with", "dummy", "example") ): raise ValueError( "rollback executor token must be a non-placeholder value " "containing at least 32 characters" ) if self.environment != "dev": forbidden: list[str] = [] if self.auth_dev_login_enabled: forbidden.append("AUTH_DEV_LOGIN_ENABLED") if self.auto_seed_personas: forbidden.append("AUTO_SEED_PERSONAS") if self.allow_seed_persona_fallback: forbidden.append("ALLOW_SEED_PERSONA_FALLBACK") if self.voice_poc_sample_tts_enabled: forbidden.append("VIGNETTE_VOICE_POC_SAMPLE_TTS") if self.voice_tts_provider == "higgs": # Higgs Audio v3 TTS 4B는 연구/비상업 라이선스이므로 로컬 dev에서만 쓴다. forbidden.append("VIGNETTE_VOICE_TTS_PROVIDER=higgs") if not self.oauth_google_client_id.strip(): forbidden.append("OAUTH_GOOGLE_CLIENT_ID") if not self.oauth_google_client_secret.strip(): forbidden.append("OAUTH_GOOGLE_CLIENT_SECRET") if self.session_secret == "dev-insecure-change-me": forbidden.append("SESSION_SECRET") if _is_local_url(self.frontend_base_url): forbidden.append("FRONTEND_BASE_URL") if any(_is_local_or_forbidden_non_dev_origin(origin) for origin in self.cors_origins): forbidden.append("CORS_ORIGINS") if any( _is_local_or_forbidden_non_dev_origin(origin) for origin in self.frontend_origin_map.values() ): forbidden.append("FRONTEND_ORIGIN_MAP") if self.notification_email_provider == "smtp": if not self.smtp_host.strip(): forbidden.append("SMTP_HOST") if not self.smtp_from_email.strip(): forbidden.append("SMTP_FROM_EMAIL") if forbidden: joined = ", ".join(forbidden) raise ValueError(f"{joined} must be production-safe when ENVIRONMENT={self.environment}") if self.smtp_ssl and self.smtp_starttls: raise ValueError("SMTP_SSL and SMTP_STARTTLS cannot both be true") if self.auth_saml_enabled: missing_saml: list[str] = [] if not self.saml_sp_entity_id.strip(): missing_saml.append("SAML_SP_ENTITY_ID") if not self.saml_sso_url.strip(): missing_saml.append("SAML_SSO_URL") if missing_saml: joined = ", ".join(missing_saml) raise ValueError(f"{joined} must be configured when AUTH_SAML_ENABLED=true") return self @lru_cache def get_settings() -> Settings: """프로세스 1회 로드 (lru_cache). 의존성 주입은 deps.get_settings_dep 사용.""" return Settings() settings = get_settings()