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 dotenv import load_dotenv 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 # 텔레그램 서비스 (별도 모듈)에서 가져옴 from telegram_bot_service import run_polling as telegram_run_polling # ───────────────────────────────────────────────────────────── # .env 파일 로드 (환경변수 우선순위 보장) # ───────────────────────────────────────────────────────────── load_dotenv() # ───────────────────────────────────────────────────────────── # 템플릿/정적 경로를 파일 위치 기준으로 안전하게 설정 # structure: /backend/templates, /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) # ───────────────────────────────────────────────────────────── # 텔레그램 봇 폴링 서비스 (중복 실행 방지 포함) # ───────────────────────────────────────────────────────────── _bot_polling_started = False # 전역 플래그로 중복 방지 def start_telegram_bot_polling() -> None: """텔레그램 봇 폴링을 백그라운드 스레드로 시작 (한 번만 실행)""" import threading global _bot_polling_started if _bot_polling_started: app.logger.warning("🤖 텔레그램 봇 폴링은 이미 시작됨 - 중복 요청 무시") return _bot_polling_started = True def _runner(): try: # telegram_bot_service.run_polling(app) 호출 telegram_run_polling(app) except Exception as e: app.logger.error("텔레그램 봇 폴링 서비스 오류: %s", e) polling_thread = threading.Thread(target=_runner, daemon=True) polling_thread.start() app.logger.info("🤖 텔레그램 봇 폴링 스레드 생성됨 (중복 방지 플래그 적용)") # ───────────────────────────────────────────────────────────── # 텔레그램 봇 폴링 자동 시작 # Flask 앱이 초기화되면 자동으로 봇 폴링 시작 # ───────────────────────────────────────────────────────────── start_telegram_bot_polling() # ───────────────────────────────────────────────────────────── # 엔트리포인트 # ───────────────────────────────────────────────────────────── 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, use_reloader=False)