[251022] fix: VoiceInputOverlay FinalText processing updated

🕐 커밋 시간: 2025. 10. 22. 16:56:15

📊 변경 통계:
  • 총 파일: 7개
  • 추가: +271줄
  • 삭제: -198줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/actions/webSpeechActions.js
  ~ com.twin.app.shoptime/src/reducers/voiceReducer.js
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceDebugDashboard.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/VoiceInputOverlay/modes/VoicePromptScreen.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript):
    🔄 Modified: clearAllTimers()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/modes/VoicePromptScreen.jsx (javascript):
     Added: createBubbleClickHandler()
    🔄 Modified: SpotlightContainerDecorator()
     Deleted: handleBubbleClick()

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
This commit is contained in:
2025-10-22 16:56:17 +09:00
parent 466d2b7440
commit be6dee5f13
7 changed files with 517 additions and 438 deletions

View File

@@ -2,227 +2,226 @@
export const types = {
// panel actions
PUSH_PANEL: "PUSH_PANEL",
POP_PANEL: "POP_PANEL",
UPDATE_PANEL: "UPDATE_PANEL",
RESET_PANELS: "RESET_PANELS",
PUSH_PANEL: 'PUSH_PANEL',
POP_PANEL: 'POP_PANEL',
UPDATE_PANEL: 'UPDATE_PANEL',
RESET_PANELS: 'RESET_PANELS',
// device actions
GET_AUTHENTICATION_CODE: "GET_AUTHENTICATION_CODE",
REGISTER_DEVICE: "REGISTER_DEVICE",
REGISTER_DEVICE_INFO: "REGISTER_DEVICE_INFO",
GET_DEVICE_INFO: "GET_DEVICE_INFO",
CLEAR_REGISTER_DEVICE_INFO: "CLEAR_REGISTER_DEVICE_INFO",
REGISTER_DEVICE_RESET: "REGISTER_DEVICE_RESET",
GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE',
REGISTER_DEVICE: 'REGISTER_DEVICE',
REGISTER_DEVICE_INFO: 'REGISTER_DEVICE_INFO',
GET_DEVICE_INFO: 'GET_DEVICE_INFO',
CLEAR_REGISTER_DEVICE_INFO: 'CLEAR_REGISTER_DEVICE_INFO',
REGISTER_DEVICE_RESET: 'REGISTER_DEVICE_RESET',
// common actions
GET_HTTP_HEADER: "GET_HTTP_HEADER",
CHANGE_APP_STATUS: "CHANGE_APP_STATUS",
SEND_BROADCAST: "SEND_BROADCAST",
CHANGE_LOCAL_SETTINGS: "CHANGE_LOCAL_SETTINGS",
GNB_OPENED: "GNB_OPENED",
SET_SHOW_POPUP: "SET_SHOW_POPUP",
SET_SHOW_SECONDARY_POPUP: "SET_SHOW_SECONDARY_POPUP",
SET_HIDE_POPUP: "SET_HIDE_POPUP",
SET_HIDE_SECONDARY_POPUP: "SET_HIDE_SECONDARY_POPUP",
SHOW_OPTIONAL_TERMS_CONFIRM_POPUP: "SHOW_OPTIONAL_TERMS_CONFIRM_POPUP",
HIDE_OPTIONAL_TERMS_CONFIRM_POPUP: "HIDE_OPTIONAL_TERMS_CONFIRM_POPUP",
TOGGLE_OPTIONAL_TERMS_CONFIRM: "TOGGLE_OPTIONAL_TERMS_CONFIRM",
GET_HTTP_HEADER: 'GET_HTTP_HEADER',
CHANGE_APP_STATUS: 'CHANGE_APP_STATUS',
SEND_BROADCAST: 'SEND_BROADCAST',
CHANGE_LOCAL_SETTINGS: 'CHANGE_LOCAL_SETTINGS',
GNB_OPENED: 'GNB_OPENED',
SET_SHOW_POPUP: 'SET_SHOW_POPUP',
SET_SHOW_SECONDARY_POPUP: 'SET_SHOW_SECONDARY_POPUP',
SET_HIDE_POPUP: 'SET_HIDE_POPUP',
SET_HIDE_SECONDARY_POPUP: 'SET_HIDE_SECONDARY_POPUP',
SHOW_OPTIONAL_TERMS_CONFIRM_POPUP: 'SHOW_OPTIONAL_TERMS_CONFIRM_POPUP',
HIDE_OPTIONAL_TERMS_CONFIRM_POPUP: 'HIDE_OPTIONAL_TERMS_CONFIRM_POPUP',
TOGGLE_OPTIONAL_TERMS_CONFIRM: 'TOGGLE_OPTIONAL_TERMS_CONFIRM',
// 선택약관 팝업 상태 관리
SET_OPTIONAL_TERMS_POPUP_SHOWN: "SET_OPTIONAL_TERMS_POPUP_SHOWN",
SET_OPTIONAL_TERMS_USER_DECISION: "SET_OPTIONAL_TERMS_USER_DECISION",
RESET_OPTIONAL_TERMS_SESSION: "RESET_OPTIONAL_TERMS_SESSION",
SET_OPTIONAL_TERMS_POPUP_SHOWN: 'SET_OPTIONAL_TERMS_POPUP_SHOWN',
SET_OPTIONAL_TERMS_USER_DECISION: 'SET_OPTIONAL_TERMS_USER_DECISION',
RESET_OPTIONAL_TERMS_SESSION: 'RESET_OPTIONAL_TERMS_SESSION',
// 선택약관 직접 상태 업데이트 (API 호출 없이)
UPDATE_OPTIONAL_TERMS_AGREE_DIRECT: "UPDATE_OPTIONAL_TERMS_AGREE_DIRECT",
UPDATE_TERMS_AGREEMENT_STATUS_DIRECT: "UPDATE_TERMS_AGREEMENT_STATUS_DIRECT",
SET_EXIT_APP: "SET_EXIT_APP",
GET_LOGIN_USER_DATA: "GET_LOGIN_USER_DATA",
GET_TERMS_AGREE_YN: "GET_TERMS_AGREE_YN",
LAUNCH_MEMBERSHIP_APP: "LAUNCH_MEMBERSHIP_APP",
SET_FOCUS: "SET_FOCUS",
SET_GNB_MENU: "SET_GNB_MENU",
SET_SYSTEM_NOTICE: "SET_SYSTEM_NOTICE",
SET_SYSTEM_TERMINATION: "SET_SYSTEM_TERMINATION",
SET_DEEP_LINK: "SET_DEEP_LINK",
SET_SECOND_LAYER_INFO: "SET_SECOND_LAYER_INFO",
SET_ERROR_MESSAGE: "SET_ERROR_MESSAGE",
CLEAR_ERROR_MESSAGE: "CLEAR_ERROR_MESSAGE",
GET_DEVICE_MACADDRESS: "GET_DEVICE_MACADDRESS",
SET_DEVICE_REGISTERED: "SET_DEVICE_REGISTERED",
UPDATE_OPTIONAL_TERMS_AGREE_DIRECT: 'UPDATE_OPTIONAL_TERMS_AGREE_DIRECT',
UPDATE_TERMS_AGREEMENT_STATUS_DIRECT: 'UPDATE_TERMS_AGREEMENT_STATUS_DIRECT',
SET_EXIT_APP: 'SET_EXIT_APP',
GET_LOGIN_USER_DATA: 'GET_LOGIN_USER_DATA',
GET_TERMS_AGREE_YN: 'GET_TERMS_AGREE_YN',
LAUNCH_MEMBERSHIP_APP: 'LAUNCH_MEMBERSHIP_APP',
SET_FOCUS: 'SET_FOCUS',
SET_GNB_MENU: 'SET_GNB_MENU',
SET_SYSTEM_NOTICE: 'SET_SYSTEM_NOTICE',
SET_SYSTEM_TERMINATION: 'SET_SYSTEM_TERMINATION',
SET_DEEP_LINK: 'SET_DEEP_LINK',
SET_SECOND_LAYER_INFO: 'SET_SECOND_LAYER_INFO',
SET_ERROR_MESSAGE: 'SET_ERROR_MESSAGE',
CLEAR_ERROR_MESSAGE: 'CLEAR_ERROR_MESSAGE',
GET_DEVICE_MACADDRESS: 'GET_DEVICE_MACADDRESS',
SET_DEVICE_REGISTERED: 'SET_DEVICE_REGISTERED',
// billing actions
GET_MY_INFO_BILLING_SEARCH: "GET_MY_INFO_BILLING_SEARCH",
GET_MY_INFO_BILLING_SEARCH: 'GET_MY_INFO_BILLING_SEARCH',
// card actions
GET_MY_INFO_CARD_SEARCH: "GET_MY_INFO_CARD_SEARCH",
GET_MY_INFO_CARD_SEARCH: 'GET_MY_INFO_CARD_SEARCH',
// shipping actions
GET_MY_INFO_SHIPPING_SEARCH: "GET_MY_INFO_SHIPPING_SEARCH",
GET_MY_INFO_SHIPPING_SEARCH: 'GET_MY_INFO_SHIPPING_SEARCH',
// cart actions
GET_MY_INFO_CART_SEARCH: "GET_MY_INFO_CART_SEARCH",
ADD_TO_CART: "ADD_TO_CART",
REMOVE_FROM_CART: "REMOVE_FROM_CART",
UPDATE_CART_ITEM: "UPDATE_CART_ITEM",
CLEAR_CART: "CLEAR_CART",
GET_MY_INFO_CART_SEARCH: 'GET_MY_INFO_CART_SEARCH',
ADD_TO_CART: 'ADD_TO_CART',
REMOVE_FROM_CART: 'REMOVE_FROM_CART',
UPDATE_CART_ITEM: 'UPDATE_CART_ITEM',
CLEAR_CART: 'CLEAR_CART',
// appData actions
ADD_MAIN_INDEX: "ADD_MAIN_INDEX",
SEND_SMS: "SEND_SMS",
CLEAR_SMS: "CLEAR_SMS",
ADD_MAIN_INDEX: 'ADD_MAIN_INDEX',
SEND_SMS: 'SEND_SMS',
CLEAR_SMS: 'CLEAR_SMS',
// home actions
GET_HOME_TERMS: "GET_HOME_TERMS",
SET_TERMS_ID_MAP: "SET_TERMS_ID_MAP",
SET_OPTIONAL_TERMS_AVAILABILITY: "SET_OPTIONAL_TERMS_AVAILABILITY",
GET_HOME_MENU: "GET_HOME_MENU",
GET_HOME_LAYOUT: "GET_HOME_LAYOUT",
GET_HOME_MAIN_CONTENTS: "GET_HOME_MAIN_CONTENTS",
GET_HOME_FULL_VIDEO_INFO: "GET_HOME_FULL_VIDEO_INFO",
GET_THEME_CURATION_INFO: "GET_THEME_CURATION_INFO",
GET_THEME_CURATION_DETAIL_INFO: "GET_THEME_CURATION_DETAIL_INFO",
GET_THEME_HOTEL_DETAIL_INFO: "GET_THEME_HOTEL_DETAIL_INFO",
GET_THEME_MENU_SHELF_INFO: "GET_THEME_MENU_SHELF_INFO",
CLEAR_THEME_MENU_SHELF_INFO: "CLEAR_THEME_MENU_SHELF_INFO",
CLEAR_THEME_DETAIL: "CLEAR_THEME_DETAIL",
CHECK_ENTER_THROUGH_GNB: "CHECK_ENTER_THROUGH_GNB",
SET_DEFAULT_FOCUS: "SET_DEFAULT_FOCUS",
SET_BANNER_INDEX: "SET_BANNER_INDEX",
RESET_HOME_INFO: "RESET_HOME_INFO",
UPDATE_HOME_INFO: "UPDATE_HOME_INFO",
GET_HOME_TERMS: 'GET_HOME_TERMS',
SET_TERMS_ID_MAP: 'SET_TERMS_ID_MAP',
SET_OPTIONAL_TERMS_AVAILABILITY: 'SET_OPTIONAL_TERMS_AVAILABILITY',
GET_HOME_MENU: 'GET_HOME_MENU',
GET_HOME_LAYOUT: 'GET_HOME_LAYOUT',
GET_HOME_MAIN_CONTENTS: 'GET_HOME_MAIN_CONTENTS',
GET_HOME_FULL_VIDEO_INFO: 'GET_HOME_FULL_VIDEO_INFO',
GET_THEME_CURATION_INFO: 'GET_THEME_CURATION_INFO',
GET_THEME_CURATION_DETAIL_INFO: 'GET_THEME_CURATION_DETAIL_INFO',
GET_THEME_HOTEL_DETAIL_INFO: 'GET_THEME_HOTEL_DETAIL_INFO',
GET_THEME_MENU_SHELF_INFO: 'GET_THEME_MENU_SHELF_INFO',
CLEAR_THEME_MENU_SHELF_INFO: 'CLEAR_THEME_MENU_SHELF_INFO',
CLEAR_THEME_DETAIL: 'CLEAR_THEME_DETAIL',
CHECK_ENTER_THROUGH_GNB: 'CHECK_ENTER_THROUGH_GNB',
SET_DEFAULT_FOCUS: 'SET_DEFAULT_FOCUS',
SET_BANNER_INDEX: 'SET_BANNER_INDEX',
RESET_HOME_INFO: 'RESET_HOME_INFO',
UPDATE_HOME_INFO: 'UPDATE_HOME_INFO',
// brand actions
GET_BRAND_LIST: "GET_BRAND_LIST",
GET_BRAND_LAYOUT_INFO: "GET_BRAND_LAYOUT_INFO",
GET_BRAND_LIVE_CHANNEL_INFO: "GET_BRAND_LIVE_CHANNEL_INFO",
GET_BRAND_CHAN_INFO: "GET_BRAND_CHAN_INFO",
GET_BRAND_TSV_INFO: "GET_BRAND_TSV_INFO",
GET_BRAND_RECOMMENDED_SHOW_INFO: "GET_BRAND_RECOMMENDED_SHOW_INFO",
GET_BRAND_SERIES_INFO: "GET_BRAND_SERIES_INFO",
GET_BRAND_CATEGORY_INFO: "GET_BRAND_CATEGORY_INFO",
GET_BRAND_CATEGORY_PRODUCT_INFO: "GET_BRAND_CATEGORY_PRODUCT_INFO",
GET_BRAND_BEST_SELLER: "GET_BRAND_BEST_SELLER",
GET_BRAND_CREATORS_INFO: "GET_BRAND_CREATORS_INFO",
GET_BRAND_SHOWROOM: "GET_BRAND_SHOWROOM",
GET_BRAND_RECENTLY_AIRED: "GET_BRAND_RECENTLY_AIRED",
SET_BRAND_LIVE_CHANNEL_UPCOMING: "SET_BRAND_LIVE_CHANNEL_UPCOMING",
SET_BRAND_CHAN_INFO: "SET_BRAND_CHAN_INFO",
RESET_BRAND_STATE: "RESET_BRAND_STATE",
RESET_BRAND_STATE_EXCEPT_BRAND_INFO: "RESET_BRAND_STATE_EXCEPT_BRAND_INFO",
RESET_BRAND_LAYOUT_INFO: "RESET_BRAND_LAYOUT_INFO",
GET_BRAND_LIST: 'GET_BRAND_LIST',
GET_BRAND_LAYOUT_INFO: 'GET_BRAND_LAYOUT_INFO',
GET_BRAND_LIVE_CHANNEL_INFO: 'GET_BRAND_LIVE_CHANNEL_INFO',
GET_BRAND_CHAN_INFO: 'GET_BRAND_CHAN_INFO',
GET_BRAND_TSV_INFO: 'GET_BRAND_TSV_INFO',
GET_BRAND_RECOMMENDED_SHOW_INFO: 'GET_BRAND_RECOMMENDED_SHOW_INFO',
GET_BRAND_SERIES_INFO: 'GET_BRAND_SERIES_INFO',
GET_BRAND_CATEGORY_INFO: 'GET_BRAND_CATEGORY_INFO',
GET_BRAND_CATEGORY_PRODUCT_INFO: 'GET_BRAND_CATEGORY_PRODUCT_INFO',
GET_BRAND_BEST_SELLER: 'GET_BRAND_BEST_SELLER',
GET_BRAND_CREATORS_INFO: 'GET_BRAND_CREATORS_INFO',
GET_BRAND_SHOWROOM: 'GET_BRAND_SHOWROOM',
GET_BRAND_RECENTLY_AIRED: 'GET_BRAND_RECENTLY_AIRED',
SET_BRAND_LIVE_CHANNEL_UPCOMING: 'SET_BRAND_LIVE_CHANNEL_UPCOMING',
SET_BRAND_CHAN_INFO: 'SET_BRAND_CHAN_INFO',
RESET_BRAND_STATE: 'RESET_BRAND_STATE',
RESET_BRAND_STATE_EXCEPT_BRAND_INFO: 'RESET_BRAND_STATE_EXCEPT_BRAND_INFO',
RESET_BRAND_LAYOUT_INFO: 'RESET_BRAND_LAYOUT_INFO',
// main actions
GET_SUB_CATEGORY: "GET_SUB_CATEGORY",
APPEND_SUB_CATEGORY: "APPEND_SUB_CATEGORY",
GET_TOP_20_SHOW: "GET_TOP_20_SHOW",
GET_PRODUCT_DETAIL: "GET_PRODUCT_DETAIL",
GET_YOUMAYLIKE: "GET_YOUMAYLIKE",
SET_MAIN_LIKE_CATEGORY: "SET_MAIN_LIKE_CATEGORY",
SET_MAIN_LIVE_UPCOMING_ALARM: "SET_MAIN_LIVE_UPCOMING_ALARM",
GET_MAIN_LIVE_SHOW_NOW_PRODUCT: "GET_MAIN_LIVE_SHOW_NOW_PRODUCT",
GET_MAIN_LIVE_SHOW: "GET_MAIN_LIVE_SHOW",
GET_MAIN_CATEGORY_SHOW_DETAIL: "GET_MAIN_CATEGORY_SHOW_DETAIL",
CLEAR_PRODUCT_DETAIL: "CLEAR_PRODUCT_DETAIL",
CLEAR_SUB_CATEGORY: "CLEAR_SUB_CATEGORY",
CLEAR_SHOPNOW_INFO: "CLEAR_SHOPNOW_INFO",
GET_SUB_CATEGORY: 'GET_SUB_CATEGORY',
APPEND_SUB_CATEGORY: 'APPEND_SUB_CATEGORY',
GET_TOP_20_SHOW: 'GET_TOP_20_SHOW',
GET_PRODUCT_DETAIL: 'GET_PRODUCT_DETAIL',
GET_YOUMAYLIKE: 'GET_YOUMAYLIKE',
SET_MAIN_LIKE_CATEGORY: 'SET_MAIN_LIKE_CATEGORY',
SET_MAIN_LIVE_UPCOMING_ALARM: 'SET_MAIN_LIVE_UPCOMING_ALARM',
GET_MAIN_LIVE_SHOW_NOW_PRODUCT: 'GET_MAIN_LIVE_SHOW_NOW_PRODUCT',
GET_MAIN_LIVE_SHOW: 'GET_MAIN_LIVE_SHOW',
GET_MAIN_CATEGORY_SHOW_DETAIL: 'GET_MAIN_CATEGORY_SHOW_DETAIL',
CLEAR_PRODUCT_DETAIL: 'CLEAR_PRODUCT_DETAIL',
CLEAR_SUB_CATEGORY: 'CLEAR_SUB_CATEGORY',
CLEAR_SHOPNOW_INFO: 'CLEAR_SHOPNOW_INFO',
// myPage actions
GET_MY_RECOMMANDED_KEYWORD: "GET_MY_RECOMMANDED_KEYWORD",
GET_MY_FAQ_INFO: "GET_MY_FAQ_INFO",
GET_NOTICE: "GET_NOTICE",
GET_MY_CUSTOMERS: "GET_MY_CUSTOMERS",
GET_MY_FAVORITE: "GET_MY_FAVORITE",
DELETE_MY_FAVORITE: "DELETE_MY_FAVORITE",
GET_MY_FAVORITE_FLAG: "GET_MY_FAVORITE_FLAG",
SET_MY_TERMS_WITHDRAW: "SET_MY_TERMS_WITHDRAW",
GET_MY_UPCOMING_CHANGE_INFO: "GET_MY_UPCOMING_CHANGE_INFO",
GET_MY_UPCOMING_ALERT_SHOW: "GET_MY_UPCOMING_ALERT_SHOW",
DELETE_MY_UPCOMING_ALERT_SHOW: "DELETE_MY_UPCOMING_ALERT_SHOW",
GET_MY_UPCOMING_ALERT_SHOW_KEYS: "GET_MY_UPCOMING_ALERT_SHOW_KEYS",
SET_MY_UPCOMING_USE_ALERT: "SET_MY_UPCOMING_USE_ALERT",
GET_UPCOMING_ALERT_SHOW_CHANGE_INFO: "GET_UPCOMING_ALERT_SHOW_CHANGE_INFO",
GET_MY_RECENTLY_VIEWED_INFO: "GET_MY_RECENTLY_VIEWED_INFO",
CLEAR_RECENTLY_VIEWED_INFO: "CLEAR_RECENTLY_VIEWED_INFO",
CLEAR_FAVORITES: "CLEAR_FAVORITES",
SET_MYPAGE_TERMS_AGREE: "SET_MYPAGE_TERMS_AGREE",
SET_MYPAGE_TERMS_AGREE_SUCCESS: "SET_MYPAGE_TERMS_AGREE_SUCCESS",
SET_MYPAGE_TERMS_AGREE_FAIL: "SET_MYPAGE_TERMS_AGREE_FAIL",
GET_MY_RECOMMANDED_KEYWORD: 'GET_MY_RECOMMANDED_KEYWORD',
GET_MY_FAQ_INFO: 'GET_MY_FAQ_INFO',
GET_NOTICE: 'GET_NOTICE',
GET_MY_CUSTOMERS: 'GET_MY_CUSTOMERS',
GET_MY_FAVORITE: 'GET_MY_FAVORITE',
DELETE_MY_FAVORITE: 'DELETE_MY_FAVORITE',
GET_MY_FAVORITE_FLAG: 'GET_MY_FAVORITE_FLAG',
SET_MY_TERMS_WITHDRAW: 'SET_MY_TERMS_WITHDRAW',
GET_MY_UPCOMING_CHANGE_INFO: 'GET_MY_UPCOMING_CHANGE_INFO',
GET_MY_UPCOMING_ALERT_SHOW: 'GET_MY_UPCOMING_ALERT_SHOW',
DELETE_MY_UPCOMING_ALERT_SHOW: 'DELETE_MY_UPCOMING_ALERT_SHOW',
GET_MY_UPCOMING_ALERT_SHOW_KEYS: 'GET_MY_UPCOMING_ALERT_SHOW_KEYS',
SET_MY_UPCOMING_USE_ALERT: 'SET_MY_UPCOMING_USE_ALERT',
GET_UPCOMING_ALERT_SHOW_CHANGE_INFO: 'GET_UPCOMING_ALERT_SHOW_CHANGE_INFO',
GET_MY_RECENTLY_VIEWED_INFO: 'GET_MY_RECENTLY_VIEWED_INFO',
CLEAR_RECENTLY_VIEWED_INFO: 'CLEAR_RECENTLY_VIEWED_INFO',
CLEAR_FAVORITES: 'CLEAR_FAVORITES',
SET_MYPAGE_TERMS_AGREE: 'SET_MYPAGE_TERMS_AGREE',
SET_MYPAGE_TERMS_AGREE_SUCCESS: 'SET_MYPAGE_TERMS_AGREE_SUCCESS',
SET_MYPAGE_TERMS_AGREE_FAIL: 'SET_MYPAGE_TERMS_AGREE_FAIL',
// onSale actions
GET_HOME_ON_SALE_INFO: "GET_HOME_ON_SALE_INFO",
GET_ON_SALE_INFO: "GET_ON_SALE_INFO",
COPY_CATEGORY_INFO: "COPY_CATEGORY_INFO",
RESET_ON_SALE_STATE: "RESET_ON_SALE_STATE",
GET_HOME_ON_SALE_INFO: 'GET_HOME_ON_SALE_INFO',
GET_ON_SALE_INFO: 'GET_ON_SALE_INFO',
COPY_CATEGORY_INFO: 'COPY_CATEGORY_INFO',
RESET_ON_SALE_STATE: 'RESET_ON_SALE_STATE',
// product actions
GET_BEST_SELLER: "GET_BEST_SELLER",
GET_PRODUCT_GROUP: "GET_PRODUCT_GROUP",
GET_PRODUCT_OPTION: "GET_PRODUCT_OPTION",
GET_PRODUCT_IMAGE_LENGTH: "GET_PRODUCT_IMAGE_LENGTH",
GET_VIDEO_INDECATOR_FOCUS: "GET_VIDEO_INDECATOR_FOCUS",
GET_PRODUCT_OPTION_ID: "GET_PRODUCT_OPTION_ID",
CLEAR_PRODUCT_OPTIONS: "CLEAR_PRODUCT_OPTIONS",
GET_USER_REVIEW: "GET_USER_REVIEW",
TOGGLE_SHOW_ALL_REVIEWS: "TOGGLE_SHOW_ALL_REVIEWS",
RESET_SHOW_ALL_REVIEWS: "RESET_SHOW_ALL_REVIEWS",
GET_BEST_SELLER: 'GET_BEST_SELLER',
GET_PRODUCT_GROUP: 'GET_PRODUCT_GROUP',
GET_PRODUCT_OPTION: 'GET_PRODUCT_OPTION',
GET_PRODUCT_IMAGE_LENGTH: 'GET_PRODUCT_IMAGE_LENGTH',
GET_VIDEO_INDECATOR_FOCUS: 'GET_VIDEO_INDECATOR_FOCUS',
GET_PRODUCT_OPTION_ID: 'GET_PRODUCT_OPTION_ID',
CLEAR_PRODUCT_OPTIONS: 'CLEAR_PRODUCT_OPTIONS',
GET_USER_REVIEW: 'GET_USER_REVIEW',
TOGGLE_SHOW_ALL_REVIEWS: 'TOGGLE_SHOW_ALL_REVIEWS',
RESET_SHOW_ALL_REVIEWS: 'RESET_SHOW_ALL_REVIEWS',
// search actions
GET_SEARCH: "GET_SEARCH",
GET_SEARCH_MAIN: "GET_SEARCH_MAIN",
CLEAR_SEARCH_MAIN_DATA: "CLEAR_SEARCH_MAIN_DATA",
GET_SHOPPERHOUSE_SEARCH: "GET_SHOPPERHOUSE_SEARCH",
CLEAR_SHOPPERHOUSE_DATA: "CLEAR_SHOPPERHOUSE_DATA",
RESET_SEARCH: "RESET_SEARCH",
RESET_VOICE_SEARCH: "RESET_VOICE_SEARCH",
GET_SEARCH_PROCESSED: "GET_SEARCH_PROCESSED",
SET_SEARCH_INIT_PERFORMED: "SET_SEARCH_INIT_PERFORMED",
UPDATE_SEARCH_TIMESTAMP: "UPDATE_SEARCH_TIMESTAMP",
GET_SEARCH: 'GET_SEARCH',
GET_SEARCH_MAIN: 'GET_SEARCH_MAIN',
CLEAR_SEARCH_MAIN_DATA: 'CLEAR_SEARCH_MAIN_DATA',
GET_SHOPPERHOUSE_SEARCH: 'GET_SHOPPERHOUSE_SEARCH',
CLEAR_SHOPPERHOUSE_DATA: 'CLEAR_SHOPPERHOUSE_DATA',
RESET_SEARCH: 'RESET_SEARCH',
RESET_VOICE_SEARCH: 'RESET_VOICE_SEARCH',
GET_SEARCH_PROCESSED: 'GET_SEARCH_PROCESSED',
SET_SEARCH_INIT_PERFORMED: 'SET_SEARCH_INIT_PERFORMED',
UPDATE_SEARCH_TIMESTAMP: 'UPDATE_SEARCH_TIMESTAMP',
SET_SHOPPERHOUSE_ERROR: 'SET_SHOPPERHOUSE_ERROR',
// event actions
GET_WELCOME_EVENT_INFO: "GET_WELCOME_EVENT_INFO",
GET_EVENT_ISSUED_STATUS: "GET_EVENT_ISSUED_STATUS",
SET_EVENT_ISSUE_REQ: "SET_EVENT_ISSUE_REQ",
SET_EVENT_POP_CLICK_INFO: "SET_EVENT_POP_CLICK_INFO",
CLEAR_EVENT_INFO: "CLEAR_EVENT_INFO",
SKIP_CLICK_INFO: "SKIP_CLICK_INFO",
CLEAR_CURATION_COUPON: "CLEAR_CURATION_COUPON",
CLEAR_EVENT_ISSUED_STATUS_SUCCEESS: "CLEAR_EVENT_ISSUED_STATUS_SUCCEESS",
GET_WELCOME_EVENT_INFO: 'GET_WELCOME_EVENT_INFO',
GET_EVENT_ISSUED_STATUS: 'GET_EVENT_ISSUED_STATUS',
SET_EVENT_ISSUE_REQ: 'SET_EVENT_ISSUE_REQ',
SET_EVENT_POP_CLICK_INFO: 'SET_EVENT_POP_CLICK_INFO',
CLEAR_EVENT_INFO: 'CLEAR_EVENT_INFO',
SKIP_CLICK_INFO: 'SKIP_CLICK_INFO',
CLEAR_CURATION_COUPON: 'CLEAR_CURATION_COUPON',
CLEAR_EVENT_ISSUED_STATUS_SUCCEESS: 'CLEAR_EVENT_ISSUED_STATUS_SUCCEESS',
// coupon actions
GET_PRODUCT_COUPON_INFO: "GET_PRODUCT_COUPON_INFO",
GET_PRODUCT_COUPON_SEARCH: "GET_PRODUCT_COUPON_SEARCH",
GET_PRODUCT_COUPON_DOWNLOAD: "GET_PRODUCT_COUPON_DOWNLOAD",
GET_PRODUCT_COUPON_TOTDOWNLOAD: "GET_PRODUCT_COUPON_TOTDOWNLOAD",
CLEAR_COUPON_INFO: "CLEAR_COUPON_INFO",
CLEAR_GET_PRODUCT_COUPON_DOWNLOAD: "CLEAR_GET_PRODUCT_COUPON_DOWNLOAD",
GET_PRODUCT_COUPON_INFO: 'GET_PRODUCT_COUPON_INFO',
GET_PRODUCT_COUPON_SEARCH: 'GET_PRODUCT_COUPON_SEARCH',
GET_PRODUCT_COUPON_DOWNLOAD: 'GET_PRODUCT_COUPON_DOWNLOAD',
GET_PRODUCT_COUPON_TOTDOWNLOAD: 'GET_PRODUCT_COUPON_TOTDOWNLOAD',
CLEAR_COUPON_INFO: 'CLEAR_COUPON_INFO',
CLEAR_GET_PRODUCT_COUPON_DOWNLOAD: 'CLEAR_GET_PRODUCT_COUPON_DOWNLOAD',
// checkout actions
GET_CHECKOUT_INFO: "GET_CHECKOUT_INFO",
INSERT_MY_INFO_CHECKOUT_ORDER: "INSERT_MY_INFO_CHECKOUT_ORDER",
GET_TAX_INFOS: "GET_TAX_INFOS",
UPDATE_SELECTED_SHIPPING_ADDR: "UPDATE_SELECTED_SHIPPING_ADDR",
UPDATE_SELECTED_BILLING_ADDR: "UPDATE_SELECTED_BILLING_ADDR",
UPDATE_SELECTED_COUPON: "UPDATE_SELECTED_COUPON",
GET_CHECKOUT_TOTAL_AMT: "GET_CHECKOUT_TOTAL_AMT",
CHECKOUT_DATA_RESET: "CHECKOUT_DATA_RESET",
GET_CHECKOUT_INFO: 'GET_CHECKOUT_INFO',
INSERT_MY_INFO_CHECKOUT_ORDER: 'INSERT_MY_INFO_CHECKOUT_ORDER',
GET_TAX_INFOS: 'GET_TAX_INFOS',
UPDATE_SELECTED_SHIPPING_ADDR: 'UPDATE_SELECTED_SHIPPING_ADDR',
UPDATE_SELECTED_BILLING_ADDR: 'UPDATE_SELECTED_BILLING_ADDR',
UPDATE_SELECTED_COUPON: 'UPDATE_SELECTED_COUPON',
GET_CHECKOUT_TOTAL_AMT: 'GET_CHECKOUT_TOTAL_AMT',
CHECKOUT_DATA_RESET: 'CHECKOUT_DATA_RESET',
// order actions
SET_PURCHASE_TERMS_AGREE: "SET_PURCHASE_TERMS_AGREE",
SET_PURCHASE_TERMS_WITHDRAW: "SET_PURCHASE_TERMS_WITHDRAW",
GET_MY_INFO_ORDER_SEARCH: "GET_MY_INFO_ORDER_SEARCH",
GET_MY_INFO_ORDER_SEARCH_CANCEL: "GET_MY_INFO_ORDER_SEARCH_CANCEL",
GET_MY_INFO_ORDER_DETAIL_SEARCH: "GET_MY_INFO_ORDER_DETAIL_SEARCH",
GET_MY_INFO_ORDER_SHIPPING_SEARCH: "GET_MY_INFO_ORDER_SHIPPING_SEARCH",
CLEAR_MY_INFO_ORDER_SEARCH: "CLEAR_MY_INFO_ORDER_SEARCH",
SET_PURCHASE_TERMS_AGREE: 'SET_PURCHASE_TERMS_AGREE',
SET_PURCHASE_TERMS_WITHDRAW: 'SET_PURCHASE_TERMS_WITHDRAW',
GET_MY_INFO_ORDER_SEARCH: 'GET_MY_INFO_ORDER_SEARCH',
GET_MY_INFO_ORDER_SEARCH_CANCEL: 'GET_MY_INFO_ORDER_SEARCH_CANCEL',
GET_MY_INFO_ORDER_DETAIL_SEARCH: 'GET_MY_INFO_ORDER_DETAIL_SEARCH',
GET_MY_INFO_ORDER_SHIPPING_SEARCH: 'GET_MY_INFO_ORDER_SHIPPING_SEARCH',
CLEAR_MY_INFO_ORDER_SEARCH: 'CLEAR_MY_INFO_ORDER_SEARCH',
// play controller
GET_CHAT_LOG: "GET_CHAT_LOG",
GET_SUBTITLE: "GET_SUBTITLE",
CLEAR_PLAYER_INFO: "CLEAR_PLAYER_INFO",
UPDATE_VIDEO_PLAY_STATE: "UPDATE_VIDEO_PLAY_STATE",
GET_CHAT_LOG: 'GET_CHAT_LOG',
GET_SUBTITLE: 'GET_SUBTITLE',
CLEAR_PLAYER_INFO: 'CLEAR_PLAYER_INFO',
UPDATE_VIDEO_PLAY_STATE: 'UPDATE_VIDEO_PLAY_STATE',
// 🔽 [추가] 플레이 제어 매니저 액션 타입
/**
@@ -233,89 +232,89 @@ export const types = {
* SET_PLAYER_CONTROL: 특정 컴포넌트에게 비디오 재생 제어권을 부여합니다.
* CLEAR_PLAYER_CONTROL: 컴포넌트로부터 비디오 재생 제어권을 회수합니다.
*/
SET_PLAYER_CONTROL: "SET_PLAYER_CONTROL",
CLEAR_PLAYER_CONTROL: "CLEAR_PLAYER_CONTROL",
SET_PLAYER_CONTROL: 'SET_PLAYER_CONTROL',
CLEAR_PLAYER_CONTROL: 'CLEAR_PLAYER_CONTROL',
// reset action
RESET_REDUX_STATE: "RESET_REDUX_STATE",
RESET_REDUX_STATE: 'RESET_REDUX_STATE',
// cancel actions
GET_MY_INFO_ORDER_CANCEL_SEARCH: "GET_MY_INFO_ORDER_CANCEL_SEARCH",
GET_MY_INFO_ORDER_CANCEL_COLUMNS_SEARCH:
"GET_MY_INFO_ORDER_CANCEL_COLUMNS_SEARCH",
UPDATE_ORDER_PARTIAL_CANCEL: "UPDATE_ORDER_PARTIAL_CANCEL",
PAYMENT_TOTAL_CANCEL: "PAYMENT_TOTAL_CANCEL",
GET_MY_INFO_ORDER_CANCEL_SEARCH: 'GET_MY_INFO_ORDER_CANCEL_SEARCH',
GET_MY_INFO_ORDER_CANCEL_COLUMNS_SEARCH: 'GET_MY_INFO_ORDER_CANCEL_COLUMNS_SEARCH',
UPDATE_ORDER_PARTIAL_CANCEL: 'UPDATE_ORDER_PARTIAL_CANCEL',
PAYMENT_TOTAL_CANCEL: 'PAYMENT_TOTAL_CANCEL',
// emp actions
GET_SHOPTIME_TERMS: "GET_SHOPTIME_TERMS",
GET_SHOPTIME_TERMS: 'GET_SHOPTIME_TERMS',
// pinCode actions
GET_MY_INFO_CARD_PINCODE_CHECK: "GET_MY_INFO_CARD_PINCODE_CHECK",
GET_MY_INFO_CARD_PINCODE_CHECK: 'GET_MY_INFO_CARD_PINCODE_CHECK',
// new actions
CANCEL_FOCUS_ELEMENT: "CANCEL_FOCUS_ELEMENT",
CANCEL_FOCUS_ELEMENT: 'CANCEL_FOCUS_ELEMENT',
// 약관동의 여부 확인 상태
GET_TERMS_AGREE_YN_START: "GET_TERMS_AGREE_YN_START",
GET_TERMS_AGREE_YN_SUCCESS: "GET_TERMS_AGREE_YN_SUCCESS",
GET_TERMS_AGREE_YN_FAILURE: "GET_TERMS_AGREE_YN_FAILURE",
GET_TERMS_AGREE_YN_START: 'GET_TERMS_AGREE_YN_START',
GET_TERMS_AGREE_YN_SUCCESS: 'GET_TERMS_AGREE_YN_SUCCESS',
GET_TERMS_AGREE_YN_FAILURE: 'GET_TERMS_AGREE_YN_FAILURE',
// device
REQ_REG_DEVICE_INFO: "REQ_REG_DEVICE_INFO",
REQ_REG_DEVICE_INFO: 'REQ_REG_DEVICE_INFO',
// 🔽 [추가] 영구재생 비디오 정보 저장
SET_PERSISTENT_VIDEO_INFO: "SET_PERSISTENT_VIDEO_INFO",
SET_PERSISTENT_VIDEO_INFO: 'SET_PERSISTENT_VIDEO_INFO',
// 🔽 [추가] 배너 비디오 제어 액션 타입
/**
* HomeBanner의 배너 간 비디오 재생 제어를 위한 액션 타입들.
* 첫 번째 배너 상시 재생과 두 번째 배너 포커스 재생을 관리합니다.
*/
SET_BANNER_STATE: "SET_BANNER_STATE",
SET_BANNER_FOCUS: "SET_BANNER_FOCUS",
SET_BANNER_AVAILABILITY: "SET_BANNER_AVAILABILITY",
SET_BANNER_TRANSITION: "SET_BANNER_TRANSITION",
PAUSE_PLAYER_CONTROL: "PAUSE_PLAYER_CONTROL",
RESUME_PLAYER_CONTROL: "RESUME_PLAYER_CONTROL",
SET_BANNER_STATE: 'SET_BANNER_STATE',
SET_BANNER_FOCUS: 'SET_BANNER_FOCUS',
SET_BANNER_AVAILABILITY: 'SET_BANNER_AVAILABILITY',
SET_BANNER_TRANSITION: 'SET_BANNER_TRANSITION',
PAUSE_PLAYER_CONTROL: 'PAUSE_PLAYER_CONTROL',
RESUME_PLAYER_CONTROL: 'RESUME_PLAYER_CONTROL',
// 🔽 [추가] HomeBanner 동영상 포커스 정책 관리
SET_CURRENT_FOCUS_BANNER: "SET_CURRENT_FOCUS_BANNER",
UPDATE_VIDEO_POLICY: "UPDATE_VIDEO_POLICY",
SET_MODAL_BORDER: "SET_MODAL_BORDER",
SET_BANNER_VISIBILITY: "SET_BANNER_VISIBILITY",
SET_CURRENT_FOCUS_BANNER: 'SET_CURRENT_FOCUS_BANNER',
UPDATE_VIDEO_POLICY: 'UPDATE_VIDEO_POLICY',
SET_MODAL_BORDER: 'SET_MODAL_BORDER',
SET_BANNER_VISIBILITY: 'SET_BANNER_VISIBILITY',
// 🔽 [추가] JustForYou 상품 관리 부분
GET_RECENTLY_SAW_ITEM: "GET_RECENTLY_SAW_ITEM",
GET_LIKE_BRAND_PRODUCT: "GET_LIKE_BRAND_PRODUCT",
GET_MORE_TO_CONCIDER_AT_THIS_PRICE: "GET_MORE_TO_CONCIDER_AT_THIS_PRICE",
GET_JUSTFORYOU_INFO: "GET_JUSTFORYOU_INFO",
GET_RECENTLY_SAW_ITEM: 'GET_RECENTLY_SAW_ITEM',
GET_LIKE_BRAND_PRODUCT: 'GET_LIKE_BRAND_PRODUCT',
GET_MORE_TO_CONCIDER_AT_THIS_PRICE: 'GET_MORE_TO_CONCIDER_AT_THIS_PRICE',
GET_JUSTFORYOU_INFO: 'GET_JUSTFORYOU_INFO',
// 🔽 Voice Conductor 관련 액션 타입
VOICE_REGISTER_SUCCESS: "VOICE_REGISTER_SUCCESS",
VOICE_REGISTER_FAILURE: "VOICE_REGISTER_FAILURE",
VOICE_SET_TICKET: "VOICE_SET_TICKET",
VOICE_SET_CONTEXT_SUCCESS: "VOICE_SET_CONTEXT_SUCCESS",
VOICE_SET_CONTEXT_FAILURE: "VOICE_SET_CONTEXT_FAILURE",
VOICE_PERFORM_ACTION: "VOICE_PERFORM_ACTION",
VOICE_REPORT_RESULT_SUCCESS: "VOICE_REPORT_RESULT_SUCCESS",
VOICE_REPORT_RESULT_FAILURE: "VOICE_REPORT_RESULT_FAILURE",
VOICE_UPDATE_INTENTS: "VOICE_UPDATE_INTENTS",
VOICE_CLEAR_STATE: "VOICE_CLEAR_STATE",
VOICE_ADD_LOG: "VOICE_ADD_LOG",
VOICE_CLEAR_LOGS: "VOICE_CLEAR_LOGS",
VOICE_STT_TEXT_RECEIVED: "VOICE_STT_TEXT_RECEIVED", // STT 텍스트 수신
VOICE_REGISTER_SUCCESS: 'VOICE_REGISTER_SUCCESS',
VOICE_REGISTER_FAILURE: 'VOICE_REGISTER_FAILURE',
VOICE_SET_TICKET: 'VOICE_SET_TICKET',
VOICE_SET_CONTEXT_SUCCESS: 'VOICE_SET_CONTEXT_SUCCESS',
VOICE_SET_CONTEXT_FAILURE: 'VOICE_SET_CONTEXT_FAILURE',
VOICE_PERFORM_ACTION: 'VOICE_PERFORM_ACTION',
VOICE_REPORT_RESULT_SUCCESS: 'VOICE_REPORT_RESULT_SUCCESS',
VOICE_REPORT_RESULT_FAILURE: 'VOICE_REPORT_RESULT_FAILURE',
VOICE_UPDATE_INTENTS: 'VOICE_UPDATE_INTENTS',
VOICE_CLEAR_STATE: 'VOICE_CLEAR_STATE',
VOICE_ADD_LOG: 'VOICE_ADD_LOG',
VOICE_CLEAR_LOGS: 'VOICE_CLEAR_LOGS',
VOICE_STT_TEXT_RECEIVED: 'VOICE_STT_TEXT_RECEIVED', // STT 텍스트 수신
// 🔽 Web Speech API 관련
WEB_SPEECH_INITIALIZED: "WEB_SPEECH_INITIALIZED",
WEB_SPEECH_START: "WEB_SPEECH_START",
WEB_SPEECH_INTERIM_RESULT: "WEB_SPEECH_INTERIM_RESULT",
WEB_SPEECH_END: "WEB_SPEECH_END",
WEB_SPEECH_ERROR: "WEB_SPEECH_ERROR",
WEB_SPEECH_CLEANUP: "WEB_SPEECH_CLEANUP",
WEB_SPEECH_INITIALIZED: 'WEB_SPEECH_INITIALIZED',
WEB_SPEECH_START: 'WEB_SPEECH_START',
WEB_SPEECH_INTERIM_RESULT: 'WEB_SPEECH_INTERIM_RESULT',
WEB_SPEECH_FINAL_RESULT: 'WEB_SPEECH_FINAL_RESULT', // ⭐ TV 환경 대응을 위한 final result
WEB_SPEECH_END: 'WEB_SPEECH_END',
WEB_SPEECH_ERROR: 'WEB_SPEECH_ERROR',
WEB_SPEECH_CLEANUP: 'WEB_SPEECH_CLEANUP',
//convert img
CONVERT_PDF_TO_IMAGE: "CONVERT_PDF_TO_IMAGE",
CONVERT_PDF_TO_IMAGE_SUCCESS: "CONVERT_PDF_TO_IMAGE_SUCCESS",
CONVERT_PDF_TO_IMAGE_FAILURE: "CONVERT_PDF_TO_IMAGE_FAILURE",
CLEAR_CONVERTED_IMAGE: "CLEAR_CONVERTED_IMAGE",
CLEAR_CONVERTED_IMAGE_BY_URL: "CLEAR_CONVERTED_IMAGE_BY_URL", // ⭐ 새로 추가
CONVERT_PDF_TO_IMAGE: 'CONVERT_PDF_TO_IMAGE',
CONVERT_PDF_TO_IMAGE_SUCCESS: 'CONVERT_PDF_TO_IMAGE_SUCCESS',
CONVERT_PDF_TO_IMAGE_FAILURE: 'CONVERT_PDF_TO_IMAGE_FAILURE',
CLEAR_CONVERTED_IMAGE: 'CLEAR_CONVERTED_IMAGE',
CLEAR_CONVERTED_IMAGE_BY_URL: 'CLEAR_CONVERTED_IMAGE_BY_URL', // ⭐ 새로 추가
};

