[251119] feat: FloatingGradientLayer..Experimental..2

🕐 커밋 시간: 2025. 11. 19. 19:24:28

📊 변경 통계:
  • 총 파일: 10개
  • 추가: +95줄
  • 삭제: -181줄

📝 수정된 파일:
  ~ 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/DetailPanel/components/DetailPanelBackground/DetailPanelBackground.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.module.less
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx

🗑️ 삭제된 파일:
  - com.twin.app.shoptime/src/components/FloatingGradientBackground/FloatingGradientBackground.jsx
  - com.twin.app.shoptime/src/components/FloatingGradientBackground/FloatingGradientBackground.module.less

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
  • 소규모 기능 개선
  • 코드 정리 및 최적화
  • 모듈 구조 개선

Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-11-19 19:24:29 +09:00
parent d8dce0a89d
commit 276ee65979
10 changed files with 95 additions and 181 deletions

View File

@@ -46,7 +46,6 @@ import { enqueuePanelHistory } from '../actions/panelHistoryActions';
import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion'; import NotSupportedVersion from '../components/NotSupportedVersion/NotSupportedVersion';
import ToastContainer from '../components/TToast/ToastContainer'; import ToastContainer from '../components/TToast/ToastContainer';
import GlobalPopup from '../components/GlobalPopup/GlobalPopup'; import GlobalPopup from '../components/GlobalPopup/GlobalPopup';
import FloatingGradientBackground from '../components/FloatingGradientBackground/FloatingGradientBackground';
import usePrevious from '../hooks/usePrevious'; import usePrevious from '../hooks/usePrevious';
import { lunaTest } from '../lunaSend/lunaTest'; import { lunaTest } from '../lunaSend/lunaTest';
import { store } from '../store/store'; import { store } from '../store/store';
@@ -442,9 +441,6 @@ if (typeof Spotlight !== 'undefined' && Spotlight.addEventListener) {
function AppBase(props) { function AppBase(props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
// 그라데이션 배경 표시 상태 관리 (기본적으로 숨김)
const [showGradientBackground, setShowGradientBackground] = React.useState(false);
const httpHeader = useSelector((state) => state.common.httpHeader); const httpHeader = useSelector((state) => state.common.httpHeader);
const httpHeaderRef = useRef(httpHeader); const httpHeaderRef = useRef(httpHeader);
const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion); const webOSVersion = useSelector((state) => state.common.appStatus.webOSVersion);
@@ -896,24 +892,8 @@ function AppBase(props) {
); );
}, [dispatch, initService]); }, [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 ( return (
<ErrorBoundary> <ErrorBoundary>
{/* 항상 메모리에 로드되는 떠 있는 그라데이션 배경 */}
<FloatingGradientBackground visible={showGradientBackground} />
{webOSVersion === '' ? null : Number(webOSVersion) < 4 ? ( {webOSVersion === '' ? null : Number(webOSVersion) < 4 ? (
<NotSupportedVersion /> <NotSupportedVersion />
) : ( ) : (

View File

@@ -104,10 +104,13 @@ export const navigateToDetail = ({
}); });
// ✅ 그라데이션 배경 표시 - HomePanel→DetailPanel 전환 시 // ✅ 그라데이션 배경 표시 - HomePanel→DetailPanel 전환 시
if (window.showFloatingGradient) { dispatch(updateHomeInfo({
window.showFloatingGradient(); name: panel_names.HOME_PANEL,
console.log('[navigateToDetail] Floating gradient background shown'); panelInfo: {
} showGradientBackground: true,
}
}));
console.log('[TRACE-GRADIENT] 🟢 navigateToDetail set showGradientBackground: true - source:', sourceMenu);
// sourceMenu에 따른 사전 처리 // sourceMenu에 따른 사전 처리
switch (sourceMenu) { switch (sourceMenu) {

View File

@@ -1,60 +0,0 @@
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

@@ -1,65 +0,0 @@
@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

@@ -152,29 +152,37 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
useEffect(() => { useEffect(() => {
// DOM이 렌더링된 후 약간의 지연 시간을 두고 그라데이션 숨김 // DOM이 렌더링된 후 약간의 지연 시간을 두고 그라데이션 숨김
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (window.hideFloatingGradient) { dispatch(updateHomeInfo({
window.hideFloatingGradient(); name: panel_names.HOME_PANEL,
console.log('[DetailPanel] Floating gradient background hidden'); panelInfo: {
} showGradientBackground: false,
}
}));
console.log('[TRACE-GRADIENT] 🔴 DetailPanel mounted 100ms timeout set showGradientBackground: false');
}, 100); // 100ms 지연으로 DOM 렌더링 완료 후 실행 }, 100); // 100ms 지연으로 DOM 렌더링 완료 후 실행
return () => { return () => {
clearTimeout(timer); // 컴포넌트 언마운트 시 타이머 정리 clearTimeout(timer); // 컴포넌트 언마운트 시 타이머 정리
console.log('[TRACE-GRADIENT] 🔴 DetailPanel unmount - gradient timer cleared');
}; };
}, []); // 마운트 시 한 번만 실행 }, [dispatch]); // dispatch 포함
// ✅ [251118] DetailPanel이 사라질 때 HomePanel의 비디오 재생 활성화 // ✅ [251118] DetailPanel이 사라질 때 HomePanel 활성화
useEffect(() => { useEffect(() => {
return () => { return () => {
// DetailPanel이 unmount되는 시점 // DetailPanel이 unmount되는 시점
console.log('[DetailPanel] unmount - HomePanel 활성화 신호 전송'); console.log('[DetailPanel] unmount - HomePanel 활성화 신호 전송');
// HomePanel에서 비디오 재생을 다시 시작하도록 신호 보내기 // HomePanel에서 비디오 재생을 다시 시작하도록 신호 보내기
console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - Creating new panelInfo');
console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - Existing panelInfo before update:', JSON.stringify(panelInfo));
dispatch(updateHomeInfo({ dispatch(updateHomeInfo({
name: panel_names.HOME_PANEL, name: panel_names.HOME_PANEL,
panelInfo: { panelInfo: {
shouldResumeVideo: true, // ✅ 신호 shouldResumeVideo: true, // ✅ 신호
lastDetailPanelClosed: Date.now(), // ✅ 시점 기록 lastDetailPanelClosed: Date.now(), // ✅ 시점 기록
showGradientBackground: false, // ✅ 명시적으로 그라데이션 끔기
} }
})); }));
}; };

View File

@@ -43,14 +43,9 @@ export default function DetailPanelBackground({ launchedFromPlayer = false, patn
return BG_MAP[patnrId] || qvc; return BG_MAP[patnrId] || qvc;
}, [patnrId]); }, [patnrId]);
// ✅ [251119] 프리로드된 이미지 사용 로직 // ✅ [251119] 프리로드된 이미지 사용 로직 수정
// launchedFromPlayer와 상관없이 항상 배경 이미지를 표시하도록 수정
useEffect(() => { useEffect(() => {
if (launchedFromPlayer) {
// PlayerPanel에서 진입한 경우 이미지가 필요 없음
setImageReady(true);
return;
}
// 이미지가 프리로드되었는지 확인 // 이미지가 프리로드되었는지 확인
if (ImagePreloader.isLoaded(detailPanelBg)) { if (ImagePreloader.isLoaded(detailPanelBg)) {
console.log('[DetailPanelBackground] Using preloaded image:', detailPanelBg); console.log('[DetailPanelBackground] Using preloaded image:', detailPanelBg);
@@ -69,7 +64,7 @@ export default function DetailPanelBackground({ launchedFromPlayer = false, patn
setImageReady(true); setImageReady(true);
}); });
} }
}, [detailPanelBg, launchedFromPlayer]); }, [detailPanelBg]); // launchedFromPlayer 제거
useEffect(() => { useEffect(() => {
console.log('[DetailPanelBackground] 배경 이미지 경로:', detailPanelBg); console.log('[DetailPanelBackground] 배경 이미지 경로:', detailPanelBg);
@@ -82,15 +77,15 @@ export default function DetailPanelBackground({ launchedFromPlayer = false, patn
return ( return (
<div className={css.backgroundContainer}> <div className={css.backgroundContainer}>
{/* 이미지가 준비되지 않았을 때 placeholder 표시 */} {/* 이미지가 준비되지 않았을 때 placeholder 표시 */}
{!imageReady && !launchedFromPlayer && ( {!imageReady && (
<div <div
className={css.backgroundPlaceholder} className={css.backgroundPlaceholder}
aria-hidden="true" aria-hidden="true"
/> />
)} )}
{/* 실제 배경 이미지 - 프리로드된 경우 즉시 표시 */} {/* 실제 배경 이미지 - 항상 표시되도록 수정 */}
{!launchedFromPlayer && imageReady && ( {imageReady && (
<img <img
src={detailPanelBg} src={detailPanelBg}
alt="" alt=""
@@ -101,7 +96,7 @@ export default function DetailPanelBackground({ launchedFromPlayer = false, patn
/> />
)} )}
{/* 그라데이션 레이어들 - CSS의 linear-gradient를 div로 구현 */} {/* 그라데이션 레이어들 - launchedFromPlayer일 때만 추가 */}
{/* 1. 270도 방향 그라데이션 (왼쪽→오른쪽, 투명→불투명) */} {/* 1. 270도 방향 그라데이션 (왼쪽→오른쪽, 투명→불투명) */}
{launchedFromPlayer && ( {launchedFromPlayer && (
<div className={css.gradientLayer1} aria-hidden="true" /> <div className={css.gradientLayer1} aria-hidden="true" />

View File

@@ -94,9 +94,21 @@ export const TEMPLATE_CODE_CONF = {
PICK_FOR_YOU: 'DSP00106', PICK_FOR_YOU: 'DSP00106',
}; };
const HomePanel = ({ isOnTop }) => { const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
// ✅ showGradientBackground prop 변경 추적 로그
const prevShowGradientBackground = usePrevious(showGradientBackground);
useEffect(() => {
if (prevShowGradientBackground !== showGradientBackground) {
console.log('[TRACE-GRADIENT] 📊 HomePanel prop changed:', {
prev: prevShowGradientBackground,
current: showGradientBackground,
isOnTop: isOnTop
});
}
}, [showGradientBackground, prevShowGradientBackground, isOnTop]);
useDebugKey({ isLandingPage: true }); useDebugKey({ isLandingPage: true });
// 🔽 HomeBanner 외부 7개 아이콘들의 focusHistory 추적 // 🔽 HomeBanner 외부 7개 아이콘들의 focusHistory 추적
@@ -805,7 +817,8 @@ const HomePanel = ({ isOnTop }) => {
useEffect(() => { useEffect(() => {
if (detailPanelClosedTime && isOnTop) { if (detailPanelClosedTime && isOnTop) {
// if (isOnTop) { // if (isOnTop) {
console.log('@@[HomePanel] *** ✅ HomePanel isOnTop = true'); console.log('[TRACE-GRADIENT] 🔄 lastDetailPanelClosed triggered - HomePanel reactivated');
console.log('[HomePanel] *** ✅ HomePanel isOnTop = true');
console.log('[HomePanel] *** lastDetailPanelClosed:', detailPanelClosedTime); console.log('[HomePanel] *** lastDetailPanelClosed:', detailPanelClosedTime);
console.log('[HomePanel] *** isOnTop:', isOnTop); console.log('[HomePanel] *** isOnTop:', isOnTop);
console.log('[HomePanel] *** videoPlayIntentRef.current:', videoPlayIntentRef.current); console.log('[HomePanel] *** videoPlayIntentRef.current:', videoPlayIntentRef.current);
@@ -978,6 +991,12 @@ const HomePanel = ({ isOnTop }) => {
return ( return (
<> <>
{/* HomePanel용 메모리 상주 그라데이션 배경 */}
<div
className={classNames(css.gradientBackground, { [css.visible]: showGradientBackground })}
aria-hidden="true"
/>
<TPanel className={css.panel} onCancel={onCancel}> <TPanel className={css.panel} onCancel={onCancel}>
{homeLayoutInfo && ( {homeLayoutInfo && (
<TBody <TBody

View File

@@ -53,3 +53,24 @@
} }
} }
// HomePanel용 메모리 상주 단색 배경
.gradientBackground {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 20; // HomePanel 위, DetailPanel(21) 아래
overflow: hidden;
pointer-events: none; // 클릭 이벤트가 아래로 통과하도록
// 기본 상태에서는 숨김
display: none;
// 활성화 상태 - 전체 화면을 어둡게 하는 단색 배경
&.visible {
display: block;
background: rgba(0, 0, 0, 0.85); // 85% 투명도의 검은색으로 전체 화면 어둡게
}
}

View File

@@ -151,6 +151,7 @@ export default function MainView({ className, initService }) {
const { showLoadingPanel, toast, toastText, isLoading, webOSVersion, deviceId } = useSelector( const { showLoadingPanel, toast, toastText, isLoading, webOSVersion, deviceId } = useSelector(
(state) => state.common.appStatus (state) => state.common.appStatus
); );
const homeInfo = useSelector((state) => state.home.homeInfo);
const skipEndOfServicePopup = useSelector((state) => state.localSettings.skipEndOfServicePopup); const skipEndOfServicePopup = useSelector((state) => state.localSettings.skipEndOfServicePopup);
const isInternetConnected = useSelector((state) => state.common.appStatus.isInternetConnected); const isInternetConnected = useSelector((state) => state.common.appStatus.isInternetConnected);
@@ -269,7 +270,11 @@ export default function MainView({ className, initService }) {
(panels[0]?.name === Config.panel_names.PLAYER_PANEL || (panels[0]?.name === Config.panel_names.PLAYER_PANEL ||
panels[0]?.name === Config.panel_names.PLAYER_PANEL_NEW || panels[0]?.name === Config.panel_names.PLAYER_PANEL_NEW ||
panels[0]?.name === Config.panel_names.MEDIA_PANEL))) && ( panels[0]?.name === Config.panel_names.MEDIA_PANEL))) && (
<HomePanel key={Config.panel_names.HOME_PANEL} isOnTop={isHomeOnTop} /> <HomePanel
key={Config.panel_names.HOME_PANEL}
isOnTop={isHomeOnTop}
showGradientBackground={homeInfo?.panelInfo?.showGradientBackground || false}
/>
)} )}
{renderingPanels.map((panel, index) => { {renderingPanels.map((panel, index) => {
const Component = panelMap[panel.name]; const Component = panelMap[panel.name];
@@ -323,11 +328,17 @@ export default function MainView({ className, initService }) {
</> </>
); );
} else if (isHomeOnTop) { } else if (isHomeOnTop) {
return <HomePanel key={Config.panel_names.HOME_PANEL} isOnTop={isHomeOnTop} />; return (
<HomePanel
key={Config.panel_names.HOME_PANEL}
isOnTop={isHomeOnTop}
showGradientBackground={homeInfo?.panelInfo?.showGradientBackground || false}
/>
);
} }
return null; return null;
}, [panels, tabActivated, isHomeOnTop]); }, [panels, tabActivated, isHomeOnTop, homeInfo]);
const onTabActivated = useCallback((activated) => { const onTabActivated = useCallback((activated) => {
setTabActivated(activated); setTabActivated(activated);

View File

@@ -2006,15 +2006,17 @@ const MediaPanel = React.forwardRef(
const onEnded = useCallback((e) => { const onEnded = useCallback((e) => {
if (panelInfoRef.current.shptmBanrTpNm === 'MEDIA') { if (panelInfoRef.current.shptmBanrTpNm === 'MEDIA') {
dispatch( // ⚠️ 배경 설정 복원 취소 - DetailPanel 업데이트 없이 바로 패널 제거
updatePanel({ // dispatch(
name: panel_names.DETAIL_PANEL, // updatePanel({
panelInfo: { // name: panel_names.DETAIL_PANEL,
launchedFromPlayer: true, // panelInfo: {
isPlayerFinished: true, // launchedFromPlayer: true,
}, // isPlayerFinished: true,
}) // },
); // })
// );
console.log('[MediaPanel] 🚫 Skipping background restoration for ended media');
Spotlight.pause(); Spotlight.pause();
setTimeout(() => { setTimeout(() => {
Spotlight.resume(); Spotlight.resume();