[Voice_search]
- VoiceNotRecognized, VoiceNotRecognizedCircle 피그마에 맞춰 추가. - VoiceListening 스타일 수정
This commit is contained in:
BIN
com.twin.app.shoptime/assets/images/icons/ico_microphone.png
Normal file
BIN
com.twin.app.shoptime/assets/images/icons/ico_microphone.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -1,48 +1,64 @@
|
||||
// src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
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 { useDispatch, useSelector } from 'react-redux';
|
||||
import TInput, { ICONS, KINDS } from '../../../components/TInput/TInput';
|
||||
import TFullPopup from '../../../components/TFullPopup/TFullPopup';
|
||||
import micIcon from '../../../../assets/images/searchpanel/image-mic.png';
|
||||
import { getShopperHouseSearch } from '../../../actions/searchActions';
|
||||
import css from './VoiceInputOverlay.module.less';
|
||||
import VoicePromptScreen from './modes/VoicePromptScreen';
|
||||
import TFullPopup from '../../../components/TFullPopup/TFullPopup';
|
||||
import TInput, {
|
||||
ICONS,
|
||||
KINDS,
|
||||
} from '../../../components/TInput/TInput';
|
||||
import VoiceListening from './modes/VoiceListening';
|
||||
import VoiceNotRecognized from './modes/VoiceNotRecognized';
|
||||
import VoiceNotRecognizedCircle from './modes/VoiceNotRecognizedCircle';
|
||||
import VoicePromptScreen from './modes/VoicePromptScreen';
|
||||
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,
|
||||
}) => {
|
||||
@@ -53,12 +69,17 @@ const VoiceInputOverlay = ({
|
||||
const [currentMode, setCurrentMode] = useState(mode);
|
||||
|
||||
// Redux에서 voice 상태 가져오기
|
||||
const { isRegistered, lastSTTText, sttTimestamp } = useSelector((state) => state.voice);
|
||||
const { isRegistered, lastSTTText, sttTimestamp } = useSelector(
|
||||
(state) => state.voice
|
||||
);
|
||||
|
||||
// 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) {
|
||||
@@ -101,9 +122,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 });
|
||||
@@ -114,7 +135,7 @@ const VoiceInputOverlay = ({
|
||||
|
||||
// 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()));
|
||||
@@ -129,7 +150,7 @@ const VoiceInputOverlay = ({
|
||||
// Input 창에서 엔터키 핸들러
|
||||
const handleInputKeyDown = useCallback(
|
||||
(e) => {
|
||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||
if (e.key === "Enter" || e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
handleSearchSubmit();
|
||||
}
|
||||
@@ -142,19 +163,25 @@ const VoiceInputOverlay = ({
|
||||
switch (currentMode) {
|
||||
case VOICE_MODES.PROMPT:
|
||||
return (
|
||||
<VoicePromptScreen suggestions={suggestions} onSuggestionClick={handleSuggestionClick} />
|
||||
<VoicePromptScreen
|
||||
suggestions={suggestions}
|
||||
onSuggestionClick={handleSuggestionClick}
|
||||
/>
|
||||
);
|
||||
case VOICE_MODES.LISTENING:
|
||||
return <VoiceListening />;
|
||||
case VOICE_MODES.MODE_3:
|
||||
// 추후 MODE_3 컴포넌트 추가
|
||||
return <div>Mode 3 (Coming soon)</div>;
|
||||
return <VoiceNotRecognized />;
|
||||
case VOICE_MODES.MODE_4:
|
||||
// 추후 MODE_4 컴포넌트 추가
|
||||
return <div>Mode 4 (Coming soon)</div>;
|
||||
return <VoiceNotRecognizedCircle />;
|
||||
default:
|
||||
return (
|
||||
<VoicePromptScreen suggestions={suggestions} onSuggestionClick={handleSuggestionClick} />
|
||||
<VoicePromptScreen
|
||||
suggestions={suggestions}
|
||||
onSuggestionClick={handleSuggestionClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -171,7 +198,10 @@ const VoiceInputOverlay = ({
|
||||
// 마이크 버튼 클릭 (모드 전환: 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) {
|
||||
@@ -183,17 +213,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가 자동으로 음성 인식 시작
|
||||
// (이미 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();
|
||||
}
|
||||
},
|
||||
@@ -203,7 +233,7 @@ const VoiceInputOverlay = ({
|
||||
// dim 레이어 클릭 핸들러 (마이크 버튼과 분리)
|
||||
const handleDimClick = useCallback(
|
||||
(e) => {
|
||||
console.log('[VoiceInputOverlay] dimBackground clicked');
|
||||
console.log("[VoiceInputOverlay] dimBackground clicked");
|
||||
onClose();
|
||||
},
|
||||
[onClose]
|
||||
@@ -225,9 +255,17 @@ const VoiceInputOverlay = ({
|
||||
<div className={css.dimBackground} onClick={handleDimClick} />
|
||||
|
||||
{/* Voice 등록 상태 표시 (디버깅용) */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<div style={{ position: 'absolute', top: 10, right: 10, color: '#fff', zIndex: 10000 }}>
|
||||
Voice: {isRegistered ? '✓ Ready' : '✗ Not Ready'}
|
||||
{process.env.NODE_ENV === "development" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
right: 10,
|
||||
color: "#fff",
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
Voice: {isRegistered ? "✓ Ready" : "✗ Not Ready"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -238,7 +276,10 @@ 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}
|
||||
@@ -260,14 +301,18 @@ const VoiceInputOverlay = ({
|
||||
)}
|
||||
onClick={handleMicClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.key === "Enter") {
|
||||
handleMicClick(e);
|
||||
}
|
||||
}}
|
||||
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">
|
||||
@@ -307,7 +352,7 @@ VoiceInputOverlay.propTypes = {
|
||||
VoiceInputOverlay.defaultProps = {
|
||||
mode: VOICE_MODES.PROMPT,
|
||||
suggestions: [],
|
||||
searchQuery: '',
|
||||
searchQuery: "",
|
||||
onSearchChange: null,
|
||||
onSearchSubmit: null,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
align-items: center;
|
||||
pointer-events: none; // 포커스 받지 않음
|
||||
position: relative;
|
||||
margin-top: -210px;
|
||||
}
|
||||
|
||||
.listeningText {
|
||||
@@ -71,7 +72,7 @@
|
||||
.bar2 {
|
||||
width: 510px;
|
||||
left: 0;
|
||||
background: #FFB3B3;
|
||||
background: #ffb3b3;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 1.4s; // 가장 큰 막대 - 마지막
|
||||
opacity: 0; // 애니메이션으로 제어
|
||||
@@ -80,7 +81,7 @@
|
||||
.bar3 {
|
||||
width: 480px;
|
||||
left: 15px;
|
||||
background: #FF8080;
|
||||
background: #ff8080;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 1.2s;
|
||||
opacity: 0;
|
||||
@@ -89,16 +90,16 @@
|
||||
.bar4 {
|
||||
width: 390px;
|
||||
left: 60px;
|
||||
background: #FF6666;
|
||||
background: #ff6666;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 1.0s;
|
||||
animation-delay: 1s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.bar5 {
|
||||
width: 350px;
|
||||
left: 80px;
|
||||
background: #FF4D4D;
|
||||
background: #ff4d4d;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.8s;
|
||||
opacity: 0;
|
||||
@@ -107,7 +108,7 @@
|
||||
.bar6 {
|
||||
width: 320px;
|
||||
left: 95px;
|
||||
background: #FF3333;
|
||||
background: #ff3333;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.6s;
|
||||
opacity: 0;
|
||||
@@ -116,7 +117,7 @@
|
||||
.bar7 {
|
||||
width: 260px;
|
||||
left: 125px;
|
||||
background: #FF1A1A;
|
||||
background: #ff1a1a;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.4s;
|
||||
opacity: 0;
|
||||
@@ -125,7 +126,7 @@
|
||||
.bar8 {
|
||||
width: 200px;
|
||||
left: 155px;
|
||||
background: #FF0000;
|
||||
background: #ff0000;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.2s;
|
||||
opacity: 0;
|
||||
@@ -134,7 +135,7 @@
|
||||
.bar9 {
|
||||
width: 150px;
|
||||
left: 180px;
|
||||
background: #E00000;
|
||||
background: #e00000;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0.1s;
|
||||
opacity: 0;
|
||||
@@ -143,7 +144,7 @@
|
||||
.bar10 {
|
||||
width: 100px;
|
||||
left: 205px;
|
||||
background: #CC0000;
|
||||
background: #cc0000;
|
||||
animation: waveAppear 1.6s ease-in-out infinite;
|
||||
animation-delay: 0s; // 가장 작은 막대 - 처음 시작
|
||||
opacity: 0;
|
||||
@@ -174,7 +175,8 @@
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
33%, 100% {
|
||||
33%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import defaultMicImg
|
||||
from '../../../../../assets/images/icons/ico_microphone.png';
|
||||
import CustomImage from '../../../../components/CustomImage/CustomImage';
|
||||
import css from './VoiceNotRecognized.module.less';
|
||||
|
||||
const VoiceNotRecognized = () => {
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<div className={css.micBox}>
|
||||
<CustomImage src={defaultMicImg} className={css.microPhone} />
|
||||
<span className={css.infoText}>
|
||||
Voice is not recognized. Try again .
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceNotRecognized;
|
||||
@@ -0,0 +1,31 @@
|
||||
@import "../../../../style/CommonStyle.module.less";
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none; // 포커스 받지 않음
|
||||
position: relative;
|
||||
margin-top: -210px;
|
||||
.micBox {
|
||||
width: 634px;
|
||||
height: 276px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
.microPhone {
|
||||
height: 200px;
|
||||
}
|
||||
.infoText {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -1px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
import css from './VoiceNotRecognizedCircle.module.less';
|
||||
|
||||
const VoiceNotRecognizedCircle = () => {
|
||||
return (
|
||||
<div className={css.container}>
|
||||
<div className={css.micBox}>
|
||||
<svg className={css.rippleSvg} width="100" height="100">
|
||||
<circle
|
||||
className={css.rippleCircleBackground}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="47"
|
||||
fill="none"
|
||||
stroke="#C70850"
|
||||
strokeWidth="6"
|
||||
/>
|
||||
|
||||
{/* 애니메이션 원 - 진행 상황 표시 */}
|
||||
<circle
|
||||
className={css.rippleCircle}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="47"
|
||||
fill="none"
|
||||
stroke="#C70850"
|
||||
strokeWidth="6"
|
||||
/>
|
||||
</svg>
|
||||
<span className={css.infoText}>
|
||||
Voice is not recognized. Try again .
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VoiceNotRecognizedCircle;
|
||||
@@ -0,0 +1,53 @@
|
||||
@import "../../../../style/CommonStyle.module.less";
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none; // 포커스 받지 않음
|
||||
position: relative;
|
||||
margin-top: -210px;
|
||||
padding-right: 120xp;
|
||||
.micBox {
|
||||
width: 634px;
|
||||
height: 276px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.infoText {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -1px;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rippleCircleBackground {
|
||||
stroke: rgba(199, 8, 80, 0.2);
|
||||
stroke-width: 6;
|
||||
opacity: 1;
|
||||
}
|
||||
.rippleCircle {
|
||||
stroke-dasharray: 295.3; // 2 * PI * 47 (원의 둘레)
|
||||
stroke-dashoffset: 295.3; // 초기값: 완전히 숨김
|
||||
transform-origin: center;
|
||||
transform: rotate(-90deg); // 12시 방향에서 시작
|
||||
animation: drawCircleReco 2s ease-in-out infinite;
|
||||
background-color: rgba(199, 8, 80, 0.2);
|
||||
}
|
||||
|
||||
@keyframes drawCircleReco {
|
||||
0% {
|
||||
stroke-dashoffset: 295.3; // 점에서 시작
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0; // 원 완성 (계속 시계방향으로)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user