- 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
203 lines
6.2 KiB
Python
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
|
|
)
|