Update 2025-12-19 16:23:03

This commit is contained in:
unknown
2025-12-19 16:23:03 +09:00
parent 804204ab97
commit b18412ecb2
30 changed files with 6607 additions and 1165 deletions

View File

@@ -1,72 +1,151 @@
{# backend/templates/admin.html #}
{% extends "base.html" %}
{% block title %}관리자 패널 - Dell Server Info{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="card-title mb-0">Admin Page</h3>
<a href="{{ url_for('admin.settings') }}" class="btn btn-outline-primary">
<i class="bi bi-gear-fill me-1"></i>시스템 설정
</a>
</div>
<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>
{% 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>
<!-- 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>
{% endfor %}
</div>
{% endif %}
{% endwith %}
</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-striped align-middle">
<thead>
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<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>
<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>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<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">Yes</span>
<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-secondary">No</span>
<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>
{% 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>
<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 %}
<!-- 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>
<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>
<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="4" class="text-center py-5 text-muted">사용자가 없습니다.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
@@ -75,41 +154,44 @@
</div>
{# ========== Change Password Modal ========== #}
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-labelledby="changePasswordModalLabel"
aria-hidden="true">
<div class="modal fade" id="changePasswordModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<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">
<h5 class="modal-title" id="changePasswordModalLabel">Change Password</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<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">
<div class="mb-2">
<small class="text-muted">User:</small>
<div id="modalUserInfo" class="fw-bold"></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">New password</label>
<label for="newPasswordInput" class="form-label fw-semibold">새 비밀번호</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>
placeholder="최소 8자 이상">
</div>
<div class="mb-3">
<label for="confirmPasswordInput" class="form-label">Confirm password</label>
<label for="confirmPasswordInput" class="form-label fw-semibold">비밀번호 확인</label>
<input id="confirmPasswordInput" name="confirm_password" type="password" class="form-control" required
minlength="8" placeholder="Confirm new password">
minlength="8" placeholder="비밀번호 재입력">
<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 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>