Files
iDRAC_Info/backend/templates/manage_xml.html
2025-12-19 16:23:03 +09:00

525 lines
23 KiB
HTML

{% extends "base.html" %}
{% block title %}XML 설정 관리 & 배포 - Dell Server Info{% endblock %}
{% block extra_css %}
<!-- Existing SCP CSS for legacy support or specific components -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/scp.css') }}">
<!-- Overriding/New Styles for Modern Look -->
<style>
/* 드래그 앤 드롭 영역 스타일 */
.drop-zone {
border: 2px dashed #cbd5e1;
border-radius: 12px;
background-color: #f8fafc;
transition: all 0.2s ease;
text-align: center;
padding: 2rem;
cursor: pointer;
position: relative;
}
.drop-zone:hover,
.drop-zone.dragover {
border-color: #3b82f6;
background-color: #eff6ff;
}
.drop-zone-icon {
font-size: 2.5rem;
color: #64748b;
margin-bottom: 1rem;
}
.drop-zone-text {
font-weight: 500;
color: #334155;
margin-bottom: 0.5rem;
}
.drop-zone-hint {
font-size: 0.875rem;
color: #94a3b8;
}
.drop-zone input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
/* 카드 그리드 스타일 (index.html과 유사) */
.xml-file-card {
background: white;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 1rem;
transition: all 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.xml-file-card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
border-color: #3b82f6;
}
.file-icon-wrapper {
width: 48px;
height: 48px;
border-radius: 10px;
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
color: #2563eb;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.file-name {
font-weight: 600;
color: #1e293b;
margin-bottom: 0.5rem;
word-break: break-all;
line-height: 1.4;
}
.file-meta {
font-size: 0.75rem;
color: #64748b;
margin-bottom: 1rem;
}
.card-actions {
display: flex;
gap: 0.5rem;
margin-top: auto;
}
.btn-action {
flex: 1;
padding: 0.4rem;
font-size: 0.8rem;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
transition: all 0.15s;
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.section-title {
font-size: 1.25rem;
font-weight: 700;
color: #0f172a;
margin: 0;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header Section -->
<div class="row mb-4">
<div class="col">
<h2 class="fw-bold mb-1">
<i class="bi bi-file-earmark-code text-primary me-2"></i>
설정 파일 관리
</h2>
<p class="text-muted mb-0">서버 설정(XML) 파일을 업로드, 관리 및 배포합니다.</p>
</div>
<div class="col-auto align-self-end">
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#exportModal">
<i class="bi bi-server me-2"></i>iDRAC에서 추출
</button>
</div>
</div>
<div class="row g-4">
<!-- Left: Upload Section (30% on large screens) -->
<div class="col-lg-4 col-xl-3">
<div class="card border shadow-sm h-100">
<div class="card-header bg-white border-bottom-0 pt-4 pb-0">
<h6 class="fw-bold mb-0 text-dark">
<i class="bi bi-cloud-upload me-2 text-primary"></i>파일 업로드
</h6>
</div>
<div class="card-body">
<form action="{{ url_for('xml.upload_xml') }}" method="POST" enctype="multipart/form-data"
id="uploadForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="drop-zone" id="dropZone">
<input type="file" name="xmlFile" id="xmlFile" accept=".xml"
onchange="handleFileSelect(this)">
<div class="drop-zone-icon">
<i class="bi bi-file-earmark-arrow-up"></i>
</div>
<div class="drop-zone-text" id="dropZoneText">
클릭하여 파일 선택<br>또는 파일을 여기로 드래그
</div>
<div class="drop-zone-hint">XML 파일만 지원됩니다.</div>
</div>
<button type="submit" class="btn btn-primary w-100 mt-3 shadow-sm">
<i class="bi bi-upload me-2"></i>업로드 시작
</button>
</form>
<div class="alert alert-light mt-4 border" role="alert">
<h6 class="alert-heading fs-6 fw-bold"><i class="bi bi-info-circle me-2"></i>도움말</h6>
<p class="mb-0 fs-small text-muted" style="font-size: 0.85rem;">
업로드된 XML 파일을 사용하여 여러 서버에 동일한 설정을 일괄 배포할 수 있습니다.
'비교' 기능을 사용하여 버전 간 차이를 확인하세요.
</p>
</div>
</div>
</div>
</div>
<!-- Right: File List (70%) -->
<div class="col-lg-8 col-xl-9">
<div class="card border shadow-sm h-100">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<h6 class="fw-bold mb-0 text-dark me-3">
<i class="bi bi-list-check me-2 text-success"></i>파일 목록
</h6>
<span class="badge bg-light text-dark border">{{ xml_files|length }}개 파일</span>
</div>
<div>
<button class="btn btn-sm btn-outline-secondary" id="compareBtn"
data-url="{{ url_for('scp.diff_scp') }}" onclick="compareSelected()">
<i class="bi bi-arrow-left-right me-1"></i>선택 비교
</button>
</div>
</div>
<div class="card-body bg-light">
{% if xml_files %}
<!-- 카드 크기 조정: 한 줄에 4개(xxl), 3개(xl) 등으로 조금 더 키움 -->
<div class="row row-cols-1 row-cols-lg-2 row-cols-xl-3 row-cols-xxl-4 g-3">
{% for xml_file in xml_files %}
<div class="col">
<div class="xml-file-card position-relative p-3 h-100 d-flex flex-column">
<div class="position-absolute top-0 end-0 p-2 me-1">
<input type="checkbox" class="form-check-input file-selector border-secondary"
value="{{ xml_file }}" style="cursor: pointer;">
</div>
<div class="d-flex align-items-center mb-3">
<div class="file-icon-wrapper me-3 mb-0 shadow-sm"
style="width: 42px; height: 42px; font-size: 1.4rem;">
<i class="bi bi-filetype-xml"></i>
</div>
<div class="file-name text-truncate fw-bold mb-0 text-dark"
style="max-width: 140px; font-size: 0.95rem;" title="{{ xml_file }}">
{{ xml_file }}
</div>
</div>
<div class="mt-auto pt-3 border-top">
<div class="d-flex gap-2">
<!-- 배포 버튼 -->
<button type="button"
class="btn btn-sm btn-primary flex-fill d-flex align-items-center justify-content-center gap-1"
onclick="openDeployModal('{{ xml_file }}')" title="배포">
<i class="bi bi-send-fill"></i> <span class="small fw-bold">배포</span>
</button>
<!-- 편집 버튼 -->
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}"
class="btn btn-sm btn-white border flex-fill d-flex align-items-center justify-content-center gap-1 text-dark bg-white"
title="편집">
<i class="bi bi-pencil-fill text-secondary"></i> <span
class="small fw-bold">편집</span>
</a>
<!-- 삭제 버튼 -->
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}" method="POST"
class="d-flex flex-fill m-0" style="min-width: 0;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button type="submit"
class="btn btn-sm btn-white border w-100 d-flex align-items-center justify-content-center gap-1 text-danger bg-white"
onclick="return confirm('정말 삭제하시겠습니까?')" title="삭제">
<i class="bi bi-trash-fill"></i> <span class="small fw-bold">삭제</span>
</button>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5 my-5">
<div class="mb-3 text-secondary" style="font-size: 3rem; opacity: 0.3;">
<i class="bi bi-folder2-open"></i>
</div>
<h5 class="text-secondary fw-normal">등록된 파일이 없습니다.</h5>
<p class="text-muted">좌측 패널에서 XML 파일을 업로드해주세요.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Export Modal (Include existing modal logic but restyled) -->
<div class="modal fade" id="exportModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<form action="{{ url_for('scp.export_scp') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="modal-header bg-primary text-white">
<h5 class="modal-title fs-6 fw-bold"><i class="bi bi-download me-2"></i>iDRAC 설정 내보내기</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="alert alert-info py-2 small mb-4">
<i class="bi bi-info-circle-fill me-2"></i> CIFS 네트워크 공유 폴더가 필요합니다.
</div>
<h6 class="text-primary fw-bold mb-3 small text-uppercase">대상 iDRAC</h6>
<div class="form-floating mb-2">
<input type="text" class="form-control" id="targetIp" name="target_ip" placeholder="IP"
required>
<label for="targetIp">iDRAC IP Address</label>
</div>
<div class="row g-2 mb-4">
<div class="col">
<div class="form-floating">
<input type="text" class="form-control" id="targetUser" name="username"
placeholder="User" required>
<label for="targetUser">Username</label>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="password" class="form-control" id="targetPwd" name="password"
placeholder="Pwd" required>
<label for="targetPwd">Password</label>
</div>
</div>
</div>
<h6 class="text-success fw-bold mb-3 small text-uppercase">저장소 (CIFS Share)</h6>
<div class="row g-2 mb-2">
<div class="col-8">
<div class="form-floating">
<input type="text" class="form-control" name="share_ip" placeholder="IP" required>
<label>Share IP</label>
</div>
</div>
<div class="col-4">
<div class="form-floating">
<input type="text" class="form-control" name="share_name" placeholder="Name" required>
<label>Share Name</label>
</div>
</div>
</div>
<div class="form-floating mb-2">
<input type="text" class="form-control" name="filename" placeholder="Filename" required>
<label>저장할 파일명 (예: backup.xml)</label>
</div>
<div class="row g-2">
<div class="col">
<div class="form-floating">
<input type="text" class="form-control" name="share_user" placeholder="User">
<label>Share User</label>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="password" class="form-control" name="share_pwd" placeholder="Pwd">
<label>Share Password</label>
</div>
</div>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-primary px-4">내보내기 실행</button>
</div>
</form>
</div>
</div>
</div>
<!-- Deploy Modal -->
<div class="modal fade" id="deployModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg">
<form action="{{ url_for('scp.import_scp') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div class="modal-header bg-danger text-white">
<h5 class="modal-title fs-6 fw-bold"><i class="bi bi-send-fill me-2"></i>설정 배포 (Import)</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div class="alert alert-warning py-2 small mb-4">
<i class="bi bi-exclamation-triangle-fill me-2"></i> 적용 후 서버가 재부팅될 수 있습니다.
</div>
<div class="mb-4">
<label class="form-label fw-bold small text-muted">배포 파일</label>
<div class="input-group">
<span class="input-group-text bg-light"><i class="bi bi-file-code"></i></span>
<input type="text" class="form-control fw-bold text-primary" id="deployFilename"
name="filename" readonly>
</div>
</div>
<h6 class="text-primary fw-bold mb-3 small text-uppercase">대상 iDRAC</h6>
<div class="form-floating mb-2">
<input type="text" class="form-control" name="target_ip" placeholder="IP" required>
<label>iDRAC IP</label>
</div>
<div class="row g-2 mb-4">
<div class="col">
<div class="form-floating">
<input type="text" class="form-control" name="username" placeholder="User" required>
<label>Username</label>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="password" class="form-control" name="password" placeholder="Pwd" required>
<label>Password</label>
</div>
</div>
</div>
<h6 class="text-success fw-bold mb-3 small text-uppercase">소스 위치 (CIFS Share)</h6>
<div class="row g-2 mb-2">
<div class="col-8">
<div class="form-floating">
<input type="text" class="form-control" name="share_ip" placeholder="IP" required>
<label>Share IP</label>
</div>
</div>
<div class="col-4">
<div class="form-floating">
<input type="text" class="form-control" name="share_name" placeholder="Name" required>
<label>Share Name</label>
</div>
</div>
</div>
<div class="row g-2 mb-4">
<div class="col">
<div class="form-floating">
<input type="text" class="form-control" name="share_user" placeholder="User">
<label>Share User</label>
</div>
</div>
<div class="col">
<div class="form-floating">
<input type="password" class="form-control" name="share_pwd" placeholder="Pwd">
<label>Share Password</label>
</div>
</div>
</div>
<div class="form-floating">
<select class="form-select" name="import_mode" id="importMode">
<option value="Replace">전체 교체 (Replace)</option>
<option value="Append">변경분만 적용 (Append)</option>
</select>
<label for="importMode">적용 모드</label>
</div>
</div>
<div class="modal-footer bg-light">
<button type="button" class="btn btn-light" data-bs-dismiss="modal">취소</button>
<button type="submit" class="btn btn-danger px-4">배포 시작</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/scp.js') }}"></script>
<script>
// 드래그 앤 드롭 파일 처리
function handleFileSelect(input) {
const fileName = input.files[0]?.name;
const dropZoneText = document.getElementById('dropZoneText');
if (fileName) {
dropZoneText.innerHTML = `<span class="text-primary fw-bold">${fileName}</span><br><span class="text-muted small">파일이 선택되었습니다.</span>`;
document.getElementById('dropZone').classList.add('border-primary', 'bg-light');
} else {
dropZoneText.innerHTML = '클릭하여 파일 선택<br>또는 파일을 여기로 드래그';
document.getElementById('dropZone').classList.remove('border-primary', 'bg-light');
}
}
// 드래그 효과
const dropZone = document.getElementById('dropZone');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, unhighlight, false);
});
function highlight(e) {
dropZone.classList.add('dragover');
}
function unhighlight(e) {
dropZone.classList.remove('dragover');
}
dropZone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
const input = document.getElementById('xmlFile');
if (files.length > 0) {
input.files = files; // input에 파일 할당
handleFileSelect(input);
}
}
// 툴팁 초기화 및 자동 닫기
document.addEventListener('DOMContentLoaded', function () {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
});
</script>
{% endblock %}
```