View File

@@ -50,19 +50,29 @@ export const initializeWebSpeech =
webSpeechService.on('result', (result) => {
console.log(
`[VoiceInput]-[WebSpeech] ACTION-EVENT: WEB_SPEECH_INTERIM_RESULT 디스패치 - isFinal=${result.isFinal}, text="${result.transcript}"`
`[VoiceInput]-[WebSpeech] ACTION-EVENT: result 수신 - isFinal=${result.isFinal}, text="${result.transcript}"`
);
// ✅ continuous: true 모드에서는 중간 final result를 무시하고
// 항상 interim result만 사용 (15초 타이머로 제어)
// Interim 결과 (중간 결과) - 전체 연결된 텍스트 사용
// Interim 결과 (중간 결과) - 항상 디스패치하여 실시간 표시
dispatch({
type: types.WEB_SPEECH_INTERIM_RESULT,
payload: result.transcript, // 이미 전체 연결된 텍스트 (final + interim)
payload: result.transcript, // 전체 연결된 텍스트 (final + interim)
});
// ✅ Final 결과는 무시 (15초 타이머가 끝날 때 VoiceInputOverlay에서 처리)
// continuous: true일 때 중간에 final이 와도 계속 듣기
// ✅ Final 결과 처리 추가 (TV 환경 대응)
// TV에서는 final result가 와야 API 호출이 가능할 수 있음
if (result.isFinal) {
console.log(
`[VoiceInput]-[WebSpeech] ACTION-EVENT: WEB_SPEECH_FINAL_RESULT 디스패치 - finalText="${result.transcript}"`
);
dispatch({
type: types.WEB_SPEECH_FINAL_RESULT,
payload: {
finalText: result.transcript,
confidence: result.confidence,
},
});
}
});
webSpeechService.on('error', (errorInfo) => {

View File

@@ -36,6 +36,8 @@ const initialState = {
isInitialized: false,
isListening: false,
interimText: null,
finalText: null, // ⭐ TV 환경 대응을 위한 final text
confidence: null,
error: null,
},
};
@@ -159,6 +161,8 @@ export const voiceReducer = (state = initialState, action) => {
...state.webSpeech,
isListening: true,
interimText: null,
finalText: null, // ⭐ 새로운 인식 시작 시 초기화
confidence: null,
error: null,
},
};
@@ -172,13 +176,29 @@ export const voiceReducer = (state = initialState, action) => {
},
};
case types.WEB_SPEECH_FINAL_RESULT:
// ⭐ TV 환경 대응: Final result 처리
return {
...state,
webSpeech: {
...state.webSpeech,
finalText: action.payload.finalText,
confidence: action.payload.confidence,
interimText: action.payload.finalText, // final이 오면 interim도 업데이트
},
lastSTTText: action.payload.finalText, // ⭐ API 호출을 위한 STT 텍스트 업데이트
sttTimestamp: new Date().toISOString(),
};
case types.WEB_SPEECH_END:
return {
...state,
webSpeech: {
...state.webSpeech,
isListening: false,
interimText: null,
// ⭐ END 시에는 텍스트를 유지 (API 호출을 위해)
// interimText: null,
// finalText: null,
},
};
@@ -199,6 +219,8 @@ export const voiceReducer = (state = initialState, action) => {
isInitialized: false,
isListening: false,
interimText: null,
finalText: null,
confidence: null,
error: null,
},
};

