- 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>
122 lines
3.9 KiB
Python
122 lines
3.9 KiB
Python
"""
|
|
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
|