[251106] feat: Queued Panel functions

🕐 커밋 시간: 2025. 11. 06. 09:25:09

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

📁 추가된 파일:
  + com.twin.app.shoptime/src/actions/queuedPanelActions.js
  + com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js
  + com.twin.app.shoptime/src/utils/panelQueueExamples.js
  + com.twin.app.shoptime/src/utils/panelQueueSetupGuide.js

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/actions/actionTypes.js
  ~ com.twin.app.shoptime/src/reducers/panelReducer.js
  ~ com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx

🔧 주요 변경 내용:
  • 타입 시스템 안정성 강화
  • 핵심 비즈니스 로직 개선
  • 공통 유틸리티 함수 최적화
  • 중간 규모 기능 개선
  • 모듈 구조 개선
This commit is contained in:
2025-11-06 09:25:10 +09:00
parent 49fc2f6906
commit 5bd2774784
7 changed files with 666 additions and 1 deletions

View File

@@ -12,6 +12,12 @@ export const types = {
CLEAR_PANEL_HISTORY: 'CLEAR_PANEL_HISTORY', CLEAR_PANEL_HISTORY: 'CLEAR_PANEL_HISTORY',
RESET_PANEL_HISTORY: 'RESET_PANEL_HISTORY', RESET_PANEL_HISTORY: 'RESET_PANEL_HISTORY',
// 🔽 [251106] panel queue actions - 순차적 실행을 보장하는 패널 액션 큐 시스템
ENQUEUE_PANEL_ACTION: 'ENQUEUE_PANEL_ACTION',
PROCESS_PANEL_QUEUE: 'PROCESS_PANEL_QUEUE',
CLEAR_PANEL_QUEUE: 'CLEAR_PANEL_QUEUE',
SET_QUEUE_PROCESSING: 'SET_QUEUE_PROCESSING',
// device actions // device actions
GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE', GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE',
REGISTER_DEVICE: 'REGISTER_DEVICE', REGISTER_DEVICE: 'REGISTER_DEVICE',

View File

@@ -0,0 +1,154 @@
import { types } from "./actionTypes";
/**
* [251106] 큐 기반 패널 액션들
*
* 이 액션들은 패널 액션 큐를 통해 순차적으로 실행되어,
* 비동기 dispatch로 인한 순서 문제를 해결합니다.
*
* 기존의 pushPanel, popPanel 등과 완전히 호환되며,
* 새로운 큐 시스템이 필요한 경우에만 사용합니다.
*/
// 고유 ID 생성을 위한 카운터
let queueItemId = 0;
/**
* 패널을 큐에 추가하여 순차적으로 실행합니다.
* @param {Object} panel - 패널 정보
* @param {boolean} duplicatable - 중복 허용 여부
* @returns {Object} Redux action
*/
export const pushPanelQueued = (panel, duplicatable = false) => ({
type: types.ENQUEUE_PANEL_ACTION,
payload: {
id: `queue_item_${++queueItemId}_${Date.now()}`,
action: 'PUSH_PANEL',
panel,
duplicatable,
timestamp: Date.now()
}
});
/**
* 큐를 통해 패널을 제거합니다.
* @param {string} panelName - 제거할 패널 이름 (optional, 없으면 마지막 패널 제거)
* @returns {Object} Redux action
*/
export const popPanelQueued = (panelName = null) => ({
type: types.ENQUEUE_PANEL_ACTION,
payload: {
id: `queue_item_${++queueItemId}_${Date.now()}`,
action: 'POP_PANEL',
panelName,
timestamp: Date.now()
}
});
/**
* 큐를 통해 패널 정보를 업데이트합니다.
* @param {Object} panelInfo - 업데이트할 패널 정보
* @returns {Object} Redux action
*/
export const updatePanelQueued = (panelInfo) => ({
type: types.ENQUEUE_PANEL_ACTION,
payload: {
id: `queue_item_${++queueItemId}_${Date.now()}`,
action: 'UPDATE_PANEL',
panelInfo,
timestamp: Date.now()
}
});
/**
* 큐를 통해 패널들을 초기화합니다.
* @param {Array} panels - 초기화할 패널 배열 (optional)
* @returns {Object} Redux action
*/
export const resetPanelsQueued = (panels = null) => ({
type: types.ENQUEUE_PANEL_ACTION,
payload: {
id: `queue_item_${++queueItemId}_${Date.now()}`,
action: 'RESET_PANELS',
panels,
timestamp: Date.now()
}
});
/**
* 패널 액션 큐를 즉시 비웁니다.
* @returns {Object} Redux action
*/
export const clearPanelQueue = () => ({
type: types.CLEAR_PANEL_QUEUE,
payload: {
timestamp: Date.now()
}
});
/**
* 패널 액션 큐 처리를 시작합니다.
* (주로 내부 사용용)
* @returns {Object} Redux action
*/
export const processPanelQueue = () => ({
type: types.PROCESS_PANEL_QUEUE,
payload: {
timestamp: Date.now()
}
});
/**
* 큐 처리 상태를 설정합니다.
* (주로 내부 사용용)
* @param {boolean} isProcessing - 처리 중 상태
* @returns {Object} Redux action
*/
export const setQueueProcessing = (isProcessing) => ({
type: types.SET_QUEUE_PROCESSING,
payload: {
isProcessing,
timestamp: Date.now()
}
});
/**
* 여러 패널 액션들을 한 번에 큐에 추가합니다.
* @param {Array} actions - 패널 액션 배열
* @returns {Function} dispatch 함수
*/
export const enqueueMultiplePanelActions = (actions) => {
return (dispatch) => {
actions.forEach(action => {
dispatch(action);
});
// 마지막에 큐 처리 시작
dispatch(processPanelQueue());
};
};
/**
* 패널 시퀀스를 큐에 추가하는 헬퍼 함수
* @param {Array} sequence - 패널 액션 시퀀스
* @returns {Function} dispatch 함수
*/
export const createPanelSequence = (sequence) => {
return (dispatch) => {
const queuedActions = sequence.map(item => {
switch (item.type) {
case 'push':
return pushPanelQueued(item.panel, item.duplicatable);
case 'pop':
return popPanelQueued(item.panelName);
case 'update':
return updatePanelQueued(item.panelInfo);
case 'reset':
return resetPanelsQueued(item.panels);
default:
return null;
}
}).filter(Boolean);
dispatch(enqueueMultiplePanelActions(queuedActions));
};
};

View File

@@ -0,0 +1,57 @@
import { types } from '../actions/actionTypes';
/**
* [251106] 패널 액션 큐 미들웨어
*
* 이 미들웨어는 패널 액션 큐를 자동으로 관리합니다:
* 1. 큐에 액션이 추가되면 자동으로 큐 처리를 시작
* 2. 큐 처리가 완료되면 다음 액션을 처리
* 3. 에러 발생 시 적절한 처리 및 로깅
*
* 이 미들웨어는 기존 패널 액션들에는 전혀 영향을 주지 않습니다.
*/
const panelQueueMiddleware = (store) => (next) => (action) => {
const result = next(action);
// 큐 관련 액션들만 처리
if (action.type === types.ENQUEUE_PANEL_ACTION) {
console.log('[panelQueueMiddleware] 🚀 ACTION_ENQUEUED', {
action: action.payload.action,
queueId: action.payload.id,
});
// 액션이 큐에 추가되면 바로 처리 시작
// setTimeout을 사용하여 현재 액션이 완전히 처리된 후에 큐 처리 시작
setTimeout(() => {
const currentState = store.getState();
// 패널 리듀서가 있는지 확인
if (currentState.panels) {
// 이미 처리 중이 아니면 큐 처리 시작
if (!currentState.panels.isProcessingQueue && currentState.panels.panelActionQueue.length > 0) {
console.log('[panelQueueMiddleware] ⚡ STARTING_QUEUE_PROCESS');
store.dispatch({ type: types.PROCESS_PANEL_QUEUE });
}
}
}, 0);
}
// 큐 처리가 완료되고 남은 큐가 있으면 계속 처리
if (action.type === types.PROCESS_PANEL_QUEUE) {
setTimeout(() => {
const currentState = store.getState();
if (currentState.panels) {
// 처리 중이 아니고 큐에 남은 액션이 있으면 계속 처리
if (!currentState.panels.isProcessingQueue && currentState.panels.panelActionQueue.length > 0) {
console.log('[panelQueueMiddleware] 🔄 CONTINUING_QUEUE_PROCESS');
store.dispatch({ type: types.PROCESS_PANEL_QUEUE });
}
}
}, 0);
}
return result;
};
export default panelQueueMiddleware;

View File

@@ -2,8 +2,19 @@ import { types } from '../actions/actionTypes';
import { panel_names } from '../utils/Config'; import { panel_names } from '../utils/Config';
const initialState = { const initialState = {
// 기존 상태 - 완전히 호환됨
panels: [], panels: [],
lastPanelAction: '', //"", "push", "pop", "update", "reset", "previewPush", "previewPop", "previewUpdate" lastPanelAction: '', //"", "push", "pop", "update", "reset", "previewPush", "previewPop", "previewUpdate"
// [251106] 패널 액션 큐 관련 상태 - 기존 기능에 전혀 영향 없음
panelActionQueue: [], // 처리 대기 중인 패널 액션 큐
isProcessingQueue: false, // 현재 큐 처리 중인지 여부
queueError: null, // 큐 처리 중 발생한 에러
queueStats: {
totalProcessed: 0, // 총 처리된 액션 수
failedCount: 0, // 실패한 액션 수
averageProcessingTime: 0 // 평균 처리 시간
}
}; };
// last one will be on top // last one will be on top
@@ -165,6 +176,162 @@ export const panelsReducer = (state = initialState, action) => {
}; };
} }
// [251106] 패널 액션 큐 관련 reducer 케이스들
case types.ENQUEUE_PANEL_ACTION: {
console.log('[panelReducer] 🟠 ENQUEUE_PANEL_ACTION', {
action: action.payload.action,
queueId: action.payload.id,
currentQueueLength: state.panelActionQueue.length,
});
const newQueueItem = action.payload;
const updatedQueue = [...state.panelActionQueue, newQueueItem];
return {
...state,
panelActionQueue: updatedQueue,
queueError: null, // 에러 초기화
};
}
case types.PROCESS_PANEL_QUEUE: {
console.log('[panelReducer] 🟡 PROCESS_PANEL_QUEUE', {
isProcessing: state.isProcessingQueue,
queueLength: state.panelActionQueue.length,
});
// 이미 처리 중이거나 큐가 비어있으면 아무것도 하지 않음
if (state.isProcessingQueue || state.panelActionQueue.length === 0) {
return state;
}
// 큐의 첫 번째 아이템을 가져옴
const firstQueueItem = state.panelActionQueue[0];
const remainingQueue = state.panelActionQueue.slice(1);
console.log('[panelReducer] 🟡 PROCESSING_QUEUE_ITEM', {
action: firstQueueItem.action,
queueId: firstQueueItem.id,
remainingQueueLength: remainingQueue.length,
});
// 실제 패널 액션을 실행하여 새로운 상태 계산
let newState = state;
const startTime = Date.now();
try {
switch (firstQueueItem.action) {
case 'PUSH_PANEL': {
const mockAction = {
type: types.PUSH_PANEL,
payload: firstQueueItem.panel,
duplicatable: firstQueueItem.duplicatable,
};
newState = panelsReducer(state, mockAction);
break;
}
case 'POP_PANEL': {
const mockAction = {
type: types.POP_PANEL,
payload: firstQueueItem.panelName,
};
newState = panelsReducer(state, mockAction);
break;
}
case 'UPDATE_PANEL': {
const mockAction = {
type: types.UPDATE_PANEL,
payload: firstQueueItem.panelInfo,
};
newState = panelsReducer(state, mockAction);
break;
}
case 'RESET_PANELS': {
const mockAction = {
type: types.RESET_PANELS,
payload: firstQueueItem.panels,
};
newState = panelsReducer(state, mockAction);
break;
}
default:
console.warn('[panelReducer] ⚠️ UNKNOWN_QUEUE_ACTION', firstQueueItem.action);
newState = state;
}
const processingTime = Date.now() - startTime;
const newTotalProcessed = state.queueStats.totalProcessed + 1;
const newAverageTime =
(state.queueStats.averageProcessingTime * state.queueStats.totalProcessed + processingTime) /
newTotalProcessed;
console.log('[panelReducer] ✅ QUEUE_ITEM_PROCESSED', {
action: firstQueueItem.action,
queueId: firstQueueItem.id,
processingTime,
newTotalProcessed,
});
return {
...newState,
panelActionQueue: remainingQueue,
isProcessingQueue: remainingQueue.length > 0,
queueStats: {
...newState.queueStats,
totalProcessed: newTotalProcessed,
averageProcessingTime: Math.round(newAverageTime * 100) / 100,
},
};
} catch (error) {
console.error('[panelReducer] ❌ QUEUE_PROCESSING_ERROR', {
action: firstQueueItem.action,
queueId: firstQueueItem.id,
error: error.message,
});
return {
...state,
panelActionQueue: remainingQueue,
isProcessingQueue: remainingQueue.length > 0,
queueError: {
action: firstQueueItem.action,
queueId: firstQueueItem.id,
error: error.message,
timestamp: Date.now(),
},
queueStats: {
...state.queueStats,
failedCount: state.queueStats.failedCount + 1,
},
};
}
}
case types.CLEAR_PANEL_QUEUE: {
console.log('[panelReducer] 🔵 CLEAR_PANEL_QUEUE', {
currentQueueLength: state.panelActionQueue.length,
});
return {
...state,
panelActionQueue: [],
isProcessingQueue: false,
queueError: null,
};
}
case types.SET_QUEUE_PROCESSING: {
console.log('[panelReducer] 🟣 SET_QUEUE_PROCESSING', {
isProcessing: action.payload.isProcessing,
queueLength: state.panelActionQueue.length,
});
return {
...state,
isProcessingQueue: action.payload.isProcessing,
};
}
default: default:
return state; return state;
} }

