fix(tkuser): 권한 기반 탭 자동 라우팅 — 제한 사용자 진입 시 첫 허용 탭 표시
users 권한 없는 일반 사용자가 빈 화면(비밀번호 변경 폼만) 보이던 문제 수정. 허용된 탭으로 자동 전환하고, 탭 버튼에 data-tab 속성 추가하여 프로그래밍적 switchTab() 호출 시 active 버튼도 정확히 갱신되도록 개선. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,43 +34,43 @@
|
|||||||
<nav id="tabNav" class="bg-white border-b shadow-sm sticky top-14 z-40 hidden">
|
<nav id="tabNav" class="bg-white border-b shadow-sm sticky top-14 z-40 hidden">
|
||||||
<div id="tabNavInner" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div id="tabNavInner" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex gap-1 py-2 overflow-x-auto">
|
<div class="flex gap-1 py-2 overflow-x-auto">
|
||||||
<button class="tab-btn active px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('users')">
|
<button class="tab-btn active px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="users" onclick="switchTab('users', event)">
|
||||||
<i class="fas fa-users mr-2"></i>사용자
|
<i class="fas fa-users mr-2"></i>사용자
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('projects')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="projects" onclick="switchTab('projects', event)">
|
||||||
<i class="fas fa-folder-open mr-2"></i>프로젝트
|
<i class="fas fa-folder-open mr-2"></i>프로젝트
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('workplaces')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="workplaces" onclick="switchTab('workplaces', event)">
|
||||||
<i class="fas fa-building mr-2"></i>작업장
|
<i class="fas fa-building mr-2"></i>작업장
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('workers')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="workers" onclick="switchTab('workers', event)">
|
||||||
<i class="fas fa-hard-hat mr-2"></i>작업자
|
<i class="fas fa-hard-hat mr-2"></i>작업자
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('departments')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="departments" onclick="switchTab('departments', event)">
|
||||||
<i class="fas fa-sitemap mr-2"></i>부서
|
<i class="fas fa-sitemap mr-2"></i>부서
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('permissions')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="permissions" onclick="switchTab('permissions', event)">
|
||||||
<i class="fas fa-shield-alt mr-2"></i>권한
|
<i class="fas fa-shield-alt mr-2"></i>권한
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('issueTypes')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="issueTypes" onclick="switchTab('issueTypes', event)">
|
||||||
<i class="fas fa-exclamation-triangle mr-2"></i>이슈 유형
|
<i class="fas fa-exclamation-triangle mr-2"></i>이슈 유형
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('tasks')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="tasks" onclick="switchTab('tasks', event)">
|
||||||
<i class="fas fa-tasks mr-2"></i>작업
|
<i class="fas fa-tasks mr-2"></i>작업
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('vacations')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="vacations" onclick="switchTab('vacations', event)">
|
||||||
<i class="fas fa-umbrella-beach mr-2"></i>휴가
|
<i class="fas fa-umbrella-beach mr-2"></i>휴가
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('partners')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="partners" onclick="switchTab('partners', event)">
|
||||||
<i class="fas fa-truck mr-2"></i>협력업체
|
<i class="fas fa-truck mr-2"></i>협력업체
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('vendors')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="vendors" onclick="switchTab('vendors', event)">
|
||||||
<i class="fas fa-store mr-2"></i>업체
|
<i class="fas fa-store mr-2"></i>업체
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('consumables')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="consumables" onclick="switchTab('consumables', event)">
|
||||||
<i class="fas fa-box-open mr-2"></i>소모품
|
<i class="fas fa-box-open mr-2"></i>소모품
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" onclick="switchTab('notificationRecipients')">
|
<button class="tab-btn px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap" data-tab="notificationRecipients" onclick="switchTab('notificationRecipients', event)">
|
||||||
<i class="fas fa-bell mr-2"></i>알림 수신자
|
<i class="fas fa-bell mr-2"></i>알림 수신자
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -2000,9 +2000,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- JS: Core (config, token, api, toast, helpers, init) -->
|
<!-- JS: Core (config, token, api, toast, helpers, init) -->
|
||||||
<script src="/static/js/tkuser-core.js?v=2026031601"></script>
|
<script src="/static/js/tkuser-core.js?v=2026031602"></script>
|
||||||
<!-- JS: Tabs -->
|
<!-- JS: Tabs -->
|
||||||
<script src="/static/js/tkuser-tabs.js?v=2026031401"></script>
|
<script src="/static/js/tkuser-tabs.js?v=2026031602"></script>
|
||||||
<!-- JS: Individual modules -->
|
<!-- JS: Individual modules -->
|
||||||
<script src="/static/js/tkuser-users.js?v=2026031601"></script>
|
<script src="/static/js/tkuser-users.js?v=2026031601"></script>
|
||||||
<script src="/static/js/tkuser-projects.js?v=2026031401"></script>
|
<script src="/static/js/tkuser-projects.js?v=2026031401"></script>
|
||||||
|
|||||||
@@ -128,9 +128,6 @@ async function init() {
|
|||||||
await loadDepartmentsCache();
|
await loadDepartmentsCache();
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
} else {
|
} else {
|
||||||
// 비밀번호 변경은 항상 표시
|
|
||||||
document.getElementById('passwordChangeSection').classList.remove('hidden');
|
|
||||||
|
|
||||||
// tkuser 탭별 권한 확인
|
// tkuser 탭별 권한 확인
|
||||||
try {
|
try {
|
||||||
const result = await api(`/permissions/users/${currentUser.id}/effective-permissions`);
|
const result = await api(`/permissions/users/${currentUser.id}/effective-permissions`);
|
||||||
@@ -154,10 +151,8 @@ async function init() {
|
|||||||
document.getElementById('tabNav').classList.remove('hidden');
|
document.getElementById('tabNav').classList.remove('hidden');
|
||||||
// 비허용 탭 버튼 숨김
|
// 비허용 탭 버튼 숨김
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
const onclick = btn.getAttribute('onclick') || '';
|
const tabName = btn.getAttribute('data-tab');
|
||||||
const match = onclick.match(/switchTab\('(\w+)'\)/);
|
if (tabName) {
|
||||||
if (match) {
|
|
||||||
const tabName = match[1];
|
|
||||||
// permissions 탭은 admin 전용
|
// permissions 탭은 admin 전용
|
||||||
if (tabName === 'permissions') {
|
if (tabName === 'permissions') {
|
||||||
btn.style.display = 'none';
|
btn.style.display = 'none';
|
||||||
@@ -167,12 +162,20 @@ async function init() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// tkuser.users 권한 시 adminSection 표시
|
// tkuser.users 권한 시 adminSection + passwordChangeSection 표시
|
||||||
if (currentUserAllowedTabs.has('users')) {
|
if (currentUserAllowedTabs.has('users')) {
|
||||||
document.getElementById('adminSection').classList.remove('hidden');
|
document.getElementById('adminSection').classList.remove('hidden');
|
||||||
|
document.getElementById('passwordChangeSection').classList.remove('hidden');
|
||||||
await loadDepartmentsCache();
|
await loadDepartmentsCache();
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
|
} else {
|
||||||
|
// users 권한 없음 → tab-users 숨기고 첫 허용 탭으로 자동 전환
|
||||||
|
document.getElementById('tab-users').classList.add('hidden');
|
||||||
|
switchTab([...currentUserAllowedTabs][0]);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 아무 권한 없음 → 비밀번호 변경만 표시 (fallback)
|
||||||
|
document.getElementById('passwordChangeSection').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -194,7 +197,7 @@ async function init() {
|
|||||||
'consumables','notificationRecipients'];
|
'consumables','notificationRecipients'];
|
||||||
const urlTab = new URLSearchParams(location.search).get('tab');
|
const urlTab = new URLSearchParams(location.search).get('tab');
|
||||||
if (urlTab && ALLOWED_TABS.includes(urlTab)) {
|
if (urlTab && ALLOWED_TABS.includes(urlTab)) {
|
||||||
const tabBtn = document.querySelector(`.tab-btn[onclick*="switchTab('${urlTab}')"]`);
|
const tabBtn = document.querySelector(`.tab-btn[data-tab="${urlTab}"]`);
|
||||||
if (tabBtn && tabBtn.style.display !== 'none') {
|
if (tabBtn && tabBtn.style.display !== 'none') {
|
||||||
tabBtn.click();
|
tabBtn.click();
|
||||||
const url = new URL(location);
|
const url = new URL(location);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* ===== Tab ===== */
|
/* ===== Tab ===== */
|
||||||
function switchTab(name) {
|
function switchTab(name, event) {
|
||||||
// 권한 guard: currentUserAllowedTabs가 Set이면 허용된 탭만 접근
|
// 권한 guard: currentUserAllowedTabs가 Set이면 허용된 탭만 접근
|
||||||
if (typeof currentUserAllowedTabs !== 'undefined'
|
if (typeof currentUserAllowedTabs !== 'undefined'
|
||||||
&& currentUserAllowedTabs
|
&& currentUserAllowedTabs
|
||||||
@@ -8,7 +8,12 @@ function switchTab(name) {
|
|||||||
document.querySelectorAll('[id^="tab-"]').forEach(el => el.classList.add('hidden'));
|
document.querySelectorAll('[id^="tab-"]').forEach(el => el.classList.add('hidden'));
|
||||||
document.getElementById('tab-' + name)?.classList.remove('hidden');
|
document.getElementById('tab-' + name)?.classList.remove('hidden');
|
||||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||||
event.currentTarget.classList.add('active');
|
if (event?.currentTarget) {
|
||||||
|
event.currentTarget.classList.add('active');
|
||||||
|
} else {
|
||||||
|
const btn = document.querySelector(`.tab-btn[data-tab="${name}"]`);
|
||||||
|
if (btn) btn.classList.add('active');
|
||||||
|
}
|
||||||
// 사이드바 레이아웃 탭에서 main/nav/header 너비 확장
|
// 사이드바 레이아웃 탭에서 main/nav/header 너비 확장
|
||||||
const mainEl = document.querySelector('main');
|
const mainEl = document.querySelector('main');
|
||||||
const navInner = document.getElementById('tabNavInner');
|
const navInner = document.getElementById('tabNavInner');
|
||||||
|
|||||||
Reference in New Issue
Block a user