diff --git a/com.twin.app.shoptime/src/utils/ImagePreloader.js b/com.twin.app.shoptime/src/utils/ImagePreloader.js new file mode 100644 index 00000000..ab11bee8 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/ImagePreloader.js @@ -0,0 +1,122 @@ +/** + * 이미지 프리로더 유틸리티 + * HomePanel에서 백그라운드로 DetailPanelBackground 이미지들을 미리 로드하여 + * DetailPanel 진입 시 로딩 지연을 방지 + */ +class ImagePreloader { + constructor() { + this.cache = new Map(); // 로드된 이미지 캐시 + this.loadPromises = new Map(); // 로딩 중인 Promise 관리 + this.preloadStarted = false; // 프리로딩 시작 여부 + } + + /** + * 단일 이미지 프리로드 + * @param {string} src - 이미지 경로 + * @returns {Promise} + */ + preloadImage(src) { + // 이미 캐시된 경우 즉시 반환 + if (this.cache.has(src)) { + return Promise.resolve(this.cache.get(src)); + } + + // 현재 로딩 중인 Promise가 있으면 재사용 + if (this.loadPromises.has(src)) { + return this.loadPromises.get(src); + } + + // 새로운 이미지 로드 Promise 생성 + const promise = new Promise((resolve, reject) => { + const img = new window.Image(); // ESLint 해결을 위해 window.Image 사용 + + img.onload = () => { + this.cache.set(src, img); + this.loadPromises.delete(src); + console.log(`[ImagePreloader] Image loaded: ${src}`); + resolve(img); + }; + + img.onerror = () => { + this.loadPromises.delete(src); + console.error(`[ImagePreloader] Failed to load: ${src}`); + reject(new Error(`Failed to load image: ${src}`)); + }; + + // 이미지 로드 시작 + img.src = src; + }); + + this.loadPromises.set(src, promise); + return promise; + } + + /** + * 여러 이미지 한꺼번에 프리로드 + * @param {Object} imageMap - { patnrId: imagePath } 형태의 맵 + * @returns {Promise} - 로드 결과 배열 + */ + preloadAllImages(imageMap) { + if (this.preloadStarted) { + console.log('[ImagePreloader] Preloading already started'); + return Promise.resolve([]); + } + + this.preloadStarted = true; + console.log('[ImagePreloader] Starting background preload...'); + + const promises = Object.values(imageMap).map(src => + this.preloadImage(src).catch(error => { + // 개별 이미지 로드 실패 시 전체 작업을 중단하지 않음 + console.warn('[ImagePreloader] Single image load failed:', error.message); + return null; + }) + ); + + return Promise.all(promises); + } + + /** + * 이미지가 로드되었는지 확인 + * @param {string} src - 이미지 경로 + * @returns {boolean} + */ + isLoaded(src) { + return this.cache.has(src); + } + + /** + * 캐시된 이미지 가져오기 + * @param {string} src - 이미지 경로 + * @returns {HTMLImageElement|null} + */ + getCachedImage(src) { + return this.cache.get(src) || null; + } + + /** + * 캐시 통계 정보 + * @returns {Object} + */ + getStats() { + return { + cached: this.cache.size, + loading: this.loadPromises.size, + preloadStarted: this.preloadStarted + }; + } + + /** + * 캐시 초기화 (테스트용) + */ + clearCache() { + this.cache.clear(); + this.loadPromises.clear(); + this.preloadStarted = false; + } +} + +// 싱글톤 인스턴스 생성 +const imagePreloader = new ImagePreloader(); + +export default imagePreloader; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx index 5407bdcf..1abb1205 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx @@ -2,8 +2,11 @@ import React, { useEffect, useMemo, + useState, } from 'react'; +import ImagePreloader from '../../../../utils/ImagePreloader'; + import hsn from '../../../../../assets/images/bg/hsn_new.png'; import koreaKiosk from '../../../../../assets/images/bg/koreaKiosk_new.png'; import lgelectronics @@ -24,10 +27,7 @@ import css from './DetailPanelBackground.module.less'; * - 이 값에 따라 배경 UI를 다르게 표시할 수 있음 */ export default function DetailPanelBackground({ launchedFromPlayer = false, patnrId }) { - useEffect(() => { - console.log('[DetailPanelBackground] 배경 이미지 경로:', detailPanelBg); - console.log('[DetailPanelBackground] launchedFromPlayer:', launchedFromPlayer); - }, [launchedFromPlayer]); + const [imageReady, setImageReady] = useState(false); const BG_MAP = { 1: qvc, @@ -43,12 +43,54 @@ export default function DetailPanelBackground({ launchedFromPlayer = false, patn return BG_MAP[patnrId] || qvc; }, [patnrId]); - //partnrId 1 = QVC, 2 = HSN, 4 = ONTV, 9 = LG ELECTRONICS, 11 = SHOPLC, 19 = PINKPONG, 16 = KOREA KIOSK, + // ✅ [251119] 프리로드된 이미지 사용 로직 + useEffect(() => { + if (launchedFromPlayer) { + // PlayerPanel에서 진입한 경우 이미지가 필요 없음 + setImageReady(true); + return; + } + + // 이미지가 프리로드되었는지 확인 + if (ImagePreloader.isLoaded(detailPanelBg)) { + console.log('[DetailPanelBackground] Using preloaded image:', detailPanelBg); + setImageReady(true); + } else { + // 프리로드되지 않았다면 즉시 로드 시도 + console.log('[DetailPanelBackground] Image not preloaded, loading on-demand:', detailPanelBg); + ImagePreloader.preloadImage(detailPanelBg) + .then(() => { + console.log('[DetailPanelBackground] On-demand image loaded:', detailPanelBg); + setImageReady(true); + }) + .catch((e) => { + console.error('[DetailPanelBackground] On-demand image load failed:', e); + // 실패해도 이미지를 표시해야 함 + setImageReady(true); + }); + } + }, [detailPanelBg, launchedFromPlayer]); + + useEffect(() => { + console.log('[DetailPanelBackground] 배경 이미지 경로:', detailPanelBg); + console.log('[DetailPanelBackground] launchedFromPlayer:', launchedFromPlayer); + console.log('[DetailPanelBackground] imageReady:', imageReady); + }, [detailPanelBg, launchedFromPlayer, imageReady]); + + //partnrId 1 = QVC, 2 = HSN, 4 = ONTV, 9 = LG ELECTRONICS, 11 = SHOPLC, 19 = PINKPONG, 16 = KOREA KIOSK, return (
- {/* 실제 배경 이미지 */} - {!launchedFromPlayer && ( + {/* 이미지가 준비되지 않았을 때 placeholder 표시 */} + {!imageReady && !launchedFromPlayer && ( +