사용자 모달 기능: - 사용자 정보 모달 팝업 (조회/삭제/수정) - 권한 요약 정보 (Web/API 권한 카운트) - 2x2 그리드 레이아웃 (테넌트, 역할, 부서, 권한) - 테이블 행 클릭으로 모달 열기 - 권한 관리 링크 클릭 시 해당 사용자 자동 선택 컨텍스트 메뉴 확장: - permission-analyze 페이지 사용자 이름에 컨텍스트 메뉴 - user-permissions 페이지 사용자 버튼에 컨텍스트 메뉴 - 사용자 모달 내 테넌트 칩에 컨텍스트 메뉴 - 헤더 테넌트 배지에 컨텍스트 메뉴 - 테넌트 메뉴에 "이 테넌트로 전환" 기능 추가
213 lines
6.6 KiB
JavaScript
213 lines
6.6 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('contextmenu', (e) => this.handleContextMenu(e));
|
|
|
|
// 클릭 시 메뉴 닫기
|
|
document.addEventListener('click', () => this.hide());
|
|
|
|
// ESC 키로 메뉴 닫기
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') this.hide();
|
|
});
|
|
|
|
// 스크롤 시 메뉴 닫기
|
|
document.addEventListener('scroll', () => this.hide(), true);
|
|
}
|
|
|
|
handleContextMenu(e) {
|
|
const trigger = e.target.closest('[data-context-menu]');
|
|
if (!trigger) return;
|
|
|
|
e.preventDefault();
|
|
|
|
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);
|
|
alert('테넌트 전환에 실패했습니다.');
|
|
}
|
|
}
|
|
|
|
// 사용자 삭제 (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) {
|
|
// 테이블 새로고침
|
|
if (typeof htmx !== 'undefined') {
|
|
htmx.trigger('#user-table', 'filterSubmit');
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
} else {
|
|
alert(data.message || '삭제에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to delete user:', error);
|
|
alert('삭제에 실패했습니다.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 전역 인스턴스 생성
|
|
const contextMenu = new ContextMenu();
|
|
|
|
// 전역 함수로 노출 (onclick에서 사용)
|
|
function handleContextMenuAction(action) {
|
|
contextMenu.handleMenuAction(action);
|
|
} |