[검색] 스타일 및 변경 작업#1

- Figma 기준 02-1 작업완료.
    (관련 내용 주석으로 메모달아둠, 분리 필요)
 - 02-2작업완료
    (관련 내용 주석으로 메모달아둠, 분리 필요)
 - 02-3 (카테고리 부분에대한 문의점으로 오현주 팀장님에게 문의넣어둠)
This commit is contained in:
junghoon86.park
2025-09-26 17:32:22 +09:00
parent 177d233ce9
commit d21984af12
10 changed files with 898 additions and 397 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

View File

@@ -1,14 +1,17 @@
import React, { useCallback, useState } from "react"; import React, {
useCallback,
useState,
} from 'react';
import classNames from "classnames"; import classNames from 'classnames';
import { InputField } from "@enact/sandstone/Input"; import { InputField } from '@enact/sandstone/Input';
import Spotlight from "@enact/spotlight"; import Spotlight from '@enact/spotlight';
import Spottable from "@enact/spotlight/Spottable"; import Spottable from '@enact/spotlight/Spottable';
import useScrollReset from "../../hooks/useScrollReset"; import useScrollReset from '../../hooks/useScrollReset';
import TIconButton from "../TIconButton/TIconButton"; import TIconButton from '../TIconButton/TIconButton';
import css from "./TInput.module.less"; import css from './TInput.module.less';
const KINDS = { withIcon: "withIcon" }; const KINDS = { withIcon: "withIcon" };
const ICONS = { search: "search" }; const ICONS = { search: "search" };
@@ -30,6 +33,8 @@ export default function TInput({
scrollTop, scrollTop,
onIconClick, onIconClick,
forcedSpotlight, forcedSpotlight,
onFocus,
onBlur,
...rest ...rest
}) { }) {
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollTop); const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollTop);
@@ -66,6 +71,20 @@ export default function TInput({
} }
}, []); }, []);
//focus,blur 추가
const _onFocus = useCallback(() => {
if (onFocus) {
onFocus();
}
handleScrollReset();
}, [onFocus]);
const _onBlur = useCallback(() => {
if (onBlur) {
onBlur();
}
handleStopScrolling();
}, [onBlur]);
return ( return (
<Container <Container
className={classNames(css.container, className ? className : null)} className={classNames(css.container, className ? className : null)}
@@ -78,8 +97,8 @@ export default function TInput({
spotlightDisabled={spotlightDisabled} spotlightDisabled={spotlightDisabled}
className={classNames(css.input)} className={classNames(css.input)}
autoFocus autoFocus
onFocus={handleScrollReset} onFocus={_onFocus}
onBlur={handleStopScrolling} onBlur={_onBlur}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
spotlightId={"input-field-box"} spotlightId={"input-field-box"}
aria-label="Keyword edit box" aria-label="Keyword edit box"

View File

@@ -5,49 +5,71 @@ import React, {
useMemo, useMemo,
useRef, useRef,
useState, useState,
} from "react"; } from 'react';
import micIcon from "../../../assets/images/searchpanel/image-mic.png"; import classNames from 'classnames';
import hotPicksImage from "../../../assets/images/searchpanel/img-hotpicks.png"; import {
import hotPicksBrandImage from "../../../assets/images/searchpanel/img-search-hotpicks.png"; useDispatch,
useSelector,
import { useDispatch, useSelector } from "react-redux"; } from 'react-redux';
import { Job } from "@enact/core/util"; import { Job } from '@enact/core/util';
import Spotlight from "@enact/spotlight"; import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator"; import SpotlightContainerDecorator
import Spottable from "@enact/spotlight/Spottable"; from '@enact/spotlight/SpotlightContainerDecorator';
import { setContainerLastFocusedElement } from "@enact/spotlight/src/container"; import Spottable from '@enact/spotlight/Spottable';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import { sendLogGNB, sendLogTotalRecommend } from "../../actions/logActions";
import { getMyRecommandedKeyword } from "../../actions/myPageActions"; import micIcon from '../../../assets/images/searchpanel/image-mic.png';
import { popPanel, updatePanel } from "../../actions/panelActions"; import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
import { getSearch, resetSearch } from "../../actions/searchActions"; import hotPicksBrandImage
from '../../../assets/images/searchpanel/img-search-hotpicks.png';
import {
sendLogGNB,
sendLogTotalRecommend,
} from '../../actions/logActions';
import { getMyRecommandedKeyword } from '../../actions/myPageActions';
import {
popPanel,
updatePanel,
} from '../../actions/panelActions';
import {
getSearch,
resetSearch,
} from '../../actions/searchActions';
import { import {
showSuccessToast,
showErrorToast, showErrorToast,
showSearchSuccessToast, showInfoToast,
showSearchErrorToast, showSearchErrorToast,
showSearchSuccessToast,
showSuccessToast,
showWarningToast, showWarningToast,
showInfoToast } from '../../actions/toastActions';
} from "../../actions/toastActions"; import TBody from '../../components/TBody/TBody';
import TBody from "../../components/TBody/TBody"; import TInput, {
import TInput, { ICONS, KINDS } from "../../components/TInput/TInput"; ICONS,
import TPanel from "../../components/TPanel/TPanel"; KINDS,
import TVerticalPagenator from "../../components/TVerticalPagenator/TVerticalPagenator"; } from '../../components/TInput/TInput';
import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer"; import TPanel from '../../components/TPanel/TPanel';
import usePrevious from "../../hooks/usePrevious"; import TScroller from '../../components/TScroller/TScroller';
import TVerticalPagenator
from '../../components/TVerticalPagenator/TVerticalPagenator';
import TVirtualGridList
from '../../components/TVirtualGridList/TVirtualGridList';
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
import usePrevious from '../../hooks/usePrevious';
import { import {
LOG_CONTEXT_NAME, LOG_CONTEXT_NAME,
LOG_MENU, LOG_MENU,
LOG_MESSAGE_ID, LOG_MESSAGE_ID,
panel_names, panel_names,
} from "../../utils/Config"; } from '../../utils/Config';
import { SpotlightIds } from "../../utils/SpotlightIds"; import { SpotlightIds } from '../../utils/SpotlightIds';
import NoSearchResults from "./NoSearchResults/NoSearchResults"; import NoSearchResults from './NoSearchResults/NoSearchResults';
import RecommendedKeywords from "./RecommendedKeywords/RecommendedKeywords"; import RecommendedKeywords from './RecommendedKeywords/RecommendedKeywords';
import css from "./SearchPanel.new.module.less"; import css from './SearchPanel.new.module.less';
import SearchResults from "./SearchResults/SearchResults"; import SearchResultsNew from './SearchResults.new';
import SearchResults from './SearchResults/SearchResults';
const ContainerBasic = SpotlightContainerDecorator( const ContainerBasic = SpotlightContainerDecorator(
{ enterTo: "last-focused" }, { enterTo: "last-focused" },
@@ -82,10 +104,15 @@ const SPOTLIGHT_IDS = {
TOP_SEARCHES_SECTION: "top-searches-section", TOP_SEARCHES_SECTION: "top-searches-section",
POPULAR_BRANDS_SECTION: "popular-brands-section", POPULAR_BRANDS_SECTION: "popular-brands-section",
HOT_PICKS_SECTION: "hot-picks-section", HOT_PICKS_SECTION: "hot-picks-section",
SEARCH_VERTICAL_PAGENATOR: "search_verticalPagenator" SEARCH_VERTICAL_PAGENATOR: "search_verticalPagenator",
}; };
export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) { export default function SearchPanel({
panelInfo,
isOnTop,
spotlightId,
scrollOptions = [],
}) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const loadingComplete = useSelector((state) => state.common?.loadingComplete); const loadingComplete = useSelector((state) => state.common?.loadingComplete);
const recommandedKeywords = useSelector( const recommandedKeywords = useSelector(
@@ -105,6 +132,18 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const [position, setPosition] = useState(null); const [position, setPosition] = useState(null);
const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false); const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false);
//인풋창 포커스 구분을 위함
const [inputFocus, setInputFocus] = useState(false);
const _onFocus = () => {
setInputFocus(true);
};
const _onBlur = () => {
setInputFocus(false);
};
useEffect(() => {
console.log("###inputFocus", inputFocus);
}, [inputFocus]);
let searchQueryRef = usePrevious(searchQuery); let searchQueryRef = usePrevious(searchQuery);
let isOnTopRef = usePrevious(isOnTop); let isOnTopRef = usePrevious(isOnTop);
@@ -122,17 +161,34 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
); );
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함 // 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
const recentSearches = useMemo(() => ["Puppy food", "Dog toy", "Fitness"], []); const recentSearches = useMemo(
const topSearches = useMemo(() => ["Mather's Day", "Gift", "Easter Day", "Royal Canin puppy food", "Fitness", "Parrot"], []); () => ["Puppy food", "Dog toy", "Fitness"],
const popularBrands = useMemo(() => ["Shark", "Ninja", "Skechers", "LocknLock", "8Greens", "LGE"], []); []
const hotPicks = useMemo(() => [ );
const topSearches = useMemo(
() => [
"Mather's Day",
"Gift",
"Easter Day",
"Royal Canin puppy food",
"Fitness",
"Parrot",
],
[]
);
const popularBrands = useMemo(
() => ["Shark", "Ninja", "Skechers", "LocknLock", "8Greens", "LGE"],
[]
);
const hotPicks = useMemo(
() => [
{ {
id: 1, id: 1,
image: hotPicksImage, image: hotPicksImage,
brandLogo: hotPicksBrandImage, brandLogo: hotPicksBrandImage,
brandName: "Product Name", brandName: "Product Name",
title: "New Shark Vacuum! Your pet Hair Solution!", title: "New Shark Vacuum! Your pet Hair Solution!",
isForYou: false isForYou: false,
}, },
{ {
id: 2, id: 2,
@@ -140,7 +196,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
brandLogo: hotPicksBrandImage, brandLogo: hotPicksBrandImage,
brandName: "Product Name", brandName: "Product Name",
title: "New Shark Vacuum! Your pet Hair Solution!", title: "New Shark Vacuum! Your pet Hair Solution!",
isForYou: false isForYou: false,
}, },
{ {
id: 3, id: 3,
@@ -148,7 +204,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
brandLogo: hotPicksBrandImage, brandLogo: hotPicksBrandImage,
brandName: "Product Name", brandName: "Product Name",
title: "New Shark Vacuum! Your pet Hair Solution!", title: "New Shark Vacuum! Your pet Hair Solution!",
isForYou: false isForYou: false,
}, },
{ {
id: 4, id: 4,
@@ -156,9 +212,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
brandLogo: hotPicksBrandImage, brandLogo: hotPicksBrandImage,
brandName: "Product Name", brandName: "Product Name",
title: "New Shark Vacuum! Your pet Hair Solution!", title: "New Shark Vacuum! Your pet Hair Solution!",
isForYou: true isForYou: true,
} },
], []); ],
[]
);
useEffect(() => { useEffect(() => {
if (loadingComplete && !recommandedKeywords) { if (loadingComplete && !recommandedKeywords) {
@@ -305,7 +363,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}; };
}, []); }, []);
const handleKeydown = useCallback((e) => { const handleKeydown = useCallback(
(e) => {
if (!isOnTopRef.current) { if (!isOnTopRef.current) {
return; return;
} }
@@ -324,10 +383,23 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
} }
// 방향키 처리 - Spotlight 네비게이션 허용 // 방향키 처리 - Spotlight 네비게이션 허용
const arrowKeys = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Left", "Right", "Up", "Down"]; const arrowKeys = [
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Left",
"Right",
"Up",
"Down",
];
if (arrowKeys.includes(e.key)) { if (arrowKeys.includes(e.key)) {
// 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지 // 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지
if (position === 0 && (e.key === "Left" || e.key === "ArrowLeft") && !searchQuery) { if (
position === 0 &&
(e.key === "Left" || e.key === "ArrowLeft") &&
!searchQuery
) {
e.preventDefault(); e.preventDefault();
return; return;
} }
@@ -335,7 +407,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 오른쪽 화살표 키 처리 - 포커스 이동 허용 // 오른쪽 화살표 키 처리 - 포커스 이동 허용
if (e.key === "ArrowRight" || e.key === "Right") { if (e.key === "ArrowRight" || e.key === "Right") {
// 커서가 텍스트 끝에 있을 때만 포커스 이동 허용 // 커서가 텍스트 끝에 있을 때만 포커스 이동 허용
const input = document.querySelector(`[data-spotlight-id="input-field-box"] > input`); const input = document.querySelector(
`[data-spotlight-id="input-field-box"] > input`
);
if (input && position === input.value.length) { if (input && position === input.value.length) {
// 커서가 텍스트 끝에 있으면 포커스 이동 허용 // 커서가 텍스트 끝에 있으면 포커스 이동 허용
return; return;
@@ -345,7 +419,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// 나머지 방향키는 Spotlight가 처리하도록 허용 // 나머지 방향키는 Spotlight가 처리하도록 허용
return; return;
} }
}, [searchQuery, position, handleSearchSubmit, showVirtualKeyboard]); },
[searchQuery, position, handleSearchSubmit, showVirtualKeyboard]
);
const cursorPosition = useCallback(() => { const cursorPosition = useCallback(() => {
const input = document.querySelector( const input = document.querySelector(
@@ -397,12 +473,19 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, [panelInfo, firstSpot]); }, [panelInfo, firstSpot]);
// 키워드 클릭 핸들러 // 키워드 클릭 핸들러
const handleKeywordClick = useCallback((keyword) => { const handleKeywordClick = useCallback(
(keyword) => {
setSearchQuery(keyword); setSearchQuery(keyword);
handleSearchSubmit(keyword); handleSearchSubmit(keyword);
// 키워드 선택 알림 // 키워드 선택 알림
dispatch(showSuccessToast(`"${keyword}" 키워드로 검색합니다.`, { duration: 2000 })); dispatch(
}, [handleSearchSubmit, dispatch]); showSuccessToast(`"${keyword}" 키워드로 검색합니다.`, {
duration: 2000,
})
);
},
[handleSearchSubmit, dispatch]
);
// 상품 클릭 핸들러 // 상품 클릭 핸들러
const handleProductClick = useCallback((product) => { const handleProductClick = useCallback((product) => {
@@ -417,58 +500,62 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, [dispatch]); }, [dispatch]);
// ProductCard 컴포넌트 // ProductCard 컴포넌트
const ProductCard = useCallback(({ const renderItem = useCallback(
product, (
index, // {
onClick, // product,
showForYouBadge = true, // index,
// onClick,
// showBrandLogo = true,
// showBrandName = true,
// showProductTitle = true,
// ...rest
// }
{ index, ...rest }
) => {
const {
showBrandLogo = true, showBrandLogo = true,
showBrandName = true, showBrandName = true,
showProductTitle = true, showProductTitle = true,
badgeText = "For You", image,
badgePosition = "top-right" title,
}) => { brandLogo,
brandName,
} = hotPicks[index];
return ( return (
<SpottableProduct <SpottableProduct
key={`product-${index}`} key={`product-${index}`}
className={css.productCard} className={css.productCard}
onClick={() => onClick?.(product)}
spotlightId={`product-${index}`} spotlightId={`product-${index}`}
{...rest}
> >
<div className={css.productImageWrapper}> <div className={css.productImageWrapper}>
<img <img src={image} alt={title} className={css.productImage} />
src={product.image}
alt={product.title}
className={css.productImage}
/>
{showForYouBadge && product.isForYou && (
<div className={`${css.forYouBadge} ${css[badgePosition]}`}>
{badgeText}
</div>
)}
</div> </div>
<div className={css.productInfo}> <div className={css.productInfo}>
{showBrandLogo && ( {showBrandLogo && (
<div className={css.productBrandWrapper}> <div className={css.productBrandWrapper}>
<img <img
src={product.brandLogo} src={brandLogo}
alt={product.brandName} alt={brandName}
className={css.brandLogo} className={css.brandLogo}
/> />
</div> </div>
)} )}
<div className={css.productDetails}> <div className={css.productDetails}>
{showBrandName && ( {showBrandName && (
<div className={css.brandName}>{product.brandName}</div> <div className={css.brandName}>{brandName}</div>
)} )}
{showProductTitle && ( {showProductTitle && (
<div className={css.productTitle}>{product.title}</div> <div className={css.productTitle}>{title}</div>
)} )}
</div> </div>
</div> </div>
</SpottableProduct> </SpottableProduct>
); );
}, []); },
[]
);
return ( return (
<TPanel <TPanel
@@ -495,7 +582,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
> >
{/* 검색 입력 영역 */} {/* 검색 입력 영역 */}
<InputContainer <InputContainer
className={css.inputContainer} className={classNames(
css.inputContainer,
inputFocus === true && css.inputFocus,
css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */
)}
data-wheel-point={true} data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER} spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
> >
@@ -520,6 +611,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
tabIndex={0} tabIndex={0}
spotlightDisabled={false} spotlightDisabled={false}
spotlightBoxDisabled={true} spotlightBoxDisabled={true}
onFocus={_onFocus}
onBlur={_onBlur}
/> />
<SpottableMicButton <SpottableMicButton
className={css.microphoneButton} className={css.microphoneButton}
@@ -536,19 +629,19 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
</SpottableMicButton> </SpottableMicButton>
{/* 테스트용 Toast 버튼 (개발용) */} {/* 테스트용 Toast 버튼 (개발용) */}
<SpottableMicButton {/* <SpottableMicButton
className={css.testToastButton} className={css.testToastButton}
onClick={handleTestToasts} onClick={handleTestToasts}
spotlightId="test-toast-button" spotlightId="test-toast-button"
> >
<div className={css.testButtonCircle}> <div className={css.testButtonCircle}>🧪</div>
🧪 </SpottableMicButton> */}
</div>
</SpottableMicButton>
</div> </div>
</InputContainer> </InputContainer>
{/* 결과갑 부분 작업중 */}
{/* 검색 결과 표시 영역 */} <SearchResultsNew />
{/* //결과갑 부분 작업중 */}
{/* 검색 결과 표시 영역
{searchPerformed && searchQuery !== null ? ( {searchPerformed && searchQuery !== null ? (
Object.keys(searchDatas).length > 0 ? ( Object.keys(searchDatas).length > 0 ? (
<SearchResults <SearchResults
@@ -561,7 +654,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
) )
) : ( ) : (
<ContainerBasic className={css.contentContainer}> <ContainerBasic className={css.contentContainer}>
{/* 최근 검색어 섹션 */} {/* 노출 조건 변경 필요. 포커스 블러만으로는 안됌.(가상 키보드 노출시가 맞을듯)
{inputFocus === false ? (
<>
{/* 최근 검색어 섹션
<SectionContainer <SectionContainer
className={css.section} className={css.section}
data-wheel-point={true} data-wheel-point={true}
@@ -569,7 +665,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
> >
<div className={css.sectionHeader}> <div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div> <div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Your Recent Searches</div> <div className={css.sectionTitle}>
Your Recent Searches
</div>
</div> </div>
<div className={css.keywordList}> <div className={css.keywordList}>
{recentSearches.map((keyword, index) => ( {recentSearches.map((keyword, index) => (
@@ -585,7 +683,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
</div> </div>
</SectionContainer> </SectionContainer>
{/* 인기 검색어 섹션 */} {/* 인기 검색어 섹션
<SectionContainer <SectionContainer
className={css.section} className={css.section}
data-wheel-point={true} data-wheel-point={true}
@@ -609,7 +707,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
</div> </div>
</SectionContainer> </SectionContainer>
{/* 인기 브랜드 섹션 */} {/* 인기 브랜드 섹션
<SectionContainer <SectionContainer
className={css.section} className={css.section}
data-wheel-point={true} data-wheel-point={true}
@@ -633,45 +731,62 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
</div> </div>
</SectionContainer> </SectionContainer>
{/* Hot Picks for You 섹션 */} {/* Hot Picks for You 섹션
<SectionContainer <SectionContainer
className={css.section} className={css.hotpicksSection}
data-wheel-point={true} data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.HOT_PICKS_SECTION} spotlightId={SPOTLIGHT_IDS.HOT_PICKS_SECTION}
> >
<div className={css.sectionHeader}> <div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div> <div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Hot Picks for You</div> <div className={css.sectionTitle}>
Hot Picks for You
</div>
</div> </div>
<div className={css.productList}> <div className={css.productList}>
{hotPicks.map((product, index) => ( {hotPicks && hotPicks.length > 0 && (
<ProductCard <TVirtualGridList
key={`product-${index}`} dataSize={hotPicks.length}
product={product} direction="horizontal"
index={index} renderItem={renderItem}
onClick={handleProductClick} // itemWidth={546}
showForYouBadge={true} itemWidth={416}
showBrandLogo={true} itemHeight={436}
showBrandName={true} spacing={20}
showProductTitle={true}
badgeText="For You"
badgePosition="top-right"
/> />
))} )}
</div> </div>
</SectionContainer> </SectionContainer>
</>
) : (
<div className={css.inputFocusBox}>
<div className={css.keywordList}>
{recentSearches.map((keyword, index) => (
<SpottableKeyword
key={`recent-${index}`}
className={css.keywordButton}
onClick={() => handleKeywordClick(keyword)}
spotlightId={`recent-keyword-${index}`}
>
{keyword}
</SpottableKeyword>
))}
</div>
</div>
)}
</ContainerBasic> </ContainerBasic>
)} )}
*/}
</TVerticalPagenator> </TVerticalPagenator>
)} )}
</ContainerBasic> </ContainerBasic>
</TBody> </TBody>
{/* Virtual Keyboard */} {/* Virtual Keyboard */}
<VirtualKeyboardContainer {/* <VirtualKeyboardContainer
isVisible={showVirtualKeyboard} isVisible={showVirtualKeyboard}
onClose={() => setShowVirtualKeyboard(false)} onClose={() => setShowVirtualKeyboard(false)}
/> /> */}
</TPanel> </TPanel>
); );
} }

