[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:
2025-10-19 13:15:12 +09:00
parent 325b648993
commit 9308fa0912
4 changed files with 427 additions and 1 deletions

View 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(),
},
});

View 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;
}
};

View 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;
};

View File

@@ -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)
);