/**
* 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;