// src/hooks/useFocusHistory/useFocusHistory.js import { useRef, useCallback, useState, useMemo } from 'react'; import fp from '../../utils/fp.js'; // Toggle debug logging in this file (false by default) const DEBUG_MODE = false; const dlog = (...args) => { if (DEBUG_MODE) console.log(...args); }; const dwarn = (...args) => { if (DEBUG_MODE) console.warn(...args); }; const derror = (...args) => { console.error(...args); }; /** * 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') { dlog('[DEBUG] 🔄 icons → banner1 복원 패턴'); return { pattern: 'restore-banner1', videoTarget: 'banner1', confidence: 1.0, shouldShowBorder: true, }; } dlog('[DEBUG] 🎯 banner1 직접 포커스 패턴'); return { pattern: 'direct-banner1', videoTarget: 'banner1', confidence: 1.0, shouldShowBorder: true, }; } if (current === 'banner2') { if (previous === 'icons') { dlog('[DEBUG] 🔄 icons → banner2 복원 패턴'); return { pattern: 'restore-banner2', videoTarget: 'banner2', confidence: 1.0, shouldShowBorder: true, }; } dlog('[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') { dlog(`[DEBUG] 🔍 간접 포커스 (${current}) - 히스토리 분석 시작`); dlog(`[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; dlog(`[DEBUG] 발견된 비디오 배너: ${lastVideoBanner} (거리: ${i})`); break; // 가장 최근 것만 찾으면 됨 } } if (lastVideoBanner) { dlog(`[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 { dlog(`[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) { dwarn('[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; // 최후의 수단 - 빈 객체 dwarn('[FocusHistory] 전역 객체 접근 불가, 빈 객체 사용'); return {}; } catch (error) { derror('[FocusHistory] 전역 객체 접근 오류:', error); return {}; } }; /** * 전역 버퍼 복원 시도 * @returns {object|null} 복원된 버퍼 또는 null */ const attemptBufferRestore = () => { try { const globalObj = getGlobalObject(); const existingBuffer = globalObj[GLOBAL_BUFFER_KEY]; if (isValidBuffer(existingBuffer)) { dlog('[FocusHistory] ✅ 기존 전역 버퍼 복원 성공'); return existingBuffer; } else if (existingBuffer) { dwarn('[FocusHistory] ⚠️ 손상된 전역 버퍼 발견, 제거 후 재생성'); delete globalObj[GLOBAL_BUFFER_KEY]; } } catch (error) { derror('[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; } } dlog('[FocusHistory] 🆕 새 전역 버퍼 생성 및 등록 완료'); return newBuffer; } catch (error) { derror('[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) { derror('[FocusHistory] 전역 버퍼 초기화 실패:', error); // 최후의 수단: 최소한의 로컬 버퍼라도 제공 try { if (!globalFocusBuffer) { globalFocusBuffer = createFocusRingBuffer(); } return globalFocusBuffer; } catch (fallbackError) { derror('[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) { derror('[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) { derror('[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') { dwarn(`${logPrefix} Invalid focus ID:`, focusId); return { inserted: false, policy: null }; } // 버퍼 유효성 재검증 if (!isValidBuffer(buffer)) { derror(`${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) { dlog(`${logPrefix} 🎯 ${previous} → ${current}`); dlog(`${logPrefix} 📋 buffer:`, buffer.getHistory()); } else { dlog(`${logPrefix} 🎯 초기 포커스: ${current}`); dlog(`${logPrefix} 📋 buffer:`, buffer.getHistory()); } // 디버그 모드에서는 전체 히스토리 표시 // if (process.env.NODE_ENV === 'development') { // const debugInfo = buffer.getDebugInfo(); // console.log(`${logPrefix} 🔍 디버그:`, debugInfo); // } } } return result; // { inserted, policy } 반환 } catch (error) { derror(`${logPrefix} enqueue 실행 중 오류:`, error, { focusId }); // 오류 발생 시 안전한 기본값 반환 return { inserted: false, policy: null }; } }, [buffer, enableLogging, logPrefix, triggerUpdate] ); // 🔽 [개선] 안전한 상태 가져오기 const getQueueState = useCallback(() => { try { if (!isValidBuffer(buffer)) { dwarn(`${logPrefix} getQueueState: 버퍼 무효`); return { current: null, previous: null, history: [], haTransition: false, size: 0, head: -1, }; } return buffer.getState(); } catch (error) { derror(`${logPrefix} getQueueState 오류:`, error); return { current: null, previous: null, history: [], hasTransition: false, size: 0, head: -1, }; } }, [buffer, logPrefix]); // 🔽 [개선] 안전한 상태 초기화 const clearHistory = useCallback(() => { try { if (!isValidBuffer(buffer)) { dwarn(`${logPrefix} clearHistory: 버퍼 무효`); return; } buffer.clear(); triggerUpdate(); // 상태 변경 시 리렌더링 트리거 if (enableLogging) { dlog(`${logPrefix} 히스토리 초기화됨`); } } catch (error) { derror(`${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) { derror(`${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) { derror(`${logPrefix} getHistory 오류:`, error); return []; } }, [buffer, logPrefix]); const getHistoryAt = useCallback( (distance) => { try { return isValidBuffer(buffer) ? buffer.getHistoryAt(distance) : null; } catch (error) { derror(`${logPrefix} getHistoryAt 오류:`, error); return null; } }, [buffer, logPrefix] ); const detectPattern = useCallback(() => { try { return isValidBuffer(buffer) ? buffer.detectPattern() : { pattern: 'error', videoTarget: null, confidence: 0 }; } catch (error) { derror(`${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) { derror(`${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) { derror(`${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) { derror(`${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) { derror(`${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;