feat: Add Phase 2 stability improvements to MediaPlayer.v2

Phase 2 안정성 향상 적용:

1. sourceUnavailable 함수형 업데이트 (15% → 3%)
   - handleUpdate에서 stale closure 방지
   - 함수형 업데이트 패턴 사용: setSourceUnavailable((prev) => ...)
   - 의존성 배열에서 sourceUnavailable 제거

2. YouTube URL 정규식 검증 (10% → 2%)
   - URL 객체로 hostname 파싱 시도
   - youtube.com, youtu.be, m.youtube.com 도메인 체크
   - 파싱 실패 시 정규식 fallback
   - 파일명 충돌 오탐 방지

3. Modal → Fullscreen 전환 시 controls 연장 (20% UX 개선)
   - showControls에 timeout 파라미터 추가 (기본 3초)
   - Fullscreen 전환 시 10초로 연장
   - 사용자가 리모컨 조작 준비할 시간 제공

예상 안정성 개선:
- 기능 저하: 20% → 5%
- 완벽한 작동: 80% → 95%
- UX 만족도: +20%

관련 문서: .docs/MediaPlayer-v2-Risk-Analysis.md (Phase 2)
This commit is contained in:
Claude
2025-11-10 09:04:25 +00:00
parent 10b6942af8
commit 679c37ae32

View File

@@ -135,7 +135,18 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
// ========== Computed Values ========== // ========== Computed Values ==========
const isYoutube = useMemo(() => { const isYoutube = useMemo(() => {
return src && src.includes('youtu'); if (!src) return false;
try {
// URL 파싱 시도
const url = new URL(src);
return ['youtube.com', 'youtu.be', 'm.youtube.com'].some(domain =>
url.hostname.includes(domain)
);
} catch {
// URL 파싱 실패 시 정규식 검사
return /https?:\/\/(www\.|m\.)?youtu(\.be|be\.com)/.test(src);
}
}, [src]); }, [src]);
const isModal = panelInfo?.modal; const isModal = panelInfo?.modal;
@@ -214,7 +225,11 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
setPaused(el.paused); setPaused(el.paused);
setLoading(el.loading || false); setLoading(el.loading || false);
setError(el.error || null); setError(el.error || null);
setSourceUnavailable((el.loading && sourceUnavailable) || el.error);
// 함수형 업데이트로 stale closure 방지
setSourceUnavailable((prevUnavailable) =>
(el.loading && prevUnavailable) || el.error
);
// Proportion 계산 // Proportion 계산
updateProportionLoaded(); // 플랫폼별 계산 함수 호출 updateProportionLoaded(); // 플랫폼별 계산 함수 호출
@@ -233,7 +248,7 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
if (ev.type === 'durationchange' && onDurationChange) { if (ev.type === 'durationchange' && onDurationChange) {
onDurationChange(ev); onDurationChange(ev);
} }
}, [onTimeUpdate, onLoadedData, onLoadedMetadata, onDurationChange, sourceUnavailable, updateProportionLoaded]); }, [onTimeUpdate, onLoadedData, onLoadedMetadata, onDurationChange, updateProportionLoaded]);
const handleEnded = useCallback((e) => { const handleEnded = useCallback((e) => {
if (onEnded) { if (onEnded) {
@@ -249,18 +264,18 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
}, [onError]); }, [onError]);
// ========== Controls Management ========== // ========== Controls Management ==========
const showControls = useCallback(() => { const showControls = useCallback((timeout = 3000) => {
if (disabled || isModal) return; if (disabled || isModal) return;
setControlsVisible(true); setControlsVisible(true);
// 3초 후 자동 숨김 // timeout 후 자동 숨김 (기본 3초, Modal 전환 시 10초)
if (controlsTimeoutRef.current) { if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current); clearTimeout(controlsTimeoutRef.current);
} }
controlsTimeoutRef.current = setTimeout(() => { controlsTimeoutRef.current = setTimeout(() => {
setControlsVisible(false); setControlsVisible(false);
}, 3000); }, timeout);
}, [disabled, isModal]); }, [disabled, isModal]);
const hideControls = useCallback(() => { const hideControls = useCallback(() => {
@@ -375,7 +390,8 @@ const MediaPlayerV2 = forwardRef((props, ref) => {
if (videoRef.current?.paused) { if (videoRef.current?.paused) {
play(); play();
} }
showControls(); // Fullscreen 전환 시 controls를 10초로 연장 표시
showControls(10000);
} }
prevModalRef.current = isModal; prevModalRef.current = isModal;
}, [isModal, play, showControls]); }, [isModal, play, showControls]);