server_dashboard.html.bak•104 kB
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenManager AI - 서버 모니터링 대시보드</title>
<link rel="stylesheet" href="style.css">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- 부트스트랩 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 부트스트랩 아이콘 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
<!-- Font Awesome 아이콘 추가 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 차트 라이브러리 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* CSS 변수 정의 */
:root {
--primary: #4a69bd;
--primary-dark: #3a559d;
--primary-light: rgba(74, 105, 189, 0.3);
--background: #f5f6fa;
--surface: #ffffff;
--text: #333333;
--text-white: #ffffff;
--border: #e0e0e0;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--border-radius-sm: 4px;
--border-radius-md: 12px;
--border-radius-lg: 20px;
}
body {
font-family: 'Noto Sans KR', sans-serif;
background-color: #f5f6fa;
color: #333;
margin: 0;
padding: 0;
}
.dashboard-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.logo {
font-size: 1.8rem;
font-weight: 700;
color: #4a69bd;
}
/* 서버 카드 스타일 개선 */
.server-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.server-card {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 15px;
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.7) 100%);
backdrop-filter: blur(10px);
cursor: pointer;
height: auto;
min-height: 140px;
}
.server-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
border-color: rgba(0, 0, 0, 0.1);
}
.server-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 100%;
background: linear-gradient(90deg, #4a69bd, #2c3e50);
}
.server-card.status-warning::before {
background: linear-gradient(90deg, #ff8f00, #ffa000);
}
.server-card.status-critical::before {
background: linear-gradient(90deg, #d32f2f, #b71c1c);
}
/* 컴팩트 서버 카드 레이아웃 */
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.server-name {
font-size: 0.95rem;
font-weight: 600;
color: #2c3e50;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
.server-status {
font-size: 0.65rem;
padding: 2px 8px;
border-radius: 20px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-normal {
background-color: #e8f5e9;
color: #2e7d32;
}
.status-warning {
background-color: #fff8e1;
color: #ff8f00;
}
.status-critical {
background-color: #ffebee;
color: #d32f2f;
}
/* 컴팩트 메트릭 스타일 */
.server-metrics {
display: flex;
flex-direction: column;
gap: 8px;
}
.metric-item {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.metric-label {
font-size: 0.7rem;
color: #7f8c8d;
margin-right: 10px;
min-width: 80px;
display: flex;
align-items: center;
}
.metric-label::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
background-color: #4a69bd;
}
.metric-item:nth-child(2) .metric-label::before {
background-color: #3498db;
}
.metric-item:nth-child(3) .metric-label::before {
background-color: #2ecc71;
}
.metric-item:nth-child(4) .metric-label::before {
background-color: #9b59b6;
}
.metric-value {
font-size: 0.8rem;
font-weight: 600;
color: #2c3e50;
margin-right: 10px;
min-width: 45px;
}
.progress-bar-container {
flex-grow: 1;
height: 5px;
background-color: #ecf0f1;
border-radius: 5px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 6px;
transition: width 0.5s ease;
}
.progress-normal {
background: linear-gradient(90deg, #2ecc71, #27ae60);
}
.progress-warning {
background: linear-gradient(90deg, #f39c12, #e67e22);
}
.progress-critical {
background: linear-gradient(90deg, #e74c3c, #c0392b);
}
.main-content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
@media (max-width: 1100px) {
.main-content-grid {
grid-template-columns: 1fr;
}
}
.dashboard-section {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 20px;
margin-bottom: 20px;
}
.section-header {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
display: flex;
align-items: center;
}
.filter-container {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
padding: 20px;
margin-bottom: 20px;
}
.chart-container {
height: 300px;
position: relative;
}
/* AI 질의 컨테이너 */
.ai-query-container {
background-color: var(--surface);
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-md);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
transition: transform 0.3s ease;
}
.ai-query-container:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
.query-input {
width: 100%;
padding: 12px 15px;
border-radius: var(--border-radius-md);
border: 1px solid var(--border);
font-size: 16px;
transition: all 0.3s ease;
}
.query-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.query-loading {
display: none;
text-align: center;
margin: var(--spacing-md) 0;
}
.query-loading.active {
display: block;
}
.query-result {
display: none;
background-color: var(--background);
border-radius: var(--border-radius-md);
padding: var(--spacing-md);
margin-top: var(--spacing-md);
white-space: pre-line;
border: 1px solid var(--border);
}
.query-result.active {
display: block;
animation: fadeIn 0.3s ease-out forwards;
}
/* 반응형 */
@media (max-width: 768px) {
.header {
flex-direction: column;
padding: 10px;
}
.logo {
margin-bottom: 10px;
}
.nav-menu {
width: 100%;
justify-content: center;
}
.nav-item {
margin: 0 5px;
padding: 5px 10px;
font-size: 14px;
}
}
.error-badge {
display: inline-block;
margin-top: 10px;
padding: 3px 8px;
background-color: #ffebee;
color: #d32f2f;
border-radius: 4px;
font-size: 0.8rem;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(211, 47, 47, 0.3);
}
70% {
box-shadow: 0 0 0 5px rgba(211, 47, 47, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(211, 47, 47, 0);
}
}
.service-status-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.service-status-tag {
padding: 5px 10px;
border-radius: 4px;
font-size: 0.85rem;
display: inline-flex;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.2s ease;
}
.service-status-tag:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.service-status-tag.service-running {
background-color: #e8f5e9;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.service-status-tag.service-stopped {
background-color: #ffebee;
color: #d32f2f;
border: 1px solid #ffcdd2;
}
.status-indicator {
margin-left: 5px;
font-size: 0.75rem;
opacity: 0.8;
}
/* 장애 프리셋 스타일 */
.preset-container {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.preset-tag {
cursor: pointer;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
background: #f5f6fa;
border: 1px solid #e9ecef;
transition: all 0.2s ease;
}
.preset-tag:hover {
background: #e9ecef;
transform: translateY(-2px);
}
.preset-tag.tag-critical {
background-color: #ffebee;
color: #d32f2f;
border-color: #ffcdd2;
}
.preset-tag.tag-warning {
background-color: #fff8e1;
color: #ff8f00;
border-color: #ffecb3;
}
/* 자동 장애 보고서 스타일 개선 */
.auto-report-container {
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
margin-top: 20px;
background-color: #fff;
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
}
.auto-report-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 15px;
}
/* 추가적인 반응형 디자인 개선 */
/* FHD (1920x1080) */
@media (min-width: 1920px) {
.container-fluid {
max-width: 1800px;
margin: 0 auto;
}
.server-grid {
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
}
}
/* QHD (2560x1440) */
@media (min-width: 2560px) {
.container-fluid {
max-width: 2400px;
margin: 0 auto;
}
.server-grid {
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
}
.main-content-grid {
grid-template-columns: 3fr 1fr;
}
}
/* 태블릿 디바이스 */
@media (max-width: 992px) {
.main-content-grid {
grid-template-columns: 1fr;
}
.server-grid {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
}
/* 모바일 디바이스 */
@media (max-width: 768px) {
.server-grid {
grid-template-columns: 1fr;
}
.header {
flex-direction: column;
text-align: center;
}
.nav-menu {
margin-top: 10px;
}
.section-header {
flex-direction: column;
align-items: flex-start;
}
.section-header button {
margin-top: 10px;
margin-left: 0;
}
.card-body .row {
flex-direction: column;
}
.preset-container {
flex-direction: column;
}
.preset-tag {
width: 100%;
text-align: center;
margin-bottom: 8px;
}
}
/* 작은 모바일 화면 */
@media (max-width: 480px) {
.container-fluid {
padding: 10px;
}
h1.display-5 {
font-size: 1.8rem;
}
.dashboard-section {
padding: 15px;
}
.server-card {
padding: 15px;
}
.pagination-container {
flex-direction: column;
align-items: center;
}
#prevPageBtn, #nextPageBtn {
margin: 5px 0;
}
}
/* 자동 장애 보고서 반응형 조정 */
.auto-report-container {
max-height: 70vh;
overflow-y: auto;
}
@media (max-width: 768px) {
.auto-report-container {
max-height: 50vh;
}
}
@media (min-width: 1600px) {
.auto-report-container {
max-height: 90vh;
}
}
/* 서비스 태그 스타일 추가 */
.services-container {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.service-tag {
padding: 5px 10px;
border-radius: 15px;
font-size: 0.75rem;
display: inline-flex;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.2s ease;
}
.service-tag:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.service-tag.service-running {
background-color: #e8f5e9;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.service-tag.service-stopped {
background-color: #ffebee;
color: #d32f2f;
border: 1px solid #ffcdd2;
}
/* 반응형 그리드 개선 */
@media (min-width: 1400px) {
.server-grid {
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
}
@media (min-width: 1700px) {
.server-grid {
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
}
}
@media (min-width: 2000px) {
.server-grid {
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}
}
/* 오른쪽 장애 패널 개선 */
.incident-panel {
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
}
.collapsible-panel {
border: 1px solid #e0e0e0;
border-radius: 12px;
margin-bottom: 15px;
overflow: hidden;
}
.collapsible-header {
background-color: #f8f9fa;
padding: 12px 15px;
font-weight: 600;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
}
.collapsible-header:hover {
background-color: #f0f2f5;
}
.collapsible-body {
padding: 15px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.collapsible-body.expanded {
max-height: 500px;
}
.collapse-toggle {
color: #6c757d;
transition: transform 0.3s ease;
}
.collapse-toggle.expanded {
transform: rotate(180deg);
}
/* 사이드바 토글 버튼 */
.sidebar-toggle {
position: fixed;
right: 0;
top: 50%;
transform: translateY(-50%);
background-color: var(--primary);
color: white;
border: none;
border-radius: 4px 0 0 4px;
width: 30px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.main-content-grid {
position: relative;
transition: grid-template-columns 0.3s ease;
}
.main-content-grid.sidebar-hidden {
grid-template-columns: 1fr 0fr;
}
.main-content-grid.sidebar-hidden .incident-panel {
display: none;
}
/* 서비스 태그 간소화 */
.services-container {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.service-tag {
padding: 3px 6px;
border-radius: 4px;
font-size: 0.65rem;
display: inline-flex;
align-items: center;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
/* 모달 스타일 개선 */
.modal-content {
border: none;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.modal-header {
border-bottom: 1px solid #f0f0f0;
padding: 15px 20px;
background-color: #f8f9fa;
}
.modal-title {
font-weight: 600;
display: flex;
align-items: center;
}
.modal-title i {
margin-right: 10px;
color: var(--primary);
}
.modal-body {
padding: 20px;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 8px;
}
/* AI 자동 장애 보고서 리스트 아이템 스타일 */
.problem-item {
cursor: pointer;
transition: background-color 0.2s ease;
}
.problem-item:hover {
background-color: #f8f9fa; /* 약간 밝은 배경으로 호버 효과 */
}
.problem-item .problem-description {
font-weight: 500;
}
.problem-item .problem-solution {
font-size: 0.9em;
color: #555;
}
.problem-item .problem-severity-text {
/* 이미 부트스트랩 fw-bold가 적용됨 */
}
.list-group-item.severity-critical {
border-left: 5px solid var(--bs-danger, #dc3545);
background-color: rgba(220, 53, 69, 0.05); /* 매우 연한 빨강 배경 */
}
.list-group-item.severity-warning,
.list-group-item.severity-error { /* Error도 Warning과 동일하게 처리 */
border-left: 5px solid var(--bs-warning, #ffc107);
background-color: rgba(255, 193, 7, 0.05); /* 매우 연한 노랑 배경 */
}
#aiProblemList {
/* 스크롤바는 JS에서 maxHeight와 함께 overflowY: auto로 제어 */
}
</style>
</head>
<body>
<div class="loading" id="loadingIndicator">
<div class="loading-content">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">로딩 중...</span>
</div>
<div>데이터를 가져오는 중입니다...</div>
</div>
</div>
<header class="header">
<div class="logo">
<i class="fas fa-server"></i>
OpenManager AI
</div>
<nav class="nav-menu">
<a href="index.html" class="nav-item">소개</a>
<a href="server_dashboard.html" class="nav-item active">서버 모니터링</a>
</nav>
</header>
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col">
<h1 class="display-5 mb-0">서버 모니터링 대시보드</h1>
<p class="text-muted" id="timestamp">데이터 기준 시각: 로딩 중...</p>
</div>
</div>
<!-- AI 질의 영역을 최상단으로 이동 -->
<div class="row mb-4">
<div class="col">
<div class="ai-query-container">
<h5 class="mb-3"><i class="fas fa-robot me-2"></i>AI 서버 분석</h5>
<p class="text-muted mb-3">서버 상태나 성능에 대해 자연어로 질문해보세요. (예: "CPU 사용률이 높은 서버는?", "메모리 상태 알려줘")</p>
<div class="input-group mb-3">
<input type="text" class="form-control query-input" id="queryInput" placeholder="자연어로 질문하세요... (Enter 키를 눌러 질문)">
<button class="btn btn-primary ai-query-submit" id="ai-query-submit" type="button">
<i class="fas fa-paper-plane"></i> 질문하기
</button>
</div>
<!-- 장애 프리셋 추가 -->
<div class="preset-container">
<div class="preset-tag tag-critical" data-preset="CPU 사용률이 높은 서버 목록과 원인 분석">
<i class="fas fa-microchip me-1"></i> CPU 과부하 분석
</div>
<div class="preset-tag tag-warning" data-preset="메모리 사용량이 많은 서버 목록 및 조치방안">
<i class="fas fa-memory me-1"></i> 메모리 부족 분석
</div>
<div class="preset-tag tag-critical" data-preset="서비스가 중단된 서버 목록과 재시작 방법">
<i class="fas fa-exclamation-triangle me-1"></i> 서비스 중단 분석
</div>
<div class="preset-tag tag-warning" data-preset="디스크 공간이 부족한 서버 분석 및 해결방안">
<i class="fas fa-hdd me-1"></i> 디스크 공간 분석
</div>
<div class="preset-tag" data-preset="전체 서버 상태 요약 보고서 생성">
<i class="fas fa-file-alt me-1"></i> 전체 상태 요약
</div>
</div>
<div class="query-loading" id="queryLoading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">분석 중...</span>
</div>
<p class="mt-2">AI가 서버 데이터를 분석 중입니다...</p>
</div>
<div class="query-result" id="queryResult"></div>
</div>
</div>
</div>
<!-- 자동 장애 보고서 섹션 및 서버 현황 요약을 포함하는 incident-panel을 AI 질의 영역 아래로 이동 -->
<div class="incident-panel mt-4"> <!-- mt-4 클래스 추가하여 위쪽과 간격 조정 -->
<!-- AI 자동 장애 보고서 패널 -->
<div class="collapsible-panel" id="aiReportPanel"> <!-- 패널에 ID 추가 -->
<div class="collapsible-header" onclick="toggleCollapsible(this)">
<div><i class="fas fa-exclamation-triangle me-2 text-danger"></i> AI 자동 장애 보고서</div> <!-- 제목 수정 -->
<div class="collapse-toggle"><i class="fas fa-chevron-down"></i></div>
</div>
<div class="collapsible-body expanded"> <!-- 기본적으로 펼쳐진 상태로 시작 -->
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">감지된 문제</h6>
<button class="btn btn-sm btn-outline-primary" id="downloadAllReportsBtn">
<i class="bi bi-download"></i> 전체 보고서 다운로드
</button>
</div>
<div class="alert alert-info" id="aiProblemsLoading" style="display: none;"> <!-- ID 변경 및 초기 숨김 -->
<div class="spinner-border spinner-border-sm text-primary me-2" role="status">
<span class="visually-hidden">로딩 중...</span>
</div>
AI가 문제를 분석 중입니다...
</div>
<div id="aiProblemsEmpty" style="display: none;" class="text-center py-4">
<i class="fas fa-check-circle text-success" style="font-size: 2rem;"></i>
<p class="mt-3 mb-0\">현재 감지된 문제가 없습니다.</p>
</div>
<ul class="list-group" id="aiProblemList">
<!-- AI 문제 항목들이 여기에 추가됩니다 -->
</ul>
<div class="mt-3 text-center" id="aiProblemListToggle" style="display: none;">
<button class="btn btn-sm btn-outline-secondary" id="toggleAiProblemListBtn" data-expanded="false">
더 보기 (0개 더 있음)
</button>
</div>
</div>
</div>
<!-- 서버 현황 요약 패널 (기존 incident-panel 내 다른 패널이 있다면 여기에 유지 또는 추가) -->
<div class="collapsible-panel mt-3" id="serverStatusSummaryPanel">
<div class="collapsible-header" onclick="toggleCollapsible(this)">
<div><i class="fas fa-tasks me-2 text-primary"></i> 서버 현황 요약</div>
<div class="collapse-toggle"><i class="fas fa-chevron-down"></i></div>
</div>
<div class="collapsible-body expanded">
<div id="statusSummaryContainer">
<!-- 요약 정보가 여기에 동적으로 채워집니다 (data_processor.js) -->
<p>요약 정보를 불러오는 중...</p>
</div>
</div>
</div>
</div>
<!-- 메인 컨텐츠 그리드에서 incident-panel 제거하고, 서버 그리드 및 필터만 남김 -->
<div class="main-content-grid mt-4" id="mainContentGrid">
<!-- 좌측: 서버 그리드 및 검색 필터 -->
<div>
<div class="card search-container mb-4">
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3 mb-md-0">
<div class="input-group">
<input type="text" class="form-control" id="searchInput" placeholder="호스트명 검색...">
<button class="btn btn-refresh" type="button" id="refreshBtn">
<i class="fas fa-sync-alt me-1"></i> 새로고침
</button>
</div>
</div>
<div class="col-md-3 mb-3 mb-md-0">
<select class="form-select" id="statusFilter">
<option value="all">모든 상태</option>
<option value="critical">Critical</option>
<option value="warning">Warning</option>
<option value="normal">Normal</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select" id="pageSize">
<option value="10">10개씩 보기</option>
<option value="20">20개씩 보기</option>
<option value="50">50개씩 보기</option>
</select>
</div>
</div>
</div>
</div>
<!-- 서버 카드 그리드 -->
<div class="server-grid" id="serverGrid"></div>
<!-- 페이징 -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="text-muted" id="serverCount">전체 0 서버 중 0-0 표시 중</div>
<div class="pagination-container">
<button class="btn btn-sm btn-outline-secondary" id="prevPageBtn">
<i class="fas fa-chevron-left"></i> 이전
</button>
<span class="mx-3" id="currentPage">1 / 1</span>
<button class="btn btn-sm btn-outline-secondary" id="nextPageBtn">
다음 <i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
<!-- 우측: 자동 장애 보고서 (접이식 패널) -->
<!-- <div class="incident-panel"> 이 부분은 위로 이동되었음 -->
<!-- 이전에 여기에 있던 다른 패널들이 있다면, 위로 옮겨진 incident-panel 내부로 통합하거나, 별도 처리 필요 -->
<!-- </div> -->
</div>
<!-- 서버 상세 정보 모달 -->
<div class="modal fade" id="serverDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalServerName">서버 상세 정보</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row mb-4">
<!-- 시스템 정보 -->
<div class="col-md-6">
<h6 class="mb-3">시스템 정보</h6>
<table class="table table-sm">
<tbody id="systemInfoTable">
<tr>
<th>OS</th>
<td id="modalOS">-</td>
</tr>
<tr>
<th>가동 시간</th>
<td id="modalUptime">-</td>
</tr>
<tr>
<th>프로세스 수</th>
<td id="modalProcessCount">-</td>
</tr>
<tr>
<th>좀비 프로세스</th>
<td id="modalZombieCount">-</td>
</tr>
<tr>
<th>로드 평균 (1분)</th>
<td id="modalLoadAvg">-</td>
</tr>
<tr>
<th>마지막 업데이트</th>
<td id="modalLastUpdate">-</td>
</tr>
</tbody>
</table>
</div>
<!-- 리소스 현황 -->
<div class="col-md-6">
<h6 class="mb-3">리소스 현황</h6>
<div class="chart-container">
<canvas id="resourceBarChart"></canvas>
</div>
</div>
</div>
<div class="row mb-4">
<!-- 네트워크 정보 -->
<div class="col-md-6">
<h6 class="mb-3">네트워크 정보</h6>
<table class="table table-sm">
<tbody id="networkInfoTable">
<tr>
<th>인터페이스</th>
<td id="modalNetInterface">-</td>
</tr>
<tr>
<th>수신 바이트</th>
<td id="modalRxBytes">-</td>
</tr>
<tr>
<th>송신 바이트</th>
<td id="modalTxBytes">-</td>
</tr>
<tr>
<th>수신 오류</th>
<td id="modalRxErrors">-</td>
</tr>
<tr>
<th>송신 오류</th>
<td id="modalTxErrors">-</td>
</tr>
</tbody>
</table>
</div>
<!-- 서비스 상태 -->
<div class="col-md-6">
<h6 class="mb-3">서비스 상태</h6>
<div id="modalServiceStatus" class="service-status-container">
<!-- 서비스 상태 태그들이 여기에 추가됨 -->
</div>
</div>
</div>
<!-- 오류 메시지 -->
<div class="row mb-4">
<div class="col-12">
<h6 class="mb-3">오류 메시지</h6>
<div id="modalErrorsContainer" class="alert alert-info">
현재 보고된 오류가 없습니다.
</div>
</div>
</div>
<!-- 24시간 리소스 사용 추이 -->
<div class="row">
<div class="col-12">
<h6 class="mb-3">24시간 리소스 사용 추이</h6>
<div class="chart-container" style="height: 300px;">
<canvas id="resourceHistoryChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 스크립트는 순서가 중요: 먼저 dummy_data_generator.js, 그 다음 ai_processor.js 로드 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="dummy_data_generator.js"></script>
<script src="ai_processor.js"></script>
<script>
// 전역 변수
let serverData = [];
let currentSort = { column: 'hostname', ascending: true };
let charts = {};
// 페이지네이션 변수
let currentPage = 1;
let pageSize = 10;
let filteredData = [];
// 초기화 함수
function init() {
// 서버 데이터 로드
loadServerData();
// 검색 및 필터링 이벤트 리스너 추가
document.getElementById('searchInput').addEventListener('keyup', function() { // 익명 함수로 감싸서 호출
if (window.dataProcessor) {
window.dataProcessor.searchQuery = this.value.toLowerCase();
window.dataProcessor.currentPage = 1;
window.dataProcessor.applyFiltersAndSort();
}
});
document.getElementById('statusFilter').addEventListener('change', function() { // 익명 함수로 감싸서 호출
if (window.dataProcessor) {
// dataProcessor의 필터 상태를 직접 업데이트하는 로직이 필요할 수 있음
// 여기서는 applyFiltersAndSort가 내부적으로 필터 값을 읽는다고 가정
window.dataProcessor.currentFilter = this.value; // 예시: statusFilter의 value를 직접 사용
window.dataProcessor.currentPage = 1;
window.dataProcessor.applyFiltersAndSort();
}
});
document.getElementById('pageSize').addEventListener('change', function() {
currentPage = 1; // 이 currentPage는 전역 변수를 의미하는 것으로 보임
// dataProcessor.itemsPerPage도 업데이트 필요
if(window.dataProcessor) {
window.dataProcessor.itemsPerPage = parseInt(this.value);
window.dataProcessor.currentPage = 1;
window.dataProcessor.applyFiltersAndSort();
}
});
// 페이지 이동 버튼
document.getElementById('prevPageBtn').addEventListener('click', function() {
// currentPage는 전역 변수를 사용하고, dataProcessor의 currentPage와 동기화 필요
if (currentPage > 1) {
currentPage--;
if(window.dataProcessor) {
window.dataProcessor.currentPage = currentPage;
window.dataProcessor.applyFiltersAndSort();
}
}
});
document.getElementById('nextPageBtn').addEventListener('click', function() {
// filteredData는 html 로컬 변수, dataProcessor.filteredData와 동기화 또는 직접 사용 필요
// 여기서는 dataProcessor의 상태를 기준으로 동작하도록 수정
if(window.dataProcessor && window.dataProcessor.currentPage < Math.ceil(window.dataProcessor.filteredData.length / window.dataProcessor.itemsPerPage)) {
currentPage++; // 로컬 currentPage도 업데이트
window.dataProcessor.currentPage = currentPage;
window.dataProcessor.applyFiltersAndSort();
}
});
// 새로고침 버튼
document.getElementById('refreshBtn').addEventListener('click', loadServerData); // loadServerData는 dataProcessor.loadData() 호출로 이어질 것
// AI 질의 처리
document.getElementById('queryInput').addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
handleQuery(this.value); // processAIQuery -> handleQuery
}
});
document.getElementById('ai-query-submit').addEventListener('click', function() { // 익명 함수로 감싸서 호출
handleQuery(document.getElementById('queryInput').value); // processAIQuery -> handleQuery
});
// 전체 보고서 다운로드 버튼
document.getElementById('downloadAllReportsBtn').addEventListener('click', downloadAllReports);
// 장애 프리셋 이벤트 추가
document.querySelectorAll('.preset-tag').forEach(tag => {
tag.addEventListener('click', function() {
const preset = this.dataset.preset;
document.getElementById('queryInput').value = preset;
handleQuery(preset); // processAIQuery -> handleQuery
});
});
// 사이드바 토글 버튼 이벤트 리스너
// document.getElementById('sidebarToggle').addEventListener('click', toggleSidebar); // sidebarToggle ID가 현재 HTML에 없음
// 저장된 사이드바 상태 복원
// const savedSidebarState = localStorage.getItem('sidebar_state');
// if (savedSidebarState === 'hidden') {
// toggleSidebar();
// }
// 저장된 패널 상태 복원 (페이지 로드 시 각 패널 헤더에 대해 실행)
document.querySelectorAll('.collapsible-header').forEach(header => {
// const panelId = header.textContent.trim(); // 불안정한 방식
const panelBody = header.nextElementSibling;
if (panelBody && panelBody.parentElement && panelBody.parentElement.id) {
const panelId = panelBody.parentElement.id; // 부모 .collapsible-panel의 ID를 사용
const savedState = localStorage.getItem(`panel_${panelId}_state`); // 키 이름 일관성 유지
if (savedState === 'collapsed' && panelBody.classList.contains('expanded')) {
// 현재 펼쳐져 있는데 저장된 상태가 'collapsed'이면 토글
toggleCollapsible(header);
} else if (savedState === 'expanded' && !panelBody.classList.contains('expanded')) {
// 현재 접혀 있는데 저장된 상태가 'expanded'이면 토글
toggleCollapsible(header);
}
}
});
}
// window.serverData가 생성될 때까지 대기
function checkServerData() {
// 새로 추가: 초기화 시간을 추적하기 위한 정적 변수
if (typeof checkServerData.startTime === 'undefined') {
checkServerData.startTime = Date.now();
}
// 새로 추가: 10초 제한시간 설정
const timeoutMs = 10000; // 10초
const elapsedTime = Date.now() - checkServerData.startTime;
if (window.serverData && Array.isArray(window.serverData) && window.serverData.length > 0) {
serverData = window.serverData;
processServerData();
document.getElementById('loadingIndicator').style.display = 'none';
} else if (elapsedTime > timeoutMs) {
// 제한시간 초과 시 빈 배열로 초기화하고 계속 진행
console.warn('서버 데이터 로드 타임아웃. 빈 데이터로 초기화합니다.');
window.serverData = [];
serverData = [];
processServerData();
document.getElementById('loadingIndicator').style.display = 'none';
} else {
// 로딩 메시지 업데이트 (사용자에게 진행상황 알림)
const loadingText = document.querySelector('.loading-content div:last-child');
if (loadingText) {
const dots = '.'.repeat((new Date().getSeconds() % 3) + 1);
loadingText.textContent = `서버 데이터를 가져오는 중입니다${dots}`;
}
// 데이터가 아직 없으면 100ms 후 재시도
setTimeout(checkServerData, 100);
}
}
// 서버 데이터 처리
function processServerData() {
// applyFilters(); // 이 호출은 data_processor.js의 handleDataUpdate 내부 applyFiltersAndSort로 대체됨
updatePagination(); // 이 함수도 data_processor.js의 updatePagination으로 대체되었는지 확인 필요
renderServerTable(); // 이 함수도 data_processor.js의 updateServerGrid으로 대체되었는지 확인 필요
updateCharts();
updateTimestamp();
// updateStatusCounts(); // 상태 요약 카운트 업데이트 - 이 줄을 주석 처리 (data_processor.js의 updateGlobalStatusSummary로 대체됨)
createStatusChart(); // 상태 요약 차트 생성 - 이 함수도 data_processor.js로 옮겨졌거나 대체되었는지 확인 필요
// generateIncidentReports(); // 장애 보고서 자동 생성 - 이 함수 내 generateIncidentAccordionItems 호출로 인해 오류 발생. data_processor.js의 updateProblemsList로 대체됨.
}
// 서버 상태 요약 카운트 업데이트 함수
function updateStatusCounts() {
if (!serverData || serverData.length === 0) return;
const criticalCount = serverData.filter(server =>
server.cpu_usage >= 90 ||
server.memory_usage_percent >= 90 ||
server.disk[0].disk_usage_percent >= 90
).length;
const warningCount = serverData.filter(server =>
(server.cpu_usage >= 70 && server.cpu_usage < 90) ||
(server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) ||
(server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90)
).length;
const normalCount = serverData.length - criticalCount - warningCount;
// 카운트 업데이트
document.getElementById('normalCount').textContent = normalCount;
document.getElementById('warningCount').textContent = warningCount;
document.getElementById('criticalCount').textContent = criticalCount;
}
// 상태 차트 생성 함수
function createStatusChart() {
const ctx = document.getElementById('statusChart');
if (!ctx) return;
// 이미 차트가 존재하면 제거
if (charts.statusChart) {
charts.statusChart.destroy();
}
// 상태별 서버 수 계산
const criticalCount = serverData.filter(server =>
server.cpu_usage >= 90 ||
server.memory_usage_percent >= 90 ||
server.disk[0].disk_usage_percent >= 90
).length;
const warningCount = serverData.filter(server =>
(server.cpu_usage >= 70 && server.cpu_usage < 90) ||
(server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) ||
(server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90)
).length;
const normalCount = serverData.length - criticalCount - warningCount;
// 차트 생성
charts.statusChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['정상', '경고', '심각'],
datasets: [{
data: [normalCount, warningCount, criticalCount],
backgroundColor: ['#28a745', '#ffc107', '#dc3545'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// 리소스 개요 차트 생성 함수 (CPU, 메모리)
function updateCharts() {
if (!serverData || serverData.length === 0) return;
// CPU 사용량 차트
createResourceChart('cpuUsageChart', 'CPU 사용량', serverData.map(s => s.cpu_usage));
// 메모리 사용량 차트
createResourceChart('memoryUsageChart', '메모리 사용량', serverData.map(s => s.memory_usage_percent));
}
// 리소스 차트 생성 함수
function createResourceChart(canvasId, label, dataValues) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
// 기존 차트 제거
if (charts[canvasId]) {
charts[canvasId].destroy();
}
// 서버 이름과 값 페어로 정렬
const serverValuePairs = serverData.map((server, index) => ({
name: server.hostname,
value: dataValues[index]
})).sort((a, b) => b.value - a.value).slice(0, 10);
// 차트 생성
charts[canvasId] = new Chart(canvas, {
type: 'bar',
data: {
labels: serverValuePairs.map(pair => pair.name),
datasets: [{
label: label,
data: serverValuePairs.map(pair => pair.value),
backgroundColor: serverValuePairs.map(pair => {
if (pair.value >= 90) return '#dc3545';
if (pair.value >= 70) return '#ffc107';
return '#28a745';
}),
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: '사용률 (%)'
}
},
x: {
ticks: {
autoSkip: false,
maxRotation: 45,
minRotation: 45
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
// 필터 적용
function applyFilters() {
// 상태 필터링
const statusFilter = document.getElementById('statusFilter').value;
filteredData = serverData;
if (statusFilter !== 'all') {
filteredData = serverData.filter(server => {
if (statusFilter === 'critical' && (server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90)) {
return true;
} else if (statusFilter === 'warning' &&
((server.cpu_usage >= 70 && server.cpu_usage < 90) ||
(server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) ||
(server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90))) {
return true;
} else if (statusFilter === 'normal' &&
(server.cpu_usage < 70 && server.memory_usage_percent < 70 && server.disk[0].disk_usage_percent < 70)) {
return true;
}
return false;
});
}
// 검색 필터링
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
if (searchTerm) {
filteredData = filteredData.filter(server =>
server.hostname.toLowerCase().includes(searchTerm)
);
}
// 정렬 적용
filteredData.sort((a, b) => {
let valueA = a[currentSort.column];
let valueB = b[currentSort.column];
if (currentSort.column === 'disk_usage_percent') {
valueA = a.disk[0].disk_usage_percent;
valueB = b.disk[0].disk_usage_percent;
}
if (currentSort.ascending) {
return valueA > valueB ? 1 : -1;
} else {
return valueA < valueB ? 1 : -1;
}
});
// 페이지 리셋 (필터가 변경됐을 때)
currentPage = 1;
updatePagination();
updateServerCount(filteredData.length);
}
// 페이지네이션 컨트롤 업데이트
function updatePagination() {
const totalPages = Math.ceil(filteredData.length / pageSize);
const prevPageBtn = document.getElementById('prevPageBtn');
const nextPageBtn = document.getElementById('nextPageBtn');
prevPageBtn.disabled = currentPage === 1;
nextPageBtn.disabled = currentPage === totalPages || totalPages === 0;
// 페이지 정보 업데이트
const startIndex = (currentPage - 1) * pageSize + 1;
const endIndex = Math.min(currentPage * pageSize, filteredData.length);
document.getElementById('currentPage').textContent =
filteredData.length > 0 ?
`${startIndex}-${endIndex} / ${filteredData.length} 서버` :
'0 / 0 서버';
}
// 데이터 로드 함수
function loadServerData() {
document.getElementById('loadingIndicator').style.display = 'flex';
// 최신 serverData 사용
if (window.serverData && Array.isArray(window.serverData)) {
serverData = window.serverData;
processServerData();
}
document.getElementById('loadingIndicator').style.display = 'none';
}
// window.serverData 갱신 이벤트 리스너
window.addEventListener('serverDataUpdated', function(e) {
serverData = e.detail;
processServerData();
});
// 사용률에 따른 색상 클래스 반환
function getUsageColorClass(usage) {
if (usage >= 80) return 'usage-high';
if (usage >= 60) return 'usage-medium';
return 'usage-low';
}
// 테이블 정렬 함수
function sortTable(column) {
if (currentSort.column === column) {
currentSort.ascending = !currentSort.ascending;
} else {
currentSort.column = column;
currentSort.ascending = true;
}
applyFilters();
renderServerTable();
}
// 서버 테이블 렌더링 함수
function renderServerTable() {
const serverGrid = document.getElementById('serverGrid');
if (!serverGrid) return;
serverGrid.innerHTML = '';
if (!filteredData || filteredData.length === 0) {
serverGrid.innerHTML = `<div class="text-center py-4">표시할 서버 데이터가 없습니다.</div>`;
return;
}
// 현재 페이지에 해당하는 서버만 표시
const startIndex = (currentPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, filteredData.length);
const currentPageData = filteredData.slice(startIndex, endIndex);
// 서버 카드 생성 (컴팩트 형태)
currentPageData.forEach(server => {
const statusClass = getServerStatusClass(server);
const statusText = getServerStatusText(server);
const serverCard = document.createElement('div');
serverCard.className = `server-card ${statusClass}`;
serverCard.onclick = () => showServerDetail(server);
serverCard.innerHTML = `
<div class="server-header">
<div class="server-name"><i class="fas fa-server me-1"></i>${server.hostname}</div>
<div class="server-status ${statusClass}">${statusText}</div>
</div>
<div class="server-metrics">
<div class="metric-item">
<div class="metric-label"><i class="fas fa-microchip me-1"></i>CPU</div>
<div class="metric-value">${server.cpu_usage.toFixed(1)}%</div>
<div class="progress-bar-container">
<div class="progress-bar ${getProgressClass(server.cpu_usage)}" style="width: ${server.cpu_usage}%"></div>
</div>
</div>
<div class="metric-item">
<div class="metric-label"><i class="fas fa-memory me-1"></i>메모리</div>
<div class="metric-value">${server.memory_usage_percent.toFixed(1)}%</div>
<div class="progress-bar-container">
<div class="progress-bar ${getProgressClass(server.memory_usage_percent)}" style="width: ${server.memory_usage_percent}%"></div>
</div>
</div>
<div class="metric-item">
<div class="metric-label"><i class="fas fa-hdd me-1"></i>디스크</div>
<div class="metric-value">${server.disk[0].disk_usage_percent.toFixed(1)}%</div>
<div class="progress-bar-container">
<div class="progress-bar ${getProgressClass(server.disk[0].disk_usage_percent)}" style="width: ${server.disk[0].disk_usage_percent}%"></div>
</div>
</div>
</div>
${server.errors.length > 0 ? `<div class="error-badge"><i class="fas fa-exclamation-triangle me-1"></i>${server.errors.length}개 오류</div>` : ''}
`;
serverGrid.appendChild(serverCard);
});
}
// 접이식 패널 토글 함수
function toggleCollapsible(header) {
const body = header.nextElementSibling;
const icon = header.querySelector('.collapse-toggle i');
body.classList.toggle('expanded');
icon.classList.toggle('fa-chevron-down');
icon.classList.toggle('fa-chevron-up');
// 각 패널의 확장 상태를 로컬 스토리지에 저장
// const panelId = header.textContent.trim(); // 불안정한 방식
let panelId = null;
if (body && body.parentElement && body.parentElement.id) {
panelId = body.parentElement.id; // 부모 .collapsible-panel의 ID를 사용
localStorage.setItem(`panel_${panelId}_state`, body.classList.contains('expanded') ? 'expanded' : 'collapsed');
}
}
// 사이드바 토글 함수
function toggleSidebar() {
const mainContentGrid = document.getElementById('mainContentGrid');
mainContentGrid.classList.toggle('sidebar-hidden');
const toggleButton = document.getElementById('sidebarToggle');
const icon = toggleButton.querySelector('i');
if (mainContentGrid.classList.contains('sidebar-hidden')) {
icon.classList.remove('fa-chevron-left');
icon.classList.add('fa-chevron-right');
} else {
icon.classList.remove('fa-chevron-right');
icon.classList.add('fa-chevron-left');
}
// 사이드바 상태 저장
localStorage.setItem('sidebar_state', mainContentGrid.classList.contains('sidebar-hidden') ? 'hidden' : 'visible');
}
// 서비스 태그 렌더링 함수
function renderServiceTags(services) {
return Object.entries(services)
.map(([service, status]) =>
`<span class="service-tag ${status === 'running' ? 'service-running' : 'service-stopped'}">${service} (${status})</span>`
)
.join('');
}
// 서버 상태에 따른 클래스 반환
function getServerStatusClass(server) {
if (server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90) {
return 'status-critical';
} else if (server.cpu_usage >= 70 || server.memory_usage_percent >= 70 || server.disk[0].disk_usage_percent >= 70) {
return 'status-warning';
} else {
return 'status-normal';
}
}
// 서버 상태 텍스트 반환
function getServerStatusText(server) {
if (server.cpu_usage >= 90 || server.memory_usage_percent >= 90 || server.disk[0].disk_usage_percent >= 90) {
return '심각';
} else if (server.cpu_usage >= 70 || server.memory_usage_percent >= 70 || server.disk[0].disk_usage_percent >= 70) {
return '경고';
} else {
return '정상';
}
}
// 진행률 바 클래스 반환
function getProgressClass(value) {
if (value >= 90) {
return 'progress-critical';
} else if (value >= 70) {
return 'progress-warning';
} else {
return 'progress-normal';
}
}
// 서버 상세 정보 표시 함수
function showServerDetail(server) {
// 모달 제목 설정
const statusClass = getServerStatusClass(server);
const statusText = getServerStatusText(server);
document.getElementById('modalServerName').innerHTML = `
<i class="fas fa-server"></i>
${server.hostname} <span class="badge ${statusClass === 'status-normal' ? 'bg-success' : (statusClass === 'status-warning' ? 'bg-warning' : 'bg-danger')}">${statusText}</span>
`;
// 시스템 정보 설정
document.getElementById('modalOS').textContent = server.os || '-';
document.getElementById('modalUptime').textContent = server.uptime || '-';
document.getElementById('modalProcessCount').textContent = server.process_count || '0';
document.getElementById('modalZombieCount').textContent = server.zombie_count || '0';
document.getElementById('modalLoadAvg').textContent = server.load_avg_1m || '0';
document.getElementById('modalLastUpdate').textContent = new Date().toLocaleString('ko-KR');
// 네트워크 정보 설정
const bytesToGB = bytes => (bytes / (1024 * 1024 * 1024)).toFixed(2);
document.getElementById('modalNetInterface').textContent = server.net.interface || 'eth0';
document.getElementById('modalRxBytes').textContent = `${bytesToGB(server.net.rx_bytes)} GB`;
document.getElementById('modalTxBytes').textContent = `${bytesToGB(server.net.tx_bytes)} GB`;
document.getElementById('modalRxErrors').textContent = server.net.rx_errors || '0';
document.getElementById('modalTxErrors').textContent = server.net.tx_errors || '0';
// 서비스 상태 설정
const serviceStatusContainer = document.getElementById('modalServiceStatus');
serviceStatusContainer.innerHTML = '';
Object.entries(server.services).forEach(([service, status]) => {
const serviceTag = document.createElement('div');
serviceTag.className = `service-status-tag ${status === 'running' ? 'service-running' : 'service-stopped'}`;
serviceTag.innerHTML = `${service} <span class="status-indicator">${status}</span>`;
serviceStatusContainer.appendChild(serviceTag);
});
// 오류 메시지 설정
const errorsContainer = document.getElementById('modalErrorsContainer');
if (server.errors && server.errors.length > 0) {
errorsContainer.className = 'alert alert-danger';
errorsContainer.innerHTML = `
<ul class="mb-0">
${server.errors.map(error => `<li>${error}</li>`).join('')}
</ul>
`;
} else {
errorsContainer.className = 'alert alert-info';
errorsContainer.innerHTML = '현재 보고된 오류가 없습니다.';
}
// 리소스 현황 차트 생성
createResourceBarChart(server);
// 리소스 사용 추이 차트 생성
createResourceHistoryChart(server);
// 모달 표시
const modal = new bootstrap.Modal(document.getElementById('serverDetailModal'));
modal.show();
}
// 리소스 현황 차트 생성 함수
function createResourceBarChart(server) {
const ctx = document.getElementById('resourceBarChart');
// 기존 차트가 있으면 제거
if (charts.resourceBar) {
charts.resourceBar.destroy();
}
// 리소스 데이터
const data = {
labels: ['CPU', '메모리', '디스크'],
datasets: [{
label: '사용률 (%)',
data: [
server.cpu_usage,
server.memory_usage_percent,
server.disk[0].disk_usage_percent
],
backgroundColor: [
getResourceBarColor(server.cpu_usage),
getResourceBarColor(server.memory_usage_percent),
getResourceBarColor(server.disk[0].disk_usage_percent)
],
borderWidth: 0
}]
};
// 차트 옵션
const options = {
indexAxis: 'y',
plugins: {
legend: {
display: false
}
},
scales: {
x: {
beginAtZero: true,
max: 100,
grid: {
display: true
}
},
y: {
grid: {
display: false
}
}
}
};
// 차트 생성
charts.resourceBar = new Chart(ctx, {
type: 'bar',
data: data,
options: options
});
}
// 리소스 사용 추이 차트 생성 함수
function createResourceHistoryChart(server) {
const ctx = document.getElementById('resourceHistoryChart');
// 기존 차트가 있으면 제거
if (charts.resourceHistory) {
charts.resourceHistory.destroy();
}
// 가상의 시계열 데이터 생성 (실제로는 서버에서 가져와야 함)
const timePoints = [];
const cpuData = [];
const memoryData = [];
const diskData = [];
// 지난 24시간의 데이터 포인트 생성 (1시간 간격)
const now = new Date();
for (let i = 24; i >= 0; i--) {
const time = new Date(now.getTime() - i * 60 * 60 * 1000);
timePoints.push(time.getHours() + ':' + (time.getMinutes() < 10 ? '0' : '') + time.getMinutes());
// 가상의 데이터 포인트 (실제 구현에서는 서버에서 가져와야 함)
// 약간의 변동을 주기 위해 기본값에 랜덤 값을 더함
cpuData.push(Math.max(10, Math.min(100, server.cpu_usage + (Math.random() * 20 - 10))));
memoryData.push(Math.max(10, Math.min(100, server.memory_usage_percent + (Math.random() * 20 - 10))));
diskData.push(Math.max(10, Math.min(100, server.disk[0].disk_usage_percent + (Math.random() * 10 - 5))));
}
// 차트 데이터
const data = {
labels: timePoints,
datasets: [
{
label: 'CPU',
data: cpuData,
borderColor: '#FF6384',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
borderWidth: 2,
tension: 0.4
},
{
label: '메모리',
data: memoryData,
borderColor: '#36A2EB',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
borderWidth: 2,
tension: 0.4
},
{
label: '디스크',
data: diskData,
borderColor: '#4BC0C0',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
borderWidth: 2,
tension: 0.4
}
]
};
// 차트 옵션
const options = {
plugins: {
legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
title: {
display: true,
text: '사용률 (%)'
}
},
x: {
title: {
display: true,
text: '시간'
}
}
},
responsive: true,
maintainAspectRatio: false
};
// 차트 생성
charts.resourceHistory = new Chart(ctx, {
type: 'line',
data: data,
options: options
});
}
// 리소스 바 색상 결정 함수
function getResourceBarColor(value) {
if (value >= 90) return '#dc3545'; // 빨간색 (심각)
if (value >= 70) return '#ffc107'; // 노란색 (주의)
return '#28a745'; // 녹색 (정상)
}
// 서버 상세 정보 닫기 함수 제거
// 타임스탬프 업데이트 함수
function updateTimestamp() {
const timestampEl = document.getElementById('timestamp');
if (timestampEl && serverData.length > 0) {
const now = new Date();
timestampEl.textContent = `데이터 기준 시각: ${now.toLocaleString('ko-KR')}`;
}
}
// 서버 수 업데이트 함수
function updateServerCount(count = null) {
const serverCountEl = document.getElementById('serverCount');
if (serverCountEl) {
serverCountEl.textContent = `${count || serverData.length} 서버`;
}
}
// AI 질의 처리
async function handleQuery(queryText) {
if (!queryText.trim()) return;
const queryLoading = document.getElementById('queryLoading');
const queryResult = document.getElementById('queryResult');
// 요소가 존재하는지 확인 후 처리
if (queryLoading) queryLoading.classList.add('active');
if (queryResult) {
queryResult.classList.remove('active');
queryResult.style.display = 'block'; // 항상 보이게
}
try {
if (typeof window.processQuery !== 'function') {
throw new Error('AI 처리 함수가 로드되지 않았습니다.');
}
const response = await window.processQuery(queryText);
if (queryResult) {
queryResult.innerHTML = response;
queryResult.classList.add('active');
}
} catch (error) {
console.error("AI 질의 처리 오류:", error);
if (queryResult) {
queryResult.innerHTML = `<div class="alert alert-danger">처리 중 오류가 발생했습니다: ${error.message}</div>`;
queryResult.classList.add('active');
}
} finally {
if (queryLoading) queryLoading.classList.remove('active');
}
}
// 이벤트 리스너 설정
function setupEventListeners() {
// 새로고침 버튼
document.getElementById('refreshBtn').addEventListener('click', loadServerData);
// 검색 입력
document.getElementById('searchInput').addEventListener('input', function() {
applyFilters();
renderServerTable();
});
// 상태 필터
document.getElementById('statusFilter').addEventListener('change', function() {
applyFilters();
renderServerTable();
});
// 페이지 사이즈 변경
document.getElementById('pageSize').addEventListener('change', function() {
pageSize = parseInt(this.value);
currentPage = 1; // 페이지 리셋
applyFilters();
renderServerTable();
});
// 이전 페이지 버튼
document.getElementById('prevPageBtn').addEventListener('click', function() {
if (currentPage > 1) {
currentPage--;
updatePagination();
renderServerTable();
}
});
// 다음 페이지 버튼
document.getElementById('nextPageBtn').addEventListener('click', function() {
const totalPages = Math.ceil(filteredData.length / pageSize);
if (currentPage < totalPages) {
currentPage++;
updatePagination();
renderServerTable();
}
});
// AI 질의 입력 - Enter 키 및 버튼 클릭 지원
const queryInput = document.getElementById('queryInput');
if (queryInput) {
queryInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
handleQuery(this.value); // init 함수와 동일하게 수정
}
});
// 버튼 클릭도 지원
const queryBtn = document.getElementById('ai-query-submit');
if (queryBtn) {
queryBtn.addEventListener('click', function() {
handleQuery(queryInput.value); // init 함수와 동일하게 수정
});
}
}
// 보고서 생성 버튼 - generate-report-btn ID가 현재 HTML에 없음.
// document.getElementById('generate-report-btn').addEventListener('click', generateIncidentReports);
// 전체 보고서 다운로드 버튼
const downloadAllBtn = document.getElementById('downloadAllReportsBtn'); // download-all-reports-btn -> downloadAllReportsBtn
if (downloadAllBtn) {
downloadAllBtn.addEventListener('click', downloadAllReports);
}
// 페이지 로드 시 보고서 자동 생성 (3초 후)
// setTimeout(generateIncidentReports, 3000); // generateIncidentReports는 이제 직접 사용하지 않음
// 10분마다 자동 갱신
// setInterval(loadServerData, 10 * 60 * 1000); // 데이터는 dummy_data_generator에서 업데이트 후 이벤트 발생시킴
}
// 장애 보고서 관련 함수들
// 장애 보고서 생성 함수
function generateIncidentReports() {
// 문제가 있는 서버 찾기
const criticalServers = serverData.filter(server =>
server.cpu_usage >= 90 ||
server.memory_usage_percent >= 90 ||
(server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= 90) ||
(server.errors && server.errors.length > 0)
);
const warningServers = serverData.filter(server =>
((server.cpu_usage >= 70 && server.cpu_usage < 90) ||
(server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) ||
(server.disk && server.disk.length > 0 && server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90)) ||
(server.services && Object.values(server.services).includes('stopped'))
);
// UI 업데이트
const incidentsLoading = document.getElementById('incidentsLoading'); // 이 ID도 현재 HTML에 없음
const incidentsEmpty = document.getElementById('incidentsEmpty'); // 이 ID도 현재 HTML에 없음
if (incidentsLoading) incidentsLoading.style.display = 'none';
if (criticalServers.length === 0 && warningServers.length === 0) {
if (incidentsEmpty) incidentsEmpty.style.display = 'block';
return;
}
// 장애가 있으면 요약 표시
if (incidentsEmpty) incidentsEmpty.style.display = 'none';
// 아코디언 항목 생성 - 이 함수 호출로 인해 오류 발생. data_processor.js의 updateProblemsList로 대체됨.
// generateIncidentAccordionItems(criticalServers, warningServers);
}
// 장애 아코디언 항목 생성 - 이 함수는 이제 사용되지 않음 (오류 원인)
// function generateIncidentAccordionItems(criticalServers, warningServers) {
// const accordion = document.getElementById('incidents-accordion'); // 존재하지 않는 ID
// if (!accordion) {
// console.error("'incidents-accordion' element not found. Cannot generate accordion items.");
// return;
// }
// accordion.innerHTML = '';
// // ... (이하 로직도 data_processor.js의 updateProblemsList로 대체됨)
// }
// 서버 문제 감지 함수
function detectServerProblems(server) {
const problems = [];
// CPU 문제
if (server.cpu_usage >= 90) {
problems.push({
type: 'CPU 과부하',
description: `CPU 사용률이 ${server.cpu_usage.toFixed(2)}%로 매우 높음 (임계값: 90%)`,
solution: '불필요한 프로세스 종료, CPU 사용량이 높은 애플리케이션 최적화, 서버 스케일업 고려'
});
} else if (server.cpu_usage >= 70) {
problems.push({
type: 'CPU 부하',
description: `CPU 사용률이 ${server.cpu_usage.toFixed(2)}%로 높음 (임계값: 70%)`,
solution: 'CPU 사용량 모니터링, 지속적인 증가 시 원인 파악 필요'
});
}
// 메모리 문제
if (server.memory_usage_percent >= 90) {
problems.push({
type: '메모리 부족',
description: `메모리 사용률이 ${server.memory_usage_percent.toFixed(2)}%로 매우 높음 (임계값: 90%)`,
solution: '메모리 누수 점검, 불필요한 프로세스 종료, 메모리 증설 고려'
});
} else if (server.memory_usage_percent >= 70) {
problems.push({
type: '메모리 부하',
description: `메모리 사용률이 ${server.memory_usage_percent.toFixed(2)}%로 높음 (임계값: 70%)`,
solution: '메모리 사용량 모니터링, 캐시 설정 최적화 검토'
});
}
// 디스크 문제
if (server.disk[0].disk_usage_percent >= 90) {
problems.push({
type: '디스크 공간 부족',
description: `디스크 사용률이 ${server.disk[0].disk_usage_percent.toFixed(2)}%로 매우 높음 (임계값: 90%)`,
solution: '불필요한 파일 정리, 로그 파일 압축/제거, 디스크 확장 고려'
});
} else if (server.disk[0].disk_usage_percent >= 70) {
problems.push({
type: '디스크 공간 주의',
description: `디스크 사용률이 ${server.disk[0].disk_usage_percent.toFixed(2)}%로 높음 (임계값: 70%)`,
solution: '디스크 사용량 모니터링, 대용량 파일 위치 확인'
});
}
// 서비스 문제
const stoppedServices = [];
Object.entries(server.services).forEach(([service, status]) => {
if (status === 'stopped') {
stoppedServices.push(service);
}
});
if (stoppedServices.length > 0) {
problems.push({
type: '서비스 중단',
description: `${stoppedServices.length}개 서비스 중단됨: ${stoppedServices.join(', ')}`,
solution: '서비스 로그 확인 후 재시작, 의존성 확인, 서비스 구성 파일 검토'
});
}
// 오류 메시지
if (server.errors.length > 0) {
problems.push({
type: '오류 발생',
description: `${server.errors.length}개의 오류 발생: ${server.errors.join(', ')}`,
solution: '오류 로그 분석, 애플리케이션 재시작, 관련 구성 파일 검토'
});
}
return problems;
}
// 장애 보고서 다운로드 함수
function downloadIncidentReport(serverName) {
const server = serverData.find(s => s.hostname === serverName);
if (!server) return;
const problems = detectServerProblems(server);
// 보고서 내용 생성
let reportContent = `=== OpenManager AI - 장애 보고서 ===\n\n`;
reportContent += `서버: ${server.hostname}\n`;
reportContent += `시간: ${new Date().toLocaleString()}\n`;
reportContent += `OS: ${server.os}\n`;
reportContent += `가동시간: ${server.uptime}\n\n`;
reportContent += `=== 시스템 상태 ===\n`;
reportContent += `CPU 사용률: ${server.cpu_usage.toFixed(2)}%\n`;
reportContent += `메모리 사용률: ${server.memory_usage_percent.toFixed(2)}%\n`;
reportContent += `디스크 사용률: ${server.disk[0].disk_usage_percent.toFixed(2)}%\n`;
reportContent += `Load Average (1m): ${server.load_avg_1m}\n\n`;
reportContent += `=== 감지된 문제 ===\n`;
problems.forEach((problem, index) => {
reportContent += `${index + 1}. ${problem.type}: ${problem.description}\n`;
});
reportContent += `\n=== 권장 조치 ===\n`;
problems.forEach((problem, index) => {
reportContent += `${index + 1}. ${problem.solution}\n`;
});
reportContent += `\n=== 서비스 상태 ===\n`;
Object.entries(server.services).forEach(([service, status]) => {
reportContent += `${service}: ${status}\n`;
});
if (server.errors.length > 0) {
reportContent += `\n=== 오류 메시지 ===\n`;
server.errors.forEach((error, index) => {
reportContent += `${index + 1}. ${error}\n`;
});
}
// 보고서 파일 다운로드
const blob = new Blob([reportContent], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `장애보고서_${server.hostname}_${new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-')}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 전체 보고서 다운로드
function downloadAllReports() {
const criticalServers = serverData.filter(server =>
server.cpu_usage >= 90 ||
server.memory_usage_percent >= 90 ||
server.disk[0].disk_usage_percent >= 90 ||
server.errors.length > 0
);
const warningServers = serverData.filter(server =>
(server.cpu_usage >= 70 && server.cpu_usage < 90) ||
(server.memory_usage_percent >= 70 && server.memory_usage_percent < 90) ||
(server.disk[0].disk_usage_percent >= 70 && server.disk[0].disk_usage_percent < 90) ||
Object.values(server.services).includes('stopped')
);
if (criticalServers.length === 0 && warningServers.length === 0) {
alert('다운로드할 장애 보고서가 없습니다.');
return;
}
// 요약 보고서 생성
let summaryReport = `=== OpenManager AI - 전체 장애 요약 보고서 ===\n\n`;
summaryReport += `생성 시간: ${new Date().toLocaleString()}\n`;
summaryReport += `전체 서버 수: ${serverData.length}대\n`;
summaryReport += `심각 상태 서버: ${criticalServers.length}대\n`;
summaryReport += `경고 상태 서버: ${warningServers.length}대\n`;
summaryReport += `정상 상태 서버: ${serverData.length - criticalServers.length - warningServers.length}대\n\n`;
summaryReport += `=== 심각 상태 서버 목록 ===\n`;
criticalServers.forEach((server, index) => {
const problems = detectServerProblems(server);
summaryReport += `${index + 1}. ${server.hostname}: ${problems.map(p => p.type).join(', ')}\n`;
});
summaryReport += `\n=== 경고 상태 서버 목록 ===\n`;
warningServers.forEach((server, index) => {
const problems = detectServerProblems(server);
summaryReport += `${index + 1}. ${server.hostname}: ${problems.map(p => p.type).join(', ')}\n`;
});
// 개별 서버 보고서 추가
summaryReport += `\n\n============= 개별 서버 상세 보고서 =============\n\n`;
// 심각 서버 먼저
criticalServers.forEach(server => {
summaryReport += `\n\n=============================================\n`;
summaryReport += `서버: ${server.hostname} (심각)\n`;
summaryReport += `=============================================\n\n`;
const problems = detectServerProblems(server);
summaryReport += `CPU: ${server.cpu_usage.toFixed(2)}%, 메모리: ${server.memory_usage_percent.toFixed(2)}%, 디스크: ${server.disk[0].disk_usage_percent.toFixed(2)}%\n\n`;
summaryReport += `--- 감지된 문제 ---\n`;
problems.forEach((problem, index) => {
summaryReport += `${index + 1}. ${problem.type}: ${problem.description}\n`;
});
summaryReport += `\n--- 권장 조치 ---\n`;
problems.forEach((problem, index) => {
summaryReport += `${index + 1}. ${problem.solution}\n`;
});
if (server.errors.length > 0) {
summaryReport += `\n--- 오류 메시지 ---\n`;
server.errors.forEach((error, index) => {
summaryReport += `${index + 1}. ${error}\n`;
});
}
});
// 경고 서버 추가
warningServers.forEach(server => {
summaryReport += `\n\n=============================================\n`;
summaryReport += `서버: ${server.hostname} (경고)\n`;
summaryReport += `=============================================\n\n`;
const problems = detectServerProblems(server);
summaryReport += `CPU: ${server.cpu_usage.toFixed(2)}%, 메모리: ${server.memory_usage_percent.toFixed(2)}%, 디스크: ${server.disk[0].disk_usage_percent.toFixed(2)}%\n\n`;
summaryReport += `--- 감지된 문제 ---\n`;
problems.forEach((problem, index) => {
summaryReport += `${index + 1}. ${problem.type}: ${problem.description}\n`;
});
summaryReport += `\n--- 권장 조치 ---\n`;
problems.forEach((problem, index) => {
summaryReport += `${index + 1}. ${problem.solution}\n`;
});
if (server.errors.length > 0) {
summaryReport += `\n--- 오류 메시지 ---\n`;
server.errors.forEach((error, index) => {
summaryReport += `${index + 1}. ${error}\n`;
});
}
});
// 전체 보고서 다운로드
const blob = new Blob([summaryReport], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `전체_장애_보고서_${new Date().toISOString().slice(0, 10)}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', init);
</script>
<!-- 서버 상세 정보 모달 창 -->
<div class="modal fade" id="serverDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalServerName">서버 상세 정보</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row mb-4">
<!-- 시스템 정보 -->
<div class="col-md-6">
<h6 class="mb-3">시스템 정보</h6>
<table class="table table-sm">
<tbody id="systemInfoTable">
<tr>
<th>OS</th>
<td id="modalOS">-</td>
</tr>
<tr>
<th>가동 시간</th>
<td id="modalUptime">-</td>
</tr>
<tr>
<th>프로세스 수</th>
<td id="modalProcessCount">-</td>
</tr>
<tr>
<th>좀비 프로세스</th>
<td id="modalZombieCount">-</td>
</tr>
<tr>
<th>로드 평균 (1분)</th>
<td id="modalLoadAvg">-</td>
</tr>
<tr>
<th>마지막 업데이트</th>
<td id="modalLastUpdate">-</td>
</tr>
</tbody>
</table>
</div>
<!-- 리소스 현황 -->
<div class="col-md-6">
<h6 class="mb-3">리소스 현황</h6>
<div class="chart-container">
<canvas id="resourceBarChart"></canvas>
</div>
</div>
</div>
<div class="row mb-4">
<!-- 네트워크 정보 -->
<div class="col-md-6">
<h6 class="mb-3">네트워크 정보</h6>
<table class="table table-sm">
<tbody id="networkInfoTable">
<tr>
<th>인터페이스</th>
<td id="modalNetInterface">-</td>
</tr>
<tr>
<th>수신 바이트</th>
<td id="modalRxBytes">-</td>
</tr>
<tr>
<th>송신 바이트</th>
<td id="modalTxBytes">-</td>
</tr>
<tr>
<th>수신 오류</th>
<td id="modalRxErrors">-</td>
</tr>
<tr>
<th>송신 오류</th>
<td id="modalTxErrors">-</td>
</tr>
</tbody>
</table>
</div>
<!-- 서비스 상태 -->
<div class="col-md-6">
<h6 class="mb-3">서비스 상태</h6>
<div id="modalServiceStatus" class="service-status-container">
<!-- 서비스 상태 태그들이 여기에 추가됨 -->
</div>
</div>
</div>
<!-- 오류 메시지 -->
<div class="row mb-4">
<div class="col-12">
<h6 class="mb-3">오류 메시지</h6>
<div id="modalErrorsContainer" class="alert alert-info">
현재 보고된 오류가 없습니다.
</div>
</div>
</div>
<!-- 24시간 리소스 사용 추이 -->
<div class="row">
<div class="col-12">
<h6 class="mb-3">24시간 리소스 사용 추이</h6>
<div class="chart-container" style="height: 300px;">
<canvas id="resourceHistoryChart"></canvas>
</div>
</div>
</div>
</div>
</div>
</div>
</div> <!-- 자바스크립트 라이브러리 및 소스 파일 --> <!-- 부트스트랩 JS 및 Popper.js --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- AI 프로세서, 데이터 처리기, 더미 데이터 생성기 --> <script src="ai_processor.js"></script> <script src="data_processor.js"></script> <script src="dummy_data_generator.js"></script>
</body>
</html>