View File

@@ -0,0 +1,157 @@
/**
* [251106] 패널 액션 큐 사용 예시
*
* 이 파일은 패널 액션 큐 시스템의 사용 방법을 보여줍니다.
* 기존 패널 액션들과 비교하여 어떻게 순서를 보장하는지 보여줍니다.
*/
import {
pushPanelQueued,
popPanelQueued,
updatePanelQueued,
resetPanelsQueued,
clearPanelQueue,
createPanelSequence,
enqueueMultiplePanelActions
} from '../actions/queuedPanelActions';
import { panel_names } from './Config';
/**
* 예시 1: 기본적인 큐 기반 패널 조작
*
* 이전 방식: 순서가 보장되지 않음
* dispatch(pushPanel({ name: 'SEARCH_PANEL' }));
* dispatch(updatePanel({ name: 'SEARCH_PANEL', panelInfo: { data: 'search' } }));
* dispatch(popPanel('SEARCH_PANEL'));
*
* 새로운 방식: 순서가 완벽하게 보장됨
*/
export const basicPanelSequenceExample = (dispatch) => {
console.log('🎯 Basic Panel Sequence Example');
// 순서대로 실행됨이 보장됨
dispatch(pushPanelQueued({ name: panel_names.SEARCH_PANEL }));
dispatch(updatePanelQueued({
name: panel_names.SEARCH_PANEL,
panelInfo: { searchData: 'example search' }
}));
dispatch(popPanelQueued(panel_names.SEARCH_PANEL));
};
/**
* 예시 2: 복잡한 패널 시퀀스
*
* 여러 패널을 순차적으로 조작할 때 유용
*/
export const complexPanelFlowExample = (dispatch) => {
console.log('🔀 Complex Panel Flow Example');
const sequence = [
{ type: 'push', panel: { name: panel_names.SEARCH_PANEL } },
{ type: 'update', panelInfo: { name: panel_names.SEARCH_PANEL, panelInfo: { query: 'product' } } },
{ type: 'push', panel: { name: panel_names.DETAIL_PANEL } },
{ type: 'update', panelInfo: { name: panel_names.DETAIL_PANEL, panelInfo: { productId: '123' } } },
{ type: 'push', panel: { name: panel_names.CART_PANEL } },
{ type: 'pop', panelName: panel_names.CART_PANEL },
{ type: 'pop', panelName: panel_names.DETAIL_PANEL },
{ type: 'pop', panelName: panel_names.SEARCH_PANEL },
];
dispatch(createPanelSequence(sequence));
};
/**
* 예시 3: 에러 핸들링과 큐 관리
*/
export const errorHandlingExample = (dispatch) => {
console.log('⚠️ Error Handling Example');
// 일부 액션들을 큐에 추가
dispatch(pushPanelQueued({ name: panel_names.SEARCH_PANEL }));
dispatch(updatePanelQueued({ name: panel_names.SEARCH_PANEL, panelInfo: { invalid: 'data' } }));
dispatch(pushPanelQueued({ name: panel_names.DETAIL_PANEL }));
// 나중에 큐를 비우고 싶을 때
setTimeout(() => {
console.log('🧹 Clearing queue due to error');
dispatch(clearPanelQueue());
}, 1000);
};
/**
* 예시 4: 조건부 패널 액션
*/
export const conditionalPanelActionsExample = (dispatch, userAction) => {
console.log('🤔 Conditional Panel Actions Example');
const actions = [];
// 사용자 행동에 따라 다른 패널 시퀀스
if (userAction === 'search') {
actions.push(pushPanelQueued({ name: panel_names.SEARCH_PANEL }));
actions.push(updatePanelQueued({
name: panel_names.SEARCH_PANEL,
panelInfo: { mode: 'search' }
}));
} else if (userAction === 'browse') {
actions.push(pushPanelQueued({ name: panel_names.MAIN_PANEL }));
actions.push(updatePanelQueued({
name: panel_names.MAIN_PANEL,
panelInfo: { category: 'all' }
}));
}
// 공통 액션들
actions.push(pushPanelQueued({ name: panel_names.PLAYER_PANEL, panelInfo: { modal: true } }));
actions.push(popPanelQueued(panel_names.PLAYER_PANEL));
dispatch(enqueueMultiplePanelActions(actions));
};
/**
* 예시 5: 디버깅을 위한 큐 상태 확인
*/
export const debugQueueStateExample = (dispatch, getState) => {
console.log('🐛 Debug Queue State Example');
// 액션 추가
dispatch(pushPanelQueued({ name: panel_names.DEBUG_PANEL }));
// 상태 확인
const currentState = getState();
const panelState = currentState.panels;
console.log('Current Panel State:', {
panels: panelState.panels.map(p => p.name),
queueLength: panelState.panelActionQueue.length,
isProcessing: panelState.isProcessingQueue,
stats: panelState.queueStats,
error: panelState.queueError
});
};
/**
* 예시 6: 기존 방식과 새로운 방식의 비교
*/
export const comparisonExample = (dispatch) => {
console.log('📊 Comparison Example');
// 기존 방식 (순서 보장되지 않음)
console.log('❌ Old way (order not guaranteed):');
setTimeout(() => {
dispatch({ type: 'PUSH_PANEL', payload: { name: 'PANEL_A' } });
dispatch({ type: 'UPDATE_PANEL', payload: { name: 'PANEL_A', panelInfo: { data: 'test' } } });
dispatch({ type: 'PUSH_PANEL', payload: { name: 'PANEL_B' } });
dispatch({ type: 'POP_PANEL', payload: 'PANEL_A' });
}, 1000);
// 새로운 방식 (순서 완벽 보장)
console.log('✅ New way (order guaranteed):');
setTimeout(() => {
dispatch(pushPanelQueued({ name: 'PANEL_A' }));
dispatch(updatePanelQueued({ name: 'PANEL_A', panelInfo: { data: 'test' } }));
dispatch(pushPanelQueued({ name: 'PANEL_B' }));
dispatch(popPanelQueued('PANEL_A'));
}, 2000);
};

