[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:
2025-11-19 17:35:54 +09:00
parent e797a8a399
commit d8dce0a89d
8 changed files with 398 additions and 0 deletions

View File

@@ -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 />
) : (

View File

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

View File

@@ -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);
}

View File

@@ -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; // 가장 높은 레이어
}

View File

@@ -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 () => {

View File

@@ -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}
/>
);
})}
</>
);
}

View File

@@ -168,6 +168,7 @@ const HomePanel = ({ isOnTop }) => {
const verticalPagenatorRef = useRef(null);
const currentSentMenuRef = useRef(null);
// ✅ [251119] DetailPanelBackground 이미지 프리로딩
// HomePanel 마운트 시 백그라운드로 모든 파트너사 배경 이미지를 미리 로드하여
// DetailPanel 진입 시 로딩 지연을 방지함

View File

@@ -52,3 +52,4 @@
top: 20px;
}
}