[251024] fix: SearchInputOverlay Recent Searches
🕐 커밋 시간: 2025. 10. 24. 10:51:08 📊 변경 통계: • 총 파일: 7개 • 추가: +146줄 • 삭제: -52줄 📁 추가된 파일: + com.twin.app.shoptime/src/hooks/useSearchHistory.js + com.twin.app.shoptime/src/utils/searchHistory.js 📝 수정된 파일: ~ com.twin.app.shoptime/src/views/SearchPanel/SearchInputOverlay.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선 • 공통 유틸리티 함수 최적화 • 중간 규모 기능 개선 • 모듈 구조 개선
This commit is contained in:
134
com.twin.app.shoptime/src/hooks/useSearchHistory.js
Normal file
134
com.twin.app.shoptime/src/hooks/useSearchHistory.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { getSearch, getShopperHouseSearch } from '../actions/searchActions';
|
||||
import {
|
||||
readSearchHistory,
|
||||
writeSearchHistory,
|
||||
addSearchHistory,
|
||||
removeSearchHistory,
|
||||
clearAllSearchHistory,
|
||||
migrateVoiceSearchHistory,
|
||||
} from '../utils/searchHistory';
|
||||
|
||||
export const useSearchHistory = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [searchHistory, setSearchHistory] = useState(() => {
|
||||
// 초기 마이그레이션 실행
|
||||
const migrated = migrateVoiceSearchHistory();
|
||||
return migrated;
|
||||
});
|
||||
|
||||
// 일반 검색 기록 필터링
|
||||
const normalSearches = useMemo(() => {
|
||||
return searchHistory
|
||||
.filter(item => item.type === 'normal')
|
||||
.slice(0, 8); // SearchPanel에서 최대 8개 표시
|
||||
}, [searchHistory]);
|
||||
|
||||
// 음성 검색 기록 필터링
|
||||
const voiceSearches = useMemo(() => {
|
||||
return searchHistory
|
||||
.filter(item => item.type === 'voice')
|
||||
.slice(0, 6); // SearchInputOverlay에서 최대 6개 표시
|
||||
}, [searchHistory]);
|
||||
|
||||
// 최근 검색어 (전체, 최신순)
|
||||
const recentSearches = useMemo(() => {
|
||||
return searchHistory.slice(0, 10); // 전체 최신 10개
|
||||
}, [searchHistory]);
|
||||
|
||||
// 일반 검색 기록 추가
|
||||
const addNormalSearch = useCallback((query) => {
|
||||
if (!query || typeof query !== 'string' || !query.trim()) {
|
||||
console.warn('[useSearchHistory] Invalid query for normal search');
|
||||
return;
|
||||
}
|
||||
|
||||
const newHistory = addSearchHistory(query.trim(), 'normal');
|
||||
setSearchHistory(newHistory);
|
||||
}, []);
|
||||
|
||||
// 음성 검색 기록 추가
|
||||
const addVoiceSearch = useCallback((query, searchId = null) => {
|
||||
if (!query || typeof query !== 'string' || !query.trim()) {
|
||||
console.warn('[useSearchHistory] Invalid query for voice search');
|
||||
return;
|
||||
}
|
||||
|
||||
const newHistory = addSearchHistory(query.trim(), 'voice', searchId);
|
||||
setSearchHistory(newHistory);
|
||||
}, []);
|
||||
|
||||
// 검색 기록에서 실행
|
||||
const executeSearchFromHistory = useCallback((historyItem) => {
|
||||
if (!historyItem || !historyItem.query) {
|
||||
console.warn('[useSearchHistory] Invalid history item');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[useSearchHistory] Executing ${historyItem.type} search: "${historyItem.query}"`);
|
||||
|
||||
if (historyItem.type === 'normal') {
|
||||
// 일반 검색 API 호출
|
||||
dispatch(getSearch({
|
||||
service: 'com.lgshop.app',
|
||||
query: historyItem.query,
|
||||
domain: 'theme,show,item'
|
||||
}));
|
||||
} else if (historyItem.type === 'voice') {
|
||||
// 음성 검색 API 호출 (searchId 사용)
|
||||
dispatch(getShopperHouseSearch(historyItem.query, historyItem.searchId));
|
||||
} else {
|
||||
console.warn('[useSearchHistory] Unknown search type:', historyItem.type);
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
// 특정 검색 기록 삭제
|
||||
const removeHistoryItem = useCallback((timestamp) => {
|
||||
const newHistory = removeSearchHistory(timestamp);
|
||||
setSearchHistory(newHistory);
|
||||
}, []);
|
||||
|
||||
// 전체 검색 기록 삭제
|
||||
const clearHistory = useCallback(() => {
|
||||
const newHistory = clearAllSearchHistory();
|
||||
setSearchHistory(newHistory);
|
||||
}, []);
|
||||
|
||||
// 외부에서 searchHistory가 업데이트되었을 때 동기화
|
||||
const refreshHistory = useCallback(() => {
|
||||
const currentHistory = readSearchHistory();
|
||||
setSearchHistory(currentHistory);
|
||||
}, []);
|
||||
|
||||
// 컴포넌트 마운트 시 localStorage 동기화
|
||||
useEffect(() => {
|
||||
const handleStorageChange = (e) => {
|
||||
if (e.key === 'searchHistory') {
|
||||
refreshHistory();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleStorageChange);
|
||||
};
|
||||
}, [refreshHistory]);
|
||||
|
||||
return {
|
||||
// 데이터
|
||||
searchHistory,
|
||||
normalSearches,
|
||||
voiceSearches,
|
||||
recentSearches,
|
||||
|
||||
// 액션 함수
|
||||
addNormalSearch,
|
||||
addVoiceSearch,
|
||||
executeSearchFromHistory,
|
||||
removeHistoryItem,
|
||||
clearHistory,
|
||||
refreshHistory,
|
||||
};
|
||||
};
|
||||
142
com.twin.app.shoptime/src/utils/searchHistory.js
Normal file
142
com.twin.app.shoptime/src/utils/searchHistory.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { readLocalStorage, writeLocalStorage } from './helperMethods';
|
||||
|
||||
export const SEARCH_HISTORY_KEY = 'searchHistory';
|
||||
export const MAX_HISTORY_SIZE = 10;
|
||||
|
||||
export const readSearchHistory = () => {
|
||||
try {
|
||||
const history = readLocalStorage(SEARCH_HISTORY_KEY, []);
|
||||
// 데이터 형식 검증 및 마이그레이션
|
||||
return Array.isArray(history) ? history.filter(item =>
|
||||
item &&
|
||||
typeof item === 'object' &&
|
||||
typeof item.query === 'string' &&
|
||||
typeof item.type === 'string' &&
|
||||
(item.type === 'normal' || item.type === 'voice')
|
||||
) : [];
|
||||
} catch (error) {
|
||||
console.error('[SearchHistory] Failed to read search history:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const writeSearchHistory = (history) => {
|
||||
try {
|
||||
// 배열 형식 검증
|
||||
if (!Array.isArray(history)) {
|
||||
console.error('[SearchHistory] Invalid history format, expected array');
|
||||
return;
|
||||
}
|
||||
|
||||
// 최대 개수 제한 및 정렬 (최신순)
|
||||
const sortedHistory = history
|
||||
.filter(item => item && typeof item.query === 'string')
|
||||
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
|
||||
.slice(0, MAX_HISTORY_SIZE);
|
||||
|
||||
writeLocalStorage(SEARCH_HISTORY_KEY, sortedHistory);
|
||||
} catch (error) {
|
||||
console.error('[SearchHistory] Failed to write search history:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const addSearchHistory = (query, type, searchId = null) => {
|
||||
if (!query || typeof query !== 'string' || !query.trim()) {
|
||||
console.warn('[SearchHistory] Invalid query provided');
|
||||
return readSearchHistory();
|
||||
}
|
||||
|
||||
if (!type || (type !== 'normal' && type !== 'voice')) {
|
||||
console.warn('[SearchHistory] Invalid type provided, must be "normal" or "voice"');
|
||||
return readSearchHistory();
|
||||
}
|
||||
|
||||
try {
|
||||
const history = readSearchHistory();
|
||||
const trimmedQuery = query.trim();
|
||||
|
||||
// 동일한 검색어가 있으면 제거 (타입과 상관없이)
|
||||
const filteredHistory = history.filter(item => item.query !== trimmedQuery);
|
||||
|
||||
// 새로운 검색 기록 추가
|
||||
const newHistory = [
|
||||
{
|
||||
query: trimmedQuery,
|
||||
type,
|
||||
timestamp: Date.now(),
|
||||
searchId: type === 'voice' ? searchId : null
|
||||
},
|
||||
...filteredHistory
|
||||
].slice(0, MAX_HISTORY_SIZE);
|
||||
|
||||
writeSearchHistory(newHistory);
|
||||
console.log(`[SearchHistory] Added ${type} search: "${trimmedQuery}"`);
|
||||
return newHistory;
|
||||
} catch (error) {
|
||||
console.error('[SearchHistory] Failed to add search history:', error);
|
||||
return readSearchHistory();
|
||||
}
|
||||
};
|
||||
|
||||
export const removeSearchHistory = (timestamp) => {
|
||||
try {
|
||||
const history = readSearchHistory();
|
||||
const filteredHistory = history.filter(item => item.timestamp !== timestamp);
|
||||
writeSearchHistory(filteredHistory);
|
||||
console.log(`[SearchHistory] Removed search history item: ${timestamp}`);
|
||||
return filteredHistory;
|
||||
} catch (error) {
|
||||
console.error('[SearchHistory] Failed to remove search history:', error);
|
||||
return readSearchHistory();
|
||||
}
|
||||
};
|
||||
|
||||
export const clearAllSearchHistory = () => {
|
||||
try {
|
||||
writeSearchHistory([]);
|
||||
console.log('[SearchHistory] Cleared all search history');
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('[SearchHistory] Failed to clear search history:', error);
|
||||
return readSearchHistory();
|
||||
}
|
||||
};
|
||||
|
||||
// 기존 음성 검색 기록 마이그레이션 함수
|
||||
export const migrateVoiceSearchHistory = () => {
|
||||
try {
|
||||
const oldVoiceHistoryKey = 'voiceSearchHistory';
|
||||
const oldVoiceHistory = readLocalStorage(oldVoiceHistoryKey, []);
|
||||
|
||||
if (Array.isArray(oldVoiceHistory) && oldVoiceHistory.length > 0) {
|
||||
console.log('[SearchHistory] Migrating old voice search history...');
|
||||
|
||||
const currentHistory = readSearchHistory();
|
||||
const migratedItems = oldVoiceHistory
|
||||
.filter(query => typeof query === 'string' && query.trim())
|
||||
.map(query => ({
|
||||
query: query.trim(),
|
||||
type: 'voice',
|
||||
timestamp: Date.now() - Math.random() * 86400000, // 랜덤한 과거 시간
|
||||
searchId: null
|
||||
}));
|
||||
|
||||
const combinedHistory = [...migratedItems, ...currentHistory]
|
||||
.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
|
||||
.slice(0, MAX_HISTORY_SIZE);
|
||||
|
||||
writeSearchHistory(combinedHistory);
|
||||
|
||||
// 기존 음성 검색 기록 삭제 (선택적)
|
||||
// writeLocalStorage(oldVoiceHistoryKey, []);
|
||||
|
||||
console.log(`[SearchHistory] Migrated ${migratedItems.length} items from voice search history`);
|
||||
return combinedHistory;
|
||||
}
|
||||
|
||||
return readSearchHistory();
|
||||
} catch (error) {
|
||||
console.error('[SearchHistory] Failed to migrate voice search history:', error);
|
||||
return readSearchHistory();
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,7 @@ 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 { useSearchHistory } from '../../hooks/useSearchHistory';
|
||||
import css from './SearchInputOverlay.module.less';
|
||||
|
||||
// const OverlayContainer = SpotlightContainerDecorator(
|
||||
@@ -30,8 +31,10 @@ const SearchInputOverlay = ({
|
||||
handleClick,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const { addNormalSearch, normalSearches } = useSearchHistory();
|
||||
|
||||
const recentResultSearches = useMemo(
|
||||
// 더미 데이터 - localStorage에 저장된 일반 검색어가 없을 경우에만 표시
|
||||
const fallbackSearches = useMemo(
|
||||
() => [
|
||||
'Puppy food',
|
||||
'Dog toy',
|
||||
@@ -43,14 +46,29 @@ const SearchInputOverlay = ({
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
// localStorage에서 가져온 일반 검색어 (최대 5개) 또는 더미 데이터 (검색어 없을 때)
|
||||
const recentResultSearches = useMemo(() => {
|
||||
// normalSearches가 있으면 최대 5개만 표시
|
||||
if (normalSearches && normalSearches.length > 0) {
|
||||
return normalSearches.slice(0, 5);
|
||||
}
|
||||
// normalSearches가 없으면 더미 데이터 표시
|
||||
return fallbackSearches;
|
||||
}, [normalSearches, fallbackSearches]);
|
||||
|
||||
const handleSearchSubmit = useCallback(
|
||||
(query) => {
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
// 파라미터로 받은 query 값으로 직접 검증
|
||||
if (query && query.trim()) {
|
||||
// 일반 검색 기록 저장
|
||||
addNormalSearch(query.trim());
|
||||
|
||||
// 일반 검색 API 호출
|
||||
dispatch(
|
||||
getSearch({
|
||||
service: 'com.lgshop.app',
|
||||
query: query,
|
||||
query: query.trim(),
|
||||
domain: 'theme,show,item',
|
||||
})
|
||||
);
|
||||
@@ -61,10 +79,21 @@ const SearchInputOverlay = ({
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
[dispatch, searchQuery, onClose]
|
||||
[dispatch, addNormalSearch, onClose]
|
||||
);
|
||||
|
||||
|
||||
const handleKeydown = useCallback(
|
||||
(e) => {
|
||||
// Enter 키 또는 OK 키 처리
|
||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
handleSearchSubmit(searchQuery);
|
||||
}
|
||||
},
|
||||
[searchQuery, handleSearchSubmit]
|
||||
);
|
||||
|
||||
const handleDimClick = useCallback(
|
||||
(e) => {
|
||||
onClose();
|
||||
@@ -105,6 +134,7 @@ const SearchInputOverlay = ({
|
||||
icon={ICONS.search}
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
onKeyDown={handleKeydown}
|
||||
onIconClick={() => {
|
||||
handleSearchSubmit(searchQuery);
|
||||
}}
|
||||
@@ -114,18 +144,26 @@ const SearchInputOverlay = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 이곳에 작업 */}
|
||||
{/* 최근 검색어 섹션 - localStorage 기반 일반 검색어만 표시 (최대 5개) */}
|
||||
<div className={css.overLayRecent}>
|
||||
{recentResultSearches.map((keyword, index) => (
|
||||
<SpottableKeyword
|
||||
key={`recentResult-${index}`}
|
||||
className={css.keywordButton}
|
||||
onClick={() => handleClick(keyword)}
|
||||
spotlightId={`recent-Resultkeyword-${index}`}
|
||||
>
|
||||
{keyword}
|
||||
</SpottableKeyword>
|
||||
))}
|
||||
{recentResultSearches.map((item, index) => {
|
||||
// normalSearches의 경우 object, fallbackSearches의 경우 string
|
||||
const keyword = typeof item === 'object' ? item.query : item;
|
||||
|
||||
return (
|
||||
<SpottableKeyword
|
||||
key={`recentResult-${index}`}
|
||||
className={css.keywordButton}
|
||||
onClick={() => {
|
||||
// 검색어 설정 후 검색 실행
|
||||
handleClick(keyword);
|
||||
}}
|
||||
spotlightId={`recent-Resultkeyword-${index}`}
|
||||
>
|
||||
{keyword}
|
||||
</SpottableKeyword>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,7 +62,7 @@ import {
|
||||
} from '../../utils/Config';
|
||||
import NoSearchResults from './NoSearchResults/NoSearchResults';
|
||||
// import NoSearchResults from './NoSearchResults/NoSearchResults';
|
||||
import SearchInputOverlay from './SearchInpuOverlay';
|
||||
import SearchInputOverlay from './SearchInputOverlay';
|
||||
import css from './SearchPanel.new.module.less';
|
||||
import SearchResultsNew from './SearchResults.new';
|
||||
import TInput, {
|
||||
|
||||
@@ -35,10 +35,11 @@ import TVerticalPagenator from '../../components/TVerticalPagenator/TVerticalPag
|
||||
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
|
||||
// import VirtualKeyboardContainer from "../../components/TToast/VirtualKeyboardContainer";
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import { useSearchHistory } from '../../hooks/useSearchHistory';
|
||||
import { LOG_CONTEXT_NAME, LOG_MENU, LOG_MESSAGE_ID, panel_names } from '../../utils/Config';
|
||||
import NoSearchResults from './NoSearchResults/NoSearchResults';
|
||||
// import NoSearchResults from './NoSearchResults/NoSearchResults';
|
||||
import SearchInputOverlay from './SearchInpuOverlay';
|
||||
import SearchInputOverlay from './SearchInputOverlay';
|
||||
import css from './SearchPanel.new.module.less';
|
||||
import SearchResultsNew from './SearchResults.new.v2';
|
||||
import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple';
|
||||
@@ -252,10 +253,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
const SafeImage = useCallback(SafeImageComponent, []);
|
||||
|
||||
/**
|
||||
* memoized variables
|
||||
* useSearchHistory Hook 적용
|
||||
*/
|
||||
// 가짜 데이터 - 실제로는 Redux store나 API에서 가져와야 함
|
||||
const recentSearches = useMemo(() => ['Puppy food', 'Dog toy', 'Fitness'], []);
|
||||
const {
|
||||
normalSearches,
|
||||
addNormalSearch,
|
||||
executeSearchFromHistory,
|
||||
} = useSearchHistory();
|
||||
|
||||
// Voice overlay suggestions (동적으로 변경 가능)
|
||||
const voiceSuggestions = useMemo(
|
||||
@@ -373,6 +377,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}
|
||||
|
||||
if (query.trim()) {
|
||||
// 일반 검색 기록 저장
|
||||
addNormalSearch(query.trim());
|
||||
|
||||
dispatch(
|
||||
getSearch({
|
||||
domain: 'theme,show,item',
|
||||
@@ -390,7 +397,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
setSearchQuery(query);
|
||||
},
|
||||
|
||||
[searchPerformed, dispatch] // ✨ [Phase 3] dispatch 추가, showVirtualKeyboard 제거
|
||||
[searchPerformed, dispatch, addNormalSearch] // ✨ [Phase 3] dispatch 추가, showVirtualKeyboard 제거, addNormalSearch 추가
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -558,6 +565,28 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
[handleSearchSubmit]
|
||||
);
|
||||
|
||||
/**
|
||||
* 검색 기록에서 키워드 클릭 핸들러
|
||||
*/
|
||||
const handleHistoryKeywordClick = useCallback(
|
||||
(historyItem) => {
|
||||
if (!historyItem || !historyItem.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 검색어 설정
|
||||
setSearchQuery(historyItem.query);
|
||||
|
||||
// 검색 타입에 따라 다른 API 호출
|
||||
executeSearchFromHistory(historyItem);
|
||||
|
||||
// Overlay 닫기 및 포커스 해제
|
||||
setIsSearchOverlayVisible(false);
|
||||
setInputFocus(false);
|
||||
},
|
||||
[executeSearchFromHistory]
|
||||
);
|
||||
|
||||
/**
|
||||
* 키워드 클릭 핸들러 생성 함수
|
||||
*/
|
||||
@@ -568,6 +597,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
[handleKeywordClick]
|
||||
);
|
||||
|
||||
/**
|
||||
* 검색 기록 키워드 클릭 핸들러 생성 함수
|
||||
*/
|
||||
const createHistoryKeywordClickHandler = useCallback(
|
||||
(historyItem) => {
|
||||
return () => handleHistoryKeywordClick(historyItem);
|
||||
},
|
||||
[handleHistoryKeywordClick]
|
||||
);
|
||||
|
||||
/**
|
||||
* Search overlay close handler
|
||||
*/
|
||||
@@ -727,8 +766,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
default:
|
||||
return (
|
||||
<ContainerBasic className={css.contentContainer}>
|
||||
{/* 최근 검색어 섹션 */}
|
||||
{recentSearches && recentSearches.length > 0 && (
|
||||
{/* 최근 검색어 섹션 (일반검색만 표시) */}
|
||||
{normalSearches && normalSearches.length > 0 && (
|
||||
<>
|
||||
<SectionContainer
|
||||
className={css.section}
|
||||
@@ -740,14 +779,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
<div className={css.sectionTitle}>Your Recent Searches</div>
|
||||
</div>
|
||||
<div className={css.keywordList}>
|
||||
{recentSearches.map((keyword, index) => (
|
||||
{normalSearches.map((historyItem, index) => (
|
||||
<SpottableKeyword
|
||||
key={`recent-${index}`}
|
||||
key={`recent-${historyItem.timestamp}`}
|
||||
className={css.keywordButton}
|
||||
onClick={createKeywordClickHandler(keyword)}
|
||||
onClick={createHistoryKeywordClickHandler(historyItem)}
|
||||
spotlightId={`recent-keyword-${index}`}
|
||||
>
|
||||
{keyword}
|
||||
{historyItem.query}
|
||||
</SpottableKeyword>
|
||||
))}
|
||||
</div>
|
||||
@@ -847,12 +886,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
searchDatas?.item?.length,
|
||||
searchDatas?.show?.length,
|
||||
shopperHouseData?.results?.[0]?.docs?.length,
|
||||
recentSearches?.length,
|
||||
normalSearches?.length,
|
||||
topSearchs?.length,
|
||||
popularBrands?.length,
|
||||
hotPicksForYou?.length,
|
||||
handleKeywordClick,
|
||||
createKeywordClickHandler,
|
||||
createHistoryKeywordClickHandler,
|
||||
renderItem,
|
||||
panelInfo?.currentSpot,
|
||||
]);
|
||||
|
||||
@@ -16,6 +16,7 @@ import TFullPopup from '../../../components/TFullPopup/TFullPopup';
|
||||
import TInput, { ICONS, KINDS } from '../TInput/TInput';
|
||||
import { useWebSpeech } from '../../../hooks/useWebSpeech';
|
||||
import { readLocalStorage, writeLocalStorage } from '../../../utils/helperMethods';
|
||||
import { useSearchHistory } from '../../../hooks/useSearchHistory';
|
||||
import VoiceListening from './modes/VoiceListening';
|
||||
import VoiceNotRecognized from './modes/VoiceNotRecognized';
|
||||
import VoiceNotRecognizedCircle from './modes/VoiceNotRecognizedCircle';
|
||||
@@ -176,11 +177,14 @@ const VoiceInputOverlay = ({
|
||||
const [isBubbleClickSearch, setIsBubbleClickSearch] = useState(false);
|
||||
// 디버그 대시보드 표시 여부
|
||||
const [showDashboard, setShowDashboard] = useState(false);
|
||||
// 검색 기록 관리 (localStorage 기반, 최근 5개)
|
||||
const [searchHistory, setSearchHistory] = useState(() => {
|
||||
// useSearchHistory Hook 적용 (음성검색 기록 관리)
|
||||
const { addVoiceSearch, recentSearches } = useSearchHistory();
|
||||
|
||||
// 기존 검색 기록 (마이그레이션용)
|
||||
const [legacySearchHistory, setLegacySearchHistory] = useState(() => {
|
||||
const history = readLocalStorage(SEARCH_HISTORY_KEY, DEFAULT_SUGGESTIONS);
|
||||
if (DEBUG_MODE) {
|
||||
console.log('📚 [DEBUG] Loaded searchHistory from localStorage:', history);
|
||||
console.log('📚 [DEBUG] Loaded legacy searchHistory from localStorage:', history);
|
||||
}
|
||||
return history;
|
||||
});
|
||||
@@ -263,13 +267,17 @@ const VoiceInputOverlay = ({
|
||||
}
|
||||
}, [webSpeechEventLogs]);
|
||||
|
||||
// 🔍 검색 기록 저장 함수 (성능 최적화: stable reference)
|
||||
const addToSearchHistory = useCallback((searchText) => {
|
||||
// 🔍 음성 검색 기록 저장 함수 (새로운 searchHistory 시스템 사용)
|
||||
const addToSearchHistory = useCallback((searchText, searchId = null) => {
|
||||
if (!searchText || searchText.trim().length < 3) return;
|
||||
|
||||
const trimmedText = searchText.trim();
|
||||
|
||||
setSearchHistory((prevHistory) => {
|
||||
// 새로운 searchHistory 시스템에 음성검색 기록 저장
|
||||
addVoiceSearch(trimmedText, searchId);
|
||||
|
||||
// 기존 시스템에도 저장 (호환성 유지)
|
||||
setLegacySearchHistory((prevHistory) => {
|
||||
// 중복 제거 (대소문자 구분 없이)
|
||||
const filtered = prevHistory.filter(
|
||||
(item) => item.toLowerCase() !== trimmedText.toLowerCase()
|
||||
@@ -278,16 +286,20 @@ const VoiceInputOverlay = ({
|
||||
// 최신 항목을 맨 앞에 추가
|
||||
const newHistory = [trimmedText, ...filtered].slice(0, MAX_HISTORY_SIZE);
|
||||
|
||||
// localStorage에 저장
|
||||
// localStorage에 저장 (기존 키)
|
||||
writeLocalStorage(SEARCH_HISTORY_KEY, newHistory);
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
console.log('💾 [VoiceInputOverlay.v2] Search history updated:', newHistory);
|
||||
console.log('💾 [VoiceInputOverlay.v2] Legacy search history updated:', newHistory);
|
||||
}
|
||||
|
||||
return newHistory;
|
||||
});
|
||||
}, []); // dependency 없음 (setSearchHistory는 stable)
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
console.log('🎤 [VoiceInputOverlay] Voice search added to history:', trimmedText, 'searchId:', searchId);
|
||||
}
|
||||
}, [addVoiceSearch]); // addVoiceSearch dependency 추가
|
||||
|
||||
// WebSpeech config 메모이제이션 (불필요한 재초기화 방지)
|
||||
const webSpeechConfig = useMemo(
|
||||
@@ -745,13 +757,12 @@ const VoiceInputOverlay = ({
|
||||
setCurrentMode(VOICE_MODES.RESPONSE);
|
||||
setVoiceInputMode(null);
|
||||
|
||||
// ✨ 검색 기록에 추가
|
||||
addToSearchHistory(finalText);
|
||||
// ✨ 검색 기록에 추가 (searchId 포함)
|
||||
const currentSearchId = shopperHouseSearchIdRef.current;
|
||||
addToSearchHistory(finalText, currentSearchId);
|
||||
|
||||
// ✨ ShopperHouse API 자동 호출 (2차 발화 시 searchId 포함)
|
||||
const query = finalText.trim();
|
||||
// ✅ Ref에서 최신 searchId 읽기 (useCallback closure 문제 해결)
|
||||
const currentSearchId = shopperHouseSearchIdRef.current;
|
||||
console.log('[VoiceInput] 📤 API 요청 전송');
|
||||
console.log('[VoiceInput] ├─ query:', query);
|
||||
console.log('[VoiceInput] ├─ ref 값:', shopperHouseSearchIdRef.current);
|
||||
@@ -1098,8 +1109,8 @@ const VoiceInputOverlay = ({
|
||||
onSearchChange({ value: query });
|
||||
}
|
||||
|
||||
// ✨ 검색 기록에 추가
|
||||
addToSearchHistory(query);
|
||||
// ✨ 검색 기록에 추가 (searchId 포함)
|
||||
addToSearchHistory(query, shopperHouseSearchIdRef.current);
|
||||
|
||||
// ✨ RESPONSE 모드로 전환을 위한 텍스트 설정
|
||||
setSttResponseText(query);
|
||||
@@ -1112,6 +1123,7 @@ const VoiceInputOverlay = ({
|
||||
if (query && query.length >= 3) {
|
||||
// ✅ Ref에서 최신 searchId 읽기 (이전 검색이 있는 경우 2차 발화 처리)
|
||||
const currentSearchId = shopperHouseSearchIdRef.current;
|
||||
|
||||
if (DEBUG_MODE) {
|
||||
console.log('🔍 [DEBUG] Calling ShopperHouse API from bubble click');
|
||||
console.log('[VoiceInput] ├─ query:', query);
|
||||
@@ -1225,8 +1237,8 @@ const VoiceInputOverlay = ({
|
||||
// ✨ relativeQueries가 있으면 'How about these ?' 표시, 없으면 'Try saying' 표시
|
||||
const hasRelativeQueries = !!shopperHouseRelativeQueries;
|
||||
const promptTitle = hasRelativeQueries ? 'How about these ?' : 'Try saying';
|
||||
// relativeQueries가 있으면 사용, 없으면 searchHistory 사용
|
||||
const promptSuggestions = hasRelativeQueries ? shopperHouseRelativeQueries : searchHistory;
|
||||
// relativeQueries가 있으면 사용, 없으면 legacySearchHistory 사용
|
||||
const promptSuggestions = hasRelativeQueries ? shopperHouseRelativeQueries : legacySearchHistory;
|
||||
|
||||
// ✨ [DEBUG] Redux 상태 확인 로그
|
||||
console.log('[VoiceInput]-shopperHouseRelativeQueries');
|
||||
@@ -1241,7 +1253,7 @@ const VoiceInputOverlay = ({
|
||||
promptTitle: promptTitle,
|
||||
promptSuggestions: promptSuggestions,
|
||||
promptSuggestions_length: promptSuggestions?.length || 0,
|
||||
searchHistory_length: searchHistory?.length || 0,
|
||||
searchHistory_length: legacySearchHistory?.length || 0,
|
||||
shopperHouseSearchId: shopperHouseSearchId || '(없음)',
|
||||
shopperHouseData_exists: !!shopperHouseData,
|
||||
},
|
||||
@@ -1253,7 +1265,7 @@ const VoiceInputOverlay = ({
|
||||
if (DEBUG_MODE) {
|
||||
console.log(
|
||||
'✅ [DEBUG][VoiceInputOverlay] MODE = PROMPT | Rendering VoicePromptScreen',
|
||||
hasRelativeQueries ? '(with relativeQueries)' : '(with searchHistory)',
|
||||
hasRelativeQueries ? '(with relativeQueries)' : '(with legacySearchHistory)',
|
||||
promptSuggestions.length,
|
||||
'suggestions'
|
||||
);
|
||||
@@ -1266,7 +1278,7 @@ const VoiceInputOverlay = ({
|
||||
console.log('[DEBUG][VoiceInputOverlay] └─ promptTitle:', promptTitle);
|
||||
} else {
|
||||
console.log('[DEBUG][VoiceInputOverlay] ├─ relativeQueries가 없음');
|
||||
console.log('[DEBUG][VoiceInputOverlay] └─ searchHistory 사용:', promptSuggestions);
|
||||
console.log('[DEBUG][VoiceInputOverlay] └─ legacySearchHistory 사용:', promptSuggestions);
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -1339,22 +1351,26 @@ const VoiceInputOverlay = ({
|
||||
case VOICE_MODES.MODE_4:
|
||||
// 추후 MODE_4 컴포넌트 추가
|
||||
return <VoiceNotRecognizedCircle />;
|
||||
default:
|
||||
default: {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('🔄 [DEBUG][VoiceInputOverlay] MODE = DEFAULT | Rendering VoicePromptScreen');
|
||||
}
|
||||
// default 케이스에서도 promptSuggestions 계산
|
||||
const hasRelativeQueries = !!shopperHouseRelativeQueries;
|
||||
const defaultPromptSuggestions = hasRelativeQueries ? shopperHouseRelativeQueries : legacySearchHistory;
|
||||
return (
|
||||
<VoicePromptScreen
|
||||
suggestions={searchHistory}
|
||||
suggestions={defaultPromptSuggestions}
|
||||
onSuggestionClick={handleSuggestionClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentMode,
|
||||
voiceInputMode,
|
||||
isListening,
|
||||
searchHistory,
|
||||
legacySearchHistory,
|
||||
handleSuggestionClick,
|
||||
interimText,
|
||||
sttResponseText,
|
||||
|
||||
Reference in New Issue
Block a user