Files
sam-manage/public/js/context-menu.js
hskwon c383494d84 refactor: 브라우저 alert를 showToast로 변경
- user-modal.js: 삭제/복원/비밀번호 초기화 알림 개선
- context-menu.js: 테넌트 전환/사용자 삭제 알림 개선
- 시스템 일관성을 위해 SweetAlert2 토스트 사용
2025-12-17 15:44:31 +09:00

220 lines
6.9 KiB
JavaScript

/**
* 컨텍스트 메뉴 (클릭 메뉴)
* 테넌트명, 사용자명 등에서 클릭 시 해당 위치에 메뉴 표시
*/
class ContextMenu {
constructor() {
this.menuElement = null;
this.currentTarget = null;
this.init();
}
init() {
// 컨텍스트 메뉴 요소 찾기
this.menuElement = document.getElementById('context-menu');
if (!this.menuElement) return;
// 좌클릭 이벤트 등록 (캡처링 - 다른 핸들러보다 먼저 실행)
document.addEventListener('click', (e) => this.handleClick(e), true);
// ESC 키로 메뉴 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.hide();
});
// 스크롤 시 메뉴 닫기
document.addEventListener('scroll', () => this.hide(), true);
}
handleClick(e) {
const trigger = e.target.closest('[data-context-menu]');
// 메뉴 영역 내 클릭은 무시 (메뉴 항목 클릭 허용)
if (e.target.closest('#context-menu')) return;
// 트리거가 아니면 메뉴 숨기기
if (!trigger) {
this.hide();
return;
}
e.preventDefault();
e.stopPropagation();
const menuType = trigger.dataset.contextMenu;
const entityId = trigger.dataset.entityId;
const entityName = trigger.dataset.entityName;
this.currentTarget = {
type: menuType,
id: entityId,
name: entityName,
element: trigger
};
this.show(e.clientX, e.clientY, menuType);
}
show(x, y, menuType) {
if (!this.menuElement) return;
// 메뉴 타입에 따라 항목 표시/숨김
this.updateMenuItems(menuType);
// 메뉴 표시
this.menuElement.classList.remove('hidden');
// 위치 조정 (화면 밖으로 나가지 않도록)
const menuRect = this.menuElement.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let posX = x;
let posY = y;
if (x + menuRect.width > viewportWidth) {
posX = viewportWidth - menuRect.width - 10;
}
if (y + menuRect.height > viewportHeight) {
posY = viewportHeight - menuRect.height - 10;
}
this.menuElement.style.left = `${posX}px`;
this.menuElement.style.top = `${posY}px`;
}
hide() {
if (this.menuElement) {
this.menuElement.classList.add('hidden');
}
this.currentTarget = null;
}
updateMenuItems(menuType) {
// 모든 메뉴 항목 숨기기
const allItems = this.menuElement.querySelectorAll('[data-menu-for]');
allItems.forEach(item => {
const forTypes = item.dataset.menuFor.split(',');
if (forTypes.includes(menuType) || forTypes.includes('all')) {
item.classList.remove('hidden');
} else {
item.classList.add('hidden');
}
});
}
// 메뉴 항목 클릭 핸들러
handleMenuAction(action) {
if (!this.currentTarget) return;
const { type, id, name } = this.currentTarget;
switch (action) {
case 'view-tenant':
if (typeof TenantModal !== 'undefined') {
TenantModal.open(id);
}
break;
case 'edit-tenant':
window.location.href = `/tenants/${id}/edit`;
break;
case 'switch-tenant':
this.switchTenant(id, name);
break;
case 'view-user':
if (typeof UserModal !== 'undefined') {
UserModal.open(id);
}
break;
case 'edit-user':
window.location.href = `/users/${id}/edit`;
break;
case 'delete-user':
if (typeof UserModal !== 'undefined') {
// 모달 열고 삭제 실행
UserModal.currentUserId = id;
UserModal.deleteUser();
} else {
// UserModal이 없으면 직접 삭제 확인
if (confirm(`"${name}"을(를) 삭제하시겠습니까?`)) {
this.deleteUser(id);
}
}
break;
default:
console.log('Unknown action:', action, { type, id, name });
}
this.hide();
}
// 테넌트 전환
async switchTenant(tenantId, tenantName) {
try {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/tenant/switch';
// CSRF 토큰
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_token';
csrfInput.value = csrfToken;
form.appendChild(csrfInput);
// 테넌트 ID
const tenantInput = document.createElement('input');
tenantInput.type = 'hidden';
tenantInput.name = 'tenant_id';
tenantInput.value = tenantId;
form.appendChild(tenantInput);
document.body.appendChild(form);
form.submit();
} catch (error) {
console.error('Failed to switch tenant:', error);
showToast('테넌트 전환에 실패했습니다.', 'error');
}
}
// 사용자 삭제 (fallback)
async deleteUser(userId) {
try {
const response = await fetch(`/api/admin/users/${userId}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Accept': 'application/json'
}
});
const data = await response.json();
if (data.success) {
showToast(data.message || '사용자가 삭제되었습니다.', 'success');
// 테이블 새로고침
if (typeof htmx !== 'undefined') {
htmx.trigger('#user-table', 'filterSubmit');
} else {
window.location.reload();
}
} else {
showToast(data.message || '삭제에 실패했습니다.', 'error');
}
} catch (error) {
console.error('Failed to delete user:', error);
showToast('삭제에 실패했습니다.', 'error');
}
}
}
// 전역 인스턴스 생성
const contextMenu = new ContextMenu();
// 전역 함수로 노출 (onclick에서 사용)
function handleContextMenuAction(action) {
contextMenu.handleMenuAction(action);
}