diff --git a/com.twin.app.shoptime/src/components/TabLayout/TabItemSub.jsx b/com.twin.app.shoptime/src/components/TabLayout/TabItemSub.jsx index 9effcc9a..134453f7 100644 --- a/com.twin.app.shoptime/src/components/TabLayout/TabItemSub.jsx +++ b/com.twin.app.shoptime/src/components/TabLayout/TabItemSub.jsx @@ -1,18 +1,18 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from 'react'; -import classNames from "classnames"; -import compose from "ramda/src/compose"; +import classNames from 'classnames'; +import compose from 'ramda/src/compose'; -import { Job } from "@enact/core/util"; -import { Marquee, MarqueeController } from "@enact/sandstone/Marquee"; -import Spottable from "@enact/spotlight/Spottable"; +import { Job } from '@enact/core/util'; +import { Marquee, MarqueeController } from '@enact/sandstone/Marquee'; +import Spottable from '@enact/spotlight/Spottable'; -import css from "./TabItemSub.module.less"; -import { sendLogTotalRecommend } from "../../actions/logActions"; -import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID } from "../../utils/Config"; -import { useDispatch } from "react-redux"; +import css from './TabItemSub.module.less'; +import { sendLogTotalRecommend } from '../../actions/logActions'; +import { LOG_CONTEXT_NAME, LOG_MESSAGE_ID } from '../../utils/Config'; +import { useDispatch } from 'react-redux'; -const SpottableComponent = Spottable("div"); +const SpottableComponent = Spottable('div'); const TabItemBase = ({ selected = false, @@ -34,6 +34,7 @@ const TabItemBase = ({ setSelectedSubItemId, setSelectedSubIndex, label, + icons, ...rest }) => { const [focused, setFocused] = useState(false); @@ -43,7 +44,7 @@ const TabItemBase = ({ const _onClick = useCallback( (ev) => { - const subtitle = title.split("-")[0]; + const subtitle = title.split('-')[0]; const buttonTitle = patncNm ? patncNm : subtitle; clearPressedJob.current.start(() => { if (itemId) { @@ -63,7 +64,7 @@ const TabItemBase = ({ contextName: LOG_CONTEXT_NAME.GNB, messageId: LOG_MESSAGE_ID.GNB, buttonTitle: buttonTitle, - buttonId: `GNB_CLICK_${buttonTitle.toUpperCase().replace(" ", "_")}`, + buttonId: `GNB_CLICK_${buttonTitle.toUpperCase().replace(' ', '_')}`, }) ); }); @@ -88,7 +89,7 @@ const TabItemBase = ({ const onKeyDown = useCallback( (event) => { - if (event.key === "ArrowRight") { + if (event.key === 'ArrowRight') { _onClick(); } }, @@ -105,9 +106,7 @@ const TabItemBase = ({ focused && selected && css.selectedFocus )} > -
+

{patncNm}

@@ -117,21 +116,29 @@ const TabItemBase = ({ }, [path, focused]); const TextComponent = useCallback(() => { - const subtitle = title.split("-")[0]; + const subtitle = title.split('-')[0]; + const IconComponent = icons; return ( <> {subtitle && ( - - {subtitle} - +
+ {IconComponent && ( + + + + )} + + {subtitle} + +
)} ); - }, [title, focused, expanded]); + }, [title, focused, expanded, icons, selected]); delete rest.hasChildren; delete rest.getChildren; @@ -154,9 +161,9 @@ const TabItemBase = ({ aria-label={ patncNm ? selected && path - ? "Selected Channel " + patncNm + " button " + label - : "Channel " + patncNm + " button " + label - : title.split("-")[0] + " Button " + label + ? 'Selected Channel ' + patncNm + ' button ' + label + : 'Channel ' + patncNm + ' button ' + label + : title.split('-')[0] + ' Button ' + label } >
diff --git a/com.twin.app.shoptime/src/components/TabLayout/TabItemSub.module.less b/com.twin.app.shoptime/src/components/TabLayout/TabItemSub.module.less index aa9d09cf..ff809bf0 100644 --- a/com.twin.app.shoptime/src/components/TabLayout/TabItemSub.module.less +++ b/com.twin.app.shoptime/src/components/TabLayout/TabItemSub.module.less @@ -92,6 +92,19 @@ } } + .textWithIcon { + display: flex; + align-items: center; + gap: 12px; + } + + .iconWrapper { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + .text { height: 42px; line-height: 1.2; diff --git a/com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx b/com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx index a7c2c12a..414f6b35 100644 --- a/com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx +++ b/com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx @@ -31,6 +31,7 @@ import MyPageIcon from './iconComponents/MyPageIcon'; import OnSaleIcon from './iconComponents/OnSaleIcon'; import SearchIcon from './iconComponents/SearchIcon'; import TrendingNowIcon from './iconComponents/TrendingNowIcon'; +import VoiceIcon from './iconComponents/VoiceIcon'; import CartIcon from './iconComponents/CartIcon'; import TabItem from './TabItem'; import TabItemSub from './TabItemSub'; @@ -99,6 +100,7 @@ const PANELS_HAS_TAB = [ panel_names.MY_PAGE_PANEL, panel_names.ON_SALE_PANEL, panel_names.SEARCH_PANEL, + // VOICE_PANEL은 서브메뉴로만 접근되므로 제외 panel_names.TRENDING_NOW_PANEL, ]; @@ -230,9 +232,18 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) { result = [ { icons: SearchIcon, + id: 'search', + title: 'Search', spotlightId: 'spotlight_search', target: [{ name: panel_names.SEARCH_PANEL }], }, + { + icons: VoiceIcon, + id: 'voice', + title: 'Voice', + spotlightId: 'spotlight_voice', + target: [{ name: panel_names.VOICE_PANEL }], + }, ]; break; @@ -297,19 +308,27 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) { const menuInfo = getMenuData(currentKey || 'GNB'); for (let j = 0; j < menuInfo.length; j++) { - if (![10600, 10500, 10300].includes(currentKey)) { + if (![10600, 10500, 10300, 10700].includes(currentKey)) { menuItems[i].target = menuInfo[j].target; } - menuItems[i].spotlightId = menuInfo[j].spotlightId; - menuItems[i].icons = menuInfo[j].icons; - if ([10600, 10500, 10300].includes(currentKey)) { + // 10700(Search)의 경우 메인 아이콘은 SearchIcon으로 고정 + if (currentKey === 10700) { + menuItems[i].icons = SearchIcon; + menuItems[i].spotlightId = 'spotlight_search'; + } else { + menuItems[i].spotlightId = menuInfo[j].spotlightId; + menuItems[i].icons = menuInfo[j].icons; + } + + if ([10600, 10500, 10300, 10700].includes(currentKey)) { menuItems[i].children = menuInfo.map((item) => ({ id: item.id, title: item.title, path: item.path, patncNm: item.patncNm, target: item.target, + icons: item.icons, spotlightId: `secondDepth-${item.id}`, })); } diff --git a/com.twin.app.shoptime/src/components/TabLayout/iconComponents/VoiceIcon.jsx b/com.twin.app.shoptime/src/components/TabLayout/iconComponents/VoiceIcon.jsx new file mode 100644 index 00000000..85a254b2 --- /dev/null +++ b/com.twin.app.shoptime/src/components/TabLayout/iconComponents/VoiceIcon.jsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { scaleW } from '../../../utils/helperMethods'; +import useConvertThemeColor from './useConvertThemeColor'; + +const VoiceIcon = ({ iconType = 'normal' }) => { + const themeColor = useConvertThemeColor({ iconType }); + return ( + + + + + ); +}; + +export default VoiceIcon; diff --git a/com.twin.app.shoptime/src/utils/Config.js b/com.twin.app.shoptime/src/utils/Config.js index f9b20659..b84409bc 100644 --- a/com.twin.app.shoptime/src/utils/Config.js +++ b/com.twin.app.shoptime/src/utils/Config.js @@ -16,6 +16,7 @@ export const panel_names = { MY_PAGE_PANEL: 'mypagepanel', CATEGORY_PANEL: 'categorypanel', SEARCH_PANEL: 'searchpanel', + VOICE_PANEL: 'voicepanel', ON_SALE_PANEL: 'onsalepanel', TRENDING_NOW_PANEL: 'trendingnowpanel', HOT_PICKS_PANEL: 'hotpickpanel', diff --git a/com.twin.app.shoptime/src/views/MainView/MainView.jsx b/com.twin.app.shoptime/src/views/MainView/MainView.jsx index 11c92b77..45f52abe 100644 --- a/com.twin.app.shoptime/src/views/MainView/MainView.jsx +++ b/com.twin.app.shoptime/src/views/MainView/MainView.jsx @@ -70,6 +70,7 @@ import OnSalePanel from '../OnSalePanel/OnSalePanel'; import PlayerPanel from '../PlayerPanel/PlayerPanel'; import PlayerPanelNew from '../PlayerPanel/PlayerPanel.new'; import SearchPanel from '../SearchPanel/SearchPanel'; +import VoicePanel from '../VoicePanel/VoicePanel'; import ThemeCurationPanel from '../ThemeCurationPanel/ThemeCurationPanel'; import TrendingNowPanel from '../TrendingNowPanel/TrendingNowPanel'; import UserReviewPanel from '../UserReview/UserReviewPanel'; @@ -90,6 +91,7 @@ const panelMap = { [Config.panel_names.MY_PAGE_PANEL]: MyPagePanel, [Config.panel_names.CATEGORY_PANEL]: CategoryPanel, [Config.panel_names.SEARCH_PANEL]: SearchPanel, + [Config.panel_names.VOICE_PANEL]: VoicePanel, [Config.panel_names.ON_SALE_PANEL]: OnSalePanel, [Config.panel_names.TRENDING_NOW_PANEL]: TrendingNowPanel, [Config.panel_names.HOT_PICKS_PANEL]: HotPicksPanel, diff --git a/com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.jsx b/com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.jsx new file mode 100644 index 00000000..5e925dc5 --- /dev/null +++ b/com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.jsx @@ -0,0 +1,65 @@ +import React, { useCallback } from 'react'; + +import classNames from 'classnames'; + +import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; +import Spottable from '@enact/spotlight/Spottable'; + +import css from './VoiceHeader.module.less'; + +const Container = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); +const SpottableComponent = Spottable('button'); + +export default function VoiceHeader({ + title, + className, + onBackButton, + onSpotlightUp, + onSpotlightLeft, + onClick, + ...rest +}) { + const _onClick = useCallback( + (e) => { + if (onClick) { + onClick(e); + } + }, + [onClick] + ); + + const _onSpotlightUp = useCallback( + (e) => { + if (onSpotlightUp) { + onSpotlightUp(e); + } + }, + [onSpotlightUp] + ); + + const _onSpotlightLeft = useCallback( + (e) => { + if (onSpotlightLeft) { + onSpotlightLeft(e); + } + }, + [onSpotlightLeft] + ); + + return ( + + {onBackButton && ( + + )} + {title &&
{title}
} +
+ ); +} diff --git a/com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.module.less b/com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.module.less new file mode 100644 index 00000000..bf782cba --- /dev/null +++ b/com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.module.less @@ -0,0 +1,40 @@ +@import "../../style/CommonStyle.module.less"; +@import "../../style/utils.module.less"; + +.voiceHeader { + align-self: stretch; + margin: 30px 60px; + height: 60px; + display: flex; + justify-content: flex-start; + align-items: center; + background-color: transparent; + + .title { + font-size: 36px; + font-weight: 700; + color: #FFFFFF; + padding-left: 0; + letter-spacing: 1px; + white-space: nowrap; + overflow: hidden; + line-height: 60px; + height: 60px; + } +} + +.backButton { + .size(@w: 50px, @h: 50px); + background-size: 50px 50px; + background-position: center; + background-image: url("../../../assets/images/btn/btn-60-bk-back-nor@3x.png"); + border: none; + flex-shrink: 0; + margin-right: 20px; + + &:focus { + border-radius: 10px; + background-image: url("../../../assets/images/btn/btn-60-wh-back-foc@3x.png"); + box-shadow: 0px 6px 30px 0 rgba(0, 0, 0, 0.4); + } +} diff --git a/com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.jsx b/com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.jsx new file mode 100644 index 00000000..50cf2e58 --- /dev/null +++ b/com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.jsx @@ -0,0 +1,59 @@ +import React, { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator'; + +import TBody from '../../components/TBody/TBody'; +import TPanel from '../../components/TPanel/TPanel'; +import { sendLogGNB } from '../../actions/logActions'; +import { popPanel } from '../../actions/panelActions'; +import { LOG_MENU } from '../../utils/Config'; +import VoiceHeader from './VoiceHeader'; + +import css from './VoicePanel.module.less'; + +const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div'); + +export default function VoicePanel({ panelInfo, isOnTop, spotlightId }) { + const dispatch = useDispatch(); + const loadingComplete = useSelector((state) => state.common?.loadingComplete); + + useEffect(() => { + if (isOnTop) { + dispatch(sendLogGNB(LOG_MENU.SEARCH_SEARCH)); + } + }, [isOnTop, dispatch]); + + const handleBackButton = useCallback(() => { + console.log(`[VoicePanel] Back button clicked - returning to previous panel`); + dispatch(popPanel()); + }, [dispatch]); + + return ( + + + + {loadingComplete && ( + +
+

Voice Panel

+

+ Voice search functionality will be implemented here. +

+
+
+ )} + +
+ ); +} diff --git a/com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.module.less b/com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.module.less new file mode 100644 index 00000000..ab2a315e --- /dev/null +++ b/com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.module.less @@ -0,0 +1,41 @@ +.voicePanel { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.header { + flex-shrink: 0; +} + +.tbody { + flex: 1; + overflow: hidden; +} + +.voiceContainer { + display: flex; + justify-content: center; + align-items: center; + min-height: 600px; + padding: 60px; +} + +.voiceContent { + text-align: center; + max-width: 800px; +} + +.title { + font-size: 48px; + font-weight: bold; + color: #ffffff; + margin-bottom: 30px; +} + +.description { + font-size: 24px; + color: #cccccc; + line-height: 1.6; +}