[251120] fix: PlayerPanel Return Video Playback

🕐 커밋 시간: 2025. 11. 20. 12:29:35

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +270줄
  • 삭제: -97줄

📝 수정된 파일:
  ~ 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/PlayerPanel/PlayerPanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/ShopNowContents.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 대규모 기능 개발
This commit is contained in:
2025-11-20 12:29:36 +09:00
parent e21f6a1072
commit 8644527502
5 changed files with 268 additions and 95 deletions

View File

@@ -17,6 +17,8 @@ export const SOURCE_MENUS = {
HOME_GENERAL: 'home_general', HOME_GENERAL: 'home_general',
THEMED_PRODUCT: 'themed_product', THEMED_PRODUCT: 'themed_product',
GENERAL_PRODUCT: 'general_product', GENERAL_PRODUCT: 'general_product',
PLAYER_SHOP_NOW: 'player_shop_now', // PlayerPanel의 ShopNow에서 진입
PLAYER_MEDIA: 'player_media', // PlayerPanel의 Media에서 진입
}; };
/* /*
@@ -218,6 +220,7 @@ export const navigateToDetail = ({
}, },
}) })
); );
panelInfo.sourcePanel = panel_names.HOME_PANEL; // ✅ source panel 정보
panelInfo.fromHome = true; panelInfo.fromHome = true;
break; break;
} }
@@ -236,14 +239,35 @@ export const navigateToDetail = ({
}) })
); );
} }
panelInfo.sourcePanel = panel_names.SEARCH_PANEL; // ✅ source panel 정보
panelInfo.fromSearch = true; panelInfo.fromSearch = true;
panelInfo.searchQuery = additionalInfo.searchVal; panelInfo.searchQuery = additionalInfo.searchVal;
break; break;
case SOURCE_MENUS.THEMED_PRODUCT: case SOURCE_MENUS.THEMED_PRODUCT:
// 테마 상품: 별도 처리 필요할 경우 // 테마 상품: 별도 처리 필요할 경우
panelInfo.sourcePanel = panel_names.HOME_PANEL; // ✅ source panel 정보 (HOME으로 간주)
break; break;
case SOURCE_MENUS.PLAYER_SHOP_NOW:
case SOURCE_MENUS.PLAYER_MEDIA: {
// PlayerPanel에서 온 경우
const { hidePlayerOverlays } = require('./videoPlayActions');
// DetailPanel push 전에 VideoPlayer 오버레이 숨김
dispatch(hidePlayerOverlays());
// 현재 포커스된 요소 저장
if (Object.keys(focusSnapshot).length > 0) {
panelInfo.lastFocusedTargetId = focusSnapshot.lastFocusedTargetId;
}
// PlayerPanel 정보 보존 (복귀 시 필요)
panelInfo.sourcePanel = panel_names.PLAYER_PANEL; // ✅ source panel 정보
panelInfo.fromPlayer = true;
break;
}
case SOURCE_MENUS.GENERAL_PRODUCT: case SOURCE_MENUS.GENERAL_PRODUCT:
default: default:
// 일반 상품: 기본 처리 // 일반 상품: 기본 처리

View File

@@ -31,6 +31,8 @@ import {
finishVideoPreview, finishVideoPreview,
pauseFullscreenVideo, pauseFullscreenVideo,
resumeFullscreenVideo, resumeFullscreenVideo,
pauseModalVideo,
resumeModalVideo,
} from '../../actions/playActions'; } from '../../actions/playActions';
import { import {
clearProductDetail, clearProductDetail,
@@ -159,62 +161,175 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
})); }));
}, [dispatch]); }, [dispatch]);
// ✅ [251118] DetailPanel이 사라질 때 HomePanel 활성화 // ✅ [251120] DetailPanel이 사라질 때 처리 - sourcePanel에 따라 switch 문으로 처리
useEffect(() => { useEffect(() => {
return () => { return () => {
const sourcePanel = panelInfo?.sourcePanel;
const sourceMenu = panelInfo?.sourceMenu;
// DetailPanel이 unmount되는 시점 // DetailPanel이 unmount되는 시점
console.log('[DetailPanel] unmount - HomePanel 활성화 신호 전송'); console.log('[DetailPanel] unmount:', {
sourcePanel,
sourceMenu,
timestamp: Date.now(),
});
// HomePanel에서 비디오 재생을 다시 시작하도록 신호 보내기 // sourcePanel에 따른 상태 업데이트
console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - Creating new panelInfo'); switch (sourcePanel) {
console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - Existing panelInfo before update:', JSON.stringify(panelInfo)); case panel_names.PLAYER_PANEL: {
// PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달
dispatch(updateHomeInfo({ console.log('[DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달');
name: panel_names.HOME_PANEL, dispatch(updatePanel({
panelInfo: { name: panel_names.PLAYER_PANEL,
shouldResumeVideo: true, // ✅ 신호 panelInfo: {
lastDetailPanelClosed: Date.now(), // ✅ 시점 기록 detailPanelClosed: true, // ✅ flag
showGradientBackground: false, // ✅ 명시적으로 그라데이션 끔기 detailPanelClosedAt: Date.now(), // ✅ 시점 기록
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
}
}));
break;
} }
}));
case panel_names.HOME_PANEL: {
// HomePanel에서 온 경우: HomePanel에 detailPanelClosed flag 전달
console.log('[DetailPanel] unmount - HomePanel에 detailPanelClosed flag 전달');
console.log('[TRACE-GRADIENT] 🔶 DetailPanel unmount - HomePanel 복귀');
dispatch(updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: {
detailPanelClosed: true, // ✅ flag
detailPanelClosedAt: Date.now(), // ✅ 시점 기록
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
showGradientBackground: false, // ✅ 명시적으로 그라데이션 끔기
}
}));
break;
}
case panel_names.SEARCH_PANEL: {
// SearchPanel에서 온 경우: SearchPanel에 detailPanelClosed flag 전달
console.log('[DetailPanel] unmount - SearchPanel에 detailPanelClosed flag 전달');
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
detailPanelClosed: true, // ✅ flag
detailPanelClosedAt: Date.now(), // ✅ 시점 기록
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
},
})
);
break;
}
default:
console.warn('[DetailPanel] unmount - 처리되지 않은 sourcePanel:', sourcePanel);
break;
}
}; };
}, [dispatch]); }, [dispatch, panelInfo?.sourcePanel]);
const onBackClick = useCallback( const onBackClick = useCallback(
(isCancelClick) => (ev) => { (isCancelClick) => (ev) => {
const sourcePanel = panelInfo?.sourcePanel;
const sourceMenu = panelInfo?.sourceMenu;
fp.pipe( fp.pipe(
() => { () => {
dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거 dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거
dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 // sourcePanel에 따른 사전 처리
dispatch(finishVideoPreview()); switch (sourcePanel) {
case panel_names.PLAYER_PANEL:
// PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리
console.log('[DetailPanel] onBackClick - PlayerPanel 출신: 모달 정리만 수행');
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview());
break;
case panel_names.HOME_PANEL:
case panel_names.SEARCH_PANEL:
default:
// HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지
console.log('[DetailPanel] onBackClick - source panel:', sourcePanel, '백그라운드 비디오 일시 중지');
dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview());
break;
}
dispatch(popPanel(panel_names.DETAIL_PANEL)); dispatch(popPanel(panel_names.DETAIL_PANEL));
}, },
() => { () => {
// 패널 업데이트 조건 체크 // sourcePanel에 따른 상태 업데이트
const shouldUpdatePanel = switch (sourcePanel) {
fp.pipe( case panel_names.PLAYER_PANEL: {
() => panels, // PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달
fp.get('length'), const shouldUpdatePanel =
(length) => length === 4 fp.pipe(
)() && () => panels,
fp.pipe( fp.get('length'),
() => panels, (length) => length === 3 // PlayerPanel이 [1]에 있고 DetailPanel이 [2]에 있는 상태
fp.get('1.name'), )() &&
(name) => name === panel_names.PLAYER_PANEL fp.pipe(
)(); () => panels,
fp.get('1.name'),
(name) => name === panel_names.PLAYER_PANEL
)();
if (shouldUpdatePanel) { if (shouldUpdatePanel) {
dispatch( console.log('[DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달');
updatePanel({ dispatch(
name: panel_names.PLAYER_PANEL, updatePanel({
name: panel_names.PLAYER_PANEL,
panelInfo: {
thumbnail: fp.pipe(() => panelInfo, fp.get('thumbnailUrl'))(),
detailPanelClosed: true, // ✅ flag
detailPanelClosedAt: Date.now(), // ✅ 시점 기록
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
},
})
);
}
break;
}
case panel_names.HOME_PANEL: {
// HomePanel에서 온 경우: HomePanel에 detailPanelClosed flag 전달
console.log('[DetailPanel] onBackClick - HomePanel에 detailPanelClosed flag 전달');
dispatch(updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: { panelInfo: {
thumbnail: fp.pipe(() => panelInfo, fp.get('thumbnailUrl'))(), detailPanelClosed: true, // ✅ flag
}, detailPanelClosedAt: Date.now(), // ✅ 시점 기록
}) detailPanelClosedFromSource: sourceMenu, // ✅ 출처
); showGradientBackground: false,
}
}));
break;
}
case panel_names.SEARCH_PANEL: {
// SearchPanel에서 온 경우: SearchPanel에 detailPanelClosed flag 전달
console.log('[DetailPanel] onBackClick - SearchPanel에 detailPanelClosed flag 전달');
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
detailPanelClosed: true, // ✅ flag
detailPanelClosedAt: Date.now(), // ✅ 시점 기록
detailPanelClosedFromSource: sourceMenu, // ✅ 출처
},
})
);
break;
}
default:
console.warn('[DetailPanel] onBackClick - 처리되지 않은 sourcePanel:', sourcePanel);
break;
} }
// PlayerPanel의 isOnTop useEffect가 자동으로 오버레이 표시
} }
)(); )();
@@ -725,46 +840,48 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
// 백그라운드 전체화면 비디오 제어: DetailPanel 진입/퇴장 시 // 백그라운드 전체화면 비디오 제어: DetailPanel 진입/퇴장 시
useEffect(() => { useEffect(() => {
// console.log('[BgVideo] DetailPanel mounted - checking panels:', { // PlayerPanel이 존재하는지 확인 (Modal 또는 Fullscreen)
// panelsCount: panels?.length, const playerPanel = panels.find(
// panels: panels?.map(p => ({ name: p.name, modal: p.panelInfo?.modal })) (panel) => panel.name === panel_names.PLAYER_PANEL
// }); );
const hasPlayerPanel = !!playerPanel;
// 전체화면 PlayerPanel(modal=false)이 존재하는지 확인 const isModal = playerPanel?.panelInfo?.modal;
const hasFullscreenPlayerPanel = fp.pipe(
() => panels,
(panelList) =>
panelList.some(
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
)
)();
// ProductAllSection에 비디오가 있는지 확인 // ProductAllSection에 비디오가 있는지 확인
const hasProductVideo = fp.pipe(() => productData, fp.get('prdtMediaUrl'), fp.isNotNil)(); const hasProductVideo = fp.pipe(() => productData, fp.get('prdtMediaUrl'), fp.isNotNil)();
// console.log('[BgVideo] hasFullscreenPlayerPanel:', hasFullscreenPlayerPanel); console.log('[BgVideo] DetailPanel - Video Control Check:', {
// console.log('[BgVideo] hasProductVideo:', hasProductVideo); hasPlayerPanel,
isModal,
hasProductVideo,
sourceMenu: panelInfo?.sourceMenu
});
// 전체화면 PlayerPanel이 있고, 제품에 비디오가 있을 때만 백그라운드 비디오 멈춤 // PlayerPanel이 있고, 제품에 비디오가 있을 때만 비디오 멈춤
if (hasFullscreenPlayerPanel && hasProductVideo) { if (hasPlayerPanel && hasProductVideo) {
// console.log('[BgVideo] DetailPanel - Product has video, dispatching pauseFullscreenVideo()'); console.log('[BgVideo] DetailPanel - Pausing video');
dispatch(pauseFullscreenVideo()); if (isModal) {
dispatch(pauseModalVideo());
} else {
dispatch(pauseFullscreenVideo());
}
} else { } else {
console.log('[BgVideo] DetailPanel - Skipping pause:', { console.log('[BgVideo] DetailPanel - Skipping pause');
reason: !hasFullscreenPlayerPanel ? 'no fullscreen PlayerPanel' : 'no product video',
});
} }
return () => { return () => {
// DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개 // DetailPanel 언마운트 시: 비디오가 있었고 멈췄던 경우만 재생 재개
// console.log('[BgVideo] DetailPanel unmounting'); if (hasPlayerPanel && hasProductVideo) {
if (hasFullscreenPlayerPanel && hasProductVideo) { console.log('[BgVideo] DetailPanel - Resuming video');
// console.log('[BgVideo] DetailPanel - Product had video, dispatching resumeFullscreenVideo()'); if (isModal) {
dispatch(resumeFullscreenVideo()); dispatch(resumeModalVideo());
} else {
dispatch(resumeFullscreenVideo());
}
} }
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 마운트/언마운트 시에만 실행 }, [panelInfo?.sourceMenu, productData?.prdtMediaUrl]);
// MediaPanel modal 상태 변화 감지 -> ProductVideo로 포커스 이동 // MediaPanel modal 상태 변화 감지 -> ProductVideo로 포커스 이동
useEffect(() => { useEffect(() => {

View File

@@ -809,17 +809,20 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
console.log('[HomeActive] 재생 기록 업데이트:', bannerId); console.log('[HomeActive] 재생 기록 업데이트:', bannerId);
}, [isOnTop, dispatch]); }, [isOnTop, dispatch]);
// ✅ [251118] DetailPanel 닫힘 감지 useEffect // ✅ [251120] DetailPanel 닫힘 감지 useEffect - detailPanelClosed flag 사용
const detailPanelClosed = useSelector(
(state) => state.home.homeInfo?.panelInfo?.detailPanelClosed
);
const detailPanelClosedTime = useSelector( const detailPanelClosedTime = useSelector(
(state) => state.home.homeInfo?.panelInfo?.lastDetailPanelClosed (state) => state.home.homeInfo?.panelInfo?.detailPanelClosedAt
); );
useEffect(() => { useEffect(() => {
if (detailPanelClosedTime && isOnTop) { if (detailPanelClosed && isOnTop) {
// if (isOnTop) { console.log('[TRACE-GRADIENT] 🔄 detailPanelClosed flag triggered - HomePanel reactivated');
console.log('[TRACE-GRADIENT] 🔄 lastDetailPanelClosed triggered - HomePanel reactivated');
console.log('[HomePanel] *** ✅ HomePanel isOnTop = true'); console.log('[HomePanel] *** ✅ HomePanel isOnTop = true');
console.log('[HomePanel] *** lastDetailPanelClosed:', detailPanelClosedTime); console.log('[HomePanel] *** detailPanelClosed:', detailPanelClosed);
console.log('[HomePanel] *** detailPanelClosedTime:', 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);
console.log('[HomePanel] *** lastPlayedBannerIdRef.current:', lastPlayedBannerIdRef.current); console.log('[HomePanel] *** lastPlayedBannerIdRef.current:', lastPlayedBannerIdRef.current);
@@ -931,9 +934,20 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// refs 초기화 // refs 초기화
videoPlayIntentRef.current = null; videoPlayIntentRef.current = null;
lastPlayedBannerIdRef.current = null; lastPlayedBannerIdRef.current = null;
// detailPanelClosed 플래그 초기화 (다음 사이클에서 재사용 방지)
console.log('[HomePanel] *** detailPanelClosed flag 초기화');
dispatch(updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: {
detailPanelClosed: false,
detailPanelClosedAt: undefined,
detailPanelClosedFromSource: undefined,
}
}));
} }
} }
}, [detailPanelClosedTime, isOnTop, bannerDataList, dispatch]); }, [detailPanelClosed, isOnTop, bannerDataList, dispatch]);
useEffect(() => { useEffect(() => {
return () => { return () => {

View File

@@ -38,6 +38,7 @@ import {
startVideoPlayer, startVideoPlayer,
pauseModalVideo, pauseModalVideo,
resumeModalVideo, resumeModalVideo,
resumeFullscreenVideo,
} from '../../actions/playActions'; } from '../../actions/playActions';
import { resetPlayerOverlays } from '../../actions/videoPlayActions'; import { resetPlayerOverlays } from '../../actions/videoPlayActions';
import { convertUtcToLocal } from '../../components/MediaPlayer/util'; import { convertUtcToLocal } from '../../components/MediaPlayer/util';
@@ -340,21 +341,46 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// } // }
// },[isOnTop, panelInfo]) // },[isOnTop, panelInfo])
// PlayerPanel.jsx의 라인 313-327 useEffect 수정 // PlayerPanel.jsx의 라인 313-327 useEffect 수정 - detailPanelClosed flag 감지 추가
useEffect(() => { useEffect(() => {
console.log('[PlayerPanel] isOnTop useEffect:', { console.log('[PlayerPanel] isOnTop useEffect:', {
isOnTop, isOnTop,
modal: panelInfo?.modal, modal: panelInfo?.modal,
isPaused: panelInfo?.isPaused, isPaused: panelInfo?.isPaused,
detailPanelClosed: panelInfo?.detailPanelClosed,
}); });
if (panelInfo && panelInfo.modal) { if (isOnTop) {
if (!isOnTop) { // 1. Resume Video if needed (isPaused or detailPanelClosed)
console.log('[PlayerPanel] Not on top - pausing video'); if (panelInfo.isPaused || panelInfo.detailPanelClosed) {
dispatch(pauseModalVideo()); if (panelInfo.modal) {
} else if (isOnTop && panelInfo.isPaused) { console.log('[PlayerPanel] Back on top (Modal) - resuming video');
console.log('[PlayerPanel] Back on top - resuming video ← 이곳에서 resumeModalVideo 호출!'); dispatch(resumeModalVideo());
dispatch(resumeModalVideo()); } else {
console.log('[PlayerPanel] Back on top (Fullscreen) - resuming video');
dispatch(resumeFullscreenVideo());
}
}
// 2. Reset detailPanelClosed flag
if (panelInfo.detailPanelClosed) {
console.log('[PlayerPanel] detailPanelClosed flag 초기화');
dispatch(
updatePanel({
name: panel_names.PLAYER_PANEL,
panelInfo: {
detailPanelClosed: false,
detailPanelClosedAt: undefined,
detailPanelClosedFromSource: undefined,
},
})
);
}
} else {
// Not on top
if (panelInfo && panelInfo.modal) {
// console.log('[PlayerPanel] Not on top - pausing video');
// dispatch(pauseModalVideo());
} }
} }
}, [isOnTop, panelInfo]); }, [isOnTop, panelInfo]);

View File

@@ -9,7 +9,7 @@ import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDeco
import { getContainerNode, setContainerLastFocusedElement } from '@enact/spotlight/src/container'; import { getContainerNode, setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import { sendLogTotalRecommend } from '../../../../actions/logActions'; import { sendLogTotalRecommend } from '../../../../actions/logActions';
import { pushPanel } from '../../../../actions/panelActions'; import { navigateToDetail, SOURCE_MENUS, pushPanel } from '../../../../actions/panelActions';
import { hidePlayerOverlays } from '../../../../actions/videoPlayActions'; import { hidePlayerOverlays } from '../../../../actions/videoPlayActions';
import TItemCard, { TYPES } from '../../../../components/TItemCard/TItemCard'; import TItemCard, { TYPES } from '../../../../components/TItemCard/TItemCard';
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList';
@@ -151,26 +151,18 @@ export default function ShopNowContents({
const isYouMayLikeStart = shopNowInfo && index === shopNowInfo.length; const isYouMayLikeStart = shopNowInfo && index === shopNowInfo.length;
const handleItemClick = () => { const handleItemClick = () => {
// 현재 포커스된 요소의 spotlightId 저장 console.log('[ShopNowContents] DetailPanel 진입 - sourceMenu:', SOURCE_MENUS.PLAYER_SHOP_NOW);
const currentFocusedElement = Spotlight.getCurrent();
const currentSpotlightId = currentFocusedElement?.getAttribute('data-spotlight-id');
console.log('[ShopNowContents] 현재 포커스된 spotlightId:', currentSpotlightId);
// DetailPanel push 전에 VideoPlayer 오버레이 숨김
dispatch(hidePlayerOverlays());
dispatch( dispatch(
pushPanel({ navigateToDetail({
name: panel_names.DETAIL_PANEL, patnrId,
panelInfo: { prdtId,
sourceMenu: SOURCE_MENUS.PLAYER_SHOP_NOW,
additionalInfo: {
showNm: playListInfo?.showNm, showNm: playListInfo?.showNm,
showId: playListInfo?.showId, showId: playListInfo?.showId,
liveFlag: playListInfo?.liveFlag, liveFlag: playListInfo?.liveFlag,
thumbnailUrl: playListInfo?.thumbnailUrl, thumbnailUrl: playListInfo?.thumbnailUrl,
patnrId,
prdtId,
launchedFromPlayer: true,
lastFocusedTargetId: currentSpotlightId, // 현재 포커스된 spotlightId 저장
}, },
}) })
); );