feat: 안전 코드 tksafety 이관 + 사용자 관리 정리 + UI Tailwind 전환
Phase 1: tksafety에 출입신청/체크리스트 API·웹 추가, tkfb 안전 코드 삭제
Phase 2: 사용자 관리 페이지 삭제, API 축소, 알림 수신자 tkuser 이관
Phase 3: tkuser 권한 페이지 정의 업데이트
Phase 4: 전체 34개 페이지 Tailwind CSS + tkfb-core.js 전환,
미사용 CSS 20개·인프라 JS 10개·템플릿·컴포넌트 삭제
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
139
user-management/web/static/js/tkuser-notificationRecipients.js
Normal file
139
user-management/web/static/js/tkuser-notificationRecipients.js
Normal file
@@ -0,0 +1,139 @@
|
||||
/* ===== 알림 수신자 관리 ===== */
|
||||
let nrLoaded = false;
|
||||
let nrData = {}; // { type: { label, recipients: [...] } }
|
||||
let nrTypes = {}; // { type: label }
|
||||
let nrAllUsers = []; // 사용자 목록 (수신자 추가용)
|
||||
|
||||
async function loadNotificationRecipientsTab() {
|
||||
if (nrLoaded) return;
|
||||
try {
|
||||
const [typesRes, allRes, usersRes] = await Promise.all([
|
||||
api('/notification-recipients/types'),
|
||||
api('/notification-recipients'),
|
||||
api('/users?status=active')
|
||||
]);
|
||||
nrTypes = typesRes.data || {};
|
||||
nrData = allRes.data || {};
|
||||
nrAllUsers = (usersRes.data || usersRes || []).filter(u => u.status !== 'inactive');
|
||||
nrLoaded = true;
|
||||
renderNrTab();
|
||||
} catch (e) {
|
||||
document.getElementById('nrContent').innerHTML =
|
||||
`<p class="text-red-500 text-center py-8">데이터 로드 실패: ${escapeHtml(e.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderNrTab() {
|
||||
const container = document.getElementById('nrContent');
|
||||
if (!Object.keys(nrTypes).length) {
|
||||
container.innerHTML = '<p class="text-gray-400 text-center py-8">등록된 알림 유형이 없습니다.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
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">
|
||||
<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)}
|
||||
<span class="ml-2 text-xs font-normal text-gray-400">${recipients.length}명</span>
|
||||
</h3>
|
||||
<button onclick="openNrAddModal('${type}')" class="px-3 py-1 bg-slate-700 text-white rounded-lg text-xs hover:bg-slate-800">
|
||||
<i class="fas fa-plus mr-1"></i>추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2" id="nrList-${type}">
|
||||
${recipients.length === 0
|
||||
? '<span class="text-gray-400 text-sm">수신자 없음</span>'
|
||||
: recipients.map(r => `
|
||||
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-gray-100 rounded-full text-sm">
|
||||
<span class="w-6 h-6 bg-slate-600 text-white rounded-full flex items-center justify-center text-xs font-bold">${escapeHtml((r.user_name || r.username || '?')[0])}</span>
|
||||
${escapeHtml(r.user_name || r.username)}
|
||||
<button onclick="removeNrRecipient('${type}', ${r.user_id})" class="text-gray-400 hover:text-red-500 ml-1" title="제거">
|
||||
<i class="fas fa-times text-xs"></i>
|
||||
</button>
|
||||
</span>
|
||||
`).join('')
|
||||
}
|
||||
</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',
|
||||
system: 'fa-server'
|
||||
};
|
||||
return icons[type] || 'fa-bell';
|
||||
}
|
||||
|
||||
/* ===== 수신자 추가 모달 ===== */
|
||||
let nrAddType = '';
|
||||
|
||||
function openNrAddModal(type) {
|
||||
nrAddType = type;
|
||||
const label = nrTypes[type] || type;
|
||||
document.getElementById('nrAddModalTitle').textContent = `${label} 수신자 추가`;
|
||||
|
||||
const currentIds = (nrData[type]?.recipients || []).map(r => r.user_id);
|
||||
const available = nrAllUsers.filter(u => !currentIds.includes(u.user_id));
|
||||
|
||||
const listEl = document.getElementById('nrAddUserList');
|
||||
if (available.length === 0) {
|
||||
listEl.innerHTML = '<p class="text-gray-400 text-sm text-center py-4">추가 가능한 사용자가 없습니다.</p>';
|
||||
} else {
|
||||
listEl.innerHTML = available.map(u => `
|
||||
<label class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 cursor-pointer">
|
||||
<input type="checkbox" value="${u.user_id}" class="nrAddCheck rounded">
|
||||
<span class="text-sm">${escapeHtml(u.name || u.username)}</span>
|
||||
<span class="text-xs text-gray-400">${escapeHtml(u.username)}</span>
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
document.getElementById('nrAddModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeNrAddModal() {
|
||||
document.getElementById('nrAddModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function submitNrAdd() {
|
||||
const checked = [...document.querySelectorAll('.nrAddCheck:checked')].map(c => Number(c.value));
|
||||
if (checked.length === 0) { showToast('사용자를 선택해주세요.', 'error'); return; }
|
||||
|
||||
try {
|
||||
for (const userId of checked) {
|
||||
await api('/notification-recipients', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ notification_type: nrAddType, user_id: userId })
|
||||
});
|
||||
}
|
||||
showToast(`${checked.length}명 수신자 추가 완료`);
|
||||
closeNrAddModal();
|
||||
nrLoaded = false;
|
||||
await loadNotificationRecipientsTab();
|
||||
} catch (e) {
|
||||
showToast('수신자 추가 실패: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeNrRecipient(type, userId) {
|
||||
if (!confirm('이 수신자를 제거하시겠습니까?')) return;
|
||||
try {
|
||||
await api(`/notification-recipients/${type}/${userId}`, { method: 'DELETE' });
|
||||
showToast('수신자 제거 완료');
|
||||
nrLoaded = false;
|
||||
await loadNotificationRecipientsTab();
|
||||
} catch (e) {
|
||||
showToast('수신자 제거 실패: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user