[검색] 스타일 및 변경 작업#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 Spotlight from "@enact/spotlight";
import Spottable from "@enact/spotlight/Spottable";
import { InputField } from '@enact/sandstone/Input';
import Spotlight from '@enact/spotlight';
import Spottable from '@enact/spotlight/Spottable';
import useScrollReset from "../../hooks/useScrollReset";
import TIconButton from "../TIconButton/TIconButton";
import css from "./TInput.module.less";
import useScrollReset from '../../hooks/useScrollReset';
import TIconButton from '../TIconButton/TIconButton';
import css from './TInput.module.less';
const KINDS = { withIcon: "withIcon" };
const ICONS = { search: "search" };
@@ -30,6 +33,8 @@ export default function TInput({
scrollTop,
onIconClick,
forcedSpotlight,
onFocus,
onBlur,
...rest
}) {
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 (
<Container
className={classNames(css.container, className ? className : null)}
@@ -78,8 +97,8 @@ export default function TInput({
spotlightDisabled={spotlightDisabled}
className={classNames(css.input)}
autoFocus
onFocus={handleScrollReset}
onBlur={handleStopScrolling}
onFocus={_onFocus}
onBlur={_onBlur}
onKeyDown={onKeyDown}
spotlightId={"input-field-box"}
aria-label="Keyword edit box"

View File

@@ -5,49 +5,71 @@ import React, {
useMemo,
useRef,
useState,
} from "react";
} from 'react';
import micIcon from "../../../assets/images/searchpanel/image-mic.png";
import hotPicksImage from "../../../assets/images/searchpanel/img-hotpicks.png";
import hotPicksBrandImage from "../../../assets/images/searchpanel/img-search-hotpicks.png";
import classNames from 'classnames';
import {
useDispatch,
useSelector,
} from 'react-redux';
import { useDispatch, useSelector } from "react-redux";
import { Job } from '@enact/core/util';
import Spotlight from '@enact/spotlight';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
import { Job } from "@enact/core/util";
import Spotlight from "@enact/spotlight";
import SpotlightContainerDecorator from "@enact/spotlight/SpotlightContainerDecorator";
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 { popPanel, updatePanel } from "../../actions/panelActions";
import { getSearch, resetSearch } from "../../actions/searchActions";
import {
showSuccessToast,
showErrorToast,
showSearchSuccessToast,
import micIcon from '../../../assets/images/searchpanel/image-mic.png';
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
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 {
showErrorToast,
showInfoToast,
showSearchErrorToast,
showSearchSuccessToast,
showSuccessToast,
showWarningToast,
showInfoToast
} from "../../actions/toastActions";
import TBody from "../../components/TBody/TBody";
import TInput, { ICONS, KINDS } from "../../components/TInput/TInput";
import TPanel from "../../components/TPanel/TPanel";
import TVerticalPagenator from "../../components/TVerticalPagenator/TVerticalPagenator";
import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
import usePrevious from "../../hooks/usePrevious";
} from '../../actions/toastActions';
import TBody from '../../components/TBody/TBody';
import TInput, {
ICONS,
KINDS,
} from '../../components/TInput/TInput';
import TPanel from '../../components/TPanel/TPanel';
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 {
LOG_CONTEXT_NAME,
LOG_MENU,
LOG_MESSAGE_ID,
panel_names,
} from "../../utils/Config";
import { SpotlightIds } from "../../utils/SpotlightIds";
import NoSearchResults from "./NoSearchResults/NoSearchResults";
import RecommendedKeywords from "./RecommendedKeywords/RecommendedKeywords";
import css from "./SearchPanel.new.module.less";
import SearchResults from "./SearchResults/SearchResults";
} from '../../utils/Config';
import { SpotlightIds } from '../../utils/SpotlightIds';
import NoSearchResults from './NoSearchResults/NoSearchResults';
import RecommendedKeywords from './RecommendedKeywords/RecommendedKeywords';
import css from './SearchPanel.new.module.less';
import SearchResultsNew from './SearchResults.new';
import SearchResults from './SearchResults/SearchResults';
const ContainerBasic = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
@@ -82,10 +104,15 @@ const SPOTLIGHT_IDS = {
TOP_SEARCHES_SECTION: "top-searches-section",
POPULAR_BRANDS_SECTION: "popular-brands-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 loadingComplete = useSelector((state) => state.common?.loadingComplete);
const recommandedKeywords = useSelector(
@@ -105,6 +132,18 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const [position, setPosition] = useState(null);
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 isOnTopRef = usePrevious(isOnTop);
@@ -122,43 +161,62 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
);
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
const recentSearches = useMemo(() => ["Puppy food", "Dog toy", "Fitness"], []);
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,
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
}
], []);
const recentSearches = useMemo(
() => ["Puppy food", "Dog toy", "Fitness"],
[]
);
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,
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,
},
],
[]
);
useEffect(() => {
if (loadingComplete && !recommandedKeywords) {
@@ -250,7 +308,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
domain: "theme,show,item",
})
);
// 검색 시작 알림 (선택사항)
dispatch(showSuccessToast(`"${query}" 검색 중...`, { duration: 2000 }));
} else {
@@ -305,47 +363,65 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
};
}, []);
const handleKeydown = useCallback((e) => {
if (!isOnTopRef.current) {
return;
}
// Enter 키 처리
if (e.key === "Enter") {
e.preventDefault();
if (showVirtualKeyboard) {
// 가상 키보드가 열려있으면 검색 실행하고 키보드 닫기
handleSearchSubmit(searchQuery);
} else {
// 가상 키보드가 닫혀있으면 키보드 열기
setShowVirtualKeyboard(true);
}
return;
}
// 방향키 처리 - Spotlight 네비게이션 허용
const arrowKeys = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Left", "Right", "Up", "Down"];
if (arrowKeys.includes(e.key)) {
// 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지
if (position === 0 && (e.key === "Left" || e.key === "ArrowLeft") && !searchQuery) {
e.preventDefault();
const handleKeydown = useCallback(
(e) => {
if (!isOnTopRef.current) {
return;
}
// 오른쪽 화살표 키 처리 - 포커스 이동 허용
if (e.key === "ArrowRight" || e.key === "Right") {
// 커서가 텍스트 끝에 있을 때만 포커스 이동 허용
const input = document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
if (input && position === input.value.length) {
// 커서가 텍스트 끝에 있으면 포커스 이동 허용
// Enter 키 처리
if (e.key === "Enter") {
e.preventDefault();
if (showVirtualKeyboard) {
// 가상 키보드가 열려있으면 검색 실행하고 키보드 닫기
handleSearchSubmit(searchQuery);
} else {
// 가상 키보드가 닫혀있으면 키보드 열기
setShowVirtualKeyboard(true);
}
return;
}
// 방향키 처리 - Spotlight 네비게이션 허용
const arrowKeys = [
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"ArrowDown",
"Left",
"Right",
"Up",
"Down",
];
if (arrowKeys.includes(e.key)) {
// 입력 필드가 비어있고 왼쪽 화살표인 경우에만 방지
if (
position === 0 &&
(e.key === "Left" || e.key === "ArrowLeft") &&
!searchQuery
) {
e.preventDefault();
return;
}
// 오른쪽 화살표 키 처리 - 포커스 이동 허용
if (e.key === "ArrowRight" || e.key === "Right") {
// 커서가 텍스트 끝에 있을 때만 포커스 이동 허용
const input = document.querySelector(
`[data-spotlight-id="input-field-box"] > input`
);
if (input && position === input.value.length) {
// 커서가 텍스트 끝에 있으면 포커스 이동 허용
return;
}
}
// 나머지 방향키는 Spotlight가 처리하도록 허용
return;
}
// 나머지 방향키는 Spotlight가 처리하도록 허용
return;
}
}, [searchQuery, position, handleSearchSubmit, showVirtualKeyboard]);
},
[searchQuery, position, handleSearchSubmit, showVirtualKeyboard]
);
const cursorPosition = useCallback(() => {
const input = document.querySelector(
@@ -397,12 +473,19 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, [panelInfo, firstSpot]);
// 키워드 클릭 핸들러
const handleKeywordClick = useCallback((keyword) => {
setSearchQuery(keyword);
handleSearchSubmit(keyword);
// 키워드 선택 알림
dispatch(showSuccessToast(`"${keyword}" 키워드로 검색합니다.`, { duration: 2000 }));
}, [handleSearchSubmit, dispatch]);
const handleKeywordClick = useCallback(
(keyword) => {
setSearchQuery(keyword);
handleSearchSubmit(keyword);
// 키워드 선택 알림
dispatch(
showSuccessToast(`"${keyword}" 키워드로 검색합니다.`, {
duration: 2000,
})
);
},
[handleSearchSubmit, dispatch]
);
// 상품 클릭 핸들러
const handleProductClick = useCallback((product) => {
@@ -417,58 +500,62 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, [dispatch]);
// ProductCard 컴포넌트
const ProductCard = useCallback(({
product,
index,
onClick,
showForYouBadge = true,
showBrandLogo = true,
showBrandName = true,
showProductTitle = true,
badgeText = "For You",
badgePosition = "top-right"
}) => {
return (
<SpottableProduct
key={`product-${index}`}
className={css.productCard}
onClick={() => onClick?.(product)}
spotlightId={`product-${index}`}
>
<div className={css.productImageWrapper}>
<img
src={product.image}
alt={product.title}
className={css.productImage}
/>
{showForYouBadge && product.isForYou && (
<div className={`${css.forYouBadge} ${css[badgePosition]}`}>
{badgeText}
</div>
)}
</div>
<div className={css.productInfo}>
{showBrandLogo && (
<div className={css.productBrandWrapper}>
<img
src={product.brandLogo}
alt={product.brandName}
className={css.brandLogo}
/>
</div>
)}
<div className={css.productDetails}>
{showBrandName && (
<div className={css.brandName}>{product.brandName}</div>
)}
{showProductTitle && (
<div className={css.productTitle}>{product.title}</div>
)}
const renderItem = useCallback(
(
// {
// product,
// index,
// onClick,
// showBrandLogo = true,
// showBrandName = true,
// showProductTitle = true,
// ...rest
// }
{ index, ...rest }
) => {
const {
showBrandLogo = true,
showBrandName = true,
showProductTitle = true,
image,
title,
brandLogo,
brandName,
} = hotPicks[index];
return (
<SpottableProduct
key={`product-${index}`}
className={css.productCard}
spotlightId={`product-${index}`}
{...rest}
>
<div className={css.productImageWrapper}>
<img src={image} alt={title} className={css.productImage} />
</div>
</div>
</SpottableProduct>
);
}, []);
<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>
</SpottableProduct>
);
},
[]
);
return (
<TPanel
@@ -494,8 +581,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
scrollable={true}
>
{/* 검색 입력 영역 */}
<InputContainer
className={css.inputContainer}
<InputContainer
className={classNames(
css.inputContainer,
inputFocus === true && css.inputFocus,
css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */
)}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
>
@@ -520,35 +611,37 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
tabIndex={0}
spotlightDisabled={false}
spotlightBoxDisabled={true}
onFocus={_onFocus}
onBlur={_onBlur}
/>
<SpottableMicButton
className={css.microphoneButton}
<SpottableMicButton
className={css.microphoneButton}
onClick={onCancel}
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
>
<div className={css.microphoneCircle}>
<img
src={micIcon}
alt="Microphone"
<img
src={micIcon}
alt="Microphone"
className={css.microphoneIcon}
/>
</div>
</SpottableMicButton>
{/* 테스트용 Toast 버튼 (개발용) */}
<SpottableMicButton
className={css.testToastButton}
{/* <SpottableMicButton
className={css.testToastButton}
onClick={handleTestToasts}
spotlightId="test-toast-button"
>
<div className={css.testButtonCircle}>
🧪
</div>
</SpottableMicButton>
<div className={css.testButtonCircle}>🧪</div>
</SpottableMicButton> */}
</div>
</InputContainer>
{/* 검색 결과 표시 영역 */}
{/* 결과갑 부분 작업중 */}
<SearchResultsNew />
{/* //결과갑 부분 작업중 */}
{/* 검색 결과 표시 영역
{searchPerformed && searchQuery !== null ? (
Object.keys(searchDatas).length > 0 ? (
<SearchResults
@@ -561,117 +654,139 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
)
) : (
<ContainerBasic className={css.contentContainer}>
{/* 최근 검색어 섹션 */}
<SectionContainer
className={css.section}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.RECENT_SEARCHES_SECTION}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Your Recent Searches</div>
</div>
<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>
</SectionContainer>
{/* 노출 조건 변경 필요. 포커스 블러만으로는 안됌.(가상 키보드 노출시가 맞을듯)
{inputFocus === false ? (
<>
{/* 최근 검색어 섹션
<SectionContainer
className={css.section}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.RECENT_SEARCHES_SECTION}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>
Your Recent Searches
</div>
</div>
<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>
</SectionContainer>
{/* 인기 검색어 섹션 */}
<SectionContainer
className={css.section}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.TOP_SEARCHES_SECTION}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Top Searches</div>
</div>
<div className={css.keywordList}>
{topSearches.map((keyword, index) => (
<SpottableKeyword
key={`top-${index}`}
className={css.keywordButton}
onClick={() => handleKeywordClick(keyword)}
spotlightId={`top-keyword-${index}`}
>
{keyword}
</SpottableKeyword>
))}
</div>
</SectionContainer>
{/* 인기 검색어 섹션
<SectionContainer
className={css.section}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.TOP_SEARCHES_SECTION}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Top Searches</div>
</div>
<div className={css.keywordList}>
{topSearches.map((keyword, index) => (
<SpottableKeyword
key={`top-${index}`}
className={css.keywordButton}
onClick={() => handleKeywordClick(keyword)}
spotlightId={`top-keyword-${index}`}
>
{keyword}
</SpottableKeyword>
))}
</div>
</SectionContainer>
{/* 인기 브랜드 섹션 */}
<SectionContainer
className={css.section}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.POPULAR_BRANDS_SECTION}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Popular Brands</div>
</div>
<div className={css.keywordList}>
{popularBrands.map((brand, index) => (
<SpottableKeyword
key={`brand-${index}`}
className={css.keywordButton}
onClick={() => handleKeywordClick(brand)}
spotlightId={`brand-${index}`}
>
{brand}
</SpottableKeyword>
))}
</div>
</SectionContainer>
{/* 인기 브랜드 섹션
<SectionContainer
className={css.section}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.POPULAR_BRANDS_SECTION}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Popular Brands</div>
</div>
<div className={css.keywordList}>
{popularBrands.map((brand, index) => (
<SpottableKeyword
key={`brand-${index}`}
className={css.keywordButton}
onClick={() => handleKeywordClick(brand)}
spotlightId={`brand-${index}`}
>
{brand}
</SpottableKeyword>
))}
</div>
</SectionContainer>
{/* Hot Picks for You 섹션 */}
<SectionContainer
className={css.section}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.HOT_PICKS_SECTION}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>Hot Picks for You</div>
{/* Hot Picks for You 섹션
<SectionContainer
className={css.hotpicksSection}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.HOT_PICKS_SECTION}
>
<div className={css.sectionHeader}>
<div className={css.sectionIndicator}></div>
<div className={css.sectionTitle}>
Hot Picks for You
</div>
</div>
<div className={css.productList}>
{hotPicks && hotPicks.length > 0 && (
<TVirtualGridList
dataSize={hotPicks.length}
direction="horizontal"
renderItem={renderItem}
// itemWidth={546}
itemWidth={416}
itemHeight={436}
spacing={20}
/>
)}
</div>
</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>
<div className={css.productList}>
{hotPicks.map((product, index) => (
<ProductCard
key={`product-${index}`}
product={product}
index={index}
onClick={handleProductClick}
showForYouBadge={true}
showBrandLogo={true}
showBrandName={true}
showProductTitle={true}
badgeText="For You"
badgePosition="top-right"
/>
))}
</div>
</SectionContainer>
)}
</ContainerBasic>
)}
*/}
</TVerticalPagenator>
)}
</ContainerBasic>
</TBody>
{/* Virtual Keyboard */}
<VirtualKeyboardContainer
{/* <VirtualKeyboardContainer
isVisible={showVirtualKeyboard}
onClose={() => setShowVirtualKeyboard(false)}
/>
/> */}
</TPanel>
);
}
}

