@@ -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 && (
-
)}
>
);
- }, [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;
+}