Update intranet tools and voucher comparison

This commit is contained in:
b17301
2026-06-04 09:00:51 +09:00
parent 2ab74bac88
commit b93289bfa8
55 changed files with 32809 additions and 1063 deletions
+289
View File
@@ -0,0 +1,289 @@
{% extends "base.html" %}
{% block title %}사용자 관리{% endblock %}
{% block head_extra %}
<style>
.admin-user-form {
display: grid;
gap: 12px;
padding: 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: #f8fafc;
margin-bottom: 18px;
}
.admin-user-main-row {
display: grid;
grid-template-columns: minmax(110px, 0.9fr) minmax(120px, 1fr) minmax(140px, 1fr) minmax(130px, 0.9fr) auto;
gap: 10px;
align-items: end;
}
.admin-user-form .field {
min-width: 0;
}
.admin-user-form input:not([type="checkbox"]),
.admin-user-form select {
width: 100%;
min-height: 36px;
border: 1px solid var(--line);
border-radius: 8px;
background: #ffffff;
color: var(--ink);
box-sizing: border-box;
padding: 8px 10px;
}
.admin-user-actions {
display: flex;
align-items: end;
justify-content: flex-end;
min-width: 76px;
}
.admin-user-actions button {
width: 76px;
min-height: 36px;
padding: 0 14px;
border-radius: 8px;
}
.admin-user-permission-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 12px;
align-items: center;
}
.permission-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 7px 0 0;
}
.permission-grid label {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
min-height: 28px;
padding: 4px 9px;
border: 1px solid var(--line);
border-radius: 999px;
background: #ffffff;
color: var(--ink);
font-size: 12px;
font-weight: 800;
line-height: 1;
white-space: nowrap;
cursor: pointer;
}
.permission-grid input[type="checkbox"],
.admin-active-toggle input[type="checkbox"] {
width: 14px;
height: 14px;
min-height: 0;
margin: 0;
accent-color: #111827;
}
.permission-chip-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.permission-chip {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 2px 7px;
border: 1px solid var(--line);
background: #f7f9fb;
font-size: 12px;
font-weight: 800;
}
.admin-active-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 30px;
padding-top: 19px;
white-space: nowrap;
font-size: 12px;
font-weight: 800;
color: var(--ink);
}
@media (max-width: 1100px) {
.admin-user-main-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.admin-user-actions {
justify-content: flex-start;
}
}
@media (max-width: 720px) {
.admin-user-main-row,
.admin-user-permission-row {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block content %}
<section class="panel">
<div class="section-title">
<div>
<h2>사용자 관리</h2>
</div>
</div>
<form class="admin-user-form" method="post" action="/admin/users">
<div class="admin-user-main-row">
<div class="field">
<label for="username">아이디</label>
<input id="username" name="username" required autocomplete="off">
</div>
<div class="field">
<label for="displayName">이름</label>
<input id="displayName" name="display_name">
</div>
<div class="field">
<label for="password">비밀번호</label>
<input id="password" name="password" type="password" autocomplete="new-password">
</div>
<div class="field">
<label for="role">역할</label>
<select id="role" name="role">
<option value="viewer">사용자별 권한</option>
<option value="admin">관리자</option>
</select>
</div>
<div class="admin-user-actions">
<button type="submit">저장</button>
</div>
</div>
<div class="admin-user-permission-row">
<div>
<label>페이지 권한</label>
<div class="permission-grid">
{% for permission in permission_options %}
<label>
<input type="checkbox" name="permissions" value="{{ permission.key }}" {% if permission.key != 'admin' %}checked{% endif %}>
{{ permission.label }}
</label>
{% endfor %}
</div>
</div>
<label class="admin-active-toggle">
<input type="checkbox" name="is_active" value="1" checked>
활성
</label>
</div>
</form>
<table>
<thead>
<tr>
<th>아이디</th>
<th>이름</th>
<th>역할</th>
<th>페이지 권한</th>
<th>상태</th>
<th>관리자</th>
<th>수정일</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr
data-username="{{ user.username }}"
data-display-name="{{ user.display_name }}"
data-role="{{ 'admin' if user.is_admin else 'viewer' }}"
data-active="{{ '1' if user.is_active else '0' }}"
data-permissions="{{ ','.join(user.direct_permissions) }}"
>
<td><button type="button" class="button-secondary user-edit-button">{{ user.username }}</button></td>
<td>{{ user.display_name }}</td>
<td>{{ ', '.join(user.roles) }}</td>
<td>
<div class="permission-chip-list">
{% for permission in permission_options %}
{% if permission.key in user.direct_permissions %}
<span class="permission-chip">{{ permission.label }}</span>
{% endif %}
{% endfor %}
{% if user.is_admin %}
<span class="permission-chip">전체</span>
{% elif not user.direct_permissions %}
<span class="muted">역할 기본값</span>
{% endif %}
</div>
</td>
<td>{{ '활성' if user.is_active else '비활성' }}</td>
<td>{{ '예' if user.is_admin else '-' }}</td>
<td>{{ user.updated_at }}</td>
</tr>
{% else %}
<tr><td colspan="7" class="empty">사용자가 없습니다.</td></tr>
{% endfor %}
</tbody>
</table>
</section>
{% endblock %}
{% block script %}
<script>
(() => {
const form = document.querySelector("form[action='/admin/users']");
if (!form) return;
const username = form.querySelector("[name='username']");
const displayName = form.querySelector("[name='display_name']");
const password = form.querySelector("[name='password']");
const role = form.querySelector("[name='role']");
const active = form.querySelector("[name='is_active']");
const permissionInputs = [...form.querySelectorAll("[name='permissions']")];
document.querySelectorAll(".user-edit-button").forEach((button) => {
button.addEventListener("click", () => {
const row = button.closest("tr");
const permissions = new Set(String(row?.dataset.permissions || "").split(",").filter(Boolean));
username.value = row?.dataset.username || "";
displayName.value = row?.dataset.displayName || "";
password.value = "";
role.value = row?.dataset.role || "viewer";
active.checked = row?.dataset.active === "1";
permissionInputs.forEach((input) => {
input.checked = permissions.has(input.value) || (role.value !== "admin" && !permissions.size && input.value !== "admin");
});
username.focus();
});
});
role.addEventListener("change", () => {
const isAdmin = role.value === "admin";
permissionInputs.forEach((input) => {
input.disabled = isAdmin;
if (isAdmin) input.checked = false;
});
});
form.addEventListener("submit", () => {
const isAdmin = role.value === "admin";
permissionInputs.forEach((input) => {
input.disabled = isAdmin;
});
});
})();
</script>
{% endblock %}