From e44bcaf19f8690bd44578195551df985696b1a28 Mon Sep 17 00:00:00 2001 From: optrader Date: Sun, 23 Nov 2025 12:12:36 +0900 Subject: [PATCH] [251123] fix: DetailPanel ThemeContent-2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ• 컀밋 μ‹œκ°„: 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: μ½”λ“œ μ΅œμ ν™”λ‘œ μ„±λŠ₯ κ°œμ„  κΈ°λŒ€ --- .../ProductAllSection/ThemeItemCard.jsx | 42 +++ .../ThemeProduct/ThemeContents.jsx | 247 +++++++------- .../ThemeProduct/ThemeContents.module.less | 26 +- .../ThemeProduct/ThemeItemCard.figma.jsx | 322 ++++++++++++++++++ .../ThemeProduct/ThemeItemCard.jsx | 83 +++++ .../ThemeProduct/ThemeItemCard.module.less | 168 +++++++++ 6 files changed, 751 insertions(+), 137 deletions(-) create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ThemeItemCard.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.figma.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.jsx create mode 100644 com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.module.less diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ThemeItemCard.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ThemeItemCard.jsx new file mode 100644 index 00000000..f2641c76 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ProductAllSection/ThemeItemCard.jsx @@ -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 ( + +
+ {children} + {iconSrc && ( +
+ arrow down +
+ )} +
+
+ ); +} + +ThemeItemCard.propTypes = { + onClick: PropTypes.func, + onMouseDown: PropTypes.func, + spotlightId: PropTypes.string, + className: PropTypes.string, + children: PropTypes.node, + iconSrc: PropTypes.string, +}; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.jsx index 700b23b0..9897c544 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.jsx +++ b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.jsx @@ -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 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 { updatePanel } from '../../../actions/panelActions'; import TButton from '../../../components/TButton/TButton'; +import ThemeItemCard from './ThemeItemCard'; import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID, panel_names } from '../../../utils/Config'; import { $L } from '../../../utils/helperMethods'; import css from './ThemeContents.module.less'; @@ -25,30 +25,23 @@ const Container = SpotlightContainerDecorator( 'div' ); -const ItemsContainer = SpotlightContainerDecorator( - { - enterTo: 'default-element', - preserveId: true, - spotlightDirection: 'horizontal', // 리슀트 λ‚΄ 쒌/우 이동 - spotlightRestrict: 'self-only', - }, - 'div' -); - const ButtonContainer = SpotlightContainerDecorator( { - enterTo: 'default-element', + enterTo: 'last-focused', preserveId: true, spotlightDirection: 'horizontal', - // λ²„νŠΌμ—μ„œ μ•„λž˜ 리슀트둜 포컀슀 이동해야 ν•˜λ―€λ‘œ self-onlyλŠ” μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€ }, 'div' ); -const SpottableItemCard = Spottable({ - allowDisabledFocus: true, - noAutoFocus: true, -})('div'); +const ItemsContainer = SpotlightContainerDecorator( + { + enterTo: 'last-focused', + preserveId: true, + spotlightDirection: 'horizontal', + }, + 'div' +); export default function ThemeContents({ themeItems, @@ -66,7 +59,6 @@ export default function ThemeContents({ const dispatch = useDispatch(); const isClickBlocked = useRef(false); const blockTimeoutRef = useRef(null); - const [focusedIndex, setFocusedIndex] = useState(-1); // Mock 데이터 const mockItems = [ @@ -141,6 +133,95 @@ export default function ThemeContents({ } }, [onThemeItemClose]); + const renderItem = useCallback( + (item, index) => { + console.log('[Theme] renderItem index', index, 'displayItems size', displayItems?.length); + const { + prdtId, + prdtNm, + prdtImgPath, + salePrice, + originalPrice, + patnrLogoPath, + patncNm, + showId, + catNm, + energyLabels, + } = item; + + const spotlightItemId = `theme-toast-item-${index}`; + + const handleItemClick = () => { + const params = { + tabTitle: tabTitle[tabIndex], + productId: prdtId, + productTitle: prdtNm, + showType: panelInfo?.shptmBanrTpNm, + category: catNm, + partner: patncNm, + contextName: LOG_CONTEXT_NAME.PRODUCT, + messageId: LOG_MESSAGE_ID.CONTENTCLICK, + }; + dispatch(sendLogTotalRecommend(params)); + + if (isClickBlocked.current) { + return; + } + + isClickBlocked.current = true; + + if (blockTimeoutRef.current) { + clearTimeout(blockTimeoutRef.current); + } + + blockTimeoutRef.current = setTimeout(() => { + isClickBlocked.current = false; + blockTimeoutRef.current = null; + }, 600); + + if (!prdtId) return; + + setSelectedIndex(index); + dispatch( + updatePanel({ + name: panel_names.PLAYER_PANEL, + panelInfo: { + prdtId, + showId, + shptmBanrTpNm: 'THEME', + isUpdatedByClick: true, + }, + }) + ); + }; + + return ( + { + if (e.key === 'Enter' || e.key === ' ') { + handleItemClick(); + } + }} + onFocus={() => { + handleItemFocus?.(index); + }} + onMouseEnter={() => Spotlight.focus(spotlightItemId)} + /> + ); + }, + [displayItems, tabTitle, tabIndex, panelInfo, dispatch] + ); + // cleanup useEffect useEffect(() => { return () => { @@ -155,20 +236,37 @@ export default function ThemeContents({ Spotlight.focus('theme-contents-close-button'); }, []); - // ν‚€ 이동 보정: μœ„/μ•„λž˜ 이동 μ‹œ THEME ITEM λ²„νŠΌ <-> 첫 번째 μ•„μ΄ν…œμœΌλ‘œ ν™•μ‹€νžˆ μ—°κ²° + // ν‚€ 이동 보정: μœ„/μ•„λž˜/쒌우 이동 μ„€μ • 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.slice(0, 5).forEach((_, index) => { + // λͺ¨λ“  μ•„μ΄ν…œμ˜ μœ„/μ•„λž˜/쒌/우 λ„€λΉ„κ²Œμ΄μ…˜ μ„€μ • + displayItems.forEach((_, index) => { const itemId = `theme-toast-item-${index}`; - Spotlight.set(itemId, { - next: { up: 'theme-contents-close-button' }, - }); + 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]); @@ -195,106 +293,7 @@ export default function ThemeContents({ spotlightDefaultElement="theme-toast-item-0" > {displayItems && displayItems.length > 0 ? ( - displayItems.slice(0, 5).map((item, index) => { - const { - prdtId, - prdtNm, - prdtImgPath, - salePrice, - originalPrice, - patnrLogoPath, - patncNm, - showId, - catNm, - energyLabels, - } = item; - - const spotlightItemId = `theme-toast-item-${index}`; - - const handleItemClick = () => { - const params = { - tabTitle: tabTitle[tabIndex], - productId: prdtId, - productTitle: prdtNm, - showType: panelInfo?.shptmBanrTpNm, - category: catNm, - partner: patncNm, - contextName: LOG_CONTEXT_NAME.PRODUCT, - messageId: LOG_MESSAGE_ID.CONTENTCLICK, - }; - dispatch(sendLogTotalRecommend(params)); - - if (isClickBlocked.current) { - return; - } - - isClickBlocked.current = true; - - if (blockTimeoutRef.current) { - clearTimeout(blockTimeoutRef.current); - } - - blockTimeoutRef.current = setTimeout(() => { - isClickBlocked.current = false; - blockTimeoutRef.current = null; - }, 600); - - if (!prdtId) return; - - setSelectedIndex(index); - dispatch( - updatePanel({ - name: panel_names.PLAYER_PANEL, - panelInfo: { - prdtId, - showId, - shptmBanrTpNm: 'THEME', - isUpdatedByClick: true, - }, - }) - ); - }; - - return ( - { - if (e.key === 'Enter' || e.key === ' ') { - handleItemClick(); - } - }} - onFocus={() => { - setFocusedIndex(index); - handleItemFocus?.(index); - }} - onBlur={() => setFocusedIndex(-1)} - onMouseEnter={() => Spotlight.focus(spotlightItemId)} - > - {prdtId} -
-
{prdtNm}
-
- {salePrice} - {originalPrice} -
- {energyLabels && energyLabels.length > 0 && ( -
- {energyLabels.map((label, labelIndex) => ( -
- {/* μ—λ„ˆμ§€ 라벨 λ Œλ”λ§ */} - {label} -
- ))} -
- )} -
-
- ); - }) + displayItems.map((item, index) => renderItem(item, index)) ) : (
No items
)} diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.module.less index a1537559..61d4c7fb 100644 --- a/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.module.less +++ b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeContents.module.less @@ -4,7 +4,7 @@ // Toast wrapper μŠ€νƒ€μΌ .toastWrapper { width: 100%; - height: 390px; + height: 450px; // padding(120) + button(60) + gap(30) + items(240) = 450px display: inline-flex; flex-direction: column; justify-content: flex-start; @@ -31,6 +31,8 @@ } .itemsWrapper { + width: 100%; + height: 180px; display: inline-flex; flex-direction: row; gap: 18px; @@ -39,40 +41,38 @@ flex-wrap: nowrap; overflow-x: auto; overflow-y: visible; // 포컀슀 ν…Œλ‘λ¦¬ 보쑴 - flex: none; - padding: 4px; // 포컀슀 ν…Œλ‘λ¦¬(4px box-shadow) ν‘œμ‹œ 곡간 확보 + flex-shrink: 0; + scrollbar-width: none; // Firefox μŠ€ν¬λ‘€λ°” μˆ¨κΉ€ + + &::-webkit-scrollbar { + display: none; // WebKit μŠ€ν¬λ‘€λ°” μˆ¨κΉ€ + } } .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: inline-flex; + display: 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; - } - + &:focus, &:global(.spotlight) { box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED; + z-index: 2; } } -.focused { - box-shadow: 0 0 0 4px @PRIMARY_COLOR_RED; -} - .itemImage { width: 120px; height: 120px; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.figma.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.figma.jsx new file mode 100644 index 00000000..542f9231 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.figma.jsx @@ -0,0 +1,322 @@ +
+ +
+
+ Orlgami Removable Connecting Rack 2 -pack +
+
+
+ $32.98 +
+
+ $150.00 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.jsx b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.jsx new file mode 100644 index 00000000..8bcec792 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.jsx @@ -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 ( + + {prdtId} +
+
{prdtNm}
+
+ {salePrice} + {originalPrice} +
+ {energyLabels && energyLabels.length > 0 && ( +
+ {energyLabels.map((label, labelIndex) => ( +
+ {label} +
+ ))} +
+ )} +
+
+ ); +} + +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, +}; diff --git a/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.module.less b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.module.less new file mode 100644 index 00000000..69250365 --- /dev/null +++ b/com.twin.app.shoptime/src/views/DetailPanel/ThemeProduct/ThemeItemCard.module.less @@ -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; + } + } +} \ No newline at end of file