feat(notifications): 알림 유형 개선 - 카테고리 그룹화 + 구매팀 세분화

- equipment/maintenance 삭제, partner_work/day_labor 신규 추가
- 알림 수신자 관리 UI: 카테고리별 그룹 렌더링 (생산/안전/구매/시스템)
- tkpurchase 컨트롤러 알림 타입 변경
- notification-bell 라벨 및 notifications.html 아이콘 업데이트
- 전 서비스 cache busting 갱신

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-13 15:29:29 +09:00
parent 0a712813e2
commit b1154a8bc7
13 changed files with 118 additions and 49 deletions

View File

@@ -365,7 +365,7 @@
} }
function _typeLabel(type) { function _typeLabel(type) {
var map = { system: '시스템', repair: '수리', safety: '안전', maintenance: '구매' }; var map = { system: '시스템', repair: '설비수리', safety: '안전', nonconformity: '부적합', partner_work: '협력업체', day_labor: '일용공' };
return map[type] || type || '알림'; return map[type] || type || '알림';
} }

View File

@@ -187,6 +187,18 @@
background: var(--primary-100); background: var(--primary-100);
} }
.notification-icon.nonconformity {
background: #FEE2E2;
}
.notification-icon.partner_work {
background: #DBEAFE;
}
.notification-icon.day_labor {
background: #E0E7FF;
}
.notification-content { .notification-content {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
@@ -456,8 +468,9 @@
repair: '🔧', repair: '🔧',
safety: '⚠️', safety: '⚠️',
system: '📢', system: '📢',
equipment: '🔩', nonconformity: '',
maintenance: '🛠' partner_work: '🏗',
day_labor: '👷'
}; };
return icons[type] || '🔔'; return icons[type] || '🔔';
} }

View File

