[251006] feat: useFocusHistory,useVideoMove Migration
🕐 커밋 시간: 2025. 10. 06. 15:22:49 📊 변경 통계: • 총 파일: 27개 • 추가: +151줄 • 삭제: -21줄 📁 추가된 파일: + com.twin.app.shoptime/src/actions/videoPlayActions.js + com.twin.app.shoptime/src/hooks/useFocusHistory/index.js + com.twin.app.shoptime/src/hooks/useFocusHistory/useFocusHistory.js + com.twin.app.shoptime/src/hooks/useVideoPlay/index.js + com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.complete.js + com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.final.js + com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.fixed.js + com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.js + com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.old.js + com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.opus-improved.js + com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.simple.js + com.twin.app.shoptime/src/hooks/useVideoPlay/videoState.js + com.twin.app.shoptime/src/hooks/useVideoTransition/index.js + com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.bak.js + com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.brief.js + com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.complete.js + com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.fixed.js + com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.js + com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.original.js + com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoMove.simple.js + com.twin.app.shoptime/src/hooks/useVideoTransition/useVideoTransition.js + com.twin.app.shoptime/src/reducers/videoPlayReducer.js 📝 수정된 파일: ~ com.twin.app.shoptime/src/components/TabLayout/TabLayout.jsx ~ com.twin.app.shoptime/src/store/store.js ~ com.twin.app.shoptime/src/utils/domUtils.js ~ com.twin.app.shoptime/src/views/HomePanel/HomeBanner/HomeBanner.jsx ~ com.twin.app.shoptime/src/views/HomePanel/HomePanel.jsx 🔧 함수 변경 내용: 📊 Function-level changes summary across 27 files: • Functions added: 55 • Functions modified: 0 • Functions deleted: 0 📋 By language: • javascript: 27 files, 55 function changes 🔧 주요 변경 내용: • UI 컴포넌트 아키텍처 개선 • 핵심 비즈니스 로직 개선 • 공통 유틸리티 함수 최적화
This commit is contained in:
377
com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.js
Normal file
377
com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.js
Normal file
@@ -0,0 +1,377 @@
|
||||
// src/hooks/useVideoPlay/useVideoPlay.js
|
||||
|
||||
import { useCallback, useRef, useState, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
startBannerVideo,
|
||||
stopBannerVideo,
|
||||
stopAndHideVideo,
|
||||
hidePlayerVideo,
|
||||
} from '../../actions/playActions';
|
||||
import fp from '../../utils/fp.js';
|
||||
import { videoState } from './videoState.js';
|
||||
|
||||
/**
|
||||
* useVideoPlay Hook - 안전한 동영상 재생 제어
|
||||
*
|
||||
* 주요 기능:
|
||||
* - 동영상 재생/중지 안전 제어
|
||||
* - 중복 재생 방지
|
||||
* - 재생 상태 추적
|
||||
* - 오류 처리 및 복구
|
||||
* - useFocusHistory와 연동 지원
|
||||
*/
|
||||
|
||||
/**
|
||||
* 동영상 재생 제어 훅
|
||||
* @param {object} options - 옵션 설정
|
||||
* @param {boolean} options.enableLogging - 로깅 활성화 여부
|
||||
* @param {string} options.logPrefix - 로그 접두사
|
||||
* @param {boolean} options.autoErrorRecovery - 자동 오류 복구 여부
|
||||
* @returns {object} 동영상 재생 제어 인터페이스
|
||||
*/
|
||||
export const useVideoPlay = (options = {}) => {
|
||||
const {
|
||||
enableLogging = process.env.NODE_ENV === 'development',
|
||||
logPrefix = '[useVideoPlay]',
|
||||
autoErrorRecovery = true,
|
||||
} = options;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// 로그 함수
|
||||
const log = (message) => enableLogging && console.log(`${logPrefix} ${message}`);
|
||||
|
||||
// 🔽 Redux 상태 구독
|
||||
const currentOwnerId = useSelector((state) => state.home.playerControl?.ownerId);
|
||||
const bannerDataList = useSelector((state) => state.home.bannerData?.bannerInfos);
|
||||
const bannerVisibility = useSelector((state) => state.home.bannerVisibility);
|
||||
|
||||
// 🔽 [단순화] 현재 재생 중인 배너 가져오기
|
||||
const getCurrentPlayingBanner = useCallback(() => {
|
||||
return videoState.getCurrentPlaying();
|
||||
}, []);
|
||||
|
||||
// 🔽 [단순화] 마지막으로 재생된 배너 가져오기
|
||||
const getLastPlayedBanner = useCallback(() => {
|
||||
return videoState.getLastPlayedBanner();
|
||||
}, []);
|
||||
|
||||
// 🔽 로컬 상태 관리 (단순화)
|
||||
const [errorCount, setErrorCount] = useState(0);
|
||||
|
||||
// 🔽 타이머 및 참조 관리
|
||||
const playDelayTimerRef = useRef(null);
|
||||
const retryTimerRef = useRef(null);
|
||||
|
||||
// 🔽 [유틸리티] 배너 가용성 검사 (0부터 시작 통일)
|
||||
const isBannerAvailable = useCallback(
|
||||
(bannerId) => {
|
||||
try {
|
||||
if (!bannerDataList || bannerDataList.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// banner0/banner1 -> bannerDataList[0], banner2/banner3 -> bannerDataList[1]
|
||||
const bannerIndex = ['banner0', 'banner1'].includes(bannerId) ? 0 : 1;
|
||||
const bannerData = bannerDataList[bannerIndex];
|
||||
|
||||
if (!bannerData) return false;
|
||||
|
||||
if (['banner0', 'banner1'].includes(bannerId)) {
|
||||
// banner0/banner1: 첫 번째 배너의 첫 번째 상세 정보
|
||||
const detailInfo = fp.get('bannerDetailInfos[0]', bannerData);
|
||||
return !!fp.get('showUrl', detailInfo);
|
||||
} else if (['banner2', 'banner3'].includes(bannerId)) {
|
||||
// banner2/banner3: randomIndex에 따른 상세 정보
|
||||
const randomIndex = fp.get('randomIndex', bannerData) || 0;
|
||||
const detailInfo = fp.get(`bannerDetailInfos[${randomIndex}]`, bannerData);
|
||||
return !!fp.get('showUrl', detailInfo);
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
log(`배너 가용성 검사 오류: ${error.message}, bannerId: ${bannerId}`);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[bannerDataList, enableLogging, logPrefix]
|
||||
);
|
||||
|
||||
// 🔽 [핵심] 안전한 동영상 재생
|
||||
const playVideo = useCallback(
|
||||
(bannerId, options = {}) => {
|
||||
const { delay = 0, force = false, reason = 'manual' } = options;
|
||||
|
||||
try {
|
||||
// 입력값 검증
|
||||
if (!bannerId || typeof bannerId !== 'string') {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// 지원되는 배너 확인 (0부터 시작 통일)
|
||||
if (!['banner0', 'banner1'].includes(bannerId)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// 배너 가용성 확인
|
||||
if (!isBannerAvailable(bannerId)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
// 🔽 [개선] 중복 재생 방지 - Redux 상태 기준으로 체크
|
||||
const isCurrentlyPlaying = currentOwnerId === `${bannerId}_player`;
|
||||
|
||||
// 🔽 [정확한 중복 방지] 실제 Redux에서 재생 중일 때만 skip
|
||||
if (!force && isCurrentlyPlaying) {
|
||||
log(`🎬 ${bannerId} 이미 재생 중 - skip`);
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 기존 타이머 정리
|
||||
if (playDelayTimerRef.current) {
|
||||
clearTimeout(playDelayTimerRef.current);
|
||||
}
|
||||
|
||||
const executePlay = () => {
|
||||
try {
|
||||
console.log(`[DEBUG-playVideo] 🎬 ${bannerId} 재생 시작 (${reason})`);
|
||||
log(`🎬 ${bannerId} 재생 시작 (${reason})`);
|
||||
|
||||
// 🔽 [단순화] 전역 상태 업데이트
|
||||
videoState.setCurrentPlaying(bannerId);
|
||||
|
||||
// Redux 액션 dispatch - bannerId를 modalContainerId로 사용
|
||||
dispatch(startBannerVideo(bannerId, { modalContainerId: bannerId, force }));
|
||||
|
||||
// 성공 상태 업데이트
|
||||
setErrorCount(0); // 성공 시 오류 카운트 초기화
|
||||
|
||||
resolve(true);
|
||||
} catch (error) {
|
||||
log(`playVideo 실행 오류: ${error.message}, bannerId: ${bannerId}`);
|
||||
|
||||
// 오류 카운트 증가
|
||||
setErrorCount((prev) => prev + 1);
|
||||
|
||||
// 자동 복구 시도
|
||||
if (autoErrorRecovery && errorCount < 3) {
|
||||
log(`자동 복구 시도 ${errorCount + 1}/3`);
|
||||
|
||||
retryTimerRef.current = setTimeout(
|
||||
() => {
|
||||
playVideo(bannerId, { ...options, force: true, reason: 'auto-retry' });
|
||||
},
|
||||
1000 * Math.pow(2, errorCount)
|
||||
); // 지수 백오프
|
||||
}
|
||||
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 지연 실행 또는 즉시 실행
|
||||
if (delay > 0) {
|
||||
playDelayTimerRef.current = setTimeout(executePlay, delay);
|
||||
} else {
|
||||
executePlay();
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
log(
|
||||
`playVideo 전체 오류: ${error.message}, bannerId: ${bannerId}, options: ${JSON.stringify(options)}`
|
||||
);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
(bannerId) => isBannerAvailable(bannerId),
|
||||
currentOwnerId,
|
||||
dispatch,
|
||||
enableLogging,
|
||||
logPrefix,
|
||||
autoErrorRecovery,
|
||||
errorCount,
|
||||
]
|
||||
);
|
||||
|
||||
// 🔽 [핵심] 안전한 동영상 중지 - 파라미터 없이 사용 가능
|
||||
const stopVideo = useCallback(
|
||||
(options = {}) => {
|
||||
const { reason = 'manual' } = options;
|
||||
|
||||
try {
|
||||
// 현재 재생 중인 배너 파악
|
||||
const currentPlayingBanner = getCurrentPlayingBanner();
|
||||
|
||||
if (!currentPlayingBanner) {
|
||||
log('🛑 재생 중인 비디오 없음 - skip');
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
log(`🛑 ${currentPlayingBanner} 중지 (${reason})`);
|
||||
|
||||
// Redux 액션 dispatch - 모든 비디오 중지 및 숨김
|
||||
dispatch(stopAndHideVideo());
|
||||
|
||||
// 🔽 [단순화] 전역 상태 업데이트
|
||||
videoState.setCurrentPlaying(null);
|
||||
|
||||
return Promise.resolve(true);
|
||||
} catch (error) {
|
||||
log(`stopVideo 오류: ${error.message}, options: ${JSON.stringify(options)}`);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
},
|
||||
[getCurrentPlayingBanner, dispatch, enableLogging, logPrefix]
|
||||
);
|
||||
|
||||
// 🔽 [새로운 기능] 비디오를 완전히 중지하지 않고 소리만 나오도록 안보이게 함
|
||||
const hideVideo = useCallback(
|
||||
(options = {}) => {
|
||||
const { reason = 'hide' } = options;
|
||||
|
||||
try {
|
||||
// 현재 재생 중인 배너 파악
|
||||
const currentPlayingBanner = getCurrentPlayingBanner();
|
||||
|
||||
if (!currentPlayingBanner) {
|
||||
log('👁️🗨️ 재생 중인 비디오 없음 - skip');
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
log(`👁️🗨️ ${currentPlayingBanner} 숨김 (${reason}) - 소리 유지`);
|
||||
|
||||
// Redux 액션 dispatch - 비디오 숨김 (일시정지 + 모달 숨김)
|
||||
dispatch(hidePlayerVideo());
|
||||
|
||||
// 🔽 [중요] 전역 상태는 유지 (재생 상태는 그대로)
|
||||
// videoState.setCurrentPlaying(null); // <- 숨김 상태이므로 재생 상태 유지
|
||||
|
||||
return Promise.resolve(true);
|
||||
} catch (error) {
|
||||
log(`hideVideo 오류: ${error.message}, options: ${JSON.stringify(options)}`);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
},
|
||||
[getCurrentPlayingBanner, dispatch, enableLogging, logPrefix]
|
||||
);
|
||||
|
||||
// 🔽 [새로운] 비디오 재시작 - 마지막으로 재생된 배너로 재시작 (기본 banner0)
|
||||
const restartVideo = useCallback(
|
||||
(bannerId = null, options = {}) => {
|
||||
const { reason = 'restart' } = options;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
// 🔥 핵심: bannerId가 주어지면 그걸 사용, 없으면 마지막 재생된 배너, 없으면 banner0
|
||||
const targetBannerId = bannerId || getLastPlayedBanner() || 'banner0';
|
||||
|
||||
console.log(`[DEBUG-restartVideo] ♾️ ${targetBannerId} 재시작 (${reason})`);
|
||||
log(`♾️ ${targetBannerId} 재시작 (${reason})`);
|
||||
|
||||
// 대상 배너를 다시 재생 (force 옵션으로 중복 방지 해제)
|
||||
playVideo(targetBannerId, { force: true, reason })
|
||||
.then(resolve)
|
||||
.catch(() => resolve(false));
|
||||
} catch (error) {
|
||||
log(
|
||||
`restartVideo 오류: ${error.message}, bannerId: ${bannerId}, options: ${JSON.stringify(options)}`
|
||||
);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
[getLastPlayedBanner, playVideo, enableLogging, logPrefix]
|
||||
);
|
||||
|
||||
// 🔽 [편의 함수] 포커스 정책 기반 재생 제어
|
||||
const applyVideoPolicy = useCallback(
|
||||
(policy) => {
|
||||
try {
|
||||
if (!policy || typeof policy !== 'object') {
|
||||
log(`applyVideoPolicy: 잘못된 정책 - ${JSON.stringify(policy)}`);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
const { videoTarget, transition, confidence = 1.0 } = policy;
|
||||
|
||||
if (videoTarget) {
|
||||
// 동영상 재생
|
||||
return playVideo(videoTarget, {
|
||||
delay: confidence < 0.8 ? 200 : 100, // 신뢰도가 낮으면 지연
|
||||
reason: transition || 'policy',
|
||||
});
|
||||
} else {
|
||||
// 동영상 중지
|
||||
return stopVideo(null, {
|
||||
reason: transition || 'policy',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
log(`applyVideoPolicy 오류: ${error.message}, policy: ${JSON.stringify(policy)}`);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
},
|
||||
[playVideo, stopVideo, enableLogging, logPrefix]
|
||||
);
|
||||
|
||||
// 🔽 정리 작업
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// 컴포넌트 언마운트 시 타이머 정리
|
||||
if (playDelayTimerRef.current) {
|
||||
clearTimeout(playDelayTimerRef.current);
|
||||
}
|
||||
if (retryTimerRef.current) {
|
||||
clearTimeout(retryTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 🔽 현재 상태 계산
|
||||
const isPlaying = currentOwnerId && currentOwnerId.endsWith('_player');
|
||||
const currentBanner = currentOwnerId ? currentOwnerId.replace('_player', '') : null;
|
||||
|
||||
// 🔽 디버그 정보 (단순화)
|
||||
const getDebugInfo = useCallback(
|
||||
() => ({
|
||||
currentOwnerId,
|
||||
currentBanner,
|
||||
isPlaying,
|
||||
errorCount,
|
||||
videoState: videoState.getDebugInfo(),
|
||||
bannerAvailability: {
|
||||
banner0: isBannerAvailable('banner0'),
|
||||
banner1: isBannerAvailable('banner1'),
|
||||
},
|
||||
}),
|
||||
[currentOwnerId, currentBanner, isPlaying, errorCount, isBannerAvailable]
|
||||
);
|
||||
|
||||
return {
|
||||
// 🎯 핵심 함수
|
||||
playVideo,
|
||||
stopVideo,
|
||||
hideVideo, // 🔽 [새로운] 비디오 숨김 (소리 유지)
|
||||
restartVideo, // 🔽 [새로운] 파라미터 없이 재시작
|
||||
applyVideoPolicy,
|
||||
|
||||
// 📊 상태 정보
|
||||
getCurrentPlayingBanner,
|
||||
getLastPlayedBanner,
|
||||
isPlaying,
|
||||
currentBanner,
|
||||
bannerVisibility,
|
||||
|
||||
// 🔍 유틸리티
|
||||
isBannerAvailable,
|
||||
getDebugInfo,
|
||||
|
||||
// 📈 통계
|
||||
errorCount,
|
||||
};
|
||||
};
|
||||
|
||||
export default useVideoPlay;
|
||||
Reference in New Issue
Block a user