[251024] [251025] feat: SearchPanel.new.v2 HowAboutThese 검색 대기화면

🕐 커밋 시간: 2025. 10. 24. 20:45:45

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +67줄
  • 삭제: -1줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript):
     Added: clearAllTimers()
This commit is contained in:
2025-10-24 20:45:45 +00:00
parent 8fe080f343
commit 587acbd2ee
3 changed files with 79 additions and 45 deletions

View File

@@ -20,6 +20,7 @@ import {
resetSearch,
resetVoiceSearch,
clearShopperHouseData,
getShopperHouseSearch,
} from '../../actions/searchActions';
// import {
// showErrorToast,
@@ -179,6 +180,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// ✨ [Phase 1] SearchPanel의 현재 모드 상태 (VoiceInputOverlay의 VOICE_MODES와 동일한 개념)
const [currentMode, setCurrentMode] = useState(SEARCH_PANEL_MODES.INITIAL);
const [voiceOverlayMode, setVoiceOverlayMode] = useState(VOICE_MODES.PROMPT);
const [voiceOverlayResponseText, setVoiceOverlayResponseText] = useState('');
const [isVoiceOverlayBubbleSearch, setIsVoiceOverlayBubbleSearch] = useState(false);
// 🐛 [DEBUG] shopperHouseData 상태 변경 추적 (DEBUG_MODE가 true일 경우에만)
useEffect(() => {
@@ -438,6 +442,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
isVoiceOverlayVisible
);
}
setVoiceOverlayMode(VOICE_MODES.PROMPT);
setVoiceOverlayResponseText('');
setIsVoiceOverlayBubbleSearch(false);
setIsVoiceOverlayVisible(true);
// setIsVoiceOverlayVisible((prev) => !prev);
}, [isVoiceOverlayVisible]);
@@ -646,6 +653,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
// VoiceInputOverlay가 닫히는 순간 clearShopperHouseData()를 호출하면,
// SearchPanel에서 shopperHouseData를 표시할 수 없으므로 여기서는 정리하지 않음
setVoiceOverlayMode(VOICE_MODES.PROMPT);
setVoiceOverlayResponseText('');
setIsVoiceOverlayBubbleSearch(false);
setIsVoiceOverlayVisible(false);
// ✅ VoiceOverlay가 닫힐 때 항상 TInput으로 포커스 이동
setTimeout(() => {
@@ -653,6 +663,27 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, 150); // Overlay 닫히는 시간을 고려한 지연
}, []);
const handleHowAboutTheseQueryClick = useCallback(
(rawQuery) => {
if (!rawQuery) return;
const trimmedQuery = typeof rawQuery === 'string' ? rawQuery.trim() : '';
if (!trimmedQuery) return;
// 검색 입력 필드에도 동일한 쿼리를 표시
setSearchQuery(trimmedQuery);
// Voice overlay를 RESPONSE 모드로 전환하여 대기 화면 표시
setVoiceOverlayResponseText(trimmedQuery);
setVoiceOverlayMode(VOICE_MODES.RESPONSE);
setIsVoiceOverlayBubbleSearch(true);
setIsVoiceOverlayVisible(true);
dispatch(getShopperHouseSearch(trimmedQuery, shopperHouseSearchId));
},
[dispatch, shopperHouseSearchId]
);
// ✨ [Phase 3] handleInputIconClick 제거 (돋보기 아이콘 기능 비활성화)
// /**
// * ✨ [Phase 3] TInput icon click handler (showVirtualKeyboard 제거)
@@ -770,6 +801,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
shopperHouseRelativeQueries={shopperHouseRelativeQueries}
keywordClick={handleKeywordClick}
panelInfo={panelInfo}
onRelativeQueryClick={handleHowAboutTheseQueryClick}
/>
);
@@ -1331,12 +1363,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
<VoiceInputOverlay
isVisible={isVoiceOverlayVisible}
onClose={handleVoiceOverlayClose}
mode={VOICE_MODES.PROMPT}
mode={voiceOverlayMode}
suggestions={voiceSuggestions}
searchQuery={searchQuery}
onSearchChange={handleSearchChange}
onSearchSubmit={handleSearchSubmit}
isVoiceResultMode={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT}
externalResponseText={voiceOverlayResponseText}
isExternalBubbleSearch={isVoiceOverlayBubbleSearch}
/>
{/* ✨ [Phase 1] Search Input Overlay - currentMode로 visibility 제어 */}

