Files
AutonetSellCar/backend/app/main.py
AutonetSellCar Deploy e661d91c72 fix: banner translations and deployment improvements
- Add translateCarName import from i18n.ts for proper multilingual support
- Change default API language from 'ko' to 'en' for hero banners
- Add checkbox column for Local Cars banner registration
- Update Dockerfile with Playwright dependencies
- Add PostgreSQL migration script
- Add banner translation fix script

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-31 10:41:42 +09:00

212 lines
7.1 KiB
Python

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import RedirectResponse
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()
class TrailingSlashMiddleware(BaseHTTPMiddleware):
"""
Middleware to normalize trailing slashes on API paths.
Uses redirect to strip trailing slashes from non-root routes.
Routes defined with "/" (like /hero-banners/) keep trailing slash.
Routes defined without "/" (like /cars) get redirected.
"""
# Routes that are defined WITH trailing slash (router.get("/"))
TRAILING_SLASH_ROUTES = {
"/api/hero-banners/",
"/api/settings/",
"/api/notifications/",
"/api/vehicle-requests/",
}
async def dispatch(self, request: Request, call_next):
path = request.url.path
# Skip if it's a known trailing-slash route
if path in self.TRAILING_SLASH_ROUTES:
return await call_next(request)
# Redirect trailing slash from other /api/* paths
if path.startswith("/api/") and path.endswith("/") and len(path) > 5:
new_path = path.rstrip("/")
if request.url.query:
new_url = f"{new_path}?{request.url.query}"
else:
new_url = new_path
return RedirectResponse(url=new_url, status_code=307)
return await call_next(request)
# 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
)
# Trailing slash middleware (must be added before CORS)
app.add_middleware(TrailingSlashMiddleware)
# 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
"https://autonetsellcar.com", # Production
"http://autonetsellcar.com", # Production (HTTP redirect)
],
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"}