[251115] fix: DetailPanel FullScreen Focus Move

🕐 커밋 시간: 2025. 11. 15. 22:03:44

📊 변경 통계:
  • 총 파일: 17개
  • 추가: +573줄
  • 삭제: -87줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/panelActions.js
  ~ com.twin.app.shoptime/src/components/MediaPlayer/MediaControls.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.module.less
  ~ com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.js
  ~ com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less
  ~ com.twin.app.shoptime/src/utils/SpotlightIds.js
  ~ com.twin.app.shoptime/src/views/HomePanel/BestSeller/BestSeller.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/EventPopUpBanner/EventPopUpBanner.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/PickedForYou/PickedForYou.jsx
  ~ com.twin.app.shoptime/src/views/HomePanel/SubCategory/SubCategory.jsx
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResultsNew/ItemCard.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/actions/panelActions.js (javascript):
     Added: updatePanel()
  📄 com.twin.app.shoptime/src/components/MediaPlayer/MediaControls.js (javascript):
     Added: onSpotlightRight(), onSpotlightUp(), MediaControlsDecoratorHOC(), handleCancel()
     Deleted: onSpotlightRight(), onSpotlightUp(), MediaControlsDecoratorHOC(), handleCancel()
  📄 com.twin.app.shoptime/src/components/VideoPlayer/MediaPlayer.v2.jsx (javascript):
    🔄 Modified: SpotlightContainerDecorator()
  📄 com.twin.app.shoptime/src/components/VideoPlayer/VideoPlayer.v3.module.less (unknown):
     Added: position()
     Deleted: position()
  📄 com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx (javascript):
    🔄 Modified: SpotlightContainerDecorator()
  📄 com.twin.app.shoptime/src/views/HomePanel/SubCategory/SubCategory.jsx (javascript):
    🔄 Modified: getExpsOrdByLgCatCd()
  📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx (javascript):
    🔄 Modified: normalizeModalStyle()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/PlayerOverlayContents.jsx (javascript):
    🔄 Modified: SpotlightContainerDecorator(), PlayerOverlayContents()
  📄 com.twin.app.shoptime/src/views/PlayerPanel/PlayerOverlay/MediaOverlayContents.jsx (javascript):
     Added: MediaOverlayContents()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
  • 공통 유틸리티 함수 최적화
This commit is contained in:
2025-11-15 22:03:44 +09:00
parent 70381438ac
commit a503bf923a
17 changed files with 1878 additions and 1492 deletions

View File

