[251015] fix: VoiceInputOverlay dim click

🕐 커밋 시간: 2025. 10. 15. 21:00:03

📊 변경 통계:
  • 총 파일: 6개
  • 추가: +2줄
  • 삭제: -1781줄

📁 추가된 파일:
  + com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoiceListening.figma.jsx

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.module.less
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.module.less

🗑️ 삭제된 파일:
  - com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new copy.jsx
  - com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.module copy.less
  - com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/OverlayFirst.figma.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new copy.jsx (javascript):
     Deleted: _onFocus(), _onBlur()
  📄 com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.module copy.less (unknown):
     Deleted: translateY(), child(), media()

Performance: 코드 최적화로 성능 개선 기대
This commit is contained in:
2025-10-15 21:00:05 +09:00
parent 31bd1a34b3
commit 0994ca98a8
6 changed files with 2 additions and 1781 deletions

View File

@@ -1,868 +0,0 @@
// src/views/SearchPanel/SearchPanel.new.jsx
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import classNames from 'classnames';
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 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,
searchMain,
} from '../../actions/searchActions';
import {
showErrorToast,
showInfoToast,
showSearchErrorToast,
showSearchSuccessToast,
showSuccessToast,
showWarningToast,
} 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 SearchResultsNew from './SearchResults.new';
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';
const ContainerBasic = SpotlightContainerDecorator(
{ enterTo: "last-focused" },
"div"
);
// 검색 입력 영역 컨테이너
const InputContainer = 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 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",
};
export default function SearchPanel({
panelInfo,
isOnTop,
spotlightId,
scrollOptions = [],
}) {
const dispatch = useDispatch();
const loadingComplete = useSelector((state) => state.common?.loadingComplete);
const recommandedKeywords = useSelector(
(state) => state.myPage.recommandedKeywordData.data?.keywords
);
const { searchDatas: searchDatas } = useSelector((state) => state.search);
const searchPerformed = useSelector((state) => state.search.searchPerformed);
const panels = useSelector((state) => state.panels.panels);
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 [position, setPosition] = useState(null);
const [showVirtualKeyboard, setShowVirtualKeyboard] = useState(false);
const [isVoiceOverlayVisible, setIsVoiceOverlayVisible] = useState(false);
const [voiceMode, setVoiceMode] = useState(VOICE_MODES.PROMPT);
//인풋창 포커스 구분을 위함
const [inputFocus, setInputFocus] = useState(false);
const _onFocus = () => {
setInputFocus(true);
};
const _onBlur = () => {
setInputFocus(false);
};
let searchQueryRef = usePrevious(searchQuery);
let isOnTopRef = usePrevious(isOnTop);
const firstButtonSpotlightId = "first-keyword-button";
const cbChangePageRef = useRef(null);
const [focusedContainerId, setFocusedContainerId] = useState(
panelInfo?.focusedContainerId
);
const focusedContainerIdRef = usePrevious(focusedContainerId);
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
const recentSearches = useMemo(
() => ["Puppy food", "Dog toy", "Fitness"],
[]
);
const recentResultSearches = useMemo(
() => [
"Puppy food",
"Dog toy",
"Mather's Day",
"Gift",
"Easter Day",
"Royal Canin puppy food",
"Shark",
],
[]
);
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,
},
],
[]
);
// Voice overlay suggestions (동적으로 변경 가능)
const voiceSuggestions = useMemo(
() => [
'" Can you recommend a good budget cordless vacuum? "',
'" Show me trending skincare. "',
'" Find the newest Nike sneakers. "',
'" Show me snail cream that helps with sensitive skin. "',
'" Recommend a tasty melatonin gummy. "',
],
[]
);
useEffect(() => {
if (loadingComplete && !recommandedKeywords) {
dispatch(getMyRecommandedKeyword());
}
}, [loadingComplete]);
useEffect(() => {
if (isOnTop) {
let menu;
if (!searchPerformed) menu = LOG_MENU.SEARCH_SEARCH;
else {
if (searchQueryRef.current)
menu =
Object.keys(searchDatas).length > 0
? LOG_MENU.SEARCH_RESULT
: LOG_MENU.SEARCH_BEST_SELLER;
}
dispatch(sendLogGNB(menu));
}
}, [isOnTop, searchDatas, searchPerformed]);
useEffect(() => {
if (!searchQuery) {
dispatch(resetSearch());
}
}, [dispatch]);
useEffect(() => {
if (recommandedKeywords) {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
setPaginatedKeywords(recommandedKeywords.slice(startIndex, endIndex));
}
}, [recommandedKeywords, currentPage]);
useEffect(() => {
if (pageChanged && paginatedKeywords.length > 0) {
Spotlight.focus(firstButtonSpotlightId);
setPageChanged(false);
}
}, [pageChanged, paginatedKeywords]);
const handleSearchChange = useCallback((e) => {
const query = e.value;
if (query.length <= 255) {
setSearchQuery(query);
}
}, []);
useEffect(() => {
const result = Object.values(searchDatas).reduce((acc, curr) => {
return acc + curr.length;
}, 0);
if (searchQuery) {
dispatch(
sendLogTotalRecommend({
query: searchQuery,
searchType: searchPerformed ? "query" : "keyword",
result: result,
contextName: LOG_CONTEXT_NAME.SEARCH,
messageId: LOG_MESSAGE_ID.SEARCH_ITEM,
})
);
// 검색 완료 후 결과에 따른 Toast 표시
if (searchPerformed && searchQuery.trim()) {
if (result > 0) {
dispatch(showSearchSuccessToast(searchQuery, result));
} else {
dispatch(showSearchErrorToast(searchQuery));
}
}
}
}, [searchDatas, searchPerformed, searchQuery, dispatch]);
const handleSearchSubmit = useCallback(
(query) => {
if (!searchPerformed && !query) return;
if (query.trim()) {
dispatch(
getSearch({
service: "com.lgshop.app",
query: query,
domain: "theme,show,item",
})
);
// 검색 시작 알림 (선택사항)
dispatch(showSuccessToast(`"${query}" 검색 중...`, { duration: 2000 }));
} else {
dispatch(resetSearch());
}
setSearchQuery(query);
// 검색 시 가상 키보드 숨김
setShowVirtualKeyboard(false);
},
[dispatch, searchPerformed, searchDatas, searchQuery]
);
const handleNext = useCallback(() => {
if (!isOnTopRef.current) {
return;
}
setCurrentPage((prev) => prev + 1);
setPageChanged(true);
}, [currentPage]);
const handlePrev = useCallback(() => {
if (!isOnTopRef.current) {
return;
}
setCurrentPage((prev) => (prev > 1 ? prev - 1 : prev));
setPageChanged(true);
}, [currentPage]);
const hasPrevPage = currentPage > 1;
const hasNextPage =
currentPage * ITEMS_PER_PAGE < recommandedKeywords?.length;
useEffect(() => {
if (panelInfo && isOnTop) {
if (panelInfo.currentSpot && firstSpot) {
Spotlight.focus(panel_names.SEARCH_PANEL);
}
}
}, [panelInfo, isOnTop]);
useEffect(() => {
return () => {
dispatch(
updatePanel({
name: panel_names.SEARCH_PANEL,
panelInfo: {
searchVal: searchQueryRef.current,
focusedContainerId: focusedContainerIdRef.current,
},
})
);
};
}, []);
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();
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;
}
},
[searchQuery, position, handleSearchSubmit, showVirtualKeyboard]
);
const cursorPosition = useCallback(() => {
const input = document.querySelector(
`[data-spotlight-id="input-field-box"] > input`
);
if (input) {
setPosition(input.selectionStart);
}
}, []);
const onClickMic = useCallback(() => {
if (!isOnTopRef.current) {
return;
}
// 마이크 버튼 클릭 시 voice overlay 토글
setIsVoiceOverlayVisible((prev) => !prev);
}, []);
const onCancel = useCallback(() => {
if (!isOnTopRef.current) {
return;
}
if (searchQuery === null || searchQuery === "") {
dispatch(popPanel(panel_names.SEARCH_PANEL));
} else {
setSearchQuery("");
setCurrentPage(1);
dispatch(resetSearch());
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}
}, [searchQuery, dispatch]);
const onFocusedContainerId = useCallback(
(containerId) => {
setFocusedContainerId(containerId);
if (!firstSpot) {
setTimeout(() => {
Spotlight.resume();
setFirstSpot(true);
if (panelInfo.currentSpot) {
if (panels[panels.length - 1]?.name === "searchpanel") {
Spotlight.focus(panelInfo.currentSpot);
}
}
}, 0);
}
},
[panelInfo, firstSpot]
);
const panelInfoFall = useMemo(() => {
const newPanelInfo = { ...panelInfo };
if (firstSpot) {
newPanelInfo.currentSpot = null;
}
return newPanelInfo;
}, [panelInfo, firstSpot]);
// 키워드 클릭 핸들러
const handleKeywordClick = useCallback(
(keyword) => {
setSearchQuery(keyword);
handleSearchSubmit(keyword);
// 키워드 선택 알림
dispatch(
showSuccessToast(`"${keyword}" 키워드로 검색합니다.`, {
duration: 2000,
})
);
},
[handleSearchSubmit, dispatch]
);
// 상품 클릭 핸들러
const handleProductClick = useCallback((product) => {
// 상품 상세 페이지로 이동하는 로직 구현
console.log("Product clicked:", product);
}, []);
// 테스트용 Toast 핸들러들
const handleTestToasts = useCallback(() => {
// 간단한 Toast 테스트
dispatch(showSuccessToast("테스트 메시지입니다", { duration: 3000 }));
}, [dispatch]);
// ProductCard 컴포넌트
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 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>
);
},
[]
);
//test
useEffect(() => {
console.log("###searchDatas", searchDatas);
console.log("###panelInfo", panelInfo);
}, [searchDatas, panelInfo]);
return (
<TPanel
className={css.container}
handleCancel={onCancel}
spotlightId={spotlightId}
>
<TBody
className={css.tBody}
scrollable={true}
spotlightDisabled={!isOnTop}
>
<ContainerBasic>
{isOnTop && (
<TVerticalPagenator
className={css.tVerticalPagenator}
spotlightId={SPOTLIGHT_IDS.SEARCH_VERTICAL_PAGENATOR}
defaultContainerId={panelInfo?.focusedContainerId}
disabled={!isOnTop}
onFocusedContainerId={onFocusedContainerId}
cbChangePageRef={cbChangePageRef}
topMargin={36}
scrollable={true}
>
{/* 검색 내용있을때 검색 부분 */}
{/* 검색 입력 영역 - overlay 열릴 때 숨김 */}
{!isVoiceOverlayVisible && (
<InputContainer
className={classNames(
css.inputContainer,
inputFocus === true && css.inputFocus,
searchDatas &&
css.searchValue /* 이건 결과값 있을때만. 조건 추가필요 */
)}
data-wheel-point={true}
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_LAYER}
>
<div className={css.searchInputWrapper}>
<TInput
className={css.inputBox}
kind={KINDS.withIcon}
icon={ICONS.search}
value={searchQuery}
onChange={handleSearchChange}
onIconClick={() => {
if (showVirtualKeyboard) {
handleSearchSubmit(searchQuery);
} else {
setShowVirtualKeyboard(true);
}
}}
onKeyDown={handleKeydown}
onKeyUp={cursorPosition}
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
forcedSpotlight="recent-keyword-0"
tabIndex={0}
spotlightBoxDisabled={true}
onFocus={_onFocus}
onBlur={_onBlur}
/>
<SpottableMicButton
className={css.microphoneButton}
onClick={onClickMic}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onClickMic();
}
}}
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
>
<div className={css.microphoneCircle}>
<img
src={micIcon}
alt="Microphone"
className={css.microphoneIcon}
/>
</div>
</SpottableMicButton>
{/* 테스트용 Toast 버튼 (개발용) */}
{/* <SpottableMicButton
className={css.testToastButton}
onClick={handleTestToasts}
spotlightId="test-toast-button"
>
<div className={css.testButtonCircle}>🧪</div>
</SpottableMicButton> */}
</div>
</InputContainer>
)}
{/* 검색내용이 존재하고, 인풋창에 포커스가 가서 노출 시작 */}
{inputFocus === true &&
(searchDatas?.item?.length > 0 ||
searchDatas?.show?.length > 0) && (
<>
<div className={css.overLay}></div>
<div className={css.overLayRecent}>
{recentResultSearches.map((keyword, index) => (
<SpottableKeyword
key={`recentResult-${index}`}
className={css.keywordButton}
onClick={() => handleKeywordClick(keyword)}
spotlightId={`recent-Resultkeyword-${index}`}
>
{keyword}
</SpottableKeyword>
))}
</div>
</>
)}
{/* 검색내용이 존재하고, 인풋창에 포커스가 가서 노출 끝! */}
{/* 결과갑 부분 작업중 시작! */}
{/* 결과갑 부분 작업중 끝! */}
{/* 검색 결과 표시 영역 */}
{searchPerformed && searchQuery !== null ? (
<SearchResultsNew
themeInfo={searchDatas.theme}
itemInfo={searchDatas.item}
showInfo={searchDatas.show}
/>
) : (
<ContainerBasic
className={css.contentContainer}
>
{/* 노출 조건 변경 필요. 포커스 블러만으로는 안됌.(가상 키보드 노출시가 맞을듯) */}
{/* {inputFocus === false ? ( */}
{/* {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.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.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> */}
{/* )} */}
</ContainerBasic>
)}
</TVerticalPagenator>
)}
</ContainerBasic>
</TBody>
{/* Virtual Keyboard */}
{/* <VirtualKeyboardContainer
isVisible={showVirtualKeyboard}
onClose={() => setShowVirtualKeyboard(false)}
/> */}
{/* Voice Input Overlay */}
<VoiceInputOverlay
isVisible={isVoiceOverlayVisible}
onClose={() => setIsVoiceOverlayVisible(false)}
mode={voiceMode}
suggestions={voiceSuggestions}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
onSearchSubmit={handleSearchSubmit}
/>
</TPanel>
);
}

