feat(admin): 권한 열을 칩 토글로 — 모듈 늘어도 가로 안 넓어짐
모듈마다 토글 열을 두던 방식은 모듈이 늘수록 표가 가로로 무한 확장됐다. 모듈 열 전체를 단일 '접근 권한' 셀의 칩(pill) 토글로 교체. 칩이 줄바꿈되어 모듈이 늘어도 세로로만 늘고 한 화면에서 관리 가능. 칩 클릭=토글, admin/슈퍼는 잠금(전체 ON). 승인 권한 칩은 켜지면 파랑으로 구분. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+57
-31
@@ -24,11 +24,30 @@
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.erp-mod-th { text-align: center; font-size: 11px; padding: 6px 4px; }
|
||||
.erp-mod-cell { text-align: center; padding: 4px; }
|
||||
.erp-mod-group { display: inline-block; padding: 2px 6px; border-radius: 6px;
|
||||
.erp-mod-group { display: inline-block; margin-left: 6px; padding: 2px 6px; border-radius: 6px;
|
||||
background: var(--color-ghost-gray); color: var(--color-midtone-gray);
|
||||
font-size: 10px; font-weight: 500; }
|
||||
font-size: 10px; font-weight: 500; vertical-align: middle; }
|
||||
|
||||
/* 권한 칩 — 모듈 수가 늘어도 줄바꿈되어 가로로 안 넓어짐 */
|
||||
.erp-perm-cell { padding: 8px 6px; }
|
||||
.erp-perm-chip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
margin: 3px; padding: 4px 11px; border-radius: 999px;
|
||||
border: 1px solid var(--color-subtle-ash); background: #fff;
|
||||
color: var(--color-midtone-gray); font-size: 12px; font-weight: 500;
|
||||
font-family: inherit; cursor: pointer; user-select: none;
|
||||
transition: background .1s, color .1s, border-color .1s;
|
||||
}
|
||||
.erp-perm-chip:hover { border-color: var(--color-midtone-gray); }
|
||||
.erp-perm-chip.is-on {
|
||||
background: var(--color-rich-black); color: #fff; border-color: var(--color-rich-black);
|
||||
}
|
||||
/* 승인 권한 칩 — 켜지면 파랑 강조로 구분 */
|
||||
.erp-perm-chip.is-approver.is-on {
|
||||
background: #1d4ed8; border-color: #1d4ed8;
|
||||
}
|
||||
.erp-perm-chip[data-locked="true"] { opacity: .5; cursor: not-allowed; }
|
||||
.erp-perm-chip[data-locked="true"]:hover { border-color: var(--color-subtle-ash); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="erp-body">
|
||||
@@ -89,15 +108,10 @@
|
||||
<table class="erp-table" id="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 22%;">사용자</th>
|
||||
<th style="width: 200px;">사용자</th>
|
||||
<th style="width: 80px;">역할</th>
|
||||
{% for key in module_keys %}
|
||||
<th class="erp-mod-th">
|
||||
{{ module_labels.get(key, key) }}
|
||||
{% if key in approver_keys %}<br><span class="erp-mod-group">승인</span>{% endif %}
|
||||
</th>
|
||||
{% endfor %}
|
||||
<th style="width: 120px;">최근 로그인</th>
|
||||
<th>접근 권한 <span class="erp-mod-group">클릭하여 토글</span></th>
|
||||
<th style="width: 110px;">최근 로그인</th>
|
||||
<th style="width: 130px; text-align: right;">동작</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -130,18 +144,19 @@
|
||||
<option value="admin" {% if u.role == 'admin' %}selected{% endif %}>admin</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="erp-perm-cell">
|
||||
{% for key in module_keys %}
|
||||
<td class="erp-mod-cell">
|
||||
<label class="erp-switch">
|
||||
<input type="checkbox"
|
||||
data-field="module"
|
||||
<button type="button"
|
||||
class="erp-perm-chip
|
||||
{% if u.modules[key] %}is-on{% endif %}
|
||||
{% if key in approver_keys %}is-approver{% endif %}"
|
||||
data-module="{{ key }}"
|
||||
{% if u.modules[key] %}checked{% endif %}
|
||||
{% if u.is_super_admin or u.role == 'admin' %}disabled{% endif %} />
|
||||
<span class="erp-switch-slider"></span>
|
||||
</label>
|
||||
</td>
|
||||
aria-pressed="{{ 'true' if u.modules[key] else 'false' }}"
|
||||
{% if u.is_super_admin or u.role == 'admin' %}data-locked="true"{% endif %}>
|
||||
{{ module_labels.get(key, key) }}{% if key in approver_keys %} 승인{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td style="font-size:12px; color: var(--color-midtone-gray);">
|
||||
{{ (u.last_login or '미접속')[:16].replace('T', ' ') }}
|
||||
</td>
|
||||
@@ -188,15 +203,16 @@
|
||||
showToast._t = setTimeout(() => { toastEl.dataset.visible = "false"; }, 2400);
|
||||
}
|
||||
|
||||
function syncModuleSwitches(tr) {
|
||||
function syncPermChips(tr) {
|
||||
const isSuper = tr.dataset.super === "true";
|
||||
const role = tr.querySelector("select[data-field='role']").value;
|
||||
const adminRole = role === "admin";
|
||||
tr.querySelectorAll("input[data-field='module']").forEach((cb) => {
|
||||
const adminRole = tr.querySelector("select[data-field='role']").value === "admin";
|
||||
tr.querySelectorAll(".erp-perm-chip").forEach((chip) => {
|
||||
if (isSuper || adminRole) {
|
||||
cb.checked = true; cb.disabled = true;
|
||||
chip.classList.add("is-on");
|
||||
chip.setAttribute("aria-pressed", "true");
|
||||
chip.setAttribute("data-locked", "true");
|
||||
} else {
|
||||
cb.disabled = false;
|
||||
chip.removeAttribute("data-locked");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -204,12 +220,22 @@
|
||||
tbody.addEventListener("change", (e) => {
|
||||
const tr = e.target.closest("tr");
|
||||
if (!tr) return;
|
||||
if (e.target.dataset.field === "role") syncModuleSwitches(tr);
|
||||
if (e.target.dataset.field === "role") syncPermChips(tr);
|
||||
markDirty(tr);
|
||||
});
|
||||
|
||||
tbody.addEventListener("click", async (e) => {
|
||||
const btn = e.target.closest("button");
|
||||
// 권한 칩 토글 (잠금 상태 제외)
|
||||
const chip = e.target.closest(".erp-perm-chip");
|
||||
if (chip) {
|
||||
if (chip.getAttribute("data-locked") === "true") return;
|
||||
const on = chip.classList.toggle("is-on");
|
||||
chip.setAttribute("aria-pressed", on ? "true" : "false");
|
||||
markDirty(chip.closest("tr"));
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = e.target.closest("button[data-action]");
|
||||
if (!btn) return;
|
||||
const tr = btn.closest("tr");
|
||||
const email = tr.dataset.email;
|
||||
@@ -217,8 +243,8 @@
|
||||
if (btn.dataset.action === "save") {
|
||||
const role = tr.querySelector("select[data-field='role']").value;
|
||||
const modules = {};
|
||||
tr.querySelectorAll("input[data-field='module']").forEach((cb) => {
|
||||
modules[cb.dataset.module] = cb.checked;
|
||||
tr.querySelectorAll(".erp-perm-chip").forEach((c) => {
|
||||
modules[c.dataset.module] = c.classList.contains("is-on");
|
||||
});
|
||||
btn.disabled = true; btn.textContent = "저장 중…";
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user