feat(tkuser): 부서 마스터 + 개인 추가 부여 권한 시스템 구현

부서 권한을 바닥(마스터)으로 설정하고 개인은 추가 부여만 가능하도록 변경.
부서 허용 항목은 개인 페이지에서 잠금(해제 불가) 표시되며,
부서 이동 시 기존 개인 권한이 자동 초기화됨.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-16 11:49:25 +09:00
parent f711a721ec
commit 4108a6e64a
5 changed files with 130 additions and 39 deletions

View File

@@ -104,7 +104,7 @@ function loadPermissionsTab() {
}
/* ===== Users State ===== */
let users = [], selectedUserId = null, currentPermissions = {}, currentPermSources = {};
let users = [], selectedUserId = null, currentPermissions = {}, currentPermSources = {}, currentDeptGranted = {};
/* ===== Users CRUD ===== */
async function loadUsers() {
@@ -219,14 +219,16 @@ document.getElementById('permissionUserSelect').addEventListener('change', async
async function loadUserPermissions(userId) {
currentPermissions = {};
currentPermSources = {};
currentDeptGranted = {};
const allDefs = { ...SYSTEM1_PAGES, ...SYSTEM3_PAGES, ...TKPURCHASE_PAGES, ...TKSAFETY_PAGES, ...TKUSER_PAGES };
Object.values(allDefs).flat().forEach(p => { currentPermissions[p.key] = p.def; currentPermSources[p.key] = 'default'; });
Object.values(allDefs).flat().forEach(p => { currentPermissions[p.key] = p.def; currentPermSources[p.key] = 'default'; currentDeptGranted[p.key] = false; });
try {
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;
currentDeptGranted[pageName] = !!info.dept_granted;
}
}
} catch(e) { console.warn('권한 로드 실패:', e); }
@@ -270,6 +272,17 @@ function renderSystemPerms(containerId, pageDef, color) {
${pages.map(p => {
const checked = currentPermissions[p.key] || false;
const src = currentPermSources[p.key] || 'default';
const deptLocked = currentDeptGranted[p.key] || false;
if (deptLocked) {
return `
<label class="perm-item flex items-center gap-2.5 p-2.5 border rounded-lg opacity-75 cursor-not-allowed checked" data-group="${groupId}">
<input type="checkbox" id="perm_${p.key}" checked disabled class="h-4 w-4 text-${color}-500 rounded border-gray-300">
<i class="fas fa-lock text-xs text-gray-400"></i>
<i class="fas ${p.icon} text-sm text-${color}-500" data-color="${color}"></i>
<span class="text-sm text-gray-700">${p.title}</span>
<span class="ml-auto text-[10px] px-1.5 py-0.5 rounded bg-emerald-50 text-emerald-500"><i class="fas fa-lock text-[8px] mr-0.5"></i>부서</span>
</label>`;
}
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"
@@ -308,7 +321,7 @@ function toggleGroup(groupId) {
}
function toggleGroupAll(groupId, checked) {
document.querySelectorAll(`#${groupId} input[type="checkbox"]`).forEach(cb => {
document.querySelectorAll(`#${groupId} input[type="checkbox"]:not(:disabled)`).forEach(cb => {
cb.checked = checked;
onPermChange(cb);
});
@@ -317,7 +330,7 @@ function toggleGroupAll(groupId, checked) {
function toggleSystemAll(prefix, checked) {
const containerMap = { s1: 's1-perms', s3: 's3-perms', tkpurchase: 'tkpurchase-perms' };
const containerId = containerMap[prefix] || prefix + '-perms';
document.querySelectorAll(`#${containerId} input[type="checkbox"]`).forEach(cb => {
document.querySelectorAll(`#${containerId} input[type="checkbox"]:not(:disabled)`).forEach(cb => {
cb.checked = checked;
onPermChange(cb);
});
@@ -334,10 +347,12 @@ document.getElementById('savePermissionsBtn').addEventListener('click', async ()
try {
const allPages = [...Object.values(SYSTEM1_PAGES).flat(), ...Object.values(SYSTEM3_PAGES).flat(), ...Object.values(TKPURCHASE_PAGES).flat(), ...Object.values(TKSAFETY_PAGES).flat(), ...Object.values(TKUSER_PAGES).flat()];
const permissions = allPages.map(p => {
const cb = document.getElementById('perm_' + p.key);
return { page_name: p.key, can_access: cb ? cb.checked : false };
});
const permissions = allPages
.filter(p => !currentDeptGranted[p.key])
.map(p => {
const cb = document.getElementById('perm_' + p.key);
return { page_name: p.key, can_access: cb ? cb.checked : false };
});
await api('/permissions/bulk-grant', { method:'POST', body: JSON.stringify({ user_id: parseInt(selectedUserId), permissions }) });
st.textContent = '저장 완료'; st.className = 'text-sm text-emerald-600';
showToast('권한이 저장되었습니다.');