- Frontend: Next.js 14 with TypeScript - Backend: FastAPI with SQLAlchemy - Agent: Carmodoo sync agent - Deployment: Docker Compose based staging/production setup - Scripts: Automated deployment with rollback support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
169 lines
5.6 KiB
Python
169 lines
5.6 KiB
Python
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"}
|