fix: Show skeleton instead of sample images during banner loading

Sample car images (Tesla, Sportage etc.) now only appear as fallback
when the API fails to respond within 3 seconds. During normal loading,
a pulse-animated skeleton is shown instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AutonetSellCar Deploy
2026-02-18 08:58:57 +09:00
parent 8e230c537c
commit d98e9287d3
2 changed files with 57 additions and 4 deletions

View File

@@ -12,8 +12,18 @@ export default function Home() {
const { t, language } = useTranslation(); const { t, language } = useTranslation();
const [banners, setBanners] = useState<HeroBanner[]>([]); const [banners, setBanners] = useState<HeroBanner[]>([]);
const [bannerSettings, setBannerSettings] = useState<HeroBannerSettings | undefined>(); const [bannerSettings, setBannerSettings] = useState<HeroBannerSettings | undefined>();
const [bannerLoaded, setBannerLoaded] = useState(false);
const [showFallback, setShowFallback] = useState(false);
useEffect(() => { useEffect(() => {
setBannerLoaded(false);
setShowFallback(false);
// 3초 후에도 로딩 안 되면 샘플 배너 표시
const fallbackTimer = setTimeout(() => {
setShowFallback(true);
}, 3000);
const loadBanners = async () => { const loadBanners = async () => {
try { try {
const [bannersData, settingsData] = await Promise.all([ const [bannersData, settingsData] = await Promise.all([
@@ -24,10 +34,15 @@ export default function Home() {
setBannerSettings(settingsData); setBannerSettings(settingsData);
} catch (error) { } catch (error) {
console.error('Failed to load banners:', error); console.error('Failed to load banners:', error);
// 에러 시 샘플 배너 사용 (FilmStripSlider 내부에서 처리) setShowFallback(true);
} finally {
setBannerLoaded(true);
clearTimeout(fallbackTimer);
} }
}; };
loadBanners(); loadBanners();
return () => clearTimeout(fallbackTimer);
}, [language]); }, [language]);
return ( return (
@@ -44,7 +59,11 @@ export default function Home() {
</div> </div>
{/* Film Strip Slider */} {/* Film Strip Slider */}
<FilmStripSlider banners={banners} settings={bannerSettings} /> <FilmStripSlider
banners={banners}
settings={bannerSettings}
loading={!bannerLoaded && !showFallback}
/>
<div className="container mx-auto px-4 py-4 sm:py-8"> <div className="container mx-auto px-4 py-4 sm:py-8">
<div className="flex flex-col lg:flex-row items-center gap-4 lg:gap-6"> <div className="flex flex-col lg:flex-row items-center gap-4 lg:gap-6">

View File

@@ -10,6 +10,7 @@ import { useLanguageStore, Language, translateCarName } from '@/lib/i18n';
interface FilmStripSliderProps { interface FilmStripSliderProps {
banners: HeroBanner[]; banners: HeroBanner[];
settings?: HeroBannerSettings; settings?: HeroBannerSettings;
loading?: boolean;
} }
// 기본 설정 // 기본 설정
@@ -124,9 +125,11 @@ const sampleBanners: HeroBanner[] = [
}, },
]; ];
export default function FilmStripSlider({ banners, settings }: FilmStripSliderProps) { export default function FilmStripSlider({ banners, settings, loading }: FilmStripSliderProps) {
const effectiveSettings = settings || defaultSettings; const effectiveSettings = settings || defaultSettings;
const effectiveBanners = banners.length > 0 ? banners : sampleBanners; // 로딩 중이면 빈 배열 사용 → 스켈레톤 표시
// 로딩 완료 후 데이터 없으면 sampleBanners (3초 이상 미응답 시에만 도달)
const effectiveBanners = loading ? [] : (banners.length > 0 ? banners : sampleBanners);
// 무한 루프를 위해 배너를 3배로 복제 // 무한 루프를 위해 배너를 3배로 복제
const duplicatedBanners = [...effectiveBanners, ...effectiveBanners, ...effectiveBanners]; const duplicatedBanners = [...effectiveBanners, ...effectiveBanners, ...effectiveBanners];
@@ -285,6 +288,37 @@ export default function FilmStripSlider({ banners, settings }: FilmStripSliderPr
} }
}; };
// 로딩 중이면 스켈레톤 표시
if (loading || effectiveBanners.length === 0) {
return (
<div className="relative w-full overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800 py-4 sm:py-8">
<div className="absolute top-0 left-0 right-0 h-3 sm:h-4 bg-gray-900 hidden sm:flex items-center justify-around">
{Array.from({ length: 30 }).map((_, i) => (
<div key={i} className="w-2 sm:w-3 h-1.5 sm:h-2 bg-gray-700 rounded-sm" />
))}
</div>
<div className="relative overflow-hidden mx-auto" style={{ height: imageHeight + 20 }}>
<div className="flex absolute" style={{ paddingLeft: gap, gap: gap }}>
{Array.from({ length: 5 }).map((_, i) => (
<div
key={i}
className="flex-shrink-0 rounded-lg overflow-hidden bg-gray-700 animate-pulse"
style={{ width: imageWidth, height: imageHeight }}
/>
))}
</div>
</div>
<div className="absolute bottom-0 left-0 right-0 h-3 sm:h-4 bg-gray-900 hidden sm:flex items-center justify-around">
{Array.from({ length: 30 }).map((_, i) => (
<div key={i} className="w-2 sm:w-3 h-1.5 sm:h-2 bg-gray-700 rounded-sm" />
))}
</div>
<div className="absolute top-0 sm:top-4 bottom-0 sm:bottom-4 left-0 w-8 sm:w-24 bg-gradient-to-r from-gray-900 to-transparent pointer-events-none z-10" />
<div className="absolute top-0 sm:top-4 bottom-0 sm:bottom-4 right-0 w-8 sm:w-24 bg-gradient-to-l from-gray-900 to-transparent pointer-events-none z-10" />
</div>
);
}
return ( return (
<div className="relative w-full overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800 py-4 sm:py-8"> <div className="relative w-full overflow-hidden bg-gradient-to-b from-gray-900 to-gray-800 py-4 sm:py-8">
{/* Film strip effect - top perforation (hidden on very small screens) */} {/* Film strip effect - top perforation (hidden on very small screens) */}