Files
AutonetSellCar/backend/app/api/hero_banners.py
AutonetSellCar Deploy 1f0dcb1ddb Initial commit: AutonetSellCar platform with deployment system
- Frontend: Next.js 14 with TypeScript
- Backend: FastAPI with SQLAlchemy
- Agent: Carmodoo sync agent
- Deployment: Docker Compose based staging/production setup
- Scripts: Automated deployment with rollback support

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 13:24:39 +09:00

266 lines
8.1 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 Korean then English"""
localized = getattr(obj, f"{field}_{lang}", None)
if localized:
return localized
# Fallback to Korean
ko_value = getattr(obj, f"{field}_ko", None)
if ko_value:
return ko_value
# Fallback to English
return getattr(obj, f"{field}_en", None)
# ==================== Public Endpoints ====================
@router.get("/", response_model=List[HeroBannerLocalizedResponse])
def get_hero_banners(
lang: str = Query("ko", regex="^(ko|en|mn)$"),
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,
}