fix: 전 시스템 Chrome 무한 로그인 루프 해결 및 role 대소문자 통일

- gateway: 로그인 페이지 자동 리다이렉트 시 SSO 쿠키 재설정 + Cache-Control no-store
- tkreport(system2): SW 해제, 401 핸들러 리다이렉트 제거, 루프 방지, localStorage 백업
- TKQC 모바일(system3): mCheckAuth를 authManager 위임으로 변경, 루프 방지
- TKQC 공통(system3): api.js 로그인 URL 캐시 버스팅, auth-manager localStorage 백업
- tkuser: SW 해제, 401 핸들러 수정, 루프 방지, localStorage 백업, requireAdmin role 소문자 통일
- system1: 작업보고서 admin role 대소문자 무시, refresh 토큰에 role 필드 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-09 14:10:46 +09:00
parent df0a125faa
commit 5aeda43605
18 changed files with 144 additions and 49 deletions

View File

@@ -1,3 +1,9 @@
/* ===== 서비스 워커 해제 (캐시 간섭으로 인한 인증 루프 방지) ===== */
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function(regs) { regs.forEach(function(r) { r.unregister(); }); });
if (typeof caches !== 'undefined') { caches.keys().then(function(ns) { ns.forEach(function(n) { caches.delete(n); }); }); }
}
/* ===== Config ===== */
const API_BASE = '/api';
@@ -7,16 +13,26 @@ function _cookieRemove(n) { let c = n + '=; path=/; max-age=0'; if (location.hos
function getToken() { return _cookieGet('sso_token') || localStorage.getItem('sso_token'); }
function getLoginUrl() {
const h = location.hostname;
if (h.includes('technicalkorea.net')) return location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(location.href);
return location.protocol + '//' + h + ':30000/login?redirect=' + encodeURIComponent(location.href);
const t = Date.now(); // 캐시 버스팅
if (h.includes('technicalkorea.net')) return location.protocol + '//tkfb.technicalkorea.net/login?redirect=' + encodeURIComponent(location.href) + '&_t=' + t;
return location.protocol + '//' + h + ':30000/login?redirect=' + encodeURIComponent(location.href) + '&_t=' + t;
}
function decodeToken(t) { try { return JSON.parse(atob(t.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))); } catch { return null; } }
/* ===== 리다이렉트 루프 방지 ===== */
const _REDIRECT_KEY = '_sso_redirect_ts';
function _safeRedirect() {
const last = parseInt(sessionStorage.getItem(_REDIRECT_KEY) || '0', 10);
if (Date.now() - last < 5000) { console.warn('[tkuser] 리다이렉트 루프 감지'); return; }
sessionStorage.setItem(_REDIRECT_KEY, String(Date.now()));
location.href = getLoginUrl();
}
/* ===== API ===== */
async function api(path, opts = {}) {
const token = getToken();
const res = await fetch(API_BASE + path, { ...opts, headers: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '', ...(opts.headers||{}) } });
if (res.status === 401) { location.href = getLoginUrl(); throw new Error('인증 만료'); }
if (res.status === 401) { throw new Error('인증 만료'); }
const data = await res.json();
if (!res.ok) throw new Error(data.error || data.detail || '요청 실패');
return data;
@@ -68,23 +84,31 @@ let currentUser = null;
/* ===== Init ===== */
async function init() {
const token = getToken();
if (!token) { location.href = getLoginUrl(); return; }
if (!token) { _safeRedirect(); return; }
const decoded = decodeToken(token);
if (!decoded) { location.href = getLoginUrl(); return; }
if (!decoded) { _safeRedirect(); return; }
sessionStorage.removeItem(_REDIRECT_KEY);
currentUser = { id: decoded.user_id||decoded.id, username: decoded.username||decoded.sub, name: decoded.name||decoded.full_name, role: decoded.role||decoded.access_level };
// 쿠키에서 읽었으면 localStorage에도 백업 (다음 방문 시 쿠키 소실 대비)
if (!localStorage.getItem('sso_token')) localStorage.setItem('sso_token', token);
currentUser = { id: decoded.user_id||decoded.id, username: decoded.username||decoded.sub, name: decoded.name||decoded.full_name, role: (decoded.role||decoded.access_level||'').toLowerCase() };
const dn = currentUser.name || currentUser.username;
document.getElementById('headerUserName').textContent = dn;
document.getElementById('headerUserRole').textContent = currentUser.role === 'admin' ? '관리자' : '사용자';
document.getElementById('headerUserAvatar').textContent = dn.charAt(0).toUpperCase();
if (currentUser.role === 'admin') {
document.getElementById('tabNav').classList.remove('hidden');
document.getElementById('adminSection').classList.remove('hidden');
await loadDepartmentsCache();
await loadUsers();
} else {
document.getElementById('passwordChangeSection').classList.remove('hidden');
try {
if (currentUser.role === 'admin' || currentUser.role === 'system') {
document.getElementById('tabNav').classList.remove('hidden');
document.getElementById('adminSection').classList.remove('hidden');
await loadDepartmentsCache();
await loadUsers();
} else {
document.getElementById('passwordChangeSection').classList.remove('hidden');
}
} catch (e) {
console.error('[tkuser] init 오류:', e);
}
setTimeout(() => document.querySelector('.fade-in').classList.add('visible'), 50);
}