@@ -288,6 +288,6 @@ async function initAuth() {
/* ===== 알림 벨 ===== */ /* ===== 알림 벨 ===== */
function _loadNotificationBell() { function _loadNotificationBell() {
const s = document.createElement('script'); 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'; s.src = (location.hostname.includes('technicalkorea.net') ? 'https://tkfb.technicalkorea.net' : location.protocol + '//' + location.hostname + ':30000') + '/shared/notification-bell.js?v=2';
document.head.appendChild(s); document.head.appendChild(s);
} }

View File

@@ -153,7 +153,7 @@ if ('serviceWorker' in navigator) {
window._loadNotificationBell = function() { window._loadNotificationBell = function() {
var h = window.location.hostname; var h = window.location.hostname;
var s = document.createElement('script'); 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'; s.src = (h.includes('technicalkorea.net') ? 'https://tkfb.technicalkorea.net' : window.location.protocol + '//' + h + ':30000') + '/shared/notification-bell.js?v=2';
document.head.appendChild(s); document.head.appendChild(s);
}; };

View File

@@ -401,7 +401,7 @@ class App {
_loadNotificationBell() { _loadNotificationBell() {
var h = window.location.hostname; var h = window.location.hostname;
var s = document.createElement('script'); 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'; s.src = (h.includes('technicalkorea.net') ? 'https://tkfb.technicalkorea.net' : window.location.protocol + '//' + h + ':30000') + '/shared/notification-bell.js?v=2';
document.head.appendChild(s); document.head.appendChild(s);
} }

View File

@@ -51,7 +51,7 @@ async function create(req, res) {
// 알림: 일용공 신청 // 알림: 일용공 신청
notify.send({ notify.send({
type: 'maintenance', type: 'day_labor',
title: '일용공 신청', title: '일용공 신청',
message: `${work_date} ${worker_count}명 일용공 신청`, message: `${work_date} ${worker_count}명 일용공 신청`,
link_url: '/daylabor.html', link_url: '/daylabor.html',

View File

@@ -94,7 +94,7 @@ async function create(req, res) {
// 알림: 작업 보고서 제출 // 알림: 작업 보고서 제출
notify.send({ notify.send({
type: 'maintenance', type: 'partner_work',
title: '작업 보고서 제출', title: '작업 보고서 제출',
message: `${report_date} 작업 보고서가 제출되었습니다.`, message: `${report_date} 작업 보고서가 제출되었습니다.`,
link_url: '/workreport.html', link_url: '/workreport.html',

View File

@@ -156,6 +156,6 @@ function initAuth() {
/* ===== 알림 벨 ===== */ /* ===== 알림 벨 ===== */
function _loadNotificationBell() { function _loadNotificationBell() {
const s = document.createElement('script'); 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'; s.src = (location.hostname.includes('technicalkorea.net') ? 'https://tkfb.technicalkorea.net' : location.protocol + '//' + location.hostname + ':30000') + '/shared/notification-bell.js?v=2';
document.head.appendChild(s); document.head.appendChild(s);
} }

View File

@@ -147,6 +147,6 @@ function initAuth() {
/* ===== 알림 벨 ===== */ /* ===== 알림 벨 ===== */
function _loadNotificationBell() { function _loadNotificationBell() {
const s = document.createElement('script'); 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'; s.src = (location.hostname.includes('technicalkorea.net') ? 'https://tkfb.technicalkorea.net' : location.protocol + '//' + location.hostname + ':30000') + '/shared/notification-bell.js?v=2';
document.head.appendChild(s); document.head.appendChild(s);
} }

View File

@@ -2,18 +2,41 @@
const { getPool } = require('./userModel'); const { getPool } = require('./userModel');
const NOTIFICATION_TYPES = { const NOTIFICATION_TYPES = {
repair: '설비 수리', repair: '설비수리',
safety: '안전 신고', safety: '안전신고',
nonconformity: '부적합 신고', nonconformity: '부적합 신고',
equipment: '설비 관련', system: '시스템',
maintenance: '정기점검', partner_work: '협력업체 작업',
system: '시스템' day_labor: '일용공 신청'
};
const NOTIFICATION_CATEGORIES = {
production: {
label: '생산',
icon: 'fa-industry',
types: ['repair', 'nonconformity']
},
safety: {
label: '안전',
icon: 'fa-shield-alt',
types: ['safety']
},
purchase: {
label: '구매',
icon: 'fa-shopping-cart',
types: ['partner_work', 'day_labor']
},
system: {
label: '시스템',
icon: 'fa-server',
types: ['system']
}
}; };
const notificationRecipientModel = { const notificationRecipientModel = {
// 알림 유형 목록 가져오기 // 알림 유형 목록 가져오기
getTypes() { getTypes() {
return NOTIFICATION_TYPES; return { types: NOTIFICATION_TYPES, categories: NOTIFICATION_CATEGORIES };
}, },
// 유형별 수신자 목록 조회 // 유형별 수신자 목록 조회

View File

@@ -1753,7 +1753,7 @@
<script src="/static/js/tkuser-vacations.js?v=20260224"></script> <script src="/static/js/tkuser-vacations.js?v=20260224"></script>
<script src="/static/js/tkuser-layout-map.js?v=20260305"></script> <script src="/static/js/tkuser-layout-map.js?v=20260305"></script>
<script src="/static/js/tkuser-partners.js?v=20260312"></script> <script src="/static/js/tkuser-partners.js?v=20260312"></script>
<script src="/static/js/tkuser-notificationRecipients.js?v=20260313"></script> <script src="/static/js/tkuser-notificationRecipients.js?v=20260313b"></script>
<!-- Boot --> <!-- Boot -->
<script>init();</script> <script>init();</script>
</body> </body>

View File

@@ -192,6 +192,6 @@ async function init() {
/* ===== 알림 벨 ===== */ /* ===== 알림 벨 ===== */
function _loadNotificationBell() { function _loadNotificationBell() {
const s = document.createElement('script'); 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'; s.src = (location.hostname.includes('technicalkorea.net') ? 'https://tkfb.technicalkorea.net' : location.protocol + '//' + location.hostname + ':30000') + '/shared/notification-bell.js?v=2';
document.head.appendChild(s); document.head.appendChild(s);
} }

View File

@@ -2,6 +2,7 @@
let nrLoaded = false; let nrLoaded = false;
let nrData = {}; // { type: { label, recipients: [...] } } let nrData = {}; // { type: { label, recipients: [...] } }
let nrTypes = {}; // { type: label } let nrTypes = {}; // { type: label }
let nrCategories = {}; // { category: { label, icon, types: [...] } }
let nrAllUsers = []; // 사용자 목록 (수신자 추가용) let nrAllUsers = []; // 사용자 목록 (수신자 추가용)
async function loadNotificationRecipientsTab() { async function loadNotificationRecipientsTab() {
@@ -12,7 +13,8 @@ async function loadNotificationRecipientsTab() {
api('/notification-recipients'), api('/notification-recipients'),
api('/users?status=active') api('/users?status=active')
]); ]);
nrTypes = typesRes.data || {}; nrTypes = typesRes.data?.types || typesRes.data || {};
nrCategories = typesRes.data?.categories || {};
nrData = allRes.data || {}; nrData = allRes.data || {};
nrAllUsers = (usersRes.data || usersRes || []).filter(u => u.status !== 'inactive'); nrAllUsers = (usersRes.data || usersRes || []).filter(u => u.status !== 'inactive');
nrLoaded = true; nrLoaded = true;
@@ -31,10 +33,43 @@ function renderNrTab() {
} }
let html = ''; let html = '';
const categoryKeys = Object.keys(nrCategories);
if (categoryKeys.length > 0) {
// 카테고리별 그룹 렌더링
for (const [catKey, cat] of Object.entries(nrCategories)) {
html += `
<div class="mb-6">
<h2 class="text-base font-bold text-gray-700 mb-3 flex items-center gap-2">
<i class="fas ${cat.icon} text-slate-500"></i>${escapeHtml(cat.label)}
</h2>
<div class="pl-4 space-y-3">`;
for (const type of cat.types) {
const label = nrTypes[type];
if (!label) continue;
const recipients = nrData[type]?.recipients || [];
html += renderNrTypeCard(type, label, recipients);
}
html += `
</div>
</div>`;
}
} else {
// 폴백: 카테고리 없이 flat 리스트
for (const [type, label] of Object.entries(nrTypes)) { for (const [type, label] of Object.entries(nrTypes)) {
const recipients = nrData[type]?.recipients || []; const recipients = nrData[type]?.recipients || [];
html += ` html += renderNrTypeCard(type, label, recipients);
<div class="bg-white rounded-xl shadow-sm p-5 mb-4"> }
}
container.innerHTML = html;
}
function renderNrTypeCard(type, label, recipients) {
return `
<div class="bg-white rounded-xl shadow-sm p-5 mb-3">
<div class="flex items-center justify-between mb-3"> <div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-800"> <h3 class="text-sm font-semibold text-gray-800">
<i class="fas ${nrTypeIcon(type)} text-slate-500 mr-2"></i>${escapeHtml(label)} <i class="fas ${nrTypeIcon(type)} text-slate-500 mr-2"></i>${escapeHtml(label)}
@@ -59,8 +94,6 @@ function renderNrTab() {
} }
</div> </div>
</div>`; </div>`;
}
container.innerHTML = html;
} }
function nrTypeIcon(type) { function nrTypeIcon(type) {
@@ -68,8 +101,8 @@ function nrTypeIcon(type) {
repair: 'fa-wrench', repair: 'fa-wrench',
safety: 'fa-shield-alt', safety: 'fa-shield-alt',
nonconformity: 'fa-exclamation-triangle', nonconformity: 'fa-exclamation-triangle',
equipment: 'fa-cogs', partner_work: 'fa-hard-hat',
maintenance: 'fa-calendar-check', day_labor: 'fa-user-clock',
system: 'fa-server' system: 'fa-server'
}; };
return icons[type] || 'fa-bell'; return icons[type] || 'fa-bell';