[251123] fix: DetailPanel ThemeContent-1
🕐 커밋 시간: 2025. 11. 23. 08:13:50 📊 변경 통계: • 총 파일: 5개 • 추가: +500줄 • 삭제: -171줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/components/TToast/TToastEnhanced.jsx ~ com.twin.app.shoptime/src/components/TToast/TToastEnhanced.module.less ~ com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.jsx ~ com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.module.less 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/components/TToast/TToastEnhanced.module.less (unknown): ✅ Added: gradient() 📄 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ProductAllSection.jsx (javascript): 🔄 Modified: extractProductMeta() 📄 com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.jsx (javascript): ✅ Added: handleItemClick() ❌ Deleted: handleItemClick(), productNameDangerouslySetInnerHTML() 📄 com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.module.less (unknown): ✅ Added: gradient(), global() 🔧 주요 변경 내용: • UI 컴포넌트 아키텍처 개선
This commit is contained in:
@@ -1,19 +1,14 @@
|
|||||||
import React, {
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
useDispatch,
|
|
||||||
useSelector,
|
|
||||||
} from 'react-redux';
|
|
||||||
|
|
||||||
|
import Spotlight from '@enact/spotlight';
|
||||||
import Spottable from '@enact/spotlight/Spottable';
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
|
||||||
import { changeAppStatus } from '../../actions/commonActions';
|
import { changeAppStatus } from '../../actions/commonActions';
|
||||||
import BuyOption from '../../views/DetailPanel/components/BuyOption';
|
import BuyOption from '../../views/DetailPanel/components/BuyOption';
|
||||||
|
import ThemeContents from '../../views/DetailPanel/ThemeProduct/ThemeContents';
|
||||||
import css from './TToastEnhanced.module.less';
|
import css from './TToastEnhanced.module.less';
|
||||||
|
|
||||||
const SpottableToast = Spottable('div');
|
const SpottableToast = Spottable('div');
|
||||||
@@ -37,6 +32,17 @@ export default function TToastEnhanced({
|
|||||||
productInfo, // 🚀 BuyOption에 전달할 상품 정보
|
productInfo, // 🚀 BuyOption에 전달할 상품 정보
|
||||||
selectedPatnrId, // 🚀 BuyOption에 전달할 파트너 ID
|
selectedPatnrId, // 🚀 BuyOption에 전달할 파트너 ID
|
||||||
selectedPrdtId, // 🚀 BuyOption에 전달할 상품 ID
|
selectedPrdtId, // 🚀 BuyOption에 전달할 상품 ID
|
||||||
|
// 🚀 ThemeContents 관련 props
|
||||||
|
themeItems,
|
||||||
|
setSelectedIndex,
|
||||||
|
videoVerticalVisible,
|
||||||
|
currentVideoShowId,
|
||||||
|
tabIndex,
|
||||||
|
handleItemFocus,
|
||||||
|
tabTitle,
|
||||||
|
panelInfo,
|
||||||
|
direction,
|
||||||
|
version,
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -60,6 +66,12 @@ export default function TToastEnhanced({
|
|||||||
// 약간의 지연을 두고 애니메이션 시작
|
// 약간의 지연을 두고 애니메이션 시작
|
||||||
const showTimer = setTimeout(() => {
|
const showTimer = setTimeout(() => {
|
||||||
setIsVisible(true);
|
setIsVisible(true);
|
||||||
|
// themeContents 타입일 때 포커스 설정
|
||||||
|
if (type === 'themeContents') {
|
||||||
|
setTimeout(() => {
|
||||||
|
Spotlight.focus('theme-contents-close-button');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
startTimer();
|
startTimer();
|
||||||
@@ -68,36 +80,40 @@ export default function TToastEnhanced({
|
|||||||
clearTimeout(showTimer);
|
clearTimeout(showTimer);
|
||||||
clearTimer();
|
clearTimer();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [type]);
|
||||||
|
|
||||||
// BuyOption 컨테이너 ref
|
// BuyOption, ThemeContents 컨테이너 ref
|
||||||
const buyOptionRef = useRef(null);
|
const buyOptionRef = useRef(null);
|
||||||
|
const themeContentsRef = useRef(null);
|
||||||
|
|
||||||
// BuyOption 타입일 때 전역 포커스 감지
|
// BuyOption, ThemeContents 타입일 때 전역 포커스 감지
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === 'buyOption') {
|
if (type === 'buyOption' || type === 'themeContents') {
|
||||||
// BuyOption이 포커스를 받았는지 추적하는 플래그
|
// 포커스를 받았는지 추적하는 플래그
|
||||||
let hasBuyOptionReceivedFocus = false;
|
let hasComponentReceivedFocus = false;
|
||||||
|
const componentRef = type === 'buyOption' ? buyOptionRef : themeContentsRef;
|
||||||
|
|
||||||
const handleFocusChange = (e) => {
|
const handleFocusChange = (e) => {
|
||||||
// 1. BuyOption 내부로 포커스가 들어온 경우 - 플래그를 true로 설정
|
// 1. 컴포넌트 내부로 포커스가 들어온 경우 - 플래그를 true로 설정
|
||||||
if (!cursorVisible) {
|
if (!cursorVisible) {
|
||||||
if (buyOptionRef.current && buyOptionRef.current.contains(e.target)) {
|
if (componentRef.current && componentRef.current.contains(e.target)) {
|
||||||
if (!hasBuyOptionReceivedFocus) {
|
if (!hasComponentReceivedFocus) {
|
||||||
hasBuyOptionReceivedFocus = true;
|
hasComponentReceivedFocus = true;
|
||||||
console.log('[TToastEnhanced] BuyOption received focus - now tracking focus leaving');
|
console.log(`[TToastEnhanced] ${type} received focus - now tracking focus leaving`);
|
||||||
}
|
}
|
||||||
return; // 내부에 포커스가 있으면 아무것도 하지 않음
|
return; // 내부에 포커스가 있으면 아무것도 하지 않음
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. BuyOption이 포커스를 받은 적이 있고, 현재 외부로 포커스가 이동한 경우 - Toast 닫기
|
// 2. 컴포넌트가 포커스를 받은 적이 있고, 현재 외부로 포커스가 이동한 경우 - Toast 닫기
|
||||||
|
// themeContents는 spotlightRestrict: 'self-only'이므로 keyboard로는 포커스가 나가지 않음
|
||||||
|
// 따라서 이는 mouse click 등으로 다른 요소를 클릭한 경우만 해당
|
||||||
if (
|
if (
|
||||||
hasBuyOptionReceivedFocus &&
|
hasComponentReceivedFocus &&
|
||||||
buyOptionRef.current &&
|
componentRef.current &&
|
||||||
!buyOptionRef.current.contains(e.target)
|
!componentRef.current.contains(e.target)
|
||||||
) {
|
) {
|
||||||
console.log(
|
console.log(
|
||||||
'[TToastEnhanced] Focus left BuyOption after receiving focus - closing toast'
|
`[TToastEnhanced] Focus left ${type} after receiving focus - closing toast`
|
||||||
);
|
);
|
||||||
handleClose();
|
handleClose();
|
||||||
}
|
}
|
||||||
@@ -195,6 +211,22 @@ export default function TToastEnhanced({
|
|||||||
selectedPrdtId={selectedPrdtId}
|
selectedPrdtId={selectedPrdtId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : type === 'themeContents' ? (
|
||||||
|
<div ref={themeContentsRef}>
|
||||||
|
<ThemeContents
|
||||||
|
themeItems={themeItems}
|
||||||
|
setSelectedIndex={setSelectedIndex}
|
||||||
|
videoVerticalVisible={videoVerticalVisible}
|
||||||
|
currentVideoShowId={currentVideoShowId}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
handleItemFocus={handleItemFocus}
|
||||||
|
tabTitle={tabTitle}
|
||||||
|
panelInfo={panelInfo}
|
||||||
|
direction={direction}
|
||||||
|
version={version}
|
||||||
|
onThemeItemClose={handleClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={css.content}>
|
<div className={css.content}>
|
||||||
<div className={css.message}>{text}</div>
|
<div className={css.message}>{text}</div>
|
||||||
|
|||||||
@@ -115,6 +115,14 @@
|
|||||||
height: 400px;
|
height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.themeContents {
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 0.53) 0%, rgba(20.56, 4.68, 32.71, 0.53) 60%, rgba(199, 32, 84, 0) 98%), linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.40) 45%, rgba(0, 0, 0, 0.40) 100%), rgba(30, 30, 30, 0.80);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
// 컨텐츠 스타일 - 간단한 메시지만
|
// 컨텐츠 스타일 - 간단한 메시지만
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -923,16 +923,33 @@ export default function ProductAllSection({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleThemeItemButtonClick = useCallback(
|
const handleThemeItemButtonClick = useCallback(() => {
|
||||||
pipe(
|
dispatch(
|
||||||
() => setOpenThemeItemOverlay(true),
|
showToast({
|
||||||
tap(() => {
|
id: 'theme-contents-toast',
|
||||||
const timerId = setTimeout(() => Spotlight.focus('theme-close-button'), 0);
|
message: '',
|
||||||
timersRef.current.push(timerId);
|
type: 'themeContents',
|
||||||
|
duration: 0, // 수동으로 닫을 때까지 유지
|
||||||
|
position: 'bottom-center',
|
||||||
|
// ThemeContents 관련 props
|
||||||
|
themeItems: themeProducts,
|
||||||
|
setSelectedIndex,
|
||||||
|
videoVerticalVisible: false,
|
||||||
|
currentVideoShowId: null,
|
||||||
|
tabIndex: 0,
|
||||||
|
handleItemFocus: () => {},
|
||||||
|
tabTitle: ['THEME'],
|
||||||
|
panelInfo,
|
||||||
|
direction: 'horizontal',
|
||||||
|
version: 2,
|
||||||
|
onToastClose: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
Spotlight.focus('theme-open-button');
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
})
|
})
|
||||||
),
|
|
||||||
[setOpenThemeItemOverlay]
|
|
||||||
);
|
);
|
||||||
|
}, [dispatch, themeProducts, setSelectedIndex, panelInfo]);
|
||||||
|
|
||||||
const handleProductDetailsClick = useCallback(() => {
|
const handleProductDetailsClick = useCallback(() => {
|
||||||
dispatch(minimizeModalMedia());
|
dispatch(minimizeModalMedia());
|
||||||
@@ -1463,6 +1480,10 @@ export default function ProductAllSection({
|
|||||||
className={css.themeButton}
|
className={css.themeButton}
|
||||||
onClick={handleThemeItemButtonClick}
|
onClick={handleThemeItemButtonClick}
|
||||||
spotlightId="theme-open-button"
|
spotlightId="theme-open-button"
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleThemeItemButtonClick();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div>{$L('THEME ITEM')}</div>
|
<div>{$L('THEME ITEM')}</div>
|
||||||
<img src={arrowDownIcon} className={css.themeButtonIcon} />
|
<img src={arrowDownIcon} className={css.themeButtonIcon} />
|
||||||
|
|||||||
@@ -1,20 +1,55 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
|
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||||
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
|
||||||
import arrowDownIcon from '../../../../assets/images/icons/ic-arrow-down.svg';
|
import arrowDownIcon from '../../../../assets/images/icons/ic-arrow-down.svg';
|
||||||
import { updatePanel } from '../../../actions/panelActions';
|
import { updatePanel } from '../../../actions/panelActions';
|
||||||
import TButton from '../../../components/TButton/TButton';
|
import TButton from '../../../components/TButton/TButton';
|
||||||
import TVirtualGridList from '../../../components/TVirtualGridList/TVirtualGridList';
|
import { LOG_CONTEXT_NAME, 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 '../../PlayerPanel/PlayerItemCard/PlayerItemCard';
|
|
||||||
import ListEmptyContents from '../../PlayerPanel/PlayerTabContents/TabContents/ListEmptyContents/ListEmptyContents';
|
|
||||||
import css from './ThemeContents.module.less';
|
import css from './ThemeContents.module.less';
|
||||||
import { sendLogTotalRecommend } from '../../../actions/logActions';
|
import { sendLogTotalRecommend } from '../../../actions/logActions';
|
||||||
|
|
||||||
|
const Container = SpotlightContainerDecorator(
|
||||||
|
{
|
||||||
|
enterTo: 'default-element',
|
||||||
|
preserveId: true,
|
||||||
|
spotlightDirection: 'vertical', // THEME ITEM 버튼 -> 리스트(아래) 이동
|
||||||
|
spotlightRestrict: 'self-only',
|
||||||
|
leaveFor: {}, // arrow key로 빠져나가지 않도록 설정
|
||||||
|
},
|
||||||
|
'div'
|
||||||
|
);
|
||||||
|
|
||||||
|
const ItemsContainer = SpotlightContainerDecorator(
|
||||||
|
{
|
||||||
|
enterTo: 'default-element',
|
||||||
|
preserveId: true,
|
||||||
|
spotlightDirection: 'horizontal', // 리스트 내 좌/우 이동
|
||||||
|
spotlightRestrict: 'self-only',
|
||||||
|
},
|
||||||
|
'div'
|
||||||
|
);
|
||||||
|
|
||||||
|
const ButtonContainer = SpotlightContainerDecorator(
|
||||||
|
{
|
||||||
|
enterTo: 'default-element',
|
||||||
|
preserveId: true,
|
||||||
|
spotlightDirection: 'horizontal',
|
||||||
|
// 버튼에서 아래 리스트로 포커스 이동해야 하므로 self-only는 사용하지 않는다
|
||||||
|
},
|
||||||
|
'div'
|
||||||
|
);
|
||||||
|
|
||||||
|
const SpottableItemCard = Spottable({
|
||||||
|
allowDisabledFocus: true,
|
||||||
|
noAutoFocus: true,
|
||||||
|
})('div');
|
||||||
|
|
||||||
export default function ThemeContents({
|
export default function ThemeContents({
|
||||||
themeItems,
|
themeItems,
|
||||||
setSelectedIndex,
|
setSelectedIndex,
|
||||||
@@ -31,6 +66,74 @@ export default function ThemeContents({
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isClickBlocked = useRef(false);
|
const isClickBlocked = useRef(false);
|
||||||
const blockTimeoutRef = useRef(null);
|
const blockTimeoutRef = useRef(null);
|
||||||
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||||
|
|
||||||
|
// Mock 데이터
|
||||||
|
const mockItems = [
|
||||||
|
{
|
||||||
|
prdtId: 'mock-1',
|
||||||
|
prdtNm: 'Origami Removable Connecting Rack 2-pack',
|
||||||
|
prdtImgPath: 'https://placehold.co/120x120',
|
||||||
|
salePrice: '$32.98',
|
||||||
|
originalPrice: '$150.00',
|
||||||
|
patnrLogoPath: null,
|
||||||
|
patncNm: 'Partner 1',
|
||||||
|
showId: 'show-1',
|
||||||
|
catNm: 'Category',
|
||||||
|
energyLabels: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prdtId: 'mock-2',
|
||||||
|
prdtNm: 'Origami Removable Connecting Rack 2-pack',
|
||||||
|
prdtImgPath: 'https://placehold.co/120x120',
|
||||||
|
salePrice: '$32.98',
|
||||||
|
originalPrice: '$150.00',
|
||||||
|
patnrLogoPath: null,
|
||||||
|
patncNm: 'Partner 2',
|
||||||
|
showId: 'show-2',
|
||||||
|
catNm: 'Category',
|
||||||
|
energyLabels: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prdtId: 'mock-3',
|
||||||
|
prdtNm: 'Origami Removable Connecting Rack 2-pack',
|
||||||
|
prdtImgPath: 'https://placehold.co/120x120',
|
||||||
|
salePrice: '$32.98',
|
||||||
|
originalPrice: '$150.00',
|
||||||
|
patnrLogoPath: null,
|
||||||
|
patncNm: 'Partner 3',
|
||||||
|
showId: 'show-3',
|
||||||
|
catNm: 'Category',
|
||||||
|
energyLabels: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prdtId: 'mock-4',
|
||||||
|
prdtNm: 'Origami Removable Connecting Rack 2-pack',
|
||||||
|
prdtImgPath: 'https://placehold.co/120x120',
|
||||||
|
salePrice: '$32.98',
|
||||||
|
originalPrice: '$150.00',
|
||||||
|
patnrLogoPath: null,
|
||||||
|
patncNm: 'Partner 4',
|
||||||
|
showId: 'show-4',
|
||||||
|
catNm: 'Category',
|
||||||
|
energyLabels: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prdtId: 'mock-5',
|
||||||
|
prdtNm: 'Origami Removable Connecting Rack 2-pack',
|
||||||
|
prdtImgPath: 'https://placehold.co/120x120',
|
||||||
|
salePrice: '$32.98',
|
||||||
|
originalPrice: '$150.00',
|
||||||
|
patnrLogoPath: null,
|
||||||
|
patncNm: 'Partner 5',
|
||||||
|
showId: 'show-5',
|
||||||
|
catNm: 'Category',
|
||||||
|
energyLabels: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// themeItems가 비어있으면 mock 데이터 사용
|
||||||
|
const displayItems = themeItems && themeItems.length > 0 ? themeItems : mockItems;
|
||||||
|
|
||||||
const handleThemeItemButtonClick = useCallback(() => {
|
const handleThemeItemButtonClick = useCallback(() => {
|
||||||
if (onThemeItemClose) {
|
if (onThemeItemClose) {
|
||||||
@@ -38,17 +141,61 @@ export default function ThemeContents({
|
|||||||
}
|
}
|
||||||
}, [onThemeItemClose]);
|
}, [onThemeItemClose]);
|
||||||
|
|
||||||
const handleFocus = useCallback(
|
// cleanup useEffect
|
||||||
() => () => {
|
useEffect(() => {
|
||||||
if (handleItemFocus) {
|
return () => {
|
||||||
handleItemFocus(LOG_MENU.THEME_ITEMS);
|
if (blockTimeoutRef.current) {
|
||||||
|
clearTimeout(blockTimeoutRef.current);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
[handleItemFocus]
|
}, []);
|
||||||
);
|
|
||||||
|
|
||||||
const renderItem = useCallback(
|
// 토스트가 열리면 닫기(=THEME ITEM) 버튼부터 포커스되도록 보정
|
||||||
({ index, ...rest }) => {
|
useEffect(() => {
|
||||||
|
Spotlight.focus('theme-contents-close-button');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 키 이동 보정: 위/아래 이동 시 THEME ITEM 버튼 <-> 첫 번째 아이템으로 확실히 연결
|
||||||
|
useEffect(() => {
|
||||||
|
if (!displayItems || displayItems.length === 0) return;
|
||||||
|
|
||||||
|
const firstItemId = 'theme-toast-item-0';
|
||||||
|
Spotlight.set('theme-contents-close-button', {
|
||||||
|
next: { down: firstItemId },
|
||||||
|
});
|
||||||
|
|
||||||
|
displayItems.slice(0, 5).forEach((_, index) => {
|
||||||
|
const itemId = `theme-toast-item-${index}`;
|
||||||
|
Spotlight.set(itemId, {
|
||||||
|
next: { up: 'theme-contents-close-button' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [displayItems]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
className={css.toastWrapper}
|
||||||
|
spotlightId="theme-contents-container"
|
||||||
|
spotlightDefaultElement="theme-contents-close-button"
|
||||||
|
>
|
||||||
|
<ButtonContainer className={css.topButtonWrapper} spotlightId="theme-button-container">
|
||||||
|
<TButton
|
||||||
|
className={css.themeButton}
|
||||||
|
onClick={handleThemeItemButtonClick}
|
||||||
|
spotlightId="theme-contents-close-button"
|
||||||
|
data-spotlight-default
|
||||||
|
>
|
||||||
|
<div>{$L('THEME ITEM')}</div>
|
||||||
|
<img src={arrowDownIcon} className={css.themeButtonIcon} />
|
||||||
|
</TButton>
|
||||||
|
</ButtonContainer>
|
||||||
|
<ItemsContainer
|
||||||
|
className={css.itemsWrapper}
|
||||||
|
spotlightId="theme-items-container"
|
||||||
|
spotlightDefaultElement="theme-toast-item-0"
|
||||||
|
>
|
||||||
|
{displayItems && displayItems.length > 0 ? (
|
||||||
|
displayItems.slice(0, 5).map((item, index) => {
|
||||||
const {
|
const {
|
||||||
prdtId,
|
prdtId,
|
||||||
prdtNm,
|
prdtNm,
|
||||||
@@ -60,7 +207,9 @@ export default function ThemeContents({
|
|||||||
showId,
|
showId,
|
||||||
catNm,
|
catNm,
|
||||||
energyLabels,
|
energyLabels,
|
||||||
} = themeItems[index];
|
} = item;
|
||||||
|
|
||||||
|
const spotlightItemId = `theme-toast-item-${index}`;
|
||||||
|
|
||||||
const handleItemClick = () => {
|
const handleItemClick = () => {
|
||||||
const params = {
|
const params = {
|
||||||
@@ -75,14 +224,12 @@ export default function ThemeContents({
|
|||||||
};
|
};
|
||||||
dispatch(sendLogTotalRecommend(params));
|
dispatch(sendLogTotalRecommend(params));
|
||||||
|
|
||||||
// 중복클릭방지
|
|
||||||
if (isClickBlocked.current) {
|
if (isClickBlocked.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isClickBlocked.current = true;
|
isClickBlocked.current = true;
|
||||||
|
|
||||||
// 이전 타이머가 있으면 정리
|
|
||||||
if (blockTimeoutRef.current) {
|
if (blockTimeoutRef.current) {
|
||||||
clearTimeout(blockTimeoutRef.current);
|
clearTimeout(blockTimeoutRef.current);
|
||||||
}
|
}
|
||||||
@@ -108,91 +255,50 @@ export default function ThemeContents({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const productNameDangerouslySetInnerHTML = () => {
|
|
||||||
return prdtNm ? { __html: prdtNm } : { __html: '' };
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PlayerItemCard
|
<SpottableItemCard
|
||||||
{...rest}
|
|
||||||
key={prdtId}
|
key={prdtId}
|
||||||
imageAlt={prdtId}
|
className={`${css.itemCard} ${focusedIndex === index ? css.focused : ''}`}
|
||||||
logo={patnrLogoPath}
|
|
||||||
imageSource={prdtImgPath}
|
|
||||||
videoVerticalVisible={videoVerticalVisible}
|
|
||||||
productName={productNameDangerouslySetInnerHTML}
|
|
||||||
patnerName={patncNm}
|
|
||||||
salePrice={salePrice}
|
|
||||||
originalPrice={originalPrice}
|
|
||||||
energyLabels={energyLabels}
|
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
onFocus={handleFocus()}
|
spotlightId={spotlightItemId}
|
||||||
onSpotlightUp={
|
data-spotlight-default={index === 0}
|
||||||
version === 2 && index === 0
|
onKeyDown={(e) => {
|
||||||
? (e) => {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
// v2에서 첫 번째 아이템일 때 위로 가면 THEME ITEM 버튼으로
|
handleItemClick();
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
Spotlight.focus('below-tab-theme-button');
|
|
||||||
}
|
}
|
||||||
: undefined
|
}}
|
||||||
}
|
onFocus={() => {
|
||||||
type={TYPES.themeHorizontal}
|
setFocusedIndex(index);
|
||||||
spotlightId={`tabTheme-item-${index}`}
|
handleItemFocus?.(index);
|
||||||
version={version}
|
}}
|
||||||
/>
|
onBlur={() => setFocusedIndex(-1)}
|
||||||
);
|
onMouseEnter={() => Spotlight.focus(spotlightItemId)}
|
||||||
},
|
>
|
||||||
[
|
<img src={prdtImgPath} alt={prdtId} className={css.itemImage} />
|
||||||
themeItems,
|
<div className={css.itemInfo}>
|
||||||
currentVideoShowId,
|
<div className={css.itemName}>{prdtNm}</div>
|
||||||
isClickBlocked,
|
<div className={css.itemPrice}>
|
||||||
dispatch,
|
<span className={css.salePrice}>{salePrice}</span>
|
||||||
handleFocus,
|
<span className={css.originalPrice}>{originalPrice}</span>
|
||||||
version,
|
</div>
|
||||||
tabIndex,
|
{energyLabels && energyLabels.length > 0 && (
|
||||||
tabTitle,
|
<div className={css.energyLabels}>
|
||||||
panelInfo,
|
{energyLabels.map((label, labelIndex) => (
|
||||||
setSelectedIndex,
|
<div key={labelIndex} className={css.energyLabel}>
|
||||||
]
|
{/* 에너지 라벨 렌더링 */}
|
||||||
);
|
{label}
|
||||||
|
</div>
|
||||||
// cleanup useEffect
|
))}
|
||||||
useEffect(() => {
|
</div>
|
||||||
return () => {
|
|
||||||
if (blockTimeoutRef.current) {
|
|
||||||
clearTimeout(blockTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={css.container}>
|
|
||||||
{themeItems && themeItems.length > 0 ? (
|
|
||||||
<TVirtualGridList
|
|
||||||
dataSize={themeItems.length}
|
|
||||||
direction={direction}
|
|
||||||
renderItem={renderItem}
|
|
||||||
itemWidth={470}
|
|
||||||
itemHeight={180}
|
|
||||||
spacing={18}
|
|
||||||
noScrollByWheel={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ListEmptyContents tabIndex={tabIndex} />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={css.bottomButtonWrapper}>
|
</SpottableItemCard>
|
||||||
<TButton
|
);
|
||||||
className={css.themeButton}
|
})
|
||||||
onClick={handleThemeItemButtonClick}
|
) : (
|
||||||
spotlightId="below-tab-theme-button"
|
<div>No items</div>
|
||||||
>
|
)}
|
||||||
<div>{$L('THEME ITEM')}</div>
|
</ItemsContainer>
|
||||||
<img src={arrowDownIcon} className={css.themeButtonIcon} />
|
</Container>
|
||||||
</TButton>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,149 @@
|
|||||||
@import "../../../../style/CommonStyle.module.less";
|
@import "../../../style/CommonStyle.module.less";
|
||||||
@import "../../../../style/utils.module.less";
|
@import "../../../style/utils.module.less";
|
||||||
|
|
||||||
|
// Toast wrapper 스타일
|
||||||
|
.toastWrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 390px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 30px;
|
||||||
|
padding: 60px;
|
||||||
|
background: linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(0, 0, 0, 0.53) 0%,
|
||||||
|
rgba(20.56, 4.68, 32.71, 0.53) 60%,
|
||||||
|
rgba(199, 32, 84, 0) 98%
|
||||||
|
),
|
||||||
|
linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.4) 45%, rgba(0, 0, 0, 0.4) 100%),
|
||||||
|
rgba(30, 30, 30, 0.8);
|
||||||
|
overflow: visible; // 포커스 테두리가 잘리지 않도록
|
||||||
|
}
|
||||||
|
|
||||||
|
.topButtonWrapper {
|
||||||
|
width: 635px;
|
||||||
|
height: 60px;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemsWrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 18px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: visible; // 포커스 테두리 보존
|
||||||
|
flex: none;
|
||||||
|
padding: 4px; // 포커스 테두리(4px box-shadow) 표시 공간 확보
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemCard {
|
||||||
|
width: 470px;
|
||||||
|
height: 180px;
|
||||||
|
padding: 30px;
|
||||||
|
background: linear-gradient(0deg, 0%, 100%), #2C2C2C;
|
||||||
|
border-radius: 12px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.spotlight) {
|
||||||
|
box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused {
|
||||||
|
box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemImage {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
padding-left: 0.41px;
|
||||||
|
padding-right: 0.41px;
|
||||||
|
padding-top: 0.51px;
|
||||||
|
padding-bottom: 0.51px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemInfo {
|
||||||
|
flex: 1 1 0;
|
||||||
|
align-self: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemName {
|
||||||
|
align-self: stretch;
|
||||||
|
color: #EAEAEA;
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: @baseFont;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 25px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPrice {
|
||||||
|
padding-top: 1px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.salePrice {
|
||||||
|
color: #C70850;
|
||||||
|
font-size: 20px;
|
||||||
|
font-family: @baseFont;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 16.67;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.originalPrice {
|
||||||
|
color: #C2C2C2;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: @baseFont;
|
||||||
|
font-weight: 400;
|
||||||
|
text-decoration: line-through;
|
||||||
|
line-height: 16.67;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.energyLabels {
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 5px;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.energyLabel {
|
||||||
|
width: 54px;
|
||||||
|
height: 30px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -33,10 +177,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.themeButton {
|
.themeButton {
|
||||||
width: 100% !important;
|
width: 635px !important;
|
||||||
height: 60px !important;
|
height: 60px !important;
|
||||||
padding: 20px 30px !important;
|
padding: 20px 30px !important;
|
||||||
background: rgba(255, 255, 255, 0.05) !important;
|
background: #c72054 !important;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
border-radius: 6px !important;
|
border-radius: 6px !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user