[search] searcinputoverlay
- searchInputoverlay작성 및 수정 - 패널 및 검색결과부분 수정.(버튼링크등등)
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import { getSearch } from '../../actions/searchActions';
|
||||
import TFullPopup from '../../components/TFullPopup/TFullPopup';
|
||||
import TInput, {
|
||||
ICONS,
|
||||
KINDS,
|
||||
} from '../../components/TInput/TInput';
|
||||
import css from './SearchInputOverlay.module.less';
|
||||
|
||||
const OverlayContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
enterTo: "default-element",
|
||||
restrict: "self-only", // 포커스를 overlay 내부로만 제한
|
||||
},
|
||||
"div"
|
||||
);
|
||||
|
||||
const SpottableKeyword = Spottable("div");
|
||||
|
||||
const SearchInputOverlay = ({
|
||||
isVisible,
|
||||
onClose,
|
||||
searchQuery = "",
|
||||
onSearchChange,
|
||||
onSearchSubmit,
|
||||
handleClick,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const recentResultSearches = useMemo(
|
||||
() => [
|
||||
"Puppy food",
|
||||
"Dog toy",
|
||||
"Mather's Day",
|
||||
"Gift",
|
||||
"Easter Day",
|
||||
"Royal Canin puppy food2",
|
||||
"Shark",
|
||||
],
|
||||
[]
|
||||
);
|
||||
// Input 창에서 API 호출 핸들러 (돋보기 아이콘 클릭 시에만)
|
||||
const handleSearchSubmit = useCallback(
|
||||
(query) => {
|
||||
console.log("[SearchInputOverlay] Search submit:", searchQuery);
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
dispatch(
|
||||
getSearch({
|
||||
service: "com.lgshop.app",
|
||||
query: query,
|
||||
domain: "theme,show,item",
|
||||
})
|
||||
);
|
||||
|
||||
// API 호출 후 Input 박스로 포커스 이동
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
Spotlight.focus("voice-overlay-input-box");
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[dispatch, searchQuery]
|
||||
);
|
||||
|
||||
// Input 창에서 엔터키 핸들러 (API 호출하지 않음)
|
||||
const handleInputKeyDown = useCallback((e) => {
|
||||
if (e.key === "Enter" || e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
// Enter 키로는 API 호출하지 않음
|
||||
// 돋보기 아이콘 클릭/Enter로만 API 호출
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDimClick = useCallback(
|
||||
(e) => {
|
||||
console.log("[VoiceInputOverlay] dimBackground clicked");
|
||||
onClose();
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const spotTime = setTimeout(() => {
|
||||
Spotlight.focus("search_input_overlay");
|
||||
}, 100);
|
||||
return () => {
|
||||
clearTimeout(spotTime);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TFullPopup
|
||||
open={isVisible}
|
||||
onClose={onClose}
|
||||
noAutoDismiss={true}
|
||||
spotlightRestrict="self-only"
|
||||
spotlightId={"search_input_overlay_container"}
|
||||
noAnimation={false}
|
||||
scrimType="transparent"
|
||||
className={css.tFullPopupWrapper}
|
||||
>
|
||||
<div className={css.overlayContainer}>
|
||||
{/* 배경 dim 레이어 - 클릭하면 닫힘 */}
|
||||
<div className={css.dimBackground} onClick={handleDimClick} />
|
||||
<OverlayContainer
|
||||
className={css.contentArea}
|
||||
spotlightId={"search_input_overlay"}
|
||||
spotlightDisabled={!isVisible}
|
||||
>
|
||||
{/* 입력창과 마이크 버튼 - SearchPanel.inputContainer와 동일한 구조 */}
|
||||
<div
|
||||
className={css.inputWrapper}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className={css.searchInputWrapper}>
|
||||
<TInput
|
||||
className={css.inputBox}
|
||||
kind={KINDS.withIcon}
|
||||
icon={ICONS.search}
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onIconClick={() => {
|
||||
handleSearchSubmit(searchQuery);
|
||||
}}
|
||||
spotlightId={"search_overlay_input_box"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 이곳에 작업 */}
|
||||
<div className={css.overLayRecent}>
|
||||
{recentResultSearches.map((keyword, index) => (
|
||||
<SpottableKeyword
|
||||
key={`recentResult-${index}`}
|
||||
className={css.keywordButton}
|
||||
onClick={() => handleClick(keyword)}
|
||||
spotlightId={`recent-Resultkeyword-${index}`}
|
||||
>
|
||||
{keyword}
|
||||
</SpottableKeyword>
|
||||
))}
|
||||
</div>
|
||||
</OverlayContainer>
|
||||
</div>
|
||||
</TFullPopup>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchInputOverlay;
|
||||
@@ -0,0 +1,171 @@
|
||||
@import "../../style/CommonStyle.module.less";
|
||||
|
||||
// TFullPopup wrapper - TFullPopup의 기본 스타일을 override하지 않음
|
||||
.tFullPopupWrapper {
|
||||
// TFullPopup의 기본 동작 유지
|
||||
}
|
||||
.overlayContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 999;
|
||||
pointer-events: all;
|
||||
}
|
||||
.dimBackground {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
cursor: pointer;
|
||||
}
|
||||
.contentArea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding-left: 120px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1002;
|
||||
pointer-events: none; // 빈 공간 클릭 시 dimBackground로 이벤트 전달
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// 입력창과 마이크 버튼 영역 - SearchPanel.inputContainer와 동일 (210px 높이)
|
||||
.inputWrapper {
|
||||
width: 100%;
|
||||
padding-top: 55px;
|
||||
padding-bottom: 30px;
|
||||
padding-left: 60px;
|
||||
padding-right: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1003;
|
||||
position: relative;
|
||||
pointer-events: all; // 입력 영역은 클릭 가능
|
||||
|
||||
.searchInputWrapper {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
min-height: 100px;
|
||||
max-height: 100px;
|
||||
|
||||
> * {
|
||||
margin-right: 15px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputBox {
|
||||
width: 880px;
|
||||
height: 100px !important;
|
||||
padding-left: 50px;
|
||||
padding-right: 40px;
|
||||
background: white;
|
||||
border-radius: 1000px;
|
||||
border: 5px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
z-index: 1003;
|
||||
position: relative;
|
||||
|
||||
> div:first-child {
|
||||
margin: 0 !important;
|
||||
width: calc(100% - 121px) !important;
|
||||
height: 90px !important;
|
||||
padding: 20px 40px 20px 0px !important;
|
||||
border: none !important;
|
||||
background-color: #fff !important;
|
||||
|
||||
input {
|
||||
text-align: left;
|
||||
color: black;
|
||||
font-size: 42px;
|
||||
font-family: "LG Smart UI";
|
||||
font-weight: 700;
|
||||
line-height: 42px;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
* {
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-within,
|
||||
&:focus {
|
||||
border: 5px solid @PRIMARY_COLOR_RED;
|
||||
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.overLayRecent {
|
||||
width: 790px;
|
||||
height: 488px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
* {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.keywordButton {
|
||||
height: 64px;
|
||||
background: white;
|
||||
border-radius: 100px;
|
||||
border: 5px solid #dadada;
|
||||
padding: 0 20px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
width: fit-content;
|
||||
> * {
|
||||
margin-bottom: 5px;
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border: 5px solid @PRIMARY_COLOR_RED;
|
||||
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,43 @@
|
||||
// src/views/SearchPanel/SearchPanel.new.jsx
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
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 SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
import { setContainerLastFocusedElement } from '@enact/spotlight/src/container';
|
||||
|
||||
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 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, searchMain } from '../../actions/searchActions';
|
||||
import {
|
||||
popPanel,
|
||||
updatePanel,
|
||||
} from '../../actions/panelActions';
|
||||
import {
|
||||
getSearch,
|
||||
resetSearch,
|
||||
searchMain,
|
||||
} from '../../actions/searchActions';
|
||||
import {
|
||||
showErrorToast,
|
||||
showInfoToast,
|
||||
@@ -26,50 +47,77 @@ import {
|
||||
showWarningToast,
|
||||
} from '../../actions/toastActions';
|
||||
import TBody from '../../components/TBody/TBody';
|
||||
import TInput, { ICONS, KINDS } from '../../components/TInput/TInput';
|
||||
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 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 {
|
||||
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 SearchInputOverlay from './SearchInpuOverlay';
|
||||
import css from './SearchPanel.new.module.less';
|
||||
import SearchResultsNew from './SearchResults.new';
|
||||
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';
|
||||
import VoiceInputOverlay, {
|
||||
VOICE_MODES,
|
||||
} from './VoiceInputOverlay/VoiceInputOverlay';
|
||||
|
||||
const ContainerBasic = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
||||
const ContainerBasic = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
"div"
|
||||
);
|
||||
|
||||
// 검색 입력 영역 컨테이너
|
||||
const InputContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
||||
const InputContainer = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
"div"
|
||||
);
|
||||
|
||||
// 콘텐츠 섹션 컨테이너
|
||||
const SectionContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
||||
const SectionContainer = SpotlightContainerDecorator(
|
||||
{ enterTo: "last-focused" },
|
||||
"div"
|
||||
);
|
||||
|
||||
// Spottable 컴포넌트들
|
||||
const SpottableMicButton = Spottable('div');
|
||||
const SpottableKeyword = Spottable('div');
|
||||
const SpottableProduct = Spottable('div');
|
||||
const SpottableLi = Spottable('li');
|
||||
const SpottableMicButton = Spottable("div");
|
||||
const SpottableKeyword = Spottable("div");
|
||||
const SpottableProduct = Spottable("div");
|
||||
const SpottableLi = Spottable("li");
|
||||
|
||||
const ITEMS_PER_PAGE = 9;
|
||||
|
||||
// Spotlight ID 상수
|
||||
const SPOTLIGHT_IDS = {
|
||||
SEARCH_INPUT_LAYER: 'search-input-layer',
|
||||
SEARCH_INPUT_BOX: 'search-input-box',
|
||||
MICROPHONE_BUTTON: 'microphone-button',
|
||||
RECENT_SEARCHES_SECTION: 'recent-searches-section',
|
||||
TOP_SEARCHES_SECTION: 'top-searches-section',
|
||||
POPULAR_BRANDS_SECTION: 'popular-brands-section',
|
||||
HOT_PICKS_SECTION: 'hot-picks-section',
|
||||
SEARCH_VERTICAL_PAGENATOR: 'search_verticalPagenator',
|
||||
SEARCH_INPUT_LAYER: "search-input-layer",
|
||||
SEARCH_INPUT_BOX: "search-input-box",
|
||||
MICROPHONE_BUTTON: "microphone-button",
|
||||
RECENT_SEARCHES_SECTION: "recent-searches-section",
|
||||
TOP_SEARCHES_SECTION: "top-searches-section",
|
||||
POPULAR_BRANDS_SECTION: "popular-brands-section",
|
||||
HOT_PICKS_SECTION: "hot-picks-section",
|
||||
SEARCH_VERTICAL_PAGENATOR: "search_verticalPagenator",
|
||||
};
|
||||
|
||||
export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOptions = [] }) {
|
||||
export default function SearchPanel({
|
||||
panelInfo,
|
||||
isOnTop,
|
||||
spotlightId,
|
||||
scrollOptions = [],
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const loadingComplete = useSelector((state) => state.common?.loadingComplete);
|
||||
const recommandedKeywords = useSelector(
|
||||
@@ -78,57 +126,74 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
const { searchDatas: searchDatas } = useSelector((state) => state.search);
|
||||
const searchPerformed = useSelector((state) => state.search.searchPerformed);
|
||||
const panels = useSelector((state) => state.panels.panels);
|
||||
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
|
||||
const shopperHouseData = useSelector(
|
||||
(state) => state.search.shopperHouseData
|
||||
);
|
||||
|
||||
const [firstSpot, setFirstSpot] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [paginatedKeywords, setPaginatedKeywords] = useState([]);
|
||||
const [pageChanged, setPageChanged] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState(panelInfo.searchVal ? panelInfo.searchVal : null);
|
||||
const [searchQuery, setSearchQuery] = useState(
|
||||
panelInfo.searchVal ? panelInfo.searchVal : null
|
||||
);
|
||||
const [position, setPosition] = useState(null);
|
||||
const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false);
|
||||
const [isVoiceOverlayVisible, setIsVoiceOverlayVisible] = useState(false);
|
||||
const [isSearchOverlayVisible, setIsSearchOverlayVisible] = useState(false);
|
||||
const [voiceMode, setVoiceMode] = useState(VOICE_MODES.PROMPT);
|
||||
|
||||
//인풋창 포커스 구분을 위함
|
||||
const [inputFocus, setInputFocus] = useState(false);
|
||||
const _onFocus = () => {
|
||||
const _onFocus = useCallback(() => {
|
||||
setInputFocus(true);
|
||||
};
|
||||
const _onBlur = () => {
|
||||
}, [inputFocus]);
|
||||
const _onBlur = useCallback(() => {
|
||||
setInputFocus(false);
|
||||
};
|
||||
}, [inputFocus]);
|
||||
|
||||
let searchQueryRef = usePrevious(searchQuery);
|
||||
let isOnTopRef = usePrevious(isOnTop);
|
||||
|
||||
const firstButtonSpotlightId = 'first-keyword-button';
|
||||
const firstButtonSpotlightId = "first-keyword-button";
|
||||
const cbChangePageRef = useRef(null);
|
||||
const [focusedContainerId, setFocusedContainerId] = useState(panelInfo?.focusedContainerId);
|
||||
const [focusedContainerId, setFocusedContainerId] = useState(
|
||||
panelInfo?.focusedContainerId
|
||||
);
|
||||
const focusedContainerIdRef = usePrevious(focusedContainerId);
|
||||
|
||||
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
|
||||
const recentSearches = useMemo(() => ['Puppy food', 'Dog toy', 'Fitness'], []);
|
||||
const recentSearches = useMemo(
|
||||
() => ["Puppy food", "Dog toy", "Fitness"],
|
||||
[]
|
||||
);
|
||||
|
||||
const recentResultSearches = useMemo(
|
||||
() => [
|
||||
'Puppy food',
|
||||
'Dog toy',
|
||||
"Puppy food",
|
||||
"Dog toy",
|
||||
"Mather's Day",
|
||||
'Gift',
|
||||
'Easter Day',
|
||||
'Royal Canin puppy food',
|
||||
'Shark',
|
||||
"Gift",
|
||||
"Easter Day",
|
||||
"Royal Canin puppy food2",
|
||||
"Shark",
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const topSearches = useMemo(
|
||||
() => ["Mather's Day", 'Gift', 'Easter Day', 'Royal Canin puppy food', 'Fitness', 'Parrot'],
|
||||
() => [
|
||||
"Mather's Day",
|
||||
"Gift",
|
||||
"Easter Day",
|
||||
"Royal Canin puppy food",
|
||||
"Fitness",
|
||||
"Parrot",
|
||||
],
|
||||
[]
|
||||
);
|
||||
const popularBrands = useMemo(
|
||||
() => ['Shark', 'Ninja', 'Skechers', 'LocknLock', '8Greens', 'LGE'],
|
||||
() => ["Shark", "Ninja", "Skechers", "LocknLock", "8Greens", "LGE"],
|
||||
[]
|
||||
);
|
||||
const hotPicks = useMemo(
|
||||
@@ -137,32 +202,32 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
id: 1,
|
||||
image: hotPicksImage,
|
||||
brandLogo: hotPicksBrandImage,
|
||||
brandName: 'Product Name',
|
||||
title: 'New Shark Vacuum! Your pet Hair Solution!',
|
||||
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!',
|
||||
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!',
|
||||
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!',
|
||||
brandName: "Product Name",
|
||||
title: "New Shark Vacuum! Your pet Hair Solution!",
|
||||
isForYou: true,
|
||||
},
|
||||
],
|
||||
@@ -242,7 +307,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
dispatch(
|
||||
sendLogTotalRecommend({
|
||||
query: searchQuery,
|
||||
searchType: searchPerformed ? 'query' : 'keyword',
|
||||
searchType: searchPerformed ? "query" : "keyword",
|
||||
result: result,
|
||||
contextName: LOG_CONTEXT_NAME.SEARCH,
|
||||
messageId: LOG_MESSAGE_ID.SEARCH_ITEM,
|
||||
@@ -266,9 +331,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
if (query.trim()) {
|
||||
dispatch(
|
||||
getSearch({
|
||||
service: 'com.lgshop.app',
|
||||
service: "com.lgshop.app",
|
||||
query: query,
|
||||
domain: 'theme,show,item',
|
||||
domain: "theme,show,item",
|
||||
})
|
||||
);
|
||||
|
||||
@@ -301,7 +366,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
}, [currentPage]);
|
||||
|
||||
const hasPrevPage = currentPage > 1;
|
||||
const hasNextPage = currentPage * ITEMS_PER_PAGE < recommandedKeywords?.length;
|
||||
const hasNextPage =
|
||||
currentPage * ITEMS_PER_PAGE < recommandedKeywords?.length;
|
||||
|
||||
useEffect(() => {
|
||||
if (panelInfo && isOnTop) {
|
||||
@@ -332,7 +398,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
}
|
||||
|
||||
// Enter 키 처리
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (showVirtualKeyboard) {
|
||||
// 가상 키보드가 열려있으면 검색 실행하고 키보드 닫기
|
||||
@@ -346,26 +412,32 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
|
||||
// 방향키 처리 - Spotlight 네비게이션 허용
|
||||
const arrowKeys = [
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'Left',
|
||||
'Right',
|
||||
'Up',
|
||||
'Down',
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowDown",
|
||||
"Left",
|
||||
"Right",
|
||||
"Up",
|
||||
"Down",
|
||||
];
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
// 오른쪽 화살표 키 처리 - 포커스 이동 허용
|
||||
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) {
|
||||
// 커서가 텍스트 끝에 있으면 포커스 이동 허용
|
||||
return;
|
||||
@@ -380,7 +452,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
);
|
||||
|
||||
const cursorPosition = useCallback(() => {
|
||||
const input = document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
|
||||
const input = document.querySelector(
|
||||
`[data-spotlight-id="input-field-box"] > input`
|
||||
);
|
||||
if (input) {
|
||||
setPosition(input.selectionStart);
|
||||
}
|
||||
@@ -403,10 +477,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
setIsVoiceOverlayVisible(false);
|
||||
return;
|
||||
}
|
||||
if (searchQuery === null || searchQuery === '') {
|
||||
if (searchQuery === null || searchQuery === "") {
|
||||
dispatch(popPanel(panel_names.SEARCH_PANEL));
|
||||
} else {
|
||||
setSearchQuery('');
|
||||
setSearchQuery("");
|
||||
setCurrentPage(1);
|
||||
dispatch(resetSearch());
|
||||
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
|
||||
@@ -421,7 +495,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
Spotlight.resume();
|
||||
setFirstSpot(true);
|
||||
if (panelInfo.currentSpot) {
|
||||
if (panels[panels.length - 1]?.name === 'searchpanel') {
|
||||
if (panels[panels.length - 1]?.name === "searchpanel") {
|
||||
Spotlight.focus(panelInfo.currentSpot);
|
||||
}
|
||||
}
|
||||
@@ -443,27 +517,31 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
const handleKeywordClick = useCallback(
|
||||
(keyword) => {
|
||||
setSearchQuery(keyword);
|
||||
|
||||
handleSearchSubmit(keyword);
|
||||
// 키워드 선택 알림
|
||||
dispatch(
|
||||
showSuccessToast(`"${keyword}" 키워드로 검색합니다.`, {
|
||||
duration: 2000,
|
||||
})
|
||||
);
|
||||
|
||||
setIsSearchOverlayVisible(false);
|
||||
setInputFocus(false);
|
||||
},
|
||||
[handleSearchSubmit, dispatch]
|
||||
);
|
||||
|
||||
const handleKeywordInput = (keyword) => {
|
||||
setSearchQuery(keyword);
|
||||
setIsSearchOverlayVisible(false);
|
||||
setInputFocus(false);
|
||||
};
|
||||
|
||||
// 상품 클릭 핸들러
|
||||
const handleProductClick = useCallback((product) => {
|
||||
// 상품 상세 페이지로 이동하는 로직 구현
|
||||
console.log('Product clicked:', product);
|
||||
console.log("Product clicked:", product);
|
||||
}, []);
|
||||
|
||||
// 테스트용 Toast 핸들러들
|
||||
const handleTestToasts = useCallback(() => {
|
||||
// 간단한 Toast 테스트
|
||||
dispatch(showSuccessToast('테스트 메시지입니다', { duration: 3000 }));
|
||||
dispatch(showSuccessToast("테스트 메시지입니다", { duration: 3000 }));
|
||||
}, [dispatch]);
|
||||
|
||||
// ProductCard 컴포넌트
|
||||
@@ -502,12 +580,20 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
<div className={css.productInfo}>
|
||||
{showBrandLogo && (
|
||||
<div className={css.productBrandWrapper}>
|
||||
<img src={brandLogo} alt={brandName} className={css.brandLogo} />
|
||||
<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>}
|
||||
{showBrandName && (
|
||||
<div className={css.brandName}>{brandName}</div>
|
||||
)}
|
||||
{showProductTitle && (
|
||||
<div className={css.productTitle}>{title}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SpottableProduct>
|
||||
@@ -516,15 +602,26 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
[]
|
||||
);
|
||||
|
||||
//test
|
||||
useEffect(() => {
|
||||
console.log('###searchDatas', searchDatas);
|
||||
console.log('###panelInfo', panelInfo);
|
||||
}, [searchDatas, panelInfo]);
|
||||
if (searchPerformed && searchQuery !== null && inputFocus === true) {
|
||||
setIsSearchOverlayVisible(true);
|
||||
if (cbChangePageRef.current) {
|
||||
cbChangePageRef.current(0, true);
|
||||
}
|
||||
}
|
||||
}, [searchPerformed, searchQuery, inputFocus]);
|
||||
|
||||
return (
|
||||
<TPanel className={css.container} handleCancel={onCancel} spotlightId={spotlightId}>
|
||||
<TBody className={css.tBody} scrollable={true} spotlightDisabled={!isOnTop}>
|
||||
<TPanel
|
||||
className={css.container}
|
||||
handleCancel={onCancel}
|
||||
spotlightId={spotlightId}
|
||||
>
|
||||
<TBody
|
||||
className={css.tBody}
|
||||
scrollable={true}
|
||||
spotlightDisabled={!isOnTop}
|
||||
>
|
||||
<ContainerBasic>
|
||||
{isOnTop && (
|
||||
<TVerticalPagenator
|
||||
@@ -543,8 +640,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
className={classNames(
|
||||
css.inputContainer,
|
||||
inputFocus === true && css.inputFocus,
|
||||
searchDatas && css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
|
||||
isVoiceOverlayVisible && css.hidden
|
||||
searchDatas &&
|
||||
css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */,
|
||||
(isVoiceOverlayVisible || isSearchOverlayVisible) &&
|
||||
css.hidden
|
||||
)}
|
||||
data-wheel-point={true}
|
||||
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
|
||||
@@ -576,14 +675,18 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
className={css.microphoneButton}
|
||||
onClick={onClickMic}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
onClickMic();
|
||||
}
|
||||
}}
|
||||
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
|
||||
>
|
||||
<div className={css.microphoneCircle}>
|
||||
<img src={micIcon} alt="Microphone" className={css.microphoneIcon} />
|
||||
<img
|
||||
src={micIcon}
|
||||
alt="Microphone"
|
||||
className={css.microphoneIcon}
|
||||
/>
|
||||
</div>
|
||||
</SpottableMicButton>
|
||||
|
||||
@@ -598,9 +701,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
</div>
|
||||
</InputContainer>
|
||||
|
||||
{/* 검색내용이 존재하고, 인풋창에 포커스가 가서 노출 시작 */}
|
||||
{inputFocus === true &&
|
||||
(searchDatas?.item?.length > 0 || searchDatas?.show?.length > 0) && (
|
||||
{/* 검색내용이 존재하고, 인풋창에 포커스가 가서 노출 시작 searchInputOverlay 변경중*/}
|
||||
|
||||
{/* {inputFocus === true &&
|
||||
(searchDatas?.item?.length > 0 ||
|
||||
searchDatas?.show?.length > 0) && (
|
||||
<>
|
||||
<div className={css.overLay}></div>
|
||||
<div className={css.overLayRecent}>
|
||||
@@ -616,7 +721,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
{/* 검색내용이 존재하고, 인풋창에 포커스가 가서 노출 끝! */}
|
||||
|
||||
{/* 결과갑 부분 작업중 시작! */}
|
||||
@@ -629,6 +734,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
itemInfo={searchDatas.item}
|
||||
showInfo={searchDatas.show}
|
||||
shopperHouseInfo={shopperHouseData}
|
||||
keywordClick={handleKeywordClick}
|
||||
/>
|
||||
) : (
|
||||
<ContainerBasic className={css.contentContainer}>
|
||||
@@ -644,7 +750,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
>
|
||||
<div className={css.sectionHeader}>
|
||||
<div className={css.sectionIndicator}></div>
|
||||
<div className={css.sectionTitle}>Your Recent Searches</div>
|
||||
<div className={css.sectionTitle}>
|
||||
Your Recent Searches
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.keywordList}>
|
||||
{recentSearches.map((keyword, index) => (
|
||||
@@ -716,7 +824,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
>
|
||||
<div className={css.sectionHeader}>
|
||||
<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 className={css.productList}>
|
||||
{hotPicks && hotPicks.length > 0 && (
|
||||
@@ -775,6 +885,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
onSearchSubmit={handleSearchSubmit}
|
||||
/>
|
||||
)}
|
||||
{isSearchOverlayVisible && (
|
||||
<SearchInputOverlay
|
||||
isVisible={isSearchOverlayVisible}
|
||||
onClose={() => setIsSearchOverlayVisible(false)}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
onSearchSubmit={handleSearchSubmit}
|
||||
handleClick={handleKeywordInput}
|
||||
/>
|
||||
)}
|
||||
</TPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ const SearchResultsNew = ({
|
||||
showInfo,
|
||||
themeInfo,
|
||||
shopperHouseInfo,
|
||||
onClick,
|
||||
keywordClick,
|
||||
}) => {
|
||||
// ShopperHouse 데이터를 ItemCard 형식으로 변환
|
||||
const convertedShopperHouseItems = useMemo(() => {
|
||||
@@ -204,23 +204,29 @@ const SearchResultsNew = ({
|
||||
[themeInfo]
|
||||
);
|
||||
|
||||
const _onClick = useCallback(
|
||||
(e) => {
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
},
|
||||
[onClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={css.searchBox}>
|
||||
<div 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>
|
||||
<SpottableLi
|
||||
className={css.topBoxListItem}
|
||||
onClick={() => keywordClick("Puppy food")}
|
||||
>
|
||||
Puppy food
|
||||
</SpottableLi>
|
||||
<SpottableLi
|
||||
className={css.topBoxListItem}
|
||||
onClick={() => keywordClick("Dog toy")}
|
||||
>
|
||||
Dog toy
|
||||
</SpottableLi>
|
||||
<SpottableLi
|
||||
className={css.topBoxListItem}
|
||||
onClick={() => keywordClick("Fitness")}
|
||||
>
|
||||
Fitness
|
||||
</SpottableLi>
|
||||
</ul>
|
||||
</div>
|
||||
{themeInfo && themeInfo?.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user