폴더단위 권한 제어 기능 추가
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
<!--
|
||||
* [변경 이력 (Auto-Generated by AI)]
|
||||
* - 수정일시: 2026-06-15 11:40:00
|
||||
* - 수정원인: 폴더별 권한 관리 트리 노드의 data_permission 기준 정렬, 수치 표시 및 트리 뎁스 꼬임 오류 조치
|
||||
* - 수정내용:
|
||||
* 1) renderFolderTree 함수에서 sibling 노드 간 data_permission 우선순위(1->4->8->0) 및 이름으로 정렬하고 우측 수치 렌더링.
|
||||
* 2) folders 트리 구성 전 data_depth 오름차순으로 사전 정렬하여 부모-자식 노드 구성 순서 오류 핫픽스 적용.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
@@ -631,11 +639,12 @@
|
||||
<div class="menu-category">
|
||||
<div class="menu-category-title">사용자 및 권한</div>
|
||||
<a class="menu-item" id="menu-user-mgmt" onclick="switchTab('user-mgmt')">👥 사용자 관리</a>
|
||||
<a class="menu-item" id="menu-folder-permission" onclick="switchTab('folder-permission')">📂 폴더별 권한 관리</a>
|
||||
</div>
|
||||
|
||||
<div class="menu-category">
|
||||
<div class="menu-category-title">시스템 감사 및 환경</div>
|
||||
<a class="menu-item" id="menu-audit-logs" onclick="switchTab('audit-logs')">🔎 감사 로그 조회</a>
|
||||
<a class="menu-item" id="menu-audit-logs" onclick="switchTab('audit-logs')">🔎 활동 로그 조회</a>
|
||||
<a class="menu-item" id="menu-delete-policy" onclick="switchTab('delete-policy')">⚙️ 보관 및 삭제 정책 설정</a>
|
||||
<a class="menu-item" id="menu-code-mgmt" onclick="switchTab('code-mgmt')">🔑 공통 코드 관리</a>
|
||||
</div>
|
||||
@@ -940,21 +949,65 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= TAB: FOLDER LEVEL PERMISSIONS ================= -->
|
||||
<div id="tab-folder-permission" class="tab-content">
|
||||
<div class="grid-2col">
|
||||
<!-- Left Side: Project Selector & Folder Tree -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">📂 폴더 구조 (1~3단계 제한)</h3>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<label style="font-weight: 600; min-width: 80px;">프로젝트:</label>
|
||||
<select class="select-input" id="folder-perm-project-select" onchange="loadFolderStructure()" style="flex: 1; padding: 8px; border: 1px solid var(--border); border-radius: var(--radius-md);"></select>
|
||||
</div>
|
||||
<div id="folder-tree-container" class="table-wrapper" style="max-height: 500px; overflow-y: auto; padding: 10px; border: 1px solid var(--border); border-radius: var(--radius-md); background: #fafafa; min-height: 300px;">
|
||||
<div style="color: var(--text-light); text-align: center; padding: 40px 0;">프로젝트를 선택하시면 폴더 트리가 여기에 로드됩니다.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Side: Folder Specific User Permissions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title" id="folder-perm-detail-title">👥 폴더별 사용자 권한 설정</h3>
|
||||
<p style="margin: 4px 0 0 0; font-size: 0.8rem; color: var(--text-muted);" id="folder-perm-detail-desc">좌측 트리에서 폴더를 선택해 주세요.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrapper" style="max-height: 520px; overflow-y: auto;">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>사용자 ID</th>
|
||||
<th>이름</th>
|
||||
<th>부서/직급</th>
|
||||
<th>프로젝트 기본권한</th>
|
||||
<th>폴더 권한 등급</th>
|
||||
<th>설정</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="folder-perm-user-body">
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; color: var(--text-light); padding: 40px 0;">폴더를 선택하시면 사용자의 개별 권한 지정 목록이 여기에 표시됩니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ================= TAB: AUDIT LOGS ================= -->
|
||||
<div id="tab-audit-logs" class="tab-content">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">🔎 시스템 민감 파일 감사 로그 조회 (tb_log)</h3>
|
||||
<h3 class="card-title">🔎 시스템 활동 로그 조회 (tb_log)</h3>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input type="text" class="text-input" id="search-log-user" placeholder="유저 ID 검색...">
|
||||
<select class="select-input" id="filter-log-action">
|
||||
<option value="">모든 조작 액션</option>
|
||||
<option value="delete">파일 삭제 (Delete)</option>
|
||||
<option value="move">파일 이동 (Move)</option>
|
||||
<option value="download">압축 다운로드 (Zip)</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" onclick="renderAuditLogs()">감사 로그 필터링</button>
|
||||
<input type="text" class="text-input" id="search-log-user" placeholder="사용자 ID 검색...">
|
||||
<input type="text" class="text-input" id="search-log-project" placeholder="프로젝트명 검색...">
|
||||
<input type="text" class="text-input" id="filter-log-action" placeholder="조작 액션 검색...">
|
||||
<button class="btn btn-secondary" onclick="renderAuditLogs()">활동 로그 필터링</button>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="admin-table">
|
||||
@@ -971,7 +1024,7 @@
|
||||
</thead>
|
||||
<tbody id="audit-log-body">
|
||||
<tr>
|
||||
<td colspan="7" style="text-align: center; color: var(--text-light); padding: 40px 0;">감사 로그 데이터를 조회 중입니다...</td>
|
||||
<td colspan="7" style="text-align: center; color: var(--text-light); padding: 40px 0;">활동 로그 데이터를 조회 중입니다...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1430,7 +1483,8 @@
|
||||
'project-mgmt': '🏗️ 프로젝트 관리',
|
||||
'banner-notice': '📢 실시간 배너 공지',
|
||||
'user-mgmt': '👥 사용자 관리',
|
||||
'audit-logs': '🔎 감사 로그 조회',
|
||||
'folder-permission': '📂 폴더별 권한 관리',
|
||||
'audit-logs': '🔎 활동 로그 조회',
|
||||
'delete-policy': '⚙️ 보관 및 삭제 정책 설정',
|
||||
'code-mgmt': '🔑 공통 코드 관리'
|
||||
};
|
||||
@@ -1441,6 +1495,7 @@
|
||||
else if (tabId === 'project-mgmt') renderProjects();
|
||||
else if (tabId === 'banner-notice') renderBanners();
|
||||
else if (tabId === 'user-mgmt') renderUsers();
|
||||
else if (tabId === 'folder-permission') initFolderPermissionTab();
|
||||
else if (tabId === 'audit-logs') renderAuditLogs();
|
||||
else if (tabId === 'delete-policy') renderDeletePolicy();
|
||||
else if (tabId === 'code-mgmt') renderCommonCodes();
|
||||
@@ -2127,6 +2182,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function submitCreateUser(event) {
|
||||
event.preventDefault();
|
||||
const payload = {
|
||||
user_id: document.getElementById('form-user-id').value.trim(),
|
||||
user_pw: document.getElementById('form-user-pw').value,
|
||||
user_nm: document.getElementById('form-user-nm').value.trim(),
|
||||
company: document.getElementById('form-user-company').value.trim(),
|
||||
dept: document.getElementById('form-user-dept').value.trim(),
|
||||
position: document.getElementById('form-user-position').value.trim(),
|
||||
group: document.getElementById('form-user-group').value,
|
||||
is_resigned: document.getElementById('form-user-resigned').value === 'true'
|
||||
};
|
||||
|
||||
try {
|
||||
await fetchAPI('/api/admin/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
alert('신규 사용자가 등록되었습니다.');
|
||||
closeUserModal();
|
||||
renderUsers();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function closeUserModal() {
|
||||
document.getElementById('userModalOverlay').style.display = 'none';
|
||||
}
|
||||
@@ -2176,14 +2258,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
// --- 7. 감사 로그 조회 탭 (Audit Logs) ---
|
||||
// --- 7. 활동 로그 조회 탭 (Activity Logs) ---
|
||||
async function renderAuditLogs() {
|
||||
const userId = document.getElementById('search-log-user').value.trim();
|
||||
const action = document.getElementById('filter-log-action').value;
|
||||
const projectNm = document.getElementById('search-log-project').value.trim();
|
||||
const action = document.getElementById('filter-log-action').value.trim();
|
||||
|
||||
let queryUrl = '/api/admin/audit-logs?';
|
||||
if (userId) queryUrl += `user_id=${userId}&`;
|
||||
if (action) queryUrl += `activity=${action}`;
|
||||
const params = new URLSearchParams();
|
||||
if (userId) params.append('user_id', userId);
|
||||
if (projectNm) params.append('project_nm', projectNm);
|
||||
if (action) params.append('activity', action);
|
||||
|
||||
let queryUrl = `/api/admin/audit-logs?${params.toString()}`;
|
||||
|
||||
try {
|
||||
const logs = await fetchAPI(queryUrl);
|
||||
@@ -2191,7 +2277,7 @@
|
||||
body.innerHTML = '';
|
||||
|
||||
if (logs.length === 0) {
|
||||
body.innerHTML = `<tr><td colspan="7" style="text-align: center; color: var(--text-light); padding: 24px 0;">조회된 감사 로그 내역이 없습니다.</td></tr>`;
|
||||
body.innerHTML = `<tr><td colspan="7" style="text-align: center; color: var(--text-light); padding: 24px 0;">조회된 활동 로그 내역이 없습니다.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2206,7 +2292,7 @@
|
||||
tr.innerHTML = `
|
||||
<td>${idx + 1}</td>
|
||||
<td>${log.clean_date ? log.clean_date.replace('T', ' ').substring(0, 19) : '-'}</td>
|
||||
<td><strong>${log.project_id || '-'}</strong></td>
|
||||
<td><strong>${log.project_nm || log.project_id || '-'}</strong></td>
|
||||
<td>${log.user_id || '-'}</td>
|
||||
<td>${log.user_ip || '-'}</td>
|
||||
<td><span class="badge ${log.clean_path?.includes('delete') ? 'danger' : 'active'}" style="background-color: ${log.clean_path?.includes('delete') ? '#fee2e2' : '#eef8ee'}; color: ${log.clean_path?.includes('delete') ? '#ef4444' : '#4db251'};">${log.clean_path || '-'}</span></td>
|
||||
@@ -2604,6 +2690,303 @@
|
||||
}
|
||||
}
|
||||
|
||||
// --- 9. 폴더별 권한 관리 탭 (Folder Permission) ---
|
||||
let selectedFolderPermProjectId = null;
|
||||
let selectedFolderPermPathKey = null;
|
||||
let folderPermDataCache = {
|
||||
folders: [],
|
||||
folderPermissions: [],
|
||||
users: []
|
||||
};
|
||||
|
||||
async function initFolderPermissionTab() {
|
||||
const selectEl = document.getElementById('folder-perm-project-select');
|
||||
selectEl.innerHTML = '<option value="">-- 프로젝트 선택 --</option>';
|
||||
|
||||
try {
|
||||
const projects = await fetchAPI('/api/admin/projects');
|
||||
projects.forEach(p => {
|
||||
const option = document.createElement('option');
|
||||
option.value = p.project_id;
|
||||
option.innerText = `[${p.project_id}] ${p.project_nm || p.task_nm_kr}`;
|
||||
selectEl.appendChild(option);
|
||||
});
|
||||
|
||||
selectedFolderPermProjectId = null;
|
||||
selectedFolderPermPathKey = null;
|
||||
document.getElementById('folder-tree-container').innerHTML = `
|
||||
<div style="color: var(--text-light); text-align: center; padding: 40px 0;">프로젝트를 선택하시면 폴더 트리가 여기에 로드됩니다.</div>
|
||||
`;
|
||||
resetFolderUserPermissionTable();
|
||||
} catch (err) {
|
||||
console.error("initFolderPermissionTab error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFolderStructure() {
|
||||
const selectEl = document.getElementById('folder-perm-project-select');
|
||||
const projectId = selectEl.value;
|
||||
selectedFolderPermProjectId = projectId;
|
||||
selectedFolderPermPathKey = null;
|
||||
resetFolderUserPermissionTable();
|
||||
|
||||
if (!projectId) {
|
||||
document.getElementById('folder-tree-container').innerHTML = `
|
||||
<div style="color: var(--text-light); text-align: center; padding: 40px 0;">프로젝트를 선택하시면 폴더 트리가 여기에 로드됩니다.</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('folder-tree-container').innerHTML = `
|
||||
<div style="color: var(--text-muted); text-align: center; padding: 40px 0;">폴더 데이터를 불러오는 중...</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
const data = await fetchAPI(`/api/admin/permissions/folders/${projectId}`);
|
||||
folderPermDataCache = data;
|
||||
renderFolderTree();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
document.getElementById('folder-tree-container').innerHTML = `
|
||||
<div style="color: red; text-align: center; padding: 40px 0;">데이터 로드 실패: ${err.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderFolderTree() {
|
||||
const container = document.getElementById('folder-tree-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
const folders = folderPermDataCache.folders || [];
|
||||
const folderPerms = folderPermDataCache.folderPermissions || [];
|
||||
|
||||
// 1. Build tree structure to preserve parent-child relation while sorting siblings
|
||||
const depth1Map = {};
|
||||
const depth2Map = {};
|
||||
const rootNodes = [];
|
||||
|
||||
// data_depth 기준 오름차순 정렬하여 부모 노드가 맵에 먼저 등록되도록 보장
|
||||
const sortedByDepthFolders = [...folders].sort((a, b) => Number(a.data_depth) - Number(b.data_depth));
|
||||
|
||||
sortedByDepthFolders.forEach(f => {
|
||||
const node = {
|
||||
folder: f,
|
||||
children: [],
|
||||
name: f.data_depth === 1 ? f.path1 : (f.data_depth === 2 ? f.path2 : f.path3),
|
||||
pathKey: f.data_depth === 1 ? f.path1 : (f.data_depth === 2 ? `${f.path1}/${f.path2}` : `${f.path1}/${f.path2}/${f.path3}`)
|
||||
};
|
||||
|
||||
if (f.data_depth === 1) {
|
||||
depth1Map[f.path1] = node;
|
||||
rootNodes.push(node);
|
||||
} else if (f.data_depth === 2) {
|
||||
depth2Map[`${f.path1}/${f.path2}`] = node;
|
||||
const parent = depth1Map[f.path1];
|
||||
if (parent) parent.children.push(node);
|
||||
else rootNodes.push(node);
|
||||
} else if (f.data_depth === 3) {
|
||||
const parent = depth2Map[`${f.path1}/${f.path2}`];
|
||||
if (parent) parent.children.push(node);
|
||||
else rootNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Sort siblings by data_permission (1 -> 4 -> 8 -> 0) then name
|
||||
function getSortWeight(perm) {
|
||||
const p = Number(perm);
|
||||
if (p === 1) return 1; // 상속폴더 (Viewer)
|
||||
if (p === 4) return 2; // 일반 (Worker)
|
||||
if (p === 8) return 3; // 보안 (Security)
|
||||
if (p === 0) return 4; // 관리 (Sub-Master)
|
||||
return 5;
|
||||
}
|
||||
|
||||
function sortNodes(nodeList) {
|
||||
nodeList.sort((a, b) => {
|
||||
const wa = getSortWeight(a.folder.data_permission);
|
||||
const wb = getSortWeight(b.folder.data_permission);
|
||||
if (wa !== wb) {
|
||||
return wa - wb;
|
||||
}
|
||||
return a.name.localeCompare(b.name, 'ko');
|
||||
});
|
||||
|
||||
nodeList.forEach(node => {
|
||||
if (node.children.length > 0) {
|
||||
sortNodes(node.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sortNodes(rootNodes);
|
||||
|
||||
// 3. Preorder traversal to flatten tree
|
||||
const sortedFolders = [];
|
||||
function traverse(nodeList) {
|
||||
nodeList.forEach(node => {
|
||||
sortedFolders.push(node.folder);
|
||||
if (node.children.length > 0) {
|
||||
traverse(node.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
traverse(rootNodes);
|
||||
|
||||
if (sortedFolders.length === 0) {
|
||||
container.innerHTML = `<div style="color: var(--text-muted); text-align: center; padding: 40px 0;">생성된 폴더가 없습니다.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const treeWrapper = document.createElement('div');
|
||||
treeWrapper.style.display = 'flex';
|
||||
treeWrapper.style.flexDirection = 'column';
|
||||
treeWrapper.style.gap = '4px';
|
||||
|
||||
sortedFolders.forEach(f => {
|
||||
let pathKey = f.path1;
|
||||
if (f.data_depth === 2) pathKey = `${f.path1}/${f.path2}`;
|
||||
else if (f.data_depth === 3) pathKey = `${f.path1}/${f.path2}/${f.path3}`;
|
||||
|
||||
const folderName = f.data_depth === 1 ? f.path1 : (f.data_depth === 2 ? f.path2 : f.path3);
|
||||
const isInherited = !folderPerms.some(p => p.folder_path_key === pathKey);
|
||||
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.style.display = 'flex';
|
||||
itemDiv.style.alignItems = 'center';
|
||||
itemDiv.style.padding = '8px 12px';
|
||||
itemDiv.style.borderRadius = 'var(--radius-md)';
|
||||
itemDiv.style.cursor = 'pointer';
|
||||
itemDiv.style.marginLeft = `${(f.data_depth - 1) * 20}px`;
|
||||
itemDiv.style.background = selectedFolderPermPathKey === pathKey ? 'var(--primary-soft)' : 'transparent';
|
||||
itemDiv.style.border = selectedFolderPermPathKey === pathKey ? '1px solid var(--border)' : '1px solid transparent';
|
||||
itemDiv.style.transition = 'var(--transition)';
|
||||
|
||||
itemDiv.innerHTML = `
|
||||
<span style="margin-right: 6px;">📂</span>
|
||||
<span style="font-size: 0.9rem; font-weight: 500; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${folderName}</span>
|
||||
<span style="font-size: 0.75rem; color: ${isInherited ? '#4db251' : '#ff9800'}; border: 1px solid ${isInherited ? '#4db251' : '#ff9800'}; padding: 1px 5px; border-radius: var(--radius-sm); margin-left: 8px; flex-shrink: 0; font-weight: 600;">${isInherited ? '상속' : '설정'}</span>
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted); padding: 2px 6px; background: rgba(0,0,0,0.05); border-radius: var(--radius-sm); margin-left: 6px; flex-shrink: 0;">${f.data_depth}단계</span>
|
||||
<span style="font-size: 0.85rem; color: var(--text-muted); margin-left: auto; padding-left: 10px; flex-shrink: 0; font-weight: 500;">${f.data_permission ?? 0}</span>
|
||||
`;
|
||||
|
||||
itemDiv.onclick = () => {
|
||||
selectedFolderPermPathKey = pathKey;
|
||||
renderFolderTree();
|
||||
renderFolderUserPermissions(pathKey);
|
||||
};
|
||||
|
||||
treeWrapper.appendChild(itemDiv);
|
||||
});
|
||||
|
||||
container.appendChild(treeWrapper);
|
||||
}
|
||||
|
||||
function resetFolderUserPermissionTable() {
|
||||
document.getElementById('folder-perm-detail-title').innerText = '👥 폴더별 사용자 권한 설정';
|
||||
document.getElementById('folder-perm-detail-desc').innerText = '좌측 트리에서 폴더를 선택해 주세요.';
|
||||
document.getElementById('folder-perm-user-body').innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; color: var(--text-light); padding: 40px 0;">폴더를 선택하시면 사용자의 개별 권한 지정 목록이 여기에 표시됩니다.</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFolderUserPermissions(pathKey) {
|
||||
document.getElementById('folder-perm-detail-title').innerText = `📂 [${pathKey}] 권한 설정`;
|
||||
document.getElementById('folder-perm-detail-desc').innerText = '이 폴더에 대한 사용자의 접근 등급을 설정합니다. (미설정 시 프로젝트 기본 권한 적용)';
|
||||
|
||||
const tbody = document.getElementById('folder-perm-user-body');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
const users = folderPermDataCache.users || [];
|
||||
const folderPerms = folderPermDataCache.folderPermissions || [];
|
||||
|
||||
if (users.length === 0) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" style="text-align: center; color: var(--text-muted); padding: 20px 0;">이 프로젝트에 참여 중인 사용자가 없습니다.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
users.forEach(u => {
|
||||
const fp = folderPerms.find(p => p.folder_path_key === pathKey && p.user_id === u.user_id);
|
||||
const currentLev = fp ? fp.lev : null;
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
const projectLevName = getPermissionLabel(u.project_lev);
|
||||
const folderLevName = currentLev !== null ? getPermissionLabel(currentLev) : '상속 (Inherited)';
|
||||
|
||||
tr.innerHTML = `
|
||||
<td><strong>${u.user_id}</strong></td>
|
||||
<td>${u.user_nm}</td>
|
||||
<td>${u.company} / ${u.dept} ${u.position}</td>
|
||||
<td><span class="badge active" style="background: #e2e8f0; color: #475569;">${projectLevName}</span></td>
|
||||
<td><span class="badge ${currentLev !== null ? (currentLev === 0 ? 'inactive' : 'active') : ''}">${folderLevName}</span></td>
|
||||
<td>
|
||||
<select class="select-input select-folder-lev" data-userid="${u.user_id}" style="padding: 4px; font-size: 0.85rem; border: 1px solid var(--border); border-radius: var(--radius-sm);">
|
||||
<option value="inherit" ${currentLev === null ? 'selected' : ''}>상속 (Inherited)</option>
|
||||
<option value="0" ${currentLev === 0 ? 'selected' : ''}>접근 차단 (Block)</option>
|
||||
<option value="1" ${currentLev === 1 ? 'selected' : ''}>참관자 (Viewer)</option>
|
||||
<option value="7" ${currentLev === 7 ? 'selected' : ''}>일반참여자 (Worker)</option>
|
||||
<option value="191" ${currentLev === 191 ? 'selected' : ''}>부관리자 (Sub-Master)</option>
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" onclick="saveFolderUserPermission('${u.user_id}', this)" style="padding: 3px 8px; font-size: 0.8rem; margin-left: 6px;">적용</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function getPermissionLabel(lev) {
|
||||
if (lev === 255) return '관리자 (Admin)';
|
||||
if (lev === 191) return '부관리자 (Sub-Master)';
|
||||
if (lev === 8) return '보안참여자 (Security)';
|
||||
if (lev === 7) return '일반참여자 (Worker)';
|
||||
if (lev === 4) return '일반참여자 (Worker)';
|
||||
if (lev === 1) return '참관자 (Viewer)';
|
||||
if (lev === 0) return '접근 차단 (Block)';
|
||||
return `레벨 ${lev}`;
|
||||
}
|
||||
|
||||
async function saveFolderUserPermission(userId, btnEl) {
|
||||
const selectEl = btnEl.previousElementSibling;
|
||||
const value = selectEl.value;
|
||||
|
||||
try {
|
||||
if (value === 'inherit') {
|
||||
await fetchAPI('/api/admin/permissions/folders/remove', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project_id: selectedFolderPermProjectId,
|
||||
folder_path_key: selectedFolderPermPathKey,
|
||||
user_id: userId
|
||||
})
|
||||
});
|
||||
alert(`${userId}님의 폴더 권한이 제거되었으며, 프로젝트 기본 권한으로 초기화되었습니다.`);
|
||||
} else {
|
||||
await fetchAPI('/api/admin/permissions/folders/assign', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
project_id: selectedFolderPermProjectId,
|
||||
folder_path_key: selectedFolderPermPathKey,
|
||||
user_id: userId,
|
||||
lev: Number(value)
|
||||
})
|
||||
});
|
||||
alert(`${userId}님의 폴더 권한이 [${getPermissionLabel(Number(value))}] 등급으로 지정되었습니다.`);
|
||||
}
|
||||
|
||||
const updatedData = await fetchAPI(`/api/admin/permissions/folders/${selectedFolderPermProjectId}`);
|
||||
folderPermDataCache = updatedData;
|
||||
renderFolderUserPermissions(selectedFolderPermPathKey);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert(`⚠️ 권한 저장에 실패하였습니다: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 10. 초기 로딩 ---
|
||||
window.onload = function() {
|
||||
loadUserProfile();
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from './common.js';
|
||||
import { toggleModal } from './modalManager.js'
|
||||
import { showNotification, toggleContextmenu, toggleContextFocusBox } from './eventManager.js'
|
||||
import { renderMemo, resetViewer } from './pageRenderer.js'
|
||||
import { renderMemo, resetViewer, preparePageRendering } from './pageRenderer.js'
|
||||
|
||||
let listContainer = document.querySelector('.archive-main-center .list-container');
|
||||
|
||||
@@ -476,8 +476,33 @@ export async function createFolder(inputWrap, resourcePath, folderType) {
|
||||
let createFolderRes = await axios.post(`${vars.path_name}/createFolder`, { params: createFolderParams });
|
||||
if (createFolderRes.data.message == 'createFolder_success') {
|
||||
console.log(createFolderRes.data.message);
|
||||
// console.log(res.data.folderPath);
|
||||
// console.log(res.data);
|
||||
|
||||
// 폴더 생성 성공 시 로컬 브라우저 화면 즉시 갱신 (소켓 지연/연결끊김 대비)
|
||||
let userCurPath = (vars.users && vars.socket && vars.users[vars.socket.id]?.curPath) || JSON.parse(vars.userInfoString).curPath || '';
|
||||
let extractedPath = extractPathByLength(userCurPath, 1);
|
||||
|
||||
// 1. 헤더 버튼 갱신
|
||||
await preparePageRendering({
|
||||
scope: 'headerBtn',
|
||||
from: 'createFolder - local',
|
||||
resourcePath: userCurPath,
|
||||
pushState: false
|
||||
});
|
||||
|
||||
// 2. 트리 갱신
|
||||
await preparePageRendering({
|
||||
scope: 'tree',
|
||||
resourcePath: extractedPath,
|
||||
userCurPath: userCurPath,
|
||||
pushState: false
|
||||
});
|
||||
|
||||
// 3. 리스트 갱신
|
||||
await preparePageRendering({
|
||||
scope: 'list',
|
||||
resourcePath: userCurPath,
|
||||
pushState: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,18 @@ const socket = io();
|
||||
//// 소켓은 하나만...
|
||||
vars.socket = socket;
|
||||
|
||||
function getMyCurPath() {
|
||||
if (vars.users && vars.users[socket.id] && vars.users[socket.id].curPath) {
|
||||
return vars.users[socket.id].curPath;
|
||||
}
|
||||
if (vars.userInfoString) {
|
||||
try {
|
||||
return JSON.parse(vars.userInfoString).curPath || '';
|
||||
} catch (e) {}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
//// 접속자 정보 가져오기
|
||||
export function getUsers() {
|
||||
vars.socket.emit('getUsers');
|
||||
@@ -536,8 +548,7 @@ socket.on('createFolder_success', async (resultData) => {
|
||||
|
||||
renderLog();
|
||||
|
||||
let userCurPath;
|
||||
if (vars.users[socket.id] && vars.users[socket.id].curPath) userCurPath = vars.users[socket.id].curPath;
|
||||
let userCurPath = getMyCurPath();
|
||||
let extractedPath = extractPathByLength(userCurPath, 1);
|
||||
|
||||
// let resourcePath = resultData.resourcePath;
|
||||
@@ -578,6 +589,21 @@ socket.on('createFolder_success', async (resultData) => {
|
||||
let mainTreeItem = document.querySelector(`.archive-main-left .tree-container .tree-wrap .tree-item-wrap[data-resource-path="${vars.lastMainTreeItem.dataset.resourcePath}"]`)
|
||||
if (mainTreeItem) changeTreeItemStyle(mainTreeItem);
|
||||
}
|
||||
|
||||
// 폴더 생성 시 중앙 리스트 영역(list) 즉시 갱신 추가
|
||||
let pageRanderingOptionList = {
|
||||
scope: 'list',
|
||||
resourcePath: userCurPath,
|
||||
pushState: false,
|
||||
debug: '폴더생성완료 - list'
|
||||
}
|
||||
await preparePageRendering(pageRanderingOptionList);
|
||||
|
||||
if (vars.lastListItem) {
|
||||
let lastListItemPath = vars.lastListItem.dataset.resourcePath;
|
||||
let listItem = document.querySelector(`.archive-main-center .list-container .list-body .list-item[data-resource-path="${lastListItemPath}"]`);
|
||||
if (listItem) changeListItemStyle(listItem);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user