From 8fd74ad22fc5d6674815a8e57231e8406956c63f Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Wed, 4 Mar 2026 14:41:27 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20tkfb=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=9D=84=20SSO=20=EC=9D=B8=EC=A6=9D=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- system1-factory/web/index.html | 30 +++- system1-factory/web/js/api-base.js | 13 +- system1-factory/web/js/api-helper.js | 15 +- system1-factory/web/js/app-init.js | 228 ++++++++------------------- 4 files changed, 112 insertions(+), 174 deletions(-) diff --git a/system1-factory/web/index.html b/system1-factory/web/index.html index 96a1ad0..7b93a9a 100644 --- a/system1-factory/web/index.html +++ b/system1-factory/web/index.html @@ -1,15 +1,29 @@ - - - (주)테크니컬코리아 생산팀 포털 - - + + + (주)테크니컬코리아 생산팀 포털 + + + + + -

로딩 중...

+ diff --git a/system1-factory/web/js/api-base.js b/system1-factory/web/js/api-base.js index fb4952a..f90b434 100644 --- a/system1-factory/web/js/api-base.js +++ b/system1-factory/web/js/api-base.js @@ -20,9 +20,10 @@ /** * SSO 토큰 가져오기 (쿠키 우선, localStorage 폴백) + * sso_token이 없으면 기존 token도 확인 (하위 호환) */ 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() { 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; } }; @@ -41,7 +46,8 @@ if (hostname.includes('technicalkorea.net')) { 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_user'); localStorage.removeItem('sso_refresh_token'); + // 기존 키도 삭제 (하위 호환) + localStorage.removeItem('token'); + localStorage.removeItem('user'); localStorage.removeItem('userPageAccess'); }; diff --git a/system1-factory/web/js/api-helper.js b/system1-factory/web/js/api-helper.js index ce77e84..3987901 100644 --- a/system1-factory/web/js/api-helper.js +++ b/system1-factory/web/js/api-helper.js @@ -6,13 +6,18 @@ const API_BASE_URL = window.API_BASE_URL || 'http://localhost:30005/api'; // 인증 관련 함수들 (직접 구현) 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; } function clearAuthData() { + if (window.clearSSOAuth) { window.clearSSOAuth(); return; } localStorage.removeItem('sso_token'); localStorage.removeItem('sso_user'); + localStorage.removeItem('token'); + localStorage.removeItem('user'); } /** @@ -45,11 +50,11 @@ async function login(username, password) { */ async function authFetch(endpoint, options = {}) { const token = getToken(); - + if (!token) { console.error('토큰이 없습니다. 로그인이 필요합니다.'); clearAuthData(); // 인증 정보 정리 - window.location.href = '/login'; // 로그인 페이지로 리디렉션 + window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href); // 에러를 던져서 후속 실행을 중단 throw new Error('인증 토큰이 없습니다.'); } @@ -71,7 +76,7 @@ async function authFetch(endpoint, options = {}) { if (response.status === 401) { console.error('인증 실패. 토큰이 만료되었거나 유효하지 않습니다.'); clearAuthData(); // 만료된 인증 정보 정리 - window.location.href = '/login'; + window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href); throw new Error('인증에 실패했습니다.'); } @@ -133,4 +138,4 @@ window.apiPost = apiPost; window.apiPut = apiPut; window.apiDelete = apiDelete; window.getToken = getToken; -window.clearAuthData = clearAuthData; \ No newline at end of file +window.clearAuthData = clearAuthData; diff --git a/system1-factory/web/js/app-init.js b/system1-factory/web/js/app-init.js index 086b118..f07c54a 100644 --- a/system1-factory/web/js/app-init.js +++ b/system1-factory/web/js/app-init.js @@ -1,6 +1,7 @@ // /js/app-init.js // 앱 초기화 - 인증, 네비바, 사이드바를 한 번에 로드 // 모든 페이지에서 이 하나의 스크립트만 로드하면 됨 +// api-base.js가 먼저 로드되어야 함 (getSSOToken, getSSOUser, clearSSOAuth 등) (function() { 'use strict'; @@ -9,24 +10,29 @@ const CACHE_DURATION = 10 * 60 * 1000; // 10분 const COMPONENT_CACHE_PREFIX = 'component_v3_'; - // ===== 인증 함수 (api-base.js의 전역 헬퍼 활용) ===== + // ===== 인증 함수 (api-base.js의 SSO 함수 활용) ===== 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'; } function getUser() { - return window.getSSOUser ? window.getSSOUser() : (function() { - var u = localStorage.getItem('sso_user'); - return u ? JSON.parse(u) : null; - })(); + if (window.getSSOUser) return window.getSSOUser(); + const user = localStorage.getItem('sso_user') || localStorage.getItem('user'); + 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() { if (window.clearSSOAuth) { window.clearSSOAuth(); return; } localStorage.removeItem('sso_token'); 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; // 캐시 확인 - const cached = localStorage.getItem('userPageAccess_v2'); + const cached = localStorage.getItem('userPageAccess'); if (cached) { try { const cacheData = JSON.parse(cached); @@ -44,7 +50,7 @@ return cacheData.pages; } } catch (e) { - localStorage.removeItem('userPageAccess_v2'); + localStorage.removeItem('userPageAccess'); } } @@ -54,11 +60,12 @@ // 새로운 API 호출 pageAccessPromise = (async () => { try { + const token = getToken(); const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, { method: 'GET', headers: { '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 pages = data.data.pageAccess || []; - localStorage.setItem('userPageAccess_v2', JSON.stringify({ + localStorage.setItem('userPageAccess', JSON.stringify({ pages: pages, timestamp: Date.now() })); @@ -87,15 +94,17 @@ async function getAccessiblePageKeys(currentUser) { const pages = await getPageAccess(currentUser); 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 = { - 'work.tbm-create': 'work.tbm', + const PAGE_KEY_ALIASES = { '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() { @@ -186,7 +195,6 @@ async function processSidebar(doc, currentUser, accessiblePageKeys) { const userRole = (currentUser.role || '').toLowerCase(); const accessLevel = (currentUser.access_level || '').toLowerCase(); - // role 또는 access_level로 관리자 확인 const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === '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 sidebar = doc.querySelector('.sidebar-nav'); @@ -281,7 +269,8 @@ logoutButton.addEventListener('click', () => { if (confirm('로그아웃 하시겠습니까?')) { 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() { try { - const token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'); + const token = getToken(); if (!token) return; const response = await fetch(`${window.API_BASE_URL}/notifications/unread`, { @@ -351,11 +340,11 @@ 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 => `
-
${icons[n.type] || '🔔'}
+
${icons[n.type] || '\ud83d\udd14'}
${escapeHtml(n.title)}
${escapeHtml(n.message || '')}
@@ -367,7 +356,6 @@ list.querySelectorAll('.notification-item').forEach(item => { item.addEventListener('click', () => { const url = item.dataset.url; - // 수리 알림은 클릭해도 읽음 처리 안함 (수리 처리 페이지에서 확인 처리해야 함) window.location.href = url || '/pages/admin/notifications.html'; }); }); @@ -388,8 +376,12 @@ return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }); } - // escapeHtml은 api-base.js에서 window.escapeHtml로 전역 제공 - var escapeHtml = window.escapeHtml; + function escapeHtml(text) { + if (!text) return ''; + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } // ===== 날짜/시간 업데이트 ===== 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: '흐림' }; async function updateWeather() { try { - const token = window.getSSOToken ? window.getSSOToken() : localStorage.getItem('sso_token'); + const token = getToken(); if (!token) return; // 캐시 확인 @@ -446,7 +438,7 @@ const descEl = document.getElementById('weatherDesc'); if (conditions && conditions.length > 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] || '맑음'; } } @@ -457,72 +449,60 @@ // ===== 메인 초기화 ===== async function init() { - console.log('🚀 app-init 시작'); - // 1. 인증 확인 if (!isLoggedIn()) { clearAuthData(); - window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login'; + window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href); return; } const currentUser = getUser(); if (!currentUser || !currentUser.username) { clearAuthData(); - window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login'; + window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login?redirect=' + encodeURIComponent(window.location.href); return; } - console.log('✅ 인증 확인:', currentUser.username); - const userRole = (currentUser.role || '').toLowerCase(); const accessLevel = (currentUser.access_level || '').toLowerCase(); - // role 또는 access_level로 관리자 확인 const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' || accessLevel === 'admin' || accessLevel === 'system'; - // 2. 페이지 접근 권한 체크 (Admin은 건너뛰기) + // 2. 페이지 접근 권한 체크 (Admin은 건너뛰기, API 실패시 허용) let accessiblePageKeys = []; + const pageKey = getCurrentPageKey(); if (!isAdmin) { - const pageKey = getCurrentPageKey(); - if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) { - accessiblePageKeys = await getAccessiblePageKeys(currentUser); - if (!accessiblePageKeys.includes(pageKey)) { - alert('이 페이지에 접근할 권한이 없습니다.'); - window.location.href = '/pages/dashboard.html'; - return; + const pages = await getPageAccess(currentUser); + if (pages) { + accessiblePageKeys = pages.filter(p => p.can_access == 1).map(p => p.page_key); + if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) { + if (!accessiblePageKeys.includes(pageKey)) { + alert('이 페이지에 접근할 권한이 없습니다.'); + window.location.href = '/pages/dashboard.html'; + return; + } } } } - // 3. 네비바 로드 (모바일이면 사이드바 스킵) - var isMobile = window.innerWidth <= 768; - - if (!isMobile) { - // 데스크톱: 사이드바 컨테이너 생성 및 로드 - let sidebarContainer = document.getElementById('sidebar-container'); - 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(); + // 3. 사이드바 컨테이너 생성 (없으면) + let sidebarContainer = document.getElementById('sidebar-container'); + if (!sidebarContainer) { + sidebarContainer = document.createElement('div'); + sidebarContainer.id = 'sidebar-container'; + document.body.prepend(sidebarContainer); } - 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. 페이지 전환 로딩 인디케이터 설정 setupPageTransitionLoader(); @@ -537,73 +517,10 @@ // 9. 알림 로드 (30초마다 갱신) setTimeout(loadNotifications, 200); 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() { - // 로딩 바 스타일 추가 const style = document.createElement('style'); style.textContent = ` #page-loader { @@ -634,12 +551,10 @@ `; document.head.appendChild(style); - // 로딩 바 엘리먼트 생성 const loader = document.createElement('div'); loader.id = 'page-loader'; document.body.appendChild(loader); - // 모든 내부 링크에 클릭 이벤트 추가 document.addEventListener('click', (e) => { const link = e.target.closest('a'); if (!link) return; @@ -647,19 +562,14 @@ const href = link.getAttribute('href'); if (!href) return; - // 외부 링크, 해시 링크, javascript: 링크 제외 if (href.startsWith('http') || href.startsWith('#') || href.startsWith('javascript:')) return; - - // 새 탭 링크 제외 if (link.target === '_blank') return; - // 로딩 시작 loader.classList.remove('done'); loader.classList.add('loading'); document.body.classList.add('page-loading'); }); - // 페이지 떠날 때 완료 표시 window.addEventListener('beforeunload', () => { const loader = document.getElementById('page-loader'); if (loader) { @@ -677,5 +587,5 @@ } // 전역 노출 (필요시) - window.appInit = { getUser, clearAuthData, isLoggedIn }; + window.appInit = { getUser, getToken, clearAuthData, isLoggedIn }; })();