from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Body, BackgroundTasks from sqlalchemy.orm import Session from typing import List, Optional import os import uuid import aiofiles from ..database import get_db from ..models.hero_banner import HeroBanner, HeroBannerSettings from ..models.car import Car from ..schemas.hero_banner import ( HeroBannerCreate, HeroBannerUpdate, HeroBannerResponse, HeroBannerListResponse, HeroBannerLocalizedResponse, HeroBannerSettingsUpdate, HeroBannerSettingsResponse, BannerReorderRequest, ) from .auth import get_current_user from ..models import User from ..config import get_settings from ..services.promo_notification_service import notify_users_for_promo_vehicle router = APIRouter(prefix="/hero-banners", tags=["hero-banners"]) settings = get_settings() ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} def get_localized_field(obj, field: str, lang: str) -> Optional[str]: """Get localized field value with fallback to English""" # 1. 선택된 언어의 필드 localized = getattr(obj, f"{field}_{lang}", None) if localized: return localized # 2. 영어 폴백 en_value = getattr(obj, f"{field}_en", None) if en_value: return en_value # 3. 한국어 폴백 (마지막 수단) return getattr(obj, f"{field}_ko", None) # ==================== Public Endpoints ==================== @router.get("/", response_model=List[HeroBannerLocalizedResponse]) def get_hero_banners( lang: str = Query("en", regex="^(ko|en|mn|ru)$"), db: Session = Depends(get_db) ): """활성 히어로 배너 목록 조회 (Public)""" banners = db.query(HeroBanner).filter( HeroBanner.is_active == True ).order_by(HeroBanner.display_order.asc(), HeroBanner.id.desc()).all() result = [] for b in banners: result.append(HeroBannerLocalizedResponse( id=b.id, title=get_localized_field(b, "title", lang), subtitle=get_localized_field(b, "subtitle", lang), image_url=b.image_url, link_url=b.link_url, car_id=b.car_id, )) return result @router.get("/check-car/{car_id}") def check_banner_car(car_id: int, db: Session = Depends(get_db)): """차량이 Hero Banner에 연결되어 있는지 확인 (Public) Banner에 연결된 차량은 샘플로 모든 정보를 무료로 공개합니다. """ banner = db.query(HeroBanner).filter( HeroBanner.car_id == car_id, HeroBanner.is_active == True ).first() return { "car_id": car_id, "is_banner_car": banner is not None, "banner_id": banner.id if banner else None } @router.get("/settings", response_model=HeroBannerSettingsResponse) def get_banner_settings(db: Session = Depends(get_db)): """배너 슬라이더 설정 조회 (Public)""" settings_obj = db.query(HeroBannerSettings).first() if not settings_obj: # 기본 설정 생성 settings_obj = HeroBannerSettings( slide_interval=3000, animation_type="film-strip", image_width=500, image_height=300, auto_play=True, ) db.add(settings_obj) db.commit() db.refresh(settings_obj) return settings_obj # ==================== Admin Endpoints ==================== def get_admin_user(current_user: User = Depends(get_current_user)) -> User: """관리자 권한 확인 (임시: 모든 로그인 사용자 허용)""" # TODO: 실제 관리자 역할 체크 추가 # if current_user.role != "admin": # raise HTTPException(status_code=403, detail="Admin access required") return current_user @router.get("/admin/list", response_model=List[HeroBannerListResponse]) def admin_get_banners( db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """모든 히어로 배너 조회 (Admin)""" banners = db.query(HeroBanner).order_by( HeroBanner.display_order.asc(), HeroBanner.id.desc() ).all() return banners @router.get("/admin/banner-cars") def get_banner_cars( db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """배너 등록된 차량 목록 조회 (Admin) display_order 순으로 정렬된 차량 ID 목록 반환 """ banners = db.query(HeroBanner).filter( HeroBanner.car_id.isnot(None) ).order_by(HeroBanner.display_order.asc()).all() return { "car_ids": [b.car_id for b in banners], "count": len(banners) } @router.put("/admin/reorder") def reorder_banners( request: BannerReorderRequest, db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """배너 순서 재정렬 (Admin) car_ids: 배너 차량 ID 목록 (원하는 순서대로) """ for order, car_id in enumerate(request.car_ids): banner = db.query(HeroBanner).filter(HeroBanner.car_id == car_id).first() if banner: banner.display_order = order db.commit() return {"message": "Banner order updated", "count": len(request.car_ids)} @router.put("/admin/settings", response_model=HeroBannerSettingsResponse) def update_banner_settings( settings_data: HeroBannerSettingsUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """배너 슬라이더 설정 수정 (Admin)""" settings_obj = db.query(HeroBannerSettings).first() if not settings_obj: settings_obj = HeroBannerSettings() db.add(settings_obj) update_data = settings_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(settings_obj, field, value) db.commit() db.refresh(settings_obj) return settings_obj @router.get("/admin/{banner_id}", response_model=HeroBannerResponse) def admin_get_banner( banner_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """히어로 배너 상세 조회 (Admin)""" banner = db.query(HeroBanner).filter(HeroBanner.id == banner_id).first() if not banner: raise HTTPException(status_code=404, detail="Banner not found") return banner @router.post("/admin", response_model=HeroBannerResponse) def create_banner( banner_data: HeroBannerCreate, db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """히어로 배너 생성 (Admin)""" banner = HeroBanner(**banner_data.model_dump()) db.add(banner) db.commit() db.refresh(banner) return banner @router.put("/admin/{banner_id}", response_model=HeroBannerResponse) def update_banner( banner_id: int, banner_data: HeroBannerUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """히어로 배너 수정 (Admin)""" banner = db.query(HeroBanner).filter(HeroBanner.id == banner_id).first() if not banner: raise HTTPException(status_code=404, detail="Banner not found") update_data = banner_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(banner, field, value) db.commit() db.refresh(banner) return banner @router.delete("/admin/{banner_id}") def delete_banner( banner_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """히어로 배너 삭제 (Admin)""" banner = db.query(HeroBanner).filter(HeroBanner.id == banner_id).first() if not banner: raise HTTPException(status_code=404, detail="Banner not found") # 로컬 이미지 파일 삭제 if banner.image_url and banner.image_url.startswith("/uploads/"): try: filepath = os.path.join(settings.UPLOAD_DIR if hasattr(settings, 'UPLOAD_DIR') else "./uploads", os.path.basename(banner.image_url)) if os.path.exists(filepath): os.remove(filepath) except Exception: pass db.delete(banner) db.commit() return {"message": "Banner deleted successfully"} # ==================== Image Upload ==================== @router.post("/admin/upload-image") async def upload_banner_image( file: UploadFile = File(...), db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """배너 이미지 업로드 (Admin)""" # 파일 확장자 검증 ext = os.path.splitext(file.filename)[1].lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException( status_code=400, detail=f"File type not allowed. Allowed: {ALLOWED_EXTENSIONS}" ) # 파일 읽기 및 크기 검증 contents = await file.read() max_size = 10 * 1024 * 1024 # 10MB if len(contents) > max_size: raise HTTPException( status_code=400, detail=f"File too large. Max size: {max_size / 1024 / 1024}MB" ) # 업로드 디렉토리 생성 upload_dir = "./uploads/hero-banners" os.makedirs(upload_dir, exist_ok=True) # 고유 파일명 생성 filename = f"hero_{uuid.uuid4()}{ext}" filepath = os.path.join(upload_dir, filename) # 파일 저장 async with aiofiles.open(filepath, 'wb') as f: await f.write(contents) # 상대 URL 반환 image_url = f"/uploads/hero-banners/{filename}" return { "message": "Image uploaded successfully", "image_url": image_url, "filename": filename, } # ==================== Banner Toggle & Ordering ==================== @router.post("/admin/toggle/{car_id}") async def toggle_banner( car_id: int, background_tasks: BackgroundTasks, db: Session = Depends(get_db), current_user: User = Depends(get_admin_user) ): """차량의 배너 상태 토글 (Admin) - HeroBanner 존재 → 삭제 - HeroBanner 없음 → 생성 (+ 프로모션 알림 이메일 전송) """ car = db.query(Car).filter(Car.id == car_id).first() if not car: raise HTTPException(status_code=404, detail="Car not found") # HeroBanner 테이블을 기준으로 판단 (car.is_banner 필드 대신) existing_banner = db.query(HeroBanner).filter(HeroBanner.car_id == car_id).first() if existing_banner: # 배너에서 제거 db.delete(existing_banner) car.is_banner = False db.commit() return {"car_id": car_id, "is_banner": False, "message": "Removed from banner"} else: # 배너에 추가 # 현재 최대 display_order 찾기 max_order = db.query(HeroBanner).count() # 차량 이미지 URL image_url = f"/uploads/cars/{car_id}/image_0.jpg" # 배너 생성 banner = HeroBanner( title_ko=car.car_name or "", title_en=car.car_name or "", # 프론트엔드에서 번역 title_mn=car.car_name or "", title_ru=car.car_name or "", subtitle_ko=f"{car.year or ''}년식 | {car.mileage:,}km" if car.mileage else f"{car.year or ''}년식", subtitle_en=f"{car.year or ''} | {car.mileage:,}km" if car.mileage else f"{car.year or ''}", subtitle_mn=f"{car.year or ''} | {car.mileage:,}km" if car.mileage else f"{car.year or ''}", subtitle_ru=f"{car.year or ''} | {car.mileage:,}km" if car.mileage else f"{car.year or ''}", image_url=image_url, link_url=f"/cars/{car_id}", car_id=car_id, display_order=max_order, is_active=True, ) db.add(banner) car.is_banner = True db.commit() db.refresh(banner) # Send promo notification emails in background background_tasks.add_task(notify_users_for_promo_vehicle, db, car) return {"car_id": car_id, "is_banner": True, "banner_id": banner.id, "message": "Added to banner"}