[251123] fix: DetailPanel ThemeContent-2
🕐 커밋 시간: 2025. 11. 23. 12:12:36 📊 변경 통계: • 총 파일: 6개 • 추가: +132줄 • 삭제: -133줄 📁 추가된 파일: + com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ThemeItemCard.jsx + com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.figma.jsx + com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.jsx + com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.module.less 📝 수정된 파일: ~ 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/views/DetailPanel/ThemeProduct/ThemeContents.jsx (javascript): ✅ Added: handleItemClick() 🔄 Modified: SpotlightContainerDecorator() ❌ Deleted: handleItemClick() 📄 com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.jsx (javascript): ✅ Added: handleFocus(), handleBlur() 📄 com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.module.less (unknown): ✅ Added: gradient(), global() Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
import css from './ThemeItemCard.module.less';
|
||||||
|
|
||||||
|
const SpottableDiv = Spottable('div');
|
||||||
|
|
||||||
|
export default function ThemeItemCard({
|
||||||
|
onClick,
|
||||||
|
onMouseDown,
|
||||||
|
spotlightId = 'theme-open-button',
|
||||||
|
className = '',
|
||||||
|
children = 'THEME ITEM',
|
||||||
|
iconSrc = null,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SpottableDiv
|
||||||
|
className={`${css.themeItemCard} ${className}`}
|
||||||
|
onClick={onClick}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
spotlightId={spotlightId}
|
||||||
|
>
|
||||||
|
<div className={css.themeItemCardContent}>
|
||||||
|
<span className={css.themeItemCardText}>{children}</span>
|
||||||
|
{iconSrc && (
|
||||||
|
<div className={css.themeItemCardIcon}>
|
||||||
|
<img src={iconSrc} alt="arrow down" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SpottableDiv>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeItemCard.propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onMouseDown: PropTypes.func,
|
||||||
|
spotlightId: PropTypes.string,
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
iconSrc: PropTypes.string,
|
||||||
|
};
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef } 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 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 ThemeItemCard from './ThemeItemCard';
|
||||||
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config';
|
import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config';
|
||||||
import { $L } from '../../../utils/helperMethods';
|
import { $L } from '../../../utils/helperMethods';
|
||||||
import css from './ThemeContents.module.less';
|
import css from './ThemeContents.module.less';
|
||||||
@@ -25,30 +25,23 @@ const Container = SpotlightContainerDecorator(
|
|||||||
'div'
|
'div'
|
||||||
);
|
);
|
||||||
|
|
||||||
const ItemsContainer = SpotlightContainerDecorator(
|
|
||||||
{
|
|
||||||
enterTo: 'default-element',
|
|
||||||
preserveId: true,
|
|
||||||
spotlightDirection: 'horizontal', // 리스트 내 좌/우 이동
|
|
||||||
spotlightRestrict: 'self-only',
|
|
||||||
},
|
|
||||||
'div'
|
|
||||||
);
|
|
||||||
|
|
||||||
const ButtonContainer = SpotlightContainerDecorator(
|
const ButtonContainer = SpotlightContainerDecorator(
|
||||||
{
|
{
|
||||||
enterTo: 'default-element',
|
enterTo: 'last-focused',
|
||||||
preserveId: true,
|
preserveId: true,
|
||||||
spotlightDirection: 'horizontal',
|
spotlightDirection: 'horizontal',
|
||||||
// 버튼에서 아래 리스트로 포커스 이동해야 하므로 self-only는 사용하지 않는다
|
|
||||||
},
|
},
|
||||||
'div'
|
'div'
|
||||||
);
|
);
|
||||||
|
|
||||||
const SpottableItemCard = Spottable({
|
const ItemsContainer = SpotlightContainerDecorator(
|
||||||
allowDisabledFocus: true,
|
{
|
||||||
noAutoFocus: true,
|
enterTo: 'last-focused',
|
||||||
})('div');
|
preserveId: true,
|
||||||
|
spotlightDirection: 'horizontal',
|
||||||
|
},
|
||||||
|
'div'
|
||||||
|
);
|
||||||
|
|
||||||
export default function ThemeContents({
|
export default function ThemeContents({
|
||||||
themeItems,
|
themeItems,
|
||||||
@@ -66,7 +59,6 @@ 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 데이터
|
// Mock 데이터
|
||||||
const mockItems = [
|
const mockItems = [
|
||||||
@@ -141,61 +133,9 @@ export default function ThemeContents({
|
|||||||
}
|
}
|
||||||
}, [onThemeItemClose]);
|
}, [onThemeItemClose]);
|
||||||
|
|
||||||
// cleanup useEffect
|
const renderItem = useCallback(
|
||||||
useEffect(() => {
|
(item, index) => {
|
||||||
return () => {
|
console.log('[Theme] renderItem index', index, 'displayItems size', displayItems?.length);
|
||||||
if (blockTimeoutRef.current) {
|
|
||||||
clearTimeout(blockTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 토스트가 열리면 닫기(=THEME ITEM) 버튼부터 포커스되도록 보정
|
|
||||||
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,
|
||||||
@@ -256,45 +196,104 @@ export default function ThemeContents({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SpottableItemCard
|
<ThemeItemCard
|
||||||
key={prdtId}
|
key={prdtId || `theme-item-${index}`}
|
||||||
className={`${css.itemCard} ${focusedIndex === index ? css.focused : ''}`}
|
prdtId={prdtId}
|
||||||
|
prdtNm={prdtNm}
|
||||||
|
prdtImgPath={prdtImgPath}
|
||||||
|
salePrice={salePrice}
|
||||||
|
originalPrice={originalPrice}
|
||||||
|
energyLabels={energyLabels}
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
spotlightId={spotlightItemId}
|
spotlightId={spotlightItemId}
|
||||||
data-spotlight-default={index === 0}
|
dataSpotlightDefault={index === 0}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
handleItemClick();
|
handleItemClick();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
setFocusedIndex(index);
|
|
||||||
handleItemFocus?.(index);
|
handleItemFocus?.(index);
|
||||||
}}
|
}}
|
||||||
onBlur={() => setFocusedIndex(-1)}
|
|
||||||
onMouseEnter={() => Spotlight.focus(spotlightItemId)}
|
onMouseEnter={() => Spotlight.focus(spotlightItemId)}
|
||||||
>
|
/>
|
||||||
<img src={prdtImgPath} alt={prdtId} className={css.itemImage} />
|
|
||||||
<div className={css.itemInfo}>
|
|
||||||
<div className={css.itemName}>{prdtNm}</div>
|
|
||||||
<div className={css.itemPrice}>
|
|
||||||
<span className={css.salePrice}>{salePrice}</span>
|
|
||||||
<span className={css.originalPrice}>{originalPrice}</span>
|
|
||||||
</div>
|
|
||||||
{energyLabels && energyLabels.length > 0 && (
|
|
||||||
<div className={css.energyLabels}>
|
|
||||||
{energyLabels.map((label, labelIndex) => (
|
|
||||||
<div key={labelIndex} className={css.energyLabel}>
|
|
||||||
{/* 에너지 라벨 렌더링 */}
|
|
||||||
{label}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</SpottableItemCard>
|
|
||||||
);
|
);
|
||||||
})
|
},
|
||||||
|
[displayItems, tabTitle, tabIndex, panelInfo, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
// cleanup useEffect
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (blockTimeoutRef.current) {
|
||||||
|
clearTimeout(blockTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 토스트가 열리면 닫기(=THEME ITEM) 버튼부터 포커스되도록 보정
|
||||||
|
useEffect(() => {
|
||||||
|
Spotlight.focus('theme-contents-close-button');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 키 이동 보정: 위/아래/좌우 이동 설정
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[Theme] focus mapping displayItems', displayItems, displayItems?.length);
|
||||||
|
if (!displayItems || displayItems.length === 0) return;
|
||||||
|
|
||||||
|
const firstItemId = 'theme-toast-item-0';
|
||||||
|
const lastIndex = displayItems.length - 1;
|
||||||
|
|
||||||
|
// THEME ITEM 버튼과 첫 번째 아이템 연결
|
||||||
|
Spotlight.set('theme-contents-close-button', {
|
||||||
|
next: { down: firstItemId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 아이템의 위/아래/좌/우 네비게이션 설정
|
||||||
|
displayItems.forEach((_, index) => {
|
||||||
|
const itemId = `theme-toast-item-${index}`;
|
||||||
|
const nextItemId = index < lastIndex ? `theme-toast-item-${index + 1}` : null;
|
||||||
|
const prevItemId = index > 0 ? `theme-toast-item-${index - 1}` : null;
|
||||||
|
|
||||||
|
const nextConfig = {
|
||||||
|
up: 'theme-contents-close-button',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nextItemId) {
|
||||||
|
nextConfig.right = nextItemId;
|
||||||
|
}
|
||||||
|
if (prevItemId) {
|
||||||
|
nextConfig.left = prevItemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Spotlight.set(itemId, { next: nextConfig });
|
||||||
|
});
|
||||||
|
}, [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.map((item, index) => renderItem(item, index))
|
||||||
) : (
|
) : (
|
||||||
<div>No items</div>
|
<div>No items</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// Toast wrapper 스타일
|
// Toast wrapper 스타일
|
||||||
.toastWrapper {
|
.toastWrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 390px;
|
height: 450px; // padding(120) + button(60) + gap(30) + items(240) = 450px
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -31,6 +31,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.itemsWrapper {
|
.itemsWrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
@@ -39,40 +41,38 @@
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: visible; // 포커스 테두리 보존
|
overflow-y: visible; // 포커스 테두리 보존
|
||||||
flex: none;
|
flex-shrink: 0;
|
||||||
padding: 4px; // 포커스 테두리(4px box-shadow) 표시 공간 확보
|
scrollbar-width: none; // Firefox 스크롤바 숨김
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none; // WebKit 스크롤바 숨김
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.itemCard {
|
.itemCard {
|
||||||
width: 470px;
|
width: 470px;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
background: linear-gradient(0deg, 0%, 100%), #2C2C2C;
|
background: linear-gradient(0deg, 0%, 100%), #2C2C2C;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
display: inline-flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&:focus {
|
&:focus,
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:global(.spotlight) {
|
&:global(.spotlight) {
|
||||||
box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED;
|
box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.focused {
|
|
||||||
box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED;
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemImage {
|
.itemImage {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
|
|||||||
@@ -0,0 +1,322 @@
|
|||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '470px',
|
||||||
|
height: '180px',
|
||||||
|
padding: 30,
|
||||||
|
background: 'linear-gradient(0deg, 0%, 100%), #2C2C2C',
|
||||||
|
borderRadius: 12,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 15,
|
||||||
|
display: 'inline-flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
paddingLeft: 0.41,
|
||||||
|
paddingRight: 0.41,
|
||||||
|
paddingTop: 0.51,
|
||||||
|
paddingBottom: 0.51,
|
||||||
|
}}
|
||||||
|
src="https://placehold.co/120x120"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: '1 1 0',
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: 10,
|
||||||
|
display: 'inline-flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
alignSelf: 'stretch',
|
||||||
|
color: '#EAEAEA',
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 'LG Smart UI',
|
||||||
|
fontWeight: '400',
|
||||||
|
lineHeight: 25,
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Orlgami Removable Connecting Rack 2 -pack
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
paddingTop: 1,
|
||||||
|
paddingBottom: 1,
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 5,
|
||||||
|
display: 'inline-flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: '#C70850',
|
||||||
|
fontSize: 20,
|
||||||
|
fontFamily: 'LG Smart UI',
|
||||||
|
fontWeight: '700',
|
||||||
|
lineHeight: 16.67,
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
$32.98
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: '#C2C2C2',
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: 'LG Smart UI',
|
||||||
|
fontWeight: '400',
|
||||||
|
textDecoration: 'line-through',
|
||||||
|
lineHeight: 16.67,
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
$150.00
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{ justifyContent: 'flex-end', alignItems: 'flex-end', gap: 5, display: 'inline-flex' }}
|
||||||
|
>
|
||||||
|
<div style={{ width: 54, height: 30, position: 'relative', overflow: 'hidden' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 12.21,
|
||||||
|
height: 28.05,
|
||||||
|
left: 41.07,
|
||||||
|
top: 1.03,
|
||||||
|
position: 'absolute',
|
||||||
|
background: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40.83,
|
||||||
|
height: 27.81,
|
||||||
|
left: 1.11,
|
||||||
|
top: 1.05,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#BFD730',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 51.71,
|
||||||
|
height: 28.23,
|
||||||
|
left: 1.18,
|
||||||
|
top: 0.84,
|
||||||
|
position: 'absolute',
|
||||||
|
outline: '1.37px #231F20 solid',
|
||||||
|
outlineOffset: '-0.68px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 6.42,
|
||||||
|
height: 7.05,
|
||||||
|
left: 44.28,
|
||||||
|
top: 2.81,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#231F20',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 3.14,
|
||||||
|
height: 1.57,
|
||||||
|
left: 45.94,
|
||||||
|
top: 12.04,
|
||||||
|
position: 'absolute',
|
||||||
|
outline: '1.46px #231F20 solid',
|
||||||
|
outlineOffset: '-0.73px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 5.44,
|
||||||
|
height: 7.12,
|
||||||
|
left: 44.77,
|
||||||
|
top: 19.99,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#231F20',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ width: 11.7, height: 13.88, left: 21.49, top: 8.02, position: 'absolute' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 8.95,
|
||||||
|
height: 13.52,
|
||||||
|
left: 1.37,
|
||||||
|
top: 0.18,
|
||||||
|
position: 'absolute',
|
||||||
|
background: 'white',
|
||||||
|
outline: '0.68px #231F20 solid',
|
||||||
|
outlineOffset: '-0.34px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 54, height: 30, position: 'relative', overflow: 'hidden' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 12.21,
|
||||||
|
height: 28.05,
|
||||||
|
left: 41.07,
|
||||||
|
top: 1.03,
|
||||||
|
position: 'absolute',
|
||||||
|
background: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40.83,
|
||||||
|
height: 27.81,
|
||||||
|
left: 1.45,
|
||||||
|
top: 1.05,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#F37021',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 51.71,
|
||||||
|
height: 28.23,
|
||||||
|
left: 1.52,
|
||||||
|
top: 0.84,
|
||||||
|
position: 'absolute',
|
||||||
|
outline: '1.37px #231F20 solid',
|
||||||
|
outlineOffset: '-0.68px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 6.42,
|
||||||
|
height: 7.05,
|
||||||
|
left: 44.62,
|
||||||
|
top: 2.81,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#231F20',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 3.14,
|
||||||
|
height: 1.57,
|
||||||
|
left: 46.28,
|
||||||
|
top: 12.04,
|
||||||
|
position: 'absolute',
|
||||||
|
outline: '1.46px #231F20 solid',
|
||||||
|
outlineOffset: '-0.73px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 5.44,
|
||||||
|
height: 7.12,
|
||||||
|
left: 45.11,
|
||||||
|
top: 19.99,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#231F20',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ width: 11.7, height: 13.88, left: 21.49, top: 8.02, position: 'absolute' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 7.13,
|
||||||
|
height: 13.81,
|
||||||
|
left: 2.29,
|
||||||
|
top: 0.04,
|
||||||
|
position: 'absolute',
|
||||||
|
background: 'white',
|
||||||
|
outline: '0.68px #231F20 solid',
|
||||||
|
outlineOffset: '-0.34px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 54, height: 30, position: 'relative', overflow: 'hidden' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 12.21,
|
||||||
|
height: 28.05,
|
||||||
|
left: 41.07,
|
||||||
|
top: 1.03,
|
||||||
|
position: 'absolute',
|
||||||
|
background: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 40.83,
|
||||||
|
height: 27.81,
|
||||||
|
left: 1.45,
|
||||||
|
top: 1.05,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#00A651',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 51.71,
|
||||||
|
height: 28.23,
|
||||||
|
left: 1.52,
|
||||||
|
top: 0.84,
|
||||||
|
position: 'absolute',
|
||||||
|
outline: '1.37px #231F20 solid',
|
||||||
|
outlineOffset: '-0.68px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 6.42,
|
||||||
|
height: 7.05,
|
||||||
|
left: 44.62,
|
||||||
|
top: 2.81,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#231F20',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 3.14,
|
||||||
|
height: 1.57,
|
||||||
|
left: 46.28,
|
||||||
|
top: 12.04,
|
||||||
|
position: 'absolute',
|
||||||
|
outline: '1.46px #231F20 solid',
|
||||||
|
outlineOffset: '-0.73px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 5.44,
|
||||||
|
height: 7.12,
|
||||||
|
left: 45.11,
|
||||||
|
top: 19.99,
|
||||||
|
position: 'absolute',
|
||||||
|
background: '#231F20',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: 11.7,
|
||||||
|
height: 13.88,
|
||||||
|
left: 21.49,
|
||||||
|
top: 8.01,
|
||||||
|
position: 'absolute',
|
||||||
|
background: 'white',
|
||||||
|
outline: '0.68px #231F20 solid',
|
||||||
|
outlineOffset: '-0.34px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Spotlight from '@enact/spotlight';
|
||||||
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
import css from './ThemeItemCard.module.less';
|
||||||
|
|
||||||
|
const SpottableDiv = Spottable('div');
|
||||||
|
|
||||||
|
export default function ThemeItemCard({
|
||||||
|
prdtId,
|
||||||
|
prdtNm,
|
||||||
|
prdtImgPath,
|
||||||
|
salePrice,
|
||||||
|
originalPrice,
|
||||||
|
energyLabels,
|
||||||
|
onClick,
|
||||||
|
onFocus,
|
||||||
|
onMouseEnter,
|
||||||
|
onKeyDown,
|
||||||
|
spotlightId,
|
||||||
|
dataSpotlightDefault,
|
||||||
|
onFocused,
|
||||||
|
}) {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
|
const handleFocus = (e) => {
|
||||||
|
setIsFocused(true);
|
||||||
|
onFocus?.(e);
|
||||||
|
onFocused?.(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (e) => {
|
||||||
|
setIsFocused(false);
|
||||||
|
onFocused?.(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SpottableDiv
|
||||||
|
className={`${css.itemCard} ${isFocused ? css.focused : ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
spotlightId={spotlightId}
|
||||||
|
data-spotlight-default={dataSpotlightDefault}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onMouseEnter={onMouseEnter}
|
||||||
|
>
|
||||||
|
<img src={prdtImgPath} alt={prdtId} className={css.itemImage} />
|
||||||
|
<div className={css.itemInfo}>
|
||||||
|
<div className={css.itemName}>{prdtNm}</div>
|
||||||
|
<div className={css.itemPrice}>
|
||||||
|
<span className={css.salePrice}>{salePrice}</span>
|
||||||
|
<span className={css.originalPrice}>{originalPrice}</span>
|
||||||
|
</div>
|
||||||
|
{energyLabels && energyLabels.length > 0 && (
|
||||||
|
<div className={css.energyLabels}>
|
||||||
|
{energyLabels.map((label, labelIndex) => (
|
||||||
|
<div key={labelIndex} className={css.energyLabel}>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SpottableDiv>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeItemCard.propTypes = {
|
||||||
|
prdtId: PropTypes.string,
|
||||||
|
prdtNm: PropTypes.string,
|
||||||
|
prdtImgPath: PropTypes.string,
|
||||||
|
salePrice: PropTypes.string,
|
||||||
|
originalPrice: PropTypes.string,
|
||||||
|
energyLabels: PropTypes.array,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
onFocus: PropTypes.func,
|
||||||
|
onFocused: PropTypes.func,
|
||||||
|
onMouseEnter: PropTypes.func,
|
||||||
|
onKeyDown: PropTypes.func,
|
||||||
|
spotlightId: PropTypes.string,
|
||||||
|
dataSpotlightDefault: PropTypes.bool,
|
||||||
|
};
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
@import "../../../style/CommonStyle.module.less";
|
||||||
|
@import "../../../style/utils.module.less";
|
||||||
|
|
||||||
|
.itemCard {
|
||||||
|
width: 470px;
|
||||||
|
height: 180px;
|
||||||
|
padding: 30px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: linear-gradient(0deg, 0%, 100%), #2C2C2C;
|
||||||
|
border-radius: 12px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.focused {
|
||||||
|
outline: 4px solid @PRIMARY_COLOR_RED;
|
||||||
|
outline-offset: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:global(.spotlight) {
|
||||||
|
outline: 4px solid @PRIMARY_COLOR_RED;
|
||||||
|
outline-offset: -4px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Button 스타일 (기존)
|
||||||
|
.themeItemCard {
|
||||||
|
align-self: stretch;
|
||||||
|
height: 60px;
|
||||||
|
background: rgba(255, 255, 255, 0.05); // 기본 회색 배경
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: @PRIMARY_COLOR_RED; // 포커스시만 빨간색
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border: 4px solid #4f172c;
|
||||||
|
background: #4f172c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 내부 콘텐츠 컨테이너
|
||||||
|
.themeItemCardContent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px; // 텍스트와 아이콘 사이 간격
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 텍스트 스타일
|
||||||
|
.themeItemCardText {
|
||||||
|
color: white;
|
||||||
|
font-size: 25px;
|
||||||
|
font-family: 'LG Smart UI';
|
||||||
|
font-weight: 400; // Regular (Bold가 아님)
|
||||||
|
line-height: 35;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1; // 남은 공간 차지
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 아이콘 스타일
|
||||||
|
.themeItemCardIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 26.25px;
|
||||||
|
height: 15.63px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user