[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:
2025-11-19 16:45:56 +09:00
parent db109f77c1
commit e797a8a399
5 changed files with 238 additions and 7 deletions

View 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;

View File

@@ -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]);
// ✅ [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=""

View File

@@ -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도 방향 (왼쪽→오른쪽, 투명→불투명)

View File

@@ -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(

View File

@@ -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 자동 다음 단계로 이동