import axios, { AxiosError, AxiosResponse } from 'axios'; import { isSensorData, isDevice, isHistoryResponse, isApiResponse } from '../utils/typeGuards'; const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://sensor.geumdo.net'; const api = axios.create({ baseURL: API_BASE_URL, timeout: 30000, // 30초로 증가 }); // API 설정 로깅 console.log('🔧 API Configuration:', { baseURL: API_BASE_URL, timeout: 30000, env: process.env.REACT_APP_API_URL }); // API 응답 구조 검증 함수 - 강화된 검증 const validateApiResponse = (response: any, endpoint: string): boolean => { if (!response || typeof response !== 'object') { console.error(`Invalid response type for ${endpoint}:`, typeof response); return false; } // 엔드포인트별 검증 로직 if (endpoint.includes('/latest')) { return response.hasOwnProperty('data') && response.data !== null; } if (endpoint.includes('/devices') && !endpoint.includes('/history')) { return response.hasOwnProperty('devices') && Array.isArray(response.devices); } if (endpoint.includes('/history')) { const isValid = response.hasOwnProperty('success') && response.hasOwnProperty('data') && response.hasOwnProperty('total') && Array.isArray(response.data); if (!isValid) { console.error(`Invalid history response structure for ${endpoint}:`, { hasSuccess: response.hasOwnProperty('success'), hasData: response.hasOwnProperty('data'), hasTotal: response.hasOwnProperty('total'), dataIsArray: Array.isArray(response.data), response: response }); } return isValid; } return true; }; // 센서 데이터 필수 필드 검증 const validateSensorDataFields = (data: any): boolean => { const requiredFields = [ 'id', 'device_id', 'node_id', 'temperature', 'humidity', 'longitude', 'latitude', 'recorded_time', 'received_time' ]; return requiredFields.every(field => { const value = data[field]; if (field === 'device_id' || field === 'recorded_time' || field === 'received_time') { return typeof value === 'string' && value.length > 0; } if (field === 'id' || field === 'node_id') { return typeof value === 'number' && Number.isInteger(value) && value >= 0; } if (field === 'temperature' || field === 'humidity' || field === 'longitude' || field === 'latitude') { return typeof value === 'number' && !isNaN(value) && isFinite(value); } return true; }); }; // 디바이스 데이터 필수 필드 검증 const validateDeviceFields = (data: any): boolean => { const requiredFields = [ 'id', 'device_id', 'name', 'description', 'status', 'last_seen', 'created_at', 'updated_at' ]; return requiredFields.every(field => { const value = data[field]; if (field === 'id') { return typeof value === 'number' && Number.isInteger(value) && value >= 0; } if (['device_id', 'name', 'status'].includes(field)) { return typeof value === 'string' && value.length > 0; } if (field === 'description') { return typeof value === 'string'; // description은 빈 문자열도 허용 } if (['last_seen', 'created_at', 'updated_at'].includes(field)) { return typeof value === 'string' && value.length > 0 && !isNaN(Date.parse(value)); } return true; }); }; // 데이터 타입 일관성 검사 const validateDataConsistency = (data: any, type: 'sensor' | 'device' | 'history'): boolean => { try { switch (type) { case 'sensor': return isSensorData(data) && validateSensorDataFields(data); case 'device': return isDevice(data) && validateDeviceFields(data); case 'history': return isHistoryResponse(data); default: return false; } } catch (error) { console.error(`Data consistency validation failed for type ${type}:`, error); return false; } }; // 전역 에러 처리 인터셉터 api.interceptors.response.use( (response: AxiosResponse) => { console.log('API Response:', response.config.url, response.data); // 응답 구조 검증 if (!validateApiResponse(response.data, response.config.url || '')) { throw new Error('Invalid API response structure'); } return response; }, (error: AxiosError) => { console.error('Axios Error Details:', { message: error.message, code: error.code, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, url: error.config?.url, method: error.config?.method, headers: error.config?.headers }); // 네트워크 에러 처리 if (!error.response) { console.error('Network error:', error.message); throw new Error('네트워크 연결에 실패했습니다. 인터넷 연결을 확인해주세요.'); } // HTTP 상태 코드별 에러 처리 const status = error.response.status; let errorMessage = '알 수 없는 오류가 발생했습니다.'; switch (status) { case 400: errorMessage = '잘못된 요청입니다.'; break; case 401: errorMessage = '인증이 필요합니다.'; break; case 403: errorMessage = '접근 권한이 없습니다.'; break; case 404: errorMessage = '요청한 리소스를 찾을 수 없습니다.'; break; case 500: errorMessage = '서버 내부 오류가 발생했습니다.'; break; case 503: errorMessage = '서비스가 일시적으로 사용할 수 없습니다.'; break; default: errorMessage = `서버 오류 (${status})가 발생했습니다.`; } console.error(`API Error ${status}:`, error.response.data); throw new Error(errorMessage); } ); export interface SensorData { id: number; device_id: string; node_id: number; temperature: number; humidity: number; longitude: number; latitude: number; // 추가 센서 데이터 필드 float_value?: number; signed_int32_value?: number; unsigned_int32_value?: number; raw_tem?: number; raw_hum?: number; // 환경 센서 데이터 pm10?: number; pm25?: number; pressure?: number; illumination?: number; tvoc?: number; co2?: number; o2?: number; co?: number; recorded_time: string; received_time: string; } export interface Device { id: number; device_id: string; name: string; description: string; status: string; last_seen: string; created_at: string; updated_at: string; } export interface HistoryResponse { success: boolean; data: SensorData[]; total: number; } // 센서 데이터 수신 export const sendSensorData = async (data: any): Promise => { console.log('📤 센서 데이터 전송 시작:', data); try { const response = await api.post('/api/sensor-data', data); console.log('✅ 센서 데이터 전송 성공:', response.data); return response.data; } catch (error) { console.error('❌ 센서 데이터 전송 실패:', error); throw error; } }; // 최신 센서 데이터 조회 - 타입 안전성 및 데이터 검증 강화 export const getLatestData = async (deviceId: string): Promise => { const startTime = Date.now(); const url = `/api/devices/${deviceId}/latest`; console.log(`📡 최신 데이터 요청 시작 - 디바이스: ${deviceId}`); console.log(`🔗 요청 URL: ${process.env.REACT_APP_API_URL || 'http://sensor.geumdo.net'}${url}`); try { const response = await api.get(url); const responseTime = Date.now() - startTime; console.log(`✅ 최신 데이터 요청 성공 - 디바이스: ${deviceId} (응답시간: ${responseTime}ms)`); console.log(`📊 응답 데이터 크기: ${JSON.stringify(response.data).length} bytes`); // 응답 데이터 검증 if (!response.data || !response.data.data) { console.warn(`⚠️ 디바이스 ${deviceId}에 대한 데이터가 없습니다.`); console.log('🔍 응답 구조:', response.data); return null; } const sensorData = response.data.data; // 필수 필드 검증 if (!sensorData.device_id || typeof sensorData.device_id !== 'string') { console.error(`❌ 디바이스 ${deviceId} 응답에서 잘못된 device_id:`, sensorData); return null; } // 데이터 타입 일관성 검사 if (!validateDataConsistency(sensorData, 'sensor')) { console.error(`❌ 디바이스 ${deviceId} 데이터 일관성 검증 실패:`, sensorData); return null; } console.log(`✅ 디바이스 ${deviceId} 데이터 검증 완료:`, { device_id: sensorData.device_id, temperature: sensorData.temperature, humidity: sensorData.humidity, recorded_time: sensorData.recorded_time }); return sensorData; } catch (error) { const responseTime = Date.now() - startTime; console.error(`❌ 디바이스 ${deviceId} 최신 데이터 요청 실패 (응답시간: ${responseTime}ms):`, error); if (error instanceof Error) { console.error('🔍 에러 상세 정보:', { message: error.message, name: error.name, stack: error.stack }); } return null; } }; // 히스토리 데이터 조회 - 데이터 검증 강화 export const getHistory = async ( deviceId: string, limit: number = 100, offset: number = 0, startTime?: string, endTime?: string ): Promise => { const startTimeRequest = Date.now(); const params: any = { limit, offset }; if (startTime) params.start_time = startTime; if (endTime) params.end_time = endTime; console.log(`📡 히스토리 데이터 요청 시작 - 디바이스: ${deviceId}`); console.log(`🔧 요청 파라미터:`, params); try { const response = await api.get(`/api/devices/${deviceId}/history`, { params }); const responseTime = Date.now() - startTimeRequest; console.log(`✅ 히스토리 데이터 요청 성공 - 디바이스: ${deviceId} (응답시간: ${responseTime}ms)`); console.log(`📊 수신된 데이터 수: ${response.data.data?.length || 0}개`); // 응답 데이터 검증 if (!validateDataConsistency(response.data, 'history')) { console.error(`❌ 디바이스 ${deviceId} 히스토리 데이터 검증 실패:`, response.data); throw new Error('Invalid history data format'); } console.log(`✅ 디바이스 ${deviceId} 히스토리 데이터 검증 완료`); return response.data; } catch (error) { const responseTime = Date.now() - startTimeRequest; console.error(`❌ 디바이스 ${deviceId} 히스토리 데이터 요청 실패 (응답시간: ${responseTime}ms):`, error); throw error; } }; // 디바이스 목록 조회 - 데이터 검증 강화 export const getDevices = async (): Promise => { const startTime = Date.now(); try { const fullUrl = `${api.defaults.baseURL}/api/devices`; console.log('🔍 디바이스 목록 조회 시작'); console.log('🔗 요청 URL:', fullUrl); console.log('🔧 API 설정:', { baseURL: api.defaults.baseURL, timeout: api.defaults.timeout, headers: api.defaults.headers }); const response = await api.get('/api/devices'); const responseTime = Date.now() - startTime; console.log('✅ 디바이스 목록 조회 성공 (응답시간:', responseTime, 'ms)'); console.log('📊 응답 데이터:', response.data); // 응답 데이터 검증 if (!response.data.devices || !Array.isArray(response.data.devices)) { console.error('❌ 잘못된 디바이스 응답 구조:', response.data); throw new Error('Invalid devices data format'); } console.log(`📊 총 디바이스 수: ${response.data.devices.length}개`); // 각 디바이스 데이터 검증 const validDevices = response.data.devices.filter((device: any) => { if (!validateDataConsistency(device, 'device')) { console.warn('⚠️ 잘못된 디바이스 데이터 발견:', device); return false; } return true; }); if (validDevices.length !== response.data.devices.length) { console.warn(`⚠️ ${response.data.devices.length - validDevices.length}개의 잘못된 디바이스 데이터가 필터링되었습니다.`); } console.log(`✅ 유효한 디바이스 수: ${validDevices.length}개`); return validDevices; } catch (error) { const responseTime = Date.now() - startTime; console.error('❌ 디바이스 목록 조회 실패 (응답시간:', responseTime, 'ms):', error); throw error; } }; // 헬스체크 export const healthCheck = async (): Promise => { const startTime = Date.now(); console.log('🏥 헬스체크 시작'); try { const response = await api.get('/api/health'); const responseTime = Date.now() - startTime; console.log('✅ 헬스체크 성공 (응답시간:', responseTime, 'ms):', response.data); return response.data; } catch (error) { const responseTime = Date.now() - startTime; console.error('❌ 헬스체크 실패 (응답시간:', responseTime, 'ms):', error); throw error; } }; export default api;