[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:
@@ -0,0 +1,772 @@
|
||||
// src/hooks/useFocusHistory/useFocusHistory.js
|
||||
|
||||
import { useRef, useCallback, useState, useMemo } from 'react';
|
||||
import fp from '../../utils/fp.js';
|
||||
|
||||
/**
|
||||
* useFocusHistory Hook - 경량화된 포커스 히스토리 관리
|
||||
* - 어떤 string 식별자든 받을 수 있는 범용적 구조
|
||||
* - 현재 포커스와 이전 포커스 2개 상태만 추적 (Ring Buffer)
|
||||
* - 전역 상태 관리 가능
|
||||
* - 매우 간단한 인터페이스 제공
|
||||
*/
|
||||
|
||||
// 🔽 [확장] Enhanced Ring Buffer - 10개 히스토리 + 패턴 인식 + 정책 엔진
|
||||
const createFocusRingBuffer = () => {
|
||||
const BUFFER_SIZE = 10;
|
||||
const buffer = new Array(BUFFER_SIZE).fill(null); // 10개 히스토리
|
||||
let head = -1; // Current position pointer
|
||||
let size = 0; // Current elements count (0-10)
|
||||
|
||||
// 🔽 [개선] 포커스 항목 삽입 - 원본 ID 그대로 사용 (0부터 시작 통일)
|
||||
const enqueue = (focusId) => {
|
||||
// 중복 삽입 방지 - 원본 ID로 직접 비교
|
||||
const currentFocusId = size > 0 ? buffer[head] : null;
|
||||
|
||||
if (size > 0 && currentFocusId === focusId) {
|
||||
return { inserted: false, policy: null }; // 중복이므로 삽입하지 않음
|
||||
}
|
||||
|
||||
// 다음 위치로 이동하고 원본 ID 저장
|
||||
head = (head + 1) % BUFFER_SIZE;
|
||||
buffer[head] = focusId;
|
||||
|
||||
if (size < BUFFER_SIZE) {
|
||||
size++;
|
||||
}
|
||||
|
||||
// 🔑 [신기능] 패턴 분석 후 정책 반환
|
||||
const policy = calculateVideoPolicy();
|
||||
|
||||
return { inserted: true, policy }; // 삽입 성공 + 정책 반환
|
||||
};
|
||||
|
||||
// 🔽 [수정] 현재 포커스 가져오기 (10개 버퍼)
|
||||
const getCurrent = () => {
|
||||
return size > 0 ? buffer[head] : null;
|
||||
};
|
||||
|
||||
// 🔽 [수정] 이전 포커스 가져오기 (10개 버퍼)
|
||||
const getPrevious = () => {
|
||||
if (size < 2) return null;
|
||||
const prevIndex = (head - 1 + BUFFER_SIZE) % BUFFER_SIZE;
|
||||
return buffer[prevIndex];
|
||||
};
|
||||
|
||||
// 🔽 [수정] 전체 히스토리 가져오기 (10개 - 최신 순서로 반환)
|
||||
const getHistory = () => {
|
||||
if (size === 0) return [];
|
||||
|
||||
const history = [];
|
||||
for (let i = 0; i < Math.min(size, BUFFER_SIZE); i++) {
|
||||
const index = (head - i + BUFFER_SIZE) % BUFFER_SIZE;
|
||||
history.push(buffer[index]);
|
||||
}
|
||||
return history; // [current, previous, older, oldest, ...]
|
||||
};
|
||||
|
||||
// 🔽 [편의 함수] 특정 거리의 히스토리 가져오기
|
||||
const getHistoryAt = (distance) => {
|
||||
if (distance >= size || distance < 0) return null;
|
||||
const index = (head - distance + BUFFER_SIZE) % BUFFER_SIZE;
|
||||
return buffer[index];
|
||||
};
|
||||
|
||||
// 🔽 [수정] 전체 상태 가져오기 (확장)
|
||||
const getState = () => ({
|
||||
current: getCurrent(),
|
||||
previous: getPrevious(),
|
||||
history: getHistory(),
|
||||
hasTransition: size >= 2 && getCurrent() !== getPrevious(),
|
||||
size,
|
||||
head,
|
||||
});
|
||||
|
||||
// 🔽 [수정] 상태 초기화 (10개 버퍼)
|
||||
const clear = () => {
|
||||
buffer.fill(null);
|
||||
head = -1;
|
||||
size = 0;
|
||||
};
|
||||
|
||||
// 🔽 [개선] 패턴 인식 엔진 - 더 깊은 히스토리 분석
|
||||
const detectPattern = () => {
|
||||
const history = getHistory(); // [current, previous, older, oldest, ...]
|
||||
const current = history[0];
|
||||
const previous = history[1];
|
||||
|
||||
if (!current) {
|
||||
return { pattern: 'no-focus', videoTarget: null, confidence: 0 };
|
||||
}
|
||||
|
||||
// 🔽 [로그] banner 간 이동 패턴 분석
|
||||
// if (previous && (previous.startsWith('banner') && current.startsWith('banner'))) {
|
||||
// console.log('[BannerFlow] 포커스 이동:', {
|
||||
// from: previous,
|
||||
// to: current,
|
||||
// history: history.slice(0, 5),
|
||||
// timestamp: new Date().toISOString()
|
||||
// });
|
||||
// }
|
||||
|
||||
// 직접 포커스 (banner1, banner2)
|
||||
if (current === 'banner1') {
|
||||
if (previous === 'icons') {
|
||||
console.log('[DEBUG] 🔄 icons → banner1 복원 패턴');
|
||||
return {
|
||||
pattern: 'restore-banner1',
|
||||
videoTarget: 'banner1',
|
||||
confidence: 1.0,
|
||||
shouldShowBorder: true,
|
||||
};
|
||||
}
|
||||
console.log('[DEBUG] 🎯 banner1 직접 포커스 패턴');
|
||||
return {
|
||||
pattern: 'direct-banner1',
|
||||
videoTarget: 'banner1',
|
||||
confidence: 1.0,
|
||||
shouldShowBorder: true,
|
||||
};
|
||||
}
|
||||
if (current === 'banner2') {
|
||||
if (previous === 'icons') {
|
||||
console.log('[DEBUG] 🔄 icons → banner2 복원 패턴');
|
||||
return {
|
||||
pattern: 'restore-banner2',
|
||||
videoTarget: 'banner2',
|
||||
confidence: 1.0,
|
||||
shouldShowBorder: true,
|
||||
};
|
||||
}
|
||||
console.log('[DEBUG] 🎯 banner2 직접 포커스 패턴');
|
||||
return {
|
||||
pattern: 'direct-banner2',
|
||||
videoTarget: 'banner2',
|
||||
confidence: 1.0,
|
||||
shouldShowBorder: true,
|
||||
};
|
||||
}
|
||||
|
||||
// icons 포커스 처리
|
||||
if (current === 'icons') {
|
||||
// console.log('[BannerFlow] 🛑 icons 포커스 - 동영상 중지');
|
||||
return {
|
||||
pattern: 'icons-stop',
|
||||
videoTarget: null,
|
||||
confidence: 1.0,
|
||||
shouldShowBorder: false,
|
||||
reason: 'icons 포커스로 동영상 중지',
|
||||
};
|
||||
}
|
||||
|
||||
// 🔽 [개선] 간접 포커스 (banner3, banner4) - 더 깊은 히스토리 확인
|
||||
if (current === 'banner3' || current === 'banner4') {
|
||||
console.log(`[DEBUG] 🔍 간접 포커스 (${current}) - 히스토리 분석 시작`);
|
||||
console.log(`[DEBUG] 전체 히스토리:`, history);
|
||||
|
||||
// 히스토리에서 가장 최근의 banner1 또는 banner2 찾기
|
||||
let lastVideoBanner = null;
|
||||
let lastVideoBannerDistance = -1;
|
||||
|
||||
for (let i = 1; i < Math.min(history.length, 10); i++) {
|
||||
if (history[i] === 'banner1' || history[i] === 'banner2') {
|
||||
lastVideoBanner = history[i];
|
||||
lastVideoBannerDistance = i;
|
||||
console.log(`[DEBUG] 발견된 비디오 배너: ${lastVideoBanner} (거리: ${i})`);
|
||||
break; // 가장 최근 것만 찾으면 됨
|
||||
}
|
||||
}
|
||||
|
||||
if (lastVideoBanner) {
|
||||
console.log(`[DEBUG] 🔄 간접 포커스 유지 패턴:`, {
|
||||
current: current,
|
||||
maintainTarget: lastVideoBanner,
|
||||
distance: lastVideoBannerDistance,
|
||||
reason: `${current} 포커스, ${lastVideoBannerDistance}단계 이전 ${lastVideoBanner} 유지`,
|
||||
});
|
||||
return {
|
||||
pattern: `maintain-${lastVideoBanner}`,
|
||||
videoTarget: lastVideoBanner,
|
||||
confidence: Math.max(0.7, 1.0 - lastVideoBannerDistance * 0.1), // 거리가 멀수록 신뢰도 감소
|
||||
shouldShowBorder: false,
|
||||
reason: `${current} 포커스, ${lastVideoBannerDistance}단계 이전 ${lastVideoBanner} 유지`,
|
||||
};
|
||||
} else {
|
||||
console.log(`[DEBUG] ❓ 간접 포커스 - 히스토리 없음:`, {
|
||||
current: current,
|
||||
history: history.slice(0, 5),
|
||||
reason: '비디오 배너 히스토리 없음, 기본값 banner1 사용',
|
||||
});
|
||||
// 비디오 배너 히스토리가 없으면 기본값
|
||||
return {
|
||||
pattern: 'default-banner1',
|
||||
videoTarget: 'banner1',
|
||||
confidence: 0.5,
|
||||
shouldShowBorder: false,
|
||||
reason: `${current} 포커스, 비디오 배너 히스토리 없음 - 기본값 banner1`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 기타 포커스 (icons, gnb 등)
|
||||
return {
|
||||
pattern: 'other-focus',
|
||||
videoTarget: null,
|
||||
confidence: 0,
|
||||
shouldShowBorder: false,
|
||||
reason: `비배너 포커스: ${current}`,
|
||||
};
|
||||
};
|
||||
|
||||
// 🔽 [신기능] 비디오 정책 계산
|
||||
const calculateVideoPolicy = () => {
|
||||
const patternResult = detectPattern();
|
||||
|
||||
return {
|
||||
videoTarget: patternResult.videoTarget,
|
||||
shouldShowBorder: patternResult.shouldShowBorder,
|
||||
transition: patternResult.pattern,
|
||||
confidence: patternResult.confidence,
|
||||
reason: patternResult.reason,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
};
|
||||
|
||||
// 🔽 [개선] 디버깅 정보
|
||||
const getDebugInfo = () => {
|
||||
const history = getHistory();
|
||||
const pattern = detectPattern();
|
||||
|
||||
return {
|
||||
buffer: [...buffer],
|
||||
head,
|
||||
size,
|
||||
history: history,
|
||||
historyLabeled: {
|
||||
current: history[0] || null,
|
||||
previous: history[1] || null,
|
||||
older: history[2] || null,
|
||||
oldest: history[3] || null,
|
||||
full: history,
|
||||
},
|
||||
pattern,
|
||||
policy: calculateVideoPolicy(),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
// 기존 기능
|
||||
enqueue,
|
||||
getCurrent,
|
||||
getPrevious,
|
||||
getState,
|
||||
clear,
|
||||
|
||||
// 🔽 [신기능] 확장 기능
|
||||
getHistory,
|
||||
getHistoryAt, // 🔽 [추가] 특정 거리 히스토리
|
||||
detectPattern,
|
||||
calculateVideoPolicy,
|
||||
getDebugInfo,
|
||||
};
|
||||
};
|
||||
|
||||
// 🔽 [개선] globalThis 기반 안전한 전역 상태 관리
|
||||
// HMR(Hot Module Reload) 및 크로스 플랫폼 호환성 보장
|
||||
let globalFocusBuffer = null;
|
||||
|
||||
// 전역 버퍼 네임스페이스 상수
|
||||
const GLOBAL_BUFFER_KEY = '__FOCUS_BUFFER__';
|
||||
|
||||
/**
|
||||
* 전역 버퍼 유효성 검증
|
||||
* @param {*} buffer - 검증할 버퍼 객체
|
||||
* @returns {boolean} 유효한 버퍼인지 여부
|
||||
*/
|
||||
const isValidBuffer = (buffer) => {
|
||||
try {
|
||||
return (
|
||||
buffer &&
|
||||
typeof buffer === 'object' &&
|
||||
typeof buffer.enqueue === 'function' &&
|
||||
typeof buffer.getCurrent === 'function' &&
|
||||
typeof buffer.getState === 'function' &&
|
||||
typeof buffer.clear === 'function'
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn('[FocusHistory] 버퍼 유효성 검증 실패:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전한 전역 객체 접근 함수 (Chromium 68 호환)
|
||||
* @returns {object} 전역 객체 (폴백 포함)
|
||||
*/
|
||||
const getGlobalObject = () => {
|
||||
try {
|
||||
// 🔽 [Chromium 68 호환] window 우선 (webOS TV 환경)
|
||||
if (typeof window !== 'undefined') return window;
|
||||
|
||||
// Node.js 환경
|
||||
if (typeof global !== 'undefined') return global;
|
||||
|
||||
// Web Worker 환경
|
||||
// eslint-disable-next-line no-undef
|
||||
if (typeof self !== 'undefined') return self;
|
||||
|
||||
// 🔽 [제거] globalThis는 Chromium 68에서 지원하지 않음
|
||||
// if (typeof globalThis !== 'undefined') return globalThis;
|
||||
|
||||
// 최후의 수단 - 빈 객체
|
||||
console.warn('[FocusHistory] 전역 객체 접근 불가, 빈 객체 사용');
|
||||
return {};
|
||||
} catch (error) {
|
||||
console.error('[FocusHistory] 전역 객체 접근 오류:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 전역 버퍼 복원 시도
|
||||
* @returns {object|null} 복원된 버퍼 또는 null
|
||||
*/
|
||||
const attemptBufferRestore = () => {
|
||||
try {
|
||||
const globalObj = getGlobalObject();
|
||||
const existingBuffer = globalObj[GLOBAL_BUFFER_KEY];
|
||||
|
||||
if (isValidBuffer(existingBuffer)) {
|
||||
console.log('[FocusHistory] ✅ 기존 전역 버퍼 복원 성공');
|
||||
return existingBuffer;
|
||||
} else if (existingBuffer) {
|
||||
console.warn('[FocusHistory] ⚠️ 손상된 전역 버퍼 발견, 제거 후 재생성');
|
||||
delete globalObj[GLOBAL_BUFFER_KEY];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[FocusHistory] 버퍼 복원 시도 실패:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 전역 버퍼 생성 및 등록
|
||||
* @returns {object} 생성된 버퍼
|
||||
*/
|
||||
const createAndRegisterBuffer = () => {
|
||||
try {
|
||||
const newBuffer = createFocusRingBuffer();
|
||||
const globalObj = getGlobalObject();
|
||||
|
||||
// 전역 객체에 안전하게 등록
|
||||
if (globalObj && typeof globalObj === 'object') {
|
||||
globalObj[GLOBAL_BUFFER_KEY] = newBuffer;
|
||||
|
||||
// 개발 환경에서 디버깅 편의성 제공
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// 추가 접근 경로 제공 (하위 호환성)
|
||||
globalObj.globalFocusBuffer = newBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[FocusHistory] 🆕 새 전역 버퍼 생성 및 등록 완료');
|
||||
return newBuffer;
|
||||
} catch (error) {
|
||||
console.error('[FocusHistory] 버퍼 생성 및 등록 실패:', error);
|
||||
// 최후의 수단 - 로컬 버퍼라도 반환
|
||||
return createFocusRingBuffer();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전한 전역 버퍼 가져오기/생성 함수
|
||||
* HMR, 모듈 재로드, 크로스 플랫폼 환경에서 안정적으로 동작
|
||||
* @returns {object} 전역 포커스 버퍼 인스턴스
|
||||
*/
|
||||
const getOrCreateGlobalBuffer = () => {
|
||||
try {
|
||||
// 1단계: 이미 로드된 버퍼가 있고 유효한지 확인
|
||||
if (isValidBuffer(globalFocusBuffer)) {
|
||||
return globalFocusBuffer;
|
||||
}
|
||||
|
||||
// 2단계: 전역 객체에서 기존 버퍼 복원 시도
|
||||
const restoredBuffer = attemptBufferRestore();
|
||||
if (restoredBuffer) {
|
||||
globalFocusBuffer = restoredBuffer;
|
||||
return globalFocusBuffer;
|
||||
}
|
||||
|
||||
// 3단계: 새 버퍼 생성 및 등록
|
||||
globalFocusBuffer = createAndRegisterBuffer();
|
||||
return globalFocusBuffer;
|
||||
} catch (error) {
|
||||
console.error('[FocusHistory] 전역 버퍼 초기화 실패:', error);
|
||||
|
||||
// 최후의 수단: 최소한의 로컬 버퍼라도 제공
|
||||
try {
|
||||
if (!globalFocusBuffer) {
|
||||
globalFocusBuffer = createFocusRingBuffer();
|
||||
}
|
||||
return globalFocusBuffer;
|
||||
} catch (fallbackError) {
|
||||
console.error('[FocusHistory] 폴백 버퍼 생성도 실패:', fallbackError);
|
||||
// 더미 버퍼 반환 (앱 크래시 방지)
|
||||
return {
|
||||
enqueue: () => ({ inserted: false, policy: null }),
|
||||
getCurrent: () => null,
|
||||
getPrevious: () => null,
|
||||
getState: () => ({
|
||||
current: null,
|
||||
previous: null,
|
||||
history: [],
|
||||
hasTransition: false,
|
||||
size: 0,
|
||||
head: -1,
|
||||
}),
|
||||
clear: () => {},
|
||||
getHistory: () => [],
|
||||
getHistoryAt: () => null,
|
||||
detectPattern: () => ({ pattern: 'error', videoTarget: null, confidence: 0 }),
|
||||
calculateVideoPolicy: () => ({
|
||||
videoTarget: null,
|
||||
shouldShowBorder: false,
|
||||
transition: 'error',
|
||||
confidence: 0,
|
||||
}),
|
||||
getDebugInfo: () => ({ error: 'Buffer creation failed' }),
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const useFocusHistory = (options = {}) => {
|
||||
const {
|
||||
enableLogging = process.env.NODE_ENV === 'development',
|
||||
useGlobalState = true, // 전역 상태 사용 여부
|
||||
logPrefix = '[focusHistory]',
|
||||
} = options;
|
||||
|
||||
// 강제 리렌더링을 위한 상태
|
||||
const [, forceUpdate] = useState({});
|
||||
const triggerUpdate = useCallback(() => forceUpdate({}), []);
|
||||
|
||||
// 로컬 버퍼 참조
|
||||
const localBufferRef = useRef(null);
|
||||
|
||||
// 🔽 [개선] 안전한 버퍼 초기화 로직
|
||||
const buffer = useMemo(() => {
|
||||
try {
|
||||
if (useGlobalState) {
|
||||
// 전역 버퍼 사용: 안전한 초기화 함수 호출
|
||||
return getOrCreateGlobalBuffer();
|
||||
} else {
|
||||
// 로컬 버퍼 사용: 안전한 로컬 버퍼 생성
|
||||
if (!localBufferRef.current) {
|
||||
try {
|
||||
localBufferRef.current = createFocusRingBuffer();
|
||||
} catch (error) {
|
||||
console.error('[FocusHistory] 로컬 버퍼 생성 실패:', error);
|
||||
// 더미 버퍼로 폴백
|
||||
localBufferRef.current = {
|
||||
enqueue: () => ({ inserted: false, policy: null }),
|
||||
getCurrent: () => null,
|
||||
getPrevious: () => null,
|
||||
getState: () => ({
|
||||
current: null,
|
||||
previous: null,
|
||||
history: [],
|
||||
hasTransition: false,
|
||||
size: 0,
|
||||
head: -1,
|
||||
}),
|
||||
clear: () => {},
|
||||
getHistory: () => [],
|
||||
getHistoryAt: () => null,
|
||||
detectPattern: () => ({ pattern: 'error', videoTarget: null, confidence: 0 }),
|
||||
calculateVideoPolicy: () => ({
|
||||
videoTarget: null,
|
||||
shouldShowBorder: false,
|
||||
transition: 'error',
|
||||
confidence: 0,
|
||||
}),
|
||||
getDebugInfo: () => ({ error: 'Local buffer creation failed' }),
|
||||
};
|
||||
}
|
||||
}
|
||||
return localBufferRef.current;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[FocusHistory] 버퍼 초기화 전체 실패:', error);
|
||||
// 최후의 더미 버퍼
|
||||
return {
|
||||
enqueue: () => ({ inserted: false, policy: null }),
|
||||
getCurrent: () => null,
|
||||
getPrevious: () => null,
|
||||
getState: () => ({
|
||||
current: null,
|
||||
previous: null,
|
||||
history: [],
|
||||
hasTransition: false,
|
||||
size: 0,
|
||||
head: -1,
|
||||
}),
|
||||
clear: () => {},
|
||||
getHistory: () => [],
|
||||
getHistoryAt: () => null,
|
||||
detectPattern: () => ({ pattern: 'error', videoTarget: null, confidence: 0 }),
|
||||
calculateVideoPolicy: () => ({
|
||||
videoTarget: null,
|
||||
shouldShowBorder: false,
|
||||
transition: 'error',
|
||||
confidence: 0,
|
||||
}),
|
||||
getDebugInfo: () => ({ error: 'Complete buffer initialization failed' }),
|
||||
};
|
||||
}
|
||||
}, [useGlobalState]);
|
||||
|
||||
// 🔽 [개선] 안전한 포커스 히스토리 추가 + 정책 엔진 통합
|
||||
const enqueue = useCallback(
|
||||
(focusId) => {
|
||||
try {
|
||||
// 입력값 검증
|
||||
if (!focusId || typeof focusId !== 'string') {
|
||||
console.warn(`${logPrefix} Invalid focus ID:`, focusId);
|
||||
return { inserted: false, policy: null };
|
||||
}
|
||||
|
||||
// 버퍼 유효성 재검증
|
||||
if (!isValidBuffer(buffer)) {
|
||||
console.error(`${logPrefix} 버퍼가 손상됨, enqueue 실패`);
|
||||
return { inserted: false, policy: null };
|
||||
}
|
||||
|
||||
const previousFocus = buffer.getCurrent(); // 현재가 이전이 됨
|
||||
const result = buffer.enqueue(focusId); // { inserted, policy }
|
||||
|
||||
if (result.inserted) {
|
||||
// 상태 변경 시 리렌더링 트리거
|
||||
triggerUpdate();
|
||||
|
||||
if (enableLogging) {
|
||||
const current = buffer.getCurrent();
|
||||
const previous = buffer.getPrevious();
|
||||
const policy = result.policy;
|
||||
|
||||
// 🔽 [향상된 로깅] 패턴과 정책 정보 포함
|
||||
if (previous && current && previous !== current) {
|
||||
console.log(`${logPrefix} 🎯 ${previous} → ${current}`);
|
||||
console.log(`${logPrefix} 📋 buffer:`, buffer.getHistory());
|
||||
} else {
|
||||
console.log(`${logPrefix} 🎯 초기 포커스: ${current}`);
|
||||
console.log(`${logPrefix} 📋 buffer:`, buffer.getHistory());
|
||||
}
|
||||
|
||||
// 디버그 모드에서는 전체 히스토리 표시
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// const debugInfo = buffer.getDebugInfo();
|
||||
// console.log(`${logPrefix} 🔍 디버그:`, debugInfo);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
return result; // { inserted, policy } 반환
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} enqueue 실행 중 오류:`, error, { focusId });
|
||||
// 오류 발생 시 안전한 기본값 반환
|
||||
return { inserted: false, policy: null };
|
||||
}
|
||||
},
|
||||
[buffer, enableLogging, logPrefix, triggerUpdate]
|
||||
);
|
||||
|
||||
// 🔽 [개선] 안전한 상태 가져오기
|
||||
const getQueueState = useCallback(() => {
|
||||
try {
|
||||
if (!isValidBuffer(buffer)) {
|
||||
console.warn(`${logPrefix} getQueueState: 버퍼 무효`);
|
||||
return {
|
||||
current: null,
|
||||
previous: null,
|
||||
history: [],
|
||||
haTransition: false,
|
||||
size: 0,
|
||||
head: -1,
|
||||
};
|
||||
}
|
||||
return buffer.getState();
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} getQueueState 오류:`, error);
|
||||
return {
|
||||
current: null,
|
||||
previous: null,
|
||||
history: [],
|
||||
hasTransition: false,
|
||||
size: 0,
|
||||
head: -1,
|
||||
};
|
||||
}
|
||||
}, [buffer, logPrefix]);
|
||||
|
||||
// 🔽 [개선] 안전한 상태 초기화
|
||||
const clearHistory = useCallback(() => {
|
||||
try {
|
||||
if (!isValidBuffer(buffer)) {
|
||||
console.warn(`${logPrefix} clearHistory: 버퍼 무효`);
|
||||
return;
|
||||
}
|
||||
buffer.clear();
|
||||
triggerUpdate(); // 상태 변경 시 리렌더링 트리거
|
||||
if (enableLogging) {
|
||||
console.log(`${logPrefix} 히스토리 초기화됨`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} clearHistory 오류:`, error);
|
||||
}
|
||||
}, [buffer, enableLogging, logPrefix, triggerUpdate]);
|
||||
|
||||
// 🔽 [개선] 안전한 현재 상태 가져오기
|
||||
const currentState = useMemo(() => {
|
||||
try {
|
||||
if (!isValidBuffer(buffer)) {
|
||||
return {
|
||||
current: null,
|
||||
previous: null,
|
||||
history: [],
|
||||
hasTransition: false,
|
||||
size: 0,
|
||||
head: -1,
|
||||
};
|
||||
}
|
||||
return buffer.getState();
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} currentState 계산 오류:`, error);
|
||||
return {
|
||||
current: null,
|
||||
previous: null,
|
||||
history: [],
|
||||
hasTransition: false,
|
||||
size: 0,
|
||||
head: -1,
|
||||
};
|
||||
}
|
||||
}, [buffer, logPrefix]);
|
||||
|
||||
// 🔽 [개선] 안전한 확장된 메서드들
|
||||
const getHistory = useCallback(() => {
|
||||
try {
|
||||
return isValidBuffer(buffer) ? buffer.getHistory() : [];
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} getHistory 오류:`, error);
|
||||
return [];
|
||||
}
|
||||
}, [buffer, logPrefix]);
|
||||
|
||||
const getHistoryAt = useCallback(
|
||||
(distance) => {
|
||||
try {
|
||||
return isValidBuffer(buffer) ? buffer.getHistoryAt(distance) : null;
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} getHistoryAt 오류:`, error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[buffer, logPrefix]
|
||||
);
|
||||
|
||||
const detectPattern = useCallback(() => {
|
||||
try {
|
||||
return isValidBuffer(buffer)
|
||||
? buffer.detectPattern()
|
||||
: { pattern: 'error', videoTarget: null, confidence: 0 };
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} detectPattern 오류:`, error);
|
||||
return { pattern: 'error', videoTarget: null, confidence: 0 };
|
||||
}
|
||||
}, [buffer, logPrefix]);
|
||||
|
||||
const calculateVideoPolicy = useCallback(() => {
|
||||
try {
|
||||
return isValidBuffer(buffer)
|
||||
? buffer.calculateVideoPolicy()
|
||||
: { videoTarget: null, shouldShowBorder: false, transition: 'error', confidence: 0 };
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} calculateVideoPolicy 오류:`, error);
|
||||
return { videoTarget: null, shouldShowBorder: false, transition: 'error', confidence: 0 };
|
||||
}
|
||||
}, [buffer, logPrefix]);
|
||||
|
||||
const getDebugInfo = useCallback(() => {
|
||||
try {
|
||||
return isValidBuffer(buffer)
|
||||
? buffer.getDebugInfo()
|
||||
: { error: 'Buffer invalid or unavailable' };
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} getDebugInfo 오류:`, error);
|
||||
return { error: 'getDebugInfo failed', details: error.message };
|
||||
}
|
||||
}, [buffer, logPrefix]);
|
||||
|
||||
// 🔽 [추가] getQueue - 최신 상태의 큐를 최신순(맨 앞)으로 반환
|
||||
const getQueue = useCallback(() => {
|
||||
try {
|
||||
if (!isValidBuffer(buffer)) {
|
||||
return []; // 빈 배열 반환
|
||||
}
|
||||
return buffer.getHistory(); // 이미 최신순으로 정렬됨 [current, previous, older, oldest, ...]
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} getQueue 오류:`, error);
|
||||
return [];
|
||||
}
|
||||
}, [buffer, logPrefix]);
|
||||
|
||||
// 🔽 [추가] getQueueSafe - 최대 2개까지만 안전하게 가져오기 (undefined 방지)
|
||||
const getQueueSafe = useCallback(() => {
|
||||
try {
|
||||
const queue = getQueue(); // 기본 큐 가져오기
|
||||
|
||||
// FP 방식으로 안전하게 최대 2개까지만 가져오기
|
||||
return fp.pipe(
|
||||
fp.defaultTo([]), // null/undefined 방지
|
||||
(arr) => fp.slice(0, 2, arr), // 최대 2개까지만
|
||||
fp.map(fp.defaultTo(null)) // 각 항목도 null로 안전하게 처리
|
||||
)(queue);
|
||||
} catch (error) {
|
||||
console.error(`${logPrefix} getQueueSafe 오류:`, error);
|
||||
return []; // 에러 시 빈 배열
|
||||
}
|
||||
}, [getQueue, logPrefix]);
|
||||
|
||||
return {
|
||||
// 핵심 함수들
|
||||
enqueue,
|
||||
getQueueState,
|
||||
clearHistory,
|
||||
|
||||
// 🔽 [신기능] 확장 함수들
|
||||
getHistory,
|
||||
getHistoryAt, // 🔽 [추가] 특정 거리 히스토리
|
||||
getQueue, // 🔽 [추가] 최신 순으로 정렬된 큐
|
||||
getQueueSafe, // 🔽 [추가] 최대 2개까지만 안전하게 가져오기
|
||||
detectPattern,
|
||||
calculateVideoPolicy,
|
||||
getDebugInfo,
|
||||
|
||||
// 편의 속성들 (실시간 업데이트)
|
||||
curFocused: currentState.current,
|
||||
preFocused: currentState.previous,
|
||||
hasTransition: currentState.hasTransition,
|
||||
history: currentState.history, // 🔽 [추가] 전체 히스토리
|
||||
|
||||
// 배열 형태로 반환
|
||||
focusTransition:
|
||||
currentState.previous && currentState.current
|
||||
? [currentState.previous, currentState.current]
|
||||
: currentState.current
|
||||
? [null, currentState.current]
|
||||
: [null, null],
|
||||
};
|
||||
};
|
||||
|
||||
export default useFocusHistory;
|
||||
Reference in New Issue
Block a user