[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:
442
com.twin.app.shoptime/ai_poc_list.json
Normal file
442
com.twin.app.shoptime/ai_poc_list.json
Normal 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": "I’m 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": "What’s 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 that’s 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": "What’s 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": "What’s 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": "What’s 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": "What’s 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": "What’s 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": "What’s 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": "What’s 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 men’s 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": "Women’s 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": "Women’s 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": "Women’s 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?"
|
||||
}
|
||||
]
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
27
com.twin.app.shoptime/src/constants/featureFlags.js
Normal file
27
com.twin.app.shoptime/src/constants/featureFlags.js
Normal 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,
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user