106 lines
4.4 KiB
Python
106 lines
4.4 KiB
Python
from __future__ import annotations
|
|
import os
|
|
import platform
|
|
from pathlib import Path
|
|
|
|
from flask import Flask
|
|
from flask_login import LoginManager
|
|
from flask_migrate import Migrate
|
|
from flask_socketio import SocketIO
|
|
from flask_wtf import CSRFProtect
|
|
|
|
from config import Config
|
|
from backend.models.user import db, load_user
|
|
from backend.routes import register_routes
|
|
from backend.services.logger import setup_logging
|
|
from backend.services import watchdog_handler
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# 템플릿/정적 경로를 파일 위치 기준으로 안전하게 설정
|
|
# structure: <project_root>/backend/templates, <project_root>/backend/static
|
|
BASE_DIR = Path(__file__).resolve().parent
|
|
TEMPLATE_DIR = (BASE_DIR / "backend" / "templates").resolve()
|
|
STATIC_DIR = (BASE_DIR / "backend" / "static").resolve()
|
|
|
|
# Flask 애플리케이션 생성
|
|
app = Flask(__name__, template_folder=str(TEMPLATE_DIR), static_folder=str(STATIC_DIR))
|
|
app.config.from_object(Config)
|
|
|
|
# 로그 설정
|
|
setup_logging(app)
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# CSRF 보호 + 템플릿에서 {{ csrf_token() }} 사용 가능하게 주입
|
|
csrf = CSRFProtect()
|
|
csrf.init_app(app)
|
|
|
|
@app.context_processor
|
|
def inject_csrf():
|
|
try:
|
|
from flask_wtf.csrf import generate_csrf
|
|
return dict(csrf_token=generate_csrf)
|
|
except Exception:
|
|
# Flask-WTF 미설치/에러 시에도 앱이 뜨도록 방어
|
|
return dict(csrf_token=lambda: "")
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# SocketIO: Windows 기본 threading, Linux는 eventlet 설치 시 eventlet 사용
|
|
# 환경변수 SOCKETIO_ASYNC_MODE 로 강제 지정 가능 ("threading"/"eventlet"/"gevent"/"auto")
|
|
async_mode = (app.config.get("SOCKETIO_ASYNC_MODE") or "threading").lower()
|
|
if async_mode == "auto":
|
|
async_mode = "threading"
|
|
|
|
if async_mode == "eventlet":
|
|
# Windows에선 eventlet 비권장, Linux에서만 시도
|
|
if platform.system() != "Windows":
|
|
try:
|
|
import eventlet # type: ignore
|
|
eventlet.monkey_patch()
|
|
except Exception:
|
|
async_mode = "threading" # 폴백
|
|
else:
|
|
async_mode = "threading"
|
|
|
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode=async_mode)
|
|
|
|
# watchdog에서 socketio 사용
|
|
watchdog_handler.socketio = socketio
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# DB / 마이그레이션
|
|
app.logger.info("DB URI = %s", app.config.get("SQLALCHEMY_DATABASE_URI"))
|
|
db.init_app(app)
|
|
Migrate(app, db)
|
|
|
|
# (선택) 개발 편의용: 테이블 자동 부트스트랩
|
|
# 환경변수 AUTO_BOOTSTRAP_DB=true 일 때만 동작 (운영에서는 flask db upgrade 사용 권장)
|
|
if (os.getenv("AUTO_BOOTSTRAP_DB", "false").lower() == "true"):
|
|
from sqlalchemy import inspect
|
|
with app.app_context():
|
|
insp = inspect(db.engine)
|
|
if "user" not in insp.get_table_names():
|
|
db.create_all()
|
|
app.logger.info("DB bootstrap: created tables via create_all()")
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Login
|
|
login_manager = LoginManager()
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = "auth.login"
|
|
|
|
@login_manager.user_loader
|
|
def _load_user(user_id: str):
|
|
return load_user(user_id)
|
|
|
|
# 라우트 등록 (Blueprints 등)
|
|
register_routes(app, socketio)
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# 엔트리포인트
|
|
if __name__ == "__main__":
|
|
host = os.getenv("FLASK_HOST", "0.0.0.0")
|
|
port = int(os.getenv("FLASK_PORT", 5000))
|
|
debug = os.getenv("FLASK_DEBUG", "true").lower() == "true"
|
|
socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
|