feat: Add car availability check feature

- Add daily scheduled check for Carmodoo car availability
- Add manual trigger button in admin settings
- Mark sold cars as soldout=True automatically
- Add settings for check time and enable/disable toggle
- Display check status and statistics in admin UI

🤖 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
2026-01-05 08:01:40 +09:00
parent 1c45840c70
commit 4858965087
6 changed files with 474 additions and 2 deletions

View File

@@ -0,0 +1,200 @@
"""
Car Availability Verification Service
Checks if imported cars are still available on Carmodoo
"""
import asyncio
import httpx
from datetime import datetime
from typing import List, Tuple
from sqlalchemy.orm import Session
from lxml import etree
from ..models.car import Car
from ..models.settings import SystemSettings
# Carmodoo base URL
CARMODOO_BASE_URL = "https://dealer.carmodoo.com"
class CarAvailabilityChecker:
"""카모두에서 차량 판매상태 확인"""
def __init__(self):
self.cookies = {}
self.is_logged_in = False
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept-Language': 'ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7',
}
async def login(self) -> bool:
"""카모두 로그인"""
import os
user_id = os.environ.get('CARMODOO_USER_ID', '')
password = os.environ.get('CARMODOO_PASSWORD', '')
if not user_id or not password:
print("[CarAvailability] No Carmodoo credentials found")
return False
try:
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
login_url = f"{CARMODOO_BASE_URL}/member/memberLoginOk.html"
form_data = {
'userId': user_id,
'userPwd': password,
'auto_login': 'N',
}
response = await client.post(
login_url,
data=form_data,
headers=self.headers
)
self.cookies = dict(response.cookies)
if 'JSESSIONID' in self.cookies or response.status_code == 200:
self.is_logged_in = True
print("[CarAvailability] Login successful")
return True
else:
print("[CarAvailability] Login failed")
return False
except Exception as e:
print(f"[CarAvailability] Login error: {e}")
return False
async def check_car_availability(self, source_id: str) -> bool:
"""
특정 차량이 카모두에서 아직 판매중인지 확인
source_id: 카모두 차량 번호 (c_cNo)
Returns: True if still available, False if sold/removed
"""
if not self.is_logged_in:
await self.login()
try:
async with httpx.AsyncClient(timeout=30.0, cookies=self.cookies) as client:
# 차량 상세 페이지 직접 접근
detail_url = f"{CARMODOO_BASE_URL}/car/carPopView.html"
params = {'c_cNo': source_id}
response = await client.get(
detail_url,
params=params,
headers=self.headers
)
if response.status_code != 200:
return False
# EUC-KR 디코딩
try:
content = response.content.decode('euc-kr', errors='replace')
except:
content = response.text
# 판매완료/삭제 표시 확인
sold_indicators = [
'판매완료',
'판매 완료',
'삭제된 차량',
'존재하지 않는',
'해당 차량이 없습니다',
'매물이 존재하지',
]
for indicator in sold_indicators:
if indicator in content:
return False
# 차량 정보가 있으면 판매중
if 'carViewWrap' in content or '차량정보' in content or '차량번호' in content:
return True
# 기본적으로 응답이 있으면 판매중으로 간주
return len(content) > 500
except Exception as e:
print(f"[CarAvailability] Error checking car {source_id}: {e}")
# 에러 시 기존 상태 유지 (판매중으로 간주)
return True
async def verify_all_cars(self, db: Session) -> Tuple[int, int, int]:
"""
모든 활성 차량의 판매상태 확인
Returns: (total_checked, still_available, sold_count)
"""
# soldout=False인 차량만 확인
cars = db.query(Car).filter(
Car.soldout == False,
Car.source == 'carmodoo'
).all()
if not cars:
return (0, 0, 0)
# 로그인
if not self.is_logged_in:
login_success = await self.login()
if not login_success:
print("[CarAvailability] Failed to login, aborting verification")
return (0, 0, 0)
total = len(cars)
available = 0
sold = 0
print(f"[CarAvailability] Starting verification of {total} cars...")
for i, car in enumerate(cars):
try:
is_available = await self.check_car_availability(car.source_id)
if is_available:
available += 1
else:
sold += 1
car.soldout = True
print(f"[CarAvailability] Car {car.id} ({car.car_name}) marked as sold")
# Rate limiting - 0.5초 대기
if i < total - 1:
await asyncio.sleep(0.5)
# 진행상황 로그 (10개마다)
if (i + 1) % 10 == 0:
print(f"[CarAvailability] Progress: {i + 1}/{total}")
except Exception as e:
print(f"[CarAvailability] Error checking car {car.id}: {e}")
available += 1 # 에러 시 판매중으로 간주
# DB 커밋
db.commit()
print(f"[CarAvailability] Verification complete: {total} checked, {available} available, {sold} sold")
return (total, available, sold)
async def run_car_availability_check(db: Session) -> str:
"""
차량 판매상태 검증 실행 및 결과 저장
Returns: 결과 요약 문자열
"""
checker = CarAvailabilityChecker()
total, available, sold = await checker.verify_all_cars(db)
# 설정에 결과 저장
settings = db.query(SystemSettings).first()
if settings:
settings.car_availability_last_check = datetime.now()
settings.car_availability_last_result = f"Checked: {total}, Available: {available}, Sold: {sold}"
db.commit()
return f"Checked: {total}, Available: {available}, Sold: {sold}"