View File

@@ -26,7 +26,13 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
&.inputFocus {
padding-bottom: 10px;
}
&.searchValue {
padding-bottom: 55px;
padding-top: 55px;
}
> * { > * {
margin-bottom: 10px; margin-bottom: 10px;
@@ -73,10 +79,10 @@
background-color: #fff !important; background-color: #fff !important;
input { input {
text-align: center; text-align: left;
color: black; color: black;
font-size: 42px; font-size: 42px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 700; font-weight: 700;
line-height: 42px; line-height: 42px;
outline: none; outline: none;
@@ -166,13 +172,13 @@
position: relative; position: relative;
&::before { &::before {
content: ''; content: "";
width: 36.27px; width: 36.27px;
height: 36.27px; height: 36.27px;
position: absolute; position: absolute;
left: 1.95px; left: 1.95px;
top: 1.95px; top: 1.95px;
border: 3.90px solid black; border: 3.9px solid black;
border-radius: 50%; border-radius: 50%;
} }
} }
@@ -202,10 +208,7 @@
align-items: center; align-items: center;
.microphoneIcon { .microphoneIcon {
width: 100%; height: 50px;
height: 100%;
object-fit: contain;
padding: 10px;
box-sizing: border-box; box-sizing: border-box;
} }
} }
@@ -277,6 +280,64 @@
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
display: inline-flex; display: inline-flex;
.inputFocusBox {
width: 995px;
height: 355px;
margin: 0 auto;
.keywordList {
align-self: stretch;
padding-top: 10px;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
flex-wrap: wrap;
flex-direction: column;
> * {
margin-bottom: 5px;
}
.keywordButton {
padding: 20px;
background: white;
border-radius: 100px;
border: 5px solid #dadada;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
cursor: pointer;
transition: all 0.2s ease;
height: 64px;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
color: black;
font-size: 24px;
font-family: "LG Smart UI";
font-weight: 700;
line-height: 24px;
text-align: center;
word-wrap: break-word;
&:hover {
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
&:focus {
border: 5px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
outline: none;
}
}
}
}
} }
// 섹션 공통 스타일 // 섹션 공통 스타일
@@ -315,14 +376,14 @@
.sectionIndicator { .sectionIndicator {
width: 6px; width: 6px;
height: 36px; height: 36px;
background: #C70850; background: #c70850;
} }
.sectionTitle { .sectionTitle {
text-align: center; text-align: center;
color: black; color: black;
font-size: 42px; font-size: 42px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 700; font-weight: 700;
line-height: 42px; line-height: 42px;
word-wrap: break-word; word-wrap: break-word;
@@ -341,17 +402,13 @@
> * { > * {
margin-right: 19px; margin-right: 19px;
margin-bottom: 19px; margin-bottom: 19px;
&:nth-child(3n) {
margin-right: 0;
}
} }
.keywordButton { .keywordButton {
padding: 20px; padding: 20px;
background: white; background: white;
border-radius: 100px; border-radius: 100px;
border: 5px solid #DADADA; border: 5px solid #dadada;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
@@ -369,7 +426,7 @@
color: black; color: black;
font-size: 24px; font-size: 24px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 700; font-weight: 700;
line-height: 24px; line-height: 24px;
text-align: center; text-align: center;
@@ -388,50 +445,68 @@
} }
} }
} }
}
// 상품 리스트 스타일 (Hot Picks for You) .hotpicksSection {
.productList { padding-top: 63px;
align-self: stretch; padding-left: 60px;
padding-top: 30px; width: 1800px;
height: 580px;
.sectionHeader {
width: 1800px;
height: 42px;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: center;
display: inline-flex; display: inline-flex;
flex-wrap: wrap;
> * { > * {
margin-right: 19px; margin-right: 12px;
margin-bottom: 19px;
&:nth-child(2n) { &:last-child {
margin-right: 0; margin-right: 0;
} }
} }
.sectionIndicator {
width: 6px;
height: 36px;
background: #c70850;
}
.sectionTitle {
text-align: center;
color: black;
font-size: 42px;
font-family: "LG Smart UI";
font-weight: 700;
line-height: 42px;
word-wrap: break-word;
}
}
// 상품 리스트 스타일 (Hot Picks for You)
.productList {
padding-top: 30px;
.size(@w: 100%, @h: inherit);
> div:nth-child(1) {
.size(@w: 100%, @h: inherit);
}
.productCard { .productCard {
width: 546px; width: 546px;
padding: 18px; padding: 18px;
background: white; background: white;
border-radius: 12px; border-radius: 12px;
border: 5px solid #DADADA; border: 5px solid #dadada;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
display: inline-flex; display: inline-flex;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
border-color: @PRIMARY_COLOR_RED; border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
} }
&:focus { &:focus {
@@ -454,51 +529,6 @@
object-fit: cover; object-fit: cover;
border-radius: 8px; border-radius: 8px;
} }
.forYouBadge {
height: 29.33px;
padding-left: 5.33px;
padding-right: 5.33px;
position: absolute;
background: #C70850;
border-radius: 5.33px;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 21.33px;
font-family: 'LG Smart UI';
font-weight: 700;
word-wrap: break-word;
z-index: 10;
// 위치별 스타일
&.top-right {
right: 10px;
top: 10px;
}
&.top-left {
left: 10px;
top: 10px;
}
&.bottom-right {
right: 10px;
bottom: 10px;
}
&.bottom-left {
left: 10px;
bottom: 10px;
}
&.center {
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
} }
.productInfo { .productInfo {
@@ -557,7 +587,7 @@
text-align: center; text-align: center;
color: #808080; color: #808080;
font-size: 18px; font-size: 18px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 700; font-weight: 700;
line-height: 18px; line-height: 18px;
word-wrap: break-word; word-wrap: break-word;
@@ -567,7 +597,7 @@
align-self: stretch; align-self: stretch;
color: black; color: black;
font-size: 24px; font-size: 24px;
font-family: 'LG Smart UI'; font-family: "LG Smart UI";
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 24px;
word-wrap: break-word; word-wrap: break-word;
@@ -606,7 +636,6 @@
[data-spotlight-id] { [data-spotlight-id] {
&:focus { &:focus {
outline: 2px solid @PRIMARY_COLOR_RED; outline: 2px solid @PRIMARY_COLOR_RED;
outline-offset: 2px;
} }
} }
} }

View File

@@ -0,0 +1,135 @@
import React, {
useCallback,
useMemo,
} from 'react';
import { useDispatch } from 'react-redux';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
import hotPicksBrandImage
from '../../../assets/images/searchpanel/img-search-hotpicks.png';
import TVirtualGridList
from '../../components/TVirtualGridList/TVirtualGridList';
import css from './SearchResults.new.module.less';
const SearchResultsNew = () => {
const Container = SpotlightContainerDecorator("div");
const SectionContainer = SpotlightContainerDecorator("div");
const SpottableLi = Spottable("li");
const SpottableDiv = Spottable("div");
const hotPicks = useMemo(
() => [
{
id: 1,
image: hotPicksImage,
brandLogo: hotPicksBrandImage,
brandName: "Product Name",
title: "New Shark Vacuum! Your pet Hair Solution!",
isForYou: false,
},
{
id: 2,
image: hotPicksImage,
brandLogo: hotPicksBrandImage,
brandName: "Product Name",
title: "New Shark Vacuum! Your pet Hair Solution!",
isForYou: false,
},
{
id: 3,
image: hotPicksImage,
brandLogo: hotPicksBrandImage,
brandName: "Product Name",
title: "New Shark Vacuum! Your pet Hair Solution!",
isForYou: false,
},
{
id: 4,
image: hotPicksImage,
brandLogo: hotPicksBrandImage,
brandName: "Product Name",
title: "New Shark Vacuum! Your pet Hair Solution!",
isForYou: true,
},
],
[]
);
// ProductCard 컴포넌트
const renderItem = useCallback(({ index, ...rest }) => {
const {
showBrandLogo = true,
showBrandName = true,
showProductTitle = true,
image,
title,
brandLogo,
brandName,
} = hotPicks[index];
return (
<SpottableDiv
key={`product-${index}`}
className={css.productCard}
spotlightId={`product-${index}`}
{...rest}
>
<div className={css.productImageWrapper}>
<img src={image} alt={title} className={css.productImage} />
</div>
<div className={css.productInfo}>
{showBrandLogo && (
<div className={css.productBrandWrapper}>
<img src={brandLogo} alt={brandName} className={css.brandLogo} />
</div>
)}
<div className={css.productDetails}>
{showBrandName && <div className={css.brandName}>{brandName}</div>}
{showProductTitle && (
<div className={css.productTitle}>{title}</div>
)}
</div>
</div>
</SpottableDiv>
);
}, []);
return (
<Container className={css.searchBox}>
<SectionContainer className={css.topBox}>
<span className={css.topBoxTitle}>How about these?</span>
<ul className={css.topBoxList}>
<SpottableLi className={css.topBoxListItem}>Puppy food</SpottableLi>
<SpottableLi className={css.topBoxListItem}>Dog toy</SpottableLi>
<SpottableLi className={css.topBoxListItem}>Fitness</SpottableLi>
</ul>
</SectionContainer>
<SectionContainer
className={css.hotpicksSection}
data-wheel-point={true}
spotlightId={"hot-picks-section"}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Hot Picks (#)</div>
</div>
<div className={css.productList}>
{hotPicks && hotPicks.length > 0 && (
<TVirtualGridList
dataSize={hotPicks.length}
direction="horizontal"
renderItem={renderItem}
itemWidth={416}
itemHeight={436}
spacing={20}
/>
)}
</div>
</SectionContainer>
</Container>
);
};
export default SearchResultsNew;

View File

@@ -0,0 +1,203 @@
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
.searchBox {
width: 100%;
height: 100%;
.topBox {
width: 100%;
height: 124px;
background-color: #ddd;
display: flex;
align-items: center;
.topBoxTitle {
margin-left: 60px;
font-size: 24px;
font-weight: 700;
color: #272727;
text-decoration: underline;
}
.topBoxList {
margin-left: 20px;
display: flex;
.topBoxListItem {
margin-right: 20px;
display: flex;
align-items: center;
justify-content: center;
height: 64px;
padding: 0 20px;
background-color: @COLOR_WHITE;
border-radius: 32px;
color: #000;
font-weight: 700;
font-size: 24px;
border: 4px solid @COLOR_WHITE;
&:focus {
border: 4px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
}
}
}
}
.hotpicksSection {
padding-top: 63px;
padding-left: 60px;
width: 1800px;
height: 580px;
.sectionHeader {
width: 1800px;
height: 42px;
justify-content: flex-start;
align-items: center;
display: inline-flex;
> * {
margin-right: 12px;
&:last-child {
margin-right: 0;
}
}
.sectionIndicator {
width: 6px;
height: 36px;
background: #c70850;
}
.sectionTitle {
text-align: center;
color: black;
font-size: 42px;
font-family: "LG Smart UI";
font-weight: 700;
line-height: 42px;
word-wrap: break-word;
}
}
// 상품 리스트 스타일 (Hot Picks for You)
.productList {
padding-top: 30px;
.size(@w: 100%, @h: inherit);
> div:nth-child(1) {
.size(@w: 100%, @h: inherit);
}
.productCard {
width: 546px;
padding: 18px;
background: white;
border-radius: 12px;
border: 5px solid #dadada;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
&:focus {
border: 5px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
outline: none;
}
.productImageWrapper {
align-self: stretch;
height: 287px;
position: relative;
.productImage {
width: 510px;
height: 287px;
position: absolute;
left: 0;
top: 0;
object-fit: cover;
border-radius: 8px;
}
}
.productInfo {
align-self: stretch;
padding-top: 15px;
padding-bottom: 15px;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
> * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.productBrandWrapper {
justify-content: flex-start;
align-items: center;
display: flex;
> * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.brandLogo {
width: 60px;
height: 60px;
border-radius: 8px;
object-fit: cover;
}
}
.productDetails {
flex: 1;
flex-direction: column;
justify-content: center;
align-items: flex-start;
display: inline-flex;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.brandName {
text-align: center;
color: #808080;
font-size: 18px;
font-family: "LG Smart UI";
font-weight: 700;
line-height: 18px;
word-wrap: break-word;
}
.productTitle {
align-self: stretch;
color: black;
font-size: 24px;
font-family: "LG Smart UI";
font-weight: 400;
line-height: 24px;
word-wrap: break-word;
}
}
}
}
}
}
}