Files
Todo-Project/backend/src/main.py
Hyungi Ahn 2ccdf8c411 🌐 Fix CORS configuration for Synology deployment
- Add CORS_ORIGINS environment variable support in config.py
- Update main.py to properly parse CORS_ORIGINS (support both wildcard * and comma-separated URLs)
- Enable wildcard CORS for production deployment
- Fix Preflight CORS errors in browser

Resolves: CORS 400 Bad Request errors during API calls
Tested: Successfully deployed and working on Synology NAS
2025-09-24 12:25:02 +09:00

203 lines
6.2 KiB
Python

"""
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
)