v4.0.10: 관리자 계정 변경 안전장치와 접속시간 지표 추가
역할 변경은 명시적으로 버튼을 눌렀을 때만 가능하게 제한하고, 마지막 접속시간과 비밀번호 분실 안내를 추가해 운영 관리성을 높인다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
79
admin.html
79
admin.html
@@ -92,12 +92,20 @@
|
||||
<input type="text" id="edit-username" class="w-full box-border rounded-xl border border-slate-200 px-3 py-2.5 text-sm disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-400" autocomplete="off" />
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase text-slate-400" for="edit-role">역할</label>
|
||||
<select id="edit-role" class="w-full box-border rounded-xl border border-slate-200 px-3 py-2.5 text-sm disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-400">
|
||||
<option value="user">일반 회원</option>
|
||||
<option value="admin">관리자</option>
|
||||
</select>
|
||||
<p id="edit-role-hint" class="mt-1.5 hidden text-[0.6875rem] leading-snug text-amber-600">본인 계정의 관리자 권한은 변경할 수 없습니다.</p>
|
||||
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase text-slate-400">역할</label>
|
||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span id="edit-role-summary" class="text-sm font-semibold text-slate-700"></span>
|
||||
<button type="button" id="toggle-role-editor" class="cursor-pointer rounded-lg border-0 bg-slate-200 px-2.5 py-1 text-[0.6875rem] font-bold text-slate-600 hover:bg-slate-300">역할 변경하기</button>
|
||||
</div>
|
||||
<div id="edit-role-editor" class="mt-2 hidden">
|
||||
<select id="edit-role" class="w-full box-border rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-sm disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-400">
|
||||
<option value="user">일반 회원</option>
|
||||
<option value="admin">관리자</option>
|
||||
</select>
|
||||
<p id="edit-role-hint" class="mt-1.5 hidden text-[0.6875rem] leading-snug text-amber-600">본인 계정의 관리자 권한은 변경할 수 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="mb-1.5 block text-[0.6875rem] font-bold uppercase text-slate-400" for="edit-isActive">상태</label>
|
||||
@@ -171,6 +179,7 @@
|
||||
let pendingAvatarImage = null;
|
||||
let pendingRemoveAvatar = false;
|
||||
let adminDecks = [];
|
||||
let isRoleEditorOpen = false;
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str ?? '')
|
||||
@@ -191,6 +200,41 @@
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 날짜를 상대 시간 텍스트로 변환
|
||||
* @param {string|null|undefined} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatRelativeTime(value) {
|
||||
if (!value) return '기록 없음';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '기록 없음';
|
||||
|
||||
const diffMs = Date.now() - date.getTime();
|
||||
if (diffMs < 0) return '방금 전';
|
||||
|
||||
const minuteMs = 60 * 1000;
|
||||
const hourMs = 60 * minuteMs;
|
||||
const dayMs = 24 * hourMs;
|
||||
const monthMs = 30 * dayMs;
|
||||
|
||||
if (diffMs < minuteMs) return '방금 전';
|
||||
if (diffMs < hourMs) return `${Math.floor(diffMs / minuteMs)}분 전`;
|
||||
if (diffMs < dayMs) return `${Math.floor(diffMs / hourMs)}시간 전`;
|
||||
if (diffMs < monthMs) return `${Math.floor(diffMs / dayMs)}일 전`;
|
||||
return `${Math.floor(diffMs / monthMs)}달 전`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 역할 변경 입력창 표시 여부
|
||||
* @param {boolean} isOpen
|
||||
*/
|
||||
function setRoleEditorOpen(isOpen) {
|
||||
isRoleEditorOpen = isOpen;
|
||||
document.getElementById('edit-role-editor').classList.toggle('hidden', !isOpen);
|
||||
document.getElementById('toggle-role-editor').textContent = isOpen ? '역할 변경 닫기' : '역할 변경하기';
|
||||
}
|
||||
|
||||
async function guardAdmin() {
|
||||
if (!UaDeckApi.getToken()) {
|
||||
location.href = '/login.html?redirect=/admin.html';
|
||||
@@ -228,10 +272,13 @@
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="mb-0.5 text-sm font-bold leading-snug text-slate-900">${escapeHtml(u.username)}${isSelf ? ' <span class="rounded-md bg-blue-50 px-1.5 py-0.5 text-[0.625rem] font-bold text-blue-600">나</span>' : ''}</h3>
|
||||
<div class="truncate text-xs text-slate-400">${escapeHtml(u.email)}</div>
|
||||
<div class="mt-1.5 text-xs font-semibold text-slate-600" title="공개 덱 / 전체 저장 덱">덱 ${deckPublic}/${deckTotal} <span class="font-normal text-slate-400">(공개/전체)</span></div>
|
||||
<div class="mt-1.5 flex flex-wrap gap-1">
|
||||
<span class="rounded-md bg-blue-50 px-1.5 py-0.5 text-[0.625rem] font-bold text-blue-600">${escapeHtml(u.role)}</span>
|
||||
<span class="rounded-md px-1.5 py-0.5 text-[0.625rem] font-bold ${u.isActive ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-500'}">${u.isActive ? '활성' : '비활성'}</span>
|
||||
<div class="mt-1 text-[0.6875rem] text-slate-500">마지막 접속: ${escapeHtml(formatRelativeTime(u.lastLoginAt))}</div>
|
||||
<div class="mt-1.5 flex flex-wrap justify-between gap-1">
|
||||
<div class="flex gap-1">
|
||||
<span class="rounded-md bg-blue-50 px-1.5 py-0.5 text-[0.625rem] font-bold text-blue-600">${escapeHtml(u.role)}</span>
|
||||
<span class="rounded-md px-1.5 py-0.5 text-[0.625rem] font-bold ${u.isActive ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-100 text-slate-500'}">${u.isActive ? '활성' : '비활성'}</span>
|
||||
</div>
|
||||
<div class="mt-1.5 text-xs font-semibold text-slate-600" title="공개 덱 / 전체 저장 덱">덱 ${deckPublic}/${deckTotal} <span class="font-normal text-slate-400">(공개/전체)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" data-edit='${JSON.stringify(u).replace(/'/g, ''')}' class="shrink-0 cursor-pointer rounded-lg border-0 bg-slate-100 px-2.5 py-1.5 text-[0.6875rem] font-bold text-slate-600 hover:bg-slate-200">수정</button>
|
||||
@@ -256,13 +303,17 @@
|
||||
document.getElementById('edit-username').value = u.username;
|
||||
document.getElementById('edit-role').value = u.role;
|
||||
document.getElementById('edit-isActive').value = u.isActive ? '1' : '0';
|
||||
document.getElementById('edit-role-summary').textContent = u.role === 'admin' ? '관리자' : '일반 회원';
|
||||
document.getElementById('edit-modal-title').textContent = u.username;
|
||||
document.getElementById('edit-modal-sub').textContent = u.email;
|
||||
document.getElementById('edit-avatar-preview').src = resolveAvatarSrc(u);
|
||||
document.getElementById('edit-avatar-remove').classList.toggle('hidden', !u.avatarUrl);
|
||||
setRoleEditorOpen(false);
|
||||
|
||||
document.getElementById('edit-role').disabled = isSelf;
|
||||
document.getElementById('edit-isActive').disabled = isSelf;
|
||||
document.getElementById('toggle-role-editor').disabled = isSelf;
|
||||
document.getElementById('toggle-role-editor').classList.toggle('hidden', isSelf);
|
||||
document.getElementById('edit-role-hint').classList.toggle('hidden', !isSelf);
|
||||
document.getElementById('edit-status-hint').classList.toggle('hidden', !isSelf);
|
||||
document.getElementById('modal-error').classList.add('hidden');
|
||||
@@ -449,6 +500,10 @@
|
||||
document.getElementById('password-error').classList.add('hidden');
|
||||
setModalOpen(passwordModal, true);
|
||||
});
|
||||
document.getElementById('toggle-role-editor').addEventListener('click', () => {
|
||||
if (!editingUser || editingUser.id === currentAdminId) return;
|
||||
setRoleEditorOpen(!isRoleEditorOpen);
|
||||
});
|
||||
|
||||
document.getElementById('password-cancel').addEventListener('click', () => {
|
||||
setModalOpen(passwordModal, false);
|
||||
@@ -549,7 +604,9 @@
|
||||
username: document.getElementById('edit-username').value,
|
||||
};
|
||||
if (!isSelf) {
|
||||
body.role = document.getElementById('edit-role').value;
|
||||
if (isRoleEditorOpen) {
|
||||
body.role = document.getElementById('edit-role').value;
|
||||
}
|
||||
body.isActive = document.getElementById('edit-isActive').value === '1';
|
||||
}
|
||||
if (pendingAvatarImage) body.avatarImage = pendingAvatarImage;
|
||||
|
||||
Reference in New Issue
Block a user