38 Commits

Author SHA1 Message Date
d933ca6bb7 [251217] merge: gitlab develop_si 변경사항 병합
🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2025-12-17 16:25:11 +09:00
e86b56e14e [251217] fix: MediaPanel Spinner제거
🕐 커밋 시간: 2025. 12. 17. 16:23:08

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +2줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화
2025-12-17 16:23:08 +09:00
junghoon86.park
eee8e73b97 [리뷰 팝업] 리뷰 이미지가 여러개일시에 처리
- 리뷰 이미지가 여러개일때 handlePrevious,handleNext 눌렀을때 이미지부터 변경되도록 수정.
2025-12-17 16:12:29 +09:00
ec76d2cfc9 [251217] fix: VOD 경과시간 표시
🕐 커밋 시간: 2025. 12. 17. 16:09:01

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +18줄
  • 삭제: -205줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.jsx
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.js

🔧 주요 변경 내용:
  • UI 컴포넌트 아키텍처 개선
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-12-17 16:09:01 +09:00
a7161b8a80 [251217] fix: LiveChannelContents 동영상 전환시 스크롤
🕐 커밋 시간: 2025. 12. 17. 15:45:48

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +14줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx
2025-12-17 15:45:48 +09:00
13e32298a7 [251217] fix: LiveChannelContents TScrollerLiveContents 추가
🕐 커밋 시간: 2025. 12. 17. 15:39:39

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +2줄
  • 삭제: -20줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.jsx
  + com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/TScrollerLiveChannel.module.less

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/TabContents/LiveChannelContents.jsx

🔧 주요 변경 내용:
  • 코드 정리 및 최적화

Performance: 코드 최적화로 성능 개선 기대
2025-12-17 15:39:40 +09:00
5ef0d8afae [251217] merge: gitlab develop_si 변경사항 병합 2025-12-17 14:21:27 +09:00
f6073d78c1 [251217] fix: LiveChannelContents Navigation
🕐 커밋 시간: 2025. 12. 17. 14:17:24

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +100줄
  • 삭제: -9줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-17 14:17:24 +09:00
junghoon86.park
0223499e12 [영상]
- 라이브 채널 next부분 관련해서 버튼부분의 배경 색상 제거
2025-12-17 13:46:23 +09:00
3fd3b66cb3 [251217] fix: PlayerPanel activity check 추가
🕐 커밋 시간: 2025. 12. 17. 13:43:33

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +133줄
  • 삭제: -6줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 중간 규모 기능 개선
2025-12-17 13:43:33 +09:00
be9b1faeec [251217] fix: 비디오배너 클릭 방어로직추가
🕐 커밋 시간: 2025. 12. 17. 12:11:04

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +10줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx
2025-12-17 12:11:04 +09:00
07a042cca6 [251217] fix: PlayerPanel 배너동영상 위치 검증추가
🕐 커밋 시간: 2025. 12. 17. 12:03:44

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +70줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
2025-12-17 12:03:44 +09:00
junghoon86.park
d93960f40a [영상]
- 프로그레스바 크기변경
 - 타임 노출부분 변경
 - cc 버튼 위치변경
2025-12-17 10:38:36 +09:00
4dfa15b4c0 [251217] fix: TabContainer.v2.jsx ShopNowButton 포커스 10ms
🕐 커밋 시간: 2025. 12. 17. 09:13:23

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductContentSection/ProductDescription/ProductDescription.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerTabContents/v2/TabContainer.v2.jsx
2025-12-17 09:13:23 +09:00
d83e9d38f0 [251217] fix: ProductAllSection PRODUCT DETAIL버튼 조건부 렌더링
🕐 커밋 시간: 2025. 12. 17. 08:58:40

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +21줄
  • 삭제: -11줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
2025-12-17 08:58:40 +09:00
8589cde061 [251216] fix: FeaturedBrandsPanel TItemCard to DetailPanel
🕐 커밋 시간: 2025. 12. 16. 17:29:43

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +28줄
  • 삭제: -19줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/FeaturedBrandsPanel/RecommendedShows/RecommendedShowsContents/RecommendedShowsProductList/RecommendedShowsProductList.jsx

🔧 주요 변경 내용:
  • 소규모 기능 개선
  • 코드 정리 및 최적화
2025-12-16 17:29:44 +09:00
92964a5063 [251216] fix: DetailPanel skeleton비활성화
🕐 커밋 시간: 2025. 12. 16. 17:11:50

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +2줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx

🔧 주요 변경 내용:
  • 코드 정리 및 최적화
2025-12-16 17:11:50 +09:00
dba79789a8 [251216] fix: DeepLink 처리 isDeepLinkEntry
🕐 커밋 시간: 2025. 12. 16. 17:00:52

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

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/deepLinkHandler.js
  ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
2025-12-16 17:00:52 +09:00
junghoon86.park
8a882c28ca [영상 노출에 따른 부분 수정]
- 영상 상하 블랙라인 관련 하여 스타일 수정
 - discimir부분 노출관련하여 처리.
2025-12-16 16:27:49 +09:00
61f67708a9 [251216] fix: 로그정리,PlayerPanel ShopNowContents
🕐 커밋 시간: 2025. 12. 16. 16:07:17

📊 변경 통계:
  • 총 파일: 11개
  • 추가: +94줄
  • 삭제: -90줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ com.twin.app.shoptime/src/actions/panelActions.js
  ~ com.twin.app.shoptime/src/actions/playActions.js
  ~ com.twin.app.shoptime/src/middleware/panelHistoryMiddleware.js
  ~ com.twin.app.shoptime/src/reducers/panelReducer.js
  ~ com.twin.app.shoptime/src/utils/lodashFpEx.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.module.less
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 소규모 기능 개선
  • 코드 정리 및 최적화
  • 모듈 구조 개선
2025-12-16 16:07:19 +09:00
c9b2e5daf5 [251216] fix: TrendingNowPanel DetailPanel MediaPanel popPanel fix
🕐 커밋 시간: 2025. 12. 16. 15:21:13

📊 변경 통계:
  • 총 파일: 6개
  • 추가: +62줄
  • 삭제: -14줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/mediaActions.js
  ~ com.twin.app.shoptime/src/actions/panelActions.js
  ~ com.twin.app.shoptime/src/reducers/panelReducer.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 소규모 기능 개선
  • 모듈 구조 개선
2025-12-16 15:21:13 +09:00
4f4887ebdb [251216] fix: TrendingNowPanel PlayerPanel DetailPanel Bg Video Pause
🕐 커밋 시간: 2025. 12. 16. 14:39:08

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +3줄
  • 삭제: -3줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 코드 정리 및 최적화
2025-12-16 14:39:08 +09:00
9d8cafc0a9 [251216] fix: TrendingNowPanel PlayerPanel Bg
🕐 커밋 시간: 2025. 12. 16. 14:29:22

📊 변경 통계:
  • 총 파일: 2개
  • 추가: +14줄
  • 삭제: -3줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx
  ~ com.twin.app.shoptime/src/views/TrendingNowPanel/TrendingNowPanel.jsx
2025-12-16 14:29:22 +09:00
junghoon86.park
929a9020a1 [youmayalsolike]
- updatePanel이 정상적으로 먹지 않는 문제가있어 pushpanel로 변경처리.
2025-12-16 14:20:25 +09:00
junghoon86.park
83905a092d [상품상세] description 클릭시 팝업 노출 처리
- 너무 긴경우 포커스가 넘어가는문제가 있어 클릭시에는 팝업이 노출되어 전체 영역을 확인할수있도록 수정.
2025-12-16 13:55:51 +09:00
e4a64644dd [251216] merge: resolve develop_si branch conflicts
Merge remote changes from gitlab/develop_si.

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2025-12-16 12:41:38 +09:00
55af96bd00 [251216] fix: Panel logs update - 1
🕐 커밋 시간: 2025. 12. 16. 12:39:11

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +20줄
  • 삭제: -8줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/middleware/panelHistoryMiddleware.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/DetailPanel.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
2025-12-16 12:39:12 +09:00
junghoon86.park
8325070138 [Tscrolldetail] 상세에서 오른쪽 스크롤 포커스 가지않도록 변경.
- focusableScrollbar를 false 로 강제로 줌.
2025-12-16 12:22:30 +09:00
junghoon86.park
bbb9e64120 [TheaderCustom]
- theme 상품 상세에서 theme명 노출부분 변경 처리.
2025-12-16 12:20:23 +09:00
99ea3e6595 [251216] fix: TrendingNowPanel 3-layer add
🕐 커밋 시간: 2025. 12. 16. 10:26:10

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +13줄
  • 삭제: -2줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx
2025-12-16 10:26:10 +09:00
3dc4699479 [251216] fix: panelHistoryMiddleware log
🕐 커밋 시간: 2025. 12. 16. 10:15:57

📊 변경 통계:
  • 총 파일: 1개
  • 추가: +7줄
  • 삭제: -4줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/middleware/panelHistoryMiddleware.js

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 코드 정리 및 최적화
2025-12-16 10:15:57 +09:00
junghoon86.park
8a3bcc1f9c [테마 아이템카드] 목업 데이터 제거
- 목업 데이터 제거.
2025-12-16 09:31:34 +09:00
junghoon86.park
486fb5efd5 [상품 상세] panelInfo?.patnrId 내려오는 부분 수정
- string 으로 내려와서 number로 넣어둔부분은 제거.
2025-12-15 17:01:27 +09:00
junghoon86.park
802484debd [검색] 음성검색관련 마이크 노출 주석처리
- 음성 검색우선 제외로 인하여 마이크 버튼주석처리.
2025-12-15 16:23:54 +09:00
junghoon86.park
c540378cb5 [foryou] 로그인, 비로그인시 노출 차이 수정
- 포유 아이콘 관련 처리부분 비로그인시 노출안되도록 처리.
 - 픽포유는 홈패널에서 노출로 처리.
2025-12-15 16:15:51 +09:00
junghoon86.park
cb3a4e9bc7 [live영상] now playing노출 관련 수정
- currentVideoVisible 추가
2025-12-15 15:22:09 +09:00
junghoon86.park
3ce4398e67 [상품상세] 폰트수정
- 폰트수정
2025-12-15 14:31:13 +09:00
junghoon86.park
78153bae0c [상품상세] 폰트수정
- 글자크기가 디자인과 맞지 않아 이부분 수정.
2025-12-15 14:29:54 +09:00
51 changed files with 2211 additions and 1095 deletions

View File

