325 lines
11 KiB
JavaScript
325 lines
11 KiB
JavaScript
let allServers = [];
|
|
let analysisData = {};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadServers();
|
|
|
|
document.getElementById('check-all').addEventListener('change', (e) => {
|
|
const checked = e.target.checked;
|
|
document.querySelectorAll('.server-checkbox').forEach(cb => cb.checked = checked);
|
|
});
|
|
});
|
|
|
|
async function loadServers() {
|
|
try {
|
|
const res = await axios.get('/idrac/api/servers');
|
|
if (res.data.success) {
|
|
allServers = res.data.servers;
|
|
renderServerList(allServers);
|
|
} else {
|
|
alert('서버 목록 로드 실패: ' + res.data.message);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('서버 목록 로드 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
function renderServerList(servers) {
|
|
const tbody = document.querySelector('#selection-table tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
if (servers.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5">등록된 서버가 없습니다.</td></tr>';
|
|
return;
|
|
}
|
|
|
|
servers.forEach(server => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td><input type="checkbox" class="server-checkbox" value="${server.id}"></td>
|
|
<td>${server.name}</td>
|
|
<td>${server.ip_address}</td>
|
|
<td>${server.model || '-'}</td>
|
|
<td>${server.status || 'Unknown'}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
|
|
// 시작: 서버 선택 후 동기화 (iDRAC -> DB)
|
|
async function syncSelectedServers() {
|
|
const selectedIds = Array.from(document.querySelectorAll('.server-checkbox:checked')).map(cb => parseInt(cb.value));
|
|
|
|
if (selectedIds.length === 0) {
|
|
alert('동기화할 서버를 하나 이상 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
if (!confirm(`선택한 ${selectedIds.length}대 서버의 설정을 iDRAC에서 가져와 DB에 저장하시겠습니까?`)) {
|
|
return;
|
|
}
|
|
|
|
document.getElementById('loading').style.display = 'flex';
|
|
updateLoadingMessage('설정 데이터 동기화 중...');
|
|
|
|
try {
|
|
const res = await axios.post('/server-config/api/sync', {
|
|
server_ids: selectedIds,
|
|
categories: ['bios', 'system', 'idrac']
|
|
});
|
|
|
|
if (res.data.success) {
|
|
const failed = res.data.results.failed;
|
|
if (failed.length > 0) {
|
|
const failedNames = failed.map(f => f.name || f.id).join(', ');
|
|
alert(`일부 서버 동기화 실패:\n${failedNames}\n\n성공한 서버는 DB에 저장되었습니다.`);
|
|
} else {
|
|
alert('모든 서버 동기화가 완료되었습니다. 이제 분석을 수행할 수 있습니다.');
|
|
}
|
|
|
|
// 만약 이미 결과 화면이 보이고 있다면 자동 갱신
|
|
if (document.getElementById('result-card').style.display !== 'none') {
|
|
startAnalysis();
|
|
}
|
|
} else {
|
|
alert('동기화 실패: ' + res.data.message);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('동기화 요청 중 오류가 발생했습니다: ' + e.message);
|
|
} finally {
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// 시작: DB 데이터 기반 분석 (DB -> Web)
|
|
async function startAnalysis() {
|
|
const selectedIds = Array.from(document.querySelectorAll('.server-checkbox:checked')).map(cb => parseInt(cb.value));
|
|
|
|
if (selectedIds.length === 0) {
|
|
alert('분석할 서버를 하나 이상 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('loading').style.display = 'flex';
|
|
document.getElementById('result-card').style.display = 'none';
|
|
updateLoadingMessage('데이터 분석 중...');
|
|
|
|
try {
|
|
const res = await axios.post('/server-config/api/fetch', {
|
|
server_ids: selectedIds,
|
|
categories: ['bios', 'system', 'idrac']
|
|
});
|
|
|
|
if (res.data.success) {
|
|
analysisData = res.data.results;
|
|
renderComparison();
|
|
document.getElementById('result-card').style.display = 'block';
|
|
} else {
|
|
alert('데이터 조회 실패: ' + res.data.message);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('데이터 조회 중 오류가 발생했습니다.');
|
|
} finally {
|
|
document.getElementById('loading').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function updateLoadingMessage(msg) {
|
|
const loadingText = document.getElementById('loading-text');
|
|
if (loadingText) loadingText.textContent = msg;
|
|
}
|
|
|
|
function renderComparison() {
|
|
const container = document.getElementById('result-container');
|
|
const showDiffOnly = document.getElementById('show-diff-only').checked;
|
|
|
|
container.innerHTML = '';
|
|
|
|
const serverIds = Object.keys(analysisData);
|
|
if (serverIds.length === 0) {
|
|
container.innerHTML = '<p>데이터가 없습니다.</p>';
|
|
return;
|
|
}
|
|
|
|
// 통합된 모든 키(Attribute Name) 수집
|
|
const allKeys = new Set();
|
|
const serverNames = {};
|
|
const serverUpdated = {}; // 마지막 업데이트 시간
|
|
|
|
serverIds.forEach(id => {
|
|
const data = analysisData[id];
|
|
serverNames[id] = data.name;
|
|
|
|
// 가장 최근 업데이트 시간 찾기
|
|
const times = [data.bios_updated, data.idrac_updated, data.system_updated].filter(t => t);
|
|
if (times.length > 0) {
|
|
times.sort();
|
|
serverUpdated[id] = times[times.length - 1]; // 가장 최신
|
|
} else {
|
|
serverUpdated[id] = 'N/A';
|
|
}
|
|
|
|
// BIOS
|
|
if (data.bios) Object.keys(data.bios).forEach(k => allKeys.add('BIOS: ' + k));
|
|
// iDRAC
|
|
if (data.idrac) Object.keys(data.idrac).forEach(k => allKeys.add('iDRAC: ' + k));
|
|
// System
|
|
if (data.system) Object.keys(flattenObject(data.system)).forEach(k => allKeys.add('System: ' + k));
|
|
});
|
|
|
|
const sortedKeys = Array.from(allKeys).sort();
|
|
|
|
// 테이블 생성
|
|
const table = document.createElement('table');
|
|
table.className = 'comparison-table';
|
|
|
|
// 헤더
|
|
const thead = document.createElement('thead');
|
|
let headerHtml = '<tr><th style="min-width: 200px;">Attribute</th>';
|
|
serverIds.forEach(id => {
|
|
headerHtml += `<th>${serverNames[id]}</th>`;
|
|
});
|
|
headerHtml += '</tr>';
|
|
|
|
// 업데이트 시간 행 추가
|
|
headerHtml += '<tr style="background-color: #fafafa; font-size: 0.8em; color: #666;"><th>Local DB Updated</th>';
|
|
serverIds.forEach(id => {
|
|
const timeStr = serverUpdated[id] !== 'N/A' ? new Date(serverUpdated[id]).toLocaleString() : 'Never';
|
|
headerHtml += `<th>${timeStr}</th>`;
|
|
});
|
|
headerHtml += '</tr>';
|
|
|
|
thead.innerHTML = headerHtml;
|
|
table.appendChild(thead);
|
|
|
|
// 바디
|
|
const tbody = document.createElement('tbody');
|
|
let hasDiff = false;
|
|
|
|
sortedKeys.forEach(key => {
|
|
const row = document.createElement('tr');
|
|
let rowHtml = `<td>${key}</td>`;
|
|
|
|
// 값 비교를 위한 배열
|
|
const values = [];
|
|
|
|
serverIds.forEach(id => {
|
|
const val = getValueByKey(analysisData[id], key);
|
|
values.push(val);
|
|
});
|
|
|
|
// 값들이 모두 같은지 확인
|
|
const firstVal = values[0];
|
|
const isDiff = !values.every(v => v === firstVal);
|
|
|
|
if (isDiff) {
|
|
row.classList.add('diff-row');
|
|
hasDiff = true;
|
|
}
|
|
|
|
if (showDiffOnly && !isDiff) {
|
|
return; // Diff Only 모드이고 차이가 없으면 건너뜀
|
|
}
|
|
|
|
values.forEach(val => {
|
|
const displayVal = (val === undefined || val === null) ? '<span style="color:#ccc">N/A</span>' : val;
|
|
const cellClass = (isDiff && val !== firstVal) ? 'diff-highlight' : ''; // 첫번째 서버(기준)와 다르면 강조
|
|
rowHtml += `<td class="${cellClass}">${displayVal}</td>`;
|
|
});
|
|
|
|
row.innerHTML = rowHtml;
|
|
tbody.appendChild(row);
|
|
});
|
|
|
|
if (showDiffOnly && !hasDiff) {
|
|
// 차이점이 하나도 없는데 차이점만 보기 체크된 경우 빈 row가 아니라 메시지 표시가 나을지 고민
|
|
// tbody가 비어있음
|
|
}
|
|
|
|
if (tbody.children.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="' + (serverIds.length + 1) + '">표시할 항목이 없습니다.</td></tr>';
|
|
}
|
|
|
|
table.appendChild(tbody);
|
|
container.appendChild(table);
|
|
}
|
|
|
|
// 헬퍼: 키(Category: Name)로 값 추출
|
|
function getValueByKey(data, compositeKey) {
|
|
if (!data) return undefined;
|
|
|
|
const [category, ...rest] = compositeKey.split(': ');
|
|
const realKey = rest.join(': '); // 혹시 키 안에 콜론이 있을 경우 복구
|
|
|
|
if (category === 'BIOS') return data.bios ? data.bios[realKey] : undefined;
|
|
if (category === 'iDRAC') return data.idrac ? data.idrac[realKey] : undefined;
|
|
if (category === 'System') {
|
|
const flatSys = flattenObject(data.system);
|
|
return flatSys ? flatSys[realKey] : undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// 객체 평탄화 (Nested Object -> Flat Key)
|
|
function flattenObject(obj, prefix = '') {
|
|
if (!obj) return {};
|
|
return Object.keys(obj).reduce((acc, k) => {
|
|
const pre = prefix.length ? prefix + '.' : '';
|
|
if (typeof obj[k] === 'object' && obj[k] !== null) {
|
|
Object.assign(acc, flattenObject(obj[k], pre + k));
|
|
} else {
|
|
acc[pre + k] = obj[k];
|
|
}
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
function downloadReport() {
|
|
const table = document.querySelector('.comparison-table');
|
|
if (!table) {
|
|
alert('다운로드할 데이터가 없습니다. 먼저 분석을 진행해주세요.');
|
|
return;
|
|
}
|
|
|
|
let csvContent = "\uFEFF"; // BOM for Excel UTF-8
|
|
const rows = table.querySelectorAll('tr');
|
|
|
|
rows.forEach(row => {
|
|
const cols = row.querySelectorAll('th, td');
|
|
const rowData = [];
|
|
|
|
cols.forEach(col => {
|
|
// 따옴표 처리 및 텍스트 추출
|
|
let text = col.innerText || col.textContent;
|
|
text = text.replace(/"/g, '""'); // 따옴표 이스케이프
|
|
|
|
// 줄바꿈 제거
|
|
text = text.replace(/(\r\n|\n|\r)/gm, " ");
|
|
|
|
if (text.includes(',') || text.includes('"')) {
|
|
text = `"${text}"`;
|
|
}
|
|
rowData.push(text);
|
|
});
|
|
|
|
csvContent += rowData.join(',') + "\r\n";
|
|
});
|
|
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-');
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', `server_config_comparison_${timestamp}.csv`);
|
|
link.style.visibility = 'hidden';
|
|
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|