|
|
|
|
@@ -56,18 +56,38 @@ const SYSTEM3_PAGES = {
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ===== Permissions Tab State ===== */
|
|
|
|
|
let permissionsTabLoaded = false;
|
|
|
|
|
|
|
|
|
|
function loadPermissionsTab() {
|
|
|
|
|
populateDeptPermSelect();
|
|
|
|
|
updatePermissionUserSelect();
|
|
|
|
|
permissionsTabLoaded = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ===== Users State ===== */
|
|
|
|
|
let users = [], selectedUserId = null, currentPermissions = {};
|
|
|
|
|
let users = [], selectedUserId = null, currentPermissions = {}, currentPermSources = {};
|
|
|
|
|
|
|
|
|
|
/* ===== Users CRUD ===== */
|
|
|
|
|
async function loadUsers() {
|
|
|
|
|
try {
|
|
|
|
|
const r = await api('/users'); users = r.data || r;
|
|
|
|
|
displayUsers(); updatePermissionUserSelect();
|
|
|
|
|
populateUserDeptSelects();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
document.getElementById('userList').innerHTML = `<div class="text-red-500 text-center py-6"><i class="fas fa-exclamation-triangle text-xl"></i><p class="text-sm mt-2">${err.message}</p></div>`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function populateUserDeptSelects() {
|
|
|
|
|
['newDepartmentId','editDepartmentId'].forEach(id => {
|
|
|
|
|
const sel = document.getElementById(id); if (!sel) return;
|
|
|
|
|
const val = sel.value;
|
|
|
|
|
sel.innerHTML = '<option value="">선택</option>';
|
|
|
|
|
departmentsCache.forEach(d => { const o = document.createElement('option'); o.value = d.department_id; o.textContent = d.department_name; sel.appendChild(o); });
|
|
|
|
|
sel.value = val;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
function displayUsers() {
|
|
|
|
|
const c = document.getElementById('userList');
|
|
|
|
|
if (!users.length) { c.innerHTML = '<p class="text-gray-400 text-center py-4 text-sm">등록된 사용자가 없습니다.</p>'; return; }
|
|
|
|
|
@@ -77,7 +97,7 @@ function displayUsers() {
|
|
|
|
|
<div class="text-sm font-medium text-gray-800 truncate"><i class="fas fa-user mr-1.5 text-gray-400 text-xs"></i>${u.name||u.username}</div>
|
|
|
|
|
<div class="text-xs text-gray-500 flex items-center gap-1.5 mt-0.5 flex-wrap">
|
|
|
|
|
<span>${u.username}</span>
|
|
|
|
|
${u.department?`<span class="px-1.5 py-0.5 rounded bg-green-50 text-green-600">${deptLabel(u.department)}</span>`:''}
|
|
|
|
|
${u.department||u.department_id?`<span class="px-1.5 py-0.5 rounded bg-green-50 text-green-600">${deptLabel(u.department, u.department_id)}</span>`:''}
|
|
|
|
|
<span class="px-1.5 py-0.5 rounded ${u.role==='admin'?'bg-red-50 text-red-600':'bg-slate-50 text-slate-500'}">${u.role==='admin'?'관리자':'사용자'}</span>
|
|
|
|
|
${u.is_active===0||u.is_active===false?'<span class="px-1.5 py-0.5 rounded bg-gray-100 text-gray-400">비활성</span>':''}
|
|
|
|
|
</div>
|
|
|
|
|
@@ -92,8 +112,9 @@ function displayUsers() {
|
|
|
|
|
|
|
|
|
|
document.getElementById('addUserForm').addEventListener('submit', async e => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const deptIdVal = document.getElementById('newDepartmentId').value;
|
|
|
|
|
try {
|
|
|
|
|
await api('/users', { method:'POST', body: JSON.stringify({ username: document.getElementById('newUsername').value.trim(), name: document.getElementById('newFullName').value.trim(), password: document.getElementById('newPassword').value, department: document.getElementById('newDepartment').value||null, role: document.getElementById('newRole').value }) });
|
|
|
|
|
await api('/users', { method:'POST', body: JSON.stringify({ username: document.getElementById('newUsername').value.trim(), name: document.getElementById('newFullName').value.trim(), password: document.getElementById('newPassword').value, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('newRole').value }) });
|
|
|
|
|
showToast('사용자가 추가되었습니다.'); document.getElementById('addUserForm').reset(); await loadUsers();
|
|
|
|
|
} catch(e) { showToast(e.message,'error'); }
|
|
|
|
|
});
|
|
|
|
|
@@ -101,15 +122,19 @@ document.getElementById('addUserForm').addEventListener('submit', async e => {
|
|
|
|
|
function editUser(id) {
|
|
|
|
|
const u = users.find(x=>x.user_id===id); if(!u) return;
|
|
|
|
|
document.getElementById('editUserId').value=u.user_id; document.getElementById('editUsername').value=u.username;
|
|
|
|
|
document.getElementById('editFullName').value=u.name||''; document.getElementById('editDepartment').value=u.department||''; document.getElementById('editRole').value=u.role;
|
|
|
|
|
document.getElementById('editFullName').value=u.name||'';
|
|
|
|
|
populateUserDeptSelects();
|
|
|
|
|
document.getElementById('editDepartmentId').value=u.department_id||'';
|
|
|
|
|
document.getElementById('editRole').value=u.role;
|
|
|
|
|
document.getElementById('editUserModal').classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
function closeEditModal() { document.getElementById('editUserModal').classList.add('hidden'); }
|
|
|
|
|
|
|
|
|
|
document.getElementById('editUserForm').addEventListener('submit', async e => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const deptIdVal = document.getElementById('editDepartmentId').value;
|
|
|
|
|
try {
|
|
|
|
|
await api(`/users/${document.getElementById('editUserId').value}`, { method:'PUT', body: JSON.stringify({ name: document.getElementById('editFullName').value.trim()||null, department: document.getElementById('editDepartment').value||null, role: document.getElementById('editRole').value }) });
|
|
|
|
|
await api(`/users/${document.getElementById('editUserId').value}`, { method:'PUT', body: JSON.stringify({ name: document.getElementById('editFullName').value.trim()||null, department_id: deptIdVal ? parseInt(deptIdVal) : null, role: document.getElementById('editRole').value }) });
|
|
|
|
|
showToast('수정되었습니다.'); closeEditModal(); await loadUsers();
|
|
|
|
|
} catch(e) { showToast(e.message,'error'); }
|
|
|
|
|
});
|
|
|
|
|
@@ -154,13 +179,18 @@ document.getElementById('permissionUserSelect').addEventListener('change', async
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function loadUserPermissions(userId) {
|
|
|
|
|
// 기본값 세팅
|
|
|
|
|
currentPermissions = {};
|
|
|
|
|
currentPermSources = {};
|
|
|
|
|
const allDefs = { ...SYSTEM1_PAGES, ...SYSTEM3_PAGES };
|
|
|
|
|
Object.values(allDefs).flat().forEach(p => { currentPermissions[p.key] = p.def; });
|
|
|
|
|
Object.values(allDefs).flat().forEach(p => { currentPermissions[p.key] = p.def; currentPermSources[p.key] = 'default'; });
|
|
|
|
|
try {
|
|
|
|
|
const perms = await api(`/users/${userId}/page-permissions`);
|
|
|
|
|
(Array.isArray(perms)?perms:[]).forEach(p => { currentPermissions[p.page_name] = !!p.can_access; });
|
|
|
|
|
const result = await api(`/permissions/users/${userId}/effective-permissions`);
|
|
|
|
|
if (result.permissions) {
|
|
|
|
|
for (const [pageName, info] of Object.entries(result.permissions)) {
|
|
|
|
|
currentPermissions[pageName] = info.can_access;
|
|
|
|
|
currentPermSources[pageName] = info.source;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch(e) { console.warn('권한 로드 실패:', e); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -169,6 +199,12 @@ function renderPermissionGrid() {
|
|
|
|
|
renderSystemPerms('s3-perms', SYSTEM3_PAGES, 'purple');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sourceLabel(src) {
|
|
|
|
|
if (src === 'explicit') return '<span class="ml-auto text-[10px] px-1.5 py-0.5 rounded bg-blue-50 text-blue-500">개인</span>';
|
|
|
|
|
if (src === 'department') return '<span class="ml-auto text-[10px] px-1.5 py-0.5 rounded bg-gray-100 text-gray-500">부서</span>';
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderSystemPerms(containerId, pageDef, color) {
|
|
|
|
|
const container = document.getElementById(containerId);
|
|
|
|
|
let html = '';
|
|
|
|
|
@@ -192,12 +228,14 @@ function renderSystemPerms(containerId, pageDef, color) {
|
|
|
|
|
<div id="${groupId}" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
|
|
|
|
|
${pages.map(p => {
|
|
|
|
|
const checked = currentPermissions[p.key] || false;
|
|
|
|
|
const src = currentPermSources[p.key] || 'default';
|
|
|
|
|
return `
|
|
|
|
|
<label class="perm-item flex items-center gap-2.5 p-2.5 border rounded-lg cursor-pointer ${checked?'checked':'border-gray-200'}" data-group="${groupId}">
|
|
|
|
|
<input type="checkbox" id="perm_${p.key}" ${checked?'checked':''} class="h-4 w-4 text-${color}-500 rounded border-gray-300 focus:ring-${color}-400"
|
|
|
|
|
onchange="onPermChange(this)">
|
|
|
|
|
<i class="fas ${p.icon} text-sm ${checked?`text-${color}-500`:'text-gray-400'}" data-color="${color}"></i>
|
|
|
|
|
<span class="text-sm text-gray-700">${p.title}</span>
|
|
|
|
|
${sourceLabel(src)}
|
|
|
|
|
</label>`;
|
|
|
|
|
}).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
@@ -268,6 +306,157 @@ document.getElementById('savePermissionsBtn').addEventListener('click', async ()
|
|
|
|
|
} finally { btn.disabled = false; btn.innerHTML = '<i class="fas fa-save mr-2"></i>권한 저장'; }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/* ===== Department Permissions ===== */
|
|
|
|
|
let selectedDeptId = null, deptPermissions = {};
|
|
|
|
|
|
|
|
|
|
function populateDeptPermSelect() {
|
|
|
|
|
const sel = document.getElementById('deptPermSelect'); if (!sel) return;
|
|
|
|
|
sel.innerHTML = '<option value="">부서 선택</option>';
|
|
|
|
|
departmentsCache.forEach(d => { const o = document.createElement('option'); o.value = d.department_id; o.textContent = d.department_name; sel.appendChild(o); });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
const sel = document.getElementById('deptPermSelect');
|
|
|
|
|
if (sel) sel.addEventListener('change', async e => {
|
|
|
|
|
selectedDeptId = e.target.value;
|
|
|
|
|
if (selectedDeptId) {
|
|
|
|
|
await loadDeptPermissions(selectedDeptId);
|
|
|
|
|
renderDeptPermissionGrid();
|
|
|
|
|
document.getElementById('deptPermPanel').classList.remove('hidden');
|
|
|
|
|
document.getElementById('deptPermEmpty').classList.add('hidden');
|
|
|
|
|
} else {
|
|
|
|
|
document.getElementById('deptPermPanel').classList.add('hidden');
|
|
|
|
|
document.getElementById('deptPermEmpty').classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const saveBtn = document.getElementById('saveDeptPermBtn');
|
|
|
|
|
if (saveBtn) saveBtn.addEventListener('click', saveDeptPermissions);
|
|
|
|
|
|
|
|
|
|
const resetBtn = document.getElementById('resetToDefaultBtn');
|
|
|
|
|
if (resetBtn) resetBtn.addEventListener('click', resetUserPermToDefault);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
async function loadDeptPermissions(deptId) {
|
|
|
|
|
deptPermissions = {};
|
|
|
|
|
const allDefs = { ...SYSTEM1_PAGES, ...SYSTEM3_PAGES };
|
|
|
|
|
Object.values(allDefs).flat().forEach(p => { deptPermissions[p.key] = p.def; });
|
|
|
|
|
try {
|
|
|
|
|
const result = await api(`/permissions/departments/${deptId}/permissions`);
|
|
|
|
|
(result.data || []).forEach(p => { deptPermissions[p.page_name] = !!p.can_access; });
|
|
|
|
|
} catch(e) { console.warn('부서 권한 로드 실패:', e); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDeptPermissionGrid() {
|
|
|
|
|
renderDeptSystemPerms('dept-s1-perms', SYSTEM1_PAGES, 'blue');
|
|
|
|
|
renderDeptSystemPerms('dept-s3-perms', SYSTEM3_PAGES, 'purple');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDeptSystemPerms(containerId, pageDef, color) {
|
|
|
|
|
const container = document.getElementById(containerId);
|
|
|
|
|
if (!container) return;
|
|
|
|
|
let html = '';
|
|
|
|
|
Object.entries(pageDef).forEach(([groupName, pages]) => {
|
|
|
|
|
const groupId = containerId + '-' + groupName.replace(/\s/g,'');
|
|
|
|
|
const allChecked = pages.every(p => deptPermissions[p.key]);
|
|
|
|
|
html += `
|
|
|
|
|
<div>
|
|
|
|
|
<div class="group-header flex items-center justify-between py-2 px-1 rounded" onclick="toggleGroup('${groupId}')">
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<i class="fas fa-chevron-down text-xs text-gray-400 transition-transform" id="arrow-${groupId}"></i>
|
|
|
|
|
<span class="text-xs font-semibold text-gray-600 uppercase tracking-wide">${groupName}</span>
|
|
|
|
|
<span class="text-xs text-gray-400">${pages.length}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<label class="flex items-center gap-1.5 text-xs text-gray-500 cursor-pointer" onclick="event.stopPropagation()">
|
|
|
|
|
<input type="checkbox" ${allChecked?'checked':''} onchange="toggleDeptGroupAll('${groupId}', this.checked)"
|
|
|
|
|
class="h-3.5 w-3.5 text-${color}-500 rounded border-gray-300">
|
|
|
|
|
<span>전체</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="${groupId}" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
|
|
|
|
|
${pages.map(p => {
|
|
|
|
|
const checked = deptPermissions[p.key] || false;
|
|
|
|
|
return `
|
|
|
|
|
<label class="perm-item flex items-center gap-2.5 p-2.5 border rounded-lg cursor-pointer ${checked?'checked':'border-gray-200'}" data-group="${groupId}">
|
|
|
|
|
<input type="checkbox" id="dperm_${p.key}" ${checked?'checked':''} class="h-4 w-4 text-${color}-500 rounded border-gray-300 focus:ring-${color}-400"
|
|
|
|
|
onchange="onDeptPermChange(this)">
|
|
|
|
|
<i class="fas ${p.icon} text-sm ${checked?`text-${color}-500`:'text-gray-400'}" data-color="${color}"></i>
|
|
|
|
|
<span class="text-sm text-gray-700">${p.title}</span>
|
|
|
|
|
</label>`;
|
|
|
|
|
}).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
});
|
|
|
|
|
container.innerHTML = html;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onDeptPermChange(cb) {
|
|
|
|
|
const item = cb.closest('.perm-item');
|
|
|
|
|
const icon = item.querySelector('i[data-color]');
|
|
|
|
|
const color = icon.dataset.color;
|
|
|
|
|
item.classList.toggle('checked', cb.checked);
|
|
|
|
|
icon.classList.toggle(`text-${color}-500`, cb.checked);
|
|
|
|
|
icon.classList.toggle('text-gray-400', !cb.checked);
|
|
|
|
|
const group = item.dataset.group;
|
|
|
|
|
const groupCbs = document.querySelectorAll(`[data-group="${group}"] input[type="checkbox"]`);
|
|
|
|
|
const allChecked = [...groupCbs].every(c => c.checked);
|
|
|
|
|
const groupHeader = document.getElementById(group)?.previousElementSibling;
|
|
|
|
|
if (groupHeader) { const gc = groupHeader.querySelector('input[type="checkbox"]'); if(gc) gc.checked = allChecked; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleDeptGroupAll(groupId, checked) {
|
|
|
|
|
document.querySelectorAll(`#${groupId} input[type="checkbox"]`).forEach(cb => {
|
|
|
|
|
cb.checked = checked;
|
|
|
|
|
onDeptPermChange(cb);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleDeptSystemAll(prefix, checked) {
|
|
|
|
|
const containerId = prefix === 's1' ? 'dept-s1-perms' : 'dept-s3-perms';
|
|
|
|
|
document.querySelectorAll(`#${containerId} input[type="checkbox"]`).forEach(cb => {
|
|
|
|
|
cb.checked = checked;
|
|
|
|
|
onDeptPermChange(cb);
|
|
|
|
|
});
|
|
|
|
|
document.querySelectorAll(`#${containerId} .group-header input[type="checkbox"]`).forEach(cb => cb.checked = checked);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveDeptPermissions() {
|
|
|
|
|
if (!selectedDeptId) return;
|
|
|
|
|
const btn = document.getElementById('saveDeptPermBtn');
|
|
|
|
|
const st = document.getElementById('deptPermSaveStatus');
|
|
|
|
|
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>저장 중...';
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const allPages = [...Object.values(SYSTEM1_PAGES).flat(), ...Object.values(SYSTEM3_PAGES).flat()];
|
|
|
|
|
const permissions = allPages.map(p => {
|
|
|
|
|
const cb = document.getElementById('dperm_' + p.key);
|
|
|
|
|
return { page_name: p.key, can_access: cb ? cb.checked : false };
|
|
|
|
|
});
|
|
|
|
|
await api(`/permissions/departments/${selectedDeptId}/bulk-set`, { method:'POST', body: JSON.stringify({ permissions }) });
|
|
|
|
|
st.textContent = '저장 완료'; st.className = 'text-sm text-emerald-600';
|
|
|
|
|
showToast('부서 권한이 저장되었습니다.');
|
|
|
|
|
setTimeout(() => { st.textContent = ''; }, 3000);
|
|
|
|
|
} catch(e) {
|
|
|
|
|
st.textContent = e.message; st.className = 'text-sm text-red-500';
|
|
|
|
|
showToast('저장 실패: ' + e.message, 'error');
|
|
|
|
|
} finally { btn.disabled = false; btn.innerHTML = '<i class="fas fa-save mr-2"></i>부서 권한 저장'; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resetUserPermToDefault() {
|
|
|
|
|
if (!selectedUserId) return;
|
|
|
|
|
if (!confirm('이 사용자의 개인 권한을 모두 삭제하고 부서 기본 권한으로 되돌리시겠습니까?')) return;
|
|
|
|
|
try {
|
|
|
|
|
const perms = await api(`/users/${selectedUserId}/page-permissions`);
|
|
|
|
|
const permArr = Array.isArray(perms) ? perms : [];
|
|
|
|
|
for (const p of permArr) {
|
|
|
|
|
await api(`/permissions/${p.id}`, { method: 'DELETE' });
|
|
|
|
|
}
|
|
|
|
|
showToast('부서 기본 권한으로 초기화되었습니다.');
|
|
|
|
|
await loadUserPermissions(selectedUserId);
|
|
|
|
|
renderPermissionGrid();
|
|
|
|
|
} catch(e) { showToast('초기화 실패: ' + e.message, 'error'); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ===== Workers CRUD ===== */
|
|
|
|
|
let workers = [], workersLoaded = false, departmentsForSelect = [];
|
|
|
|
|
|
|
|
|
|
|