View File

@@ -26,10 +26,16 @@
flex-direction: column;
justify-content: center;
align-items: center;
&.inputFocus {
padding-bottom: 10px;
}
&.searchValue {
padding-bottom: 55px;
padding-top: 55px;
}
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
@@ -45,7 +51,7 @@
> * {
margin-right: 15px;
&:last-child {
margin-right: 0;
}
@@ -71,12 +77,12 @@
padding: 20px 40px 20px 50px !important;
border: none !important;
background-color: #fff !important;
input {
text-align: center;
text-align: left;
color: black;
font-size: 42px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 700;
line-height: 42px;
outline: none;
@@ -89,13 +95,13 @@
outline: none !important;
border: none !important;
}
// TInput 내부 컨테이너의 포커스 스타일 완전 제거
&[data-spotlight-container="true"] {
outline: none !important;
border: none !important;
box-shadow: none !important;
&:focus,
&:focus-within {
outline: none !important;
@@ -110,7 +116,7 @@
border: 5px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
}
// TInput 컴포넌트 자체의 내부 포커스 스타일 제거
> div {
&:focus,
@@ -120,7 +126,7 @@
box-shadow: none !important;
}
}
// 모든 내부 요소의 포커스 스타일 완전 제거
* {
&:focus,
@@ -130,13 +136,13 @@
box-shadow: none !important;
}
}
// TInput 내부 Container의 포커스 제거
> div[data-spotlight-container="true"] {
outline: none !important;
border: none !important;
box-shadow: none !important;
&:focus,
&:focus-within {
outline: none !important;
@@ -144,13 +150,13 @@
box-shadow: none !important;
}
}
// InputField의 포커스 제거
input {
outline: none !important;
border: none !important;
box-shadow: none !important;
&:focus,
&:focus-within {
outline: none !important;
@@ -166,13 +172,13 @@
position: relative;
&::before {
content: '';
content: "";
width: 36.27px;
height: 36.27px;
position: absolute;
left: 1.95px;
top: 1.95px;
border: 3.90px solid black;
border: 3.9px solid black;
border-radius: 50%;
}
}
@@ -202,10 +208,7 @@
align-items: center;
.microphoneIcon {
width: 100%;
height: 100%;
object-fit: contain;
padding: 10px;
height: 50px;
box-sizing: border-box;
}
}
@@ -223,7 +226,7 @@
}
}
}
// 테스트용 Toast 버튼 스타일
.testToastButton {
width: 100px;
@@ -277,6 +280,64 @@
justify-content: flex-start;
align-items: flex-start;
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;
}
}
}
}
}
// 섹션 공통 스타일
@@ -291,7 +352,7 @@
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
@@ -306,7 +367,7 @@
> * {
margin-right: 12px;
&:last-child {
margin-right: 0;
}
@@ -315,14 +376,14 @@
.sectionIndicator {
width: 6px;
height: 36px;
background: #C70850;
background: #c70850;
}
.sectionTitle {
text-align: center;
color: black;
font-size: 42px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 700;
line-height: 42px;
word-wrap: break-word;
@@ -341,17 +402,13 @@
> * {
margin-right: 19px;
margin-bottom: 19px;
&:nth-child(3n) {
margin-right: 0;
}
}
.keywordButton {
padding: 20px;
background: white;
border-radius: 100px;
border: 5px solid #DADADA;
border: 5px solid #dadada;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
@@ -361,7 +418,7 @@
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
@@ -369,7 +426,7 @@
color: black;
font-size: 24px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 700;
line-height: 24px;
text-align: center;
@@ -388,50 +445,68 @@
}
}
}
}
// 상품 리스트 스타일 (Hot Picks for You)
.productList {
align-self: stretch;
padding-top: 30px;
.hotpicksSection {
padding-top: 63px;
padding-left: 60px;
width: 1800px;
height: 580px;
.sectionHeader {
width: 1800px;
height: 42px;
justify-content: flex-start;
align-items: flex-start;
align-items: center;
display: inline-flex;
flex-wrap: wrap;
> * {
margin-right: 19px;
margin-bottom: 19px;
&:nth-child(2n) {
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;
border: 5px solid #dadada;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
&:focus {
@@ -454,51 +529,6 @@
object-fit: cover;
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 {
@@ -511,7 +541,7 @@
> * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
@@ -524,7 +554,7 @@
> * {
margin-right: 10px;
&:last-child {
margin-right: 0;
}
@@ -547,7 +577,7 @@
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
@@ -557,7 +587,7 @@
text-align: center;
color: #808080;
font-size: 18px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 700;
line-height: 18px;
word-wrap: break-word;
@@ -567,7 +597,7 @@
align-self: stretch;
color: black;
font-size: 24px;
font-family: 'LG Smart UI';
font-family: "LG Smart UI";
font-weight: 400;
line-height: 24px;
word-wrap: break-word;
@@ -584,11 +614,11 @@
-ms-overflow-style: none;
overflow-y: auto;
height: 100%;
&::-webkit-scrollbar {
display: none;
}
// 스크롤 동작 개선
scroll-behavior: smooth;
}
@@ -606,7 +636,6 @@
[data-spotlight-id] {
&:focus {
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;
}
}
}
}
}
}
}