Initial project upload
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
{# backend/templates/admin.html #}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}관리자 패널 - Dell Server Info{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1">
|
||||
<i class="bi bi-shield-lock text-primary me-2"></i>관리자 패널
|
||||
</h2>
|
||||
<p class="text-muted mb-0">사용자 관리 및 시스템 설정을 수행합니다.</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('admin.view_logs') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-journal-text me-1"></i>로그 보기
|
||||
</a>
|
||||
<a href="{{ url_for('admin.settings') }}" class="btn btn-primary">
|
||||
<i class="bi bi-gear-fill me-1"></i>시스템 설정
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dashboard Stats -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm h-100 bg-primary bg-opacity-10">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<div class="rounded-circle bg-primary text-white p-3 me-3">
|
||||
<i class="bi bi-people-fill fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-primary fw-bold mb-1">총 사용자</h6>
|
||||
<h3 class="mb-0 fw-bold">{{ users|length }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm h-100 bg-success bg-opacity-10">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<div class="rounded-circle bg-success text-white p-3 me-3">
|
||||
<i class="bi bi-person-check-fill fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-success fw-bold mb-1">활성 사용자</h6>
|
||||
{% set active_users = users | selectattr("is_active") | list %}
|
||||
<h3 class="mb-0 fw-bold">{{ active_users|length }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm h-100 bg-warning bg-opacity-10">
|
||||
<div class="card-body d-flex align-items-center">
|
||||
<div class="rounded-circle bg-warning text-white p-3 me-3">
|
||||
<i class="bi bi-person-dash-fill fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="text-warning fw-bold mb-1">승인 대기</h6>
|
||||
<h3 class="mb-0 fw-bold">{{ (users|length) - (active_users|length) }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Table -->
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<i class="bi bi-person-lines-fill text-primary me-2"></i>사용자 목록
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4 py-3 text-secondary text-uppercase small fw-bold" style="width: 60px;">NO</th>
|
||||
<th class="py-3 text-secondary text-uppercase small fw-bold">이름</th>
|
||||
<th class="py-3 text-secondary text-uppercase small fw-bold">ID (Email)</th>
|
||||
<th class="py-3 text-secondary text-uppercase small fw-bold">상태</th>
|
||||
<th class="py-3 text-secondary text-uppercase small fw-bold text-end pe-4">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td class="ps-4 fw-bold text-secondary">{{ loop.index }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="avatar-initial rounded-circle bg-light text-primary fw-bold me-2 d-flex align-items-center justify-content-center border"
|
||||
style="width: 32px; height: 32px; font-size: 0.9rem;">
|
||||
{{ user.username[:1] | upper }}
|
||||
</div>
|
||||
<span class="fw-bold text-dark">{{ user.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-secondary small font-monospace">{{ user.email }}</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
<span class="badge bg-success-subtle text-success border border-success-subtle rounded-pill px-3">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>Active
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning-subtle text-warning border border-warning-subtle rounded-pill px-3">
|
||||
<i class="bi bi-hourglass-split me-1"></i>Pending
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_admin %}
|
||||
<span
|
||||
class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill px-2 ms-1">Admin</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end pe-4">
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
{% if not user.is_active %}
|
||||
<a href="{{ url_for('admin.approve_user', user_id=user.id) }}"
|
||||
class="btn btn-sm btn-success text-white d-flex align-items-center gap-1" title="가입 승인">
|
||||
<i class="bi bi-check-lg"></i>승인
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary d-flex align-items-center gap-1"
|
||||
data-user-id="{{ user.id }}" data-username="{{ user.username | e }}" data-bs-toggle="modal"
|
||||
data-bs-target="#changePasswordModal">
|
||||
<i class="bi bi-key"></i>비밀번호
|
||||
</button>
|
||||
|
||||
{% if not user.is_admin and user.id != current_user.id %}
|
||||
<form action="{{ url_for('admin.promote_user', user_id=user.id) }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary d-flex align-items-center gap-1"
|
||||
onclick="return confirm('[{{ user.username }}] 사용자를 관리자로 승격하시겠습니까?');">
|
||||
<i class="bi bi-shield-check"></i>관리자 승격
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('admin.delete_user', user_id=user.id) }}"
|
||||
class="btn btn-sm btn-outline-danger d-flex align-items-center gap-1"
|
||||
onclick="return confirm('⚠️ 경고: 사용자 [{{ user.username }}]님을 정말 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.');">
|
||||
<i class="bi bi-trash"></i>삭제
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if not users %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-muted">사용자가 없습니다.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ========== Change Password Modal ========== #}
|
||||
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<form id="changePasswordForm" method="post" action="">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title fw-bold">
|
||||
<i class="bi bi-key-fill me-2"></i>비밀번호 변경
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body p-4">
|
||||
<div class="alert alert-light border mb-4 d-flex align-items-center">
|
||||
<i class="bi bi-person-circle fs-4 me-3 text-secondary"></i>
|
||||
<div>
|
||||
<small class="text-muted d-block">대상 사용자</small>
|
||||
<span id="modalUserInfo" class="fw-bold text-dark fs-5"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="newPasswordInput" class="form-label fw-semibold">새 비밀번호</label>
|
||||
<input id="newPasswordInput" name="new_password" type="password" class="form-control" required minlength="8"
|
||||
placeholder="최소 8자 이상">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmPasswordInput" class="form-label fw-semibold">비밀번호 확인</label>
|
||||
<input id="confirmPasswordInput" name="confirm_password" type="password" class="form-control" required
|
||||
minlength="8" placeholder="비밀번호 재입력">
|
||||
<div id="pwMismatch" class="invalid-feedback">비밀번호가 일치하지 않습니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button id="modalSubmitBtn" type="submit" class="btn btn-primary px-4">변경 저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='js/admin.js') }}"></script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,254 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}시스템 로그 - Dell Server Info{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* 전체 레이아웃 */
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 600px;
|
||||
/* 초기 높이 */
|
||||
min-height: 300px;
|
||||
/* 최소 높이 */
|
||||
/* max-height 제거하여 제한 없이 확장 가능하게 함 */
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||
overflow: hidden;
|
||||
/* resize를 위해 필수 */
|
||||
resize: vertical;
|
||||
/* 수직 리사이징만 활성화 (수평 고정) */
|
||||
position: relative;
|
||||
/* 자식 요소 relative 기준 */
|
||||
}
|
||||
|
||||
/* 툴바 (헤더) */
|
||||
.editor-toolbar {
|
||||
background-color: #252526;
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* 에디터 본문 */
|
||||
#monaco-editor-root {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 로딩 인디케이터 */
|
||||
.editor-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: #d4d4d4;
|
||||
font-size: 1.1rem;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<!-- Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div>
|
||||
<h2 class="fw-bold mb-1">
|
||||
<i class="bi bi-terminal text-dark me-2"></i>시스템 로그
|
||||
</h2>
|
||||
<p class="text-muted mb-0 small">최근 생성된 1000줄의 시스템 로그를 실시간으로 확인합니다.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>돌아가기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-container">
|
||||
<!-- Toolbar -->
|
||||
<div class="editor-toolbar">
|
||||
<div class="d-flex gap-2 align-items-center flex-wrap">
|
||||
<div class="input-group input-group-sm" style="width: 250px;">
|
||||
<span class="input-group-text bg-dark border-secondary text-light"><i
|
||||
class="bi bi-search"></i></span>
|
||||
<input type="text" id="logSearch" class="form-control bg-dark border-secondary text-light"
|
||||
placeholder="검색어 입력...">
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<input type="checkbox" class="btn-check" id="checkInfo" checked autocomplete="off">
|
||||
<label class="btn btn-outline-secondary text-light" for="checkInfo">INFO</label>
|
||||
|
||||
<input type="checkbox" class="btn-check" id="checkWarn" checked autocomplete="off">
|
||||
<label class="btn btn-outline-warning" for="checkWarn">WARN</label>
|
||||
|
||||
<input type="checkbox" class="btn-check" id="checkError" checked autocomplete="off">
|
||||
<label class="btn btn-outline-danger" for="checkError">ERROR</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-light" id="btnScrollBottom">
|
||||
<i class="bi bi-arrow-down-circle me-1"></i>맨 아래로
|
||||
</button>
|
||||
<a href="{{ url_for('admin.view_logs') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>새로고침
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Area -->
|
||||
<div id="monaco-editor-root">
|
||||
<div class="editor-loading">
|
||||
<div class="spinner-border text-light me-3" role="status"></div>
|
||||
<div>로그 뷰어를 불러오는 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<!-- Monaco Editor Loader -->
|
||||
<script src="{{ url_for('static', filename='vendor/monaco-editor/min/vs/loader.js') }}"></script>
|
||||
|
||||
<script>
|
||||
// 서버에서 전달된 로그 데이터 (Python list -> JS array)
|
||||
// tojson safe 필터 사용
|
||||
const allLogs = {{ logs | tojson | safe }};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof require === 'undefined') {
|
||||
document.querySelector('.editor-loading').innerHTML =
|
||||
'<div class="text-danger"><i class="bi bi-exclamation-triangle me-2"></i>Monaco Editor를 로드할 수 없습니다.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
require.config({ paths: { 'vs': "{{ url_for('static', filename='vendor/monaco-editor/min/vs') }}" } });
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
var container = document.getElementById('monaco-editor-root');
|
||||
container.innerHTML = ''; // 로딩 제거
|
||||
|
||||
// 1. 커스텀 로그 언어 정의 (간단한 하이라이팅)
|
||||
monaco.languages.register({ id: 'simpleLog' });
|
||||
monaco.languages.setMonarchTokensProvider('simpleLog', {
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/\[INFO\]|INFO:/, 'info-token'],
|
||||
[/\[WARNING\]|\[WARN\]|WARNING:|WARN:/, 'warn-token'],
|
||||
[/\[ERROR\]|ERROR:|Traceback/, 'error-token'],
|
||||
[/\[DEBUG\]|DEBUG:/, 'debug-token'],
|
||||
[/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}/, 'date-token'],
|
||||
[/".*?"/, 'string']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 테마 정의
|
||||
monaco.editor.defineTheme('logTheme', {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'info-token', foreground: '4ec9b0' },
|
||||
{ token: 'warn-token', foreground: 'cca700', fontStyle: 'bold' },
|
||||
{ token: 'error-token', foreground: 'f44747', fontStyle: 'bold' },
|
||||
{ token: 'debug-token', foreground: '808080' },
|
||||
{ token: 'date-token', foreground: '569cd6' },
|
||||
],
|
||||
colors: {
|
||||
'editor.background': '#1e1e1e'
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 에디터 생성
|
||||
var editor = monaco.editor.create(container, {
|
||||
value: allLogs.join('\n'),
|
||||
language: 'simpleLog',
|
||||
theme: 'logTheme',
|
||||
readOnly: true,
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: true },
|
||||
fontSize: 13,
|
||||
lineHeight: 19, // 밀도 조절
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: 'on',
|
||||
wordWrap: 'on',
|
||||
renderLineHighlight: 'all',
|
||||
contextmenu: false,
|
||||
padding: { top: 10, bottom: 10 }
|
||||
});
|
||||
|
||||
// 4. 필터링 로직
|
||||
function updateLogs() {
|
||||
const query = document.getElementById('logSearch').value.toLowerCase();
|
||||
const showInfo = document.getElementById('checkInfo').checked;
|
||||
const showWarn = document.getElementById('checkWarn').checked;
|
||||
const showError = document.getElementById('checkError').checked;
|
||||
|
||||
const filtered = allLogs.filter(line => {
|
||||
const lower = line.toLowerCase();
|
||||
|
||||
// 레벨 체크 (매우 단순화)
|
||||
let levelMatch = false;
|
||||
|
||||
const isError = lower.includes('[error]') || lower.includes('error:') || lower.includes('traceback');
|
||||
const isWarn = lower.includes('[warning]') || lower.includes('[warn]') || lower.includes('warn:');
|
||||
const isInfo = lower.includes('[info]') || lower.includes('info:');
|
||||
|
||||
if (isError) {
|
||||
if (showError) levelMatch = true;
|
||||
} else if (isWarn) {
|
||||
if (showWarn) levelMatch = true;
|
||||
} else if (isInfo) {
|
||||
if (showInfo) levelMatch = true;
|
||||
} else {
|
||||
// 레벨 키워드가 없는 줄은 기본적으로 표시 (맥락 유지)
|
||||
levelMatch = true;
|
||||
}
|
||||
|
||||
if (!levelMatch) return false;
|
||||
|
||||
// 검색어 체크
|
||||
if (query && !lower.includes(query)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// 현재 스크롤 위치 저장? 아니면 항상 아래로? -> 보통 필터링하면 아래로 가는게 편함
|
||||
const currentModel = editor.getModel();
|
||||
if (currentModel) {
|
||||
currentModel.setValue(filtered.join('\n'));
|
||||
}
|
||||
// editor.revealLine(editor.getModel().getLineCount());
|
||||
}
|
||||
|
||||
// 이벤트 연결
|
||||
document.getElementById('logSearch').addEventListener('keyup', updateLogs);
|
||||
document.getElementById('checkInfo').addEventListener('change', updateLogs);
|
||||
document.getElementById('checkWarn').addEventListener('change', updateLogs);
|
||||
document.getElementById('checkError').addEventListener('change', updateLogs);
|
||||
|
||||
// 맨 아래로 버튼
|
||||
document.getElementById('btnScrollBottom').addEventListener('click', function () {
|
||||
editor.revealLine(editor.getModel().getLineCount());
|
||||
});
|
||||
|
||||
// 초기 스크롤 (약간의 지연 후)
|
||||
setTimeout(() => {
|
||||
editor.revealLine(editor.getModel().getLineCount());
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,399 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}시스템 설정 - Dell Server Info{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin_settings.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold mb-0">
|
||||
<i class="bi bi-gear-fill text-primary me-2"></i>시스템 설정
|
||||
</h2>
|
||||
<a href="{{ url_for('admin.admin_panel') }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>관리자 패널로 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- TSR 저장소 설정 섹션 -->
|
||||
<div class="card border shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<h5 class="mb-1 fw-bold">
|
||||
<i class="bi bi-folder-symlink text-primary me-2"></i>TSR 저장소 설정
|
||||
</h5>
|
||||
<p class="text-muted small mb-0">System Control의 TSR Save가 사용할 CIFS Share URL과 인증 정보를 관리합니다.</p>
|
||||
</div>
|
||||
<span class="badge bg-primary-subtle text-primary border border-primary-subtle rounded-pill">
|
||||
TSR Save
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('admin.save_tsr_share_settings') }}" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="tsr-share-url-box mb-3">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<label for="tsrShareUrl" class="form-label fw-bold mb-1">Share URL</label>
|
||||
<p class="text-muted small mb-0">TSR Save 결과가 저장될 CIFS 공유 경로를 입력합니다.</p>
|
||||
</div>
|
||||
<span class="tsr-share-url-badge align-self-start">
|
||||
<i class="bi bi-hdd-network me-1"></i>CIFS Share
|
||||
</span>
|
||||
</div>
|
||||
<div class="input-group input-group-lg tsr-share-url-input">
|
||||
<span class="input-group-text"><i class="bi bi-link-45deg"></i></span>
|
||||
<input type="text" class="form-control font-monospace" id="tsrShareUrl" name="share_url"
|
||||
value="{{ tsr_share_settings.share_url }}" placeholder="//10.10.3.15/share/" required>
|
||||
</div>
|
||||
<div class="tsr-share-url-help mt-2">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
예: <span class="font-monospace">//10.10.3.15/share/</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<label for="tsrShareUser" class="form-label fw-semibold">Share 계정</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-person"></i></span>
|
||||
<input type="text" class="form-control" id="tsrShareUser" name="share_user"
|
||||
value="{{ tsr_share_settings.share_user }}" placeholder="User">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<label for="tsrSharePassword" class="form-label fw-semibold">Share 패스워드</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="password" class="form-control" id="tsrSharePassword" name="share_password"
|
||||
value="{{ tsr_share_settings.share_password }}" placeholder="Password"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mt-3">
|
||||
<div class="text-muted small">
|
||||
iDRAC 접속 계정은 기존처럼 <span class="font-monospace">IDRAC_USER</span>,
|
||||
<span class="font-monospace">IDRAC_PASS</span> 환경변수를 사용합니다.
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary px-4">
|
||||
<i class="bi bi-save me-1"></i>저장
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 텔레그램 봇 설정 섹션 -->
|
||||
<div class="card border shadow-sm mb-4">
|
||||
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0 fw-bold">
|
||||
<i class="bi bi-telegram text-info me-2"></i>텔레그램 봇 관리
|
||||
</h5>
|
||||
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addBotModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>봇 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info d-flex align-items-center" role="alert">
|
||||
<i class="bi bi-info-circle-fill me-2 flex-shrink-0 fs-5"></i>
|
||||
<div>
|
||||
등록된 모든 <strong>활성(Active)</strong> 봇에게 알림이 동시에 전송됩니다.<br>
|
||||
<small class="text-muted">알림 종류: 로그인, 회원가입, 로그아웃, 서버 작업 등</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if bots %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-4">
|
||||
{% for bot in bots %}
|
||||
<div class="col">
|
||||
<div class="card h-100 border-0 shadow-sm hover-shadow transition-all">
|
||||
<div
|
||||
class="card-header bg-white border-0 pt-3 pb-0 d-flex justify-content-between align-items-start">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar-circle bg-primary bg-opacity-10 text-primary me-2 rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="width: 40px; height: 40px;">
|
||||
<i class="bi bi-robot fs-5"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="card-title fw-bold mb-0 text-truncate" style="max-width: 150px;"
|
||||
title="{{ bot.name }}">
|
||||
{{ bot.name }}
|
||||
</h5>
|
||||
<small class="text-muted" style="font-size: 0.75rem;">ID: {{ bot.id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% if bot.is_active %}
|
||||
<span
|
||||
class="badge bg-success-subtle text-success border border-success-subtle rounded-pill px-2">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>Active
|
||||
</span>
|
||||
{% else %}
|
||||
<span
|
||||
class="badge bg-secondary-subtle text-secondary border border-secondary-subtle rounded-pill px-2">
|
||||
<i class="bi bi-dash-circle me-1"></i>Inactive
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<p class="card-text text-muted small mb-3 text-truncate-2" style="min-height: 2.5em;">
|
||||
{{ bot.description or "설명이 없습니다." }}
|
||||
</p>
|
||||
|
||||
<div class="bg-light rounded p-2 mb-2">
|
||||
<div class="d-flex align-items-center text-secondary small mb-1">
|
||||
<i class="bi bi-key me-2 text-primary"></i>
|
||||
<span class="fw-semibold me-1">Token:</span>
|
||||
<span class="font-monospace text-truncate">{{ bot.token[:10] }}...</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center text-secondary small">
|
||||
<i class="bi bi-chat-dots me-2 text-success"></i>
|
||||
<span class="fw-semibold me-1">Chat ID:</span>
|
||||
<span class="font-monospace">{{ bot.chat_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{% set types = (bot.notification_types or "").split(",") %}
|
||||
{% if 'auth' in types %}
|
||||
<span
|
||||
class="badge bg-info-subtle text-info border border-info-subtle rounded-pill me-1"><i
|
||||
class="bi bi-shield-lock-fill me-1"></i>인증</span>
|
||||
{% endif %}
|
||||
{% if 'activity' in types %}
|
||||
<span
|
||||
class="badge bg-warning-subtle text-warning border border-warning-subtle rounded-pill me-1"><i
|
||||
class="bi bi-activity me-1"></i>활동</span>
|
||||
{% endif %}
|
||||
{% if 'system' in types %}
|
||||
<span
|
||||
class="badge bg-danger-subtle text-danger border border-danger-subtle rounded-pill me-1"><i
|
||||
class="bi bi-gear-fill me-1"></i>시스템</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer bg-white border-0 pt-0 pb-3">
|
||||
<div class="btn-group w-100 shadow-sm" role="group">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal"
|
||||
data-bs-target="#editBotModal{{ bot.id }}">
|
||||
<i class="bi bi-pencil me-1"></i>수정
|
||||
</button>
|
||||
|
||||
<button type="submit" form="testForm{{ bot.id }}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-send me-1"></i>테스트
|
||||
</button>
|
||||
|
||||
<button type="submit" form="deleteForm{{ bot.id }}"
|
||||
class="btn btn-outline-danger btn-sm" onclick="return confirm('정말 삭제하시겠습니까?');">
|
||||
<i class="bi bi-trash me-1"></i>삭제
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Forms -->
|
||||
<form id="testForm{{ bot.id }}" action="{{ url_for('admin.test_bot', bot_id=bot.id) }}"
|
||||
method="post" class="d-none">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
</form>
|
||||
<form id="deleteForm{{ bot.id }}" action="{{ url_for('admin.delete_bot', bot_id=bot.id) }}"
|
||||
method="post" class="d-none">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 수정 모달 -->
|
||||
<div class="modal fade" id="editBotModal{{ bot.id }}" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<form action="{{ url_for('admin.edit_bot', bot_id=bot.id) }}" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title fw-bold">
|
||||
<i class="bi bi-pencil-square me-2"></i>봇 설정 수정
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">이름 (식별용)</label>
|
||||
<input type="text" class="form-control" name="name" value="{{ bot.name }}"
|
||||
required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Bot Token</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="token"
|
||||
value="{{ bot.token }}" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Chat ID</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-chat-dots"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="chat_id"
|
||||
value="{{ bot.chat_id }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">알림 유형</label>
|
||||
<div class="d-flex gap-3">
|
||||
{% set types = (bot.notification_types or "").split(",") %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="notify_types"
|
||||
value="auth" id="edit_auth_{{ bot.id }}" {{ 'checked' if 'auth' in
|
||||
types else '' }}>
|
||||
<label class="form-check-label" for="edit_auth_{{ bot.id }}">
|
||||
인증
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="notify_types"
|
||||
value="activity" id="edit_activity_{{ bot.id }}" {{ 'checked'
|
||||
if 'activity' in types else '' }}>
|
||||
<label class="form-check-label" for="edit_activity_{{ bot.id }}">
|
||||
활동
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="notify_types"
|
||||
value="system" id="edit_system_{{ bot.id }}" {{ 'checked'
|
||||
if 'system' in types else '' }}>
|
||||
<label class="form-check-label" for="edit_system_{{ bot.id }}">
|
||||
시스템
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">선택한 알림 유형만 전송됩니다</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">설명</label>
|
||||
<textarea class="form-control" name="description"
|
||||
rows="2">{{ bot.description or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="is_active"
|
||||
id="activeCheck{{ bot.id }}" {{ 'checked' if bot.is_active else '' }}>
|
||||
<label class="form-check-label" for="activeCheck{{ bot.id }}">
|
||||
활성화
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="submit" class="btn btn-primary px-4">저장</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<div class="mb-3">
|
||||
<div class="d-inline-flex align-items-center justify-content-center bg-light rounded-circle"
|
||||
style="width: 80px; height: 80px;">
|
||||
<i class="bi bi-robot fs-1 text-secondary"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="text-muted fw-normal">등록된 텔레그램 봇이 없습니다.</h5>
|
||||
<p class="text-muted small mb-4">우측 상단의 '봇 추가' 버튼을 눌러 알림을 설정하세요.</p>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addBotModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>봇 추가
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 봇 추가 모달 -->
|
||||
<div class="modal fade" id="addBotModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow">
|
||||
<form action="{{ url_for('admin.add_bot') }}" method="post">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="modal-header bg-light">
|
||||
<h5 class="modal-title fw-bold">
|
||||
<i class="bi bi-plus-circle me-2"></i>새 텔레그램 봇 추가
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">이름 (식별용)</label>
|
||||
<input type="text" class="form-control" name="name" placeholder="예: 알림용 봇" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Bot Token</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-key"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="token"
|
||||
placeholder="123456:ABC..." required>
|
||||
</div>
|
||||
<div class="form-text">BotFather에게 받은 API Token</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Chat ID</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-chat-dots"></i></span>
|
||||
<input type="text" class="form-control font-monospace" name="chat_id"
|
||||
placeholder="-100..." required>
|
||||
</div>
|
||||
<div class="form-text">메시지를 받을 채팅방 ID (그룹은 음수)</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">알림 유형</label>
|
||||
<div class="d-flex gap-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="notify_types" value="auth"
|
||||
id="add_auth" checked>
|
||||
<label class="form-check-label" for="add_auth">
|
||||
인증
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="notify_types" value="activity"
|
||||
id="add_activity" checked>
|
||||
<label class="form-check-label" for="add_activity">
|
||||
활동
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="notify_types" value="system"
|
||||
id="add_system" checked>
|
||||
<label class="form-check-label" for="add_system">
|
||||
시스템
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">선택한 알림 유형만 전송됩니다</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">설명</label>
|
||||
<textarea class="form-control" name="description" rows="2" placeholder="선택 사항"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="submit" class="btn btn-primary px-4">추가</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,173 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="description" content="Dell Server 정보 및 MAC 주소 처리 시스템">
|
||||
|
||||
<title>{% block title %}Dell Server Info, MAC 정보 처리{% endblock %}</title>
|
||||
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<!-- Bootstrap 5.3.3 CSS -->
|
||||
<link href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap-icons/font/bootstrap-icons.min.css') }}">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Skip to main content (접근성) -->
|
||||
<a href="#main-content" class="visually-hidden-focusable">본문으로 건너뛰기</a>
|
||||
|
||||
{# 플래시 메시지 (좌측 상단 토스트 스타일) #}
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 2000; margin-top: 60px;">
|
||||
{% for cat, msg in messages %}
|
||||
<div class="toast align-items-center text-white bg-{{ 'success' if cat == 'success' else 'danger' if cat == 'error' else 'primary' }} border-0 fade show"
|
||||
role="alert" aria-live="assertive" aria-atomic="true" data-bs-delay="3000">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body d-flex align-items-center">
|
||||
<i
|
||||
class="bi bi-{{ 'check-circle-fill' if cat == 'success' else 'exclamation-diamond-fill' }} me-2 fs-5"></i>
|
||||
<div>{{ msg }}</div>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"
|
||||
aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('home.home') }}">
|
||||
<i class="bi bi-server me-2"></i>
|
||||
Dell Server Info
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="네비게이션 토글">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('home.home') }}">
|
||||
<i class="bi bi-house-door me-1"></i>Home
|
||||
</a>
|
||||
</li>
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('main.index') }}">
|
||||
<i class="bi bi-hdd-network me-1"></i>ServerInfo
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('xml.xml_management') }}">
|
||||
<i class="bi bi-file-code me-1"></i>XML Management
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('jobs.jobs_page') }}">
|
||||
<i class="bi bi-list-task me-1"></i>Job Monitor
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('bios_baseline.index') }}">
|
||||
<i class="bi bi-clipboard-data me-1"></i>BIOS Baseline
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('script_manager.index') }}">
|
||||
<i class="bi bi-code-square me-1"></i>스크립트 관리
|
||||
</a>
|
||||
</li>
|
||||
{% if current_user.is_admin %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('admin.admin_panel') }}">
|
||||
<i class="bi bi-shield-lock me-1"></i>Admin
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.logout') }}">
|
||||
<i class="bi bi-box-arrow-right me-1"></i>Logout
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.login') }}">
|
||||
<i class="bi bi-box-arrow-in-right me-1"></i>Login
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.register') }}">
|
||||
<i class="bi bi-person-plus me-1"></i>Register
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main id="main-content"
|
||||
class="{% if request.endpoint in ['auth.login', 'auth.register', 'auth.reset_password'] %}container mt-5{% else %}container mt-4 container-card{% endif %}">
|
||||
|
||||
{# 플래시 메시지 (컨텐츠 상단 표시) #}
|
||||
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer (선택사항) -->
|
||||
<footer class="mt-5 py-3 bg-light text-center">
|
||||
<div class="container">
|
||||
<small class="text-muted">© 2025 Dell Server Info. All rights reserved.</small>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap 5.3.3 JS Bundle (Popper.js 포함) -->
|
||||
<script src="{{ url_for('static', filename='vendor/bootstrap/js/bootstrap.bundle.min.js') }}"></script>
|
||||
|
||||
<!-- Socket.IO (필요한 경우만) -->
|
||||
{% if config.USE_SOCKETIO %}
|
||||
<script src="{{ url_for('static', filename='vendor/socket.io/socket.io-4.7.4.min.js') }}"></script>
|
||||
{% endif %}
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
<!-- Auto-hide Toasts -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var toastElList = [].slice.call(document.querySelectorAll('.toast'));
|
||||
var toastList = toastElList.map(function (toastEl) {
|
||||
// 부트스트랩 토스트 인스턴스 생성 (autohide: true 기본값)
|
||||
var toast = new bootstrap.Toast(toastEl, { delay: 3000 });
|
||||
toast.show();
|
||||
|
||||
// 3초 후 자동으로 DOM에서 제거하고 싶다면 이벤트 리스너 추가 가능
|
||||
toastEl.addEventListener('hidden.bs.toast', function () {
|
||||
// toastEl.remove(); // 필요시 제거
|
||||
});
|
||||
return toast;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,512 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
/* 모달이 Navbar 위에 오도록 z-index 조정 */
|
||||
.modal {
|
||||
z-index: 1060 !important;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: 1050 !important;
|
||||
}
|
||||
|
||||
/* 모달 헤더가 항상 잘 보이도록 배경색 및 위치 조정 */
|
||||
.modal-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1070;
|
||||
}
|
||||
|
||||
/* 모달이 너무 위로 붙지 않게 여백 추가 */
|
||||
.modal-dialog {
|
||||
margin-top: 5rem;
|
||||
}
|
||||
|
||||
/* ===== 탭 가시성 개선 ===== */
|
||||
/* Pills 스타일 탭 버튼 */
|
||||
.nav-pills .nav-link {
|
||||
color: #495057 !important;
|
||||
background-color: #e9ecef !important;
|
||||
border: 1px solid #ced4da !important;
|
||||
margin-right: 0.25rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link:hover {
|
||||
background-color: #dee2e6 !important;
|
||||
border-color: #adb5bd !important;
|
||||
color: #212529 !important;
|
||||
}
|
||||
|
||||
.nav-pills .nav-link.active {
|
||||
background-color: #0d6efd !important;
|
||||
border-color: #0d6efd !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Select 드롭다운 가시성 개선 */
|
||||
.form-select {
|
||||
border: 2px solid #ced4da !important;
|
||||
font-weight: 500;
|
||||
color: #212529 !important;
|
||||
background-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
border-color: #0d6efd !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
|
||||
}
|
||||
|
||||
.form-select option {
|
||||
color: #212529 !important;
|
||||
background-color: #ffffff !important;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* 탭 컨테이너 배경 */
|
||||
.nav-pills-container {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<!-- 헤더 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2 class="fw-bold mb-1">
|
||||
<i class="bi bi-clipboard-data text-primary me-2"></i>
|
||||
BIOS Baseline 비교
|
||||
</h2>
|
||||
<p class="text-muted mb-0">납품 전 서버의 BIOS 설정을 표준 설정과 비교합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 메인 작업 영역 -->
|
||||
<div class="row g-4 mb-4">
|
||||
<!-- 왼쪽: IP 입력 및 설정 -->
|
||||
<div class="col-lg-5">
|
||||
<div class="card border shadow-sm h-100">
|
||||
<div class="card-header bg-light border-0 py-2">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-search me-2"></i>
|
||||
비교 설정
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4 d-flex flex-column">
|
||||
|
||||
<!-- Baseline 선택 -->
|
||||
<div class="mb-3">
|
||||
<label for="baseline-select" class="form-label">Baseline 선택</label>
|
||||
<select id="baseline-select" class="form-select">
|
||||
<option value="">Baseline을 선택하세요</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary mt-2 w-100" onclick="showBaselineManageModal()">
|
||||
<i class="bi bi-plus-circle me-1"></i> 새 Baseline 생성
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- IP 주소 입력 -->
|
||||
<div class="mb-3 flex-grow-1 d-flex flex-column">
|
||||
<label for="ip-addresses"
|
||||
class="form-label w-100 d-flex justify-content-between align-items-end mb-2">
|
||||
<span class="mb-1">
|
||||
서버 IP 주소
|
||||
<span class="badge bg-secondary ms-1" id="ip-count">0</span>
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary px-2 py-1"
|
||||
onclick="clearIPs()" title="입력 내용 지우기" style="font-size: 0.75rem;">
|
||||
<i class="bi bi-trash me-1"></i>지우기
|
||||
</button>
|
||||
</label>
|
||||
<textarea id="ip-addresses" class="form-control font-monospace flex-grow-1"
|
||||
placeholder="예: 10.10.0.1 10.10.0.2 10.10.0.3" style="resize: none;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 인증 정보 -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<input type="text" id="username" class="form-control" value="root" placeholder="Username">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<input type="password" id="password" class="form-control" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 비교 시작 버튼 -->
|
||||
<button onclick="startComparison()" class="btn btn-primary w-100 py-2">
|
||||
<i class="bi bi-arrows-angle-contract me-1"></i> 비교 시작
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 오른쪽: 결과 요약 -->
|
||||
<div class="col-lg-7">
|
||||
<div class="card border shadow-sm h-100">
|
||||
<div class="card-header bg-light border-0 py-2">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-bar-chart me-2"></i>
|
||||
비교 결과 요약
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4" id="summary-section">
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="bi bi-info-circle fs-1 mb-3"></i>
|
||||
<p class="mb-0">비교를 시작하면 결과가 여기에 표시됩니다</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 진행률 -->
|
||||
<div class="row mb-4" id="progress-section" style="display: none;">
|
||||
<div class="col">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="bi bi-activity text-primary me-2"></i>
|
||||
<span class="fw-semibold">처리 진행률</span>
|
||||
</div>
|
||||
<div class="progress" style="height: 25px;">
|
||||
<div id="progress-bar"
|
||||
class="progress-bar progress-bar-striped progress-bar-animated bg-success"
|
||||
role="progressbar" style="width: 0%">
|
||||
<span class="fw-semibold">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 상세 비교 결과 -->
|
||||
<div class="row" id="results-section" style="display: none;">
|
||||
<div class="col">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light border-0 py-2 d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="bi bi-list-check me-2"></i>
|
||||
상세 비교 결과
|
||||
</h6>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="toggleDiffOnly()" id="diff-only-btn">
|
||||
<i class="bi bi-funnel me-1"></i> 차이점만 보기
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="downloadResults()">
|
||||
<i class="bi bi-download me-1"></i> Excel 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-4" id="results-container">
|
||||
<!-- 서버별 결과가 여기에 표시됨 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Baseline 관리 모달 -->
|
||||
<div class="modal fade" id="baseline-manage-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-clipboard-data me-2"></i>
|
||||
Baseline 관리
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- 탭 -->
|
||||
<div class="nav-pills-container">
|
||||
<ul class="nav nav-pills mb-0" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="pill" data-bs-target="#create-tab">
|
||||
새로 생성
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#list-tab">
|
||||
목록 관리
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- 생성 탭 -->
|
||||
<div class="tab-pane fade show active" id="create-tab">
|
||||
<form id="create-baseline-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">서버 IP 주소 *</label>
|
||||
<input type="text" class="form-control" id="create-ip" placeholder="10.10.0.1" required>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="create-username" value="root">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Password *</label>
|
||||
<input type="password" class="form-control" id="create-password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Baseline 이름 *</label>
|
||||
<input type="text" class="form-control" id="create-name"
|
||||
placeholder="R6615_GPU_Standard_2024" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">서버 타입</label>
|
||||
<input type="text" class="form-control" id="create-type" placeholder="GPU Server">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설명</label>
|
||||
<textarea class="form-control" id="create-description" rows="2"
|
||||
placeholder="표준 GPU 서버 설정"></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-download me-1"></i> Redfish로 가져와서 생성
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 목록 탭 -->
|
||||
<div class="tab-pane fade" id="list-tab">
|
||||
<div id="baseline-list">
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="bi bi-hourglass-split"></i> 로딩 중...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Baseline 상세 편집 모달 -->
|
||||
<div class="modal fade" id="edit-baseline-modal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi bi-pencil-square me-2"></i>
|
||||
Baseline 상세 편집
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body bg-light">
|
||||
<div class="row g-3">
|
||||
<!-- 왼쪽: 메타데이터 편집 -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white fw-bold">기본 정보</div>
|
||||
<div class="card-body">
|
||||
<form id="edit-baseline-meta-form">
|
||||
<input type="hidden" id="edit-id">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">Baseline 이름</label>
|
||||
<input type="text" class="form-control" id="edit-name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">서버 모델</label>
|
||||
<input type="text" class="form-control bg-light" id="edit-model" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">서버 타입</label>
|
||||
<input type="text" class="form-control" id="edit-type">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">설명</label>
|
||||
<textarea class="form-control" id="edit-description" rows="4"></textarea>
|
||||
</div>
|
||||
<div class="text-muted small">
|
||||
<p class="mb-1"><i class="bi bi-calendar me-1"></i>생성일: <span
|
||||
id="edit-created-at">-</span></p>
|
||||
<p class="mb-1"><i class="bi bi-cpu me-1"></i>원본 IP: <span
|
||||
id="edit-source-ip">-</span></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 오른쪽: BIOS 설정 편집 -->
|
||||
<!-- 오른쪽: 상세 설정 편집 -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card h-100 border shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<!-- 탭 메뉴 -->
|
||||
<div class="nav-pills-container">
|
||||
<ul class="nav nav-pills mb-0" id="edit-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="tab-edit-bios" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-bios" type="button">BIOS</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-idrac" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-idrac" type="button">iDRAC</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-raid" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-raid" type="button">RAID</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-nic" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-nic" type="button">NIC</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-boot" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-boot" type="button">Boot</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tab-edit-firmware" data-bs-toggle="pill"
|
||||
data-bs-target="#content-edit-firmware" type="button">Firmware</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tab-content h-100">
|
||||
|
||||
<!-- BIOS 탭 -->
|
||||
<div class="tab-pane fade show active h-100" id="content-edit-bios">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
||||
<span class="small text-muted fw-bold ms-2">항목 수: <span
|
||||
id="edit-bios-count">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary bg-white"
|
||||
onclick="addSettingRow('bios')">
|
||||
<i class="bi bi-plus-lg"></i> 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 450px;">
|
||||
<table class="table table-hover mb-0" id="table-edit-bios">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th style="width: 40%;">설정 항목 (Key)</th>
|
||||
<th style="width: 40%;">설정값 (Value)</th>
|
||||
<th style="width: 20%;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- JS 채움 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- iDRAC 탭 -->
|
||||
<div class="tab-pane fade h-100" id="content-edit-idrac">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
||||
<span class="small text-muted fw-bold ms-2">항목 수: <span
|
||||
id="edit-idrac-count">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary bg-white"
|
||||
onclick="addSettingRow('idrac')">
|
||||
<i class="bi bi-plus-lg"></i> 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 450px;">
|
||||
<table class="table table-hover mb-0" id="table-edit-idrac">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th style="width: 40%;">설정 항목 (Key)</th>
|
||||
<th style="width: 40%;">설정값 (Value)</th>
|
||||
<th style="width: 20%;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- JS 채움 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RAID 탭 -->
|
||||
<div class="tab-pane fade h-100 p-3" id="content-edit-raid">
|
||||
<div class="alert alert-info py-2 small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
RAID 구성은 복잡하므로 현재 JSON 직접 편집만 지원합니다. (Read-Only 권장)
|
||||
</div>
|
||||
<textarea class="form-control font-monospace small" id="json-edit-raid"
|
||||
rows="15" style="height: 400px;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- NIC 탭 -->
|
||||
<div class="tab-pane fade h-100 p-3" id="content-edit-nic">
|
||||
<div class="alert alert-info py-2 small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
NIC 설정은 JSON 형식으로 편집합니다.
|
||||
</div>
|
||||
<textarea class="form-control font-monospace small" id="json-edit-nic" rows="15"
|
||||
style="height: 400px;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Boot 탭 -->
|
||||
<div class="tab-pane fade h-100 p-3" id="content-edit-boot">
|
||||
<div class="alert alert-info py-2 small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
부팅 순서 및 설정을 JSON 형식으로 편집합니다.
|
||||
</div>
|
||||
<textarea class="form-control font-monospace small" id="json-edit-boot"
|
||||
rows="15" style="height: 400px;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Firmware 탭 -->
|
||||
<div class="tab-pane fade h-100" id="content-edit-firmware">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center p-2 border-bottom bg-light">
|
||||
<span class="small text-muted fw-bold ms-2">항목 수: <span
|
||||
id="edit-firmware-count">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary bg-white"
|
||||
onclick="addSettingRow('firmware')">
|
||||
<i class="bi bi-plus-lg"></i> 항목 추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="table-responsive" style="max-height: 450px;">
|
||||
<table class="table table-hover mb-0" id="table-edit-firmware">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th style="width: 40%;">컴포넌트</th>
|
||||
<th style="width: 40%;">버전</th>
|
||||
<th style="width: 20%;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><!-- JS 채움 --></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveBaselineChanges()">
|
||||
<i class="bi bi-save me-1"></i> 변경사항 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{ url_for('static', filename='js/bios_baseline.js') }}?v={{ range(1, 100000) | random }}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,170 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}XML 편집: {{ filename }} - Dell Server Info{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* 전체 레이아웃 */
|
||||
.editor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 160px);
|
||||
/* 헤더/푸터 제외 높이 (조정 가능) */
|
||||
min-height: 600px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 툴바 (헤더) */
|
||||
.editor-toolbar {
|
||||
background-color: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* 에디터 본문 */
|
||||
#monaco-editor-root {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 로딩 인디케이터 */
|
||||
.editor-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
color: #64748b;
|
||||
font-size: 1.2rem;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4 h-100">
|
||||
<!-- Breadcrumb / Navigation -->
|
||||
<div class="mb-3 d-flex align-items-center">
|
||||
<a href="{{ url_for('xml.xml_management') }}" class="text-decoration-none text-muted small fw-bold">
|
||||
<i class="bi bi-arrow-left me-1"></i>목록으로 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form id="editorForm" method="post" style="height: 100%;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<!-- Monaco Editor의 내용은 submit 시 이 textarea에 동기화됨 -->
|
||||
<textarea name="content" id="hiddenContent" style="display:none;">{{ content }}</textarea>
|
||||
|
||||
<div class="editor-container">
|
||||
<!-- Toolbar -->
|
||||
<div class="editor-toolbar">
|
||||
<div class="editor-title">
|
||||
<i class="bi bi-filetype-xml text-primary fs-4"></i>
|
||||
<span>{{ filename }}</span>
|
||||
<span class="badge bg-light text-secondary border ms-2">XML</span>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<!-- 포맷팅 버튼 (Monaco 기능 호출) -->
|
||||
<button type="button" class="btn btn-white border text-dark btn-sm fw-bold" id="btnFormat">
|
||||
<i class="bi bi-magic me-1 text-info"></i> 자동 정렬
|
||||
</button>
|
||||
<!-- 저장 버튼 -->
|
||||
<button type="submit" class="btn btn-primary btn-sm fw-bold px-4">
|
||||
<i class="bi bi-save me-1"></i> 저장하기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Area -->
|
||||
<div id="monaco-editor-root">
|
||||
<div class="editor-loading">
|
||||
<div class="spinner-border text-primary me-3" role="status"></div>
|
||||
<div>에디터를 불러오는 중...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<!-- Monaco Editor Loader -->
|
||||
<script src="{{ url_for('static', filename='vendor/monaco-editor/min/vs/loader.js') }}"></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
if (typeof require === 'undefined') {
|
||||
document.querySelector('.editor-loading').innerHTML =
|
||||
'<div class="text-danger"><i class="bi bi-exclamation-triangle me-2"></i>Monaco Editor를 로드할 수 없습니다. 인터넷 연결을 확인하거나 CDN 차단을 확인하세요.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
require.config({ paths: { 'vs': "{{ url_for('static', filename='vendor/monaco-editor/min/vs') }}" } });
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
// 초기 컨텐츠 가져오기
|
||||
var initialContent = document.getElementById('hiddenContent').value;
|
||||
var container = document.getElementById('monaco-editor-root');
|
||||
|
||||
// 기존 로딩 메시지 제거
|
||||
container.innerHTML = '';
|
||||
|
||||
// 에디터 생성
|
||||
var editor = monaco.editor.create(container, {
|
||||
value: initialContent,
|
||||
language: 'xml',
|
||||
theme: 'vs', // or 'vs-dark'
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: true },
|
||||
fontSize: 14,
|
||||
scrollBeyondLastLine: false,
|
||||
lineNumbers: 'on',
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
wordWrap: 'on'
|
||||
});
|
||||
|
||||
// 1. 폼 제출 시 에디터 내용을 textarea에 동기화
|
||||
document.getElementById('editorForm').addEventListener('submit', function () {
|
||||
document.getElementById('hiddenContent').value = editor.getValue();
|
||||
});
|
||||
|
||||
// 2. 자동 정렬(Format) 버튼 기능 연결
|
||||
document.getElementById('btnFormat').addEventListener('click', function () {
|
||||
editor.getAction('editor.action.formatDocument').run();
|
||||
});
|
||||
|
||||
// 3. Ctrl+S 저장 단축키 지원
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () {
|
||||
document.getElementById('editorForm').requestSubmit();
|
||||
});
|
||||
|
||||
}, function (err) {
|
||||
// 로드 실패 시 에러 표시
|
||||
document.querySelector('.editor-loading').innerHTML =
|
||||
'<div class="text-danger"><i class="bi bi-exclamation-triangle me-2"></i>에디터 리소스 로드 실패: ' + err.message + '</div>';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Dell Server & NAVER Settings Info</h2>
|
||||
<p>사이트 가입 및 로그인후 이용 가능</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<title>Dell iDRAC 멀티 서버 관리</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/idrac_style.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🖥️ Dell iDRAC 멀티 서버 관리</h1>
|
||||
<p class="subtitle">서버 등록, 연결 상태 확인 및 설정 정밀 분석 시스템</p>
|
||||
</header>
|
||||
|
||||
<!-- 서버 관리 섹션 -->
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<h2>📋 서버 목록 관리</h2>
|
||||
<div class="header-actions">
|
||||
<button onclick="showAddServerModal()" class="btn btn-primary">+ 서버 추가</button>
|
||||
<a href="/server-config/" class="btn btn-secondary">⚙️ 설정 정밀 분석 및 DB 동기화</a>
|
||||
<button onclick="refreshServers()" class="btn btn-info">🔄 새로고침</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 그룹 필터 -->
|
||||
<div class="filter-group">
|
||||
<label>그룹:</label>
|
||||
<select id="group-filter" onchange="filterByGroup()">
|
||||
<option value="all">전체</option>
|
||||
</select>
|
||||
<span id="server-count" class="count-badge">0대</span>
|
||||
</div>
|
||||
|
||||
<!-- 서버 목록 테이블 -->
|
||||
<div class="table-container">
|
||||
<table class="server-table" id="server-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="40"><input type="checkbox" id="select-all" onchange="toggleSelectAll()"></th>
|
||||
<th>서버명</th>
|
||||
<th>IP 주소</th>
|
||||
<th>그룹</th>
|
||||
<th>상태</th>
|
||||
<th>모델</th>
|
||||
<th>최근 연결</th>
|
||||
<th width="150">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="server-list">
|
||||
<tr>
|
||||
<td colspan="8" class="empty-message">등록된 서버가 없습니다</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 선택 작업 -->
|
||||
<div class="bulk-actions">
|
||||
<span id="selected-count">선택: 0대</span>
|
||||
<button onclick="testSelectedConnections()" class="btn btn-info">연결 테스트</button>
|
||||
<button onclick="rebootSelected()" class="btn btn-danger">재부팅</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>Dell iDRAC 멀티 서버 관리 시스템</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 서버 추가 모달 -->
|
||||
<div id="add-server-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>서버 추가</h3>
|
||||
<span class="close" onclick="closeAddServerModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="server-name">서버명 *</label>
|
||||
<input type="text" id="server-name" placeholder="예: Server-01">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="server-ip">iDRAC IP *</label>
|
||||
<input type="text" id="server-ip" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="server-username">사용자명</label>
|
||||
<input type="text" id="server-username" value="root">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="server-password">비밀번호 *</label>
|
||||
<input type="password" id="server-password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="server-group">그룹</label>
|
||||
<input type="text" id="server-group" placeholder="예: 삼성전자 납품">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="server-model">모델</label>
|
||||
<input type="text" id="server-model" placeholder="예: PowerEdge R750">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button onclick="closeAddServerModal()" class="btn btn-secondary">취소</button>
|
||||
<button onclick="addServer()" class="btn btn-primary">추가</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='vendor/socket.io/socket.io-4.5.4.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/idrac_main.js') }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}iDRAC Job Queue 모니터링 (Redfish){% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/jobs.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<!-- CSRF Token for JavaScript -->
|
||||
<script>
|
||||
const csrfToken = "{{ csrf_token() }}";
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/jobs.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-3">
|
||||
<!-- 헤더 -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-list-task"></i> iDRAC Job Queue 모니터링
|
||||
</h4>
|
||||
<span class="badge bg-success">Redfish API</span>
|
||||
<span id="last-updated" class="text-muted small"></span>
|
||||
<span id="loading-indicator" class="d-none">
|
||||
<span class="spinner-border spinner-border-sm" role="status"></span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="monitor-status" class="d-flex align-items-center gap-2">
|
||||
<span class="status-dot" id="status-dot"></span>
|
||||
<span class="small fw-semibold" id="status-text">모니터링 꺼짐</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP 입력 -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="fw-semibold">
|
||||
<i class="bi bi-hdd-network"></i> 모니터링 IP 목록
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button id="btn-load-file" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-file-earmark-text"></i> 파일에서 불러오기
|
||||
</button>
|
||||
<button id="btn-apply" class="btn btn-success btn-sm">
|
||||
<i class="bi bi-check-circle"></i> 적용
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted d-block mb-2">
|
||||
한 줄에 하나씩 입력 (쉼표/세미콜론/공백 구분 가능, # 주석 지원)
|
||||
</small>
|
||||
<textarea id="ipInput" class="form-control font-monospace" rows="4"
|
||||
placeholder="10.10.0.11 10.10.0.12 # 주석 가능"></textarea>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">총 <strong id="ip-count">0</strong>개 IP</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 컨트롤 -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row g-3 align-items-center">
|
||||
<div class="col-auto">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="monitorSwitch">
|
||||
<label class="form-check-label" for="monitorSwitch">
|
||||
<strong>모니터링 켜기</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<button id="btn-refresh" class="btn btn-primary btn-sm" disabled>
|
||||
<i class="bi bi-arrow-clockwise"></i> 지금 새로고침
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="autoRefreshSwitch">
|
||||
<label class="form-check-label" for="autoRefreshSwitch">
|
||||
자동 새로고침 (<span id="poll-interval">10</span>초)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="showCompletedSwitch" checked>
|
||||
<label class="form-check-label" for="showCompletedSwitch">
|
||||
최근 완료 Job 표시
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 통계 -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center">
|
||||
<div class="card-body py-2">
|
||||
<h6 class="card-title text-muted mb-1 small">총 서버</h6>
|
||||
<h3 class="mb-0" id="stat-total">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center border-info">
|
||||
<div class="card-body py-2">
|
||||
<h6 class="card-title text-info mb-1 small">진행 중</h6>
|
||||
<h3 class="mb-0 text-info" id="stat-running">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center border-success">
|
||||
<div class="card-body py-2">
|
||||
<h6 class="card-title text-success mb-1 small">완료</h6>
|
||||
<h3 class="mb-0 text-success" id="stat-completed">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center border-danger">
|
||||
<div class="card-body py-2">
|
||||
<h6 class="card-title text-danger mb-1 small">에러</h6>
|
||||
<h3 class="mb-0 text-danger" id="stat-error">0</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 테이블 -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm align-middle" id="jobs-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width:140px;">IP</th>
|
||||
<th>JID</th>
|
||||
<th>작업명</th>
|
||||
<th style="width:160px;">상태</th>
|
||||
<th style="width:140px;">진행률</th>
|
||||
<th>메시지</th>
|
||||
<th style="width:240px;">마지막 업데이트</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobs-body">
|
||||
<tr>
|
||||
<td colspan="7" class="text-center text-muted py-4">
|
||||
<i class="bi bi-power fs-2 d-block mb-2"></i>
|
||||
모니터링이 꺼져 있습니다. 상단 스위치를 켜면 조회가 시작됩니다.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-center mt-5">
|
||||
<div class="card shadow-sm p-4" style="min-width: 400px; max-width: 500px; width: 100%;">
|
||||
<h3 class="text-center mb-4">Login</h3>
|
||||
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group mb-3">
|
||||
{{ form.email.label(class="form-label") }}
|
||||
{{ form.email(class="form-control") }}
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
{{ form.password.label(class="form-label") }}
|
||||
{{ form.password(class="form-control") }}
|
||||
</div>
|
||||
<div class="form-group form-check mb-3">
|
||||
{{ form.remember(class="form-check-input") }}
|
||||
{{ form.remember.label(class="form-check-label") }}
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-block">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,552 @@
|
||||
{% 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;
|
||||
}
|
||||
|
||||
/* 테이블 뷰 스타일 */
|
||||
#xmlFileTable {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f8fafc;
|
||||
}
|
||||
|
||||
.file-icon-wrapper-sm {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #eff6ff;
|
||||
color: #3b82f6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.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="table-responsive">
|
||||
<table class="table table-hover table-sm align-middle mb-0" id="xmlFileTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th scope="col" style="width: 50px;" class="text-center">
|
||||
<input type="checkbox" class="form-check-input" id="checkAll">
|
||||
</th>
|
||||
<th scope="col">파일명</th>
|
||||
<th scope="col" style="width: 180px;" class="text-center">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for xml_file in xml_files %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input file-selector"
|
||||
value="{{ xml_file }}">
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="file-icon-wrapper-sm me-3">
|
||||
<i class="bi bi-filetype-xml fs-5"></i>
|
||||
</div>
|
||||
<span class="fw-bold text-dark">{{ xml_file }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="d-flex justify-content-center gap-1">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
onclick="openDeployModal('{{ xml_file }}')" title="배포">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}"
|
||||
class="btn btn-sm btn-outline-secondary" title="편집">
|
||||
<i class="bi bi-pencil-fill"></i>
|
||||
</a>
|
||||
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}"
|
||||
method="POST" class="d-inline-block m-0">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger"
|
||||
onclick="return confirm('정말 삭제하시겠습니까?')" title="삭제">
|
||||
<i class="bi bi-trash-fill"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</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)
|
||||
});
|
||||
|
||||
// 전체 선택 체크박스 로직
|
||||
const checkAll = document.getElementById('checkAll');
|
||||
if (checkAll) {
|
||||
checkAll.addEventListener('change', function () {
|
||||
const checkboxes = document.querySelectorAll('.file-selector');
|
||||
checkboxes.forEach(cb => cb.checked = checkAll.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-center mt-5">
|
||||
<div class="card shadow-sm p-4" style="min-width: 400px; max-width: 500px; width: 100%;">
|
||||
<h3 class="text-center mb-4">Register</h3>
|
||||
<form method="POST" action="{{ url_for('auth.register') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group mb-3">
|
||||
{{ form.username.label(class="form-label") }}
|
||||
{{ form.username(class="form-control") }}
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
{{ form.email.label(class="form-label") }}
|
||||
{{ form.email(class="form-control") }}
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
{{ form.password.label(class="form-label") }}
|
||||
{{ form.password(class="form-control") }}
|
||||
</div>
|
||||
<div class="form-group mb-4">
|
||||
{{ form.confirm_password.label(class="form-label") }}
|
||||
{{ form.confirm_password(class="form-control") }}
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-block">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,173 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}설정 파일 비교: {{ file1 }} vs {{ file2 }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<!-- Monaco Diff Editor Styles -->
|
||||
<style>
|
||||
.diff-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 140px);
|
||||
min-height: 600px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diff-toolbar {
|
||||
background-color: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 0.75rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.diff-files {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.file-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #fff;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.diff-arrow {
|
||||
color: #94a3b8;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
#monaco-diff-root {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4 h-100">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="mb-3">
|
||||
<a href="{{ url_for('xml.xml_management') }}" class="text-decoration-none text-muted small fw-bold">
|
||||
<i class="bi bi-arrow-left me-1"></i>목록으로 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="diff-page-container">
|
||||
<!-- Toolbar -->
|
||||
<div class="diff-toolbar">
|
||||
<div class="diff-files">
|
||||
<div class="file-badge text-danger border-danger-subtle bg-danger-subtle">
|
||||
<i class="bi bi-file-earmark-minus"></i>
|
||||
<span>{{ file1 }}</span> <!-- Original -->
|
||||
</div>
|
||||
<i class="bi bi-arrow-right diff-arrow"></i>
|
||||
<div class="file-badge text-success border-success-subtle bg-success-subtle">
|
||||
<i class="bi bi-file-earmark-plus"></i>
|
||||
<span>{{ file2 }}</span> <!-- Modified -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-secondary" onclick="toggleInlineDiff()" id="viewToggleBtn">
|
||||
<i class="bi bi-layout-split me-1"></i>Inline View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monaco Diff Editor -->
|
||||
<div id="monaco-diff-root"></div>
|
||||
</div>
|
||||
|
||||
<!-- Raw Content Hidden Inputs (Jinja2 will escape HTML entities automatically) -->
|
||||
<textarea id="hidden_content1" style="display:none;">{{ content1 }}</textarea>
|
||||
<textarea id="hidden_content2" style="display:none;">{{ content2 }}</textarea>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='vendor/monaco-editor/min/vs/loader.js') }}"></script>
|
||||
<script>
|
||||
let diffEditor = null;
|
||||
let isInline = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 1. Check for loader failure
|
||||
if (typeof require === 'undefined') {
|
||||
document.getElementById('monaco-diff-root').innerHTML =
|
||||
'<div class="d-flex justify-content-center align-items-center h-100 text-danger">' +
|
||||
'<i class="bi bi-exclamation-triangle me-2"></i>Monaco Editor 리소스를 불러올 수 없습니다. 인터넷 연결을 확인하세요.' +
|
||||
'</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
require.config({ paths: { 'vs': "{{ url_for('static', filename='vendor/monaco-editor/min/vs') }}" } });
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
// 2. Read content from hidden textareas
|
||||
// Jinja2가 HTML escaping을 처리하므로, .value를 통해 원본 XML을 얻을 수 있습니다.
|
||||
const content1 = document.getElementById('hidden_content1').value;
|
||||
const content2 = document.getElementById('hidden_content2').value;
|
||||
|
||||
const originalModel = monaco.editor.createModel(content1, 'xml');
|
||||
const modifiedModel = monaco.editor.createModel(content2, 'xml');
|
||||
|
||||
const container = document.getElementById('monaco-diff-root');
|
||||
|
||||
// 3. Create Diff Editor
|
||||
diffEditor = monaco.editor.createDiffEditor(container, {
|
||||
theme: 'vs',
|
||||
originalEditable: false,
|
||||
readOnly: true,
|
||||
renderSideBySide: true, // Default: Split View
|
||||
automaticLayout: true,
|
||||
minimap: { enabled: true },
|
||||
diffWordWrap: 'off'
|
||||
});
|
||||
|
||||
diffEditor.setModel({
|
||||
original: originalModel,
|
||||
modified: modifiedModel
|
||||
});
|
||||
|
||||
// 네비게이션 기능 추가
|
||||
diffEditor.getNavigator();
|
||||
|
||||
}, function (err) {
|
||||
document.getElementById('monaco-diff-root').innerHTML =
|
||||
'<div class="d-flex justify-content-center align-items-center h-100 text-danger">' +
|
||||
'<i class="bi bi-exclamation-triangle me-2"></i>에디터 로드 실패: ' + err.message + '</div>';
|
||||
});
|
||||
});
|
||||
|
||||
function toggleInlineDiff() {
|
||||
if (!diffEditor) return;
|
||||
|
||||
isInline = !isInline;
|
||||
diffEditor.updateOptions({
|
||||
renderSideBySide: !isInline
|
||||
});
|
||||
|
||||
const btn = document.getElementById('viewToggleBtn');
|
||||
if (isInline) {
|
||||
btn.innerHTML = '<i class="bi bi-layout-sidebar me-1"></i>Split View';
|
||||
} else {
|
||||
btn.innerHTML = '<i class="bi bi-layout-split me-1"></i>Inline View';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,691 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}스크립트 관리{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-code-square"></i> 스크립트 관리</h2>
|
||||
<button class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#yamlGuideModal">
|
||||
<i class="bi bi-question-circle"></i> YAML 작성 가이드
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 탭 네비게이션 -->
|
||||
<ul class="nav nav-tabs mb-3" id="scriptTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="mac-tab" data-bs-toggle="tab" data-bs-target="#mac" type="button">
|
||||
<i class="bi bi-ethernet"></i> MAC 수집
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="server-tab" data-bs-toggle="tab" data-bs-target="#server" type="button">
|
||||
<i class="bi bi-server"></i> 서버 정보
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="gpu-tab" data-bs-toggle="tab" data-bs-target="#gpu" type="button">
|
||||
<i class="bi bi-gpu-card"></i> GPU 서버 정보
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="terminal-tab" data-bs-toggle="tab" data-bs-target="#terminal" type="button">
|
||||
<i class="bi bi-terminal"></i> 터미널
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 탭 컨텐츠 -->
|
||||
<div class="tab-content" id="scriptTabContent">
|
||||
<!-- MAC 수집 탭 -->
|
||||
<div class="tab-pane fade show active" id="mac" role="tabpanel">
|
||||
<div class="row g-3" id="macProfiles">
|
||||
<!-- 프로파일 카드가 여기에 동적으로 추가됨 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 서버 정보 탭 -->
|
||||
<div class="tab-pane fade" id="server" role="tabpanel">
|
||||
<div class="row g-3" id="serverProfiles">
|
||||
<!-- 프로파일 카드가 여기에 동적으로 추가됨 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GPU 서버 정보 탭 -->
|
||||
<div class="tab-pane fade" id="gpu" role="tabpanel">
|
||||
<div class="row g-3" id="gpuProfiles">
|
||||
<!-- 프로파일 카드가 여기에 동적으로 추가됨 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 터미널 탭 (Racadm Tester) -->
|
||||
<div class="tab-pane fade" id="terminal" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light py-3">
|
||||
<h5 class="mb-0"><i class="bi bi-terminal-fill me-2"></i>Racadm Command Tester</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<form id="racadmForm" onsubmit="event.preventDefault(); runRacadmCommand();">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">iDRAC IP</label>
|
||||
<input type="text" class="form-control" id="targetIp" placeholder="192.168.0.1"
|
||||
required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">Username</label>
|
||||
<input type="text" class="form-control" id="targetUser" value="root">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-bold">Password</label>
|
||||
<input type="password" class="form-control" id="targetPass" value="calvin">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-dark w-100" id="btnRunCommand">
|
||||
<i class="bi bi-play-fill me-1"></i> Run Command
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-bold">Command</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text font-monospace bg-light">racadm</span>
|
||||
<input type="text" class="form-control font-monospace" id="targetCmd"
|
||||
placeholder="getsysinfo" value="getsysinfo" required>
|
||||
</div>
|
||||
<div class="form-text">Note: 'racadm' is automatically prepended. Just enter the
|
||||
arguments (e.g. 'getsysinfo' or 'get NIC.NICConfig.1').</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label fw-bold mb-0">Output</label>
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
onclick="document.getElementById('cmdOutput').innerText=''">
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-dark text-light p-3 rounded font-monospace"
|
||||
style="min-height: 200px; overflow: auto; resize: vertical;">
|
||||
<pre id="cmdOutput" class="mb-0 text-break" style="white-space: pre-wrap;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- YAML 가이드 모달 -->
|
||||
<div class="modal fade" id="yamlGuideModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-book"></i> YAML 프로파일 작성 가이드</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- 탭 메뉴 -->
|
||||
<ul class="nav nav-tabs mb-3" id="yamlGuideTabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#guide-basic"><i
|
||||
class="bi bi-lightbulb"></i> YAML 기초</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#guide-mac">MAC 수집</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#guide-server">서버 정보</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#guide-gpu">GPU/GUID</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 탭 컨텐츠 -->
|
||||
<div class="tab-content border p-3 rounded bg-light" style="max-height: 60vh; overflow-y: auto;">
|
||||
|
||||
<!-- 1. YAML 기초 -->
|
||||
<div class="tab-pane fade show active" id="guide-basic">
|
||||
<h5 class="mb-3 text-primary">YAML 파일 작성 규칙 (초보자 필독)</h5>
|
||||
<div class="alert alert-warning">
|
||||
<strong><i class="bi bi-exclamation-triangle-fill"></i> 가장 중요한 규칙 2가지</strong>
|
||||
<ol class="mb-0 mt-2">
|
||||
<li><strong>들여쓰기(띄어쓰기)</strong>: 절대 <code>Tab</code> 키를 쓰지 마세요! <span
|
||||
class="badge bg-danger">스페이스바 2칸</span>을 사용해야 합니다.</li>
|
||||
<li><strong>콜론(:) 다음 공백</strong>: <code>key:value</code> (X) -> <code>key: value</code>
|
||||
(O) <br>콜론 뒤에는 반드시 한 칸을 띄워야 합니다.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header fw-bold">구조 이해하기</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">설정은 크게 <strong>이름(Key)</strong>과 <strong>값(Value)</strong>으로 나뉩니다.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6><i class="bi bi-list-ul"></i> 목록 (List) 작성법</h6>
|
||||
<p class="small text-muted">여러 개의 값을 넣으려면 대쉬(<code>-</code>)를 사용합니다.</p>
|
||||
<pre class="bg-dark text-light p-2 rounded small"><code># 과일 목록 예시
|
||||
fruits:
|
||||
- "사과" # 리스트 1
|
||||
- "바나나" # 리스트 2
|
||||
- "포도" # 리스트 3</code></pre>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6><i class="bi bi-card-text"></i> 설정 (Object) 작성법</h6>
|
||||
<p class="small text-muted">하위 설정을 넣으려면 들여쓰기를 합니다.</p>
|
||||
<pre class="bg-dark text-light p-2 rounded small"><code># 자동차 설정 예시
|
||||
car:
|
||||
color: "Red" # 스페이스 2칸
|
||||
speed: 100 # 스페이스 2칸
|
||||
is_active: true # 스페이스 2칸</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. MAC 수집 -->
|
||||
<div class="tab-pane fade" id="guide-mac">
|
||||
<h5 class="mb-3">MAC 주소 수집 프로파일</h5>
|
||||
<p class="text-muted">서버의 네트워크 카드(NIC)의 물리적 주소(MAC)를 가져오는 설정입니다.</p>
|
||||
|
||||
<div class="card mb-3 border-primary">
|
||||
<div class="card-header bg-primary text-white">복사해서 사용하세요</div>
|
||||
<div class="card-body bg-dark text-light p-0">
|
||||
<pre class="m-0 p-3"><code>description: "기본 MAC 수집 설정"
|
||||
|
||||
# 1. 수집할 NIC(네트워크 카드)의 이름 패턴
|
||||
nic_patterns:
|
||||
- "NIC.Integrated.1" # 메인보드 내장 랜카드 1번
|
||||
- "NIC.Slot.*" # 슬롯에 꽂힌 모든 랜카드 (PCIe)
|
||||
|
||||
# 2. InfiniBand(고속 네트워크) 카드가 있다면 슬롯 번호를 적으세요
|
||||
infiniband_slots:
|
||||
- "PCIeSlot3" # 3번 슬롯에 꽂힌 카드
|
||||
|
||||
# 3. iDRAC 전용 포트의 MAC도 필요하면 true로 설정
|
||||
include_idrac_mac: true</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4"><i class="bi bi-question-circle-fill"></i> 상세 설명</h6>
|
||||
<table class="table table-sm table-bordered bg-white">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>항목</th>
|
||||
<th>설명</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>nic_patterns</code></td>
|
||||
<td>가져올 랜카드의 이름입니다. <code>*</code>(별표)는 '모두'를 의미합니다.<br>예: <code>NIC.*</code>는 모든
|
||||
랜카드를 다 가져옵니다.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>infiniband_slots</code></td>
|
||||
<td>InfiniBand는 일반 NIC와 달라서 슬롯 번호(예: PCIeSlot3)를 정확히 적어야 합니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 3. 서버 정보 -->
|
||||
<div class="tab-pane fade" id="guide-server">
|
||||
<h5 class="mb-3">서버 정보 수집 프로파일</h5>
|
||||
<p class="text-muted">서버의 펌웨어, BIOS 설정, 하드웨어 상태 등을 확인하는 설정입니다.</p>
|
||||
|
||||
<div class="card mb-3 border-success">
|
||||
<div class="card-header bg-success text-white">복사해서 사용하세요</div>
|
||||
<div class="card-body bg-dark text-light p-0">
|
||||
<pre class="m-0 p-3"><code>description: "서버 점검용 설정"
|
||||
|
||||
# 1. 버전 정보를 알고 싶은 펌웨어 이름들
|
||||
firmware:
|
||||
- "iDRAC"
|
||||
- "BIOS"
|
||||
- "NIC"
|
||||
|
||||
# 2. 확인하고 싶은 BIOS 설정값
|
||||
bios_settings:
|
||||
- "MemTest" # 메모리 테스트 옵션
|
||||
- "LogicalProc" # 하이퍼스레딩(논리코어)
|
||||
|
||||
# 3. RAID 컨트롤러의 디스크 정보
|
||||
raid_settings:
|
||||
- "Disk.Bay.0" # 0번 베이의 하드디스크 정보
|
||||
|
||||
# 4. 기타 명령어 실행 (상급자용)
|
||||
custom_items:
|
||||
- command: "getsysinfo"
|
||||
label: "전체 시스템 요약"</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. GPU/GUID -->
|
||||
<div class="tab-pane fade" id="guide-gpu">
|
||||
<h5 class="mb-3">GPU 및 GUID 설정</h5>
|
||||
<p class="text-muted">GPU의 시리얼 번호를 수집하거나, iDRAC MAC 주소를 기반으로 GUID를 생성할 때 사용합니다.</p>
|
||||
|
||||
<div class="card mb-3 border-danger">
|
||||
<div class="card-header bg-danger text-white">복사해서 사용하세요</div>
|
||||
<div class="card-body bg-dark text-light p-0">
|
||||
<pre class="m-0 p-3"><code>description: "GPU 서버용 설정"
|
||||
|
||||
# 1. GPU 시리얼 수집 설정 (GPU가 있는 경우 필수)
|
||||
gpu_settings:
|
||||
target_slots:
|
||||
- "Video.Slot.*" # 모든 비디오 카드 수집
|
||||
sort_order: "slot_index" # 슬롯 순서대로 정렬
|
||||
|
||||
# 2. GUID 생성 시 슬롯 우선순위
|
||||
# iDRAC MAC 말고, 특정 랜카드의 MAC을 GUID로 쓰고 싶을 때 사용
|
||||
slot_priority:
|
||||
- 38 # 38번 슬롯 카드의 MAC을 1순위로 사용
|
||||
- 1 # 없으면 1번 슬롯 사용
|
||||
|
||||
output_format: "combined"</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-light border">
|
||||
<strong><i class="bi bi-lightbulb"></i> 팁:</strong> GPU 시리얼만 필요하다면
|
||||
<code>slot_priority</code> 부분은 지워도 됩니다.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 프로파일 편집 모달 -->
|
||||
<div class="modal fade" id="profileModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="profileModalTitle">프로파일 편집</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">프로파일 이름</label>
|
||||
<input type="text" class="form-control" id="profileName" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설명</label>
|
||||
<input type="text" class="form-control" id="profileDescription">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">설정 (YAML)</label>
|
||||
<textarea class="form-control font-monospace" id="profileConfig" rows="15"></textarea>
|
||||
<small class="text-muted">YAML 형식으로 프로파일 설정을 편집할 수 있습니다.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">취소</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveProfile()">
|
||||
<i class="bi bi-save"></i> 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.profile-card {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.profile-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.profile-card .card-body {
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.border-dashed {
|
||||
border-style: dashed !important;
|
||||
}
|
||||
|
||||
/* 탭 가시성 강제 설정 */
|
||||
.nav-tabs .nav-link {
|
||||
color: #495057 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #fff !important;
|
||||
background-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: #0a58ca !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentCategory = 'mac';
|
||||
let currentProfile = null;
|
||||
let allProfiles = [];
|
||||
|
||||
// 페이지 로드 시 프로파일 목록 가져오기
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadProfiles();
|
||||
});
|
||||
|
||||
// 프로파일 목록 로드
|
||||
async function loadProfiles() {
|
||||
// 로딩 표시
|
||||
['macProfiles', 'serverProfiles', 'gpuProfiles'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerHTML = '<div class="col-12 text-center py-5"><div class="spinner-border text-primary" role="status"></div><p class="mt-2 text-muted">프로파일을 불러오는 중입니다...</p></div>';
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/profiles');
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
allProfiles = await response.json();
|
||||
|
||||
if (!Array.isArray(allProfiles)) throw new Error('데이터 형식이 올바르지 않습니다.');
|
||||
|
||||
renderProfiles();
|
||||
} catch (error) {
|
||||
console.error('프로파일 로드 오류:', error);
|
||||
// 에러 표시
|
||||
['macProfiles', 'serverProfiles', 'gpuProfiles'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerHTML = `<div class="col-12 text-center py-5"><i class="bi bi-exclamation-triangle text-danger fs-1"></i><p class="mt-2 text-danger">프로파일을 불러오지 못했습니다.<br><small>${error.message}</small></p></div>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 프로파일 렌더링
|
||||
function renderProfiles() {
|
||||
const gpuContainer = document.getElementById('gpuProfiles');
|
||||
const categories = {
|
||||
'mac': document.getElementById('macProfiles'),
|
||||
'server_info': document.getElementById('serverProfiles'),
|
||||
'guid': gpuContainer,
|
||||
'gpu': gpuContainer
|
||||
};
|
||||
|
||||
// 각 카테고리 초기화
|
||||
Object.values(categories).forEach(el => { if (el) el.innerHTML = ''; });
|
||||
|
||||
// 프로파일 카드 생성
|
||||
allProfiles.forEach(profile => {
|
||||
const container = categories[profile.category];
|
||||
if (!container) return;
|
||||
|
||||
const card = createProfileCard(profile);
|
||||
container.insertAdjacentHTML('beforeend', card);
|
||||
});
|
||||
|
||||
// 새 프로파일 추가 카드
|
||||
const targets = [
|
||||
{ cat: 'mac', el: document.getElementById('macProfiles') },
|
||||
{ cat: 'server_info', el: document.getElementById('serverProfiles') },
|
||||
{ cat: 'gpu', el: gpuContainer }
|
||||
];
|
||||
|
||||
targets.forEach(t => {
|
||||
if (t.el) t.el.insertAdjacentHTML('beforeend', createNewProfileCard(t.cat));
|
||||
});
|
||||
}
|
||||
|
||||
// 프로파일 카드 HTML 생성
|
||||
function createProfileCard(profile) {
|
||||
const badgeClass = profile.is_default ? 'bg-success' : 'bg-secondary';
|
||||
const badgeText = profile.is_default ? '기본' : '사용자 정의';
|
||||
|
||||
return `
|
||||
<div class="col-md-4">
|
||||
<div class="card profile-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<h5 class="card-title">
|
||||
${profile.is_default ? '<i class="bi bi-star-fill text-warning"></i>' : ''}
|
||||
${profile.name}
|
||||
</h5>
|
||||
<p class="card-text text-muted small">${profile.description}</p>
|
||||
</div>
|
||||
<span class="badge ${badgeClass}">${badgeText}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<small class="text-muted">수집 항목:</small>
|
||||
<div class="mt-1">
|
||||
<span class="badge bg-light text-dark">${profile.items}개</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary flex-fill" onclick="editProfile('${profile.category}', '${profile.name}')">
|
||||
<i class="bi bi-pencil"></i> 편집
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="exportProfile('${profile.category}', '${profile.name}')">
|
||||
<i class="bi bi-download"></i> 내보내기
|
||||
</button>
|
||||
${!profile.is_default ? `
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteProfile('${profile.category}', '${profile.name}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 새 프로파일 추가 카드
|
||||
function createNewProfileCard(category) {
|
||||
return `
|
||||
<div class="col-md-4">
|
||||
<div class="card profile-card border-dashed" style="border: 2px dashed #dee2e6;">
|
||||
<div class="card-body d-flex flex-column align-items-center justify-content-center" style="min-height: 180px;">
|
||||
<i class="bi bi-plus-circle fs-1 text-muted mb-3"></i>
|
||||
<button class="btn btn-outline-primary" onclick="createNewProfile('${category}')">
|
||||
새 프로파일 만들기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 프로파일 편집
|
||||
async function editProfile(category, name) {
|
||||
try {
|
||||
const response = await fetch(`/api/profiles/${category}/${name}`);
|
||||
const profile = await response.json();
|
||||
|
||||
currentCategory = category;
|
||||
currentProfile = name;
|
||||
|
||||
document.getElementById('profileName').value = name;
|
||||
document.getElementById('profileDescription').value = profile.description;
|
||||
document.getElementById('profileConfig').value = JSON.stringify(profile.config, null, 2);
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('profileModal'));
|
||||
modal.show();
|
||||
} catch (error) {
|
||||
console.error('프로파일 로드 오류:', error);
|
||||
alert('프로파일을 불러오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로파일 저장
|
||||
async function saveProfile() {
|
||||
try {
|
||||
const config = JSON.parse(document.getElementById('profileConfig').value);
|
||||
|
||||
const response = await fetch(`/api/profiles/${currentCategory}/${currentProfile}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
config: config
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('프로파일이 저장되었습니다.');
|
||||
bootstrap.Modal.getInstance(document.getElementById('profileModal')).hide();
|
||||
loadProfiles();
|
||||
} else {
|
||||
alert('저장 실패: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 오류:', error);
|
||||
alert('프로파일 저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로파일 내보내기
|
||||
function exportProfile(category, name) {
|
||||
window.location.href = `/api/profiles/${category}/export?name=${encodeURIComponent(name)}`;
|
||||
}
|
||||
|
||||
// 프로파일 삭제
|
||||
async function deleteProfile(category, name) {
|
||||
if (!confirm(`'${name}' 프로파일을 삭제하시겠습니까?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/profiles/${category}/${name}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
alert('프로파일이 삭제되었습니다.');
|
||||
loadProfiles();
|
||||
} else {
|
||||
alert('삭제 실패: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('삭제 오류:', error);
|
||||
alert('프로파일 삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
// 새 프로파일 만들기
|
||||
function createNewProfile(category) {
|
||||
const name = prompt('새 프로파일 이름을 입력하세요:');
|
||||
if (!name) return;
|
||||
|
||||
// 기본 설정으로 프로파일 생성
|
||||
const defaultConfig = {
|
||||
description: "새 프로파일",
|
||||
extends: "default"
|
||||
};
|
||||
|
||||
fetch(`/api/profiles/${category}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
config: defaultConfig
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert('프로파일이 생성되었습니다.');
|
||||
loadProfiles();
|
||||
} else {
|
||||
alert('생성 실패: ' + result.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('생성 오류:', error);
|
||||
alert('프로파일 생성 중 오류가 발생했습니다.');
|
||||
});
|
||||
}
|
||||
|
||||
async function runRacadmCommand() {
|
||||
const ip = document.getElementById('targetIp').value;
|
||||
const user = document.getElementById('targetUser').value;
|
||||
const pass = document.getElementById('targetPass').value;
|
||||
const cmd = document.getElementById('targetCmd').value;
|
||||
const outputPre = document.getElementById('cmdOutput');
|
||||
const btn = document.getElementById('btnRunCommand');
|
||||
|
||||
if (!ip || !user || !pass || !cmd) {
|
||||
alert('모든 필드를 입력하세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// UI Reset
|
||||
outputPre.innerText = "Executing...";
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Running...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/test_racadm', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ip: ip,
|
||||
user: user,
|
||||
password: pass,
|
||||
command: cmd
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Return code가 0이 아니면 실패로 간주하거나, 그대로 출력
|
||||
let displayText = `[Success]\n${result.output}`;
|
||||
if (result.return_code !== 0) {
|
||||
displayText = `[Error (Code: ${result.return_code})]\n${result.output}`;
|
||||
}
|
||||
outputPre.innerText = displayText;
|
||||
} else {
|
||||
outputPre.innerText = `[Error]\n${result.error}`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Execution error:', error);
|
||||
outputPre.innerText = `[System Error]\n${error.message}`;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-1"></i> Run Command';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,109 @@
|
||||
{% set repo_label = 'MAC' if folder == 'mac' else 'GUID' if folder == 'guid' else 'GPU' %}
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
|
||||
<div class="small text-muted">
|
||||
<i class="bi bi-folder2-open me-1"></i>{{ files|length }}개 파일
|
||||
</div>
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
{% if files and files|length > 0 %}
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="{{ repo_label }} 선택 파일 작업">
|
||||
<button type="button" class="btn btn-outline-primary repo-selected-action"
|
||||
data-repo="{{ folder }}"
|
||||
data-action="{{ url_for('main.download_repository_files', repo_type=folder) }}" disabled>
|
||||
<i class="bi bi-download me-1"></i>선택 다운로드
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger repo-selected-action"
|
||||
data-repo="{{ folder }}"
|
||||
data-action="{{ url_for('main.delete_repository_files', repo_type=folder) }}"
|
||||
data-confirm="{{ repo_label }} 저장소에서 선택한 파일을 삭제하시겠습니까?" disabled>
|
||||
<i class="bi bi-trash me-1"></i>선택 삭제
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form action="{{ url_for('main.archive_repository', repo_type=folder) }}" method="post" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm"
|
||||
onclick="return confirm('{{ repo_label }} 저장소의 모든 파일을 아카이브로 이동하시겠습니까?');">
|
||||
<i class="bi bi-archive me-1"></i>{{ repo_label }} 아카이브로 이동
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url_for('main.upload_workspace_files', target_type=folder) }}" method="post"
|
||||
enctype="multipart/form-data" class="d-inline-flex m-0">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="duplicate_policy" value="rename">
|
||||
<label class="btn btn-outline-success btn-sm mb-0" title="{{ repo_label }} 저장소에 파일 업로드">
|
||||
<i class="bi bi-upload me-1"></i>업로드
|
||||
<input type="file" name="files" class="d-none workspace-file-upload-input" multiple
|
||||
data-existing-files='{{ files|map(attribute="name")|list|tojson }}'>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% if files and files|length > 0 %}
|
||||
<div class="table-shell">
|
||||
<div class="table-responsive bg-white">
|
||||
<table class="table table-hover table-sm mb-0 align-middle font-monospace repo-file-table"
|
||||
id="{{ table_id }}" data-repo="{{ folder }}" style="font-size: 0.9rem;">
|
||||
<thead class="bg-light sticky-top">
|
||||
<tr>
|
||||
<th class="text-center" style="width: 44px;">
|
||||
<input type="checkbox" class="form-check-input repo-select-all"
|
||||
data-repo="{{ folder }}" data-table-id="{{ table_id }}" aria-label="{{ repo_label }} 전체 선택">
|
||||
</th>
|
||||
<th class="ps-3" style="cursor:pointer;" onclick="sortTable('{{ table_id }}', 1)">파일명 <i
|
||||
class="bi bi-arrow-down-up small text-muted"></i></th>
|
||||
<th style="cursor:pointer; width: 100px;" onclick="sortTable('{{ table_id }}', 2)">크기 <i
|
||||
class="bi bi-arrow-down-up small text-muted"></i></th>
|
||||
<th style="cursor:pointer; width: 160px;" onclick="sortTable('{{ table_id }}', 3)">날짜 <i
|
||||
class="bi bi-arrow-down-up small text-muted"></i></th>
|
||||
<th class="text-end pe-3" style="width: 96px;">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for file in files %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" class="form-check-input repo-file-checkbox"
|
||||
data-repo="{{ folder }}" value="{{ file.name }}" aria-label="{{ file.name }} 선택">
|
||||
</td>
|
||||
<td class="ps-3">
|
||||
<i class="bi bi-file-earmark text-secondary me-2"></i>{{ file.name }}
|
||||
</td>
|
||||
<td>{{ (file.size / 1024)|round(1) }} KB</td>
|
||||
<td data-sort="{{ file.mtime }}">{{ file.date }}</td>
|
||||
<td class="text-end pe-3">
|
||||
<div class="d-inline-flex align-items-center gap-2">
|
||||
<button type="button" class="btn btn-xs btn-link text-secondary p-0 btn-view-repo"
|
||||
data-bs-toggle="modal" data-bs-target="#fileViewModal" data-folder="{{ folder }}"
|
||||
data-filename="{{ file.name }}" title="내용 보기">
|
||||
<i class="bi bi-eye"></i>
|
||||
</button>
|
||||
<form action="{{ url_for('main.delete_repository_file', repo_type=folder) }}"
|
||||
method="post" class="d-inline m-0">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="filename" value="{{ file.name }}">
|
||||
<button type="submit" class="btn btn-xs btn-link text-danger p-0"
|
||||
title="삭제" onclick="return confirm('{{ file.name }} 파일을 삭제하시겠습니까?');">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state-card">
|
||||
<div class="empty-state-icon">
|
||||
<i class="bi bi-folder2-open"></i>
|
||||
</div>
|
||||
<h6 class="fw-bold mb-2">{{ empty_msg }}</h6>
|
||||
<p class="text-muted">Quick Move를 실행하거나 업로드 버튼으로 파일을 추가하면 이곳에서 바로 확인할 수 있습니다.</p>
|
||||
<a href="#{{ 'macMoveForm' if folder == 'mac' else 'guidMoveForm' if folder == 'guid' else 'gpuMoveForm' }}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-arrow-down-circle me-1"></i>Quick Move 실행하기
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Uploaded XML Files</h1>
|
||||
<ul>
|
||||
{% for file in files %}
|
||||
<li>
|
||||
{{ file }}
|
||||
<a href="{{ url_for('download_xml', filename=file) }}">Download</a>
|
||||
<a href="{{ url_for('edit_xml', filename=file) }}">Edit</a>
|
||||
<form action="{{ url_for('delete_xml', filename=file) }}" method="post" style="display:inline;">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button type="submit">Delete</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{{ url_for('upload_xml') }}">Upload new file</a>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user