[251016] fix: VUI Test-2
🕐 커밋 시간: 2025. 10. 16. 15:01:09 📊 변경 통계: • 총 파일: 5개 • 추가: +240줄 • 삭제: -48줄 📁 추가된 파일: + com.twin.app.shoptime/vui-test.1.md + com.twin.app.shoptime/web-speech.md 📝 수정된 파일: ~ 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/VoicePanel/VoicePanel.jsx 🔧 함수 변경 내용: 📄 com.twin.app.shoptime/web-speech.md (md파일): ✅ Added: Framework(), Hook(), constructor(), checkSupport(), initialize(), SpeechRecognition(), setupEventHandlers(), onStart(), onResult(), onError(), getErrorMessage(), onEnd(), start(), abort(), cleanup(), WebSpeechService(), dispatch(), Date(), useDispatch(), useSelector(), useEffect(), initializeWebSpeech(), onSTTText(), useCallback(), SearchPanel(), useState(), setSearchQuery(), setTimeout(), setIsVoiceOverlayVisible(), useWebSpeech(), setVoiceMode(), stopListening(), handleSearchSubmit(), onSearchChange(), setCurrentMode(), onClose(), stopPropagation(), classNames(), renderModeContent(), async(), getUserMedia(), getTracks(), preventDefault(), startListening(), useSearchVoice() 🔧 주요 변경 내용: • 테스트 커버리지 및 안정성 향상 • 개발 문서 및 가이드 개선
This commit is contained in:
@@ -37,9 +37,16 @@ export const VOICE_MODES = {
|
|||||||
MODE_4: 'mode4', // 추후 추가
|
MODE_4: 'mode4', // 추후 추가
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 음성인식 입력 모드 (VUI vs WebSpeech)
|
||||||
|
export const VOICE_INPUT_MODE = {
|
||||||
|
VUI: 'vui', // VUI (Voice UI Framework)
|
||||||
|
WEBSPEECH: 'webspeech', // Web Speech API
|
||||||
|
};
|
||||||
|
|
||||||
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 MIC_WEBSPEECH_SPOTLIGHT_ID = 'voice-overlay-mic-webspeech-button';
|
||||||
|
|
||||||
const VoiceInputOverlay = ({
|
const VoiceInputOverlay = ({
|
||||||
isVisible,
|
isVisible,
|
||||||
@@ -54,11 +61,15 @@ const VoiceInputOverlay = ({
|
|||||||
const lastFocusedElement = useRef(null);
|
const lastFocusedElement = useRef(null);
|
||||||
const [inputFocus, setInputFocus] = useState(false);
|
const [inputFocus, setInputFocus] = useState(false);
|
||||||
const [micFocused, setMicFocused] = useState(false);
|
const [micFocused, setMicFocused] = useState(false);
|
||||||
|
const [micWebSpeechFocused, setMicWebSpeechFocused] = useState(false);
|
||||||
// 내부 모드 상태 관리 (prompt -> listening -> close)
|
// 내부 모드 상태 관리 (prompt -> listening -> close)
|
||||||
const [currentMode, setCurrentMode] = useState(mode);
|
const [currentMode, setCurrentMode] = useState(mode);
|
||||||
|
// 음성인식 입력 모드 (VUI vs WebSpeech)
|
||||||
|
const [voiceInputMode, setVoiceInputMode] = useState(null);
|
||||||
|
|
||||||
|
// ⛔ VUI 테스트 비활성화: VoicePanel 독립 테스트 시 충돌 방지
|
||||||
// Redux에서 voice 상태 가져오기
|
// Redux에서 voice 상태 가져오기
|
||||||
const { isRegistered, lastSTTText, sttTimestamp } = useSelector((state) => state.voice);
|
// const { isRegistered, lastSTTText, sttTimestamp } = useSelector((state) => state.voice);
|
||||||
|
|
||||||
// Redux에서 shopperHouse 검색 결과 가져오기
|
// Redux에서 shopperHouse 검색 결과 가져오기
|
||||||
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
|
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
|
||||||
@@ -78,25 +89,26 @@ const VoiceInputOverlay = ({
|
|||||||
}
|
}
|
||||||
}, [shopperHouseData, isVisible, onClose]);
|
}, [shopperHouseData, isVisible, onClose]);
|
||||||
|
|
||||||
|
// ⛔ VUI 테스트 비활성화: STT 텍스트 수신 처리
|
||||||
// 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) {
|
||||||
onSearchChange({ value: lastSTTText });
|
// onSearchChange({ value: lastSTTText });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// listening 모드로 전환 (시각적 피드백)
|
// // listening 모드로 전환 (시각적 피드백)
|
||||||
setCurrentMode(VOICE_MODES.LISTENING);
|
// setCurrentMode(VOICE_MODES.LISTENING);
|
||||||
|
|
||||||
// 1초 후 자동 닫기 (선택사항)
|
// // 1초 후 자동 닫기 (선택사항)
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
onClose();
|
// onClose();
|
||||||
}, 1000);
|
// }, 1000);
|
||||||
}
|
// }
|
||||||
}, [lastSTTText, sttTimestamp, isVisible, onSearchChange, onClose]);
|
// }, [lastSTTText, sttTimestamp, isVisible, onSearchChange, onClose]);
|
||||||
|
|
||||||
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
|
// Overlay가 열릴 때 포커스를 overlay 내부로 이동
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -106,13 +118,15 @@ const VoiceInputOverlay = ({
|
|||||||
|
|
||||||
// 모드 초기화 (항상 prompt 모드로 시작)
|
// 모드 초기화 (항상 prompt 모드로 시작)
|
||||||
setCurrentMode(mode);
|
setCurrentMode(mode);
|
||||||
|
setVoiceInputMode(null);
|
||||||
|
|
||||||
// Overlay 내부로 포커스 이동
|
// Overlay 내부로 포커스 이동
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Spotlight.focus(OVERLAY_SPOTLIGHT_ID);
|
Spotlight.focus(OVERLAY_SPOTLIGHT_ID);
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
// Overlay가 닫힐 때 원래 포커스 복원
|
// Overlay가 닫힐 때 원래 포커스 복원 및 상태 초기화
|
||||||
|
setVoiceInputMode(null);
|
||||||
if (lastFocusedElement.current) {
|
if (lastFocusedElement.current) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Spotlight.focus(lastFocusedElement.current);
|
Spotlight.focus(lastFocusedElement.current);
|
||||||
@@ -199,7 +213,7 @@ const VoiceInputOverlay = ({
|
|||||||
setInputFocus(false);
|
setInputFocus(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 마이크 버튼 포커스 핸들러
|
// 마이크 버튼 포커스 핸들러 (VUI)
|
||||||
const handleMicFocus = useCallback(() => {
|
const handleMicFocus = useCallback(() => {
|
||||||
setMicFocused(true);
|
setMicFocused(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -208,10 +222,55 @@ const VoiceInputOverlay = ({
|
|||||||
setMicFocused(false);
|
setMicFocused(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 마이크 버튼 클릭 (모드 전환: prompt -> listening -> close)
|
// WebSpeech 마이크 버튼 포커스 핸들러
|
||||||
const handleMicClick = useCallback(
|
const handleMicWebSpeechFocus = useCallback(() => {
|
||||||
|
setMicWebSpeechFocused(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMicWebSpeechBlur = useCallback(() => {
|
||||||
|
setMicWebSpeechFocused(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ⛔ VUI 테스트 비활성화: VUI 마이크 버튼 클릭 핸들러
|
||||||
|
// VUI 마이크 버튼 클릭 (모드 전환: prompt -> listening -> close)
|
||||||
|
// const handleVUIMicClick = useCallback(
|
||||||
|
// (e) => {
|
||||||
|
// console.log('[VoiceInputOverlay] handleVUIMicClick called, currentMode:', currentMode);
|
||||||
|
|
||||||
|
// // 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
||||||
|
// if (e && e.stopPropagation) {
|
||||||
|
// e.stopPropagation();
|
||||||
|
// }
|
||||||
|
// if (e && e.nativeEvent && e.nativeEvent.stopImmediatePropagation) {
|
||||||
|
// e.nativeEvent.stopImmediatePropagation();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (currentMode === VOICE_MODES.PROMPT) {
|
||||||
|
// // prompt 모드에서 클릭 시 -> VUI listening 모드로 전환
|
||||||
|
// console.log('[VoiceInputOverlay] Switching to VUI LISTENING mode');
|
||||||
|
// setVoiceInputMode(VOICE_INPUT_MODE.VUI);
|
||||||
|
// setCurrentMode(VOICE_MODES.LISTENING);
|
||||||
|
// // 이 시점에서 webOS Voice Framework가 자동으로 음성인식 시작
|
||||||
|
// // (이미 registerVoiceFramework()로 등록되어 있으므로)
|
||||||
|
// } else if (currentMode === VOICE_MODES.LISTENING && voiceInputMode === VOICE_INPUT_MODE.VUI) {
|
||||||
|
// // VUI listening 모드에서 클릭 시 -> 종료
|
||||||
|
// console.log('[VoiceInputOverlay] Closing from VUI LISTENING mode');
|
||||||
|
// setVoiceInputMode(null);
|
||||||
|
// onClose();
|
||||||
|
// } else {
|
||||||
|
// // 기타 모드에서는 바로 종료
|
||||||
|
// console.log('[VoiceInputOverlay] Closing from other mode');
|
||||||
|
// setVoiceInputMode(null);
|
||||||
|
// onClose();
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// [currentMode, voiceInputMode, onClose]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// WebSpeech 마이크 버튼 클릭 (모드 전환: prompt -> listening -> close)
|
||||||
|
const handleWebSpeechMicClick = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
console.log('[VoiceInputOverlay] handleMicClick called, currentMode:', currentMode);
|
console.log('[VoiceInputOverlay] handleWebSpeechMicClick called, currentMode:', currentMode);
|
||||||
|
|
||||||
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
// 이벤트 전파 방지 - dim 레이어의 onClick 실행 방지
|
||||||
if (e && e.stopPropagation) {
|
if (e && e.stopPropagation) {
|
||||||
@@ -222,28 +281,34 @@ const VoiceInputOverlay = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentMode === VOICE_MODES.PROMPT) {
|
if (currentMode === VOICE_MODES.PROMPT) {
|
||||||
// prompt 모드에서 클릭 시 -> listening 모드로 전환
|
// prompt 모드에서 클릭 시 -> WebSpeech listening 모드로 전환
|
||||||
console.log('[VoiceInputOverlay] Switching to LISTENING mode');
|
console.log('[VoiceInputOverlay] Switching to WebSpeech LISTENING mode');
|
||||||
|
setVoiceInputMode(VOICE_INPUT_MODE.WEBSPEECH);
|
||||||
setCurrentMode(VOICE_MODES.LISTENING);
|
setCurrentMode(VOICE_MODES.LISTENING);
|
||||||
// 이 시점에서 webOS Voice Framework가 자동으로 음성인식 시작
|
// TODO: Web Speech API 시작 로직 추가
|
||||||
// (이미 registerVoiceFramework()로 등록되어 있으므로)
|
} else if (
|
||||||
} else if (currentMode === VOICE_MODES.LISTENING) {
|
currentMode === VOICE_MODES.LISTENING &&
|
||||||
// listening 모드에서 클릭 시 -> 종료
|
voiceInputMode === VOICE_INPUT_MODE.WEBSPEECH
|
||||||
console.log('[VoiceInputOverlay] Closing from LISTENING mode');
|
) {
|
||||||
|
// WebSpeech listening 모드에서 클릭 시 -> 종료
|
||||||
|
console.log('[VoiceInputOverlay] Closing from WebSpeech LISTENING mode');
|
||||||
|
setVoiceInputMode(null);
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
// 기타 모드에서는 바로 종료
|
// 기타 모드에서는 바로 종료
|
||||||
console.log('[VoiceInputOverlay] Closing from other mode');
|
console.log('[VoiceInputOverlay] Closing from other mode');
|
||||||
|
setVoiceInputMode(null);
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currentMode, onClose]
|
[currentMode, voiceInputMode, onClose]
|
||||||
);
|
);
|
||||||
|
|
||||||
// dim 레이어 클릭 핸들러 (마이크 버튼과 분리)
|
// dim 레이어 클릭 핸들러 (마이크 버튼과 분리)
|
||||||
const handleDimClick = useCallback(
|
const handleDimClick = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
console.log('[VoiceInputOverlay] dimBackground clicked');
|
console.log('[VoiceInputOverlay] dimBackground clicked');
|
||||||
|
setVoiceInputMode(null);
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
[onClose]
|
[onClose]
|
||||||
@@ -264,8 +329,8 @@ const VoiceInputOverlay = ({
|
|||||||
{/* 배경 dim 레이어 - 클릭하면 닫힘 */}
|
{/* 배경 dim 레이어 - 클릭하면 닫힘 */}
|
||||||
<div className={css.dimBackground} onClick={handleDimClick} />
|
<div className={css.dimBackground} onClick={handleDimClick} />
|
||||||
|
|
||||||
{/* Voice 등록 상태 표시 (디버깅용) */}
|
{/* ⛔ VUI 테스트 비활성화: Voice 등록 상태 표시 (디버깅용) */}
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{/* {process.env.NODE_ENV === 'development' && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -276,8 +341,10 @@ const VoiceInputOverlay = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Voice: {isRegistered ? '✓ Ready' : '✗ Not Ready'}
|
Voice: {isRegistered ? '✓ Ready' : '✗ Not Ready'}
|
||||||
|
<br />
|
||||||
|
Mode: {voiceInputMode || 'None'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{/* 모드별 컨텐츠 영역 - Spotlight Container (self-only) */}
|
{/* 모드별 컨텐츠 영역 - Spotlight Container (self-only) */}
|
||||||
<OverlayContainer
|
<OverlayContainer
|
||||||
@@ -300,19 +367,27 @@ const VoiceInputOverlay = ({
|
|||||||
onFocus={handleInputFocus}
|
onFocus={handleInputFocus}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
/>
|
/>
|
||||||
|
{/* VUI 마이크 버튼 (⛔ 기능 비활성화: 클릭 핸들러만 무효화) */}
|
||||||
<SpottableMicButton
|
<SpottableMicButton
|
||||||
className={classNames(
|
className={classNames(
|
||||||
css.microphoneButton,
|
css.microphoneButton,
|
||||||
css.active,
|
css.active,
|
||||||
currentMode === VOICE_MODES.LISTENING && css.listening,
|
currentMode === VOICE_MODES.LISTENING &&
|
||||||
|
voiceInputMode === VOICE_INPUT_MODE.VUI &&
|
||||||
|
css.listening,
|
||||||
micFocused && css.focused
|
micFocused && css.focused
|
||||||
)}
|
)}
|
||||||
onClick={handleMicClick}
|
onClick={(e) => {
|
||||||
|
// ⛔ VUI 테스트 비활성화: handleVUIMicClick 호출 안 함
|
||||||
|
e.stopPropagation();
|
||||||
|
console.log('[VoiceInputOverlay] VUI mic clicked (disabled for testing)');
|
||||||
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' || e.keyCode === 13) {
|
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleMicClick(e);
|
// ⛔ VUI 테스트 비활성화: handleVUIMicClick 호출 안 함
|
||||||
|
console.log('[VoiceInputOverlay] VUI mic Enter key (disabled for testing)');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={handleMicFocus}
|
onFocus={handleMicFocus}
|
||||||
@@ -320,21 +395,63 @@ const VoiceInputOverlay = ({
|
|||||||
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="Voice AI" className={css.microphoneIcon} />
|
||||||
</div>
|
</div>
|
||||||
{currentMode === VOICE_MODES.LISTENING && (
|
{currentMode === VOICE_MODES.LISTENING &&
|
||||||
<svg className={css.rippleSvg} width="100" height="100">
|
voiceInputMode === VOICE_INPUT_MODE.VUI && (
|
||||||
<circle
|
<svg className={css.rippleSvg} width="100" height="100">
|
||||||
className={css.rippleCircle}
|
<circle
|
||||||
cx="50"
|
className={css.rippleCircle}
|
||||||
cy="50"
|
cx="50"
|
||||||
r="47"
|
cy="50"
|
||||||
fill="none"
|
r="47"
|
||||||
stroke="#C70850"
|
fill="none"
|
||||||
strokeWidth="6"
|
stroke="#C70850"
|
||||||
/>
|
strokeWidth="6"
|
||||||
</svg>
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</SpottableMicButton>
|
||||||
|
|
||||||
|
{/* WebSpeech 마이크 버튼 */}
|
||||||
|
<SpottableMicButton
|
||||||
|
className={classNames(
|
||||||
|
css.microphoneButtonWebSpeech,
|
||||||
|
css.active,
|
||||||
|
currentMode === VOICE_MODES.LISTENING &&
|
||||||
|
voiceInputMode === VOICE_INPUT_MODE.WEBSPEECH &&
|
||||||
|
css.listening,
|
||||||
|
micWebSpeechFocused && css.focused
|
||||||
)}
|
)}
|
||||||
|
onClick={handleWebSpeechMicClick}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.keyCode === 13) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleWebSpeechMicClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onFocus={handleMicWebSpeechFocus}
|
||||||
|
onBlur={handleMicWebSpeechBlur}
|
||||||
|
spotlightId={MIC_WEBSPEECH_SPOTLIGHT_ID}
|
||||||
|
>
|
||||||
|
<div className={css.microphoneCircle}>
|
||||||
|
<img src={micIcon} alt="Voice Input" className={css.microphoneIcon} />
|
||||||
|
</div>
|
||||||
|
{currentMode === VOICE_MODES.LISTENING &&
|
||||||
|
voiceInputMode === VOICE_INPUT_MODE.WEBSPEECH && (
|
||||||
|
<svg className={css.rippleSvg} width="100" height="100">
|
||||||
|
<circle
|
||||||
|
className={css.rippleCircle}
|
||||||
|
cx="50"
|
||||||
|
cy="50"
|
||||||
|
r="47"
|
||||||
|
fill="none"
|
||||||
|
stroke="#4A90E2"
|
||||||
|
strokeWidth="6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
</SpottableMicButton>
|
</SpottableMicButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -197,6 +197,79 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebSpeech 마이크 버튼 (블루 계열)
|
||||||
|
.microphoneButtonWebSpeech {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
z-index: 1003;
|
||||||
|
|
||||||
|
.microphoneCircle {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 1000px;
|
||||||
|
border: 5px solid #ccc;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
.microphoneIcon {
|
||||||
|
height: 50px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: filter 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.microphoneCircle {
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// active 상태 (음성 입력 모드 - 블루 색상)
|
||||||
|
&.active {
|
||||||
|
.microphoneCircle {
|
||||||
|
background-color: #4A90E2;
|
||||||
|
border-color: #4A90E2;
|
||||||
|
box-shadow: 0 0 22px 0 rgba(74, 144, 226, 0.5);
|
||||||
|
|
||||||
|
.microphoneIcon {
|
||||||
|
filter: brightness(0) invert(1); // 아이콘을 흰색으로 변경
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active.focused {
|
||||||
|
.microphoneCircle {
|
||||||
|
background-color: #4A90E2;
|
||||||
|
border-color: white;
|
||||||
|
box-shadow: 0 0 22px 0 rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// listening 상태 (배경 투명, 테두리 ripple 애니메이션)
|
||||||
|
&.listening {
|
||||||
|
.microphoneCircle {
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent; // 테두리 투명
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.microphoneIcon {
|
||||||
|
filter: brightness(0) invert(1); // 아이콘은 흰색 유지
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ripple 애니메이션 (원형 테두리가 점에서 시작해서 그려짐)
|
// Ripple 애니메이션 (원형 테두리가 점에서 시작해서 그려짐)
|
||||||
.rippleSvg {
|
.rippleSvg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ export default function VoicePanel({ panelInfo, isOnTop, spotlightId }) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOnTop) {
|
if (isOnTop) {
|
||||||
dispatch(sendLogGNB(LOG_MENU.SEARCH_SEARCH));
|
dispatch(sendLogGNB(LOG_MENU.SEARCH_SEARCH));
|
||||||
|
} else {
|
||||||
|
// ⭐ VoicePanel이 백그라운드로 가면 voice framework 자동 해제
|
||||||
|
// (SearchPanel 등 다른 패널과 충돌 방지)
|
||||||
|
console.log('[VoicePanel] Going to background, unregistering voice framework');
|
||||||
|
dispatch(unregisterVoiceFramework());
|
||||||
}
|
}
|
||||||
}, [isOnTop, dispatch]);
|
}, [isOnTop, dispatch]);
|
||||||
|
|
||||||
|
|||||||
309
com.twin.app.shoptime/vui-test.1.md
Normal file
309
com.twin.app.shoptime/vui-test.1.md
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
1-1 Can you recommend a 4K TV with Dolby Atmos support under $1,500?
|
||||||
|
|
||||||
|
|
||||||
|
오버레이 인식 : 확인
|
||||||
|
|
||||||
|
STT Text : Can you recommend a 4K TV with Dolby Atmos support under $1,500?
|
||||||
|
|
||||||
|
Event Logs :
|
||||||
|
|
||||||
|
RESPONSE :
|
||||||
|
|
||||||
|
{
|
||||||
|
"subscribed": true,
|
||||||
|
"command": "setContext",
|
||||||
|
"returnValue": true,
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-0036"
|
||||||
|
}
|
||||||
|
|
||||||
|
COMMAND :
|
||||||
|
|
||||||
|
{
|
||||||
|
"command": "setContext",
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-0036"
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUEST :
|
||||||
|
{
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-0036",
|
||||||
|
"intentCount": 3,
|
||||||
|
"intents": [
|
||||||
|
{
|
||||||
|
"intent": "UseIME",
|
||||||
|
"supportAsrOnly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "Select",
|
||||||
|
"supportOrdinal": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemId": "voice-search-button",
|
||||||
|
"value": [
|
||||||
|
"Search",
|
||||||
|
"Search Products",
|
||||||
|
"Find Items"
|
||||||
|
],
|
||||||
|
"title": "Search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-cart-button",
|
||||||
|
"value": [
|
||||||
|
"Cart",
|
||||||
|
"Shopping Cart",
|
||||||
|
"My Cart"
|
||||||
|
],
|
||||||
|
"title": "Cart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-home-button",
|
||||||
|
"value": [
|
||||||
|
"Home",
|
||||||
|
"Go Home",
|
||||||
|
"Main Page"
|
||||||
|
],
|
||||||
|
"title": "Home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-mypage-button",
|
||||||
|
"value": [
|
||||||
|
"My Page",
|
||||||
|
"Account",
|
||||||
|
"Profile"
|
||||||
|
],
|
||||||
|
"title": "My Page"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "Scroll",
|
||||||
|
"supportOrdinal": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemId": "voice-scroll-up",
|
||||||
|
"value": [
|
||||||
|
"Scroll Up",
|
||||||
|
"Page Up"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-scroll-down",
|
||||||
|
"value": [
|
||||||
|
"Scroll Down",
|
||||||
|
"Page Down"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
RESPONSE :
|
||||||
|
|
||||||
|
{
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-0036",
|
||||||
|
"intentCount": 3,
|
||||||
|
"intents": [
|
||||||
|
{
|
||||||
|
"intent": "UseIME",
|
||||||
|
"supportAsrOnly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "Select",
|
||||||
|
"supportOrdinal": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemId": "voice-search-button",
|
||||||
|
"value": [
|
||||||
|
"Search",
|
||||||
|
"Search Products",
|
||||||
|
"Find Items"
|
||||||
|
],
|
||||||
|
"title": "Search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-cart-button",
|
||||||
|
"value": [
|
||||||
|
"Cart",
|
||||||
|
"Shopping Cart",
|
||||||
|
"My Cart"
|
||||||
|
],
|
||||||
|
"title": "Cart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-home-button",
|
||||||
|
"value": [
|
||||||
|
"Home",
|
||||||
|
"Go Home",
|
||||||
|
"Main Page"
|
||||||
|
],
|
||||||
|
"title": "Home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-mypage-button",
|
||||||
|
"value": [
|
||||||
|
"My Page",
|
||||||
|
"Account",
|
||||||
|
"Profile"
|
||||||
|
],
|
||||||
|
"title": "My Page"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "Scroll",
|
||||||
|
"supportOrdinal": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemId": "voice-scroll-up",
|
||||||
|
"value": [
|
||||||
|
"Scroll Up",
|
||||||
|
"Page Up"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-scroll-down",
|
||||||
|
"value": [
|
||||||
|
"Scroll Down",
|
||||||
|
"Page Down"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ACTION :
|
||||||
|
|
||||||
|
{
|
||||||
|
"message": "Context set successfully. Press the MIC button on remote and speak.",
|
||||||
|
"nextStep": "Waiting for performAction event...",
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-0036"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
그러나 두번째 시도에서
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
STT Text : No STT text received yet. Speak after registering to see the result.
|
||||||
|
|
||||||
|
RESPONSE :
|
||||||
|
|
||||||
|
{
|
||||||
|
"subscribed": true,
|
||||||
|
"command": "setContext",
|
||||||
|
"returnValue": true,
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-003b"
|
||||||
|
}
|
||||||
|
|
||||||
|
COMMAND :
|
||||||
|
{
|
||||||
|
"command": "setContext",
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-003b"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
REQUEST :
|
||||||
|
{
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-003b",
|
||||||
|
"intentCount": 3,
|
||||||
|
"intents": [
|
||||||
|
{
|
||||||
|
"intent": "UseIME",
|
||||||
|
"supportAsrOnly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "Select",
|
||||||
|
"supportOrdinal": true,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemId": "voice-search-button",
|
||||||
|
"value": [
|
||||||
|
"Search",
|
||||||
|
"Search Products",
|
||||||
|
"Find Items"
|
||||||
|
],
|
||||||
|
"title": "Search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-cart-button",
|
||||||
|
"value": [
|
||||||
|
"Cart",
|
||||||
|
"Shopping Cart",
|
||||||
|
"My Cart"
|
||||||
|
],
|
||||||
|
"title": "Cart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-home-button",
|
||||||
|
"value": [
|
||||||
|
"Home",
|
||||||
|
"Go Home",
|
||||||
|
"Main Page"
|
||||||
|
],
|
||||||
|
"title": "Home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-mypage-button",
|
||||||
|
"value": [
|
||||||
|
"My Page",
|
||||||
|
"Account",
|
||||||
|
"Profile"
|
||||||
|
],
|
||||||
|
"title": "My Page"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"intent": "Scroll",
|
||||||
|
"supportOrdinal": false,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"itemId": "voice-scroll-up",
|
||||||
|
"value": [
|
||||||
|
"Scroll Up",
|
||||||
|
"Page Up"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"itemId": "voice-scroll-down",
|
||||||
|
"value": [
|
||||||
|
"Scroll Down",
|
||||||
|
"Page Down"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
RESPONSE :
|
||||||
|
{
|
||||||
|
"returnValue": true
|
||||||
|
}
|
||||||
|
|
||||||
|
ACTION :
|
||||||
|
{
|
||||||
|
"message": "Context set successfully. Press the MIC button on remote and speak.",
|
||||||
|
"nextStep": "Waiting for performAction event...",
|
||||||
|
"voiceTicket": "13799ec6-fd1f-4cdd-b02e-34d58fe5f34b-68ef-003b"
|
||||||
|
}
|
||||||
|
|
||||||
|
ERROR :
|
||||||
|
{
|
||||||
|
"message": "performAction event was not received within 15 seconds after setContext.",
|
||||||
|
"possibleReasons": [
|
||||||
|
"1. Did you press the MIC button on the remote control?",
|
||||||
|
"2. Did you speak after pressing the MIC button?",
|
||||||
|
"3. UseIME intent might not be supported on this webOS version",
|
||||||
|
"4. Voice framework might not be routing events correctly"
|
||||||
|
],
|
||||||
|
"suggestion": "Try pressing the remote MIC button and speaking clearly. Check VoicePanel logs for performAction event."
|
||||||
|
}
|
||||||
1417
com.twin.app.shoptime/web-speech.md
Normal file
1417
com.twin.app.shoptime/web-speech.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user