/** * RAG 검색 챗봇 장치 관리 JavaScript */ // DOM 요소 const deviceTab = document.getElementById('deviceTab'); const deviceSection = document.getElementById('deviceSection'); const checkDeviceStatusButton = document.getElementById('checkDeviceStatusButton'); const deviceStatusLoading = document.getElementById('deviceStatusLoading'); const deviceStatusResult = document.getElementById('deviceStatusResult'); const statusIcon = document.getElementById('statusIcon'); const statusText = document.getElementById('statusText'); const refreshDevicesButton = document.getElementById('refreshDevicesButton'); const deviceList = document.getElementById('deviceList'); const devicesLoading = document.getElementById('devicesLoading'); const noDevicesMessage = document.getElementById('noDevicesMessage'); const loadProgramsButton = document.getElementById('loadProgramsButton'); const programsLoading = document.getElementById('programsLoading'); const programsList = document.getElementById('programsList'); const noProgramsMessage = document.getElementById('noProgramsMessage'); // 장치 서버 URL 설정 // 직접 접근 설정: 빈 문자열이면 현재 호스트의 5050 포트를 사용 let DEVICE_SERVER_URL = ''; // 서버에서 전달된 URL이 있으면 초기화 시 설정됨 /** * 장치 서버 API 경로 생성 함수 * @param {string} endpoint - API 엔드포인트 경로 * @returns {string} - 완전한 API URL */ function getDeviceApiUrl(endpoint) { // 직접 접근 모드인 경우 (별도 서버에 직접 요청) if (DEVICE_SERVER_URL) { return `${DEVICE_SERVER_URL}${endpoint}`; } // 프록시 모드인 경우 (내부 API 경로 사용) return endpoint; } /** * 장치 서버 설정 초기화 (페이지 로드 시 호출) */ function initDeviceServerSettings() { console.log("장치 서버 설정 초기화 시작"); // 서버 URL 설정 (웹 서버로부터 설정 불러오기) fetch('/api/device/settings') .then(response => { if (!response.ok) { throw new Error('설정을 가져올 수 없습니다'); } return response.json(); }) .then(data => { if (data.server_url) { console.log(`장치 서버 URL 설정됨: ${data.server_url}`); DEVICE_SERVER_URL = data.server_url; } else { // 설정 없음 - 자동 생성 (현재 호스트 + 포트 5050) const currentHost = window.location.hostname; const protocol = window.location.protocol; DEVICE_SERVER_URL = `${protocol}//${currentHost}:5051`; console.log(`장치 서버 URL 자동 설정: ${DEVICE_SERVER_URL}`); } }) .catch(error => { console.error('장치 서버 설정 초기화 오류:', error); // 기본값으로 설정 (현재 호스트 + 포트 5051) const currentHost = window.location.hostname; const protocol = window.location.protocol; DEVICE_SERVER_URL = `${protocol}//${currentHost}:5051`; console.log(`장치 서버 URL 기본값 설정: ${DEVICE_SERVER_URL}`); }); } // 페이지 로드 시 초기화 document.addEventListener('DOMContentLoaded', () => { console.log("장치 관리 모듈 초기화"); // 장치 서버 설정 초기화 initDeviceServerSettings(); // 탭 전환 이벤트 리스너는 이미 app.js에서 등록되어 있으므로 여기서는 등록하지 않음 // 대신 전역 함수가 올바르게 설정되어 있는지 확인 if (typeof window.switchTab !== 'function') { console.log("window.switchTab 함수가 정의되지 않았습니다. 내부 구현을 사용합니다."); } else { console.log("window.switchTab 함수 사용 가능합니다."); } // 장치 상태 확인 버튼 이벤트 리스너 checkDeviceStatusButton.addEventListener('click', () => { console.log("장치 상태 확인 버튼 클릭"); checkDeviceStatus(); }); // 장치 목록 새로고침 버튼 이벤트 리스너 refreshDevicesButton.addEventListener('click', () => { console.log("장치 목록 새로고침 버튼 클릭"); loadDevices(); }); // 프로그램 목록 로드 버튼 이벤트 리스너 loadProgramsButton.addEventListener('click', () => { console.log("프로그램 목록 로드 버튼 클릭"); loadPrograms(); }); }); /** * 에러 처리 헬퍼 함수 * @param {Error} error - 발생한 오류 * @returns {string} - 사용자에게 표시할 오류 메시지 */ function handleError(error) { console.error("오류 발생:", error); if (error.name === 'AbortError') { return '요청 시간이 초과되었습니다. 서버가 응답하지 않습니다.'; } if (error.message && (error.message.includes('NetworkError') || error.message.includes('Failed to fetch'))) { return '네트워크 오류가 발생했습니다. 서버에 연결할 수 없습니다.'; } return `오류가 발생했습니다: ${error.message || '알 수 없는 오류'}`; } /** * HTML 이스케이프 함수 (XSS 방지) * @param {string} unsafe - 이스케이프 전 문자열 * @returns {string} - 이스케이프 후 문자열 */ function escapeHtml(unsafe) { if (typeof unsafe !== 'string') return unsafe; return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** * 장치 관리 서버 상태 확인 함수 - 전역 함수로 export */ async function checkDeviceStatus() { console.log("장치 상태 확인 중..."); // UI 업데이트 deviceStatusResult.classList.add('hidden'); deviceStatusLoading.classList.remove('hidden'); try { // 타임아웃 설정을 위한 컨트롤러 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5초 타임아웃 // API 요청 - 수정된 URL 구성 사용 const response = await fetch(getDeviceApiUrl('/api/status'), { signal: controller.signal }); clearTimeout(timeoutId); // 타임아웃 해제 // 응답 처리 if (!response.ok) { throw new Error(`HTTP 오류: ${response.status}`); } const data = await response.json(); console.log("장치 상태 응답:", data); // UI 업데이트 deviceStatusLoading.classList.add('hidden'); deviceStatusResult.classList.remove('hidden'); if (data.status === "online") { // 온라인 상태인 경우 statusIcon.innerHTML = ''; statusText.textContent = `서버 상태: ${data.status || '정상'}`; // 자동으로 장치 목록 로드 loadDevices(); } else { // 오프라인 또는 오류 상태인 경우 statusIcon.innerHTML = ''; statusText.textContent = `서버 오류: ${data.error || '알 수 없는 오류'}`; } } catch (error) { console.error("장치 상태 확인 오류:", error); // UI 업데이트 deviceStatusLoading.classList.add('hidden'); deviceStatusResult.classList.remove('hidden'); statusIcon.innerHTML = ''; statusText.textContent = handleError(error); } } /** * 장치 목록 로드 함수 */ async function loadDevices() { console.log("장치 목록 로드 중..."); // UI 업데이트 deviceList.innerHTML = ''; noDevicesMessage.classList.add('hidden'); devicesLoading.classList.remove('hidden'); try { // 타임아웃 설정을 위한 컨트롤러 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5초 타임아웃 // API 요청 - 수정된 URL 구성 사용 const response = await fetch(getDeviceApiUrl('/api/devices'), { signal: controller.signal }); clearTimeout(timeoutId); // 타임아웃 해제 // 응답 처리 if (!response.ok) { throw new Error(`HTTP 오류: ${response.status}`); } const data = await response.json(); console.log("장치 목록 응답:", data); // UI 업데이트 devicesLoading.classList.add('hidden'); if (data.devices && data.devices.length > 0) { // 장치 목록 표시 data.devices.forEach(device => { const deviceElement = createDeviceItem(device); deviceList.appendChild(deviceElement); }); } else { // 장치 없음 메시지 표시 noDevicesMessage.classList.remove('hidden'); } } catch (error) { console.error("장치 목록 로드 오류:", error); // UI 업데이트 devicesLoading.classList.add('hidden'); noDevicesMessage.classList.remove('hidden'); noDevicesMessage.textContent = handleError(error); } } /** * 장치 아이템 생성 함수 * @param {Object} device - 장치 정보 객체 * @returns {HTMLElement} - 장치 아이템 DOM 요소 */ function createDeviceItem(device) { const deviceItem = document.createElement('div'); deviceItem.classList.add('device-item'); // 상태에 따른 클래스 추가 if (device.status === 'online' || device.status === '온라인') { deviceItem.classList.add('online'); } else if (device.status === 'offline' || device.status === '오프라인') { deviceItem.classList.add('offline'); } else if (device.status === 'warning' || device.status === '경고') { deviceItem.classList.add('warning'); } // 장치 헤더 (이름 및 상태) const deviceHeader = document.createElement('div'); deviceHeader.classList.add('device-item-header'); const deviceName = document.createElement('div'); deviceName.classList.add('device-name'); deviceName.textContent = device.name || '알 수 없는 장치'; const deviceStatusBadge = document.createElement('div'); deviceStatusBadge.classList.add('device-status-badge'); // 상태에 따른 배지 설정 if (device.status === 'online' || device.status === '온라인') { deviceStatusBadge.classList.add('online'); deviceStatusBadge.textContent = '온라인'; } else if (device.status === 'offline' || device.status === '오프라인') { deviceStatusBadge.classList.add('offline'); deviceStatusBadge.textContent = '오프라인'; } else if (device.status === 'warning' || device.status === '경고') { deviceStatusBadge.classList.add('warning'); deviceStatusBadge.textContent = '경고'; } else { deviceStatusBadge.textContent = device.status || '알 수 없음'; } deviceHeader.appendChild(deviceName); deviceHeader.appendChild(deviceStatusBadge); // 장치 정보 const deviceInfo = document.createElement('div'); deviceInfo.classList.add('device-info'); deviceInfo.textContent = `유형: ${device.type || '알 수 없음'}`; // 장치 세부 정보 const deviceDetails = document.createElement('div'); deviceDetails.classList.add('device-details'); // 추가 정보가 있는 경우 표시 if (device.ip) { deviceDetails.textContent += `IP: ${device.ip}`; } if (device.mac) { deviceDetails.textContent += (deviceDetails.textContent ? ', ' : '') + `MAC: ${device.mac}`; } if (device.lastSeen) { deviceDetails.textContent += (deviceDetails.textContent ? ', ' : '') + `마지막 활동: ${device.lastSeen}`; } // 아이템에 요소 추가 deviceItem.appendChild(deviceHeader); deviceItem.appendChild(deviceInfo); if (deviceDetails.textContent) { deviceItem.appendChild(deviceDetails); } return deviceItem; } /** * 프로그램 목록 로드 함수 */ async function loadPrograms() { console.log("프로그램 목록 로드 중..."); // UI 업데이트 programsList.innerHTML = ''; noProgramsMessage.classList.add('hidden'); programsLoading.classList.remove('hidden'); try { // 타임아웃 설정을 위한 컨트롤러 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5초 타임아웃 // API 요청 - 수정된 URL 구성 사용 const response = await fetch(getDeviceApiUrl('/api/programs'), { signal: controller.signal }); clearTimeout(timeoutId); // 타임아웃 해제 // 응답 처리 if (!response.ok) { throw new Error(`HTTP 오류: ${response.status}`); } const data = await response.json(); console.log("프로그램 목록 응답:", data); // UI 업데이트 programsLoading.classList.add('hidden'); if (data.programs && data.programs.length > 0) { // 프로그램 목록 표시 data.programs.forEach(program => { const programElement = createProgramItem(program); programsList.appendChild(programElement); }); } else { // 프로그램 없음 메시지 표시 noProgramsMessage.classList.remove('hidden'); } } catch (error) { console.error("프로그램 목록 로드 오류:", error); // UI 업데이트 programsLoading.classList.add('hidden'); noProgramsMessage.classList.remove('hidden'); noProgramsMessage.textContent = handleError(error); } } /** * 프로그램 아이템 생성 함수 * @param {Object} program - 프로그램 정보 객체 * @returns {HTMLElement} - 프로그램 아이템 DOM 요소 */ function createProgramItem(program) { const programItem = document.createElement('div'); programItem.classList.add('program-item'); // 프로그램 헤더 (이름) const programHeader = document.createElement('div'); programHeader.classList.add('program-item-header'); const programName = document.createElement('div'); programName.classList.add('program-name'); programName.textContent = program.name || '알 수 없는 프로그램'; programHeader.appendChild(programName); // 프로그램 설명 if (program.description) { const programDescription = document.createElement('div'); programDescription.classList.add('program-description'); programDescription.textContent = program.description; programItem.appendChild(programDescription); } // 실행 버튼 const executeButton = document.createElement('button'); executeButton.classList.add('execute-btn'); executeButton.textContent = '실행'; executeButton.addEventListener('click', () => { executeProgram(program.id, program.name); }); // 아이템에 요소 추가 programItem.appendChild(programHeader); programItem.appendChild(executeButton); return programItem; } /** * 프로그램 실행 함수 * @param {string} programId - 실행할 프로그램 ID * @param {string} programName - 프로그램 이름 (알림용) */ async function executeProgram(programId, programName) { if (!programId) return; console.log(`프로그램 실행 요청: ${programId} (${programName})`); // 실행 확인 if (!confirm(`'${programName}' 프로그램을 실행하시겠습니까?`)) { console.log('프로그램 실행 취소됨'); return; } try { // 로딩 알림 표시 showNotification(`'${programName}' 실행 중...`, 'info'); // API 요청 - 수정된 URL 구성 사용 const response = await fetch(getDeviceApiUrl(`/api/programs/${programId}/execute`), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) // 필요 시 추가 파라미터 전달 }); // 응답 처리 if (!response.ok) { throw new Error(`HTTP 오류: ${response.status}`); } const data = await response.json(); console.log(`프로그램 실행 응답:`, data); // 결과 처리 if (data.success) { showNotification(`'${programName}' 실행 성공: ${data.message}`, 'success'); } else { showNotification(`'${programName}' 실행 실패: ${data.message || '알 수 없는 오류'}`, 'error'); } } catch (error) { console.error(`프로그램 실행 오류 (${programId}):`, error); showNotification(`'${programName}' 실행 오류: ${handleError(error)}`, 'error'); } } /** * 알림 표시 함수 * @param {string} message - 알림 메시지 * @param {string} type - 알림 유형 ('success', 'error', 'warning') */ function showNotification(message, type = 'info') { // 기존 알림이 있으면 제거 const existingNotification = document.querySelector('.notification'); if (existingNotification) { existingNotification.remove(); } // 새 알림 생성 const notification = document.createElement('div'); notification.classList.add('notification', type); notification.textContent = message; // 알림 닫기 버튼 const closeButton = document.createElement('button'); closeButton.classList.add('notification-close'); closeButton.innerHTML = '×'; closeButton.addEventListener('click', () => { notification.remove(); }); notification.appendChild(closeButton); // 문서에 알림 추가 document.body.appendChild(notification); // 일정 시간 후 자동으로 사라지도록 설정 setTimeout(() => { if (document.body.contains(notification)) { notification.remove(); } }, 5000); // 5초 후 사라짐 } // checkDeviceStatus 함수를 전역으로 노출 window.checkDeviceStatus = checkDeviceStatus;