Skip to main content
Glama

OpenManager Vibe V4 MCP Server

by skyasu2
DataProcessor.js153 kB
/** * OpenManager AI - 데이터 처리기 * 서버 데이터 처리, 페이지네이션, 필터링 및 UI 업데이트 로직을 구현합니다. */ import { AIProcessor } from '../utils/AIProcessor.js'; import { CONFIG } from '../utils/config.js'; import { generateDummyData } from '../utils/DummyDataGenerator.js'; class DataProcessor { constructor() { try { // 기본 데이터 초기화 this.serverData = window.serverData || []; this.filteredData = []; this.currentFilter = 'all'; this.currentSort = 'status-critical'; // 기본 정렬을 상태 기준(심각 > 경고 > 정상)으로 변경 this.searchQuery = ''; this.currentPage = 1; this.itemsPerPage = 6; // 페이지당 서버 수 // AI 문제 페이지네이션 this.currentProblemPage = 1; this.problemsPerPage = 5; // 페이지당, 처음에 표시될 문제 수 // 초기화 로깅 console.log('DataProcessor 초기화 시작...'); // AIProcessor 인스턴스 초기화 개선 if (window.aiProcessor) { this.aiProcessor = window.aiProcessor; console.log('기존 AIProcessor 인스턴스를 사용합니다.'); } else if (typeof AIProcessor === 'function') { // AIProcessor 클래스가 존재하면 인스턴스 생성 try { window.aiProcessor = new AIProcessor(); this.aiProcessor = window.aiProcessor; console.log("AIProcessor 인스턴스를 새로 생성했습니다."); } catch (e) { console.error("AIProcessor 인스턴스 생성 중 오류 발생:", e); this.aiProcessor = null; } } else { console.warn("AIProcessor 클래스를 찾을 수 없습니다. 기본 상태 판단 로직을 사용합니다."); this.aiProcessor = null; } // 서버 상태 평가 임계값 - 통합 관리 this.thresholds = { critical: { cpu: 90, memory: 90, disk: 90 }, warning: { cpu: 70, memory: 70, disk: 70 } }; // UI 요소 참조 - 안전하게 참조를 시도합니다 this.findUIElements(); // 이벤트 리스너 등록 if (this.hasRequiredElements()) { this.registerEventListeners(); console.log('이벤트 리스너가 등록되었습니다.'); } else { console.warn('일부 UI 요소를 찾을 수 없어 이벤트 리스너 등록이 제한됩니다.'); } // 자동 데이터 업데이트 (1분 간격) setInterval(() => this.refreshData(), 60 * 1000); // 초기 데이터 로드 this.loadData(); // 서버 상태 판단 통합 로직을 전역 함수로 등록 window.getServerStatus = (server) => this.getServerStatus(server); console.log('DataProcessor 초기화 완료.'); } catch (error) { console.error('DataProcessor 초기화 중 심각한 오류 발생:', error); // 최소한의 기능 보장 this.serverData = window.serverData || []; this.filteredData = window.serverData || []; // 기본 필수 함수들은 최소한으로라도 구현 if (!this.showLoading) { this.showLoading = function() { const loadingIndicator = document.getElementById('loadingIndicator'); const serverGrid = document.getElementById('serverGrid'); if (loadingIndicator) loadingIndicator.style.display = 'block'; if (serverGrid) serverGrid.style.opacity = '0.3'; }; } if (!this.hideLoading) { this.hideLoading = function() { const loadingIndicator = document.getElementById('loadingIndicator'); const serverGrid = document.getElementById('serverGrid'); if (loadingIndicator) loadingIndicator.style.display = 'none'; if (serverGrid) serverGrid.style.opacity = '1'; }; } // 기본 데이터 처리 함수 if (!this.handleDataUpdate) { this.handleDataUpdate = function(data) { this.serverData = data || []; this.filteredData = data || []; this.renderServerGrid(); this.hideLoading(); }; } } } // UI 요소를 찾아 참조를 저장하는 메소드 findUIElements() { try { // UI 요소 참조 this.serverGrid = document.getElementById('serverGrid'); this.loadingIndicator = document.getElementById('loadingIndicator'); this.searchInput = document.getElementById('searchInput'); this.statusFilter = document.getElementById('statusFilter'); this.pageSize = document.getElementById('pageSize'); this.prevPageBtn = document.getElementById('prevPageBtn'); this.nextPageBtn = document.getElementById('nextPageBtn'); this.serverCount = document.getElementById('serverCount'); this.currentPageElement = document.getElementById('currentPage'); this.refreshButton = document.getElementById('refreshBtn'); this.modalElement = document.getElementById('serverDetailModal'); this.closeModalButton = document.querySelector('.btn-close[data-bs-dismiss="modal"]'); // 페이지네이션 컨테이너 요소 this.pagination = document.querySelector('.pagination-container'); // 모든 요소 참조가 필요하지 않는지 확인 로깅 const missingElements = []; if (!this.serverGrid) missingElements.push('serverGrid'); if (!this.loadingIndicator) missingElements.push('loadingIndicator'); if (!this.refreshButton) missingElements.push('refreshBtn'); if (missingElements.length > 0) { console.warn(`다음 UI 요소를 찾을 수 없습니다: ${missingElements.join(', ')}`); } } catch (error) { console.error('UI 요소 참조 중 오류:', error); } } // 필수 UI 요소가 있는지 확인 hasRequiredElements() { // 최소한 서버 그리드와 로딩 인디케이터는 필요 return this.serverGrid && this.loadingIndicator; } registerEventListeners() { // 필터링 이벤트 if (this.statusFilter) { this.statusFilter.addEventListener('change', () => { this.currentFilter = this.statusFilter.value; this.currentPage = 1; // 필터 변경 시 첫 페이지로 리셋 this.applyFiltersAndSort(); }); } // 검색 이벤트 if (this.searchInput) { this.searchInput.addEventListener('input', () => { this.searchQuery = this.searchInput.value.toLowerCase(); this.currentPage = 1; // 검색 시 첫 페이지로 리셋 this.applyFiltersAndSort(); }); } // 페이지 크기 이벤트 if (this.pageSize) { this.pageSize.addEventListener('change', () => { this.itemsPerPage = parseInt(this.pageSize.value); this.currentPage = 1; this.applyFiltersAndSort(); }); } // 이전 페이지 버튼 if (this.prevPageBtn) { this.prevPageBtn.addEventListener('click', () => { if (this.currentPage > 1) { this.currentPage--; this.applyFiltersAndSort(); } }); } // 다음 페이지 버튼 if (this.nextPageBtn) { this.nextPageBtn.addEventListener('click', () => { const totalPages = Math.ceil(this.filteredData.length / this.itemsPerPage); if (this.currentPage < totalPages) { this.currentPage++; this.applyFiltersAndSort(); } }); } // 새로고침 이벤트 if (this.refreshButton) { this.refreshButton.addEventListener('click', () => this.refreshData()); } // 모달 닫기 이벤트 if (this.closeModalButton) { this.closeModalButton.addEventListener('click', () => this.closeModal()); } // 서버 데이터 업데이트 이벤트 리스너 window.addEventListener('serverDataUpdated', (event) => { this.handleDataUpdate(event.detail); }); // 초기 AI 도우미 기능 설정 const aiQuerySubmitButton = document.getElementById('ai-query-submit'); if (aiQuerySubmitButton) { aiQuerySubmitButton.addEventListener('click', () => this.processAIQuery()); } // 장애 보고서 다운로드 이벤트 const downloadReportButton = document.getElementById('downloadAllReportsBtn'); if (downloadReportButton) { downloadReportButton.addEventListener('click', () => this.downloadErrorReport()); } // 전체 문제 보기 버튼 이벤트 처리 const viewAllProblemsBtn = document.getElementById('viewAllProblemsBtn'); if (viewAllProblemsBtn) { viewAllProblemsBtn.addEventListener('click', () => { // 현재 모든 문제를 모달로 표시하도록 수정 this.showAllProblems(); }); } // 프리셋 태그 버튼 이벤트 document.querySelectorAll('.preset-tag').forEach(tag => { tag.addEventListener('click', () => { // 이전에 선택된 태그의 active 클래스 제거 document.querySelectorAll('.preset-tag.active').forEach(el => { el.classList.remove('active'); }); // 클릭한 태그에 active 클래스 추가 tag.classList.add('active'); const presetText = tag.dataset.preset; const input = document.getElementById('queryInput'); if (input) { input.value = presetText; input.focus(); // 자동 실행 const submitButton = document.getElementById('ai-query-submit'); if (submitButton) { submitButton.click(); } } // 0.5초 후 active 클래스 제거하여 클릭 효과 리셋 setTimeout(() => { tag.classList.remove('active'); }, 500); }); }); // 전체 보고서 다운로드 버튼 이벤트 리스너 const downloadAllReportsBtn = document.getElementById('downloadAllReportsBtn'); if (downloadAllReportsBtn) { downloadAllReportsBtn.addEventListener('click', () => { this.downloadAllProblemsReport(); }); } // 쿼리 결과 닫기 버튼 이벤트 리스너 const closeQueryResultBtn = document.getElementById('closeQueryResult'); if (closeQueryResultBtn) { closeQueryResultBtn.addEventListener('click', () => { const queryResultElement = document.getElementById('queryResult'); const queryResultContent = document.getElementById('queryResultContent'); if (queryResultElement) { queryResultElement.style.display = 'none'; queryResultElement.classList.remove('active'); } if (queryResultContent) { queryResultContent.innerHTML = ''; } // 입력창도 비우기 const queryInput = document.getElementById('queryInput'); if (queryInput) { queryInput.value = ''; } console.log('AI 분석 결과가 닫혔습니다.'); }); } } loadData() { this.showLoading(); // 데이터가 이미 로드되어 있으면 사용 if (window.serverData && window.serverData.length > 0) { this.handleDataUpdate(window.serverData); return; } // 데이터 로드 시도 (최대 10초 대기) let attempts = 0; const maxAttempts = 20; // 10초 = 500ms * 20 const checkInterval = setInterval(() => { if (window.serverData && window.serverData.length > 0) { clearInterval(checkInterval); this.handleDataUpdate(window.serverData); return; } attempts++; if (attempts >= maxAttempts) { clearInterval(checkInterval); this.hideLoading(); console.error('서버 데이터를 로드하지 못했습니다. 백업 데이터 생성을 시도합니다.'); this.createBackupData(); } }, 500); } // 백업 데이터 생성 함수 createBackupData() { console.log('백업 데이터 생성 시도...'); try { // 더미 데이터 생성 함수 사용 window.serverData = generateDummyData(10); // 10개의 서버 데이터 생성 if (window.serverData && window.serverData.length > 0) { this.handleDataUpdate(window.serverData); return; } // 백업 생성 실패 시 기본 더미 데이터 생성 console.log('기본 더미 데이터 생성...'); const backupServers = []; // 기본 더미 서버 10대 생성 for (let i = 1; i <= 10; i++) { // 약 30%의 확률로 문제 있는 서버 생성 const hasProblem = Math.random() < 0.3; const problemLevel = hasProblem ? (Math.random() < 0.3 ? 'critical' : 'warning') : 'normal'; const cpuUsage = problemLevel === 'critical' ? Math.floor(Math.random() * 10) + 90 : // 90-99% problemLevel === 'warning' ? Math.floor(Math.random() * 20) + 70 : // 70-89% Math.floor(Math.random() * 50) + 10; // 10-59% const memoryUsage = problemLevel === 'critical' ? Math.floor(Math.random() * 10) + 90 : // 90-99% problemLevel === 'warning' ? Math.floor(Math.random() * 20) + 70 : // 70-89% Math.floor(Math.random() * 50) + 10; // 10-59% const diskUsage = problemLevel === 'critical' ? Math.floor(Math.random() * 10) + 90 : // 90-99% problemLevel === 'warning' ? Math.floor(Math.random() * 20) + 70 : // 70-89% Math.floor(Math.random() * 50) + 20; // 20-69% backupServers.push({ hostname: `server-${i}`, os: 'Linux', uptime: '3 days, 12:30:15', cpu_usage: cpuUsage, memory_usage_percent: memoryUsage, memory_total: '16GB', memory_used: '8GB', disk: [{ mount: '/', disk_total: '500GB', disk_used: '300GB', disk_usage_percent: diskUsage }], load_avg_1m: (Math.random() * 5).toFixed(2), load_avg_5m: (Math.random() * 4).toFixed(2), load_avg_15m: (Math.random() * 3).toFixed(2), process_count: Math.floor(Math.random() * 200) + 50, zombie_count: Math.floor(Math.random() * 3), timestamp: new Date().toISOString(), net: { interface: 'eth0', rx_bytes: Math.floor(Math.random() * 1000000), tx_bytes: Math.floor(Math.random() * 1000000), rx_errors: Math.floor(Math.random() * 10), tx_errors: Math.floor(Math.random() * 10) }, services: { 'nginx': problemLevel === 'critical' ? 'stopped' : 'running', 'mysql': Math.random() > 0.9 ? 'stopped' : 'running', 'redis': Math.random() > 0.9 ? 'stopped' : 'running' }, errors: problemLevel !== 'normal' ? [problemLevel === 'critical' ? 'Critical: 서버 응답 없음' : '경고: 높은 부하 감지'] : [] }); } // 전역 변수에 저장 및 이벤트 발생 window.serverData = backupServers; this.handleDataUpdate(backupServers); // 이벤트 발생시키기 const event = new CustomEvent('serverDataUpdated', { detail: backupServers }); window.dispatchEvent(event); } catch (error) { console.error('백업 데이터 생성 중 오류:', error); // 최종 실패 시 오류 메시지 표시 this.serverGrid.innerHTML = ` <div class="alert alert-danger"> <i class="bi bi-exclamation-triangle"></i> 서버 데이터를 로드하지 못했습니다. 새로고침 버튼을 클릭하여 다시 시도해 주세요. </div> `; } } handleDataUpdate(data) { this.serverData = [...data]; // 데이터 복사 this.hideLoading(); // 추가 데이터 처리 (AI 분석 등) if (this.aiProcessor) { this.aiProcessor.updateData(this.serverData); this.updateProblemsList(); // AI 자동 장애 보고서 업데이트 } // 필터 및 정렬 적용 this.applyFiltersAndSort(); this.updateGlobalStatusSummary(); // 서버 현황 요약 업데이트 추가 this.updatePresetTagClasses(); // 프리셋 태그 클래스 업데이트 } // 프리셋 태그 클래스 업데이트 (서버 상태에 따라 색상 동적 변경) updatePresetTagClasses() { // 1. CPU 과부하 프리셋 const cpuPresetTag = document.getElementById('cpu-preset'); if (cpuPresetTag) { const highCpuServers = this.serverData.filter(server => server.cpu_usage >= this.thresholds.warning.cpu); const criticalCpuServers = highCpuServers.filter(server => server.cpu_usage >= this.thresholds.critical.cpu); // 클래스 초기화 및 상태에 따라 설정 cpuPresetTag.classList.remove('tag-normal', 'tag-warning', 'tag-critical'); const badgeElement = cpuPresetTag.querySelector('.badge'); if (criticalCpuServers.length > 0) { cpuPresetTag.classList.add('tag-critical'); if (badgeElement) { badgeElement.className = 'badge bg-danger'; badgeElement.textContent = '심각'; } } else if (highCpuServers.length > 0) { cpuPresetTag.classList.add('tag-warning'); if (badgeElement) { badgeElement.className = 'badge bg-warning'; badgeElement.textContent = '경고'; } } else { cpuPresetTag.classList.add('tag-normal'); if (badgeElement) { badgeElement.className = 'badge bg-success'; badgeElement.textContent = '정상'; } } } // 2. 메모리 부족 프리셋 const memoryPresetTag = document.getElementById('memory-preset'); if (memoryPresetTag) { const highMemoryServers = this.serverData.filter(server => server.memory_usage_percent >= this.thresholds.warning.memory); const criticalMemoryServers = highMemoryServers.filter(server => server.memory_usage_percent >= this.thresholds.critical.memory); // 클래스 초기화 및 상태에 따라 설정 memoryPresetTag.classList.remove('tag-normal', 'tag-warning', 'tag-critical'); const badgeElement = memoryPresetTag.querySelector('.badge'); if (criticalMemoryServers.length > 0) { memoryPresetTag.classList.add('tag-critical'); if (badgeElement) { badgeElement.className = 'badge bg-danger'; badgeElement.textContent = '심각'; } } else if (highMemoryServers.length > 0) { memoryPresetTag.classList.add('tag-warning'); if (badgeElement) { badgeElement.className = 'badge bg-warning'; badgeElement.textContent = '경고'; } } else { memoryPresetTag.classList.add('tag-normal'); if (badgeElement) { badgeElement.className = 'badge bg-success'; badgeElement.textContent = '정상'; } } } // 3. 서비스 중단 프리셋 const servicePresetTag = document.getElementById('service-preset'); if (servicePresetTag) { const stoppedServiceServers = this.serverData.filter(server => server.services && Object.values(server.services).some(status => status === 'stopped') ); // 클래스 초기화 및 상태에 따라 설정 servicePresetTag.classList.remove('tag-normal', 'tag-warning', 'tag-critical'); const badgeElement = servicePresetTag.querySelector('.badge'); if (stoppedServiceServers.length > 0) { servicePresetTag.classList.add('tag-critical'); if (badgeElement) { badgeElement.className = 'badge bg-danger'; badgeElement.textContent = '심각'; } } else { servicePresetTag.classList.add('tag-normal'); if (badgeElement) { badgeElement.className = 'badge bg-success'; badgeElement.textContent = '정상'; } } } // 4. 디스크 부족 프리셋 const diskPresetTag = document.getElementById('disk-preset'); if (diskPresetTag) { const highDiskServers = this.serverData.filter(server => server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= this.thresholds.warning.disk ); const criticalDiskServers = highDiskServers.filter(server => server.disk[0].disk_usage_percent >= this.thresholds.critical.disk ); // 클래스 초기화 및 상태에 따라 설정 diskPresetTag.classList.remove('tag-normal', 'tag-warning', 'tag-critical'); const badgeElement = diskPresetTag.querySelector('.badge'); if (criticalDiskServers.length > 0) { diskPresetTag.classList.add('tag-critical'); if (badgeElement) { badgeElement.className = 'badge bg-danger'; badgeElement.textContent = '심각'; } } else if (highDiskServers.length > 0) { diskPresetTag.classList.add('tag-warning'); if (badgeElement) { badgeElement.className = 'badge bg-warning'; badgeElement.textContent = '경고'; } } else { diskPresetTag.classList.add('tag-normal'); if (badgeElement) { badgeElement.className = 'badge bg-success'; badgeElement.textContent = '정상'; } } } // 5. 정상 서버 프리셋 (항상 정상 클래스 유지, 정상 배지만 표시) const normalPresetTag = document.getElementById('normal-preset'); if (normalPresetTag) { const normalServers = this.serverData.filter(server => this.getServerStatus(server) === 'normal'); normalPresetTag.classList.remove('tag-warning', 'tag-critical'); normalPresetTag.classList.add('tag-normal'); // 배지 업데이트 const badgeElement = normalPresetTag.querySelector('.badge'); if (badgeElement) { badgeElement.className = 'badge bg-success'; badgeElement.textContent = `정상 (${normalServers.length})`; } } } refreshData() { this.showLoading(); // 새로고침 버튼 상태 업데이트 const refreshBtn = document.getElementById('refreshBtn'); if (refreshBtn) { refreshBtn.classList.add('loading'); refreshBtn.setAttribute('disabled', 'disabled'); const refreshContent = refreshBtn.querySelector('.refresh-content'); const loadingContent = refreshBtn.querySelector('.loading-content'); if (refreshContent) refreshContent.style.display = 'none'; if (loadingContent) loadingContent.style.display = 'inline-block'; } // 페이지 재로드가 아닌 데이터만 새로 생성 try { console.log('더미 데이터 다시 생성...'); window.serverData = generateDummyData(10); // 10개의 서버 데이터 생성 // 데이터 업데이트 이벤트 발생시키기 const event = new CustomEvent('serverDataUpdated', { detail: window.serverData }); window.dispatchEvent(event); // 3초 후 로딩 숨기기 (새로고침 효과) setTimeout(() => { this.hideLoading(); // 새로고침 버튼 상태 복원 this.resetRefreshButton(refreshBtn); }, 3000); return; } catch (e) { console.error('데이터 새로고침 중 오류:', e); // 오류 발생 시 원래 데이터로 UI 복원 this.hideLoading(); this.resetRefreshButton(refreshBtn); // 오류 메시지 표시 const errorAlert = document.createElement('div'); errorAlert.className = 'alert alert-danger alert-dismissible fade show'; errorAlert.innerHTML = ` <strong>오류 발생!</strong> 데이터를 새로고침하는 중 문제가 발생했습니다. <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> `; // 서버 그리드 위에 오류 메시지 삽입 const parent = this.serverGrid.parentNode; parent.insertBefore(errorAlert, this.serverGrid); // 5초 후 자동으로 오류 메시지 제거 setTimeout(() => { if (errorAlert.parentNode) { errorAlert.parentNode.removeChild(errorAlert); } }, 5000); return; } // 데이터가 업데이트되면 serverDataUpdated 이벤트로 처리됨 // 하지만 10초 내에 업데이트가 없으면 현재 데이터로 UI 다시 로드 setTimeout(() => { if (this.loadingIndicator.style.display !== 'none') { this.hideLoading(); this.applyFiltersAndSort(); // 새로고침 버튼 상태 복원 this.resetRefreshButton(refreshBtn); } }, 10000); } // 새로고침 버튼 상태 초기화 유틸리티 메소드 resetRefreshButton(refreshBtn) { if (refreshBtn) { refreshBtn.classList.remove('loading'); refreshBtn.removeAttribute('disabled'); const refreshContent = refreshBtn.querySelector('.refresh-content'); const loadingContent = refreshBtn.querySelector('.loading-content'); if (refreshContent) refreshContent.style.display = 'inline-block'; if (loadingContent) loadingContent.style.display = 'none'; } } showLoading() { this.loadingIndicator.style.display = 'block'; this.serverGrid.style.opacity = '0.3'; } hideLoading() { this.loadingIndicator.style.display = 'none'; this.serverGrid.style.opacity = '1'; } applyFiltersAndSort() { try { // 필터 적용 this.filteredData = this.serverData.filter(server => { // 검색어 필터 if (this.searchQuery && !server.hostname.toLowerCase().includes(this.searchQuery)) { return false; } // 상태 필터 if (this.currentFilter !== 'all') { const status = this.getServerStatus(server); return status === this.currentFilter; } return true; }); // 정렬 적용 this.sortData(); // UI 업데이트를 별도 메소드로 분리하여 안전하게 실행 this.updateUI(); } catch (error) { console.error('필터 및 정렬 적용 중 오류:', error); // 오류 발생 시 기본 처리 this.filteredData = [...this.serverData]; // 최소한의 UI 업데이트 시도 this.hideLoading(); this.safeUpdateServerGrid(); } } // 안전하게 UI 요소들을 업데이트하는 메소드 updateUI() { try { // 서버 그리드 업데이트 this.updateServerGrid(); } catch (e) { console.error('서버 그리드 업데이트 중 오류:', e); this.safeUpdateServerGrid(); } try { // 페이지네이션 업데이트 this.updateNumericPagination(); } catch (e) { console.error('페이지네이션 업데이트 중 오류:', e); } try { // 서버 카운트 업데이트 this.updateServerCount(); } catch (e) { console.error('서버 카운트 업데이트 중 오류:', e); } } // 최소한의 안전한 서버 그리드 업데이트 safeUpdateServerGrid() { if (!this.serverGrid) return; try { this.serverGrid.innerHTML = ''; if (this.filteredData.length === 0) { this.serverGrid.innerHTML = ` <div class="alert alert-info"> <i class="bi bi-info-circle"></i> 검색 조건에 맞는 서버가 없습니다. </div> `; return; } // 가장 기본적인 서버 목록만 표시 const startIndex = (this.currentPage - 1) * this.itemsPerPage; const endIndex = Math.min(startIndex + this.itemsPerPage, this.filteredData.length); for (let i = startIndex; i < endIndex; i++) { const server = this.filteredData[i]; if (!server) continue; const div = document.createElement('div'); div.className = 'server-card'; div.textContent = server.hostname || 'Unknown Server'; this.serverGrid.appendChild(div); } } catch (e) { console.error('안전한 서버 그리드 업데이트 중 오류:', e); this.serverGrid.innerHTML = '<div class="alert alert-danger">서버 데이터 표시 중 오류가 발생했습니다.</div>'; } } sortData() { switch(this.currentSort) { case 'name': this.filteredData.sort((a, b) => a.hostname.localeCompare(b.hostname)); break; case 'cpu-high': this.filteredData.sort((a, b) => b.cpu_usage - a.cpu_usage); break; case 'cpu-low': this.filteredData.sort((a, b) => a.cpu_usage - b.cpu_usage); break; case 'memory-high': this.filteredData.sort((a, b) => b.memory_usage_percent - a.memory_usage_percent); break; case 'memory-low': this.filteredData.sort((a, b) => a.memory_usage_percent - b.memory_usage_percent); break; case 'disk-high': this.filteredData.sort((a, b) => b.disk[0].disk_usage_percent - a.disk[0].disk_usage_percent); break; case 'disk-low': this.filteredData.sort((a, b) => a.disk[0].disk_usage_percent - b.disk[0].disk_usage_percent); break; case 'status-critical': this.filteredData.sort((a, b) => { const statusA = this.getServerStatus(a); const statusB = this.getServerStatus(b); const statusWeight = { 'critical': 3, 'warning': 2, 'normal': 1 }; return statusWeight[statusB] - statusWeight[statusA]; }); break; default: // 기본 정렬: 심각 > 경고 > 정상 순 this.filteredData.sort((a, b) => { const statusA = this.getServerStatus(a); const statusB = this.getServerStatus(b); const statusWeight = { 'critical': 3, 'warning': 2, 'normal': 1 }; return statusWeight[statusB] - statusWeight[statusA]; }); } } updateServerGrid() { if (!this.serverGrid) { console.error("서버 그리드 요소를 찾을 수 없습니다."); return; } this.serverGrid.innerHTML = ''; // 현재 페이지 사이즈 설정 사용 (더 이상 고정 값 사용 안 함) const startIndex = (this.currentPage - 1) * this.itemsPerPage; const endIndex = Math.min(startIndex + this.itemsPerPage, this.filteredData.length); if (this.filteredData.length === 0) { this.serverGrid.innerHTML = ` <div class="alert alert-info"> <i class="bi bi-info-circle"></i> 검색 조건에 맞는 서버가 없습니다. </div> `; return; } for (let i = startIndex; i < endIndex; i++) { const server = this.filteredData[i]; const serverCard = this.createServerCard(server); this.serverGrid.appendChild(serverCard); } // 페이지네이션 업데이트 this.updateNumericPagination(); } updateNumericPagination() { const paginationContainer = document.querySelector('.pagination-container'); if (!paginationContainer) return; paginationContainer.innerHTML = ''; const totalPages = Math.ceil(this.filteredData.length / this.itemsPerPage); // 이전 페이지 버튼 const prevBtn = document.createElement('button'); prevBtn.className = 'btn btn-sm btn-outline-secondary'; prevBtn.innerHTML = '<i class="fas fa-chevron-left"></i>'; prevBtn.disabled = this.currentPage === 1; prevBtn.addEventListener('click', () => { if (this.currentPage > 1) { this.currentPage--; this.updateServerGrid(); } }); paginationContainer.appendChild(prevBtn); // 페이지 번호 버튼 const maxVisiblePages = 5; let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2)); let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); // 표시할 페이지 버튼이 최대 개수보다 적을 경우 startPage 조정 if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) { startPage = Math.max(1, endPage - maxVisiblePages + 1); } // 첫 페이지로 이동 버튼 (필요 시) if (startPage > 1) { const firstPageBtn = document.createElement('button'); firstPageBtn.className = 'btn btn-sm btn-outline-secondary mx-1'; firstPageBtn.textContent = '1'; firstPageBtn.addEventListener('click', () => { this.currentPage = 1; this.updateServerGrid(); }); paginationContainer.appendChild(firstPageBtn); // 생략 표시 (첫 페이지 버튼과 현재 범위 사이) if (startPage > 2) { const ellipsis = document.createElement('span'); ellipsis.className = 'mx-1'; ellipsis.textContent = '...'; paginationContainer.appendChild(ellipsis); } } // 페이지 버튼 생성 for (let i = startPage; i <= endPage; i++) { const pageBtn = document.createElement('button'); pageBtn.className = `btn btn-sm mx-1 ${i === this.currentPage ? 'btn-primary' : 'btn-outline-secondary'}`; pageBtn.textContent = i.toString(); pageBtn.addEventListener('click', () => { this.currentPage = i; this.updateServerGrid(); }); paginationContainer.appendChild(pageBtn); } // 마지막 페이지로 이동 버튼 (필요 시) if (endPage < totalPages) { // 생략 표시 (현재 범위와 마지막 페이지 버튼 사이) if (endPage < totalPages - 1) { const ellipsis = document.createElement('span'); ellipsis.className = 'mx-1'; ellipsis.textContent = '...'; paginationContainer.appendChild(ellipsis); } const lastPageBtn = document.createElement('button'); lastPageBtn.className = 'btn btn-sm btn-outline-secondary mx-1'; lastPageBtn.textContent = totalPages.toString(); lastPageBtn.addEventListener('click', () => { this.currentPage = totalPages; this.updateServerGrid(); }); paginationContainer.appendChild(lastPageBtn); } // 다음 페이지 버튼 const nextBtn = document.createElement('button'); nextBtn.className = 'btn btn-sm btn-outline-secondary'; nextBtn.innerHTML = '<i class="fas fa-chevron-right"></i>'; nextBtn.disabled = this.currentPage === totalPages; nextBtn.addEventListener('click', () => { if (this.currentPage < totalPages) { this.currentPage++; this.updateServerGrid(); } }); paginationContainer.appendChild(nextBtn); // 현재 페이지 정보 업데이트 if (this.currentPageElement) { this.currentPageElement.style.display = 'none'; // 기존 페이지 표시 숨김 } } createServerCard(server) { if (!server) { console.error("서버 데이터가 없습니다."); return document.createElement('div'); // 빈 요소 반환 } const status = this.getServerStatus(server); // 안전하게 services 체크 const hasStoppedServices = server.services && Object.values(server.services).some(status => status === 'stopped'); // 안전하게 errors 체크 const hasErrors = server.errors && Array.isArray(server.errors) && server.errors.length > 0; // 서버 카드 생성 const serverCard = document.createElement('div'); serverCard.className = 'server-card status-' + status; serverCard.dataset.serverId = server.hostname || 'unknown'; // 툴팁 요소 추가 const tooltip = document.createElement('div'); tooltip.className = 'tooltip-wrapper'; tooltip.innerHTML = ` <div class="tooltip-content"> 클릭하여 ${server.hostname} 서버의 상세 정보를 확인하세요. </div> `; serverCard.appendChild(tooltip); // 카드 클릭 시 상세 정보 모달 표시 serverCard.addEventListener('click', () => this.showServerDetail(server)); // CPU, 메모리, 디스크 사용률을 안전하게 가져오기 const cpuUsage = server.cpu_usage || 0; const memoryUsage = server.memory_usage_percent || 0; const diskUsage = server.disk && server.disk[0] ? server.disk[0].disk_usage_percent || 0 : 0; const diskMount = server.disk && server.disk[0] ? server.disk[0].mount || '/' : '/'; // 리소스 상태 판별 const cpuStatus = this.getResourceStatus(cpuUsage, 'cpu'); const memoryStatus = this.getResourceStatus(memoryUsage, 'memory'); const diskStatus = this.getResourceStatus(diskUsage, 'disk'); // 상태별 색상 클래스 const getStatusColorClass = (status) => { switch(status) { case 'critical': return 'text-danger'; case 'warning': return 'text-warning'; default: return 'text-success'; } }; // 상태 아이콘 가져오기 const getStatusIcon = (status) => { switch(status) { case 'critical': return '<i class="fas fa-exclamation-circle me-1"></i>'; case 'warning': return '<i class="fas fa-exclamation-triangle me-1"></i>'; default: return '<i class="fas fa-check-circle me-1"></i>'; } }; // 카드 내용 구성 serverCard.innerHTML = ` <div class="card-interaction-hint"> <i class="fas fa-info-circle"></i> </div> <div class="server-header"> <div class="server-name">${server.hostname || 'Unknown Server'}</div> <div class="server-status status-${status}">${this.getStatusLabel(status)}</div> </div> <div class="server-details"> <div class="detail-item"> <div class="detail-label">CPU 사용량</div> <div class="detail-value ${getStatusColorClass(cpuStatus)}"> ${getStatusIcon(cpuStatus)}<strong>${cpuUsage}%</strong> </div> <div class="progress-bar-container"> <div class="progress-bar progress-${cpuStatus}" style="width: ${cpuUsage}%"></div> </div> </div> <div class="detail-item"> <div class="detail-label">메모리</div> <div class="detail-value ${getStatusColorClass(memoryStatus)}"> ${getStatusIcon(memoryStatus)}<strong>${typeof memoryUsage === 'number' ? memoryUsage.toFixed(1) : '0'}%</strong> </div> <div class="progress-bar-container"> <div class="progress-bar progress-${memoryStatus}" style="width: ${memoryUsage}%"></div> </div> </div> <div class="detail-item"> <div class="detail-label">디스크 (${diskMount})</div> <div class="detail-value ${getStatusColorClass(diskStatus)}"> ${getStatusIcon(diskStatus)}<strong>${typeof diskUsage === 'number' ? diskUsage.toFixed(1) : '0'}%</strong> </div> <div class="progress-bar-container"> <div class="progress-bar progress-${diskStatus}" style="width: ${diskUsage}%"></div> </div> </div> <div class="detail-item"> <div class="detail-label">로드 평균</div> <div class="detail-value">${server.load_avg_1m || '0'}</div> </div> </div> <div class="services-list"> ${server.services ? Object.entries(server.services).map(([name, status]) => ` <div class="service-badge service-${status}">${name} (${status})</div> `).join('') : '<div class="service-badge">서비스 정보 없음</div>'} </div> ${hasErrors ? ` <div class="error-messages"> <i class="bi bi-exclamation-triangle-fill"></i> ${server.errors.length}개의 오류 </div> ` : ''} `; return serverCard; } showServerDetail(server) { if (!server) { console.error("서버 데이터가 없습니다."); return; } try { // 부트스트랩 모달 요소 및 내용을 찾기 const modalElement = document.getElementById('serverDetailModal'); if (!modalElement) { console.error("모달 요소(serverDetailModal)를 찾을 수 없습니다."); return; } // 모달 헤더 내에서 제목 요소 찾기 const modalTitle = modalElement.querySelector('.modal-title'); if (!modalTitle) { console.error("모달 제목 요소를 찾을 수 없습니다."); return; } // 모달 내용을 표시할 요소 찾기 const modalBody = modalElement.querySelector('.modal-body'); if (!modalBody) { console.error("모달 내용 요소를 찾을 수 없습니다."); return; } // 서버 상태 정보 const status = this.getServerStatus(server); // 서버 이름과 상태 설정 modalTitle.innerHTML = ` ${server.hostname} <span class="server-status status-${status}">${this.getStatusLabel(status)}</span> `; // 모달 내용 업데이트 - 개별 필드 업데이트 방식으로 변경 // OS 정보 const modalOS = document.getElementById('modalOS'); if (modalOS) modalOS.textContent = server.os || '-'; // 가동 시간 const modalUptime = document.getElementById('modalUptime'); if (modalUptime) modalUptime.textContent = server.uptime || '-'; // 프로세스 수 const modalProcessCount = document.getElementById('modalProcessCount'); if (modalProcessCount) modalProcessCount.textContent = server.process_count || '-'; // 좀비 프로세스 const modalZombieCount = document.getElementById('modalZombieCount'); if (modalZombieCount) modalZombieCount.textContent = server.zombie_count || '-'; // 로드 평균 const modalLoadAvg = document.getElementById('modalLoadAvg'); if (modalLoadAvg) modalLoadAvg.textContent = server.load_avg_1m || '-'; // 마지막 업데이트 const modalLastUpdate = document.getElementById('modalLastUpdate'); if (modalLastUpdate) modalLastUpdate.textContent = new Date(server.timestamp).toLocaleString() || '-'; // 네트워크 정보 const modalNetInterface = document.getElementById('modalNetInterface'); if (modalNetInterface) modalNetInterface.textContent = server.net?.interface || '-'; const modalRxBytes = document.getElementById('modalRxBytes'); if (modalRxBytes) modalRxBytes.textContent = this.formatBytes(server.net?.rx_bytes) || '-'; const modalTxBytes = document.getElementById('modalTxBytes'); if (modalTxBytes) modalTxBytes.textContent = this.formatBytes(server.net?.tx_bytes) || '-'; const modalRxErrors = document.getElementById('modalRxErrors'); if (modalRxErrors) modalRxErrors.textContent = server.net?.rx_errors || '-'; const modalTxErrors = document.getElementById('modalTxErrors'); if (modalTxErrors) modalTxErrors.textContent = server.net?.tx_errors || '-'; // 서비스 상태 const modalServiceStatus = document.getElementById('modalServiceStatus'); if (modalServiceStatus) { modalServiceStatus.innerHTML = ''; if (server.services && Object.keys(server.services).length > 0) { Object.entries(server.services).forEach(([name, status]) => { const serviceTag = document.createElement('div'); serviceTag.className = `service-status-tag service-${status}`; serviceTag.innerHTML = ` ${name} <span class="status-indicator"> <i class="fas fa-${status === 'running' ? 'check-circle' : 'times-circle'}"></i> </span> `; modalServiceStatus.appendChild(serviceTag); }); } else { modalServiceStatus.innerHTML = '<div class="alert alert-info">서비스 정보가 없습니다.</div>'; } } // 오류 메시지 const modalErrorsContainer = document.getElementById('modalErrorsContainer'); if (modalErrorsContainer) { if (server.errors && server.errors.length > 0) { modalErrorsContainer.innerHTML = ` <div class="alert alert-danger"> <ul class="mb-0"> ${server.errors.map(error => `<li>${error}</li>`).join('')} </ul> </div> `; } else { modalErrorsContainer.innerHTML = '<div class="alert alert-info">현재 보고된 오류가 없습니다.</div>'; } } // jQuery로 모달 표시 시도 (부트스트랩 의존성 줄이기) if (window.jQuery && window.jQuery.fn.modal) { window.jQuery(modalElement).modal('show'); // 리소스 차트 생성 this.createResourceChart(server); // 서버 이름을 서버 이름 요소에 설정 const modalServerName = document.getElementById('modalServerName'); if (modalServerName) modalServerName.textContent = `${server.hostname} 상세 정보`; return; // jQuery로 성공적으로 표시했으면 여기서 종료 } // 부트스트랩 모달 인스턴스 생성 및 표시 if (typeof bootstrap !== 'undefined' && typeof bootstrap.Modal === 'function') { // 부트스트랩 5+ const bsModal = new bootstrap.Modal(modalElement); bsModal.show(); } else { // 순수 JavaScript로 모달 표시 (fallback) modalElement.style.display = 'block'; modalElement.classList.add('show'); document.body.classList.add('modal-open'); // 배경 요소 추가 const backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop fade show'; document.body.appendChild(backdrop); // 닫기 버튼에 이벤트 리스너 추가 const closeButtons = modalElement.querySelectorAll('[data-bs-dismiss="modal"]'); closeButtons.forEach(btn => { btn.addEventListener('click', () => { modalElement.style.display = 'none'; modalElement.classList.remove('show'); document.body.classList.remove('modal-open'); document.querySelector('.modal-backdrop')?.remove(); }); }); } // 리소스 차트 생성 this.createResourceChart(server); // 서버 이름을 서버 이름 요소에 설정 const modalServerName = document.getElementById('modalServerName'); if (modalServerName) modalServerName.textContent = `${server.hostname} 상세 정보`; } catch (error) { console.error('서버 상세 정보 표시 중 오류 발생:', error); alert('서버 상세 정보를 표시할 수 없습니다.'); } } closeModal() { this.modalElement.style.display = 'none'; } createResourceChart(server) { const chartElement = document.getElementById('resourceBarChart'); if (!chartElement) { console.error("리소스 차트 요소(resourceBarChart)를 찾을 수 없습니다."); return; } const ctx = chartElement.getContext('2d'); if (!ctx) { console.error("리소스 차트 컨텍스트를 가져올 수 없습니다."); return; } // 기존 차트가 있다면 파괴 if (this.resourceChartInstance) { this.resourceChartInstance.destroy(); } this.resourceChartInstance = new Chart(ctx, { type: 'bar', data: { labels: ['CPU', '메모리', '디스크'], datasets: [{ label: '사용량 (%)', data: [ server.cpu_usage || 0, server.memory_usage_percent || 0, (server.disk && server.disk.length > 0) ? server.disk[0].disk_usage_percent || 0 : 0 ], backgroundColor: [ this.getChartColor(server.cpu_usage || 0), this.getChartColor(server.memory_usage_percent || 0), this.getChartColor((server.disk && server.disk.length > 0) ? server.disk[0].disk_usage_percent || 0 : 0) ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: 100 } } } }); } createHistoryChart(hostname) { const historicalData = window.serverHistoricalData[hostname]; if (!historicalData || historicalData.length === 0) return; // history-chart-modal ID를 사용하도록 수정 const canvasElement = document.getElementById('history-chart-modal'); if (!canvasElement) { console.error("History chart canvas element not found in modal"); return; } const ctx = canvasElement.getContext('2d'); if (!ctx) { console.error("히스토리 차트 컨텍스트를 가져올 수 없습니다."); return; } // 기존 차트가 있다면 파괴 if (this.historyChartInstance) { this.historyChartInstance.destroy(); } // 시간 레이블 생성 (최근 24시간) const labels = historicalData.map(data => { const date = new Date(data.timestamp); return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`; }); // CPU, 메모리, 디스크 데이터 추출 const cpuData = historicalData.map(data => data.cpu_usage); const memoryData = historicalData.map(data => data.memory_usage_percent); const diskData = historicalData.map(data => data.disk[0].disk_usage_percent); this.historyChartInstance = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: 'CPU', data: cpuData, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.2)', borderWidth: 2, tension: 0.1 }, { label: '메모리', data: memoryData, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.2)', borderWidth: 2, tension: 0.1 }, { label: '디스크', data: diskData, borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.2)', borderWidth: 2, tension: 0.1 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: 100, title: { display: true, text: '사용량 (%)' } }, x: { title: { display: true, text: '시간' } } } } }); } updateProblemsList() { if (!this.aiProcessor) return; const problemListContainer = document.getElementById('aiProblemList'); const loadingIndicator = document.getElementById('aiProblemsLoading'); const emptyIndicator = document.getElementById('aiProblemsEmpty'); const paginationContainer = document.querySelector('.problem-pagination'); if (!problemListContainer || !loadingIndicator || !emptyIndicator) { console.error("AI Problem list UI elements not found."); return; } loadingIndicator.style.display = 'block'; emptyIndicator.style.display = 'none'; problemListContainer.innerHTML = ''; if (paginationContainer) paginationContainer.innerHTML = ''; try { // detectProblems 메소드가 존재하고 호출 가능한지 확인 let problems = []; if (typeof this.aiProcessor.detectProblems === 'function') { problems = this.aiProcessor.detectProblems(); } else { console.warn("AI 프로세서에 detectProblems 메소드가 없습니다. 기본 문제 감지 로직을 사용합니다."); // 기본 문제 감지 로직: 리소스 사용량이 높은 서버 감지 problems = this.serverData.filter(server => { const status = this.getServerStatus(server); if (status === 'critical') { return { severity: 'Critical', serverHostname: server.hostname, description: `리소스 과부하 감지`, solution: '서버 자원 확인 및 불필요한 프로세스를 종료하세요.' }; } else if (status === 'warning') { return { severity: 'Warning', serverHostname: server.hostname, description: `자원 사용량 높음`, solution: '서버 상태를 모니터링하고 추세를 확인하세요.' }; } return null; }).filter(p => p !== null); } // 결과가 배열이 아니거나 undefined인 경우 빈 배열로 처리 if (!Array.isArray(problems)) { console.warn("detectProblems() 함수가 배열을 반환하지 않았습니다."); problems = []; } // Normal 상태는 제외 (detectProblems에서 이미 처리되었거나, 여기서 한번 더 필터링) problems = problems.filter(p => p && (p.severity === 'Critical' || p.severity === 'Warning' || p.severity === 'Error')); // 정렬: Critical 우선, 그 다음 Warning(Error 포함) (내림차순) problems.sort((a, b) => { const severityScore = (severity) => { if (severity === 'Critical') return 2; if (severity === 'Warning' || severity === 'Error') return 1; return 0; // 그 외 (정상 등, 실제로는 필터링됨) }; return severityScore(b.severity) - severityScore(a.severity); }); loadingIndicator.style.display = 'none'; if (problems.length === 0) { emptyIndicator.style.display = 'block'; return; } // 전체 문제 데이터 저장 this.problemsData = problems; // 페이지네이션 계산 const totalProblems = problems.length; const totalPages = Math.ceil(totalProblems / this.problemsPerPage); // 현재 페이지가 범위를 벗어나면 조정 if (this.currentProblemPage > totalPages) { this.currentProblemPage = totalPages; } // 현재 페이지에 표시할 문제 계산 const startIdx = (this.currentProblemPage - 1) * this.problemsPerPage; const endIdx = Math.min(startIdx + this.problemsPerPage, totalProblems); const currentPageProblems = problems.slice(startIdx, endIdx); // 문제 항목 렌더링 currentPageProblems.forEach((problem, index) => { const listItem = document.createElement('li'); listItem.className = `list-group-item list-group-item-action problem-item severity-${problem.severity.toLowerCase()}`; // 인덱스 변수 추가 listItem.style.setProperty('--item-index', index); listItem.innerHTML = ` <div class="d-flex w-100 justify-content-between"> <h6 class="mb-1 problem-description">${problem.description}</h6> <small class="text-muted">${problem.serverHostname || '알 수 없는 서버'}</small> </div> <p class="mb-1 problem-solution">${problem.solution || '제안된 해결책 없음'}</p> <small class="text-muted">심각도: <span class="fw-bold problem-severity-text">${problem.severity}</span></small> <div class="problem-hint-icon"> <i class="fas fa-search-plus"></i> </div> `; // 문제 항목 클릭 시 액션 (서버 상세 모달) listItem.addEventListener('click', () => { // 상세 보고서 모달 표시 this.showProblemDetailModal(problem); }); problemListContainer.appendChild(listItem); }); // 페이지네이션 생성 if (paginationContainer && totalPages > 1) { // 이전 페이지 버튼 const prevBtn = document.createElement('button'); prevBtn.className = 'btn btn-sm btn-outline-secondary'; prevBtn.innerHTML = '<i class="fas fa-chevron-left"></i>'; prevBtn.disabled = this.currentProblemPage === 1; prevBtn.addEventListener('click', () => { if (this.currentProblemPage > 1) { this.currentProblemPage--; this.updateProblemsList(); } }); paginationContainer.appendChild(prevBtn); // 페이지 번호 버튼 const maxVisiblePages = 5; let startPage = Math.max(1, this.currentProblemPage - Math.floor(maxVisiblePages / 2)); let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); // 표시할 페이지 버튼 조정 if (endPage - startPage + 1 < maxVisiblePages && startPage > 1) { startPage = Math.max(1, endPage - maxVisiblePages + 1); } // 페이지 버튼 생성 for (let i = startPage; i <= endPage; i++) { const pageBtn = document.createElement('button'); pageBtn.className = `btn btn-sm mx-1 ${i === this.currentProblemPage ? 'btn-primary' : 'btn-outline-secondary'}`; pageBtn.textContent = i.toString(); pageBtn.addEventListener('click', () => { this.currentProblemPage = i; this.updateProblemsList(); }); paginationContainer.appendChild(pageBtn); } // 다음 페이지 버튼 const nextBtn = document.createElement('button'); nextBtn.className = 'btn btn-sm btn-outline-secondary'; nextBtn.innerHTML = '<i class="fas fa-chevron-right"></i>'; nextBtn.disabled = this.currentProblemPage === totalPages; nextBtn.addEventListener('click', () => { if (this.currentProblemPage < totalPages) { this.currentProblemPage++; this.updateProblemsList(); } }); paginationContainer.appendChild(nextBtn); } } catch (error) { console.error("AI 문제 목록 업데이트 중 오류 발생:", error); loadingIndicator.style.display = 'none'; emptyIndicator.textContent = "문제 목록을 불러오는 중 오류가 발생했습니다."; emptyIndicator.style.display = 'block'; } } // 서버 수 표시 업데이트하는 메소드 updateServerCount() { if (!this.serverCount) return; try { const endIndex = Math.min(this.currentPage * this.itemsPerPage, this.filteredData.length); const startIndex = this.filteredData.length > 0 ? (this.currentPage - 1) * this.itemsPerPage + 1 : 0; if (this.filteredData.length > 0) { this.serverCount.textContent = `전체 ${this.serverData.length} 서버 중 ${startIndex}-${endIndex} 표시 중`; } else { this.serverCount.textContent = `전체 ${this.serverData.length} 서버 중 0 표시 중`; } } catch (e) { console.error("서버 카운트 업데이트 중 오류:", e); } } updateGlobalStatusSummary() { if (!this.serverData || this.serverData.length === 0) return; const summaryContainer = document.getElementById('statusSummaryContainer'); if (!summaryContainer) { console.error("Status summary container not found."); return; } let normalCount = 0; let warningCount = 0; let criticalCount = 0; this.serverData.forEach(server => { const status = this.getServerStatus(server); // 중앙 집중식 상태 판단 함수 사용 if (status === 'normal') normalCount++; else if (status === 'warning') warningCount++; else if (status === 'critical') criticalCount++; }); // 타임스탬프 업데이트 const timestampElement = document.getElementById('timestamp'); if (timestampElement) { const latestTimestamp = this.serverData.reduce((latest, server) => { const serverTime = new Date(server.timestamp).getTime(); return serverTime > latest ? serverTime : latest; }, 0); if (latestTimestamp > 0) { timestampElement.textContent = `데이터 기준 시각: ${new Date(latestTimestamp).toLocaleString()}`; } else { timestampElement.textContent = `데이터 기준 시각: ${new Date().toLocaleString()}`; } } summaryContainer.innerHTML = ` <div class="row mb-3"> <div class="col-4 text-center"> <h3 class="mb-0 display-6 text-success">${normalCount}</h3> <p class="text-success mb-0">정상</p> </div> <div class="col-4 text-center"> <h3 class="mb-0 display-6 text-warning">${warningCount}</h3> <p class="text-warning mb-0">경고</p> </div> <div class="col-4 text-center"> <h3 class="mb-0 display-6 text-danger">${criticalCount}</h3> <p class="text-danger mb-0">심각</p> </div> </div> <div> <canvas id="globalStatusChart" height="150"></canvas> </div> `; // 상태 알림 생성 및 표시 this.updateStatusAlert(normalCount, warningCount, criticalCount); // 차트 업데이트 const chartElement = document.getElementById('globalStatusChart'); if (!chartElement) { console.error("Global status chart element not found."); return; } const chartCtx = chartElement.getContext('2d'); if (!chartCtx) { console.error("Global status chart context could not be obtained."); return; } if (this.globalStatusChartInstance) { this.globalStatusChartInstance.destroy(); } if (normalCount === 0 && warningCount === 0 && criticalCount === 0) { // 데이터가 없는 경우 빈 차트가 아닌 메시지 표시 chartElement.parentElement.innerHTML = ` <div class="alert alert-info text-center"> <i class="fas fa-info-circle me-2"></i> 표시할 서버 상태 데이터가 없습니다. </div> `; return; } this.globalStatusChartInstance = new Chart(chartCtx, { type: 'doughnut', data: { labels: ['정상', '경고', '심각'], datasets: [{ data: [normalCount, warningCount, criticalCount], backgroundColor: [ 'rgba(40, 167, 69, 0.7)', // 정상 (초록 계열) 'rgba(253, 154, 20, 0.7)', // 경고 (주황 계열) 'rgba(220, 53, 69, 0.7)' // 심각 (빨강 계열) ], borderColor: [ 'rgba(40, 167, 69, 1)', 'rgba(253, 154, 20, 1)', 'rgba(220, 53, 69, 1)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', }, tooltip: { callbacks: { label: function(context) { let label = context.label || ''; if (label) { label += ': '; } if (context.parsed !== null) { label += context.parsed + ' 대'; } return label; } } } } } }); // 서버 수 표시 업데이트 this.updateServerCount(); } // 서버 상태 요약 알림 업데이트 updateStatusAlert(normalCount, warningCount, criticalCount) { const alertElement = document.getElementById('statusSummaryAlert'); if (!alertElement) return; const iconContainer = alertElement.querySelector('.status-icon'); const messageContainer = alertElement.querySelector('.status-message'); if (!iconContainer || !messageContainer) return; const totalServers = normalCount + warningCount + criticalCount; // 상태 결정 (최악의 상태를 기준으로) let status = 'normal'; if (criticalCount > 0) status = 'critical'; else if (warningCount > 0) status = 'warning'; // 알림 클래스와 아이콘 설정 let alertClass, iconHTML, message; switch(status) { case 'critical': alertClass = 'alert-danger'; iconHTML = '<i class="fas fa-exclamation-circle fa-2x"></i>'; if (criticalCount === 1) { message = `<strong>긴급 주의 필요:</strong> 1개 서버가 심각한 상태입니다. 즉시 확인이 필요합니다.`; } else { message = `<strong>긴급 주의 필요:</strong> ${criticalCount}개 서버가 심각한 상태입니다. 즉시 확인이 필요합니다.`; } break; case 'warning': alertClass = 'alert-warning'; iconHTML = '<i class="fas fa-exclamation-triangle fa-2x"></i>'; if (warningCount === 1) { message = `<strong>주의:</strong> 1개 서버에 경고 상태가 감지되었습니다. 상태를 확인해 주세요.`; } else { message = `<strong>주의:</strong> ${warningCount}개 서버에 경고 상태가 감지되었습니다. 상태를 확인해 주세요.`; } break; default: // normal alertClass = 'alert-success'; iconHTML = '<i class="fas fa-check-circle fa-2x"></i>'; message = `<strong>모두 정상:</strong> 현재 ${totalServers}개의 서버가 모두 정상 작동 중입니다.`; } // 알림 업데이트 alertElement.className = `alert d-flex align-items-center ${alertClass}`; iconContainer.innerHTML = iconHTML; messageContainer.innerHTML = message; // 애니메이션 효과로 표시 alertElement.style.display = 'flex'; alertElement.style.opacity = '0'; alertElement.style.transform = 'translateY(-10px)'; // 부드럽게 표시 setTimeout(() => { alertElement.style.transition = 'all 0.5s ease'; alertElement.style.opacity = '1'; alertElement.style.transform = 'translateY(0)'; }, 100); } // 프리셋 쿼리 처리 개선 processPresetQuery(query) { if (!this.aiProcessor) return "AI 프로세서를 초기화할 수 없습니다."; // 서버 상태 데이터 수집 const criticalServers = this.serverData.filter(server => this.getServerStatus(server) === 'critical'); const warningServers = this.serverData.filter(server => this.getServerStatus(server) === 'warning'); const normalServers = this.serverData.filter(server => this.getServerStatus(server) === 'normal'); // AI 장애 보고서 데이터 가져오기 (있을 경우) let aiProblemsData = []; if (this.problemsData && Array.isArray(this.problemsData)) { aiProblemsData = [...this.problemsData]; } else if (typeof this.aiProcessor.detectProblems === 'function') { try { aiProblemsData = this.aiProcessor.detectProblems(); } catch (err) { console.warn("AI 문제 데이터를 가져오는 중 오류 발생:", err); } } // CPU 과부하 관련 쿼리 if (query.includes("CPU 사용률이 높은 서버") || query.includes("CPU 과부하")) { const highCpuServers = this.serverData.filter(server => server.cpu_usage >= this.thresholds.warning.cpu); // 서버 상태에 따라 응답 형식 조정 if (highCpuServers.length === 0) { return `### CPU 사용률 분석 결과 현재 CPU 사용률이 높은 서버가 없습니다. 모든 서버가 정상적인 CPU 사용률을 보이고 있습니다. **총 서버 수**: ${this.serverData.length}대 **평균 CPU 사용률**: ${(this.serverData.reduce((acc, server) => acc + server.cpu_usage, 0) / this.serverData.length).toFixed(1)}% 모든 서버의 CPU 사용률이 정상 범위(${this.thresholds.warning.cpu}% 미만) 내에 있습니다.`; } // 해당 문제와 관련된 AI 장애 보고서 항목 찾기 const cpuRelatedProblems = aiProblemsData.filter(problem => problem.description && problem.description.toLowerCase().includes('cpu') ); let response = `### CPU 사용률 분석 결과 CPU 사용률이 높은 서버가 ${highCpuServers.length}대 발견되었습니다. `; // 심각한 수준(Critical)의 서버 먼저 표시 const criticalCpuServers = highCpuServers.filter(server => server.cpu_usage >= this.thresholds.critical.cpu); if (criticalCpuServers.length > 0) { response += `#### 심각한 CPU 과부하 (${this.thresholds.critical.cpu}% 이상) `; criticalCpuServers.forEach(server => { response += `- **${server.hostname}**: CPU ${server.cpu_usage}% (심각) - 프로세스 수: ${server.process_count || '정보 없음'} - 부하 평균: ${server.load_avg_1m || '정보 없음'} `; }); response += "\n"; } // 경고 수준(Warning)의 서버 표시 const warningCpuServers = highCpuServers.filter(server => server.cpu_usage >= this.thresholds.warning.cpu && server.cpu_usage < this.thresholds.critical.cpu ); if (warningCpuServers.length > 0) { response += `#### 경고 수준의 CPU 사용 (${this.thresholds.warning.cpu}% ~ ${this.thresholds.critical.cpu-0.1}%) `; warningCpuServers.forEach(server => { response += `- **${server.hostname}**: CPU ${server.cpu_usage}% (경고) - 프로세스 수: ${server.process_count || '정보 없음'} - 부하 평균: ${server.load_avg_1m || '정보 없음'} `; }); response += "\n"; } // 문제 원인 및 해결 방안 (AI 장애 보고서에서 가져옴) response += "### 원인 분석 및 조치 방안\n\n"; if (cpuRelatedProblems.length > 0) { cpuRelatedProblems.forEach((problem, index) => { if (index < 3) { // 상위 3개만 표시 response += `${index+1}. **${problem.serverHostname || '전체 서버'}**: ${problem.description}\n`; response += ` - 권장 조치: ${problem.solution || '서버 부하 원인 확인 필요'}\n\n`; } }); } else { response += `1. **높은 CPU 사용률 원인**: 서버에서 리소스를 많이 사용하는 프로세스가 실행 중이거나, 동시에 많은 요청이 처리되고 있을 수 있습니다.\n`; response += ` - 권장 조치: 'top' 명령어로 CPU를 많이 사용하는 프로세스를 확인하고, 불필요한 프로세스 종료나 부하 분산을 고려하세요.\n\n`; response += `2. **성능 병목 현상**: CPU 사용률이 지속적으로 높은 경우 성능 병목 현상이 발생할 수 있습니다.\n`; response += ` - 권장 조치: 서버 스케일 업 또는 로드 밸런싱을 통한 스케일 아웃을 검토하세요.\n\n`; } return response; } // 메모리 부족 관련 쿼리 else if (query.includes("메모리 사용량이 많은 서버") || query.includes("메모리 부족")) { const highMemoryServers = this.serverData.filter(server => server.memory_usage_percent >= this.thresholds.warning.memory); // 서버 상태에 따라 응답 형식 조정 if (highMemoryServers.length === 0) { return `### 메모리 사용량 분석 결과 현재 메모리 사용량이 높은 서버가 없습니다. 모든 서버가 정상적인 메모리 사용량을 보이고 있습니다. **총 서버 수**: ${this.serverData.length}대 **평균 메모리 사용률**: ${(this.serverData.reduce((acc, server) => acc + server.memory_usage_percent, 0) / this.serverData.length).toFixed(1)}% 모든 서버의 메모리 사용률이 정상 범위(${this.thresholds.warning.memory}% 미만) 내에 있습니다.`; } // 해당 문제와 관련된 AI 장애 보고서 항목 찾기 const memoryRelatedProblems = aiProblemsData.filter(problem => problem.description && ( problem.description.toLowerCase().includes('memory') || problem.description.toLowerCase().includes('메모리') ) ); let response = `### 메모리 사용량 분석 결과 메모리 사용량이 높은 서버가 ${highMemoryServers.length}대 발견되었습니다. `; // 심각한 수준(Critical)의 서버 먼저 표시 const criticalMemServers = highMemoryServers.filter(server => server.memory_usage_percent >= this.thresholds.critical.memory); if (criticalMemServers.length > 0) { response += `#### 심각한 메모리 부족 (${this.thresholds.critical.memory}% 이상) `; criticalMemServers.forEach(server => { response += `- **${server.hostname}**: 메모리 ${server.memory_usage_percent}% (심각) - 총 메모리: ${server.memory_total || '정보 없음'} - 사용 메모리: ${server.memory_used || '정보 없음'} `; }); response += "\n"; } // 경고 수준(Warning)의 서버 표시 const warningMemServers = highMemoryServers.filter(server => server.memory_usage_percent >= this.thresholds.warning.memory && server.memory_usage_percent < this.thresholds.critical.memory ); if (warningMemServers.length > 0) { response += `#### 경고 수준의 메모리 사용 (${this.thresholds.warning.memory}% ~ ${this.thresholds.critical.memory-0.1}%) `; warningMemServers.forEach(server => { response += `- **${server.hostname}**: 메모리 ${server.memory_usage_percent}% (경고) - 총 메모리: ${server.memory_total || '정보 없음'} - 사용 메모리: ${server.memory_used || '정보 없음'} `; }); response += "\n"; } // 문제 원인 및 해결 방안 (AI 장애 보고서에서 가져옴) response += "### 원인 분석 및 조치 방안\n\n"; if (memoryRelatedProblems.length > 0) { memoryRelatedProblems.forEach((problem, index) => { if (index < 3) { // 상위 3개만 표시 response += `${index+1}. **${problem.serverHostname || '전체 서버'}**: ${problem.description}\n`; response += ` - 권장 조치: ${problem.solution || '메모리 누수 또는 과다 사용 프로세스 확인 필요'}\n\n`; } }); } else { response += `1. **높은 메모리 사용률 원인**: 서버에서 메모리 누수가 있거나, 메모리를 많이 사용하는 애플리케이션이 실행 중일 수 있습니다.\n`; response += ` - 권장 조치: 'free -m', 'top' 명령어로 메모리를 많이 사용하는 프로세스를 확인하고, 필요시 재시작하세요.\n\n`; response += `2. **스왑 사용 확인**: 메모리 부족 시 스왑 사용량이 증가할 수 있습니다.\n`; response += ` - 권장 조치: 'vmstat' 명령어로 스왑 사용량을 확인하고, 메모리 증설을 고려하세요.\n\n`; } return response; } // 서비스 중단 관련 쿼리 else if (query.includes("서비스가 중단된 서버")) { // 서비스 중단 서버 찾기 const stoppedServiceServers = this.serverData.filter(server => server.services && Object.values(server.services).some(status => status === 'stopped') ); // 서버 상태에 따라 응답 형식 조정 if (stoppedServiceServers.length === 0) { return `### 서비스 상태 분석 결과 현재 중단된 서비스가 있는 서버가 없습니다. 모든 서버의 서비스가 정상 작동 중입니다. **총 서버 수**: ${this.serverData.length}대 모든 서버에서 서비스가 정상적으로 실행 중입니다.`; } // 해당 문제와 관련된 AI 장애 보고서 항목 찾기 const serviceRelatedProblems = aiProblemsData.filter(problem => problem.description && ( problem.description.toLowerCase().includes('service') || problem.description.toLowerCase().includes('서비스') ) ); let response = `### 서비스 상태 분석 결과 서비스가 중단된 서버가 ${stoppedServiceServers.length}대 발견되었습니다. #### 중단된 서비스가 있는 서버 `; stoppedServiceServers.forEach(server => { const stoppedServices = Object.entries(server.services) .filter(([_, status]) => status === 'stopped') .map(([name, _]) => name); response += `- **${server.hostname}**: - 중단된 서비스: ${stoppedServices.join(', ')} - 서버 상태: ${this.getStatusLabel(this.getServerStatus(server))} `; }); // 문제 원인 및 해결 방안 (AI 장애 보고서에서 가져옴) response += "\n### 원인 분석 및 서비스 재시작 방법\n\n"; if (serviceRelatedProblems.length > 0) { serviceRelatedProblems.forEach((problem, index) => { if (index < 3) { // 상위 3개만 표시 response += `${index+1}. **${problem.serverHostname || '전체 서버'}**: ${problem.description}\n`; response += ` - 권장 조치: ${problem.solution || '서비스 재시작 필요'}\n\n`; } }); } else { response += `1. **서비스 중단 원인**: 서비스 충돌, 리소스 부족, 또는 강제 종료로 인해 서비스가 중단되었을 수 있습니다.\n`; response += ` - 권장 조치: 'systemctl restart SERVICE_NAME' 명령어로 해당 서비스를 재시작하세요.\n\n`; response += `2. **로그 확인**: 중단된 원인을 파악하기 위해 로그 확인이 필요합니다.\n`; response += ` - 권장 조치: 'journalctl -u SERVICE_NAME' 명령어로 서비스 로그를 확인하세요.\n\n`; } return response; } // 디스크 공간 부족 관련 쿼리 else if (query.includes("디스크 공간이 부족한 서버")) { const highDiskServers = this.serverData.filter(server => server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= this.thresholds.warning.disk ); // 서버 상태에 따라 응답 형식 조정 if (highDiskServers.length === 0) { return `### 디스크 공간 분석 결과 현재 디스크 공간이 부족한 서버가 없습니다. 모든 서버가 충분한 디스크 공간을 보유하고 있습니다. **총 서버 수**: ${this.serverData.length}대 **평균 디스크 사용률**: ${(this.serverData.reduce((acc, server) => acc + (server.disk && server.disk.length > 0 ? server.disk[0].disk_usage_percent : 0), 0) / this.serverData.length).toFixed(1)}% 모든 서버의 디스크 사용률이 정상 범위(${this.thresholds.warning.disk}% 미만) 내에 있습니다.`; } // 해당 문제와 관련된 AI 장애 보고서 항목 찾기 const diskRelatedProblems = aiProblemsData.filter(problem => problem.description && ( problem.description.toLowerCase().includes('disk') || problem.description.toLowerCase().includes('디스크') || problem.description.toLowerCase().includes('공간') ) ); let response = `### 디스크 공간 분석 결과 디스크 공간이 부족한 서버가 ${highDiskServers.length}대 발견되었습니다. `; // 심각한 수준(Critical)의 서버 먼저 표시 const criticalDiskServers = highDiskServers.filter(server => server.disk[0].disk_usage_percent >= this.thresholds.critical.disk ); if (criticalDiskServers.length > 0) { response += `#### 심각한 디스크 공간 부족 (${this.thresholds.critical.disk}% 이상) `; criticalDiskServers.forEach(server => { response += `- **${server.hostname}**: 디스크 ${server.disk[0].disk_usage_percent}% (심각) - 마운트 지점: ${server.disk[0].mount || '/'} - 총 용량: ${server.disk[0].disk_total || '정보 없음'} - 사용 용량: ${server.disk[0].disk_used || '정보 없음'} `; }); response += "\n"; } // 경고 수준(Warning)의 서버 표시 const warningDiskServers = highDiskServers.filter(server => server.disk[0].disk_usage_percent >= this.thresholds.warning.disk && server.disk[0].disk_usage_percent < this.thresholds.critical.disk ); if (warningDiskServers.length > 0) { response += `#### 경고 수준의 디스크 사용 (${this.thresholds.warning.disk}% ~ ${this.thresholds.critical.disk-0.1}%) `; warningDiskServers.forEach(server => { response += `- **${server.hostname}**: 디스크 ${server.disk[0].disk_usage_percent}% (경고) - 마운트 지점: ${server.disk[0].mount || '/'} - 총 용량: ${server.disk[0].disk_total || '정보 없음'} - 사용 용량: ${server.disk[0].disk_used || '정보 없음'} `; }); response += "\n"; } // 문제 원인 및 해결 방안 (AI 장애 보고서에서 가져옴) response += "### 원인 분석 및 조치 방안\n\n"; if (diskRelatedProblems.length > 0) { diskRelatedProblems.forEach((problem, index) => { if (index < 3) { // 상위 3개만 표시 response += `${index+1}. **${problem.serverHostname || '전체 서버'}**: ${problem.description}\n`; response += ` - 권장 조치: ${problem.solution || '불필요한 파일 정리 필요'}\n\n`; } }); } else { response += `1. **디스크 공간 부족 원인**: 로그 파일 증가, 임시 파일 누적, 또는 데이터 증가로 인해 디스크 공간이 부족할 수 있습니다.\n`; response += ` - 권장 조치: 'du -h --max-depth=1 /path' 명령어로 용량이 큰 디렉토리를 찾고, 불필요한 파일을 정리하세요.\n\n`; response += `2. **로그 정리**: 오래된 로그 파일이 많은 공간을 차지할 수 있습니다.\n`; response += ` - 권장 조치: '/var/log' 디렉토리의 오래된 로그 파일을 정리하고, logrotate 설정을 확인하세요.\n\n`; } return response; } // 정상 서버 목록 표시 else if (query.includes("정상 작동 중인 서버 목록")) { if (normalServers.length === 0) { return "### 정상 서버 목록\n\n현재 모든 서버에 문제가 있어 정상 작동 중인 서버가 없습니다."; } let response = `### 정상 작동 중인 서버 목록 (총 ${normalServers.length}대)\n\n`; normalServers.forEach(server => { response += `#### ${server.hostname}\n`; response += `- CPU: ${server.cpu_usage}% (정상)\n`; response += `- 메모리: ${server.memory_usage_percent}% (정상)\n`; response += `- 디스크: ${server.disk && server.disk.length > 0 ? server.disk[0].disk_usage_percent : 0}% (정상)\n`; response += `- 업타임: ${server.uptime || '정보 없음'}\n\n`; }); response += "모든 리소스가 정상 임계값 이내에서 작동 중이며, 특별한 조치가 필요하지 않습니다."; return response; } // 전체 서버 상태 요약 보고서 else if (query.includes("전체 서버 상태 요약")) { let response = `### 전체 서버 상태 요약 보고서\n\n`; response += `**총 서버 수**: ${this.serverData.length}대\n`; response += `- 정상 서버: ${normalServers.length}대\n`; response += `- 경고 상태: ${warningServers.length}대\n`; response += `- 심각 상태: ${criticalServers.length}대\n\n`; // 임계값 초과 현황 const highCpuCount = this.serverData.filter(s => s.cpu_usage >= this.thresholds.warning.cpu).length; const highMemCount = this.serverData.filter(s => s.memory_usage_percent >= this.thresholds.warning.memory).length; const highDiskCount = this.serverData.filter(s => s.disk && s.disk.length > 0 && s.disk[0].disk_usage_percent >= this.thresholds.warning.disk ).length; const stoppedServiceCount = this.serverData.filter(s => s.services && Object.values(s.services).some(status => status === 'stopped') ).length; response += `### 리소스 사용 현황\n\n`; response += `- CPU 임계치 초과: ${highCpuCount}대\n`; response += `- 메모리 임계치 초과: ${highMemCount}대\n`; response += `- 디스크 임계치 초과: ${highDiskCount}대\n`; response += `- 서비스 중단 서버: ${stoppedServiceCount}대\n\n`; // 심각한 문제 서버 목록 (최대 3대) if (criticalServers.length > 0) { response += `### 심각한 상태의 서버 (상위 ${Math.min(3, criticalServers.length)}대)\n\n`; criticalServers.slice(0, 3).forEach(server => { response += `#### ${server.hostname}\n`; // 서버의 문제 원인 파악 const issues = []; if (server.cpu_usage >= this.thresholds.critical.cpu) { issues.push(`CPU 사용률 ${server.cpu_usage}% (임계치 ${this.thresholds.critical.cpu}%)`); } if (server.memory_usage_percent >= this.thresholds.critical.memory) { issues.push(`메모리 사용률 ${server.memory_usage_percent}% (임계치 ${this.thresholds.critical.memory}%)`); } if (server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= this.thresholds.critical.disk) { issues.push(`디스크 사용률 ${server.disk[0].disk_usage_percent}% (임계치 ${this.thresholds.critical.disk}%)`); } if (server.services && Object.values(server.services).some(status => status === 'stopped')) { const stoppedServices = Object.entries(server.services) .filter(([_, status]) => status === 'stopped') .map(([name, _]) => name); issues.push(`중단된 서비스: ${stoppedServices.join(', ')}`); } issues.forEach(issue => response += `- ${issue}\n`); response += '\n'; }); } // 관련 AI 문제 항목 추가 if (aiProblemsData.length > 0) { response += `### AI 분석 문제 항목 (상위 ${Math.min(3, aiProblemsData.length)}개)\n\n`; aiProblemsData.slice(0, 3).forEach((problem, index) => { response += `${index+1}. **${problem.serverHostname || '전체 서버'}**: ${problem.description}\n`; response += ` - 심각도: ${problem.severity}\n`; response += ` - 권장 조치: ${problem.solution || '문제 원인 분석 필요'}\n\n`; }); } return response; } // 기본 AI 프로세서 호출 return this.aiProcessor.processQuery(query); } processAIQuery() { if (!this.aiProcessor) return; const queryInput = document.getElementById('queryInput'); if (!queryInput) { console.error("쿼리 입력 요소(queryInput)를 찾을 수 없습니다."); return; } const query = queryInput.value.trim(); if (!query) return; const queryLoadingElement = document.getElementById('queryLoading'); const queryResultElement = document.getElementById('queryResult'); const queryResultContent = document.getElementById('queryResultContent'); // 응답 영역 보이기 if (queryLoadingElement) queryLoadingElement.classList.add('active'); if (queryResultElement) queryResultElement.style.display = 'none'; // 프리셋 기반 응답 처리 (자체 처리) const isPresetQuery = query.includes("CPU 사용률이 높은 서버") || query.includes("CPU 과부하") || query.includes("메모리 사용량이 많은 서버") || query.includes("메모리 부족") || query.includes("서비스가 중단된 서버") || query.includes("디스크 공간이 부족한 서버") || query.includes("정상 작동 중인 서버 목록") || query.includes("전체 서버 상태 요약"); if (isPresetQuery) { const result = this.processPresetQuery(query); if (result) { if (queryResultContent && queryResultElement) { queryResultContent.innerHTML = result; queryResultElement.classList.add('active'); queryResultElement.style.display = 'block'; } if (queryLoadingElement) queryLoadingElement.classList.remove('active'); return; } } // AI 질의 처리 (서버 상태 데이터와 AI 문제 데이터 연동) let enhancedQuery = query; // AI 자동 장애 보고서 데이터가 있다면 쿼리에 추가 컨텍스트 제공 if (this.problemsData && this.problemsData.length > 0) { const relevantProblems = this.findRelevantProblems(query); if (relevantProblems.length > 0) { enhancedQuery += "\n\n관련 문제 데이터:\n"; relevantProblems.forEach(problem => { enhancedQuery += `- ${problem.serverHostname || '전체 서버'}: ${problem.description} (심각도: ${problem.severity})\n`; if (problem.solution) { enhancedQuery += ` 해결책: ${problem.solution}\n`; } }); } } // 서버 상태 데이터 통계 추가 const criticalServers = this.serverData.filter(server => this.getServerStatus(server) === 'critical').length; const warningServers = this.serverData.filter(server => this.getServerStatus(server) === 'warning').length; const normalServers = this.serverData.filter(server => this.getServerStatus(server) === 'normal').length; enhancedQuery += `\n\n서버 현황: 총 ${this.serverData.length}대 (정상: ${normalServers}, 경고: ${warningServers}, 심각: ${criticalServers})`; this.aiProcessor.processQuery(enhancedQuery) .then(response => { if (queryResultContent && queryResultElement) { queryResultContent.innerHTML = response; queryResultElement.classList.add('active'); queryResultElement.style.display = 'block'; } }) .catch(error => { if (queryResultContent && queryResultElement) { queryResultContent.innerHTML = `오류가 발생했습니다: ${error.message}`; queryResultElement.classList.add('active'); queryResultElement.style.display = 'block'; } }) .finally(() => { if (queryLoadingElement) queryLoadingElement.classList.remove('active'); }); } // 쿼리와 관련된 문제 찾기 findRelevantProblems(query) { if (!this.problemsData || !Array.isArray(this.problemsData)) { return []; } const keywords = query.toLowerCase().split(/\s+/); return this.problemsData.filter(problem => { if (!problem.description) return false; const description = problem.description.toLowerCase(); return keywords.some(keyword => keyword.length > 3 && description.includes(keyword) ); }); } downloadErrorReport() { if (!this.aiProcessor) return; let report = ''; try { // generateErrorReport 메소드가 존재하고 호출 가능한지 확인 if (typeof this.aiProcessor.generateErrorReport === 'function') { report = this.aiProcessor.generateErrorReport(); } else { console.warn("AI 프로세서에 generateErrorReport 메소드가 없습니다. 기본 보고서를 생성합니다."); // 기본 장애 보고서 생성 로직 report = '# 서버 상태 보고서\n\n'; report += `생성 시간: ${new Date().toLocaleString()}\n\n`; // 서버 상태에 따라 요약 정보 생성 const criticalServers = this.serverData.filter(s => this.getServerStatus(s) === 'critical'); const warningServers = this.serverData.filter(s => this.getServerStatus(s) === 'warning'); const normalServers = this.serverData.filter(s => this.getServerStatus(s) === 'normal'); report += `## 서버 상태 요약\n\n`; report += `- 총 서버 수: ${this.serverData.length}\n`; report += `- 정상: ${normalServers.length}\n`; report += `- 경고: ${warningServers.length}\n`; report += `- 심각: ${criticalServers.length}\n\n`; // 문제 상태의 서버에 대한 세부 정보 if (criticalServers.length > 0) { report += '## 심각한 상태의 서버\n\n'; criticalServers.forEach(server => { report += `### ${server.hostname}\n`; report += `- CPU: ${server.cpu_usage}%\n`; report += `- 메모리: ${server.memory_usage_percent}%\n`; report += `- 디스크: ${server.disk[0].disk_usage_percent}%\n`; if (server.errors && server.errors.length > 0) { report += `- 오류: ${server.errors.join(', ')}\n`; } report += `\n`; }); } if (warningServers.length > 0) { report += '## 경고 상태의 서버\n\n'; warningServers.forEach(server => { report += `### ${server.hostname}\n`; report += `- CPU: ${server.cpu_usage}%\n`; report += `- 메모리: ${server.memory_usage_percent}%\n`; report += `- 디스크: ${server.disk[0].disk_usage_percent}%\n`; if (server.errors && server.errors.length > 0) { report += `- 오류: ${server.errors.join(', ')}\n`; } report += `\n`; }); } } } catch (e) { console.error("장애 보고서 생성 중 오류 발생:", e); report = '# 오류 발생\n\n장애 보고서를 생성하는 중 오류가 발생했습니다.\n\n' + e.message; } const blob = new Blob([report], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `server_error_report_${new Date().toISOString().slice(0, 10)}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 유틸리티 함수 getServerStatus(server) { // AI Processor의 getEffectiveServerStatus를 사용하여 상태 결정 if (this.aiProcessor && typeof this.aiProcessor.getEffectiveServerStatus === 'function') { try { return this.aiProcessor.getEffectiveServerStatus(server); } catch (e) { // 에러 발생 시 한 번만 경고 출력 (fallback 로직 수행) if (!this._hasLoggedAIProcessorError) { console.error("Error calling aiProcessor.getEffectiveServerStatus:", e); this._hasLoggedAIProcessorError = true; } } } else if (!this._hasLoggedNoAIProcessor) { // 경고 메시지를 한 번만 출력 console.warn("AIProcessor 또는 getEffectiveServerStatus가 없어 기본 상태 판단 로직을 사용합니다."); this._hasLoggedNoAIProcessor = true; } // 폴백 로직 (AI Processor 사용 불가 또는 에러 시) // 리소스 사용률 기반 명확한 기준으로 상태 판단 // 1. Critical 조건 판단 if (server.cpu_usage >= this.thresholds.critical.cpu || server.memory_usage_percent >= this.thresholds.critical.memory || (server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= this.thresholds.critical.disk)) { return 'critical'; } // 2. 오류 메시지 기반 Critical 판단 const hasCriticalError = server.errors && server.errors.some(err => typeof err === 'string' && err.toLowerCase().includes('critical')); if (hasCriticalError) { return 'critical'; } // 3. 서비스 중단 기반 Critical 판단 const hasStoppedService = server.services && Object.values(server.services).includes('stopped'); if (hasStoppedService) { return 'critical'; } // 4. Warning 조건 판단 if (server.cpu_usage >= this.thresholds.warning.cpu || server.memory_usage_percent >= this.thresholds.warning.memory || (server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= this.thresholds.warning.disk)) { return 'warning'; } // 5. 오류 메시지 기반 Warning 판단 const hasWarningError = server.errors && server.errors.some(err => typeof err === 'string' && (err.toLowerCase().includes('warning') || err.toLowerCase().includes('error'))); if (hasWarningError) { return 'warning'; } // 6. 위 조건에 해당하지 않으면 normal 상태 return 'normal'; } getResourceStatus(value, type = 'generic') { // 리소스 유형에 따른 임계값 적용 const criticalThreshold = type in this.thresholds.critical ? this.thresholds.critical[type] : this.thresholds.critical.cpu; const warningThreshold = type in this.thresholds.warning ? this.thresholds.warning[type] : this.thresholds.warning.cpu; if (value >= criticalThreshold) return 'critical'; if (value >= warningThreshold) return 'warning'; return 'normal'; } getStatusLabel(status) { switch(status) { case 'normal': return '정상'; case 'warning': return '경고'; case 'critical': return '심각'; default: return '알 수 없음'; } } // 서버 상태에 따른 부트스트랩 색상 클래스 반환 getStatusColorClass(status) { switch(status) { case 'normal': return 'success'; case 'warning': return 'warning'; case 'critical': return 'danger'; default: return 'secondary'; } } getChartColor(value, type = 'generic') { const status = this.getResourceStatus(value, type); switch(status) { case 'critical': return 'rgba(220, 53, 69, 0.7)'; // 심각 case 'warning': return 'rgba(253, 154, 20, 0.7)'; // 경고 default: return 'rgba(40, 167, 69, 0.7)'; // 정상 } } formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // 모든 문제를 보여주는 모달 표시 showAllProblems() { if (!this.aiProcessor) { alert('AI 프로세서가 초기화되지 않아 문제 목록을 가져올 수 없습니다.'); return; } // 문제 데이터 가져오기 let problems = []; try { if (typeof this.aiProcessor.detectProblems === 'function') { problems = this.aiProcessor.detectProblems(); } else { console.warn("AI 프로세서에 detectProblems 메소드가 없습니다."); alert('문제 목록을 불러올 수 없습니다.'); return; } // 결과가 배열이 아니거나 undefined인 경우 빈 배열로 처리 if (!Array.isArray(problems)) { console.warn("detectProblems() 함수가 배열을 반환하지 않았습니다."); problems = []; } // Normal 상태는 제외 problems = problems.filter(p => p && (p.severity === 'Critical' || p.severity === 'Warning' || p.severity === 'Error')); // 정렬: Critical 우선, 그 다음 Warning/Error problems.sort((a, b) => { const severityScore = (severity) => { if (severity === 'Critical') return 2; if (severity === 'Warning' || severity === 'Error') return 1; return 0; }; return severityScore(b.severity) - severityScore(a.severity); }); } catch (error) { console.error("문제 목록 가져오기 오류:", error); alert('문제 목록을 불러오는 중 오류가 발생했습니다.'); return; } // 문제가 없는 경우 if (problems.length === 0) { alert('현재 감지된 문제가 없습니다.'); return; } try { // 기존 모달이 있으면 제거 const existingModal = document.getElementById('allProblemsModal'); if (existingModal) { existingModal.remove(); } // 모든 문제를 표시하는 모달 생성 const modalHTML = ` <div class="modal fade" id="allProblemsModal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-lg modal-dialog-scrollable"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"> <i class="fas fa-exclamation-triangle me-2 text-danger"></i> 전체 서버 문제 목록 </h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div class="problems-count mb-3"> 총 <span class="fw-bold">${problems.length}</span>개의 문제가 감지되었습니다. </div> <div class="alert alert-info mb-3"> <i class="fas fa-info-circle me-2"></i> 각 문제를 클릭하면 해당 서버의 상세 정보를 확인할 수 있습니다. </div> <ul class="list-group all-problems-list"> ${problems.map((problem, idx) => ` <li class="list-group-item list-group-item-action problem-item severity-${problem.severity.toLowerCase()}" data-index="${idx}"> <div class="d-flex w-100 justify-content-between"> <h6 class="mb-1 problem-description">${problem.description}</h6> <small class="text-muted">${problem.serverHostname || '알 수 없는 서버'}</small> </div> <p class="mb-1 problem-solution">${problem.solution || '제안된 해결책 없음'}</p> <small class="text-muted">심각도: <span class="fw-bold problem-severity-text">${problem.severity}</span></small> <div class="problem-hint-icon"> <i class="fas fa-search-plus"></i> </div> </li> `).join('')} </ul> </div> <div class="modal-footer"> <button type="button" class="btn btn-primary" id="downloadModalReport"> <i class="bi bi-download"></i> 보고서 다운로드 </button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button> </div> </div> </div> </div> `; // 모달을 페이지에 추가 document.body.insertAdjacentHTML('beforeend', modalHTML); // 모달 요소 가져오기 const modalElement = document.getElementById('allProblemsModal'); if (!modalElement) { console.error("모달 요소 생성 실패"); return; } // jQuery로 모달 표시 시도 (부트스트랩 의존성 줄이기) if (window.jQuery && window.jQuery.fn.modal) { const $modal = window.jQuery(modalElement); $modal.modal('show'); // 문제 항목에 클릭 이벤트 추가 window.jQuery('.problem-item').on('click', function() { const index = parseInt(window.jQuery(this).data('index')); const serverHostname = problems[index].serverHostname; if (!serverHostname) return; const server = this.serverData.find(s => s.hostname === serverHostname); if (server) { $modal.modal('hide'); setTimeout(() => { this.showServerDetail(server); }, 500); } }.bind(this)); // 보고서 다운로드 버튼 이벤트 window.jQuery('#downloadModalReport').on('click', this.downloadErrorReport.bind(this)); return; // jQuery로 성공적으로 표시했으면 여기서 종료 } // 부트스트랩 모달 인스턴스 생성 및 표시 if (typeof bootstrap !== 'undefined' && typeof bootstrap.Modal === 'function') { // 부트스트랩 5+ const bsModal = new bootstrap.Modal(modalElement); bsModal.show(); // 문제 항목 클릭 이벤트 추가 const problemItems = modalElement.querySelectorAll('.problem-item'); problemItems.forEach(item => { // 애니메이션 딜레이 설정 const index = parseInt(item.dataset.index); item.style.setProperty('--item-index', index); item.addEventListener('click', () => { // 해당 서버 모달 표시 const serverHostname = problems[index].serverHostname; if (!serverHostname) return; const server = this.serverData.find(s => s.hostname === serverHostname); if (server) { bsModal.hide(); // 현재 모달 닫기 setTimeout(() => { this.showServerDetail(server); // 서버 상세 모달 표시 }, 500); } }); }); } else { // 순수 JavaScript로 모달 표시 (fallback) modalElement.style.display = 'block'; modalElement.classList.add('show'); document.body.classList.add('modal-open'); // 배경 요소 추가 const backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop fade show'; document.body.appendChild(backdrop); // 닫기 버튼에 이벤트 리스너 추가 const closeButtons = modalElement.querySelectorAll('[data-bs-dismiss="modal"]'); closeButtons.forEach(btn => { btn.addEventListener('click', () => { modalElement.style.display = 'none'; modalElement.classList.remove('show'); document.body.classList.remove('modal-open'); document.querySelector('.modal-backdrop')?.remove(); }); }); // 문제 항목 클릭 이벤트 추가 const problemItems = modalElement.querySelectorAll('.problem-item'); problemItems.forEach(item => { const index = parseInt(item.dataset.index); item.style.setProperty('--item-index', index); item.addEventListener('click', () => { const serverHostname = problems[index].serverHostname; if (!serverHostname) return; const server = this.serverData.find(s => s.hostname === serverHostname); if (server) { // 모달 수동 닫기 modalElement.style.display = 'none'; modalElement.classList.remove('show'); document.body.classList.remove('modal-open'); document.querySelector('.modal-backdrop')?.remove(); setTimeout(() => { this.showServerDetail(server); }, 500); } }); }); } // 보고서 다운로드 버튼 이벤트 const downloadBtn = document.getElementById('downloadModalReport'); if (downloadBtn) { downloadBtn.addEventListener('click', () => { this.downloadErrorReport(); }); } } catch (error) { console.error('모달 표시 중 오류 발생:', error); alert('문제 목록을 표시할 수 없습니다.'); } } // 문제 상세 보고서 모달 표시 (새로 추가) showProblemDetailModal(problem) { if (!problem) { console.error("문제 데이터가 없습니다."); return; } // 서버 정보 가져오기 const server = this.serverData.find(s => s.hostname === problem.serverHostname); try { // 기존 모달 제거 (중복 생성 방지) const existingModal = document.getElementById('problemDetailModal'); if (existingModal) { existingModal.remove(); } // 모달 HTML 생성 및 추가 const modalHTML = ` <div class="modal fade" id="problemDetailModal" tabindex="-1" aria-hidden="true"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"><i class="fas fa-exclamation-triangle me-2 text-danger"></i> <span id="problemTitle">문제 상세 보고서</span></h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> <div class="row mb-3"> <div class="col-12"> <div class="alert alert-danger"> <h6 class="alert-heading">문제 설명</h6> <p id="problemDescription" class="mb-0"></p> </div> </div> </div> <div class="row mb-3"> <div class="col-md-6"> <div class="card"> <div class="card-header bg-light"> <h6 class="mb-0">서버 정보</h6> </div> <div class="card-body"> <table class="table table-sm"> <tbody id="problemServerInfoTable"> </tbody> </table> </div> </div> </div> <div class="col-md-6"> <div class="card"> <div class="card-header bg-light"> <h6 class="mb-0">문제 원인</h6> </div> <div class="card-body"> <ul id="problemCausesList" class="mb-0"></ul> </div> </div> </div> </div> <div class="row mb-3"> <div class="col-12"> <div class="card"> <div class="card-header bg-light"> <h6 class="mb-0">해결 방법</h6> </div> <div class="card-body"> <ul id="problemSolutionsList" class="mb-0"></ul> </div> </div> </div> </div> <div class="row"> <div class="col-12"> <div class="card"> <div class="card-header bg-light"> <h6 class="mb-0">상세 메트릭 정보</h6> </div> <div class="card-body"> <div id="problemMetricsInfo"></div> </div> </div> </div> </div> </div> <div class="modal-footer"> <button type="button" class="btn btn-primary" id="downloadReportBtn"> <i class="bi bi-download me-1"></i> 보고서 다운로드 (.txt) </button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button> </div> </div> </div> </div>`; // 모달을 페이지에 추가 document.body.insertAdjacentHTML('beforeend', modalHTML); // 모달 요소를 다시 가져옴 const problemModal = document.getElementById('problemDetailModal'); if (!problemModal) { console.error("모달 요소 생성 실패"); return; } // 문제 상세 정보 표시 const problemTitle = document.getElementById('problemTitle'); const problemDescription = document.getElementById('problemDescription'); const serverInfoTable = document.getElementById('problemServerInfoTable'); const causesList = document.getElementById('problemCausesList'); const solutionsList = document.getElementById('problemSolutionsList'); const metricsInfo = document.getElementById('problemMetricsInfo'); // 안전하게 내용 업데이트 if (problemTitle) problemTitle.textContent = `${problem.severity} 문제 보고서: ${server ? server.hostname : '알 수 없는 서버'}`; if (problemDescription) problemDescription.textContent = problem.description; // 서버 정보 테이블 업데이트 if (serverInfoTable && server) { serverInfoTable.innerHTML = ` <tr> <th>호스트명</th> <td>${server.hostname}</td> </tr> <tr> <th>IP 주소</th> <td>${server.ip}</td> </tr> <tr> <th>OS</th> <td>${server.os}</td> </tr> <tr> <th>상태</th> <td><span class="badge bg-${this.getStatusColorClass(this.getServerStatus(server))}">${this.getStatusLabel(this.getServerStatus(server))}</span></td> </tr> <tr> <th>CPU 사용률</th> <td>${server.cpu_usage}%</td> </tr> <tr> <th>메모리 사용률</th> <td>${server.memory_usage_percent}%</td> </tr> <tr> <th>디스크 사용률</th> <td>${server.disk && server.disk.length > 0 ? server.disk[0].disk_usage_percent + '%' : 'N/A'}</td> </tr> `; } // 원인 및 해결 방법 목록 채우기 if (causesList) { causesList.innerHTML = ''; const causes = problem.causes || ["원인 데이터 없음"]; causes.forEach(cause => { const li = document.createElement('li'); li.textContent = cause; causesList.appendChild(li); }); } if (solutionsList) { solutionsList.innerHTML = ''; const solutions = problem.solutions || [problem.solution || "해결 방법 데이터 없음"]; solutions.forEach(solution => { const li = document.createElement('li'); li.textContent = solution; solutionsList.appendChild(li); }); } // 상세 메트릭 정보 if (metricsInfo && server) { metricsInfo.innerHTML = ` <div class="row"> <div class="col-md-6"> <h6 class="mt-2 mb-3">리소스 사용량</h6> <div class="progress mb-2" style="height: 25px;"> <div class="progress-bar ${this.getResourceStatus(server.cpu_usage, 'cpu') !== 'normal' ? 'bg-danger' : 'bg-success'}" role="progressbar" style="width: ${server.cpu_usage}%"> CPU ${server.cpu_usage}% </div> </div> <div class="progress mb-2" style="height: 25px;"> <div class="progress-bar ${this.getResourceStatus(server.memory_usage_percent, 'memory') !== 'normal' ? 'bg-danger' : 'bg-success'}" role="progressbar" style="width: ${server.memory_usage_percent}%"> 메모리 ${server.memory_usage_percent}% </div> </div> <div class="progress mb-2" style="height: 25px;"> <div class="progress-bar ${server.disk && server.disk.length > 0 && this.getResourceStatus(server.disk[0].disk_usage_percent, 'disk') !== 'normal' ? 'bg-danger' : 'bg-success'}" role="progressbar" style="width: ${server.disk && server.disk.length > 0 ? server.disk[0].disk_usage_percent : 0}%"> 디스크 ${server.disk && server.disk.length > 0 ? server.disk[0].disk_usage_percent : 0}% </div> </div> </div> <div class="col-md-6"> <h6 class="mt-2 mb-3">네트워크 정보</h6> <p><strong>수신:</strong> ${this.formatBytes(server.net.rx_bytes)}</p> <p><strong>송신:</strong> ${this.formatBytes(server.net.tx_bytes)}</p> <p><strong>오류:</strong> RX: ${server.net.rx_errors}, TX: ${server.net.tx_errors}</p> </div> </div> `; } // 보고서 다운로드 버튼 이벤트 리스너 const downloadBtn = document.getElementById('downloadReportBtn'); if (downloadBtn) { // 이전 이벤트 리스너 제거 downloadBtn.replaceWith(downloadBtn.cloneNode(true)); // 새 이벤트 리스너 추가 document.getElementById('downloadReportBtn').addEventListener('click', () => { this.downloadProblemReport(problem, server); }); } // jQuery로 모달 표시 시도 (부트스트랩 의존성 줄이기) if (window.jQuery && window.jQuery.fn.modal) { window.jQuery(problemModal).modal('show'); return; // jQuery로 성공적으로 표시했으면 여기서 종료 } // 부트스트랩 모달 인스턴스 생성 및 표시 if (typeof bootstrap !== 'undefined' && typeof bootstrap.Modal === 'function') { // 부트스트랩 5+ const bsModal = new bootstrap.Modal(problemModal); bsModal.show(); } else { // 순수 JavaScript로 모달 표시 (fallback) problemModal.style.display = 'block'; problemModal.classList.add('show'); document.body.classList.add('modal-open'); // 배경 요소 추가 const backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop fade show'; document.body.appendChild(backdrop); // 닫기 버튼에 이벤트 리스너 추가 const closeButtons = problemModal.querySelectorAll('[data-bs-dismiss="modal"]'); closeButtons.forEach(btn => { btn.addEventListener('click', () => { problemModal.style.display = 'none'; problemModal.classList.remove('show'); document.body.classList.remove('modal-open'); document.querySelector('.modal-backdrop')?.remove(); }); }); } } catch (error) { console.error('모달 표시 중 오류 발생:', error); alert('문제 상세 정보를 표시할 수 없습니다.'); } } // 문제 보고서 다운로드 (새로 추가) downloadProblemReport(problem, server) { if (!problem || !server) return; const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); const filename = `${server.hostname}_${problem.severity}_report_${timestamp}.txt`; let reportContent = `=========================================\n`; reportContent += ` OpenManager AI 자동 장애 보고서\n`; reportContent += `=========================================\n\n`; reportContent += `[생성 일시]: ${new Date().toLocaleString()}\n\n`; reportContent += `[문제 요약]\n`; reportContent += `심각도: ${problem.severity}\n`; reportContent += `설명: ${problem.description}\n\n`; reportContent += `[서버 정보]\n`; reportContent += `호스트명: ${server.hostname}\n`; reportContent += `IP: ${server.ip}\n`; reportContent += `OS: ${server.os}\n`; reportContent += `상태: ${this.getStatusLabel(this.getServerStatus(server))}\n\n`; reportContent += `[리소스 현황]\n`; reportContent += `CPU 사용률: ${server.cpu_usage}%\n`; reportContent += `메모리 사용률: ${server.memory_usage_percent}%\n`; reportContent += `디스크 사용률: ${server.disk && server.disk.length > 0 ? server.disk[0].disk_usage_percent + '%' : 'N/A'}\n`; reportContent += `네트워크 수신: ${this.formatBytes(server.net.rx_bytes)}\n`; reportContent += `네트워크 송신: ${this.formatBytes(server.net.tx_bytes)}\n`; reportContent += `네트워크 오류 (RX/TX): ${server.net.rx_errors}/${server.net.tx_errors}\n\n`; if (problem.causes && problem.causes.length) { reportContent += `[추정 원인]\n`; problem.causes.forEach((cause, index) => { reportContent += `${index + 1}. ${cause}\n`; }); reportContent += `\n`; } reportContent += `[해결 방법]\n`; const solutions = problem.solutions || [problem.solution || "해결 방법 데이터 없음"]; solutions.forEach((solution, index) => { reportContent += `${index + 1}. ${solution}\n`; }); reportContent += `\n`; if (server.errors && server.errors.length) { reportContent += `[오류 로그]\n`; server.errors.forEach((error, index) => { reportContent += `${index + 1}. ${error}\n`; }); } reportContent += `\n=========================================\n`; reportContent += `이 보고서는 OpenManager AI에 의해 자동 생성되었습니다.\n`; reportContent += `문의: support@openmanager.ai\n`; // 텍스트 파일로 다운로드 const blob = new Blob([reportContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // 전체 문제 보고서 다운로드 (새로 추가) downloadAllProblemsReport() { if (!this.problemsData || this.problemsData.length === 0) { alert('다운로드할 문제 보고서가 없습니다.'); return; } const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); const filename = `OpenManager_all_problems_report_${timestamp}.txt`; let reportContent = `=========================================\n`; reportContent += ` OpenManager AI 자동 장애 보고서 (전체)\n`; reportContent += `=========================================\n\n`; reportContent += `[생성 일시]: ${new Date().toLocaleString()}\n`; reportContent += `[총 문제 수]: ${this.problemsData.length}개\n\n`; this.problemsData.forEach((problem, index) => { const server = this.serverData.find(s => s.hostname === problem.serverHostname); if (!server) return; reportContent += `\n=========================================\n`; reportContent += `문제 #${index + 1}: ${problem.severity} - ${server.hostname}\n`; reportContent += `=========================================\n\n`; reportContent += `[문제 요약]\n`; reportContent += `심각도: ${problem.severity}\n`; reportContent += `설명: ${problem.description}\n\n`; reportContent += `[서버 정보]\n`; reportContent += `호스트명: ${server.hostname}\n`; reportContent += `IP: ${server.ip}\n`; reportContent += `OS: ${server.os}\n`; reportContent += `상태: ${this.getStatusLabel(this.getServerStatus(server))}\n\n`; reportContent += `[리소스 현황]\n`; reportContent += `CPU 사용률: ${server.cpu_usage}%\n`; reportContent += `메모리 사용률: ${server.memory_usage_percent}%\n`; reportContent += `디스크 사용률: ${server.disk && server.disk.length > 0 ? server.disk[0].disk_usage_percent + '%' : 'N/A'}\n`; reportContent += `네트워크 수신: ${this.formatBytes(server.net.rx_bytes)}\n`; reportContent += `네트워크 송신: ${this.formatBytes(server.net.tx_bytes)}\n`; reportContent += `네트워크 오류 (RX/TX): ${server.net.rx_errors}/${server.net.tx_errors}\n\n`; if (problem.causes && problem.causes.length) { reportContent += `[추정 원인]\n`; problem.causes.forEach((cause, causeIndex) => { reportContent += `${causeIndex + 1}. ${cause}\n`; }); reportContent += `\n`; } reportContent += `[해결 방법]\n`; const solutions = problem.solutions || [problem.solution || "해결 방법 데이터 없음"]; solutions.forEach((solution, solutionIndex) => { reportContent += `${solutionIndex + 1}. ${solution}\n`; }); reportContent += `\n`; if (server.errors && server.errors.length) { reportContent += `[오류 로그]\n`; server.errors.forEach((error, errorIndex) => { reportContent += `${errorIndex + 1}. ${error}\n`; }); reportContent += `\n`; } }); reportContent += `\n=========================================\n`; reportContent += `이 보고서는 OpenManager AI에 의해 자동 생성되었습니다.\n`; reportContent += `생성 일시: ${new Date().toLocaleString()}\n`; reportContent += `문의: support@openmanager.ai\n`; // 텍스트 파일로 다운로드 const blob = new Blob([reportContent], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } } // 데이터 프로세서 인스턴스 생성 window.addEventListener('DOMContentLoaded', () => { if (!window.aiProcessor) window.aiProcessor = new AIProcessor(); new MCPQueryManager(); }); // MCP 질문/응답 확장 기능 추가 class MCPQueryManager { constructor() { this.historyKey = 'mcpQueryHistory'; this.history = this.loadHistory(); this.context = null; this.mcpUrl = 'https://openmanager-vibe-v4.onrender.com/query'; // 실제 MCP 서버 주소로 교체 this.init(); } async init() { await this.loadContext(); this.initUI(); this.renderHistory(); } async loadContext() { try { const res = await fetch('/public/context/server-status.txt'); this.context = await res.text(); } catch (e) { this.context = ''; } } initUI() { const input = document.getElementById('queryInput'); const button = document.getElementById('ai-query-submit'); const result = document.getElementById('queryResultContent'); const resultBox = document.getElementById('queryResult'); const loading = document.getElementById('queryLoading'); const closeBtn = document.getElementById('closeQueryResult'); const exampleBtns = document.querySelectorAll('.example-query-btn'); if (input && button) { input.addEventListener('keypress', (e) => { if (e.key === 'Enter') this.handleQuery(input, result, resultBox, loading); }); button.addEventListener('click', () => this.handleQuery(input, result, resultBox, loading)); } if (closeBtn && resultBox) { closeBtn.addEventListener('click', () => { resultBox.classList.remove('active'); resultBox.style.display = 'none'; }); } if (exampleBtns) { exampleBtns.forEach(btn => { btn.addEventListener('click', () => { input.value = btn.textContent; this.handleQuery(input, result, resultBox, loading); }); }); } } async handleQuery(input, result, resultBox, loading) { const query = input.value?.trim(); if (!query || query === 'undefined') return; // 빈 입력 방지 loading.style.display = 'block'; resultBox.classList.remove('active'); resultBox.style.display = 'none'; try { // processQuery 대신 aiProcessor 인스턴스를 통해 호출 const answer = window.aiProcessor ? await window.aiProcessor.processQuery(query) : "AI 처리 컴포넌트를 찾을 수 없습니다."; if (!answer || answer === 'undefined' || answer.trim() === '') { result.innerHTML = '적절한 답변을 찾지 못했습니다.'; } else { result.innerHTML = window.marked ? window.marked.parse(answer) : answer; } resultBox.classList.add('active'); resultBox.style.display = 'block'; this.addHistory(query, answer); this.renderHistory(); } catch (e) { result.innerHTML = '질문 처리 중 오류가 발생했습니다.'; resultBox.classList.add('active'); resultBox.style.display = 'block'; } finally { loading.style.display = 'none'; input.value = ''; } } addHistory(query, answer) { this.history.unshift({ query, answer, ts: new Date().toISOString() }); if (this.history.length > 30) this.history.pop(); localStorage.setItem(this.historyKey, JSON.stringify(this.history)); } loadHistory() { try { return JSON.parse(localStorage.getItem(this.historyKey)) || []; } catch { return []; } } renderHistory() { const histBox = document.getElementById('queryHistory'); if (!histBox) return; histBox.innerHTML = this.history.map(item => ` <div class="query-item"> <div class="query-question"><i class="fas fa-user"></i> <span>${item.query}</span></div> <div class="query-answer markdown-content">${window.marked ? window.marked.parse(item.answer) : item.answer}</div> <div class="query-timestamp">${new Date(item.ts).toLocaleString()}</div> </div> `).join(''); } } window.addEventListener('DOMContentLoaded', () => { new MCPQueryManager(); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/skyasu2/openmanager-vibe-v4'

If you have feedback or need assistance with the MCP directory API, please join our Discord server