[251119] fix: HomePanel,DetailPanel PreLoadImages
🕐 커밋 시간: 2025. 11. 19. 16:45:55 📊 변경 통계: • 총 파일: 5개 • 추가: +116줄 • 삭제: -7줄 📁 추가된 파일: + com.twin.app.shoptime/src/utils/ImagePreloader.js 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.module.less ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx 🔧 주요 변경 내용: • UI 컴포넌트 아키텍처 개선 • 공통 유틸리티 함수 최적화 • 중간 규모 기능 개선
This commit is contained in:
122
com.twin.app.shoptime/src/utils/ImagePreloader.js
Normal file
122
com.twin.app.shoptime/src/utils/ImagePreloader.js
Normal file
@@ -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<HTMLImageElement>}
|
||||
*/
|
||||
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<Array>} - 로드 결과 배열
|
||||
*/
|
||||
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;
|
||||
@@ -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 (
|
||||
<div className={css.backgroundContainer}>
|
||||
{/* 실제 배경 이미지 */}
|
||||
{!launchedFromPlayer && (
|
||||
{/* 이미지가 준비되지 않았을 때 placeholder 표시 */}
|
||||
{!imageReady && !launchedFromPlayer && (
|
||||
<div
|
||||
className={css.backgroundPlaceholder}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 실제 배경 이미지 - 프리로드된 경우 즉시 표시 */}
|
||||
{!launchedFromPlayer && imageReady && (
|
||||
<img
|
||||
src={detailPanelBg}
|
||||
alt=""
|
||||
|
||||
@@ -13,6 +13,19 @@
|
||||
pointer-events: none; // 클릭 이벤트가 아래로 통과하도록
|
||||
}
|
||||
|
||||
// 배경 이미지 로딩 전 placeholder
|
||||
.backgroundPlaceholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
|
||||
z-index: 2;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// 실제 배경 이미지
|
||||
.backgroundImage {
|
||||
position: absolute;
|
||||
@@ -23,6 +36,8 @@
|
||||
object-fit: cover; // 화면 크기에 맞춰 이미지 조정
|
||||
object-position: center;
|
||||
z-index: 2;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
// 그라데이션 레이어 1: 270도 방향 (왼쪽→오른쪽, 투명→불투명)
|
||||
|
||||
@@ -44,6 +44,27 @@ import useDebugKey from '../../hooks/useDebugKey';
|
||||
import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useVideoPlay } from '../../hooks/useVideoPlay/useVideoPlay';
|
||||
import ImagePreloader from '../../utils/ImagePreloader';
|
||||
|
||||
// DetailPanelBackground 이미지 imports for preloading
|
||||
import hsn from '../../../assets/images/bg/hsn_new.png';
|
||||
import koreaKiosk from '../../../assets/images/bg/koreaKiosk_new.png';
|
||||
import lgelectronics from '../../../assets/images/bg/lgelectronics_new.png';
|
||||
import ontv4u from '../../../assets/images/bg/ontv4u_new.png';
|
||||
import Pinkfong from '../../../assets/images/bg/Pinkfong_new.png';
|
||||
import qvc from '../../../assets/images/bg/qvc_new.png';
|
||||
import shoplc from '../../../assets/images/bg/shoplc_new.png';
|
||||
|
||||
// 파트너사별 배경 이미지 맵
|
||||
const BACKGROUND_IMAGES = {
|
||||
1: qvc, // QVC
|
||||
2: hsn, // HSN
|
||||
4: ontv4u, // ONTV
|
||||
9: lgelectronics, // LG ELECTRONICS
|
||||
11: shoplc, // SHOPLC
|
||||
16: koreaKiosk, // KOREA KIOSK
|
||||
19: Pinkfong, // PINKFONG
|
||||
};
|
||||
// [COMMENTED OUT] useVideoMove 관련 코드 주석 처리 - 향후 사용 검토 필요
|
||||
// import { useVideoMove } from '../../hooks/useVideoTransition/useVideoMove';
|
||||
import {
|
||||
@@ -147,6 +168,30 @@ const HomePanel = ({ isOnTop }) => {
|
||||
const verticalPagenatorRef = useRef(null);
|
||||
const currentSentMenuRef = useRef(null);
|
||||
|
||||
// ✅ [251119] DetailPanelBackground 이미지 프리로딩
|
||||
// HomePanel 마운트 시 백그라운드로 모든 파트너사 배경 이미지를 미리 로드하여
|
||||
// DetailPanel 진입 시 로딩 지연을 방지함
|
||||
useEffect(() => {
|
||||
console.log('[HomePanel] Starting background image preloading...');
|
||||
|
||||
// HomePanel의 다른 기능들에 영향을 주지 않도록 비동기로 조용히 실행
|
||||
setTimeout(() => {
|
||||
ImagePreloader.preloadAllImages(BACKGROUND_IMAGES)
|
||||
.then((results) => {
|
||||
const successCount = results.filter(r => r !== null).length;
|
||||
console.log(`[HomePanel] Background images preloaded: ${successCount}/${results.length} images`);
|
||||
|
||||
// 프리로딩 통계 정보 로깅 (디버깅용)
|
||||
const stats = ImagePreloader.getStats();
|
||||
console.log('[HomePanel] Preloader stats:', stats);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('[HomePanel] Background image preloading failed:', error);
|
||||
// 프리로딩 실패가 HomePanel 기능에 영향을 주지 않도록 조용히 처리
|
||||
});
|
||||
}, 1000); // HomePanel 안정화 후 1초 뒤 시작
|
||||
}, []); // 마운트 시 한 번만 실행
|
||||
|
||||
useEffect(() => {
|
||||
if (nowMenu === 'Home/Top') {
|
||||
dispatch(
|
||||
|
||||
@@ -2157,6 +2157,12 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
return;
|
||||
}
|
||||
|
||||
// modal === false일 때만 실행
|
||||
if (panelInfo?.modal) {
|
||||
prevTabIndexV2.current = tabIndexV2;
|
||||
return;
|
||||
}
|
||||
|
||||
// tabIndexV2가 1에서 2로 정확하게 변경되는 시점만 감지
|
||||
const isTransitionedTo2 = prevTabIndexV2.current === 1 && tabIndexV2 === 2;
|
||||
prevTabIndexV2.current = tabIndexV2;
|
||||
@@ -2194,6 +2200,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
|
||||
tabIndexV2,
|
||||
belowContentsVisible,
|
||||
videoVerticalVisible,
|
||||
panelInfo?.modal,
|
||||
]);
|
||||
|
||||
// TabIndex 1 자동 다음 단계로 이동
|
||||
|
||||
Reference in New Issue
Block a user