'use client'; import { useEffect, useState, useRef } from 'react'; import { motion, useAnimationControls } from 'framer-motion'; import Image from 'next/image'; import Link from 'next/link'; import { HeroBanner, HeroBannerSettings } from '@/types'; import { useLanguageStore, Language, translateCarName } from '@/lib/i18n'; interface FilmStripSliderProps { banners: HeroBanner[]; settings?: HeroBannerSettings; } // 기본 설정 const defaultSettings: HeroBannerSettings = { id: 0, slide_interval: 3000, animation_type: 'film-strip', image_width: 500, image_height: 300, auto_play: true, }; // 샘플 배너 데이터 (API 데이터가 없을 때 사용) const sampleBanners: HeroBanner[] = [ { id: 1, title_en: 'Hyundai Sonata 2023', title_ko: '현대 소나타 2023', title_mn: 'Хёндай Соната 2023', subtitle_en: 'Premium Sedan', subtitle_ko: '프리미엄 세단', subtitle_mn: 'Премиум седан', image_url: 'https://images.unsplash.com/photo-1605559424843-9e4c228bf1c2?w=500&h=300&fit=crop', link_url: '/cars', is_active: true, display_order: 0, }, { id: 2, title_en: 'Kia Sportage 2022', title_ko: '기아 스포티지 2022', title_mn: 'Киа Спортаж 2022', subtitle_en: 'Compact SUV', subtitle_ko: '컴팩트 SUV', subtitle_mn: 'Компакт SUV', image_url: 'https://images.unsplash.com/photo-1494976388531-d1058494cdd8?w=500&h=300&fit=crop', link_url: '/cars', is_active: true, display_order: 1, }, { id: 3, title_en: 'Genesis G80 2023', title_ko: '제네시스 G80 2023', title_mn: 'Женезис G80 2023', subtitle_en: 'Luxury Sedan', subtitle_ko: '럭셔리 세단', subtitle_mn: 'Люкс седан', image_url: 'https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=500&h=300&fit=crop', link_url: '/cars', is_active: true, display_order: 2, }, { id: 4, title_en: 'Hyundai Tucson 2023', title_ko: '현대 투싼 2023', title_mn: 'Хёндай Туксон 2023', subtitle_en: 'Family SUV', subtitle_ko: '패밀리 SUV', subtitle_mn: 'Гэр бүлийн SUV', image_url: 'https://images.unsplash.com/photo-1552519507-da3b142c6e3d?w=500&h=300&fit=crop', link_url: '/cars', is_active: true, display_order: 3, }, { id: 5, title_en: 'Kia EV6 2023', title_ko: '기아 EV6 2023', title_mn: 'Киа EV6 2023', subtitle_en: 'Electric Vehicle', subtitle_ko: '전기차', subtitle_mn: 'Цахилгаан машин', image_url: 'https://images.unsplash.com/photo-1617788138017-80ad40651399?w=500&h=300&fit=crop', link_url: '/cars', is_active: true, display_order: 4, }, ]; export default function FilmStripSlider({ banners, settings }: FilmStripSliderProps) { const effectiveSettings = settings || defaultSettings; const effectiveBanners = banners.length > 0 ? banners : sampleBanners; // 무한 루프를 위해 배너를 3배로 복제 const duplicatedBanners = [...effectiveBanners, ...effectiveBanners, ...effectiveBanners]; const controls = useAnimationControls(); const containerRef = useRef(null); const [isPaused, setIsPaused] = useState(false); const currentXRef = useRef(0); const animationStartTimeRef = useRef(0); const imageWidth = effectiveSettings.image_width; const imageHeight = effectiveSettings.image_height; const gap = 16; // gap between images const singleSetWidth = (imageWidth + gap) * effectiveBanners.length; const totalDuration = effectiveBanners.length * (effectiveSettings.slide_interval / 1000); // 무한 슬라이드 애니메이션 useEffect(() => { if (!effectiveSettings.auto_play) return; if (isPaused) { // 일시정지 시 현재 위치 계산 및 저장 const elapsed = (Date.now() - animationStartTimeRef.current) / 1000; const progress = (elapsed % totalDuration) / totalDuration; currentXRef.current = -singleSetWidth * progress; controls.stop(); return; } // 현재 위치에서 남은 거리 계산 const currentX = currentXRef.current; const remainingDistance = -singleSetWidth - currentX; const remainingDuration = (remainingDistance / -singleSetWidth) * totalDuration; const animate = async () => { // 먼저 남은 거리를 완료 if (remainingDistance < 0) { await controls.start({ x: -singleSetWidth, transition: { duration: remainingDuration, ease: 'linear', }, }); } // 그 후 무한 루프 시작 animationStartTimeRef.current = Date.now(); currentXRef.current = 0; controls.set({ x: 0 }); await controls.start({ x: -singleSetWidth, transition: { duration: totalDuration, ease: 'linear', repeat: Infinity, repeatType: 'loop', }, }); }; // 현재 위치 설정 후 애니메이션 시작 controls.set({ x: currentX }); animationStartTimeRef.current = Date.now() - ((-currentX / singleSetWidth) * totalDuration * 1000); animate(); return () => { controls.stop(); }; }, [controls, effectiveBanners.length, singleSetWidth, effectiveSettings.auto_play, totalDuration, isPaused]); const handleMouseEnter = () => { setIsPaused(true); }; const handleMouseLeave = () => { setIsPaused(false); }; // 현재 X 위치 계산 const getCurrentX = () => { if (isPaused) { return currentXRef.current; } const elapsed = (Date.now() - animationStartTimeRef.current) / 1000; const progress = (elapsed % totalDuration) / totalDuration; return -singleSetWidth * progress; }; // 다음/이전 차량으로 빠르게 이동 const handleNext = async (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); const itemWidth = imageWidth + gap; const currentX = getCurrentX(); // 현재 위치에서 다음 아이템 위치 계산 const currentItem = Math.floor(-currentX / itemWidth); const nextX = -((currentItem + 1) * itemWidth); // 범위 체크 (singleSetWidth를 넘으면 처음으로) const targetX = nextX <= -singleSetWidth ? 0 : nextX; // 애니메이션 중지 후 위치 설정 controls.stop(); currentXRef.current = targetX; animationStartTimeRef.current = Date.now() - ((-targetX / singleSetWidth) * totalDuration * 1000); // 부드러운 이동 애니메이션 await controls.start({ x: targetX, transition: { duration: 0.3, ease: 'easeOut' } }); // 일시정지 상태가 아니면 자동 재생 트리거 if (!isPaused) { setIsPaused(true); setTimeout(() => setIsPaused(false), 50); } }; const handlePrev = async (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); const itemWidth = imageWidth + gap; const currentX = getCurrentX(); // 현재 위치에서 이전 아이템 위치 계산 const currentItem = Math.floor(-currentX / itemWidth); const prevX = -((currentItem - 1) * itemWidth); // 범위 체크 (0보다 크면 끝으로) const targetX = prevX > 0 ? -(singleSetWidth - itemWidth) : prevX; // 애니메이션 중지 후 위치 설정 controls.stop(); currentXRef.current = targetX; animationStartTimeRef.current = Date.now() - ((-targetX / singleSetWidth) * totalDuration * 1000); // 부드러운 이동 애니메이션 await controls.start({ x: targetX, transition: { duration: 0.3, ease: 'easeOut' } }); // 일시정지 상태가 아니면 자동 재생 트리거 if (!isPaused) { setIsPaused(true); setTimeout(() => setIsPaused(false), 50); } }; return (
{/* Film strip effect - top perforation */}
{Array.from({ length: 30 }).map((_, i) => (
))}
{/* Main slider container */}
{duplicatedBanners.map((banner, index) => ( ))}
{/* Film strip effect - bottom perforation */}
{Array.from({ length: 30 }).map((_, i) => (
))}
{/* Gradient overlays for fade effect at edges */}
{/* Navigation Arrows */}
); } interface BannerCardProps { banner: HeroBanner; width: number; height: number; } const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'; // 이미지 URL 변환 (로컬 경로는 백엔드 URL 추가) const getImageUrl = (url: string): string => { if (!url) return ''; if (url.startsWith('http://') || url.startsWith('https://')) { return url; } // 로컬 경로인 경우 백엔드 URL 추가 return `${API_BASE_URL}${url}`; }; // Helper to get localized title/subtitle based on language function getLocalizedText(banner: HeroBanner, field: 'title' | 'subtitle', language: Language): string { // 1. 먼저 선택된 언어의 필드 확인 (title_mn, title_en, etc.) const langKey = `${field}_${language}` as keyof HeroBanner; const langValue = banner[langKey] as string | undefined; if (langValue) { return langValue; } // 2. 영어 폴백 const enKey = `${field}_en` as keyof HeroBanner; const enValue = banner[enKey] as string | undefined; if (enValue) { return enValue; } // 3. 마지막으로 직접 필드 (API가 단일 필드로 반환하는 경우) const directField = banner[field as keyof HeroBanner] as string | undefined; return directField || ''; } function BannerCard({ banner, width, height }: BannerCardProps) { const { language } = useLanguageStore(); const imageUrl = getImageUrl(banner.image_url); // 언어별 제목과 부제목 가져오기 (한국어인 경우 번역 적용) const rawTitle = getLocalizedText(banner, 'title', language); const rawSubtitle = getLocalizedText(banner, 'subtitle', language); // 한국어 차량명을 선택한 언어로 번역 const title = translateCarName(rawTitle, language); const subtitle = translateCarName(rawSubtitle, language); const content = (
{/* Image */}
{/* eslint-disable-next-line @next/next/no-img-element */} {title
{/* Overlay on hover */}
{/* Text overlay */}
{title && (

{title}

)} {subtitle && (

{subtitle}

)}
{/* Film frame border effect */}
); if (banner.link_url) { return ( {content} ); } if (banner.car_id) { return ( {content} ); } return content; }