diff --git a/docker-compose.yml b/docker-compose.yml index 12adeb7..14e6945 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -103,6 +103,10 @@ services: - REDIS_PORT=6379 - WEATHER_API_URL=${WEATHER_API_URL:-} - WEATHER_API_KEY=${WEATHER_API_KEY:-} + - VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY} + - VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY} + - VAPID_SUBJECT=${VAPID_SUBJECT:-mailto:admin@technicalkorea.net} + - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY} volumes: - system1_uploads:/usr/src/app/uploads - system1_logs:/usr/src/app/logs @@ -172,6 +176,7 @@ services: - M_PROJECT_USERNAME=${M_PROJECT_USERNAME:-api_service} - M_PROJECT_PASSWORD=${M_PROJECT_PASSWORD:-} - M_PROJECT_DEFAULT_PROJECT_ID=${M_PROJECT_DEFAULT_PROJECT_ID:-1} + - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY} volumes: - system2_uploads:/usr/src/app/uploads - system2_logs:/usr/src/app/logs @@ -306,6 +311,7 @@ services: - DB_PASSWORD=${MYSQL_PASSWORD} - DB_NAME=${MYSQL_DATABASE:-hyungi} - SSO_JWT_SECRET=${SSO_JWT_SECRET} + - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY} depends_on: mariadb: condition: service_healthy @@ -346,6 +352,7 @@ services: - DB_PASSWORD=${MYSQL_PASSWORD} - DB_NAME=${MYSQL_DATABASE:-hyungi} - SSO_JWT_SECRET=${SSO_JWT_SECRET} + - INTERNAL_SERVICE_KEY=${INTERNAL_SERVICE_KEY} depends_on: mariadb: condition: service_healthy diff --git a/gateway/html/shared/notification-bell.js b/gateway/html/shared/notification-bell.js new file mode 100644 index 0000000..b8f0766 --- /dev/null +++ b/gateway/html/shared/notification-bell.js @@ -0,0 +1,410 @@ +/** + * 공유 알림 벨 — 모든 서비스 헤더에 자동 삽입 + * + * 사용법: initAuth() 성공 후 동적 + + diff --git a/system1-factory/web/pages/admin/equipment-detail.html b/system1-factory/web/pages/admin/equipment-detail.html index c881a10..0534502 100644 --- a/system1-factory/web/pages/admin/equipment-detail.html +++ b/system1-factory/web/pages/admin/equipment-detail.html @@ -314,7 +314,7 @@ - + + + diff --git a/system1-factory/web/pages/admin/notifications.html b/system1-factory/web/pages/admin/notifications.html index be29eeb..649baa6 100644 --- a/system1-factory/web/pages/admin/notifications.html +++ b/system1-factory/web/pages/admin/notifications.html @@ -375,7 +375,7 @@ - + + + + + diff --git a/system1-factory/web/pages/attendance/annual-overview.html b/system1-factory/web/pages/attendance/annual-overview.html index bb16840..cd1e8a4 100644 --- a/system1-factory/web/pages/attendance/annual-overview.html +++ b/system1-factory/web/pages/attendance/annual-overview.html @@ -328,7 +328,7 @@ - + + + + + + diff --git a/system1-factory/web/pages/attendance/vacation-approval.html b/system1-factory/web/pages/attendance/vacation-approval.html index e58ceb3..5175712 100644 --- a/system1-factory/web/pages/attendance/vacation-approval.html +++ b/system1-factory/web/pages/attendance/vacation-approval.html @@ -123,7 +123,7 @@ - + diff --git a/system1-factory/web/pages/attendance/vacation-input.html b/system1-factory/web/pages/attendance/vacation-input.html index 45a648f..023ffba 100644 --- a/system1-factory/web/pages/attendance/vacation-input.html +++ b/system1-factory/web/pages/attendance/vacation-input.html @@ -123,7 +123,7 @@ - + diff --git a/system1-factory/web/pages/attendance/vacation-management.html b/system1-factory/web/pages/attendance/vacation-management.html index b03a0d4..5ee573d 100644 --- a/system1-factory/web/pages/attendance/vacation-management.html +++ b/system1-factory/web/pages/attendance/vacation-management.html @@ -205,7 +205,7 @@ - + diff --git a/system1-factory/web/pages/attendance/vacation-request.html b/system1-factory/web/pages/attendance/vacation-request.html index b83181c..96e94eb 100644 --- a/system1-factory/web/pages/attendance/vacation-request.html +++ b/system1-factory/web/pages/attendance/vacation-request.html @@ -117,7 +117,7 @@ - + diff --git a/system1-factory/web/pages/attendance/work-status.html b/system1-factory/web/pages/attendance/work-status.html index 01de510..1b87f42 100644 --- a/system1-factory/web/pages/attendance/work-status.html +++ b/system1-factory/web/pages/attendance/work-status.html @@ -276,7 +276,7 @@ - + + diff --git a/system1-factory/web/pages/dashboard.html b/system1-factory/web/pages/dashboard.html index 3940a68..4cb5bea 100644 --- a/system1-factory/web/pages/dashboard.html +++ b/system1-factory/web/pages/dashboard.html @@ -323,7 +323,7 @@ - + diff --git a/system1-factory/web/pages/inspection/daily-patrol.html b/system1-factory/web/pages/inspection/daily-patrol.html index 0ae0cd0..4a9129a 100644 --- a/system1-factory/web/pages/inspection/daily-patrol.html +++ b/system1-factory/web/pages/inspection/daily-patrol.html @@ -209,7 +209,7 @@ }, 50); })(); - + diff --git a/system1-factory/web/pages/inspection/zone-detail.html b/system1-factory/web/pages/inspection/zone-detail.html index da060a9..7a8894b 100644 --- a/system1-factory/web/pages/inspection/zone-detail.html +++ b/system1-factory/web/pages/inspection/zone-detail.html @@ -304,7 +304,7 @@ }, 50); })(); - + diff --git a/system1-factory/web/pages/profile/info.html b/system1-factory/web/pages/profile/info.html index aeb6eed..a6092e9 100644 --- a/system1-factory/web/pages/profile/info.html +++ b/system1-factory/web/pages/profile/info.html @@ -320,7 +320,7 @@ - + diff --git a/system1-factory/web/pages/profile/password.html b/system1-factory/web/pages/profile/password.html index 8a75b6b..612153d 100644 --- a/system1-factory/web/pages/profile/password.html +++ b/system1-factory/web/pages/profile/password.html @@ -390,7 +390,7 @@ - + diff --git a/system1-factory/web/pages/work/analysis.html b/system1-factory/web/pages/work/analysis.html index ff258f9..62618a9 100644 --- a/system1-factory/web/pages/work/analysis.html +++ b/system1-factory/web/pages/work/analysis.html @@ -277,7 +277,7 @@ - + diff --git a/system1-factory/web/pages/work/nonconformity.html b/system1-factory/web/pages/work/nonconformity.html index 4228ace..b41cede 100644 --- a/system1-factory/web/pages/work/nonconformity.html +++ b/system1-factory/web/pages/work/nonconformity.html @@ -90,7 +90,7 @@ - + diff --git a/system1-factory/web/pages/work/report-create-mobile.html b/system1-factory/web/pages/work/report-create-mobile.html index ac7c7ce..67f60df 100644 --- a/system1-factory/web/pages/work/report-create-mobile.html +++ b/system1-factory/web/pages/work/report-create-mobile.html @@ -189,7 +189,7 @@ - + diff --git a/system1-factory/web/pages/work/report-create.html b/system1-factory/web/pages/work/report-create.html index 982a1cf..5c69ede 100644 --- a/system1-factory/web/pages/work/report-create.html +++ b/system1-factory/web/pages/work/report-create.html @@ -149,7 +149,7 @@ - + diff --git a/system1-factory/web/pages/work/tbm-create.html b/system1-factory/web/pages/work/tbm-create.html index e0833ed..7c6cd4f 100644 --- a/system1-factory/web/pages/work/tbm-create.html +++ b/system1-factory/web/pages/work/tbm-create.html @@ -843,7 +843,7 @@ - + diff --git a/system1-factory/web/pages/work/tbm-mobile.html b/system1-factory/web/pages/work/tbm-mobile.html index b79c0cd..863aebf 100644 --- a/system1-factory/web/pages/work/tbm-mobile.html +++ b/system1-factory/web/pages/work/tbm-mobile.html @@ -296,7 +296,7 @@ - + diff --git a/system1-factory/web/pages/work/tbm.html b/system1-factory/web/pages/work/tbm.html index f8cad9c..44dec6c 100644 --- a/system1-factory/web/pages/work/tbm.html +++ b/system1-factory/web/pages/work/tbm.html @@ -560,7 +560,7 @@
- + diff --git a/system1-factory/web/push-sw.js b/system1-factory/web/push-sw.js new file mode 100644 index 0000000..930acbc --- /dev/null +++ b/system1-factory/web/push-sw.js @@ -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); + }) + ); +}); diff --git a/system1-factory/web/static/js/tkfb-core.js b/system1-factory/web/static/js/tkfb-core.js index bb9f110..0f08a19 100644 --- a/system1-factory/web/static/js/tkfb-core.js +++ b/system1-factory/web/static/js/tkfb-core.js @@ -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); +} diff --git a/system2-report/api/controllers/workIssueController.js b/system2-report/api/controllers/workIssueController.js index 928a15d..18e4061 100644 --- a/system2-report/api/controllers/workIssueController.js +++ b/system2-report/api/controllers/workIssueController.js @@ -5,6 +5,7 @@ const workIssueModel = require('../models/workIssueModel'); const imageUploadService = require('../services/imageUploadService'); const mProjectService = require('../services/mProjectService'); +const notify = require('../utils/notifyHelper'); // ==================== 신고 카테고리 관리 ==================== @@ -191,6 +192,17 @@ exports.createReport = async (req, res) => { const reportId = await workIssueModel.createReport(reportData); + // 알림: 신고 접수 + notify.send({ + type: 'safety', + title: '신고 접수', + message: `${catInfo ? catInfo.category_name : '문제'} 신고가 접수되었습니다.`, + link_url: '/pages/safety-report-list.html', + reference_type: 'work_issue_reports', + reference_id: reportId, + created_by: req.user.id || req.user.user_id + }); + res.status(201).json({ success: true, message: '문제 신고가 등록되었습니다.', @@ -398,6 +410,18 @@ exports.receiveReport = async (req, res) => { try { const { id } = req.params; await workIssueModel.receiveReport(id, req.user.user_id); + + // 알림: 신고 상태 변경 → 접수됨 + notify.send({ + type: 'safety', + title: '신고 접수 확인', + message: '신고가 접수되어 처리가 시작됩니다.', + link_url: '/pages/safety-report-list.html', + reference_type: 'work_issue_reports', + reference_id: parseInt(id), + created_by: req.user.user_id + }); + res.json({ success: true, message: '신고가 접수되었습니다.' }); } catch (err) { console.error('신고 접수 실패:', err); @@ -442,6 +466,18 @@ exports.completeReport = async (req, res) => { if (resolution_photos[1]) resolution_photo_path2 = await imageUploadService.saveBase64Image(resolution_photos[1], 'resolution'); await workIssueModel.completeReport(id, { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by: req.user.user_id }); + + // 알림: 신고 처리 완료 + notify.send({ + type: 'safety', + title: '신고 처리 완료', + message: '신고 처리가 완료되었습니다.', + link_url: '/pages/safety-report-list.html', + reference_type: 'work_issue_reports', + reference_id: parseInt(id), + created_by: req.user.user_id + }); + res.json({ success: true, message: '처리가 완료되었습니다.' }); } catch (err) { console.error('처리 완료 실패:', err); diff --git a/system2-report/api/utils/notifyHelper.js b/system2-report/api/utils/notifyHelper.js new file mode 100644 index 0000000..503e16f --- /dev/null +++ b/system2-report/api/utils/notifyHelper.js @@ -0,0 +1,63 @@ +// utils/notifyHelper.js — 공용 알림 헬퍼 +// system1-factory의 내부 알림 API를 통해 DB 저장 + Push 전송 +const http = require('http'); + +const NOTIFY_URL = 'http://system1-api:3005/api/notifications/internal'; +const SERVICE_KEY = process.env.INTERNAL_SERVICE_KEY || ''; + +const notifyHelper = { + /** + * 알림 전송 + * @param {Object} opts + * @param {string} opts.type - 알림 유형 (safety, maintenance, repair, system) + * @param {string} opts.title - 알림 제목 + * @param {string} [opts.message] - 알림 내용 + * @param {string} [opts.link_url] - 클릭 시 이동 URL + * @param {string} [opts.reference_type] - 연관 테이블명 + * @param {number} [opts.reference_id] - 연관 레코드 ID + * @param {number} [opts.created_by] - 생성자 user_id + */ + async send(opts) { + try { + const body = JSON.stringify(opts); + const url = new URL(NOTIFY_URL); + + return new Promise((resolve) => { + const req = http.request({ + hostname: url.hostname, + port: url.port, + path: url.pathname, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Internal-Service-Key': SERVICE_KEY, + 'Content-Length': Buffer.byteLength(body) + }, + timeout: 5000 + }, (res) => { + res.resume(); // drain + resolve(true); + }); + + req.on('error', (err) => { + console.error('[notifyHelper] 알림 전송 실패:', err.message); + resolve(false); + }); + + req.on('timeout', () => { + req.destroy(); + console.error('[notifyHelper] 알림 전송 타임아웃'); + resolve(false); + }); + + req.write(body); + req.end(); + }); + } catch (err) { + console.error('[notifyHelper] 알림 전송 오류:', err.message); + return false; + } + } +}; + +module.exports = notifyHelper; diff --git a/system2-report/web/js/api-base.js b/system2-report/web/js/api-base.js index 17a1f84..1175244 100644 --- a/system2-report/web/js/api-base.js +++ b/system2-report/web/js/api-base.js @@ -1,10 +1,14 @@ // /js/api-base.js // API 기본 설정 및 보안 유틸리티 - System 2 (신고 시스템) -// 서비스 워커 해제 (캐시 간섭으로 인한 인증 루프 방지) +// 서비스 워커 해제 (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) { @@ -145,5 +149,13 @@ if ('serviceWorker' in navigator) { return response.json(); }; + // 알림 벨 로드 + window._loadNotificationBell = function() { + 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); + }; + console.log('[System2] API 설정 완료:', window.API_BASE_URL); })(); diff --git a/system2-report/web/js/app-init.js b/system2-report/web/js/app-init.js index 765cc53..51a39f4 100644 --- a/system2-report/web/js/app-init.js +++ b/system2-report/web/js/app-init.js @@ -90,6 +90,9 @@ var token = window.getSSOToken ? window.getSSOToken() : null; if (token && !localStorage.getItem('sso_token')) localStorage.setItem('sso_token', token); console.log('[System2] 인증 확인:', currentUser.username); + + // 알림 벨 로드 + if (window._loadNotificationBell) window._loadNotificationBell(); } // DOMContentLoaded 시 실행 diff --git a/system2-report/web/pages/safety/chat-report.html b/system2-report/web/pages/safety/chat-report.html index 1da9c3b..5fc484c 100644 --- a/system2-report/web/pages/safety/chat-report.html +++ b/system2-report/web/pages/safety/chat-report.html @@ -6,8 +6,8 @@ AI 신고 도우미 | (주)테크니컬코리아 - - + + diff --git a/system2-report/web/pages/safety/issue-detail.html b/system2-report/web/pages/safety/issue-detail.html index 2861fe0..2a73603 100644 --- a/system2-report/web/pages/safety/issue-detail.html +++ b/system2-report/web/pages/safety/issue-detail.html @@ -8,8 +8,8 @@ - - + +