From 5bd27747848ffdb7a6cc263e2cc55ea75adfc3cd Mon Sep 17 00:00:00 2001 From: optrader Date: Thu, 6 Nov 2025 09:25:10 +0900 Subject: [PATCH] [251106] feat: Queued Panel functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ• 컀밋 μ‹œκ°„: 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 πŸ”§ μ£Όμš” λ³€κ²½ λ‚΄μš©: β€’ νƒ€μž… μ‹œμŠ€ν…œ μ•ˆμ •μ„± κ°•ν™” β€’ 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 κ°œμ„  β€’ 곡톡 μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ μ΅œμ ν™” β€’ 쀑간 규λͺ¨ κΈ°λŠ₯ κ°œμ„  β€’ λͺ¨λ“ˆ ꡬ쑰 κ°œμ„  --- .../src/actions/actionTypes.js | 6 + .../src/actions/queuedPanelActions.js | 154 ++++++++++++++++ .../src/middleware/panelQueueMiddleware.js | 57 ++++++ .../src/reducers/panelReducer.js | 167 ++++++++++++++++++ .../src/utils/panelQueueExamples.js | 157 ++++++++++++++++ .../src/utils/panelQueueSetupGuide.js | 124 +++++++++++++ .../VoiceInputOverlay/VoiceInputOverlay.jsx | 2 +- 7 files changed, 666 insertions(+), 1 deletion(-) create mode 100644 com.twin.app.shoptime/src/actions/queuedPanelActions.js create mode 100644 com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js create mode 100644 com.twin.app.shoptime/src/utils/panelQueueExamples.js create mode 100644 com.twin.app.shoptime/src/utils/panelQueueSetupGuide.js diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js index 23f87141..c088ae3f 100644 --- a/com.twin.app.shoptime/src/actions/actionTypes.js +++ b/com.twin.app.shoptime/src/actions/actionTypes.js @@ -12,6 +12,12 @@ export const types = { CLEAR_PANEL_HISTORY: 'CLEAR_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 GET_AUTHENTICATION_CODE: 'GET_AUTHENTICATION_CODE', REGISTER_DEVICE: 'REGISTER_DEVICE', diff --git a/com.twin.app.shoptime/src/actions/queuedPanelActions.js b/com.twin.app.shoptime/src/actions/queuedPanelActions.js new file mode 100644 index 00000000..81c826ef --- /dev/null +++ b/com.twin.app.shoptime/src/actions/queuedPanelActions.js @@ -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)); + }; +}; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js b/com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js new file mode 100644 index 00000000..f603fe98 --- /dev/null +++ b/com.twin.app.shoptime/src/middleware/panelQueueMiddleware.js @@ -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; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/reducers/panelReducer.js b/com.twin.app.shoptime/src/reducers/panelReducer.js index 6edc160e..b9a324a5 100644 --- a/com.twin.app.shoptime/src/reducers/panelReducer.js +++ b/com.twin.app.shoptime/src/reducers/panelReducer.js @@ -2,8 +2,19 @@ import { types } from '../actions/actionTypes'; import { panel_names } from '../utils/Config'; const initialState = { + // κΈ°μ‘΄ μƒνƒœ - μ™„μ „νžˆ ν˜Έν™˜λ¨ panels: [], 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 @@ -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: return state; } diff --git a/com.twin.app.shoptime/src/utils/panelQueueExamples.js b/com.twin.app.shoptime/src/utils/panelQueueExamples.js new file mode 100644 index 00000000..488fe8a9 --- /dev/null +++ b/com.twin.app.shoptime/src/utils/panelQueueExamples.js @@ -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); +}; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/utils/panelQueueSetupGuide.js b/com.twin.app.shoptime/src/utils/panelQueueSetupGuide.js new file mode 100644 index 00000000..e33215eb --- /dev/null +++ b/com.twin.app.shoptime/src/utils/panelQueueSetupGuide.js @@ -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; \ No newline at end of file diff --git a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx index ed8904ce..74f07ae5 100644 --- a/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx +++ b/com.twin.app.shoptime/src/views/SearchPanel/VoiceInputOverlay/VoiceInputOverlay.jsx @@ -213,7 +213,7 @@ const VoiceInputOverlay = ({ const [isSilenceCheckActive, setIsSilenceCheckActive] = useState(false); const [hasReached5Chars, setHasReached5Chars] = useState(false); // 처음 5κΈ€μž 도달 좔적 // πŸ’¬ 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 적용 (μŒμ„±κ²€μƒ‰ 기둝 관리) const { addVoiceSearch } = useSearchHistory();