[251012] fear: VoicePanel and GNB update
🕐 커밋 시간: 2025. 10. 12. 16:42:36 📊 변경 통계: • 총 파일: 10개 • 추가: +56줄 • 삭제: -12줄 📁 추가된 파일: + com.twin.app.shoptime/src/components/TabLayout/iconComponents/VoiceIcon.jsx + com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.jsx + com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.module.less + com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.jsx + com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.module.less 📝 수정된 파일: ~ com.twin.app.shoptime/src/components/TabLayout/TabItemSub.jsx ~ com.twin.app.shoptime/src/components/TabLayout/TabItemSub.module.less ~ com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx ~ com.twin.app.shoptime/src/utils/Config.js ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx 🔧 주요 변경 내용: • UI 컴포넌트 아키텍처 개선 • 공통 유틸리티 함수 최적화 • 소규모 기능 개선 • 모듈 구조 개선
This commit is contained in:
@@ -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
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(css.iconContainer, focused && css.focused)}
|
||||
>
|
||||
<div className={classNames(css.iconContainer, focused && css.focused)}>
|
||||
<img src={path} alt="" />
|
||||
<h3>{patncNm}</h3>
|
||||
</div>
|
||||
@@ -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 && (
|
||||
<Marquee
|
||||
marqueeDisabled={!focused}
|
||||
marqueeOn={"focus"}
|
||||
className={classNames(css.text, isSubItem && css.subItem)}
|
||||
>
|
||||
{subtitle}
|
||||
</Marquee>
|
||||
<div className={css.textWithIcon}>
|
||||
{IconComponent && (
|
||||
<span className={css.iconWrapper}>
|
||||
<IconComponent iconType={focused ? 'focused' : selected ? 'selected' : 'normal'} />
|
||||
</span>
|
||||
)}
|
||||
<Marquee
|
||||
marqueeDisabled={!focused}
|
||||
marqueeOn={'focus'}
|
||||
className={classNames(css.text, isSubItem && css.subItem)}
|
||||
>
|
||||
{subtitle}
|
||||
</Marquee>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [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
|
||||
}
|
||||
>
|
||||
<div className={classNames(isSubItem && css.subWrap)}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<svg
|
||||
width={scaleW(48)}
|
||||
height={scaleW(48)}
|
||||
viewBox="0 0 45 45"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M22.5 7.5c-2.5 0-4.5 2-4.5 4.5v10.5c0 2.5 2 4.5 4.5 4.5s4.5-2 4.5-4.5V12c0-2.5-2-4.5-4.5-4.5z"
|
||||
fill={themeColor}
|
||||
stroke={themeColor}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M31.5 22.5c0 5-4 9-9 9s-9-4-9-9M22.5 31.5v6M16.5 37.5h12"
|
||||
stroke={themeColor}
|
||||
strokeWidth="2.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceIcon;
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
65
com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.jsx
Normal file
65
com.twin.app.shoptime/src/views/VoicePanel/VoiceHeader.jsx
Normal file
@@ -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 (
|
||||
<Container className={classNames(css.voiceHeader, className)} {...rest}>
|
||||
{onBackButton && (
|
||||
<SpottableComponent
|
||||
className={css.backButton}
|
||||
onClick={_onClick}
|
||||
spotlightId={'spotlightId_voiceBackBtn'}
|
||||
onSpotlightUp={_onSpotlightUp}
|
||||
onSpotlightLeft={_onSpotlightLeft}
|
||||
aria-label="Back"
|
||||
role="button"
|
||||
/>
|
||||
)}
|
||||
{title && <div className={css.title}>{title}</div>}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
59
com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.jsx
Normal file
59
com.twin.app.shoptime/src/views/VoicePanel/VoicePanel.jsx
Normal file
@@ -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 (
|
||||
<TPanel
|
||||
panelInfo={panelInfo}
|
||||
className={css.voicePanel}
|
||||
isOnTop={isOnTop}
|
||||
spotlightId={spotlightId}
|
||||
>
|
||||
<VoiceHeader
|
||||
title="Voice Search"
|
||||
onBackButton={handleBackButton}
|
||||
onClick={handleBackButton}
|
||||
className={css.header}
|
||||
/>
|
||||
<TBody spotlightId={spotlightId} className={css.tbody}>
|
||||
{loadingComplete && (
|
||||
<ContainerBasic className={css.voiceContainer}>
|
||||
<div className={css.voiceContent}>
|
||||
<h1 className={css.title}>Voice Panel</h1>
|
||||
<p className={css.description}>
|
||||
Voice search functionality will be implemented here.
|
||||
</p>
|
||||
</div>
|
||||
</ContainerBasic>
|
||||
)}
|
||||
</TBody>
|
||||
</TPanel>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user