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,11 +1,13 @@
"""
Carmodoo Sync Agent - Syncs makers/models from Carmodoo to AutonetSellCar Backend
Also performs nightly soldout checks.
"""
import asyncio
import os
import logging
import httpx
from datetime import datetime, time
from dotenv import load_dotenv
from .carmodoo_client import CarmodooClient, CarmodooConfig
@@ -149,10 +151,139 @@ class SyncAgent:
finally:
await self.stop()
async def check_soldout(self):
"""Check all cars for soldout status"""
logger.info("Starting soldout check...")
if not await self.start():
return
try:
# Get all active cars from backend
response = await self.http_client.get(
f"{self.api_url}/cars",
params={"admin": True, "page_size": 1000, "status": "active"}
)
if response.status_code != 200:
logger.error(f"Failed to get cars: {response.status_code}")
return
data = response.json()
cars = data.get("cars", [])
logger.info(f"Checking {len(cars)} cars for soldout status...")
soldout_count = 0
checked = 0
for car in cars:
if car.get("soldout"):
continue # Already soldout
source_id = car.get("source_id")
car_id = car.get("id")
if not source_id:
continue
# Check if car exists on Carmodoo
is_available = await self._check_car_on_carmodoo(source_id)
checked += 1
if not is_available:
# Mark as soldout via API
try:
resp = await self.http_client.post(
f"{self.api_url}/cars/{car_id}/soldout"
)
if resp.status_code == 200:
soldout_count += 1
logger.info(f"Car {car_id} ({car.get('car_name')}) marked as SOLD OUT")
except Exception as e:
logger.error(f"Failed to mark car {car_id} as soldout: {e}")
# Rate limiting
if checked % 10 == 0:
logger.info(f"Progress: {checked}/{len(cars)} checked, {soldout_count} sold out")
await asyncio.sleep(1)
logger.info(f"Soldout check completed: {checked} checked, {soldout_count} sold out")
except Exception as e:
logger.error(f"Soldout check error: {e}")
finally:
await self.stop()
async def _check_car_on_carmodoo(self, source_id: str) -> bool:
"""Check if car exists on Carmodoo"""
try:
# Try to get car info from Carmodoo
url = f"https://dealer.carmodoo.com/car/carPopView.html"
params = {"carNo": source_id}
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.get(url, params=params)
if response.status_code == 404:
return False
content = response.text.lower()
sold_keywords = ["판매완료", "판매 완료", "삭제된", "없는 차량", "존재하지 않"]
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}")
return True # Assume available on error
async def run_scheduled(self):
"""Run agent with scheduled tasks"""
logger.info("Starting scheduled agent...")
# Run initial sync
await self.run_sync()
# Schedule nightly soldout check at 3:00 AM
while True:
now = datetime.now()
target_time = time(3, 0) # 3:00 AM
# Calculate seconds until next 3:00 AM
target_datetime = datetime.combine(now.date(), target_time)
if now.time() >= target_time:
# Already past 3 AM today, schedule for tomorrow
from datetime import timedelta
target_datetime += timedelta(days=1)
seconds_until = (target_datetime - now).total_seconds()
logger.info(f"Next soldout check in {seconds_until / 3600:.1f} hours at {target_datetime}")
await asyncio.sleep(seconds_until)
# Run soldout check
logger.info("Running scheduled soldout check...")
await self.check_soldout()
async def main():
agent = SyncAgent()
await agent.run_sync()
# Check command line args
import sys
if len(sys.argv) > 1:
if sys.argv[1] == "sync":
await agent.run_sync()
elif sys.argv[1] == "soldout":
await agent.check_soldout()
elif sys.argv[1] == "scheduled":
await agent.run_scheduled()
else:
# Default: run scheduled
await agent.run_scheduled()
if __name__ == '__main__':