fix(tkuser): 권한 기반 탭 자동 라우팅 — 제한 사용자 진입 시 첫 허용 탭 표시

users 권한 없는 일반 사용자가 빈 화면(비밀번호 변경 폼만) 보이던 문제 수정.
허용된 탭으로 자동 전환하고, 탭 버튼에 data-tab 속성 추가하여 프로그래밍적
switchTab() 호출 시 active 버튼도 정확히 갱신되도록 개선.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-16 12:08:48 +09:00
parent 4108a6e64a
commit 817002f798
3 changed files with 34 additions and 26 deletions

View File

@@ -34,43 +34,43 @@
<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 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>사용자
</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>프로젝트
</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>작업장
</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>작업자
</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>부서
</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>권한
</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>이슈 유형
</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>작업
</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>휴가
</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>협력업체
</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>업체
</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>소모품
</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>알림 수신자
</button>
</div>
@@ -2000,9 +2000,9 @@
</div>
<!-- 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 -->
<script src="/static/js/tkuser-tabs.js?v=2026031401"></script>
<script src="/static/js/tkuser-tabs.js?v=2026031602"></script>
<!-- JS: Individual modules -->
<script src="/static/js/tkuser-users.js?v=2026031601"></script>
<script src="/static/js/tkuser-projects.js?v=2026031401"></script>

View File

@@ -128,9 +128,6 @@ async function init() {
await loadDepartmentsCache();
await loadUsers();
} else {
// 비밀번호 변경은 항상 표시
document.getElementById('passwordChangeSection').classList.remove('hidden');
// tkuser 탭별 권한 확인
try {
const result = await api(`/permissions/users/${currentUser.id}/effective-permissions`);
@@ -154,10 +151,8 @@ async function init() {
document.getElementById('tabNav').classList.remove('hidden');
// 비허용 탭 버튼 숨김
document.querySelectorAll('.tab-btn').forEach(btn => {
const onclick = btn.getAttribute('onclick') || '';
const match = onclick.match(/switchTab\('(\w+)'\)/);
if (match) {
const tabName = match[1];
const tabName = btn.getAttribute('data-tab');
if (tabName) {
// permissions 탭은 admin 전용
if (tabName === 'permissions') {
btn.style.display = 'none';
@@ -167,12 +162,20 @@ async function init() {
}
});
// tkuser.users 권한 시 adminSection 표시
// tkuser.users 권한 시 adminSection + passwordChangeSection 표시
if (currentUserAllowedTabs.has('users')) {
document.getElementById('adminSection').classList.remove('hidden');
document.getElementById('passwordChangeSection').classList.remove('hidden');
await loadDepartmentsCache();
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) {
@@ -194,7 +197,7 @@ async function init() {
'consumables','notificationRecipients'];
const urlTab = new URLSearchParams(location.search).get('tab');
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') {
tabBtn.click();
const url = new URL(location);

View File

@@ -1,5 +1,5 @@
/* ===== Tab ===== */
function switchTab(name) {
function switchTab(name, event) {
// 권한 guard: currentUserAllowedTabs가 Set이면 허용된 탭만 접근
if (typeof currentUserAllowedTabs !== 'undefined'
&& currentUserAllowedTabs
@@ -8,7 +8,12 @@ function switchTab(name) {
document.querySelectorAll('[id^="tab-"]').forEach(el => el.classList.add('hidden'));
document.getElementById('tab-' + name)?.classList.remove('hidden');
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 너비 확장
const mainEl = document.querySelector('main');
const navInner = document.getElementById('tabNavInner');