@@ -11,6 +11,8 @@ export const SOURCE_MENUS = {
HOME_ROLLING_UNIT: 'home_rolling_unit',
HOME_EVENT_POPUP: 'home_event_popup',
HOME_TODAYS_DEAL: 'home_todays_deal',
SEARCH_RESULT: 'search_result',
HOME_GENERAL: 'home_general',
THEMED_PRODUCT: 'themed_product',
GENERAL_PRODUCT: 'general_product',
};
@@ -43,6 +45,417 @@ export const resetPanels = (panels) => ({
payload: panels,
});
/**
* DetailPanel로 이동하는 공통 액션 함수
* @param {Object} params - 이동 파라미터
* @param {string} params.patnrId - 파트너 ID
* @param {string} params.prdtId - 상품 ID
* @param {string} [params.curationId] - 큐레이션 ID (테마 상품인 경우)
* @param {string} [params.nowShelf] - 현재 셸프 ID
* @param {string} [params.type] - 상품 타입 ('theme' 등)
* @param {string} [params.sourceMenu] - 시작 메뉴 (SOURCE_MENUS 상수 사용)
* @param {Object} [params.additionalInfo] - 추가 정보
* @returns {Function} Redux thunk 함수
*/
export const navigateToDetail = ({
patnrId,
prdtId,
curationId,
nowShelf,
type,
sourceMenu,
additionalInfo = {},
}) => {
return (dispatch, getState) => {
const panelInfo = {
patnrId,
prdtId,
...additionalInfo,
};
// 선택적 파라미터들 추가
if (curationId) panelInfo.curationId = curationId;
if (nowShelf) panelInfo.nowShelf = nowShelf;
if (type) panelInfo.type = type;
if (sourceMenu) panelInfo.sourceMenu = sourceMenu;
// 로깅
console.log(`[navigateToDetail] ${sourceMenu || 'unknown'} → DetailPanel`, {
patnrId,
prdtId,
curationId,
nowShelf,
type,
sourceMenu,
timestamp: Date.now(),
});
// sourceMenu에 따른 사전 처리
switch (sourceMenu) {
case SOURCE_MENUS.HOME_BEST_SELLER:
case SOURCE_MENUS.HOME_PICKED_FOR_YOU:
case SOURCE_MENUS.HOME_SUB_CATEGORY:
case SOURCE_MENUS.HOME_EVENT_POPUP:
case SOURCE_MENUS.HOME_TODAYS_DEAL:
case SOURCE_MENUS.HOME_RANDOM_UNIT:
case SOURCE_MENUS.HOME_ROLLING_UNIT:
case SOURCE_MENUS.HOME_GENERAL: {
// 🔽 모든 HomePanel에서 DetailPanel로 이동 시 HomeBanner modal 비디오 정지
const state = getState();
const playerPanelInfo = state.panels.panels.find(
(p) => p.name === panel_names.PLAYER_PANEL
);
// playerPanel이 없는 경우 비디오 정지 로직 건너뛰기
if (!playerPanelInfo) {
// 비디오가 없어도 HomePanel 상태 저장
dispatch(
updatePanel({
name: panel_names.HOME_PANEL,
panelInfo: {
lastSelectedProduct: { patnrId, prdtId },
lastActionSource: sourceMenu,
...additionalInfo,
},
})
);
panelInfo.fromHome = true;
break;
}
const isCurrentBannerVideoPlaying = playerPanelInfo.panelInfo?.modal !== false;
// HomeBanner의 modal=true 비디오가 재생 중이면 정지
if (isCurrentBannerVideoPlaying) {
// 🔽 비디오 상태 저장 후 정지
const { finishVideoPreview } = require('./playActions');
// 비디오 복원을 위한 상태 저장
const videoStateToRestore = {
...playerPanelInfo.panelInfo,
wasPlaying: true,
restoreOnBack: true,
sourceMenu,
timestamp: Date.now(),
};
// HomePanel에 비디오 복원 상태 저장
dispatch(
updatePanel({
name: panel_names.HOME_PANEL,
panelInfo: {
videoStateToRestore,
lastSelectedProduct: { patnrId, prdtId },
lastActionSource: sourceMenu,
...additionalInfo,
},
})
);
// 비디오 상태 저장 후 정지 (로그는 개발 시 필요 시 주석 해제)
dispatch(finishVideoPreview());
} else {
// 비디오가 재생 중이 아니어도 HomePanel 상태 저장
dispatch(
updatePanel({
name: panel_names.HOME_PANEL,
panelInfo: {
lastSelectedProduct: { patnrId, prdtId },
lastActionSource: sourceMenu,
...additionalInfo,
},
})
);
}
// HomePanel 내부 컴포넌트들: 기본 HomePanel 상태 저장
dispatch(
updatePanel({
name: panel_names.HOME_PANEL,
panelInfo: {
lastSelectedProduct: { patnrId, prdtId },
lastActionSource: sourceMenu,
...additionalInfo,
},
})
);
panelInfo.fromHome = true;
break;
}
case SOURCE_MENUS.SEARCH_RESULT:
// Search: 현재 패널 상태 저장 (updatePanel)
if (additionalInfo.searchVal && additionalInfo.currentSpot) {
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: additionalInfo.searchVal,
currentSpot: additionalInfo.currentSpot,
tab: additionalInfo.tab || 0,
},
})
);
}
panelInfo.fromSearch = true;
panelInfo.searchQuery = additionalInfo.searchVal;
break;
case SOURCE_MENUS.THEMED_PRODUCT:
// 테마 상품: 별도 처리 필요할 경우
break;
case SOURCE_MENUS.GENERAL_PRODUCT:
default:
// 일반 상품: 기본 처리
break;
}
// DetailPanel push
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo,
})
);
};
};
/**
* 테마 상품을 위한 DetailPanel 이동 헬퍼 함수
* @param {Object} params - 이동 파라미터
* @returns {Function} Redux thunk
*/
export const navigateToThemeDetail = ({
patnrId,
prdtId,
curationId,
sourceMenu = SOURCE_MENUS.THEMED_PRODUCT,
...additionalInfo
}) => {
return navigateToDetail({
patnrId,
prdtId,
curationId,
type: 'theme',
sourceMenu,
...additionalInfo,
});
};
/**
* 홈패널 BestSeller에서 DetailPanel로 이동
* @param {Object} params - 상품 정보
* @returns {Function} Redux thunk
*/
export const navigateFromBestSeller = ({ patnrId, prdtId, spotlightId }) => {
return navigateToDetail({
patnrId,
prdtId,
nowShelf: spotlightId,
sourceMenu: SOURCE_MENUS.HOME_BEST_SELLER,
});
};
/**
* 홈패널 PickedForYou에서 DetailPanel로 이동
* @param {Object} params - 상품 정보
* @returns {Function} Redux thunk
*/
export const navigateFromPickedForYou = ({ patnrId, prdtId, spotlightId }) => {
return navigateToDetail({
patnrId,
prdtId,
nowShelf: spotlightId,
sourceMenu: SOURCE_MENUS.HOME_PICKED_FOR_YOU,
});
};
/**
* 홈패널 SubCategory에서 DetailPanel로 이동
* @param {Object} params - 상품 정보
* @returns {Function} Redux thunk
*/
export const navigateFromSubCategory = ({ patnrId, prdtId, spotlightId }) => {
return navigateToDetail({
patnrId,
prdtId,
nowShelf: spotlightId,
sourceMenu: SOURCE_MENUS.HOME_SUB_CATEGORY,
});
};
/**
* 홈패널 RandomUnit 배너에서 DetailPanel로 이동
* @param {Object} params - 상품 정보
* @returns {Function} Redux thunk
*/
export const navigateFromRandomUnit = ({ patnrId, prdtId, curationId, type = 'product' }) => {
return navigateToDetail({
patnrId,
prdtId,
curationId,
type: type === 'theme' ? 'theme' : undefined,
sourceMenu: SOURCE_MENUS.HOME_RANDOM_UNIT,
});
};
/**
* 홈패널 RollingUnit 배너에서 DetailPanel로 이동
* @param {Object} params - 상품 정보
* @returns {Function} Redux thunk
*/
export const navigateFromRollingUnit = ({ patnrId, prdtId, curationId, additionalInfo = {} }) => {
return navigateToDetail({
patnrId,
prdtId,
curationId,
sourceMenu: SOURCE_MENUS.HOME_ROLLING_UNIT,
...additionalInfo,
});
};
/**
* 홈패널 EventPopUpBanner에서 DetailPanel로 이동
* @param {Object} params - 상품 정보
* @returns {Function} Redux thunk
*/
export const navigateFromEventPopup = ({ patnrId, prdtId }) => {
return navigateToDetail({
patnrId,
prdtId,
sourceMenu: SOURCE_MENUS.HOME_EVENT_POPUP,
});
};
/**
* SearchPanel에서 DetailPanel로 이동
* @param {Object} params - 검색 및 상품 정보
* @returns {Function} Redux thunk
*/
export const navigateFromSearch = ({
patnrId,
prdtId,
searchQuery,
currentSpot,
additionalInfo = {},
}) => {
return navigateToDetail({
patnrId,
prdtId,
sourceMenu: SOURCE_MENUS.SEARCH_RESULT,
additionalInfo: {
searchVal: searchQuery,
currentSpot,
tab: 0,
...additionalInfo,
},
});
};
/**
* HomePanel 일반 클릭에서 DetailPanel로 이동
* @param {Object} params - 상품 정보
* @returns {Function} Redux thunk
*/
export const navigateFromHomeGeneral = ({ patnrId, prdtId, additionalInfo = {} }) => {
return navigateToDetail({
patnrId,
prdtId,
sourceMenu: SOURCE_MENUS.HOME_GENERAL,
additionalInfo,
});
};
/**
* DetailPanel에서 돌아올 때 비디오 복원 함수
* HomePanel에 저장된 비디오 상태를 확인하고 복원
* @returns {Function} Redux thunk
*/
export const restoreVideoOnBack = () => {
return (dispatch, getState) => {
const state = getState();
const panels = state.panels.panels;
// HomePanel 찾기
const homePanel = panels.find((p) => p.name === panel_names.HOME_PANEL);
const videoStateToRestore = homePanel?.panelInfo?.videoStateToRestore;
if (!videoStateToRestore || !videoStateToRestore.restoreOnBack) {
return;
}
// 비디오 복원 시작 (로그는 개발 시 필요 시 주석 해제)
// 비디오 상태 복원
const { startVideoPlayerNew } = require('./playActions');
// 복원할 비디오 정보 추출
const restoreInfo = {
bannerId: videoStateToRestore.bannerId || videoStateToRestore.playerState?.currentBannerId,
patnrId: videoStateToRestore.patnrId,
showId: videoStateToRestore.showId,
shptmBanrTpNm: videoStateToRestore.shptmBanrTpNm,
lgCatCd: videoStateToRestore.lgCatCd,
modal: true, // HomeBanner는 항상 modal
modalContainerId: videoStateToRestore.modalContainerId,
modalClassName: videoStateToRestore.modalClassName,
chanId: videoStateToRestore.chanId,
};
// 비디오 재생 시작
dispatch(
startVideoPlayerNew({
...restoreInfo,
spotlightDisable: false,
})
);
// 복원 상태 정리
dispatch(
updatePanel({
name: panel_names.HOME_PANEL,
panelInfo: {
...homePanel.panelInfo,
videoStateToRestore: {
...videoStateToRestore,
restoreOnBack: false, // 복원 완료 후 플래그 초기화
},
},
})
);
};
};
/**
* DetailPanel 닫기 시 비디오 복원 확인 함수
* DetailPanel 패널이 제거될 때 자동으로 비디오 복원 시도
* @returns {Function} Redux thunk
*/
export const handleDetailPanelCloseWithVideoRestore = () => {
return (dispatch, getState) => {
const state = getState();
const panels = state.panels.panels;
// 현재 최상단 패널이 DetailPanel인지 확인
const topPanel = panels[panels.length - 1];
if (topPanel?.name === panel_names.DETAIL_PANEL) {
// 기존 DetailPanel 닫기 로직 수행
dispatch({
type: 'POP_PANEL_WITH_VIDEO_RESTORE',
payload: panel_names.DETAIL_PANEL,
});
// 비디오 복원 시도 (약간의 지연 후)
setTimeout(() => {
dispatch(restoreVideoOnBack());
}, 100);
}
};
};
/**
* [251114] 명시적 포커스 이동
* Panel의 비동기 작업(useEffect, 타이머 등)이 포커스를 탈취하는 것을 방지

View File

@@ -1,60 +1,60 @@
import React from "react";
import ReactDOM from "react-dom";
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from "classnames";
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import onlyUpdateForKeys from "recompose/onlyUpdateForKeys";
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys';
import { off, on } from "@enact/core/dispatcher";
import { forward } from "@enact/core/handle";
import hoc from "@enact/core/hoc";
import ApiDecorator from "@enact/core/internal/ApiDecorator";
import { is } from "@enact/core/keymap";
import kind from "@enact/core/kind";
import { Job } from "@enact/core/util";
import ActionGuide from "@enact/sandstone/ActionGuide";
import Button from "@enact/sandstone/Button";
import $L from "@enact/sandstone/internal/$L";
import { compareChildren } from "@enact/sandstone/internal/util";
import Spotlight from "@enact/spotlight";
import Pause from "@enact/spotlight/Pause";
import { off, on } from '@enact/core/dispatcher';
import { forward } from '@enact/core/handle';
import hoc from '@enact/core/hoc';
import ApiDecorator from '@enact/core/internal/ApiDecorator';
import { is } from '@enact/core/keymap';
import kind from '@enact/core/kind';
import { Job } from '@enact/core/util';
import ActionGuide from '@enact/sandstone/ActionGuide';
import Button from '@enact/sandstone/Button';
import $L from '@enact/sandstone/internal/$L';
import { compareChildren } from '@enact/sandstone/internal/util';
import Spotlight from '@enact/spotlight';
import Pause from '@enact/spotlight/Pause';
import {
SpotlightContainerDecorator,
spotlightDefaultClass,
} from "@enact/spotlight/SpotlightContainerDecorator";
import Cancelable from "@enact/ui/Cancelable";
import Slottable from "@enact/ui/Slottable";
} from '@enact/spotlight/SpotlightContainerDecorator';
import Cancelable from '@enact/ui/Cancelable';
import Slottable from '@enact/ui/Slottable';
import ShopTimePlayIcon from "../../../assets/images/btn/btn-video-play-nor@3x.png";
import ShopTimePauseIcon from "../../../assets/images/btn/btn-voc-pause-nor@3x.png";
import { SpotlightIds } from "../../utils/SpotlightIds";
import css from "./MediaControls.module.less";
import { countReactChildren } from "./util";
import ShopTimePlayIcon from '../../../assets/images/btn/btn-video-play-nor@3x.png';
import ShopTimePauseIcon from '../../../assets/images/btn/btn-voc-pause-nor@3x.png';
import { SpotlightIds } from '../../utils/SpotlightIds';
import css from './MediaControls.module.less';
import { countReactChildren } from './util';
const OuterContainer = SpotlightContainerDecorator(
{
defaultElement: [`.${spotlightDefaultClass}`],
},
"div"
'div'
);
const Container = SpotlightContainerDecorator(
{
enterTo: "default-element",
enterTo: 'default-element',
},
"div"
'div'
);
const MediaButton = onlyUpdateForKeys([
"children",
"className",
"disabled",
"icon",
"onClick",
"spotlightDisabled",
"onSpotlightRight",
'children',
'className',
'disabled',
'icon',
'onClick',
'spotlightDisabled',
'onSpotlightRight',
])(Button);
const forwardToggleMore = forward("onToggleMore");
const forwardToggleMore = forward('onToggleMore');
const animationDuration = 300;
@@ -67,7 +67,7 @@ const animationDuration = 300;
* @private
*/
const MediaControlsBase = kind({
name: "MediaControls",
name: 'MediaControls',
// intentionally assigning these props to MediaControls instead of Base (which is private)
propTypes: /** @lends sandstone/MediaPlayer.MediaControls.prototype */ {
@@ -286,18 +286,18 @@ const MediaControlsBase = kind({
},
defaultProps: {
jumpBackwardIcon: "jumpbackward",
jumpForwardIcon: "jumpforward",
moreComponentsSpotlightId: "moreComponents",
spotlightId: "mediaControls",
pauseIcon: "pause",
playIcon: "play",
jumpBackwardIcon: 'jumpbackward',
jumpForwardIcon: 'jumpforward',
moreComponentsSpotlightId: 'moreComponents',
spotlightId: 'mediaControls',
pauseIcon: 'pause',
playIcon: 'play',
visible: true,
},
styles: {
css,
className: "controlsFrame",
className: 'controlsFrame',
},
computed: {
@@ -306,8 +306,7 @@ const MediaControlsBase = kind({
actionGuideShowing: ({ bottomComponents, children }) =>
countReactChildren(children) || bottomComponents,
className: ({ visible, styler }) => styler.append({ hidden: !visible }),
moreButtonsClassName: ({ styler }) =>
styler.join("mediaControls", "moreButtonsComponents"),
moreButtonsClassName: ({ styler }) => styler.join('mediaControls', 'moreButtonsComponents'),
moreComponentsRendered: ({ showMoreComponents, moreComponentsRendered }) =>
showMoreComponents || moreComponentsRendered,
},
@@ -353,8 +352,8 @@ const MediaControlsBase = kind({
e.preventDefault();
e.stopPropagation();
if (videoVerticalVisible && countryCode !== "US") {
Spotlight.focus("tab-0");
if (videoVerticalVisible && countryCode !== 'US') {
Spotlight.focus('tab-0');
}
Spotlight.focus(SpotlightIds.PLAYER_SUBTITLE_BUTTON);
};
@@ -367,19 +366,16 @@ const MediaControlsBase = kind({
return (
<OuterContainer {...rest} id={id} spotlightId={spotlightId}>
{type !== "LIVE" && (
{type !== 'LIVE' && (
<Container
className={classNames(
css.mediaControls,
videoVerticalVisible && css.videoVertical
)}
className={classNames(css.mediaControls, videoVerticalVisible && css.videoVertical)}
spotlightDisabled={spotlightDisabled}
onKeyDown={onKeyDownFromMediaButtons}
>
{/* {noJumpButtons ? null : <MediaButton aria-label={$L('Previous')} backgroundOpacity="transparent" css={css} disabled={mediaDisabled || jumpBackwardDisabled} icon={jumpBackwardIcon} onClick={onJumpBackwardButtonClick} size="large" spotlightDisabled={spotlightDisabled} />} */}
<MediaButton
aria-label={paused ? $L("Play") : $L("Pause")}
aria-label={paused ? $L('Play') : $L('Pause')}
className={spotlightDefaultClass}
backgroundOpacity="transparent"
css={css}
@@ -388,6 +384,7 @@ const MediaControlsBase = kind({
onClick={onPlayButtonClick}
size="large"
spotlightDisabled={spotlightDisabled}
spotlightId={SpotlightIds.PLAYER_PLAY_BUTTON}
onSpotlightRight={onSpotlightRight}
onSpotlightUp={onSpotlightUp}
/>
@@ -398,9 +395,7 @@ const MediaControlsBase = kind({
{actionGuideShowing ? (
<ActionGuide
id={`${id}_actionGuide`}
aria-label={
actionGuideAriaLabel != null ? actionGuideAriaLabel : null
}
aria-label={actionGuideAriaLabel != null ? actionGuideAriaLabel : null}
css={css}
className={actionGuideClassName}
icon="arrowsmalldown"
@@ -438,165 +433,164 @@ const MediaControlsBase = kind({
const MediaControlsDecorator = hoc((config, Wrapped) => {
// eslint-disable-line no-unused-vars
class MediaControlsDecoratorHOC extends React.Component {
static displayName = "MediaControlsDecorator";
static displayName = 'MediaControlsDecorator';
static propTypes =
/** @lends sandstone/MediaPlayer.MediaControlsDecorator.prototype */ {
/**
* The label for the action guide.
*
* @type {String}
* @public
*/
actionGuideLabel: PropTypes.string,
static propTypes = /** @lends sandstone/MediaPlayer.MediaControlsDecorator.prototype */ {
/**
* The label for the action guide.
*
* @type {String}
* @public
*/
actionGuideLabel: PropTypes.string,
/**
* These components are placed below the children. Typically these will be media playlist items.
*
* @type {Node}
* @public
*/
bottomComponents: PropTypes.node,
/**
* These components are placed below the children. Typically these will be media playlist items.
*
* @type {Node}
* @public
*/
bottomComponents: PropTypes.node,
/**
* The number of milliseconds that the player will pause before firing the
* first jump event on a right or left pulse.
*
* @type {Number}
* @default 400
* @public
*/
initialJumpDelay: PropTypes.number,
/**
* The number of milliseconds that the player will pause before firing the
* first jump event on a right or left pulse.
*
* @type {Number}
* @default 400
* @public
*/
initialJumpDelay: PropTypes.number,
/**
* The number of milliseconds that the player will throttle before firing a
* jump event on a right or left pulse.
*
* @type {Number}
* @default 200
* @public
*/
jumpDelay: PropTypes.number,
/**
* The number of milliseconds that the player will throttle before firing a
* jump event on a right or left pulse.
*
* @type {Number}
* @default 200
* @public
*/
jumpDelay: PropTypes.number,
/**
* Disables the media buttons.
*
* @type {Boolean}
* @public
*/
mediaDisabled: PropTypes.bool,
/**
* Disables the media buttons.
*
* @type {Boolean}
* @public
*/
mediaDisabled: PropTypes.bool,
/**
* Disables showing more components.
*
* @type {Boolean}
* @public
*/
moreActionDisabled: PropTypes.bool,
/**
* Disables showing more components.
*
* @type {Boolean}
* @public
*/
moreActionDisabled: PropTypes.bool,
/**
* Setting this to `true` will disable left and right keys for seeking.
*
* @type {Boolean}
* @public
*/
no5WayJump: PropTypes.bool,
/**
* Setting this to `true` will disable left and right keys for seeking.
*
* @type {Boolean}
* @public
*/
no5WayJump: PropTypes.bool,
/**
* Called when media fast forwards.
*
* @type {Function}
* @public
*/
onFastForward: PropTypes.func,
/**
* Called when media fast forwards.
*
* @type {Function}
* @public
*/
onFastForward: PropTypes.func,
/**
* Called when media jumps.
*
* @type {Function}
* @public
*/
onJump: PropTypes.func,
/**
* Called when media jumps.
*
* @type {Function}
* @public
*/
onJump: PropTypes.func,
/**
* Called when media gets paused.
*
* @type {Function}
* @public
*/
onPause: PropTypes.func,
/**
* Called when media gets paused.
*
* @type {Function}
* @public
*/
onPause: PropTypes.func,
/**
* Called when media starts playing.
*
* @type {Function}
* @public
*/
onPlay: PropTypes.func,
/**
* Called when media starts playing.
*
* @type {Function}
* @public
*/
onPlay: PropTypes.func,
/**
* Called when media rewinds.
*
* @type {Function}
* @public
*/
onRewind: PropTypes.func,
/**
* Called when media rewinds.
*
* @type {Function}
* @public
*/
onRewind: PropTypes.func,
/**
* Called when the visibility of more components is changed
*
* Event payload includes:
*
* * `type` - Type of event, `'onToggleMore'`
* * `showMoreComponents` - `true` when the components are visible`
* * `liftDistance` - The distance, in pixels, the component animates
*
* @type {Function}
* @public
*/
onToggleMore: PropTypes.func,
/**
* Called when the visibility of more components is changed
*
* Event payload includes:
*
* * `type` - Type of event, `'onToggleMore'`
* * `showMoreComponents` - `true` when the components are visible`
* * `liftDistance` - The distance, in pixels, the component animates
*
* @type {Function}
* @public
*/
onToggleMore: PropTypes.func,
/**
* The video pause state.
*
* @type {Boolean}
* @public
*/
paused: PropTypes.bool,
/**
* The video pause state.
*
* @type {Boolean}
* @public
*/
paused: PropTypes.bool,
/**
* Disables state on the media "play"/"pause" button
*
* @type {Boolean}
* @public
*/
playPauseButtonDisabled: PropTypes.bool,
/**
* Disables state on the media "play"/"pause" button
*
* @type {Boolean}
* @public
*/
playPauseButtonDisabled: PropTypes.bool,
/**
* Disables the media playback-rate control via rewind and fast forward keys
*
* @type {Boolean}
* @public
*/
rateChangeDisabled: PropTypes.bool,
/**
* Disables the media playback-rate control via rewind and fast forward keys
*
* @type {Boolean}
* @public
*/
rateChangeDisabled: PropTypes.bool,
/**
* Registers the MediaControls component with an
* [ApiDecorator]{@link core/internal/ApiDecorator.ApiDecorator}.
*
* @type {Function}
* @private
*/
setApiProvider: PropTypes.func,
/**
* Registers the MediaControls component with an
* [ApiDecorator]{@link core/internal/ApiDecorator.ApiDecorator}.
*
* @type {Function}
* @private
*/
setApiProvider: PropTypes.func,
/**
* The visibility of the component. When `false`, the component will be hidden.
*
* @type {Boolean}
* @public
*/
visible: PropTypes.bool,
};
/**
* The visibility of the component. When `false`, the component will be hidden.
*
* @type {Boolean}
* @public
*/
visible: PropTypes.bool,
};
static defaultProps = {
initialJumpDelay: 400,
@@ -614,7 +608,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
this.keyLoop = null;
this.pulsingKeyCode = null;
this.pulsing = null;
this.paused = new Pause("MediaPlayer");
this.paused = new Pause('MediaPlayer');
this.state = {
showMoreComponents: false,
@@ -636,10 +630,10 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
}
componentDidMount() {
on("keydown", this.handleKeyDown);
on("keyup", this.handleKeyUp);
on("blur", this.handleBlur, window);
on("wheel", this.handleWheel);
on('keydown', this.handleKeyDown);
on('keyup', this.handleKeyUp);
on('blur', this.handleBlur, window);
on('wheel', this.handleWheel);
}
componentDidUpdate(prevProps, prevState) {
@@ -650,16 +644,12 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
!this.state.moreComponentsRendered
) {
this.moreComponentsRenderingJob.startRafAfter();
} else if (
prevState.showMoreComponents &&
!this.state.showMoreComponents
) {
} else if (prevState.showMoreComponents && !this.state.showMoreComponents) {
this.moreComponentsRenderingJob.stop();
}
if (
(!prevState.moreComponentsRendered &&
this.state.moreComponentsRendered) ||
(!prevState.moreComponentsRendered && this.state.moreComponentsRendered) ||
(this.state.moreComponentsRendered &&
prevProps.bottomComponents !== this.props.bottomComponents) ||
!compareChildren(this.props.children, prevProps.children)
@@ -676,7 +666,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
) {
forwardToggleMore(
{
type: "onToggleMore",
type: 'onToggleMore',
showMoreComponents: this.state.showMoreComponents,
liftDistance: this.bottomComponentsHeight - this.actionGuideHeight,
},
@@ -690,7 +680,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
this.paused.pause();
this.animation = this.moreComponentsNode.animate(
[
{ transform: "none", opacity: 0, offset: 0 },
{ transform: 'none', opacity: 0, offset: 0 },
{
transform: `translateY(${-this.actionGuideHeight}px)`,
opacity: 1,
@@ -699,7 +689,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
],
{
duration: animationDuration,
fill: "forwards",
fill: 'forwards',
}
);
this.animation.onfinish = this.handleFinish;
@@ -717,10 +707,10 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
}
componentWillUnmount() {
off("keydown", this.handleKeyDown);
off("keyup", this.handleKeyUp);
off("blur", this.handleBlur, window);
off("wheel", this.handleWheel);
off('keydown', this.handleKeyDown);
off('keyup', this.handleKeyUp);
off('blur', this.handleBlur, window);
off('wheel', this.handleWheel);
this.stopListeningForPulses();
this.moreComponentsRenderingJob.stop();
if (this.animation) {
@@ -740,17 +730,13 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
return;
}
const bottomElement = this.mediaControlsNode.querySelector(
`.${css.moreComponents}`
);
this.bottomComponentsHeight = bottomElement
? bottomElement.scrollHeight
: 0;
const bottomElement = this.mediaControlsNode.querySelector(`.${css.moreComponents}`);
this.bottomComponentsHeight = bottomElement ? bottomElement.scrollHeight : 0;
};
handleKeyDownFromMediaButtons = (ev) => {
if (
is("down", ev.keyCode) &&
is('down', ev.keyCode) &&
!this.state.showMoreComponents &&
!this.props.moreActionDisabled
) {
@@ -769,7 +755,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
!visible &&
!mediaDisabled &&
!current &&
(is("left", ev.keyCode) || is("right", ev.keyCode))
(is('left', ev.keyCode) || is('right', ev.keyCode))
) {
this.paused.pause();
this.startListeningForPulses(ev.keyCode);
@@ -777,33 +763,28 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
};
handleKeyUp = (ev) => {
const {
mediaDisabled,
no5WayJump,
rateChangeDisabled,
playPauseButtonDisabled,
} = this.props;
const { mediaDisabled, no5WayJump, rateChangeDisabled, playPauseButtonDisabled } = this.props;
if (mediaDisabled) return;
if (!playPauseButtonDisabled) {
if (is("play", ev.keyCode)) {
forward("onPlay", ev, this.props);
} else if (is("pause", ev.keyCode)) {
forward("onPause", ev, this.props);
if (is('play', ev.keyCode)) {
forward('onPlay', ev, this.props);
} else if (is('pause', ev.keyCode)) {
forward('onPause', ev, this.props);
}
}
if (!no5WayJump && (is("left", ev.keyCode) || is("right", ev.keyCode))) {
if (!no5WayJump && (is('left', ev.keyCode) || is('right', ev.keyCode))) {
this.stopListeningForPulses();
this.paused.resume();
}
if (!rateChangeDisabled) {
if (is("rewind", ev.keyCode)) {
forward("onRewind", ev, this.props);
} else if (is("fastForward", ev.keyCode)) {
forward("onFastForward", ev, this.props);
if (is('rewind', ev.keyCode)) {
forward('onRewind', ev, this.props);
} else if (is('fastForward', ev.keyCode)) {
forward('onFastForward', ev, this.props);
}
}
};
@@ -832,25 +813,22 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
if (!this.pulsing) {
this.pulsingKeyCode = keyCode;
this.pulsing = true;
this.keyLoop = setTimeout(
this.handlePulse,
this.props.initialJumpDelay
);
forward("onJump", { keyCode }, this.props);
this.keyLoop = setTimeout(this.handlePulse, this.props.initialJumpDelay);
forward('onJump', { keyCode }, this.props);
}
};
handlePulse = () => {
forward("onJump", { keyCode: this.pulsingKeyCode }, this.props);
forward('onJump', { keyCode: this.pulsingKeyCode }, this.props);
this.keyLoop = setTimeout(this.handlePulse, this.props.jumpDelay);
};
handlePlayButtonClick = (ev) => {
forward("onPlayButtonClick", ev, this.props);
forward('onPlayButtonClick', ev, this.props);
if (this.props.paused) {
forward("onPlay", ev, this.props);
forward('onPlay', ev, this.props);
} else {
forward("onPause", ev, this.props);
forward('onPause', ev, this.props);
}
};
@@ -869,9 +847,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
}
this.mediaControlsNode = ReactDOM.findDOMNode(node); // eslint-disable-line react/no-find-dom-node
const guideElement = this.mediaControlsNode.querySelector(
`.${css.actionGuide}`
);
const guideElement = this.mediaControlsNode.querySelector(`.${css.actionGuide}`);
this.actionGuideHeight = guideElement ? guideElement.scrollHeight : 0;
};
@@ -897,7 +873,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
handleClose = (ev) => {
if (this.props.visible) {
forward("onClose", ev, this.props);
forward('onClose', ev, this.props);
}
};
@@ -905,7 +881,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
if (this.state.showMoreComponents) {
this.paused.resume();
if (!Spotlight.getPointerMode()) {
Spotlight.move("down");
Spotlight.move('down');
}
}
};
@@ -947,7 +923,7 @@ const MediaControlsDecorator = hoc((config, Wrapped) => {
}
}
return Slottable({ slots: ["bottomComponents"] }, MediaControlsDecoratorHOC);
return Slottable({ slots: ['bottomComponents'] }, MediaControlsDecoratorHOC);
});
const handleCancel = (ev, { onClose }) => {
@@ -972,18 +948,12 @@ const handleCancel = (ev, { onClose }) => {
*/
const MediaControls = ApiDecorator(
{
api: [
"areMoreComponentsAvailable",
"showMoreComponents",
"hideMoreComponents",
],
api: ['areMoreComponentsAvailable', 'showMoreComponents', 'hideMoreComponents'],
},
MediaControlsDecorator(
Cancelable({ modal: true, onCancel: handleCancel }, MediaControlsBase)
)
MediaControlsDecorator(Cancelable({ modal: true, onCancel: handleCancel }, MediaControlsBase))
);
MediaControls.defaultSlot = "mediaControlsComponent";
MediaControls.defaultSlot = 'mediaControlsComponent';
export default MediaControls;
export { MediaControls, MediaControlsBase, MediaControlsDecorator };

View File

@@ -588,6 +588,8 @@
.overlay {
.position(@position: absolute, @top: 0, @right: 0, @bottom: 0, @left: 0);
pointer-events: auto;
z-index: 10;
}
@keyframes spin {
0% {
@@ -714,6 +716,7 @@
.controlsHandleAbove {
pointer-events: none;
z-index: -1;
.position(@position: absolute, @top: 0, @right: 0, @bottom: auto, @left: 0);
}

View File

@@ -9,7 +9,15 @@
* - 메모리 효율성 우선
*/
import React, { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';
import React, {
useState,
useRef,
useEffect,
useCallback,
useMemo,
forwardRef,
useImperativeHandle,
} from 'react';
import classNames from 'classnames';
import DurationFmt from 'ilib/lib/DurationFmt';
import PropTypes from 'prop-types';
@@ -33,7 +41,7 @@ import {
setMediaControlToggle,
startMediaAutoClose,
stopMediaAutoClose,
resetMediaAutoClose
resetMediaAutoClose,
} from '../../actions/mediaOverlayActions';
import css from './MediaPlayer.module.less';
@@ -49,11 +57,12 @@ const RootContainer = SpotlightContainerDecorator(
// DurationFmt memoization
const memoGetDurFmt = memoize(
() => new DurationFmt({
length: 'medium',
style: 'clock',
useNative: false,
})
() =>
new DurationFmt({
length: 'medium',
style: 'clock',
useNative: false,
})
);
const getDurFmt = () => {
@@ -68,7 +77,7 @@ const getDurFmt = () => {
format: (time) => {
if (!time || !time.millisecond) return '00:00';
return secondsToTime(Math.floor(time.millisecond / 1000));
}
},
};
}
};
@@ -159,7 +168,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
try {
// URL 파싱 시도
const url = new URL(src);
return ['youtube.com', 'youtu.be', 'm.youtube.com'].some(domain =>
return ['youtube.com', 'youtu.be', 'm.youtube.com'].some((domain) =>
url.hostname.includes(domain)
);
} catch {
@@ -231,65 +240,75 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
}
}, [ActualVideoComponent]);
const handleUpdate = useCallback((ev) => {
const el = videoRef.current;
if (!el) return;
const handleUpdate = useCallback(
(ev) => {
const el = videoRef.current;
if (!el) return;
const newCurrentTime = el.currentTime || 0;
const newDuration = el.duration || 0;
const newCurrentTime = el.currentTime || 0;
const newDuration = el.duration || 0;
// 상태 업데이트
setCurrentTime(newCurrentTime);
setDuration(newDuration);
setPaused(el.paused);
setLoading(el.loading || false);
setError(el.error || null);
// 상태 업데이트
setCurrentTime(newCurrentTime);
setDuration(newDuration);
setPaused(el.paused);
setLoading(el.loading || false);
setError(el.error || null);
// 함수형 업데이트로 stale closure 방지
setSourceUnavailable((prevUnavailable) =>
(el.loading && prevUnavailable) || el.error
);
// 함수형 업데이트로 stale closure 방지
setSourceUnavailable((prevUnavailable) => (el.loading && prevUnavailable) || el.error);
// Proportion 계산
updateProportionLoaded(); // 플랫폼별 계산 함수 호출
setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0);
// Proportion 계산
updateProportionLoaded(); // 플랫폼별 계산 함수 호출
setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0);
// 콜백 호출
if (ev.type === 'timeupdate' && onTimeUpdate) {
onTimeUpdate(ev);
}
if (ev.type === 'loadeddata' && onLoadedData) {
onLoadedData(ev);
}
if (ev.type === 'loadedmetadata' && onLoadedMetadata) {
onLoadedMetadata(ev);
}
if (ev.type === 'durationchange' && onDurationChange) {
onDurationChange(ev);
}
}, [onTimeUpdate, onLoadedData, onLoadedMetadata, onDurationChange, updateProportionLoaded]);
// 콜백 호출
if (ev.type === 'timeupdate' && onTimeUpdate) {
onTimeUpdate(ev);
}
if (ev.type === 'loadeddata' && onLoadedData) {
onLoadedData(ev);
}
if (ev.type === 'loadedmetadata' && onLoadedMetadata) {
onLoadedMetadata(ev);
}
if (ev.type === 'durationchange' && onDurationChange) {
onDurationChange(ev);
}
},
[onTimeUpdate, onLoadedData, onLoadedMetadata, onDurationChange, updateProportionLoaded]
);
const handleEnded = useCallback((e) => {
if (onEnded) {
onEnded(e);
}
}, [onEnded]);
const handleEnded = useCallback(
(e) => {
if (onEnded) {
onEnded(e);
}
},
[onEnded]
);
const handleErrorEvent = useCallback((e) => {
setError(e);
if (onError) {
onError(e);
}
}, [onError]);
const handleErrorEvent = useCallback(
(e) => {
setError(e);
if (onError) {
onError(e);
}
},
[onError]
);
// ========== Controls Management ==========
const showControls = useCallback((timeout = 3000) => {
if (disabled || isModal) return;
const showControls = useCallback(
(timeout = 3000) => {
if (disabled || isModal) return;
console.log('🎬 [MediaPlayer.v2] showControls called, dispatching setMediaControlShow');
dispatch(setMediaControlShow());
dispatch(startMediaAutoClose(timeout));
}, [disabled, isModal, dispatch]);
console.log('🎬 [MediaPlayer.v2] showControls called, dispatching setMediaControlShow');
dispatch(setMediaControlShow());
dispatch(startMediaAutoClose(timeout));
},
[disabled, isModal, dispatch]
);
const hideControls = useCallback(() => {
console.log('🎬 [MediaPlayer.v2] hideControls called, dispatching setMediaControlHide');
@@ -351,10 +370,13 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
}, [currentTime, duration, paused, loading, error, proportionLoaded]);
// ========== Slider Event Handlers ==========
const handleSliderChange = useCallback(({ value }) => {
const time = value * duration;
seek(time);
}, [duration, seek]);
const handleSliderChange = useCallback(
({ value }) => {
const time = value * duration;
seek(time);
},
[duration, seek]
);
const handleKnobMove = useCallback((ev) => {
if (!videoRef.current) return;
@@ -459,17 +481,21 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
}, [dispatch]);
// ========== Imperative Handle (API) ==========
useImperativeHandle(ref, () => ({
play,
pause,
seek,
getMediaState,
showControls,
hideControls,
toggleControls,
areControlsVisible: () => controlsVisible,
getVideoNode: () => videoRef.current,
}), [play, pause, seek, getMediaState, showControls, hideControls, toggleControls, controlsVisible]);
useImperativeHandle(
ref,
() => ({
play,
pause,
seek,
getMediaState,
showControls,
hideControls,
toggleControls,
areControlsVisible: () => controlsVisible,
getVideoNode: () => videoRef.current,
}),
[play, pause, seek, getMediaState, showControls, hideControls, toggleControls, controlsVisible]
);
// ========== setApiProvider 호출 ==========
useEffect(() => {
@@ -486,7 +512,16 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
getVideoNode: () => videoRef.current,
});
}
}, [setApiProvider, play, pause, seek, getMediaState, showControls, hideControls, toggleControls]);
}, [
setApiProvider,
play,
pause,
seek,
getMediaState,
showControls,
hideControls,
toggleControls,
]);
// ========== Video Props ==========
const videoProps = useMemo(() => {
@@ -535,7 +570,19 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
...baseProps,
ref: assignVideoNode,
};
}, [ActualVideoComponent, assignVideoNode, src, paused, loop, muted, handleLoadStart, handleUpdate, handleEnded, handleErrorEvent, reactPlayerConfig]);
}, [
ActualVideoComponent,
assignVideoNode,
src,
paused,
loop,
muted,
handleLoadStart,
handleUpdate,
handleEnded,
handleErrorEvent,
reactPlayerConfig,
]);
// ========== Spotlight Handler ==========
const handleSpotlightFocus = useCallback(() => {
@@ -564,18 +611,13 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
>
{/* Video Element */}
{ActualVideoComponent === Media ? (
<ActualVideoComponent {...videoProps}>
{children}
</ActualVideoComponent>
<ActualVideoComponent {...videoProps}>{children}</ActualVideoComponent>
) : (
<ActualVideoComponent {...videoProps} />
)}
{/* Overlay */}
<Overlay
bottomControlsVisible={controlsVisible}
onClick={handleVideoClick}
>
<Overlay bottomControlsVisible={controlsVisible} onClick={handleVideoClick}>
{/* Loading + Thumbnail */}
{loading && thumbnailUrl && (
<>
@@ -594,12 +636,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
{/* Slider Section */}
<div className={css.sliderContainer}>
{/* Times - Total */}
<Times
className={css.times}
noCurrentTime
total={duration}
formatter={getDurFmt()}
/>
<Times className={css.times} noCurrentTime total={duration} formatter={getDurFmt()} />
{/* Times - Current */}
<Times
@@ -663,7 +700,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
onSpotlightRight={handleSpotlightFocus}
onSpotlightLeft={handleSpotlightFocus}
onClick={handleSpotlightFocus}
spotlightDisabled={controlsVisible || shouldDisableSpotlight}
spotlightDisabled={controlsVisible || shouldDisableSpotlight || !isModal}
/>
</RootContainer>
);

View File

@@ -724,6 +724,7 @@ const VideoPlayerBase = class extends React.Component {
belowContentsVisible: PropTypes.bool,
tabContainerVersion: PropTypes.number,
tabIndexV2: PropTypes.number,
overlayContentsComponent: PropTypes.elementType,
dispatch: PropTypes.func,
};
@@ -1121,6 +1122,8 @@ const VideoPlayerBase = class extends React.Component {
return;
}
Spotlight.setPointerMode(false);
// this.startDelayedFeedbackHide();
// this.startDelayedTitleHide();
@@ -1426,6 +1429,15 @@ const VideoPlayerBase = class extends React.Component {
this.showControlsFromPointer
);
onVideoClickCapture = (ev) => {
if (!this.state.mediaControlsVisible && !this.props.panelInfo?.modal) {
this.activityDetected();
this.onVideoClick();
ev.stopPropagation();
ev.preventDefault();
}
};
handleControlsHandleAboveHoldPulse = () => {
if (shouldJump(this.props, this.state)) {
this.handleJump({
@@ -1997,6 +2009,11 @@ const VideoPlayerBase = class extends React.Component {
// Player Interaction events
//
onVideoClick = () => {
console.log('[VideoPlayer] onVideoClick', {
controlsVisible: this.state.mediaControlsVisible,
panelInfoModal: this.props.panelInfo?.modal,
pointerMode: Spotlight.getPointerMode(),
});
// tabContainerVersion === 2일 때 belowContentsVisible도 함께 토글
if (this.props.tabContainerVersion === 2 && this.props.setBelowContentsVisible) {
const willShowControls = !this.state.mediaControlsVisible;
@@ -2012,7 +2029,18 @@ const VideoPlayerBase = class extends React.Component {
return;
}
const willShowControls = !this.state.mediaControlsVisible;
this.toggleControls();
if (willShowControls && !this.props.panelInfo?.modal) {
this.restoreOverlayFocus();
}
};
restoreOverlayFocus = () => {
setTimeout(() => {
Spotlight.setPointerMode(false);
Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
});
};
onSliderChange = ({ value }) => {
@@ -2216,6 +2244,7 @@ const VideoPlayerBase = class extends React.Component {
setSideContentsVisible,
belowContentsVisible,
tabContainerVersion,
overlayContentsComponent,
disclaimer,
liveTotalTime,
currentLiveTimeSeconds,
@@ -2264,6 +2293,7 @@ const VideoPlayerBase = class extends React.Component {
delete mediaProps.belowContentsVisible;
delete mediaProps.tabContainerVersion;
delete mediaProps.tabIndexV2;
delete mediaProps.overlayContentsComponent;
mediaProps.autoPlay = !noAutoPlay;
mediaProps.className = type !== 'MEDIA' ? css.video : css.media;
@@ -2272,6 +2302,9 @@ const VideoPlayerBase = class extends React.Component {
mediaProps.onLoadStart = this.handleLoadStart;
mediaProps.onUpdate = this.handleEvent;
mediaProps.ref = this.setVideoRef;
if (!panelInfo.modal) {
mediaProps.tabIndex = -1;
}
//yhcho ReactPlayer
if ((typeof window === 'object' && !window.PalmSystem) || isYoutube) {
@@ -2287,6 +2320,7 @@ const VideoPlayerBase = class extends React.Component {
}
const controlsAriaProps = this.getControlsAriaProps();
const OverlayContents = overlayContentsComponent || PlayerOverlayContents;
let proportionSelection = selection;
if (proportionSelection != null && this.state.duration) {
@@ -2356,6 +2390,7 @@ const VideoPlayerBase = class extends React.Component {
modalClassName
)}
onClick={this.activityDetected}
onClickCapture={this.onVideoClickCapture}
ref={this.setPlayerRef}
spotlightDisabled={spotlightDisabled}
spotlightId={spotlightId}
@@ -2439,7 +2474,7 @@ const VideoPlayerBase = class extends React.Component {
css.controlFeedbackBtnLayer + (this.state.infoVisible ? ' ' + css.lift : '')
}
>
<PlayerOverlayContents
<OverlayContents
playListInfo={playListInfo && playListInfo}
selectedIndex={selectedIndex}
onClick={onBackButton}
@@ -2571,7 +2606,7 @@ const VideoPlayerBase = class extends React.Component {
onClick={this.onSpotlightFocus}
selectionKeys={controlsHandleAboveSelectionKeys}
spotlightDisabled={
this.state.mediaControlsVisible || spotlightDisabled || panelInfo.modal
this.state.mediaControlsVisible || spotlightDisabled || !panelInfo.modal
}
/>
<Announce ref={this.setAnnounceRef} />

View File

@@ -596,6 +596,8 @@
.overlay {
.position(@position: absolute, @top: 0, @right: 0, @bottom: 0, @left: 0);
pointer-events: auto;
z-index: 10;
}
@keyframes spin {
0% {
@@ -722,6 +724,7 @@
.controlsHandleAbove {
pointer-events: none;
z-index: -1;
.position(@position: absolute, @top: 0, @right: 0, @bottom: auto, @left: 0);
}

View File

@@ -42,6 +42,7 @@ export const SpotlightIds = {
PLAYER_TAB_BUTTON: 'playerTabArrow',
PLAYER_BACK_BUTTON: 'player-back-button',
PLAYER_SUBTITLE_BUTTON: 'player-subtitlebutton',
PLAYER_PLAY_BUTTON: 'player-play-button',
// searchPanel
SEARCH_THEME: 'search_theme',

View File

@@ -1,28 +1,13 @@
import React, {
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Spotlight from '@enact/spotlight';
import {
SpotlightContainerDecorator,
} from '@enact/spotlight/SpotlightContainerDecorator';
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import {
pushPanel,
updatePanel,
} from '../../../actions/panelActions';
import {
navigateToDetailFromHome,
} from '../../../actions/panelNavigationActions';
import { pushPanel, updatePanel, navigateFromBestSeller } from '../../../actions/panelActions';
import { navigateToDetailFromHome } from '../../../actions/panelNavigationActions';
import SectionTitle from '../../../components/SectionTitle/SectionTitle';
import Tag from '../../../components/TItemCard/Tag';
import TItemCard from '../../../components/TItemCard/TItemCard';
@@ -30,23 +15,13 @@ import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller';
import useScrollReset from '../../../hooks/useScrollReset';
import useScrollTo from '../../../hooks/useScrollTo';
import {
LOG_CONTEXT_NAME,
LOG_MESSAGE_ID,
panel_names,
} from '../../../utils/Config';
import {
$L,
scaleW,
} from '../../../utils/helperMethods';
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config';
import { $L, scaleW } from '../../../utils/helperMethods';
import { SpotlightIds } from '../../../utils/SpotlightIds';
import css from './BestSeller.module.less';
const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
const BestSeller = ({
order,
@@ -58,25 +33,15 @@ const BestSeller = ({
shelfTitle,
}) => {
const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset(
scrollLeft,
true
);
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, true);
const dispatch = useDispatch();
const { cursorVisible } = useSelector((state) => state.common.appStatus);
const bestSellerDatas = useSelector(
(state) => state.product.bestSellerData?.bestSeller
);
const bestSellerNewDatas = useSelector(
(state) =>
state.foryou?.recommendInfo?.recommendProduct
);
const bestSellerDatas = useSelector((state) => state.product.bestSellerData?.bestSeller);
const bestSellerNewDatas = useSelector((state) => state.foryou?.recommendInfo?.recommendProduct);
const [drawChk, setDrawChk] = useState(false);
const [firstChk, setFirstChk] = useState(0);
@@ -84,35 +49,36 @@ const BestSeller = ({
const [bestInfos, setBestInfos] = useState(null);
const [bestItemNewData, setBestItemNewData] = useState([]);
useEffect(()=>{
useEffect(() => {
setBestInfos(
bestSellerNewDatas?.filter(
(item) => item.recommendTpCd === "BESTSELLER"
) || [] // 기본값으로 빈 배열 설정
)
},[bestSellerNewDatas])
bestSellerNewDatas?.filter((item) => item.recommendTpCd === 'BESTSELLER') || [] // 기본값으로 빈 배열 설정
);
}, [bestSellerNewDatas]);
useEffect(() => {
if (!bestInfos || bestInfos.length === 0) {
const baseData = bestSellerDatas?.map((item) => ({
...item,
foryou: false,
})) || [];
const baseData =
bestSellerDatas?.map((item) => ({
...item,
foryou: false,
})) || [];
setBestItemNewData(baseData);
return;
}
const recommendedData = bestInfos[0].productInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedData =
bestInfos[0].productInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(recommendedData.map(item => item.prdtId));
const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId));
const baseData = bestSellerDatas?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.prdtId),
})) || [];
const baseData =
bestSellerDatas?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.prdtId),
})) || [];
setBestItemNewData(baseData);
}, [bestSellerDatas, bestInfos]);
@@ -126,17 +92,14 @@ const BestSeller = ({
const handleCardClick = useCallback(
(patnrId, prdtId) => () => {
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: {
patnrId: patnrId,
prdtId: prdtId,
nowShelf: spotlightId,
},
navigateFromBestSeller({
patnrId,
prdtId,
spotlightId,
})
);
},
[dispatch]
[dispatch, spotlightId]
);
const handleMoreCardClick = useCallback(() => {
@@ -144,7 +107,7 @@ const BestSeller = ({
pushPanel({
name: panel_names.TRENDING_NOW_PANEL,
panelInfo: {
pageName: "BS",
pageName: 'BS',
focusedContainerId: SpotlightIds.TRENDING_NOW_BEST_SELLER,
},
})
@@ -171,21 +134,18 @@ const BestSeller = ({
if (firstChk === 0 && itemIndex === 0) {
const c = Spotlight.getCurrent();
if (c) {
let cAriaLabel = c.getAttribute("aria-label");
cAriaLabel = "Best Seller, Heading 1," + cAriaLabel;
c.setAttribute("aria-label", cAriaLabel);
let cAriaLabel = c.getAttribute('aria-label');
cAriaLabel = 'Best Seller, Heading 1,' + cAriaLabel;
c.setAttribute('aria-label', cAriaLabel);
}
setFirstChk(1);
} else if (firstChk === 1 && itemIndex === 0) {
const c = Spotlight.getCurrent();
if (c) {
let cAriaLabel = c.getAttribute("aria-label");
let cAriaLabel = c.getAttribute('aria-label');
if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace(
"Best Seller, Heading 1,",
""
);
c.setAttribute("aria-label", newcAriaLabel);
const newcAriaLabel = cAriaLabel.replace('Best Seller, Heading 1,', '');
c.setAttribute('aria-label', newcAriaLabel);
}
}
} else {
@@ -220,7 +180,6 @@ const BestSeller = ({
}
}, [handleShelfFocus]);
return (
<Container
className={css.bestSellerWrap}
@@ -230,7 +189,7 @@ const BestSeller = ({
onFocus={_handleShelfFocus}
>
<SectionTitle
title={$L("BEST SELLER")}
title={$L('BEST SELLER')}
data-title-index="homeBestSellerTitle"
label="BEST SELLER"
/>
@@ -262,15 +221,15 @@ const BestSeller = ({
) => {
const rankText =
rankOrd === 1
? rankOrd + "st"
? rankOrd + 'st'
: rankOrd === 2
? rankOrd + "nd"
? rankOrd + 'nd'
: rankOrd === 3
? rankOrd + "rd"
: rankOrd + "th";
? rankOrd + 'rd'
: rankOrd + 'th';
return (
<TItemCardNew
key={"subItem" + itemIndex}
key={'subItem' + itemIndex}
contextName={LOG_CONTEXT_NAME.HOME}
messageId={LOG_MESSAGE_ID.SHELF_CLICK}
order={itemIndex + 1}
@@ -291,13 +250,13 @@ const BestSeller = ({
onBlur={handleBlur(itemIndex)}
onClick={handleCardClick(patnrId, prdtId)}
offerInfo={offerInfo}
spotlightId={"bestsellerItem" + itemIndex}
spotlightId={'bestsellerItem' + itemIndex}
firstLabel={rankText}
label={itemIndex * 1 + 1 + " of " + bestItemNewData.length}
label={itemIndex * 1 + 1 + ' of ' + bestItemNewData.length}
lastLabel=" go to detail, button"
euEnrgLblInfos={euEnrgLblInfos}
>
{foryou === true && <Tag text={"For You"} />}
{foryou === true && <Tag text={'For You'} />}
</TItemCardNew>
);
}
@@ -308,7 +267,7 @@ const BestSeller = ({
<SpottableComponent
className={css.displayBox}
onClick={handleMoreCardClick}
spotlightId={"bestseller-item-more-btn"}
spotlightId={'bestseller-item-more-btn'}
aria-label="See more button"
></SpottableComponent>
</div>

View File

@@ -1,57 +1,38 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { useDispatch, useSelector } from 'react-redux';
import Spotlight from "@enact/spotlight";
import { SpotlightContainerDecorator } from "@enact/spotlight/SpotlightContainerDecorator";
import Spotlight from '@enact/spotlight';
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
import { clearSMS } from "../../../actions/appDataActions";
import {
alertToast,
setHidePopup,
setShowPopup,
} from "../../../actions/commonActions";
import { clearSMS } from '../../../actions/appDataActions';
import { alertToast, setHidePopup, setShowPopup } from '../../../actions/commonActions';
import {
clearGetProductCouponDownload,
getProductCouponDownload,
} from "../../../actions/couponActions";
import { setEventPopClickInfo } from "../../../actions/eventActions";
import { sendLogGNB, sendLogShopByMobile } from "../../../actions/logActions";
import { pushPanel } from "../../../actions/panelActions";
import { startVideoPlayer } from "../../../actions/playActions";
import MobileSendPopUp from "../../../components/MobileSend/MobileSendPopUp";
import TPopUp from "../../../components/TPopUp/TPopUp";
import { launchMembershipApp } from "../../../lunaSend";
import {
ACTIVE_POPUP,
LOG_MENU,
LOG_TP_NO,
MYINFO_TABS,
panel_names,
} from "../../../utils/Config";
import { $L, formatLocalDateTime } from "../../../utils/helperMethods";
import css from "../EventPopUpBanner/EventPopUpBanner.module.less";
} from '../../../actions/couponActions';
import { setEventPopClickInfo } from '../../../actions/eventActions';
import { sendLogGNB, sendLogShopByMobile } from '../../../actions/logActions';
import { pushPanel, navigateFromEventPopup } from '../../../actions/panelActions';
import { startVideoPlayer } from '../../../actions/playActions';
import MobileSendPopUp from '../../../components/MobileSend/MobileSendPopUp';
import TPopUp from '../../../components/TPopUp/TPopUp';
import { launchMembershipApp } from '../../../lunaSend';
import { ACTIVE_POPUP, LOG_MENU, LOG_TP_NO, MYINFO_TABS, panel_names } from '../../../utils/Config';
import { $L, formatLocalDateTime } from '../../../utils/helperMethods';
import css from '../EventPopUpBanner/EventPopUpBanner.module.less';
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
export default function EventPopUpBanner() {
const dispatch = useDispatch();
const eventPopInfosData = useSelector(
(state) => state.event.eventData.eventPopInfo
);
const eventPopInfosData = useSelector((state) => state.event.eventData.eventPopInfo);
const popupVisible = useSelector((state) => state.common.popup.popupVisible);
const activePopup = useSelector((state) => state.common.popup.activePopup);
const userNumber = useSelector(
(state) => state.common?.appStatus.loginUserData.userNumber
);
const userNumber = useSelector((state) => state.common?.appStatus.loginUserData.userNumber);
const smsSuccess = useSelector((state) => state.appData.sendSms.retCode);
const couponDownloadSuccess = useSelector(
(state) => state.coupon?.couponDownloadSuccess
);
const couponDownloadSuccess = useSelector((state) => state.coupon?.couponDownloadSuccess);
const timerRef = useRef();
const shopByMobileLogRef = useRef(null);
@@ -61,7 +42,7 @@ export default function EventPopUpBanner() {
const evntTpCd = eventPopInfosData?.evntTpCd;
const evntNm = eventPopInfosData?.evntNm;
const [smsTpCd, setSmsTpCd] = useState("");
const [smsTpCd, setSmsTpCd] = useState('');
const [eventPopData, setEventPopData] = useState({});
useEffect(() => {
@@ -77,12 +58,12 @@ export default function EventPopUpBanner() {
const handleApply = useCallback(() => {
if (eventPopData) {
dispatch(setEventPopClickInfo({ evntApplcnFlag: "Y", evntId }));
dispatch(setEventPopClickInfo({ evntApplcnFlag: 'Y', evntId }));
switch (evntTpCd) {
case "EVT00102": // curation
case "EVT00104": // curation + prize
case "EVT00107": {
case 'EVT00102': // curation
case 'EVT00104': // curation + prize
case 'EVT00107': {
// curation + billing coupon
dispatch(setHidePopup());
dispatch(
@@ -96,43 +77,43 @@ export default function EventPopUpBanner() {
);
break;
}
case "EVT00105": {
case 'EVT00105': {
// coupon Only
setSmsTpCd("APP00212");
setSmsTpCd('APP00212');
dispatch(setShowPopup(ACTIVE_POPUP.smsPopup));
const params = {
befPrice: "",
curationId: eventPopData?.curationId ?? "",
curationNm: eventPopData?.curationNm ?? "",
evntId: eventPopData?.evntId ?? "",
evntNm: eventPopData?.evntNm ?? "",
lastPrice: "",
lgCatCd: "",
lgCatNm: "",
liveFlag: "N",
befPrice: '',
curationId: eventPopData?.curationId ?? '',
curationNm: eventPopData?.curationNm ?? '',
evntId: eventPopData?.evntId ?? '',
evntNm: eventPopData?.evntNm ?? '',
lastPrice: '',
lgCatCd: '',
lgCatNm: '',
liveFlag: 'N',
locDt: formatLocalDateTime(new Date()),
logTpNo: LOG_TP_NO.SHOP_BY_MOBILE.SHOP_BY_MOBILE,
mbphNoFlag: "N",
mbphNoFlag: 'N',
patncNm: eventPopData?.patncNm,
patnrId: eventPopData?.patnrId,
prdtId: "",
prdtNm: "",
revwGrd: "",
rewdAplyFlag: "N",
shopByMobileFlag: "Y",
shopTpNm: "popevent",
showId: "",
showNm: "",
trmsAgrFlag: "N",
tsvFlag: "N",
prdtId: '',
prdtNm: '',
revwGrd: '',
rewdAplyFlag: 'N',
shopByMobileFlag: 'Y',
shopTpNm: 'popevent',
showId: '',
showNm: '',
trmsAgrFlag: 'N',
tsvFlag: 'N',
};
dispatch(sendLogShopByMobile(params));
shopByMobileLogRef.current = params;
break;
}
case "EVT00108": {
case 'EVT00108': {
// Direct+Billng+Coupon
if (!userNumber) {
dispatch(setShowPopup(ACTIVE_POPUP.loginPopup));
@@ -152,7 +133,7 @@ export default function EventPopUpBanner() {
useEffect(() => {
if (couponDownloadSuccess === 0) {
dispatch(alertToast($L("Downloading coupon complete.")));
dispatch(alertToast($L('Downloading coupon complete.')));
if (eventPopData && eventPopData?.shptmLnkInfo?.shptmLnkTpCd) {
dispatch(setHidePopup());
navigateToLinkByTypeCode();
@@ -174,7 +155,7 @@ export default function EventPopUpBanner() {
const handleSkip = useCallback(() => {
if (eventPopInfosData) {
dispatch(setEventPopClickInfo({ evntApplcnFlag: "N", evntId }));
dispatch(setEventPopClickInfo({ evntApplcnFlag: 'N', evntId }));
dispatch(setHidePopup());
}
@@ -185,7 +166,7 @@ export default function EventPopUpBanner() {
useEffect(() => {
timerRef.current = setTimeout(() => {
Spotlight.focus("tPopupBtn1");
Spotlight.focus('tPopupBtn1');
}, 100);
return () => clearTimeout(timerRef.current);
}, []);
@@ -200,23 +181,23 @@ export default function EventPopUpBanner() {
if (eventPopData && eventPopData.shptmLnkInfo) {
switch (eventPopData.shptmLnkInfo.shptmLnkTpCd) {
case "EVT00201":
case 'EVT00201':
break;
case "EVT00202":
case 'EVT00202':
dispatch(
pushPanel({
name: panel_names.FEATURED_BRANDS_PANEL,
panelInfo: {
from: "gnb",
from: 'gnb',
patnrId: eventPopData.shptmLnkInfo?.lnkPatnrId,
},
})
);
break;
case "EVT00203":
case 'EVT00203':
dispatch(pushPanel({ name: panel_names.TRENDING_NOW_PANEL }));
break;
case "EVT00204":
case 'EVT00204':
pushPanel({
name: panel_names.HOT_PICKS_PANEL,
panelInfo: {
@@ -225,7 +206,7 @@ export default function EventPopUpBanner() {
},
});
break;
case "EVT00205":
case 'EVT00205':
dispatch(
pushPanel({
name: panel_names.ON_SALE_PANEL,
@@ -233,7 +214,7 @@ export default function EventPopUpBanner() {
})
);
break;
case "EVT00206":
case 'EVT00206':
dispatch(
pushPanel({
name: panel_names.CATEGORY_PANEL,
@@ -243,28 +224,25 @@ export default function EventPopUpBanner() {
})
);
break;
case "EVT00207":
case 'EVT00207':
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: {
patnrId: eventPopData.shptmLnkInfo?.lnkPatnrId,
prdtId: eventPopData.shptmLnkInfo?.lnkPrdtId,
},
navigateFromEventPopup({
patnrId: eventPopData.shptmLnkInfo?.lnkPatnrId,
prdtId: eventPopData.shptmLnkInfo?.lnkPrdtId,
})
);
break;
case "EVT00208":
case 'EVT00208':
dispatch(
startVideoPlayer({
modal: false,
patnrId: eventPopData.shptmLnkInfo?.lnkPatnrId,
showId: eventPopData.shptmLnkInfo?.lnkBrdcId,
shptmBanrTpNm: "VOD",
shptmBanrTpNm: 'VOD',
})
);
break;
case "EVT00209":
case 'EVT00209':
dispatch(
pushPanel({
name: panel_names.THEME_CURATION_PANEL,
@@ -274,12 +252,12 @@ export default function EventPopUpBanner() {
})
);
break;
case "EVT00210":
case 'EVT00210':
dispatch(
pushPanel({
name: panel_names.MY_PAGE_PANEL,
panelInfo: {
menuNm: "My Info",
menuNm: 'My Info',
menuOrd: 3,
tabForced: MYINFO_TABS.COUPON,
},
@@ -307,8 +285,8 @@ export default function EventPopUpBanner() {
onClose={handleSkip}
onClick={handleApply}
hasButton
button1Text={$L("Apply Now")}
button2Text={$L("Skip")}
button1Text={$L('Apply Now')}
button2Text={$L('Skip')}
className={css.eventPopup}
>
{eventPopInfosData && (
@@ -322,10 +300,8 @@ export default function EventPopUpBanner() {
<MobileSendPopUp
open={popupVisible}
onClose={onClose}
title={$L("Send To Mobile")}
subTitle={$L(
"Enter your mobile number to receive detailed information via SMS"
)}
title={$L('Send To Mobile')}
subTitle={$L('Enter your mobile number to receive detailed information via SMS')}
smsTpCd={smsTpCd}
evntId={eventPopData.evntId}
patnrId={eventPopData.patnrId}
@@ -337,7 +313,7 @@ export default function EventPopUpBanner() {
kind="textPopup"
open={popupVisible}
hasText
text={$L("Downloading coupon complete.")}
text={$L('Downloading coupon complete.')}
/>
)}
{activePopup === ACTIVE_POPUP.loginPopup && (
@@ -345,10 +321,10 @@ export default function EventPopUpBanner() {
kind="textPopup"
hasText
open={popupVisible}
text={$L("Would you like to sign in?")}
text={$L('Would you like to sign in?')}
hasButton
button1Text={$L("OK")}
button2Text={$L("CANCEL")}
button1Text={$L('OK')}
button2Text={$L('CANCEL')}
onClick={handleLogin}
onClose={onClose}
></TPopUp>

View File

@@ -17,7 +17,7 @@ import liveShow from '../../../../assets/images/tag-liveshow.png';
import { changeAppStatus } from '../../../actions/commonActions';
import { updateHomeInfo, setVideoTransitionLock } from '../../../actions/homeActions';
import { sendLogTopContents, sendLogTotalRecommend } from '../../../actions/logActions';
import { pushPanel } from '../../../actions/panelActions';
import { pushPanel, navigateFromRandomUnit } from '../../../actions/panelActions';
import {
finishVideoPreview,
startVideoPlayer,
@@ -462,17 +462,11 @@ export default function RandomUnit({
// 투데이즈딜 클릭
const todayDealClick = useCallback(() => {
if (playerPanelInfo?.modal !== false) {
finishAndUnlock();
}
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: {
patnrId: randomData.patnrId,
prdtId: randomData.prdtId,
},
navigateFromRandomUnit({
patnrId: randomData.patnrId,
prdtId: randomData.prdtId,
type: 'product',
})
);

View File

@@ -1,41 +1,24 @@
import React, {
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import Spotlight from '@enact/spotlight';
import {
SpotlightContainerDecorator,
} from '@enact/spotlight/SpotlightContainerDecorator';
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { pushPanel } from '../../../actions/panelActions';
import { pushPanel, navigateFromPickedForYou } from '../../../actions/panelActions';
import SectionTitle from '../../../components/SectionTitle/SectionTitle';
import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller';
import useScrollReset from '../../../hooks/useScrollReset';
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 { SpotlightIds } from '../../../utils/SpotlightIds';
import css from './PickedForYou.module.less';
const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
const PickedForYou = ({
order,
@@ -46,20 +29,14 @@ const PickedForYou = ({
shelfLocation,
shelfTitle,
}) => {
const { getScrollTo, scrollLeft } = useScrollTo();
const { handleScrollReset, handleStopScrolling } = useScrollReset(
scrollLeft,
true
);
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollLeft, true);
const dispatch = useDispatch();
const { cursorVisible } = useSelector((state) => state.common.appStatus);
const justForYouDatasNew = useSelector(
(state) => state.foryou?.recommendInfo?.recommendProduct
)
const justForYouDatasNew = useSelector((state) => state.foryou?.recommendInfo?.recommendProduct);
const [drawChk, setDrawChk] = useState(false);
const [firstChk, setFirstChk] = useState(0);
@@ -67,14 +44,9 @@ const PickedForYou = ({
const [pickedForYou, setPickedForYou] = useState(null);
const orderStyle = useMemo(() => ({ order: order }), [order]);
useEffect(()=>{
setPickedForYou(
justForYouDatasNew?.filter(
(item) => item.recommendTpCd === "PICKEDFORYOU"
)
)
},[justForYouDatasNew])
useEffect(() => {
setPickedForYou(justForYouDatasNew?.filter((item) => item.recommendTpCd === 'PICKEDFORYOU'));
}, [justForYouDatasNew]);
useEffect(() => {
setDrawChk(true);
@@ -83,17 +55,14 @@ const PickedForYou = ({
const handleCardClick = useCallback(
(patnrId, prdtId) => () => {
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: {
patnrId: patnrId,
prdtId: prdtId,
nowShelf: spotlightId,
},
navigateFromPickedForYou({
patnrId,
prdtId,
spotlightId,
})
);
},
[dispatch]
[dispatch, spotlightId]
);
const handleMoreCardClick = useCallback(() => {
@@ -101,7 +70,7 @@ const PickedForYou = ({
pushPanel({
name: panel_names.TRENDING_NOW_PANEL,
panelInfo: {
pageName: "BS",
pageName: 'BS',
focusedContainerId: SpotlightIds.TRENDING_NOW_BEST_SELLER,
},
})
@@ -128,21 +97,18 @@ const PickedForYou = ({
if (firstChk === 0 && itemIndex === 0) {
const c = Spotlight.getCurrent();
if (c) {
let cAriaLabel = c.getAttribute("aria-label");
cAriaLabel = "Best Seller, Heading 1," + cAriaLabel;
c.setAttribute("aria-label", cAriaLabel);
let cAriaLabel = c.getAttribute('aria-label');
cAriaLabel = 'Best Seller, Heading 1,' + cAriaLabel;
c.setAttribute('aria-label', cAriaLabel);
}
setFirstChk(1);
} else if (firstChk === 1 && itemIndex === 0) {
const c = Spotlight.getCurrent();
if (c) {
let cAriaLabel = c.getAttribute("aria-label");
let cAriaLabel = c.getAttribute('aria-label');
if (cAriaLabel) {
const newcAriaLabel = cAriaLabel.replace(
"Best Seller, Heading 1,",
""
);
c.setAttribute("aria-label", newcAriaLabel);
const newcAriaLabel = cAriaLabel.replace('Best Seller, Heading 1,', '');
c.setAttribute('aria-label', newcAriaLabel);
}
}
} else {
@@ -186,7 +152,7 @@ const PickedForYou = ({
onFocus={_handleShelfFocus}
>
<SectionTitle
title={$L("PICKED FOR YOU")}
title={$L('PICKED FOR YOU')}
data-title-index="homeBestSellerTitle"
label="PICKED FOR YOU"
/>
@@ -198,7 +164,7 @@ const PickedForYou = ({
noScrollByWheel
>
{pickedForYou &&
pickedForYou?.[0] &&
pickedForYou?.[0] &&
pickedForYou?.[0].productInfos.map(
(
{
@@ -211,13 +177,13 @@ const PickedForYou = ({
brndNm,
patncNm,
catNm,
euEnrgLblInfos
euEnrgLblInfos,
},
itemIndex
) => {
return (
<TItemCardNew
key={"subItem" + itemIndex}
key={'subItem' + itemIndex}
contextName={LOG_CONTEXT_NAME.HOME}
messageId={LOG_MESSAGE_ID.SHELF_CLICK}
order={itemIndex + 1}
@@ -236,8 +202,8 @@ const PickedForYou = ({
onBlur={handleBlur(itemIndex)}
onClick={handleCardClick(patnrId, prdtId)}
offerInfo={offerInfo}
spotlightId={"bestsellerItem" + itemIndex}
label={itemIndex * 1 + 1 + " of " + pickedForYou?.[0].productInfos.length}
spotlightId={'bestsellerItem' + itemIndex}
label={itemIndex * 1 + 1 + ' of ' + pickedForYou?.[0].productInfos.length}
lastLabel=" go to detail, button"
euEnrgLblInfos={euEnrgLblInfos}
/>
@@ -250,7 +216,7 @@ const PickedForYou = ({
<SpottableComponent
className={css.displayBox}
onClick={handleMoreCardClick}
spotlightId={"bestseller-item-more-btn"}
spotlightId={'bestseller-item-more-btn'}
aria-label="See more button"
></SpottableComponent>
</div>

View File

@@ -1,46 +1,28 @@
import React, {
memo,
useCallback,
useEffect,
useState,
} from 'react';
import React, { memo, useCallback, useEffect, useState } from 'react';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import {
SpotlightContainerDecorator,
} from '@enact/spotlight/SpotlightContainerDecorator';
import { SpotlightContainerDecorator } from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import { sendLogCuration } from '../../../actions/logActions';
import { getSubCategory } from '../../../actions/mainActions';
import { pushPanel } from '../../../actions/panelActions';
import { pushPanel, navigateFromSubCategory } from '../../../actions/panelActions';
import Tag from '../../../components/TItemCard/Tag';
import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller';
import usePrevious from '../../../hooks/usePrevious';
import useScrollReset from '../../../hooks/useScrollReset';
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 CategoryNav from '../../HomePanel/SubCategory/CategoryNav/CategoryNav';
import css from '../../HomePanel/SubCategory/SubCategory.module.less';
const SpottableComponent = Spottable("div");
const Container = SpotlightContainerDecorator({ enterTo: null }, "div");
const ContainerBasic = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
const SpottableComponent = Spottable('div');
const Container = SpotlightContainerDecorator({ enterTo: null }, 'div');
const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
const getExpsOrdByLgCatCd = (array, value) => {
const expsOrd = array.findIndex(({ lgCatCd }) => value === lgCatCd) + 1;
@@ -58,21 +40,12 @@ export default memo(function SubCategory({
}) {
const dispatch = useDispatch();
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 categoryItemInfos = useSelector(
(state) => state.main.subCategoryData?.categoryItemInfos
);
const categoryInfos = useSelector((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 nowMenu = useSelector((state) => state.common.menu.nowMenu);
@@ -110,9 +83,9 @@ export default memo(function SubCategory({
getSubCategory(
{
lgCatCd: currentLgCatCd,
pageSize: "10",
tabType: "CAT00102",
filterType: "CAT00202",
pageSize: '10',
tabType: 'CAT00102',
filterType: 'CAT00202',
recommendIncFlag: 'Y',
},
1
@@ -124,7 +97,7 @@ export default memo(function SubCategory({
}, [currentLgCatCd, dispatch, firstChk]);
useEffect(() => {
if (!nowMenuRef.current || !nowMenuRef.current.startsWith("Home")) {
if (!nowMenuRef.current || !nowMenuRef.current.startsWith('Home')) {
return;
}
@@ -179,40 +152,47 @@ export default memo(function SubCategory({
const handleCardClick = useCallback(
(patnrId, prdtId) => () => {
dispatch(
pushPanel({
name: panel_names.DETAIL_PANEL,
panelInfo: { patnrId, prdtId, nowShelf: spotlightId },
navigateFromSubCategory({
patnrId,
prdtId,
spotlightId,
})
);
},
[dispatch, spotlightId]
);
const handleScrollRight = useCallback((e) => {
const container = e.currentTarget?.parentNode;
const x = container.scrollWidth - container.clientWidth;
const handleScrollRight = useCallback(
(e) => {
const container = e.currentTarget?.parentNode;
const x = container.scrollWidth - container.clientWidth;
setTimeout(() => scrollLeft({ x, animate: true }));
}, [scrollLeft]);
setTimeout(() => scrollLeft({ x, animate: true }));
},
[scrollLeft]
);
const handleMoreCardClick = useCallback((e) => {
const lgCatCd = e.currentTarget?.getAttribute("data-catcd-num");
const lgCatNm = e.currentTarget?.getAttribute("data-catcd-nm");
const tab = 0;
const handleMoreCardClick = useCallback(
(e) => {
const lgCatCd = e.currentTarget?.getAttribute('data-catcd-num');
const lgCatNm = e.currentTarget?.getAttribute('data-catcd-nm');
const tab = 0;
if (lgCatCd && lgCatNm) {
dispatch(
pushPanel({
name: panel_names.CATEGORY_PANEL,
panelInfo: {
lgCatCd,
lgCatNm,
tab,
},
})
);
}
}, [dispatch]);
if (lgCatCd && lgCatNm) {
dispatch(
pushPanel({
name: panel_names.CATEGORY_PANEL,
panelInfo: {
lgCatCd,
lgCatNm,
tab,
},
})
);
}
},
[dispatch]
);
const _handleItemFocus = useCallback(() => {
if (handleItemFocus) {
@@ -228,35 +208,34 @@ export default memo(function SubCategory({
useEffect(() => {
if (!foruItemInfos || foruItemInfos.length === 0) {
const baseData = categoryItemInfos?.subCatItemList?.map((item) => ({
...item,
foryou: false,
})) || [];
const baseData =
categoryItemInfos?.subCatItemList?.map((item) => ({
...item,
foryou: false,
})) || [];
setCategoryItemNewData(baseData);
return;
}
const recommendedData = foruItemInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedData =
foruItemInfos?.map((item) => ({
...item,
foryou: true,
})) || [];
const recommendedPrdtIds = new Set(recommendedData.map(item => item.prdtId));
const recommendedPrdtIds = new Set(recommendedData.map((item) => item.prdtId));
const baseData = categoryItemInfos?.subCatItemList?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.prdtId),
})) || [];
const baseData =
categoryItemInfos?.subCatItemList?.map((item) => ({
...item,
foryou: recommendedPrdtIds.has(item.prdtId),
})) || [];
setCategoryItemNewData([...baseData]);
}, [categoryItemInfos?.subCatItemList, foruItemInfos]);
return (
<Container
spotlightId={spotlightId}
data-wheel-point
onFocus={_handleShelfFocus}
>
<Container spotlightId={spotlightId} data-wheel-point onFocus={_handleShelfFocus}>
<CategoryNav
categoryInfos={categoryInfos}
currentCategoryCode={currentLgCatCd}
@@ -293,7 +272,7 @@ export default memo(function SubCategory({
) => {
return (
<TItemCardNew
key={"subItem" + itemIndex}
key={'subItem' + itemIndex}
contextName={LOG_CONTEXT_NAME.HOME}
messageId={LOG_MESSAGE_ID.SHELF_CLICK}
catNm={categoryItemInfos?.catNm}
@@ -314,16 +293,11 @@ export default memo(function SubCategory({
offerInfo={offerInfo}
data-catcd-num={currentLgCatCd}
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"
euEnrgLblInfos={euEnrgLblInfos}
>
{foryou === true && <Tag text={"For You"} />}
{foryou === true && <Tag text={'For You'} />}
</TItemCardNew>
);
}
@@ -333,7 +307,7 @@ export default memo(function SubCategory({
<SpottableComponent
className={css.displayBox}
onClick={handleMoreCardClick}
spotlightId={"home-cate-more-btn-" + currentLgCatCd}
spotlightId={'home-cate-more-btn-' + currentLgCatCd}
onFocus={_handleItemFocus}
data-catcd-num={currentLgCatCd}
data-catcd-nm={currentLgCatNm}

View File

@@ -186,6 +186,10 @@ export default function MainView({ className, initService }) {
const isHomeOnTop = useMemo(() => {
return !mainIndex && (panels.length <= 0 || (panels.length === 1 && panels[0].panelInfo.modal));
}, [mainIndex, panels]);
const hasDetailPanel = useMemo(
() => panels.some((panel) => panel?.name === Config.panel_names.DETAIL_PANEL),
[panels]
);
const onPreImageLoadComplete = useCallback(() => {
// console.log('MainView onPreImageLoadComplete');
@@ -257,6 +261,7 @@ export default function MainView({ className, initService }) {
return (
<>
{(isHomeOnTop ||
hasDetailPanel ||
(panels.length === 1 &&
(panels[0]?.name === Config.panel_names.PLAYER_PANEL ||
panels[0]?.name === Config.panel_names.PLAYER_PANEL_NEW ||

View File

@@ -61,6 +61,7 @@ import TPopUp from '../../components/TPopUp/TPopUp';
import Media from '../../components/VideoPlayer/Media';
import TReactPlayer from '../../components/VideoPlayer/TReactPlayer';
import { VideoPlayer } from '../../components/VideoPlayer/VideoPlayer.v3';
import MediaOverlayContents from '../PlayerPanel/PlayerOverlay/MediaOverlayContents';
import usePrevious from '../../hooks/usePrevious';
import useWhyDidYouUpdate from '../../hooks/useWhyDidYouUpdate';
import * as Config from '../../utils/Config';
@@ -2244,6 +2245,7 @@ const MediaPanel = React.forwardRef(
spotlightId={
panelInfo.modal ? 'modal-video-player' : panelInfo.modalContainerId || spotlightId
}
overlayContentsComponent={MediaOverlayContents}
handleIndicatorDownClick={handleIndicatorDownClick}
handleIndicatorUpClick={handleIndicatorUpClick}
onError={mediainfoHandler}

View File

@@ -0,0 +1,9 @@
import React from 'react';
import PlayerOverlayContents from './PlayerOverlayContents';
function MediaOverlayContents(props) {
return <PlayerOverlayContents {...props} forceShowMediaOverlay />;
}
export default MediaOverlayContents;

View File

@@ -37,11 +37,11 @@ function PlayerOverlayContents({
handleIndicatorDownClick,
tabContainerVersion,
tabIndexV2,
forceShowMediaOverlay = false,
}) {
const cntry_cd = useSelector((state) => state.common.httpHeader?.cntry_cd);
const dispatch = useDispatch();
const onClickBack = (ev) => {
// TabContainerV2가 표시된 상태에서 백버튼 클릭 시 이벤트 버블링 방지
// (Overlay의 onClick으로 전파되어 toggleControls()가 호출되는 것을 막음)
if (tabContainerVersion === 2 && belowContentsVisible) {
@@ -107,11 +107,15 @@ function PlayerOverlayContents({
if (type === 'LIVE') {
return Spotlight.focus('videoIndicator-down-button');
}
Spotlight.focus('videoPlayer_mediaControls');
return Spotlight.focus(SpotlightIds.PLAYER_PLAY_BUTTON);
},
[type]
);
const onSpotlightMoveSubtitleButton = useCallback(() => {
return Spotlight.focus('player-subtitlebutton');
}, []);
const onSpotlightMoveSlider = useCallback(
(e) => {
if (type === 'VOD') {
@@ -154,9 +158,41 @@ function PlayerOverlayContents({
return Spotlight.focus(SpotlightIds.PLAYER_BACK_BUTTON);
}, []);
const handleOverlayKeyDownCapture = useCallback(
(ev) => {
const currentId = Spotlight.getCurrent()?.getAttribute('data-spotlight-id');
if (ev.keyCode === 40 && currentId === SpotlightIds.PLAYER_BACK_BUTTON) {
ev.preventDefault();
ev.stopPropagation();
return onSpotlightMoveMediaButton(ev);
}
if (ev.keyCode === 39 && currentId === SpotlightIds.PLAYER_BACK_BUTTON) {
ev.preventDefault();
ev.stopPropagation();
return onSpotlightMoveSubtitleButton(ev);
}
if (ev.keyCode === 37 && currentId === SpotlightIds.PLAYER_SUBTITLE_BUTTON) {
ev.preventDefault();
ev.stopPropagation();
return onSpotlightMoveMediaButton(ev);
}
if (ev.keyCode === 38 && currentId === SpotlightIds.PLAYER_PLAY_BUTTON) {
ev.preventDefault();
ev.stopPropagation();
return onSpotlightMoveBackButton();
}
},
[onSpotlightMoveBackButton, onSpotlightMoveMediaButton, onSpotlightMoveSubtitleButton]
);
const shouldShowExtendedControls = useMemo(
() => forceShowMediaOverlay || type !== 'MEDIA',
[forceShowMediaOverlay, type]
);
const currentSideButtonStatus = useMemo(() => {
if (
type !== 'MEDIA' &&
shouldShowExtendedControls &&
!panelInfo?.modal &&
!sideContentsVisible &&
tabContainerVersion === 1
@@ -164,7 +200,7 @@ function PlayerOverlayContents({
return true;
}
return false;
}, [type, panelInfo, sideContentsVisible, tabContainerVersion]);
}, [shouldShowExtendedControls, panelInfo, sideContentsVisible, tabContainerVersion]);
const noLiveContentsVisible = useMemo(() => {
if (!Array.isArray(playListInfo) || playListInfo.length === 0) {
@@ -181,8 +217,8 @@ function PlayerOverlayContents({
return (
<>
<Container className={css.overlayContainer}>
{type !== 'MEDIA' && playListInfo.length > 1 && noLiveContentsVisible && (
<Container className={css.overlayContainer} onKeyDownCapture={handleOverlayKeyDownCapture}>
{shouldShowExtendedControls && playListInfo.length > 1 && noLiveContentsVisible && (
<>
<div className={css.indicatorUpButton}>
<SpottableBtn
@@ -232,7 +268,7 @@ function PlayerOverlayContents({
setSideContentsVisible={setSideContentsVisible}
sideContentsVisible={sideContentsVisible}
onSpotlightLeft={
type !== 'MEDIA' && playListInfo.length < 2 && onSpotlightMoveBackButton
shouldShowExtendedControls && playListInfo.length < 2 && onSpotlightMoveBackButton
}
videoType={type}
/>
@@ -247,8 +283,10 @@ function PlayerOverlayContents({
)}
onClick={handleSubtitleOnClick}
spotlightId="player-subtitlebutton"
onSpotlightUp={onSpotlightMoveTabButton}
onSpotlightLeft={onSpotlightMoveMediaButton}
onSpotlightUp={onSpotlightMoveBackButton}
onSpotlightLeft={onSpotlightMoveBackButton}
onSpotlightRight={onSpotlightMoveMediaButton}
onSpotlightDown={onSpotlightMoveMediaButton}
aria-label="Caption"
/>
</div>
@@ -263,6 +301,8 @@ function PlayerOverlayContents({
? onSpotlightMoveBelowTab
: onSpotlightMoveMediaButton
}
onSpotlightRight={onSpotlightMoveSubtitleButton}
onSpotlightUp={onSpotlightMoveSubtitleButton}
aria-label="Video Player Close"
ref={backBtnRef}
/>
@@ -312,7 +352,8 @@ const propsAreEqual = (prev, next) => {
prev.sideContentsVisible === next.sideContentsVisible &&
prev.belowContentsVisible === next.belowContentsVisible &&
prev.tabContainerVersion === next.tabContainerVersion &&
prev.tabIndexV2 === next.tabIndexV2
prev.tabIndexV2 === next.tabIndexV2 &&
prev.forceShowMediaOverlay === next.forceShowMediaOverlay
);
};

View File

@@ -4,11 +4,7 @@ import { useDispatch } from 'react-redux';
import Spotlight from '@enact/spotlight';
import {
pushPanel,
updatePanel,
} from '../../../actions/panelActions';
import { navigateToDetailPanel } from '../../../actions/panelNavigationActions';
import { pushPanel, updatePanel, navigateFromSearch } from '../../../actions/panelActions';
import TItemCardNew from '../../../components/TItemCard/TItemCard.new';
import TScroller from '../../../components/TScroller/TScroller';
import { panel_names } from '../../../utils/Config';
@@ -49,14 +45,16 @@ const ItemCard = ({ onClick, itemInfo, searchQuery }) => {
onClick(ev);
}
// 순차 네비게이션 사용 (Chrome 68 호환)
dispatch(navigateToDetailPanel(
patnrId,
prdtId,
searchQuery,
currentSpot,
{ tab: 0 }
));
// 통합된 navigateToDetail 사용
dispatch(
navigateFromSearch({
patnrId,
prdtId,
searchQuery,
currentSpot,
additionalInfo: { tab: 0 },
})
);
},
[onClick, dispatch, searchQuery]
);
@@ -65,7 +63,7 @@ const ItemCard = ({ onClick, itemInfo, searchQuery }) => {
<>
<TScroller className={css.container} spotlightId={SpotlightIds.SEARCH_ITEM}>
{itemInfo.map((item, index) => {
const { thumbnail, title, dcPrice, price, soldout, contentId,euEnrgLblInfos } = item;
const { thumbnail, title, dcPrice, price, soldout, contentId, euEnrgLblInfos } = item;
const tokens = contentId && contentId.split('_');
const patnrId = tokens?.[4] || '';
const prdtId = tokens?.[5] || '';