diff --git a/backend/app/main.py b/backend/app/main.py index 8905471..2ac0fc7 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,10 +1,12 @@ from __future__ import annotations import logging +import os from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text from app.api.chart import router as chart_router from app.api.metrics import router as metrics_router @@ -13,7 +15,7 @@ from app.api.predict import router as predict_router from app.api.refresh import router as refresh_router from app.api.symbols import router as symbols_router from app.config import settings -from app.db.connection import ping as db_ping +from app.db.connection import get_engine, ping as db_ping from app.fetch import dart as dart_mod from app.fetch import kis as kis_mod from app.pipelines.scheduler import shutdown_scheduler, start_scheduler @@ -25,13 +27,55 @@ logging.basicConfig( logger = logging.getLogger(__name__) +def _bootstrap_db() -> None: + """첫 부팅 자동화: + 1) migrations/*.sql idempotent 적용 (timescale/pgvector 확장 + 스키마) + 2) symbols 테이블 비어있으면 pykrx 로 전 종목 시드 (SEED 10 마크 포함) + + BOOTSTRAP_DISABLED=1 이면 스킵 (테스트/CI 용). 어떤 단계든 실패해도 서버는 + 뜬다 — /health/db 가 진단을 알려준다. + """ + if os.environ.get("BOOTSTRAP_DISABLED") == "1": + logger.info("bootstrap skipped (BOOTSTRAP_DISABLED=1)") + return + + # 1) migrations + try: + from app.db.migrate import apply_all + res = apply_all() + logger.info("bootstrap migrate: %s", res) + except Exception: # noqa: BLE001 + logger.exception("bootstrap migrate failed") + return # 스키마 없으면 시드 불가 + + # 2) symbols 시드 (비어있을 때만 — pykrx 호출이 비싸므로 항상 돌리지 않음) + try: + eng = get_engine() + with eng.connect() as conn: + row = conn.execute(text("SELECT COUNT(*) FROM symbols")).first() + count = int(row[0]) if row else 0 + if count == 0: + logger.info("symbols empty — running initial seed") + from app.fetch.symbols_seed import seed_symbols + report = seed_symbols() + logger.info("bootstrap seed_symbols: %s", report) + else: + logger.info("symbols already populated (count=%d) — skip seed", count) + except Exception: # noqa: BLE001 + logger.exception("bootstrap seed_symbols failed") + + @asynccontextmanager async def lifespan(_: FastAPI): + _bootstrap_db() # 스케줄러는 옵션. CI/테스트에서 disable 하고 싶으면 SCHEDULER_DISABLED 같은 env 추가 가능. - try: - start_scheduler() - except Exception: # noqa: BLE001 - logger.exception("scheduler start failed") + if os.environ.get("SCHEDULER_DISABLED") == "1": + logger.info("scheduler skipped (SCHEDULER_DISABLED=1)") + else: + try: + start_scheduler() + except Exception: # noqa: BLE001 + logger.exception("scheduler start failed") yield shutdown_scheduler()