[251017] fix: VUI disable

🕐 커밋 시간: 2025. 10. 17. 17:14:07

📊 변경 통계:
  • 총 파일: 9개
  • 추가: +201줄
  • 삭제: -45줄

📁 추가된 파일:
  + com.twin.app.shoptime/ai_poc_list.json
  + com.twin.app.shoptime/src/constants/featureFlags.js

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/App.js
  ~ com.twin.app.shoptime/src/actions/voiceActions.js
  ~ com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx
  ~ com.twin.app.shoptime/src/hooks/useSearchVoice.js
  ~ com.twin.app.shoptime/src/views/MainView/MainView.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/App/App.js (javascript):
    🔄 Modified: function()
  📄 com.twin.app.shoptime/src/actions/voiceActions.js (javascript):
     Added: addLog()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
This commit is contained in:
2025-10-17 17:14:09 +09:00
parent 58641c1bac
commit 0e6da8922d
9 changed files with 668 additions and 43 deletions

View File

@@ -0,0 +1,442 @@
[
{
"index": 1,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Can you recommend a 4K TV with Dolby Atmos support under $1,500?",
"secondUtter": "From those, can you narrow it down to models with at least 1,000 nits brightness?"
},
{
"index": 2,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Im looking for a white bezel TV that matches modern living room decor—any suggestions?",
"secondUtter": "From those, can you pick ones with ultra-thin bezels and a minimalist stand design?"
},
{
"index": 3,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Whats a good value 55-inch smart TV with an excellent game mode?",
"secondUtter": "From those, can you recommend only models that support 120Hz refresh rate?"
},
{
"index": 4,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Which TVs are best optimized for Netflix and Disney+ streaming?",
"secondUtter": "From those, can you choose ones with the fastest app launch times?"
},
{
"index": 5,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Can you suggest a 65-inch TV thats easy to mount on the wall?",
"secondUtter": "From those, can you narrow it to models weighing under 20kg?"
},
{
"index": 6,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Whats the best gaming monitor with 144Hz refresh rate and QHD resolution?",
"secondUtter": "From those, can you pick only models with 1ms or lower response time?"
},
{
"index": 7,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Can you recommend a monitor with USB-C that can also charge a laptop?",
"secondUtter": "From those, can you choose ones that support 90W or higher charging?"
},
{
"index": 8,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Whats a good monitor for video editing with accurate color calibration?",
"secondUtter": "From those, can you narrow it to models with 99% AdobeRGB coverage?"
},
{
"index": 9,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Which monitors are easiest on the eyes for long office hours?",
"secondUtter": "From those, can you pick ones with built-in blue light reduction?"
},
{
"index": 10,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Which monitors with HDMI 2.1 are ideal for console gaming?",
"secondUtter": "From those, can you choose ones that support 4K at 120Hz output?"
},
{
"index": 11,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Whats the best ultra-light laptop under 1.2kg with long battery life?",
"secondUtter": "From those, can you narrow it to models with at least 20 hours battery life?"
},
{
"index": 12,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Can you suggest an $800 laptop for college work and streaming videos?",
"secondUtter": "From those, can you pick ones with a backlit keyboard?"
},
{
"index": 13,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Which laptops have touchscreens and 360-degree hinges?",
"secondUtter": "From those, can you choose ones that support pen input?"
},
{
"index": 14,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Whats a good laptop for AI and machine learning tasks with a strong GPU?",
"secondUtter": "From those, can you narrow it to models with 32GB or more RAM?"
},
{
"index": 15,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Can you recommend a gaming laptop with excellent cooling performance?",
"secondUtter": "From those, can you pick ones with noise levels under 40dB?"
},
{
"index": 16,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Can you recommend a microwave with an air fryer function?",
"secondUtter": "From those, can you choose ones with at least 30L capacity?"
},
{
"index": 17,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Which electric ovens heat up quickly?",
"secondUtter": "From those, can you narrow it to models that preheat in under 5 minutes?"
},
{
"index": 18,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Are there microwaves you can control with a smartphone app?",
"secondUtter": "From those, can you pick ones with the most stable Wi-Fi connection?"
},
{
"index": 19,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Can you suggest a stylish black gas range?",
"secondUtter": "From those, can you choose ones with four burners?"
},
{
"index": 20,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Which stainless steel ovens are easiest to clean?",
"secondUtter": "From those, can you narrow it to models with ceramic-coated interiors?"
},
{
"index": 21,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Whats a quiet, compact refrigerator for small apartments?",
"secondUtter": "From those, can you choose ones with top energy efficiency ratings?"
},
{
"index": 22,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Are there fridges with transparent doors so you can see inside?",
"secondUtter": "From those, can you pick ones with thinner doors to save space?"
},
{
"index": 23,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Which bottom-freezer refrigerators offer the best value?",
"secondUtter": "From those, can you narrow it to models with at least 100L freezer capacity?"
},
{
"index": 24,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Can you suggest a refrigerator with a wine storage feature?",
"secondUtter": "From those, can you choose ones that hold 20 or more bottles?"
},
{
"index": 25,
"partner": "LG",
"category1": "",
"category2": "",
"firstUtter": "Whats the best large-capacity fridge for big families?",
"secondUtter": "From those, can you narrow it to models with four doors?"
},
{
"index": 26,
"partner": "QVC",
"category1": "Beauty",
"category2": "Fragrance",
"firstUtter": "Can you recommend a light, fresh fragrance for daily wear?",
"secondUtter": "From those, can you pick ones that last at least 8 hours?"
},
{
"index": 27,
"partner": "QVC",
"category1": "Beauty",
"category2": "Hair Care",
"firstUtter": "Whats the best hair oil for repairing damaged hair?",
"secondUtter": "From those, can you choose ones without silicone?"
},
{
"index": 28,
"partner": "QVC",
"category1": "Beauty",
"category2": "Makeup",
"firstUtter": "Which foundations provide good coverage while still looking natural?",
"secondUtter": "From those, can you pick ones suitable for sensitive skin?"
},
{
"index": 29,
"partner": "QVC",
"category1": "Beauty",
"category2": "Skincare",
"firstUtter": "Can you recommend a daily moisturizer that provides long-lasting hydration?",
"secondUtter": "From those, can you choose ones suitable for teenagers?"
},
{
"index": 30,
"partner": "QVC",
"category1": "Beauty",
"category2": "Personal Care",
"firstUtter": "From those, can you pick ones with whitening and anti-wrinkle benefits?",
"secondUtter": "Which body lotions are best for intense hydration in winter?"
},
{
"index": 31,
"partner": "QVC",
"category1": "Beauty",
"category2": "Skincare (Seasonal)",
"firstUtter": "What creams help prevent dryness in late autumn to early winter?",
"secondUtter": "From those, can you pick ones with herbal scents?"
},
{
"index": 32,
"partner": "QVC",
"category1": "Fashion",
"category2": "Shoes",
"firstUtter": "Can you recommend trendy sneakers that go well with casual outfits?",
"secondUtter": "From those, can you choose ones with extra arch support?"
},
{
"index": 33,
"partner": "QVC",
"category1": "Fashion",
"category2": "Shoes",
"firstUtter": "Which mens boots are durable enough for both work and casual wear?",
"secondUtter": "From those, can you pick ones made of waterproof leather?"
},
{
"index": 34,
"partner": "QVC",
"category1": "Fashion",
"category2": "Womens Clothing",
"firstUtter": "What kind of dress works best for a wedding guest outfit?",
"secondUtter": "From those, can you choose ones under $200?"
},
{
"index": 35,
"partner": "QVC",
"category1": "Fashion",
"category2": "Womens Clothing",
"firstUtter": "Which brands make the warmest yet lightweight winter coats?",
"secondUtter": "From those, can you pick ones with removable liners?"
},
{
"index": 36,
"partner": "QVC",
"category1": "Fashion",
"category2": "Womens Clothing",
"firstUtter": "From those, can you choose ones with tummy control features?",
"secondUtter": "What swimsuits are most flattering for different body types?"
},
{
"index": 37,
"partner": "QVC",
"category1": "Fashion",
"category2": "Outerwear (Seasonal)",
"firstUtter": "What trendy puffer jackets are best for wearing in November?",
"secondUtter": "From those, can you pick ones with lightweight insulation?"
},
{
"index": 38,
"partner": "QVC",
"category1": "Foods",
"category2": "Frozen Foods",
"firstUtter": "What are the best frozen ready meals for a quick and healthy dinner?",
"secondUtter": "From those, can you choose ones under 500 calories per serving?"
},
{
"index": 39,
"partner": "QVC",
"category1": "Foods",
"category2": "Frozen Foods",
"firstUtter": "Which frozen pizzas taste the most like restaurant-style?",
"secondUtter": "From those, can you pick ones with thin crust?"
},
{
"index": 40,
"partner": "QVC",
"category1": "Foods",
"category2": "Snack & Sweet",
"firstUtter": "From those, can you choose ones with less than 150mg sodium per serving?",
"secondUtter": "What are the most popular potato chips for parties?"
},
{
"index": 41,
"partner": "QVC",
"category1": "Foods",
"category2": "Bakery & Beverage",
"firstUtter": "Which cakes or pastries are the most popular for birthday celebrations?",
"secondUtter": "From those, can you pick ones available in gluten-free options?"
},
{
"index": 42,
"partner": "QVC",
"category1": "Foods",
"category2": "Snack & Sweet",
"firstUtter": "From those, can you choose ones with at least 15g protein per bar?",
"secondUtter": "Which protein or energy bars are the most filling and nutritious?"
},
{
"index": 43,
"partner": "QVC",
"category1": "Foods",
"category2": "Bakery & Beverage (Seasonal)",
"firstUtter": "What warm-flavored seasonal desserts are perfect for November?",
"secondUtter": "From those, can you pick ones with cinnamon or pumpkin spice?"
},
{
"index": 44,
"partner": "QVC",
"category1": "Home",
"category2": "Bedding & Bath",
"firstUtter": "What sheet sets are best for keeping cool during summer nights?",
"secondUtter": "From those, can you choose ones made of organic cotton?"
},
{
"index": 45,
"partner": "QVC",
"category1": "Home",
"category2": "Decor",
"firstUtter": "Which scented candles last the longest and fill the room effectively?",
"secondUtter": "From those, can you pick ones made with soy wax?"
},
{
"index": 46,
"partner": "QVC",
"category1": "Home",
"category2": "Kitchen & Dining",
"firstUtter": "Which nonstick cookware lasts the longest without scratching?",
"secondUtter": "From those, can you choose ones that are dishwasher-safe?"
},
{
"index": 47,
"partner": "QVC",
"category1": "Home",
"category2": "Kitchen & Dining",
"firstUtter": "What dinnerware sets are both durable for daily use and elegant for hosting guests?",
"secondUtter": "From those, can you pick ones that are microwave-safe?"
},
{
"index": 48,
"partner": "QVC",
"category1": "Home",
"category2": "Garden & Outdoor",
"firstUtter": "From those, can you choose ones with solar charging capability?",
"secondUtter": "Which outdoor string lights are durable and weather-resistant?"
},
{
"index": 49,
"partner": "QVC",
"category1": "Home",
"category2": "Decor (Seasonal)",
"firstUtter": "From those, can you pick ones with woody or spicy scents?",
"secondUtter": "What seasonal candles best capture the late autumn mood?"
},
{
"index": 50,
"partner": "QVC",
"category1": "Jewelry",
"category2": "Demi-Fine Jewelry",
"firstUtter": "What earrings are trending for special occasions this year?",
"secondUtter": "From those, can you choose ones that are hypoallergenic?"
},
{
"index": 51,
"partner": "QVC",
"category1": "Jewelry",
"category2": "Demi-Fine Jewelry",
"firstUtter": "What are the most popular engagement ring designs right now?",
"secondUtter": "From those, can you pick ones with lab-grown diamonds?"
},
{
"index": 52,
"partner": "QVC",
"category1": "Accessories",
"category2": "Bags & Wallets",
"firstUtter": "What crossbody bags are best for travel and hands-free convenience?",
"secondUtter": "From those, can you choose ones with anti-theft zippers?"
},
{
"index": 53,
"partner": "QVC",
"category1": "Accessories",
"category2": "Bags & Wallets",
"firstUtter": "From those, can you pick ones with padded laptop compartments?",
"secondUtter": "What are the most durable tote bags for everyday work use?"
},
{
"index": 54,
"partner": "QVC",
"category1": "Accessories",
"category2": "Bags & Wallets",
"firstUtter": "Which shoulder bags combine both style and practicality?",
"secondUtter": "From those, can you choose ones under 1kg in weight?"
},
{
"index": 55,
"partner": "QVC",
"category1": "Accessories",
"category2": "Jewelry (Seasonal)",
"firstUtter": "What glamorous jewelry is perfect for year-end parties?",
"secondUtter": "From those, can you pick ones in gold-tone metal?"
}
]