@@ -85,6 +85,7 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
patnrId: patnrId, patnrId: patnrId,
chanId: chanId, chanId: chanId,
shptmBanrTpNm: "LIVE", shptmBanrTpNm: "LIVE",
modal: false, // DeepLink 진입 시 fullscreen으로 재생
// expsOrd: expsOrd, // expsOrd: expsOrd,
}; };
break; break;
@@ -101,6 +102,7 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
patnrId: patnrId, patnrId: patnrId,
showId: showId, showId: showId,
shptmBanrTpNm: "VOD", shptmBanrTpNm: "VOD",
modal: false, // DeepLink 진입 시 fullscreen으로 재생
// expsOrd: expsOrd, // expsOrd: expsOrd,
}; };
break; break;
@@ -274,6 +276,18 @@ export const handleDeepLink = (contentTarget) => (dispatch, _getState) => {
const action = const action =
panelName === panel_names.HOME_PANEL ? updateHomeInfo : pushPanel; panelName === panel_names.HOME_PANEL ? updateHomeInfo : pushPanel;
// 🔽 LS(Live Show) 또는 VS(VOD Show)인 경우 DeepLink 진입 플래그 설정
if ((type === 'LS' || type === 'VS') && action === pushPanel) {
dispatch(
updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: {
isDeepLinkEntry: true, // DeepLink PlayerPanel 진입 플래그
},
})
);
}
dispatch( dispatch(
action({ action({
name: panelName, name: panelName,

View File

@@ -99,17 +99,25 @@ export const finishMediaPreview = () => (dispatch, getState) => {
export const finishModalMediaForce = () => (dispatch, getState) => { export const finishModalMediaForce = () => (dispatch, getState) => {
const panels = getState().panels.panels; const panels = getState().panels.panels;
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] finishModalMediaForce called', {
// panelCount: panels.length,
// panelNames: panels.map((p) => p.name),
// });
const hasProductVideoPanel = panels.some( const hasProductVideoPanel = panels.some(
(panel) => (panel) =>
panel.name === panel_names.MEDIA_PANEL && panel.name === panel_names.MEDIA_PANEL &&
(panel.panelInfo?.modal || panel.panelInfo?.modalContainerId === 'product-video-player') (panel.panelInfo?.modal || panel.panelInfo?.modalContainerId === 'product-video-player')
); );
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] hasProductVideoPanel:', hasProductVideoPanel);
if (hasProductVideoPanel) { if (hasProductVideoPanel) {
if (startMediaFocusTimer) { if (startMediaFocusTimer) {
clearTimeout(startMediaFocusTimer); clearTimeout(startMediaFocusTimer);
startMediaFocusTimer = null; startMediaFocusTimer = null;
} }
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] Calling popPanel(panel_names.MEDIA_PANEL)');
dispatch(popPanel(panel_names.MEDIA_PANEL)); dispatch(popPanel(panel_names.MEDIA_PANEL));
} }
}; };

View File

@@ -6,7 +6,7 @@ import { updateHomeInfo } from './homeActions';
import { createDebugHelpers } from '../utils/debug'; import { createDebugHelpers } from '../utils/debug';
// 디버그 헬퍼 설정 // 디버그 헬퍼 설정
const DEBUG_MODE = true; const DEBUG_MODE = false;
const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE); const { dlog, dwarn, derror } = createDebugHelpers(DEBUG_MODE);
// 시작 메뉴 추적을 위한 상수 // 시작 메뉴 추적을 위한 상수
@@ -40,12 +40,20 @@ export const pushPanel = (panel, duplicatable = false) => ({
}); });
export const popPanel = (panelName) => { export const popPanel = (panelName) => {
const stack = new Error().stack;
const stackLines = stack?.split('\n') || [];
// console.log('[💜UNIQUE_PANEL_STACK💜] popPanel action dispatcher - REMOVING PANEL:', {
// panelName,
// timestamp: Date.now(),
// fullStack: stackLines.slice(1, 6).map((line) => line.trim()),
// });
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log('[PANEL-TRACE] popPanel action creator', { console.log('[💜UNIQUE_PANEL_STACK💜] popPanel action creator stack:', {
panelName, panelName,
caller: new Error().stack?.split('\n')[2]?.trim(), caller: stackLines[2]?.trim(),
}); });
console.trace('[PANEL-TRACE] popPanel stack trace');
} }
return { return {
type: types.POP_PANEL, type: types.POP_PANEL,

View File

@@ -74,7 +74,7 @@ export const startVideoPlayer =
}) => }) =>
(dispatch, getState) => { (dispatch, getState) => {
const caller = new Error().stack?.split('\n')[2]?.trim(); const caller = new Error().stack?.split('\n')[2]?.trim();
console.log('[PTRACE-SP] startVideoPlayer call', { dlog('[PTRACE-SP] startVideoPlayer call', {
modal, modal,
modalContainerId, modalContainerId,
modalClassName, modalClassName,
@@ -115,7 +115,7 @@ export const startVideoPlayer =
// 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push // 기존 PlayerPanel이 어디든 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
if (existingPlayerPanel) { if (existingPlayerPanel) {
console.log('[PTRACE-SP] startVideoPlayer: popping existing player before push', { dlog('[PTRACE-SP] startVideoPlayer: popping existing player before push', {
stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
}); });
dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start'); dlog('[startVideoPlayer] 🔄 Resetting existing PLAYER_PANEL before start');
@@ -148,14 +148,14 @@ export const startVideoPlayer =
// [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화
// if (modal && modalContainerId && !spotlightDisable) { // if (modal && modalContainerId && !spotlightDisable) {
// console.log('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId); // dlog('[startVideoPlayer] 🎯 Setting Spotlight focus - containerId:', modalContainerId);
// Spotlight.setPointerMode(false); // Spotlight.setPointerMode(false);
// startVideoFocusTimer = setTimeout(() => { // startVideoFocusTimer = setTimeout(() => {
// console.log('[startVideoPlayer] 🔍 Spotlight.focus called'); // dlog('[startVideoPlayer] 🔍 Spotlight.focus called');
// Spotlight.focus(modalContainerId); // Spotlight.focus(modalContainerId);
// }, 0); // }, 0);
// } else { // } else {
// console.log('[startVideoPlayer] ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable); // dlog('[startVideoPlayer] ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable);
// } // }
dlog('[startVideoPlayer] ✅ END'); dlog('[startVideoPlayer] ✅ END');
@@ -196,7 +196,7 @@ export const startVideoPlayerNew =
}) => }) =>
(dispatch, getState) => { (dispatch, getState) => {
const caller = new Error().stack?.split('\n')[2]?.trim(); const caller = new Error().stack?.split('\n')[2]?.trim();
console.log('[PTRACE-SPN] startVideoPlayerNew call', { dlog('[PTRACE-SPN] startVideoPlayerNew call', {
bannerId, bannerId,
modal, modal,
modalContainerId, modalContainerId,
@@ -239,7 +239,7 @@ export const startVideoPlayerNew =
// 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push // 기존 PlayerPanel이 있으면 완전히 초기화: 타이머 정리 후 pop → 새로 push
if (existingPlayerPanel) { if (existingPlayerPanel) {
console.log('[PTRACE-SPN] popping existing player before push', { dlog('[PTRACE-SPN] popping existing player before push', {
stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
}); });
dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start'); dlog('[startVideoPlayerNew] *** 🔄 Resetting existing PLAYER_PANEL before start');
@@ -332,14 +332,14 @@ export const startVideoPlayerNew =
// [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화 // [COMMENTED OUT] 비디오 재생 시 강제 포커스 이동 비활성화
// if (modal && modalContainerId && !spotlightDisable) { // if (modal && modalContainerId && !spotlightDisable) {
// console.log('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId); // dlog('[startVideoPlayerNew] *** 🎯 Setting Spotlight focus - containerId:', modalContainerId);
// Spotlight.setPointerMode(false); // Spotlight.setPointerMode(false);
// startVideoFocusTimer = setTimeout(() => { // startVideoFocusTimer = setTimeout(() => {
// console.log('[startVideoPlayerNew] *** 🔍 Spotlight.focus called'); // dlog('[startVideoPlayerNew] *** 🔍 Spotlight.focus called');
// Spotlight.focus(modalContainerId); // Spotlight.focus(modalContainerId);
// }, 0); // }, 0);
// } else { // } else {
// console.log('[startVideoPlayerNew] *** ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable); // dlog('[startVideoPlayerNew] *** ⏭️ Spotlight focus skipped - modal:', modal, ', modalContainerId:', !!modalContainerId, ', spotlightDisable:', spotlightDisable);
// } // }
dlog('[startVideoPlayerNew] *** ✅ END'); dlog('[startVideoPlayerNew] *** ✅ END');
@@ -352,7 +352,7 @@ export const finishVideoPreview = () => (dispatch, getState) => {
const panels = getState().panels.panels; const panels = getState().panels.panels;
const topPanel = panels[panels.length - 1]; const topPanel = panels[panels.length - 1];
if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) { if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) {
console.log('[PANEL-TRACE] finishVideoPreview: popping modal player', { dlog('[PANEL-TRACE] finishVideoPreview: popping modal player', {
topPanelName: topPanel.name, topPanelName: topPanel.name,
modal: topPanel.panelInfo.modal, modal: topPanel.panelInfo.modal,
stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
@@ -417,7 +417,7 @@ export const pauseModalVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal
); );
console.log('[Detail-BG] ⏸️ pauseModalVideo - Pausing modal video', { dlog('[Detail-BG] ⏸️ pauseModalVideo - Pausing modal video', {
found: !!modalPlayerPanel, found: !!modalPlayerPanel,
playerPanelModal: modalPlayerPanel?.panelInfo?.modal, playerPanelModal: modalPlayerPanel?.panelInfo?.modal,
currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused, currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused,
@@ -438,11 +438,11 @@ export const pauseModalVideo = () => (dispatch, getState) => {
}) })
); );
console.log('[Detail-BG] ✅ pauseModalVideo - Modal video paused successfully', { dlog('[Detail-BG] ✅ pauseModalVideo - Modal video paused successfully', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
console.log('[Detail-BG] ⚠️ pauseModalVideo - No modal PlayerPanel found', { dlog('[Detail-BG] ⚠️ pauseModalVideo - No modal PlayerPanel found', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@@ -457,7 +457,7 @@ export const resumeModalVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal
); );
console.log('[Detail-BG] ▶️ resumeModalVideo - Resuming modal video', { dlog('[Detail-BG] ▶️ resumeModalVideo - Resuming modal video', {
found: !!modalPlayerPanel, found: !!modalPlayerPanel,
playerPanelModal: modalPlayerPanel?.panelInfo?.modal, playerPanelModal: modalPlayerPanel?.panelInfo?.modal,
currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused, currentIsPaused: modalPlayerPanel?.panelInfo?.isPaused,
@@ -478,11 +478,11 @@ export const resumeModalVideo = () => (dispatch, getState) => {
}) })
); );
console.log('[Detail-BG] ✅ resumeModalVideo - Modal video resumed successfully', { dlog('[Detail-BG] ✅ resumeModalVideo - Modal video resumed successfully', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
console.log('[Detail-BG] ⚠️ resumeModalVideo - Modal video not paused or panel not found', { dlog('[Detail-BG] ⚠️ resumeModalVideo - Modal video not paused or panel not found', {
found: !!modalPlayerPanel, found: !!modalPlayerPanel,
isPaused: modalPlayerPanel?.panelInfo?.isPaused, isPaused: modalPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(), timestamp: Date.now(),
@@ -499,7 +499,7 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
); );
console.log('[Detail-BG] ⏸️ pauseFullscreenVideo - Pausing fullscreen video', { dlog('[Detail-BG] ⏸️ pauseFullscreenVideo - Pausing fullscreen video', {
found: !!fullscreenPlayerPanel, found: !!fullscreenPlayerPanel,
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal, playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
@@ -517,11 +517,11 @@ export const pauseFullscreenVideo = () => (dispatch, getState) => {
}) })
); );
console.log('[Detail-BG] ✅ pauseFullscreenVideo - Fullscreen video paused successfully', { dlog('[Detail-BG] ✅ pauseFullscreenVideo - Fullscreen video paused successfully', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
console.log('[Detail-BG] ⚠️ pauseFullscreenVideo - No fullscreen PlayerPanel found', { dlog('[Detail-BG] ⚠️ pauseFullscreenVideo - No fullscreen PlayerPanel found', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} }
@@ -536,7 +536,7 @@ export const resumeFullscreenVideo = () => (dispatch, getState) => {
(panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal (panel) => panel.name === panel_names.PLAYER_PANEL && !panel.panelInfo?.modal
); );
console.log('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', { dlog('[Detail-BG] ▶️ resumeFullscreenVideo - Resuming fullscreen video', {
found: !!fullscreenPlayerPanel, found: !!fullscreenPlayerPanel,
playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal, playerPanelModal: fullscreenPlayerPanel?.panelInfo?.modal,
currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, currentIsPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
@@ -554,11 +554,11 @@ export const resumeFullscreenVideo = () => (dispatch, getState) => {
}) })
); );
console.log('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', { dlog('[Detail-BG] ✅ resumeFullscreenVideo - Fullscreen video resumed successfully', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
console.log('[Detail-BG] ⚠️ resumeFullscreenVideo - Fullscreen video not paused or panel not found', { dlog('[Detail-BG] ⚠️ resumeFullscreenVideo - Fullscreen video not paused or panel not found', {
found: !!fullscreenPlayerPanel, found: !!fullscreenPlayerPanel,
isPaused: fullscreenPlayerPanel?.panelInfo?.isPaused, isPaused: fullscreenPlayerPanel?.panelInfo?.isPaused,
timestamp: Date.now(), timestamp: Date.now(),
@@ -601,7 +601,7 @@ export const hideModalVideo = () => (dispatch, getState) => {
}, },
}; };
// console.log('[HomePanel] hideModalVideo: saving shrinkInfo', { // dlog('[HomePanel] hideModalVideo: saving shrinkInfo', {
// shrinkInfo: updatedPlayerState.shrinkInfo, // shrinkInfo: updatedPlayerState.shrinkInfo,
// modalStyle: panelInfo.modalStyle, // modalStyle: panelInfo.modalStyle,
// }); // });
@@ -1038,7 +1038,7 @@ export const resumePlayerControl = (ownerId) => (dispatch, getState) => {
* 이 액션은 어떤 배너에서든 클릭 시 호출됩니다. * 이 액션은 어떤 배너에서든 클릭 시 호출됩니다.
*/ */
export const goToFullScreen = () => (dispatch, getState) => { export const goToFullScreen = () => (dispatch, getState) => {
console.log('[Detail-BG] 🎬 goToFullScreen - Setting PlayerPanel to fullscreen mode', { dlog('[Detail-BG] 🎬 goToFullScreen - Setting PlayerPanel to fullscreen mode', {
targetModal: false, targetModal: false,
action: 'updatePanel', action: 'updatePanel',
timestamp: Date.now(), timestamp: Date.now(),
@@ -1055,7 +1055,7 @@ export const goToFullScreen = () => (dispatch, getState) => {
}) })
); );
console.log('[Detail-BG] ✅ goToFullScreen - PlayerPanel modal set to false (fullscreen)', { dlog('[Detail-BG] ✅ goToFullScreen - PlayerPanel modal set to false (fullscreen)', {
timestamp: Date.now(), timestamp: Date.now(),
}); });
}; };
@@ -1268,7 +1268,7 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
...rest ...rest
} = videoInfo; } = videoInfo;
console.log('[Detail-BG] 🎥 startBannerVideo - Starting banner video', { dlog('[Detail-BG] 🎥 startBannerVideo - Starting banner video', {
modalStatus: modal, modalStatus: modal,
bannerId, bannerId,
displayMode: modal ? 'VISIBLE (modal=true)' : 'FULLSCREEN (modal=false)', displayMode: modal ? 'VISIBLE (modal=true)' : 'FULLSCREEN (modal=false)',
@@ -1295,7 +1295,7 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
// 기존 PlayerPanel이 있으면 초기화 // 기존 PlayerPanel이 있으면 초기화
if (existingPlayerPanel) { if (existingPlayerPanel) {
dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start'); dlog('[startBannerVideo] 🔄 Resetting existing PLAYER_PANEL before start');
console.log('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', { dlog('[Detail-BG] 🔄 startBannerVideo - Clearing existing PlayerPanel', {
existingModalStatus: existingPlayerPanel.panelInfo?.modal, existingModalStatus: existingPlayerPanel.panelInfo?.modal,
timestamp: Date.now(), timestamp: Date.now(),
}); });
@@ -1304,7 +1304,7 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
} }
// 새로운 PlayerPanel push // 새로운 PlayerPanel push
console.log('[Detail-BG] startBannerVideo - Pushing new PlayerPanel with modal status', { dlog('[Detail-BG] startBannerVideo - Pushing new PlayerPanel with modal status', {
modal, modal,
modalContainerId, modalContainerId,
timestamp: Date.now(), timestamp: Date.now(),
@@ -1331,7 +1331,7 @@ export const startBannerVideo = (videoInfo) => (dispatch, getState) => {
) )
); );
console.log('[Detail-BG] ✅ startBannerVideo - PlayerPanel pushed with modal=' + modal, { dlog('[Detail-BG] ✅ startBannerVideo - PlayerPanel pushed with modal=' + modal, {
timestamp: Date.now(), timestamp: Date.now(),
}); });

View File

@@ -34,7 +34,7 @@
padding: @slider-padding-v 0; padding: @slider-padding-v 0;
height: @sand-mediaplayer-slider-height; height: @sand-mediaplayer-slider-height;
right: 154px; right: 154px;
width: 1466px; width: 1558px;
// Add a tap area that extends to the edges of the screen, to make the slider more accessible // Add a tap area that extends to the edges of the screen, to make the slider more accessible
&::before { &::before {
content: ""; content: "";

View File

@@ -7,7 +7,7 @@
position: absolute; position: absolute;
font-family: @baseFont; font-family: @baseFont;
width: 100%; width: 100%;
right: 90px; right: 20px;
bottom: -5px; bottom: -5px;
font-size: 24px; font-size: 24px;
font-weight: bold; font-weight: bold;
@@ -16,12 +16,12 @@
letter-spacing: -1px; letter-spacing: -1px;
.separator { .separator {
position: absolute; position: absolute;
right: 105px; right: 95px;
bottom: -5px; bottom: -5px;
} }
.currentTime { .currentTime {
position: absolute; position: absolute;
right: 130px; right: 120px;
bottom: -5px; bottom: -5px;
} }
.totalTime { .totalTime {

View File

@@ -483,7 +483,7 @@
.default-style(); .default-style();
.scrollInfo { .scrollInfo {
width: 900px; width: 850px;
background-color: @BG_COLOR_01; background-color: @BG_COLOR_01;
color: @COLOR_GRAY03; color: @COLOR_GRAY03;
display: flex; display: flex;

View File

@@ -840,9 +840,11 @@ const VideoPlayerBase = class extends React.Component {
this.state.mediaSliderVisible === nextState.mediaSliderVisible && this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
this.state.loading === nextState.loading && this.state.loading === nextState.loading &&
this.props.loading === nextProps.loading && this.props.loading === nextProps.loading &&
(this.state.currentTime !== nextState.currentTime || this.state.currentTime === nextState.currentTime &&
this.state.proportionPlayed !== nextState.proportionPlayed || this.state.proportionPlayed === nextState.proportionPlayed &&
this.state.sliderTooltipTime !== nextState.sliderTooltipTime) this.state.sliderTooltipTime === nextState.sliderTooltipTime &&
this.state.mediaControlsVisible === nextState.mediaControlsVisible &&
this.state.bottomControlsRendered === nextState.bottomControlsRendered
) { ) {
return false; return false;
} }
@@ -1279,14 +1281,11 @@ const VideoPlayerBase = class extends React.Component {
sourceUnavailable: true, sourceUnavailable: true,
proportionPlayed: 0, proportionPlayed: 0,
proportionLoaded: 0, proportionLoaded: 0,
bottomControlsRendered: true,
}); });
if (!this.props.noAutoShowMediaControls) { if (!this.props.noAutoShowMediaControls) {
if (!this.state.bottomControlsRendered) { this.showControls();
this.renderBottomControl.idle();
} else {
this.showControls();
}
} }
}; };

View File

@@ -639,7 +639,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
{/* Overlay */} {/* Overlay */}
<Overlay bottomControlsVisible={controlsVisible} onClick={handleVideoClick}> <Overlay bottomControlsVisible={controlsVisible} onClick={handleVideoClick}>
{/* Loading + Thumbnail */} {/* Loading + Thumbnail */}
{loading && thumbnailUrl && ( {/* {loading && thumbnailUrl && (
<> <>
<p className={classNames(css.thumbnail, isModal && css.smallThumbnail)}> <p className={classNames(css.thumbnail, isModal && css.smallThumbnail)}>
<img src={thumbnailUrl} alt="" /> <img src={thumbnailUrl} alt="" />
@@ -648,7 +648,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
<Loader /> <Loader />
</div> </div>
</> </>
)} )} */}
{/* Controls with MediaSlider */} {/* Controls with MediaSlider */}
{controlsVisible && !isModal && ( {controlsVisible && !isModal && (

View File

@@ -855,9 +855,11 @@ const VideoPlayerBase = class extends React.Component {
this.state.mediaSliderVisible === nextState.mediaSliderVisible && this.state.mediaSliderVisible === nextState.mediaSliderVisible &&
this.state.loading === nextState.loading && this.state.loading === nextState.loading &&
this.props.loading === nextProps.loading && this.props.loading === nextProps.loading &&
(this.state.currentTime !== nextState.currentTime || this.state.currentTime === nextState.currentTime &&
this.state.proportionPlayed !== nextState.proportionPlayed || this.state.proportionPlayed === nextState.proportionPlayed &&
this.state.sliderTooltipTime !== nextState.sliderTooltipTime) this.state.sliderTooltipTime === nextState.sliderTooltipTime &&
this.state.mediaControlsVisible === nextState.mediaControlsVisible &&
this.state.bottomControlsRendered === nextState.bottomControlsRendered
) { ) {
return false; return false;
} }
@@ -1423,14 +1425,11 @@ const VideoPlayerBase = class extends React.Component {
sourceUnavailable: true, sourceUnavailable: true,
proportionPlayed: 0, proportionPlayed: 0,
proportionLoaded: 0, proportionLoaded: 0,
bottomControlsRendered: true,
}); });
if (!this.props.noAutoShowMediaControls) { if (!this.props.noAutoShowMediaControls) {
if (!this.state.bottomControlsRendered) { this.showControls();
this.renderBottomControl.idle();
} else {
this.showControls();
}
} }
}; };
@@ -1636,103 +1635,6 @@ const VideoPlayerBase = class extends React.Component {
updatedState.thumbnailUrl = null; updatedState.thumbnailUrl = null;
} }
this.setState(updatedState); this.setState(updatedState);
// Redux에 비디오 재생 상태 업데이트 (기존 로직 유지)
if (this.props.dispatch) {
// 🔥 onProgress 이벤트는 Redux 업데이트하지 않음 (빈번한 이벤트)
const shouldUpdateRedux = !['onProgress'].includes(ev.type);
if (shouldUpdateRedux) {
const updateState = {
isPlaying: !updatedState.paused,
isPaused: updatedState.paused,
currentTime: updatedState.currentTime,
duration: updatedState.duration,
playbackRate: updatedState.playbackRate,
};
// 가장 중요한 이벤트만 로그
const shouldLogEvent = ['play', 'pause', 'ended'].includes(ev.type);
if (shouldLogEvent) {
dlog('🔄 [PlayerPanel][VideoPlayer] Event-driven Redux update', {
eventType: ev.type,
videoState: updatedState,
updateState,
timestamp: new Date().toISOString(),
});
}
// 🔍 Redux dispatch 확인
dlog('📤 [PlayerPanel][VideoPlayer] Dispatching Redux update', {
eventType: ev.type,
updateState,
hasDispatch: !!this.props.dispatch,
propsVideoPlayState: this.props.videoPlayState,
});
this.props.dispatch(updateVideoPlayState(updateState));
}
} else {
derror('❌ [PlayerPanel][VideoPlayer] No dispatch prop available', {
props: Object.keys(this.props),
hasDispatch: !!this.props.dispatch,
hasVideoPlayState: !!this.props.videoPlayState,
});
}
// 🔹 [강화] 내부 상태와 Redux 상태 동기화
// Redux 상태를 우선적으로 사용하여 내부 상태 일관성 확보
if (this.props.videoPlayState && typeof this.props.videoPlayState === 'object') {
// Redux 상태 디버깅 (최소한의 중요 이벤트만)
if (ev.type === 'play' || ev.type === 'pause') {
dlog('🔍 [PlayerPanel][VideoPlayer] Redux state debug', {
videoPlayState: this.props.videoPlayState,
isPaused: this.props.videoPlayState?.isPaused,
isPlaying: this.props.videoPlayState?.isPlaying,
currentTime: this.props.videoPlayState?.currentTime,
eventType: ev.type,
timestamp: new Date().toISOString(),
});
}
const { currentTime, paused, playbackRate } = this.props.videoPlayState;
// Redux 상태와 현재 내부 상태가 크게 다를 경우 내부 상태 업데이트
const timeDiff = Math.abs(currentTime - this.state.currentTime);
const shouldUpdateTime = timeDiff > 0.5; // 0.5초 이상 차이 시 업데이트
// 빈번한 이벤트는 로그에서 제외
const isFrequentEvent = [
'onProgress',
'onBuffer',
'onBufferEnd',
'onReady',
'onDuration',
'onStart',
].includes(ev.type);
const hasSignificantChange =
shouldUpdateTime || (paused !== this.state.paused && !isFrequentEvent);
// 중요한 상태 변화가 있고 빈번한 이벤트가 아닐 때만 로그
if (hasSignificantChange && !isFrequentEvent) {
dlog('🔄 [PlayerPanel][VideoPlayer] Syncing internal state with Redux', {
timeDiff,
shouldUpdateTime,
pausedDiff: paused !== this.state.paused,
reduxPaused: paused,
internalPaused: this.state.paused,
eventType: ev.type,
timestamp: new Date().toISOString(),
});
}
if (hasSignificantChange) {
this.setState({
currentTime: shouldUpdateTime ? currentTime : this.state.currentTime,
paused: paused !== undefined ? paused : this.state.paused,
playbackRate: playbackRate !== undefined ? playbackRate : this.state.playbackRate,
});
}
}
}; };
renderBottomControl = new Job(() => { renderBottomControl = new Job(() => {
@@ -1744,7 +1646,6 @@ const VideoPlayerBase = class extends React.Component {
/** /**
* Returns an object with the current state of the media including `currentTime`, `duration`, * Returns an object with the current state of the media including `currentTime`, `duration`,
* `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`. * `paused`, `playbackRate`, `proportionLoaded`, and `proportionPlayed`.
* Redux 상태와 내부 상태를 우선적으로 사용하여 일관성 보장
* *
* @function * @function
* @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype * @memberof sandstone/VideoPlayer.VideoPlayerBase.prototype
@@ -1752,19 +1653,13 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
getMediaState = () => { getMediaState = () => {
// Redux 상태를 우선적으로 사용하여 일관성 보장
// Redux 상태가 없으면 내부 상태 사용 (fallback)
const reduxState = this.props.videoPlayState;
return { return {
currentTime: reduxState?.currentTime ?? this.state.currentTime, currentTime: this.state.currentTime,
duration: reduxState?.duration ?? this.state.duration, duration: this.state.duration,
paused: reduxState?.isPaused ?? this.state.paused, paused: this.state.paused,
playbackRate: reduxState?.playbackRate ?? this.video?.playbackRate ?? this.state.playbackRate, playbackRate: this.video?.playbackRate,
proportionLoaded: this.state.proportionLoaded, proportionLoaded: this.state.proportionLoaded,
proportionPlayed: this.state.proportionPlayed, proportionPlayed: this.state.proportionPlayed,
// Redux 상태 정보도 포함
isPlaying: reduxState?.isPlaying ?? !this.state.paused,
}; };
}; };
@@ -1793,16 +1688,7 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
play = () => { play = () => {
dlog('🟢 [PlayerPanel][VideoPlayer] play() called', {
currentTime: this.state.currentTime,
duration: this.state.duration,
paused: this.state.paused,
sourceUnavailable: this.state.sourceUnavailable,
prevCommand: this.prevCommand,
});
if (this.state.sourceUnavailable) { if (this.state.sourceUnavailable) {
dwarn('⚠️ [PlayerPanel][VideoPlayer] play() aborted - source unavailable');
return; return;
} }
@@ -1814,19 +1700,6 @@ const VideoPlayerBase = class extends React.Component {
this.send('play'); this.send('play');
this.announce($L('Play')); this.announce($L('Play'));
this.startDelayedMiniFeedbackHide(5000); this.startDelayedMiniFeedbackHide(5000);
// Redux 상태 업데이트 - 재생 상태로 변경
if (this.props.dispatch) {
this.props.dispatch(
updateVideoPlayState({
isPlaying: true,
isPaused: false,
currentTime: this.state.currentTime,
duration: this.state.duration,
playbackRate: 1,
})
);
}
}; };
/** /**
@@ -1837,16 +1710,7 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
pause = () => { pause = () => {
dlog('🔴 [VideoPlayer] pause() called', {
currentTime: this.state.currentTime,
duration: this.state.duration,
paused: this.state.paused,
sourceUnavailable: this.state.sourceUnavailable,
prevCommand: this.prevCommand,
});
if (this.state.sourceUnavailable) { if (this.state.sourceUnavailable) {
dwarn('⚠️ [VideoPlayer] pause() aborted - source unavailable');
return; return;
} }
@@ -1858,22 +1722,6 @@ const VideoPlayerBase = class extends React.Component {
this.send('pause'); this.send('pause');
this.announce($L('Pause')); this.announce($L('Pause'));
this.stopDelayedMiniFeedbackHide(); this.stopDelayedMiniFeedbackHide();
// Redux 상태 업데이트 - 일시정지 상태로 변경
if (this.props.dispatch) {
const pauseState = {
isPlaying: false,
isPaused: true,
currentTime: this.state.currentTime,
duration: this.state.duration,
playbackRate: 1,
};
dlog('📤 [VideoPlayer] Dispatching pause state', pauseState);
this.props.dispatch(updateVideoPlayState(pauseState));
} else {
dwarn('⚠️ [VideoPlayer] No dispatch prop available - Redux state not updated');
}
}; };
/** /**
@@ -1885,15 +1733,6 @@ const VideoPlayerBase = class extends React.Component {
* @public * @public
*/ */
seek = (timeIndex) => { seek = (timeIndex) => {
dlog('⏩ [VideoPlayer] seek() called', {
timeIndex,
currentTime: this.state.currentTime,
duration: this.state.duration,
videoDuration: this.video?.duration,
seekDisabled: this.props.seekDisabled,
sourceUnavailable: this.state.sourceUnavailable,
});
if (this.video) { if (this.video) {
if ( if (
!this.props.seekDisabled && !this.props.seekDisabled &&
@@ -1904,34 +1743,9 @@ const VideoPlayerBase = class extends React.Component {
const actualSeekTime = const actualSeekTime =
timeIndex >= this.video.duration ? this.video.duration - 1 : timeIndex; timeIndex >= this.video.duration ? this.video.duration - 1 : timeIndex;
this.video.currentTime = actualSeekTime; this.video.currentTime = actualSeekTime;
dlog('⏩ [VideoPlayer] Video seek completed', {
requestedTime: timeIndex,
actualTime: actualSeekTime,
videoDuration: this.video.duration,
});
// Redux 상태 업데이트 - 시간 이동 상태 반영
if (this.props.dispatch) {
const seekState = {
isPlaying: !this.state.paused,
isPaused: this.state.paused,
currentTime: actualSeekTime,
duration: this.state.duration,
playbackRate: this.state.playbackRate,
};
dlog('📤 [VideoPlayer] Dispatching seek state', seekState);
this.props.dispatch(updateVideoPlayState(seekState));
} else {
dwarn('⚠️ [VideoPlayer] No dispatch prop available - Redux state not updated');
}
} else { } else {
derror('❌ [VideoPlayer] seek failed - disabled or source unavailable');
forward('onSeekFailed', {}, this.props); forward('onSeekFailed', {}, this.props);
} }
} else {
derror('❌ [VideoPlayer] seek failed - no video element');
} }
}; };

View File

@@ -16,7 +16,7 @@ import { calculateIsPanelOnTop } from '../utils/panelUtils'; // 🎯 isOnTop 유
// DEBUG_MODE - true인 경우에만 로그 출력 // DEBUG_MODE - true인 경우에만 로그 출력
// ⚠️ [251122] panelHistory 로그 비활성화 - 로그 생성 차단 // ⚠️ [251122] panelHistory 로그 비활성화 - 로그 생성 차단
const DEBUG_MODE = true; const DEBUG_MODE = false;
/** /**
* Panel history middleware * Panel history middleware
@@ -33,8 +33,8 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
(action.type.includes('PANEL') || action.type === 'CLEAR_PANEL_HISTORY') (action.type.includes('PANEL') || action.type === 'CLEAR_PANEL_HISTORY')
) { ) {
const caller = new Error().stack.split('\n')[1]?.trim(); const caller = new Error().stack.split('\n')[1]?.trim();
console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`); // console.log(`[PANEL DEBUG] ${action.type} from: ${caller}`);
console.log(' Payload:', action.payload); // console.log(' Payload:', action.payload);
} }
// GNB 호출 식별을 위한 helper 함수 // GNB 호출 식별을 위한 helper 함수
@@ -81,7 +81,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL] PUSH_PANEL:', { console.log(`[PANEL] PUSH_PANEL: ${panelName}`, {
panelName, panelName,
panelInfo, panelInfo,
isGNB, isGNB,
@@ -106,7 +106,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const stateAfter = store.getState(); const stateAfter = store.getState();
const panelHistoryAfter = stateAfter.panelHistory; const panelHistoryAfter = stateAfter.panelHistory;
const panelsAfter = stateAfter.panels.panels; const panelsAfter = stateAfter.panels.panels;
console.log('[PANEL_HISTORY] After PUSH_PANEL:', { console.log(`[PANEL_HISTORY] PUSH_PANEL: ${panelName}`, {
panelHistory: panelHistoryAfter, panelHistory: panelHistoryAfter,
panels: panelsAfter, panels: panelsAfter,
}); });
@@ -126,7 +126,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
if (panels.length > 0) { if (panels.length > 0) {
const topPanel = panels[panels.length - 1]; const topPanel = panels[panels.length - 1];
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log('[PANEL-TRACE] POP_PANEL middleware stack', { console.log(`[PANEL-TRACE] POP_PANEL middleware stack: ${topPanel?.name}`, {
stack: panels.map((p) => p.name), stack: panels.map((p) => p.name),
topPanel: topPanel?.name, topPanel: topPanel?.name,
payload: action.payload, payload: action.payload,
@@ -137,7 +137,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(topPanel.name); // 🎯 isOnTop 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL] POP_PANEL:', { console.log(`[PANEL] POP_PANEL: ${topPanel.name}`, {
panelName: topPanel.name, panelName: topPanel.name,
panelInfo: topPanel.panelInfo || {}, panelInfo: topPanel.panelInfo || {},
isGNB, isGNB,
@@ -162,7 +162,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const stateAfter = store.getState(); const stateAfter = store.getState();
const panelHistoryAfter = stateAfter.panelHistory; const panelHistoryAfter = stateAfter.panelHistory;
const panelsAfter = stateAfter.panels.panels; const panelsAfter = stateAfter.panels.panels;
console.log('[PANEL_HISTORY] After POP_PANEL:', { console.log(`[PANEL_HISTORY] POP_PANEL: ${topPanel.name}`, {
panelHistory: panelHistoryAfter, panelHistory: panelHistoryAfter,
panels: panelsAfter, panels: panelsAfter,
}); });
@@ -185,7 +185,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const isGNB = isGNBCall(); const isGNB = isGNBCall();
const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산 const isOnTop = calculateIsOnTop(panelName); // 🎯 isOnTop 계산
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL] UPDATE_PANEL:', { console.log(`[PANEL] UPDATE_PANEL: ${panelName}`, {
panelName, panelName,
panelInfo, panelInfo,
isGNB, isGNB,
@@ -210,7 +210,7 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const stateAfter = store.getState(); const stateAfter = store.getState();
const panelHistoryAfter = stateAfter.panelHistory; const panelHistoryAfter = stateAfter.panelHistory;
const panelsAfter = stateAfter.panels.panels; const panelsAfter = stateAfter.panels.panels;
console.log('[PANEL_HISTORY] After UPDATE_PANEL:', { console.log(`[PANEL_HISTORY] UPDATE_PANEL: ${panelName}`, {
panelHistory: panelHistoryAfter, panelHistory: panelHistoryAfter,
panels: panelsAfter, panels: panelsAfter,
}); });
@@ -226,11 +226,15 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
// RESET_PANELS: GNB 네비게이션 또는 완전 초기화 // RESET_PANELS: GNB 네비게이션 또는 완전 초기화
case types.RESET_PANELS: { case types.RESET_PANELS: {
if (DEBUG_MODE) if (DEBUG_MODE) {
console.log('[PANEL] RESET_PANELS:', { const resetPanelNameForLog = (action.payload && action.payload.length > 0)
? action.payload[0].name
: 'homepanel';
console.log(`[PANEL] RESET_PANELS: ${resetPanelNameForLog}`, {
payload: action.payload, payload: action.payload,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
}
if (DEBUG_MODE) if (DEBUG_MODE)
console.log('[PANEL_HISTORY] Before RESET_PANELS:', store.getState().panelHistory); console.log('[PANEL_HISTORY] Before RESET_PANELS:', store.getState().panelHistory);
@@ -292,7 +296,10 @@ export const panelHistoryMiddleware = (store) => (next) => (action) => {
const stateAfter = store.getState(); const stateAfter = store.getState();
const panelHistoryAfter = stateAfter.panelHistory; const panelHistoryAfter = stateAfter.panelHistory;
const panelsAfter = stateAfter.panels.panels; const panelsAfter = stateAfter.panels.panels;
console.log('[PANEL_HISTORY] After RESET_PANELS:', { const resetPanelName = (action.payload && action.payload.length > 0)
? action.payload[0].name
: 'homepanel';
console.log(`[PANEL_HISTORY] RESET_PANELS: ${resetPanelName}`, {
panelHistory: panelHistoryAfter, panelHistory: panelHistoryAfter,
panels: panelsAfter, panels: panelsAfter,
}); });

View File

@@ -94,6 +94,12 @@ export const panelsReducer = (state = initialState, action) => {
} }
case types.POP_PANEL: { case types.POP_PANEL: {
// console.log('[💜UNIQUE_PANEL_STACK💜] POP_PANEL reducer START', {
// targetPanel: action.payload || 'last_panel',
// currentPanels: state.panels.map((p) => p.name),
// timestamp: Date.now(),
// });
dlog('[panelReducer] 🔴 POP_PANEL START', { dlog('[panelReducer] 🔴 POP_PANEL START', {
targetPanel: action.payload || 'last_panel', targetPanel: action.payload || 'last_panel',
currentPanels: state.panels.map((p) => p.name), currentPanels: state.panels.map((p) => p.name),
@@ -118,6 +124,13 @@ export const panelsReducer = (state = initialState, action) => {
resultPanels = state.panels.slice(0, state.panels.length - 1); resultPanels = state.panels.slice(0, state.panels.length - 1);
} }
// console.log('[💜UNIQUE_PANEL_STACK💜] POP_PANEL reducer END', {
// resultPanels: resultPanels.map((p) => p.name),
// panelCount: resultPanels.length,
// lastAction,
// timestamp: Date.now(),
// });
dlog('[panelReducer] 🔴 POP_PANEL END', { dlog('[panelReducer] 🔴 POP_PANEL END', {
resultPanels: resultPanels.map((p) => p.name), resultPanels: resultPanels.map((p) => p.name),
lastAction, lastAction,

View File

@@ -113,7 +113,8 @@ export const ACTIVE_POPUP = {
toast: 'toast', toast: 'toast',
optionalConfirm: 'optionalConfirm', optionalConfirm: 'optionalConfirm',
energyPopup: 'energyPopup', energyPopup: 'energyPopup',
addCartPopup: 'addCartPopup', addCartPopup: 'addCartPopup',
scrollPopup: 'scrollPopup',
}; };
export const DEBUG_VIDEO_SUBTITLE_TEST = false; export const DEBUG_VIDEO_SUBTITLE_TEST = false;
export const AUTO_SCROLL_DELAY = 600; export const AUTO_SCROLL_DELAY = 600;

View File

@@ -458,7 +458,7 @@ const tap = fp.curry((fn, value) => {
* @param {*} value 대상 값 * @param {*} value 대상 값
*/ */
const trace = fp.curry((label, value) => { const trace = fp.curry((label, value) => {
console.log(label, value); // console.log(label, value);
return value; return value;
}); });

View File

@@ -280,6 +280,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
useEffect(() => { useEffect(() => {
return () => { return () => {
// console.log('[🟡UNIQUE_DETAIL_CLEANUP🟡] DetailPanel cleanup - calling finishModalMediaForce');
dispatch(finishModalMediaForce()); dispatch(finishModalMediaForce());
}; };
}, [dispatch]); }, [dispatch]);
@@ -303,26 +304,25 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const sourcePanel = panelInfo?.sourcePanel; const sourcePanel = panelInfo?.sourcePanel;
const sourceMenu = panelInfo?.sourceMenu; const sourceMenu = panelInfo?.sourceMenu;
console.log('[DP-TRACE] Detail unmount start', { // console.log('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel cleanup/unmount triggered', {
sourcePanel, // sourcePanel,
sourceMenu, // sourceMenu,
panelsSnapshot: panels.map((p) => p.name), // panelsSnapshot: panels.map((p) => p.name),
}); // timestamp: Date.now(),
// });
console.log('[Detail-BG] 306-line sourcePanel:', sourcePanel, 'sourceMenu:', sourceMenu);
// DetailPanel이 unmount되는 시점 // DetailPanel이 unmount되는 시점
console.log('[DetailPanel] unmount:', { // console.log('[🔴UNIQUE_DETAIL_UNMOUNT🔴] DetailPanel unmount details:', {
sourcePanel, // sourcePanel,
sourceMenu, // sourceMenu,
timestamp: Date.now(), // timestamp: Date.now(),
}); // });
// sourcePanel에 따른 상태 업데이트 // sourcePanel에 따른 상태 업데이트
switch (sourcePanel) { switch (sourcePanel) {
case panel_names.PLAYER_PANEL: { case panel_names.PLAYER_PANEL: {
// PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달 // PlayerPanel에서 온 경우: PlayerPanel에 detailPanelClosed flag 전달
console.log('[DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달'); console.log('[PANEL][DetailPanel] unmount - PlayerPanel에 detailPanelClosed flag 전달');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -385,6 +385,14 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
const sourcePanel = panelInfo?.sourcePanel; const sourcePanel = panelInfo?.sourcePanel;
const sourceMenu = panelInfo?.sourceMenu; const sourceMenu = panelInfo?.sourceMenu;
// console.log('[🟠UNIQUE_DETAIL_BACK🟠] onBackClick triggered', {
// sourcePanel,
// sourceMenu,
// isCancelClick,
// currentPanels: panels.map((p) => p.name),
// timestamp: Date.now(),
// });
fp.pipe( fp.pipe(
() => { () => {
dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거 dispatch(clearAllToasts()); // BuyOption Toast 포함 모든 토스트 제거
@@ -393,7 +401,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
switch (sourcePanel) { switch (sourcePanel) {
case panel_names.PLAYER_PANEL: case panel_names.PLAYER_PANEL:
// PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리 // PlayerPanel에서 온 경우: 플레이어 비디오는 그대로 두고 모달만 정리
console.log('[DetailPanel] onBackClick - PlayerPanel 출신: 모달 정리만 수행'); console.log('[🟠UNIQUE_DETAIL_BACK🟠] PlayerPanel 출신: 모달 정리만 수행');
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview()); dispatch(finishVideoPreview());
break; break;
@@ -402,17 +410,18 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
case panel_names.SEARCH_PANEL: case panel_names.SEARCH_PANEL:
default: default:
// HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지 // HomePanel, SearchPanel 등에서 온 경우: 백그라운드 비디오 일시 중지
console.log( // console.log(
'[DetailPanel] onBackClick - source panel:', // '[🟠UNIQUE_DETAIL_BACK🟠] source panel:',
sourcePanel, // sourcePanel,
'백그라운드 비디오 일시 중지' // '백그라운드 비디오 일시 중지'
); // );
dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지 dispatch(pauseFullscreenVideo()); // PLAYER_PANEL 비디오 중지
dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료 dispatch(finishModalMediaForce()); // MEDIA_PANEL(ProductVideo) 강제 종료
dispatch(finishVideoPreview()); dispatch(finishVideoPreview());
break; break;
} }
// console.log('[🟠UNIQUE_DETAIL_BACK🟠] Calling popPanel(DETAIL_PANEL)');
dispatch(popPanel(panel_names.DETAIL_PANEL)); dispatch(popPanel(panel_names.DETAIL_PANEL));
}, },
() => { () => {
@@ -434,7 +443,7 @@ export default function DetailPanel({ panelInfo, isOnTop, spotlightId }) {
if (shouldUpdatePanel) { if (shouldUpdatePanel) {
console.log( console.log(
'[DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달' '[PANEL][DetailPanel] onBackClick - PlayerPanel에 detailPanelClosed flag 전달'
); );
dispatch( dispatch(
updatePanel({ updatePanel({

View File

@@ -1,9 +1,32 @@
import React, { useCallback } from "react"; import React, {
import css from "./ProductDescription.module.less"; useCallback,
import { $L, removeSpecificTags } from "../../../../utils/helperMethods"; useMemo,
import Spottable from "@enact/spotlight/Spottable"; } from 'react';
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
import Spotlight from "@enact/spotlight"; import {
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import {
setHidePopup,
setShowPopup,
} from '../../../../actions/commonActions';
import TButtonScroller
from '../../../../components/TButtonScroller/TButtonScroller';
import TNewPopUp from '../../../../components/TPopUp/TNewPopUp';
import * as Config from '../../../../utils/Config';
import {
$L,
removeSpecificTags,
} from '../../../../utils/helperMethods';
import css from './ProductDescription.module.less';
// TVerticalPagenator 제거됨 - TScrollerNew와 충돌 문제로 인해 // TVerticalPagenator 제거됨 - TScrollerNew와 충돌 문제로 인해
const SpottableComponent = Spottable("div"); const SpottableComponent = Spottable("div");
@@ -18,12 +41,23 @@ const Container = SpotlightContainerDecorator(
"div" "div"
); );
export default function ProductDescription({ productInfo }) { export default function ProductDescription({ productInfo }) {
const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
);
const dispatch = useDispatch();
const productDescription = useCallback(() => { const productDescription = useCallback(() => {
const sanitizedString = removeSpecificTags(productInfo?.prdtDesc); const sanitizedString = removeSpecificTags(productInfo?.prdtDesc);
return { __html: sanitizedString }; return { __html: sanitizedString };
}, [productInfo?.prdtDesc]); }, [productInfo?.prdtDesc]);
const productDescriptionText = useMemo(() => {
return removeSpecificTags(productInfo?.prdtDesc);
}, [productInfo?.prdtDesc]);
// 왼쪽 화살표 키 이벤트 처리 // 왼쪽 화살표 키 이벤트 처리
const handleKeyDown = useCallback((ev) => { const handleKeyDown = useCallback((ev) => {
if (ev.keyCode === 37) { // 왼쪽 화살표 키 if (ev.keyCode === 37) { // 왼쪽 화살표 키
@@ -34,6 +68,20 @@ export default function ProductDescription({ productInfo }) {
} }
}, []); }, []);
const descriptionClick = useCallback(() => {
dispatch(setShowPopup(Config.ACTIVE_POPUP.scrollPopup));
},
[dispatch]
);
const _onClose = useCallback(()=>{
dispatch(setHidePopup());
// Restore focus to the description content after popup closes
setTimeout(() => {
Spotlight.focus('product-description-content');
}, 100);
},[dispatch])
// ProductDescription: Container 직접 사용 패턴 // ProductDescription: Container 직접 사용 패턴
// prdtDesc가 없으면 렌더링하지 않음 // prdtDesc가 없으면 렌더링하지 않음
if (!productInfo?.prdtDesc) { if (!productInfo?.prdtDesc) {
@@ -41,36 +89,61 @@ export default function ProductDescription({ productInfo }) {
} }
return ( return (
<Container <>
className={css.descriptionContainer} <Container
spotlightId="product-description-container" className={css.descriptionContainer}
> spotlightId="product-description-container"
{/* <SpottableComponent
className={css.titleWrapper}
spotlightId="product-description-title"
onClick={() => console.log("[ProductDescription] Title clicked")}
onFocus={() => console.log("[ProductDescription] Title focused")}
onBlur={() => console.log("[ProductDescription] Title blurred")}
> */}
<div className={css.titleWrapper}>
<div className={css.title}>{$L("DESCRIPTION")}</div>
</div>
{/* </SpottableComponent> */}
<SpottableComponent
className={css.descriptionWrapper}
spotlightId="product-description-content"
onClick={() => console.log("[ProductDescription] Content clicked")}
onFocus={() => console.log("[ProductDescription] Content focused")}
onBlur={() => console.log("[ProductDescription] Content blurred")}
onKeyDown={handleKeyDown}
> >
<div {/* <SpottableComponent
className={css.productDescription} className={css.titleWrapper}
dangerouslySetInnerHTML={productDescription()} spotlightId="product-description-title"
/> onClick={() => console.log("[ProductDescription] Title clicked")}
</SpottableComponent> onFocus={() => console.log("[ProductDescription] Title focused")}
</Container> onBlur={() => console.log("[ProductDescription] Title blurred")}
> */}
<div className={css.titleWrapper}>
<div className={css.title}>{$L("DESCRIPTION")}</div>
</div>
{/* </SpottableComponent> */}
<SpottableComponent
className={css.descriptionWrapper}
spotlightId="product-description-content"
// onClick={() => console.log("[ProductDescription] Content clicked")}
onClick={descriptionClick}
onFocus={() => console.log("[ProductDescription] Content focused")}
onBlur={() => console.log("[ProductDescription] Content blurred")}
onKeyDown={handleKeyDown}
>
<div
className={css.productDescription}
dangerouslySetInnerHTML={productDescription()}
/>
</SpottableComponent>
</Container>
{activePopup === Config.ACTIVE_POPUP.scrollPopup && (
<TNewPopUp
kind="scrollPopup"
open={popupVisible}
hasText
title={$L("DESCRIPTION")}
onClick={_onClose}
hasButton
button1Text={$L("OK")}
>
<TButtonScroller
boxHeight={460}
width={844}
kind={"figmaTermsPopup"}
>
<div
className={css.scrollContainer}
dangerouslySetInnerHTML={{ __html: productDescriptionText }}
/>
</TButtonScroller>
</TNewPopUp>
)}
</>
); );
} }

View File

@@ -51,3 +51,8 @@
} }
} }
.scrollContainer {
padding: 31px;
font-size: 26px;
line-height: 1.5;
}

View File

@@ -5,11 +5,10 @@
position: relative; position: relative;
width: 1114px; // ProductDetail과 동일한 고정 크기 width: 1114px; // ProductDetail과 동일한 고정 크기
max-width: 1114px; max-width: 1114px;
height: 740px; // ProductDetail과 동일한 고정 높이 height: 632px !important; // ProductDetail과 동일한 고정 높이
margin-bottom: 30px; // ProductDetail과 동일한 간격
cursor: pointer; cursor: pointer;
background-color: rgba(0, 0, 0, 1); background-color: rgba(0, 0, 0, 1);
border-radius: 12px; border-radius: 12px 12px 0 0;
box-sizing: border-box; box-sizing: border-box;
padding: 6px; // 포커스 테두리를 위한 공간 padding: 6px; // 포커스 테두리를 위한 공간
overflow: hidden; overflow: hidden;
@@ -80,7 +79,7 @@
z-index: 23; // MediaPanel(z-index: 22)보다 위에 표시되어야 비디오 재생 중에도 포커스 테두리가 보임 z-index: 23; // MediaPanel(z-index: 22)보다 위에 표시되어야 비디오 재생 중에도 포커스 테두리가 보임
border: 6px solid @PRIMARY_COLOR_RED; border: 6px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5); box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
border-radius: 12px; border-radius: 12px 12px 0px 0px;
content: ""; content: "";
} }
@@ -217,30 +216,28 @@
} }
.notice { .notice {
width: calc(100% - 10px);
height: 54px;
background: #000000;
.flex(@justifyCenter:flex-start);
padding: 6px 18px 18px 18px;
border-radius: 0 0 12px 12px;
margin-bottom: 30px; // ProductDetail과 동일한 간격
.marquee {
width: 100%; width: 100%;
height: 54px; height: 100%;
background: #000000; }
.flex(@justifyCenter:flex-start); img {
padding: 6px 18px 18px 18px; width: 18px;
position: absolute; height: 18px;
bottom: 0; margin: 10px 12px 0 0;
border-radius: 0 0 12px 12px; object-fit: contain;
}
.marquee { span {
width: 100%; line-height: normal;
height: 100%; letter-spacing: normal;
} text-align: left;
img { .font(@fontFamily:@baseFont, @fontSize:20px);
width: 18px; color: @COLOR_GRAY04;
height: 18px; }
margin: 10px 12px 0 0; }
object-fit: contain;
}
span {
line-height: normal;
letter-spacing: normal;
text-align: left;
.font(@fontFamily:@baseFont, @fontSize:20px);
color: @COLOR_GRAY04;
}
}

View File

@@ -311,6 +311,7 @@ export default function ProductVideo({
if (!canPlayVideo) return null; if (!canPlayVideo) return null;
return ( return (
<>
<SpottableComponent <SpottableComponent
className={css.videoContainer} className={css.videoContainer}
onClick={handleVideoClick} onClick={handleVideoClick}
@@ -330,12 +331,13 @@ export default function ProductVideo({
<img src={playImg} alt="재생" /> <img src={playImg} alt="재생" />
</div> </div>
</div> </div>
<div className={css.notice}>
<Marquee className={css.marquee} marqueeOn="render">
<img src={ic_warning} alt={disclaimer} />
<span>{disclaimer}</span>
</Marquee>
</div>
</SpottableComponent> </SpottableComponent>
<div className={css.notice}>
<Marquee className={css.marquee} marqueeOn="render">
<img src={ic_warning} alt={disclaimer} />
<span>{disclaimer}</span>
</Marquee>
</div>
</>
); );
} }

View File

@@ -1,6 +1,7 @@
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useState,
} from 'react'; } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -26,17 +27,35 @@ export default function UserReviewDetail({
onNext, onNext,
className, className,
}) { }) {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
// 새로운 리뷰가 로드될 때 이미지 인덱스 초기화
useEffect(() => {
setCurrentImageIndex(0);
}, [currentReview]);
const reviewImages = currentReview?.reviewImageList || [];
const hasMultipleImages = reviewImages.length > 1;
const handlePrevious = useCallback(() => { const handlePrevious = useCallback(() => {
if (onPrevious && currentIndex > 0) { // 이미지가 여러 개이고 현재 이미지가 첫 번째가 아니면 이미지만 변경
if (hasMultipleImages && currentImageIndex > 0) {
setCurrentImageIndex(prev => prev - 1);
} else if (onPrevious && currentIndex > 0) {
// 이미지가 첫 번째이면 이전 리뷰로 이동
onPrevious(); onPrevious();
} }
}, [onPrevious, currentIndex]); }, [onPrevious, currentIndex, hasMultipleImages, currentImageIndex]);
const handleNext = useCallback(() => { const handleNext = useCallback(() => {
if (onNext && currentIndex < totalReviews - 1) { // 이미지가 여러 개이고 현재 이미지가 마지막이 아니면 이미지만 변경
if (hasMultipleImages && currentImageIndex < reviewImages.length - 1) {
setCurrentImageIndex(prev => prev + 1);
} else if (onNext && currentIndex < totalReviews - 1) {
// 이미지가 마지막이면 다음 리뷰로 이동
onNext(); onNext();
} }
}, [onNext, currentIndex, totalReviews]); }, [onNext, currentIndex, totalReviews, hasMultipleImages, currentImageIndex, reviewImages.length]);
// 리뷰 데이터가 없을 때 처리 // 리뷰 데이터가 없을 때 처리
if (!currentReview) { if (!currentReview) {
@@ -47,9 +66,7 @@ export default function UserReviewDetail({
); );
} }
const reviewImage = const reviewImage = reviewImages[currentImageIndex];
currentReview.reviewImageList && currentReview.reviewImageList[0];
const hasMultipleReviews = totalReviews > 1;
const formatDate = (dateStr) => { const formatDate = (dateStr) => {
const [year, month, day] = dateStr.split("-"); const [year, month, day] = dateStr.split("-");
@@ -59,7 +76,7 @@ export default function UserReviewDetail({
return ( return (
<> <>
{/* Left Arrow - 이전 리뷰가 있을 때만 표시 */} {/* Left Arrow - 이전 리뷰가 있을 때만 표시 */}
{hasMultipleReviews && currentIndex > 0 && ( {(hasMultipleImages || currentIndex > 0) && (
<SpottableButton <SpottableButton
className={css.leftArrow} className={css.leftArrow}
onClick={handlePrevious} onClick={handlePrevious}
@@ -128,7 +145,7 @@ export default function UserReviewDetail({
</div> </div>
</div> </div>
{/* Right Arrow - 다음 리뷰가 있을 때만 표시 */} {/* Right Arrow - 다음 리뷰가 있을 때만 표시 */}
{hasMultipleReviews && currentIndex < totalReviews - 1 && ( {(hasMultipleImages || currentIndex < totalReviews - 1) && (
<SpottableButton <SpottableButton
className={css.rightArrow} className={css.rightArrow}
onClick={handleNext} onClick={handleNext}
@@ -138,4 +155,4 @@ export default function UserReviewDetail({
)} )}
</> </>
); );
} }

View File

@@ -1,21 +1,41 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux'; import {
useDispatch,
useSelector,
} from 'react-redux';
import { Job } from '@enact/core/util'; import { Job } from '@enact/core/util';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { clearThemeDetail } from '../../../../actions/homeActions'; import { clearThemeDetail } from '../../../../actions/homeActions';
import { finishModalMediaForce } from '../../../../actions/mediaActions'; import { finishModalMediaForce } from '../../../../actions/mediaActions';
import { popPanel, pushPanel, updatePanel } from '../../../../actions/panelActions'; import {
popPanel,
pushPanel,
updatePanel,
} from '../../../../actions/panelActions';
import { finishVideoPreview } from '../../../../actions/playActions'; import { finishVideoPreview } from '../../../../actions/playActions';
import THeader from '../../../../components/THeader/THeader'; import THeader from '../../../../components/THeader/THeader';
import TItemCardNew from '../../../../components/TItemCard/TItemCard.new'; import TItemCardNew from '../../../../components/TItemCard/TItemCard.new';
import TVerticalPagenator from '../../../../components/TVerticalPagenator/TVerticalPagenator'; import TVerticalPagenator
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; from '../../../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList
from '../../../../components/TVirtualGridList/TVirtualGridList';
import useScrollTo from '../../../../hooks/useScrollTo'; import useScrollTo from '../../../../hooks/useScrollTo';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config'; import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../../utils/Config';
import { $L } from '../../../../utils/helperMethods'; import { $L } from '../../../../utils/helperMethods';
import css from './YouMayAlsoLike.module.less'; import css from './YouMayAlsoLike.module.less';
@@ -158,7 +178,7 @@ export default function YouMayAlsoLike({
// DetailPanel을 언마운트하지 않고 상품 정보만 업데이트 // DetailPanel을 언마운트하지 않고 상품 정보만 업데이트
// 이렇게 하면 백그라운드 비디오 제어 상태가 유지됨 // 이렇게 하면 백그라운드 비디오 제어 상태가 유지됨
dispatch( dispatch(
updatePanel({ pushPanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
panelInfo: { panelInfo: {
showNm: panelInfo?.showNm, showNm: panelInfo?.showNm,
@@ -170,8 +190,8 @@ export default function YouMayAlsoLike({
launchedFromPlayer: launchedFromPlayer, launchedFromPlayer: launchedFromPlayer,
bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지 bgVideoInfo: bgVideoInfo, // 백그라운드 비디오 정보 유지
fromPanel: { fromPanel: {
fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시 fromYouMayLike: true, // YouMayLike에서 선택된 상품임을 표시
}, // 출처 정보 통합 객체 }, // 출처 정보 통합 객체
}, },
}) })
); );

View File

@@ -1,5 +1,5 @@
@import "../../../../../style/CommonStyle.module.less"; @import '../../../../../style/CommonStyle.module.less';
@import "../../../../../style/utils.module.less"; @import '../../../../../style/utils.module.less';
.wrapper { .wrapper {
height: 100%; height: 100%;
@@ -30,7 +30,7 @@
} }
.name { .name {
font-weight: bold; font-weight: bold;
font-size: 36px; font-size: 30px;
color: @COLOR_WHITE; color: @COLOR_WHITE;
width: 100%; width: 100%;
} }

View File

@@ -1,5 +1,5 @@
@import "../../../../../style/CommonStyle.module.less"; @import '../../../../../style/CommonStyle.module.less';
@import "../../../../../style/utils.module.less"; @import '../../../../../style/utils.module.less';
.wrapper { .wrapper {
height: 100%; height: 100%;
@@ -30,7 +30,7 @@
} }
.name { .name {
font-weight: bold; font-weight: bold;
font-size: 36px; font-size: 30px;
color: @COLOR_WHITE; color: @COLOR_WHITE;
width: 100%; width: 100%;
} }
@@ -40,7 +40,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.btmLayer2 { .btmLayer2 {
margin: 5px 0; margin: 5px 0;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -78,35 +78,35 @@ export default function ThemeItemCard({
const { originalPrice, discountedPrice, discountRate } = const { originalPrice, discountedPrice, discountRate } =
usePriceInfo(priceInfo) || {}; usePriceInfo(priceInfo) || {};
const mockEnergyLabel = [ // const mockEnergyLabel = [
{ // {
"enrgLblExpsOrd": "0", // "enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05", // "enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC", // "enrgLblCd": "MNLC",
"enrgClasCd": "A", // "enrgClasCd": "A",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_a.png", // "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_a.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241113229264&ORIGINAL_NAME_b1_a1=27U511SA EU (E).pdf&FILE_NAME=27U511SA EU (E)[20241113011401634].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", // "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241113229264&ORIGINAL_NAME_b1_a1=27U511SA EU (E).pdf&FILE_NAME=27U511SA EU (E)[20241113011401634].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241125231113&ORIGINAL_NAME_b1_a1=27U511SA-W.pdf&FILE_NAME=27U511SA-W[20241125231113].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" // "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241125231113&ORIGINAL_NAME_b1_a1=27U511SA-W.pdf&FILE_NAME=27U511SA-W[20241125231113].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
}, // },
{ // {
"enrgLblExpsOrd": "0", // "enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05", // "enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC", // "enrgLblCd": "MNLC",
"enrgClasCd": "D", // "enrgClasCd": "D",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png", // "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", // "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" // "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
}, // },
{ // {
"enrgLblExpsOrd": "0", // "enrgLblExpsOrd": "0",
"enrgLblTpCd": "EL_TYPE_05", // "enrgLblTpCd": "EL_TYPE_05",
"enrgLblCd": "MNLC", // "enrgLblCd": "MNLC",
"enrgClasCd": "D", // "enrgClasCd": "D",
"enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png", // "enrgLblIcnUrl": "http://eic-ngfts.lge.com/fts/gftsDownload.lge?biz_code=LGSHOPPING&func_code=IMAGE&file_path=/lgshopping/image/gb_class_arrows_ag_d.png",
"enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N", // "enrgLblUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241230236057&ORIGINAL_NAME_b1_a1=27U421A EU (E).pdf&FILE_NAME=27U421A EU (E)[20241230015816192].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N",
"enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N" // "enrgShetUrl": "https://www.lg.com/uk/lgecs.downloadFile.ldwf?DOC_ID=20241224235911&ORIGINAL_NAME_b1_a1=27U421A-B.pdf&FILE_NAME=27U421A-B[20241224235911].pdf&TC=DwnCmd&GSRI_DOC=GSRI&SPEC_DOWNLOAD=N"
} // }
]; // ];
const setEnactFitZIndex = (zIndexValue) => { const setEnactFitZIndex = (zIndexValue) => {
const target = document.getElementById("floatLayer"); const target = document.getElementById("floatLayer");
@@ -211,7 +211,7 @@ export default function ThemeItemCard({
))} ))}
</div> </div>
)} */} )} */}
{mockEnergyLabel && mockEnergyLabel.length > 0 && ( {/* {mockEnergyLabel && mockEnergyLabel.length > 0 && (
<div className={css.energyLabels}> <div className={css.energyLabels}>
{mockEnergyLabel.map((label, labelIndex) => ( {mockEnergyLabel.map((label, labelIndex) => (
<SpottableTemp <SpottableTemp
@@ -228,7 +228,7 @@ export default function ThemeItemCard({
</SpottableTemp> </SpottableTemp>
))} ))}
</div> </div>
)} )} */}
</div> </div>
</SpottableDiv> </SpottableDiv>
{(() => { {(() => {

View File

@@ -93,9 +93,6 @@ export default function THeaderCustom({
role="button" role="button"
/> />
)} )}
{type === "theme" && themeTitle && (
<span className={css.themeTitle} dangerouslySetInnerHTML={{ __html: themeTitle }} />
)}
{kind ? ( {kind ? (
"" ""
) : ( ) : (
@@ -107,6 +104,9 @@ export default function THeaderCustom({
}} }}
/> />
)} )}
{type === "theme" && themeTitle && (
<span className={css.themeTitle} dangerouslySetInnerHTML={{ __html: `[${themeTitle}]` }} />
)}
<Marquee <Marquee
marqueeOn="render" marqueeOn="render"
className={css.title} className={css.title}

View File

@@ -61,4 +61,5 @@
color: #eaeaea; color: #eaeaea;
width: max-content; width: max-content;
margin-right: 20px; margin-right: 20px;
margin-left: 10px;
} }

View File

@@ -1,14 +1,26 @@
import React, { useCallback, useEffect, useRef, useState, useMemo, forwardRef } from 'react'; import React, {
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { off, on } from '@enact/core/dispatcher'; import {
off,
on,
} from '@enact/core/dispatcher';
import { Job } from '@enact/core/util'; import { Job } from '@enact/core/util';
import Scroller from '@enact/sandstone/Scroller'; import Scroller from '@enact/sandstone/Scroller';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import AutoScrollAreaDetail, { POSITION } from '../AutoScrollAreaDetail/AutoScrollAreaDetail'; import AutoScrollAreaDetail, {
POSITION,
} from '../AutoScrollAreaDetail/AutoScrollAreaDetail';
import css from './TScrollerDetail.module.less'; import css from './TScrollerDetail.module.less';
/** /**
@@ -206,7 +218,8 @@ const TScrollerDetail = forwardRef(
onScrollStop={_onScrollStop} onScrollStop={_onScrollStop}
onScroll={_onScroll} onScroll={_onScroll}
scrollMode={scrollMode || 'translate'} scrollMode={scrollMode || 'translate'}
focusableScrollbar={focusableScrollbar} // focusableScrollbar={focusableScrollbar}
focusableScrollbar={false}
className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)} className={classNames(isMounted && css.tScroller, noScrollByWheel && css.preventScroll)}
direction={direction} direction={direction}
horizontalScrollbar={horizontalScrollbar} horizontalScrollbar={horizontalScrollbar}
@@ -220,7 +233,7 @@ const TScrollerDetail = forwardRef(
}} }}
noScrollByWheel={noScrollByWheel} noScrollByWheel={noScrollByWheel}
noScrollByDrag noScrollByDrag
// rest props에서 ref만 제외하고 전달 // rest props에서 ref만 제외하고 전달
{...(rest.ref ? { ...rest, ref: undefined } : rest)} {...(rest.ref ? { ...rest, ref: undefined } : rest)}
> >
{children} {children}

View File

@@ -5,8 +5,8 @@ import { useDispatch, useSelector } from "react-redux";
import { Job } from "@enact/core/util"; import { Job } from "@enact/core/util";
import Spotlight from "@enact/spotlight"; import Spotlight from "@enact/spotlight";
import { updatePanel } from "../../../../../actions/panelActions"; import { pushPanel, updatePanel } from "../../../../../actions/panelActions";
import { startVideoPlayer } from "../../../../../actions/playActions"; // import { startVideoPlayer } from "../../../../../actions/playActions";
import TItemCard, { import TItemCard, {
removeDotAndColon, removeDotAndColon,
} from "../../../../../components/TItemCard/TItemCard"; } from "../../../../../components/TItemCard/TItemCard";
@@ -113,27 +113,36 @@ export default function RecommendedShowsProductList({
); );
} }
let y = // 🆕 DetailPanel로 이동 (ShopByShow 방식)
index < 2
? 0
: index === 2
? scaleH(208)
: scaleH(index * 248 - 248 - 40);
dispatch( dispatch(
startVideoPlayer({ pushPanel({
modal: false, name: panel_names.DETAIL_PANEL,
patnrId, panelInfo: { patnrId, prdtId },
prdtId,
showId,
shptmBanrTpNm: "VOD",
thumbnail: videoThumbnail,
targetId: "spotlightId-" + prdtId,
y,
}) })
); );
// 🔴 기존 PlayerPanel 로직 (주석처리)
// let y =
// index < 2
// ? 0
// : index === 2
// ? scaleH(208)
// : scaleH(index * 248 - 248 - 40);
//
// dispatch(
// startVideoPlayer({
// modal: false,
// patnrId,
// prdtId,
// showId,
// shptmBanrTpNm: "VOD",
// thumbnail: videoThumbnail,
// targetId: "spotlightId-" + prdtId,
// y,
// })
// );
}, },
[catCd, dispatch, patnrId, showId, videoThumbnail] [catCd, dispatch, patnrId]
); );
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {

View File

@@ -6,7 +6,11 @@ import Spotlight from '@enact/spotlight';
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator'; import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { pushPanel, updatePanel, navigateFromBestSeller } from '../../../actions/panelActions'; import {
navigateFromBestSeller,
pushPanel,
updatePanel,
} from '../../../actions/panelActions';
import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions'; import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions';
import SectionTitle from '../../../components/SectionTitle/SectionTitle'; import SectionTitle from '../../../components/SectionTitle/SectionTitle';
import Tag from '../../../components/TItemCard/Tag'; import Tag from '../../../components/TItemCard/Tag';
@@ -15,13 +19,20 @@ import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller'; import TScroller from '../../../components/TScroller/TScroller';
import useScrollReset from '../../../hooks/useScrollReset'; import useScrollReset from '../../../hooks/useScrollReset';
import useScrollTo from '../../../hooks/useScrollTo'; import useScrollTo from '../../../hooks/useScrollTo';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config'; import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../utils/Config';
import { $L, scaleW } from '../../../utils/helperMethods'; import { $L, scaleW } from '../../../utils/helperMethods';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import css from './BestSeller.module.less'; import css from './BestSeller.module.less';
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const Container = SpotlightContainerDecorator(
{ enterTo: 'last-focused' },
'div'
);
const BestSeller = ({ const BestSeller = ({
order, order,
@@ -33,15 +44,26 @@ const BestSeller = ({
shelfTitle, shelfTitle,
}) => { }) => {
const { getScrollTo, scrollLeft } = useScrollTo(); const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, true); const { handleScrollReset, handleStopScrolling } = useScrollReset(
scrollLeft,
true
);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { cursorVisible } = useSelector((state) => state.common.appStatus); const { cursorVisible } = useSelector((state) => state.common.appStatus);
const bestSellerDatas = useSelector((state) => state.product.bestSellerData?.bestSeller); const bestSellerDatas = useSelector(
(state) => state.product.bestSellerData?.bestSeller
);
const bestSellerNewDatas = useSelector((state) => state.foryou?.recommendInfo?.recommendProduct); const bestSellerNewDatas = useSelector(
(state) => state.foryou?.recommendInfo?.recommendProduct
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const [drawChk, setDrawChk] = useState(false); const [drawChk, setDrawChk] = useState(false);
const [firstChk, setFirstChk] = useState(0); const [firstChk, setFirstChk] = useState(0);
@@ -51,37 +73,45 @@ const BestSeller = ({
useEffect(() => { useEffect(() => {
setBestInfos( setBestInfos(
bestSellerNewDatas?.filter((item) => item.recommendTpCd === 'BESTSELLER') || [] // 기본값으로 빈 배열 설정 bestSellerNewDatas?.filter(
(item) => item.recommendTpCd === 'BESTSELLER'
) || [] // 기본값으로 빈 배열 설정
); );
}, [bestSellerNewDatas]); }, [bestSellerNewDatas]);
useEffect(() => { useEffect(() => {
if (!bestInfos || bestInfos.length === 0) { if (userNumber) {
if (!bestInfos || bestInfos.length === 0) {
const baseData =
bestSellerDatas?.map((item) => ({
...item,
foryou: false,
})) || [];
setBestItemNewData(baseData);
return;
}
const recommendedData =
bestInfos[0].productInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(
recommendedData.map((item) => item.prdtId)
);
const baseData = const baseData =
bestSellerDatas?.map((item) => ({ bestSellerDatas?.map((item) => ({
...item, ...item,
foryou: false, foryou: recommendedPrdtIds.has(item.prdtId),
})) || []; })) || [];
setBestItemNewData(baseData); setBestItemNewData(baseData);
return; } else {
setBestItemNewData(bestSellerDatas);
} }
}, [bestSellerDatas, bestInfos, userNumber]);
const recommendedData =
bestInfos[0].productInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId));
const baseData =
bestSellerDatas?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.prdtId),
})) || [];
setBestItemNewData(baseData);
}, [bestSellerDatas, bestInfos]);
const orderStyle = useMemo(() => ({ order: order }), [order]); const orderStyle = useMemo(() => ({ order: order }), [order]);
@@ -144,7 +174,10 @@ const BestSeller = ({
if (c) { if (c) {
let cAriaLabel = c.getAttribute('aria-label'); let cAriaLabel = c.getAttribute('aria-label');
if (cAriaLabel) { if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace('Best Seller, Heading 1,', ''); const newcAriaLabel = cAriaLabel.replace(
'Best Seller, Heading 1,',
''
);
c.setAttribute('aria-label', newcAriaLabel); c.setAttribute('aria-label', newcAriaLabel);
} }
} }

View File

@@ -54,6 +54,9 @@ export default function HomeBanner({
const popupVisible = useSelector((state) => state.common.popup.popupVisible); const popupVisible = useSelector((state) => state.common.popup.popupVisible);
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
const isDeepLinkEntry = useSelector(
(state) => state.home.homeInfo?.panelInfo?.isDeepLinkEntry
);
// 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리 // 🔽 useFocusHistory - 경량화된 범용 포커스 히스토리
const focusHistory = useFocusHistory({ const focusHistory = useFocusHistory({
enableLogging: true, enableLogging: true,
@@ -164,10 +167,11 @@ export default function HomeBanner({
videoData = targetBannerData.bannerDetailInfos?.[0]; videoData = targetBannerData.bannerDetailInfos?.[0];
} }
// DetailPanel이 떠 있는 동안에는 배너 자동 재생을 스킵 (PlayerPanel 모달 재설정 방지) // 🔽 [251221] DetailPanel이나 DeepLink PlayerPanel이 떠 있으면 배너 자동 재생 스킵
const hasDetailPanel = panels.some((p) => p.name === panel_names.DETAIL_PANEL); const hasDetailPanel = panels.some((p) => p.name === panel_names.DETAIL_PANEL);
const hasPlayerPanel = panels.some((p) => p.name === panel_names.PLAYER_PANEL);
if (!hasDetailPanel && videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) { if (!hasDetailPanel && !hasPlayerPanel && !isDeepLinkEntry && videoData && (videoData.shptmBanrTpNm === 'LIVE' || videoData.shptmBanrTpNm === 'VOD')) {
console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus); console.log('[HomeBanner] 초기 비디오 자동 재생:', defaultFocus);
dispatch( dispatch(
@@ -185,7 +189,7 @@ export default function HomeBanner({
}) })
); );
} }
}, [bannerDataList, defaultFocus, dispatch, panels]); }, [bannerDataList, defaultFocus, dispatch, panels, isDeepLinkEntry]);
const renderItem = useCallback( const renderItem = useCallback(
(index, isHorizontal) => { (index, isHorizontal) => {

View File

@@ -617,6 +617,15 @@ export default function RandomUnit({
// 비디오 클릭 // 비디오 클릭
const videoClick = useCallback(() => { const videoClick = useCallback(() => {
// 🔽 비디오가 다른 배너에서 modal=true로 이미 재생 중이면 클릭 무시
if (playerPanelInfo?.modal === true && currentVideoBannerId && currentVideoBannerId !== spotlightId) {
console.log('[RandomUnit] videoClick 무시: 다른 배너에서 modal=true로 재생 중', {
currentVideoBannerId,
clickedBannerId: spotlightId,
});
return;
}
const lastFocusedTargetId = getContainerId(Spotlight.getCurrent()); const lastFocusedTargetId = getContainerId(Spotlight.getCurrent());
const currentSpot = Spotlight.getCurrent(); const currentSpot = Spotlight.getCurrent();
if (lastFocusedTargetId) { if (lastFocusedTargetId) {
@@ -674,6 +683,7 @@ export default function RandomUnit({
sendBannerLog, sendBannerLog,
onBlur, onBlur,
playerPanelInfo?.modal, playerPanelInfo?.modal,
currentVideoBannerId,
dispatch, dispatch,
handleStartVideo, handleStartVideo,
]); ]);

View File

@@ -7,10 +7,7 @@ import React, {
} from 'react'; } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { useDispatch, useSelector } from 'react-redux';
useDispatch,
useSelector,
} from 'react-redux';
import { applyMiddleware } from 'redux'; import { applyMiddleware } from 'redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
@@ -23,11 +20,11 @@ import {
import hsn from '../../../assets/images/bg/hsn_new.png'; import hsn from '../../../assets/images/bg/hsn_new.png';
import koreaKiosk from '../../../assets/images/bg/koreaKiosk_new.png'; import koreaKiosk from '../../../assets/images/bg/koreaKiosk_new.png';
import lgelectronics from '../../../assets/images/bg/lgelectronics_new.png'; import lgelectronics from '../../../assets/images/bg/lgelectronics_new.png';
import nbcu from '../../../assets/images/bg/nbcu_new.png';
import ontv4u from '../../../assets/images/bg/ontv4u_new.png'; import ontv4u from '../../../assets/images/bg/ontv4u_new.png';
import Pinkfong from '../../../assets/images/bg/Pinkfong_new.png'; import Pinkfong from '../../../assets/images/bg/Pinkfong_new.png';
import qvc from '../../../assets/images/bg/qvc_new.png'; import qvc from '../../../assets/images/bg/qvc_new.png';
import shoplc from '../../../assets/images/bg/shoplc_new.png'; import shoplc from '../../../assets/images/bg/shoplc_new.png';
import nbcu from '../../../assets/images/bg/nbcu_new.png';
import { types } from '../../actions/actionTypes'; import { types } from '../../actions/actionTypes';
import { import {
changeAppStatus, changeAppStatus,
@@ -45,14 +42,8 @@ import {
getHomeMainContents, getHomeMainContents,
updateHomeInfo, updateHomeInfo,
} from '../../actions/homeActions'; } from '../../actions/homeActions';
import { import { sendLogGNB, sendLogTotalRecommend } from '../../actions/logActions';
sendLogGNB, import { getSubCategory, getTop20Show } from '../../actions/mainActions';
sendLogTotalRecommend,
} from '../../actions/logActions';
import {
getSubCategory,
getTop20Show,
} from '../../actions/mainActions';
import { setMyPageTermsAgree } from '../../actions/myPageActions'; import { setMyPageTermsAgree } from '../../actions/myPageActions';
import { getHomeOnSaleInfo } from '../../actions/onSaleActions'; import { getHomeOnSaleInfo } from '../../actions/onSaleActions';
import { updatePanel } from '../../actions/panelActions'; import { updatePanel } from '../../actions/panelActions';
@@ -69,8 +60,7 @@ import TButton, { TYPES } from '../../components/TButton/TButton';
import TPanel from '../../components/TPanel/TPanel'; import TPanel from '../../components/TPanel/TPanel';
import TNewPopUp from '../../components/TPopUp/TNewPopUp'; import TNewPopUp from '../../components/TPopUp/TNewPopUp';
import TPopUp from '../../components/TPopUp/TPopUp'; import TPopUp from '../../components/TPopUp/TPopUp';
import TVerticalPagenator import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
from '../../components/TVerticalPagenator/TVerticalPagenator';
import useDebugKey from '../../hooks/useDebugKey'; import useDebugKey from '../../hooks/useDebugKey';
import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory'; import { useFocusHistory } from '../../hooks/useFocusHistory/useFocusHistory';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
@@ -178,68 +168,113 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// }); // });
const isGnbOpened = useSelector((state) => state.common.isGnbOpened); const isGnbOpened = useSelector((state) => state.common.isGnbOpened);
const homeLayoutInfo = useSelector((state) => state.home.layoutData); const homeLayoutInfo = useSelector((state) => state.home.layoutData);
const panelInfo = useSelector((state) => state.home.homeInfo?.panelInfo ?? {}); const panelInfo = useSelector(
(state) => state.home.homeInfo?.panelInfo ?? {}
);
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
const webOSVersion = useSelector((state) => state.common.appStatus?.webOSVersion); const webOSVersion = useSelector(
(state) => state.common.appStatus?.webOSVersion
);
const enterThroughGNB = useSelector((state) => state.home.enterThroughGNB); const enterThroughGNB = useSelector((state) => state.home.enterThroughGNB);
const defaultFocus = useSelector((state) => state.home.defaultFocus); const defaultFocus = useSelector((state) => state.home.defaultFocus);
const bannerDataList = useSelector((state) => state.home.bannerData?.bannerInfos); const bannerDataList = useSelector(
(state) => state.home.bannerData?.bannerInfos
);
// ✅ PlayerPanel의 shouldShrinkTo1px 상태 추적 // ✅ PlayerPanel의 shouldShrinkTo1px 상태 추적
const playerPanelShouldShrink = useSelector((state) => { const playerPanelShouldShrink = useSelector((state) => {
const playerPanel = state.panels.panels.find((p) => p.name === panel_names.PLAYER_PANEL); const playerPanel = state.panels.panels.find(
(p) => p.name === panel_names.PLAYER_PANEL
);
return playerPanel?.panelInfo?.shouldShrinkTo1px ?? false; return playerPanel?.panelInfo?.shouldShrinkTo1px ?? false;
}); });
// ✅ PlayerPanel의 modal 상태 추적 (false → true 감지용) // ✅ PlayerPanel의 modal 상태 추적 (false → true 감지용)
const playerModalState = useSelector((state) => { const playerModalState = useSelector((state) => {
const playerPanel = state.panels.panels.find((p) => p.name === panel_names.PLAYER_PANEL); const playerPanel = state.panels.panels.find(
(p) => p.name === panel_names.PLAYER_PANEL
);
return playerPanel?.panelInfo?.modal ?? false; return playerPanel?.panelInfo?.modal ?? false;
}); });
const prevPlayerModalStateRef = useRef(false); const prevPlayerModalStateRef = useRef(false);
const categoryInfos = useSelector((state) => state.onSale.homeOnSaleData?.data?.categoryInfos); const categoryInfos = useSelector(
(state) => state.onSale.homeOnSaleData?.data?.categoryInfos
);
const categoryItemInfos = useSelector((state) => state.main.subCategoryData?.categoryItemInfos); const categoryItemInfos = useSelector(
(state) => state.main.subCategoryData?.categoryItemInfos
);
const { popupVisible, activePopup } = useSelector((state) => state.common.popup); const { popupVisible, activePopup } = useSelector(
(state) => state.common.popup
);
const eventPopInfosData = useSelector((state) => state.event.eventData.eventPopInfo); const eventPopInfosData = useSelector(
(state) => state.event.eventData.eventPopInfo
);
const eventData = useSelector((state) => state.event.eventData); const eventData = useSelector((state) => state.event.eventData);
const eventClickSuccess = useSelector((state) => state.event.eventClickSuccess); const eventClickSuccess = useSelector(
const homeOnSaleInfos = useSelector((state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos); (state) => state.event.eventClickSuccess
const bestSellerDatas = useSelector((state) => state.product.bestSellerData?.bestSeller); );
const homeOnSaleInfos = useSelector(
(state) => state.onSale.homeOnSaleData?.data.homeOnSaleInfos
);
const bestSellerDatas = useSelector(
(state) => state.product.bestSellerData?.bestSeller
);
const topInfos = useSelector((state) => state.main.top20ShowData.topInfos); const topInfos = useSelector((state) => state.main.top20ShowData.topInfos);
const isDeepLink = useSelector((state) => state.common.deepLinkInfo.isDeepLink); const isDeepLink = useSelector(
(state) => state.common.deepLinkInfo.isDeepLink
);
// 선택약관 관련 Redux 상태 // 선택약관 관련 Redux 상태
const termsData = useSelector((state) => state.home.termsData); const termsData = useSelector((state) => state.home.termsData);
const termsIdMap = useSelector((state) => state.home.termsIdMap); const termsIdMap = useSelector((state) => state.home.termsIdMap);
const optionalTermsAvailable = useSelector((state) => state.home.optionalTermsAvailable); const optionalTermsAvailable = useSelector(
(state) => state.home.optionalTermsAvailable
);
const optionalTermsData = useSelector((state) => { const optionalTermsData = useSelector((state) => {
if (state.home.termsData && state.home.termsData.data && state.home.termsData.data.terms) { if (
return state.home.termsData.data.terms.find((term) => term.trmsTpCd === 'MST00405'); state.home.termsData &&
state.home.termsData.data &&
state.home.termsData.data.terms
) {
return state.home.termsData.data.terms.find(
(term) => term.trmsTpCd === 'MST00405'
);
} }
return null; return null;
}); });
const termsLoading = useSelector((state) => state.common.termsLoading); const termsLoading = useSelector((state) => state.common.termsLoading);
const currentTermsFlag = useSelector((state) => state.common.termsFlag); const currentTermsFlag = useSelector((state) => state.common.termsFlag);
const optionalTermsPopupFlow = useSelector((state) => state.common.optionalTermsPopupFlow); const optionalTermsPopupFlow = useSelector(
(state) => state.common.optionalTermsPopupFlow
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const [btnDisabled, setBtnDisabled] = useState(true); const [btnDisabled, setBtnDisabled] = useState(true);
const [arrowBottom, setArrowBottom] = useState(true); const [arrowBottom, setArrowBottom] = useState(true);
const [firstSpot, setFirstSpot] = useState(false); const [firstSpot, setFirstSpot] = useState(false);
const [eventPopOpen, setEventPopOpen] = useState(false); const [eventPopOpen, setEventPopOpen] = useState(false);
const [nowShelf, setNowShelf] = useState(panelInfo.nowShelf); const [nowShelf, setNowShelf] = useState(panelInfo.nowShelf);
const [firstLgCatCd, setFirstLgCatCd] = useState(panelInfo.currentCatCd ?? null); const [firstLgCatCd, setFirstLgCatCd] = useState(
panelInfo.currentCatCd ?? null
);
const [cateCd, setCateCd] = useState(panelInfo.currentCatCd ?? null); const [cateCd, setCateCd] = useState(panelInfo.currentCatCd ?? null);
const [cateNm, setCateNm] = useState(panelInfo.currentCateName ?? null); const [cateNm, setCateNm] = useState(panelInfo.currentCateName ?? null);
// 선택약관 팝업 상태 // 선택약관 팝업 상태
const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] = useState(false); const [isOptionalConfirmVisible, setIsOptionalConfirmVisible] =
useState(false);
const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false); const [isOptionalTermsVisible, setIsOptionalTermsVisible] = useState(false);
const [optionalTermsAgreed, setOptionalTermsAgreed] = useState(false); const [optionalTermsAgreed, setOptionalTermsAgreed] = useState(false);
const { entryMenu, nowMenu } = useSelector((state) => state.common.menu); const { entryMenu, nowMenu } = useSelector((state) => state.common.menu);
const [focusedContainerId, setFocusedContainerId] = useState(panelInfo.focusedContainerId); const [focusedContainerId, setFocusedContainerId] = useState(
panelInfo.focusedContainerId
);
// DetailPanel 진입 시 포커스 대상 저장 // DetailPanel 진입 시 포커스 대상 저장
const lastFocusedTargetRef = useRef(panelInfo.lastFocusedTargetId || null); const lastFocusedTargetRef = useRef(panelInfo.lastFocusedTargetId || null);
@@ -254,7 +289,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
useEffect(() => { useEffect(() => {
if (prevIsOnTopRef.current && !isOnTop) { if (prevIsOnTopRef.current && !isOnTop) {
const current = Spotlight.getCurrent(); const current = Spotlight.getCurrent();
const tBody = document.querySelector(`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`); const tBody = document.querySelector(
`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`
);
if (current && tBody && tBody.contains(current)) { if (current && tBody && tBody.contains(current)) {
const targetId = current.getAttribute('data-spotlight-id'); const targetId = current.getAttribute('data-spotlight-id');
@@ -286,7 +323,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
ImagePreloader.preloadAllImages(BACKGROUND_IMAGES) ImagePreloader.preloadAllImages(BACKGROUND_IMAGES)
.then((results) => { .then((results) => {
const successCount = results.filter((r) => r !== null).length; const successCount = results.filter((r) => r !== null).length;
dlog(`[HomePanel] Background images preloaded: ${successCount}/${results.length} images`); dlog(
`[HomePanel] Background images preloaded: ${successCount}/${results.length} images`
);
// 프리로딩 통계 정보 로깅 (디버깅용) // 프리로딩 통계 정보 로깅 (디버깅용)
const stats = ImagePreloader.getStats(); const stats = ImagePreloader.getStats();
@@ -312,7 +351,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const sortedHomeLayoutInfo = useMemo(() => { const sortedHomeLayoutInfo = useMemo(() => {
if (homeLayoutInfo && homeLayoutInfo.homeLayoutInfo) { if (homeLayoutInfo && homeLayoutInfo.homeLayoutInfo) {
const sorted = [...homeLayoutInfo.homeLayoutInfo].sort((x, y) => x.expsOrd - y.expsOrd); const sorted = [...homeLayoutInfo.homeLayoutInfo].sort(
(x, y) => x.expsOrd - y.expsOrd
);
return sorted; return sorted;
} }
return []; return [];
@@ -337,7 +378,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const expandAttemptRef = useRef(0); // 복구 시도 횟수 const expandAttemptRef = useRef(0); // 복구 시도 횟수
const loadingComplete = useSelector((state) => state.common?.loadingComplete); const loadingComplete = useSelector((state) => state.common?.loadingComplete);
const isVideoTransitionLocked = useSelector((state) => state.home.videoTransitionLocked); const isVideoTransitionLocked = useSelector(
(state) => state.home.videoTransitionLocked
);
// 선택약관 동의 핸들러 // 선택약관 동의 핸들러
const handleOptionalAgree = useCallback(() => { const handleOptionalAgree = useCallback(() => {
@@ -415,9 +458,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
optionalTerms: 'Y', optionalTerms: 'Y',
}, },
}); });
setTimeout(()=>{ setTimeout(() => {
Spotlight.focus('home_tbody'); Spotlight.focus('home_tbody');
},100) }, 100);
}, [handleOptionalAgree, dispatch, currentTermsFlag]); }, [handleOptionalAgree, dispatch, currentTermsFlag]);
const handleOptionalDeclineClick = useCallback(() => { const handleOptionalDeclineClick = useCallback(() => {
@@ -426,9 +469,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
} }
dispatch(updateOptionalTermsAgreement(false)); dispatch(updateOptionalTermsAgreement(false));
setIsOptionalConfirmVisible(false); setIsOptionalConfirmVisible(false);
setTimeout(()=>{ setTimeout(() => {
Spotlight.focus('home_tbody'); Spotlight.focus('home_tbody');
},100) }, 100);
}, [dispatch]); }, [dispatch]);
const handleTermsPopupClosed = useCallback(() => { const handleTermsPopupClosed = useCallback(() => {
@@ -499,7 +542,12 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [shouldShowOptionalTermsPopup, termsLoading, isOptionalConfirmVisible, dispatch]); }, [
shouldShowOptionalTermsPopup,
termsLoading,
isOptionalConfirmVisible,
dispatch,
]);
const onCancel = useCallback(() => { const onCancel = useCallback(() => {
const currentSpot = Spotlight.getCurrent(); const currentSpot = Spotlight.getCurrent();
@@ -550,7 +598,8 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const containerId = sortedHomeLayoutInfo[0].shptmApphmDspyOptCd; const containerId = sortedHomeLayoutInfo[0].shptmApphmDspyOptCd;
const navigableEls = getContainerNavigableElements(containerId); const navigableEls = getContainerNavigableElements(containerId);
const navigableIds = navigableEls.filter((el) => typeof el === 'string'); const navigableIds = navigableEls.filter((el) => typeof el === 'string');
const target = containerId === TEMPLATE_CODE_CONF.TOP ? 'banner0' : containerId; const target =
containerId === TEMPLATE_CODE_CONF.TOP ? 'banner0' : containerId;
if (navigableIds.length > 0) { if (navigableIds.length > 0) {
setContainerLastFocusedElement(null, navigableIds); setContainerLastFocusedElement(null, navigableIds);
@@ -626,7 +675,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
<HomeBanner <HomeBanner
key={el.shptmApphmDspyOptCd} key={el.shptmApphmDspyOptCd}
spotlightId={el.shptmApphmDspyOptCd} spotlightId={el.shptmApphmDspyOptCd}
firstSpot={!panelInfo.focusedContainerId && !panelInfo.currentSpot} firstSpot={
!panelInfo.focusedContainerId && !panelInfo.currentSpot
}
className={css.homeBannerWrap} className={css.homeBannerWrap}
handleShelfFocus={handleItemFocus( handleShelfFocus={handleItemFocus(
el.shptmApphmDspyOptCd, el.shptmApphmDspyOptCd,
@@ -718,7 +769,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
} else break; } else break;
} }
case TEMPLATE_CODE_CONF.PICK_FOR_YOU: { case TEMPLATE_CODE_CONF.PICK_FOR_YOU: {
if (bestSellerDatas && bestSellerDatas.length > 0) { if (userNumber) {
return ( return (
<PickedForYou <PickedForYou
key={el.shptmApphmDspyOptCd} key={el.shptmApphmDspyOptCd}
@@ -737,18 +788,20 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
} }
} }
})} })}
{loadingComplete && sortedHomeLayoutInfo && sortedHomeLayoutInfo.length > 0 && ( {loadingComplete &&
<TButton sortedHomeLayoutInfo &&
className={css.tButton} sortedHomeLayoutInfo.length > 0 && (
onClick={handleTopButtonClick} <TButton
size={null} className={css.tButton}
type={TYPES.topButton} onClick={handleTopButtonClick}
spotlightId={'home-top-btn'} size={null}
spotlightDisabled={btnDisabled} type={TYPES.topButton}
data-wheel-point={true} spotlightId={'home-top-btn'}
aria-label="Move to Top, Button" spotlightDisabled={btnDisabled}
/> data-wheel-point={true}
)} aria-label="Move to Top, Button"
/>
)}
</> </>
); );
}, [ }, [
@@ -914,7 +967,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// console.log('[HomePanel] playVideo 호출 완료'); // console.log('[HomePanel] playVideo 호출 완료');
if (isDeepLink || (!panels.length && !panelInfo.focusedContainerId)) { if (isDeepLink || (!panels.length && !panelInfo.focusedContainerId)) {
dispatch(changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } })); dispatch(
changeAppStatus({ showLoadingPanel: { show: true, type: 'wait' } })
);
dispatch(getHomeMainContents()); dispatch(getHomeMainContents());
dispatch(getHomeLayout()); dispatch(getHomeLayout());
dispatch( dispatch(
@@ -1071,6 +1126,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
const detailPanelClosedTime = useSelector( const detailPanelClosedTime = useSelector(
(state) => state.home.homeInfo?.panelInfo?.detailPanelClosedAt (state) => state.home.homeInfo?.panelInfo?.detailPanelClosedAt
); );
const isDeepLinkEntry = useSelector(
(state) => state.home.homeInfo?.panelInfo?.isDeepLinkEntry
);
useEffect(() => { useEffect(() => {
if (detailPanelClosed && isOnTop) { if (detailPanelClosed && isOnTop) {
@@ -1082,9 +1140,18 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// 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);
// 🔽 [251221] DeepLink로 PlayerPanel이 진입한 경우 자동 재생 스킵
// (플래그 리셋은 PlayerPanel cleanup에서 처리하므로 여기서는 스킵만)
if (isDeepLinkEntry) {
dlog('[HomePanel] *** [DeepLink] isDeepLinkEntry=true 감지 - 자동 재생 스킵');
return;
}
// 🔽 videoPlayIntentRef가 null인 경우: 비디오 재생 가능한 첫 번째 배너 찾기 // 🔽 videoPlayIntentRef가 null인 경우: 비디오 재생 가능한 첫 번째 배너 찾기
if (!videoPlayIntentRef.current && bannerDataList) { if (!videoPlayIntentRef.current && bannerDataList) {
dlog('[HomePanel] *** videoPlayIntentRef가 null - 첫 번째 비디오 배너 검색'); dlog(
'[HomePanel] *** videoPlayIntentRef가 null - 첫 번째 비디오 배너 검색'
);
// HomeBanner.jsx의 defaultFocus 계산 로직과 동일 // HomeBanner.jsx의 defaultFocus 계산 로직과 동일
let targetIndex = 0; let targetIndex = 0;
@@ -1138,7 +1205,10 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
}; };
lastPlayedBannerIdRef.current = bannerId; lastPlayedBannerIdRef.current = bannerId;
dlog('[HomePanel] *** videoPlayIntentRef 설정 완료:', videoPlayIntentRef.current); dlog(
'[HomePanel] *** videoPlayIntentRef 설정 완료:',
videoPlayIntentRef.current
);
} else { } else {
dlog('[HomePanel] *** ⚠️ 비디오 재생 가능한 배너를 찾지 못함'); dlog('[HomePanel] *** ⚠️ 비디오 재생 가능한 배너를 찾지 못함');
} }
@@ -1181,7 +1251,8 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
// 🔽 DetailPanel에서 돌아온 뒤 포커스를 마지막 포커스 대상에 복원 // 🔽 DetailPanel에서 돌아온 뒤 포커스를 마지막 포커스 대상에 복원
dlog('[HomePanel] *** 🎯 Focus 복원 준비'); dlog('[HomePanel] *** 🎯 Focus 복원 준비');
const targetFocusId = panelInfo.lastFocusedTargetId || lastFocusedTargetRef.current; const targetFocusId =
panelInfo.lastFocusedTargetId || lastFocusedTargetRef.current;
dlog( dlog(
'[HomePanel] *** 📍 targetFocusId:', '[HomePanel] *** 📍 targetFocusId:',
targetFocusId, targetFocusId,
@@ -1219,7 +1290,7 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
); );
} }
} }
}, [detailPanelClosed, isOnTop, bannerDataList, dispatch]); }, [detailPanelClosed, isOnTop, bannerDataList, isDeepLinkEntry, dispatch]);
// ======= // =======
// const justCameBack = !prevIsOnTopRef.current && isOnTop; // const justCameBack = !prevIsOnTopRef.current && isOnTop;
@@ -1250,7 +1321,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
targetSpotlightCateNm = c.getAttribute('data-catcd-nm'); targetSpotlightCateNm = c.getAttribute('data-catcd-nm');
} }
const tBody = document.querySelector(`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`); const tBody = document.querySelector(
`[data-spotlight-id="${SpotlightIds.HOME_TBODY}"]`
);
const currentSpot = c && tBody.contains(c) ? targetSpotlightId : null; const currentSpot = c && tBody.contains(c) ? targetSpotlightId : null;
dispatch(checkEnterThroughGNB(false)); dispatch(checkEnterThroughGNB(false));
@@ -1263,7 +1336,8 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
currentCateName: targetSpotlightCateNm, currentCateName: targetSpotlightCateNm,
// <<<<<<< HEAD // <<<<<<< HEAD
focusedContainerId: focusedContainerIdRef.current, focusedContainerId: focusedContainerIdRef.current,
lastFocusedTargetId: lastFocusedTargetRef.current || panelInfo.lastFocusedTargetId, lastFocusedTargetId:
lastFocusedTargetRef.current || panelInfo.lastFocusedTargetId,
// ======= // =======
// focusedContainerId: focusedContainerId, // focusedContainerId: focusedContainerId,
// >>>>>>> gitlab/develop // >>>>>>> gitlab/develop
@@ -1339,7 +1413,9 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
<> <>
{/* HomePanel용 메모리 상주 그라데이션 배경 */} {/* HomePanel용 메모리 상주 그라데이션 배경 */}
<div <div
className={classNames(css.gradientBackground, { [css.visible]: showGradientBackground })} className={classNames(css.gradientBackground, {
[css.visible]: showGradientBackground,
})}
aria-hidden="true" aria-hidden="true"
/> />
@@ -1369,7 +1445,10 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
)} )}
{arrowBottom && ( {arrowBottom && (
<p className={classNames(css.arrow, css.arrowBottom)} onClick={handleArrowClick} /> <p
className={classNames(css.arrow, css.arrowBottom)}
onClick={handleArrowClick}
/>
)} )}
{activePopup === ACTIVE_POPUP.exitPopup && ( {activePopup === ACTIVE_POPUP.exitPopup && (
@@ -1386,9 +1465,8 @@ const HomePanel = ({ isOnTop, showGradientBackground = false }) => {
text={$L('Are you sure you want to exit Shop Time?')} text={$L('Are you sure you want to exit Shop Time?')}
/> />
)} )}
{(activePopup === ACTIVE_POPUP.eventPopup || activePopup === ACTIVE_POPUP.smsPopup) && ( {(activePopup === ACTIVE_POPUP.eventPopup ||
<EventPopUpBanner /> activePopup === ACTIVE_POPUP.smsPopup) && <EventPopUpBanner />}
)}
{/* 선택약관 동의 팝업 */} {/* 선택약관 동의 팝업 */}
<OptionalConfirm <OptionalConfirm
open={isOptionalConfirmVisible} open={isOptionalConfirmVisible}

View File

@@ -1,24 +1,14 @@
import React, { import React, { useCallback, useEffect, useMemo, useState } from 'react';
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { import { useDispatch, useSelector } from 'react-redux';
useDispatch,
useSelector,
} from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import { import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
SpotlightContainerDecorator,
} from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { getContainerId } from '@enact/spotlight/src/container'; import { getContainerId } from '@enact/spotlight/src/container';
import { updateHomeInfo } from '../../../actions/homeActions'; import { updateHomeInfo } from '../../../actions/homeActions';
import { pushPanel, popPanel } from '../../../actions/panelActions'; import { popPanel, pushPanel } from '../../../actions/panelActions';
import { startVideoPlayer } from '../../../actions/playActions'; import { startVideoPlayer } from '../../../actions/playActions';
import SectionTitle from '../../../components/SectionTitle/SectionTitle'; import SectionTitle from '../../../components/SectionTitle/SectionTitle';
import Tag from '../../../components/TItemCard/Tag'; import Tag from '../../../components/TItemCard/Tag';
@@ -35,18 +25,15 @@ import {
LOG_MESSAGE_ID, LOG_MESSAGE_ID,
panel_names, panel_names,
} from '../../../utils/Config'; } from '../../../utils/Config';
import { import { $L, scaleW } from '../../../utils/helperMethods';
$L,
scaleW,
} from '../../../utils/helperMethods';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import { TEMPLATE_CODE_CONF } from '../HomePanel'; import { TEMPLATE_CODE_CONF } from '../HomePanel';
import css from '../PopularShow/PopularShow.module.less'; import css from '../PopularShow/PopularShow.module.less';
const SpottableComponent = Spottable("div"); const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator( const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" }, { enterTo: 'last-focused' },
"div" 'div'
); );
const PopularShow = ({ const PopularShow = ({
@@ -72,9 +59,13 @@ const PopularShow = ({
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
const topInfos = useSelector((state) => state.main.top20ShowData.topInfos); const topInfos = useSelector((state) => state.main.top20ShowData.topInfos);
const recommendInfo = useSelector((state) => state.foryou?.recommendInfo?.recommendShow); const recommendInfo = useSelector(
(state) => state.foryou?.recommendInfo?.recommendShow
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const orderStyle = useMemo(() => ({ order: order }), [order]); const orderStyle = useMemo(() => ({ order: order }), [order]);
const [drawChk, setDrawChk] = useState(false); const [drawChk, setDrawChk] = useState(false);
@@ -87,41 +78,46 @@ const PopularShow = ({
setDrawChk(true); setDrawChk(true);
}, [topInfos]); }, [topInfos]);
useEffect(()=>{ useEffect(() => {
setShowInfos( setShowInfos(
recommendInfo?.filter( recommendInfo?.filter((item) => item.recommendTpCd === 'POPULARSHOW') ||
(item) => item.recommendTpCd === "POPULARSHOW" []
) || [] );
) }, [recommendInfo]);
},[recommendInfo])
useEffect(() => { useEffect(() => {
if (!showInfos || showInfos.length === 0) { if (userNumber) {
const baseData = topInfos?.map((item) => ({ if (!showInfos || showInfos.length === 0) {
...item, const baseData =
foryou: false, topInfos?.map((item) => ({
})) || []; ...item,
setShowNewInfos(baseData); foryou: false,
return; })) || [];
setShowNewInfos(baseData);
return;
}
const recommendedData =
showInfos[0].showInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(
recommendedData?.map((item) => item.showId)
);
const baseData =
topInfos?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.showId),
})) || [];
setShowNewInfos([...baseData]);
} else {
setShowNewInfos(topInfos);
} }
}, [topInfos, showInfos, userNumber]);
const recommendedData = showInfos[0].showInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(recommendedData?.map(item => item.showId));
const baseData = topInfos?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.showId),
})) || [];
setShowNewInfos([ ...baseData]);
}, [topInfos, showInfos]);
const handleCardClick = useCallback( const handleCardClick = useCallback(
(patnrId, showId, catCd, showUrl) => () => { (patnrId, showId, catCd, showUrl) => () => {
@@ -135,7 +131,7 @@ const PopularShow = ({
startVideoPlayer({ startVideoPlayer({
showId, showId,
patnrId, patnrId,
shptmBanrTpNm: "VOD", shptmBanrTpNm: 'VOD',
lgCatCd: catCd, lgCatCd: catCd,
modal: false, modal: false,
showUrl: showUrl, showUrl: showUrl,
@@ -151,7 +147,7 @@ const PopularShow = ({
{ {
name: panel_names.TRENDING_NOW_PANEL, name: panel_names.TRENDING_NOW_PANEL,
panelInfo: { panelInfo: {
pageName: "PS", pageName: 'PS',
focusedContainerId: SpotlightIds.TRENDING_NOW_POPULAR_SHOW, focusedContainerId: SpotlightIds.TRENDING_NOW_POPULAR_SHOW,
}, },
}, },
@@ -179,23 +175,23 @@ const PopularShow = ({
if (firstChk === 0 && itemIndex === 0) { if (firstChk === 0 && itemIndex === 0) {
const c = Spotlight.getCurrent(); const c = Spotlight.getCurrent();
const getAriaLabel = c.getAttribute("aria-label"); const getAriaLabel = c.getAttribute('aria-label');
if (c) { if (c) {
let cAriaLabel = c.getAttribute("aria-label"); let cAriaLabel = c.getAttribute('aria-label');
cAriaLabel = "POPULAR SHOW, Heading 1," + cAriaLabel; cAriaLabel = 'POPULAR SHOW, Heading 1,' + cAriaLabel;
c.setAttribute("aria-label", cAriaLabel); c.setAttribute('aria-label', cAriaLabel);
} }
setFirstChk(1); setFirstChk(1);
} else if (firstChk === 1 && itemIndex === 0) { } else if (firstChk === 1 && itemIndex === 0) {
const c = Spotlight.getCurrent(); const c = Spotlight.getCurrent();
if (c) { if (c) {
let cAriaLabel = c.getAttribute("aria-label"); let cAriaLabel = c.getAttribute('aria-label');
if (cAriaLabel) { if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace( const newcAriaLabel = cAriaLabel.replace(
"POPULAR SHOW, Heading 1,", 'POPULAR SHOW, Heading 1,',
"" ''
); );
c.setAttribute("aria-label", newcAriaLabel); c.setAttribute('aria-label', newcAriaLabel);
} }
} }
} else { } else {
@@ -246,7 +242,7 @@ const PopularShow = ({
> >
<SectionTitle <SectionTitle
className={css.subTitle} className={css.subTitle}
title={$L("POPULAR SHOW")} title={$L('POPULAR SHOW')}
data-title-index="homePopularShow" data-title-index="homePopularShow"
label="POPULAR SHOW" label="POPULAR SHOW"
/> />
@@ -271,11 +267,11 @@ const PopularShow = ({
patncNm, patncNm,
catCd, catCd,
showUrl, showUrl,
// <<<<<<< HEAD // <<<<<<< HEAD
foryou, foryou,
// ======= // =======
productInfos, productInfos,
// >>>>>>> gitlab/develop // >>>>>>> gitlab/develop
}, },
itemIndex itemIndex
) => { ) => {
@@ -293,18 +289,18 @@ const PopularShow = ({
showId={showId} showId={showId}
showTitle={showNm} showTitle={showNm}
imageSource={ imageSource={
(thumbnailUrl && thumbnailUrl960) ? thumbnailUrl && thumbnailUrl960
thumbnailUrl !== thumbnailUrl960 ? thumbnailUrl !== thumbnailUrl960
? thumbnailUrl960 ? thumbnailUrl960
: thumbnailUrl
: thumbnailUrl : thumbnailUrl
: thumbnailUrl
} }
imageAlt={showNm} imageAlt={showNm}
productName={productInfos[0].prdtNm} productName={productInfos[0].prdtNm}
nonPosition={true} nonPosition={true}
type={TYPES.videoShow} type={TYPES.videoShow}
imgType={ imgType={
vtctpYn !== "Y" vtctpYn !== 'Y'
? IMAGETYPES.imgHorizontal ? IMAGETYPES.imgHorizontal
: IMAGETYPES.imgVertical : IMAGETYPES.imgVertical
} }
@@ -313,11 +309,11 @@ const PopularShow = ({
onFocus={handleFocus(itemIndex)} onFocus={handleFocus(itemIndex)}
onBlur={handleBlur(itemIndex)} onBlur={handleBlur(itemIndex)}
onClick={handleCardClick(patnrId, showId, catCd, showUrl)} onClick={handleCardClick(patnrId, showId, catCd, showUrl)}
firstLabel={patncNm + " "} firstLabel={patncNm + ' '}
label={itemIndex * 1 + 1 + " of " + showNewInfos.length} label={itemIndex * 1 + 1 + ' of ' + showNewInfos.length}
lastLabel=" go to detail, button" lastLabel=" go to detail, button"
> >
{foryou === true && <Tag text={"For You"} />} {foryou === true && <Tag text={'For You'} />}
</TItemCardNew> </TItemCardNew>
); );
} }
@@ -329,7 +325,7 @@ const PopularShow = ({
className={css.displayBox} className={css.displayBox}
onClick={handleMoreCardClick} onClick={handleMoreCardClick}
onFocus={_handleItemFocus} onFocus={_handleItemFocus}
spotlightId={"home-popularshow-more-btn"} spotlightId={'home-popularshow-more-btn'}
></SpottableComponent> ></SpottableComponent>
</div> </div>
)} )}

View File

@@ -8,21 +8,32 @@ import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import { sendLogCuration } from '../../../actions/logActions'; import { sendLogCuration } from '../../../actions/logActions';
import { getSubCategory } from '../../../actions/mainActions'; import { getSubCategory } from '../../../actions/mainActions';
import { pushPanel, navigateFromSubCategory } from '../../../actions/panelActions'; import {
navigateFromSubCategory,
pushPanel,
} from '../../../actions/panelActions';
import Tag from '../../../components/TItemCard/Tag'; import Tag from '../../../components/TItemCard/Tag';
import TItemCardNew from '../../../components/TItemCard/TItemCard.new'; import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller'; import TScroller from '../../../components/TScroller/TScroller';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useScrollReset from '../../../hooks/useScrollReset'; import useScrollReset from '../../../hooks/useScrollReset';
import useScrollTo from '../../../hooks/useScrollTo'; import useScrollTo from '../../../hooks/useScrollTo';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, LOG_TP_NO, panel_names } from '../../../utils/Config'; import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
LOG_TP_NO,
panel_names,
} from '../../../utils/Config';
import { SpotlightIds } from '../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../utils/SpotlightIds';
import CategoryNav from '../../HomePanel/SubCategory/CategoryNav/CategoryNav'; import CategoryNav from '../../HomePanel/SubCategory/CategoryNav/CategoryNav';
import css from '../../HomePanel/SubCategory/SubCategory.module.less'; import css from '../../HomePanel/SubCategory/SubCategory.module.less';
const SpottableComponent = Spottable('div'); const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: null }, 'div'); const Container = SpotlightContainerDecorator({ enterTo: null }, 'div');
const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const ContainerBasic = SpotlightContainerDecorator(
{ enterTo: 'last-focused' },
'div'
);
const getExpsOrdByLgCatCd = (array, value) => { const getExpsOrdByLgCatCd = (array, value) => {
const expsOrd = array.findIndex(({ lgCatCd }) => value === lgCatCd) + 1; const expsOrd = array.findIndex(({ lgCatCd }) => value === lgCatCd) + 1;
@@ -40,12 +51,24 @@ export default memo(function SubCategory({
}) { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { getScrollTo, scrollLeft } = useScrollTo(); const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, false); const { handleScrollReset, handleStopScrolling } = useScrollReset(
scrollLeft,
false
);
const categoryInfos = useSelector((state) => state.home.menuData?.data?.homeCategory); const categoryInfos = useSelector(
const categoryItemInfos = useSelector((state) => state.main.subCategoryData?.categoryItemInfos); (state) => state.home.menuData?.data?.homeCategory
);
const categoryItemInfos = useSelector(
(state) => state.main.subCategoryData?.categoryItemInfos
);
const foruItemInfos = useSelector((state) => state.main.recommendProduct[0]?.productInfos); const foruItemInfos = useSelector(
(state) => state.main.recommendProduct[0]?.productInfos
);
const { userNumber } = useSelector(
(state) => state.common.appStatus.loginUserData
);
const nowMenu = useSelector((state) => state.common.menu.nowMenu); const nowMenu = useSelector((state) => state.common.menu.nowMenu);
@@ -207,35 +230,45 @@ export default memo(function SubCategory({
}, [handleShelfFocus]); }, [handleShelfFocus]);
useEffect(() => { useEffect(() => {
if (!foruItemInfos || foruItemInfos.length === 0) { if (userNumber) {
if (!foruItemInfos || foruItemInfos.length === 0) {
const baseData =
categoryItemInfos?.subCatItemList?.map((item) => ({
...item,
foryou: false,
})) || [];
setCategoryItemNewData(baseData);
return;
}
const recommendedData =
foruItemInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(
recommendedData.map((item) => item.prdtId)
);
const baseData = const baseData =
categoryItemInfos?.subCatItemList?.map((item) => ({ categoryItemInfos?.subCatItemList?.map((item) => ({
...item, ...item,
foryou: false, foryou: recommendedPrdtIds.has(item.prdtId),
})) || []; })) || [];
setCategoryItemNewData(baseData);
return; setCategoryItemNewData([...baseData]);
} else {
setCategoryItemNewData(categoryItemInfos?.subCatItemList);
} }
}, [categoryItemInfos?.subCatItemList, foruItemInfos, userNumber]);
const recommendedData =
foruItemInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId));
const baseData =
categoryItemInfos?.subCatItemList?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.prdtId),
})) || [];
setCategoryItemNewData([...baseData]);
}, [categoryItemInfos?.subCatItemList, foruItemInfos]);
return ( return (
<Container spotlightId={spotlightId} data-wheel-point onFocus={_handleShelfFocus}> <Container
spotlightId={spotlightId}
data-wheel-point
onFocus={_handleShelfFocus}
>
<CategoryNav <CategoryNav
categoryInfos={categoryInfos} categoryInfos={categoryInfos}
currentCategoryCode={currentLgCatCd} currentCategoryCode={currentLgCatCd}
@@ -293,7 +326,12 @@ export default memo(function SubCategory({
offerInfo={offerInfo} offerInfo={offerInfo}
data-catcd-num={currentLgCatCd} data-catcd-num={currentLgCatCd}
data-catcd-nm={currentLgCatNm} data-catcd-nm={currentLgCatNm}
label={itemIndex * 1 + 1 + ' of ' + (categoryItemNewData?.length || 0)} label={
itemIndex * 1 +
1 +
' of ' +
(categoryItemNewData?.length || 0)
}
lastLabel=" go to detail, button" lastLabel=" go to detail, button"
euEnrgLblInfos={euEnrgLblInfos} euEnrgLblInfos={euEnrgLblInfos}
> >

View File

@@ -209,6 +209,9 @@ export default function MainView({ className, initService }) {
const hasFeaturedBrandsPanel = panels.some( const hasFeaturedBrandsPanel = panels.some(
(panel) => panel?.name === Config.panel_names.FEATURED_BRANDS_PANEL (panel) => panel?.name === Config.panel_names.FEATURED_BRANDS_PANEL
); );
const hasTrendingNowPanel = panels.some(
(panel) => panel?.name === Config.panel_names.TRENDING_NOW_PANEL
);
// 단독 패널 체크 - CheckOutPanel, CartPanel 등 단독으로 렌더링되어야 하는 패널들 // 단독 패널 체크 - CheckOutPanel, CartPanel 등 단독으로 렌더링되어야 하는 패널들
if (DEBUG_MODE) { if (DEBUG_MODE) {
console.log(`[PANEL_MainView] 🔍 Top panel name: ${topPanel?.name}`); console.log(`[PANEL_MainView] 🔍 Top panel name: ${topPanel?.name}`);
@@ -250,7 +253,11 @@ export default function MainView({ className, initService }) {
'[MainView] Rendering 3-layer structure: PlayerPanel + DetailPanel + MediaPanel' '[MainView] Rendering 3-layer structure: PlayerPanel + DetailPanel + MediaPanel'
); );
} }
renderingPanels = hasFeaturedBrandsPanel ? panels.slice(-4) : panels.slice(-3); if (hasFeaturedBrandsPanel || hasTrendingNowPanel) {
renderingPanels = panels.slice(-4);
} else {
renderingPanels = panels.slice(-3);
}
} else if ( } else if (
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL || panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL_NEW || panels[panels.length - 1]?.name === Config.panel_names.PLAYER_PANEL_NEW ||
@@ -258,7 +265,11 @@ export default function MainView({ className, initService }) {
panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL || panels[panels.length - 2]?.name === Config.panel_names.PLAYER_PANEL ||
panels[panels.length - 2]?.name === Config.panel_names.MEDIA_PANEL panels[panels.length - 2]?.name === Config.panel_names.MEDIA_PANEL
) { ) {
renderingPanels = hasFeaturedBrandsPanel ? panels.slice(-3) : panels.slice(-2); if (hasFeaturedBrandsPanel || hasTrendingNowPanel) {
renderingPanels = panels.slice(-3);
} else {
renderingPanels = panels.slice(-2);
}
} else { } else {
renderingPanels = panels.slice(-1); renderingPanels = panels.slice(-1);
} }

View File

@@ -391,6 +391,7 @@ const MediaPanel = React.forwardRef(
const onEnded = useCallback( const onEnded = useCallback(
(e) => { (e) => {
debugLog('[MediaPanel] Video ended'); debugLog('[MediaPanel] Video ended');
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] MediaPanel onEnded triggered - will pop after 1500ms');
// continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리 // continuousPlay는 MediaPlayer(VideoPlayer) 컴포넌트 내부에서 loop 속성으로 처리
// onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음 // onEnded가 호출되면 loop=false 인 경우이므로 패널을 닫음
Spotlight.pause(); Spotlight.pause();
@@ -402,6 +403,7 @@ const MediaPanel = React.forwardRef(
// ✅ 새로운 타이머 저장 (cleanup 시 정리용) // ✅ 새로운 타이머 저장 (cleanup 시 정리용)
onEndedTimerRef.current = setTimeout(() => { onEndedTimerRef.current = setTimeout(() => {
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] Executing popPanel(MEDIA_PANEL) after 1500ms');
Spotlight.resume(); Spotlight.resume();
dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL)); dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
onEndedTimerRef.current = null; onEndedTimerRef.current = null;

View File

@@ -26,7 +26,7 @@
video { video {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
object-fit: contain; /* 비율 유지하면서 컨테이너 안에 맞춤 */ object-fit: contain; /* 높이 기준으로 맞추고 좌우는 잘림 */
} }
} }

View File

@@ -912,7 +912,7 @@ const MediaPanel = React.forwardRef(
} }
if (!panelInfo.modal) { if (!panelInfo.modal) {
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
dispatch(changeAppStatus({ cursorVisible: false })); dispatch(changeAppStatus({ cursorVisible: false }));
document?.dispatchEvent?.(new CustomEvent('detailpanel-scroll-reset')); document?.dispatchEvent?.(new CustomEvent('detailpanel-scroll-reset'));
@@ -937,7 +937,7 @@ const MediaPanel = React.forwardRef(
// 패널이 2개 존재할때만 popPanel 진행 // 패널이 2개 존재할때만 popPanel 진행
// fullscreen 전환 중이면 popPanel하지 않음 // fullscreen 전환 중이면 popPanel하지 않음
if (panelInfo.modal && !isOnTop && !isTransitioningToFullscreen.current) { if (panelInfo.modal && !isOnTop && !isTransitioningToFullscreen.current) {
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
} else { } else {
Spotlight.focus('tbody'); Spotlight.focus('tbody');
} }
@@ -1576,7 +1576,7 @@ const MediaPanel = React.forwardRef(
) { ) {
// case: Featured Brands // case: Featured Brands
if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) { if (panelInfo?.sourcePanel === panel_names.FEATURED_BRANDS_PANEL) {
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
} }
} }
}, [ }, [
@@ -2043,19 +2043,25 @@ const MediaPanel = React.forwardRef(
(e) => { (e) => {
const currentInfo = panelInfoRef.current; const currentInfo = panelInfoRef.current;
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] onEnded triggered - shptmBanrTpNm:', currentInfo?.shptmBanrTpNm);
// MEDIA: 기존 동작 유지 (배경 복원 없이 즉시 pop) // MEDIA: 기존 동작 유지 (배경 복원 없이 즉시 pop)
if (currentInfo.shptmBanrTpNm === 'MEDIA') { if (currentInfo.shptmBanrTpNm === 'MEDIA') {
console.log('[MediaPanel] 🚫 Skipping background restoration for ended media'); // console.log('[🔥UNIQUE_MEDIA_ENDED🔥] MEDIA type - popPanel will be called');
Spotlight.pause(); Spotlight.pause();
setTimeout(() => { setTimeout(() => {
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] setTimeout fired - dispatching popPanel(MEDIA_PANEL)');
Spotlight.resume(); Spotlight.resume();
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel(panel_names.MEDIA_PANEL));
}, VIDEO_END_ACTION_DELAY); }, VIDEO_END_ACTION_DELAY);
e?.stopPropagation();
e?.preventDefault();
return; return;
} }
// VOD: modal 여부에 따라 동작 분리 // VOD: modal 여부에 따라 동작 분리
if (currentInfo.shptmBanrTpNm === 'VOD') { if (currentInfo.shptmBanrTpNm === 'VOD') {
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] VOD type - popPanel will be called');
Spotlight.pause(); Spotlight.pause();
setTimeout(() => { setTimeout(() => {
stopExternalPlayer(); stopExternalPlayer();
@@ -2074,6 +2080,8 @@ const MediaPanel = React.forwardRef(
e?.preventDefault(); e?.preventDefault();
return; return;
} }
// console.log('[🔥UNIQUE_MEDIA_ENDED🔥] Unknown shptmBanrTpNm - no action taken');
}, },
[dispatch, focusBackButtonOrFallback, stopExternalPlayer] [dispatch, focusBackButtonOrFallback, stopExternalPlayer]
); );

View File

@@ -390,7 +390,7 @@ function PlayerOverlayContents({
e.preventDefault(); e.preventDefault();
// tabIndexV2가 2일 때만 LiveChannelNext로 포커스 // tabIndexV2가 2일 때만 LiveChannelNext로 포커스
if (tabContainerVersion === 2 && tabIndexV2 === 2) { if (tabContainerVersion === 2 && tabIndexV2 === 2) {
Spotlight.focus('live-channel-next-button'); Spotlight.focus('below-tab-shop-now-button');
} }
}} }}
onSpotlightDown={(e) => { onSpotlightDown={(e) => {

View File

@@ -13,8 +13,8 @@
background-image: url("../../../../assets/images/btn/btn-video-cc-nor@3x.png"); background-image: url("../../../../assets/images/btn/btn-video-cc-nor@3x.png");
background-size: cover; background-size: cover;
position: absolute; position: absolute;
right: 60px; right: 300px;
top: 800px; top: 680px;
z-index: 10; z-index: 10;
&.videoVericalSubtitleButton { &.videoVericalSubtitleButton {

View File

@@ -30,6 +30,7 @@ import {
getMainLiveShow, getMainLiveShow,
getMainLiveShowNowProduct, getMainLiveShowNowProduct,
} from '../../actions/mainActions'; } from '../../actions/mainActions';
import { updateHomeInfo } from '../../actions/homeActions';
import * as PanelActions from '../../actions/panelActions'; import * as PanelActions from '../../actions/panelActions';
import { updatePanel } from '../../actions/panelActions'; import { updatePanel } from '../../actions/panelActions';
import { import {
@@ -90,6 +91,51 @@ const findSelector = (selector, maxAttempts = 5, currentAttempts = 0) => {
} }
}; };
// 배너 위치 수집 함수 (top, left만 저장)
const collectBannerPositions = () => {
const positions = [];
// banner0, banner1 등의 배너 위치 수집
for (let i = 0; i < 10; i++) {
const bannerId = `banner${i}`;
const node = document.querySelector(`[data-spotlight-id="${bannerId}"]`);
if (node) {
const { top, left } = node.getBoundingClientRect();
positions.push({
bannerId,
position: { top: Math.round(top), left: Math.round(left) }
});
dlog(`[PlayerPanel] 배너 위치 수집: ${bannerId}`, { top: Math.round(top), left: Math.round(left) });
}
}
return positions;
};
// 위치 검증 함수 (오차 범위: 1px)
const isPositionMatching = (bannerPositions, bannerId, currentPosition) => {
const validPosition = bannerPositions.find(p => p.bannerId === bannerId);
if (!validPosition) {
dlog(`[PlayerPanel] 배너 위치 검증 실패: ${bannerId} 배너를 찾을 수 없음`);
return false;
}
const tolerance = 1; // 1px 오차 범위
const isMatching =
Math.abs(currentPosition.top - validPosition.position.top) <= tolerance &&
Math.abs(currentPosition.left - validPosition.position.left) <= tolerance;
dlog(`[PlayerPanel] 배너 위치 검증: ${bannerId}`, {
expected: validPosition.position,
current: currentPosition,
matching: isMatching
});
return isMatching;
};
const getLogTpNo = (type, nowMenu) => { const getLogTpNo = (type, nowMenu) => {
if (type === 'LIVE') { if (type === 'LIVE') {
switch (nowMenu) { switch (nowMenu) {
@@ -218,6 +264,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const [tabIndexV2, setTabIndexV2] = USE_STATE('tabIndexV2', 1); // 0: ShopNow, 1: LiveChannel, 2: ShopNowButton const [tabIndexV2, setTabIndexV2] = USE_STATE('tabIndexV2', 1); // 0: ShopNow, 1: LiveChannel, 2: ShopNowButton
const [tabContainerVersion, setTabContainerVersion] = USE_STATE('tabContainerVersion', 2); // 1: TabContainer (우측), 2: TabContainerV2 (하단) const [tabContainerVersion, setTabContainerVersion] = USE_STATE('tabContainerVersion', 2); // 1: TabContainer (우측), 2: TabContainerV2 (하단)
const [isModalClosed, setIsModalClosed] = USE_STATE('isModalClosed', true); // 모달이 false 상태인지 나타내는 플래그 const [isModalClosed, setIsModalClosed] = USE_STATE('isModalClosed', true); // 모달이 false 상태인지 나타내는 플래그
const [validBannerPositions, setValidBannerPositions] = USE_STATE('validBannerPositions', []); // 유효한 배너 위치 (top, left)
const panels = USE_SELECTOR('panels', (state) => state.panels.panels); const panels = USE_SELECTOR('panels', (state) => state.panels.panels);
const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData); const chatData = USE_SELECTOR('chatData', (state) => state.play.chatData);
@@ -407,6 +454,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
lastFocusedTargetId: panelInfo.lastFocusedTargetId, lastFocusedTargetId: panelInfo.lastFocusedTargetId,
}); });
// TabContainerV2의 tabIndex를 ShopNowContents(0)로 리셋
dlog('[PlayerPanel] 📑 TabContainerV2 tabIndex를 ShopNowContents(0)로 리셋');
setTabIndexV2(0);
// 포커스 복원 로직 추가 (1000ms 지연) // 포커스 복원 로직 추가 (1000ms 지연)
if (panelInfo.lastFocusedTargetId) { if (panelInfo.lastFocusedTargetId) {
dlog( dlog(
@@ -431,6 +482,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
}; };
} }
console.log('[PANEL] PlayerPanel updatePanel - detailPanelClosed reset');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -503,10 +555,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// PanelInfo 상태 변화 모니터링 useEffect (isPaused가 실제로 변경될 때만) // PanelInfo 상태 변화 모니터링 useEffect (isPaused가 실제로 변경될 때만)
useEffect(() => { useEffect(() => {
const isOnTop = panel_names.HOME_PANEL === topPanel?.name;
const isPausedChanged = previousPanelInfo.current?.isPaused !== panelInfo?.isPaused; const isPausedChanged = previousPanelInfo.current?.isPaused !== panelInfo?.isPaused;
if (isOnTop && panelInfo?.isPaused !== undefined && isPausedChanged) { // isOnTop 여부와 관계없이 isPaused 변경을 감지하여 비디오 제어
if (panelInfo?.isPaused !== undefined && isPausedChanged) {
// 상태 변경 시에만 디버깅 로그 출력 // 상태 변경 시에만 디버깅 로그 출력
dlog('🔍 [PlayerPanel] PanelInfo isPaused changed', { dlog('🔍 [PlayerPanel] PanelInfo isPaused changed', {
previousIsPaused: previousPanelInfo.current?.isPaused, previousIsPaused: previousPanelInfo.current?.isPaused,
@@ -539,7 +591,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
} }
previousPanelInfo.current = panelInfo; previousPanelInfo.current = panelInfo;
}, [panelInfo?.isPaused, topPanel?.name, currentPlayingUrl]); }, [panelInfo?.isPaused, currentPlayingUrl]);
// VideoPlayer 인스턴스 및 소스 변경 모니터링 (중요 변화만) // VideoPlayer 인스턴스 및 소스 변경 모니터링 (중요 변화만)
useEffect(() => { useEffect(() => {
@@ -647,6 +699,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
useEffect(() => { useEffect(() => {
if (currentLiveShowInfo && Object.keys(currentLiveShowInfo).length > 0) { if (currentLiveShowInfo && Object.keys(currentLiveShowInfo).length > 0) {
if (currentLiveShowInfo.showId !== panelInfo?.showId) { if (currentLiveShowInfo.showId !== panelInfo?.showId) {
console.log('[PANEL] PlayerPanel updatePanel - LIVE showId update');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -1146,6 +1199,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
videoPlayer.current?.hideControls(); videoPlayer.current?.hideControls();
setSelectedIndex(backupInitialIndex); setSelectedIndex(backupInitialIndex);
if (panelInfo.shptmBanrTpNm === 'MEDIA') { if (panelInfo.shptmBanrTpNm === 'MEDIA') {
console.log('[PANEL] PlayerPanel updatePanel - DETAIL_PANEL launchedFromPlayer false');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
@@ -1163,6 +1217,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
if (!panelInfo.modal) { if (!panelInfo.modal) {
console.log('[PlayerPanel] popPanel - closeButtonHandler'); console.log('[PlayerPanel] popPanel - closeButtonHandler');
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel());
// 🔽 [251221] cleanup useEffect에서 isDeepLinkEntry 리셋 처리
dispatch(changeAppStatus({ cursorVisible: false })); dispatch(changeAppStatus({ cursorVisible: false }));
//딮링크로 플레이어 진입 후 이전버튼 클릭시 //딮링크로 플레이어 진입 후 이전버튼 클릭시
@@ -1193,12 +1248,23 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
return () => { return () => {
// 패널이 2개 존재할때만 popPanel 진행 // 패널이 2개 존재할때만 popPanel 진행
// 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지) // 현재 스택의 top이 PlayerPanel일 때만 pop 수행 (다른 패널이 올라온 상태에서 오작동 방지)
console.log('[PP-TRACE] cleanup start', { // console.log('[PP-TRACE] cleanup start', {
modal: panelInfo.modal, // modal: panelInfo.modal,
isOnTop, // isOnTop,
topPanel: panels[panels.length - 1]?.name, // topPanel: panels[panels.length - 1]?.name,
stack: panels.map((p) => p.name), // stack: panels.map((p) => p.name),
}); // });
// 🔽 [251221] PlayerPanel unmount 시 DeepLink 플래그 리셋
dispatch(
updateHomeInfo({
name: panel_names.HOME_PANEL,
panelInfo: {
isDeepLinkEntry: false,
},
})
);
const topPanelName = panels[panels.length - 1]?.name; const topPanelName = panels[panels.length - 1]?.name;
if ( if (
panelInfo.modal && panelInfo.modal &&
@@ -1206,13 +1272,13 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
topPanelName === panel_names.PLAYER_PANEL && topPanelName === panel_names.PLAYER_PANEL &&
panels.length === 1 // 다른 패널 존재 시 pop 금지 (DetailPanel 제거 방지) panels.length === 1 // 다른 패널 존재 시 pop 금지 (DetailPanel 제거 방지)
) { ) {
console.log('[PP-TRACE] popPanel - useEffect cleanup (top is PlayerPanel)'); // console.log('[PP-TRACE] popPanel - useEffect cleanup (top is PlayerPanel)');
dispatch(PanelActions.popPanel()); dispatch(PanelActions.popPanel());
} else { } else {
Spotlight.focus('tbody'); Spotlight.focus('tbody');
} }
}; };
}, [panelInfo?.modal, isOnTop, panels]); }, [panelInfo?.modal, isOnTop, panels, dispatch]);
useEffect(() => { useEffect(() => {
if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') { if (showNowInfos && panelInfo.shptmBanrTpNm === 'LIVE') {
@@ -1309,10 +1375,21 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
// 최상단 패널이 DetailPanel이고 PlayerPanel에서 진입했는지 확인 // 최상단 패널이 DetailPanel이고 PlayerPanel에서 진입했는지 확인
const isTopPanelDetailFromPlayer = useMemo(() => { const isTopPanelDetailFromPlayer = useMemo(() => {
const result = let result =
topPanel?.name === panel_names.DETAIL_PANEL && topPanel?.name === panel_names.DETAIL_PANEL &&
topPanel?.panelInfo?.launchedFromPlayer === true; topPanel?.panelInfo?.launchedFromPlayer === true;
// MediaPanel이 최상단에 있고 그 아래가 DetailPanel인 경우도 체크
if (!result && topPanel?.name === panel_names.MEDIA_PANEL) {
const prevPanel = panels[panels.length - 2];
if (
prevPanel?.name === panel_names.DETAIL_PANEL &&
prevPanel?.panelInfo?.launchedFromPlayer === true
) {
result = true;
}
}
// 🔍 DetailPanel 상태 변화 로깅 // 🔍 DetailPanel 상태 변화 로깅
if (result) { if (result) {
dlog('🎬 [PlayerPanel] DetailPanel is now on top (from Player)', { dlog('🎬 [PlayerPanel] DetailPanel is now on top (from Player)', {
@@ -1768,6 +1845,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
if (watchIntervalLive.current) clearInterval(watchIntervalLive.current); if (watchIntervalLive.current) clearInterval(watchIntervalLive.current);
if (watchIntervalVod.current) clearInterval(watchIntervalVod.current); if (watchIntervalVod.current) clearInterval(watchIntervalVod.current);
if (watchIntervalMedia.current) clearInterval(watchIntervalMedia.current); if (watchIntervalMedia.current) clearInterval(watchIntervalMedia.current);
if (activityCheckIntervalRef.current) clearInterval(activityCheckIntervalRef.current);
}; };
}, []); }, []);
@@ -1858,6 +1936,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
dlog('[PlayerPanel] Condition 2.5: Removing skipFlag after DOM render'); dlog('[PlayerPanel] Condition 2.5: Removing skipFlag after DOM render');
console.log('[PANEL] PlayerPanel updatePanel - skipModalStyleRecalculation remove');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -1898,12 +1977,37 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
scale = width / window.innerWidth; scale = width / window.innerWidth;
setModalScale(scale); setModalScale(scale);
} }
console.log('[PANEL] PlayerPanel updatePanel - modalStyle and scale update');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
panelInfo: { modalStyle: modalStyle, modalScale: scale }, panelInfo: { modalStyle: modalStyle, modalScale: scale },
}) })
); );
// 🔽 배너 위치 수집 (초기 로드 시에만 실행)
if (validBannerPositions.length === 0) {
const positions = collectBannerPositions();
if (positions.length > 0) {
setValidBannerPositions(positions);
dlog('[PlayerPanel] ✅ 배너 위치 초기 수집 완료:', positions);
}
}
// 🔽 배너 위치 검증 (위치가 맞지 않으면 비디오 재생 중단)
if (validBannerPositions.length > 0) {
const currentPosition = { top: Math.round(top), left: Math.round(left) };
const isValidPosition = isPositionMatching(validBannerPositions, panelInfo.modalContainerId, currentPosition);
if (!isValidPosition) {
dlog('[PlayerPanel] ⚠️ 배너 위치 검증 실패 - 비디오 재생 중단', {
bannerId: panelInfo.modalContainerId,
currentPosition,
validBannerPositions
});
return; // 비디오 재생 중단
}
}
} else { } else {
dlog('[PlayerPanel] Condition 1: Node not found, using saved modalStyle'); dlog('[PlayerPanel] Condition 1: Node not found, using saved modalStyle');
setModalStyle(panelInfo.modalStyle); setModalStyle(panelInfo.modalStyle);
@@ -2223,6 +2327,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
); );
Spotlight.focus('playVideoShopNowBox'); Spotlight.focus('playVideoShopNowBox');
} else { } else {
console.log('[PANEL] PlayerPanel updatePanel - handleIndicatorDownClick');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -2241,7 +2346,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setPrevChannelIndex(selectedIndex); setPrevChannelIndex(selectedIndex);
} }
setSideContentsVisible(true); setSideContentsVisible(true);
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]); }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter, tabContainerVersion, tabIndexV2]);
const handleIndicatorUpClick = useCallback(() => { const handleIndicatorUpClick = useCallback(() => {
if (!initialEnter) { if (!initialEnter) {
@@ -2270,6 +2375,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
); );
Spotlight.focus('playVideoShopNowBox'); Spotlight.focus('playVideoShopNowBox');
} else { } else {
console.log('[PANEL] PlayerPanel updatePanel - handleIndicatorUpClick');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.PLAYER_PANEL, name: panel_names.PLAYER_PANEL,
@@ -2288,7 +2394,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
setPrevChannelIndex(selectedIndex); setPrevChannelIndex(selectedIndex);
} }
setSideContentsVisible(true); setSideContentsVisible(true);
}, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter]); }, [dispatch, playListInfo, selectedIndex, sideContentsVisible, initialEnter, tabContainerVersion, tabIndexV2]);
useEffect(() => { useEffect(() => {
if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) { if (panelInfo.shptmBanrTpNm === 'VOD' && panelInfo.patnrId && panelInfo.showId) {
@@ -2389,6 +2495,7 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const onEnded = useCallback( const onEnded = useCallback(
(e) => { (e) => {
if (panelInfoRef.current.shptmBanrTpNm === 'MEDIA') { if (panelInfoRef.current.shptmBanrTpNm === 'MEDIA') {
console.log('[PANEL] PlayerPanel updatePanel - DETAIL_PANEL video ended');
dispatch( dispatch(
updatePanel({ updatePanel({
name: panel_names.DETAIL_PANEL, name: panel_names.DETAIL_PANEL,
@@ -2429,14 +2536,105 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
); );
const onKeyDown = (ev) => { const onKeyDown = (ev) => {
if (ev.keyCode === 34) { // tabIndex === 1 (LiveChannelContents 표시)이고 비디오 배너에 포커스가 있는 경우
const currentFocused = Spotlight.getCurrent();
const spotlightId = currentFocused?.getAttribute('data-spotlight-id');
const isVideoItemFocused = spotlightId?.startsWith('tabChannel-video');
// LiveChannelContents의 비디오 배너에 포커스가 있는 경우: PageUp/PageDown을 좌우 이동으로 변환
if (tabIndexV2 === 1 && isVideoItemFocused) {
// DOM에서 실제로 렌더링된 모든 비디오 배너 찾기 (가상화 대응)
const allVideoBanners = Array.from(
document.querySelectorAll('[data-spotlight-id^="tabChannel-video-"]')
);
if (allVideoBanners.length > 0) {
// 현재 포커스된 배너의 인덱스 찾기
const currentBannerIndex = allVideoBanners.findIndex(
(el) => el === currentFocused
);
if (currentBannerIndex !== -1) {
if (ev.keyCode === 34) { // PageDown -> 오른쪽 배너로 포커스 이동
ev.stopPropagation();
ev.preventDefault();
// DOM에 렌더링된 다음 배너로 이동 (마지막이면 무시 또는 첫 번째로)
if (currentBannerIndex < allVideoBanners.length - 1) {
// 다음 배너가 DOM에 있으면 이동
const nextBanner = allVideoBanners[currentBannerIndex + 1];
const nextSpotlightId = nextBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageDown (비디오 배너) -> 오른쪽으로 이동', {
current: spotlightId,
next: nextSpotlightId,
currentBannerIndex,
totalVisibleBanners: allVideoBanners.length,
});
Spotlight.focus(nextSpotlightId);
} else {
// 마지막 배너면 첫 번째로 이동 시도 (DOM에 있으면)
const firstBanner = allVideoBanners[0];
const firstSpotlightId = firstBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageDown (마지막 배너) -> 첫 번째 배너로 이동 시도', {
current: spotlightId,
next: firstSpotlightId,
isWrapAround: true,
});
Spotlight.focus(firstSpotlightId);
}
return;
} else if (ev.keyCode === 33) { // PageUp -> 왼쪽 배너로 포커스 이동
ev.stopPropagation();
ev.preventDefault();
// DOM에 렌더링된 이전 배너로 이동 (첫 번째면 무시 또는 마지막으로)
if (currentBannerIndex > 0) {
// 이전 배너가 DOM에 있으면 이동
const prevBanner = allVideoBanners[currentBannerIndex - 1];
const prevSpotlightId = prevBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageUp (비디오 배너) -> 왼쪽으로 이동', {
current: spotlightId,
prev: prevSpotlightId,
currentBannerIndex,
totalVisibleBanners: allVideoBanners.length,
});
Spotlight.focus(prevSpotlightId);
} else {
// 첫 번째 배너면 마지막으로 이동 시도 (DOM에 있으면)
const lastBanner = allVideoBanners[allVideoBanners.length - 1];
const lastSpotlightId = lastBanner.getAttribute('data-spotlight-id');
dlog('[PlayerPanel] 🎯 PageUp (첫 번째 배너) -> 마지막 배너로 이동 시도', {
current: spotlightId,
prev: lastSpotlightId,
isWrapAround: true,
});
Spotlight.focus(lastSpotlightId);
}
return;
}
}
}
}
// 기존 로직: LiveChannelButton 또는 다른 경우에는 상/하 이동
if (ev.keyCode === 34) { // PageDown
handleIndicatorDownClick(); handleIndicatorDownClick();
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} else if (ev.keyCode === 33) { dlog('[PlayerPanel] 📺 PageDown (버튼 또는 다른 경우) -> 다음 비디오');
} else if (ev.keyCode === 33) { // PageUp
handleIndicatorUpClick(); handleIndicatorUpClick();
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
dlog('[PlayerPanel] 📺 PageUp (버튼 또는 다른 경우) -> 이전 비디오');
} }
}; };
@@ -2447,6 +2645,11 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
const timerIdTabAutoAdvance = useRef(null); const timerIdTabAutoAdvance = useRef(null);
const prevTabIndexV2 = useRef(null); const prevTabIndexV2 = useRef(null);
// Activity Check for tabIndex auto-advance
const lastActivityTimeRef = useRef(Date.now());
const activityCheckIntervalRef = useRef(null);
const ACTIVITY_TIMEOUT = 1000; // 1초 동안 활동이 없으면 타이머 진행
const showSideContents = useMemo(() => { const showSideContents = useMemo(() => {
return ( return (
sideContentsVisible && sideContentsVisible &&
@@ -2558,17 +2761,62 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
timerIdTabAutoAdvance.current = null; timerIdTabAutoAdvance.current = null;
}, []); }, []);
// Activity 감지 함수
const onActivityDetected = useCallback(() => {
lastActivityTimeRef.current = Date.now();
dlog('[PlayerPanel] 🎯 Activity detected - timer will be delayed', {
timestamp: new Date().toISOString(),
});
}, []);
// Activity 여부를 확인하는 함수 (1초 타임아웃 체크)
const isInactive = useCallback(() => {
const now = Date.now();
const timeSinceLastActivity = now - lastActivityTimeRef.current;
return timeSinceLastActivity > ACTIVITY_TIMEOUT;
}, []);
const resetTimerTabAutoAdvance = useCallback( const resetTimerTabAutoAdvance = useCallback(
(timeout) => { (timeout) => {
if (timerIdTabAutoAdvance.current) { if (timerIdTabAutoAdvance.current) {
clearTimerTabAutoAdvance(); clearTimerTabAutoAdvance();
} }
timerIdTabAutoAdvance.current = setTimeout(() => { // Activity check interval 설정 (매 100ms마다 체크)
setTabIndexV2(2); if (activityCheckIntervalRef.current) {
}, timeout); clearInterval(activityCheckIntervalRef.current);
}
let elapsedTime = 0;
activityCheckIntervalRef.current = setInterval(() => {
// 활동이 없을 때만 경과 시간 증가
if (isInactive()) {
elapsedTime += 100;
dlog('[PlayerPanel] ⏱️ TabIndex auto-advance: inactive', {
elapsedTime,
requiredTime: timeout,
});
// 필요한 시간만큼 경과했으면 타이머 실행
if (elapsedTime >= timeout) {
dlog('[PlayerPanel] ✅ TabIndex auto-advance executing - setTabIndexV2(2)', {
totalElapsed: elapsedTime,
timeout,
});
clearInterval(activityCheckIntervalRef.current);
setTabIndexV2(2);
}
} else {
// 활동이 감지되면 경과 시간 리셋
dlog('[PlayerPanel] 🔄 Activity detected - resetting elapsed time', {
previousElapsed: elapsedTime,
});
elapsedTime = 0;
}
}, 100);
}, },
[clearTimerTabAutoAdvance] [clearTimerTabAutoAdvance, isInactive]
); );
// Redux로 오버레이 숨김 // Redux로 오버레이 숨김
@@ -2593,6 +2841,10 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
if (timerIdTabAutoAdvance.current) { if (timerIdTabAutoAdvance.current) {
clearTimerTabAutoAdvance(); clearTimerTabAutoAdvance();
} }
if (activityCheckIntervalRef.current) {
clearInterval(activityCheckIntervalRef.current);
activityCheckIntervalRef.current = null;
}
dispatch(resetPlayerOverlays()); dispatch(resetPlayerOverlays());
} }
@@ -2807,6 +3059,53 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
panelInfo?.modal, panelInfo?.modal,
]); ]);
// PageUp/PageDown으로 비디오 변경 시 현재 재생 배너로 포커스 이동
useEffect(() => {
if (tabContainerVersion === 2 &&
tabIndexV2 === 1 &&
panelInfo?.isIndicatorByClick &&
selectedIndex !== null &&
selectedIndex >= 0) {
dlog('[PlayerPanel] 🎯 PageUp/PageDown 후 포커스 이동 준비', {
selectedIndex,
tabContainerVersion,
tabIndexV2,
isIndicatorByClick: panelInfo.isIndicatorByClick
});
const bannerSpotlightId = `banner${selectedIndex}`;
setTimeout(() => {
dlog('[PlayerPanel] 🔍 포커스 이동 시도:', bannerSpotlightId);
const bannerElement = document.querySelector(`[data-spotlight-id="${bannerSpotlightId}"]`);
if (bannerElement) {
dlog('[PlayerPanel] ✅ 배너 요소 찾음, 포커스 이동 실행');
Spotlight.focus(bannerElement);
} else {
dlog('[PlayerPanel] ⚠️ 배너 요소 찾지 못함:', bannerSpotlightId);
// 모든 배너 요소 목록 출력
const allBanners = document.querySelectorAll('[data-spotlight-id^="banner"]');
dlog('[PlayerPanel] 🔍 사용 가능한 배너 목록:',
Array.from(allBanners).map(el => el.getAttribute('data-spotlight-id'))
);
}
// 플래그 리셋
dispatch(
updatePanel({
name: panel_names.PLAYER_PANEL,
panelInfo: {
isIndicatorByClick: false
},
})
);
}, 200); // DOM 업데이트 대기
}
}, [selectedIndex, tabContainerVersion, tabIndexV2, panelInfo?.isIndicatorByClick, dispatch]);
// TabIndex 1 자동 다음 단계로 이동 // TabIndex 1 자동 다음 단계로 이동
useEffect(() => { useEffect(() => {
// tabIndex === 1일 때만 실행 // tabIndex === 1일 때만 실행
@@ -2833,6 +3132,31 @@ const PlayerPanel = ({ isTabActivated, panelInfo, isOnTop, spotlightId, ...props
clearTimerTabAutoAdvance, clearTimerTabAutoAdvance,
]); ]);
// Activity detection for tabIndex auto-advance (mousemove, keydown, click)
useEffect(() => {
// tabIndex === 1일 때만 Activity 감지 활성화
if (tabIndexV2 !== 1 || !belowContentsVisible) {
return;
}
dlog('[PlayerPanel] 🎙️ Activity listener registered for tabIndex=1');
const handleMouseMove = onActivityDetected;
const handleKeyDown = onActivityDetected;
const handleClick = onActivityDetected;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('click', handleClick);
return () => {
dlog('[PlayerPanel] 🎙️ Activity listener unregistered');
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('click', handleClick);
};
}, [tabIndexV2, belowContentsVisible, onActivityDetected]);
useLayoutEffect(() => { useLayoutEffect(() => {
const videoContainer = document.querySelector(`.${css.videoContainer}`); const videoContainer = document.querySelector(`.${css.videoContainer}`);

View File

@@ -4,43 +4,33 @@ import { useDispatch } from 'react-redux';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
// <<<<<<< HEAD import { sendLogTotalRecommend } from '../../../../actions/logActions';
import { updatePanel } from '../../../../actions/panelActions'; import { updatePanel } from '../../../../actions/panelActions';
import TVirtualGridList from '../../../../components/TVirtualGridList/TVirtualGridList'; import TScrollerLiveChannel from './TScrollerLiveChannel';
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../../../utils/Config'; import {
LOG_CONTEXT_NAME,
LOG_MENU,
LOG_MESSAGE_ID,
panel_names,
} from '../../../../utils/Config';
import { $L } from '../../../../utils/helperMethods'; import { $L } from '../../../../utils/helperMethods';
import PlayerItemCard, { TYPES } from '../../PlayerItemCard/PlayerItemCard'; import PlayerItemCard, { TYPES } from '../../PlayerItemCard/PlayerItemCard';
import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContents'; import ListEmptyContents from '../TabContents/ListEmptyContents/ListEmptyContents';
import css from './LiveChannelContents.module.less'; import css from './LiveChannelContents.module.less';
import cssV2 from './LiveChannelContents.v2.module.less'; import cssV2 from './LiveChannelContents.v2.module.less';
import { sendLogTotalRecommend } from '../../../../actions/logActions';
// =======
// import { updatePanel } from "../../../../actions/panelActions";
// import TVirtualGridList from "../../../../components/TVirtualGridList/TVirtualGridList";
// import {
// LOG_CONTEXT_NAME,
// LOG_MENU,
// LOG_MESSAGE_ID,
// panel_names,
// } from "../../../../utils/Config";
// import { $L } from "../../../../utils/helperMethods";
// import PlayerItemCard, { TYPES } from "../../PlayerItemCard/PlayerItemCard";
// import ListEmptyContents from "../TabContents/ListEmptyContents/ListEmptyContents";
// import css from "./LiveChannelContents.module.less";
// import { sendLogTotalRecommend } from "../../../../actions/logActions";
// >>>>>>> gitlab/develop
export default function LiveChannelContents({ export default function LiveChannelContents({
liveInfos, liveInfos,
currentTime, currentTime,
setSelectedIndex, setSelectedIndex,
selectedIndex,
videoVerticalVisible, videoVerticalVisible,
currentVideoShowId, currentVideoShowId,
tabIndex, tabIndex,
handleItemFocus, handleItemFocus,
tabTitle, tabTitle,
panelInfo, panelInfo,
// <<<<<<< HEAD // <<<<<<< HEAD
direction = 'vertical', direction = 'vertical',
version = 1, version = 1,
isFilteredByPatnr19, isFilteredByPatnr19,
@@ -49,13 +39,13 @@ export default function LiveChannelContents({
const isClickBlocked = useRef(false); const isClickBlocked = useRef(false);
const blockTimeoutRef = useRef(null); const blockTimeoutRef = useRef(null);
// ======= // =======
// isFilteredByPatnr19, // isFilteredByPatnr19,
// }) { // }) {
// const dispatch = useDispatch(); // const dispatch = useDispatch();
// const isClickBlocked = useRef(false); // const isClickBlocked = useRef(false);
const scrollToRef = useRef(null); const scrollToRef = useRef(null);
// >>>>>>> gitlab/develop // >>>>>>> gitlab/develop
const handleFocus = useCallback( const handleFocus = useCallback(
() => () => { () => () => {
if (handleItemFocus) { if (handleItemFocus) {
@@ -77,6 +67,19 @@ export default function LiveChannelContents({
} }
}, [isFilteredByPatnr19]); }, [isFilteredByPatnr19]);
// currentVideoShowId 변경 시 해당 배너가 보이도록 스크롤
// (LiveChannelButton에서 PageUp/PageDown으로 동영상 변경 시)
// currentVideoShowId 기반으로 스크롤하면 포커스 이동 없이 배너만 화면에 보임
useEffect(() => {
if (currentVideoShowId && liveInfos && liveInfos.length > 0 && scrollToRef.current) {
// currentVideoShowId와 일치하는 배너의 인덱스 찾기
const index = liveInfos.findIndex((item) => item.showId === currentVideoShowId);
if (index !== -1) {
scrollToRef.current({ index, animate: true, focus: false });
}
}
}, [currentVideoShowId, liveInfos]);
const renderItem = useCallback( const renderItem = useCallback(
({ index, ...rest }) => { ({ index, ...rest }) => {
const { const {
@@ -181,15 +184,24 @@ export default function LiveChannelContents({
startDt={strtDt} startDt={strtDt}
endDt={endDt} endDt={endDt}
currentTime={currentTime} currentTime={currentTime}
// <<<<<<< HEAD currentVideoVisible={currentVideoShowId === liveInfos[index].showId}
// <<<<<<< HEAD
version={version} version={version}
// ======= // =======
// currentVideoVisible={currentVideoShowId === liveInfos[index].showId} // currentVideoVisible={currentVideoShowId === liveInfos[index].showId}
// >>>>>>> gitlab/develop // >>>>>>> gitlab/develop
/> />
); );
}, },
[liveInfos, currentTime, currentVideoShowId, isClickBlocked, dispatch, handleFocus, version] [
liveInfos,
currentTime,
currentVideoShowId,
isClickBlocked,
dispatch,
handleFocus,
version,
]
); );
const containerClass = version === 2 ? cssV2.container : css.container; const containerClass = version === 2 ? cssV2.container : css.container;
@@ -207,7 +219,7 @@ export default function LiveChannelContents({
<> <>
<div className={containerClass}> <div className={containerClass}>
{liveInfos && liveInfos.length > 0 ? ( {liveInfos && liveInfos.length > 0 ? (
<TVirtualGridList <TScrollerLiveChannel
cbScrollTo={handleScrollTo} cbScrollTo={handleScrollTo}
dataSize={liveInfos.length} dataSize={liveInfos.length}
direction={direction} direction={direction}
@@ -215,7 +227,6 @@ export default function LiveChannelContents({
itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600} itemWidth={version === 2 ? 470 : videoVerticalVisible ? 540 : 600}
itemHeight={version === 2 ? 155 : 236} itemHeight={version === 2 ? 155 : 236}
spacing={version === 2 ? 30 : 12} spacing={version === 2 ? 30 : 12}
noScrollByWheel={false}
/> />
) : ( ) : (
<ListEmptyContents tabIndex={tabIndex} /> <ListEmptyContents tabIndex={tabIndex} />

View File

@@ -0,0 +1,250 @@
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
import classNames from 'classnames';
import { scaleH, scaleW } from '../../../../utils/helperMethods';
import css from './TScrollerLiveChannel.module.less';
/**
* TScrollerLiveChannel - Live Channel용 간단한 스크롤 컴포넌트
*
* TVirtualGridList의 가상화 대신 모든 아이템을 DOM에 렌더링
* 20개 미만의 아이템에 최적화되어 있음
*
* @param {number} dataSize - 아이템 개수
* @param {string} direction - 'horizontal' 또는 'vertical'
* @param {function} renderItem - 아이템 렌더링 함수 ({ index })
* @param {number} itemWidth - 아이템 너비
* @param {number} itemHeight - 아이템 높이
* @param {number} spacing - 아이템 간 간격
* @param {function} cbScrollTo - 스크롤 함수를 받을 콜백
* @param {string} className - 추가 CSS 클래스
* @param {string} spotlightId - Spotlight 포커스 ID prefix
*/
export default function TScrollerLiveChannel({
dataSize,
direction = 'horizontal',
renderItem,
itemWidth,
itemHeight,
spacing,
cbScrollTo,
className,
spotlightId,
}) {
const scrollContainerRef = useRef(null);
const itemsRef = useRef([]);
// 스크롤 컨테이너 크기 계산
const containerStyle = useMemo(() => {
if (direction === 'horizontal') {
return {
display: 'flex',
overflowX: 'auto',
overflowY: 'hidden',
width: '100%',
height: scaleH(itemHeight),
alignItems: 'center',
};
} else {
return {
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
overflowX: 'hidden',
width: '100%',
};
}
}, [direction, itemHeight]);
// 아이템 래퍼 스타일 계산
const itemsWrapperStyle = useMemo(() => {
if (direction === 'horizontal') {
return {
display: 'flex',
flexDirection: 'row',
gap: scaleW(spacing),
padding: `0 ${scaleW(spacing)}px`,
alignItems: 'center',
};
} else {
return {
display: 'flex',
flexDirection: 'column',
gap: scaleH(spacing),
padding: `${scaleH(spacing)}px 0`,
};
}
}, [direction, spacing]);
// 스크롤 함수 생성
const scrollToIndex = useCallback(
(index, options = {}) => {
const container = scrollContainerRef.current;
if (!container || !itemsRef.current[index]) return;
const item = itemsRef.current[index];
const { animate = true } = options;
if (direction === 'horizontal') {
// 수평 스크롤: 현재 아이템 + 다음 아이템까지 보이도록
const itemLeft = item.offsetLeft;
const itemWidth = item.offsetWidth;
const containerWidth = container.clientWidth;
// 다음 아이템도 일부 보일 수 있도록 스크롤
// 현재 아이템 + 다음 아이템의 일부가 보이는 위치로 스크롤
const nextItem = itemsRef.current[index + 1];
let scrollLeft = itemLeft - scaleW(spacing);
if (nextItem) {
// 다음 아이템의 왼쪽 끝이 컨테이너의 오른쪽 끝과 같은 위치가 되도록
const nextItemLeft = nextItem.offsetLeft;
const nextItemWidth = nextItem.offsetWidth;
const targetScrollLeft = nextItemLeft + nextItemWidth - containerWidth + scaleW(spacing);
// 두 가지 중에서 현재 아이템이 더 잘 보이는 쪽 선택
scrollLeft = Math.min(scrollLeft, targetScrollLeft);
}
// 음수 스크롤 방지
scrollLeft = Math.max(0, scrollLeft);
if (animate) {
container.scrollTo({
left: scrollLeft,
behavior: 'smooth',
});
} else {
container.scrollLeft = scrollLeft;
}
} else {
// 수직 스크롤: 현재 아이템 + 다음 아이템까지 보이도록
const itemTop = item.offsetTop;
const itemHeight = item.offsetHeight;
const containerHeight = container.clientHeight;
// 다음 아이템도 일부 보일 수 있도록 스크롤
const nextItem = itemsRef.current[index + 1];
let scrollTop = itemTop - scaleH(spacing);
if (nextItem) {
// 다음 아이템의 위쪽 끝이 컨테이너의 아래쪽 끝과 같은 위치가 되도록
const nextItemTop = nextItem.offsetTop;
const nextItemHeight = nextItem.offsetHeight;
const targetScrollTop = nextItemTop + nextItemHeight - containerHeight + scaleH(spacing);
// 두 가지 중에서 현재 아이템이 더 잘 보이는 쪽 선택
scrollTop = Math.min(scrollTop, targetScrollTop);
}
// 음수 스크롤 방지
scrollTop = Math.max(0, scrollTop);
if (animate) {
container.scrollTo({
top: scrollTop,
behavior: 'smooth',
});
} else {
container.scrollTop = scrollTop;
}
}
},
[direction, spacing]
);
// TVirtualGridList와 호환되는 콜백 인터페이스 제공
useEffect(() => {
if (cbScrollTo) {
cbScrollTo((options) => {
const { index, animate = true, focus = true } = options;
if (typeof index === 'number' && index >= 0 && index < dataSize) {
scrollToIndex(index, { animate });
}
});
}
}, [cbScrollTo, scrollToIndex, dataSize]);
// 아이템 ref 할당 함수
const setItemRef = useCallback((el, index) => {
if (el) {
itemsRef.current[index] = el;
} else {
delete itemsRef.current[index];
}
}, []);
// 포커스된 아이템을 화면에 완전히 보이도록 스크롤
const handleItemFocus = useCallback(
(index) => {
const container = scrollContainerRef.current;
const item = itemsRef.current[index];
if (!container || !item) return;
if (direction === 'horizontal') {
const itemLeft = item.offsetLeft;
const itemWidth = item.offsetWidth;
const containerWidth = container.clientWidth;
const containerScrollLeft = container.scrollLeft;
// 아이템이 완전히 보이는지 확인
const itemRight = itemLeft + itemWidth;
const containerRight = containerScrollLeft + containerWidth;
// 아이템이 왼쪽으로 밖에 나가 있으면 왼쪽 끝에 맞춤
if (itemLeft < containerScrollLeft) {
container.scrollLeft = itemLeft - scaleW(spacing);
}
// 아이템이 오른쪽으로 밖에 나가 있으면 오른쪽 끝에 맞춤
else if (itemRight > containerRight) {
container.scrollLeft = itemRight - containerWidth + scaleW(spacing);
}
} else {
const itemTop = item.offsetTop;
const itemHeight = item.offsetHeight;
const containerHeight = container.clientHeight;
const containerScrollTop = container.scrollTop;
// 아이템이 완전히 보이는지 확인
const itemBottom = itemTop + itemHeight;
const containerBottom = containerScrollTop + containerHeight;
// 아이템이 위로 밖에 나가 있으면 위쪽 끝에 맞춤
if (itemTop < containerScrollTop) {
container.scrollTop = itemTop - scaleH(spacing);
}
// 아이템이 아래로 밖에 나가 있으면 아래쪽 끝에 맞춤
else if (itemBottom > containerBottom) {
container.scrollTop = itemBottom - containerHeight + scaleH(spacing);
}
}
},
[direction, spacing]
);
return (
<div
className={classNames(css.tScrollerLiveChannelContainer, className)}
style={containerStyle}
ref={scrollContainerRef}
>
<div className={css.itemsWrapper} style={itemsWrapperStyle}>
{Array.from({ length: dataSize }).map((_, index) => (
<div
key={`item-${index}`}
ref={(el) => setItemRef(el, index)}
className={css.item}
style={{
width: direction === 'horizontal' ? scaleW(itemWidth) : 'auto',
height: direction === 'horizontal' ? 'auto' : scaleH(itemHeight),
flexShrink: 0,
}}
onFocus={() => handleItemFocus(index)}
>
{renderItem({ index })}
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
.tScrollerLiveChannelContainer {
position: relative;
width: 100%;
height: 100%;
// 스크롤바 스타일
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
&:hover {
background: rgba(255, 255, 255, 0.5);
}
}
}
.itemsWrapper {
width: 100%;
height: 100%;
}
.item {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
// 포커스 상태 처리
&:focus-within {
outline: none;
}
}

View File

@@ -5,9 +5,13 @@ import { compose } from 'ramda/src/compose';
import Spotlight from '@enact/spotlight'; import Spotlight from '@enact/spotlight';
import Spottable from '@enact/spotlight/Spottable'; import Spottable from '@enact/spotlight/Spottable';
import { Marquee, MarqueeController } from '@enact/ui/Marquee'; import {
Marquee,
MarqueeController,
} from '@enact/ui/Marquee';
import icon_arrow_dwon from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png'; import icon_arrow_dwon
from '../../../../../assets/images/player/icon_tabcontainer_arrow_down.png';
import CustomImage from '../../../../components/CustomImage/CustomImage'; import CustomImage from '../../../../components/CustomImage/CustomImage';
import { SpotlightIds } from '../../../../utils/SpotlightIds'; import { SpotlightIds } from '../../../../utils/SpotlightIds';
import css from './LiveChannelNext.module.less'; import css from './LiveChannelNext.module.less';
@@ -18,7 +22,7 @@ export default function LiveChannelNext({
channelLogo, channelLogo,
channelName = 'ShopLC', channelName = 'ShopLC',
programName = 'Sandal Black...', programName = 'Sandal Black...',
backgroundColor = 'linear-gradient(180deg, #284998 0%, #06B0EE 100%)', backgroundColor = 'transparent',
onClick, onClick,
onFocus, onFocus,
spotlightId = 'live-channel-next-button', spotlightId = 'live-channel-next-button',
@@ -53,8 +57,7 @@ export default function LiveChannelNext({
> >
<div className={css.logoWrapper}> <div className={css.logoWrapper}>
<div <div
className={css.logoBackground} className={css.logoBackground}
style={{ background: backgroundColor }}
> >
{channelLogo ? ( {channelLogo ? (
<CustomImage <CustomImage

View File

@@ -41,6 +41,7 @@
height: 72px; height: 72px;
position: relative; position: relative;
flex-shrink: 0; flex-shrink: 0;
background-color: transparent;
} }
.logoBackground { .logoBackground {
@@ -51,6 +52,7 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
background-color: transparent;
} }
.logoImage { .logoImage {
@@ -60,6 +62,7 @@
&.qvcLogoImg { &.qvcLogoImg {
width: 70%; width: 70%;
height: 70%; height: 70%;
background-color: transparent;
} }
} }

View File

@@ -21,8 +21,16 @@ export default function ShopNowButton({ onClick }) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
// tabIndexV2가 2일 때만 CC 버튼으로 내려가기 // tabIndexV2가 2일 때만 CC 버튼으로 내려가기
Spotlight.focus('player-subtitlebutton'); Spotlight.focus('live-channel-next-button');
}; };
const handleSpotlightLeft = (e) => {
e.stopPropagation();
e.preventDefault();
// tabIndexV2가 2일 때만 CC 버튼으로 내려가기
Spotlight.focus('player-subtitlebutton');
};
return ( return (
<div className={css.container}> <div className={css.container}>
<SpottableDiv <SpottableDiv
@@ -31,6 +39,7 @@ export default function ShopNowButton({ onClick }) {
spotlightId="below-tab-shop-now-button" spotlightId="below-tab-shop-now-button"
onSpotlightUp={handleSpotlightUp} onSpotlightUp={handleSpotlightUp}
onSpotlightDown={handleSpotlightDown} onSpotlightDown={handleSpotlightDown}
onSpotlightLeft={handleSpotlightLeft}
> >
<span className={css.buttonText}>SHOP NOW</span> <span className={css.buttonText}>SHOP NOW</span>
</SpottableDiv> </SpottableDiv>

View File

@@ -196,7 +196,7 @@ export default function TabContainerV2({
// tabIndex = 2 (ShopNowButton) // tabIndex = 2 (ShopNowButton)
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
Spotlight.focus('below-tab-shop-now-button'); Spotlight.focus('below-tab-shop-now-button');
}, 100); }, 10);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
} }
}, []); }, []);

View File

@@ -1,5 +1,11 @@
// src/views/SearchPanel/SearchPanel.new.jsx // src/views/SearchPanel/SearchPanel.new.jsx
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
@@ -33,7 +39,9 @@ import {
// showWarningToast, // showWarningToast,
// } from '../../actions/toastActions'; // } from '../../actions/toastActions';
import TBody from '../../components/TBody/TBody'; import TBody from '../../components/TBody/TBody';
import TItemCardNew, { removeDotAndColon } from '../../components/TItemCard/TItemCard.new'; import TItemCardNew, {
removeDotAndColon,
} from '../../components/TItemCard/TItemCard.new';
import TPanel from '../../components/TPanel/TPanel'; import TPanel from '../../components/TPanel/TPanel';
import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator'; import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList'; import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
@@ -41,15 +49,22 @@ import usePanelHistory from '../../hooks/usePanelHistory/usePanelHistory';
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer"; // import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import { useSearchHistory } from '../../hooks/useSearchHistory'; import { useSearchHistory } from '../../hooks/useSearchHistory';
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config'; import {
LOG_CONTEXT_NAME,
LOG_MENU,
LOG_MESSAGE_ID,
panel_names,
} from '../../utils/Config';
import { createDebugHelpers } from '../../utils/debug';
import NoSearchResults from './NoSearchResults/NoSearchResults'; import NoSearchResults from './NoSearchResults/NoSearchResults';
// import NoSearchResults from './NoSearchResults/NoSearchResults'; // import NoSearchResults from './NoSearchResults/NoSearchResults';
import SearchInputOverlay from './SearchInputOverlay'; import SearchInputOverlay from './SearchInputOverlay';
import css from './SearchPanel.new.module.less'; import css from './SearchPanel.new.module.less';
import SearchResultsNew from './SearchResults.new.v2'; import SearchResultsNew from './SearchResults.new.v2';
import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple'; import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple';
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay'; import VoiceInputOverlay, {
import { createDebugHelpers } from '../../utils/debug'; VOICE_MODES,
} from './VoiceInputOverlay/VoiceInputOverlay';
// 디버그 헬퍼 설정 // 디버그 헬퍼 설정
const DEBUG_MODE = false; const DEBUG_MODE = false;
@@ -75,13 +90,22 @@ export const SEARCH_PANEL_MODES = {
VOICE_RESULT: 'voice_result', // 음성 검색 결과 표시 VOICE_RESULT: 'voice_result', // 음성 검색 결과 표시
}; };
const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const ContainerBasic = SpotlightContainerDecorator(
{ enterTo: 'last-focused' },
'div'
);
// 검색 입력 영역 컨테이너 // 검색 입력 영역 컨테이너
const InputContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const InputContainer = SpotlightContainerDecorator(
{ enterTo: 'last-focused' },
'div'
);
// 콘텐츠 섹션 컨테이너 // 콘텐츠 섹션 컨테이너
const SectionContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); const SectionContainer = SpotlightContainerDecorator(
{ enterTo: 'last-focused' },
'div'
);
// 메모리 누수 방지를 위한 안전한 이미지 컴포넌트 (컴포넌트 외부로 이동) // 메모리 누수 방지를 위한 안전한 이미지 컴포넌트 (컴포넌트 외부로 이동)
const SafeImageComponent = ({ src, alt, className, ...props }) => { const SafeImageComponent = ({ src, alt, className, ...props }) => {
@@ -118,7 +142,9 @@ const SafeImageComponent = ({ src, alt, className, ...props }) => {
}; };
}, []); }, []);
return <img ref={imgRef} src={src} alt={alt} className={className} {...props} />; return (
<img ref={imgRef} src={src} alt={alt} className={className} {...props} />
);
}; };
const ITEMS_PER_PAGE = 9; const ITEMS_PER_PAGE = 9;
@@ -152,22 +178,36 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 0hun: 패널 전역 상태 // 0hun: 패널 전역 상태
const panels = useSelector((state) => state.panels.panels); const panels = useSelector((state) => state.panels.panels);
// 0hun: 음성 검색 결과에 대한 전역 상태 // 0hun: 음성 검색 결과에 대한 전역 상태
const shopperHouseData = useSelector((state) => state.search.shopperHouseData); const shopperHouseData = useSelector(
const shopperHouseError = useSelector((state) => state.search.shopperHouseError); (state) => state.search.shopperHouseData
);
const shopperHouseError = useSelector(
(state) => state.search.shopperHouseError
);
// 0hun: 음성 검색 searchId (Redux에서 별도 관리) // 0hun: 음성 검색 searchId (Redux에서 별도 관리)
const shopperHouseSearchId = useSelector((state) => state.search.shopperHouseSearchId); const shopperHouseSearchId = useSelector(
(state) => state.search.shopperHouseSearchId
);
// 0hun: 음성 검색 relativeQueries (Redux에서 별도 관리) // 0hun: 음성 검색 relativeQueries (Redux에서 별도 관리)
const shopperHouseRelativeQueries = useSelector( const shopperHouseRelativeQueries = useSelector(
(state) => state.search.shopperHouseRelativeQueries (state) => state.search.shopperHouseRelativeQueries
); );
// 🔄 이전 shopperHouseData (sortingType 변경 시 사용) // 🔄 이전 shopperHouseData (sortingType 변경 시 사용)
const preShopperHouseData = useSelector((state) => state.search.preShopperHouseData); const preShopperHouseData = useSelector(
(state) => state.search.preShopperHouseData
);
// 0hun: 검색 메인, Hot Picks for you 영역에 대한 전역 상태 값 // 0hun: 검색 메인, Hot Picks for you 영역에 대한 전역 상태 값
const hotPicksForYou = useSelector((state) => state.search.searchMainData.hotPicksForYou); const hotPicksForYou = useSelector(
(state) => state.search.searchMainData.hotPicksForYou
);
// 0hun: 검색 메인, Popular Brands 영역에 대한 전역 상태 값 // 0hun: 검색 메인, Popular Brands 영역에 대한 전역 상태 값
const popularBrands = useSelector((state) => state.search.searchMainData.popularBrands); const popularBrands = useSelector(
(state) => state.search.searchMainData.popularBrands
);
// 0hun: 검색 메인, Top Searchs 영역에 대한 전역 상태 값 // 0hun: 검색 메인, Top Searchs 영역에 대한 전역 상태 값
const topSearchs = useSelector((state) => state.search.searchMainData.topSearchs); const topSearchs = useSelector(
(state) => state.search.searchMainData.topSearchs
);
// jhun: 검색 메인, Today Deals 영역에 대한 전역 상태 값 // jhun: 검색 메인, Today Deals 영역에 대한 전역 상태 값
const tsvInfo = useSelector((state) => state.search.searchMainData.tsvInfo); const tsvInfo = useSelector((state) => state.search.searchMainData.tsvInfo);
@@ -177,7 +217,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 0hun: 초기 포커스 유무를 나타내는 Boolean 상태 // 0hun: 초기 포커스 유무를 나타내는 Boolean 상태
const [firstSpot, setFirstSpot] = useState(false); const [firstSpot, setFirstSpot] = useState(false);
// 0hun: 검색어 상태 // 0hun: 검색어 상태
const [searchQuery, setSearchQuery] = useState(panelInfo.searchVal ? panelInfo.searchVal : null); const [searchQuery, setSearchQuery] = useState(
panelInfo.searchVal ? panelInfo.searchVal : null
);
// 0hun: 검색 컨테이너 포커스 position 상태 값 // 0hun: 검색 컨테이너 포커스 position 상태 값
const [position, setPosition] = useState(null); const [position, setPosition] = useState(null);
// 0hun: 가상 키보드 Display 유무 Boolean 값 (주석: 현재 VirtualKeyboardContainer가 비활성화됨) // 0hun: 가상 키보드 Display 유무 Boolean 값 (주석: 현재 VirtualKeyboardContainer가 비활성화됨)
@@ -191,14 +233,17 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// ✨ [Phase 3] TInput의 입력 모드 상태 제거 (더 이상 InputField가 없으므로 불필요) // ✨ [Phase 3] TInput의 입력 모드 상태 제거 (더 이상 InputField가 없으므로 불필요)
// const [isInputModeActive, setIsInputModeActive] = useState(false); // const [isInputModeActive, setIsInputModeActive] = useState(false);
// 0hun: 현재 포커스된 container의 spotlightId를 관리하는 상태 값 // 0hun: 현재 포커스된 container의 spotlightId를 관리하는 상태 값
const [focusedContainerId, setFocusedContainerId] = useState(panelInfo?.focusedContainerId); const [focusedContainerId, setFocusedContainerId] = useState(
panelInfo?.focusedContainerId
);
// ✨ [Phase 1] SearchPanel의 현재 모드 상태 (VoiceInputOverlay의 VOICE_MODES와 동일한 개념) // ✨ [Phase 1] SearchPanel의 현재 모드 상태 (VoiceInputOverlay의 VOICE_MODES와 동일한 개념)
const [currentMode, setCurrentMode] = useState(SEARCH_PANEL_MODES.INITIAL); const [currentMode, setCurrentMode] = useState(SEARCH_PANEL_MODES.INITIAL);
const [isShopperHousePending, setIsShopperHousePending] = useState(false); const [isShopperHousePending, setIsShopperHousePending] = useState(false);
const [voiceOverlayMode, setVoiceOverlayMode] = useState(VOICE_MODES.PROMPT); const [voiceOverlayMode, setVoiceOverlayMode] = useState(VOICE_MODES.PROMPT);
const [voiceOverlayResponseText, setVoiceOverlayResponseText] = useState(''); const [voiceOverlayResponseText, setVoiceOverlayResponseText] = useState('');
const [isVoiceOverlayBubbleSearch, setIsVoiceOverlayBubbleSearch] = useState(false); const [isVoiceOverlayBubbleSearch, setIsVoiceOverlayBubbleSearch] =
useState(false);
const [shouldFocusVoiceResult, setShouldFocusVoiceResult] = useState(false); const [shouldFocusVoiceResult, setShouldFocusVoiceResult] = useState(false);
// 🎯 HowAboutThese 포커스 관리 - 검색 입력 영역 포커스 감지용 상태 // 🎯 HowAboutThese 포커스 관리 - 검색 입력 영역 포커스 감지용 상태
@@ -285,11 +330,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const unifiedFocusTimerRef = useRef(null); const unifiedFocusTimerRef = useRef(null);
// ShopperHouse 에러 팝업 상태 가져오기 // ShopperHouse 에러 팝업 상태 가져오기
const shopperHouseErrorPopup = useSelector((state) => state.search.shopperHouseErrorPopup); const shopperHouseErrorPopup = useSelector(
(state) => state.search.shopperHouseErrorPopup
);
// API 실패 시 fallback reference 초기화 // API 실패 시 fallback reference 초기화
useEffect(() => { useEffect(() => {
if (shopperHouseErrorPopup?.visible && shopperHouseErrorPopup?.type === 'API_FAILURE') { if (
shopperHouseErrorPopup?.visible &&
shopperHouseErrorPopup?.type === 'API_FAILURE'
) {
dlog('[SearchPanel] 🧹 API 실패 감지 - fallbackShopperHouseData 초기화'); dlog('[SearchPanel] 🧹 API 실패 감지 - fallbackShopperHouseData 초기화');
shopperHouseDataRef.current = null; shopperHouseDataRef.current = null;
} }
@@ -307,8 +357,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
/** /**
* useSearchHistory Hook 적용 * useSearchHistory Hook 적용
*/ */
const { normalSearches, addNormalSearch, refreshHistory, executeSearchFromHistory } = const {
useSearchHistory(); normalSearches,
addNormalSearch,
refreshHistory,
executeSearchFromHistory,
} = useSearchHistory();
/** /**
* 🎯 [DetailPanel 복귀 감지] usePanelHistory Hook 적용 * 🎯 [DetailPanel 복귀 감지] usePanelHistory Hook 적용
@@ -422,7 +476,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// ✨ [Phase 4] Enter/OK 키 처리 - SearchInputOverlay 표시 // ✨ [Phase 4] Enter/OK 키 처리 - SearchInputOverlay 표시
if (e.key === 'Enter' || e.keyCode === 13) { if (e.key === 'Enter' || e.keyCode === 13) {
dlog('[DEBUG] [SearchPanel] TInputSimple에서 Enter/OK 키 감지 → SearchInputOverlay 오픈'); dlog(
'[DEBUG] [SearchPanel] TInputSimple에서 Enter/OK 키 감지 → SearchInputOverlay 오픈'
);
e.preventDefault(); e.preventDefault();
// ✨ [Phase 6] SearchInputOverlay 오픈 후 자동으로 입력 준비 완료 // ✨ [Phase 6] SearchInputOverlay 오픈 후 자동으로 입력 준비 완료
@@ -450,7 +506,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
]; ];
if (arrowKeys.includes(e.key)) { if (arrowKeys.includes(e.key)) {
// 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지 // 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지
if (position === 0 && (e.key === 'Left' || e.key === 'ArrowLeft') && !searchQuery) { if (
position === 0 &&
(e.key === 'Left' || e.key === 'ArrowLeft') &&
!searchQuery
) {
e.preventDefault(); e.preventDefault();
return; return;
} }
@@ -461,7 +521,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// DOM 쿼리 최적화: 캐싱된 input element 사용 // DOM 쿼리 최적화: 캐싱된 input element 사용
const input = const input =
inputElementRef.current || inputElementRef.current ||
document.querySelector(`[data-spotlight-id="input-field-box"] > input`); document.querySelector(
`[data-spotlight-id="input-field-box"] > input`
);
if (input) { if (input) {
inputElementRef.current = input; // 캐싱 inputElementRef.current = input; // 캐싱
if (position === input.value.length) { if (position === input.value.length) {
@@ -658,7 +720,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
'[DEBUG]-VOICE_RESULT: Clearing ShopperHouse data (searchId will be preserved for 2nd search)' '[DEBUG]-VOICE_RESULT: Clearing ShopperHouse data (searchId will be preserved for 2nd search)'
); );
dlog('[VoiceInput]-SearchPanel-onCancel-VOICE_RESULT'); dlog('[VoiceInput]-SearchPanel-onCancel-VOICE_RESULT');
dlog('[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출'); dlog(
'[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출'
);
} }
// 🎯 [포커스 로직 통합] 포커스는 상태 변경에 의해 자동으로 처리됨 // 🎯 [포커스 로직 통합] 포커스는 상태 변경에 의해 자동으로 처리됨
setIsShopperHousePending(false); setIsShopperHousePending(false);
@@ -816,7 +880,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
if (isReturningFromDetailPanel) { if (isReturningFromDetailPanel) {
const currentSpot = currentPanel?.panelInfo?.currentSpot; const currentSpot = currentPanel?.panelInfo?.currentSpot;
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[Focus] usePanelHistory로 DetailPanel 복귀 감지 - 이전 상품으로 포커스 이동'); dlog(
'[Focus] usePanelHistory로 DetailPanel 복귀 감지 - 이전 상품으로 포커스 이동'
);
dlog('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN (usePanelHistory)', { dlog('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN (usePanelHistory)', {
currentSpot, currentSpot,
mode: currentMode, mode: currentMode,
@@ -860,7 +926,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
) { ) {
const usedHistoryOnTop = currentIsOnTop && isOnTopChange?.becameOnTop; const usedHistoryOnTop = currentIsOnTop && isOnTopChange?.becameOnTop;
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[Focus] 개선된 방식으로 DetailPanel 복귀 감지 - 이전 상품으로 포커스 이동'); dlog(
'[Focus] 개선된 방식으로 DetailPanel 복귀 감지 - 이전 상품으로 포커스 이동'
);
dlog('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN (improved fallback)', { dlog('[FOCUS] 🎯 Scenario: DETAIL_PANEL_RETURN (improved fallback)', {
currentSpot: panelInfo.currentSpot, currentSpot: panelInfo.currentSpot,
mode: currentMode, mode: currentMode,
@@ -928,7 +996,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
prevMode: currentModeRef.current, prevMode: currentModeRef.current,
nextMode: currentMode, nextMode: currentMode,
isOnTopChanged: isOnTop !== isOnTopRef.current, isOnTopChanged: isOnTop !== isOnTopRef.current,
modeChanged: currentModeRef.current !== SEARCH_PANEL_MODES.VOICE_RESULT, modeChanged:
currentModeRef.current !== SEARCH_PANEL_MODES.VOICE_RESULT,
dataChanged: shopperHouseDataRef.current !== shopperHouseData, dataChanged: shopperHouseDataRef.current !== shopperHouseData,
}); });
} }
@@ -946,7 +1015,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 이렇게 하면 VOICE_OVERLAY_CLOSED 시나리오에서 TInput으로 가는 것을 방지 // 이렇게 하면 VOICE_OVERLAY_CLOSED 시나리오에서 TInput으로 가는 것을 방지
if (shopperHouseData && currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) { if (shopperHouseData && currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🔄 VOICE_OVERLAY_CLOSED + new data → NEW_SEARCH_LOADED 우선 처리'); dlog(
'[FOCUS] 🔄 VOICE_OVERLAY_CLOSED + new data → NEW_SEARCH_LOADED 우선 처리'
);
} }
return 'NEW_SEARCH_LOADED'; return 'NEW_SEARCH_LOADED';
} }
@@ -1009,7 +1080,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
let currentSpot = null; let currentSpot = null;
// 1. usePanelHistory의 currentSpot 우선 사용 // 1. usePanelHistory의 currentSpot 우선 사용
if (isReturningFromDetailPanel && currentPanel?.panelInfo?.currentSpot) { if (
isReturningFromDetailPanel &&
currentPanel?.panelInfo?.currentSpot
) {
currentSpot = currentPanel.panelInfo.currentSpot; currentSpot = currentPanel.panelInfo.currentSpot;
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🎯 usePanelHistory currentSpot 사용:', currentSpot); dlog('[FOCUS] 🎯 usePanelHistory currentSpot 사용:', currentSpot);
@@ -1019,13 +1093,19 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
else if (panelInfo?.currentSpot) { else if (panelInfo?.currentSpot) {
currentSpot = panelInfo.currentSpot; currentSpot = panelInfo.currentSpot;
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🔄 fallback으로 panelInfo.currentSpot 사용:', currentSpot); dlog(
'[FOCUS] 🔄 fallback으로 panelInfo.currentSpot 사용:',
currentSpot
);
} }
} }
if (currentSpot && currentSpot.startsWith('searchItemContents')) { if (currentSpot && currentSpot.startsWith('searchItemContents')) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🎯 DETAIL_PANEL_RETURN: 이전 상품으로 포커스 복원:', currentSpot); dlog(
'[FOCUS] 🎯 DETAIL_PANEL_RETURN: 이전 상품으로 포커스 복원:',
currentSpot
);
} }
return currentSpot; return currentSpot;
} else { } else {
@@ -1066,7 +1146,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// SearchInputOverlay에서 검색을 실행하면 isSearchOverlayVisible이 false로 설정되고 // SearchInputOverlay에서 검색을 실행하면 isSearchOverlayVisible이 false로 설정되고
// 동시에 검색 결과에 따라 모드가 변경되므로, 이 케이스는 검색어 선택 후 닫을 때만 발생 // 동시에 검색 결과에 따라 모드가 변경되므로, 이 케이스는 검색어 선택 후 닫을 때만 발생
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🎯 Scenario: SEARCH_OVERLAY_CLOSED - TInputSimple으로 포커스'); dlog(
'[FOCUS] 🎯 Scenario: SEARCH_OVERLAY_CLOSED - TInputSimple으로 포커스'
);
} }
return SPOTLIGHT_IDS.SEARCH_INPUT_BOX; return SPOTLIGHT_IDS.SEARCH_INPUT_BOX;
@@ -1290,17 +1372,27 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
{...rest} {...rest}
> >
<div className={css.productImageWrapper}> <div className={css.productImageWrapper}>
<SafeImage src={bgImgPath} alt={curationNm} className={css.productImage} /> <SafeImage
src={bgImgPath}
alt={curationNm}
className={css.productImage}
/>
</div> </div>
<div className={css.productInfo}> <div className={css.productInfo}>
{showBrandLogo && ( {showBrandLogo && (
<div className={css.productBrandWrapper}> <div className={css.productBrandWrapper}>
<SafeImage src={patncLogoPath} alt={patncNm} className={css.brandLogo} /> <SafeImage
src={patncLogoPath}
alt={patncNm}
className={css.brandLogo}
/>
</div> </div>
)} )}
<div className={css.productDetails}> <div className={css.productDetails}>
{showBrandName && <div className={css.brandName}>{patncNm}</div>} {showBrandName && <div className={css.brandName}>{patncNm}</div>}
{showProductTitle && <div className={css.productTitle}>{curationNm}</div>} {showProductTitle && (
<div className={css.productTitle}>{curationNm}</div>
)}
</div> </div>
</div> </div>
</SpottableProduct> </SpottableProduct>
@@ -1323,7 +1415,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const renderTsvItem = useCallback( const renderTsvItem = useCallback(
({ index, ...rest }) => { ({ index, ...rest }) => {
const { offerInfo, prdtId, imgUrl, patnrId, prdtNm, priceInfo } = tsvInfo[index]; const { offerInfo, prdtId, imgUrl, patnrId, prdtNm, priceInfo } =
tsvInfo[index];
return ( return (
<TItemCardNew <TItemCardNew
@@ -1336,7 +1429,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
priceInfo={priceInfo} priceInfo={priceInfo}
productId={prdtId} productId={prdtId}
productName={prdtNm} productName={prdtNm}
spotlightId={'searchMain-tsvInfo-spotlightId-' + removeDotAndColon(prdtId)} spotlightId={
'searchMain-tsvInfo-spotlightId-' + removeDotAndColon(prdtId)
}
{...rest} {...rest}
/> />
); );
@@ -1603,10 +1698,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 우선순위 2: 음성 검색 결과가 있으면 VOICE_RESULT 모드 // 우선순위 2: 음성 검색 결과가 있으면 VOICE_RESULT 모드
else if (shopperHouseData || isShopperHousePending) { else if (shopperHouseData || isShopperHousePending) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[DEBUG]-MODE: shopperHouseData EXISTS or pending VOICE_RESULT', { dlog(
hasData: !!shopperHouseData, '[DEBUG]-MODE: shopperHouseData EXISTS or pending VOICE_RESULT',
isPending: isShopperHousePending, {
}); hasData: !!shopperHouseData,
isPending: isShopperHousePending,
}
);
} }
nextMode = SEARCH_PANEL_MODES.VOICE_RESULT; nextMode = SEARCH_PANEL_MODES.VOICE_RESULT;
} }
@@ -1640,20 +1738,23 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 모드가 변경되었을 때만 업데이트 // 모드가 변경되었을 때만 업데이트
if (nextMode !== currentMode) { if (nextMode !== currentMode) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog(`[DEBUG]-VOICE_RESULT 🔀 Mode changed: ${currentMode} → ${nextMode}`, { dlog(
isVoiceOverlayVisible, `[DEBUG]-VOICE_RESULT 🔀 Mode changed: ${currentMode} → ${nextMode}`,
shopperHouseData: !!shopperHouseData, {
isShopperHousePending, isVoiceOverlayVisible,
searchPerformed, shopperHouseData: !!shopperHouseData,
searchQuery, isShopperHousePending,
hasSearchResults: !!( searchPerformed,
searchDatas?.theme?.length > 0 || searchQuery,
searchDatas?.item?.length > 0 || hasSearchResults: !!(
searchDatas?.show?.length > 0 searchDatas?.theme?.length > 0 ||
), searchDatas?.item?.length > 0 ||
isSearchOverlayVisible, searchDatas?.show?.length > 0
inputFocus, ),
}); isSearchOverlayVisible,
inputFocus,
}
);
} }
setCurrentMode(nextMode); setCurrentMode(nextMode);
} else { } else {
@@ -1779,10 +1880,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// NEW_SEARCH_LOADED: 음성 검색 결과 로드 시 VoiceInputOverlay와 충돌 방지 // NEW_SEARCH_LOADED: 음성 검색 결과 로드 시 VoiceInputOverlay와 충돌 방지
// 다른 시나리오에서는 기존과 같은 지연 시간 (100ms) // 다른 시나리오에서는 기존과 같은 지연 시간 (100ms)
const focusDelay = const focusDelay =
scenario === 'DETAIL_PANEL_RETURN' || scenario === 'NEW_SEARCH_LOADED' ? 50 : 100; scenario === 'DETAIL_PANEL_RETURN' || scenario === 'NEW_SEARCH_LOADED'
? 50
: 100;
unifiedFocusTimerRef.current = setTimeout(() => { unifiedFocusTimerRef.current = setTimeout(() => {
const targetElement = document.querySelector(`[data-spotlight-id="${targetId}"]`); const targetElement = document.querySelector(
`[data-spotlight-id="${targetId}"]`
);
if (targetElement || targetId === SPOTLIGHT_IDS.SEARCH_INPUT_BOX) { if (targetElement || targetId === SPOTLIGHT_IDS.SEARCH_INPUT_BOX) {
Spotlight.focus(targetId); Spotlight.focus(targetId);
@@ -1805,9 +1910,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
} }
// 🎯 DETAIL_PANEL_RETURN에서 요소를 찾지 못하면 fallback으로 첫 번째 상품 시도 // 🎯 DETAIL_PANEL_RETURN에서 요소를 찾지 못하면 fallback으로 첫 번째 상품 시도
if (scenario === 'DETAIL_PANEL_RETURN' && targetId.startsWith('searchItemContents')) { if (
scenario === 'DETAIL_PANEL_RETURN' &&
targetId.startsWith('searchItemContents')
) {
const fallbackTarget = 'searchItemContents0'; const fallbackTarget = 'searchItemContents0';
const fallbackElement = document.querySelector(`[data-spotlight-id="${fallbackTarget}"]`); const fallbackElement = document.querySelector(
`[data-spotlight-id="${fallbackTarget}"]`
);
if (fallbackElement) { if (fallbackElement) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog( dlog(
@@ -1825,10 +1935,15 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 🎯 [NEW_SEARCH_LOADED] 1초 후 다시 첫 번째 아이템으로 포커스 이동 // 🎯 [NEW_SEARCH_LOADED] 1초 후 다시 첫 번째 아이템으로 포커스 이동
// TInputSimple과 Mic Icon의 포커스 충돌 해결을 위해 // TInputSimple과 Mic Icon의 포커스 충돌 해결을 위해
if (scenario === 'NEW_SEARCH_LOADED' && targetId === 'searchItemContents0') { if (
scenario === 'NEW_SEARCH_LOADED' &&
targetId === 'searchItemContents0'
) {
setTimeout(() => { setTimeout(() => {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🔄 NEW_SEARCH_LOADED: 1 번째 상품으로 다시 포커스 이동'); dlog(
'[FOCUS] 🔄 NEW_SEARCH_LOADED: 1 번째 상품으로 다시 포커스 이동'
);
} }
Spotlight.focus('searchItemContents0'); Spotlight.focus('searchItemContents0');
}, 500); // 0.5초 후 }, 500); // 0.5초 후
@@ -1866,7 +1981,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
isVoiceOverlayVisibleRef.current = isVoiceOverlayVisible; isVoiceOverlayVisibleRef.current = isVoiceOverlayVisible;
isSearchOverlayVisibleRef.current = isSearchOverlayVisible; isSearchOverlayVisibleRef.current = isSearchOverlayVisible;
currentModeRef.current = currentMode; currentModeRef.current = currentMode;
}, [shopperHouseData, searchDatas, isVoiceOverlayVisible, isSearchOverlayVisible, currentMode]); }, [
shopperHouseData,
searchDatas,
isVoiceOverlayVisible,
isSearchOverlayVisible,
currentMode,
]);
/** /**
* 🎯 SearchInputOverlay 닫힘 후 포커스 관리 * 🎯 SearchInputOverlay 닫힘 후 포커스 관리
@@ -1889,10 +2010,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}); });
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🎯 SearchInputOverlay 닫힘 포커스 관리 useEffect 실행', { dlog(
shouldFocusSearchInput, '[FOCUS] 🎯 SearchInputOverlay 닫힘 포커스 관리 useEffect 실행',
timestamp: new Date().toISOString(), {
}); shouldFocusSearchInput,
timestamp: new Date().toISOString(),
}
);
} }
// 500ms 후 TInputSimple에 포커스 이동 // 500ms 후 TInputSimple에 포커스 이동
@@ -1903,10 +2027,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}); });
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 500ms 타이머 콜백 실행 - TInputSimple으로 포커스 이동', { dlog(
targetId: SPOTLIGHT_IDS.SEARCH_INPUT_BOX, '[FOCUS] 500ms 타이머 콜백 실행 - TInputSimple으로 포커스 이동',
timestamp: new Date().toISOString(), {
}); targetId: SPOTLIGHT_IDS.SEARCH_INPUT_BOX,
timestamp: new Date().toISOString(),
}
);
} }
dlog('[DEBUG] Spotlight.focus() 호출 직전', { dlog('[DEBUG] Spotlight.focus() 호출 직전', {
@@ -1950,9 +2077,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}); });
if (focusTimer) { if (focusTimer) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
dlog('[FOCUS] 🧹 SearchInputOverlay 포커스 관리 useEffect cleanup - 타이머 정리', { dlog(
timestamp: new Date().toISOString(), '[FOCUS] 🧹 SearchInputOverlay 포커스 관리 useEffect cleanup - 타이머 정리',
}); {
timestamp: new Date().toISOString(),
}
);
} }
clearTimeout(focusTimer); clearTimeout(focusTimer);
} }
@@ -2053,12 +2183,18 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, [currentMode, isOnTop, refreshHistory]); }, [currentMode, isOnTop, refreshHistory]);
return ( return (
<TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}> <TPanel
className={css.container}
handleCancel={onCancel}
spotlightId={spotlightId}
>
{/* ✨ [Phase 2] spotlightDisabled를 currentMode로 제어 */} {/* ✨ [Phase 2] spotlightDisabled를 currentMode로 제어 */}
<TBody <TBody
className={css.tBody} className={css.tBody}
scrollable scrollable
spotlightDisabled={!isOnTop || currentMode === SEARCH_PANEL_MODES.SEARCH_INPUT} spotlightDisabled={
!isOnTop || currentMode === SEARCH_PANEL_MODES.SEARCH_INPUT
}
> >
<ContainerBasic> <ContainerBasic>
{isOnTop && ( {isOnTop && (
@@ -2078,7 +2214,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
className={classNames( className={classNames(
css.inputContainer, css.inputContainer,
inputFocus === true && css.inputFocus, inputFocus === true && css.inputFocus,
searchDatas && css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */, searchDatas &&
css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
(currentMode === SEARCH_PANEL_MODES.VOICE_INPUT || (currentMode === SEARCH_PANEL_MODES.VOICE_INPUT ||
currentMode === SEARCH_PANEL_MODES.INPUT_FOCUSED) && currentMode === SEARCH_PANEL_MODES.INPUT_FOCUSED) &&
css.hidden css.hidden
@@ -2129,7 +2266,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
placeholder="Search products or brands" placeholder="Search products or brands"
/> />
</div> </div>
<SpottableMicButton {/* <SpottableMicButton
className={css.microphoneButton} className={css.microphoneButton}
onClick={onClickMic} onClick={onClickMic}
onFocus={onFocusMic} onFocus={onFocusMic}
@@ -2148,7 +2285,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
<div className={css.microphoneCircle}> <div className={css.microphoneCircle}>
<SafeImage src={micIcon} alt="Microphone" className={css.microphoneIcon} /> <SafeImage src={micIcon} alt="Microphone" className={css.microphoneIcon} />
</div> </div>
</SpottableMicButton> </SpottableMicButton> */}
</div> </div>
</InputContainer> </InputContainer>

View File

@@ -291,7 +291,7 @@ const TrendingNowPanel = ({ panelInfo, spotlightId, isOnTop, ...rest }) => {
return ( return (
<div className={css.trendingNowWrap}> <div className={css.trendingNowWrap}>
{selectedIndex >= 1 && showButton && ( {isOnTop && selectedIndex >= 1 && showButton && (
<TButton <TButton
className={classNames(css.button, css.prevBtn)} className={classNames(css.button, css.prevBtn)}
onClick={handleIndicatorClick("prev")} onClick={handleIndicatorClick("prev")}
@@ -411,7 +411,7 @@ const TrendingNowPanel = ({ panelInfo, spotlightId, isOnTop, ...rest }) => {
</TVerticalPagenator> </TVerticalPagenator>
</TBody> </TBody>
</TPanel> </TPanel>
{topInfos && {isOnTop && topInfos &&
topInfos?.length > 0 && topInfos?.length > 0 &&
selectedIndex !== topInfos?.length - 1 && selectedIndex !== topInfos?.length - 1 &&
showButton && ( showButton && (