관리자 덱 불러오기와 회원 탈퇴 동선 개선
관리자 덱 카드에서 썸네일 확대와 불러오기를 복구하고, 회원 수정은 역할 변경 메뉴 안에서 탈퇴를 선택해 2단계 확인 후 처리하도록 통합한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
94
admin.html
94
admin.html
@@ -109,6 +109,7 @@
|
||||
<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>
|
||||
<option value="delete_user">회원 탈퇴 처리</option>
|
||||
</select>
|
||||
<p id="edit-role-hint" class="mt-1.5 hidden text-[0.6875rem] leading-snug text-amber-600">본인 계정의 관리자 권한은 변경할 수 없습니다.</p>
|
||||
</div>
|
||||
@@ -126,7 +127,6 @@
|
||||
<div class="my-4 h-px bg-slate-200"></div>
|
||||
<button type="button" id="approve-email-verification" class="mb-2 hidden w-full cursor-pointer rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-2.5 text-[0.8125rem] font-bold text-emerald-700 hover:bg-emerald-100">이메일 인증 승인</button>
|
||||
<button type="button" id="open-password-modal" class="w-full cursor-pointer rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-[0.8125rem] font-bold text-slate-600 hover:bg-slate-50">비밀번호 변경하기</button>
|
||||
<button type="button" id="open-user-delete-modal" class="mt-2 w-full cursor-pointer rounded-xl border border-red-200 bg-red-50 px-4 py-2.5 text-[0.8125rem] font-bold text-red-600 hover:bg-red-100">회원 탈퇴 처리</button>
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button type="button" id="modal-cancel" class="flex-1 cursor-pointer rounded-xl border-0 bg-slate-100 px-4 py-2.5 text-[0.8125rem] font-bold text-slate-600 hover:bg-slate-200">취소</button>
|
||||
<button type="button" id="modal-save" class="flex-1 cursor-pointer rounded-xl border-0 bg-slate-800 px-4 py-2.5 text-[0.8125rem] font-bold text-white hover:bg-slate-700">저장</button>
|
||||
@@ -178,6 +178,7 @@
|
||||
|
||||
<script src="/script/api.js?v=4.0.16"></script>
|
||||
<script src="/script/image-utils.js"></script>
|
||||
<script src="/script/deck-preview-hold.js?v=4.0.17"></script>
|
||||
<script src="/script/auth-header.js"></script>
|
||||
<script>
|
||||
const DEFAULT_AVATAR =
|
||||
@@ -188,7 +189,6 @@
|
||||
|
||||
const editModal = document.getElementById('edit-modal');
|
||||
const passwordModal = document.getElementById('password-modal');
|
||||
const userDeleteModal = document.getElementById('user-delete-modal');
|
||||
|
||||
/**
|
||||
* 관리자 모달 표시 상태 변경
|
||||
@@ -343,7 +343,6 @@
|
||||
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('open-user-delete-modal').classList.toggle('hidden', isSelf);
|
||||
document.getElementById('approve-email-verification').classList.toggle(
|
||||
'hidden',
|
||||
Boolean(u.emailVerifiedAt),
|
||||
@@ -355,11 +354,12 @@
|
||||
|
||||
function renderDeckPreview(d) {
|
||||
if (!d.preview_url) {
|
||||
return '<div class="flex h-full items-center justify-center bg-white text-[0.6875rem] font-semibold text-slate-400">미리보기 없음</div>';
|
||||
return '<div class="deck-card-preview"><div class="flex h-full items-center justify-center bg-white text-[0.6875rem] font-semibold text-slate-400">미리보기 없음</div></div>';
|
||||
}
|
||||
const src = UaDeckApi.resolveMediaUrl(d.preview_url);
|
||||
const version = d.updatedAt ? `?v=${encodeURIComponent(d.updatedAt)}` : '';
|
||||
return `<img src="${escapeHtml(src + version)}" alt="" loading="lazy" class="h-full w-full bg-white object-contain" onerror="this.parentElement.innerHTML='<div class=\\'flex h-full items-center justify-center bg-white text-[0.6875rem] font-semibold text-slate-400\\'>미리보기 없음</div>'" />`;
|
||||
const previewSrc = escapeHtml(src + version);
|
||||
return `<div class="deck-card-preview deck-card-preview--holdable" data-hold-preview data-preview-src="${previewSrc}"><img src="${previewSrc}" alt="" loading="lazy" class="admin-deck-preview-img h-full w-full bg-white object-contain" draggable="false" onerror="this.parentElement.classList.remove('deck-card-preview--holdable'); this.parentElement.removeAttribute('data-hold-preview'); this.parentElement.removeAttribute('data-preview-src'); this.parentElement.innerHTML='<div class=\\'flex h-full items-center justify-center bg-white text-[0.6875rem] font-semibold text-slate-400\\'>미리보기 없음</div>'" /></div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -444,7 +444,7 @@
|
||||
<label class="absolute left-2 top-2 z-10 flex cursor-pointer items-center rounded-lg bg-white/90 p-1.5 shadow-sm" title="덱 선택">
|
||||
<input type="checkbox" class="admin-deck-select size-4 rounded border-slate-300 accent-blue-600" data-share-code="${escapeHtml(d.shareCode)}" ${selectedAdminDecks.has(d.shareCode) ? 'checked' : ''} />
|
||||
</label>
|
||||
<div class="deck-card-preview">${renderDeckPreview(d)}</div>
|
||||
${renderDeckPreview(d)}
|
||||
</div>
|
||||
<div class="deck-card-body">
|
||||
<div class="deck-card-visibility">
|
||||
@@ -454,6 +454,7 @@
|
||||
<h2 class="deck-card-title">${title}</h2>
|
||||
<div class="deck-card-meta">${escapeHtml(d.username || '비회원')} · ${date}</div>
|
||||
<div class="deck-card-actions justify-end">
|
||||
<button type="button" class="deck-card-btn deck-card-btn-load" data-admin-load="${escapeHtml(d.shareCode)}" data-series="${escapeHtml(d.series)}" data-name="${escapeHtml(d.seriesName || '')}">불러오기</button>
|
||||
<div class="deck-card-more-wrap">
|
||||
<button type="button" class="deck-card-more-btn" aria-haspopup="true" aria-expanded="false" aria-label="더 보기">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><circle cx="8" cy="3" r="1.5"/><circle cx="8" cy="8" r="1.5"/><circle cx="8" cy="13" r="1.5"/></svg>
|
||||
@@ -468,6 +469,9 @@
|
||||
})
|
||||
.join('')}</div>`;
|
||||
closeAdminDeckMenus();
|
||||
if (window.UaDeckPreviewHold?.bindHoldPreview) {
|
||||
window.UaDeckPreviewHold.bindHoldPreview(list);
|
||||
}
|
||||
updateBulkDeleteControls(filtered);
|
||||
}
|
||||
|
||||
@@ -591,49 +595,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('open-user-delete-modal').addEventListener('click', () => {
|
||||
if (!editingUser || editingUser.id === currentAdminId) return;
|
||||
const confirmationInput = document.getElementById('user-delete-confirmation');
|
||||
document.getElementById('user-delete-modal-sub').textContent =
|
||||
`${editingUser.username} (${editingUser.email})`;
|
||||
confirmationInput.value = '';
|
||||
document.getElementById('user-delete-error').classList.add('hidden');
|
||||
document.getElementById('user-delete-yes').disabled = true;
|
||||
setModalOpen(userDeleteModal, true);
|
||||
confirmationInput.focus();
|
||||
});
|
||||
|
||||
document.getElementById('user-delete-confirmation').addEventListener('input', (event) => {
|
||||
document.getElementById('user-delete-yes').disabled = event.target.value !== 'DELETE';
|
||||
});
|
||||
|
||||
document.getElementById('user-delete-no').addEventListener('click', () => {
|
||||
setModalOpen(userDeleteModal, false);
|
||||
});
|
||||
|
||||
document.getElementById('user-delete-yes').addEventListener('click', async () => {
|
||||
if (!editingUser || editingUser.id === currentAdminId) return;
|
||||
const confirmation = document.getElementById('user-delete-confirmation').value;
|
||||
const errorEl = document.getElementById('user-delete-error');
|
||||
const yesButton = document.getElementById('user-delete-yes');
|
||||
errorEl.classList.add('hidden');
|
||||
yesButton.disabled = true;
|
||||
try {
|
||||
await UaDeckApi.adminDeleteUser(editingUser.id, confirmation);
|
||||
setModalOpen(userDeleteModal, false);
|
||||
setModalOpen(editModal, false);
|
||||
editingUser = null;
|
||||
await Promise.all([
|
||||
loadUsers(document.getElementById('user-search').value),
|
||||
loadAdminDecks(),
|
||||
loadOrphanPreviews(),
|
||||
]);
|
||||
} catch (err) {
|
||||
errorEl.textContent = err.message;
|
||||
errorEl.classList.remove('hidden');
|
||||
yesButton.disabled = confirmation !== 'DELETE';
|
||||
}
|
||||
});
|
||||
document.getElementById('toggle-role-editor').addEventListener('click', () => {
|
||||
if (!editingUser || editingUser.id === currentAdminId) return;
|
||||
setRoleEditorOpen(!isRoleEditorOpen);
|
||||
@@ -716,6 +677,15 @@
|
||||
if (!confirm('덱과 연결된 썸네일을 함께 삭제하시겠습니까?')) return;
|
||||
await UaDeckApi.adminDeleteDeck(deleteBtn.dataset.adminDelete);
|
||||
await Promise.all([loadAdminDecks(), loadOrphanPreviews()]);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadBtn = e.target.closest('[data-admin-load]');
|
||||
if (loadBtn) {
|
||||
const code = loadBtn.dataset.adminLoad;
|
||||
const series = loadBtn.dataset.series;
|
||||
const name = loadBtn.dataset.name;
|
||||
location.href = `/cardList.html?series=${series}&seriesName=${encodeURIComponent(name)}&d=${code}`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -770,9 +740,31 @@
|
||||
email: document.getElementById('edit-email').value,
|
||||
username: document.getElementById('edit-username').value,
|
||||
};
|
||||
const selectedRole = document.getElementById('edit-role').value;
|
||||
if (!isSelf) {
|
||||
if (isRoleEditorOpen) {
|
||||
body.role = document.getElementById('edit-role').value;
|
||||
if (selectedRole === 'delete_user') {
|
||||
const targetLabel = `${document.getElementById('edit-username').value || editingUser?.username || ''} (${document.getElementById('edit-email').value || editingUser?.email || ''})`.trim();
|
||||
const firstConfirm = confirm(`정말 ${targetLabel} 회원을 탈퇴 처리하시겠습니까?`);
|
||||
if (!firstConfirm) return;
|
||||
const secondConfirm = confirm('탈퇴 처리 후 계정 복구는 불가능합니다. 계속 진행할까요?');
|
||||
if (!secondConfirm) return;
|
||||
try {
|
||||
await UaDeckApi.adminDeleteUser(id, 'DELETE');
|
||||
setModalOpen(editModal, false);
|
||||
editingUser = null;
|
||||
await Promise.all([
|
||||
loadUsers(document.getElementById('user-search').value),
|
||||
loadAdminDecks(),
|
||||
loadOrphanPreviews(),
|
||||
]);
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.classList.remove('hidden');
|
||||
}
|
||||
return;
|
||||
}
|
||||
body.role = selectedRole;
|
||||
}
|
||||
body.isActive = document.getElementById('edit-isActive').value === '1';
|
||||
}
|
||||
@@ -792,7 +784,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
[editModal, passwordModal, userDeleteModal].forEach((modal) => {
|
||||
[editModal, passwordModal].forEach((modal) => {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) setModalOpen(modal, false);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
- 덱 복원은 `imgSrc` 기준으로 정확 매핑하고, 기존 구데이터 호환을 위해서만 `number`를 보조 fallback으로 유지
|
||||
- `cardList.html`, `result.html` 카드 렌더링에서 이미지 로드 실패 시 공통 폴백(`/images/uapr/comingsoon.png`)을 사용
|
||||
- 카드 목록 확대 미리보기(오버레이)도 동일 폴백을 적용해 이미지 누락 시 깨짐 대신 뒷면 노출
|
||||
- 관리자 덱 카드에 `불러오기` 버튼과 길게 눌러 썸네일 확대 미리보기를 복구하고, 체크박스 다중 선택 기능과 충돌하지 않도록 이벤트를 분리
|
||||
- 관리자 회원 수정에서 별도 탈퇴 버튼을 제거하고 `역할 변경하기` 선택지에 `회원 탈퇴 처리`를 통합, 저장 시 2단계 확인 후 탈퇴 실행으로 변경
|
||||
|
||||
## 2026-06-25 / v4.0.16 — 회원 탈퇴·관리자 덱 일괄 삭제
|
||||
|
||||
|
||||
Reference in New Issue
Block a user