View File

@@ -0,0 +1,124 @@
/**
* [251106] 패널 액션 큐 설정 가이드
*
* 이 파일은 패널 액션 큐 시스템을 기존 Redux store에 통합하는 방법을 보여줍니다.
* 기존 코드에 전혀 영향을 주지 않고 새로운 기능만 추가합니다.
*/
// 1⃣ 미들웨어 import
import panelQueueMiddleware from '../middleware/panelQueueMiddleware';
/**
* 기존 store 설정 파일에 미들웨어를 추가하는 예시
*
* 기존 설정 (예: src/store.js 또는 src/App/store.js):
*
* import { createStore, applyMiddleware } from 'redux';
* import rootReducer from './reducers';
*
* const store = createStore(
* rootReducer,
* applyMiddleware(
* // 기존 미들웨어들...
* )
* );
*
* export default store;
*/
/**
* 🔧 새로운 설정 (미들웨어 추가):
*
* import { createStore, applyMiddleware } from 'redux';
* import rootReducer from './reducers';
* import panelQueueMiddleware from '../middleware/panelQueueMiddleware'; // 추가
*
* const store = createStore(
* rootReducer,
* applyMiddleware(
* // 기존 미들웨어들...
* panelQueueMiddleware, // 추가: 패널 큐 미들웨어
* )
* );
*
* export default store;
*/
/**
* 📋 설정 체크리스트
*
* ✅ 1. actionTypes.js에 새로운 액션 타입들 추가됨
* - ENQUEUE_PANEL_ACTION
* - PROCESS_PANEL_QUEUE
* - CLEAR_PANEL_QUEUE
* - SET_QUEUE_PROCESSING
*
* ✅ 2. queuedPanelActions.js 파일 생성됨
* - pushPanelQueued, popPanelQueued 등
* - createPanelSequence, enqueueMultiplePanelActions 등
*
* ✅ 3. panelReducer.js 확장됨
* - 기존 로직은 그대로 유지
* - 새로운 큐 상태 추가: panelActionQueue, isProcessingQueue 등
* - 새로운 reducer 케이스들 추가
*
* ✅ 4. panelQueueMiddleware.js 생성됨
* - 큐 자동 처리 로직
* - 에러 핸들링
*
* ✅ 5. panelQueueExamples.js 생성됨
* - 사용 예시들
* - 기존 방식과 비교
*
* 🔄 6. Redux store에 미들웨어 추가 (개발자가 직접 해야 함)
* - 기존 미들웨어들 사이에 panelQueueMiddleware 추가
*
* 🧪 7. 테스트 (선택사항)
* - 기존 패널 기능들이 정상 작동하는지 확인
* - 새로운 큐 기능들이 순서대로 작동하는지 확인
*/
/**
* 🎯 마이그레이션 전략
*
* 1. 단계별 도입:
* - 첫 주: 새로운 큐 시스템 코드 배포 (기능은 사용하지 않음)
* - 둘째 주: 테스트 환경에서 새로운 액션들 사용해보기
* - 셋째 주: 문제가 있는 부분부터 선택적으로 적용
* - 넷째 주: 전체적으로 적용 (필요한 경우에만)
*
* 2. 롤백 계획:
* - 기존 액션들은 전혀 수정되지 않았으므로 언제든 롤백 가능
* - 미들웨어만 제거하면 기존 방식으로 돌아감
*
* 3. 테스트 방법:
* - 기존 기능 테스트: pushPanel, popPanel 등이 여전히 작동하는지
* - 새로운 기능 테스트: pushPanelQueued 등이 순서대로 작동하는지
* - 성능 테스트: 큐 처리가 앱 성능에 영향을 주지 않는지
*/
/**
* 🔍 디버깅 팁
*
* 콘솔에서 다음 로그들을 확인하세요:
*
* 🟠 ENQUEUE_PANEL_ACTION: 액션이 큐에 추가됨
* 🟡 PROCESS_PANEL_QUEUE: 큐 처리 시작
* ✅ QUEUE_ITEM_PROCESSED: 액션 처리 완료
* ❌ QUEUE_PROCESSING_ERROR: 처리 중 에러 발생
*
* Redux DevTools에서 상태 확인:
* - panels.panelActionQueue: 대기 중인 액션들
* - panels.isProcessingQueue: 현재 처리 중인지 여부
* - panels.queueStats: 처리 통계
* - panels.queueError: 에러 정보
*/
export const SETUP_INSTRUCTIONS = {
middleware: 'panelQueueMiddleware를 store에 추가하세요',
import: 'import panelQueueMiddleware from "../middleware/panelQueueMiddleware"',
usage: 'pushPanelQueued, popPanelQueued 등을 사용하세요',
examples: 'panelQueueExamples.js를 참고하세요'
};
export default SETUP_INSTRUCTIONS;

View File

@@ -213,7 +213,7 @@ const VoiceInputOverlay = ({
const [isSilenceCheckActive, setIsSilenceCheckActive] = useState(false); const [isSilenceCheckActive, setIsSilenceCheckActive] = useState(false);
const [hasReached5Chars, setHasReached5Chars] = useState(false); // 처음 5글자 도달 추적 const [hasReached5Chars, setHasReached5Chars] = useState(false); // 처음 5글자 도달 추적
// 💬 Bubble 버튼 상태 (true: 버튼 O, false: 텍스트만) // 💬 Bubble 버튼 상태 (true: 버튼 O, false: 텍스트만)
const [isBubbleButton, setIsBubbleButton] = useState(false); // 첫 발화때는 true (Try Saying) const [isBubbleButton, setIsBubbleButton] = useState(process.env.NODE_ENV === 'development'); // 첫 발화때는 true (Try Saying)
// useSearchHistory Hook 적용 (음성검색 기록 관리) // useSearchHistory Hook 적용 (음성검색 기록 관리)
const { addVoiceSearch } = useSearchHistory(); const { addVoiceSearch } = useSearchHistory();