Files
AutonetSellCar/backend/app/api/hero_banners.py
AutonetSellCar Deploy e661d91c72 fix: banner translations and deployment improvements
- Add translateCarName import from i18n.ts for proper multilingual support
- Change default API language from 'ko' to 'en' for hero banners
- Add checkbox column for Local Cars banner registration
- Update Dockerfile with Playwright dependencies
- Add PostgreSQL migration script
- Add banner translation fix script

🤖 Generated with [Claude Code](https://claude.ai/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-31 10:41:42 +09:00

267 lines
8.2 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
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 ..schemas.hero_banner import (
HeroBannerCreate, HeroBannerUpdate, HeroBannerResponse,
HeroBannerListResponse, HeroBannerLocalizedResponse,
HeroBannerSettingsUpdate, HeroBannerSettingsResponse,
)
from .auth import get_current_user
from ..models import User
from ..config import get_settings
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_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"}
@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
# ==================== 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,
}