import { useState, useEffect, useRef, useCallback } from 'react'; import { getLatestData, SensorData } from '../services/api'; export type ConnectionStatus = 'connected' | 'disconnected' | 'error' | 'retrying' | 'connecting'; export interface UseRealTimeDataReturn { data: SensorData[]; isConnected: boolean; connectionStatus: ConnectionStatus; dataSource: 'polling'; lastUpdate: string; error: string | null; loading: boolean; refetch: () => Promise; retryCount: Map; } const POLLING_INTERVAL = 5000; // 5초 const CACHE_DURATION = 30 * 1000; // 30초 const RETRY_DELAY = 1000; // 1초 export const useRealTimeData = (deviceIds: string[]): UseRealTimeDataReturn => { const [data, setData] = useState([]); const [dataSource, setDataSource] = useState<'polling'>('polling'); const [lastUpdate, setLastUpdate] = useState(''); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [connectionStatus, setConnectionStatus] = useState('connecting'); const cacheRef = useRef>(new Map()); const pollingTimerRef = useRef(null); const retryTimersRef = useRef>(new Map()); const mountedRef = useRef(true); const previousDataRef = useRef>(new Map()); const retryCountRef = useRef>(new Map()); const maxRetries = 3; // 컴포넌트 언마운트 시 정리 useEffect(() => { return () => { mountedRef.current = false; if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); } // 재시도 타이머들 정리 retryTimersRef.current.forEach(timer => clearTimeout(timer)); retryTimersRef.current.clear(); cacheRef.current.clear(); previousDataRef.current.clear(); retryCountRef.current.clear(); }; }, []); // 폴링 시작 useEffect(() => { if (deviceIds.length > 0) { console.log('🔄 폴링 모드 시작 - 디바이스:', deviceIds); console.log('📡 API URL:', process.env.REACT_APP_API_URL || 'http://sensor.geumdo.net'); setConnectionStatus('connecting'); startPolling(); } return () => { if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } }; }, [deviceIds]); // 캐시에서 데이터 업데이트 const updateDataFromCache = useCallback(() => { const now = Date.now(); const validData: SensorData[] = []; cacheRef.current.forEach((item, deviceId) => { if (now - item.timestamp <= CACHE_DURATION) { validData.push(item.data); } }); if (mountedRef.current) { setData(validData); setLoading(false); console.log('✅ 캐시에서 데이터 업데이트 완료 - 유효한 데이터:', validData.length, '개'); // 연결 상태 업데이트 if (validData.length > 0) { setConnectionStatus('connected'); setError(null); } else { setConnectionStatus('disconnected'); } } }, []); // 폴링 시작 const startPolling = useCallback(() => { if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); } console.log('🔄 폴링 타이머 시작 - 간격:', POLLING_INTERVAL / 1000, '초'); console.log('📊 대상 디바이스 수:', deviceIds.length); // 즉시 첫 번째 데이터 가져오기 fetchDataFromAPI(); // 주기적 폴링 설정 pollingTimerRef.current = setInterval(() => { console.log('⏰ 폴링 타이머 실행 - 새로운 데이터 요청'); console.log('🕐 현재 시간:', new Date().toISOString()); fetchDataFromAPI(); }, POLLING_INTERVAL); }, [deviceIds]); // 재시도 로직 const scheduleRetry = useCallback((deviceId: string) => { const currentRetryCount = retryCountRef.current.get(deviceId) || 0; const newRetryCount = currentRetryCount + 1; if (newRetryCount <= maxRetries) { console.log(`🔄 디바이스 ${deviceId} ${RETRY_DELAY}ms 후 재시도 예정 (${newRetryCount}/${maxRetries})`); setConnectionStatus('retrying'); const retryTimer = setTimeout(() => { if (mountedRef.current) { console.log(`🔄 디바이스 ${deviceId} 재시도 실행 (${newRetryCount}/${maxRetries})`); fetchSingleDeviceData(deviceId); } }, RETRY_DELAY); retryTimersRef.current.set(deviceId, retryTimer); } else { console.error(`🚨 디바이스 ${deviceId} 최대 재시도 횟수 초과. 다음 폴링까지 대기합니다.`); setConnectionStatus('error'); setError(`디바이스 ${deviceId} 연결 실패 (최대 재시도 횟수 초과)`); } }, []); // 단일 디바이스 데이터 가져오기 const fetchSingleDeviceData = useCallback(async (deviceId: string) => { try { console.log(`📡 디바이스 ${deviceId} 데이터 요청 시작`); const startTime = Date.now(); const sensorData = await getLatestData(deviceId); const responseTime = Date.now() - startTime; if (sensorData) { console.log(`✅ 디바이스 ${deviceId} 데이터 수신 성공 (응답시간: ${responseTime}ms)`); // 재시도 카운트 리셋 retryCountRef.current.set(deviceId, 0); // 이전 데이터와 비교하여 변경사항 확인 const previousData = previousDataRef.current.get(deviceId); const hasChanged = !previousData || previousData.temperature !== sensorData.temperature || previousData.humidity !== sensorData.humidity || previousData.pm10 !== sensorData.pm10 || previousData.pm25 !== sensorData.pm25 || previousData.pressure !== sensorData.pressure || previousData.illumination !== sensorData.illumination || previousData.tvoc !== sensorData.tvoc || previousData.co2 !== sensorData.co2 || previousData.o2 !== sensorData.o2 || previousData.co !== sensorData.co; if (hasChanged) { console.log('🔄 데이터 변경 감지:', deviceId); console.log('📊 변경된 센서 데이터:', { device_id: sensorData.device_id, temperature: sensorData.temperature, humidity: sensorData.humidity, pm10: sensorData.pm10, pm25: sensorData.pm25, pressure: sensorData.pressure, illumination: sensorData.illumination, tvoc: sensorData.tvoc, co2: sensorData.co2, o2: sensorData.o2, co: sensorData.co, recorded_time: sensorData.recorded_time }); } else { console.log('✅ 데이터 변경 없음:', deviceId); } // 현재 데이터를 이전 데이터로 저장 previousDataRef.current.set(deviceId, { ...sensorData }); cacheRef.current.set(deviceId, { data: sensorData, timestamp: Date.now() }); return sensorData; } else { console.warn(`⚠️ 디바이스 ${deviceId}에서 데이터를 받지 못했습니다.`); return null; } } catch (err) { console.error(`❌ 디바이스 ${deviceId} 데이터 가져오기 실패:`, err); scheduleRetry(deviceId); return null; } }, [scheduleRetry]); // API에서 데이터 가져오기 const fetchDataFromAPI = useCallback(async () => { if (deviceIds.length === 0) { console.log('⚠️ 폴링 대상 디바이스가 없습니다.'); return; } try { console.log('📡 API 데이터 요청 시작 - 디바이스:', deviceIds); console.log('🔧 요청 파라미터:', { deviceIds, timestamp: new Date().toISOString() }); const promises = deviceIds.map(deviceId => fetchSingleDeviceData(deviceId)); const results = await Promise.all(promises); const successCount = results.filter(r => r !== null).length; const failureCount = results.length - successCount; console.log(`📊 API 요청 결과 - 성공: ${successCount}개, 실패: ${failureCount}개`); if (mountedRef.current) { updateDataFromCache(); setLastUpdate(new Date().toISOString()); // 연결 상태 업데이트 if (successCount > 0) { setConnectionStatus('connected'); setError(null); } else if (failureCount > 0) { setConnectionStatus('error'); } } } catch (err) { if (mountedRef.current) { const errorMessage = err instanceof Error ? err.message : '알 수 없는 오류'; setError(`데이터를 가져오는 중 오류가 발생했습니다: ${errorMessage}`); setConnectionStatus('error'); console.error('❌ API 데이터 가져오기 실패:', err); console.error('🔍 에러 상세 정보:', { message: err instanceof Error ? err.message : 'Unknown error', stack: err instanceof Error ? err.stack : 'No stack trace', timestamp: new Date().toISOString() }); } } }, [deviceIds, updateDataFromCache, fetchSingleDeviceData]); // 수동 새로고침 const refetch = useCallback(async (): Promise => { if (mountedRef.current) { console.log('🔄 수동 새로고침 시작'); setLoading(true); setError(null); setConnectionStatus('connecting'); // 캐시 무효화 cacheRef.current.clear(); previousDataRef.current.clear(); retryCountRef.current.clear(); // 재시도 타이머들 정리 retryTimersRef.current.forEach(timer => clearTimeout(timer)); retryTimersRef.current.clear(); console.log('🗑️ 캐시 및 재시도 카운트 초기화 완료'); // API를 통한 새로고침 await fetchDataFromAPI(); console.log('✅ 수동 새로고침 완료'); } }, [fetchDataFromAPI]); // 디바이스 ID 변경 시 처리 useEffect(() => { if (deviceIds.length > 0) { console.log('🔄 디바이스 목록 변경:', deviceIds); // 캐시에서 더 이상 사용하지 않는 디바이스 데이터 제거 const currentDeviceIds = new Set(deviceIds); const cachedDeviceIds = Array.from(cacheRef.current.keys()); cachedDeviceIds.forEach(deviceId => { if (!currentDeviceIds.has(deviceId)) { console.log('🗑️ 디바이스 제거:', deviceId); cacheRef.current.delete(deviceId); previousDataRef.current.delete(deviceId); retryCountRef.current.delete(deviceId); // 재시도 타이머도 정리 const retryTimer = retryTimersRef.current.get(deviceId); if (retryTimer) { clearTimeout(retryTimer); retryTimersRef.current.delete(deviceId); } } }); // 데이터 업데이트 updateDataFromCache(); } }, [deviceIds, updateDataFromCache]); // 폴링 타이머 정리 useEffect(() => { return () => { if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current); pollingTimerRef.current = null; } }; }, []); return { data, isConnected: connectionStatus === 'connected', connectionStatus, dataSource, lastUpdate, error, loading, refetch, retryCount: retryCountRef.current }; };