Files
AutonetSellCar Deploy 3f27297c4a refactor: Remove unused DB translation system
Static dictionary (i18n.ts CAR_TRANSLATIONS) already covers all terms.
DB translations table had only 179 entries used as fallback and was
never actually reached. Simplifies useTranslate hook to static-only.
DB table preserved for safety.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 23:24:38 +09:00

252 lines
8.7 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, cc, settings, vehicle_requests, dealer, vehicle_share, withdrawal, referral, notification, dashboard, push, exchange_rate, verification, visitor, sns_share, bulletin, reviews
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 .services.car_availability_service import run_car_availability_check
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()
async def scheduled_car_availability_check():
"""Check car availability on Carmodoo"""
print("[Scheduler] Starting car availability check...")
db = SessionLocal()
try:
result = await run_car_availability_check(db)
print(f"[Scheduler] Car availability check completed: {result}")
except Exception as e:
print(f"[Scheduler] Car availability check 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
)
# 차량 판매상태 검증 (매일 새벽 6시 - 설정에서 변경 가능)
# 동적 시간 설정을 위해 settings 확인
db = SessionLocal()
try:
from .models.settings import SystemSettings
settings = db.query(SystemSettings).first()
check_hour = settings.car_availability_check_hour if settings else 6
except:
check_hour = 6
finally:
db.close()
scheduler.add_job(
scheduled_car_availability_check,
CronTrigger(hour=check_hour, minute=0),
id="daily_car_availability_check",
name="Daily Car Availability Check",
replace_existing=True
)
scheduler.start()
print(f"[Startup] Scheduler started - Exchange rates: 11:30 AM, Visitor stats: 2:00 AM, Log cleanup: Sunday 3:00 AM, Car availability: {check_hour}: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", # Production
"http://192.168.0.202:3001", # Staging
"http://192.168.0.202:8001", # Staging backend
"https://autonetsellcar.com", # Production domain
"http://autonetsellcar.com", # Production (HTTP redirect)
"https://staging.autonetsellcar.com", # Staging domain
"http://staging.autonetsellcar.com", # Staging (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(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.include_router(sns_share.router, prefix="/api")
app.include_router(bulletin.router, prefix="/api")
app.include_router(reviews.router, prefix="/api")
@app.get("/")
def root():
return {"message": "AutonetSellCar API", "version": "1.0.0"}
@app.get("/health")
def health():
return {"status": "healthy"}