[251031] fix: resolve merge conflicts in voice input overlay and search components
This commit is contained in:
@@ -186,6 +186,10 @@ export const types = {
|
||||
UPDATE_SEARCH_TIMESTAMP: 'UPDATE_SEARCH_TIMESTAMP',
|
||||
SET_SHOPPERHOUSE_ERROR: 'SET_SHOPPERHOUSE_ERROR',
|
||||
|
||||
// 🎯 [Phase 1] SearchPanel 모드 제어 명령
|
||||
SWITCH_TO_SEARCH_INPUT_OVERLAY: 'SWITCH_TO_SEARCH_INPUT_OVERLAY',
|
||||
CLEAR_PANEL_COMMAND: 'CLEAR_PANEL_COMMAND',
|
||||
|
||||
// event actions
|
||||
GET_WELCOME_EVENT_INFO: 'GET_WELCOME_EVENT_INFO',
|
||||
GET_EVENT_ISSUED_STATUS: 'GET_EVENT_ISSUED_STATUS',
|
||||
|
||||
@@ -329,3 +329,95 @@ export const getSearchMain = () => (dispatch, getState) => {
|
||||
export const clearSearchMainData = () => ({
|
||||
type: types.CLEAR_SEARCH_MAIN_DATA,
|
||||
});
|
||||
|
||||
// 🎯 [Phase 1] SearchPanel 모드 제어 명령 - VoiceInputOverlay에서 SearchInputOverlay로 전환
|
||||
/**
|
||||
* VoiceInputOverlay의 TInputSimple에서 Enter 키 또는 마우스 클릭 감지 시
|
||||
* SearchInputOverlay로 전환하도록 신호를 보냅니다.
|
||||
*
|
||||
* 흐름:
|
||||
* VoiceInputOverlay (TInputSimple)
|
||||
* ↓ (Enter/Click 감지)
|
||||
* dispatch(switchToSearchInputOverlay())
|
||||
* ↓
|
||||
* Redux state 업데이트
|
||||
* ↓
|
||||
* SearchPanel useSelector로 감지
|
||||
* ↓
|
||||
* SearchPanel이 VoiceOverlay 닫고 SearchInputOverlay 오픈
|
||||
*
|
||||
* @param {string} source - 명령 발생 출처 (기본값: 'VoiceInputOverlay')
|
||||
* @returns {object} Redux action
|
||||
*/
|
||||
export const switchToSearchInputOverlay = (source = 'VoiceInputOverlay') => {
|
||||
console.log('[searchActions] 🔄 switchToSearchInputOverlay 명령 발송', {
|
||||
source,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
type: types.SWITCH_TO_SEARCH_INPUT_OVERLAY,
|
||||
payload: {
|
||||
source,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// 🎯 [Phase 1] SearchPanel 명령 초기화
|
||||
/**
|
||||
* SearchPanel에서 명령을 처리한 후 명령 상태를 초기화합니다.
|
||||
* 이를 통해 다음 명령이 제대로 감지되도록 합니다.
|
||||
*
|
||||
* @returns {object} Redux action
|
||||
*/
|
||||
export const clearPanelCommand = () => {
|
||||
console.log('[searchActions] 🧹 clearPanelCommand 호출 - 명령 초기화');
|
||||
|
||||
return {
|
||||
type: types.CLEAR_PANEL_COMMAND,
|
||||
};
|
||||
};
|
||||
|
||||
// 🎯 [Phase 2] 부드러운 전환: VoiceInputOverlay → SearchInputOverlay
|
||||
/**
|
||||
* Promise 기반의 비동기 전환 로직
|
||||
* 1. VoiceInputOverlay 닫기
|
||||
* 2. 애니메이션 대기
|
||||
* 3. SearchInputOverlay 열기
|
||||
* 4. 렌더링 대기
|
||||
* 5. Spotlight 포커스 설정
|
||||
*
|
||||
* @param {object} options - { setIsVoiceOverlayVisible, setIsSearchOverlayVisible, dispatch, Spotlight }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const transitionToSearchInputOverlay = (options) => async (dispatch) => {
|
||||
const { setIsVoiceOverlayVisible, setIsSearchOverlayVisible, Spotlight } = options;
|
||||
|
||||
console.log('[searchActions] 🔄 transitionToSearchInputOverlay 시작');
|
||||
console.log('[searchActions] ├─ Step 1: VoiceInputOverlay 닫기');
|
||||
|
||||
// Step 1: VoiceInputOverlay 닫기
|
||||
setIsVoiceOverlayVisible(false);
|
||||
|
||||
// Step 2: 애니메이션 대기 (300ms - VoiceInputOverlay 닫기 애니메이션)
|
||||
console.log('[searchActions] ├─ Step 2: 300ms 대기 (VoiceOverlay 애니메이션)');
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
// Step 3: SearchInputOverlay 열기
|
||||
console.log('[searchActions] ├─ Step 3: SearchInputOverlay 열기');
|
||||
setIsSearchOverlayVisible(true);
|
||||
|
||||
// Step 4: 렌더링 대기 (100ms - SearchInputOverlay 렌더링 및 마운트)
|
||||
console.log('[searchActions] ├─ Step 4: 100ms 대기 (SearchInputOverlay 렌더링)');
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Step 5: Spotlight 포커스 설정
|
||||
console.log('[searchActions] ├─ Step 5: Spotlight 포커스 설정 (search_overlay_input_box)');
|
||||
Spotlight.focus('search_overlay_input_box');
|
||||
|
||||
// Step 6: 명령 초기화
|
||||
console.log('[searchActions] └─ Step 6: panelCommand 초기화');
|
||||
dispatch(clearPanelCommand());
|
||||
|
||||
console.log('[searchActions] ✅ transitionToSearchInputOverlay 완료');
|
||||
};
|
||||
|
||||
@@ -17,6 +17,13 @@ const initialState = {
|
||||
hotPicksForYou: [],
|
||||
},
|
||||
shopperHouseError: null, // ShopperHouse API 오류 정보 저장
|
||||
|
||||
// 🎯 [Phase 1] SearchPanel 모드 제어 명령
|
||||
panelCommand: {
|
||||
type: null, // 명령 타입: 'SWITCH_TO_SEARCH_INPUT', 'SWITCH_TO_VOICE_INPUT' 등
|
||||
source: null, // 명령 발생 출처: 'VoiceInputOverlay', 'SearchInputOverlay' 등
|
||||
timestamp: null, // 명령 발생 시간
|
||||
},
|
||||
};
|
||||
|
||||
export const searchReducer = (state = initialState, action) => {
|
||||
@@ -236,6 +243,31 @@ export const searchReducer = (state = initialState, action) => {
|
||||
},
|
||||
};
|
||||
|
||||
// 🎯 [Phase 1] SearchPanel 모드 제어 명령
|
||||
case types.SWITCH_TO_SEARCH_INPUT_OVERLAY:
|
||||
console.log('[searchReducer] 🔄 SWITCH_TO_SEARCH_INPUT_OVERLAY 명령 저장', {
|
||||
source: action.payload?.source,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
panelCommand: {
|
||||
type: 'SWITCH_TO_SEARCH_INPUT',
|
||||
source: action.payload?.source || 'unknown',
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
|
||||
case types.CLEAR_PANEL_COMMAND:
|
||||
return {
|
||||
...state,
|
||||
panelCommand: {
|
||||
type: null,
|
||||
source: null,
|
||||
timestamp: null,
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,10 @@ const SearchInputOverlay = ({
|
||||
// ✨ [Phase 3] Input 포커스 상태에 따른 placeholder 동적 변경
|
||||
const [inputFocused, setInputFocused] = useState(false);
|
||||
|
||||
// 🎯 [Phase 2] 이전 isVisible 상태 추적 (전환 vs 일반 오픈 구분)
|
||||
const [prevIsVisible, setPrevIsVisible] = useState(isVisible);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
||||
// 더미 데이터 - localStorage에 저장된 일반 검색어가 없을 경우에만 표시
|
||||
const fallbackSearches = useMemo(
|
||||
() => [
|
||||
@@ -143,36 +147,155 @@ const SearchInputOverlay = ({
|
||||
return 'Ready to input..';
|
||||
}, [inputFocused]);
|
||||
|
||||
// 🎯 [Phase 2] isVisible 상태 변경 감지 - 전환 vs 일반 오픈 구분
|
||||
useEffect(() => {
|
||||
console.log('[SearchInputOverlay] 📊 isVisible 상태 변경 감지 useEffect 실행', {
|
||||
prevIsVisible,
|
||||
isVisible,
|
||||
isTransitioning,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// false → true로 변경되었을 때만 확인
|
||||
if (!prevIsVisible && isVisible) {
|
||||
// 🔄 VoiceInputOverlay → SearchInputOverlay 전환으로 판단
|
||||
// (일반적으로 SearchPanel에서 Enter 키나 다른 경로로 VoiceOverlay가 먼저 닫히고 이게 열림)
|
||||
console.log('[SearchInputOverlay] 🔄 isVisible false → true 변경 감지!', {
|
||||
prevIsVisible,
|
||||
isVisible,
|
||||
action: 'setIsTransitioning(true) 호출',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
setIsTransitioning(true);
|
||||
} else if (prevIsVisible && !isVisible) {
|
||||
console.log('[SearchInputOverlay] ❌ isVisible true → false 변경 감지', {
|
||||
prevIsVisible,
|
||||
isVisible,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// isVisible 상태 업데이트
|
||||
if (prevIsVisible !== isVisible) {
|
||||
console.log('[SearchInputOverlay] 📍 setPrevIsVisible 호출', {
|
||||
from: prevIsVisible,
|
||||
to: isVisible,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
setPrevIsVisible(isVisible);
|
||||
}, [isVisible]);
|
||||
|
||||
// ✨ [Phase 2] Overlay 오픈 시 input에 자동 포커스 및 입력 준비 상태 설정
|
||||
useEffect(() => {
|
||||
console.log('[SearchInputOverlay] 🎯 포커스 설정 useEffect 실행', {
|
||||
isVisible,
|
||||
isTransitioning,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (isVisible) {
|
||||
let focusTimer = null;
|
||||
|
||||
// 🎯 [Phase 2] VoiceInputOverlay 전환 중이면 250ms 대기 후 포커스
|
||||
// 일반 오픈이면 50ms 대기
|
||||
const delay = isTransitioning ? 250 : 50;
|
||||
|
||||
console.log('[SearchInputOverlay] ⏳ setTimeout 설정', {
|
||||
isTransitioning,
|
||||
delay,
|
||||
action: `${delay}ms 후 포커스 설정 예약`,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// DOM 렌더링이 완료되고 Spotlight가 준비된 후 input에 포커스
|
||||
const focusTimer = setTimeout(() => {
|
||||
focusTimer = setTimeout(() => {
|
||||
console.log('[SearchInputOverlay] ⏰ setTimeout 콜백 실행', {
|
||||
isTransitioning,
|
||||
delay,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// spotlight-id 기반으로 input 요소 찾기
|
||||
const inputContainer = document.querySelector(
|
||||
`[data-spotlight-id="search_overlay_input_box"]`
|
||||
);
|
||||
|
||||
console.log('[SearchInputOverlay] 🔍 inputContainer 검색 결과', {
|
||||
found: !!inputContainer,
|
||||
selector: '[data-spotlight-id="search_overlay_input_box"]',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (inputContainer) {
|
||||
// spotlight-id container 내의 input 요소 찾기
|
||||
const input = inputContainer.querySelector('input');
|
||||
|
||||
console.log('[SearchInputOverlay] 🔍 input 요소 검색 결과', {
|
||||
found: !!input,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (input) {
|
||||
console.log('[SearchInputOverlay] 📍 input 요소 발견 - 포커스 설정 시작', {
|
||||
inputValue: input.value,
|
||||
inputType: input.type,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 입력 필드에 포커스
|
||||
input.focus();
|
||||
console.log('[SearchInputOverlay] ✅ input.focus() 호출 완료', {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 입력 가능 상태 활성화
|
||||
setInputFocused(true);
|
||||
console.log('[SearchInputOverlay] ✅ setInputFocused(true) 호출 완료', {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 커서를 마지막 위치로 이동 (텍스트 선택 대신)
|
||||
const length = input.value.length;
|
||||
input.setSelectionRange(length, length);
|
||||
console.log(
|
||||
'[SearchInputOverlay] 📍 Input focused and ready for input'
|
||||
);
|
||||
}
|
||||
}
|
||||
}, 50); // Overlay 애니메이션 고려한 최소 지연
|
||||
console.log('[SearchInputOverlay] ✅ setSelectionRange 호출 완료', {
|
||||
length,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return () => clearTimeout(focusTimer);
|
||||
console.log('[SearchInputOverlay] 📍 Input focused and ready for input', {
|
||||
isTransitioning,
|
||||
delay,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 전환 완료 후 플래그 초기화
|
||||
if (isTransitioning) {
|
||||
console.log('[SearchInputOverlay] 🔄 setIsTransitioning(false) 호출', {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
setIsTransitioning(false);
|
||||
}
|
||||
} else {
|
||||
console.log('[SearchInputOverlay] ❌ input 요소를 찾을 수 없음!', {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('[SearchInputOverlay] ❌ inputContainer를 찾을 수 없음!', {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
console.log('[SearchInputOverlay] 🧹 useEffect cleanup - focusTimer 제거', {
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
clearTimeout(focusTimer);
|
||||
};
|
||||
}
|
||||
}, [isVisible]);
|
||||
}, [isVisible, isTransitioning]);
|
||||
|
||||
return (
|
||||
<TFullPopup
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
getShopperHouseSearch,
|
||||
resetSearch,
|
||||
resetVoiceSearch,
|
||||
clearPanelCommand,
|
||||
transitionToSearchInputOverlay,
|
||||
} from '../../actions/searchActions';
|
||||
// import {
|
||||
// showErrorToast,
|
||||
@@ -1045,6 +1047,25 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}
|
||||
}, [analyzeCurrentScenario, panelInfo?.currentSpot, shopperHouseData, DEBUG_MODE, isReturningFromDetailPanel, currentPanel, currentIsOnTop, isOnTopChange]);
|
||||
|
||||
/**
|
||||
* 🎯 [Phase 2] VoiceInputOverlay → SearchInputOverlay 전환 콜백
|
||||
* VoiceInputOverlay의 TInputSimple에서 Enter/마우스 클릭 시 호출
|
||||
*/
|
||||
const handleTransitionToSearchInput = useCallback(() => {
|
||||
if (DEBUG_MODE) {
|
||||
console.log('[SearchPanel] 🔄 handleTransitionToSearchInput 호출');
|
||||
}
|
||||
|
||||
// Redux Thunk 액션으로 모든 전환 로직 처리
|
||||
dispatch(
|
||||
transitionToSearchInputOverlay({
|
||||
setIsVoiceOverlayVisible,
|
||||
setIsSearchOverlayVisible,
|
||||
Spotlight,
|
||||
})
|
||||
);
|
||||
}, [DEBUG_MODE, dispatch]);
|
||||
|
||||
/**
|
||||
* Search overlay close handler
|
||||
*/
|
||||
@@ -1067,6 +1088,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
}
|
||||
}, [DEBUG_MODE, searchDatas, searchPerformed]);
|
||||
|
||||
|
||||
/**
|
||||
* Voice overlay close handler
|
||||
*/
|
||||
@@ -1946,7 +1968,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
|
||||
{/* ✨ [Phase 3] Virtual Keyboard - 현재 비활성화됨 (VirtualKeyboardContainer 미사용) */}
|
||||
|
||||
{/* ✨ [Phase 1] Voice Input Overlay - currentMode로 visibility 제어 */}
|
||||
{/* ✨ [Phase 2] Voice Input Overlay - 전환 콜백 추가 */}
|
||||
<VoiceInputOverlay
|
||||
isVisible={isVoiceOverlayVisible}
|
||||
onClose={handleVoiceOverlayClose}
|
||||
@@ -1955,12 +1977,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={handleSearchChange}
|
||||
onSearchSubmit={handleSearchSubmit}
|
||||
onTransitionToSearchInput={handleTransitionToSearchInput}
|
||||
isVoiceResultMode={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT}
|
||||
externalResponseText={voiceOverlayResponseText}
|
||||
isExternalBubbleSearch={isVoiceOverlayBubbleSearch}
|
||||
/>
|
||||
|
||||
{/* ✨ [Phase 1] Search Input Overlay - currentMode로 visibility 제어 */}
|
||||
{/* ✨ [Phase 2] Search Input Overlay - isVisible 감지로 전환 자동 감지 */}
|
||||
<SearchInputOverlay
|
||||
isVisible={isSearchOverlayVisible}
|
||||
onClose={handleSearchOverlayClose}
|
||||
|
||||
@@ -18,13 +18,13 @@ import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import micIcon from '../../../../assets/images/searchpanel/image-mic.png';
|
||||
import { getShopperHouseSearch, clearShopperHouseData, transitionToSearchInputOverlay } from '../../../actions/searchActions';
|
||||
import { updatePanel } from '../../../actions/panelActions';
|
||||
import {
|
||||
clearShopperHouseData,
|
||||
getShopperHouseSearch,
|
||||
} from '../../../actions/searchActions';
|
||||
// import {
|
||||
// clearShopperHouseData,
|
||||
// getShopperHouseSearch,
|
||||
// } from '../../../actions/searchActions';
|
||||
import {
|
||||
cleanupWebSpeech,
|
||||
clearSTTText,
|
||||
@@ -157,6 +157,7 @@ const VoiceInputOverlay = ({
|
||||
searchQuery = '',
|
||||
onSearchChange,
|
||||
onSearchSubmit,
|
||||
onTransitionToSearchInput,
|
||||
isVoiceResultMode = false,
|
||||
externalResponseText = '',
|
||||
isExternalBubbleSearch = false,
|
||||
@@ -1310,14 +1311,29 @@ const VoiceInputOverlay = ({
|
||||
}
|
||||
}, [dispatch, searchQuery, onSearchChange]);
|
||||
|
||||
// Input 창에서 엔터키 핸들러 (API 호출하지 않음)
|
||||
// Input 창에서 엔터키 핸들러
|
||||
// 🎯 [Phase 2] Enter 키 감지 시 SearchInputOverlay로 부드럽게 전환
|
||||
const handleInputKeyDown = useCallback((e) => {
|
||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
// Enter 키로는 API 호출하지 않음
|
||||
// 돋보기 아이콘 클릭/Enter로만 API 호출
|
||||
console.log('[VoiceInputOverlay] 🔄 TInputSimple에서 Enter 키 감지 → SearchInputOverlay 전환 시작');
|
||||
// 🎯 SearchPanel의 콜백 호출
|
||||
if (onTransitionToSearchInput) {
|
||||
onTransitionToSearchInput();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, [onTransitionToSearchInput]);
|
||||
|
||||
// 🎯 [Phase 2] Input 창에서 마우스 클릭 감지 시 SearchInputOverlay로 전환
|
||||
// ⚠️ [251031] 마우스 클릭 시 프리징 발생 - 추후 원인 분석 후 활성화 필요
|
||||
// const handleInputMouseDown = useCallback((e) => {
|
||||
// e.preventDefault();
|
||||
// console.log('[VoiceInputOverlay] 🖱️ TInputSimple에서 마우스 클릭 감지 → SearchInputOverlay 전환 시작');
|
||||
// // 🎯 SearchPanel의 콜백 호출
|
||||
// if (onTransitionToSearchInput) {
|
||||
// onTransitionToSearchInput();
|
||||
// }
|
||||
// }, [onTransitionToSearchInput]);
|
||||
|
||||
// Input 모드 변경 핸들러 - Input 모드로 전환되면 VoiceInputOverlay 닫기
|
||||
const handleInputModeChange = useCallback(
|
||||
@@ -1958,6 +1974,7 @@ const VoiceInputOverlay = ({
|
||||
inputFocused && css.inputFieldWrapperActive
|
||||
)}
|
||||
>
|
||||
{/* ⚠️ [251031] onMouseDown 제거됨 - 마우스 클릭 시 프리징 발생 현상 제거 */}
|
||||
<TInputSimple
|
||||
className={css.inputBox}
|
||||
kind={KINDS.withIcon}
|
||||
|
||||
Reference in New Issue
Block a user