View File

@@ -1,10 +1,4 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
@@ -16,8 +10,7 @@ import upBtnImg from '../../../assets/images/btn/search_btn_up_arrow.png';
import CustomImage from '../../components/CustomImage/CustomImage';
import TButtonTab, { LIST_TYPE } from '../../components/TButtonTab/TButtonTab';
import TDropDown from '../../components/TDropDown/TDropDown';
import TVirtualGridList
from '../../components/TVirtualGridList/TVirtualGridList';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
import { $L } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds';
import HowAboutThese from './HowAboutThese/HowAboutThese';
@@ -69,9 +62,7 @@ const SafeImage = ({ src, alt, className, ...props }) => {
};
}, []);
return (
<img ref={imgRef} src={src} alt={alt} className={className} {...props} />
);
return <img ref={imgRef} src={src} alt={alt} className={className} {...props} />;
};
const SearchResultsNew = ({
@@ -82,14 +73,11 @@ const SearchResultsNew = ({
shopperHouseInfo,
shopperHouseSearchId = null,
shopperHouseRelativeQueries = [],
onRelativeQueryClick,
}) => {
// ShopperHouse 데이터를 ItemCard 형식으로 변환
const convertedShopperHouseItems = useMemo(() => {
if (
!shopperHouseInfo ||
!shopperHouseInfo.results ||
shopperHouseInfo.results.length === 0
) {
if (!shopperHouseInfo || !shopperHouseInfo.results || shopperHouseInfo.results.length === 0) {
return null;
}
@@ -125,8 +113,7 @@ const SearchResultsNew = ({
const getButtonTabList = () => {
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용
const itemLength =
convertedShopperHouseItems?.length || itemInfo?.length || 0;
const itemLength = convertedShopperHouseItems?.length || itemInfo?.length || 0;
const showLength = showInfo?.length || 0;
return [
@@ -148,9 +135,7 @@ const SearchResultsNew = ({
const cbChangePageRef = useRef(null);
// HowAboutThese 모드 상태 관리
const [howAboutTheseMode, setHowAboutTheseMode] = useState(
HOW_ABOUT_THESE_MODES.SMALL
);
const [howAboutTheseMode, setHowAboutTheseMode] = useState(HOW_ABOUT_THESE_MODES.SMALL);
// HowAboutThese 모드 전환 핸들러
const handleShowFullHowAboutThese = useCallback(() => {
@@ -166,8 +151,7 @@ const SearchResultsNew = ({
const buttonTabList = useMemo(() => getButtonTabList(), [getButtonTabList]);
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선
const currentData =
tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
const currentData = tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
// 표시할 데이터 (처음부터 visibleCount 개수만큼)
const displayedData = useMemo(() => {
@@ -255,8 +239,7 @@ const SearchResultsNew = ({
return null;
}
const { bgImgPath, title, partnerLogo, partnerName, keyword } =
themeInfo[index];
const { bgImgPath, title, partnerLogo, partnerName, keyword } = themeInfo[index];
return (
<SpottableDiv
key={`searchProduct-${index}`}
@@ -265,19 +248,11 @@ const SearchResultsNew = ({
{...rest}
>
<div className={css.productImageWrapper}>
<SafeImage
src={bgImgPath}
alt={title}
className={css.productImage}
/>
<SafeImage src={bgImgPath} alt={title} className={css.productImage} />
</div>
<div className={css.productInfo}>
<div className={css.productBrandWrapper}>
<SafeImage
src={partnerLogo}
alt={partnerName}
className={css.brandLogo}
/>
<SafeImage src={partnerLogo} alt={partnerName} className={css.brandLogo} />
</div>
<div className={css.productDetails}>
{keyword && (
@@ -336,6 +311,7 @@ const SearchResultsNew = ({
<HowAboutTheseSmall
relativeQueries={relativeQueries}
searchId={shopperHouseSearchId}
onQueryClick={onRelativeQueryClick}
onSeeMoreClick={handleShowFullHowAboutThese}
/>
</SpottableDiv>
@@ -347,6 +323,12 @@ const SearchResultsNew = ({
<HowAboutThese
relativeQueries={relativeQueries}
searchId={shopperHouseSearchId}
onQueryClick={(query) => {
if (onRelativeQueryClick) {
onRelativeQueryClick(query);
}
handleCloseHowAboutThese();
}}
onClose={handleCloseHowAboutThese}
/>
</div>
@@ -355,9 +337,7 @@ const SearchResultsNew = ({
<div className={css.hotpicksSection} data-wheel-point="true">
<div className={css.sectionHeader}>
<div className={css.sectionIndicator} />
<div className={css.sectionTitle}>
Hot Picks ({themeInfo?.length})
</div>
<div className={css.sectionTitle}>Hot Picks ({themeInfo?.length})</div>
</div>
<div className={css.productList}>
<TVirtualGridList
@@ -403,11 +383,7 @@ const SearchResultsNew = ({
<div className={css.buttonContainer}>
{hasMore && (
<SpottableDiv onClick={downBtnClick} className={css.downBtn}>
<CustomImage
className={css.btnImg}
src={downBtnImg}
alt="Down arrow"
/>
<CustomImage className={css.btnImg} src={downBtnImg} alt="Down arrow" />
</SpottableDiv>
)}
<SpottableDiv onClick={upBtnClick} className={css.upBtn}>

View File

@@ -121,7 +121,10 @@ const VoiceInputOverlay = ({
mode = VOICE_MODES.PROMPT,
searchQuery = '',
onSearchChange,
onSearchSubmit,
isVoiceResultMode = false,
externalResponseText = '',
isExternalBubbleSearch = false,
}) => {
if (DEBUG_MODE) {
console.log('🔄 [DEBUG] VoiceInputOverlay render - isVisible:', isVisible, 'mode:', mode);
@@ -141,6 +144,7 @@ const VoiceInputOverlay = ({
const countdownIntervalRef = useRef(null);
const sttDebounceTimerRef = useRef(null); // STT 결과 debounce 타이머
const silenceDetectionTimerRef = useRef(null); // 3초 silence detection 타이머
const externalResponseTextRef = useRef('');
// All timer refs array for batch cleanup
const allTimerRefs = [
@@ -1094,6 +1098,7 @@ const VoiceInputOverlay = ({
setSttResponseText('');
setErrorMessage('');
setIsBubbleClickSearch(false); // bubble 클릭 상태 초기화
externalResponseTextRef.current = '';
// 3. Redux 정리 (VoiceInputOverlay의 책임)
// dispatch(clearShopperHouseData());
@@ -1103,6 +1108,21 @@ const VoiceInputOverlay = ({
onClose();
}, [onClose, dispatch]);
useEffect(() => {
if (!isVisible || !isExternalBubbleSearch) {
return;
}
const nextText = typeof externalResponseText === 'string' ? externalResponseText : '';
if (externalResponseTextRef.current !== nextText) {
setSttResponseText(nextText);
externalResponseTextRef.current = nextText;
}
setIsBubbleClickSearch(true);
}, [externalResponseText, isExternalBubbleSearch, isVisible]);
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트 설정 + API 자동 호출 + response 모드 전환
const handleSuggestionClick = useCallback(
(suggestion) => {
@@ -1843,6 +1863,8 @@ VoiceInputOverlay.propTypes = {
onSearchChange: PropTypes.func,
onSearchSubmit: PropTypes.func,
isVoiceResultMode: PropTypes.bool,
externalResponseText: PropTypes.string,
isExternalBubbleSearch: PropTypes.bool,
};
VoiceInputOverlay.defaultProps = {
@@ -1851,6 +1873,8 @@ VoiceInputOverlay.defaultProps = {
onSearchChange: null,
onSearchSubmit: null,
isVoiceResultMode: false,
externalResponseText: '',
isExternalBubbleSearch: false,
};
export default VoiceInputOverlay;