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:
286
tksafety/web/static/js/tksafety-checklist.js
Normal file
286
tksafety/web/static/js/tksafety-checklist.js
Normal file
@@ -0,0 +1,286 @@
|
||||
/* ===== Checklist Management (체크리스트 관리 - 관리자) ===== */
|
||||
let checklistItems = [];
|
||||
let weatherConditions = [];
|
||||
let workTypes = [];
|
||||
let editingItemId = null;
|
||||
let currentTab = 'basic';
|
||||
|
||||
/* ===== Tab switching ===== */
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
['basic', 'weather', 'worktype'].forEach(t => {
|
||||
document.getElementById('panel' + t.charAt(0).toUpperCase() + t.slice(1)).classList.toggle('hidden', t !== tab);
|
||||
const tabBtn = document.getElementById('tab' + t.charAt(0).toUpperCase() + t.slice(1));
|
||||
if (t === tab) {
|
||||
tabBtn.classList.add('active');
|
||||
} else {
|
||||
tabBtn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ===== Load checklist items ===== */
|
||||
async function loadChecklistItems() {
|
||||
try {
|
||||
const res = await api('/checklist');
|
||||
checklistItems = res.data || [];
|
||||
renderBasicItems();
|
||||
renderWeatherItems();
|
||||
renderWorktypeItems();
|
||||
} catch (e) {
|
||||
showToast('체크리스트 로드 실패: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Load lookup data ===== */
|
||||
async function loadLookupData() {
|
||||
try {
|
||||
const [wcRes, wtRes] = await Promise.all([
|
||||
api('/checklist/weather-conditions'),
|
||||
api('/checklist/work-types')
|
||||
]);
|
||||
weatherConditions = wcRes.data || [];
|
||||
workTypes = wtRes.data || [];
|
||||
|
||||
// Populate selects
|
||||
document.getElementById('itemWeatherCondition').innerHTML = '<option value="">선택</option>' +
|
||||
weatherConditions.map(w => `<option value="${w.condition_id}">${escapeHtml(w.condition_name)}</option>`).join('');
|
||||
|
||||
document.getElementById('itemWorkType').innerHTML = '<option value="">선택</option>' +
|
||||
workTypes.map(w => `<option value="${w.work_type_id}">${escapeHtml(w.work_type_name)}</option>`).join('');
|
||||
} catch (e) {
|
||||
console.error('Lookup data load error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Render basic items ===== */
|
||||
function renderBasicItems() {
|
||||
const items = checklistItems.filter(i => i.item_type === 'basic');
|
||||
const container = document.getElementById('basicItemsList');
|
||||
if (!items.length) {
|
||||
container.innerHTML = '<div class="text-center text-gray-400 py-8">기본 항목이 없습니다</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by category
|
||||
const groups = {};
|
||||
items.forEach(i => {
|
||||
const cat = i.category || '미분류';
|
||||
if (!groups[cat]) groups[cat] = [];
|
||||
groups[cat].push(i);
|
||||
});
|
||||
|
||||
container.innerHTML = Object.entries(groups).map(([cat, items]) => `
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="bg-gray-50 px-4 py-2 font-medium text-sm text-gray-700">${escapeHtml(cat)}</div>
|
||||
<div class="divide-y">
|
||||
${items.map(i => renderItemRow(i)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/* ===== Render weather items ===== */
|
||||
function renderWeatherItems() {
|
||||
const items = checklistItems.filter(i => i.item_type === 'weather');
|
||||
const container = document.getElementById('weatherItemsList');
|
||||
if (!items.length) {
|
||||
container.innerHTML = '<div class="text-center text-gray-400 py-8">날씨별 항목이 없습니다</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by weather condition
|
||||
const groups = {};
|
||||
items.forEach(i => {
|
||||
const cond = i.weather_condition_name || '미지정';
|
||||
if (!groups[cond]) groups[cond] = [];
|
||||
groups[cond].push(i);
|
||||
});
|
||||
|
||||
container.innerHTML = Object.entries(groups).map(([cond, items]) => `
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="bg-blue-50 px-4 py-2 font-medium text-sm text-blue-700">
|
||||
<i class="fas fa-cloud mr-1"></i>${escapeHtml(cond)}
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
${items.map(i => renderItemRow(i)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/* ===== Render worktype items ===== */
|
||||
function renderWorktypeItems() {
|
||||
const items = checklistItems.filter(i => i.item_type === 'work_type');
|
||||
const container = document.getElementById('worktypeItemsList');
|
||||
if (!items.length) {
|
||||
container.innerHTML = '<div class="text-center text-gray-400 py-8">작업별 항목이 없습니다</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by work type
|
||||
const groups = {};
|
||||
items.forEach(i => {
|
||||
const wt = i.work_type_name || '미지정';
|
||||
if (!groups[wt]) groups[wt] = [];
|
||||
groups[wt].push(i);
|
||||
});
|
||||
|
||||
container.innerHTML = Object.entries(groups).map(([wt, items]) => `
|
||||
<div class="border rounded-lg overflow-hidden">
|
||||
<div class="bg-amber-50 px-4 py-2 font-medium text-sm text-amber-700">
|
||||
<i class="fas fa-hard-hat mr-1"></i>${escapeHtml(wt)}
|
||||
</div>
|
||||
<div class="divide-y">
|
||||
${items.map(i => renderItemRow(i)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/* ===== Render single item row ===== */
|
||||
function renderItemRow(item) {
|
||||
const activeClass = item.is_active ? '' : 'opacity-50';
|
||||
const activeIcon = item.is_active
|
||||
? '<i class="fas fa-check-circle text-green-500 text-xs"></i>'
|
||||
: '<i class="fas fa-times-circle text-gray-400 text-xs"></i>';
|
||||
return `
|
||||
<div class="flex items-center justify-between px-4 py-2.5 hover:bg-gray-50 ${activeClass}">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
${activeIcon}
|
||||
<span class="text-sm text-gray-700 truncate">${escapeHtml(item.item_content)}</span>
|
||||
${item.category ? `<span class="badge badge-gray text-xs">${escapeHtml(item.category)}</span>` : ''}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 flex-shrink-0 ml-2">
|
||||
<span class="text-xs text-gray-400 mr-2">#${item.display_order}</span>
|
||||
<button onclick="openEditItem(${item.item_id})" class="text-gray-400 hover:text-gray-600 text-xs p-1" title="수정">
|
||||
<i class="fas fa-pen"></i>
|
||||
</button>
|
||||
<button onclick="doDeleteItem(${item.item_id})" class="text-gray-400 hover:text-red-500 text-xs p-1" title="삭제">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/* ===== Add/Edit Modal ===== */
|
||||
function openAddItem(tab) {
|
||||
editingItemId = null;
|
||||
document.getElementById('itemModalTitle').textContent = '체크리스트 항목 추가';
|
||||
document.getElementById('itemForm').reset();
|
||||
document.getElementById('itemIsActive').checked = true;
|
||||
document.getElementById('itemDisplayOrder').value = '0';
|
||||
|
||||
// Set type based on tab
|
||||
const typeMap = { basic: 'basic', weather: 'weather', worktype: 'work_type' };
|
||||
document.getElementById('itemType').value = typeMap[tab] || 'basic';
|
||||
toggleTypeFields();
|
||||
|
||||
document.getElementById('itemModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function openEditItem(id) {
|
||||
const item = checklistItems.find(i => i.item_id === id);
|
||||
if (!item) return;
|
||||
editingItemId = id;
|
||||
document.getElementById('itemModalTitle').textContent = '체크리스트 항목 수정';
|
||||
document.getElementById('itemType').value = item.item_type;
|
||||
document.getElementById('itemCategory').value = item.category || '';
|
||||
document.getElementById('itemContent').value = item.item_content || '';
|
||||
document.getElementById('itemDisplayOrder').value = item.display_order || 0;
|
||||
document.getElementById('itemIsActive').checked = item.is_active !== false && item.is_active !== 0;
|
||||
|
||||
if (item.weather_condition_id) {
|
||||
document.getElementById('itemWeatherCondition').value = item.weather_condition_id;
|
||||
}
|
||||
if (item.work_type_id) {
|
||||
document.getElementById('itemWorkType').value = item.work_type_id;
|
||||
}
|
||||
|
||||
toggleTypeFields();
|
||||
document.getElementById('itemModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeItemModal() {
|
||||
document.getElementById('itemModal').classList.add('hidden');
|
||||
editingItemId = null;
|
||||
}
|
||||
|
||||
function toggleTypeFields() {
|
||||
const type = document.getElementById('itemType').value;
|
||||
document.getElementById('weatherConditionField').classList.toggle('hidden', type !== 'weather');
|
||||
document.getElementById('workTypeField').classList.toggle('hidden', type !== 'work_type');
|
||||
}
|
||||
|
||||
/* ===== Submit item ===== */
|
||||
async function submitItem(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
item_type: document.getElementById('itemType').value,
|
||||
category: document.getElementById('itemCategory').value.trim() || null,
|
||||
item_content: document.getElementById('itemContent').value.trim(),
|
||||
display_order: parseInt(document.getElementById('itemDisplayOrder').value) || 0,
|
||||
is_active: document.getElementById('itemIsActive').checked
|
||||
};
|
||||
|
||||
if (!data.item_content) { showToast('점검 항목을 입력해주세요', 'error'); return; }
|
||||
|
||||
if (data.item_type === 'weather') {
|
||||
data.weather_condition_id = parseInt(document.getElementById('itemWeatherCondition').value) || null;
|
||||
if (!data.weather_condition_id) { showToast('날씨 조건을 선택해주세요', 'error'); return; }
|
||||
}
|
||||
if (data.item_type === 'work_type') {
|
||||
data.work_type_id = parseInt(document.getElementById('itemWorkType').value) || null;
|
||||
if (!data.work_type_id) { showToast('작업 유형을 선택해주세요', 'error'); return; }
|
||||
}
|
||||
|
||||
try {
|
||||
if (editingItemId) {
|
||||
await api('/checklist/' + editingItemId, { method: 'PUT', body: JSON.stringify(data) });
|
||||
showToast('수정되었습니다');
|
||||
} else {
|
||||
await api('/checklist', { method: 'POST', body: JSON.stringify(data) });
|
||||
showToast('추가되었습니다');
|
||||
}
|
||||
closeItemModal();
|
||||
await loadChecklistItems();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Delete item ===== */
|
||||
async function doDeleteItem(id) {
|
||||
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
await api('/checklist/' + id, { method: 'DELETE' });
|
||||
showToast('삭제되었습니다');
|
||||
await loadChecklistItems();
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Init ===== */
|
||||
function initChecklistPage() {
|
||||
if (!initAuth()) return;
|
||||
|
||||
// Check admin
|
||||
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.role);
|
||||
if (!isAdmin) {
|
||||
document.querySelector('.flex-1.min-w-0').innerHTML = `
|
||||
<div class="bg-white rounded-xl shadow-sm p-10 text-center">
|
||||
<i class="fas fa-lock text-4xl text-gray-300 mb-4"></i>
|
||||
<p class="text-gray-500">관리자 권한이 필요합니다</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Type change handler
|
||||
document.getElementById('itemType').addEventListener('change', toggleTypeFields);
|
||||
document.getElementById('itemForm').addEventListener('submit', submitItem);
|
||||
|
||||
loadLookupData();
|
||||
loadChecklistItems();
|
||||
}
|
||||
Reference in New Issue
Block a user