View File

@@ -1,725 +0,0 @@
// src/views/SearchPanel/SearchPanel.module.less
@import "../../style/CommonStyle.module.less";
@import "../../style/utils.module.less";
.container {
background-color: @BG_COLOR_01;
.tBody {
height: 100%;
> div {
width: 100%;
height: 100%;
}
.focusedContainerId {
height: 100%;
}
}
// 검색 입력 영역 스타일
.inputContainer {
padding-top: 180px;
padding-bottom: 94px;
padding-left: 60px;
padding-right: 60px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 2;
position: relative;
&.inputFocus {
padding-bottom: 10px;
}
&.searchValue {
padding-bottom: 55px;
padding-top: 55px;
}
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.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: 1001;
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;
}
// TInput 내부 컨테이너의 포커스 스타일 완전 제거
&[data-spotlight-container="true"] {
outline: none !important;
border: none !important;
box-shadow: none !important;
&:focus,
&:focus-within {
outline: none !important;
border: none !important;
box-shadow: none !important;
}
}
}
&:focus-within,
&:focus {
border: 5px solid @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
}
// TInput 컴포넌트 자체의 내부 포커스 스타일 제거
> div {
&:focus,
&:focus-within {
outline: none !important;
border: none !important;
box-shadow: none !important;
}
}
// 모든 내부 요소의 포커스 스타일 완전 제거
* {
&:focus,
&:focus-within {
outline: none !important;
border: none !important;
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;
border: none !important;
box-shadow: none !important;
}
}
// InputField의 포커스 제거
input {
outline: none !important;
border: none !important;
box-shadow: none !important;
&:focus,
&:focus-within {
outline: none !important;
border: none !important;
box-shadow: none !important;
}
}
// 검색 아이콘 스타일
.searchIcon {
width: 41px;
height: 41px;
position: relative;
&::before {
content: "";
width: 36.27px;
height: 36.27px;
position: absolute;
left: 1.95px;
top: 1.95px;
border: 3.9px solid black;
border-radius: 50%;
}
}
}
// 마이크 버튼 스타일
.microphoneButton {
width: 100px;
height: 100px;
position: relative;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
z-index: 1001;
.microphoneCircle {
width: 100%;
height: 100%;
position: relative;
background: white;
overflow: hidden;
border-radius: 1000px;
border: 5px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease;
.microphoneIcon {
height: 50px;
box-sizing: border-box;
transition: filter 0.3s ease;
}
}
&:hover {
.microphoneCircle {
border-color: @PRIMARY_COLOR_RED;
}
}
&:focus {
.microphoneCircle {
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
}
}
// active 상태 (음성 입력 모드)
&.active {
.microphoneCircle {
background-color: @PRIMARY_COLOR_RED;
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(229, 9, 20, 0.5);
.microphoneIcon {
filter: brightness(0) invert(1); // 아이콘을 흰색으로 변경
}
}
}
}
// 테스트용 Toast 버튼 스타일
.testToastButton {
width: 100px;
height: 100px;
position: relative;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
margin-left: 15px;
.testButtonCircle {
width: 100%;
height: 100%;
position: relative;
background: #ff6b6b;
overflow: hidden;
border-radius: 1000px;
border: 5px solid #ff4757;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
color: white;
}
&:hover {
.testButtonCircle {
border-color: @PRIMARY_COLOR_RED;
background: #ff5252;
}
}
&:focus {
.testButtonCircle {
border-color: @PRIMARY_COLOR_RED;
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
}
}
}
}
}
// 컨텐츠 컨테이너
.contentContainer {
width: 100%;
height: 100%;
overflow: hidden;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
.inputFocusBox {
width: 935px;
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;
}
}
}
}
}
// 섹션 공통 스타일
.section {
align-self: stretch;
padding-top: 63px;
padding-left: 60px;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
display: flex;
> * {
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
}
.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;
}
}
// 키워드 리스트 스타일 (최근 검색어, 인기 검색어, 브랜드)
.keywordList {
align-self: stretch;
padding-top: 30px;
justify-content: flex-start;
align-items: flex-start;
display: inline-flex;
flex-wrap: wrap;
> * {
margin-right: 19px;
margin-bottom: 19px;
}
.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;
> * {
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;
}
}
}
}
.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;
}
}
}
}
}
}
// 스크롤 스타일
.tVerticalPagenator {
scrollbar-width: none;
-ms-overflow-style: none;
overflow-y: auto;
height: 100%;
position: relative;
&::-webkit-scrollbar {
display: none;
}
// 스크롤 동작 개선
scroll-behavior: smooth;
}
// 반응형 디자인 (필요시)
@media (max-width: 1920px) {
.section {
.sectionHeader {
width: 100%;
}
}
}
// Spotlight 포커스 스타일
[data-spotlight-id] {
&:focus {
// outline: 2px solid @PRIMARY_COLOR_RED;
}
}
}
.overLay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.overLayRecent {
position: absolute;
left: 403px;
top: 172px;
width: 995px;
height: 488px;
z-index: 2;
display: flex;
flex-direction: column;
* {
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;
}
}
}

