227 lines
12 KiB
PHP
227 lines
12 KiB
PHP
<?php
|
|
// 유저 권한 설정 (트리형 + 개인 예외 모드)
|
|
$CURRENT_SECTION = 'permission';
|
|
include '../inc/header.php';
|
|
?>
|
|
<div class="container" style="max-width:1280px; margin-top:24px;">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header d-flex flex-wrap gap-2 align-items-center">
|
|
<strong>유저 권한 설정 (개인 예외)</strong>
|
|
<input class="form-control form-control-sm" style="width:220px;" id="userSearch" placeholder="이름/아이디 검색">
|
|
<button class="btn btn-sm btn-outline-secondary" id="btnUserFind">검색</button>
|
|
<select class="form-select form-select-sm" id="userSelect" style="width:220px;">
|
|
<option value="101">권혁성(kevin)</option>
|
|
<option value="102">김슬기(sally)</option>
|
|
</select>
|
|
<div class="ms-auto small">
|
|
예외 모드:
|
|
<label class="ms-2 me-1"><input type="radio" name="mode" value="INHERIT" checked> INHERIT</label>
|
|
<label class="me-1"><input type="radio" name="mode" value="ALLOW"> ALLOW</label>
|
|
<label><input type="radio" name="mode" value="DENY"> DENY</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
<div class="input-group" style="max-width:420px;">
|
|
<input type="text" class="form-control" id="searchInput" placeholder="메뉴명/코드 검색">
|
|
<button class="btn btn-outline-secondary" id="btnSearch">검색</button>
|
|
<button class="btn btn-outline-secondary" id="btnResetSearch">초기화</button>
|
|
</div>
|
|
<div class="small text-muted ms-auto">
|
|
* INHERIT: 부서/역할 합집합만 반영(개인 변경 불가) / ALLOW: 개인 추가 허용 / DENY: 개인 차단(최우선)
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive" style="max-height:64vh; overflow:auto;">
|
|
<table class="table table-sm align-middle" id="permTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:320px;">메뉴</th>
|
|
<th class="text-center" style="width:90px;">읽기<br><input type="checkbox" id="hdr_read"></th>
|
|
<th class="text-center" style="width:90px;">쓰기<br><input type="checkbox" id="hdr_create"></th>
|
|
<th class="text-center" style="width:90px;">수정<br><input type="checkbox" id="hdr_update"></th>
|
|
<th class="text-center" style="width:90px;">삭제<br><input type="checkbox" id="hdr_delete"></th>
|
|
<th class="text-center" style="width:90px;">결재<br><input type="checkbox" id="hdr_approve"></th>
|
|
<th class="text-center" style="width:90px;">최종</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody><!-- rows --></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="mt-2 d-flex gap-2 justify-content-end">
|
|
<button class="btn btn-sm btn-primary" id="btnSave">저장</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.menu-name { font-size:14px; font-weight:500; }
|
|
.menu-url { color:#6c757d; font-size:12px; }
|
|
.indent { font-family: ui-monospace, Menlo, Consolas, monospace; color:#6c757d; }
|
|
.badge-inherit{ background:#6c757d; }
|
|
.badge-allow{ background:#0d6efd; }
|
|
.badge-deny{ background:#dc3545; }
|
|
</style>
|
|
|
|
<script src="/tenant/assets/js/permission_menu.js"></script>
|
|
<script>
|
|
// ===== 샘플 데이터 (실전: AJAX) =====
|
|
const MENU_DATA = [
|
|
{id:1,parent_id:null,title:'대시보드',code:'dashboard',path:'/tenant/member/dashboard.php',source:'system'},
|
|
{id:2,parent_id:null,title:'수주',code:'order',path:null,source:'system'},
|
|
{id:21,parent_id:2,title:'수주 관리',code:'order.manage',path:'/tenant/order/manage.php',source:'system'},
|
|
{id:22,parent_id:2,title:'수주 등록/수정',code:'order.edit',path:'/tenant/order/edit.php',source:'system'},
|
|
{id:3,parent_id:null,title:'생산',code:'production',path:null,source:'system'},
|
|
{id:31,parent_id:3,title:'작업 지시',code:'production.wi',path:'/tenant/production/work_instruction.php',source:'workflow'},
|
|
{id:32,parent_id:3,title:'스크린 작업',code:'production.screen',path:'/tenant/production/screen_work.php',source:'workflow'},
|
|
{id:5,parent_id:null,title:'게시판',code:'board',path:null,source:'system'},
|
|
{id:51,parent_id:5,title:'공지사항',code:'board.notice',path:'/tenant/board/notice_list.php',source:'board'},
|
|
];
|
|
const TENANT_USE = {dashboard:true, order:true,'order.manage':true,'order.edit':false,
|
|
production:true,'production.wi':true,'production.screen':true,
|
|
board:true,'board.notice':true};
|
|
|
|
// 상속 결과(부서+역할 합집합) — 실전에서는 서버에서 계산해서 내려주는 값
|
|
const INHERITED = {
|
|
// code: {read,create,update,delete,approve}
|
|
'dashboard': {read:true,create:false,update:false,delete:false,approve:false},
|
|
'order': {read:true,create:false,update:false,delete:false,approve:false},
|
|
'order.manage': {read:true,create:true, update:true, delete:false,approve:false},
|
|
'production': {read:true,create:false,update:false,delete:false,approve:false},
|
|
'production.wi': {read:true,create:true, update:false,delete:false,approve:false},
|
|
'production.screen': {read:true,create:false,update:false,delete:false,approve:false},
|
|
'board': {read:true,create:false,update:false,delete:false,approve:false},
|
|
'board.notice': {read:true,create:true, update:true, delete:false,approve:false},
|
|
};
|
|
|
|
// 개인 예외 상태(페이지 state)
|
|
let USER_MODE = 'INHERIT'; // INHERIT | ALLOW | DENY
|
|
const USER_PERMS = {}; // code: {read,create,update,delete,approve} (ALLOW일 때만 의미)
|
|
|
|
PermissionMenu.setData(MENU_DATA, TENANT_USE);
|
|
|
|
function finalCell(code){
|
|
const inh = INHERITED[code] || {read:false,create:false,update:false,delete:false,approve:false};
|
|
if (USER_MODE === 'DENY') {
|
|
return `<span class="badge badge-deny">DENY</span>`;
|
|
}
|
|
if (USER_MODE === 'INHERIT') {
|
|
const any = Object.values(inh).some(Boolean);
|
|
return `<span class="badge badge-inherit">${any?'상속 허용':'상속 없음'}</span>`;
|
|
}
|
|
// ALLOW 모드: 상속 OR 개인허용
|
|
const up = USER_PERMS[code] || {};
|
|
const merged = {
|
|
read: !!(inh.read || up.read),
|
|
create: !!(inh.create || up.create),
|
|
update: !!(inh.update || up.update),
|
|
delete: !!(inh.delete || up.delete),
|
|
approve:!!(inh.approve|| up.approve),
|
|
};
|
|
const any = Object.values(merged).some(Boolean);
|
|
return `<span class="badge badge-allow">${any?'허용됨':'없음'}</span>`;
|
|
}
|
|
|
|
function renderRows(){
|
|
const rows = PermissionMenu.buildRows((node) => {
|
|
const inh = INHERITED[node.code] || {read:false,create:false,update:false,delete:false,approve:false};
|
|
const up = (USER_PERMS[node.code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
|
const disabled = (USER_MODE!=='ALLOW') ? 'disabled' : ''; // INHERIT/DENY 에선 비활성
|
|
return ['read','create','update','delete','approve'].map(perm=>{
|
|
const checked = (USER_MODE==='ALLOW') ? (up[perm]||false) : false;
|
|
const title = (USER_MODE==='INHERIT') ? `상속값: ${inh[perm]?'허용':'-'}` :
|
|
(USER_MODE==='DENY') ? '개인 DENY 상태' : '개인 허용값';
|
|
return `<td class="text-center">
|
|
<input type="checkbox" class="perm" data-perm="${perm}" ${checked?'checked':''} ${disabled} title="${title}">
|
|
</td>`;
|
|
}).join('') + `<td class="text-center">${finalCell(node.code)}</td>`;
|
|
});
|
|
document.querySelector('#permTable tbody').innerHTML = rows;
|
|
applyIndeterminate();
|
|
}
|
|
|
|
function applyIndeterminate(){
|
|
// 헤더 요약(모드에 따라)
|
|
['read','create','update','delete','approve'].forEach(perm=>{
|
|
const hdr = document.querySelector('#hdr_'+perm);
|
|
if (!hdr) return;
|
|
if (USER_MODE!=='ALLOW'){ // INHERIT/DENY: 헤더도 비활성
|
|
hdr.indeterminate = false; hdr.checked = false; hdr.disabled = true;
|
|
}else{
|
|
hdr.disabled = false;
|
|
const codes = PermissionMenu.activeCodes();
|
|
const vals = codes.map(code => !!(USER_PERMS[code] && USER_PERMS[code][perm]));
|
|
const onCnt = vals.filter(Boolean).length;
|
|
hdr.indeterminate = (onCnt>0 && onCnt<vals.length);
|
|
hdr.checked = (onCnt===vals.length);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 이벤트
|
|
document.addEventListener('change', e=>{
|
|
const el = e.target;
|
|
if (el.name==='mode'){
|
|
USER_MODE = el.value; renderRows(); return;
|
|
}
|
|
if (el.classList.contains('perm')){
|
|
if (USER_MODE!=='ALLOW') return;
|
|
const tr = el.closest('tr'); const code = tr.dataset.code; const perm = el.dataset.perm;
|
|
(USER_PERMS[code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
|
USER_PERMS[code][perm] = el.checked;
|
|
renderRows();
|
|
}
|
|
});
|
|
['read','create','update','delete','approve'].forEach(perm=>{
|
|
const hdr = document.querySelector('#hdr_'+perm);
|
|
if(!hdr) return;
|
|
hdr.addEventListener('change', ()=>{
|
|
if (USER_MODE!=='ALLOW') return;
|
|
PermissionMenu.activeCodes().forEach(code=>{
|
|
(USER_PERMS[code] ||= {read:false,create:false,update:false,delete:false,approve:false});
|
|
USER_PERMS[code][perm] = hdr.checked;
|
|
});
|
|
renderRows();
|
|
});
|
|
});
|
|
|
|
// 검색
|
|
document.querySelector('#btnSearch').addEventListener('click', ()=>{
|
|
const q = document.querySelector('#searchInput').value.trim().toLowerCase();
|
|
document.querySelectorAll('#permTable tbody tr').forEach(tr=>{
|
|
const name = tr.querySelector('td:first-child').innerText.toLowerCase();
|
|
const code = tr.dataset.code.toLowerCase();
|
|
tr.style.display = (q==='' || name.includes(q) || code.includes(q)) ? '' : 'none';
|
|
});
|
|
});
|
|
document.querySelector('#btnResetSearch').addEventListener('click', ()=>{
|
|
document.querySelector('#searchInput').value=''; document.querySelectorAll('#permTable tbody tr').forEach(tr=> tr.style.display='');
|
|
});
|
|
|
|
// 유저 선택/검색(샘플)
|
|
document.querySelector('#btnUserFind').addEventListener('click', ()=>alert('유저 검색(샘플)'));
|
|
document.querySelector('#userSelect').addEventListener('change', function(){
|
|
alert('유저 권한/상속 로드(샘플): GET /api/permission/user_get.php?user_id='+this.value);
|
|
// 실제: USER_MODE/USER_PERMS/INHERITED 로드 후 renderRows();
|
|
});
|
|
|
|
// 저장
|
|
document.querySelector('#btnSave').addEventListener('click', ()=>{
|
|
const userId = document.querySelector('#userSelect').value;
|
|
const payload = {
|
|
user_id: userId,
|
|
mode: USER_MODE, // INHERIT/ALLOW/DENY
|
|
perms: USER_PERMS // ALLOW일 때만 의미; 서버에서 병합/적용
|
|
};
|
|
console.log('SAVE user permissions', payload);
|
|
alert('저장(샘플): POST /api/permission/user_save.php');
|
|
});
|
|
|
|
renderRows();
|
|
</script>
|
|
|
|
<?php include '../inc/footer.php'; ?>
|