View File

@@ -461,31 +461,33 @@ function AppBase(props) {
return;
}
/* VUI_DISABLE_START - VUI Intent 처리 비활성화 */
// Voice Intent 처리 (최우선)
if (launchParams.intent) {
const { intent, intentParam, languageCode } = launchParams;
console.log('[App] Voice Intent received:', { intent, intentParam, languageCode });
// if (launchParams.intent) {
// const { intent, intentParam, languageCode } = launchParams;
// console.log('[App] Voice Intent received:', { intent, intentParam, languageCode });
// SearchContent 또는 PlayContent intent 처리
if (intent === 'SearchContent' || intent === 'PlayContent') {
dispatch(
pushPanel({
name: Config.panel_names.SEARCH_PANEL,
panelInfo: {
voiceSearch: true, // 음성 검색 플래그
searchVal: intentParam, // 검색어
languageCode: languageCode,
},
})
);
// // SearchContent 또는 PlayContent intent 처리
// if (intent === 'SearchContent' || intent === 'PlayContent') {
// dispatch(
// pushPanel({
// name: Config.panel_names.SEARCH_PANEL,
// panelInfo: {
// voiceSearch: true, // 음성 검색 플래그
// searchVal: intentParam, // 검색어
// languageCode: languageCode,
// },
// })
// );
console.log(`[App] Opening SearchPanel with voice query: ${intentParam}`);
if (typeof window === 'object' && window.PalmSystem) {
window.PalmSystem.activate();
}
return;
}
}
// console.log(`[App] Opening SearchPanel with voice query: ${intentParam}`);
// if (typeof window === 'object' && window.PalmSystem) {
// window.PalmSystem.activate();
// }
// return;
// }
// }
/* VUI_DISABLE_END */
// 기존 로직 유지
if (introTermsAgreeRef.current) {

View File

@@ -2,6 +2,7 @@
import { types } from './actionTypes';
import * as lunaSend from '../lunaSend/voice';
import { FEATURE_FLAGS } from '../constants/featureFlags';
/**
* Helper function to add log entries
@@ -24,6 +25,23 @@ const addLog = (type, title, data, success = true) => {
* This will establish a subscription to receive voice commands
*/
export const registerVoiceFramework = () => (dispatch, getState) => {
// VUI Feature Flag Check
if (!FEATURE_FLAGS.ENABLE_VUI) {
console.log('[Voice] VUI is disabled by feature flag');
dispatch(
addLog(
'ACTION',
'VUI Disabled',
{
message: 'VUI is disabled by FEATURE_FLAGS.ENABLE_VUI = false',
note: 'Set FEATURE_FLAGS.ENABLE_VUI = true to enable VUI',
},
false
)
);
return null;
}
// Platform check: Voice framework only works on TV (webOS)
const isTV = typeof window === 'object' && window.PalmSystem;
@@ -197,6 +215,12 @@ export const registerVoiceFramework = () => (dispatch, getState) => {
* This should be called when setContext command is received
*/
export const sendVoiceIntents = (voiceTicket) => (dispatch, getState) => {
// VUI Feature Flag Check
if (!FEATURE_FLAGS.ENABLE_VUI) {
console.log('[Voice] VUI is disabled - sendVoiceIntents skipped');
return;
}
console.log('[Voice] Sending voice intents...');
// Define the intents that this app supports
@@ -650,6 +674,12 @@ export const reportActionResult =
* Cancel the subscription when app goes to background or unmounts
*/
export const unregisterVoiceFramework = () => (dispatch, getState) => {
// VUI Feature Flag Check
if (!FEATURE_FLAGS.ENABLE_VUI) {
console.log('[Voice] VUI is disabled - unregisterVoiceFramework skipped');
return;
}
const { voiceHandler } = getState().voice;
const isTV = typeof window === 'object' && window.PalmSystem;

View File

@@ -31,7 +31,9 @@ import MyPageIcon from './iconComponents/MyPageIcon';
import OnSaleIcon from './iconComponents/OnSaleIcon';
import SearchIcon from './iconComponents/SearchIcon';
import TrendingNowIcon from './iconComponents/TrendingNowIcon';
import VoiceIcon from './iconComponents/VoiceIcon';
/* VUI_DISABLE_START - VoiceIcon import 비활성화 */
// import VoiceIcon from './iconComponents/VoiceIcon';
/* VUI_DISABLE_END */
import CartIcon from './iconComponents/CartIcon';
import TabItem from './TabItem';
import TabItemSub from './TabItemSub';
@@ -237,13 +239,15 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
spotlightId: 'spotlight_search',
target: [{ name: panel_names.SEARCH_PANEL }],
},
{
icons: VoiceIcon,
id: 'voice',
title: 'Voice',
spotlightId: 'spotlight_voice',
target: [{ name: panel_names.VOICE_PANEL }],
},
/* VUI_DISABLE_START - Voice submenu 비활성화 */
// {
// icons: VoiceIcon,
// id: 'voice',
// title: 'Voice',
// spotlightId: 'spotlight_voice',
// target: [{ name: panel_names.VOICE_PANEL }],
// },
/* VUI_DISABLE_END */
];
break;
@@ -308,20 +312,22 @@ export default function TabLayout({ topPanelName, onTabActivated, panelInfo }) {
const menuInfo = getMenuData(currentKey || 'GNB');
for (let j = 0; j < menuInfo.length; j++) {
if (![10600, 10500, 10300, 10700].includes(currentKey)) {
if (![10600, 10500, 10300].includes(currentKey)) {
menuItems[i].target = menuInfo[j].target;
}
// 10700(Search)의 경우 메인 아이콘은 SearchIcon으로 고정
// 10700(Search)의 경우 메인 아이콘은 SearchIcon으로 고정하고 target 직접 설정
if (currentKey === 10700) {
menuItems[i].icons = SearchIcon;
menuItems[i].spotlightId = 'spotlight_search';
menuItems[i].target = [{ name: panel_names.SEARCH_PANEL }]; // Search를 바로 열기
} else {
menuItems[i].spotlightId = menuInfo[j].spotlightId;
menuItems[i].icons = menuInfo[j].icons;
}
if ([10600, 10500, 10300, 10700].includes(currentKey)) {
// 10700(Search)는 이제 children이 없으므로 제외
if ([10600, 10500, 10300].includes(currentKey)) {
menuItems[i].children = menuInfo.map((item) => ({
id: item.id,
title: item.title,

View File

@@ -0,0 +1,27 @@
/**
* Feature Flags
*
* 기능 활성화/비활성화를 제어하는 전역 플래그
*/
export const FEATURE_FLAGS = {
/**
* VUI (Voice User Interface) 활성화 여부
* - true: webOS Voice Framework 사용 (Luna Service 기반)
* - false: Web Speech API만 사용 (브라우저 표준 API)
*
* VUI 비활성화 시:
* - registerVoiceFramework() 실행 안됨
* - Luna Service 호출 차단
* - VUI Intent 수신 안됨 (SearchContent, PlayContent)
* - VoicePanel 사용 불가
*/
ENABLE_VUI: false,
/**
* Web Speech API 활성화 여부
* - true: 브라우저 Web Speech API 사용
* - false: 음성 인식 완전 비활성화
*/
ENABLE_WEB_SPEECH: true,
};

View File

@@ -3,6 +3,7 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { registerVoiceFramework, unregisterVoiceFramework } from '../actions/voiceActions';
import { FEATURE_FLAGS } from '../constants/featureFlags';
/**
* SearchPanel용 음성 입력 Hook
@@ -18,6 +19,12 @@ export const useSearchVoice = (isOnTop, onSTTText) => {
// SearchPanel이 foreground일 때만 voice 등록
useEffect(() => {
// VUI 비활성화 시 실행 안함
if (!FEATURE_FLAGS.ENABLE_VUI) {
console.log('[useSearchVoice] VUI disabled by feature flag');
return;
}
if (isOnTop) {
console.log('[useSearchVoice] Registering voice framework');
dispatch(registerVoiceFramework());
@@ -34,6 +41,8 @@ export const useSearchVoice = (isOnTop, onSTTText) => {
// STT 텍스트 수신 처리
useEffect(() => {
// VUI 비활성화 시에도 Web Speech API의 STT는 처리
// (Web Speech도 VOICE_STT_TEXT_RECEIVED 액션을 사용)
if (lastSTTText && sttTimestamp) {
console.log('[useSearchVoice] STT text received:', lastSTTText);
if (onSTTText) {

View File

@@ -70,7 +70,9 @@ import OnSalePanel from '../OnSalePanel/OnSalePanel';
import PlayerPanel from '../PlayerPanel/PlayerPanel';
import PlayerPanelNew from '../PlayerPanel/PlayerPanel.new';
import SearchPanel from '../SearchPanel/SearchPanel.new';
import VoicePanel from '../VoicePanel/VoicePanel';
/* VUI_DISABLE_START - VoicePanel import 비활성화 */
// import VoicePanel from '../VoicePanel/VoicePanel';
/* VUI_DISABLE_END */
import ThemeCurationPanel from '../ThemeCurationPanel/ThemeCurationPanel';
import TrendingNowPanel from '../TrendingNowPanel/TrendingNowPanel';
import UserReviewPanel from '../UserReview/UserReviewPanel';
@@ -91,7 +93,9 @@ const panelMap = {
[Config.panel_names.MY_PAGE_PANEL]: MyPagePanel,
[Config.panel_names.CATEGORY_PANEL]: CategoryPanel,
[Config.panel_names.SEARCH_PANEL]: SearchPanel,
[Config.panel_names.VOICE_PANEL]: VoicePanel,
/* VUI_DISABLE_START - VoicePanel panelMap 비활성화 */
// [Config.panel_names.VOICE_PANEL]: VoicePanel,
/* VUI_DISABLE_END */
[Config.panel_names.ON_SALE_PANEL]: OnSalePanel,
[Config.panel_names.TRENDING_NOW_PANEL]: TrendingNowPanel,
[Config.panel_names.HOT_PICKS_PANEL]: HotPicksPanel,

View File

@@ -119,6 +119,10 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
const [focusedContainerId, setFocusedContainerId] = useState(panelInfo?.focusedContainerId);
const focusedContainerIdRef = usePrevious(focusedContainerId);
// Timer refs for cleanup
const initialFocusTimerRef = useRef(null);
const spotlightResumeTimerRef = useRef(null);
const onFocusMic = useCallback(() => {
console.log('[MicFocus]');
// 포커스 시 VoiceInputOverlay 표시
@@ -329,6 +333,24 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
}
}, [panelInfo, isOnTop]);
// SearchPanel이 처음 열릴 때 TInput으로 포커스 설정
useEffect(() => {
if (isOnTop && !isOnTopRef.current) {
// SearchPanel이 방금 열렸을 때 (이전에는 열려있지 않았음)
initialFocusTimerRef.current = setTimeout(() => {
Spotlight.focus(SPOTLIGHT_IDS.SEARCH_INPUT_BOX);
}, 100);
}
return () => {
// Cleanup: 컴포넌트 언마운트 또는 isOnTop 변경 시 타이머 정리
if (initialFocusTimerRef.current) {
clearTimeout(initialFocusTimerRef.current);
initialFocusTimerRef.current = null;
}
};
}, [isOnTop]);
useEffect(() => {
return () => {
dispatch(
@@ -343,6 +365,16 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
};
}, []);
// Cleanup all timers on component unmount
useEffect(() => {
return () => {
if (spotlightResumeTimerRef.current) {
clearTimeout(spotlightResumeTimerRef.current);
spotlightResumeTimerRef.current = null;
}
};
}, []);
const handleKeydown = useCallback(
(e) => {
if (!isOnTopRef.current) {
@@ -435,7 +467,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
(containerId) => {
setFocusedContainerId(containerId);
if (!firstSpot) {
setTimeout(() => {
// Clear existing timer before setting new one
if (spotlightResumeTimerRef.current) {
clearTimeout(spotlightResumeTimerRef.current);
}
spotlightResumeTimerRef.current = setTimeout(() => {
Spotlight.resume();
setFirstSpot(true);
if (panelInfo.currentSpot) {
@@ -443,6 +480,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId, scrollOpt
Spotlight.focus(panelInfo.currentSpot);
}
}
spotlightResumeTimerRef.current = null;
}, 0);
}
},

View File

@@ -83,6 +83,14 @@ const VoiceInputOverlay = ({
const lastFocusedElement = useRef(null);
const listeningTimerRef = useRef(null);
const audioContextRef = useRef(null);
// Timer refs for cleanup
const closeTimerRef = useRef(null);
const focusTimerRef = useRef(null);
const focusRestoreTimerRef = useRef(null);
const searchSubmitFocusTimerRef = useRef(null);
const wakeWordRestartTimerRef = useRef(null);
const [inputFocus, setInputFocus] = useState(false);
const [micFocused, setMicFocused] = useState(false);
const [micWebSpeechFocused, setMicWebSpeechFocused] = useState(false);
@@ -203,10 +211,18 @@ const VoiceInputOverlay = ({
shopperHouseDataRef.current = shopperHouseData;
// 약간의 지연 후 닫기 (사용자가 결과를 인지할 수 있도록)
setTimeout(() => {
closeTimerRef.current = setTimeout(() => {
onClose();
}, 500);
}
return () => {
// Cleanup: 컴포넌트 언마운트 또는 의존성 변경 시 타이머 정리
if (closeTimerRef.current) {
clearTimeout(closeTimerRef.current);
closeTimerRef.current = null;
}
};
}, [shopperHouseData, isVisible, onClose]);
// ⛔ VUI 테스트 비활성화: STT 텍스트 수신 처리
@@ -335,7 +351,7 @@ const VoiceInputOverlay = ({
setVoiceInputMode(null);
// 마이크 버튼으로 포커스 이동
setTimeout(() => {
focusTimerRef.current = setTimeout(() => {
Spotlight.focus(MIC_SPOTLIGHT_ID);
}, 100);
} else {
@@ -358,13 +374,52 @@ const VoiceInputOverlay = ({
setCurrentMode(VOICE_MODES.PROMPT);
if (lastFocusedElement.current) {
setTimeout(() => {
focusRestoreTimerRef.current = setTimeout(() => {
Spotlight.focus(lastFocusedElement.current);
}, 100);
}
}
return () => {
// Cleanup: 컴포넌트 언마운트 또는 isVisible 변경 시 타이머 정리
if (focusTimerRef.current) {
clearTimeout(focusTimerRef.current);
focusTimerRef.current = null;
}
if (focusRestoreTimerRef.current) {
clearTimeout(focusRestoreTimerRef.current);
focusRestoreTimerRef.current = null;
}
};
}, [isVisible, mode]);
// Cleanup all timers and AudioContext on component unmount
useEffect(() => {
return () => {
// Clear all timer refs
if (searchSubmitFocusTimerRef.current) {
clearTimeout(searchSubmitFocusTimerRef.current);
searchSubmitFocusTimerRef.current = null;
}
if (wakeWordRestartTimerRef.current) {
clearTimeout(wakeWordRestartTimerRef.current);
wakeWordRestartTimerRef.current = null;
}
// Close AudioContext to free audio resources
if (audioContextRef.current) {
try {
if (typeof audioContextRef.current.close === 'function') {
audioContextRef.current.close();
}
} catch (err) {
console.warn('[VoiceInputOverlay] Failed to close AudioContext:', err);
}
audioContextRef.current = null;
}
};
}, []);
// Suggestion 버튼 클릭 핸들러 - Input 창에 텍스트만 설정
const handleSuggestionClick = useCallback(
(suggestion) => {
@@ -391,8 +446,13 @@ const VoiceInputOverlay = ({
onSearchChange({ value: '' });
}
// Clear existing timer before setting new one
if (searchSubmitFocusTimerRef.current) {
clearTimeout(searchSubmitFocusTimerRef.current);
}
// API 호출 후 Input 박스로 포커스 이동
setTimeout(() => {
searchSubmitFocusTimerRef.current = setTimeout(() => {
Spotlight.focus(INPUT_SPOTLIGHT_ID);
}, 100);
@@ -423,6 +483,10 @@ const VoiceInputOverlay = ({
clearTimeout(listeningTimerRef.current);
listeningTimerRef.current = null;
}
if (wakeWordRestartTimerRef.current) {
clearTimeout(wakeWordRestartTimerRef.current);
wakeWordRestartTimerRef.current = null;
}
// LISTENING 모드로 전환
setVoiceInputMode(VOICE_INPUT_MODE.WEBSPEECH);
@@ -430,8 +494,9 @@ const VoiceInputOverlay = ({
// WebSpeech 재시작 (continuous: false로 일반 입력 모드)
stopListening();
setTimeout(() => {
wakeWordRestartTimerRef.current = setTimeout(() => {
startListening();
wakeWordRestartTimerRef.current = null;
}, 300);
// 15초 타이머 설정
@@ -763,7 +828,8 @@ const VoiceInputOverlay = ({
</SpottableMicButton>
)}
{voiceVersion === VOICE_VERSION.VUI && (
{/* VUI_DISABLE_START - VUI 마이크 버튼 비활성화 */}
{/* {voiceVersion === VOICE_VERSION.VUI && (
<SpottableMicButton
className={classNames(
css.microphoneButton,
@@ -803,7 +869,8 @@ const VoiceInputOverlay = ({
</svg>
)}
</SpottableMicButton>
)}
)} */}
{/* VUI_DISABLE_END */}
</div>
</div>