[251019] feat: Redux 기반 비디오 오버레이 제어 시스템 구축
modal=true 상태에서 비디오 controls, header 등 오버레이를 중앙화된 Redux로 관리하는 시스템 구현 - videoOverlayActions: 8개 action type 정의 (modal 전환, controls 표시/숨김, autoClose 제어) - videoOverlayReducer: overlay 상태 관리 및 상태 변경 로직 - autoCloseMiddleware: 자동 숨김 타이머를 Redux 미들웨어에서 중앙 관리 - store.js: reducer 및 middleware 등록 이를 통해 modal=true 모드에서도 autoClose 타이머와 overlay 표시/숨김을 외부에서 명시적으로 제어할 수 있음
This commit is contained in:
85
com.twin.app.shoptime/src/actions/videoOverlayActions.js
Normal file
85
com.twin.app.shoptime/src/actions/videoOverlayActions.js
Normal file
@@ -0,0 +1,85 @@
|
||||
// Video Overlay Actions - Redux를 통한 중앙화된 overlay 제어
|
||||
// modal=true 상태에서 비디오 오버레이(controls, header 등)를 제어
|
||||
|
||||
export const VIDEO_OVERLAY_ACTIONS = {
|
||||
// Modal 전환
|
||||
SWITCH_TO_MODAL: 'SWITCH_TO_MODAL',
|
||||
SWITCH_TO_FULLSCREEN: 'SWITCH_TO_FULLSCREEN',
|
||||
|
||||
// Controls 표시/숨김
|
||||
SHOW_CONTROLS: 'SHOW_CONTROLS',
|
||||
HIDE_CONTROLS: 'HIDE_CONTROLS',
|
||||
TOGGLE_CONTROLS: 'TOGGLE_CONTROLS',
|
||||
|
||||
// AutoClose 제어
|
||||
START_AUTO_CLOSE: 'START_AUTO_CLOSE',
|
||||
STOP_AUTO_CLOSE: 'STOP_AUTO_CLOSE',
|
||||
RESET_AUTO_CLOSE: 'RESET_AUTO_CLOSE',
|
||||
UPDATE_AUTO_CLOSE_TIMEOUT: 'UPDATE_AUTO_CLOSE_TIMEOUT',
|
||||
|
||||
// Overlay 세부 제어
|
||||
SET_OVERLAY_VISIBILITY: 'SET_OVERLAY_VISIBILITY',
|
||||
};
|
||||
|
||||
// Modal 전환 액션
|
||||
export const switchToModal = () => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.SWITCH_TO_MODAL,
|
||||
payload: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
export const switchToFullscreen = () => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.SWITCH_TO_FULLSCREEN,
|
||||
payload: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
// Controls 표시/숨김 액션
|
||||
export const showControls = () => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.SHOW_CONTROLS,
|
||||
payload: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
export const hideControls = () => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.HIDE_CONTROLS,
|
||||
payload: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
export const toggleControls = () => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.TOGGLE_CONTROLS,
|
||||
payload: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
// AutoClose 타이머 제어 액션
|
||||
export const startAutoClose = (timeout = 3000) => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.START_AUTO_CLOSE,
|
||||
payload: {
|
||||
timeout,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
export const stopAutoClose = () => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.STOP_AUTO_CLOSE,
|
||||
payload: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
export const resetAutoClose = () => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.RESET_AUTO_CLOSE,
|
||||
payload: { timestamp: Date.now() },
|
||||
});
|
||||
|
||||
export const updateAutoCloseTimeout = (timeout) => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.UPDATE_AUTO_CLOSE_TIMEOUT,
|
||||
payload: {
|
||||
timeout,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
// Overlay 세부 제어 액션
|
||||
export const setOverlayVisibility = (overlayData) => ({
|
||||
type: VIDEO_OVERLAY_ACTIONS.SET_OVERLAY_VISIBILITY,
|
||||
payload: {
|
||||
...overlayData,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
150
com.twin.app.shoptime/src/middleware/autoCloseMiddleware.js
Normal file
150
com.twin.app.shoptime/src/middleware/autoCloseMiddleware.js
Normal file
@@ -0,0 +1,150 @@
|
||||
// Auto Close Middleware - Redux를 통한 비디오 controls 자동 숨김 타이머 관리
|
||||
// modal=true 상태에서 비디오 오버레이(controls 등)를 자동으로 숨기는 기능 제공
|
||||
|
||||
import {
|
||||
VIDEO_OVERLAY_ACTIONS,
|
||||
hideControls,
|
||||
stopAutoClose,
|
||||
} from '../actions/videoOverlayActions';
|
||||
|
||||
// 타이머 저장소
|
||||
let autoCloseTimer = null;
|
||||
|
||||
/**
|
||||
* Auto Close Middleware
|
||||
* - START_AUTO_CLOSE: 타이머 시작
|
||||
* - STOP_AUTO_CLOSE, HIDE_CONTROLS: 타이머 중지
|
||||
* - TOGGLE_CONTROLS: Controls 표시 상태로 토글되면 타이머 시작
|
||||
* - 사용자 활동 감지 시: 타이머 리셋
|
||||
*/
|
||||
export const autoCloseMiddleware = (store) => (next) => (action) => {
|
||||
const result = next(action);
|
||||
|
||||
// 현재 상태 획득
|
||||
const state = store.getState();
|
||||
const overlayState = state.videoOverlay;
|
||||
|
||||
// Action별 처리
|
||||
switch (action.type) {
|
||||
case VIDEO_OVERLAY_ACTIONS.START_AUTO_CLOSE: {
|
||||
// 기존 타이머 정리
|
||||
clearTimeout(autoCloseTimer);
|
||||
|
||||
const timeout = action.payload?.timeout || 3000;
|
||||
|
||||
console.log('[autoCloseMiddleware] Starting auto-close timer:', timeout, 'ms');
|
||||
|
||||
// 새 타이머 시작
|
||||
autoCloseTimer = setTimeout(() => {
|
||||
console.log('[autoCloseMiddleware] Auto-close timeout reached - hiding controls');
|
||||
store.dispatch(hideControls());
|
||||
autoCloseTimer = null;
|
||||
}, timeout);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case VIDEO_OVERLAY_ACTIONS.STOP_AUTO_CLOSE: {
|
||||
if (autoCloseTimer) {
|
||||
console.log('[autoCloseMiddleware] Stopping auto-close timer');
|
||||
clearTimeout(autoCloseTimer);
|
||||
autoCloseTimer = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case VIDEO_OVERLAY_ACTIONS.HIDE_CONTROLS: {
|
||||
// Controls 숨김 시 타이머도 중지
|
||||
if (autoCloseTimer) {
|
||||
clearTimeout(autoCloseTimer);
|
||||
autoCloseTimer = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case VIDEO_OVERLAY_ACTIONS.TOGGLE_CONTROLS: {
|
||||
// Controls가 표시 상태로 토글되면 타이머 시작
|
||||
if (!overlayState.controls.visible) {
|
||||
// 다음 상태에서 visible이 true가 될 것이므로
|
||||
// 타이머를 시작해야 함
|
||||
clearTimeout(autoCloseTimer);
|
||||
|
||||
const timeout = overlayState.autoClose.timeout || 3000;
|
||||
console.log('[autoCloseMiddleware] Controls toggled to visible - starting timer:', timeout, 'ms');
|
||||
|
||||
autoCloseTimer = setTimeout(() => {
|
||||
console.log('[autoCloseMiddleware] Toggle auto-close timeout reached - hiding controls');
|
||||
store.dispatch(hideControls());
|
||||
autoCloseTimer = null;
|
||||
}, timeout);
|
||||
} else {
|
||||
// Controls 숨김 상태로 토글되면 타이머 중지
|
||||
if (autoCloseTimer) {
|
||||
clearTimeout(autoCloseTimer);
|
||||
autoCloseTimer = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case VIDEO_OVERLAY_ACTIONS.RESET_AUTO_CLOSE: {
|
||||
// 타이머 리셋 (사용자 활동 감지)
|
||||
clearTimeout(autoCloseTimer);
|
||||
|
||||
if (overlayState.controls.visible) {
|
||||
const timeout = overlayState.autoClose.timeout || 3000;
|
||||
console.log('[autoCloseMiddleware] Resetting auto-close timer:', timeout, 'ms');
|
||||
|
||||
autoCloseTimer = setTimeout(() => {
|
||||
console.log('[autoCloseMiddleware] Reset auto-close timeout reached - hiding controls');
|
||||
store.dispatch(hideControls());
|
||||
autoCloseTimer = null;
|
||||
}, timeout);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case VIDEO_OVERLAY_ACTIONS.SHOW_CONTROLS: {
|
||||
// Controls 표시 시 타이머 시작
|
||||
clearTimeout(autoCloseTimer);
|
||||
|
||||
const timeout = overlayState.autoClose.timeout || 3000;
|
||||
console.log('[autoCloseMiddleware] Controls shown - starting timer:', timeout, 'ms');
|
||||
|
||||
autoCloseTimer = setTimeout(() => {
|
||||
console.log('[autoCloseMiddleware] Show auto-close timeout reached - hiding controls');
|
||||
store.dispatch(hideControls());
|
||||
autoCloseTimer = null;
|
||||
}, timeout);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case VIDEO_OVERLAY_ACTIONS.SWITCH_TO_MODAL:
|
||||
case VIDEO_OVERLAY_ACTIONS.SWITCH_TO_FULLSCREEN: {
|
||||
// Modal/Fullscreen 전환 시 타이머 중지
|
||||
if (autoCloseTimer) {
|
||||
console.log('[autoCloseMiddleware] Modal/Fullscreen switched - stopping timer');
|
||||
clearTimeout(autoCloseTimer);
|
||||
autoCloseTimer = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cleanup 함수 (필요시 호출)
|
||||
* - 앱 종료 시 타이머 정리
|
||||
*/
|
||||
export const cleanupAutoCloseMiddleware = () => {
|
||||
if (autoCloseTimer) {
|
||||
clearTimeout(autoCloseTimer);
|
||||
autoCloseTimer = null;
|
||||
}
|
||||
};
|
||||
185
com.twin.app.shoptime/src/reducers/videoOverlayReducer.js
Normal file
185
com.twin.app.shoptime/src/reducers/videoOverlayReducer.js
Normal file
@@ -0,0 +1,185 @@
|
||||
// Video Overlay Reducer - modal=true 상태에서 비디오 오버레이 제어
|
||||
import { VIDEO_OVERLAY_ACTIONS } from '../actions/videoOverlayActions';
|
||||
|
||||
export const initialState = {
|
||||
// Modal 상태
|
||||
modal: true, // true(modal), false(fullscreen)
|
||||
|
||||
// Overlay Controls 상태
|
||||
controls: {
|
||||
visible: false, // controls 표시 여부
|
||||
sliderVisible: false, // 슬라이더 표시 여부
|
||||
titleVisible: true, // 제목 표시 여부
|
||||
feedbackVisible: false, // 피드백 표시 여부
|
||||
},
|
||||
|
||||
// AutoClose 타이머 제어
|
||||
autoClose: {
|
||||
enabled: true, // autoClose 활성화 여부
|
||||
timeout: 3000, // 타임아웃 시간 (ms)
|
||||
remainingTime: 0, // 남은 시간 (ms)
|
||||
active: false, // 타이머 실행 중 여부
|
||||
},
|
||||
|
||||
// 비디오 정보
|
||||
videoInfo: {
|
||||
url: '',
|
||||
title: '',
|
||||
thumbnailUrl: '',
|
||||
isYoutube: false,
|
||||
},
|
||||
|
||||
// 상태 변경 기록 (디버깅용)
|
||||
timestamp: null,
|
||||
lastAction: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer handlers 맵
|
||||
*/
|
||||
const handlers = {
|
||||
[VIDEO_OVERLAY_ACTIONS.SWITCH_TO_MODAL]: (state, action) => ({
|
||||
...state,
|
||||
modal: true,
|
||||
autoClose: {
|
||||
...state.autoClose,
|
||||
active: false, // 모달 전환 시 타이머 중지
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.SWITCH_TO_MODAL,
|
||||
}),
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.SWITCH_TO_FULLSCREEN]: (state, action) => ({
|
||||
...state,
|
||||
modal: false,
|
||||
autoClose: {
|
||||
...state.autoClose,
|
||||
active: false, // 전체화면 전환 시 타이머 중지
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.SWITCH_TO_FULLSCREEN,
|
||||
}),
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.SHOW_CONTROLS]: (state, action) => ({
|
||||
...state,
|
||||
controls: {
|
||||
...state.controls,
|
||||
visible: true,
|
||||
sliderVisible: true,
|
||||
titleVisible: true,
|
||||
},
|
||||
autoClose: {
|
||||
...state.autoClose,
|
||||
active: true, // Controls 표시 시 autoClose 시작
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.SHOW_CONTROLS,
|
||||
}),
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.HIDE_CONTROLS]: (state, action) => ({
|
||||
...state,
|
||||
controls: {
|
||||
...state.controls,
|
||||
visible: false,
|
||||
sliderVisible: false,
|
||||
feedbackVisible: false,
|
||||
},
|
||||
autoClose: {
|
||||
...state.autoClose,
|
||||
active: false, // Controls 숨김 시 autoClose 중지
|
||||
remainingTime: 0,
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.HIDE_CONTROLS,
|
||||
}),
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.TOGGLE_CONTROLS]: (state, action) => {
|
||||
const isCurrentlyVisible = state.controls.visible;
|
||||
return {
|
||||
...state,
|
||||
controls: {
|
||||
...state.controls,
|
||||
visible: !isCurrentlyVisible,
|
||||
sliderVisible: !isCurrentlyVisible,
|
||||
titleVisible: !isCurrentlyVisible || state.controls.titleVisible,
|
||||
},
|
||||
autoClose: {
|
||||
...state.autoClose,
|
||||
active: !isCurrentlyVisible, // 표시 상태로 토글되면 autoClose 시작
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.TOGGLE_CONTROLS,
|
||||
};
|
||||
},
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.START_AUTO_CLOSE]: (state, action) => ({
|
||||
...state,
|
||||
autoClose: {
|
||||
enabled: true,
|
||||
timeout: action.payload.timeout || state.autoClose.timeout,
|
||||
remainingTime: action.payload.timeout || state.autoClose.timeout,
|
||||
active: true,
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.START_AUTO_CLOSE,
|
||||
}),
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.STOP_AUTO_CLOSE]: (state, action) => ({
|
||||
...state,
|
||||
autoClose: {
|
||||
...state.autoClose,
|
||||
active: false,
|
||||
remainingTime: 0,
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.STOP_AUTO_CLOSE,
|
||||
}),
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.RESET_AUTO_CLOSE]: (state, action) => ({
|
||||
...state,
|
||||
autoClose: {
|
||||
...state.autoClose,
|
||||
remainingTime: state.autoClose.timeout,
|
||||
active: true,
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.RESET_AUTO_CLOSE,
|
||||
}),
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.UPDATE_AUTO_CLOSE_TIMEOUT]: (state, action) => ({
|
||||
...state,
|
||||
autoClose: {
|
||||
...state.autoClose,
|
||||
timeout: action.payload.timeout,
|
||||
remainingTime: action.payload.timeout,
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.UPDATE_AUTO_CLOSE_TIMEOUT,
|
||||
}),
|
||||
|
||||
[VIDEO_OVERLAY_ACTIONS.SET_OVERLAY_VISIBILITY]: (state, action) => ({
|
||||
...state,
|
||||
controls: {
|
||||
...state.controls,
|
||||
...action.payload,
|
||||
},
|
||||
timestamp: action.payload.timestamp,
|
||||
lastAction: VIDEO_OVERLAY_ACTIONS.SET_OVERLAY_VISIBILITY,
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Main Reducer
|
||||
* @param {Object} state - 현재 상태
|
||||
* @param {Object} action - Redux action
|
||||
* @returns {Object} 다음 상태
|
||||
*/
|
||||
export const videoOverlayReducer = (state = initialState, action = {}) => {
|
||||
const handler = handlers[action.type];
|
||||
|
||||
if (handler) {
|
||||
return handler(state, action);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
} from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { autoCloseMiddleware } from '../middleware/autoCloseMiddleware';
|
||||
import { appDataReducer } from '../reducers/appDataReducer';
|
||||
import { billingReducer } from '../reducers/billingReducer';
|
||||
import { brandReducer } from '../reducers/brandReducer';
|
||||
@@ -34,6 +35,7 @@ import { productReducer } from '../reducers/productReducer';
|
||||
import { searchReducer } from '../reducers/searchReducer';
|
||||
import { shippingReducer } from '../reducers/shippingReducer';
|
||||
import { toastReducer } from '../reducers/toastReducer';
|
||||
import { videoOverlayReducer } from '../reducers/videoOverlayReducer';
|
||||
import { videoPlayReducer } from '../reducers/videoPlayReducer';
|
||||
import { voiceReducer } from '../reducers/voiceReducer';
|
||||
|
||||
@@ -64,9 +66,13 @@ const rootReducer = combineReducers({
|
||||
emp: empReducer,
|
||||
foryou: foryouReducer,
|
||||
toast: toastReducer,
|
||||
videoOverlay: videoOverlayReducer,
|
||||
videoPlay: videoPlayReducer,
|
||||
voice: voiceReducer,
|
||||
convert: convertReducer,
|
||||
});
|
||||
|
||||
export const store = createStore(rootReducer, applyMiddleware(thunk));
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(thunk, autoCloseMiddleware)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user