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 = '등록된 서버가 없습니다.'; return; } servers.forEach(server => { const tr = document.createElement('tr'); tr.innerHTML = ` ${server.name} ${server.ip_address} ${server.model || '-'} ${server.status || 'Unknown'} `; 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 = '

데이터가 없습니다.

'; 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 = 'Attribute'; serverIds.forEach(id => { headerHtml += `${serverNames[id]}`; }); headerHtml += ''; // 업데이트 시간 행 추가 headerHtml += 'Local DB Updated'; serverIds.forEach(id => { const timeStr = serverUpdated[id] !== 'N/A' ? new Date(serverUpdated[id]).toLocaleString() : 'Never'; headerHtml += `${timeStr}`; }); headerHtml += ''; thead.innerHTML = headerHtml; table.appendChild(thead); // 바디 const tbody = document.createElement('tbody'); let hasDiff = false; sortedKeys.forEach(key => { const row = document.createElement('tr'); let rowHtml = `${key}`; // 값 비교를 위한 배열 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) ? 'N/A' : val; const cellClass = (isDiff && val !== firstVal) ? 'diff-highlight' : ''; // 첫번째 서버(기준)와 다르면 강조 rowHtml += `${displayVal}`; }); row.innerHTML = rowHtml; tbody.appendChild(row); }); if (showDiffOnly && !hasDiff) { // 차이점이 하나도 없는데 차이점만 보기 체크된 경우 빈 row가 아니라 메시지 표시가 나을지 고민 // tbody가 비어있음 } if (tbody.children.length === 0) { tbody.innerHTML = '표시할 항목이 없습니다.'; } 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); }