fix: tkfb 로그인을 SSO 인증으로 변경
system1-factory의 자체 로그인 폼을 제거하고 게이트웨이 SSO 로그인 페이지(/login)로 리다이렉트하도록 변경. 기존에는 /api/auth/login(system1-api)으로 직접 인증하여 SSO 사용자가 401 오류를 받았음. - index.html: 로그인 폼 제거, SSO 토큰 없으면 /login으로 리다이렉트 - api-base.js: getLoginUrl() 개발환경에서도 SSO 로그인 경로 반환 - api-helper.js: authFetch 401/토큰없음 시 SSO 로그인으로 리다이렉트 - app-init.js: 로그아웃 및 인증실패 시 SSO 로그인으로 리다이렉트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,29 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>(주)테크니컬코리아 생산팀 포털</title>
|
<title>(주)테크니컬코리아 생산팀 포털</title>
|
||||||
<link rel="icon" type="image/png" href="img/favicon.png">
|
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||||
<script>
|
<!-- SW 캐시 강제 해제 (Chrome 대응) -->
|
||||||
window.location.replace('/pages/dashboard.html');
|
<script>
|
||||||
</script>
|
if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(r){r.forEach(function(reg){reg.unregister()});})}
|
||||||
|
if('caches' in window){caches.keys().then(function(k){k.forEach(function(key){caches.delete(key)})})}
|
||||||
|
</script>
|
||||||
|
<script src="/js/api-base.js?v=3"></script>
|
||||||
|
<script>
|
||||||
|
// SSO 토큰 확인
|
||||||
|
var token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
|
||||||
|
if (token && token !== 'undefined' && token !== 'null') {
|
||||||
|
// 이미 로그인된 경우 대시보드로 이동
|
||||||
|
window.location.replace('/pages/dashboard.html');
|
||||||
|
} else {
|
||||||
|
// SSO 로그인 페이지로 리다이렉트 (gateway의 /login)
|
||||||
|
window.location.replace('/login?redirect=' + encodeURIComponent('/pages/dashboard.html'));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<p>로딩 중...</p>
|
<!-- SSO 로그인 페이지로 자동 리다이렉트됩니다 -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -20,9 +20,10 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백)
|
* SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백)
|
||||||
|
* sso_token이 없으면 기존 token도 확인 (하위 호환)
|
||||||
*/
|
*/
|
||||||
window.getSSOToken = function() {
|
window.getSSOToken = function() {
|
||||||
return cookieGet('sso_token') || localStorage.getItem('sso_token');
|
return cookieGet('sso_token') || localStorage.getItem('sso_token') || localStorage.getItem('token');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -30,6 +31,10 @@
|
|||||||
*/
|
*/
|
||||||
window.getSSOUser = function() {
|
window.getSSOUser = function() {
|
||||||
var raw = cookieGet('sso_user') || localStorage.getItem('sso_user');
|
var raw = cookieGet('sso_user') || localStorage.getItem('sso_user');
|
||||||
|
if (!raw) {
|
||||||
|
// 기존 user 키도 확인 (하위 호환)
|
||||||
|
raw = localStorage.getItem('user');
|
||||||
|
}
|
||||||
try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; }
|
try { return raw ? JSON.parse(raw) : null; } catch(e) { return null; }
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,7 +46,8 @@
|
|||||||
if (hostname.includes('technicalkorea.net')) {
|
if (hostname.includes('technicalkorea.net')) {
|
||||||
return window.location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(window.location.href);
|
return window.location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(window.location.href);
|
||||||
}
|
}
|
||||||
return '/login';
|
// 개발 환경: 게이트웨이 SSO 로그인 페이지
|
||||||
|
return '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +60,9 @@
|
|||||||
localStorage.removeItem('sso_token');
|
localStorage.removeItem('sso_token');
|
||||||
localStorage.removeItem('sso_user');
|
localStorage.removeItem('sso_user');
|
||||||
localStorage.removeItem('sso_refresh_token');
|
localStorage.removeItem('sso_refresh_token');
|
||||||
|
// 기존 키도 삭제 (하위 호환)
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
localStorage.removeItem('userPageAccess');
|
localStorage.removeItem('userPageAccess');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,18 @@ const API_BASE_URL = window.API_BASE_URL || 'http://localhost:30005/api';
|
|||||||
|
|
||||||
// 인증 관련 함수들 (직접 구현)
|
// 인증 관련 함수들 (직접 구현)
|
||||||
function getToken() {
|
function getToken() {
|
||||||
const token = localStorage.getItem('sso_token');
|
// SSO 토큰 우선, 기존 token 폴백
|
||||||
|
if (window.getSSOToken) return window.getSSOToken();
|
||||||
|
const token = localStorage.getItem('sso_token') || localStorage.getItem('token');
|
||||||
return token && token !== 'undefined' && token !== 'null' ? token : null;
|
return token && token !== 'undefined' && token !== 'null' ? token : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAuthData() {
|
function clearAuthData() {
|
||||||
|
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
||||||
localStorage.removeItem('sso_token');
|
localStorage.removeItem('sso_token');
|
||||||
localStorage.removeItem('sso_user');
|
localStorage.removeItem('sso_user');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,11 +50,11 @@ async function login(username, password) {
|
|||||||
*/
|
*/
|
||||||
async function authFetch(endpoint, options = {}) {
|
async function authFetch(endpoint, options = {}) {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.error('토큰이 없습니다. 로그인이 필요합니다.');
|
console.error('토큰이 없습니다. 로그인이 필요합니다.');
|
||||||
clearAuthData(); // 인증 정보 정리
|
clearAuthData(); // 인증 정보 정리
|
||||||
window.location.href = '/login'; // 로그인 페이지로 리디렉션
|
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||||
// 에러를 던져서 후속 실행을 중단
|
// 에러를 던져서 후속 실행을 중단
|
||||||
throw new Error('인증 토큰이 없습니다.');
|
throw new Error('인증 토큰이 없습니다.');
|
||||||
}
|
}
|
||||||
@@ -71,7 +76,7 @@ async function authFetch(endpoint, options = {}) {
|
|||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
console.error('인증 실패. 토큰이 만료되었거나 유효하지 않습니다.');
|
console.error('인증 실패. 토큰이 만료되었거나 유효하지 않습니다.');
|
||||||
clearAuthData(); // 만료된 인증 정보 정리
|
clearAuthData(); // 만료된 인증 정보 정리
|
||||||
window.location.href = '/login';
|
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||||
throw new Error('인증에 실패했습니다.');
|
throw new Error('인증에 실패했습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,4 +138,4 @@ window.apiPost = apiPost;
|
|||||||
window.apiPut = apiPut;
|
window.apiPut = apiPut;
|
||||||
window.apiDelete = apiDelete;
|
window.apiDelete = apiDelete;
|
||||||
window.getToken = getToken;
|
window.getToken = getToken;
|
||||||
window.clearAuthData = clearAuthData;
|
window.clearAuthData = clearAuthData;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// /js/app-init.js
|
// /js/app-init.js
|
||||||
// 앱 초기화 - 인증, 네비바, 사이드바를 한 번에 로드
|
// 앱 초기화 - 인증, 네비바, 사이드바를 한 번에 로드
|
||||||
// 모든 페이지에서 이 하나의 스크립트만 로드하면 됨
|
// 모든 페이지에서 이 하나의 스크립트만 로드하면 됨
|
||||||
|
// api-base.js가 먼저 로드되어야 함 (getSSOToken, getSSOUser, clearSSOAuth 등)
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -9,24 +10,29 @@
|
|||||||
const CACHE_DURATION = 10 * 60 * 1000; // 10분
|
const CACHE_DURATION = 10 * 60 * 1000; // 10분
|
||||||
const COMPONENT_CACHE_PREFIX = 'component_v3_';
|
const COMPONENT_CACHE_PREFIX = 'component_v3_';
|
||||||
|
|
||||||
// ===== 인증 함수 (api-base.js의 전역 헬퍼 활용) =====
|
// ===== 인증 함수 (api-base.js의 SSO 함수 활용) =====
|
||||||
function isLoggedIn() {
|
function isLoggedIn() {
|
||||||
var token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
const token = window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
|
||||||
return token && token !== 'undefined' && token !== 'null';
|
return token && token !== 'undefined' && token !== 'null';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUser() {
|
function getUser() {
|
||||||
return window.getSSOUser ? window.getSSOUser() : (function() {
|
if (window.getSSOUser) return window.getSSOUser();
|
||||||
var u = localStorage.getItem('sso_user');
|
const user = localStorage.getItem('sso_user') || localStorage.getItem('user');
|
||||||
return u ? JSON.parse(u) : null;
|
try { return user ? JSON.parse(user) : null; } catch(e) { return null; }
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
function getToken() {
|
||||||
|
return window.getSSOToken ? window.getSSOToken() : (localStorage.getItem('sso_token') || localStorage.getItem('token'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAuthData() {
|
function clearAuthData() {
|
||||||
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
if (window.clearSSOAuth) { window.clearSSOAuth(); return; }
|
||||||
localStorage.removeItem('sso_token');
|
localStorage.removeItem('sso_token');
|
||||||
localStorage.removeItem('sso_user');
|
localStorage.removeItem('sso_user');
|
||||||
localStorage.removeItem('userPageAccess_v2');
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('userPageAccess');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 페이지 권한 캐시 =====
|
// ===== 페이지 권한 캐시 =====
|
||||||
@@ -36,7 +42,7 @@
|
|||||||
if (!currentUser || !currentUser.user_id) return null;
|
if (!currentUser || !currentUser.user_id) return null;
|
||||||
|
|
||||||
// 캐시 확인
|
// 캐시 확인
|
||||||
const cached = localStorage.getItem('userPageAccess_v2');
|
const cached = localStorage.getItem('userPageAccess');
|
||||||
if (cached) {
|
if (cached) {
|
||||||
try {
|
try {
|
||||||
const cacheData = JSON.parse(cached);
|
const cacheData = JSON.parse(cached);
|
||||||
@@ -44,7 +50,7 @@
|
|||||||
return cacheData.pages;
|
return cacheData.pages;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
localStorage.removeItem('userPageAccess_v2');
|
localStorage.removeItem('userPageAccess');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,11 +60,12 @@
|
|||||||
// 새로운 API 호출
|
// 새로운 API 호출
|
||||||
pageAccessPromise = (async () => {
|
pageAccessPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
|
const token = getToken();
|
||||||
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
|
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': 'Bearer ' + (window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'))
|
'Authorization': `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +74,7 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const pages = data.data.pageAccess || [];
|
const pages = data.data.pageAccess || [];
|
||||||
|
|
||||||
localStorage.setItem('userPageAccess_v2', JSON.stringify({
|
localStorage.setItem('userPageAccess', JSON.stringify({
|
||||||
pages: pages,
|
pages: pages,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
}));
|
}));
|
||||||
@@ -87,15 +94,17 @@
|
|||||||
async function getAccessiblePageKeys(currentUser) {
|
async function getAccessiblePageKeys(currentUser) {
|
||||||
const pages = await getPageAccess(currentUser);
|
const pages = await getPageAccess(currentUser);
|
||||||
if (!pages) return [];
|
if (!pages) return [];
|
||||||
return pages.filter(p => p.can_access === 1).map(p => p.page_key);
|
return pages.filter(p => p.can_access == 1).map(p => p.page_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 현재 페이지 키 추출 =====
|
// ===== 현재 페이지 키 추출 =====
|
||||||
// 하위 페이지 → 부모 페이지 키 매핑 (동일 권한 공유)
|
// 하위 페이지 → 부모 페이지 키 매핑 (동일 권한 공유)
|
||||||
var PAGE_KEY_ALIASES = {
|
const PAGE_KEY_ALIASES = {
|
||||||
'work.tbm-create': 'work.tbm',
|
|
||||||
'work.tbm-mobile': 'work.tbm',
|
'work.tbm-mobile': 'work.tbm',
|
||||||
'work.report-create-mobile': 'work.report-create'
|
'work.tbm-create': 'work.tbm',
|
||||||
|
'work.report-create-mobile': 'work.report-create',
|
||||||
|
'admin.equipment-detail': 'admin.equipments',
|
||||||
|
'safety.issue-detail': 'safety.issue-report'
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCurrentPageKey() {
|
function getCurrentPageKey() {
|
||||||
@@ -186,7 +195,6 @@
|
|||||||
async function processSidebar(doc, currentUser, accessiblePageKeys) {
|
async function processSidebar(doc, currentUser, accessiblePageKeys) {
|
||||||
const userRole = (currentUser.role || '').toLowerCase();
|
const userRole = (currentUser.role || '').toLowerCase();
|
||||||
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
||||||
// role 또는 access_level로 관리자 확인
|
|
||||||
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
||||||
accessLevel === 'admin' || accessLevel === 'system';
|
accessLevel === 'admin' || accessLevel === 'system';
|
||||||
|
|
||||||
@@ -212,26 +220,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 크로스 시스템 링크 URL 설정
|
|
||||||
var hostname = window.location.hostname;
|
|
||||||
var protocol = window.location.protocol;
|
|
||||||
var systemUrls = {};
|
|
||||||
if (hostname.includes('technicalkorea.net')) {
|
|
||||||
systemUrls.report = protocol + '//tkreport.technicalkorea.net';
|
|
||||||
systemUrls.nc = protocol + '//tkqc.technicalkorea.net';
|
|
||||||
} else {
|
|
||||||
systemUrls.report = protocol + '//' + hostname + ':30180';
|
|
||||||
systemUrls.nc = protocol + '//' + hostname + ':30280';
|
|
||||||
}
|
|
||||||
doc.querySelectorAll('.cross-system-link').forEach(function(link) {
|
|
||||||
var system = link.getAttribute('data-system');
|
|
||||||
var path = link.getAttribute('data-path');
|
|
||||||
if (systemUrls[system]) {
|
|
||||||
link.setAttribute('href', systemUrls[system] + path);
|
|
||||||
link.setAttribute('target', '_blank');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 저장된 상태 복원 (기본값: 접힌 상태)
|
// 저장된 상태 복원 (기본값: 접힌 상태)
|
||||||
const isCollapsed = localStorage.getItem('sidebarCollapsed') !== 'false';
|
const isCollapsed = localStorage.getItem('sidebarCollapsed') !== 'false';
|
||||||
const sidebar = doc.querySelector('.sidebar-nav');
|
const sidebar = doc.querySelector('.sidebar-nav');
|
||||||
@@ -281,7 +269,8 @@
|
|||||||
logoutButton.addEventListener('click', () => {
|
logoutButton.addEventListener('click', () => {
|
||||||
if (confirm('로그아웃 하시겠습니까?')) {
|
if (confirm('로그아웃 하시겠습니까?')) {
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
if (window.clearSSOAuth) window.clearSSOAuth();
|
||||||
|
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent('/pages/dashboard.html');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -308,7 +297,7 @@
|
|||||||
// ===== 알림 로드 =====
|
// ===== 알림 로드 =====
|
||||||
async function loadNotifications() {
|
async function loadNotifications() {
|
||||||
try {
|
try {
|
||||||
const token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
const token = getToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
const response = await fetch(`${window.API_BASE_URL}/notifications/unread`, {
|
const response = await fetch(`${window.API_BASE_URL}/notifications/unread`, {
|
||||||
@@ -351,11 +340,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const icons = { repair: '🔧', safety: '⚠️', system: '📢', equipment: '🔩', maintenance: '🛠️' };
|
const icons = { repair: '\ud83d\udd27', safety: '\u26a0\ufe0f', system: '\ud83d\udce2', equipment: '\ud83d\udea9', maintenance: '\ud83d\udee0\ufe0f' };
|
||||||
|
|
||||||
list.innerHTML = notifications.slice(0, 5).map(n => `
|
list.innerHTML = notifications.slice(0, 5).map(n => `
|
||||||
<div class="notification-item ${n.is_read ? '' : 'unread'}" data-id="${n.notification_id}" data-url="${n.link_url || ''}">
|
<div class="notification-item ${n.is_read ? '' : 'unread'}" data-id="${n.notification_id}" data-url="${n.link_url || ''}">
|
||||||
<div class="notification-item-icon ${n.type || 'repair'}">${icons[n.type] || '🔔'}</div>
|
<div class="notification-item-icon ${n.type || 'repair'}">${icons[n.type] || '\ud83d\udd14'}</div>
|
||||||
<div class="notification-item-content">
|
<div class="notification-item-content">
|
||||||
<div class="notification-item-title">${escapeHtml(n.title)}</div>
|
<div class="notification-item-title">${escapeHtml(n.title)}</div>
|
||||||
<div class="notification-item-desc">${escapeHtml(n.message || '')}</div>
|
<div class="notification-item-desc">${escapeHtml(n.message || '')}</div>
|
||||||
@@ -367,7 +356,6 @@
|
|||||||
list.querySelectorAll('.notification-item').forEach(item => {
|
list.querySelectorAll('.notification-item').forEach(item => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
const url = item.dataset.url;
|
const url = item.dataset.url;
|
||||||
// 수리 알림은 클릭해도 읽음 처리 안함 (수리 처리 페이지에서 확인 처리해야 함)
|
|
||||||
window.location.href = url || '/pages/admin/notifications.html';
|
window.location.href = url || '/pages/admin/notifications.html';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -388,8 +376,12 @@
|
|||||||
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공
|
function escapeHtml(text) {
|
||||||
var escapeHtml = window.escapeHtml;
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 날짜/시간 업데이트 =====
|
// ===== 날짜/시간 업데이트 =====
|
||||||
function updateDateTime() {
|
function updateDateTime() {
|
||||||
@@ -410,12 +402,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== 날씨 업데이트 =====
|
// ===== 날씨 업데이트 =====
|
||||||
const WEATHER_ICONS = { clear: '☀️', rain: '🌧️', snow: '❄️', heat: '🔥', cold: '🥶', wind: '💨', fog: '🌫️', dust: '😷', cloudy: '⛅', overcast: '☁️' };
|
const WEATHER_ICONS = { clear: '\u2600\ufe0f', rain: '\ud83c\udf27\ufe0f', snow: '\u2744\ufe0f', heat: '\ud83e\udd75', cold: '\ud83e\udd76', wind: '\ud83c\udf2c\ufe0f', fog: '\ud83c\udf2b\ufe0f', dust: '\ud83d\ude37', cloudy: '\u26c5', overcast: '\u2601\ufe0f' };
|
||||||
const WEATHER_NAMES = { clear: '맑음', rain: '비', snow: '눈', heat: '폭염', cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지', cloudy: '구름많음', overcast: '흐림' };
|
const WEATHER_NAMES = { clear: '맑음', rain: '비', snow: '눈', heat: '폭염', cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지', cloudy: '구름많음', overcast: '흐림' };
|
||||||
|
|
||||||
async function updateWeather() {
|
async function updateWeather() {
|
||||||
try {
|
try {
|
||||||
const token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token');
|
const token = getToken();
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
// 캐시 확인
|
// 캐시 확인
|
||||||
@@ -446,7 +438,7 @@
|
|||||||
const descEl = document.getElementById('weatherDesc');
|
const descEl = document.getElementById('weatherDesc');
|
||||||
if (conditions && conditions.length > 0) {
|
if (conditions && conditions.length > 0) {
|
||||||
const primary = conditions[0];
|
const primary = conditions[0];
|
||||||
if (iconEl) iconEl.textContent = WEATHER_ICONS[primary] || '🌤️';
|
if (iconEl) iconEl.textContent = WEATHER_ICONS[primary] || '\ud83c\udf24\ufe0f';
|
||||||
if (descEl) descEl.textContent = WEATHER_NAMES[primary] || '맑음';
|
if (descEl) descEl.textContent = WEATHER_NAMES[primary] || '맑음';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,72 +449,60 @@
|
|||||||
|
|
||||||
// ===== 메인 초기화 =====
|
// ===== 메인 초기화 =====
|
||||||
async function init() {
|
async function init() {
|
||||||
console.log('🚀 app-init 시작');
|
|
||||||
|
|
||||||
// 1. 인증 확인
|
// 1. 인증 확인
|
||||||
if (!isLoggedIn()) {
|
if (!isLoggedIn()) {
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUser = getUser();
|
const currentUser = getUser();
|
||||||
if (!currentUser || !currentUser.username) {
|
if (!currentUser || !currentUser.username) {
|
||||||
clearAuthData();
|
clearAuthData();
|
||||||
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
|
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ 인증 확인:', currentUser.username);
|
|
||||||
|
|
||||||
const userRole = (currentUser.role || '').toLowerCase();
|
const userRole = (currentUser.role || '').toLowerCase();
|
||||||
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
const accessLevel = (currentUser.access_level || '').toLowerCase();
|
||||||
// role 또는 access_level로 관리자 확인
|
|
||||||
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
|
||||||
accessLevel === 'admin' || accessLevel === 'system';
|
accessLevel === 'admin' || accessLevel === 'system';
|
||||||
|
|
||||||
// 2. 페이지 접근 권한 체크 (Admin은 건너뛰기)
|
// 2. 페이지 접근 권한 체크 (Admin은 건너뛰기, API 실패시 허용)
|
||||||
let accessiblePageKeys = [];
|
let accessiblePageKeys = [];
|
||||||
|
const pageKey = getCurrentPageKey();
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
const pageKey = getCurrentPageKey();
|
const pages = await getPageAccess(currentUser);
|
||||||
if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) {
|
if (pages) {
|
||||||
accessiblePageKeys = await getAccessiblePageKeys(currentUser);
|
accessiblePageKeys = pages.filter(p => p.can_access == 1).map(p => p.page_key);
|
||||||
if (!accessiblePageKeys.includes(pageKey)) {
|
if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) {
|
||||||
alert('이 페이지에 접근할 권한이 없습니다.');
|
if (!accessiblePageKeys.includes(pageKey)) {
|
||||||
window.location.href = '/pages/dashboard.html';
|
alert('이 페이지에 접근할 권한이 없습니다.');
|
||||||
return;
|
window.location.href = '/pages/dashboard.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 네비바 로드 (모바일이면 사이드바 스킵)
|
// 3. 사이드바 컨테이너 생성 (없으면)
|
||||||
var isMobile = window.innerWidth <= 768;
|
let sidebarContainer = document.getElementById('sidebar-container');
|
||||||
|
if (!sidebarContainer) {
|
||||||
if (!isMobile) {
|
sidebarContainer = document.createElement('div');
|
||||||
// 데스크톱: 사이드바 컨테이너 생성 및 로드
|
sidebarContainer.id = 'sidebar-container';
|
||||||
let sidebarContainer = document.getElementById('sidebar-container');
|
document.body.prepend(sidebarContainer);
|
||||||
if (!sidebarContainer) {
|
|
||||||
sidebarContainer = document.createElement('div');
|
|
||||||
sidebarContainer.id = 'sidebar-container';
|
|
||||||
document.body.prepend(sidebarContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('📥 컴포넌트 로딩 시작 (데스크톱: 네비바+사이드바)');
|
|
||||||
await Promise.all([
|
|
||||||
loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys)),
|
|
||||||
loadComponent('sidebar-nav', '#sidebar-container', (doc) => processSidebar(doc, currentUser, accessiblePageKeys))
|
|
||||||
]);
|
|
||||||
|
|
||||||
setupNavbarEvents();
|
|
||||||
setupSidebarEvents();
|
|
||||||
document.body.classList.add('has-sidebar');
|
|
||||||
} else {
|
|
||||||
// 모바일: 네비바만 로드, 사이드바 없음
|
|
||||||
console.log('📥 컴포넌트 로딩 시작 (모바일: 네비바만)');
|
|
||||||
await loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys));
|
|
||||||
setupNavbarEvents();
|
|
||||||
}
|
}
|
||||||
console.log('✅ 컴포넌트 로딩 완료');
|
|
||||||
|
// 4. 네비바와 사이드바 동시 로드
|
||||||
|
await Promise.all([
|
||||||
|
loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys)),
|
||||||
|
loadComponent('sidebar-nav', '#sidebar-container', (doc) => processSidebar(doc, currentUser, accessiblePageKeys))
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5. 이벤트 설정
|
||||||
|
setupNavbarEvents();
|
||||||
|
setupSidebarEvents();
|
||||||
|
document.body.classList.add('has-sidebar');
|
||||||
|
|
||||||
// 6. 페이지 전환 로딩 인디케이터 설정
|
// 6. 페이지 전환 로딩 인디케이터 설정
|
||||||
setupPageTransitionLoader();
|
setupPageTransitionLoader();
|
||||||
@@ -537,73 +517,10 @@
|
|||||||
// 9. 알림 로드 (30초마다 갱신)
|
// 9. 알림 로드 (30초마다 갱신)
|
||||||
setTimeout(loadNotifications, 200);
|
setTimeout(loadNotifications, 200);
|
||||||
setInterval(loadNotifications, 30000);
|
setInterval(loadNotifications, 30000);
|
||||||
|
|
||||||
// 10. PWA 설정 (manifest + 서비스 워커 + iOS 메타태그)
|
|
||||||
setupPWA();
|
|
||||||
|
|
||||||
console.log('✅ app-init 완료');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== PWA 설정 =====
|
|
||||||
function setupPWA() {
|
|
||||||
// manifest.json 동적 추가
|
|
||||||
if (!document.querySelector('link[rel="manifest"]')) {
|
|
||||||
var manifest = document.createElement('link');
|
|
||||||
manifest.rel = 'manifest';
|
|
||||||
manifest.href = '/manifest.json';
|
|
||||||
document.head.appendChild(manifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// iOS 홈 화면 앱 메타태그
|
|
||||||
if (!document.querySelector('meta[name="apple-mobile-web-app-capable"]')) {
|
|
||||||
var metaTags = [
|
|
||||||
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
|
||||||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'default' },
|
|
||||||
{ name: 'apple-mobile-web-app-title', content: 'TK공장' },
|
|
||||||
{ name: 'theme-color', content: '#1e40af' }
|
|
||||||
];
|
|
||||||
metaTags.forEach(function(tag) {
|
|
||||||
var meta = document.createElement('meta');
|
|
||||||
meta.name = tag.name;
|
|
||||||
meta.content = tag.content;
|
|
||||||
document.head.appendChild(meta);
|
|
||||||
});
|
|
||||||
|
|
||||||
// iOS 아이콘
|
|
||||||
var appleIcon = document.createElement('link');
|
|
||||||
appleIcon.rel = 'apple-touch-icon';
|
|
||||||
appleIcon.href = '/img/icon-192x192.png';
|
|
||||||
document.head.appendChild(appleIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 서비스 워커 등록 (킬스위치 포함)
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
// 킬스위치: ?sw-kill 파라미터로 서비스 워커 해제
|
|
||||||
if (window.location.search.includes('sw-kill')) {
|
|
||||||
navigator.serviceWorker.getRegistrations().then(function(regs) {
|
|
||||||
regs.forEach(function(r) { r.unregister(); });
|
|
||||||
caches.keys().then(function(keys) {
|
|
||||||
keys.forEach(function(k) { caches.delete(k); });
|
|
||||||
});
|
|
||||||
console.log('SW 해제 완료');
|
|
||||||
window.location.replace(window.location.pathname);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.serviceWorker.register('/sw.js')
|
|
||||||
.then(function(reg) {
|
|
||||||
console.log('SW 등록 완료');
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
console.warn('SW 등록 실패:', err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 페이지 전환 로딩 인디케이터 =====
|
// ===== 페이지 전환 로딩 인디케이터 =====
|
||||||
function setupPageTransitionLoader() {
|
function setupPageTransitionLoader() {
|
||||||
// 로딩 바 스타일 추가
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
#page-loader {
|
#page-loader {
|
||||||
@@ -634,12 +551,10 @@
|
|||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
// 로딩 바 엘리먼트 생성
|
|
||||||
const loader = document.createElement('div');
|
const loader = document.createElement('div');
|
||||||
loader.id = 'page-loader';
|
loader.id = 'page-loader';
|
||||||
document.body.appendChild(loader);
|
document.body.appendChild(loader);
|
||||||
|
|
||||||
// 모든 내부 링크에 클릭 이벤트 추가
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const link = e.target.closest('a');
|
const link = e.target.closest('a');
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
@@ -647,19 +562,14 @@
|
|||||||
const href = link.getAttribute('href');
|
const href = link.getAttribute('href');
|
||||||
if (!href) return;
|
if (!href) return;
|
||||||
|
|
||||||
// 외부 링크, 해시 링크, javascript: 링크 제외
|
|
||||||
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('javascript:')) return;
|
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('javascript:')) return;
|
||||||
|
|
||||||
// 새 탭 링크 제외
|
|
||||||
if (link.target === '_blank') return;
|
if (link.target === '_blank') return;
|
||||||
|
|
||||||
// 로딩 시작
|
|
||||||
loader.classList.remove('done');
|
loader.classList.remove('done');
|
||||||
loader.classList.add('loading');
|
loader.classList.add('loading');
|
||||||
document.body.classList.add('page-loading');
|
document.body.classList.add('page-loading');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 페이지 떠날 때 완료 표시
|
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
const loader = document.getElementById('page-loader');
|
const loader = document.getElementById('page-loader');
|
||||||
if (loader) {
|
if (loader) {
|
||||||
@@ -677,5 +587,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 전역 노출 (필요시)
|
// 전역 노출 (필요시)
|
||||||
window.appInit = { getUser, clearAuthData, isLoggedIn };
|
window.appInit = { getUser, getToken, clearAuthData, isLoggedIn };
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user