303 lines
15 KiB
PHP
303 lines
15 KiB
PHP
<?php $CURRENT_SECTION = 'permission';
|
|
include '../inc/header.php'; ?>
|
|
|
|
<div class="container" style="max-width:1280px; margin-top:24px;">
|
|
|
|
<ul class="nav nav-tabs" role="tablist">
|
|
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tabUse" type="button">메뉴 사용 설정</button></li>
|
|
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabWorkflow" type="button">작업공정 연동(읽기전용)</button></li>
|
|
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabBoards" type="button">게시판 연동(읽기전용)</button></li>
|
|
</ul>
|
|
|
|
<div class="tab-content border border-top-0 p-3">
|
|
|
|
<!-- 메뉴 사용 설정 (작업공정/게시판 연동도 함께 표시) -->
|
|
<div class="tab-pane fade show active" id="tabUse">
|
|
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
|
|
<div class="input-group" style="max-width:420px;">
|
|
<input type="text" class="form-control" id="searchInput" placeholder="메뉴명/코드/출처 검색 (예: workflow, board)">
|
|
<button class="btn btn-outline-secondary" id="btnSearch">검색</button>
|
|
<button class="btn btn-outline-secondary" id="btnClear">초기화</button>
|
|
</div>
|
|
<div class="ms-auto d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-primary" id="btnAllOn">전체 허용</button>
|
|
<button class="btn btn-sm btn-outline-danger" id="btnAllOff">전체 금지</button>
|
|
<button class="btn btn-sm btn-primary" id="btnSave">저장</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive" style="max-height:64vh; overflow:auto;">
|
|
<table class="table table-hover align-middle small" id="menuTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:56px;">사용</th>
|
|
<th>메뉴명</th>
|
|
<th style="width:200px;">코드</th>
|
|
<th style="width:260px;">경로</th>
|
|
<!-- ▼ 신규: 출처 컬럼 -->
|
|
<th style="width:120px;">출처</th>
|
|
<th style="width:120px;">구독기본</th>
|
|
<th style="width:120px;">강제고정</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody><!-- JS 렌더 --></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="form-text">
|
|
* <b>출처</b>: system(일반), <span class="text-lowercase">workflow</span>(작업공정 연동), <span class="text-lowercase">board</span>(게시판 연동)<br>
|
|
* <b>읽기전용</b> 항목은 직접 토글할 수 없으며, 상위 메뉴 ON/OFF에 따라 상태가 함께 변합니다.<br>
|
|
* <b>강제고정</b>: 테넌트에서 끌 수 없음(최고관리자 정책).
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 작업공정 연동(읽기전용) -->
|
|
<div class="tab-pane fade" id="tabWorkflow">
|
|
<div class="alert alert-info py-2 small">작업공정에서 생성/연동된 메뉴 목록입니다. 사용 여부는 상단 “메뉴 사용 설정”에서 구조 전체와 함께 확인하세요.</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm align-middle">
|
|
<thead class="table-light"><tr><th>프로세스</th><th>연동 메뉴</th><th>메뉴 코드</th><th>경로</th></tr></thead>
|
|
<tbody id="workflowRows"><!-- JS --></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 게시판 연동(읽기전용) -->
|
|
<div class="tab-pane fade" id="tabBoards">
|
|
<div class="alert alert-info py-2 small">게시판 생성 시 연결된 메뉴 목록입니다.</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm align-middle">
|
|
<thead class="table-light"><tr><th>게시판 코드</th><th>게시판 명</th><th>연동 메뉴</th><th>메뉴 코드</th></tr></thead>
|
|
<tbody id="boardRows"><!-- JS --></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ===== 샘플 데이터 (실전에서는 AJAX 로드) =====
|
|
// source: 'system' | 'workflow' | 'board'
|
|
// readonly: true 이면 직접 토글 불가(체크박스 disabled)
|
|
const MENU_DATA = [
|
|
{id:1, parent_id:null, title:'대시보드', code:'dashboard', path:'/tenant/member/dashboard.php', forced:false, plan_default:true, source:'system', readonly:false},
|
|
{id:2, parent_id:null, title:'수주', code:'order', path:null, forced:false, plan_default:true, source:'system', readonly:false},
|
|
{id:21, parent_id:2, title:'수주 관리', code:'order.manage', path:'/tenant/order/manage.php', forced:false, plan_default:true, source:'system', readonly:false},
|
|
{id:22, parent_id:2, title:'수주 등록/수정', code:'order.edit', path:'/tenant/order/edit.php', forced:false, plan_default:false, source:'system', readonly:false},
|
|
|
|
{id:3, parent_id:null, title:'생산', code:'production', path:null, forced:true, plan_default:true, source:'system', readonly:false},
|
|
// ▼ 작업공정 연동 예시 (readonly)
|
|
{id:31, parent_id:3, title:'작업 지시', code:'production.wi', path:'/tenant/production/work_instruction.php', forced:true, plan_default:true, source:'workflow', readonly:true},
|
|
{id:32, parent_id:3, title:'스크린 작업', code:'production.screen', path:'/tenant/production/screen_work.php', forced:false, plan_default:true, source:'workflow', readonly:true},
|
|
|
|
{id:4, parent_id:null, title:'자재', code:'material', path:null, forced:false, plan_default:true, source:'system', readonly:false},
|
|
// ▼ 작업공정/품질 연동 예시 (readonly)
|
|
{id:41, parent_id:4, title:'수입 검사 대장', code:'material.insp', path:'/tenant/material/inspection_list.php', forced:false, plan_default:false, source:'workflow', readonly:true},
|
|
|
|
// ▼ 게시판 연동 예시 (readonly)
|
|
{id:5, parent_id:null, title:'게시판', code:'board', path:null, forced:false, plan_default:true, source:'system', readonly:false},
|
|
{id:51, parent_id:5, title:'공지사항', code:'board.notice', path:'/tenant/board/notice_list.php', forced:false, plan_default:true, source:'board', readonly:true},
|
|
{id:52, parent_id:5, title:'Q&A', code:'board.qna', path:'/tenant/board/qna_list.php', forced:false, plan_default:false, source:'board', readonly:true},
|
|
];
|
|
|
|
// 테넌트 현재 설정
|
|
const TENANT_USE = {
|
|
'dashboard': true,
|
|
'order': true,
|
|
'order.manage': true,
|
|
'order.edit': false,
|
|
'production': true,
|
|
'production.wi': true,
|
|
'production.screen': true,
|
|
'material': true,
|
|
'material.insp': false,
|
|
'board': true,
|
|
'board.notice': true,
|
|
'board.qna': false,
|
|
};
|
|
|
|
// ===== 유틸/트리 =====
|
|
const byId = MENU_DATA.reduce((m,x)=> (m[x.id]=x,m),{});
|
|
const childrenMap = MENU_DATA.reduce((m,x)=>{ (m[x.parent_id??0]??=([])).push(x); return m; },{});
|
|
|
|
function buildRows(parentId=null, depth=0, rows=[]){
|
|
const list = childrenMap[parentId??0] || [];
|
|
list.sort((a,b)=> (a.title > b.title ? 1 : -1));
|
|
for (const node of list){
|
|
const enabled = !!TENANT_USE[node.code];
|
|
rows.push(renderRow(node, depth, enabled));
|
|
buildRows(node.id, depth+1, rows);
|
|
}
|
|
return rows.join('');
|
|
}
|
|
|
|
function badge(text, cls){ return `<span class="badge ${cls} ms-1">${text}</span>`; }
|
|
|
|
function renderRow(node, depth, enabled){
|
|
const indent = ' '.repeat(depth*4) + (depth? '└ ' : '');
|
|
const isReadOnly = !!node.readonly;
|
|
const isForced = !!node.forced;
|
|
|
|
const disabledAttr = (isForced || isReadOnly) ? 'disabled' : '';
|
|
const checkedAttr = (isForced ? 'checked' : (enabled ? 'checked' : ''));
|
|
|
|
const src = node.source || 'system';
|
|
const sourceBadge =
|
|
src==='workflow' ? badge('workflow','bg-warning text-dark') :
|
|
src==='board' ? badge('board','bg-success') :
|
|
badge('system','bg-secondary');
|
|
|
|
const forceBadge = isForced ? badge('강제','bg-secondary') : '';
|
|
const roBadge = isReadOnly ? badge('읽기전용','bg-dark') : '';
|
|
const planBadge = node.plan_default ? badge('플랜기본','bg-info text-dark') : '';
|
|
|
|
const path = node.path ? node.path : '-';
|
|
|
|
return `
|
|
<tr data-id="${node.id}" data-code="${node.code}" data-parent="${node.parent_id??''}">
|
|
<td class="text-center">
|
|
<input type="checkbox" class="chk-use" ${checkedAttr} ${disabledAttr} title="${isReadOnly?'연동 항목은 직접 변경할 수 없습니다.':''}">
|
|
</td>
|
|
<td><span class="text-monospace">${indent}</span>${node.title}${forceBadge}${roBadge}</td>
|
|
<td class="text-muted">${node.code}</td>
|
|
<td class="text-muted">${path}</td>
|
|
<td>${sourceBadge}</td>
|
|
<td>${node.plan_default ? '기본 사용' : '-'}</td>
|
|
<td>${isForced ? '강제 고정' : '-'}</td>
|
|
</tr>`;
|
|
}
|
|
|
|
function setChildren(code, on){
|
|
const node = MENU_DATA.find(x=>x.code===code);
|
|
if (!node) return;
|
|
(childrenMap[node.id]||[]).forEach(ch=>{
|
|
// 하위는 readonly여도 부모 변경에 따라 상태는 동기화 (직접토글만 막음)
|
|
TENANT_USE[ch.code]=on;
|
|
setChildren(ch.code, on);
|
|
});
|
|
}
|
|
|
|
function updateParents(code){
|
|
const node = MENU_DATA.find(x=>x.code===code);
|
|
if (!node || !node.parent_id) return;
|
|
const parent = byId[node.parent_id];
|
|
const kids = (childrenMap[parent.id]||[]);
|
|
const states = kids.map(k=> !!TENANT_USE[k.code]);
|
|
const allOn = states.every(Boolean);
|
|
const allOff= states.every(s=>!s);
|
|
if (!parent.forced){
|
|
TENANT_USE[parent.code] = allOn ? true : (allOff ? false : true);
|
|
}
|
|
updateParents(parent.code);
|
|
}
|
|
|
|
function applyIndeterminate(){
|
|
$('#menuTable tbody tr').each(function(){
|
|
const id = $(this).data('id');
|
|
const code = $(this).data('code');
|
|
const $chk = $(this).find('.chk-use')[0];
|
|
if (!$chk) return;
|
|
$chk.indeterminate = false;
|
|
const kids = childrenMap[id]||[];
|
|
if (kids.length){
|
|
const onCnt = kids.filter(k=> TENANT_USE[k.code]).length;
|
|
if (onCnt>0 && onCnt<kids.length){ $chk.indeterminate = true; }
|
|
}
|
|
// forced/readOnly 도구팁
|
|
const node = byId[id];
|
|
if (node.forced){ $chk.title = '강제 고정 메뉴입니다.'; }
|
|
});
|
|
}
|
|
|
|
function render(){
|
|
$('#menuTable tbody').html( buildRows(null,0,[]) );
|
|
// 체크박스 상태 반영
|
|
$('#menuTable .chk-use').each(function(){
|
|
const code = $(this).closest('tr').data('code');
|
|
this.checked = !!TENANT_USE[code] || byId[$(this).closest('tr').data('id')].forced;
|
|
});
|
|
applyIndeterminate();
|
|
|
|
// 하단 탭(읽기전용 목록)도 같이 채움
|
|
renderSubLists();
|
|
}
|
|
|
|
function renderSubLists(){
|
|
// 작업공정
|
|
const wf = MENU_DATA.filter(x=>x.source==='workflow');
|
|
$('#workflowRows').html(wf.map(x=>`
|
|
<tr>
|
|
<td>${x.title.includes('작업')? x.title.replace(/ 작업.*/,' 작업') : '프로세스'}</td>
|
|
<td>${x.title}</td>
|
|
<td class="text-muted">${x.code}</td>
|
|
<td class="text-muted">${x.path||'-'}</td>
|
|
</tr>`).join(''));
|
|
|
|
// 게시판
|
|
const bd = MENU_DATA.filter(x=>x.source==='board');
|
|
$('#boardRows').html(bd.map(x=>`
|
|
<tr>
|
|
<td class="text-muted">${x.code.split('.').pop().toUpperCase()}</td>
|
|
<td>${x.title}</td>
|
|
<td>${byId[x.parent_id]?.title||'-'}</td>
|
|
<td class="text-muted">${x.code}</td>
|
|
</tr>`).join(''));
|
|
}
|
|
|
|
$(function(){
|
|
render();
|
|
|
|
// 직접 토글
|
|
$(document).on('change','.chk-use', function(){
|
|
const $tr = $(this).closest('tr');
|
|
const node = byId[$tr.data('id')];
|
|
const code = node.code;
|
|
|
|
// 강제/읽기전용은 직접 토글 금지
|
|
if (node.forced || node.readonly){
|
|
this.checked = !!TENANT_USE[code]; // 원상복구
|
|
return;
|
|
}
|
|
|
|
TENANT_USE[code] = this.checked;
|
|
setChildren(code, this.checked);
|
|
updateParents(code);
|
|
render();
|
|
});
|
|
|
|
// 전체 허용/금지 (강제/readonly 제외)
|
|
$('#btnAllOn').on('click', ()=>{
|
|
MENU_DATA.forEach(n=>{ if(!n.forced && !n.readonly) TENANT_USE[n.code]=true; });
|
|
render();
|
|
});
|
|
$('#btnAllOff').on('click', ()=>{
|
|
MENU_DATA.forEach(n=>{ if(!n.forced && !n.readonly) TENANT_USE[n.code]=false; });
|
|
render();
|
|
});
|
|
|
|
// 저장
|
|
$('#btnSave').on('click', ()=>{
|
|
const payload = Object.fromEntries(MENU_DATA.map(n=> [n.code, !!TENANT_USE[n.code]]));
|
|
console.log('SAVE', payload);
|
|
alert('저장(샘플) — /api/permission/tenant_menu_save.php');
|
|
});
|
|
|
|
// 검색: 메뉴명/코드/출처
|
|
const doSearch = ()=>{
|
|
const q = $('#searchInput').val().trim().toLowerCase();
|
|
if (!q){ $('#menuTable tbody tr').show(); return; }
|
|
$('#menuTable tbody tr').each(function(){
|
|
const name = $(this).find('td:nth-child(2)').text().toLowerCase();
|
|
const code = $(this).find('td:nth-child(3)').text().toLowerCase();
|
|
const src = $(this).find('td:nth-child(5)').text().toLowerCase();
|
|
$(this).toggle( name.includes(q) || code.includes(q) || src.includes(q) );
|
|
});
|
|
};
|
|
$('#btnSearch').on('click', doSearch);
|
|
$('#btnClear').on('click', ()=>{ $('#searchInput').val(''); $('#menuTable tbody tr').show(); });
|
|
});
|
|
</script>
|
|
|
|
<?php include '../inc/footer.php'; ?>
|