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 ..core.database import get_db from ..core.security import get_current_admin from ..core.config import settings from ..models.admin import Admin from ..models.solution import Solution, SolutionImage, Product, ProductImage from ..schemas.solution import ( SolutionCreate, SolutionUpdate, SolutionResponse, SolutionLocalizedResponse, ProductCreate, ProductUpdate, ProductResponse, ProductLocalizedResponse ) router = APIRouter(tags=["solutions"]) 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""" localized = getattr(obj, f"{field}_{lang}", None) if localized: return localized return getattr(obj, f"{field}_ko", None) # ==================== Solution Public Endpoints ==================== @router.get("/solutions/", response_model=List[SolutionLocalizedResponse]) def get_solutions( lang: str = Query("ko", regex="^(ko|en|ja|zh)$"), db: Session = Depends(get_db) ): """Get all active solutions (public endpoint)""" solutions = db.query(Solution).filter( Solution.is_active == True ).order_by(Solution.display_order.asc()).all() result = [] for s in solutions: features_str = get_localized_field(s, "features", lang) or "" features = [f.strip() for f in features_str.split(",") if f.strip()] images = [ { "id": img.id, "image_url": img.image_url, "caption": get_localized_field(img, "caption", lang) } for img in s.images ] if hasattr(s, 'images') and s.images else [] result.append(SolutionLocalizedResponse( id=s.id, title=get_localized_field(s, "title", lang), subtitle=get_localized_field(s, "subtitle", lang), description=get_localized_field(s, "description", lang), features=features, icon=s.icon, color=s.color, main_image=s.main_image, images=images )) return result @router.get("/solutions/{solution_id}", response_model=SolutionLocalizedResponse) def get_solution( solution_id: int, lang: str = Query("ko", regex="^(ko|en|ja|zh)$"), db: Session = Depends(get_db) ): """Get single solution by ID (public endpoint)""" solution = db.query(Solution).filter( Solution.id == solution_id, Solution.is_active == True ).first() if not solution: raise HTTPException(status_code=404, detail="Solution not found") features_str = get_localized_field(solution, "features", lang) or "" features = [f.strip() for f in features_str.split(",") if f.strip()] images = [ { "id": img.id, "image_url": img.image_url, "caption": get_localized_field(img, "caption", lang) } for img in solution.images ] if hasattr(solution, 'images') and solution.images else [] return SolutionLocalizedResponse( id=solution.id, title=get_localized_field(solution, "title", lang), subtitle=get_localized_field(solution, "subtitle", lang), description=get_localized_field(solution, "description", lang), features=features, icon=solution.icon, color=solution.color, main_image=solution.main_image, images=images ) # ==================== Solution Admin Endpoints ==================== @router.get("/solutions/admin/list", response_model=List[SolutionResponse]) def admin_get_solutions( db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Get all solutions (admin only)""" solutions = db.query(Solution).order_by(Solution.display_order.asc(), Solution.id.desc()).all() return solutions @router.get("/solutions/admin/{solution_id}", response_model=SolutionResponse) def admin_get_solution( solution_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Get solution with all fields (admin only)""" solution = db.query(Solution).filter(Solution.id == solution_id).first() if not solution: raise HTTPException(status_code=404, detail="Solution not found") return solution @router.post("/solutions/admin", response_model=SolutionResponse) def create_solution( solution_data: SolutionCreate, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Create new solution (admin only)""" solution = Solution(**solution_data.model_dump()) db.add(solution) db.commit() db.refresh(solution) return solution @router.put("/solutions/admin/{solution_id}", response_model=SolutionResponse) def update_solution( solution_id: int, solution_data: SolutionUpdate, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Update solution (admin only)""" solution = db.query(Solution).filter(Solution.id == solution_id).first() if not solution: raise HTTPException(status_code=404, detail="Solution not found") update_data = solution_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(solution, field, value) db.commit() db.refresh(solution) return solution @router.delete("/solutions/admin/{solution_id}") def delete_solution( solution_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Delete solution (admin only)""" solution = db.query(Solution).filter(Solution.id == solution_id).first() if not solution: raise HTTPException(status_code=404, detail="Solution not found") # Delete associated images from filesystem if solution.main_image: try: os.remove(solution.main_image) except: pass for img in solution.images: try: os.remove(img.image_url) except: pass db.delete(solution) db.commit() return {"message": "Solution deleted successfully"} # ==================== Solution Image Upload ==================== @router.post("/solutions/admin/{solution_id}/upload-image") async def upload_solution_image( solution_id: int, file: UploadFile = File(...), is_main: bool = Query(False), db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Upload image for solution (admin only)""" solution = db.query(Solution).filter(Solution.id == solution_id).first() if not solution: raise HTTPException(status_code=404, detail="Solution not found") ext = os.path.splitext(file.filename)[1].lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException(status_code=400, detail="File type not allowed") contents = await file.read() if len(contents) > settings.MAX_FILE_SIZE: raise HTTPException(status_code=400, detail="File too large") filename = f"solution_{uuid.uuid4()}{ext}" filepath = os.path.join(settings.UPLOAD_DIR, filename) async with aiofiles.open(filepath, 'wb') as f: await f.write(contents) if is_main: solution.main_image = filepath db.commit() else: new_image = SolutionImage( solution_id=solution_id, image_url=filepath, display_order=len(solution.images) if solution.images else 0 ) db.add(new_image) db.commit() return {"message": "Image uploaded successfully", "path": filepath} @router.delete("/solutions/admin/images/{image_id}") def delete_solution_image( image_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Delete solution image (admin only)""" image = db.query(SolutionImage).filter(SolutionImage.id == image_id).first() if not image: raise HTTPException(status_code=404, detail="Image not found") try: os.remove(image.image_url) except: pass db.delete(image) db.commit() return {"message": "Image deleted successfully"} # ==================== Product Public Endpoints ==================== @router.get("/products/", response_model=List[ProductLocalizedResponse]) def get_products( lang: str = Query("ko", regex="^(ko|en|ja|zh)$"), db: Session = Depends(get_db) ): """Get all active products (public endpoint)""" products = db.query(Product).filter( Product.is_active == True ).order_by(Product.display_order.asc()).all() result = [] for p in products: images = [ { "id": img.id, "image_url": img.image_url, "caption": get_localized_field(img, "caption", lang) } for img in p.images ] if hasattr(p, 'images') and p.images else [] result.append(ProductLocalizedResponse( id=p.id, name=get_localized_field(p, "name", lang), category=get_localized_field(p, "category", lang), description=get_localized_field(p, "description", lang), detail=get_localized_field(p, "detail", lang), specifications=p.specifications, icon=p.icon, main_image=p.main_image, images=images )) return result @router.get("/products/{product_id}", response_model=ProductLocalizedResponse) def get_product( product_id: int, lang: str = Query("ko", regex="^(ko|en|ja|zh)$"), db: Session = Depends(get_db) ): """Get single product by ID (public endpoint)""" product = db.query(Product).filter( Product.id == product_id, Product.is_active == True ).first() if not product: raise HTTPException(status_code=404, detail="Product not found") images = [ { "id": img.id, "image_url": img.image_url, "caption": get_localized_field(img, "caption", lang) } for img in product.images ] if hasattr(product, 'images') and product.images else [] return ProductLocalizedResponse( id=product.id, name=get_localized_field(product, "name", lang), category=get_localized_field(product, "category", lang), description=get_localized_field(product, "description", lang), detail=get_localized_field(product, "detail", lang), specifications=product.specifications, icon=product.icon, main_image=product.main_image, images=images ) # ==================== Product Admin Endpoints ==================== @router.get("/products/admin/list", response_model=List[ProductResponse]) def admin_get_products( db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Get all products (admin only)""" products = db.query(Product).order_by(Product.display_order.asc(), Product.id.desc()).all() return products @router.get("/products/admin/{product_id}", response_model=ProductResponse) def admin_get_product( product_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Get product with all fields (admin only)""" product = db.query(Product).filter(Product.id == product_id).first() if not product: raise HTTPException(status_code=404, detail="Product not found") return product @router.post("/products/admin", response_model=ProductResponse) def create_product( product_data: ProductCreate, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Create new product (admin only)""" product = Product(**product_data.model_dump()) db.add(product) db.commit() db.refresh(product) return product @router.put("/products/admin/{product_id}", response_model=ProductResponse) def update_product( product_id: int, product_data: ProductUpdate, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Update product (admin only)""" product = db.query(Product).filter(Product.id == product_id).first() if not product: raise HTTPException(status_code=404, detail="Product not found") update_data = product_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(product, field, value) db.commit() db.refresh(product) return product @router.delete("/products/admin/{product_id}") def delete_product( product_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Delete product (admin only)""" product = db.query(Product).filter(Product.id == product_id).first() if not product: raise HTTPException(status_code=404, detail="Product not found") # Delete associated images from filesystem if product.main_image: try: os.remove(product.main_image) except: pass for img in product.images: try: os.remove(img.image_url) except: pass db.delete(product) db.commit() return {"message": "Product deleted successfully"} # ==================== Product Image Upload ==================== @router.post("/products/admin/{product_id}/upload-image") async def upload_product_image( product_id: int, file: UploadFile = File(...), is_main: bool = Query(False), db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Upload image for product (admin only)""" product = db.query(Product).filter(Product.id == product_id).first() if not product: raise HTTPException(status_code=404, detail="Product not found") ext = os.path.splitext(file.filename)[1].lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException(status_code=400, detail="File type not allowed") contents = await file.read() if len(contents) > settings.MAX_FILE_SIZE: raise HTTPException(status_code=400, detail="File too large") filename = f"product_{uuid.uuid4()}{ext}" filepath = os.path.join(settings.UPLOAD_DIR, filename) async with aiofiles.open(filepath, 'wb') as f: await f.write(contents) if is_main: product.main_image = filepath db.commit() else: new_image = ProductImage( product_id=product_id, image_url=filepath, display_order=len(product.images) if product.images else 0 ) db.add(new_image) db.commit() return {"message": "Image uploaded successfully", "path": filepath} @router.delete("/products/admin/images/{image_id}") def delete_product_image( image_id: int, db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin) ): """Delete product image (admin only)""" image = db.query(ProductImage).filter(ProductImage.id == image_id).first() if not image: raise HTTPException(status_code=404, detail="Image not found") try: os.remove(image.image_url) except: pass db.delete(image) db.commit() return {"message": "Image deleted successfully"}