feat: Add banner toggle and soldout tracking to Cars page

- Add is_banner, soldout fields to Car model
- Add banner toggle API (POST /hero-banners/admin/toggle/{car_id})
- Add soldout APIs (POST/DELETE /cars/{car_id}/soldout)
- Add nightly soldout checker in agent (runs at 3:00 AM)
- Update Local Cars UI with banner checkbox and status column
- Remove hero-banners admin page (functionality moved to Cars page)
- Banner cars sorted to top with purple background
- Soldout cars displayed with gray overlay

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2025-12-31 12:50:40 +09:00
parent 9969554deb
commit c9fd7611a7
10 changed files with 579 additions and 40 deletions

View File

@@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks
from sqlalchemy.orm import Session, joinedload
from typing import Optional, List
from ..database import get_db
from ..models import Car, CarMaker, CarModel, CarImage, CarOption
from ..services.soldout_service import SoldoutChecker
from ..schemas import (
CarCreate, CarUpdate, CarResponse, CarListResponse,
CarMakerCreate, CarMakerResponse,
@@ -29,6 +30,8 @@ def car_to_response(car: Car) -> dict:
"final_price_mn": (car.price_krw or 0) + (car.margin_mn or 0),
"price_usd": car.price_usd,
"is_displayed": car.is_displayed or False,
"is_banner": car.is_banner or False,
"soldout": car.soldout or False,
"fuel": car.fuel,
"transmission": car.transmission,
"color": car.color,
@@ -338,3 +341,78 @@ def create_model(model_data: CarModelCreate, db: Session = Depends(get_db)):
db.commit()
db.refresh(model)
return model
# ==================== Soldout APIs ====================
@router.post("/{car_id}/soldout")
def mark_car_soldout(car_id: int, db: Session = Depends(get_db)):
"""차량을 SOLD OUT 처리 (수동)"""
car = db.query(Car).filter(Car.id == car_id).first()
if not car:
raise HTTPException(status_code=404, detail="Car not found")
car.soldout = True
db.commit()
return {"car_id": car_id, "soldout": True, "message": "Car marked as sold out"}
@router.delete("/{car_id}/soldout")
def mark_car_available(car_id: int, db: Session = Depends(get_db)):
"""차량을 다시 판매 가능 상태로 변경 (수동)"""
car = db.query(Car).filter(Car.id == car_id).first()
if not car:
raise HTTPException(status_code=404, detail="Car not found")
car.soldout = False
db.commit()
return {"car_id": car_id, "soldout": False, "message": "Car marked as available"}
async def run_soldout_check(db: Session):
"""백그라운드에서 soldout 체크 실행"""
checker = SoldoutChecker(db)
await checker.check_all_cars()
@router.post("/admin/check-soldout")
async def trigger_soldout_check(
background_tasks: BackgroundTasks,
db: Session = Depends(get_db)
):
"""모든 차량의 SOLD OUT 상태 확인 (Admin, 백그라운드 실행)
Carmodoo에서 더 이상 존재하지 않는 차량을 soldout=True로 표시합니다.
이 작업은 백그라운드에서 실행되며, 시간이 오래 걸릴 수 있습니다.
"""
# 현재 active이고 soldout=False인 차량 수 확인
pending_count = db.query(Car).filter(
Car.status == "active",
Car.soldout == False,
Car.source == "carmodoo"
).count()
# 백그라운드로 실행 (주의: db session 문제가 있을 수 있음)
# 실제로는 agent에서 스케줄로 실행하는 것이 좋음
# background_tasks.add_task(run_soldout_check, db)
return {
"message": "Soldout check will be performed by nightly agent job",
"pending_cars": pending_count,
"note": "Use carmodoo-agent for scheduled soldout checks"
}
@router.get("/admin/soldout-stats")
def get_soldout_stats(db: Session = Depends(get_db)):
"""SOLD OUT 통계 조회 (Admin)"""
total = db.query(Car).filter(Car.status == "active").count()
soldout = db.query(Car).filter(Car.status == "active", Car.soldout == True).count()
available = db.query(Car).filter(Car.status == "active", Car.soldout == False).count()
return {
"total_active": total,
"soldout": soldout,
"available": available,
"soldout_percentage": round(soldout / total * 100, 1) if total > 0 else 0
}