feat: 실시간 알림 시스템 (Web Push + 알림 벨 + 서비스간 알림 연동)
- Phase 1: 모든 서비스 헤더에 알림 벨 UI 추가 (notification-bell.js) - Phase 2: VAPID Web Push 구독/전송 (push-sw.js, pushSubscription API) - Phase 3: 내부 알림 API + notifyHelper로 서비스간 알림 연동 - tksafety: 출입 승인/반려, 안전교육 완료, 방문자 체크인 - tkpurchase: 일용공 신청, 작업보고서 제출 - system2-report: 신고 접수/확인/처리완료 - Phase 4: 30일 이상 알림 자동 정리 cron, Redis 캐싱 - CORS에 tkuser/tkpurchase/tksafety 서브도메인 추가 - HTML cache busting 버전 갱신 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -276,7 +276,7 @@
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/core/page-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/toast.js?v=20260308"></script>
|
||||
<script src="/static/js/components/mobile-bottom-nav.js?v=20260308"></script>
|
||||
|
||||
@@ -301,6 +301,6 @@
|
||||
<script src="/static/js/utils/date-utils.js"></script>
|
||||
<script src="/static/js/utils/image-utils.js"></script>
|
||||
<script src="/static/js/core/permissions.js"></script>
|
||||
<script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/app.js?v=20260313"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/core/page-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/photo-modal.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/toast.js?v=20260308"></script>
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/core/page-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/photo-modal.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/toast.js?v=20260308"></script>
|
||||
|
||||
@@ -554,7 +554,7 @@
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/core/page-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/photo-modal.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/toast.js?v=20260308"></script>
|
||||
|
||||
@@ -373,7 +373,7 @@
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/core/page-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/components/mobile-calendar.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/photo-modal.js?v=20260308"></script>
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/core/page-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/photo-modal.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/toast.js?v=20260308"></script>
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/core/permissions.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/m/m-common.js?v=20260309"></script>
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/core/permissions.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/m/m-common.js?v=20260309"></script>
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
|
||||
<!-- 스크립트 -->
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
<script src="/static/js/core/permissions.js?v=20260308"></script>
|
||||
<script src="/static/js/utils/issue-helpers.js?v=20260308"></script>
|
||||
<script src="/static/js/m/m-common.js?v=20260309"></script>
|
||||
|
||||
41
system3-nonconformance/web/push-sw.js
Normal file
41
system3-nonconformance/web/push-sw.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Push Notification Service Worker
|
||||
// 캐싱 없음 — Push 수신 전용
|
||||
|
||||
self.addEventListener('push', function(event) {
|
||||
var data = { title: '알림', body: '새 알림이 있습니다.', url: '/' };
|
||||
if (event.data) {
|
||||
try { data = Object.assign(data, event.data.json()); } catch(e) {}
|
||||
}
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(data.title, {
|
||||
body: data.body,
|
||||
icon: '/static/img/icon-192.png',
|
||||
badge: '/static/img/badge-72.png',
|
||||
data: { url: data.url || '/' },
|
||||
tag: 'tk-notification-' + Date.now(),
|
||||
renotify: true
|
||||
})
|
||||
);
|
||||
// 메인 페이지에 뱃지 갱신 신호 전송
|
||||
self.clients.matchAll({ type: 'window' }).then(function(clients) {
|
||||
clients.forEach(function(client) {
|
||||
client.postMessage({ type: 'NOTIFICATION_RECEIVED' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
self.addEventListener('notificationclick', function(event) {
|
||||
event.notification.close();
|
||||
var url = (event.notification.data && event.notification.data.url) || '/';
|
||||
event.waitUntil(
|
||||
self.clients.matchAll({ type: 'window' }).then(function(clients) {
|
||||
for (var i = 0; i < clients.length; i++) {
|
||||
if (clients[i].url.includes(self.location.origin)) {
|
||||
clients[i].navigate(url);
|
||||
return clients[i].focus();
|
||||
}
|
||||
}
|
||||
return self.clients.openWindow(url);
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -185,7 +185,7 @@
|
||||
<script src="/static/js/core/permissions.js?v=20260308"></script>
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
|
||||
<script>
|
||||
let projects = [];
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
<script src="/static/js/core/permissions.js?v=20260308"></script>
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
|
||||
<script>
|
||||
// 페이지 초기화
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<script src="/static/js/core/permissions.js?v=20260308"></script>
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
|
||||
<script>
|
||||
// 페이지 초기화
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
<script src="/static/js/core/permissions.js?v=20260308"></script>
|
||||
<script src="/static/js/components/common-header.js?v=20260308"></script>
|
||||
<script src="/static/js/api.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260308"></script>
|
||||
<script src="/static/js/core/auth-manager.js?v=20260313"></script>
|
||||
|
||||
<script>
|
||||
// 페이지 초기화
|
||||
|
||||
@@ -33,6 +33,9 @@ class App {
|
||||
// 라우터 초기화
|
||||
this.initializeRouter();
|
||||
|
||||
// 알림 벨 로드
|
||||
this._loadNotificationBell();
|
||||
|
||||
// 대시보드 데이터 로드
|
||||
await this.loadDashboardData();
|
||||
|
||||
@@ -392,6 +395,16 @@ class App {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 벨 로드
|
||||
*/
|
||||
_loadNotificationBell() {
|
||||
var h = window.location.hostname;
|
||||
var s = document.createElement('script');
|
||||
s.src = (h.includes('technicalkorea.net') ? 'https://tkfb.technicalkorea.net' : window.location.protocol + '//' + h + ':30000') + '/shared/notification-bell.js?v=1';
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 표시
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// 서비스 워커 해제 (캐시 간섭으로 인한 인증 루프 방지)
|
||||
// 서비스 워커 해제 (push-sw.js 제외)
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.getRegistrations().then(function(registrations) {
|
||||
registrations.forEach(function(registration) { registration.unregister(); });
|
||||
registrations.forEach(function(registration) {
|
||||
if (!registration.active || !registration.active.scriptURL.includes('push-sw.js')) {
|
||||
registration.unregister();
|
||||
}
|
||||
});
|
||||
});
|
||||
if (typeof caches !== 'undefined') {
|
||||
caches.keys().then(function(names) {
|
||||
|
||||
@@ -281,7 +281,9 @@ class PagePreloader {
|
||||
try {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
for (const registration of registrations) {
|
||||
await registration.unregister();
|
||||
if (!registration.active || !registration.active.scriptURL.includes('push-sw.js')) {
|
||||
await registration.unregister();
|
||||
}
|
||||
}
|
||||
// 모든 캐시 삭제
|
||||
const cacheNames = await caches.keys();
|
||||
|
||||
Reference in New Issue
Block a user