[251024] fix: SearchPanel.new.v2 Optimization-1
🕐 커밋 시간: 2025. 10. 24. 09:51:08 📊 변경 통계: • 총 파일: 4개 • 추가: +357줄 • 삭제: -145줄 📝 수정된 파일: ~ com.twin.app.shoptime/.gitignore ~ 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/TInput/TInputSimple.jsx 🔧 주요 변경 내용: • 대규모 기능 개발
This commit is contained in:
1
com.twin.app.shoptime/.gitignore
vendored
1
com.twin.app.shoptime/.gitignore
vendored
@@ -16,3 +16,4 @@ npm-debug.log
|
|||||||
srcBackup
|
srcBackup
|
||||||
# com.lgshop.app_*.ipk
|
# com.lgshop.app_*.ipk
|
||||||
.docs
|
.docs
|
||||||
|
nul
|
||||||
@@ -40,7 +40,7 @@ import NoSearchResults from './NoSearchResults/NoSearchResults';
|
|||||||
// import NoSearchResults from './NoSearchResults/NoSearchResults';
|
// import NoSearchResults from './NoSearchResults/NoSearchResults';
|
||||||
import SearchInputOverlay from './SearchInpuOverlay';
|
import SearchInputOverlay from './SearchInpuOverlay';
|
||||||
import css from './SearchPanel.new.module.less';
|
import css from './SearchPanel.new.module.less';
|
||||||
import SearchResultsNew from './SearchResults.new';
|
import SearchResultsNew from './SearchResults.new.v2';
|
||||||
import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple';
|
import TInputSimple, { ICONS, KINDS } from './TInput/TInputSimple';
|
||||||
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';
|
import VoiceInputOverlay, { VOICE_MODES } from './VoiceInputOverlay/VoiceInputOverlay';
|
||||||
|
|
||||||
@@ -72,10 +72,51 @@ const InputContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' },
|
|||||||
// 콘텐츠 섹션 컨테이너
|
// 콘텐츠 섹션 컨테이너
|
||||||
const SectionContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
const SectionContainer = SpotlightContainerDecorator({ enterTo: 'last-focused' }, 'div');
|
||||||
|
|
||||||
// Spottable 컴포넌트들
|
// 메모리 누수 방지를 위한 안전한 이미지 컴포넌트 (컴포넌트 외부로 이동)
|
||||||
const SpottableMicButton = Spottable('div');
|
const SafeImageComponent = ({ src, alt, className, ...props }) => {
|
||||||
const SpottableKeyword = Spottable('div');
|
const imgRef = useRef(null);
|
||||||
const SpottableProduct = Spottable('div');
|
|
||||||
|
useEffect(() => {
|
||||||
|
const img = imgRef.current;
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
// 이미지 로드 완료 핸들러
|
||||||
|
const handleLoad = () => {
|
||||||
|
// 로드 성공 시 특별한 처리 불필요
|
||||||
|
};
|
||||||
|
|
||||||
|
// 이미지 로드 에러 핸들러
|
||||||
|
const handleError = () => {
|
||||||
|
// 에러 시 src를 제거하여 깨진 이미지 방지
|
||||||
|
if (img.src) {
|
||||||
|
img.src = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
img.addEventListener('load', handleLoad);
|
||||||
|
img.addEventListener('error', handleError);
|
||||||
|
|
||||||
|
// 컴포넌트 unmount 시 이벤트 리스너 정리
|
||||||
|
return () => {
|
||||||
|
img.removeEventListener('load', handleLoad);
|
||||||
|
img.removeEventListener('error', handleError);
|
||||||
|
// 이미지 로딩 취소
|
||||||
|
if (img.src) {
|
||||||
|
img.src = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
ref={imgRef}
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ITEMS_PER_PAGE = 9;
|
const ITEMS_PER_PAGE = 9;
|
||||||
|
|
||||||
@@ -94,6 +135,9 @@ const SPOTLIGHT_IDS = {
|
|||||||
export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
// DEBUG 모드 상태 - 개발 환경에서만 활성화
|
||||||
|
const DEBUG_MODE = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* stores
|
* stores
|
||||||
*/
|
*/
|
||||||
@@ -137,37 +181,43 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
// ✨ [Phase 1] SearchPanel의 현재 모드 상태 (VoiceInputOverlay의 VOICE_MODES와 동일한 개념)
|
// ✨ [Phase 1] SearchPanel의 현재 모드 상태 (VoiceInputOverlay의 VOICE_MODES와 동일한 개념)
|
||||||
const [currentMode, setCurrentMode] = useState(SEARCH_PANEL_MODES.INITIAL);
|
const [currentMode, setCurrentMode] = useState(SEARCH_PANEL_MODES.INITIAL);
|
||||||
|
|
||||||
// 🐛 [DEBUG] shopperHouseData 상태 변경 추적
|
// 🐛 [DEBUG] shopperHouseData 상태 변경 추적 (DEBUG_MODE가 true일 경우에만)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[DEBUG] 📊 SearchPanel shopperHouseData 상태 변경:', {
|
if (DEBUG_MODE) {
|
||||||
hasData: !!shopperHouseData,
|
console.log('[DEBUG] 📊 SearchPanel shopperHouseData 상태 변경:', {
|
||||||
dataLength: shopperHouseData?.results?.[0]?.docs?.length || 0,
|
|
||||||
searchId: shopperHouseData?.results?.[0]?.searchId || '(없음)',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
}, [shopperHouseData]);
|
|
||||||
|
|
||||||
// 🐛 [DEBUG] SearchPanel 마운트/언마운트 추적
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('[DEBUG] 🚀 SearchPanel 마운트됨 - 초기 shopperHouseData 상태:', {
|
|
||||||
hasData: !!shopperHouseData,
|
|
||||||
dataLength: shopperHouseData?.results?.[0]?.docs?.length || 0,
|
|
||||||
currentMode,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
console.log('[DEBUG] 🔚 SearchPanel 언마운트됨 - shopperHouseData 상태:', {
|
|
||||||
hasData: !!shopperHouseData,
|
hasData: !!shopperHouseData,
|
||||||
dataLength: shopperHouseData?.results?.[0]?.docs?.length || 0,
|
dataLength: shopperHouseData?.results?.[0]?.docs?.length || 0,
|
||||||
|
searchId: shopperHouseData?.results?.[0]?.searchId || '(없음)',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
}, []);
|
}, [shopperHouseData?.results?.[0]?.docs?.length, shopperHouseData?.results?.[0]?.searchId, DEBUG_MODE]);
|
||||||
|
|
||||||
// 🐛 [DEBUG] isOnTop 상태 변경 추적 (DetailPanel <-> SearchPanel 전환)
|
// 🐛 [DEBUG] SearchPanel 마운트/언마운트 추적 (DEBUG_MODE가 true일 경우에만)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOnTopRef.current !== isOnTop) {
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG] 🚀 SearchPanel 마운트됨 - 초기 shopperHouseData 상태:', {
|
||||||
|
hasData: !!shopperHouseData,
|
||||||
|
dataLength: shopperHouseData?.results?.[0]?.docs?.length || 0,
|
||||||
|
currentMode,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG] 🔚 SearchPanel 언마운트됨 - shopperHouseData 상태:', {
|
||||||
|
hasData: !!shopperHouseData,
|
||||||
|
dataLength: shopperHouseData?.results?.[0]?.docs?.length || 0,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [DEBUG_MODE]);
|
||||||
|
|
||||||
|
// 🐛 [DEBUG] isOnTop 상태 변경 추적 (DetailPanel <-> SearchPanel 전환, DEBUG_MODE가 true일 경우에만)
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOnTopRef.current !== isOnTop && DEBUG_MODE) {
|
||||||
console.log('[DEBUG] 🔄 SearchPanel isOnTop 상태 변경:', {
|
console.log('[DEBUG] 🔄 SearchPanel isOnTop 상태 변경:', {
|
||||||
from: isOnTopRef.current,
|
from: isOnTopRef.current,
|
||||||
to: isOnTop,
|
to: isOnTop,
|
||||||
@@ -179,7 +229,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isOnTop, shopperHouseData]);
|
}, [isOnTop, shopperHouseData?.results?.[0]?.docs?.length, DEBUG_MODE]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* refs
|
* refs
|
||||||
@@ -190,6 +240,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
const cbChangePageRef = useRef(null);
|
const cbChangePageRef = useRef(null);
|
||||||
const initialFocusTimerRef = useRef(null);
|
const initialFocusTimerRef = useRef(null);
|
||||||
const spotlightResumeTimerRef = useRef(null);
|
const spotlightResumeTimerRef = useRef(null);
|
||||||
|
// DOM 쿼리 최적화를 위한 ref 추가
|
||||||
|
const inputElementRef = useRef(null);
|
||||||
|
|
||||||
|
// Spottable 컴포넌트 캐싱으로 메모리 누수 방지
|
||||||
|
const SpottableMicButton = useMemo(() => Spottable('div'), []);
|
||||||
|
const SpottableKeyword = useMemo(() => Spottable('div'), []);
|
||||||
|
const SpottableProduct = useMemo(() => Spottable('div'), []);
|
||||||
|
|
||||||
|
// SafeImage 컴포넌트 캐싱
|
||||||
|
const SafeImage = useCallback(SafeImageComponent, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* memoized variables
|
* memoized variables
|
||||||
@@ -232,10 +292,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
|
|
||||||
// ✨ [Phase 4] Enter/OK 키 처리 - SearchInputOverlay 표시
|
// ✨ [Phase 4] Enter/OK 키 처리 - SearchInputOverlay 표시
|
||||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||||
|
console.log('[DEBUG] [SearchPanel] handleKeydown: Enter/OK 키 누름');
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsSearchOverlayVisible(true);
|
setIsSearchOverlayVisible(true);
|
||||||
// TInputSimple의 포커스 해제
|
// ✅ 수정: Enter 키 누를 때에도 inputFocus 유지 (VOICE_RESULT 모드에서 키보드 이벤트 수신 가능)
|
||||||
setInputFocus(false);
|
// setInputFocus(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,10 +321,15 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
// 오른쪽 화살표 키 처리 - 포커스 이동 허용
|
// 오른쪽 화살표 키 처리 - 포커스 이동 허용
|
||||||
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`);
|
// DOM 쿼리 최적화: 캐싱된 input element 사용
|
||||||
if (input && position === input.value.length) {
|
const input = inputElementRef.current ||
|
||||||
// 커서가 텍스트 끝에 있으면 포커스 이동 허용
|
document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
|
||||||
return;
|
if (input) {
|
||||||
|
inputElementRef.current = input; // 캐싱
|
||||||
|
if (position === input.value.length) {
|
||||||
|
// 커서가 텍스트 끝에 있으면 포커스 이동 허용
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,11 +395,15 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 0hun: keyUp 이벤트 핸들러, keyUp 시 `position`의 상태값 변경하는 함수
|
* 0hun: keyUp 이벤트 핸들러, keyUp 시 `position`의 상태값 변경하는 함수
|
||||||
|
* DOM 쿼리 최적화: ref를 사용하여 반복적인 DOM 조회 방지
|
||||||
*/
|
*/
|
||||||
const cursorPosition = useCallback(() => {
|
const cursorPosition = useCallback(() => {
|
||||||
const input = document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
|
// ref를 사용하여 캐싱된 input element 사용
|
||||||
|
const input = inputElementRef.current ||
|
||||||
|
document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
|
||||||
|
|
||||||
if (input) {
|
if (input) {
|
||||||
|
inputElementRef.current = input; // 캐싱
|
||||||
setPosition(input.selectionStart);
|
setPosition(input.selectionStart);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@@ -346,10 +416,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
if (DEBUG_MODE) {
|
||||||
'🖱️ [DEBUG][SearchPanel] onClickMic called, current isVoiceOverlayVisible:',
|
console.log(
|
||||||
isVoiceOverlayVisible
|
'🖱️ [DEBUG][SearchPanel] onClickMic called, current isVoiceOverlayVisible:',
|
||||||
);
|
isVoiceOverlayVisible
|
||||||
|
);
|
||||||
|
}
|
||||||
setIsVoiceOverlayVisible(true);
|
setIsVoiceOverlayVisible(true);
|
||||||
// setIsVoiceOverlayVisible((prev) => !prev);
|
// setIsVoiceOverlayVisible((prev) => !prev);
|
||||||
}, [isVoiceOverlayVisible]);
|
}, [isVoiceOverlayVisible]);
|
||||||
@@ -358,59 +430,77 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
* 0hun: panel 뒤로가기 이벤트
|
* 0hun: panel 뒤로가기 이벤트
|
||||||
*/
|
*/
|
||||||
const onCancel = useCallback(() => {
|
const onCancel = useCallback(() => {
|
||||||
console.log('[DEBUG]-onCancel called', {
|
if (DEBUG_MODE) {
|
||||||
isOnTop: isOnTopRef.current,
|
console.log('[DEBUG]-onCancel called', {
|
||||||
isVoiceOverlayVisible,
|
isOnTop: isOnTopRef.current,
|
||||||
isSearchOverlayVisible,
|
isVoiceOverlayVisible,
|
||||||
currentMode,
|
isSearchOverlayVisible,
|
||||||
searchQuery,
|
currentMode,
|
||||||
hasShopperHouseData: !!shopperHouseData,
|
searchQuery,
|
||||||
});
|
hasShopperHouseData: !!shopperHouseData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!isOnTopRef.current) {
|
if (!isOnTopRef.current) {
|
||||||
console.log('[DEBUG]-onCancel: isOnTopRef is false, returning');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-onCancel: isOnTopRef is false, returning');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// VoiceInputOverlay가 열려있으면 먼저 닫기
|
// VoiceInputOverlay가 열려있으면 먼저 닫기
|
||||||
if (isVoiceOverlayVisible) {
|
if (isVoiceOverlayVisible) {
|
||||||
console.log('[DEBUG]-onCancel: closing VoiceInputOverlay');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-onCancel: closing VoiceInputOverlay');
|
||||||
|
}
|
||||||
setIsVoiceOverlayVisible(false);
|
setIsVoiceOverlayVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchInputOverlay가 열려있으면 먼저 닫기
|
// SearchInputOverlay가 열려있으면 먼저 닫기
|
||||||
if (isSearchOverlayVisible) {
|
if (isSearchOverlayVisible) {
|
||||||
console.log('[DEBUG]-onCancel: closing SearchInputOverlay');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-onCancel: closing SearchInputOverlay');
|
||||||
|
}
|
||||||
setIsSearchOverlayVisible(false);
|
setIsSearchOverlayVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✨ [Phase 5] VOICE_RESULT 모드에서 ESC/뒤로가기 누르면 INITIAL 모드로 돌아가기
|
// ✨ [Phase 5] VOICE_RESULT 모드에서 ESC/뒤로가기 누르면 INITIAL 모드로 돌아가기
|
||||||
console.log('[DEBUG]-VOICE_RESULT check:', {
|
if (DEBUG_MODE) {
|
||||||
currentMode,
|
console.log('[DEBUG]-VOICE_RESULT check:', {
|
||||||
isVoiceResultMode: currentMode === SEARCH_PANEL_MODES.VOICE_RESULT,
|
currentMode,
|
||||||
VOICE_RESULT_value: SEARCH_PANEL_MODES.VOICE_RESULT,
|
isVoiceResultMode: currentMode === SEARCH_PANEL_MODES.VOICE_RESULT,
|
||||||
});
|
VOICE_RESULT_value: SEARCH_PANEL_MODES.VOICE_RESULT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
|
if (currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
|
||||||
console.log(
|
if (DEBUG_MODE) {
|
||||||
'[DEBUG]-VOICE_RESULT: Clearing ShopperHouse data (searchId will be preserved for 2nd search)'
|
console.log(
|
||||||
);
|
'[DEBUG]-VOICE_RESULT: Clearing ShopperHouse data (searchId will be preserved for 2nd search)'
|
||||||
console.log('[VoiceInput]-SearchPanel-onCancel-VOICE_RESULT');
|
);
|
||||||
console.log('[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출');
|
console.log('[VoiceInput]-SearchPanel-onCancel-VOICE_RESULT');
|
||||||
|
console.log('[VoiceInput] 🧹 VOICE_RESULT 모드에서 ESC 누름 - clearShopperHouseData 호출');
|
||||||
|
}
|
||||||
dispatch(clearShopperHouseData()); // ✨ shopperHouseData만 초기화, searchId & relativeQuerys 유지
|
dispatch(clearShopperHouseData()); // ✨ shopperHouseData만 초기화, searchId & relativeQuerys 유지
|
||||||
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
|
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[DEBUG]-onCancel: normal cancel logic', { searchQuery });
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-onCancel: normal cancel logic', { searchQuery });
|
||||||
|
}
|
||||||
|
|
||||||
if (searchQuery === null || searchQuery === '') {
|
if (searchQuery === null || searchQuery === '') {
|
||||||
console.log('[DEBUG]-onCancel: popping panel');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-onCancel: popping panel');
|
||||||
|
}
|
||||||
dispatch(popPanel(panel_names.SEARCH_PANEL));
|
dispatch(popPanel(panel_names.SEARCH_PANEL));
|
||||||
} else {
|
} else {
|
||||||
console.log('[DEBUG]-onCancel: resetting search query');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-onCancel: resetting search query');
|
||||||
|
}
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
dispatch(resetSearch());
|
dispatch(resetSearch());
|
||||||
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
|
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
|
||||||
@@ -449,7 +539,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[panelInfo?.currentSpot, firstSpot, panels]
|
[firstSpot, panels.length, panelInfo?.currentSpot] // panels 대신 panels.length 사용
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -493,9 +583,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
* Voice overlay close handler
|
* Voice overlay close handler
|
||||||
*/
|
*/
|
||||||
const handleVoiceOverlayClose = useCallback(() => {
|
const handleVoiceOverlayClose = useCallback(() => {
|
||||||
console.log(
|
if (DEBUG_MODE) {
|
||||||
'🚪 [DEBUG][SearchPanel] handleVoiceOverlayClose called, setting isVoiceOverlayVisible to FALSE'
|
console.log(
|
||||||
);
|
'🚪 [DEBUG][SearchPanel] handleVoiceOverlayClose called, setting isVoiceOverlayVisible to FALSE'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ✨ Redux 정리는 VoiceInputOverlay.handleClose()에서 처리함
|
// ✨ Redux 정리는 VoiceInputOverlay.handleClose()에서 처리함
|
||||||
// VoiceInputOverlay가 닫히는 순간 clearShopperHouseData()를 호출하면,
|
// VoiceInputOverlay가 닫히는 순간 clearShopperHouseData()를 호출하면,
|
||||||
@@ -576,12 +668,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<div className={css.productImageWrapper}>
|
<div className={css.productImageWrapper}>
|
||||||
<img src={bgImgPath} alt={curationNm} className={css.productImage} />
|
<SafeImage src={bgImgPath} alt={curationNm} className={css.productImage} />
|
||||||
</div>
|
</div>
|
||||||
<div className={css.productInfo}>
|
<div className={css.productInfo}>
|
||||||
{showBrandLogo && (
|
{showBrandLogo && (
|
||||||
<div className={css.productBrandWrapper}>
|
<div className={css.productBrandWrapper}>
|
||||||
<img src={patncLogoPath} alt={patncNm} className={css.brandLogo} />
|
<SafeImage src={patncLogoPath} alt={patncNm} className={css.brandLogo} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={css.productDetails}>
|
<div className={css.productDetails}>
|
||||||
@@ -592,7 +684,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
</SpottableProduct>
|
</SpottableProduct>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[hotPicksForYou, dispatch]
|
[hotPicksForYou, dispatch, SafeImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -639,6 +731,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
case SEARCH_PANEL_MODES.INITIAL:
|
case SEARCH_PANEL_MODES.INITIAL:
|
||||||
case SEARCH_PANEL_MODES.INPUT_FOCUSED:
|
case SEARCH_PANEL_MODES.INPUT_FOCUSED:
|
||||||
case SEARCH_PANEL_MODES.VOICE_INPUT:
|
case SEARCH_PANEL_MODES.VOICE_INPUT:
|
||||||
|
case SEARCH_PANEL_MODES.SEARCH_INPUT:
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<ContainerBasic className={css.contentContainer}>
|
<ContainerBasic className={css.contentContainer}>
|
||||||
@@ -758,16 +851,18 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
currentMode,
|
currentMode,
|
||||||
searchDatas,
|
searchDatas?.theme?.length,
|
||||||
shopperHouseData,
|
searchDatas?.item?.length,
|
||||||
recentSearches,
|
searchDatas?.show?.length,
|
||||||
topSearchs,
|
shopperHouseData?.results?.[0]?.docs?.length,
|
||||||
popularBrands,
|
recentSearches?.length,
|
||||||
hotPicksForYou,
|
topSearchs?.length,
|
||||||
|
popularBrands?.length,
|
||||||
|
hotPicksForYou?.length,
|
||||||
handleKeywordClick,
|
handleKeywordClick,
|
||||||
createKeywordClickHandler,
|
createKeywordClickHandler,
|
||||||
renderItem,
|
renderItem,
|
||||||
panelInfo,
|
panelInfo?.currentSpot,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -787,29 +882,35 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let nextMode = SEARCH_PANEL_MODES.INITIAL;
|
let nextMode = SEARCH_PANEL_MODES.INITIAL;
|
||||||
|
|
||||||
console.log('[DEBUG]-MODE DECISION useEffect running', {
|
if (DEBUG_MODE) {
|
||||||
isVoiceOverlayVisible,
|
console.log('[DEBUG]-MODE DECISION useEffect running', {
|
||||||
hasShopperHouseData: !!shopperHouseData,
|
isVoiceOverlayVisible,
|
||||||
shopperHouseData_detail: shopperHouseData ? 'EXISTS' : 'NULL',
|
hasShopperHouseData: !!shopperHouseData,
|
||||||
searchPerformed,
|
shopperHouseData_detail: shopperHouseData ? 'EXISTS' : 'NULL',
|
||||||
searchQuery,
|
searchPerformed,
|
||||||
hasSearchResults: !!(
|
searchQuery,
|
||||||
searchDatas?.theme?.length > 0 ||
|
hasSearchResults: !!(
|
||||||
searchDatas?.item?.length > 0 ||
|
searchDatas?.theme?.length > 0 ||
|
||||||
searchDatas?.show?.length > 0
|
searchDatas?.item?.length > 0 ||
|
||||||
),
|
searchDatas?.show?.length > 0
|
||||||
isSearchOverlayVisible,
|
),
|
||||||
currentMode,
|
isSearchOverlayVisible,
|
||||||
});
|
currentMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 우선순위 1: 음성 입력 오버레이가 열려있으면 VOICE_INPUT 모드
|
// 우선순위 1: 음성 입력 오버레이가 열려있으면 VOICE_INPUT 모드
|
||||||
if (isVoiceOverlayVisible) {
|
if (isVoiceOverlayVisible) {
|
||||||
console.log('[DEBUG]-MODE: isVoiceOverlayVisible is TRUE → VOICE_INPUT');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-MODE: isVoiceOverlayVisible is TRUE → VOICE_INPUT');
|
||||||
|
}
|
||||||
nextMode = SEARCH_PANEL_MODES.VOICE_INPUT;
|
nextMode = SEARCH_PANEL_MODES.VOICE_INPUT;
|
||||||
}
|
}
|
||||||
// 우선순위 2: 음성 검색 결과가 있으면 VOICE_RESULT 모드
|
// 우선순위 2: 음성 검색 결과가 있으면 VOICE_RESULT 모드
|
||||||
else if (shopperHouseData) {
|
else if (shopperHouseData) {
|
||||||
console.log('[DEBUG]-MODE: shopperHouseData EXISTS → VOICE_RESULT');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-MODE: shopperHouseData EXISTS → VOICE_RESULT');
|
||||||
|
}
|
||||||
nextMode = SEARCH_PANEL_MODES.VOICE_RESULT;
|
nextMode = SEARCH_PANEL_MODES.VOICE_RESULT;
|
||||||
}
|
}
|
||||||
// 우선순위 3: 검색 결과가 있으면 SEARCH_RESULT 모드
|
// 우선순위 3: 검색 결과가 있으면 SEARCH_RESULT 모드
|
||||||
@@ -819,38 +920,48 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
searchDatas?.item?.length > 0 ||
|
searchDatas?.item?.length > 0 ||
|
||||||
searchDatas?.show?.length > 0
|
searchDatas?.show?.length > 0
|
||||||
) {
|
) {
|
||||||
console.log('[DEBUG]-MODE: searchResults EXISTS → SEARCH_RESULT');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-MODE: searchResults EXISTS → SEARCH_RESULT');
|
||||||
|
}
|
||||||
nextMode = SEARCH_PANEL_MODES.SEARCH_RESULT;
|
nextMode = SEARCH_PANEL_MODES.SEARCH_RESULT;
|
||||||
}
|
}
|
||||||
// 우선순위 4: 검색 입력 오버레이가 열려있으면 SEARCH_INPUT 모드
|
// 우선순위 4: 검색 입력 오버레이가 열려있으면 SEARCH_INPUT 모드
|
||||||
else if (isSearchOverlayVisible) {
|
else if (isSearchOverlayVisible) {
|
||||||
console.log('[DEBUG]-MODE: isSearchOverlayVisible is TRUE → SEARCH_INPUT');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-MODE: isSearchOverlayVisible is TRUE → SEARCH_INPUT');
|
||||||
|
}
|
||||||
nextMode = SEARCH_PANEL_MODES.SEARCH_INPUT;
|
nextMode = SEARCH_PANEL_MODES.SEARCH_INPUT;
|
||||||
}
|
}
|
||||||
// 우선순위 5: 초기 상태 (기본값)
|
// 우선순위 5: 초기 상태 (기본값)
|
||||||
else {
|
else {
|
||||||
console.log('[DEBUG]-MODE: No condition met → INITIAL');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-MODE: No condition met → INITIAL');
|
||||||
|
}
|
||||||
nextMode = SEARCH_PANEL_MODES.INITIAL;
|
nextMode = SEARCH_PANEL_MODES.INITIAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모드가 변경되었을 때만 업데이트
|
// 모드가 변경되었을 때만 업데이트
|
||||||
if (nextMode !== currentMode) {
|
if (nextMode !== currentMode) {
|
||||||
console.log(`[DEBUG]-VOICE_RESULT 🔀 Mode changed: ${currentMode} → ${nextMode}`, {
|
if (DEBUG_MODE) {
|
||||||
isVoiceOverlayVisible,
|
console.log(`[DEBUG]-VOICE_RESULT 🔀 Mode changed: ${currentMode} → ${nextMode}`, {
|
||||||
shopperHouseData: !!shopperHouseData,
|
isVoiceOverlayVisible,
|
||||||
searchPerformed,
|
shopperHouseData: !!shopperHouseData,
|
||||||
searchQuery,
|
searchPerformed,
|
||||||
hasSearchResults: !!(
|
searchQuery,
|
||||||
searchDatas?.theme?.length > 0 ||
|
hasSearchResults: !!(
|
||||||
searchDatas?.item?.length > 0 ||
|
searchDatas?.theme?.length > 0 ||
|
||||||
searchDatas?.show?.length > 0
|
searchDatas?.item?.length > 0 ||
|
||||||
),
|
searchDatas?.show?.length > 0
|
||||||
isSearchOverlayVisible,
|
),
|
||||||
inputFocus,
|
isSearchOverlayVisible,
|
||||||
});
|
inputFocus,
|
||||||
|
});
|
||||||
|
}
|
||||||
setCurrentMode(nextMode);
|
setCurrentMode(nextMode);
|
||||||
} else {
|
} else {
|
||||||
console.log('[DEBUG]-MODE: Mode unchanged -', currentMode);
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-MODE: Mode unchanged -', currentMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isVoiceOverlayVisible,
|
isVoiceOverlayVisible,
|
||||||
@@ -870,9 +981,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
* TODO: 현재 검색어가 빈 스트링일 경우, `resetSearch`가 발동해서 searchData가 reset되는 현상이 있음, 조건이 변경되어야 한다.
|
* TODO: 현재 검색어가 빈 스트링일 경우, `resetSearch`가 발동해서 searchData가 reset되는 현상이 있음, 조건이 변경되어야 한다.
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[DEBUG]-searchQuery useEffect:', { searchQuery });
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-searchQuery useEffect:', { searchQuery });
|
||||||
|
}
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
console.log('[DEBUG]-VOICE_RESULT: searchQuery is empty, calling resetSearch');
|
if (DEBUG_MODE) {
|
||||||
|
console.log('[DEBUG]-VOICE_RESULT: searchQuery is empty, calling resetSearch');
|
||||||
|
}
|
||||||
dispatch(resetSearch());
|
dispatch(resetSearch());
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
@@ -913,11 +1028,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
initialFocusTimerRef.current = setTimeout(() => {
|
initialFocusTimerRef.current = setTimeout(() => {
|
||||||
// 🎯 DetailPanel에서 돌아왔을 때 이전 상품에 포커스 복원
|
// 🎯 DetailPanel에서 돌아왔을 때 이전 상품에 포커스 복원
|
||||||
if (panelInfo?.currentSpot && currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
|
if (panelInfo?.currentSpot && currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
|
||||||
console.log('[DEBUG] 🎯 DetailPanel에서 복귀 - 이전 상품으로 포커스 복원:', {
|
if (DEBUG_MODE) {
|
||||||
currentSpot: panelInfo.currentSpot,
|
console.log('[DEBUG] 🎯 DetailPanel에서 복귀 - 이전 상품으로 포커스 복원:', {
|
||||||
currentMode,
|
currentSpot: panelInfo.currentSpot,
|
||||||
timestamp: new Date().toISOString(),
|
currentMode,
|
||||||
});
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
Spotlight.focus(panelInfo.currentSpot);
|
Spotlight.focus(panelInfo.currentSpot);
|
||||||
} else {
|
} else {
|
||||||
// 일반적인 경우: TInput으로 포커스
|
// 일반적인 경우: TInput으로 포커스
|
||||||
@@ -933,7 +1050,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
initialFocusTimerRef.current = null;
|
initialFocusTimerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isOnTop, isOnTopRef, panelInfo?.currentSpot, currentMode]);
|
}, [isOnTop, panelInfo?.currentSpot, currentMode]); // isOnTopRef 제거
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* focus 용도
|
* focus 용도
|
||||||
@@ -943,14 +1060,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
if (shopperHouseData && isOnTop) {
|
if (shopperHouseData && isOnTop) {
|
||||||
// 🎯 VOICE_RESULT 모드에서는 포커스 복원을 위해 TInput으로 이동하지 않음
|
// 🎯 VOICE_RESULT 모드에서는 포커스 복원을 위해 TInput으로 이동하지 않음
|
||||||
if (currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
|
if (currentMode === SEARCH_PANEL_MODES.VOICE_RESULT) {
|
||||||
console.log(
|
if (DEBUG_MODE) {
|
||||||
'[DEBUG] 🎯 VOICE_RESULT 모드 - ShopperHouse 데이터 수신 시 TInput 포커스 스킵:',
|
console.log(
|
||||||
{
|
'[DEBUG] 🎯 VOICE_RESULT 모드 - ShopperHouse 데이터 수신 시 TInput 포커스 스킵:',
|
||||||
currentMode,
|
{
|
||||||
hasShopperHouseData: !!shopperHouseData,
|
currentMode,
|
||||||
reason: '상품 포커스 복원 우선',
|
hasShopperHouseData: !!shopperHouseData,
|
||||||
}
|
reason: '상품 포커스 복원 우선',
|
||||||
);
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1096,7 +1215,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
kind={KINDS.withIcon}
|
kind={KINDS.withIcon}
|
||||||
icon={ICONS.search}
|
icon={ICONS.search}
|
||||||
text={searchQuery} // ✨ [Phase 8] Overlay에서 입력받은 텍스트만 표시
|
text={searchQuery} // ✨ [Phase 8] Overlay에서 입력받은 텍스트만 표시
|
||||||
alwaysShowText={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT} // 🎯 VOICE_RESULT 모드에서 항상 텍스트 표시
|
alwaysShowText={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT || currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT} // 🎯 VOICE_RESULT & SEARCH_RESULT 모드에서 항상 텍스트 표시
|
||||||
|
inputFocus={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT || currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT} // ✅ VOICE_RESULT & SEARCH_RESULT 모드에서 TInputSimple 내부 포커스 활성화
|
||||||
onKeyDown={handleKeydown}
|
onKeyDown={handleKeydown}
|
||||||
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
|
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
|
||||||
forcedSpotlight="recent-keyword-0"
|
forcedSpotlight="recent-keyword-0"
|
||||||
@@ -1115,7 +1235,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
|
spotlightId={SPOTLIGHT_IDS.MICROPHONE_BUTTON}
|
||||||
>
|
>
|
||||||
<div className={css.microphoneCircle}>
|
<div className={css.microphoneCircle}>
|
||||||
<img src={micIcon} alt="Microphone" className={css.microphoneIcon} />
|
<SafeImage src={micIcon} alt="Microphone" className={css.microphoneIcon} />
|
||||||
</div>
|
</div>
|
||||||
</SpottableMicButton>
|
</SpottableMicButton>
|
||||||
</div>
|
</div>
|
||||||
@@ -1155,7 +1275,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
|
|
||||||
{/* ✨ [Phase 1] Voice Input Overlay - currentMode로 visibility 제어 */}
|
{/* ✨ [Phase 1] Voice Input Overlay - currentMode로 visibility 제어 */}
|
||||||
<VoiceInputOverlay
|
<VoiceInputOverlay
|
||||||
isVisible={currentMode === SEARCH_PANEL_MODES.VOICE_INPUT}
|
isVisible={isVoiceOverlayVisible}
|
||||||
onClose={handleVoiceOverlayClose}
|
onClose={handleVoiceOverlayClose}
|
||||||
mode={VOICE_MODES.PROMPT}
|
mode={VOICE_MODES.PROMPT}
|
||||||
suggestions={voiceSuggestions}
|
suggestions={voiceSuggestions}
|
||||||
@@ -1166,7 +1286,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
|
|
||||||
{/* ✨ [Phase 1] Search Input Overlay - currentMode로 visibility 제어 */}
|
{/* ✨ [Phase 1] Search Input Overlay - currentMode로 visibility 제어 */}
|
||||||
<SearchInputOverlay
|
<SearchInputOverlay
|
||||||
isVisible={currentMode === SEARCH_PANEL_MODES.SEARCH_INPUT}
|
isVisible={isSearchOverlayVisible}
|
||||||
onClose={handleSearchOverlayClose}
|
onClose={handleSearchOverlayClose}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import Spotlight from '@enact/spotlight';
|
||||||
import Spottable from '@enact/spotlight/Spottable';
|
import Spottable from '@enact/spotlight/Spottable';
|
||||||
|
|
||||||
import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png';
|
import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png';
|
||||||
@@ -26,7 +27,53 @@ export const HOW_ABOUT_THESE_MODES = {
|
|||||||
FULL: 'full', // 전체 버전 (팝업)
|
FULL: 'full', // 전체 버전 (팝업)
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, keywordClick }) => {
|
// 메모리 누수 방지를 위한 안전한 이미지 컴포넌트
|
||||||
|
const SafeImage = ({ src, alt, className, ...props }) => {
|
||||||
|
const imgRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const img = imgRef.current;
|
||||||
|
if (!img) return;
|
||||||
|
|
||||||
|
// 이미지 로드 완료 핸들러
|
||||||
|
const handleLoad = () => {
|
||||||
|
// 로드 성공 시 특별한 처리 불필요
|
||||||
|
};
|
||||||
|
|
||||||
|
// 이미지 로드 에러 핸들러
|
||||||
|
const handleError = () => {
|
||||||
|
// 에러 시 src를 제거하여 깨진 이미지 방지
|
||||||
|
if (img.src) {
|
||||||
|
img.src = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
img.addEventListener('load', handleLoad);
|
||||||
|
img.addEventListener('error', handleError);
|
||||||
|
|
||||||
|
// 컴포넌트 unmount 시 이벤트 리스너 정리
|
||||||
|
return () => {
|
||||||
|
img.removeEventListener('load', handleLoad);
|
||||||
|
img.removeEventListener('error', handleError);
|
||||||
|
// 이미지 로딩 취소
|
||||||
|
if (img.src) {
|
||||||
|
img.src = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
ref={imgRef}
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className={className}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SearchResultsNew = ({ panelInfo, itemInfo, showInfo, themeInfo, shopperHouseInfo, keywordClick }) => {
|
||||||
// ShopperHouse 데이터를 ItemCard 형식으로 변환
|
// ShopperHouse 데이터를 ItemCard 형식으로 변환
|
||||||
const convertedShopperHouseItems = useMemo(() => {
|
const convertedShopperHouseItems = useMemo(() => {
|
||||||
if (!shopperHouseInfo || !shopperHouseInfo.results || shopperHouseInfo.results.length === 0) {
|
if (!shopperHouseInfo || !shopperHouseInfo.results || shopperHouseInfo.results.length === 0) {
|
||||||
@@ -73,8 +120,6 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
|
|||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
let buttonTabList = null;
|
|
||||||
|
|
||||||
//탭
|
//탭
|
||||||
const [tab, setTab] = useState(0);
|
const [tab, setTab] = useState(0);
|
||||||
//드롭다운
|
//드롭다운
|
||||||
@@ -83,7 +128,8 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
|
|||||||
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
|
const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE);
|
||||||
|
|
||||||
const [styleChange, setStyleChange] = useState(false);
|
const [styleChange, setStyleChange] = useState(false);
|
||||||
const filterMethods = [];
|
// filterMethods 빈 배열 캐싱으로 메모리 누수 방지
|
||||||
|
const filterMethods = useMemo(() => [], []);
|
||||||
const cbChangePageRef = useRef(null);
|
const cbChangePageRef = useRef(null);
|
||||||
|
|
||||||
// HowAboutThese 모드 상태 관리
|
// HowAboutThese 모드 상태 관리
|
||||||
@@ -121,9 +167,12 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
|
|||||||
[keywordClick]
|
[keywordClick]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!buttonTabList) {
|
// buttonTabList 최적화 - 의존성이 변경될 때만 재계산
|
||||||
buttonTabList = getButtonTabList();
|
const buttonTabList = useMemo(() => getButtonTabList(), [
|
||||||
}
|
convertedShopperHouseItems?.length,
|
||||||
|
itemInfo?.length,
|
||||||
|
showInfo?.length
|
||||||
|
]);
|
||||||
|
|
||||||
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선
|
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선
|
||||||
const currentData = tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
|
const currentData = tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
|
||||||
@@ -174,8 +223,9 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
|
|||||||
[dropDownTab]
|
[dropDownTab]
|
||||||
);
|
);
|
||||||
|
|
||||||
const SpottableLi = Spottable('li');
|
// Spottable 컴포넌트 캐싱하여 메모리 누수 방지
|
||||||
const SpottableDiv = Spottable('div');
|
const SpottableLi = useMemo(() => Spottable('li'), []);
|
||||||
|
const SpottableDiv = useMemo(() => Spottable('div'), []);
|
||||||
|
|
||||||
// 맨 처음으로 이동 (위 버튼)
|
// 맨 처음으로 이동 (위 버튼)
|
||||||
const upBtnClick = () => {
|
const upBtnClick = () => {
|
||||||
@@ -191,9 +241,14 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
|
|||||||
}
|
}
|
||||||
}, [hasMore]);
|
}, [hasMore]);
|
||||||
|
|
||||||
// ProductCard 컴포넌트
|
// ProductCard 컴포넌트 - 의존성 최적화 및 안전한 이미지 사용
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ index, ...rest }) => {
|
({ index, ...rest }) => {
|
||||||
|
// themeInfo가 undefined일 경우를 대비한 방어 코드
|
||||||
|
if (!themeInfo || !themeInfo[index]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const { bgImgPath, title, partnerLogo, partnerName, keyword } = themeInfo[index];
|
const { bgImgPath, title, partnerLogo, partnerName, keyword } = themeInfo[index];
|
||||||
return (
|
return (
|
||||||
<SpottableDiv
|
<SpottableDiv
|
||||||
@@ -203,11 +258,11 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
|
|||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<div className={css.productImageWrapper}>
|
<div className={css.productImageWrapper}>
|
||||||
<img src={bgImgPath} alt={title} className={css.productImage} />
|
<SafeImage src={bgImgPath} alt={title} className={css.productImage} />
|
||||||
</div>
|
</div>
|
||||||
<div className={css.productInfo}>
|
<div className={css.productInfo}>
|
||||||
<div className={css.productBrandWrapper}>
|
<div className={css.productBrandWrapper}>
|
||||||
<img src={partnerLogo} alt={partnerName} className={css.brandLogo} />
|
<SafeImage src={partnerLogo} alt={partnerName} className={css.brandLogo} />
|
||||||
</div>
|
</div>
|
||||||
<div className={css.productDetails}>
|
<div className={css.productDetails}>
|
||||||
{keyword && (
|
{keyword && (
|
||||||
@@ -223,7 +278,7 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
|
|||||||
</SpottableDiv>
|
</SpottableDiv>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[themeInfo]
|
[themeInfo?.length, SpottableDiv, SafeImage] // themeInfo 전체 대신 length와 캐싱된 컴포넌트 사용
|
||||||
);
|
);
|
||||||
|
|
||||||
// relativeQueries 가져오기 (Redux에서 제공)
|
// relativeQueries 가져오기 (Redux에서 제공)
|
||||||
@@ -237,6 +292,29 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo, key
|
|||||||
return ['Puppy food', 'Dog toy', 'Fitness'];
|
return ['Puppy food', 'Dog toy', 'Fitness'];
|
||||||
}, [shopperHouseInfo]);
|
}, [shopperHouseInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const targetId = panelInfo?.currentSpot
|
||||||
|
? panelInfo?.currentSpot
|
||||||
|
: themeInfo?.length > 0
|
||||||
|
? "searchProduct-0"
|
||||||
|
: itemInfo?.length > 0
|
||||||
|
? "searchItemContents0"
|
||||||
|
: showInfo?.length > 0
|
||||||
|
? "categoryShowContents0"
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!targetId) return;
|
||||||
|
|
||||||
|
const spotTimeout = setTimeout(() => Spotlight.focus(targetId), 100);
|
||||||
|
return () => clearTimeout(spotTimeout);
|
||||||
|
}, [
|
||||||
|
panelInfo?.currentSpot,
|
||||||
|
themeInfo?.length,
|
||||||
|
itemInfo?.length,
|
||||||
|
showInfo?.length,
|
||||||
|
convertedShopperHouseItems?.length // shopperHouseInfo 대신 구체적인 의존성 사용
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={css.searchBox}>
|
<div className={css.searchBox}>
|
||||||
{/* HowAboutThese Small 버전 - 기본 인라인 표시 */}
|
{/* HowAboutThese Small 버전 - 기본 인라인 표시 */}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState, useEffect } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -42,6 +42,7 @@ export default function TInputSimple({
|
|||||||
placeholder,
|
placeholder,
|
||||||
text, // ✨ [Phase 8] Overlay에서 입력받은 텍스트
|
text, // ✨ [Phase 8] Overlay에서 입력받은 텍스트
|
||||||
alwaysShowText = false, // 🎯 VOICE_RESULT 모드에서 항상 텍스트 표시
|
alwaysShowText = false, // 🎯 VOICE_RESULT 모드에서 항상 텍스트 표시
|
||||||
|
inputFocus = false, // ✨ 외부 inputFocus 상태 전달받기 위한 prop
|
||||||
...rest
|
...rest
|
||||||
}) {
|
}) {
|
||||||
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollTop);
|
const { handleScrollReset, handleStopScrolling } = useScrollReset(scrollTop);
|
||||||
@@ -52,6 +53,18 @@ export default function TInputSimple({
|
|||||||
// 🎯 텍스트 표시 조건: 포커스가 있거나 alwaysShowText가 true면 텍스트 표시
|
// 🎯 텍스트 표시 조건: 포커스가 있거나 alwaysShowText가 true면 텍스트 표시
|
||||||
const shouldShowText = isFocused || alwaysShowText;
|
const shouldShowText = isFocused || alwaysShowText;
|
||||||
|
|
||||||
|
// ✨ 외부 inputFocus 상태와 내부 isFocused 상태 동기화를 위한 effect
|
||||||
|
useEffect(() => {
|
||||||
|
// 외부에서 전달받은 inputFocus가 true일 때 내부 isFocused도 true로 설정
|
||||||
|
if (inputFocus && !isFocused) {
|
||||||
|
setIsFocused(true);
|
||||||
|
}
|
||||||
|
// 외부에서 전달받은 inputFocus가 false일 때 내부 isFocused도 false로 설정
|
||||||
|
if (!inputFocus && isFocused) {
|
||||||
|
setIsFocused(false);
|
||||||
|
}
|
||||||
|
}, [inputFocus]);
|
||||||
|
|
||||||
// Focus 핸들러
|
// Focus 핸들러
|
||||||
const _onFocus = useCallback(() => {
|
const _onFocus = useCallback(() => {
|
||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user