This commit is contained in:
2025-10-21 20:29:39 +09:00
parent 230ea0890d
commit bc15452181
163 changed files with 5177 additions and 16122 deletions

View File

@@ -0,0 +1,558 @@
/**
* Dell iDRAC 멀티 서버 펌웨어 관리 스타일
* backend/static/css/idrac_style.css
*/
/* 기본 스타일 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
/* 헤더 */
header {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 30px;
text-align: center;
}
header h1 {
color: #333;
font-size: 2.5em;
margin-bottom: 10px;
}
header .subtitle {
color: #666;
font-size: 1.1em;
}
/* 카드 */
.card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 30px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #eee;
}
.card-header h2 {
color: #333;
font-size: 1.8em;
}
.header-actions {
display: flex;
gap: 10px;
}
/* 필터 그룹 */
.filter-group {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.filter-group label {
font-weight: 600;
color: #555;
}
.filter-group select {
padding: 8px 15px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1em;
min-width: 200px;
}
.count-badge {
background: #667eea;
color: white;
padding: 5px 15px;
border-radius: 20px;
font-weight: 600;
}
/* 서버 테이블 */
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
.server-table {
width: 100%;
border-collapse: collapse;
}
.server-table thead {
background: #667eea;
color: white;
}
.server-table th {
padding: 15px;
text-align: left;
font-weight: 600;
}
.server-table tbody tr {
border-bottom: 1px solid #eee;
transition: background 0.2s;
}
.server-table tbody tr:hover {
background: #f8f9fa;
}
.server-table td {
padding: 15px;
}
.empty-message {
text-align: center;
color: #999;
padding: 40px !important;
font-style: italic;
}
/* 상태 배지 */
.status {
padding: 5px 12px;
border-radius: 15px;
font-size: 0.9em;
font-weight: 600;
}
.status-registered {
background: #e3f2fd;
color: #1976d2;
}
.status-online {
background: #e8f5e9;
color: #2e7d32;
}
.status-offline {
background: #ffebee;
color: #c62828;
}
.status-updating {
background: #fff3e0;
color: #f57c00;
}
.badge {
background: #f0f0f0;
padding: 4px 10px;
border-radius: 10px;
font-size: 0.9em;
color: #666;
}
/* 버튼 */
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover {
background: #138496;
}
.btn-warning {
background: #ffc107;
color: #333;
}
.btn-warning:hover {
background: #e0a800;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.btn-icon {
background: none;
border: none;
font-size: 1.2em;
cursor: pointer;
padding: 5px;
transition: transform 0.2s;
}
.btn-icon:hover {
transform: scale(1.2);
}
.action-buttons {
display: flex;
gap: 5px;
}
/* 일괄 작업 */
.bulk-actions {
display: flex;
align-items: center;
gap: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.bulk-actions span {
font-weight: 600;
color: #555;
}
/* 모달 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
border-radius: 15px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 30px;
border-bottom: 2px solid #eee;
}
.modal-header h3 {
color: #333;
font-size: 1.5em;
}
.close {
font-size: 2em;
color: #999;
cursor: pointer;
line-height: 1;
}
.close:hover {
color: #333;
}
.modal-body {
padding: 30px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 20px 30px;
border-top: 2px solid #eee;
}
/* 폼 그룹 */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 600;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1em;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #667eea;
}
/* 진행 상황 */
.progress-item {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.progress-status {
color: #666;
font-size: 0.9em;
}
.progress-bar-container {
width: 100%;
height: 30px;
background: #e0e0e0;
border-radius: 15px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-bar {
height: 100%;
transition: width 0.3s, background 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
}
.progress-uploading {
background: linear-gradient(90deg, #667eea, #764ba2);
animation: pulse 1.5s ease-in-out infinite;
}
.progress-completed {
background: #28a745;
}
.progress-failed {
background: #dc3545;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.progress-message {
font-size: 0.9em;
color: #666;
}
.progress-summary {
margin-top: 20px;
padding: 15px;
background: #e3f2fd;
border-radius: 8px;
}
.summary-stats {
display: flex;
justify-content: space-around;
font-weight: 600;
}
.summary-stats span {
padding: 5px 15px;
background: white;
border-radius: 5px;
}
/* 결과 테이블 */
.result-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.result-table thead {
background: #f8f9fa;
}
.result-table th {
padding: 12px;
text-align: left;
border-bottom: 2px solid #ddd;
}
.result-table td {
padding: 12px;
border-bottom: 1px solid #eee;
}
.result-table tr.success {
background: #e8f5e9;
}
.result-table tr.failed {
background: #ffebee;
}
.result-badge {
padding: 5px 10px;
border-radius: 10px;
font-weight: 600;
font-size: 0.9em;
}
.badge-success {
background: #28a745;
color: white;
}
.badge-failed {
background: #dc3545;
color: white;
}
/* 메시지 */
.info-text {
padding: 15px;
background: #e3f2fd;
border-left: 4px solid #2196f3;
border-radius: 5px;
color: #1976d2;
line-height: 1.8;
}
.warning-text {
padding: 15px;
background: #fff3e0;
border-left: 4px solid #ff9800;
border-radius: 5px;
color: #f57c00;
font-weight: 600;
}
/* 푸터 */
footer {
text-align: center;
padding: 20px;
color: white;
font-size: 0.9em;
}
/* 반응형 */
@media (max-width: 768px) {
.card-header {
flex-direction: column;
align-items: flex-start;
}
.header-actions {
margin-top: 10px;
width: 100%;
}
.header-actions button {
flex: 1;
}
.bulk-actions {
flex-wrap: wrap;
}
.modal-content {
width: 95%;
}
}
/* 체크박스 커스텀 */
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}

View File

@@ -0,0 +1,342 @@
/* 탭 메뉴 스타일 */
.tab-menu {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.tab-button {
padding: 12px 25px;
background: transparent;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-weight: 600;
font-size: 14px;
color: #666;
transition: all 0.3s;
}
.tab-button:hover {
color: #667eea;
background: #f8f9ff;
}
.tab-button.active {
color: #667eea;
border-bottom-color: #667eea;
background: #f8f9ff;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* 버전 상태 표시 */
.version-status {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.version-status-outdated {
background: #fee;
color: #dc3545;
border: 1px solid #fcc;
}
.version-status-outdated::before {
content: "🔴 ";
}
.version-status-latest {
background: #efe;
color: #28a745;
border: 1px solid #cfc;
}
.version-status-latest::before {
content: "🟢 ";
}
.version-status-unknown {
background: #ffeaa7;
color: #e17055;
border: 1px solid #fdcb6e;
}
.version-status-unknown::before {
content: "🟡 ";
}
/* 버전 비교 결과 카드 */
.comparison-card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.comparison-card h3 {
margin: 0 0 15px 0;
color: #333;
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.comparison-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.summary-item {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
text-align: center;
}
.summary-item .label {
font-size: 12px;
color: #666;
margin-bottom: 5px;
}
.summary-item .value {
font-size: 24px;
font-weight: bold;
color: #333;
}
.summary-item.outdated .value {
color: #dc3545;
}
.summary-item.latest .value {
color: #28a745;
}
.summary-item.unknown .value {
color: #ffc107;
}
/* 펌웨어 항목 리스트 */
.firmware-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid #f0f0f0;
}
.firmware-item:last-child {
border-bottom: none;
}
.firmware-item:hover {
background: #f8f9fa;
}
.firmware-name {
font-weight: 600;
color: #333;
flex: 1;
}
.firmware-versions {
display: flex;
align-items: center;
gap: 15px;
}
.version-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
font-family: 'Courier New', monospace;
}
.version-current {
background: #e9ecef;
color: #495057;
}
.version-arrow {
color: #999;
font-size: 18px;
}
.version-latest {
background: #d4edda;
color: #155724;
font-weight: 600;
}
.version-recommendation {
font-size: 12px;
color: #666;
margin-left: 10px;
}
/* 중요 업데이트 배지 */
.critical-badge {
display: inline-block;
background: #dc3545;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
margin-left: 8px;
}
/* 다운로드 링크 */
.download-link {
color: #667eea;
text-decoration: none;
font-size: 12px;
margin-left: 10px;
}
.download-link:hover {
text-decoration: underline;
}
/* 버전 관리 테이블 */
.version-table {
width: 100%;
border-collapse: collapse;
}
.version-table th {
background: #f8f9fa;
padding: 12px;
text-align: left;
font-weight: 600;
color: #495057;
border-bottom: 2px solid #dee2e6;
}
.version-table td {
padding: 12px;
border-bottom: 1px solid #dee2e6;
}
.version-table tr:hover {
background: #f8f9fa;
}
/* 빈 상태 메시지 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state .icon {
font-size: 48px;
margin-bottom: 20px;
}
.empty-state .message {
font-size: 16px;
margin-bottom: 20px;
}
/* 필터 그룹 개선 */
.filter-group {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.filter-group label {
font-weight: 600;
color: #495057;
}
.filter-group select {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
background: white;
}
.count-badge {
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
}
/* 진행률 표시 */
.progress-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s;
}
/* 반응형 */
@media (max-width: 768px) {
.tab-menu {
flex-direction: column;
}
.tab-button {
border-bottom: none;
border-left: 3px solid transparent;
}
.tab-button.active {
border-left-color: #667eea;
border-bottom-color: transparent;
}
.comparison-summary {
grid-template-columns: 1fr;
}
.firmware-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.firmware-versions {
width: 100%;
justify-content: space-between;
}
}

View File

@@ -0,0 +1,740 @@
/**
* Dell iDRAC 멀티 서버 펌웨어 관리 JavaScript
* backend/static/js/idrac_main.js
*/
// SocketIO 연결
const socket = io();
// 전역 변수
let servers = [];
let selectedServers = new Set();
// CSRF 토큰 가져오기
function getCSRFToken() {
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) return metaTag.getAttribute('content');
const cookies = document.cookie.split(';');
for (let cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === 'csrf_token') return value;
}
return null;
}
// fetch 래퍼 함수
async function fetchWithCSRF(url, options = {}) {
const csrfToken = getCSRFToken();
if (!options.headers) options.headers = {};
if (csrfToken) {
options.headers['X-CSRFToken'] = csrfToken;
options.headers['X-CSRF-Token'] = csrfToken;
}
if (options.body) {
if (options.body instanceof FormData) {
// 자동 처리
} else if (typeof options.body === 'string') {
if (!options.headers['Content-Type']) {
options.headers['Content-Type'] = 'application/json';
}
} else if (typeof options.body === 'object') {
options.headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(options.body);
}
}
return fetch(url, options);
}
// ========================================
// 초기화
// ========================================
document.addEventListener('DOMContentLoaded', function () {
console.log('iDRAC 멀티 서버 관리 시스템 시작');
refreshServers();
loadGroups();
setupSocketIO();
});
// ========================================
// 서버 관리
// ========================================
async function refreshServers() {
try {
const group = document.getElementById('group-filter').value;
const url = `/idrac/api/servers${group !== 'all' ? '?group=' + group : ''}`;
const response = await fetchWithCSRF(url);
const data = await response.json();
if (data.success) {
servers = data.servers;
renderServerList();
updateServerCount();
} else {
showMessage(data.message, 'error');
}
} catch (error) {
showMessage('서버 목록 로드 실패: ' + error, 'error');
}
}
function renderServerList() {
const tbody = document.getElementById('server-list');
if (servers.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="empty-message">등록된 서버가 없습니다</td></tr>';
return;
}
tbody.innerHTML = servers.map(server => `
<tr data-server-id="${server.id}">
<td><input type="checkbox" class="server-checkbox" value="${server.id}"
${selectedServers.has(server.id) ? 'checked' : ''}
onchange="toggleServerSelection(${server.id})"></td>
<td><strong>${server.name}</strong></td>
<td><code>${server.ip_address}</code></td>
<td><span class="badge">${server.group_name || '-'}</span></td>
<td><span class="status status-${server.status}">${getStatusText(server.status)}</span></td>
<td>${server.current_bios || '-'}</td>
<td>${server.last_connected ? formatDateTime(server.last_connected) : '-'}</td>
<td class="action-buttons">
<button onclick="testConnection(${server.id})" class="btn-icon" title="연결 테스트">🔌</button>
<button onclick="editServer(${server.id})" class="btn-icon" title="수정">✏️</button>
<button onclick="deleteServer(${server.id})" class="btn-icon" title="삭제">🗑️</button>
</td>
</tr>
`).join('');
}
function getStatusText(status) {
const map = { registered: '등록됨', online: '온라인', offline: '오프라인', updating: '업데이트중' };
return map[status] || status;
}
function formatDateTime(dateStr) {
const date = new Date(dateStr);
return date.toLocaleString('ko-KR', {
year: '2-digit', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'
});
}
function updateServerCount() {
document.getElementById('server-count').textContent = `${servers.length}`;
}
// ========================================
// 서버 선택
// ========================================
function toggleSelectAll() {
const checkbox = document.getElementById('select-all');
const checkboxes = document.querySelectorAll('.server-checkbox');
selectedServers.clear();
checkboxes.forEach(cb => {
cb.checked = checkbox.checked;
if (checkbox.checked) selectedServers.add(parseInt(cb.value));
});
updateSelectedCount();
}
function toggleServerSelection(serverId) {
if (selectedServers.has(serverId)) selectedServers.delete(serverId);
else selectedServers.add(serverId);
updateSelectedCount();
const all = document.querySelectorAll('.server-checkbox');
const checked = document.querySelectorAll('.server-checkbox:checked');
document.getElementById('select-all').checked = all.length === checked.length;
}
function updateSelectedCount() {
document.getElementById('selected-count').textContent = `선택: ${selectedServers.size}`;
}
function getSelectedServerIds() {
return Array.from(selectedServers);
}
// ========================================
// 그룹 관리
// ========================================
async function loadGroups() {
try {
const response = await fetchWithCSRF('/idrac/api/groups');
const data = await response.json();
if (data.success) {
const select = document.getElementById('group-filter');
const currentValue = select.value;
select.innerHTML = '<option value="all">전체</option>';
data.groups.forEach(g => select.innerHTML += `<option value="${g}">${g}</option>`);
if (currentValue) select.value = currentValue;
}
} catch (e) {
console.error('그룹 로드 실패:', e);
}
}
function filterByGroup() {
refreshServers();
}
// ========================================
// 서버 추가/수정/삭제
// ========================================
function showAddServerModal() {
document.getElementById('add-server-modal').style.display = 'flex';
}
function closeAddServerModal() {
const modal = document.getElementById('add-server-modal');
if (modal) modal.style.display = 'none';
setTimeout(() => clearServerForm(), 0); // DOM 안정 후 폼 초기화
}
function clearServerForm() {
const fieldIds = [
'server-name', 'server-ip', 'server-username',
'server-password', 'server-group', 'server-model'
];
fieldIds.forEach(id => {
const el = document.getElementById(id);
if (el) {
if (el.type === 'checkbox') el.checked = false;
else if (el.type === 'file') el.value = null;
else el.value = '';
} else {
console.warn(`[clearServerForm] 요소 없음: #${id}`);
}
});
}
async function addServer() {
const serverData = {
name: document.getElementById('server-name').value,
ip_address: document.getElementById('server-ip').value,
username: document.getElementById('server-username').value,
password: document.getElementById('server-password').value,
group_name: document.getElementById('server-group').value,
model: document.getElementById('server-model').value
};
if (!serverData.name || !serverData.ip_address || !serverData.password) {
showMessage('필수 필드를 입력하세요 (서버명, IP, 비밀번호)', 'error');
return;
}
try {
const response = await fetchWithCSRF('/idrac/api/servers', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serverData)
});
const data = await response.json();
if (data.success) {
showMessage(data.message, 'success');
closeAddServerModal();
refreshServers();
loadGroups();
} else showMessage(data.message, 'error');
} catch (e) {
showMessage('서버 추가 실패: ' + e, 'error');
}
}
async function deleteServer(serverId) {
if (!confirm('이 서버를 삭제하시겠습니까?')) return;
try {
const res = await fetchWithCSRF(`/idrac/api/servers/${serverId}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
showMessage(data.message, 'success');
refreshServers();
} else showMessage(data.message, 'error');
} catch (e) {
showMessage('서버 삭제 실패: ' + e, 'error');
}
}
// ========================================
// 연결 테스트
// ========================================
async function testConnection(serverId) {
try {
const res = await fetchWithCSRF(`/idrac/api/servers/${serverId}/test`, { method: 'POST' });
const data = await res.json();
showMessage(data.message, data.success ? 'success' : 'error');
refreshServers();
} catch (e) {
showMessage('연결 테스트 실패: ' + e, 'error');
}
}
// ========================================
// 펌웨어 업로드
// ========================================
function showUploadModal() {
const ids = getSelectedServerIds();
if (ids.length === 0) return showMessage('서버를 선택하세요', 'warning');
document.getElementById('upload-server-count').textContent = `${ids.length}`;
document.getElementById('upload-modal').style.display = 'flex';
}
function closeUploadModal() {
document.getElementById('upload-modal').style.display = 'none';
document.getElementById('firmware-file').value = '';
}
async function startMultiUpload() {
const fileInput = document.getElementById('firmware-file');
const serverIds = getSelectedServerIds();
if (!fileInput.files[0]) {
showMessage('파일을 선택하세요', 'warning');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('server_ids', serverIds.join(','));
try {
closeUploadModal();
showUploadProgress(serverIds);
const response = await fetchWithCSRF('/idrac/api/upload-multi', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showMessage(data.message, 'success');
} else {
showMessage(data.message, 'error');
hideUploadProgress();
}
} catch (error) {
showMessage('업로드 시작 실패: ' + error, 'error');
hideUploadProgress();
}
}
function showUploadProgress(serverIds) {
const section = document.getElementById('upload-progress-section');
const list = document.getElementById('upload-progress-list');
section.style.display = 'block';
// 각 서버별 진행 바 생성
list.innerHTML = serverIds.map(id => {
const server = servers.find(s => s.id === id);
return `
<div class="progress-item" id="progress-${id}">
<div class="progress-header">
<strong>${server.name}</strong> (${server.ip_address})
<span class="progress-status" id="status-${id}">대기중...</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar" id="bar-${id}" style="width: 0%"></div>
</div>
<div class="progress-message" id="message-${id}"></div>
</div>
`;
}).join('');
// 요약 초기화
document.getElementById('progress-summary').innerHTML = `
<div class="summary-stats">
<span>전체: ${serverIds.length}대</span>
<span id="completed-count">완료: 0대</span>
<span id="failed-count">실패: 0대</span>
</div>
`;
}
function hideUploadProgress() {
document.getElementById('upload-progress-section').style.display = 'none';
}
// ========================================
// SocketIO 이벤트
// ========================================
function setupSocketIO() {
// 업로드 진행 상황
socket.on('upload_progress', function(data) {
console.log('Upload progress:', data);
const statusEl = document.getElementById(`status-${data.server_id}`);
const barEl = document.getElementById(`bar-${data.server_id}`);
const messageEl = document.getElementById(`message-${data.server_id}`);
if (statusEl) statusEl.textContent = data.message;
if (barEl) {
barEl.style.width = data.progress + '%';
barEl.className = `progress-bar progress-${data.status}`;
}
if (messageEl) messageEl.textContent = data.job_id ? `Job ID: ${data.job_id}` : '';
});
// 업로드 완료
socket.on('upload_complete', function(data) {
console.log('Upload complete:', data);
const summary = data.summary;
document.getElementById('completed-count').textContent = `완료: ${summary.success}`;
document.getElementById('failed-count').textContent = `실패: ${summary.failed}`;
showMessage(`업로드 완료: 성공 ${summary.success}대, 실패 ${summary.failed}`, 'success');
showResults(data.results, '업로드 결과');
refreshServers();
});
}
// ========================================
// 결과 표시
// ========================================
function showResults(results, title) {
const section = document.getElementById('result-section');
const content = document.getElementById('result-content');
section.style.display = 'block';
content.innerHTML = `
<h3>${title}</h3>
<table class="result-table">
<thead>
<tr>
<th>서버명</th>
<th>상태</th>
<th>메시지</th>
</tr>
</thead>
<tbody>
${results.map(r => `
<tr class="${r.success ? 'success' : 'failed'}">
<td><strong>${r.server_name}</strong></td>
<td>
<span class="result-badge ${r.success ? 'badge-success' : 'badge-failed'}">
${r.success ? '✓ 성공' : '✗ 실패'}
</span>
</td>
<td>${r.message}${r.job_id ? ` (Job: ${r.job_id})` : ''}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
// ========================================
// 재부팅
// ========================================
async function rebootSelected() {
const serverIds = getSelectedServerIds();
if (serverIds.length === 0) {
showMessage('서버를 선택하세요', 'warning');
return;
}
if (!confirm(`선택한 ${serverIds.length}대 서버를 재부팅하시겠습니까?\n\n⚠️ 업로드된 펌웨어가 적용됩니다!`)) {
return;
}
try {
const response = await fetchWithCSRF('/idrac/api/servers/reboot-multi', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
server_ids: serverIds,
type: 'GracefulRestart'
})
});
const data = await response.json();
if (data.success) {
const summary = data.summary;
showMessage(`재부팅 시작: 성공 ${summary.success}대, 실패 ${summary.failed}`, 'success');
showResults(data.results, '재부팅 결과');
} else {
showMessage(data.message, 'error');
}
refreshServers();
} catch (error) {
showMessage('재부팅 실패: ' + error, 'error');
}
}
// ========================================
// Excel 가져오기 (추후 구현)
// ========================================
function importExcel() {
showMessage('Excel 가져오기 기능은 개발 중입니다', 'info');
}
// ========================================
// 유틸리티
// ========================================
function showMessage(message, type = 'info') {
// 간단한 알림 표시
alert(message);
console.log(`[${type}] ${message}`);
}
// 편의 함수들
function editServer(serverId) {
showMessage('서버 수정 기능은 개발 중입니다', 'info');
}
function getSelectedFirmware() {
const serverIds = getSelectedServerIds();
if (serverIds.length === 0) {
showMessage('서버를 선택하세요', 'warning');
return;
}
showMessage('선택한 서버의 펌웨어 조회 중...', 'info');
// 각 서버별로 펌웨어 조회
serverIds.forEach(async (serverId) => {
try {
const response = await fetchWithCSRF(`/idrac/api/servers/${serverId}/firmware`);
const data = await response.json();
if (data.success) {
console.log(`Server ${serverId} firmware:`, data.data);
}
} catch (error) {
console.error(`Server ${serverId} firmware query failed:`, error);
}
});
// 새로고침
setTimeout(() => {
refreshServers();
}, 2000);
}
// ========================================
// 다중 연결 테스트
// ========================================
async function testSelectedConnections() {
const serverIds = getSelectedServerIds();
if (serverIds.length === 0) {
showMessage('서버를 선택하세요', 'warning');
return;
}
showMessage(`선택된 ${serverIds.length}대 서버 연결 테스트 중...`, 'info');
try {
const res = await fetchWithCSRF('/idrac/api/servers/test-multi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ server_ids: serverIds })
});
const data = await res.json();
if (data.success) {
const summary = data.summary;
showMessage(`연결 성공: ${summary.success}대 / 실패: ${summary.failed}`, 'success');
showResults(data.results, '연결 테스트 결과');
} else {
showMessage(data.message, 'error');
}
refreshServers();
} catch (error) {
showMessage('연결 테스트 실패: ' + error, 'error');
}
}
// ========================================
// 다중 펌웨어 버전 비교
// ========================================
async function compareSelectedFirmware() {
const serverIds = getSelectedServerIds();
if (serverIds.length === 0) {
showMessage('서버를 선택하세요', 'warning');
return;
}
showMessage(`선택된 ${serverIds.length}대 서버 버전 비교 중...`, 'info');
try {
const res = await fetchWithCSRF('/idrac/api/servers/firmware/compare-multi', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ server_ids: serverIds })
});
const data = await res.json();
if (data.success) {
showResults(data.results, '버전 비교 결과');
} else {
showMessage(data.message, 'error');
}
} catch (error) {
showMessage('버전 비교 실패: ' + error, 'error');
}
}
// ========================================
// 펌웨어 버전 추가 모달
// ========================================
function showAddVersionModal() {
const modal = document.getElementById('add-version-modal');
if (modal) modal.style.display = 'flex';
}
function closeAddVersionModal() {
const modal = document.getElementById('add-version-modal');
if (modal) modal.style.display = 'none';
}
async function addFirmwareVersion() {
const data = {
component_name: document.getElementById('version-component').value,
latest_version: document.getElementById('version-latest').value,
server_model: document.getElementById('version-model').value,
vendor: document.getElementById('version-vendor').value,
release_date: document.getElementById('version-release-date').value,
download_url: document.getElementById('version-download-url').value,
notes: document.getElementById('version-notes').value,
is_critical: document.getElementById('version-critical').checked
};
if (!data.component_name || !data.latest_version) {
showMessage('컴포넌트명과 버전을 입력하세요', 'warning');
return;
}
try {
const response = await fetchWithCSRF('/idrac/api/firmware-versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showMessage(result.message, 'success');
closeAddVersionModal();
refreshFirmwareVersionList();
} else {
showMessage(result.message, 'error');
}
} catch (error) {
showMessage('버전 추가 실패: ' + error, 'error');
}
}
async function refreshFirmwareVersionList() {
try {
const response = await fetchWithCSRF('/idrac/api/firmware-versions');
const data = await response.json();
if (data.success) {
const tbody = document.getElementById('version-list');
tbody.innerHTML = data.versions.map(v => `
<tr>
<td>${v.component_name}</td>
<td>${v.latest_version}</td>
<td>${v.server_model || '-'}</td>
<td>${v.release_date || '-'}</td>
<td>${v.is_critical ? '⚠️' : ''}</td>
<td><button class="btn btn-danger" onclick="deleteFirmwareVersion(${v.id})">삭제</button></td>
</tr>
`).join('');
}
} catch (error) {
showMessage('버전 목록 로드 실패: ' + error, 'error');
}
}
// ========================================
// Dell Catalog에서 최신 버전 자동 가져오기
// ========================================
async function syncDellCatalog(model = "PowerEdge R750") {
showMessage(`${model} 최신 버전 정보를 Dell에서 가져오는 중...`, "info");
try {
const response = await fetchWithCSRF("/catalog/sync", {
method: "POST",
body: { model }
});
const data = await response.json();
if (data.success) {
showMessage(data.message, "success");
await refreshFirmwareVersionList();
} else {
showMessage(data.message, "error");
}
} catch (error) {
showMessage("버전 정보 동기화 실패: " + error, "error");
}
}
// ========================================
// 펌웨어 버전 목록 새로고침
// ========================================
async function refreshFirmwareVersionList() {
try {
const response = await fetchWithCSRF("/idrac/api/firmware-versions");
const data = await response.json();
if (data.success) {
const tbody = document.getElementById("version-list");
if (!tbody) return;
tbody.innerHTML = data.versions.length
? data.versions
.map(
(v) => `
<tr>
<td>${v.component_name}</td>
<td>${v.latest_version}</td>
<td>${v.server_model || "-"}</td>
<td>${v.release_date || "-"}</td>
<td>${v.is_critical ? "⚠️" : ""}</td>
<td>
<button class="btn btn-danger" onclick="deleteFirmwareVersion(${v.id})">삭제</button>
</td>
</tr>`
)
.join("")
: `<tr><td colspan="6" class="empty-message">등록된 버전 정보가 없습니다</td></tr>`;
} else {
showMessage(data.message, "error");
}
} catch (error) {
showMessage("버전 목록 로드 실패: " + error, "error");
}
}

View File

@@ -0,0 +1,283 @@
/* style.css */
/* 기본 스타일 재정의 */
body {
font-family: 'Arial', sans-serif;
background-color: #f4f4f4;
}
.container {
background-color: #fff;
padding: 20px;
margin-top: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
h1 {
color: #333;
margin-bottom: 20px;
}
h2 {
color: #333;
margin-top: 30px;
margin-bottom: 15px;
}
.form-group label {
font-weight: bold;
color: #555;
}
.form-control {
border-radius: 5px;
border: 1px solid #ddd;
}
.btn {
border-radius: 5px;
}
.btn-primary {
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #0056b3;
}
.btn-danger {
background-color: #dc3545;
border-color: #dc3545;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
}
.btn-secondary {
background-color: #6c757d;
border-color: #6c757d;
}
.btn-secondary:hover {
background-color: #5a6268;
border-color: #545b62;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px; /* 파일 목록 아래 여백 추가 */
}
.simple-card {
background-color: #f8f9fa;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
width: calc(12.5% - 10px); /* 가로로 8개 유지 */
box-sizing: border-box;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease; /* 부드러운 전환 효과 추가 */
}
.simple-card a {
font-size: 1em; /* 글씨 크기를 작게 조정 */
font-weight: bold;
color: #007bff;
text-decoration: none;
}
.simple-card a:hover {
color: #0056b3;
}
.simple-card:hover {
background-color: #e9ecef; /* 호버 시 배경색 변경 */
}
.button-group {
margin-top: 10px;
}
.pagination {
margin-top: 20px;
}
.backup-card-container .backup-card {
margin-bottom: 10px;
}
.accordion-button {
width: 100%;
text-align: left;
padding: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.accordion-button:hover {
background-color: #0056b3;
}
.accordion-content {
display: none;
background-color: #f8f9fa;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
margin-top: 5px;
margin-bottom: 20px; /* 파일 목록 아래 여백 추가 */
}
.accordion-content .card-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.accordion-content .simple-card {
width: calc(12.5% - 10px); /* 가로로 8개 유지 */
}
/* 진행 상황 스타일 */
#progressSession {
margin-top: 20px;
padding: 15px;
background-color: #e9ecef;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
margin-bottom: 20px; /* 파일 목록 아래 여백 추가 */
}
.progress {
height: 25px;
border-radius: 5px;
background-color: #f4f4f4;
border: 1px solid #ddd;
}
.progress-bar {
font-weight: bold;
line-height: 25px;
background-color: #007bff;
border-radius: 5px;
transition: width 0.3s ease;
}
#progressSession h2 {
position: absolute;
top: -30px;
left: 15px;
background-color: #e9ecef;
padding: 5px 10px;
border-radius: 5px;
font-size: 1.2em;
color: #333;
}
/* 서버 리스트 박스 높이 조정 */
#server_list_content {
height: 340px; /* 원하는 높이로 설정 */
}
/* base */
.navbar {
background-color: #333;
padding: 10px;
}
.nav-list {
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
}
.nav-list li {
margin-right: 20px;
}
.nav-list li a {
color: white;
text-decoration: none;
font-weight: bold;
}
.nav-list li a:hover {
text-decoration: underline;
}
body {
padding-top: 58px; /* 상단 네비게이션 바의 높이 */
}
/* XML 파일 목록의 스타일 정의 */
.list-group-item {
transition: background-color 0.3s ease, transform 0.3s ease;
border-radius: 8px;
margin-bottom: 10px;
padding: 15px;
border: 1px solid #ccc;
background-color: #ffffff;
}
/* 마우스 오버 시 배경색 및 효과 변경 */
.list-group-item:hover {
background-color: #f8f9fa;
transform: scale(1.02);
}
/* 삭제 및 편집 버튼 간격 조정 */
.action-buttons {
display: flex;
gap: 10px;
}
/* 파일 목록 타이틀 스타일 정의 */
.card-body h5 {
font-size: 1.3rem;
font-weight: bold;
}
/* 파일 업로드 필드의 스타일 */
.custom-file-label {
border-radius: 5px;
padding: 10px;
background-color: #f1f1f1;
border: 1px solid #ccc;
cursor: pointer;
white-space: nowrap; /* 파일 이름이 한 줄로 보이게 함 */
overflow: hidden;
text-overflow: ellipsis; /* 파일 이름이 길 경우 생략 표시 */
}
.custom-file-label:hover {
background-color: #e2e2e2;
}
/* 업로드 버튼 스타일 */
.btn-upload {
margin-top: 10px;
width: auto; /* 버튼 너비를 텍스트에 맞게 조정 */
font-weight: bold;
padding: 10px;
}
/* XML 파일 업로드 필드 레이아웃 조정 */
.upload-section {
margin-bottom: 20px;
}