- 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>
201 lines
6.6 KiB
Python
201 lines
6.6 KiB
Python
"""
|
|
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}"
|