[251031] fix: resolve merge conflicts in voice input overlay and search components

This commit is contained in:
2025-10-31 10:19:40 +09:00
parent 7912d0f1b3
commit 0b1d435e62
6 changed files with 311 additions and 20 deletions

View File

@@ -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',

View File

@@ -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 완료');
};

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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}