[251024] fix: HowAboutThese ShopperHouse API,DEBUG_MODE=false

🕐 커밋 시간: 2025. 10. 24. 12:18:21

📊 변경 통계:
  • 총 파일: 5개
  • 추가: +51줄
  • 삭제: -31줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.jsx (javascript):
     Added: HowAboutThese()
     Deleted: HowAboutThese()
  📄 com.twin.app.shoptime/src/views/SearchPanel/HowAboutThese/HowAboutThese.small.jsx (javascript):
     Added: Bubble()
  📄 com.twin.app.shoptime/src/views/SearchPanel/SearchPanel.new.v2.jsx (javascript):
    🔄 Modified: SpotlightContainerDecorator()
  📄 com.twin.app.shoptime/src/views/SearchPanel/SearchResults.new.v2.jsx (javascript):
     Added: SafeImage()
  📄 com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx (javascript):
    🔄 Modified: Spottable(), clearAllTimers()
This commit is contained in:
2025-10-24 12:18:22 +09:00
parent 6f9b6133b9
commit 09bb9fb3e9
5 changed files with 147 additions and 118 deletions

View File

@@ -1,13 +1,9 @@
import React, {
useCallback,
useMemo,
} from 'react';
import React, { useCallback, useMemo } from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import SpotlightContainerDecorator
from '@enact/spotlight/SpotlightContainerDecorator';
import SpotlightContainerDecorator from '@enact/spotlight/SpotlightContainerDecorator';
import Spottable from '@enact/spotlight/Spottable';
import icShoptime from '../../../../assets/images/icons/ic-shoptime.png';
@@ -16,16 +12,16 @@ import css from './HowAboutThese.module.less';
const OverlayContainer = SpotlightContainerDecorator(
{
enterTo: "default-element",
restrict: "self-only",
enterTo: 'default-element',
restrict: 'self-only',
},
"div"
'div'
);
const SpottableBubble = Spottable("div");
const SpottableSeeMoreButton = Spottable("div");
const SpottableBubble = Spottable('div');
const SpottableSeeMoreButton = Spottable('div');
const HowAboutThese = ({ relativeQueries = [], onQueryClick, onClose }) => {
const HowAboutThese = ({ relativeQueries = [], searchId = null, onQueryClick, onClose }) => {
const dispatch = useDispatch();
// 기본 relativeQueries가 없는 경우를 위한 fallback
@@ -33,18 +29,18 @@ const HowAboutThese = ({ relativeQueries = [], onQueryClick, onClose }) => {
return relativeQueries.length > 0
? relativeQueries
: [
"What are some luxury skincare products",
"Popular makeup gift sets suitable for mother",
"Which anti-aging cosmetics in 50s or 60s",
"Elegant fragrance or cosmetic bundles",
"What beauty brands offer special gift boxes",
'What are some luxury skincare products',
'Popular makeup gift sets suitable for mother',
'Which anti-aging cosmetics in 50s or 60s',
'Elegant fragrance or cosmetic bundles',
'What beauty brands offer special gift boxes',
];
}, [relativeQueries]);
// 검색어 클릭 핸들러
const handleQueryClick = useCallback(
(query) => {
console.log("[HowAboutThese] Query clicked:", query);
console.log('[HowAboutThese] Query clicked:', query, 'searchId:', searchId);
// 외부에서 전달된 onQueryClick이 있으면 사용
if (onQueryClick) {
@@ -52,22 +48,20 @@ const HowAboutThese = ({ relativeQueries = [], onQueryClick, onClose }) => {
return;
}
// 기본적으로 ShopperHouse API를 통해 재검색
dispatch(getShopperHouseSearch(query));
// ShopperHouse API를 통해 재검색 (searchId가 존재하면 포함)
dispatch(getShopperHouseSearch(query, searchId));
// 팝업 닫기
if (onClose) {
onClose();
}
},
[dispatch, onQueryClick, onClose]
[dispatch, onQueryClick, onClose, searchId]
);
// "COLLAPSE" 버튼 클릭 핸들러 (Full 버전을 Small로 축소)
const handleCollapseClick = useCallback(() => {
console.log(
"[HowAboutThese] Collapse clicked - 축소하여 Small 버전으로 전환"
);
console.log('[HowAboutThese] Collapse clicked - 축소하여 Small 버전으로 전환');
if (onClose) {
onClose();
}
@@ -94,10 +88,7 @@ const HowAboutThese = ({ relativeQueries = [], onQueryClick, onClose }) => {
</div>
</div>
<div className={css.headerRight}>
<SpottableSeeMoreButton
className={css.seeMoreButton}
onClick={handleCollapseClick}
>
<SpottableSeeMoreButton className={css.seeMoreButton} onClick={handleCollapseClick}>
<span className={css.seeMoreText}>COLLAPSE</span>
</SpottableSeeMoreButton>
</div>
@@ -132,6 +123,7 @@ const HowAboutThese = ({ relativeQueries = [], onQueryClick, onClose }) => {
HowAboutThese.propTypes = {
relativeQueries: PropTypes.array,
searchId: PropTypes.string,
onQueryClick: PropTypes.func,
onClose: PropTypes.func,
};

View File

@@ -19,6 +19,7 @@ const Bubble = ({ query, onClick }) => (
const HowAboutTheseSmall = ({
relativeQueries = [],
searchId = null,
onQueryClick,
onSeeMoreClick,
}) => {
@@ -29,15 +30,15 @@ const HowAboutTheseSmall = ({
relativeQueries.length > 0
? relativeQueries
: [
"What are some luxury skincare products",
"Popular makeup gift sets suitable for mother",
"Which anti-aging cosmetics in 50s or 60s",
'What are some luxury skincare products',
'Popular makeup gift sets suitable for mother',
'Which anti-aging cosmetics in 50s or 60s',
];
// 검색어 클릭 핸들러
const handleQueryClick = useCallback(
(query) => {
console.log("[HowAboutTheseSmall] Query clicked:", query);
console.log('[HowAboutTheseSmall] Query clicked:', query, 'searchId:', searchId);
// 외부에서 전달된 onQueryClick이 있으면 사용
if (onQueryClick) {
@@ -45,15 +46,15 @@ const HowAboutTheseSmall = ({
return;
}
// 기본적으로 ShopperHouse API를 통해 재검색
dispatch(getShopperHouseSearch(query));
// ShopperHouse API를 통해 재검색 (searchId가 존재하면 포함)
dispatch(getShopperHouseSearch(query, searchId));
},
[dispatch, onQueryClick]
[dispatch, onQueryClick, searchId]
);
// "SEE MORE" 버튼 클릭 핸들러
const handleSeeMoreClick = useCallback(() => {
console.log("[HowAboutTheseSmall] See More clicked");
console.log('[HowAboutTheseSmall] See More clicked');
// 외부에서 전달된 onSeeMoreClick이 있으면 사용
if (onSeeMoreClick) {
@@ -62,7 +63,7 @@ const HowAboutTheseSmall = ({
}
// 기본 동작: 확장된 뷰 표시 (나중에 구현)
console.log("[HowAboutTheseSmall] TODO: Show expanded view");
console.log('[HowAboutTheseSmall] TODO: Show expanded view');
}, [onSeeMoreClick]);
// 첫 번째 다섯개까지 쿼리만 표시 (small 버전)
@@ -84,22 +85,14 @@ const HowAboutTheseSmall = ({
{/* Bubbles Section */}
<Marquee className={css.bubblesContainer} marqueeOn="render">
{displayQueries.map((query, index) => (
<div
className={css.bubble}
key={index}
onClick={() => handleQueryClick(query)}
>
<div className={css.bubble} key={index} onClick={() => handleQueryClick(query)}>
<div className={css.bubbleText}>{query}</div>
</div>
))}
</Marquee>
{/* See More Button */}
<TButton
className={css.seeMoreButton}
size="small"
onClick={handleSeeMoreClick}
>
<TButton className={css.seeMoreButton} size="small" onClick={handleSeeMoreClick}>
<div className={css.seeMoreText}>SEE MORE</div>
</TButton>
</div>
@@ -108,6 +101,7 @@ const HowAboutTheseSmall = ({
HowAboutTheseSmall.propTypes = {
relativeQueries: PropTypes.array,
searchId: PropTypes.string,
onQueryClick: PropTypes.func,
onSeeMoreClick: PropTypes.func,
};

View File

@@ -108,15 +108,7 @@ const SafeImageComponent = ({ src, alt, className, ...props }) => {
};
}, []);
return (
<img
ref={imgRef}
src={src}
alt={alt}
className={className}
{...props}
/>
);
return <img ref={imgRef} src={src} alt={alt} className={className} {...props} />;
};
const ITEMS_PER_PAGE = 9;
@@ -150,6 +142,12 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
const panels = useSelector((state) => state.panels.panels);
// 0hun: 음성 검색 결과에 대한 전역 상태
const shopperHouseData = useSelector((state) => state.search.shopperHouseData);
// 0hun: 음성 검색 searchId (Redux에서 별도 관리)
const shopperHouseSearchId = useSelector((state) => state.search.shopperHouseSearchId);
// 0hun: 음성 검색 relativeQueries (Redux에서 별도 관리)
const shopperHouseRelativeQueries = useSelector(
(state) => state.search.shopperHouseRelativeQueries
);
// 0hun: 검색 메인, Hot Picks for you 영역에 대한 전역 상태 값
const hotPicksForYou = useSelector((state) => state.search.searchMainData.hotPicksForYou);
// 0hun: 검색 메인, Popular Brands 영역에 대한 전역 상태 값
@@ -192,7 +190,11 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
timestamp: new Date().toISOString(),
});
}
}, [shopperHouseData?.results?.[0]?.docs?.length, shopperHouseData?.results?.[0]?.searchId, DEBUG_MODE]);
}, [
shopperHouseData?.results?.[0]?.docs?.length,
shopperHouseData?.results?.[0]?.searchId,
DEBUG_MODE,
]);
// 🐛 [DEBUG] SearchPanel 마운트/언마운트 추적 (DEBUG_MODE가 true일 경우에만)
useEffect(() => {
@@ -255,11 +257,7 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
/**
* useSearchHistory Hook 적용
*/
const {
normalSearches,
addNormalSearch,
executeSearchFromHistory,
} = useSearchHistory();
const { normalSearches, addNormalSearch, executeSearchFromHistory } = useSearchHistory();
// Voice overlay suggestions (동적으로 변경 가능)
const voiceSuggestions = useMemo(
@@ -326,8 +324,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
if (e.key === 'ArrowRight' || e.key === 'Right') {
// 커서가 텍스트 끝에 있을 때만 포커스 이동 허용
// DOM 쿼리 최적화: 캐싱된 input element 사용
const input = inputElementRef.current ||
document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
const input =
inputElementRef.current ||
document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
if (input) {
inputElementRef.current = input; // 캐싱
if (position === input.value.length) {
@@ -406,8 +405,9 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
*/
const cursorPosition = useCallback(() => {
// ref를 사용하여 캐싱된 input element 사용
const input = inputElementRef.current ||
document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
const input =
inputElementRef.current ||
document.querySelector(`[data-spotlight-id="input-field-box"] > input`);
if (input) {
inputElementRef.current = input; // 캐싱
@@ -639,7 +639,6 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
}, 150); // Overlay 닫히는 시간을 고려한 지연
}, []);
// ✨ [Phase 3] handleInputIconClick 제거 (돋보기 아이콘 기능 비활성화)
// /**
// * ✨ [Phase 3] TInput icon click handler (showVirtualKeyboard 제거)
@@ -753,6 +752,8 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
itemInfo={searchDatas.item}
showInfo={searchDatas.show}
shopperHouseInfo={shopperHouseData}
shopperHouseSearchId={shopperHouseSearchId}
shopperHouseRelativeQueries={shopperHouseRelativeQueries}
keywordClick={handleKeywordClick}
panelInfo={panelInfo}
/>
@@ -1247,8 +1248,14 @@ export default function SearchPanel({ panelInfo, isOnTop, spotlightId }) {
kind={KINDS.withIcon}
icon={ICONS.search}
text={searchQuery} // [Phase 8] Overlay에서 입력받은 텍스트만 표시
alwaysShowText={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT || currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT} // 🎯 VOICE_RESULT & SEARCH_RESULT 모드에서 항상 텍스트 표시
inputFocus={currentMode === SEARCH_PANEL_MODES.VOICE_RESULT || currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT} // VOICE_RESULT & SEARCH_RESULT 모드에서 TInputSimple 내부 포커스 활성화
alwaysShowText={
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT ||
currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT
} // 🎯 VOICE_RESULT & SEARCH_RESULT 모드에서 항상 텍스트 표시
inputFocus={
currentMode === SEARCH_PANEL_MODES.VOICE_RESULT ||
currentMode === SEARCH_PANEL_MODES.SEARCH_RESULT
} // VOICE_RESULT & SEARCH_RESULT 모드에서 TInputSimple 내부 포커스 활성화
onKeyDown={handleKeydown}
spotlightId={SPOTLIGHT_IDS.SEARCH_INPUT_BOX}
forcedSpotlight="recent-keyword-0"

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { useDispatch } from 'react-redux';
import Spotlight from '@enact/spotlight';
import Spottable from '@enact/spotlight/Spottable';
@@ -13,6 +14,7 @@ import TDropDown from '../../components/TDropDown/TDropDown';
import TVirtualGridList from '../../components/TVirtualGridList/TVirtualGridList';
import { $L } from '../../utils/helperMethods';
import { SpotlightIds } from '../../utils/SpotlightIds';
import { getShopperHouseSearch } from '../../actions/searchActions';
import css from './SearchResults.new.v2.module.less';
import ItemCard from './SearchResultsNew/ItemCard';
import ShowCard from './SearchResultsNew/ShowCard';
@@ -71,8 +73,11 @@ const SearchResultsNew = ({
showInfo,
themeInfo,
shopperHouseInfo,
shopperHouseSearchId = null,
shopperHouseRelativeQueries = [],
keywordClick,
}) => {
const dispatch = useDispatch();
// ShopperHouse 데이터를 ItemCard 형식으로 변환
const convertedShopperHouseItems = useMemo(() => {
if (!shopperHouseInfo || !shopperHouseInfo.results || shopperHouseInfo.results.length === 0) {
@@ -108,6 +113,7 @@ const SearchResultsNew = ({
};
});
}, [shopperHouseInfo]);
const getButtonTabList = () => {
// ShopperHouse 데이터가 있으면 그것을 사용, 없으면 기존 검색 결과 사용
const itemLength = convertedShopperHouseItems?.length || itemInfo?.length || 0;
@@ -145,25 +151,42 @@ const SearchResultsNew = ({
}, []);
// 쿼리 클릭 핸들러 (Small 버전용)
// ShopperHouseAPI를 직접 호출 (일반 검색이 아님)
const handleSmallQueryClick = useCallback(
(query) => {
if (keywordClick) {
keywordClick(query);
}
console.log(
'[SearchResultsNew] Small query clicked:',
query,
'searchId:',
shopperHouseSearchId
);
// ShopperHouseAPI 직접 호출
dispatch(getShopperHouseSearch(query, shopperHouseSearchId));
// 쿼리 클릭 시에는 모드 유지 (small 계속 표시)
},
[keywordClick]
[dispatch, shopperHouseSearchId]
);
// 쿼리 클릭 핸들러 (Full 버전용)
// ShopperHouseAPI를 직접 호출 (일반 검색이 아님)
const handleFullQueryClick = useCallback(
(query) => {
if (keywordClick) {
keywordClick(query);
}
console.log(
'[SearchResultsNew] Full query clicked:',
query,
'searchId:',
shopperHouseSearchId
);
// ShopperHouseAPI 직접 호출
dispatch(getShopperHouseSearch(query, shopperHouseSearchId));
// Full 버전을 Small로 축소
setHowAboutTheseMode(HOW_ABOUT_THESE_MODES.SMALL);
},
[keywordClick]
[dispatch, shopperHouseSearchId]
);
// buttonTabList 최적화 - 의존성이 변경될 때만 재계산
@@ -279,16 +302,14 @@ const SearchResultsNew = ({
[themeInfo?.length, SpottableDiv, SafeImage] // themeInfo 전체 대신 length와 캐싱된 컴포넌트 사용
);
// relativeQueries 가져오기 (Redux에서 제공)
// Redux shopperHouseRelativeQueries를 사용하여 데이터 유지
// relativeQueries 가져오기 (Redux에서 전달받은 props 사용)
// Redux에서 별도로 관리되는 shopperHouseRelativeQueries를 직접 사용
// ShopperHouseData 초기화에 영향을 받지 않음
const relativeQueries = useMemo(() => {
if (shopperHouseInfo?.results?.[0]?.relativeQueries) {
// Redux에서 받은 relativeQueries 사용
return shopperHouseInfo.results[0].relativeQueries;
}
// 기본값
return ['Puppy food', 'Dog toy', 'Fitness'];
}, [shopperHouseInfo]);
return shopperHouseRelativeQueries && shopperHouseRelativeQueries.length > 0
? shopperHouseRelativeQueries
: [];
}, [shopperHouseRelativeQueries]);
useEffect(() => {
const targetId = panelInfo?.currentSpot
@@ -320,7 +341,7 @@ const SearchResultsNew = ({
<SpottableDiv spotlightId="how-about-these-small-wrapper">
<HowAboutTheseSmall
relativeQueries={relativeQueries}
onQueryClick={handleSmallQueryClick}
searchId={shopperHouseSearchId}
onSeeMoreClick={handleShowFullHowAboutThese}
/>
</SpottableDiv>
@@ -331,7 +352,7 @@ const SearchResultsNew = ({
<div className={css.howAboutTheseOverlay}>
<HowAboutThese
relativeQueries={relativeQueries}
onQueryClick={handleFullQueryClick}
searchId={shopperHouseSearchId}
onClose={handleCloseHowAboutThese}
/>
</div>

View File

@@ -39,7 +39,7 @@ const SpottableMicButton = Spottable('div');
const SpottableDebugButton = Spottable('div');
// Debug mode constant - 항상 디버그 화면 표시
const DEBUG_MODE = true;
const DEBUG_MODE = false;
// Voice overlay 모드 상수
export const VOICE_MODES = {
@@ -268,38 +268,46 @@ const VoiceInputOverlay = ({
}, [webSpeechEventLogs]);
// 🔍 음성 검색 기록 저장 함수 (새로운 searchHistory 시스템 사용)
const addToSearchHistory = useCallback((searchText, searchId = null) => {
if (!searchText || searchText.trim().length < 3) return;
const addToSearchHistory = useCallback(
(searchText, searchId = null) => {
if (!searchText || searchText.trim().length < 3) return;
const trimmedText = searchText.trim();
const trimmedText = searchText.trim();
// 새로운 searchHistory 시스템에 음성검색 기록 저장
addVoiceSearch(trimmedText, searchId);
// 새로운 searchHistory 시스템에 음성검색 기록 저장
addVoiceSearch(trimmedText, searchId);
// 기존 시스템에도 저장 (호환성 유지)
setLegacySearchHistory((prevHistory) => {
// 중복 제거 (대소문자 구분 없이)
const filtered = prevHistory.filter(
(item) => item.toLowerCase() !== trimmedText.toLowerCase()
);
// 기존 시스템에도 저장 (호환성 유지)
setLegacySearchHistory((prevHistory) => {
// 중복 제거 (대소문자 구분 없이)
const filtered = prevHistory.filter(
(item) => item.toLowerCase() !== trimmedText.toLowerCase()
);
// 최신 항목을 맨 앞에 추가
const newHistory = [trimmedText, ...filtered].slice(0, MAX_HISTORY_SIZE);
// 최신 항목을 맨 앞에 추가
const newHistory = [trimmedText, ...filtered].slice(0, MAX_HISTORY_SIZE);
// localStorage에 저장 (기존 키)
writeLocalStorage(SEARCH_HISTORY_KEY, newHistory);
// localStorage에 저장 (기존 키)
writeLocalStorage(SEARCH_HISTORY_KEY, newHistory);
if (DEBUG_MODE) {
console.log('💾 [VoiceInputOverlay.v2] Legacy search history updated:', newHistory);
}
return newHistory;
});
if (DEBUG_MODE) {
console.log('💾 [VoiceInputOverlay.v2] Legacy search history updated:', newHistory);
console.log(
'🎤 [VoiceInputOverlay] Voice search added to history:',
trimmedText,
'searchId:',
searchId
);
}
return newHistory;
});
if (DEBUG_MODE) {
console.log('🎤 [VoiceInputOverlay] Voice search added to history:', trimmedText, 'searchId:', searchId);
}
}, [addVoiceSearch]); // addVoiceSearch dependency 추가
},
[addVoiceSearch]
); // addVoiceSearch dependency 추가
// WebSpeech config 메모이제이션 (불필요한 재초기화 방지)
const webSpeechConfig = useMemo(
@@ -1238,7 +1246,9 @@ const VoiceInputOverlay = ({
const hasRelativeQueries = !!shopperHouseRelativeQueries;
const promptTitle = hasRelativeQueries ? 'How about these ?' : 'Try saying';
// relativeQueries가 있으면 사용, 없으면 legacySearchHistory 사용
const promptSuggestions = hasRelativeQueries ? shopperHouseRelativeQueries : legacySearchHistory;
const promptSuggestions = hasRelativeQueries
? shopperHouseRelativeQueries
: legacySearchHistory;
// ✨ [DEBUG] Redux 상태 확인 로그
console.log('[VoiceInput]-shopperHouseRelativeQueries');
@@ -1278,7 +1288,10 @@ const VoiceInputOverlay = ({
console.log('[DEBUG][VoiceInputOverlay] └─ promptTitle:', promptTitle);
} else {
console.log('[DEBUG][VoiceInputOverlay] ├─ relativeQueries가 없음');
console.log('[DEBUG][VoiceInputOverlay] └─ legacySearchHistory 사용:', promptSuggestions);
console.log(
'[DEBUG][VoiceInputOverlay] └─ legacySearchHistory 사용:',
promptSuggestions
);
}
}
return (
@@ -1357,7 +1370,9 @@ const VoiceInputOverlay = ({
}
// default 케이스에서도 promptSuggestions 계산
const hasRelativeQueries = !!shopperHouseRelativeQueries;
const defaultPromptSuggestions = hasRelativeQueries ? shopperHouseRelativeQueries : legacySearchHistory;
const defaultPromptSuggestions = hasRelativeQueries
? shopperHouseRelativeQueries
: legacySearchHistory;
return (
<VoicePromptScreen
suggestions={defaultPromptSuggestions}
@@ -1692,7 +1707,7 @@ const VoiceInputOverlay = ({
<div className={css.dimBackground} onClick={handleClose} />
{/* 디버깅용: Voice 상태 표시 */}
{debugUI}
{DEBUG_MODE && debugUI}
{/* 모드별 컨텐츠 영역 - Spotlight Container (self-only) */}
<OverlayContainer
@@ -1775,7 +1790,7 @@ const VoiceInputOverlay = ({
</OverlayContainer>
{/* WebSpeech 이벤트 전용 디버그 - 모드와 상관없이 항상 우측 하단에 표시 */}
<WebSpeechEventDebug logs={webSpeechEventLogs} />
{DEBUG_MODE && <WebSpeechEventDebug logs={webSpeechEventLogs} />}
{/* 풀 대시보드 - 토글 버튼으로 열기/닫기 */}
<VoiceDebugDashboard