Files
Aislo/main.py
T

209 lines
8.6 KiB
Python

"""
main.py
FastAPI 애플리케이션 진입점
- 공통 설정 로드 (config_system, config_db)
- 프론트엔드 자동 빌드 및 서빙 (npm run build)
- 정적 파일 서빙 (프론트엔드 빌드 결과)
- DB 풀 초기화/종료
- 라우터 등록 (페이지별 API)
"""
import os
import sys
import logging
import subprocess
import time
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
# 설정 import
from config.config_system import (
SERVER_HOST,
SERVER_PORT,
DEBUG,
STATIC_DIR,
STATIC_URL,
LOG_LEVEL,
CORS_ORIGINS,
ENVIRONMENT,
)
from config.config_db import init_db_pool, close_db_pool
# 로깅 설정
logging.basicConfig(level=getattr(logging, LOG_LEVEL))
logger = logging.getLogger(__name__)
# ─────────────────────────────────────────────────────────────────────────
# 프론트엔드 빌드 및 서빙 함수
# ─────────────────────────────────────────────────────────────────────────
def build_frontend() -> bool:
"""프론트엔드 빌드 (npm run build from config/)"""
root_dir = Path(__file__).resolve().parent
config_dir = root_dir / "config"
if not config_dir.exists():
logger.warning(f"⚠ config 디렉토리 없음: {config_dir}")
return False
logger.info("[Frontend] npm run build 시작...")
try:
result = subprocess.run(
"npm run build",
shell=True,
cwd=str(config_dir),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=300,
)
if result.returncode == 0:
logger.info("✓ 프론트엔드 빌드 완료")
return True
else:
logger.error(f"✗ 프론트엔드 빌드 실패:\n{result.stderr}")
return False
except subprocess.TimeoutExpired:
logger.error("✗ 프론트엔드 빌드 타임아웃 (300초)")
return False
except Exception as e:
logger.error(f"✗ 프론트엔드 빌드 중 예외: {e}")
return False
def serve_frontend_dev() -> None:
"""프론트엔드 개발 서버 실행 (npm run dev from config/) — 선택 사항"""
root_dir = Path(__file__).resolve().parent
config_dir = root_dir / "config"
if not config_dir.exists():
logger.warning(f"⚠ config 디렉토리 없음, 개발 서버 스킵: {config_dir}")
return
if ENVIRONMENT == "production":
logger.info("[Frontend] 프로덕션 환경: 개발 서버 실행 스킵")
return
logger.info("[Frontend] npm run dev 시작 (백그라운드)...")
try:
subprocess.Popen(
"npm run dev",
shell=True,
cwd=str(config_dir),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
logger.info("✓ 프론트엔드 개발 서버 백그라운드 실행")
except Exception as e:
logger.warning(f"⚠ 프론트엔드 개발 서버 실행 실패: {e}")
# ─────────────────────────────────────────────────────────────────────────
# 라이프사이클: 앱 시작/종료
# ─────────────────────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
"""FastAPI 라이프사이클 관리"""
# 시작
logger.info(f"[{ENVIRONMENT}] 앱 시작 중...")
# 프론트엔드 빌드 (필수)
build_frontend()
# 개발 환경에서 개발 서버 실행 (선택사항)
if ENVIRONMENT == "development":
serve_frontend_dev()
# DB 풀 초기화
await init_db_pool()
logger.info("✓ DB 풀 초기화 완료")
yield
# 종료
logger.info("앱 종료 중...")
await close_db_pool()
logger.info("✓ DB 풀 종료 완료")
# ─────────────────────────────────────────────────────────────────────────
# FastAPI 앱 생성
# ─────────────────────────────────────────────────────────────────────────
app = FastAPI(
title="임도 설계 및 견적 자동화",
description="Forest Road Design & Estimation API",
version="0.1.0",
debug=DEBUG,
lifespan=lifespan,
)
# ─────────────────────────────────────────────────────────────────────────
# CORS 미들웨어
# ─────────────────────────────────────────────────────────────────────────
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ─────────────────────────────────────────────────────────────────────────
# 정적 파일 서빙 (프론트엔드)
# ─────────────────────────────────────────────────────────────────────────
if os.path.isdir(STATIC_DIR):
app.mount(STATIC_URL, StaticFiles(directory=STATIC_DIR), name="static")
logger.info(f"✓ 정적 파일 서빙: {STATIC_URL}{STATIC_DIR}")
else:
logger.warning(f"⚠ 정적 파일 디렉토리 없음: {STATIC_DIR}")
# ─────────────────────────────────────────────────────────────────────────
# 기본 엔드포인트
# ─────────────────────────────────────────────────────────────────────────
@app.get("/")
async def root():
"""루트 엔드포인트 — 프론트엔드 진입점 리다이렉트"""
return {
"message": "임도 설계 및 견적 자동화 API",
"version": "0.1.0",
"frontend": f"http://{SERVER_HOST}:{SERVER_PORT}{STATIC_URL}/",
}
@app.get("/api/health")
async def health():
"""헬스 체크"""
return {"status": "ok", "environment": ENVIRONMENT}
# ─────────────────────────────────────────────────────────────────────────
# 라우터 등록 (pages → API)
# ─────────────────────────────────────────────────────────────────────────
# 예: from A01_Home.A01_Home_Api_Fetch import router as a01_router
# app.include_router(a01_router, prefix="/api/a01", tags=["A01_Home"])
#
# 나중에 각 페이지별 라우터가 구현되면 여기에 등록.
# ─────────────────────────────────────────────────────────────────────────
# 앱 실행
# ─────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import uvicorn
logger.info(f"Starting server on {SERVER_HOST}:{SERVER_PORT}")
uvicorn.run(
"main:app",
host=SERVER_HOST,
port=SERVER_PORT,
reload=DEBUG,
)