""" Todo-Project 메인 애플리케이션 - 간결함 원칙: 애플리케이션 설정 및 라우터 등록만 담당 """ from fastapi import FastAPI, Request, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles import logging import os from .core.config import settings from .api.routes import auth, todos, calendar from .core.database import AsyncSessionLocal from .models.todo import Todo from .models.user import User from datetime import datetime, timedelta from sqlalchemy import select # 로깅 설정 logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) logger = logging.getLogger(__name__) # FastAPI 앱 생성 app = FastAPI( title="Todo-Project API", description="간결한 Todo 관리 시스템 with 캘린더 연동", version="1.0.0", docs_url="/docs", redoc_url="/redoc" ) # CORS 설정 - 환경변수 처리 cors_origins = [] if settings.CORS_ORIGINS == "*": cors_origins = ["*"] else: cors_origins = [origin.strip() for origin in settings.CORS_ORIGINS.split(",")] logger.info(f"🌐 CORS Origins: {cors_origins}") app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Validation 오류 핸들러 @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): logger.error(f"Validation 오류 - URL: {request.url}") logger.error(f"Validation 오류 상세: {exc.errors()}") # JSON 직렬화 가능한 형태로 에러 변환 serializable_errors = [] for error in exc.errors(): serializable_error = {} for key, value in error.items(): if isinstance(value, bytes): serializable_error[key] = value.decode('utf-8') else: serializable_error[key] = str(value) if not isinstance(value, (str, int, float, bool, list, dict, type(None))) else value serializable_errors.append(serializable_error) return JSONResponse( status_code=422, content={ "detail": "요청 데이터 검증 실패", "errors": serializable_errors } ) # 정적 파일 서빙 (업로드된 이미지) UPLOAD_DIR = "/app/uploads" if not os.path.exists(UPLOAD_DIR): os.makedirs(UPLOAD_DIR) app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads") # 라우터 등록 from .api.routes import setup app.include_router(setup.router, prefix="/api/setup", tags=["setup"]) app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) app.include_router(todos.router, prefix="/api", tags=["todos"]) app.include_router(calendar.router, prefix="/api", tags=["calendar"]) @app.get("/") async def root(): """루트 엔드포인트""" return { "message": "Todo-Project API", "version": "1.0.0", "docs": "/docs" } @app.get("/health") async def health_check(): """헬스 체크""" return { "status": "healthy", "service": "todo-project", "version": "1.0.0" } async def create_sample_data(): """샘플 데이터 생성 - 비활성화됨""" # 더미 데이터 생성 완전히 비활성화 logger.info("샘플 데이터 생성이 비활성화되었습니다.") return async def wait_for_database(): """데이터베이스 연결 대기""" import asyncio import asyncpg from urllib.parse import urlparse # DATABASE_URL 파싱 parsed_url = urlparse(settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql://")) max_retries = 30 # 최대 30번 시도 (30초) retry_count = 0 while retry_count < max_retries: try: logger.info(f"🔄 데이터베이스 연결 시도 {retry_count + 1}/{max_retries}") # asyncpg로 직접 연결 테스트 conn = await asyncpg.connect( host=parsed_url.hostname, port=parsed_url.port or 5432, user=parsed_url.username, password=parsed_url.password, database=parsed_url.path.lstrip('/'), timeout=5 ) await conn.close() logger.info("✅ 데이터베이스 연결 성공!") return True except Exception as e: retry_count += 1 logger.warning(f"❌ 데이터베이스 연결 실패 ({retry_count}/{max_retries}): {e}") if retry_count < max_retries: await asyncio.sleep(1) else: logger.error("💥 데이터베이스 연결 최대 재시도 횟수 초과") raise Exception("데이터베이스에 연결할 수 없습니다.") @app.on_event("startup") async def startup_event(): """애플리케이션 시작 시 초기화""" logger.info("🚀 Todo-Project API 시작") logger.info(f"📊 환경: {settings.ENVIRONMENT}") logger.info(f"🔗 데이터베이스: {settings.DATABASE_URL}") # 데이터베이스 연결 대기 await wait_for_database() # 데이터베이스 초기화 from .core.database import init_db await init_db() # 샘플 데이터 생성 비활성화 logger.info("샘플 데이터 생성이 비활성화되었습니다.") # 애플리케이션 종료 시 실행 @app.on_event("shutdown") async def shutdown_event(): """애플리케이션 종료 시 정리""" logger.info("🛑 Todo-Project API 종료") # 캘린더 서비스 연결 정리 try: from .integrations.calendar import get_calendar_router calendar_router = get_calendar_router() await calendar_router.close_all() logger.info("📅 캘린더 서비스 연결 정리 완료") except Exception as e: logger.error(f"캘린더 서비스 정리 중 오류: {e}") if __name__ == "__main__": import uvicorn uvicorn.run( "main:app", host="0.0.0.0", port=settings.PORT, reload=settings.DEBUG )