import os from dotenv import load_dotenv from config.config_loader import Configs from config.config_models import WebServerConfig, LogConfig, MainDBConfig, JwtToken, ExternalApiConfig # 레포 최상위 .env 를 환경변수로 올린다 (외부 API 키·DB 접속의 배포용 주입 경로). # override=False: 이미 설정된 실제 환경변수(docker-compose 등)가 항상 이긴다. # 우선순위 = 실제 환경변수 > .env > config.{APP_ENV}.toml # # ★ APP_ENV=test 면 .env 를 읽지 않는다. 실키가 테스트 환경에 새어 들어가면 # 테스트가 실제 외부 API(Perplexity·Gemini·카카오)를 때리고 요금이 나간다. # 테스트의 외부 연동은 config.test.toml(전 키 빈 값) + MockAdapter 로만 검증한다. # config/ → backend/ → solution/ → 레포 루트. 백엔드가 solution/ 안으로 들어갈 때 # 이 계단을 안 늘려서 solution/.env 를 보고 있었다 — 도커는 compose 가 env 를 직접 # 주입해 안 드러나고, compose 밖(스크립트·로컬 실행)에서만 키가 조용히 안 들어왔다. _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) _DOTENV_PATH = os.path.join(_REPO_ROOT, ".env") if os.environ.get("APP_ENV") != "test": load_dotenv(_DOTENV_PATH, override=False) # 실행 환경 결정 (기본 local). 환경변수 APP_ENV 로 변경. APP_ENV = os.environ.get("APP_ENV", "local") _config_dir = os.path.dirname(__file__) _config_file = os.path.join(_config_dir, f"config.{APP_ENV}.toml") # 운영 전제: 항상 APP_ENV=local 로 띄운다 → config.local.toml 사용 (test/docker 도 local 로 실행). if not os.path.exists(_config_file): raise FileNotFoundError(f"설정 파일이 없습니다: {_config_file} (APP_ENV={APP_ENV}). APP_ENV=local 로 실행하세요.") configs = Configs(_config_file) web_server_config: WebServerConfig = configs.get(WebServerConfig) log_config: LogConfig = configs.get(LogConfig) main_db_config: MainDBConfig = configs.get(MainDBConfig) jwt_token_config: JwtToken = configs.get(JwtToken) # [ExternalApiConfig] 섹션이 없는 toml 에서도 죽지 않게 기본값 폴백(전 키 빈 값 → 해당 어댑터 비활성). external_api_config: ExternalApiConfig = configs.get(ExternalApiConfig) or ExternalApiConfig() # DB 접속 env override (config.local.toml 유지, 도커에서 host 만 교체). 로컬은 env 미설정 → toml 그대로. def _apply_db_env_override(cfg: MainDBConfig): h = os.environ.get("DB_HOST") if h: cfg.write_host = cfg.read_host = h if os.environ.get("DB_PORT"): cfg.write_port = cfg.read_port = int(os.environ["DB_PORT"]) if os.environ.get("DB_USER"): cfg.write_id = cfg.read_id = os.environ["DB_USER"] if os.environ.get("DB_PASSWORD"): cfg.write_pw = cfg.read_pw = os.environ["DB_PASSWORD"] if os.environ.get("DB_NAME"): cfg.name = os.environ["DB_NAME"] _apply_db_env_override(main_db_config) # 외부 API 키 env override — 키를 toml 에 안 두고 배포 환경변수로만 주입하는 경우. def _apply_external_api_env_override(cfg: ExternalApiConfig): if os.environ.get("PERPLEXITY_API_KEY"): cfg.perplexity_api_key = os.environ["PERPLEXITY_API_KEY"] if os.environ.get("KAKAO_REST_API_KEY"): cfg.kakao_rest_api_key = os.environ["KAKAO_REST_API_KEY"] if os.environ.get("NAVER_CLIENT_ID"): cfg.naver_client_id = os.environ["NAVER_CLIENT_ID"] if os.environ.get("NAVER_CLIENT_SECRET"): cfg.naver_client_secret = os.environ["NAVER_CLIENT_SECRET"] if os.environ.get("GEMINI_API_KEY"): cfg.gemini_api_key = os.environ["GEMINI_API_KEY"] if os.environ.get("GEMINI_VISION_MODEL"): cfg.gemini_vision_model = os.environ["GEMINI_VISION_MODEL"] if os.environ.get("GEMINI_TEXT_MODEL"): cfg.gemini_text_model = os.environ["GEMINI_TEXT_MODEL"] if os.environ.get("VISION_CONFIDENCE_THRESHOLD"): cfg.vision_confidence_threshold = float(os.environ["VISION_CONFIDENCE_THRESHOLD"]) if os.environ.get("TOUR_API_KEY"): cfg.tour_api_key = os.environ["TOUR_API_KEY"] _apply_external_api_env_override(external_api_config) # JWT 시크릿 env override. # ★ 도커 이미지는 config.local.toml 을 example 사본(플레이스홀더)으로 굽는다 — 시크릿을 안 굽기 위해서다. # 그래서 env 로 주입하지 않으면 "" 이라는 공개된 문자열이 서명 키가 된다. # 운영에서는 반드시 env 로 넣어야 한다. def _apply_jwt_env_override(cfg: JwtToken): if os.environ.get("JWT_ACCESS_SECRET"): cfg.access_key = os.environ["JWT_ACCESS_SECRET"] if os.environ.get("JWT_REFRESH_SECRET"): cfg.refresh_key = os.environ["JWT_REFRESH_SECRET"] if os.environ.get("JWT_ACCESS_EXPIRE_MIN"): cfg.access_expire_min = int(os.environ["JWT_ACCESS_EXPIRE_MIN"]) if os.environ.get("JWT_REFRESH_EXPIRE_DAY"): cfg.refresh_expire_day = int(os.environ["JWT_REFRESH_EXPIRE_DAY"]) _apply_jwt_env_override(jwt_token_config) # 플레이스홀더가 그대로 남아 있으면(=env 미주입 도커) 눈에 띄게 경고한다. # 부팅은 막지 않는다 — 로컬 개발에서 토큰이 안 필요한 작업까지 못 하게 되면 곤란하다. if jwt_token_config.access_key.startswith("<") or jwt_token_config.refresh_key.startswith("<"): from common.logger import LOG LOG.w( "[보안] JWT 시크릿이 플레이스홀더입니다 — JWT_ACCESS_SECRET / JWT_REFRESH_SECRET 를 env 로 주입하세요. " "이 상태로 운영에 띄우면 서명 키가 공개값입니다." )