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:
121
backend/app/services/soldout_service.py
Normal file
121
backend/app/services/soldout_service.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
Soldout Check Service - Checks if cars are still available on Carmodoo
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
import httpx
|
||||
|
||||
from ..models.car import Car
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SoldoutChecker:
|
||||
"""Carmodoo에서 차량 판매 여부를 확인하는 서비스"""
|
||||
|
||||
def __init__(self, db: Session, carmodoo_session_cookie: str = None):
|
||||
self.db = db
|
||||
self.carmodoo_session_cookie = carmodoo_session_cookie
|
||||
self.carmodoo_base_url = "https://dealer.carmodoo.com"
|
||||
|
||||
async def check_car_availability(self, source_id: str) -> bool:
|
||||
"""
|
||||
Carmodoo에서 차량이 아직 판매 중인지 확인
|
||||
|
||||
Returns:
|
||||
True if car is still available, False if sold/removed
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
# Carmodoo 차량 상세 페이지 접근
|
||||
url = f"{self.carmodoo_base_url}/car/carPopView.html"
|
||||
params = {"carNo": source_id}
|
||||
|
||||
headers = {}
|
||||
if self.carmodoo_session_cookie:
|
||||
headers["Cookie"] = self.carmodoo_session_cookie
|
||||
|
||||
response = await client.get(url, params=params, headers=headers)
|
||||
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
|
||||
# 페이지 내용에서 "판매완료", "삭제", "없는 차량" 등 확인
|
||||
content = response.text.lower()
|
||||
sold_keywords = [
|
||||
"판매완료", "판매 완료", "sold", "삭제된", "없는 차량",
|
||||
"존재하지 않", "찾을 수 없", "not found"
|
||||
]
|
||||
|
||||
for keyword in sold_keywords:
|
||||
if keyword in content:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking car {source_id}: {e}")
|
||||
# 에러 발생 시 available로 간주 (안전하게)
|
||||
return True
|
||||
|
||||
async def check_all_cars(self) -> Tuple[int, int, List[int]]:
|
||||
"""
|
||||
모든 활성 차량의 판매 여부 확인
|
||||
|
||||
Returns:
|
||||
(checked_count, soldout_count, soldout_car_ids)
|
||||
"""
|
||||
# soldout=False인 활성 차량만 조회
|
||||
cars = self.db.query(Car).filter(
|
||||
Car.status == "active",
|
||||
Car.soldout == False,
|
||||
Car.source == "carmodoo"
|
||||
).all()
|
||||
|
||||
logger.info(f"Checking {len(cars)} cars for soldout status...")
|
||||
|
||||
checked = 0
|
||||
soldout_count = 0
|
||||
soldout_ids = []
|
||||
|
||||
for car in cars:
|
||||
is_available = await self.check_car_availability(car.source_id)
|
||||
checked += 1
|
||||
|
||||
if not is_available:
|
||||
car.soldout = True
|
||||
soldout_count += 1
|
||||
soldout_ids.append(car.id)
|
||||
logger.info(f"Car {car.id} ({car.car_name}) marked as SOLD OUT")
|
||||
|
||||
# Rate limiting
|
||||
if checked % 10 == 0:
|
||||
logger.info(f"Progress: {checked}/{len(cars)} checked, {soldout_count} sold out")
|
||||
import asyncio
|
||||
await asyncio.sleep(1)
|
||||
|
||||
self.db.commit()
|
||||
|
||||
logger.info(f"Soldout check completed: {checked} checked, {soldout_count} sold out")
|
||||
return checked, soldout_count, soldout_ids
|
||||
|
||||
def mark_soldout(self, car_id: int) -> bool:
|
||||
"""수동으로 차량을 soldout 처리"""
|
||||
car = self.db.query(Car).filter(Car.id == car_id).first()
|
||||
if car:
|
||||
car.soldout = True
|
||||
self.db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
def mark_available(self, car_id: int) -> bool:
|
||||
"""수동으로 차량을 available 처리"""
|
||||
car = self.db.query(Car).filter(Car.id == car_id).first()
|
||||
if car:
|
||||
car.soldout = False
|
||||
self.db.commit()
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user