From 2d93ee6ca4a641d24bb7e31276a60ce6f50b696a Mon Sep 17 00:00:00 2001 From: optrader Date: Sun, 16 Nov 2025 17:28:35 +0900 Subject: [PATCH] =?UTF-8?q?[251116]=20feat:=20playeReducer=20,=20playActio?= =?UTF-8?q?ns.js=EC=97=90=20videoState=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ• 컀밋 μ‹œκ°„: 2025. 11. 16. 17:28:35 πŸ“Š λ³€κ²½ 톡계: β€’ 총 파일: 5개 β€’ μΆ”κ°€: +398쀄 β€’ μ‚­μ œ: -8쀄 πŸ“ μΆ”κ°€λœ 파일: + com.twin.app.shoptime/[251116]_video_state_management_design.md πŸ“ μˆ˜μ •λœ 파일: ~ com.twin.app.shoptime/src/actions/actionTypes.js ~ com.twin.app.shoptime/src/actions/playActions.js ~ com.twin.app.shoptime/src/reducers/playReducer.js ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx πŸ”§ ν•¨μˆ˜ λ³€κ²½ λ‚΄μš©: πŸ“„ com.twin.app.shoptime/src/actions/playActions.js (javascript): βœ… Added: returnToPreview() πŸ”„ Modified: finishModalVideoForce(), shrinkVideoTo1px(), resumePlayerControl() πŸ“„ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx (javascript): πŸ”„ Modified: SpotlightContainerDecorator() πŸ“„ com.twin.app.shoptime/[251116]_video_state_management_design.md (md파일): βœ… Added: curry(), dispatch(), useSelector() πŸ”§ μ£Όμš” λ³€κ²½ λ‚΄μš©: β€’ νƒ€μž… μ‹œμŠ€ν…œ μ•ˆμ •μ„± κ°•ν™” β€’ 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 κ°œμ„  β€’ 개발 λ¬Έμ„œ 및 κ°€μ΄λ“œ κ°œμ„  --- .../[251116]_video_state_management_design.md | 220 ++++++++++++++++++ .../src/actions/actionTypes.js | 21 ++ .../src/actions/playActions.js | 189 ++++++++++++++- .../src/reducers/playReducer.js | 197 ++++++++++++++++ .../views/HomePanel/HomeBanner/RandomUnit.jsx | 21 +- 5 files changed, 633 insertions(+), 15 deletions(-) create mode 100644 com.twin.app.shoptime/[251116]_video_state_management_design.md diff --git a/com.twin.app.shoptime/[251116]_video_state_management_design.md b/com.twin.app.shoptime/[251116]_video_state_management_design.md new file mode 100644 index 00000000..c96fd3f2 --- /dev/null +++ b/com.twin.app.shoptime/[251116]_video_state_management_design.md @@ -0,0 +1,220 @@ +# [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ κ΅¬ν˜„ + +## κ°œμš” + +기쑴의 videoPlayReducerλŠ” μœ μ§€ν•˜λ˜, PlayerPanelκ³Ό VideoPlayer.jsλ₯Ό μœ„ν•œ μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œμ„ playerReducer에 κ΅¬ν˜„ν•œλ‹€. μž¬μƒ μƒνƒœμ™€ ν™”λ©΄ μƒνƒœλ₯Ό λΆ„λ¦¬ν•˜μ—¬ 더 μ •λ°€ν•œ λΉ„λ””μ˜€ μƒνƒœ μ œμ–΄λ₯Ό κ°€λŠ₯ν•˜κ²Œ ν•œλ‹€. + +## 섀계 원칙 + +1. **κΈ°μ‘΄ videoPlayReducer μœ μ§€**: λ‹€λ₯Έ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ‚¬μš© 쀑일 수 μžˆμœΌλ―€λ‘œ κ·ΈλŒ€λ‘œ λ‘”λ‹€ +2. **playerReducer에 μƒˆλ‘œμš΄ μƒνƒœ μ‹œμŠ€ν…œ κ΅¬ν˜„**: PlayerPanelκ³Ό VideoPlayer.js μ „μš© +3. **이쀑 μƒνƒœ 관리**: μž¬μƒ μƒνƒœ(Playback Status) + ν™”λ©΄ μƒνƒœ(Display Status) +4. **κΈ°μ‘΄ νŒ¨ν„΄ λ”°λ₯΄κΈ°**: FP μŠ€νƒ€μΌμ˜ curry, get, set ν™œμš© + +## μƒˆλ‘œμš΄ μƒνƒœ ꡬ쑰 + +### μƒμˆ˜ μ •μ˜ (playerActions.js) + +```javascript +// μž¬μƒ μƒνƒœ +export const PLAYBACK_STATUS = { + LOADING: 'loading', // λΉ„λ””μ˜€ λ‘œλ”© 쀑 + LOAD_SUCCESS: 'load_success', // λΉ„λ””μ˜€ λ‘œλ”© 성곡 + LOAD_ERROR: 'load_error', // λΉ„λ””μ˜€ λ‘œλ”© 였λ₯˜ + PLAYING: 'playing', // λΉ„λ””μ˜€ μž¬μƒ 쀑 + NOT_PLAYING: 'not_playing', // λΉ„λ””μ˜€ μž¬μƒ μ•„λ‹˜ (μ •μ§€/μΌμ‹œμ •μ§€) + BUFFERING: 'buffering' // 버퍼링 쀑 +}; + +// ν™”λ©΄ μƒνƒœ +export const DISPLAY_STATUS = { + HIDDEN: 'hidden', // 화면에 μ•ˆλ³΄μž„ + VISIBLE: 'visible', // 화면에 λ³΄μž„ + MINIMIZED: 'minimized', // μ΅œμ†Œν™”λ¨ + FULLSCREEN: 'fullscreen' // 전체화면 +}; +``` + +### 초기 μƒνƒœ (playerReducer) + +```javascript +// κΈ°μ‘΄ playerReducer μƒνƒœμ— μΆ”κ°€ +const initialState = { + // ... κΈ°μ‘΄ μƒνƒœλ“€ + + playerVideoState: { + // ν˜„μž¬ μƒνƒœ + playback: PLAYBACK_STATUS.NOT_PLAYING, + display: DISPLAY_STATUS.HIDDEN, + videoId: null, + progress: 0, + error: null, + timestamp: null + } +}; +``` + +## μ•‘μ…˜ νƒ€μž… 및 ν•¨μˆ˜ + +### μ•‘μ…˜ νƒ€μž… + +```javascript +export const PLAYER_VIDEO_ACTIONS = { + // μž¬μƒ μƒνƒœ μ•‘μ…˜ + SET_PLAYBACK_LOADING: 'SET_PLAYBACK_LOADING', + SET_PLAYBACK_SUCCESS: 'SET_PLAYBACK_SUCCESS', + SET_PLAYBACK_ERROR: 'SET_PLAYBACK_ERROR', + SET_PLAYBACK_PLAYING: 'SET_PLAYBACK_PLAYING', + SET_PLAYBACK_NOT_PLAYING: 'SET_PLAYBACK_NOT_PLAYING', + SET_PLAYBACK_BUFFERING: 'SET_PLAYBACK_BUFFERING', + + // ν™”λ©΄ μƒνƒœ μ•‘μ…˜ + SET_DISPLAY_HIDDEN: 'SET_DISPLAY_HIDDEN', + SET_DISPLAY_VISIBLE: 'SET_DISPLAY_VISIBLE', + SET_DISPLAY_MINIMIZED: 'SET_DISPLAY_MINIMIZED', + SET_DISPLAY_FULLSCREEN: 'SET_DISPLAY_FULLSCREEN', + + // 볡합 μ•‘μ…˜ + SET_VIDEO_LOADING: 'SET_VIDEO_LOADING', + SET_VIDEO_PLAYING: 'SET_VIDEO_PLAYING', + SET_VIDEO_STOPPED: 'SET_VIDEO_STOPPED', + SET_VIDEO_MINIMIZED_PLAYING: 'SET_VIDEO_MINIMIZED_PLAYING', +}; +``` + +### μ•‘μ…˜ ν•¨μˆ˜ + +```javascript +// κΈ°λ³Έ μ•‘μ…˜ ν•¨μˆ˜λ“€ (FP μŠ€νƒ€μΌ) +export const setPlaybackLoading = curry((videoId, displayMode = 'visible') => ({ + type: PLAYER_VIDEO_ACTIONS.SET_VIDEO_LOADING, + payload: { + playback: PLAYBACK_STATUS.LOADING, + display: displayMode, + videoId, + progress: 0, + error: null, + timestamp: Date.now() + } +})); + +export const setPlaybackPlaying = curry((videoId, displayMode = 'fullscreen') => ({ + type: PLAYER_VIDEO_ACTIONS.SET_VIDEO_PLAYING, + payload: { + playback: PLAYBACK_STATUS.PLAYING, + display: displayMode, + videoId, + progress: 100, + error: null, + timestamp: Date.now() + } +})); + +export const setPlaybackError = curry((videoId, error) => ({ + type: PLAYER_VIDEO_ACTIONS.SET_PLAYBACK_ERROR, + payload: { + playback: PLAYBACK_STATUS.LOAD_ERROR, + display: DISPLAY_STATUS.VISIBLE, + videoId, + error, + progress: 0, + timestamp: Date.now() + } +})); + +export const setVideoStopped = () => ({ + type: PLAYER_VIDEO_ACTIONS.SET_VIDEO_STOPPED, + payload: { + playback: PLAYBACK_STATUS.NOT_PLAYING, + display: DISPLAY_STATUS.HIDDEN, + videoId: null, + error: null, + progress: 0, + timestamp: Date.now() + } +})); +``` + +## μƒνƒœ μ‚¬μš© μ˜ˆμ‹œ + +### PlayerPanel.jsx + +```javascript +import { + setPlaybackLoading, + setPlaybackPlaying, + setPlaybackError, + setVideoStopped +} from '../actions/playerActions'; + +// λΉ„λ””μ˜€ λ‘œλ”© μ‹œμž‘ +const handleVideoLoadStart = (videoId) => { + dispatch(setPlaybackLoading(videoId, 'fullscreen')); +}; + +// λΉ„λ””μ˜€ μž¬μƒ μ‹œμž‘ +const handleVideoPlay = (videoId) => { + dispatch(setPlaybackPlaying(videoId, 'fullscreen')); +}; + +// λΉ„λ””μ˜€ μ—λŸ¬ λ°œμƒ +const handleVideoError = (videoId, error) => { + dispatch(setPlaybackError(videoId, error)); +}; + +// μƒνƒœ 확인 +const videoState = useSelector(state => state.player.playerVideoState); +const isLoading = videoState.playback === PLAYBACK_STATUS.LOADING; +const isPlaying = videoState.playback === PLAYBACK_STATUS.PLAYING; +const hasError = videoState.playback === PLAYBACK_STATUS.LOAD_ERROR; +const isFullscreen = videoState.display === DISPLAY_STATUS.FULLSCREEN; +``` + +### VideoPlayer.js + +```javascript +// ν˜„μž¬ μƒνƒœμ— λ”°λ₯Έ UI λ Œλ”λ§ +const renderVideoState = () => { + const { playback, display, error, progress } = videoState; + + if (playback === PLAYBACK_STATUS.LOADING) { + return ; + } + + if (playback === PLAYBACK_STATUS.LOAD_ERROR) { + return ; + } + + if (playback === PLAYBACK_STATUS.BUFFERING) { + return ; + } + + if (playback === PLAYBACK_STATUS.PLAYING && display === DISPLAY_STATUS.FULLSCREEN) { + return ; + } + + return null; +}; +``` + +## κ΅¬ν˜„ μˆœμ„œ + +1. [ ] playerActions.js에 μƒμˆ˜ 및 μ•‘μ…˜ ν•¨μˆ˜λ“€ μΆ”κ°€ +2. [ ] playerReducer.js에 초기 μƒνƒœ 및 ν•Έλ“€λŸ¬λ“€ μΆ”κ°€ +3. [ ] PlayerPanel.jsxμ—μ„œ μƒˆλ‘œμš΄ μƒνƒœ μ‹œμŠ€ν…œμœΌλ‘œ μ „ν™˜ +4. [ ] VideoPlayer.jsμ—μ„œ μƒˆλ‘œμš΄ μƒνƒœ μ‹œμŠ€ν…œμœΌλ‘œ μ „ν™˜ +5. [ ] ν…ŒμŠ€νŠΈ 및 검증 + +## μž₯점 + +1. **μ •λ°€ν•œ μƒνƒœ μ œμ–΄**: μž¬μƒ μƒνƒœμ™€ ν™”λ©΄ μƒνƒœλ₯Ό λ³„λ„λ‘œ 관리 +2. **λͺ…ν™•ν•œ μƒνƒœ 의미**: 각 μƒνƒœκ°€ λͺ…ν™•ν•œ 의미λ₯Ό 가짐 +3. **ν™•μž₯μ„±**: μƒˆλ‘œμš΄ μƒνƒœ μΆ”κ°€κ°€ 용이 +4. **μœ μ§€λ³΄μˆ˜μ„±**: κΈ°μ‘΄ μ½”λ“œ 영ν–₯ μ΅œμ†Œν™” +5. **μž¬μ‚¬μš©μ„±**: λ‹€λ₯Έ μ»΄ν¬λ„ŒνŠΈμ—μ„œλ„ ν™œμš© κ°€λŠ₯ + +## μ£Όμ˜μ‚¬ν•­ + +- κΈ°μ‘΄ videoPlayReducer와 μΆ©λŒν•˜μ§€ μ•Šλ„λ‘ 주의 +- PlayerPanelκ³Ό VideoPlayer.jsμ—λ§Œ μ§‘μ€‘ν•˜μ—¬ κ΅¬ν˜„ +- κΈ°μ‘΄ λΉ„λ””μ˜€ μž¬μƒ 둜직과 ν˜Έν™˜μ„± μœ μ§€ \ No newline at end of file diff --git a/com.twin.app.shoptime/src/actions/actionTypes.js b/com.twin.app.shoptime/src/actions/actionTypes.js index b2bc63ea..0507bedb 100644 --- a/com.twin.app.shoptime/src/actions/actionTypes.js +++ b/com.twin.app.shoptime/src/actions/actionTypes.js @@ -259,6 +259,27 @@ export const types = { CLEAR_PLAYER_INFO: 'CLEAR_PLAYER_INFO', UPDATE_VIDEO_PLAY_STATE: 'UPDATE_VIDEO_PLAY_STATE', + // πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ - μž¬μƒ μƒνƒœ + SET_PLAYBACK_LOADING: 'SET_PLAYBACK_LOADING', + SET_PLAYBACK_SUCCESS: 'SET_PLAYBACK_SUCCESS', + SET_PLAYBACK_ERROR: 'SET_PLAYBACK_ERROR', + SET_PLAYBACK_PLAYING: 'SET_PLAYBACK_PLAYING', + SET_PLAYBACK_NOT_PLAYING: 'SET_PLAYBACK_NOT_PLAYING', + SET_PLAYBACK_BUFFERING: 'SET_PLAYBACK_BUFFERING', + + // πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ - ν™”λ©΄ μƒνƒœ + SET_DISPLAY_HIDDEN: 'SET_DISPLAY_HIDDEN', + SET_DISPLAY_VISIBLE: 'SET_DISPLAY_VISIBLE', + SET_DISPLAY_MINIMIZED: 'SET_DISPLAY_MINIMIZED', + SET_DISPLAY_FULLSCREEN: 'SET_DISPLAY_FULLSCREEN', + + // πŸ”½ [251116] 볡합 μƒνƒœ μ•‘μ…˜λ“€ + SET_VIDEO_LOADING: 'SET_VIDEO_LOADING', + SET_VIDEO_PLAYING: 'SET_VIDEO_PLAYING', + SET_VIDEO_STOPPED: 'SET_VIDEO_STOPPED', + SET_VIDEO_MINIMIZED_PLAYING: 'SET_VIDEO_MINIMIZED_PLAYING', + SET_VIDEO_ERROR: 'SET_VIDEO_ERROR', + // πŸ”½ [μΆ”κ°€] ν”Œλ ˆμ΄ μ œμ–΄ λ§€λ‹ˆμ € μ•‘μ…˜ νƒ€μž… /** * ν™ˆ ν™”λ©΄ λ°°λ„ˆμ˜ λΉ„λ””μ˜€ μž¬μƒ μ œμ–΄λ₯Ό μœ„ν•œ μ•‘μ…˜ νƒ€μž…. diff --git a/com.twin.app.shoptime/src/actions/playActions.js b/com.twin.app.shoptime/src/actions/playActions.js index 463074a6..2abd75c8 100644 --- a/com.twin.app.shoptime/src/actions/playActions.js +++ b/com.twin.app.shoptime/src/actions/playActions.js @@ -6,6 +6,24 @@ import { panel_names } from '../utils/Config'; import { types } from './actionTypes'; import { popPanel, pushPanel, updatePanel } from './panelActions'; +// πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ - μž¬μƒ μƒνƒœ +export const PLAYBACK_STATUS = { + LOADING: 'loading', + LOAD_SUCCESS: 'load_success', + LOAD_ERROR: 'load_error', + PLAYING: 'playing', + NOT_PLAYING: 'not_playing', + BUFFERING: 'buffering', +}; + +// πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ - ν™”λ©΄ μƒνƒœ +export const DISPLAY_STATUS = { + HIDDEN: 'hidden', + VISIBLE: 'visible', + MINIMIZED: 'minimized', + FULLSCREEN: 'fullscreen', +}; + //yhcho /* dispatch(startVideoPreview({ @@ -95,7 +113,15 @@ const shouldSkipVideoPlayback = ( }; export const startVideoPlayerNew = - ({ modal, modalContainerId, modalClassName, spotlightDisable, useNewPlayer, bannerId, ...rest }) => + ({ + modal, + modalContainerId, + modalClassName, + spotlightDisable, + useNewPlayer, + bannerId, + ...rest + }) => (dispatch, getState) => { const panels = getState().panels.panels; const topPanel = panels[panels.length - 1]; @@ -146,7 +172,7 @@ export const startVideoPlayerNew = export const finishVideoPreview = () => (dispatch, getState) => { const panels = getState().panels.panels; - const topPanel = panels[panels.length-1]; + const topPanel = panels[panels.length - 1]; if (topPanel && topPanel.name === panel_names.PLAYER_PANEL && topPanel.panelInfo.modal) { if (startVideoFocusTimer) { clearTimeout(startVideoFocusTimer); @@ -179,9 +205,7 @@ export const finishAllVideoForce = () => (dispatch, getState) => { const panels = getState().panels.panels; // λͺ¨λ“  PlayerPanel이 μ‘΄μž¬ν•˜λŠ”μ§€ 확인 (μŠ€νƒ 어디에 μžˆλ“ ) - const hasPlayerPanel = panels.some( - (panel) => panel.name === panel_names.PLAYER_PANEL - ); + const hasPlayerPanel = panels.some((panel) => panel.name === panel_names.PLAYER_PANEL); if (hasPlayerPanel) { if (startVideoFocusTimer) { @@ -352,7 +376,10 @@ export const expandVideoFrom1px = () => (dispatch, getState) => { // μΆ•μ†Œλœ modal PlayerPanel μ°ΎκΈ° const shrunkModalPlayerPanel = panels.find( - (panel) => panel.name === panel_names.PLAYER_PANEL && panel.panelInfo?.modal && panel.panelInfo?.shouldShrinkTo1px + (panel) => + panel.name === panel_names.PLAYER_PANEL && + panel.panelInfo?.modal && + panel.panelInfo?.shouldShrinkTo1px ); if (shrunkModalPlayerPanel) { @@ -581,7 +608,6 @@ export const goToFullScreen = () => (dispatch, getState) => { ); }; - /** * μ˜κ΅¬μž¬μƒ λΉ„λ””μ˜€λ₯Ό μΌμ‹œμ •μ§€ μƒνƒœλ‘œ λ§Œλ“­λ‹ˆλ‹€. (λ‚΄λΆ€ μ‚¬μš©) */ @@ -617,3 +643,152 @@ export const returnToPreview = () => (dispatch, getState) => { dispatch(finishVideoPreview()); } }; + +/* πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ - μ•‘μ…˜ ν•¨μˆ˜λ“€ */ + +/** + * λΉ„λ””μ˜€ λ‘œλ”© μ‹œμž‘ μƒνƒœλ‘œ μ„€μ • + * @param {string} videoId - λΉ„λ””μ˜€ ID + * @param {string} displayMode - ν™”λ©΄ λͺ¨λ“œ ('visible', 'fullscreen', 'minimized') + */ +export const setPlaybackLoading = (videoId, displayMode = DISPLAY_STATUS.VISIBLE) => ({ + type: types.SET_VIDEO_LOADING, + payload: { + playback: PLAYBACK_STATUS.LOADING, + display: displayMode, + videoId, + loadingProgress: 0, + loadingError: null, + lastUpdate: Date.now(), + }, +}); + +/** + * λΉ„λ””μ˜€ λ‘œλ”© 성곡 μƒνƒœλ‘œ μ„€μ • + * @param {string} videoId - λΉ„λ””μ˜€ ID + * @param {string} displayMode - ν™”λ©΄ λͺ¨λ“œ + */ +export const setPlaybackSuccess = (videoId, displayMode = DISPLAY_STATUS.VISIBLE) => ({ + type: types.SET_PLAYBACK_SUCCESS, + payload: { + playback: PLAYBACK_STATUS.LOAD_SUCCESS, + display: displayMode, + videoId, + loadingProgress: 100, + loadingError: null, + lastUpdate: Date.now(), + }, +}); + +/** + * λΉ„λ””μ˜€ λ‘œλ”© μ—λŸ¬ μƒνƒœλ‘œ μ„€μ • + * @param {string} videoId - λΉ„λ””μ˜€ ID + * @param {object} error - μ—λŸ¬ 정보 + */ +export const setPlaybackError = (videoId, error) => ({ + type: types.SET_VIDEO_ERROR, + payload: { + playback: PLAYBACK_STATUS.LOAD_ERROR, + display: DISPLAY_STATUS.VISIBLE, + videoId, + loadingProgress: 0, + loadingError: error, + lastUpdate: Date.now(), + }, +}); + +/** + * λΉ„λ””μ˜€ μž¬μƒ μƒνƒœλ‘œ μ„€μ • + * @param {string} videoId - λΉ„λ””μ˜€ ID + * @param {string} displayMode - ν™”λ©΄ λͺ¨λ“œ + */ +export const setPlaybackPlaying = (videoId, displayMode = DISPLAY_STATUS.FULLSCREEN) => ({ + type: types.SET_VIDEO_PLAYING, + payload: { + playback: PLAYBACK_STATUS.PLAYING, + display: displayMode, + videoId, + loadingProgress: 100, + loadingError: null, + lastUpdate: Date.now(), + }, +}); + +/** + * λΉ„λ””μ˜€ μ •μ§€ μƒνƒœλ‘œ μ„€μ • + */ +export const setVideoStopped = () => ({ + type: types.SET_VIDEO_STOPPED, + payload: { + playback: PLAYBACK_STATUS.NOT_PLAYING, + display: DISPLAY_STATUS.HIDDEN, + videoId: null, + loadingProgress: 0, + loadingError: null, + lastUpdate: Date.now(), + }, +}); + +/** + * λΉ„λ””μ˜€ 버퍼링 μƒνƒœλ‘œ μ„€μ • + * @param {string} videoId - λΉ„λ””μ˜€ ID + */ +export const setPlaybackBuffering = (videoId) => ({ + type: types.SET_PLAYBACK_BUFFERING, + payload: { + playback: PLAYBACK_STATUS.BUFFERING, + videoId, + lastUpdate: Date.now(), + }, +}); + +/** + * μ΅œμ†Œν™”λœ μƒνƒœλ‘œ λΉ„λ””μ˜€ μž¬μƒ + * @param {string} videoId - λΉ„λ””μ˜€ ID + */ +export const setVideoMinimizedPlaying = (videoId) => ({ + type: types.SET_VIDEO_MINIMIZED_PLAYING, + payload: { + playback: PLAYBACK_STATUS.PLAYING, + display: DISPLAY_STATUS.MINIMIZED, + videoId, + loadingProgress: 100, + loadingError: null, + lastUpdate: Date.now(), + }, +}); + +/** + * ν™”λ©΄ μƒνƒœλ§Œ λ³€κ²½ν•˜λŠ” μ•‘μ…˜λ“€ + */ +export const setDisplayHidden = () => ({ + type: types.SET_DISPLAY_HIDDEN, + payload: { + display: DISPLAY_STATUS.HIDDEN, + lastUpdate: Date.now(), + }, +}); + +export const setDisplayVisible = () => ({ + type: types.SET_DISPLAY_VISIBLE, + payload: { + display: DISPLAY_STATUS.VISIBLE, + lastUpdate: Date.now(), + }, +}); + +export const setDisplayMinimized = () => ({ + type: types.SET_DISPLAY_MINIMIZED, + payload: { + display: DISPLAY_STATUS.MINIMIZED, + lastUpdate: Date.now(), + }, +}); + +export const setDisplayFullscreen = () => ({ + type: types.SET_DISPLAY_FULLSCREEN, + payload: { + display: DISPLAY_STATUS.FULLSCREEN, + lastUpdate: Date.now(), + }, +}); diff --git a/com.twin.app.shoptime/src/reducers/playReducer.js b/com.twin.app.shoptime/src/reducers/playReducer.js index 579a5ea6..62e62914 100644 --- a/com.twin.app.shoptime/src/reducers/playReducer.js +++ b/com.twin.app.shoptime/src/reducers/playReducer.js @@ -1,14 +1,24 @@ import { types } from '../actions/actionTypes'; +import { PLAYBACK_STATUS, DISPLAY_STATUS } from '../actions/playActions'; const initialState = { subTitleBlobs: {}, chatData: null, videoPlayState: { + // κΈ°μ‘΄ μƒνƒœλ“€ μœ μ§€ isPlaying: false, isPaused: true, currentTime: 0, duration: 0, playbackRate: 1, + + // πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ + playback: PLAYBACK_STATUS.NOT_PLAYING, // μž¬μƒ μƒνƒœ + display: DISPLAY_STATUS.HIDDEN, // ν™”λ©΄ ν‘œμ‹œ μƒνƒœ + videoId: null, // ν˜„μž¬ λΉ„λ””μ˜€ ID + loadingProgress: 0, // λ‘œλ”© μ§„ν–‰λ₯  (0-100) + loadingError: null, // λ‘œλ”© μ—λŸ¬ 정보 + lastUpdate: null, // λ§ˆμ§€λ§‰ μ—…λ°μ΄νŠΈ μ‹œκ°„ }, }; @@ -59,6 +69,193 @@ export const playReducer = (state = initialState, action) => { }; } + // πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ - μž¬μƒ μƒνƒœ μ•‘μ…˜λ“€ + case types.SET_PLAYBACK_LOADING: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + playback: PLAYBACK_STATUS.LOADING, + loadingProgress: 0, + loadingError: null, + lastUpdate: Date.now(), + }, + }; + } + + case types.SET_PLAYBACK_SUCCESS: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + playback: PLAYBACK_STATUS.LOAD_SUCCESS, + loadingProgress: 100, + loadingError: null, + lastUpdate: Date.now(), + }, + }; + } + + case types.SET_PLAYBACK_ERROR: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + playback: PLAYBACK_STATUS.LOAD_ERROR, + loadingProgress: 0, + loadingError: action.payload, + lastUpdate: Date.now(), + }, + }; + } + + case types.SET_PLAYBACK_PLAYING: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + playback: PLAYBACK_STATUS.PLAYING, + isPlaying: true, + isPaused: false, + loadingProgress: 100, + loadingError: null, + lastUpdate: Date.now(), + }, + }; + } + + case types.SET_PLAYBACK_NOT_PLAYING: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + playback: PLAYBACK_STATUS.NOT_PLAYING, + isPlaying: false, + isPaused: true, + loadingProgress: 0, + lastUpdate: Date.now(), + }, + }; + } + + case types.SET_PLAYBACK_BUFFERING: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + playback: PLAYBACK_STATUS.BUFFERING, + loadingError: null, + lastUpdate: Date.now(), + }, + }; + } + + // πŸ”½ [251116] μƒˆλ‘œμš΄ λΉ„λ””μ˜€ μƒνƒœ 관리 μ‹œμŠ€ν…œ - ν™”λ©΄ μƒνƒœ μ•‘μ…˜λ“€ + case types.SET_DISPLAY_HIDDEN: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + display: DISPLAY_STATUS.HIDDEN, + lastUpdate: Date.now(), + }, + }; + } + + case types.SET_DISPLAY_VISIBLE: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + display: DISPLAY_STATUS.VISIBLE, + lastUpdate: Date.now(), + }, + }; + } + + case types.SET_DISPLAY_MINIMIZED: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + display: DISPLAY_STATUS.MINIMIZED, + lastUpdate: Date.now(), + }, + }; + } + + case types.SET_DISPLAY_FULLSCREEN: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + display: DISPLAY_STATUS.FULLSCREEN, + lastUpdate: Date.now(), + }, + }; + } + + // πŸ”½ [251116] 볡합 μƒνƒœ μ•‘μ…˜λ“€ + case types.SET_VIDEO_LOADING: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + ...action.payload, + isPlaying: false, + isPaused: true, + }, + }; + } + + case types.SET_VIDEO_PLAYING: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + ...action.payload, + isPlaying: true, + isPaused: false, + }, + }; + } + + case types.SET_VIDEO_STOPPED: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + ...action.payload, + isPlaying: false, + isPaused: true, + }, + }; + } + + case types.SET_VIDEO_MINIMIZED_PLAYING: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + ...action.payload, + isPlaying: true, + isPaused: false, + }, + }; + } + + case types.SET_VIDEO_ERROR: { + return { + ...state, + videoPlayState: { + ...state.videoPlayState, + ...action.payload, + isPlaying: false, + isPaused: true, + }, + }; + } + default: return state; } diff --git a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx index c562fc03..7b7720b5 100644 --- a/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx +++ b/com.twin.app.shoptime/src/views/HomePanel/HomeBanner/RandomUnit.jsx @@ -87,8 +87,8 @@ export default function RandomUnit({ const isVideoTransitionLocked = useSelector((state) => state.home.videoTransitionLocked); const timerRef = useRef(); - const keepTimerOnBlurRef = useRef(false); const hasAutoPlayStartedRef = useRef(false); + const hasPlaybackStartedRef = useRef(false); const isDefaultAutoPlayTarget = defaultFocus === spotlightId; const bannerDataRef = useRef(bannerData); const randomDataRef = useRef( @@ -105,6 +105,7 @@ export default function RandomUnit({ const handleStartVideo = useCallback( (videoProps) => { dispatch(setVideoTransitionLock(true)); + hasPlaybackStartedRef.current = true; dispatch(startVideoPlayerNew(videoProps)); }, [dispatch] @@ -115,6 +116,14 @@ export default function RandomUnit({ dispatch(setVideoTransitionLock(false)); }, [dispatch]); + useEffect(() => { + if (currentVideoBannerId === spotlightId) { + hasPlaybackStartedRef.current = true; + } else { + hasPlaybackStartedRef.current = false; + } + }, [currentVideoBannerId, spotlightId]); + useEffect(() => { if (isVideoTransitionLocked && isCurrentBannerVideoPlaying) { dispatch(setVideoTransitionLock(false)); @@ -595,11 +604,10 @@ export default function RandomUnit({ }, [randomData]); useEffect(() => { - if (isFocused && !videoError) { + if (isFocused && !videoError && !hasPlaybackStartedRef.current) { if (timerRef.current) { clearTimeout(timerRef.current); } - keepTimerOnBlurRef.current = isDefaultAutoPlayTarget && !hasAutoPlayStartedRef.current; timerRef.current = setTimeout(() => { handleStartVideo({ bannerId: spotlightId, @@ -617,30 +625,27 @@ export default function RandomUnit({ if (isDefaultAutoPlayTarget) { hasAutoPlayStartedRef.current = true; } - keepTimerOnBlurRef.current = false; timerRef.current = null; }, 1000); } if (!isFocused) { setVideoError(false); - if (timerRef.current && !keepTimerOnBlurRef.current) { + if (timerRef.current && !hasPlaybackStartedRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } } return () => { - if (timerRef.current) { + if (timerRef.current && !hasPlaybackStartedRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } - keepTimerOnBlurRef.current = false; }; }, [ isFocused, videoError, isHorizontal, randomData, - dispatch, isDefaultAutoPlayTarget, spotlightId, handleStartVideo,