[251119] feat: FloatingGradientLayer..Experimental
🕐 커밋 시간: 2025. 11. 19. 17:35:53 📊 변경 통계: • 총 파일: 8개 • 추가: +43줄 📁 추가된 파일: + com.twin.app.shoptime/src/components/FloatingGradientBackground/FloatingGradientBackground.jsx + com.twin.app.shoptime/src/components/FloatingGradientBackground/FloatingGradientBackground.module.less + com.twin.app.shoptime/src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.v2.jsx 📝 수정된 파일: ~ com.twin.app.shoptime/src/App/App.js ~ com.twin.app.shoptime/src/actions/panelActions.js ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.module.less 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • UI 컴포넌트 아키텍처 개선 • 소규모 기능 개선 • 모듈 구조 개선
This commit is contained in:
@@ -46,6 +46,7 @@ import { enqueuePanelHistory } from '../actions/panelHistoryActions';
|
||||
import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion';
|
||||
import ToastContainer from '../components/TToast/ToastContainer';
|
||||
import GlobalPopup from '../components/GlobalPopup/GlobalPopup';
|
||||
import FloatingGradientBackground from '../components/FloatingGradientBackground/FloatingGradientBackground';
|
||||
import usePrevious from '../hooks/usePrevious';
|
||||
import { lunaTest } from '../lunaSend/lunaTest';
|
||||
import { store } from '../store/store';
|
||||
@@ -441,6 +442,9 @@ if (typeof Spotlight !== 'undefined' && Spotlight.addEventListener) {
|
||||
|
||||
function AppBase(props) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// 그라데이션 배경 표시 상태 관리 (기본적으로 숨김)
|
||||
const [showGradientBackground, setShowGradientBackground] = React.useState(false);
|
||||
const httpHeader = useSelector((state) => state.common.httpHeader);
|
||||
const httpHeaderRef = useRef(httpHeader);
|
||||
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
|
||||
@@ -892,8 +896,24 @@ function AppBase(props) {
|
||||
);
|
||||
}, [dispatch, initService]);
|
||||
|
||||
// ✅ 그라데이션 배경 제어 함수를 전역에 노출
|
||||
useEffect(() => {
|
||||
window.toggleFloatingGradient = setShowGradientBackground;
|
||||
window.showFloatingGradient = () => setShowGradientBackground(true);
|
||||
window.hideFloatingGradient = () => setShowGradientBackground(false);
|
||||
|
||||
return () => {
|
||||
delete window.toggleFloatingGradient;
|
||||
delete window.showFloatingGradient;
|
||||
delete window.hideFloatingGradient;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
{/* 항상 메모리에 로드되는 떠 있는 그라데이션 배경 */}
|
||||
<FloatingGradientBackground visible={showGradientBackground} />
|
||||
|
||||
{webOSVersion === '' ? null : Number(webOSVersion) < 4 ? (
|
||||
<NotSupportedVersion />
|
||||
) : (
|
||||
|
||||
@@ -103,6 +103,12 @@ export const navigateToDetail = ({
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
// ✅ 그라데이션 배경 표시 - HomePanel→DetailPanel 전환 시
|
||||
if (window.showFloatingGradient) {
|
||||
window.showFloatingGradient();
|
||||
console.log('[navigateToDetail] Floating gradient background shown');
|
||||
}
|
||||
|
||||
// sourceMenu에 따른 사전 처리
|
||||
switch (sourceMenu) {
|
||||
case SOURCE_MENUS.HOME_BEST_SELLER:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import css from './FloatingGradientBackground.module.less';
|
||||
|
||||
/**
|
||||
* HomePanel과 분리된 별도의 그라데이션 배경
|
||||
* React Portal로 전역 body에 직접 렌더링됨
|
||||
* 항상 메모리에 로드되지만 visible prop으로 표시/숨김 제어
|
||||
*/
|
||||
export default function FloatingGradientBackground({ visible = false }) {
|
||||
// 전역 DOM에 렌더링할 container 참조
|
||||
const containerRef = useRef(null);
|
||||
|
||||
// portal container 생성 (마운트 시 한 번만)
|
||||
useEffect(() => {
|
||||
// body에 직접 div 추가
|
||||
const portalContainer = document.createElement('div');
|
||||
portalContainer.id = 'floating-gradient-background-container';
|
||||
portalContainer.style.position = 'fixed';
|
||||
portalContainer.style.top = '0';
|
||||
portalContainer.style.left = '0';
|
||||
portalContainer.style.width = '100%';
|
||||
portalContainer.style.height = '100%';
|
||||
portalContainer.style.pointerEvents = 'none';
|
||||
portalContainer.style.zIndex = '20'; // HomePanel과 DetailPanel 사이 (HomePanel < 20 < DetailPanel(21))
|
||||
|
||||
document.body.appendChild(portalContainer);
|
||||
containerRef.current = portalContainer;
|
||||
|
||||
console.log('[FloatingGradientBackground] Portal container created');
|
||||
|
||||
return () => {
|
||||
// 언마운트 시 portal container 제거
|
||||
if (containerRef.current && containerRef.current.parentNode) {
|
||||
containerRef.current.parentNode.removeChild(containerRef.current);
|
||||
console.log('[FloatingGradientBackground] Portal container removed');
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Portal로 렌더링될 그라데이션 컴포넌트
|
||||
const gradientContent = (
|
||||
<div className={`${css.gradientBackground} ${visible ? css.visible : ''}`} aria-hidden="true">
|
||||
{/* 1. 270도 방향 그라데이션 (왼쪽→오른쪽, 투명→불투명) */}
|
||||
<div className={css.gradientLayer1} />
|
||||
{/* 2. 180도 방향 그라데이션 (위→아래, 투명→불투명) */}
|
||||
<div className={css.gradientLayer2} />
|
||||
{/* 3. 투명 그라데이션 */}
|
||||
<div className={css.gradientLayer3} />
|
||||
</div>
|
||||
);
|
||||
|
||||
// container가 준비되면 portal로 렌더링
|
||||
if (!containerRef.current) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(gradientContent, containerRef.current);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
@import "../../style/CommonStyle.module.less";
|
||||
@import "../../style/utils.module.less";
|
||||
|
||||
// 그라데이션 배경 - 전역 body에 렌더링됨
|
||||
.gradientBackground {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none; // 클릭 이벤트가 아래로 통과하도록
|
||||
|
||||
// 기본 상태에서는 숨김
|
||||
display: none;
|
||||
|
||||
// 활성화 상태
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// 그라데이션 레이어 1: 270도 방향 (왼쪽→오른쪽, 투명→불투명)
|
||||
// linear-gradient(270deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.77) 70%, rgba(0, 0, 0, 1) 100%)
|
||||
.gradientLayer1 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.77) 70%,
|
||||
rgba(0, 0, 0, 1) 100%
|
||||
);
|
||||
z-index: 1; // container 내부 상대적 zIndex
|
||||
}
|
||||
|
||||
// 그라데이션 레이어 2: 180도 방향 (위→아래, 투명→불투명)
|
||||
// linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)
|
||||
.gradientLayer2 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 1) 100%
|
||||
);
|
||||
z-index: 2; // gradientLayer1보다 높게
|
||||
}
|
||||
|
||||
// 그라데이션 레이어 3: 투명 그라데이션
|
||||
// linear-gradient(0deg, rgba(0, 0, 0, 0))
|
||||
.gradientLayer3 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0));
|
||||
z-index: 3; // 가장 높은 레이어
|
||||
}
|
||||
@@ -148,6 +148,21 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// ✅ DOM 렌더링 후 그라데이션 배경 숨기기 - HomePanel→DetailPanel 전환 완료 시
|
||||
useEffect(() => {
|
||||
// DOM이 렌더링된 후 약간의 지연 시간을 두고 그라데이션 숨김
|
||||
const timer = setTimeout(() => {
|
||||
if (window.hideFloatingGradient) {
|
||||
window.hideFloatingGradient();
|
||||
console.log('[DetailPanel] Floating gradient background hidden');
|
||||
}
|
||||
}, 100); // 100ms 지연으로 DOM 렌더링 완료 후 실행
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer); // 컴포넌트 언마운트 시 타이머 정리
|
||||
};
|
||||
}, []); // 마운트 시 한 번만 실행
|
||||
|
||||
// ✅ [251118] DetailPanel이 사라질 때 HomePanel의 비디오 재생 활성화
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
// src/views/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.v2.jsx
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
|
||||
// 이미지 imports
|
||||
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';
|
||||
import css from './DetailPanelBackground.module.less';
|
||||
|
||||
// ==================== 로깅 함수들 ====================
|
||||
/**
|
||||
* DetailPanelBackgroundV2 초기화 로그
|
||||
* @param {number} patnrId - 파트너사 ID
|
||||
* @param {boolean} visible - 표시 여부
|
||||
* @param {string} imageUrl - 이미지 URL
|
||||
*/
|
||||
const logDetailPanelInit = (patnrId, visible, imageUrl) => {
|
||||
console.log(`[DetailPanelBackgroundV2] patnrId: ${patnrId}, visible: ${visible}, imageUrl: ${imageUrl}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 이미지 로드 성공 로그
|
||||
* @param {number} patnrId - 파트너사 ID
|
||||
*/
|
||||
const logImageLoaded = (patnrId) => {
|
||||
console.log(`[DetailPanelBackgroundV2] Image loaded: patnrId=${patnrId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 이미지 로드 실패 로그
|
||||
* @param {number} patnrId - 파트너사 ID
|
||||
* @param {Error} error - 에러 객체
|
||||
*/
|
||||
const logImageError = (patnrId, error) => {
|
||||
console.error(`[DetailPanelBackgroundV2] Image load failed: patnrId=${patnrId}`, error);
|
||||
};
|
||||
|
||||
/**
|
||||
* 개선된 배경 이미지 컴포넌트 v2
|
||||
* HomePanel에 미리 로드되어 메모리에 상주하며, visible props로 표시 여부만 제어
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {number} props.patnrId - 파트너사 ID
|
||||
* @param {boolean} props.visible - 표시 여부 (HomePanel이 isOnTop일 때 false)
|
||||
* @param {boolean} props.launchedFromPlayer - PlayerPanel에서 진입했는지 여부
|
||||
* @param {boolean} props.usePlaceholder - placeholder 표시 여부
|
||||
*/
|
||||
export default function DetailPanelBackgroundV2({
|
||||
patnrId,
|
||||
visible = true,
|
||||
launchedFromPlayer = false,
|
||||
usePlaceholder = false
|
||||
}) {
|
||||
// 파트너사별 배경 이미지 맵
|
||||
const BG_MAP = useMemo(() => ({
|
||||
1: qvc, // QVC
|
||||
2: hsn, // HSN
|
||||
4: ontv4u, // ONTV4U
|
||||
9: lgelectronics,// LG ELECTRONICS
|
||||
11: shoplc, // SHOPLC
|
||||
16: koreaKiosk, // KOREA KIOSK
|
||||
19: Pinkfong, // PINKFONG
|
||||
}), []);
|
||||
|
||||
const backgroundImageUrl = useMemo(() => {
|
||||
return BG_MAP[patnrId] || qvc; // 기본값은 QVC
|
||||
}, [patnrId, BG_MAP]);
|
||||
|
||||
// useCallback으로 메모이제이션된 핸들러
|
||||
const handleImageLoad = useCallback(() => {
|
||||
logImageLoaded(patnrId);
|
||||
}, [patnrId]);
|
||||
|
||||
const handleImageError = useCallback((e) => {
|
||||
logImageError(patnrId, e);
|
||||
}, [patnrId]);
|
||||
|
||||
// 개발 환경에서만 로깅
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// eslint-disable-next-line no-console
|
||||
logDetailPanelInit(patnrId, visible, backgroundImageUrl);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css.backgroundContainerV2}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
zIndex: 50, // HomePanel(z-index: 1)보다 높고, DetailPanel(z-index: 100)보다 낮음
|
||||
visibility: visible ? 'visible' : 'hidden',
|
||||
opacity: visible ? 1 : 0,
|
||||
transition: 'opacity 0.3s ease-in-out, visibility 0.3s ease-in-out',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
{/* PlayerPanel에서 진입한 경우 이미지를 표시하지 않고 그라데이션만 표시 */}
|
||||
{launchedFromPlayer ? (
|
||||
// 그라데이션 레이어들만 표시
|
||||
<>
|
||||
{/* 1. 270도 방향 그라데이션 (왼쪽→오른쪽, 투명→불투명) */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(270deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.77) 70%, rgba(0, 0, 0, 1) 100%)',
|
||||
zIndex: 3,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{/* 2. 180도 방향 그라데이션 (위→아래, 투명→불투명) */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 100%)',
|
||||
zIndex: 4,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{/* 3. 투명 그라데이션 */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(0deg, rgba(0, 0, 0, 0))',
|
||||
zIndex: 5,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</>
|
||||
) : usePlaceholder ? (
|
||||
// placeholder 모드
|
||||
<div
|
||||
className={css.backgroundPlaceholder}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%)',
|
||||
zIndex: 2,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
// 실제 배경 이미지
|
||||
<img
|
||||
src={backgroundImageUrl}
|
||||
alt=""
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
zIndex: 2,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* HomePanel에서 사용할 모든 배경 이미지 미리 로딩 컴포넌트
|
||||
* HomePanel이 렌더링될 때 모든 파트너사 배경 이미지를 미리 로드하여 메모리에 상주시킴
|
||||
*/
|
||||
export function PreloadedBackgroundImages({
|
||||
selectedPatnrId,
|
||||
isHomePanelOnTop = true,
|
||||
launchedFromPlayer = false
|
||||
}) {
|
||||
// 모든 파트너사 ID 목록
|
||||
const allPatnrIds = useMemo(() => [1, 2, 4, 9, 11, 16, 19], []);
|
||||
|
||||
// ✅ 원래 로직 복원: HomePanel이 onTop이 아니고 selectedPatnrId가 있을 때만 배경 표시
|
||||
const shouldShowBackground = !isHomePanelOnTop && selectedPatnrId;
|
||||
|
||||
// ✅ 디버깅 로그 추가
|
||||
useMemo(() => {
|
||||
console.log('[PreloadedBackgroundImages] Debug info:', {
|
||||
selectedPatnrId,
|
||||
isHomePanelOnTop,
|
||||
launchedFromPlayer,
|
||||
shouldShowBackground,
|
||||
allPatnrIds
|
||||
});
|
||||
}, [selectedPatnrId, isHomePanelOnTop, launchedFromPlayer, shouldShowBackground]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{allPatnrIds.map((patnrId) => {
|
||||
// ✅ 원래 로직: DetailPanel에서 선택된 patnrId와 일치하는 배경만 표시
|
||||
const isVisible = shouldShowBackground && patnrId === selectedPatnrId;
|
||||
console.log(`[PreloadedBackgroundImages] patnrId ${patnrId}, visible: ${isVisible}`);
|
||||
|
||||
return (
|
||||
<DetailPanelBackgroundV2
|
||||
key={`bg-${patnrId}`}
|
||||
patnrId={patnrId}
|
||||
visible={isVisible}
|
||||
launchedFromPlayer={launchedFromPlayer}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -168,6 +168,7 @@ const HomePanel = ({ isOnTop }) => {
|
||||
const verticalPagenatorRef = useRef(null);
|
||||
const currentSentMenuRef = useRef(null);
|
||||
|
||||
|
||||
// ✅ [251119] DetailPanelBackground 이미지 프리로딩
|
||||
// HomePanel 마운트 시 백그라운드로 모든 파트너사 배경 이미지를 미리 로드하여
|
||||
// DetailPanel 진입 시 로딩 지연을 방지함
|
||||
|
||||
@@ -52,3 +52,4 @@
|
||||
top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user