View File

@@ -22,6 +22,7 @@ const VoiceDebugDashboard = ({
lastSTTText,
sttResponseText,
interimText,
finalText,
errorMessage,
voiceInputMode,
isBubbleClickSearch,
@@ -116,6 +117,18 @@ const VoiceDebugDashboard = ({
<span className={css.textLabel}>Interim Text:</span>
<div className={css.textContent}>{interimText || '(empty)'}</div>
</div>
<div className={css.textItem}>
<span className={css.textLabel}> Final Text (isFinal):</span>
<div
className={css.textContent}
style={{
backgroundColor: finalText ? '#2d5016' : 'inherit',
padding: finalText ? '8px' : '0',
}}
>
{finalText || '(empty)'}
</div>
</div>
<div className={css.textItem}>
<span className={css.textLabel}>STT Response Text:</span>
<div className={css.textContent}>{sttResponseText || '(empty)'}</div>
@@ -185,6 +198,7 @@ VoiceDebugDashboard.propTypes = {
lastSTTText: PropTypes.string,
sttResponseText: PropTypes.string,
interimText: PropTypes.string,
finalText: PropTypes.string,
errorMessage: PropTypes.string,
voiceInputMode: PropTypes.string,
isBubbleClickSearch: PropTypes.bool,
@@ -196,6 +210,7 @@ VoiceDebugDashboard.defaultProps = {
lastSTTText: '',
sttResponseText: '',
interimText: '',
finalText: '',
errorMessage: '',
voiceInputMode: null,
isBubbleClickSearch: false,

View File

@@ -196,6 +196,46 @@ const VoiceInputOverlay = ({
}
}, [isVisible, currentMode]);
// 이벤트 타입별 색상 분류
const getWebSpeechEventType = useCallback((event) => {
const types = {
INIT: 'info',
START: 'success',
RESULT: 'info',
RESULT_FINAL: 'success',
ERROR: 'error',
END: 'warning',
STOP: 'warning',
ABORT: 'error',
RESTART: 'info',
};
return types[event] || 'info';
}, []);
// 🎤 WebSpeech 이벤트 로그 관리 함수
const addWebSpeechEventLog = useCallback(
(event, details = '') => {
const timestamp = new Date().toLocaleTimeString();
const newLog = {
id: Date.now(),
timestamp,
event,
details,
type: getWebSpeechEventType(event),
};
if (DEBUG_MODE) {
console.log(`[WebSpeech Event] ${event}: ${details}`);
}
setWebSpeechEventLogs((prev) => {
const updated = [newLog, ...prev];
return updated.slice(0, 10); // 최근 10개만 유지
});
},
[getWebSpeechEventType]
);
// ⭐ currentMode 변경 추적
useEffect(() => {
console.log('🔀 [DEBUG][VoiceInputOverlay] currentMode changed to:', currentMode);
@@ -270,46 +310,6 @@ const VoiceInputOverlay = ({
// Redux에서 STT 결과 및 에러 상태 가져오기
const { lastSTTText, sttTimestamp, webSpeech = {} } = useSelector((state) => state.voice);
// 이벤트 타입별 색상 분류
const getWebSpeechEventType = useCallback((event) => {
const types = {
INIT: 'info',
START: 'success',
RESULT: 'info',
RESULT_FINAL: 'success',
ERROR: 'error',
END: 'warning',
STOP: 'warning',
ABORT: 'error',
RESTART: 'info',
};
return types[event] || 'info';
}, []);
// 🎤 WebSpeech 이벤트 로그 관리 함수
const addWebSpeechEventLog = useCallback(
(event, details = '') => {
const timestamp = new Date().toLocaleTimeString();
const newLog = {
id: Date.now(),
timestamp,
event,
details,
type: getWebSpeechEventType(event),
};
if (DEBUG_MODE) {
console.log(`[WebSpeech Event] ${event}: ${details}`);
}
setWebSpeechEventLogs((prev) => {
const updated = [newLog, ...prev];
return updated.slice(0, 10); // 최근 10개만 유지
});
},
[getWebSpeechEventType]
);
// Redux에서 shopperHouse 검색 결과 및 에러 가져오기 (simplified ref usage)
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
const shopperHouseSearchId = useSelector((state) => state.search.shopperHouseSearchId); // 2차 발화용 searchId
@@ -340,6 +340,53 @@ const VoiceInputOverlay = ({
}
}, [shopperHouseData, isVisible]);
// 🔄 WebSpeech 에러 재시작 함수
const restartWebSpeech = useCallback(() => {
if (DEBUG_MODE) {
console.log('[VoiceInput] 🔄 Restarting WebSpeech after error');
}
// 이벤트 로그 기록
addWebSpeechEventLog('RESTART', 'WebSpeech restarted after error or manual action');
// 에러 상태 정리 (Redux)
dispatch(clearSTTText());
// 모든 타이머 정리
clearTimerRef(listeningTimerRef);
clearTimerRef(silenceDetectionTimerRef);
clearIntervalRef(countdownIntervalRef);
// Ref 초기화
listeningTimerRef.current = null;
silenceDetectionTimerRef.current = null;
countdownIntervalRef.current = null;
interimTextRef.current = '';
// 에러 메시지 정리
setErrorMessage('');
// WebSpeech 이벤트 로그 정리
setWebSpeechEventLogs([]);
// Input 필드 초기화
if (onSearchChange) {
onSearchChange({ value: '' });
}
// PROMPT 모드로 복귀
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
setIsBubbleClickSearch(false); // bubble 클릭 상태 초기화
// 약간의 지연 후 새로 시작 (안정성을 위해)
setTimeout(() => {
if (DEBUG_MODE) {
console.log('[VoiceInput] ✅ Restart complete - ready for new input');
}
}, 300);
}, [dispatch, onSearchChange, addWebSpeechEventLog]);
// 🚨 WebSpeech 에러 감시 및 처리 (Phase 1)
useEffect(() => {
// Bubble 클릭으로 검색이 시작된 경우 WebSpeech 오류를 우회
@@ -356,19 +403,19 @@ const VoiceInputOverlay = ({
typeof webSpeech.error === 'string'
? webSpeech.error
: webSpeech.error.error || webSpeech.error;
const errorMessage =
const errorMessageWebSpeech =
typeof webSpeech.error === 'object'
? webSpeech.error.message || 'No message'
: webSpeech.message || 'No message';
console.error('[VoiceInput] 🔴 WebSpeech error detected:', errorType);
console.error('[VoiceInput] ├─ error type:', errorType);
console.error('[VoiceInput] ├─ message:', errorMessage);
console.error('[VoiceInput] ├─ message:', errorMessageWebSpeech);
console.error('[VoiceInput] ├─ current mode:', currentMode);
console.error('[VoiceInput] └─ is listening:', isListening);
// WebSpeech 이벤트 로그 기록
addWebSpeechEventLog('ERROR', `${errorType}: ${errorMessage}`);
addWebSpeechEventLog('ERROR', `${errorType}: ${errorMessageWebSpeech}`);
// 사용자에게 표시할 에러 메시지 생성
let userErrorMessage = '음성 인식에 문제가 발생했습니다.';
@@ -429,6 +476,7 @@ const VoiceInputOverlay = ({
restartWebSpeech,
addWebSpeechEventLog,
isBubbleClickSearch,
webSpeech,
]);
// 🎤 WebSpeech 이벤트 감지 (전용 디버그용)
@@ -653,6 +701,87 @@ const VoiceInputOverlay = ({
}
}, [isVisible, voiceVersion, isSupported]);
// 🎤 음성 입력 최종 처리 함수 (15초 타이머 & 3초 silence detection 공통 사용)
const processFinalVoiceInput = useCallback(
(source) => {
console.log(`[VoiceInput] 🏁 음성 입력 종료 (${source})`);
// 모든 타이머 정리 및 ref 초기화
clearTimerRef(listeningTimerRef);
clearIntervalRef(countdownIntervalRef);
clearTimerRef(silenceDetectionTimerRef);
// ✅ Ref 명시적 초기화 (중요!)
listeningTimerRef.current = null;
countdownIntervalRef.current = null;
silenceDetectionTimerRef.current = null;
// 음성 인식 중지
stopListening();
// ✨ TV 환경 대응: Redux의 finalText(isFinal)를 우선 사용, 없으면 interimTextRef 사용
const reduxFinalText = webSpeech?.finalText;
const interimRefText = interimTextRef.current.trim();
// 우선순위: 1. Redux finalText (isFinal) 2. interimTextRef (누적)
const finalText =
reduxFinalText && reduxFinalText.trim().length >= 3
? reduxFinalText.trim()
: interimRefText;
console.log('[VoiceInput] ├─ Redux finalText:', reduxFinalText || '(없음)');
console.log('[VoiceInput] ├─ interimRef:', interimRefText || '(없음)');
console.log('[VoiceInput] └─ 사용할 텍스트:', finalText, `(길이: ${finalText.length})`);
if (finalText && finalText.length >= 3) {
// STT 텍스트 저장
setSttResponseText(finalText);
// RESPONSE 모드로 전환
setCurrentMode(VOICE_MODES.RESPONSE);
setVoiceInputMode(null);
// ✨ 검색 기록에 추가
addToSearchHistory(finalText);
// ✨ ShopperHouse API 자동 호출 (2차 발화 시 searchId 포함)
const query = finalText.trim();
// ✅ Ref에서 최신 searchId 읽기 (useCallback closure 문제 해결)
const currentSearchId = shopperHouseSearchIdRef.current;
console.log('[VoiceInput] 📤 API 요청 전송');
console.log('[VoiceInput] ├─ query:', query);
console.log('[VoiceInput] ├─ ref 값:', shopperHouseSearchIdRef.current);
console.log('[VoiceInput] ├─ currentSearchId:', currentSearchId);
console.log('[VoiceInput] └─ searchId:', currentSearchId || '(없음 - 첫 번째 발화)');
// API 호출 이벤트 로깅
const searchIdInfo = currentSearchId
? `(2nd query, searchId: ${currentSearchId.substring(0, 8)}...)`
: '(1st query, no searchId)';
addWebSpeechEventLog('API_REQUEST', `Query: "${query}" ${searchIdInfo}`);
dispatch(getShopperHouseSearch(query, currentSearchId));
} else {
// 입력이 없거나 너무 짧으면 PROMPT 모드로 복귀
console.log('[VoiceInput] ⚠️ 입력 없음 또는 너무 짧음 - PROMPT 모드로 복귀');
console.log('[VoiceInput] └─ finalText 길이:', finalText.length);
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
}
// Ref 초기화
interimTextRef.current = '';
},
[
stopListening,
addToSearchHistory,
dispatch,
addWebSpeechEventLog,
webSpeech, // ✨ Redux finalText를 사용하기 위해 dependency 추가
// ✅ shopperHouseSearchId 제거 - ref 사용으로 closure 문제 해결
]
);
// ⛔ 독립 테스트: WebSpeech API 호출 비활성화
// WebSpeech 모드로 전환되면 자동으로 음성 인식 시작
// useEffect(() => {
@@ -675,8 +804,6 @@ const VoiceInputOverlay = ({
// }
// }, [voiceInputMode, currentMode, isListening, startListening, stopListening]);
// 이 useEffect 제거 - renderModeContent()에서 직접 판단하므로 불필요
// 🎤 Interim 텍스트 실시간 표시 및 ref 업데이트 + 3초 silence detection
useEffect(() => {
if (currentMode !== VOICE_MODES.LISTENING) {
@@ -712,7 +839,7 @@ const VoiceInputOverlay = ({
clearTimerRef(silenceDetectionTimerRef);
silenceDetectionTimerRef.current = null;
};
}, [interimText, currentMode, onSearchChange, processFinalVoiceInput]);
}, [interimText, currentMode, onSearchChange, processFinalVoiceInput, addWebSpeechEventLog]);
// 🎉 Wake Word Detection: PROMPT 모드에서 백그라운드 리스닝 시작
useEffect(() => {
@@ -924,121 +1051,6 @@ const VoiceInputOverlay = ({
}
}, [dispatch, sttResponseText]);
// 🔄 WebSpeech 에러 재시작 함수
const restartWebSpeech = useCallback(() => {
if (DEBUG_MODE) {
console.log('[VoiceInput] 🔄 Restarting WebSpeech after error');
}
// 이벤트 로그 기록
addWebSpeechEventLog('RESTART', 'WebSpeech restarted after error or manual action');
// 에러 상태 정리 (Redux)
dispatch(clearSTTText());
// 모든 타이머 정리
clearTimerRef(listeningTimerRef);
clearTimerRef(silenceDetectionTimerRef);
clearIntervalRef(countdownIntervalRef);
// Ref 초기화
listeningTimerRef.current = null;
silenceDetectionTimerRef.current = null;
countdownIntervalRef.current = null;
interimTextRef.current = '';
// 에러 메시지 정리
setErrorMessage('');
// WebSpeech 이벤트 로그 정리
setWebSpeechEventLogs([]);
// Input 필드 초기화
if (onSearchChange) {
onSearchChange({ value: '' });
}
// PROMPT 모드로 복귀
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
setIsBubbleClickSearch(false); // bubble 클릭 상태 초기화
// 약간의 지연 후 새로 시작 (안정성을 위해)
setTimeout(() => {
if (DEBUG_MODE) {
console.log('[VoiceInput] ✅ Restart complete - ready for new input');
}
}, 300);
}, [dispatch, onSearchChange, addWebSpeechEventLog]);
// 🎤 음성 입력 최종 처리 함수 (15초 타이머 & 3초 silence detection 공통 사용)
const processFinalVoiceInput = useCallback(
(source) => {
console.log(`[VoiceInput] 🏁 음성 입력 종료 (${source})`);
// 모든 타이머 정리 및 ref 초기화
clearTimerRef(listeningTimerRef);
clearIntervalRef(countdownIntervalRef);
clearTimerRef(silenceDetectionTimerRef);
// ✅ Ref 명시적 초기화 (중요!)
listeningTimerRef.current = null;
countdownIntervalRef.current = null;
silenceDetectionTimerRef.current = null;
// 음성 인식 중지
stopListening();
// ✅ 누적된 interimText를 최종 결과로 사용
const finalText = interimTextRef.current.trim();
console.log('[VoiceInput] └─ 최종 텍스트:', finalText);
if (finalText && finalText.length >= 3) {
// STT 텍스트 저장
setSttResponseText(finalText);
// RESPONSE 모드로 전환
setCurrentMode(VOICE_MODES.RESPONSE);
setVoiceInputMode(null);
// ✨ 검색 기록에 추가
addToSearchHistory(finalText);
// ✨ ShopperHouse API 자동 호출 (2차 발화 시 searchId 포함)
const query = finalText.trim();
// ✅ Ref에서 최신 searchId 읽기 (useCallback closure 문제 해결)
const currentSearchId = shopperHouseSearchIdRef.current;
console.log('[VoiceInput] 📤 API 요청 전송');
console.log('[VoiceInput] ├─ query:', query);
console.log('[VoiceInput] ├─ ref 값:', shopperHouseSearchIdRef.current);
console.log('[VoiceInput] ├─ currentSearchId:', currentSearchId);
console.log('[VoiceInput] └─ searchId:', currentSearchId || '(없음 - 첫 번째 발화)');
// API 호출 이벤트 로깅
const searchIdInfo = currentSearchId
? `(2nd query, searchId: ${currentSearchId.substring(0, 8)}...)`
: '(1st query, no searchId)';
addWebSpeechEventLog('API_REQUEST', `Query: "${query}" ${searchIdInfo}`);
dispatch(getShopperHouseSearch(query, currentSearchId));
} else {
// 입력이 없거나 너무 짧으면 PROMPT 모드로 복귀
console.log('[VoiceInput] ⚠️ 입력 없음 또는 너무 짧음 - PROMPT 모드로 복귀');
setCurrentMode(VOICE_MODES.PROMPT);
setVoiceInputMode(null);
}
// Ref 초기화
interimTextRef.current = '';
},
[
stopListening,
addToSearchHistory,
dispatch,
// ✅ shopperHouseSearchId 제거 - ref 사용으로 closure 문제 해결
]
);
// Overlay 닫기 핸들러 (모든 닫기 동작을 통합)
const handleClose = useCallback(() => {
if (DEBUG_MODE) {
@@ -1119,7 +1131,7 @@ const VoiceInputOverlay = ({
}
}
},
[onSearchChange, dispatch, addToSearchHistory]
[onSearchChange, dispatch, addToSearchHistory, addWebSpeechEventLog]
);
// Input 창에서 API 호출 핸들러 (돋보기 아이콘 클릭 시에만)
@@ -1387,7 +1399,15 @@ const VoiceInputOverlay = ({
handleClose();
}
},
[currentMode, handleClose, startListening, onSearchChange, dispatch, processFinalVoiceInput]
[
currentMode,
handleClose,
startListening,
onSearchChange,
dispatch,
processFinalVoiceInput,
addWebSpeechEventLog,
]
);
// 마이크 버튼 키다운 핸들러
@@ -1705,6 +1725,7 @@ const VoiceInputOverlay = ({
lastSTTText={lastSTTText}
sttResponseText={sttResponseText}
interimText={interimText}
finalText={webSpeech?.finalText}
errorMessage={errorMessage}
voiceInputMode={voiceInputMode}
isBubbleClickSearch={isBubbleClickSearch}

View File

@@ -587,8 +587,8 @@
}
.dashboardTitle {
font-size: 32px; // 24px → 32px
font-weight: bold;
font-size: 36px; // 32px → 36px (더 크게)
font-weight: 900; // bold → 900 (더 굵게)
color: #FFB81C; // Shoptime 브랜드 색상
letter-spacing: 0.5px;
}
@@ -637,8 +637,8 @@
h3 {
margin: 0 0 16px 0;
font-size: 24px; // 20px → 24px
font-weight: bold;
font-size: 28px; // 24px → 28px (더 크게)
font-weight: 900; // bold → 900 (더 굵게)
color: #4CAF50;
border-bottom: 2px solid rgba(76, 175, 80, 0.3);
padding-bottom: 12px;
@@ -650,7 +650,8 @@
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
font-size: 18px; // 16px → 18px
font-size: 22px; // 18px → 22px (더 크게)
font-weight: 700; // 전체 텍스트 굵게
@media (max-width: 1400px) {
grid-template-columns: 1fr;
@@ -667,18 +668,18 @@
border-left: 3px solid rgba(255, 255, 255, 0.1);
.stateLabel {
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
font-weight: 800; // 600 → 800 (더 굵게)
min-width: 120px;
font-size: 16px; // 명시적 크기 지정
font-size: 20px; // 16px → 20px (더 크게)
}
.stateValue {
color: #2196F3;
font-weight: 500;
font-weight: 700; // 500 → 700 (더 굵게)
word-break: break-all;
max-width: 60%;
font-size: 16px; // 명시적 크기 지정
font-size: 20px; // 16px → 20px (더 크게)
&.active {
color: #4CAF50;
@@ -699,9 +700,9 @@
gap: 8px;
.textLabel {
color: rgba(255, 255, 255, 0.7);
font-size: 16px; // 14px → 16px
font-weight: 600;
color: rgba(255, 255, 255, 0.8);
font-size: 20px; // 16px → 20px (더 크게)
font-weight: 800; // 600 → 800 (더 굵게)
text-transform: uppercase;
letter-spacing: 0.5px;
}
@@ -713,7 +714,8 @@
padding: 14px; // 12px → 14px
color: #4CAF50;
font-family: monospace;
font-size: 16px; // 14px → 16px
font-size: 20px; // 16px → 20px (더 크게)
font-weight: 700; // 굵게 추가
min-height: 50px; // 40px → 50px
max-height: 120px; // 100px → 120px
overflow-y: auto;
@@ -730,7 +732,8 @@
padding: 14px; // 12px → 14px
border-radius: 4px;
color: #FF6B6B;
font-size: 17px; // 15px → 17px
font-size: 21px; // 17px → 21px (더 크게)
font-weight: 700; // 굵게 추가
line-height: 1.6; // 1.5 → 1.6
}
@@ -755,7 +758,8 @@
padding: 14px; // 12px → 14px
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
align-items: flex-start;
font-size: 16px; // 14px → 16px
font-size: 20px; // 16px → 20px (더 크게)
font-weight: 700; // 굵게 추가
transition: background 0.2s ease;
&:last-child {
@@ -775,14 +779,15 @@
.timelineIndex {
min-width: 30px;
text-align: right;
color: rgba(255, 255, 255, 0.4);
font-size: 14px; // 12px → 14px
color: rgba(255, 255, 255, 0.6);
font-size: 18px; // 14px → 18px (더 크게)
font-weight: 700; // 굵게 추가
font-family: monospace;
}
.timelineIcon {
min-width: 24px;
font-size: 20px; // 18px → 20px
font-size: 24px; // 20px → 24px (더 크게)
text-align: center;
}
@@ -797,33 +802,37 @@
margin-bottom: 4px;
.eventName {
font-weight: 600;
font-weight: 800; // 600 → 800 (더 굵게)
color: white;
min-width: 120px;
font-size: 16px; // 명시적 크기 지정
font-size: 20px; // 16px → 20px (더 크게)
}
.eventTime {
color: rgba(255, 255, 255, 0.5);
font-size: 14px; // 12px → 14px
color: rgba(255, 255, 255, 0.7);
font-size: 18px; // 14px → 18px (더 크게)
font-weight: 600; // 굵게 추가
font-family: monospace;
}
}
.timelineDetails {
color: rgba(255, 255, 255, 0.7);
color: rgba(255, 255, 255, 0.8);
margin-left: 0;
word-break: break-word;
padding: 4px 0;
font-family: monospace;
font-size: 15px; // 13px → 15px
font-size: 19px; // 15px → 19px (더 크게)
font-weight: 600; // 굵게 추가
}
.noEvents {
text-align: center;
padding: 40px;
color: rgba(255, 255, 255, 0.3);
color: rgba(255, 255, 255, 0.5);
font-style: italic;
font-size: 22px; // 더 크게
font-weight: 700; // 굵게 추가
}
// 스크롤바 스타일링 (대시보드용)

View File

@@ -16,13 +16,16 @@ const PromptContainer = SpotlightContainerDecorator(
);
const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestionClick }) => {
const handleBubbleClick = (suggestion) => {
console.log('💡 [DEBUG][VoicePromptScreen] Bubble clicked:', suggestion);
// 커링 패턴: suggestion을 미리 바인딩하는 핸들러 생성
const createBubbleClickHandler = (suggestion) => {
return () => {
console.log('💡 [DEBUG][VoicePromptScreen] Bubble clicked:', suggestion);
// 부모 컴포넌트로 suggestion 텍스트 전달 (API 호출은 부모에서 처리)
if (onSuggestionClick) {
onSuggestionClick(suggestion);
}
// 부모 컴포넌트로 suggestion 텍스트 전달 (API 호출은 부모에서 처리)
if (onSuggestionClick) {
onSuggestionClick(suggestion);
}
};
};
return (
@@ -38,7 +41,7 @@ const VoicePromptScreen = ({ title = 'Try saying', suggestions = [], onSuggestio
<SpottableBubble
key={index}
className={css.bubbleMessage}
onClick={() => handleBubbleClick(suggestion)}
onClick={createBubbleClickHandler(suggestion)}
spotlightId={`voice-bubble-${index}`}
>
<div className={css.bubbleText}>{suggestion}</div>