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:
Hyungi Ahn
2026-03-13 15:01:44 +09:00
parent 1ad82fd52c
commit 7fd646e9ba
102 changed files with 1446 additions and 94 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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);
})
);
});

View File

@@ -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 = [];

View File

@@ -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>
// 페이지 초기화

View File

@@ -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>
// 페이지 초기화

View File

@@ -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>
// 페이지 초기화

View File

@@ -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);
}
/**
* 로딩 표시
*/

View File

@@ -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) {

View File

@@ -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();