from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from contextlib import asynccontextmanager import os import asyncio from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.cron import CronTrigger from .database import engine, Base, SessionLocal from .api import cars, auth, inquiries, hero_banners, carmodoo, translations, cc, settings, vehicle_requests, dealer, vehicle_share, withdrawal, referral, notification, dashboard, push, exchange_rate, verification, visitor from .config import get_settings from .services.exchange_rate_service import update_exchange_rates from .services.visitor_service import aggregate_daily_stats, cleanup_old_visitor_logs from datetime import datetime, timedelta app_settings = get_settings() # Create tables Base.metadata.create_all(bind=engine) # APScheduler 설정 scheduler = AsyncIOScheduler() async def scheduled_update_exchange_rates(): """스케줄된 환율 업데이트 작업""" print("[Scheduler] Starting daily exchange rate update...") db = SessionLocal() try: result = await update_exchange_rates(db, force=True) print(f"[Scheduler] Exchange rate update completed: {result}") except Exception as e: print(f"[Scheduler] Exchange rate update failed: {e}") finally: db.close() async def scheduled_aggregate_visitor_stats(): """Aggregate yesterday's visitor stats""" print("[Scheduler] Aggregating visitor stats...") db = SessionLocal() try: yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") result = aggregate_daily_stats(db, yesterday) if result: print(f"[Scheduler] Visitor stats aggregated for {yesterday}") else: print(f"[Scheduler] No visitor data for {yesterday}") except Exception as e: print(f"[Scheduler] Visitor stats aggregation failed: {e}") finally: db.close() async def scheduled_cleanup_old_visitor_logs(): """Delete visitor logs older than 90 days""" print("[Scheduler] Cleaning up old visitor logs...") db = SessionLocal() try: deleted = cleanup_old_visitor_logs(db, days=90) print(f"[Scheduler] Deleted {deleted} old visitor logs") except Exception as e: print(f"[Scheduler] Visitor log cleanup failed: {e}") finally: db.close() @asynccontextmanager async def lifespan(app: FastAPI): """앱 시작/종료 시 실행되는 lifespan 이벤트""" # 시작 시 print("[Startup] Initializing scheduler...") # 환율 업데이트 스케줄 등록 (매일 오전 11시 30분 - 수출입은행 11시경 업데이트) scheduler.add_job( scheduled_update_exchange_rates, CronTrigger(hour=11, minute=30), id="daily_exchange_rate_update", name="Daily Exchange Rate Update", replace_existing=True ) # 방문자 통계 집계 (매일 새벽 2시) scheduler.add_job( scheduled_aggregate_visitor_stats, CronTrigger(hour=2, minute=0), id="daily_visitor_stats_aggregation", name="Daily Visitor Stats Aggregation", replace_existing=True ) # 오래된 방문자 로그 정리 (매주 일요일 새벽 3시) scheduler.add_job( scheduled_cleanup_old_visitor_logs, CronTrigger(day_of_week='sun', hour=3, minute=0), id="weekly_visitor_log_cleanup", name="Weekly Visitor Log Cleanup", replace_existing=True ) scheduler.start() print("[Startup] Scheduler started - Exchange rates: 11:30 AM, Visitor stats: 2:00 AM, Log cleanup: Sunday 3:00 AM") # 서버 시작 시 환율 데이터 초기화 (백그라운드에서) asyncio.create_task(scheduled_update_exchange_rates()) yield # 종료 시 print("[Shutdown] Stopping scheduler...") scheduler.shutdown() app = FastAPI( title="AutonetSellCar API", description="AutonetSellCar - Used Car Export Platform API", version="1.0.0", lifespan=lifespan ) # CORS - credentials=True requires explicit origins (not "*") app.add_middleware( CORSMiddleware, allow_origins=[ "http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:8000", "http://192.168.0.202:3000", # Local network ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Static files for uploads os.makedirs("./uploads/hero-banners", exist_ok=True) app.mount("/uploads", StaticFiles(directory="./uploads"), name="uploads") # Routes app.include_router(cars.router, prefix="/api") app.include_router(auth.router, prefix="/api") app.include_router(inquiries.router, prefix="/api") app.include_router(hero_banners.router, prefix="/api") app.include_router(carmodoo.router, prefix="/api") app.include_router(translations.router, prefix="/api") app.include_router(cc.router, prefix="/api") app.include_router(settings.router, prefix="/api") app.include_router(vehicle_requests.router, prefix="/api") app.include_router(dealer.router, prefix="/api") app.include_router(vehicle_share.router, prefix="/api") app.include_router(withdrawal.router, prefix="/api") app.include_router(referral.router, prefix="/api") app.include_router(notification.router, prefix="/api") app.include_router(dashboard.router, prefix="/api") app.include_router(push.router, prefix="/api") app.include_router(exchange_rate.router) app.include_router(verification.router, prefix="/api") app.include_router(visitor.router, prefix="/api") @app.get("/") def root(): return {"message": "AutonetSellCar API", "version": "1.0.0"} @app.get("/health") def health(): return {"status": "healthy"}