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

View File

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

View File

@@ -288,6 +288,6 @@ async function initAuth() {
/* ===== 알림 벨 ===== */
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';
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);
}

View File

@@ -153,7 +153,7 @@ if ('serviceWorker' in navigator) {
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';
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);
};

View File

@@ -401,7 +401,7 @@ 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';
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);
}

View File

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

View File

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

View File

@@ -156,6 +156,6 @@ function initAuth() {
/* ===== 알림 벨 ===== */
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';
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);
}

View File

@@ -147,6 +147,6 @@ function initAuth() {
/* ===== 알림 벨 ===== */
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';
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);
}

View File

@@ -5,15 +5,38 @@ const NOTIFICATION_TYPES = {
repair: '설비수리',
safety: '안전신고',
nonconformity: '부적합 신고',
equipment: '설비 관련',
maintenance: '정기점검',
system: '시스템'
system: '시스템',
partner_work: '협력업체 작업',
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 = {
// 알림 유형 목록 가져오기
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-layout-map.js?v=20260305"></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 -->
<script>init();</script>
</body>

View File

@@ -192,6 +192,6 @@ async function init() {
/* ===== 알림 벨 ===== */
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';
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);
}

View File

@@ -2,6 +2,7 @@
let nrLoaded = false;
let nrData = {}; // { type: { label, recipients: [...] } }
let nrTypes = {}; // { type: label }
let nrCategories = {}; // { category: { label, icon, types: [...] } }
let nrAllUsers = []; // 사용자 목록 (수신자 추가용)
async function loadNotificationRecipientsTab() {
@@ -12,7 +13,8 @@ async function loadNotificationRecipientsTab() {
api('/notification-recipients'),
api('/users?status=active')
]);
nrTypes = typesRes.data || {};
nrTypes = typesRes.data?.types || typesRes.data || {};
nrCategories = typesRes.data?.categories || {};
nrData = allRes.data || {};
nrAllUsers = (usersRes.data || usersRes || []).filter(u => u.status !== 'inactive');
nrLoaded = true;
@@ -31,10 +33,43 @@ function renderNrTab() {
}
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)) {
const recipients = nrData[type]?.recipients || [];
html += `
<div class="bg-white rounded-xl shadow-sm p-5 mb-4">
html += renderNrTypeCard(type, label, recipients);
}
}
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">
<h3 class="text-sm font-semibold text-gray-800">
<i class="fas ${nrTypeIcon(type)} text-slate-500 mr-2"></i>${escapeHtml(label)}
@@ -60,16 +95,14 @@ function renderNrTab() {
</div>
</div>`;
}
container.innerHTML = html;
}
function nrTypeIcon(type) {
const icons = {
repair: 'fa-wrench',
safety: 'fa-shield-alt',
nonconformity: 'fa-exclamation-triangle',
equipment: 'fa-cogs',
maintenance: 'fa-calendar-check',
partner_work: 'fa-hard-hat',
day_labor: 'fa-user-clock',
system: 'fa-server'
};
return icons[type] || 'fa-bell';