View File

@@ -1,187 +0,0 @@
<div style={{ width: '642px', height: '437px', position: 'relative', borderRadius: 12 }}>
<div
style={{
width: 642,
left: 0,
top: 0,
position: 'absolute',
textAlign: 'center',
color: 'white',
fontSize: 42,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 42,
wordWrap: 'break-word',
}}
>
Try saying
</div>
<div
style={{
left: 0,
top: 57,
position: 'absolute',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'center',
gap: 15,
display: 'inline-flex',
}}
>
<div
style={{
paddingLeft: 30,
paddingRight: 30,
paddingTop: 20,
paddingBottom: 20,
background: 'rgba(68, 68, 68, 0.50)',
boxShadow: '0px 10px 30px rgba(0, 0, 0, 0.35)',
borderRadius: 1000,
outline: '2px rgba(251, 251, 251, 0.20) solid',
outlineOffset: '-2px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: '#EAEAEA',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Can you recommend a good budget cordless vacuum?
</div>
</div>
<div
style={{
paddingLeft: 30,
paddingRight: 30,
paddingTop: 20,
paddingBottom: 20,
background: 'rgba(68, 68, 68, 0.50)',
boxShadow: '0px 10px 30px rgba(0, 0, 0, 0.35)',
borderRadius: 1000,
outline: '2px rgba(251, 251, 251, 0.20) solid',
outlineOffset: '-2px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: '#EAEAEA',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Show me trending skincare.
</div>
</div>
<div
style={{
paddingLeft: 30,
paddingRight: 30,
paddingTop: 20,
paddingBottom: 20,
background: 'rgba(68, 68, 68, 0.50)',
boxShadow: '0px 10px 30px rgba(0, 0, 0, 0.35)',
borderRadius: 1000,
outline: '2px rgba(251, 251, 251, 0.20) solid',
outlineOffset: '-2px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: '#EAEAEA',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Find the newest Nike sneakers.
</div>
</div>
<div
style={{
paddingLeft: 30,
paddingRight: 30,
paddingTop: 20,
paddingBottom: 20,
background: 'rgba(68, 68, 68, 0.50)',
boxShadow: '0px 10px 30px rgba(0, 0, 0, 0.35)',
borderRadius: 1000,
outline: '2px rgba(251, 251, 251, 0.20) solid',
outlineOffset: '-2px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: '#EAEAEA',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Show me snail cream that helps with sensitive skin.
</div>
</div>
<div
style={{
paddingLeft: 30,
paddingRight: 30,
paddingTop: 20,
paddingBottom: 20,
background: 'rgba(68, 68, 68, 0.50)',
boxShadow: '0px 10px 30px rgba(0, 0, 0, 0.35)',
borderRadius: 1000,
outline: '2px rgba(251, 251, 251, 0.20) solid',
outlineOffset: '-2px',
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'flex-start',
display: 'flex',
}}
>
<div
style={{
textAlign: 'center',
color: '#EAEAEA',
fontSize: 24,
fontFamily: 'LG Smart UI',
fontWeight: '700',
lineHeight: 24,
wordWrap: 'break-word',
}}
>
Recommend a tasty melatonin gummy.
</div>
</div>
</div>
</div>;

View File

@@ -185,5 +185,5 @@
margin-top: 100px;
padding-right: 120px;
z-index: 1002;
pointer-events: all; // 컨텐츠 영역은 클릭 가능
pointer-events: none; // 빈 공간 클릭 시 dimBackground로 이벤트 전달
}

View File

@@ -6,6 +6,7 @@
height: 437px;
position: relative;
border-radius: 12px;
pointer-events: all; // 실제 컨텐츠는 클릭 가능
}
.title {