[251016] fix: VoiceInputOverlay Search API Call
🕐 커밋 시간: 2025. 10. 16. 13:18:03 📊 변경 통계: • 총 파일: 5개 • 추가: +112줄 • 삭제: -27줄 📝 수정된 파일: ~ com.twin.app.shoptime/src/reducers/searchReducer.js ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.module.less 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.jsx (javascript): ✅ Added: SearchResultsNew() ❌ Deleted: SearchResultsNew() 🔧 주요 변경 내용: • 핵심 비즈니스 로직 개선
This commit is contained in:
@@ -36,6 +36,9 @@ export const searchReducer = (state = initialState, action) => {
|
||||
totalCount: updatedTotalCount,
|
||||
searchPerformed: true,
|
||||
initPerformed: !action.append,
|
||||
// 일반 검색 시 ShopperHouse 데이터 초기화
|
||||
shopperHouseData: null,
|
||||
shopperHouseSearchId: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,6 +70,11 @@ export const searchReducer = (state = initialState, action) => {
|
||||
...state,
|
||||
shopperHouseData: resultData,
|
||||
shopperHouseSearchId: searchId,
|
||||
// ShopperHouse 검색 시 일반 검색 데이터 초기화
|
||||
searchDatas: {},
|
||||
totalCount: {},
|
||||
searchPerformed: false,
|
||||
initPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
const { searchDatas: searchDatas } = useSelector((state) => state.search);
|
||||
const searchPerformed = useSelector((state) => state.search.searchPerformed);
|
||||
const panels = useSelector((state) => state.panels.panels);
|
||||
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
|
||||
|
||||
const [firstSpot, setFirstSpot] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -249,13 +250,13 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
);
|
||||
|
||||
// 검색 완료 후 결과에 따른 Toast 표시
|
||||
if (searchPerformed && searchQuery.trim()) {
|
||||
if (result > 0) {
|
||||
dispatch(showSearchSuccessToast(searchQuery, result));
|
||||
} else {
|
||||
dispatch(showSearchErrorToast(searchQuery));
|
||||
}
|
||||
}
|
||||
// if (searchPerformed && searchQuery.trim()) {
|
||||
// if (result > 0) {
|
||||
// dispatch(showSearchSuccessToast(searchQuery, result));
|
||||
// } else {
|
||||
// dispatch(showSearchErrorToast(searchQuery));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}, [searchDatas, searchPerformed, searchQuery, dispatch]);
|
||||
|
||||
@@ -617,11 +618,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
|
||||
|
||||
{/* 결과갑 부분 작업중 끝! */}
|
||||
{/* 검색 결과 표시 영역 */}
|
||||
{searchPerformed && searchQuery !== null ? (
|
||||
{(searchPerformed && searchQuery !== null) || shopperHouseData ? (
|
||||
<SearchResultsNew
|
||||
themeInfo={searchDatas.theme}
|
||||
itemInfo={searchDatas.item}
|
||||
showInfo={searchDatas.show}
|
||||
shopperHouseInfo={shopperHouseData}
|
||||
/>
|
||||
) : (
|
||||
<ContainerBasic className={css.contentContainer}>
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import downBtnImg from '../../../assets/images/btn/search_btn_down_arrow.png';
|
||||
import upBtnImg from '../../../assets/images/btn/search_btn_up_arrow.png';
|
||||
import hotPicksImage from '../../../assets/images/searchpanel/img-hotpicks.png';
|
||||
import hotPicksBrandImage
|
||||
from '../../../assets/images/searchpanel/img-search-hotpicks.png';
|
||||
import hotPicksBrandImage from '../../../assets/images/searchpanel/img-search-hotpicks.png';
|
||||
import CustomImage from '../../components/CustomImage/CustomImage';
|
||||
import TButtonTab, { LIST_TYPE } from '../../components/TButtonTab/TButtonTab';
|
||||
import TDropDown from '../../components/TDropDown/TDropDown';
|
||||
import TVirtualGridList
|
||||
from '../../components/TVirtualGridList/TVirtualGridList';
|
||||
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
|
||||
import { $L } from '../../utils/helperMethods';
|
||||
import { SpotlightIds } from '../../utils/SpotlightIds';
|
||||
import css from './SearchResults.new.module.less';
|
||||
@@ -30,9 +22,28 @@ import ShowCard from './SearchResultsNew/ShowCard';
|
||||
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
|
||||
const SearchResultsNew = ({ itemInfo, showInfo, themeInfo }) => {
|
||||
const SearchResultsNew = ({ itemInfo, showInfo, themeInfo, shopperHouseInfo }) => {
|
||||
// ShopperHouse 데이터를 ItemCard 형식으로 변환
|
||||
const convertedShopperHouseItems = useMemo(() => {
|
||||
if (!shopperHouseInfo || !shopperHouseInfo.results || shopperHouseInfo.results.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const docs = shopperHouseInfo.results[0].docs || [];
|
||||
return docs.map((doc, index) => ({
|
||||
thumbnail: doc.thumbNail || doc.imgPath || '', // 이미지 경로
|
||||
title: doc.title || doc.prdtName || '', // 제목
|
||||
dcPrice: doc.dcPrice || doc.price || '', // 할인가격
|
||||
price: doc.orgPrice || doc.price || '', // 원가
|
||||
soldout: doc.soldout || false, // 품절 여부
|
||||
contentId: `shopperhouse_${doc.partnerName}_${doc.prdtId || index}_${doc.partnerId || ''}_${doc.partnerId || ''}_${doc.prdtId || index}`, // contentId 생성
|
||||
reviewGrade: doc.reviewGrade || '', // 리뷰 점수 (추가 정보)
|
||||
partnerName: doc.partnerName || '', // 파트너 이름
|
||||
}));
|
||||
}, [shopperHouseInfo]);
|
||||
const getButtonTabList = () => {
|
||||
const itemLength = itemInfo?.length || 0;
|
||||
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용
|
||||
const itemLength = convertedShopperHouseItems?.length || itemInfo?.length || 0;
|
||||
const showLength = showInfo?.length || 0;
|
||||
|
||||
return [
|
||||
@@ -58,8 +69,8 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo }) => {
|
||||
buttonTabList = getButtonTabList();
|
||||
}
|
||||
|
||||
// 현재 탭의 데이터 가져오기
|
||||
const currentData = tab === 0 ? itemInfo : showInfo;
|
||||
// 현재 탭의 데이터 가져오기 - ShopperHouse 데이터 우선
|
||||
const currentData = tab === 0 ? convertedShopperHouseItems || itemInfo : showInfo;
|
||||
|
||||
// 표시할 데이터 (처음부터 visibleCount 개수만큼)
|
||||
const displayedData = useMemo(() => {
|
||||
@@ -107,8 +118,8 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo }) => {
|
||||
[dropDownTab]
|
||||
);
|
||||
|
||||
const SpottableLi = Spottable("li");
|
||||
const SpottableDiv = Spottable("div");
|
||||
const SpottableLi = Spottable('li');
|
||||
const SpottableDiv = Spottable('div');
|
||||
|
||||
// 맨 처음으로 이동 (위 버튼)
|
||||
const upBtnClick = () => {
|
||||
@@ -127,8 +138,7 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo }) => {
|
||||
// ProductCard 컴포넌트
|
||||
const renderItem = useCallback(
|
||||
({ index, ...rest }) => {
|
||||
const { bgImgPath, title, partnerLogo, partnerName, keyword } =
|
||||
themeInfo[index];
|
||||
const { bgImgPath, title, partnerLogo, partnerName, keyword } = themeInfo[index];
|
||||
return (
|
||||
<SpottableDiv
|
||||
key={`searchProduct-${index}`}
|
||||
@@ -141,11 +151,7 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo }) => {
|
||||
</div>
|
||||
<div className={css.productInfo}>
|
||||
<div className={css.productBrandWrapper}>
|
||||
<img
|
||||
src={partnerLogo}
|
||||
alt={partnerName}
|
||||
className={css.brandLogo}
|
||||
/>
|
||||
<img src={partnerLogo} alt={partnerName} className={css.brandLogo} />
|
||||
</div>
|
||||
<div className={css.productDetails}>
|
||||
{keyword && (
|
||||
@@ -178,13 +184,11 @@ const SearchResultsNew = ({ itemInfo, showInfo, themeInfo }) => {
|
||||
<div
|
||||
className={css.hotpicksSection}
|
||||
data-wheel-point={true}
|
||||
spotlightId={"hot-picks-section"}
|
||||
spotlightId={'hot-picks-section'}
|
||||
>
|
||||
<div className={css.sectionHeader}>
|
||||
<div className={css.sectionIndicator}></div>
|
||||
<div className={css.sectionTitle}>
|
||||
Hot Picks ({themeInfo?.length})
|
||||
</div>
|
||||
<div className={css.sectionTitle}>Hot Picks ({themeInfo?.length})</div>
|
||||
</div>
|
||||
<div className={css.productList}>
|
||||
<TVirtualGridList
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
// src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
useDispatch,
|
||||
useSelector,
|
||||
} from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import Spotlight from '@enact/spotlight';
|
||||
import SpotlightContainerDecorator
|
||||
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
||||
import Spottable from '@enact/spotlight/Spottable';
|
||||
|
||||
import micIcon from '../../../../assets/images/searchpanel/image-mic.png';
|
||||
import { getShopperHouseSearch } from '../../../actions/searchActions';
|
||||
import TFullPopup from '../../../components/TFullPopup/TFullPopup';
|
||||
import TInput, {
|
||||
ICONS,
|
||||
KINDS,
|
||||
} from '../../../components/TInput/TInput';
|
||||
import TInput, { ICONS, KINDS } from '../../../components/TInput/TInput';
|
||||
import VoiceListening from './modes/VoiceListening';
|
||||
import VoiceNotRecognized from './modes/VoiceNotRecognized';
|
||||
import VoiceNotRecognizedCircle from './modes/VoiceNotRecognizedCircle';
|
||||
@@ -33,53 +21,67 @@ import css from './VoiceInputOverlay.module.less';
|
||||
|
||||
const OverlayContainer = SpotlightContainerDecorator(
|
||||
{
|
||||
enterTo: "default-element",
|
||||
restrict: "self-only", // 포커스를 overlay 내부로만 제한
|
||||
enterTo: 'default-element',
|
||||
restrict: 'self-only', // 포커스를 overlay 내부로만 제한
|
||||
},
|
||||
"div"
|
||||
'div'
|
||||
);
|
||||
|
||||
const SpottableMicButton = Spottable("div");
|
||||
const SpottableMicButton = Spottable('div');
|
||||
|
||||
// Voice overlay 모드 상수
|
||||
export const VOICE_MODES = {
|
||||
PROMPT: "prompt", // Try saying 화면
|
||||
LISTENING: "listening", // 듣는 중 화면
|
||||
MODE_3: "mode3", // 추후 추가
|
||||
MODE_4: "mode4", // 추후 추가
|
||||
PROMPT: 'prompt', // Try saying 화면
|
||||
LISTENING: 'listening', // 듣는 중 화면
|
||||
MODE_3: 'mode3', // 추후 추가
|
||||
MODE_4: 'mode4', // 추후 추가
|
||||
};
|
||||
|
||||
const OVERLAY_SPOTLIGHT_ID = "voice-input-overlay-container";
|
||||
const INPUT_SPOTLIGHT_ID = "voice-overlay-input-box";
|
||||
const MIC_SPOTLIGHT_ID = "voice-overlay-mic-button";
|
||||
const OVERLAY_SPOTLIGHT_ID = 'voice-input-overlay-container';
|
||||
const INPUT_SPOTLIGHT_ID = 'voice-overlay-input-box';
|
||||
const MIC_SPOTLIGHT_ID = 'voice-overlay-mic-button';
|
||||
|
||||
const VoiceInputOverlay = ({
|
||||
isVisible,
|
||||
onClose,
|
||||
mode = VOICE_MODES.PROMPT,
|
||||
suggestions = [],
|
||||
searchQuery = "",
|
||||
searchQuery = '',
|
||||
onSearchChange,
|
||||
onSearchSubmit,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const lastFocusedElement = useRef(null);
|
||||
const [inputFocus, setInputFocus] = useState(false);
|
||||
const [micFocused, setMicFocused] = useState(false);
|
||||
// 내부 모드 상태 관리 (prompt -> listening -> close)
|
||||
const [currentMode, setCurrentMode] = useState(mode);
|
||||
|
||||
// Redux에서 voice 상태 가져오기
|
||||
const { isRegistered, lastSTTText, sttTimestamp } = useSelector(
|
||||
(state) => state.voice
|
||||
);
|
||||
const { isRegistered, lastSTTText, sttTimestamp } = useSelector((state) => state.voice);
|
||||
|
||||
// Redux에서 shopperHouse 검색 결과 가져오기
|
||||
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
|
||||
const shopperHouseDataRef = useRef(shopperHouseData);
|
||||
|
||||
// ShopperHouse API 응답 수신 시 overlay 닫기
|
||||
useEffect(() => {
|
||||
// 이전 값과 비교하여 새로운 데이터가 들어왔을 때만 닫기
|
||||
if (isVisible && shopperHouseData && shopperHouseData !== shopperHouseDataRef.current) {
|
||||
console.log('[VoiceInputOverlay] ShopperHouse data received, closing overlay');
|
||||
shopperHouseDataRef.current = shopperHouseData;
|
||||
|
||||
// 약간의 지연 후 닫기 (사용자가 결과를 인지할 수 있도록)
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 500);
|
||||
}
|
||||
}, [shopperHouseData, isVisible, onClose]);
|
||||
|
||||
// STT 텍스트 수신 시 처리
|
||||
useEffect(() => {
|
||||
if (lastSTTText && sttTimestamp && isVisible) {
|
||||
console.log(
|
||||
"[VoiceInputOverlay] STT text received in overlay:",
|
||||
lastSTTText
|
||||
);
|
||||
console.log('[VoiceInputOverlay] STT text received in overlay:', lastSTTText);
|
||||
|
||||
// 입력창에 텍스트 표시 (부모 컴포넌트로 전달)
|
||||
if (onSearchChange) {
|
||||
@@ -122,9 +124,9 @@ const VoiceInputOverlay = ({
|
||||
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트만 설정
|
||||
const handleSuggestionClick = useCallback(
|
||||
(suggestion) => {
|
||||
console.log("[VoiceInputOverlay] Suggestion clicked:", suggestion);
|
||||
console.log('[VoiceInputOverlay] Suggestion clicked:', suggestion);
|
||||
// 따옴표 제거
|
||||
const query = suggestion.replace(/^["']|["']$/g, "").trim();
|
||||
const query = suggestion.replace(/^["']|["']$/g, '').trim();
|
||||
// Input 창에 텍스트 설정
|
||||
if (onSearchChange) {
|
||||
onSearchChange({ value: query });
|
||||
@@ -133,40 +135,45 @@ const VoiceInputOverlay = ({
|
||||
[onSearchChange]
|
||||
);
|
||||
|
||||
// Input 창에서 API 호출 핸들러 (엔터키 또는 돋보기 아이콘 클릭)
|
||||
// Input 창에서 API 호출 핸들러 (돋보기 아이콘 클릭 시에만)
|
||||
const handleSearchSubmit = useCallback(() => {
|
||||
console.log("[VoiceInputOverlay] Search submit:", searchQuery);
|
||||
console.log('[VoiceInputOverlay] Search submit:', searchQuery);
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
// ShopperHouse API 호출
|
||||
dispatch(getShopperHouseSearch(searchQuery.trim()));
|
||||
|
||||
// Input 내용 비우기
|
||||
if (onSearchChange) {
|
||||
onSearchChange({ value: '' });
|
||||
}
|
||||
|
||||
// API 호출 후 Input 박스로 포커스 이동
|
||||
setTimeout(() => {
|
||||
Spotlight.focus(INPUT_SPOTLIGHT_ID);
|
||||
}, 100);
|
||||
|
||||
// VoiceInputOverlay는 SearchPanel과 다른 API를 사용하므로 onSearchSubmit 호출 안 함
|
||||
// if (onSearchSubmit) {
|
||||
// onSearchSubmit(searchQuery);
|
||||
// }
|
||||
}
|
||||
}, [dispatch, searchQuery]);
|
||||
}, [dispatch, searchQuery, onSearchChange]);
|
||||
|
||||
// Input 창에서 엔터키 핸들러
|
||||
const handleInputKeyDown = useCallback(
|
||||
(e) => {
|
||||
if (e.key === "Enter" || e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
handleSearchSubmit();
|
||||
}
|
||||
},
|
||||
[handleSearchSubmit]
|
||||
);
|
||||
// Input 창에서 엔터키 핸들러 (API 호출하지 않음)
|
||||
const handleInputKeyDown = useCallback((e) => {
|
||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
// Enter 키로는 API 호출하지 않음
|
||||
// 돋보기 아이콘 클릭/Enter로만 API 호출
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 모드에 따른 컨텐츠 렌더링
|
||||
const renderModeContent = () => {
|
||||
switch (currentMode) {
|
||||
case VOICE_MODES.PROMPT:
|
||||
return (
|
||||
<VoicePromptScreen
|
||||
suggestions={suggestions}
|
||||
onSuggestionClick={handleSuggestionClick}
|
||||
/>
|
||||
<VoicePromptScreen suggestions={suggestions} onSuggestionClick={handleSuggestionClick} />
|
||||
);
|
||||
case VOICE_MODES.LISTENING:
|
||||
return <VoiceListening />;
|
||||
@@ -178,10 +185,7 @@ const VoiceInputOverlay = ({
|
||||
return <VoiceNotRecognizedCircle />;
|
||||
default:
|
||||
return (
|
||||
<VoicePromptScreen
|
||||
suggestions={suggestions}
|
||||
onSuggestionClick={handleSuggestionClick}
|
||||
/>
|
||||
<VoicePromptScreen suggestions={suggestions} onSuggestionClick={handleSuggestionClick} />
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -195,13 +199,19 @@ const VoiceInputOverlay = ({
|
||||
setInputFocus(false);
|
||||
}, []);
|
||||
|
||||
// 마이크 버튼 포커스 핸들러
|
||||
const handleMicFocus = useCallback(() => {
|
||||
setMicFocused(true);
|
||||
}, []);
|
||||
|
||||
const handleMicBlur = useCallback(() => {
|
||||
setMicFocused(false);
|
||||
}, []);
|
||||
|
||||
// 마이크 버튼 클릭 (모드 전환: prompt -> listening -> close)
|
||||
const handleMicClick = useCallback(
|
||||
(e) => {
|
||||
console.log(
|
||||
"[VoiceInputOverlay] handleMicClick called, currentMode:",
|
||||
currentMode
|
||||
);
|
||||
console.log('[VoiceInputOverlay] handleMicClick called, currentMode:', currentMode);
|
||||
|
||||
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
||||
if (e && e.stopPropagation) {
|
||||
@@ -213,17 +223,17 @@ const VoiceInputOverlay = ({
|
||||
|
||||
if (currentMode === VOICE_MODES.PROMPT) {
|
||||
// prompt 모드에서 클릭 시 -> listening 모드로 전환
|
||||
console.log("[VoiceInputOverlay] Switching to LISTENING mode");
|
||||
console.log('[VoiceInputOverlay] Switching to LISTENING mode');
|
||||
setCurrentMode(VOICE_MODES.LISTENING);
|
||||
// 이 시점에서 webOS Voice Framework가 자동으로 음성 인식 시작
|
||||
// 이 시점에서 webOS Voice Framework가 자동으로 음성인식 시작
|
||||
// (이미 registerVoiceFramework()로 등록되어 있으므로)
|
||||
} else if (currentMode === VOICE_MODES.LISTENING) {
|
||||
// listening 모드에서 클릭 시 -> 종료
|
||||
console.log("[VoiceInputOverlay] Closing from LISTENING mode");
|
||||
console.log('[VoiceInputOverlay] Closing from LISTENING mode');
|
||||
onClose();
|
||||
} else {
|
||||
// 기타 모드에서는 바로 종료
|
||||
console.log("[VoiceInputOverlay] Closing from other mode");
|
||||
console.log('[VoiceInputOverlay] Closing from other mode');
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
@@ -233,7 +243,7 @@ const VoiceInputOverlay = ({
|
||||
// dim 레이어 클릭 핸들러 (마이크 버튼과 분리)
|
||||
const handleDimClick = useCallback(
|
||||
(e) => {
|
||||
console.log("[VoiceInputOverlay] dimBackground clicked");
|
||||
console.log('[VoiceInputOverlay] dimBackground clicked');
|
||||
onClose();
|
||||
},
|
||||
[onClose]
|
||||
@@ -255,17 +265,17 @@ const VoiceInputOverlay = ({
|
||||
<div className={css.dimBackground} onClick={handleDimClick} />
|
||||
|
||||
{/* Voice 등록 상태 표시 (디버깅용) */}
|
||||
{process.env.NODE_ENV === "development" && (
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
color: "#fff",
|
||||
color: '#fff',
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
Voice: {isRegistered ? "✓ Ready" : "✗ Not Ready"}
|
||||
Voice: {isRegistered ? '✓ Ready' : '✗ Not Ready'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -276,10 +286,7 @@ const VoiceInputOverlay = ({
|
||||
spotlightDisabled={!isVisible}
|
||||
>
|
||||
{/* 입력창과 마이크 버튼 - SearchPanel.inputContainer와 동일한 구조 */}
|
||||
<div
|
||||
className={css.inputWrapper}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className={css.inputWrapper} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={css.searchInputWrapper}>
|
||||
<TInput
|
||||
className={css.inputBox}
|
||||
@@ -297,22 +304,23 @@ const VoiceInputOverlay = ({
|
||||
className={classNames(
|
||||
css.microphoneButton,
|
||||
css.active,
|
||||
currentMode === VOICE_MODES.LISTENING && css.listening
|
||||
currentMode === VOICE_MODES.LISTENING && css.listening,
|
||||
micFocused && css.focused
|
||||
)}
|
||||
onClick={handleMicClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleMicClick(e);
|
||||
}
|
||||
}}
|
||||
onFocus={handleMicFocus}
|
||||
onBlur={handleMicBlur}
|
||||
spotlightId={MIC_SPOTLIGHT_ID}
|
||||
>
|
||||
<div className={css.microphoneCircle}>
|
||||
<img
|
||||
src={micIcon}
|
||||
alt="Microphone"
|
||||
className={css.microphoneIcon}
|
||||
/>
|
||||
<img src={micIcon} alt="Microphone" className={css.microphoneIcon} />
|
||||
</div>
|
||||
{currentMode === VOICE_MODES.LISTENING && (
|
||||
<svg className={css.rippleSvg} width="100" height="100">
|
||||
@@ -352,7 +360,7 @@ VoiceInputOverlay.propTypes = {
|
||||
VoiceInputOverlay.defaultProps = {
|
||||
mode: VOICE_MODES.PROMPT,
|
||||
suggestions: [],
|
||||
searchQuery: "",
|
||||
searchQuery: '',
|
||||
onSearchChange: null,
|
||||
onSearchSubmit: null,
|
||||
};
|
||||
|
||||
@@ -155,16 +155,12 @@
|
||||
|
||||
&:hover {
|
||||
.microphoneCircle {
|
||||
border-color: @PRIMARY_COLOR_RED;
|
||||
// border-color: @PRIMARY_COLOR_RED;
|
||||
border-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.microphoneCircle {
|
||||
border-color: @PRIMARY_COLOR_RED;
|
||||
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// active 상태 (음성 입력 모드 - 항상 빨간색)
|
||||
&.active {
|
||||
@@ -179,6 +175,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.active.focused {
|
||||
.microphoneCircle {
|
||||
background-color: @PRIMARY_COLOR_RED;
|
||||
border-color: white;
|
||||
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// listening 상태 (배경 투명, 테두리 ripple 애니메이션)
|
||||
&.listening {
|
||||
.microphoneCircle {
|
||||
|
||||
Reference in New Issue
Block a user