[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
|
// src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, {
|
||||||
import PropTypes from 'prop-types';
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
useDispatch,
|
||||||
|
useSelector,
|
||||||
|
} from 'react-redux';
|
||||||
|
|
||||||
import Spotlight from '@enact/spotlight';
|
import Spotlight from '@enact/spotlight';
|
||||||
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
|
import SpotlightContainerDecorator
|
||||||
|
from '@enact/spotlight/SpotlightContainerDecorator';
|
||||||
import Spottable from '@enact/spotlight/Spottable';
|
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 micIcon from '../../../../assets/images/searchpanel/image-mic.png';
|
||||||
import { getShopperHouseSearch } from '../../../actions/searchActions';
|
import { getShopperHouseSearch } from '../../../actions/searchActions';
|
||||||
import css from './VoiceInputOverlay.module.less';
|
import TFullPopup from '../../../components/TFullPopup/TFullPopup';
|
||||||
import VoicePromptScreen from './modes/VoicePromptScreen';
|
import TInput, {
|
||||||
|
ICONS,
|
||||||
|
KINDS,
|
||||||
|
} from '../../../components/TInput/TInput';
|
||||||
import VoiceListening from './modes/VoiceListening';
|
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(
|
const OverlayContainer = SpotlightContainerDecorator(
|
||||||
{
|
{
|
||||||
enterTo: 'default-element',
|
enterTo: "default-element",
|
||||||
restrict: 'self-only', // 포커스를 overlay 내부로만 제한
|
restrict: "self-only", // 포커스를 overlay 내부로만 제한
|
||||||
},
|
},
|
||||||
'div'
|
"div"
|
||||||
);
|
);
|
||||||
|
|
||||||
const SpottableMicButton = Spottable('div');
|
const SpottableMicButton = Spottable("div");
|
||||||
|
|
||||||
// Voice overlay 모드 상수
|
// Voice overlay 모드 상수
|
||||||
export const VOICE_MODES = {
|
export const VOICE_MODES = {
|
||||||
PROMPT: 'prompt', // Try saying 화면
|
PROMPT: "prompt", // Try saying 화면
|
||||||
LISTENING: 'listening', // 듣는 중 화면
|
LISTENING: "listening", // 듣는 중 화면
|
||||||
MODE_3: 'mode3', // 추후 추가
|
MODE_3: "mode3", // 추후 추가
|
||||||
MODE_4: 'mode4', // 추후 추가
|
MODE_4: "mode4", // 추후 추가
|
||||||
};
|
};
|
||||||
|
|
||||||
const OVERLAY_SPOTLIGHT_ID = 'voice-input-overlay-container';
|
const OVERLAY_SPOTLIGHT_ID = "voice-input-overlay-container";
|
||||||
const INPUT_SPOTLIGHT_ID = 'voice-overlay-input-box';
|
const INPUT_SPOTLIGHT_ID = "voice-overlay-input-box";
|
||||||
const MIC_SPOTLIGHT_ID = 'voice-overlay-mic-button';
|
const MIC_SPOTLIGHT_ID = "voice-overlay-mic-button";
|
||||||
|
|
||||||
const VoiceInputOverlay = ({
|
const VoiceInputOverlay = ({
|
||||||
isVisible,
|
isVisible,
|
||||||
onClose,
|
onClose,
|
||||||
mode = VOICE_MODES.PROMPT,
|
mode = VOICE_MODES.PROMPT,
|
||||||
suggestions = [],
|
suggestions = [],
|
||||||
searchQuery = '',
|
searchQuery = "",
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSearchSubmit,
|
onSearchSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -53,12 +69,17 @@ const VoiceInputOverlay = ({
|
|||||||
const [currentMode, setCurrentMode] = useState(mode);
|
const [currentMode, setCurrentMode] = useState(mode);
|
||||||
|
|
||||||
// Redux에서 voice 상태 가져오기
|
// Redux에서 voice 상태 가져오기
|
||||||
const { isRegistered, lastSTTText, sttTimestamp } = useSelector((state) => state.voice);
|
const { isRegistered, lastSTTText, sttTimestamp } = useSelector(
|
||||||
|
(state) => state.voice
|
||||||
|
);
|
||||||
|
|
||||||
// STT 텍스트 수신 시 처리
|
// STT 텍스트 수신 시 처리
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastSTTText && sttTimestamp && isVisible) {
|
if (lastSTTText && sttTimestamp && isVisible) {
|
||||||
console.log('[VoiceInputOverlay] STT text received in overlay:', lastSTTText);
|
console.log(
|
||||||
|
"[VoiceInputOverlay] STT text received in overlay:",
|
||||||
|
lastSTTText
|
||||||
|
);
|
||||||
|
|
||||||
// 입력창에 텍스트 표시 (부모 컴포넌트로 전달)
|
// 입력창에 텍스트 표시 (부모 컴포넌트로 전달)
|
||||||
if (onSearchChange) {
|
if (onSearchChange) {
|
||||||
@@ -101,9 +122,9 @@ const VoiceInputOverlay = ({
|
|||||||
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트만 설정
|
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트만 설정
|
||||||
const handleSuggestionClick = useCallback(
|
const handleSuggestionClick = useCallback(
|
||||||
(suggestion) => {
|
(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 창에 텍스트 설정
|
// Input 창에 텍스트 설정
|
||||||
if (onSearchChange) {
|
if (onSearchChange) {
|
||||||
onSearchChange({ value: query });
|
onSearchChange({ value: query });
|
||||||
@@ -114,7 +135,7 @@ const VoiceInputOverlay = ({
|
|||||||
|
|
||||||
// Input 창에서 API 호출 핸들러 (엔터키 또는 돋보기 아이콘 클릭)
|
// Input 창에서 API 호출 핸들러 (엔터키 또는 돋보기 아이콘 클릭)
|
||||||
const handleSearchSubmit = useCallback(() => {
|
const handleSearchSubmit = useCallback(() => {
|
||||||
console.log('[VoiceInputOverlay] Search submit:', searchQuery);
|
console.log("[VoiceInputOverlay] Search submit:", searchQuery);
|
||||||
if (searchQuery && searchQuery.trim()) {
|
if (searchQuery && searchQuery.trim()) {
|
||||||
// ShopperHouse API 호출
|
// ShopperHouse API 호출
|
||||||
dispatch(getShopperHouseSearch(searchQuery.trim()));
|
dispatch(getShopperHouseSearch(searchQuery.trim()));
|
||||||
@@ -129,7 +150,7 @@ const VoiceInputOverlay = ({
|
|||||||
// Input 창에서 엔터키 핸들러
|
// Input 창에서 엔터키 핸들러
|
||||||
const handleInputKeyDown = useCallback(
|
const handleInputKeyDown = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
if (e.key === "Enter" || e.keyCode === 13) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSearchSubmit();
|
handleSearchSubmit();
|
||||||
}
|
}
|
||||||
@@ -142,19 +163,25 @@ const VoiceInputOverlay = ({
|
|||||||
switch (currentMode) {
|
switch (currentMode) {
|
||||||
case VOICE_MODES.PROMPT:
|
case VOICE_MODES.PROMPT:
|
||||||
return (
|
return (
|
||||||
<VoicePromptScreen suggestions={suggestions} onSuggestionClick={handleSuggestionClick} />
|
<VoicePromptScreen
|
||||||
|
suggestions={suggestions}
|
||||||
|
onSuggestionClick={handleSuggestionClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
case VOICE_MODES.LISTENING:
|
case VOICE_MODES.LISTENING:
|
||||||
return <VoiceListening />;
|
return <VoiceListening />;
|
||||||
case VOICE_MODES.MODE_3:
|
case VOICE_MODES.MODE_3:
|
||||||
// 추후 MODE_3 컴포넌트 추가
|
// 추후 MODE_3 컴포넌트 추가
|
||||||
return <div>Mode 3 (Coming soon)</div>;
|
return <VoiceNotRecognized />;
|
||||||
case VOICE_MODES.MODE_4:
|
case VOICE_MODES.MODE_4:
|
||||||
// 추후 MODE_4 컴포넌트 추가
|
// 추후 MODE_4 컴포넌트 추가
|
||||||
return <div>Mode 4 (Coming soon)</div>;
|
return <VoiceNotRecognizedCircle />;
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<VoicePromptScreen suggestions={suggestions} onSuggestionClick={handleSuggestionClick} />
|
<VoicePromptScreen
|
||||||
|
suggestions={suggestions}
|
||||||
|
onSuggestionClick={handleSuggestionClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -171,7 +198,10 @@ const VoiceInputOverlay = ({
|
|||||||
// 마이크 버튼 클릭 (모드 전환: prompt -> listening -> close)
|
// 마이크 버튼 클릭 (모드 전환: prompt -> listening -> close)
|
||||||
const handleMicClick = useCallback(
|
const handleMicClick = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
console.log('[VoiceInputOverlay] handleMicClick called, currentMode:', currentMode);
|
console.log(
|
||||||
|
"[VoiceInputOverlay] handleMicClick called, currentMode:",
|
||||||
|
currentMode
|
||||||
|
);
|
||||||
|
|
||||||
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
||||||
if (e && e.stopPropagation) {
|
if (e && e.stopPropagation) {
|
||||||
@@ -183,17 +213,17 @@ const VoiceInputOverlay = ({
|
|||||||
|
|
||||||
if (currentMode === VOICE_MODES.PROMPT) {
|
if (currentMode === VOICE_MODES.PROMPT) {
|
||||||
// prompt 모드에서 클릭 시 -> listening 모드로 전환
|
// prompt 모드에서 클릭 시 -> listening 모드로 전환
|
||||||
console.log('[VoiceInputOverlay] Switching to LISTENING mode');
|
console.log("[VoiceInputOverlay] Switching to LISTENING mode");
|
||||||
setCurrentMode(VOICE_MODES.LISTENING);
|
setCurrentMode(VOICE_MODES.LISTENING);
|
||||||
// 이 시점에서 webOS Voice Framework가 자동으로 음성 인식 시작
|
// 이 시점에서 webOS Voice Framework가 자동으로 음성 인식 시작
|
||||||
// (이미 registerVoiceFramework()로 등록되어 있으므로)
|
// (이미 registerVoiceFramework()로 등록되어 있으므로)
|
||||||
} else if (currentMode === VOICE_MODES.LISTENING) {
|
} else if (currentMode === VOICE_MODES.LISTENING) {
|
||||||
// listening 모드에서 클릭 시 -> 종료
|
// listening 모드에서 클릭 시 -> 종료
|
||||||
console.log('[VoiceInputOverlay] Closing from LISTENING mode');
|
console.log("[VoiceInputOverlay] Closing from LISTENING mode");
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
// 기타 모드에서는 바로 종료
|
// 기타 모드에서는 바로 종료
|
||||||
console.log('[VoiceInputOverlay] Closing from other mode');
|
console.log("[VoiceInputOverlay] Closing from other mode");
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -203,7 +233,7 @@ const VoiceInputOverlay = ({
|
|||||||
// dim 레이어 클릭 핸들러 (마이크 버튼과 분리)
|
// dim 레이어 클릭 핸들러 (마이크 버튼과 분리)
|
||||||
const handleDimClick = useCallback(
|
const handleDimClick = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
console.log('[VoiceInputOverlay] dimBackground clicked');
|
console.log("[VoiceInputOverlay] dimBackground clicked");
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
[onClose]
|
[onClose]
|
||||||
@@ -225,9 +255,17 @@ const VoiceInputOverlay = ({
|
|||||||
<div className={css.dimBackground} onClick={handleDimClick} />
|
<div className={css.dimBackground} onClick={handleDimClick} />
|
||||||
|
|
||||||
{/* Voice 등록 상태 표시 (디버깅용) */}
|
{/* Voice 등록 상태 표시 (디버깅용) */}
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === "development" && (
|
||||||
<div style={{ position: 'absolute', top: 10, right: 10, color: '#fff', zIndex: 10000 }}>
|
<div
|
||||||
Voice: {isRegistered ? '✓ Ready' : '✗ Not Ready'}
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
color: "#fff",
|
||||||
|
zIndex: 10000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Voice: {isRegistered ? "✓ Ready" : "✗ Not Ready"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -238,7 +276,10 @@ const VoiceInputOverlay = ({
|
|||||||
spotlightDisabled={!isVisible}
|
spotlightDisabled={!isVisible}
|
||||||
>
|
>
|
||||||
{/* 입력창과 마이크 버튼 - SearchPanel.inputContainer와 동일한 구조 */}
|
{/* 입력창과 마이크 버튼 - SearchPanel.inputContainer와 동일한 구조 */}
|
||||||
<div className={css.inputWrapper} onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
|
className={css.inputWrapper}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
<div className={css.searchInputWrapper}>
|
<div className={css.searchInputWrapper}>
|
||||||
<TInput
|
<TInput
|
||||||
className={css.inputBox}
|
className={css.inputBox}
|
||||||
@@ -260,14 +301,18 @@ const VoiceInputOverlay = ({
|
|||||||
)}
|
)}
|
||||||
onClick={handleMicClick}
|
onClick={handleMicClick}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
handleMicClick(e);
|
handleMicClick(e);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
spotlightId={MIC_SPOTLIGHT_ID}
|
spotlightId={MIC_SPOTLIGHT_ID}
|
||||||
>
|
>
|
||||||
<div className={css.microphoneCircle}>
|
<div className={css.microphoneCircle}>
|
||||||
<img src={micIcon} alt="Microphone" className={css.microphoneIcon} />
|
<img
|
||||||
|
src={micIcon}
|
||||||
|
alt="Microphone"
|
||||||
|
className={css.microphoneIcon}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{currentMode === VOICE_MODES.LISTENING && (
|
{currentMode === VOICE_MODES.LISTENING && (
|
||||||
<svg className={css.rippleSvg} width="100" height="100">
|
<svg className={css.rippleSvg} width="100" height="100">
|
||||||
@@ -307,7 +352,7 @@ VoiceInputOverlay.propTypes = {
|
|||||||
VoiceInputOverlay.defaultProps = {
|
VoiceInputOverlay.defaultProps = {
|
||||||
mode: VOICE_MODES.PROMPT,
|
mode: VOICE_MODES.PROMPT,
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
searchQuery: '',
|
searchQuery: "",
|
||||||
onSearchChange: null,
|
onSearchChange: null,
|
||||||
onSearchSubmit: null,
|
onSearchSubmit: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
pointer-events: none; // 포커스 받지 않음
|
pointer-events: none; // 포커스 받지 않음
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-top: -210px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.listeningText {
|
.listeningText {
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
.bar2 {
|
.bar2 {
|
||||||
width: 510px;
|
width: 510px;
|
||||||
left: 0;
|
left: 0;
|
||||||
background: #FFB3B3;
|
background: #ffb3b3;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 1.4s; // 가장 큰 막대 - 마지막
|
animation-delay: 1.4s; // 가장 큰 막대 - 마지막
|
||||||
opacity: 0; // 애니메이션으로 제어
|
opacity: 0; // 애니메이션으로 제어
|
||||||
@@ -80,7 +81,7 @@
|
|||||||
.bar3 {
|
.bar3 {
|
||||||
width: 480px;
|
width: 480px;
|
||||||
left: 15px;
|
left: 15px;
|
||||||
background: #FF8080;
|
background: #ff8080;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 1.2s;
|
animation-delay: 1.2s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -89,16 +90,16 @@
|
|||||||
.bar4 {
|
.bar4 {
|
||||||
width: 390px;
|
width: 390px;
|
||||||
left: 60px;
|
left: 60px;
|
||||||
background: #FF6666;
|
background: #ff6666;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 1.0s;
|
animation-delay: 1s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar5 {
|
.bar5 {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
left: 80px;
|
left: 80px;
|
||||||
background: #FF4D4D;
|
background: #ff4d4d;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 0.8s;
|
animation-delay: 0.8s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -107,7 +108,7 @@
|
|||||||
.bar6 {
|
.bar6 {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
left: 95px;
|
left: 95px;
|
||||||
background: #FF3333;
|
background: #ff3333;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 0.6s;
|
animation-delay: 0.6s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
.bar7 {
|
.bar7 {
|
||||||
width: 260px;
|
width: 260px;
|
||||||
left: 125px;
|
left: 125px;
|
||||||
background: #FF1A1A;
|
background: #ff1a1a;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 0.4s;
|
animation-delay: 0.4s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -125,7 +126,7 @@
|
|||||||
.bar8 {
|
.bar8 {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
left: 155px;
|
left: 155px;
|
||||||
background: #FF0000;
|
background: #ff0000;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 0.2s;
|
animation-delay: 0.2s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -134,7 +135,7 @@
|
|||||||
.bar9 {
|
.bar9 {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
left: 180px;
|
left: 180px;
|
||||||
background: #E00000;
|
background: #e00000;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 0.1s;
|
animation-delay: 0.1s;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -143,7 +144,7 @@
|
|||||||
.bar10 {
|
.bar10 {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
left: 205px;
|
left: 205px;
|
||||||
background: #CC0000;
|
background: #cc0000;
|
||||||
animation: waveAppear 1.6s ease-in-out infinite;
|
animation: waveAppear 1.6s ease-in-out infinite;
|
||||||
animation-delay: 0s; // 가장 작은 막대 - 처음 시작
|
animation-delay: 0s; // 가장 작은 막대 - 처음 시작
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -174,7 +175,8 @@
|
|||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
33%, 100% {
|
33%,
|
||||||
|
100% {
|
||||||
opacity: 0;
|
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