Initial commit

This commit is contained in:
2025-10-05 17:37:51 +09:00
parent 5cbe9a2524
commit 3a7fabb830
219 changed files with 81295 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
{# backend/templates/admin.html #}
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="card">
<div class="card-body">
<h3 class="card-title">Admin Page</h3>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mt-2">
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<div class="table-responsive">
<table class="table table-striped align-middle">
<thead>
<tr>
<th style="width:60px">ID</th>
<th>Username</th>
<th>Email</th>
<th style="width:80px">Active</th>
<th style="width:260px">Action</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>
{% if user.is_active %}
<span class="badge bg-success">Yes</span>
{% else %}
<span class="badge bg-secondary">No</span>
{% endif %}
</td>
<td>
{% if not user.is_active %}
<a href="{{ url_for('admin.approve_user', user_id=user.id) }}" class="btn btn-success btn-sm me-1">Approve</a>
{% endif %}
<a href="{{ url_for('admin.delete_user', user_id=user.id) }}"
class="btn btn-danger btn-sm me-1"
onclick="return confirm('사용자 {{ user.username }} (ID={{ user.id }}) 를 삭제하시겠습니까?');">
Delete
</a>
<!-- Change Password 버튼: 모달 오픈 -->
<button type="button"
class="btn btn-primary btn-sm"
data-user-id="{{ user.id }}"
data-username="{{ user.username | e }}"
data-bs-toggle="modal"
data-bs-target="#changePasswordModal">
Change Password
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{# ========== Change Password Modal ========== #}
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form id="changePasswordForm" method="post" action="">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title" id="changePasswordModalLabel">Change Password</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<small class="text-muted">User:</small>
<div id="modalUserInfo" class="fw-bold"></div>
</div>
<div class="mb-3">
<label for="newPasswordInput" class="form-label">New password</label>
<input id="newPasswordInput" name="new_password" type="password" class="form-control" required minlength="8" placeholder="Enter new password">
<div class="form-text">최소 8자 이상을 권장합니다.</div>
</div>
<div class="mb-3">
<label for="confirmPasswordInput" class="form-label">Confirm password</label>
<input id="confirmPasswordInput" name="confirm_password" type="password" class="form-control" required minlength="8" placeholder="Confirm new password">
<div id="pwMismatch" class="invalid-feedback">비밀번호가 일치하지 않습니다.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="modalSubmitBtn" type="submit" class="btn btn-primary">Change Password</button>
</div>
</form>
</div>
</div>
</div>
{# ========== 스크립트: 모달에 사용자 정보 채우기 + 클라이언트 확인 ========== #}
{% block scripts %}
{{ super() }}
<script>
(function () {
// Bootstrap 5을 사용한다고 가정. data-bs-* 이벤트로 처리.
const changePasswordModal = document.getElementById('changePasswordModal');
const modalUserInfo = document.getElementById('modalUserInfo');
const changePasswordForm = document.getElementById('changePasswordForm');
const newPasswordInput = document.getElementById('newPasswordInput');
const confirmPasswordInput = document.getElementById('confirmPasswordInput');
const pwMismatch = document.getElementById('pwMismatch');
if (!changePasswordModal) return;
changePasswordModal.addEventListener('show.bs.modal', function (event) {
const button = event.relatedTarget; // 버튼 that triggered the modal
const userId = button.getAttribute('data-user-id');
const username = button.getAttribute('data-username') || ('ID ' + userId);
// 표시 텍스트 세팅
modalUserInfo.textContent = username + ' (ID: ' + userId + ')';
// 폼 action 동적 설정: admin.reset_password 라우트 기대
// 예: /admin/users/123/reset_password
changePasswordForm.action = "{{ url_for('admin.reset_password', user_id=0) }}".replace('/0/', '/' + userId + '/');
// 폼 내부 비밀번호 필드 초기화
newPasswordInput.value = '';
confirmPasswordInput.value = '';
confirmPasswordInput.classList.remove('is-invalid');
pwMismatch.style.display = 'none';
});
// 폼 제출 전 클라이언트에서 비밀번호 일치 검사
changePasswordForm.addEventListener('submit', function (e) {
const a = newPasswordInput.value || '';
const b = confirmPasswordInput.value || '';
if (a.length < 8) {
newPasswordInput.focus();
e.preventDefault();
return;
}
if (a !== b) {
e.preventDefault();
confirmPasswordInput.classList.add('is-invalid');
pwMismatch.style.display = 'block';
confirmPasswordInput.focus();
return;
}
// 제출 허용 (서버측에서도 반드시 검증)
});
})();
</script>
{% endblock %}
{% endblock %}

121
backend/templates/base.html Normal file
View File

@@ -0,0 +1,121 @@
<!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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
integrity="sha384-tViUnnbYAV00FLIhhi3v/dWt3Jxw4gZQcNoSCxCIFNJVCx7/D55/wXsrNIRANwdD"
crossorigin="anonymous">
<!-- 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>
<!-- 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>
{% 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">&copy; 2025 Dell Server Info. All rights reserved.</small>
</div>
</footer>
<!-- Bootstrap 5.3.3 JS Bundle (Popper.js 포함) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
<!-- Socket.IO (필요한 경우만) -->
{% if config.USE_SOCKETIO %}
<script src="https://cdn.socket.io/4.7.4/socket.io.min.js"
integrity="sha384-Gr6Lu2Ajx28mzwyVR8CFkULdCU7kMlZ9UthllibdOSo6qAiN+yXNHqtgdTvFXMT4"
crossorigin="anonymous"></script>
{% endif %}
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block content %}
<head>
<meta charset="UTF-8">
<title>Edit XML File</title>
<style>
::-webkit-scrollbar { width: 12px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background-color: #888; border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background-color: #555; }
html { scrollbar-width: thin; scrollbar-color: #888 #f1f1f1; }
textarea {
width: 100%;
height: 600px;
padding: 10px;
font-size: 14px;
line-height: 1.5;
background-color: #f9f9f9;
border: 1px solid #ccc;
transition: background-color 0.3s ease;
}
textarea:hover { background-color: #f0f0f0; }
.xml-list-item {
padding: 10px;
background-color: #ffffff;
transition: background-color 0.3s ease;
}
.xml-list-item:hover { background-color: #e9ecef; cursor: pointer; }
.btn { margin-top: 20px; }
</style>
</head>
<body>
<div class="card shadow-lg">
<div class="card-header bg-primary text-white">
<h3>Edit XML File: <strong>{{ filename }}</strong></h3>
</div>
<div class="card-body">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="form-group">
<label for="xmlContent">XML Content</label>
<textarea id="xmlContent" name="content" class="form-control" rows="20">{{ content }}</textarea>
</div>
<button type="submit" class="btn btn-success mt-3">Save Changes</button>
<a href="{{ url_for('xml.xml_management') }}" class="btn btn-secondary mt-3">Cancel</a>
</form>
</div>
</div>
</body>
{% endblock %}

View File

@@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
<h2>Dell Server & NAVER Settings Info</h2>
<p>사이트 가입 및 로그인후 이용 가능</p>
{% endblock %}

View File

@@ -0,0 +1,676 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid py-4">
{# 플래시 메시지 #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="position-fixed top-0 end-0 p-3" style="z-index: 11">
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show shadow-lg" role="alert">
<i class="bi bi-{{ 'check-circle' if cat == 'success' else 'exclamation-triangle' }} me-2"></i>
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{# 헤더 섹션 #}
<div class="row mb-4">
<div class="col">
<h2 class="fw-bold mb-1">
<i class="bi bi-server text-primary me-2"></i>
서버 관리 대시보드
</h2>
<p class="text-muted mb-0">IP 처리 및 파일 관리를 위한 통합 관리 도구</p>
</div>
</div>
{# 메인 작업 영역 #}
<div class="row g-4 mb-4">
{# IP 처리 카드 #}
<div class="col-lg-6">
<div class="card border shadow-sm h-100">
<div class="card-header bg-primary text-white border-0 py-2">
<h6 class="mb-0 fw-semibold">
<i class="bi bi-hdd-network me-2"></i>
IP 처리
</h6>
</div>
<div class="card-body p-4">
<form id="ipForm" method="post" action="{{ url_for('main.process_ips') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{# 스크립트 선택 #}
<div class="mb-3">
<label for="script" class="form-label">스크립트 선택</label>
<select id="script" name="script" class="form-select" required>
<option value="">스크립트를 선택하세요</option>
{% for script in scripts %}
<option value="{{ script }}">{{ script }}</option>
{% endfor %}
</select>
</div>
{# XML 파일 선택 (조건부) #}
<div class="mb-3" id="xmlFileGroup" style="display:none;">
<label for="xmlFile" class="form-label">XML 파일 선택</label>
<select id="xmlFile" name="xmlFile" class="form-select">
<option value="">XML 파일 선택</option>
{% for xml_file in xml_files %}
<option value="{{ xml_file }}">{{ xml_file }}</option>
{% endfor %}
</select>
</div>
{# IP 주소 입력 #}
<div class="mb-3">
<label for="ips" class="form-label">
IP 주소 (각 줄에 하나)
<span class="badge bg-secondary ms-2" id="ipLineCount">0줄</span>
</label>
<textarea id="ips" name="ips" rows="4" class="form-control font-monospace"
placeholder="예:&#10;192.168.1.1&#10;192.168.1.2&#10;192.168.1.3" required></textarea>
</div>
<button type="submit" class="btn btn-primary w-100">
처리
</button>
</form>
</div>
</div>
</div>
{# 공유 작업 카드 #}
<div class="col-lg-6">
<div class="card border shadow-sm h-100">
<div class="card-header bg-success text-white border-0 py-2">
<h6 class="mb-0 fw-semibold">
<i class="bi bi-share me-2"></i>
공유 작업
</h6>
</div>
<div class="card-body p-4">
<form id="sharedForm" method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="server_list_content" class="form-label">
서버 리스트 (덮어쓰기)
<span class="badge bg-secondary ms-2" id="serverLineCount">0줄</span>
</label>
<textarea id="server_list_content" name="server_list_content" rows="8"
class="form-control font-monospace" style="font-size: 0.95rem;"
placeholder="서버 리스트를 입력하세요..."></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" formaction="{{ url_for('utils.update_server_list') }}"
class="btn btn-outline-primary">
MAC to Excel
</button>
<button type="submit" formaction="{{ url_for('utils.update_guid_list') }}"
class="btn btn-success">
GUID to Excel
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{# 진행바 #}
<div class="row mb-4">
<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="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success"
role="progressbar" style="width:0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
<span class="fw-semibold">0%</span>
</div>
</div>
</div>
</div>
</div>
</div>
{# 파일 관리 도구 #}
<div class="row mb-4">
<div class="col">
<div class="card border shadow-sm">
<div class="card-header bg-light border-0 py-2">
<h6 class="mb-0">
<i class="bi bi-tools me-2"></i>
파일 관리 도구
</h6>
</div>
<div class="card-body p-4">
<div class="row g-3">
{# ZIP 다운로드 #}
<div class="col-md-6 col-xl-3">
<label class="form-label">ZIP 다운로드</label>
<form method="post" action="{{ url_for('main.download_zip') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="input-group">
<input type="text" class="form-control" name="zip_filename"
placeholder="파일명" required>
<button class="btn btn-primary" type="submit">
다운로드
</button>
</div>
</form>
</div>
{# 파일 백업 #}
<div class="col-md-6 col-xl-3">
<label class="form-label">파일 백업</label>
<form method="post" action="{{ url_for('main.backup_files') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="input-group">
<input type="text" class="form-control" name="backup_prefix"
placeholder="PO로 시작">
<button class="btn btn-success" type="submit">
백업
</button>
</div>
</form>
</div>
{# MAC 파일 이동 #}
<div class="col-md-6 col-xl-3">
<label class="form-label">MAC 파일 이동</label>
<form id="macMoveForm" method="post" action="{{ url_for('utils.move_mac_files') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-warning w-100" type="submit">
MAC Move
</button>
</form>
</div>
{# GUID 파일 이동 #}
<div class="col-md-6 col-xl-3">
<label class="form-label">GUID 파일 이동</label>
<form id="guidMoveForm" method="post" action="{{ url_for('utils.move_guid_files') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn btn-info w-100" type="submit">
GUID Move
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{# 처리된 파일 목록 - 목록별 버튼 스타일 분리 (processed-list) #}
<div class="row mb-4 processed-list">
<div class="col">
<div class="card border shadow-sm">
<div class="card-header bg-light border-0 py-2">
<h6 class="mb-0">
<i class="bi bi-files me-2"></i>
처리된 파일 목록
</h6>
</div>
<div class="card-body p-4">
{% if files_to_display and files_to_display|length > 0 %}
<div class="row g-3">
{% for file_info in files_to_display %}
<div class="col-auto">
<div class="file-card-compact border rounded p-2 text-center">
<a href="{{ url_for('main.download_file', filename=file_info.file) }}"
class="text-decoration-none text-dark fw-semibold d-block mb-2 text-nowrap px-2"
download title="{{ file_info.name or file_info.file }}">
{{ file_info.name or file_info.file }}
</a>
<div class="file-card-buttons">
<button type="button" class="btn btn-sm btn-outline btn-view-processed flex-fill"
data-bs-toggle="modal" data-bs-target="#fileViewModal"
data-folder="idrac_info"
data-filename="{{ file_info.file }}">
보기
</button>
<form action="{{ url_for('main.delete_file', filename=file_info.file) }}"
method="post" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline btn-delete-processed"
onclick="return confirm('삭제하시겠습니까?');">
삭제
</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">표시할 파일이 없습니다.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{# 백업된 파일 목록 - 목록별 버튼 스타일 분리 (backup-list) #}
<div class="row backup-list">
<div class="col">
<div class="card border shadow-sm">
<div class="card-header bg-light border-0 py-2">
<h6 class="mb-0">
<i class="bi bi-archive me-2"></i>
백업된 파일 목록
</h6>
</div>
<div class="card-body p-4">
{% if backup_files and backup_files|length > 0 %}
<div class="list-group">
{% for date, info in backup_files.items() %}
<div class="list-group-item border rounded mb-2 p-0 overflow-hidden">
<div class="d-flex justify-content-between align-items-center p-3 bg-light">
<div class="d-flex align-items-center">
<i class="bi bi-calendar3 text-primary me-2"></i>
<strong>{{ date }}</strong>
<span class="badge bg-primary ms-3">{{ info.count }} 파일</span>
</div>
<button class="btn btn-sm btn-outline-secondary" type="button"
data-bs-toggle="collapse" data-bs-target="#collapse-{{ loop.index }}"
aria-expanded="false">
<i class="bi bi-chevron-down"></i>
</button>
</div>
<div id="collapse-{{ loop.index }}" class="collapse">
<div class="p-3">
<div class="row g-3">
{% for file in info.files %}
<div class="col-auto">
<div class="file-card-compact border rounded p-2 text-center">
<a href="{{ url_for('main.download_backup_file', date=date, filename=file) }}
" class="text-decoration-none text-dark fw-semibold d-block mb-2 text-nowrap px-2"
download title="{{ file }}">
{{ file.rsplit('.', 1)[0] }}
</a>
<div class="file-card-single-button">
<button type="button" class="btn btn-sm btn-outline btn-view-backup w-100"
data-bs-toggle="modal" data-bs-target="#fileViewModal"
data-folder="backup"
data-date="{{ date }}"
data-filename="{{ file }}">
보기
</button>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5">
<i class="bi bi-inbox fs-1 text-muted mb-3"></i>
<p class="text-muted mb-0">백업된 파일이 없습니다.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{# 파일 보기 모달 #}
<div class="modal fade" id="fileViewModal" tabindex="-1" aria-labelledby="fileViewModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="fileViewModalLabel">
<i class="bi bi-file-text me-2"></i>
파일 보기
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="닫기"></button>
</div>
<div class="modal-body bg-light">
<pre id="fileViewContent" class="mb-0 p-3 bg-white border rounded font-monospace"
style="white-space:pre-wrap;word-break:break-word;max-height:70vh;">불러오는 중...</pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-1"></i>닫기
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<style>
/* ===== 공통 파일 카드 컴팩트 스타일 ===== */
.file-card-compact {
transition: all 0.2s ease;
background: #fff;
min-width: 120px;
max-width: 200px;
}
.file-card-compact:hover {
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.file-card-compact a {
font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
}
/* ===== 목록별 버튼 분리 규칙 ===== */
/* 처리된 파일 목록 전용 컨테이너(보기/삭제 2열) */
.processed-list .file-card-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .5rem;
}
/* 보기(처리된) */
.processed-list .btn-view-processed {
border-color: #3b82f6;
color: #1d4ed8;
padding: .425rem .6rem;
font-size: .8125rem;
font-weight: 600;
}
.processed-list .btn-view-processed:hover {
background: rgba(59,130,246,.08);
}
/* 삭제(처리된) — 더 작게 */
.processed-list .btn-delete-processed {
border-color: #ef4444;
color: #b91c1c;
padding: .3rem .5rem;
font-size: .75rem;
font-weight: 600;
}
.processed-list .btn-delete-processed:hover {
background: rgba(239,68,68,.08);
}
/* 백업 파일 목록 전용 컨테이너(단일 버튼) */
.backup-list .file-card-single-button {
display: flex;
margin-top: .25rem;
}
/* 보기(백업) — 강조 색상 */
.backup-list .btn-view-backup {
width: 100%;
border-color: #10b981;
color: #047857;
padding: .45rem .75rem;
font-size: .8125rem;
font-weight: 700;
}
.backup-list .btn-view-backup:hover {
background: rgba(16,185,129,.08);
}
/* ===== 백업 파일 날짜 헤더 ===== */
.list-group-item .bg-light {
transition: background-color 0.2s ease;
}
.list-group-item:hover .bg-light {
background-color: #e9ecef !important;
}
/* ===== 진행바 애니메이션 ===== */
.progress {
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
transition: width 0.6s ease;
}
/* ===== 반응형 텍스트 ===== */
@media (max-width: 768px) {
.card-body {
padding: 1.5rem !important;
}
}
/* ===== 스크롤바 스타일링(모달) ===== */
.modal-body pre::-webkit-scrollbar { width: 8px; height: 8px; }
.modal-body pre::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
.modal-body pre::-webkit-scrollbar-thumb { background: #888; border-radius: 4px; }
.modal-body pre::-webkit-scrollbar-thumb:hover { background: #555; }
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
// 스크립트 선택 시 XML 드롭다운 토글
const TARGET_SCRIPT = "02-set_config.py";
const scriptSelect = document.getElementById('script');
const xmlGroup = document.getElementById('xmlFileGroup');
function toggleXml() {
if (!scriptSelect || !xmlGroup) return;
if (scriptSelect.value === TARGET_SCRIPT) {
xmlGroup.style.display = 'block';
xmlGroup.classList.add('fade-in');
} else {
xmlGroup.style.display = 'none';
}
}
if (scriptSelect) {
toggleXml();
scriptSelect.addEventListener('change', toggleXml);
}
// 파일 보기 모달
const modalEl = document.getElementById('fileViewModal');
const titleEl = document.getElementById('fileViewModalLabel');
const contentEl = document.getElementById('fileViewContent');
if (modalEl) {
modalEl.addEventListener('show.bs.modal', async (ev) => {
const btn = ev.relatedTarget;
const folder = btn?.getAttribute('data-folder') || '';
const date = btn?.getAttribute('data-date') || '';
const filename = btn?.getAttribute('data-filename') || '';
titleEl.innerHTML = `<i class="bi bi-file-text me-2"></i>${filename || '파일'}`;
contentEl.textContent = '불러오는 중...';
const params = new URLSearchParams();
if (folder) params.set('folder', folder);
if (date) params.set('date', date);
if (filename) params.set('filename', filename);
try {
const res = await fetch(`/view_file?${params.toString()}`, { cache: 'no-store' });
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
contentEl.textContent = data?.content ?? '(빈 파일)';
} catch (e) {
contentEl.textContent = '파일을 불러오지 못했습니다: ' + (e?.message || e);
}
});
}
// 진행바 업데이트
window.updateProgress = function (val) {
const bar = document.getElementById('progressBar');
if (!bar) return;
const v = Math.max(0, Math.min(100, Number(val) || 0));
bar.style.width = v + '%';
bar.setAttribute('aria-valuenow', v);
bar.innerHTML = `<span class="fw-semibold">${v}%</span>`;
};
// CSRF 토큰
const csrfToken = document.querySelector('input[name="csrf_token"]')?.value || '';
// 공통 POST 함수
async function postFormAndHandle(url) {
const res = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'X-CSRFToken': csrfToken,
'Accept': 'application/json, text/html;q=0.9,*/*;q=0.8',
},
});
const ct = (res.headers.get('content-type') || '').toLowerCase();
if (ct.includes('application/json')) {
const data = await res.json();
if (data.success === false) {
throw new Error(data.error || ('HTTP ' + res.status));
}
return data;
}
return { success: true, html: true };
}
// MAC 파일 이동
const macForm = document.getElementById('macMoveForm');
if (macForm) {
macForm.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = macForm.querySelector('button');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>처리 중...';
try {
await postFormAndHandle(macForm.action);
location.reload();
} catch (err) {
alert('MAC 이동 중 오류: ' + (err?.message || err));
btn.disabled = false;
btn.innerHTML = originalHtml;
}
});
}
// GUID 파일 이동
const guidForm = document.getElementById('guidMoveForm');
if (guidForm) {
guidForm.addEventListener('submit', async (e) => {
e.preventDefault();
const btn = guidForm.querySelector('button');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>처리 중...';
try {
await postFormAndHandle(guidForm.action);
location.reload();
} catch (err) {
alert('GUID 이동 중 오류: ' + (err?.message || err));
btn.disabled = false;
btn.innerHTML = originalHtml;
}
});
}
// IP 폼 제출
const ipForm = document.getElementById("ipForm");
if (ipForm) {
ipForm.addEventListener("submit", async (ev) => {
ev.preventDefault();
const formData = new FormData(ipForm);
const btn = ipForm.querySelector('button[type="submit"]');
const originalHtml = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>처리 중...';
try {
const res = await fetch(ipForm.action, {
method: "POST",
body: formData
});
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
console.log("[DEBUG] process_ips 응답:", data);
if (data.job_id) {
pollProgress(data.job_id);
} else {
// job_id가 없으면 완료로 간주
window.updateProgress(100);
setTimeout(() => location.reload(), 1000);
}
} catch (err) {
console.error("처리 중 오류:", err);
alert("처리 중 오류 발생: " + err.message);
btn.disabled = false;
btn.innerHTML = originalHtml;
}
});
}
// 진행률 폴링 함수
function pollProgress(jobId) {
const interval = setInterval(async () => {
try {
const res = await fetch(`/progress_status/${jobId}`);
if (!res.ok) {
clearInterval(interval);
return;
}
const data = await res.json();
if (data.progress !== undefined) {
window.updateProgress(data.progress);
}
// 완료 시 (100%)
if (data.progress >= 100) {
clearInterval(interval);
window.updateProgress(100);
// 페이지 새로고침
setTimeout(() => location.reload(), 1500);
}
} catch (err) {
console.error('진행률 확인 중 오류:', err);
clearInterval(interval);
}
}, 500); // 0.5초마다 확인
}
// 알림 자동 닫기
setTimeout(() => {
document.querySelectorAll('.alert').forEach(alert => {
const bsAlert = new bootstrap.Alert(alert);
bsAlert.close();
});
}, 5000);
});
</script>
<!-- 외부 script.js 파일만 로드 -->
<script src="{{ url_for('static', filename='script.js') }}"></script>
{% endblock %}

View File

@@ -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 %}

View File

@@ -0,0 +1,305 @@
{% extends "base.html" %}
{% block content %}
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>XML 파일 관리</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
body {
background-color: #f5f5f5;
padding: 20px 0;
}
.main-title {
font-size: 1.8rem;
font-weight: 600;
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 0.95rem;
margin-bottom: 30px;
}
.card {
border: none;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
background: white;
}
.card-header-custom {
background-color: #007bff;
color: white;
padding: 12px 20px;
font-weight: 600;
border-radius: 8px 8px 0 0;
font-size: 1rem;
}
.card-body {
padding: 20px;
}
.form-label {
font-weight: 500;
color: #555;
margin-bottom: 8px;
font-size: 0.9rem;
}
.form-control {
border: 1px solid #ddd;
border-radius: 4px;
padding: 8px 12px;
font-size: 0.9rem;
}
.btn-primary {
background-color: #007bff;
border: none;
padding: 8px 24px;
font-weight: 500;
border-radius: 4px;
font-size: 0.9rem;
}
.btn-success {
background-color: #28a745;
border: none;
padding: 6px 16px;
font-weight: 500;
border-radius: 4px;
font-size: 0.85rem;
}
.btn-danger {
background-color: #dc3545;
border: none;
padding: 6px 16px;
font-weight: 500;
border-radius: 4px;
font-size: 0.85rem;
}
.upload-section {
background-color: #f8f9fa;
padding: 15px;
border-radius: 4px;
}
.custom-file-label {
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
}
.custom-file-label::after {
background-color: #007bff;
color: white;
border-radius: 0 3px 3px 0;
font-size: 0.85rem;
}
/* 아이콘 + 뱃지 스타일 */
.file-list {
max-height: 500px;
overflow-y: auto;
}
.icon-badge-item {
border: 1px solid #e9ecef;
border-radius: 6px;
padding: 10px 15px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
transition: all 0.3s;
background: white;
}
.icon-badge-item:hover {
background-color: #f8f9fa;
border-color: #007bff;
transform: translateX(3px);
}
.icon-badge-left {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.file-icon-small {
width: 36px;
height: 36px;
background: #007bff;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
flex-shrink: 0;
}
.file-name-section {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.file-name-badge {
font-weight: 500;
color: #333;
font-size: 0.9rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.badge-custom {
background-color: #e7f3ff;
color: #007bff;
padding: 3px 10px;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
flex-shrink: 0;
}
.action-buttons {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.empty-message {
text-align: center;
color: #999;
padding: 30px;
font-size: 0.9rem;
}
.container {
max-width: 1200px;
}
/* 스크롤바 스타일 */
.file-list::-webkit-scrollbar {
width: 6px;
}
.file-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
.file-list::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
.file-list::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body>
<div class="container">
<h1 class="main-title">XML 파일 관리</h1>
<p class="subtitle">XML 파일을 업로드하고 관리할 수 있습니다</p>
<!-- XML 파일 업로드 폼 -->
<div class="card">
<div class="card-header-custom">
<i class="fas fa-cloud-upload-alt mr-2"></i>파일 업로드
</div>
<div class="card-body">
<form action="{{ url_for('xml.upload_xml') }}" method="POST" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<div class="upload-section">
<div class="form-group mb-2">
<label for="xmlFile" class="form-label">XML 파일 선택</label>
<div class="custom-file">
<input type="file" class="custom-file-input" id="xmlFile" name="xmlFile" accept=".xml" onchange="updateFileName(this)">
<label class="custom-file-label" for="xmlFile" id="fileLabel">파일을 선택하세요</label>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload mr-1"></i>업로드
</button>
</div>
</form>
</div>
</div>
<!-- XML 파일 목록 -->
<div class="card">
<div class="card-header-custom">
<i class="fas fa-list mr-2"></i>파일 목록
</div>
<div class="card-body">
{% if xml_files %}
<div class="file-list">
{% for xml_file in xml_files %}
<div class="icon-badge-item">
<div class="icon-badge-left">
<div class="file-icon-small">
<i class="fas fa-file-code"></i>
</div>
<div class="file-name-section">
<span class="file-name-badge" title="{{ xml_file }}">{{ xml_file }}</span>
<span class="badge-custom">XML</span>
</div>
</div>
<div class="action-buttons">
<!-- 파일 편집 버튼 -->
<a href="{{ url_for('xml.edit_xml', filename=xml_file) }}" class="btn btn-success btn-sm">
<i class="fas fa-edit"></i> 편집
</a>
<!-- 파일 삭제 버튼 -->
<form action="{{ url_for('xml.delete_xml', filename=xml_file) }}" method="POST" style="display:inline;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('정말 삭제하시겠습니까?')">
<i class="fas fa-trash"></i> 삭제
</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-message">
<i class="fas fa-folder-open" style="font-size: 2rem; color: #ddd;"></i>
<p class="mt-2 mb-0">파일이 없습니다.</p>
</div>
{% endif %}
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
function updateFileName(input) {
const fileName = input.files[0]?.name || '파일을 선택하세요';
document.getElementById('fileLabel').textContent = fileName;
}
</script>
</body>
</html>
{% endblock %}

View File

@@ -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 %}

View File

@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
<body>
<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>
</body>
{% endblock %}