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

@@ -190,7 +190,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="module">

View File

@@ -324,7 +324,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/department-management.js"></script>
<script>initAuth();</script>

View File

@@ -314,7 +314,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="module">

View File

@@ -190,7 +190,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="module">

View File

@@ -329,7 +329,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script type="module" src="/js/issue-category-manage.js"></script>
<script>initAuth();</script>

View File

@@ -375,7 +375,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script>
let currentPage = 1;

View File

@@ -384,7 +384,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script>
let allProjects = [];

View File

@@ -487,7 +487,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script>
let currentReportId = null;

View File

@@ -285,7 +285,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script>
let workTypes = [];

View File

@@ -431,7 +431,7 @@
</div>
<!-- 작업장 관리 모듈 (리팩토링된 구조) -->
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/workplace-management/state.js?v=1"></script>
<script src="/js/workplace-management/utils.js?v=1"></script>

View File

@@ -328,7 +328,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>

View File

@@ -222,7 +222,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>

View File

@@ -70,7 +70,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>

View File

@@ -474,7 +474,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>

View File

@@ -265,7 +265,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>

View File

@@ -353,7 +353,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script type="module" src="/js/vacation-allocation.js" defer></script>
<script>initAuth();</script>

View File

@@ -123,7 +123,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/js/vacation-common.js"></script>

View File

@@ -123,7 +123,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/js/vacation-common.js"></script>

View File

@@ -205,7 +205,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/js/vacation-common.js"></script>

View File

@@ -117,7 +117,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="/js/vacation-common.js"></script>

View File

@@ -276,7 +276,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>

View File

@@ -138,7 +138,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/static/js/tkfb-dashboard.js"></script>
</body>
</html>

View File

@@ -323,7 +323,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script type="module" src="/js/modern-dashboard.js?v=10"></script>
<script type="module" src="/js/group-leader-dashboard.js?v=1"></script>

View File

@@ -209,7 +209,7 @@
}, 50);
})();
</script>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/daily-patrol.js?v=6"></script>
<script>initAuth();</script>

View File

@@ -304,7 +304,7 @@
}, 50);
})();
</script>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/zone-detail.js?v=6"></script>
<script>initAuth();</script>

View File

@@ -320,7 +320,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script type="module" src="/js/my-profile.js"></script>
<script>initAuth();</script>

View File

@@ -390,7 +390,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script type="module" src="/js/change-password.js"></script>
<script>initAuth();</script>

View File

@@ -277,7 +277,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script type="module" src="/js/work-analysis.js?v=5"></script>

View File

@@ -90,7 +90,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/static/js/tkfb-nonconformity.js"></script>
</body>
</html>

View File

@@ -189,7 +189,7 @@
</div>
<!-- 공통 모듈 -->
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/common/utils.js?v=1"></script>
<script src="/js/common/base-state.js?v=1"></script>

View File

@@ -149,7 +149,7 @@
</div>
</div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/common/utils.js?v=1"></script>
<script src="/js/common/base-state.js?v=1"></script>

View File

@@ -843,7 +843,7 @@
</div>
<!-- Scripts -->
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<!-- 공통 모듈 -->
<script src="/js/common/utils.js?v=2"></script>

View File

@@ -296,7 +296,7 @@
</div>
<!-- 공통 모듈 -->
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/common/utils.js?v=2"></script>
<script src="/js/common/base-state.js?v=2"></script>

View File

@@ -560,7 +560,7 @@
<!-- 토스트 -->
<div class="toast-container" id="toastContainer"></div>
<script src="/static/js/tkfb-core.js"></script>
<script src="/static/js/tkfb-core.js?v=20260313"></script>
<script src="/js/api-base.js?v=3"></script>
<script src="/js/common/utils.js?v=2"></script>
<script src="/js/common/base-state.js?v=2"></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

@@ -1,6 +1,8 @@
/* ===== 서비스 워커 해제 ===== */
/* ===== 서비스 워커 해제 (push-sw.js 제외) ===== */
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function(regs) { regs.forEach(function(r) { r.unregister(); }); });
navigator.serviceWorker.getRegistrations().then(function(regs) { regs.forEach(function(r) {
if (!r.active || !r.active.scriptURL.includes('push-sw.js')) { r.unregister(); }
}); });
if (typeof caches !== 'undefined') { caches.keys().then(function(ns) { ns.forEach(function(n) { caches.delete(n); }); }); }
}
@@ -276,6 +278,16 @@ async function initAuth() {
const overlay = document.getElementById('mobileOverlay');
if (overlay) overlay.addEventListener('click', toggleMobileMenu);
// 알림 벨 로드
_loadNotificationBell();
setTimeout(() => document.querySelector('.fade-in')?.classList.add('visible'), 50);
return true;
}
/* ===== 알림 벨 ===== */
function _loadNotificationBell() {
const s = document.createElement('script');
s.src = (location.hostname.includes('technicalkorea.net') ? 'https://tkfb.technicalkorea.net' : location.protocol + '//' + location.hostname + ':30000') + '/shared/notification-bell.js?v=1';
document.head.appendChild(s);
}