diff --git a/gateway/html/login.html b/gateway/html/login.html
index 709e503..20ede4e 100644
--- a/gateway/html/login.html
+++ b/gateway/html/login.html
@@ -161,8 +161,7 @@
// redirect 파라미터가 있으면 해당 URL로, 없으면 포털로
var redirect = new URLSearchParams(location.search).get('redirect');
- // Open redirect 방지: 같은 origin의 상대 경로만 허용
- if (redirect && /^\/[a-zA-Z0-9]/.test(redirect) && !redirect.includes('://') && !redirect.includes('//')) {
+ if (redirect && isSafeRedirect(redirect)) {
window.location.href = redirect;
} else {
window.location.href = '/';
@@ -176,6 +175,22 @@
}
}
+ // 안전한 리다이렉트인지 확인 (같은 도메인 상대 경로 또는 *.technicalkorea.net)
+ function isSafeRedirect(url) {
+ if (!url) return false;
+ // 상대 경로
+ if (/^\/[a-zA-Z0-9]/.test(url) && !url.includes('://') && !url.includes('//')) {
+ return true;
+ }
+ // technicalkorea.net 서브도메인 절대 URL
+ try {
+ var parsed = new URL(url);
+ return parsed.hostname.endsWith('.technicalkorea.net') || parsed.hostname === 'technicalkorea.net';
+ } catch (e) {
+ return false;
+ }
+ }
+
// 토큰 만료 확인
function isTokenValid(token) {
try {
@@ -191,7 +206,7 @@
if (existingToken && existingToken !== 'undefined' && existingToken !== 'null') {
if (isTokenValid(existingToken)) {
var redirect = new URLSearchParams(location.search).get('redirect');
- window.location.href = redirect || '/';
+ window.location.href = (redirect && isSafeRedirect(redirect)) ? redirect : '/';
} else {
// 만료된 토큰 정리
ssoCookie.remove('sso_token');
diff --git a/system3-nonconformance/web/static/js/core/auth-manager.js b/system3-nonconformance/web/static/js/core/auth-manager.js
index 7f806bf..39eafc5 100644
--- a/system3-nonconformance/web/static/js/core/auth-manager.js
+++ b/system3-nonconformance/web/static/js/core/auth-manager.js
@@ -1,3 +1,15 @@
+// 서비스 워커 해제 (캐시 간섭으로 인한 인증 루프 방지)
+if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.getRegistrations().then(function(registrations) {
+ registrations.forEach(function(registration) { registration.unregister(); });
+ });
+ if (typeof caches !== 'undefined') {
+ caches.keys().then(function(names) {
+ names.forEach(function(name) { caches.delete(name); });
+ });
+ }
+}
+
/**
* 중앙화된 인증 관리자
* 페이지 간 이동 시 불필요한 API 호출을 방지하고 인증 상태를 효율적으로 관리
diff --git a/system3-nonconformance/web/static/js/core/page-preloader.js b/system3-nonconformance/web/static/js/core/page-preloader.js
index 486d02b..15c40c0 100644
--- a/system3-nonconformance/web/static/js/core/page-preloader.js
+++ b/system3-nonconformance/web/static/js/core/page-preloader.js
@@ -18,12 +18,12 @@ class PagePreloader {
init() {
// 유휴 시간에 프리로딩 시작
this.schedulePreloading();
-
+
// 링크 호버 시 프리로딩
this.setupHoverPreloading();
-
- // 서비스 워커 등록 (캐싱용)
- this.registerServiceWorker();
+
+ // 기존 서비스 워커 해제 (캐시 문제 방지)
+ this.unregisterServiceWorker();
}
/**
@@ -274,14 +274,20 @@ class PagePreloader {
}
/**
- * 서비스 워커 등록
+ * 기존 서비스 워커 해제 및 캐시 정리
*/
- async registerServiceWorker() {
+ async unregisterServiceWorker() {
if ('serviceWorker' in navigator) {
try {
- const registration = await navigator.serviceWorker.register('/sw.js');
+ const registrations = await navigator.serviceWorker.getRegistrations();
+ for (const registration of registrations) {
+ await registration.unregister();
+ }
+ // 모든 캐시 삭제
+ const cacheNames = await caches.keys();
+ await Promise.all(cacheNames.map(name => caches.delete(name)));
} catch (error) {
- console.log('서비스 워커 등록 실패:', error);
+ // 무시
}
}
}
diff --git a/system3-nonconformance/web/sw.js b/system3-nonconformance/web/sw.js
index eca27e4..bd9035b 100644
--- a/system3-nonconformance/web/sw.js
+++ b/system3-nonconformance/web/sw.js
@@ -1,349 +1,21 @@
/**
- * 서비스 워커 - 페이지 및 리소스 캐싱
- * M-Project 작업보고서 시스템
+ * 서비스 워커 정리용
+ * 기존 캐시를 모두 삭제하고 자신을 비활성화합니다.
*/
-const CACHE_NAME = 'mproject-v1.1.0';
-const STATIC_CACHE = 'mproject-static-v1.1.0';
-const DYNAMIC_CACHE = 'mproject-dynamic-v1.1.0';
-
-// 캐시할 정적 리소스
-const STATIC_ASSETS = [
- '/',
- '/app.html',
- '/issue-view.html',
- '/issues-dashboard.html',
- '/issues-inbox.html',
- '/issues-management.html',
- '/issues-archive.html',
- '/ai-assistant.html',
- '/reports.html',
- '/reports-daily.html',
- '/reports-weekly.html',
- '/reports-monthly.html',
- '/static/js/api.js',
- '/static/js/app.js',
- '/static/js/core/permissions.js',
- '/static/js/core/auth-manager.js',
- '/static/js/components/common-header.js',
- '/static/js/core/page-manager.js',
- '/static/js/core/page-preloader.js',
- '/static/js/date-utils.js',
- '/static/js/image-utils.js',
- 'https://cdn.tailwindcss.com',
- 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'
-];
-
-// 캐시 전략별 URL 패턴
-const CACHE_STRATEGIES = {
- // 네트워크 우선 (API 호출)
- networkFirst: [
- /\/api\//,
- /\/auth\//
- ],
-
- // 캐시 우선 (정적 리소스)
- cacheFirst: [
- /\.css$/,
- /\.js$/,
- /\.png$/,
- /\.jpg$/,
- /\.jpeg$/,
- /\.gif$/,
- /\.svg$/,
- /\.woff$/,
- /\.woff2$/,
- /cdn\.tailwindcss\.com/,
- /cdnjs\.cloudflare\.com/
- ],
-
- // 스테일 허용 (HTML 페이지)
- staleWhileRevalidate: [
- /\.html$/,
- /\/$/
- ]
-};
-
-/**
- * 서비스 워커 설치
- */
self.addEventListener('install', (event) => {
- console.log(' 서비스 워커 설치 중...');
-
- event.waitUntil(
- caches.open(STATIC_CACHE)
- .then((cache) => {
- console.log(' 정적 리소스 캐싱 중...');
- return cache.addAll(STATIC_ASSETS);
- })
- .then(() => {
- console.log(' 서비스 워커 설치 완료');
- return self.skipWaiting();
- })
- .catch((error) => {
- console.error(' 서비스 워커 설치 실패:', error);
- })
- );
+ self.skipWaiting();
});
-/**
- * 서비스 워커 활성화
- */
self.addEventListener('activate', (event) => {
- console.log(' 서비스 워커 활성화 중...');
-
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
- cacheNames.map((cacheName) => {
- // 이전 버전 캐시 삭제
- if (cacheName !== STATIC_CACHE &&
- cacheName !== DYNAMIC_CACHE &&
- cacheName !== CACHE_NAME) {
- console.log(' 이전 캐시 삭제:', cacheName);
- return caches.delete(cacheName);
- }
- })
+ cacheNames.map((cacheName) => caches.delete(cacheName))
);
})
- .then(() => {
- console.log(' 서비스 워커 활성화 완료');
- return self.clients.claim();
- })
+ .then(() => self.clients.claim())
+ .then(() => self.registration.unregister())
);
});
-
-/**
- * 네트워크 요청 가로채기
- */
-self.addEventListener('fetch', (event) => {
- const { request } = event;
- const url = new URL(request.url);
-
- // POST 등 GET 이외 요청은 캐시 불가 → 기본 처리
- if (request.method !== 'GET') {
- return;
- }
-
- // CORS 요청이나 외부 도메인은 기본 처리
- if (url.origin !== location.origin && !isCDNResource(url)) {
- return;
- }
-
- // 캐시 전략 결정
- const strategy = getCacheStrategy(request.url);
-
- event.respondWith(
- handleRequest(request, strategy)
- );
-});
-
-/**
- * 요청 처리 (캐시 전략별)
- */
-async function handleRequest(request, strategy) {
- try {
- switch (strategy) {
- case 'networkFirst':
- return await networkFirst(request);
- case 'cacheFirst':
- return await cacheFirst(request);
- case 'staleWhileRevalidate':
- return await staleWhileRevalidate(request);
- default:
- return await fetch(request);
- }
- } catch (error) {
- console.error('요청 처리 실패:', request.url, error);
- return await handleOffline(request);
- }
-}
-
-/**
- * 네트워크 우선 전략
- */
-async function networkFirst(request) {
- try {
- const networkResponse = await fetch(request);
-
- // 성공적인 응답만 캐시
- if (networkResponse.ok) {
- const cache = await caches.open(DYNAMIC_CACHE);
- cache.put(request, networkResponse.clone());
- }
-
- return networkResponse;
- } catch (error) {
- // 네트워크 실패 시 캐시에서 반환
- const cachedResponse = await caches.match(request);
- if (cachedResponse) {
- return cachedResponse;
- }
- throw error;
- }
-}
-
-/**
- * 캐시 우선 전략
- */
-async function cacheFirst(request) {
- const cachedResponse = await caches.match(request);
-
- if (cachedResponse) {
- return cachedResponse;
- }
-
- // 캐시에 없으면 네트워크에서 가져와서 캐시
- const networkResponse = await fetch(request);
-
- if (networkResponse.ok) {
- const cache = await caches.open(STATIC_CACHE);
- cache.put(request, networkResponse.clone());
- }
-
- return networkResponse;
-}
-
-/**
- * 스테일 허용 전략
- */
-async function staleWhileRevalidate(request) {
- const cachedResponse = await caches.match(request);
-
- // 백그라운드에서 업데이트
- const networkResponsePromise = fetch(request)
- .then(async (networkResponse) => {
- if (networkResponse.ok) {
- const responseToCache = networkResponse.clone();
- const cache = await caches.open(DYNAMIC_CACHE);
- cache.put(request, responseToCache);
- }
- return networkResponse;
- })
- .catch(() => null);
-
- // 캐시된 응답이 있으면 즉시 반환, 없으면 네트워크 대기
- return cachedResponse || await networkResponsePromise;
-}
-
-/**
- * 캐시 전략 결정
- */
-function getCacheStrategy(url) {
- for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) {
- if (patterns.some(pattern => pattern.test(url))) {
- return strategy;
- }
- }
- return 'networkFirst'; // 기본값
-}
-
-/**
- * CDN 리소스 확인
- */
-function isCDNResource(url) {
- const cdnDomains = [
- 'cdn.tailwindcss.com',
- 'cdnjs.cloudflare.com',
- 'fonts.googleapis.com',
- 'fonts.gstatic.com'
- ];
-
- return cdnDomains.some(domain => url.hostname.includes(domain));
-}
-
-/**
- * 오프라인 처리
- */
-async function handleOffline(request) {
- // HTML 요청에 대한 오프라인 페이지
- if (request.destination === 'document') {
- const offlinePage = await caches.match('/app.html');
- if (offlinePage) {
- return offlinePage;
- }
- }
-
- // 이미지 요청에 대한 기본 이미지
- if (request.destination === 'image') {
- return new Response(
- '',
- { headers: { 'Content-Type': 'image/svg+xml' } }
- );
- }
-
- // 기본 오프라인 응답
- return new Response('오프라인 상태입니다.', {
- status: 503,
- statusText: 'Service Unavailable',
- headers: { 'Content-Type': 'text/plain; charset=utf-8' }
- });
-}
-
-/**
- * 메시지 처리 (캐시 관리)
- */
-self.addEventListener('message', (event) => {
- const { type, payload } = event.data;
-
- switch (type) {
- case 'CLEAR_CACHE':
- clearAllCaches().then(() => {
- event.ports[0].postMessage({ success: true });
- });
- break;
-
- case 'CACHE_PAGE':
- cachePage(payload.url).then(() => {
- event.ports[0].postMessage({ success: true });
- });
- break;
-
- case 'GET_CACHE_STATUS':
- getCacheStatus().then((status) => {
- event.ports[0].postMessage({ status });
- });
- break;
- }
-});
-
-/**
- * 모든 캐시 정리
- */
-async function clearAllCaches() {
- const cacheNames = await caches.keys();
- await Promise.all(
- cacheNames.map(cacheName => caches.delete(cacheName))
- );
- console.log(' 모든 캐시 정리 완료');
-}
-
-/**
- * 특정 페이지 캐시
- */
-async function cachePage(url) {
- try {
- const cache = await caches.open(DYNAMIC_CACHE);
- await cache.add(url);
- console.log(' 페이지 캐시 완료:', url);
- } catch (error) {
- console.error('페이지 캐시 실패:', url, error);
- }
-}
-
-/**
- * 캐시 상태 조회
- */
-async function getCacheStatus() {
- const cacheNames = await caches.keys();
- const status = {};
-
- for (const cacheName of cacheNames) {
- const cache = await caches.open(cacheName);
- const keys = await cache.keys();
- status[cacheName] = keys.length;
- }
-
- return status;
-}