feat: tkuser 통합 관리 서비스 + 전체 시스템 SSO 쿠키 인증 통합
- tkuser 서비스 신규 추가 (API + Web) - 사용자/권한/프로젝트/부서/작업자/작업장/설비/작업/휴가 통합 관리 - 작업장 탭: 공장→작업장 드릴다운 네비게이션 + 구역지도 클릭 연동 - 작업 탭: 공정(work_types)→작업(tasks) 계층 관리 - 휴가 탭: 유형 관리 + 연차 배정(근로기준법 자동계산) - 전 시스템 SSO 쿠키 인증으로 통합 (.technicalkorea.net 공유) - System 2: 작업 이슈 리포트 기능 강화 - System 3: tkuser API 연동, 페이지 권한 체계 적용 - docker-compose에 tkuser-api, tkuser-web 서비스 추가 - ARCHITECTURE.md, DEPLOYMENT.md 문서 작성 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,11 @@ const API_BASE_URL = (() => {
|
||||
return protocol + '//' + hostname + ':16080/api';
|
||||
}
|
||||
|
||||
// 통합 Docker 환경에서 직접 접근 (포트 30280)
|
||||
if (port === '30280') {
|
||||
return protocol + '//' + hostname + ':30200/api';
|
||||
}
|
||||
|
||||
// 기타 환경
|
||||
return '/api';
|
||||
})();
|
||||
@@ -77,6 +82,10 @@ const TokenManager = {
|
||||
}
|
||||
};
|
||||
|
||||
// 전역 노출 (permissions.js 등 다른 스크립트에서 접근)
|
||||
window.TokenManager = TokenManager;
|
||||
window.API_BASE_URL = API_BASE_URL;
|
||||
|
||||
// API 요청 헬퍼
|
||||
async function apiRequest(endpoint, options = {}) {
|
||||
const token = TokenManager.getToken();
|
||||
|
||||
@@ -10,81 +10,66 @@ class CommonHeader {
|
||||
this.menuItems = this.initMenuItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 관리 URL (tkuser 서브도메인 또는 로컬 포트)
|
||||
*/
|
||||
_getUserManageUrl() {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes('technicalkorea.net')) {
|
||||
return window.location.protocol + '//tkuser.technicalkorea.net';
|
||||
}
|
||||
return window.location.protocol + '//' + hostname + ':30380';
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 아이템 정의
|
||||
*/
|
||||
initMenuItems() {
|
||||
return [
|
||||
{
|
||||
id: 'daily_work',
|
||||
title: '일일 공수',
|
||||
icon: 'fas fa-calendar-check',
|
||||
url: '/daily-work.html',
|
||||
pageName: 'daily_work',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50 hover:bg-blue-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_create',
|
||||
title: '부적합 등록',
|
||||
icon: 'fas fa-plus-circle',
|
||||
url: '/index.html',
|
||||
pageName: 'issues_create',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50 hover:bg-green-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_view',
|
||||
title: '신고내용조회',
|
||||
icon: 'fas fa-search',
|
||||
url: '/issue-view.html',
|
||||
pageName: 'issues_view',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50 hover:bg-purple-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_manage',
|
||||
title: '목록 관리',
|
||||
icon: 'fas fa-tasks',
|
||||
url: '/index.html#list',
|
||||
pageName: 'issues_manage',
|
||||
color: 'text-orange-600',
|
||||
bgColor: 'bg-orange-50 hover:bg-orange-100',
|
||||
subMenus: [
|
||||
{
|
||||
id: 'issues_inbox',
|
||||
title: '수신함',
|
||||
icon: 'fas fa-inbox',
|
||||
url: '/issues-inbox.html',
|
||||
pageName: 'issues_inbox',
|
||||
color: 'text-blue-600'
|
||||
},
|
||||
{
|
||||
id: 'issues_management',
|
||||
title: '관리함',
|
||||
icon: 'fas fa-cog',
|
||||
url: '/issues-management.html',
|
||||
pageName: 'issues_management',
|
||||
color: 'text-green-600'
|
||||
},
|
||||
{
|
||||
id: 'issues_archive',
|
||||
title: '폐기함',
|
||||
icon: 'fas fa-archive',
|
||||
url: '/issues-archive.html',
|
||||
pageName: 'issues_archive',
|
||||
color: 'text-gray-600'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'issues_dashboard',
|
||||
title: '현황판',
|
||||
icon: 'fas fa-chart-line',
|
||||
url: '/issues-dashboard.html',
|
||||
pageName: 'issues_dashboard',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50 hover:bg-purple-100'
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_inbox',
|
||||
title: '수신함',
|
||||
icon: 'fas fa-inbox',
|
||||
url: '/issues-inbox.html',
|
||||
pageName: 'issues_inbox',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_management',
|
||||
title: '관리함',
|
||||
icon: 'fas fa-cog',
|
||||
url: '/issues-management.html',
|
||||
pageName: 'issues_management',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'issues_archive',
|
||||
title: '폐기함',
|
||||
icon: 'fas fa-archive',
|
||||
url: '/issues-archive.html',
|
||||
pageName: 'issues_archive',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'daily_work',
|
||||
title: '일일 공수',
|
||||
icon: 'fas fa-calendar-check',
|
||||
url: '/daily-work.html',
|
||||
pageName: 'daily_work',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'reports',
|
||||
@@ -92,8 +77,8 @@ class CommonHeader {
|
||||
icon: 'fas fa-chart-bar',
|
||||
url: '/reports.html',
|
||||
pageName: 'reports',
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-50 hover:bg-red-100',
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100',
|
||||
subMenus: [
|
||||
{
|
||||
id: 'reports_daily',
|
||||
@@ -101,7 +86,7 @@ class CommonHeader {
|
||||
icon: 'fas fa-file-excel',
|
||||
url: '/reports-daily.html',
|
||||
pageName: 'reports_daily',
|
||||
color: 'text-green-600'
|
||||
color: 'text-slate-600'
|
||||
},
|
||||
{
|
||||
id: 'reports_weekly',
|
||||
@@ -109,7 +94,7 @@ class CommonHeader {
|
||||
icon: 'fas fa-calendar-week',
|
||||
url: '/reports-weekly.html',
|
||||
pageName: 'reports_weekly',
|
||||
color: 'text-blue-600'
|
||||
color: 'text-slate-600'
|
||||
},
|
||||
{
|
||||
id: 'reports_monthly',
|
||||
@@ -117,7 +102,7 @@ class CommonHeader {
|
||||
icon: 'fas fa-calendar-alt',
|
||||
url: '/reports-monthly.html',
|
||||
pageName: 'reports_monthly',
|
||||
color: 'text-purple-600'
|
||||
color: 'text-slate-600'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -127,17 +112,18 @@ class CommonHeader {
|
||||
icon: 'fas fa-folder-open',
|
||||
url: '/project-management.html',
|
||||
pageName: 'projects_manage',
|
||||
color: 'text-indigo-600',
|
||||
bgColor: 'bg-indigo-50 hover:bg-indigo-100'
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100'
|
||||
},
|
||||
{
|
||||
id: 'users_manage',
|
||||
title: '사용자 관리',
|
||||
icon: 'fas fa-users-cog',
|
||||
url: '/admin.html',
|
||||
url: this._getUserManageUrl(),
|
||||
pageName: 'users_manage',
|
||||
color: 'text-gray-600',
|
||||
bgColor: 'bg-gray-50 hover:bg-gray-100'
|
||||
color: 'text-slate-600',
|
||||
bgColor: 'text-slate-600 hover:bg-slate-100',
|
||||
external: true
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -225,8 +211,8 @@ class CommonHeader {
|
||||
<!-- 로고 및 제목 -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<i class="fas fa-clipboard-check text-2xl text-blue-600 mr-3"></i>
|
||||
<h1 class="text-xl font-bold text-gray-900">작업보고서</h1>
|
||||
<i class="fas fa-shield-halved text-2xl text-slate-700 mr-3"></i>
|
||||
<h1 class="text-xl font-bold text-gray-900">부적합 관리</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -243,7 +229,7 @@ class CommonHeader {
|
||||
<div class="text-sm font-medium text-gray-900">${userDisplayName}</div>
|
||||
<div class="text-xs text-gray-500">${userRole}</div>
|
||||
</div>
|
||||
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
|
||||
<div class="w-8 h-8 bg-slate-600 rounded-full flex items-center justify-center">
|
||||
<span class="text-white text-sm font-semibold">
|
||||
${userDisplayName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
@@ -299,7 +285,7 @@ class CommonHeader {
|
||||
|
||||
// 권한 시스템이 로드되지 않았으면 기본 메뉴만
|
||||
if (!window.canAccessPage) {
|
||||
return ['issues_create', 'issues_view'].includes(menu.id);
|
||||
return ['issues_dashboard', 'issues_inbox'].includes(menu.id);
|
||||
}
|
||||
|
||||
// 메인 메뉴 권한 체크
|
||||
@@ -324,8 +310,8 @@ class CommonHeader {
|
||||
*/
|
||||
generateMenuItemHTML(menu) {
|
||||
const isActive = this.currentPage === menu.id;
|
||||
const activeClass = isActive ? 'bg-blue-100 text-blue-700' : `${menu.bgColor} ${menu.color}`;
|
||||
|
||||
const activeClass = isActive ? 'bg-slate-700 text-white' : `${menu.bgColor} ${menu.color}`;
|
||||
|
||||
// 하위 메뉴가 있는 경우 드롭다운 메뉴 생성
|
||||
if (menu.accessibleSubMenus && menu.accessibleSubMenus.length > 0) {
|
||||
return `
|
||||
@@ -342,7 +328,7 @@ class CommonHeader {
|
||||
<div class="py-1">
|
||||
${menu.accessibleSubMenus.map(subMenu => `
|
||||
<a href="${subMenu.url}"
|
||||
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 ${this.currentPage === subMenu.id ? 'bg-blue-50 text-blue-700' : ''}"
|
||||
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-slate-100 ${this.currentPage === subMenu.id ? 'bg-slate-100 text-slate-800 font-medium' : ''}"
|
||||
data-page="${subMenu.id}"
|
||||
onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')">
|
||||
<i class="${subMenu.icon} mr-3 ${subMenu.color}"></i>
|
||||
@@ -355,9 +341,21 @@ class CommonHeader {
|
||||
`;
|
||||
}
|
||||
|
||||
// 외부 링크 (tkuser 등)
|
||||
if (menu.external) {
|
||||
return `
|
||||
<a href="${menu.url}"
|
||||
class="nav-item flex items-center px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${activeClass}"
|
||||
data-page="${menu.id}">
|
||||
<i class="${menu.icon} mr-2"></i>
|
||||
${menu.title}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
// 일반 메뉴 아이템
|
||||
return `
|
||||
<a href="${menu.url}"
|
||||
<a href="${menu.url}"
|
||||
class="nav-item flex items-center px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${activeClass}"
|
||||
data-page="${menu.id}"
|
||||
onclick="CommonHeader.navigateToPage(event, '${menu.url}', '${menu.id}')">
|
||||
@@ -372,7 +370,7 @@ class CommonHeader {
|
||||
*/
|
||||
generateMobileMenuItemHTML(menu) {
|
||||
const isActive = this.currentPage === menu.id;
|
||||
const activeClass = isActive ? 'bg-blue-50 text-blue-700 border-blue-500' : 'text-gray-700 hover:bg-gray-50';
|
||||
const activeClass = isActive ? 'bg-slate-100 text-slate-800 border-slate-600' : 'text-gray-700 hover:bg-gray-50';
|
||||
|
||||
// 하위 메뉴가 있는 경우
|
||||
if (menu.accessibleSubMenus && menu.accessibleSubMenus.length > 0) {
|
||||
@@ -392,7 +390,7 @@ class CommonHeader {
|
||||
<div class="hidden ml-6 mt-1 space-y-1">
|
||||
${menu.accessibleSubMenus.map(subMenu => `
|
||||
<a href="${subMenu.url}"
|
||||
class="flex items-center px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:bg-gray-100 ${this.currentPage === subMenu.id ? 'bg-blue-50 text-blue-700' : ''}"
|
||||
class="flex items-center px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:bg-slate-100 ${this.currentPage === subMenu.id ? 'bg-slate-100 text-slate-800' : ''}"
|
||||
data-page="${subMenu.id}"
|
||||
onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')">
|
||||
<i class="${subMenu.icon} mr-3 ${subMenu.color}"></i>
|
||||
@@ -684,10 +682,11 @@ class CommonHeader {
|
||||
document.querySelectorAll('.nav-item').forEach(item => {
|
||||
const itemPageId = item.getAttribute('data-page');
|
||||
if (itemPageId === pageId) {
|
||||
item.classList.add('bg-blue-100', 'text-blue-700');
|
||||
item.classList.remove('bg-blue-50', 'hover:bg-blue-100');
|
||||
item.classList.add('bg-slate-700', 'text-white');
|
||||
item.classList.remove('text-slate-600', 'hover:bg-slate-100');
|
||||
} else {
|
||||
item.classList.remove('bg-blue-100', 'text-blue-700');
|
||||
item.classList.remove('bg-slate-700', 'text-white');
|
||||
item.classList.add('text-slate-600');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class PageManager {
|
||||
async checkAuthentication() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
window.location.href = '/index.html';
|
||||
window.location.href = '/issues-dashboard.html';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class PageManager {
|
||||
console.error('인증 실패:', error);
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('currentUser');
|
||||
window.location.href = '/index.html';
|
||||
window.location.href = '/issues-dashboard.html';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ class PageManager {
|
||||
|
||||
// 권한 시스템이 로드되지 않았으면 기본 페이지만 허용
|
||||
if (!window.canAccessPage) {
|
||||
return ['issues_create', 'issues_view'].includes(pageId);
|
||||
return ['issues_dashboard', 'issues_inbox'].includes(pageId);
|
||||
}
|
||||
|
||||
return window.canAccessPage(pageId);
|
||||
@@ -130,11 +130,7 @@ class PageManager {
|
||||
alert('이 페이지에 접근할 권한이 없습니다.');
|
||||
|
||||
// 기본적으로 접근 가능한 페이지로 이동
|
||||
if (window.canAccessPage && window.canAccessPage('issues_view')) {
|
||||
window.location.href = '/issue-view.html';
|
||||
} else {
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
window.location.href = '/issues-dashboard.html';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,7 +246,7 @@ class PageManager {
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
다시 시도
|
||||
</button>
|
||||
<button onclick="window.location.href='/index.html'"
|
||||
<button onclick="window.location.href='/issues-dashboard.html'"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700">
|
||||
홈으로
|
||||
</button>
|
||||
|
||||
@@ -15,13 +15,11 @@ class PagePermissionManager {
|
||||
*/
|
||||
initDefaultPages() {
|
||||
return {
|
||||
'issues_create': { title: '부적합 등록', defaultAccess: true },
|
||||
'issues_view': { title: '부적합 조회', defaultAccess: true },
|
||||
'issues_dashboard': { title: '현황판', defaultAccess: true },
|
||||
'issues_manage': { title: '부적합 관리', defaultAccess: true },
|
||||
'issues_inbox': { title: '수신함', defaultAccess: true },
|
||||
'issues_management': { title: '관리함', defaultAccess: false },
|
||||
'issues_archive': { title: '폐기함', defaultAccess: false },
|
||||
'issues_dashboard': { title: '현황판', defaultAccess: true },
|
||||
'projects_manage': { title: '프로젝트 관리', defaultAccess: false },
|
||||
'daily_work': { title: '일일 공수', defaultAccess: false },
|
||||
'reports': { title: '보고서', defaultAccess: false },
|
||||
@@ -41,15 +39,32 @@ class PagePermissionManager {
|
||||
/**
|
||||
* 사용자별 페이지 권한 로드
|
||||
*/
|
||||
/**
|
||||
* SSO 토큰 직접 읽기 (api.js 로딩 전에도 동작)
|
||||
*/
|
||||
_getToken() {
|
||||
// 1) window.TokenManager (api.js 로딩 완료 시)
|
||||
if (window.TokenManager) return window.TokenManager.getToken();
|
||||
// 2) SSO 쿠키 직접 읽기
|
||||
const match = document.cookie.match(/(?:^|; )sso_token=([^;]*)/);
|
||||
if (match) return decodeURIComponent(match[1]);
|
||||
// 3) localStorage 폴백
|
||||
return localStorage.getItem('sso_token') || localStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
async loadPagePermissions() {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
const userId = this.currentUser.id || this.currentUser.user_id;
|
||||
if (!userId) return;
|
||||
|
||||
try {
|
||||
// API에서 사용자별 페이지 권한 가져오기
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const response = await fetch(`${apiUrl}/users/${this.currentUser.id}/page-permissions`, {
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const token = this._getToken();
|
||||
const response = await fetch(`${apiUrl}/users/${userId}/page-permissions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -199,12 +214,12 @@ class PagePermissionManager {
|
||||
}
|
||||
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/page-permissions/grant`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
'Authorization': `Bearer ${this._getToken()}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
@@ -232,10 +247,10 @@ class PagePermissionManager {
|
||||
*/
|
||||
async getUserPagePermissions(userId) {
|
||||
try {
|
||||
const apiUrl = window.API_BASE_URL || 'http://localhost:16080/api';
|
||||
const apiUrl = window.API_BASE_URL || '/api';
|
||||
const response = await fetch(`${apiUrl}/users/${userId}/page-permissions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
'Authorization': `Bearer ${this._getToken()}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user