Files
shoptime/com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.js
optrader 6d0cf78534 [251124] fix: App.js 로그 정리 및 최적화
🕐 커밋 시간: 2025. 11. 24. 09:08:54

📊 변경 통계:
  • 총 파일: 10개
  • 추가: +93줄
  • 삭제: -97줄

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/App.js
  ~ com.twin.app.shoptime/src/components/MediaItem/MediaItem.module.less
  ~ com.twin.app.shoptime/src/components/MobileSend/PhoneInputSection.module.less
  ~ com.twin.app.shoptime/src/components/TPopUp/TNewPopUp.module.less
  ~ com.twin.app.shoptime/src/hooks/useVideoPlay/useVideoPlay.js
  ~ com.twin.app.shoptime/src/views/DetailPanel/ProductOverview/ProductOverview.module.less
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.jsx
  ~ com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx
  ~ com.twin.app.shoptime/src/views/MyPagePanel/MyPageSub/TermsOfService/TermsOfOptional.module copy.less
  ~ com.twin.app.shoptime/src/views/PlayerPanel/PlayerPanel.jsx

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/App/App.js (javascript):
    🔄 Modified: resolveSpotlightIdFromEvent()
     Deleted: handleFocusLog(), handleBlurLog()
  📄 com.twin.app.shoptime/src/views/MediaPanel/MediaPanel.v3.jsx (javascript):
    🔄 Modified: normalizeModalStyle()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • UI 컴포넌트 아키텍처 개선
  • API 서비스 레이어 개선

Performance: 코드 최적화로 성능 개선 기대
2025-11-24 09:08:54 +09:00

408 lines
13 KiB
JavaScript

// src/hooks/useVideoPlay/useVideoPlay.js
import { useCallback, useRef, useState, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
startVideoPlayerNew,
stopBannerVideo,
stopAndHideVideo,
hidePlayerVideo,
PLAYBACK_STATUS,
} 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 reduxVideoPlayState = useSelector((state) => state.play.videoPlayState);
// 🔽 [단순화] 현재 재생 중인 배너 가져오기
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);
// 🔽 Redux 기반 실제 재생 상태
const isVideoPlaying =
reduxVideoPlayState?.isPlaying === true &&
reduxVideoPlayState?.playback === PLAYBACK_STATUS.PLAYING;
// 🔽 전역 videoState가 실제 재생 상태와 불일치할 때 정리
useEffect(() => {
if (!isVideoPlaying && videoState.getCurrentPlaying()) {
videoState.setCurrentPlaying(null);
}
}, [isVideoPlaying]);
// 🔽 [유틸리티] 배너 가용성 검사 (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(
startVideoPlayerNew({
bannerId,
modal: true,
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 bannerAvailability = useMemo(
() => ({
banner0: isBannerAvailable('banner0'),
banner1: isBannerAvailable('banner1'),
}),
[isBannerAvailable]
);
// 🔽 디버그 정보 (단순화)
const getDebugInfo = useCallback(
() => ({
currentOwnerId,
currentBanner,
isPlaying,
errorCount,
videoState: videoState.getDebugInfo(),
bannerAvailability,
}),
[currentOwnerId, currentBanner, isPlaying, errorCount, bannerAvailability]
);
return {
// 🎯 핵심 함수
playVideo,
stopVideo,
hideVideo, // 🔽 [새로운] 비디오 숨김 (소리 유지)
restartVideo, // 🔽 [새로운] 파라미터 없이 재시작
applyVideoPolicy,
// 📊 상태 정보
getCurrentPlayingBanner,
getLastPlayedBanner,
isPlaying,
isVideoPlaying,
currentBanner,
bannerVisibility,
videoPlayState: reduxVideoPlayState,
bannerAvailability, // ✅ [최적화] 메모이제이션된 배너 가용성
// 🔍 유틸리티
isBannerAvailable,
getDebugInfo,
// 📈 통계
errorCount,
};
};
export default useVideoPlay;