feat(tksafety): 위험성평가 모듈 Phase 1 구현 — DB·API·Excel·프론트엔드
5개 테이블(risk_projects/processes/items/mitigations/templates) + 마스터 시딩, 프로젝트·항목·감소대책 CRUD API, ExcelJS 평가표 내보내기, 프로젝트 목록·평가 수행 페이지, 사진 업로드(multer), 네비게이션·CSS 추가. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,17 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
||||
.visit-table td:last-child { gap: 2px; display: flex; flex-wrap: nowrap; justify-content: flex-end; align-items: center; }
|
||||
}
|
||||
|
||||
/* Risk assessment */
|
||||
.badge-risk-high { background: #fef2f2; color: #dc2626; }
|
||||
.badge-risk-substantial { background: #fff7ed; color: #ea580c; }
|
||||
.badge-risk-moderate { background: #fefce8; color: #ca8a04; }
|
||||
.badge-risk-low { background: #f0fdf4; color: #16a34a; }
|
||||
.risk-table th { background: #dc2626; color: white; padding: 0.5rem; font-size: 0.75rem; text-align: center; white-space: nowrap; }
|
||||
.risk-table td { padding: 0.5rem; border: 1px solid #e5e7eb; font-size: 0.8rem; vertical-align: middle; }
|
||||
.risk-table tr:hover { background: #fef2f2; }
|
||||
.risk-section-header { background: #fee2e2; font-weight: 600; }
|
||||
.process-caret { transition: transform 0.2s; display: inline-block; }
|
||||
|
||||
/* iOS zoom prevention */
|
||||
@media (max-width: 768px) {
|
||||
input, select, textarea { font-size: 16px !important; }
|
||||
|
||||
@@ -111,6 +111,7 @@ function renderNavbar() {
|
||||
{ href: '/entry-dashboard.html', icon: 'fa-id-card-alt', label: '출입 현황판', match: ['entry-dashboard.html'], admin: true },
|
||||
{ href: '/education.html', icon: 'fa-graduation-cap', label: '안전교육', match: ['education.html'] },
|
||||
{ href: '/training.html', icon: 'fa-chalkboard-teacher', label: '안전교육 실시', match: ['training.html'], admin: true },
|
||||
{ href: '/risk-projects.html', icon: 'fa-exclamation-triangle', label: '위험성평가', match: ['risk-projects.html', 'risk-assess.html'], admin: true },
|
||||
{ href: '/checklist.html', icon: 'fa-tasks', label: '체크리스트 관리', match: ['checklist.html'], admin: true },
|
||||
];
|
||||
const nav = document.getElementById('sideNav');
|
||||
|
||||
484
tksafety/web/static/js/tksafety-risk.js
Normal file
484
tksafety/web/static/js/tksafety-risk.js
Normal file
@@ -0,0 +1,484 @@
|
||||
/* ===== 위험성평가 모듈 ===== */
|
||||
|
||||
const PRODUCT_TYPES = ['PKG', 'VESSEL', 'HX', 'SKID'];
|
||||
const STATUS_LABELS = { draft: '작성중', in_progress: '진행중', completed: '완료' };
|
||||
const STATUS_BADGE = { draft: 'badge-gray', in_progress: 'badge-amber', completed: 'badge-green' };
|
||||
const TYPE_LABELS = { regular: '정기', adhoc: '수시' };
|
||||
const MITIGATION_STATUS = { planned: '계획', in_progress: '진행중', completed: '완료' };
|
||||
|
||||
function riskLevelClass(score) {
|
||||
if (!score) return '';
|
||||
if (score <= 4) return 'badge-risk-low';
|
||||
if (score <= 9) return 'badge-risk-moderate';
|
||||
if (score <= 15) return 'badge-risk-substantial';
|
||||
return 'badge-risk-high';
|
||||
}
|
||||
function riskLevelLabel(score) {
|
||||
if (!score) return '-';
|
||||
if (score <= 4) return '저';
|
||||
if (score <= 9) return '보통';
|
||||
if (score <= 15) return '상당';
|
||||
return '높음';
|
||||
}
|
||||
|
||||
// ==================== 프로젝트 목록 페이지 ====================
|
||||
|
||||
async function initRiskProjectsPage() {
|
||||
if (!initAuth()) return;
|
||||
await loadProjects();
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
const typeFilter = document.getElementById('filterType')?.value;
|
||||
const yearFilter = document.getElementById('filterYear')?.value;
|
||||
const productFilter = document.getElementById('filterProduct')?.value;
|
||||
const statusFilter = document.getElementById('filterStatus')?.value;
|
||||
if (typeFilter) params.set('assessment_type', typeFilter);
|
||||
if (yearFilter) params.set('year', yearFilter);
|
||||
if (productFilter) params.set('product_type', productFilter);
|
||||
if (statusFilter) params.set('status', statusFilter);
|
||||
|
||||
const res = await api('/risk/projects?' + params.toString());
|
||||
const { projects } = res.data;
|
||||
renderProjectTable(projects);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('프로젝트 목록을 불러올 수 없습니다', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderProjectTable(projects) {
|
||||
const tbody = document.getElementById('projectTableBody');
|
||||
if (!tbody) return;
|
||||
if (!projects.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-400 py-8">프로젝트가 없습니다</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = projects.map(p => `
|
||||
<tr class="cursor-pointer hover:bg-blue-50" onclick="location.href='risk-assess.html?id=${p.id}'">
|
||||
<td class="px-3 py-2">${escapeHtml(p.title)}</td>
|
||||
<td class="px-3 py-2 text-center"><span class="badge ${p.assessment_type === 'regular' ? 'badge-blue' : 'badge-amber'}">${TYPE_LABELS[p.assessment_type]}</span></td>
|
||||
<td class="px-3 py-2 text-center">${p.product_type}</td>
|
||||
<td class="px-3 py-2 text-center">${p.year}${p.month ? '/' + p.month : ''}</td>
|
||||
<td class="px-3 py-2 text-center"><span class="badge ${STATUS_BADGE[p.status]}">${STATUS_LABELS[p.status]}</span></td>
|
||||
<td class="px-3 py-2 text-center">${p.total_items || 0}</td>
|
||||
<td class="px-3 py-2 text-center">${p.high_risk_count ? `<span class="badge badge-risk-high">${p.high_risk_count}</span>` : '-'}</td>
|
||||
<td class="px-3 py-2 text-center whitespace-nowrap">
|
||||
<a href="/api/risk/projects/${p.id}/export" class="text-green-600 hover:text-green-800 mr-2" title="Excel 다운로드" onclick="event.stopPropagation()">
|
||||
<i class="fas fa-file-excel"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function openNewProjectModal() {
|
||||
document.getElementById('projectModal').classList.remove('hidden');
|
||||
document.getElementById('projectForm').reset();
|
||||
document.getElementById('projectYear').value = new Date().getFullYear();
|
||||
}
|
||||
function closeProjectModal() {
|
||||
document.getElementById('projectModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function submitProject(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
title: document.getElementById('projectTitle').value.trim(),
|
||||
assessment_type: document.getElementById('projectAssessType').value,
|
||||
product_type: document.getElementById('projectProductType').value,
|
||||
year: parseInt(document.getElementById('projectYear').value),
|
||||
month: document.getElementById('projectMonth').value ? parseInt(document.getElementById('projectMonth').value) : null,
|
||||
assessed_by: document.getElementById('projectAssessedBy').value.trim() || null
|
||||
};
|
||||
if (!data.title) return showToast('제목을 입력하세요', 'error');
|
||||
|
||||
try {
|
||||
const res = await api('/risk/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
showToast('프로젝트가 생성되었습니다');
|
||||
closeProjectModal();
|
||||
location.href = 'risk-assess.html?id=' + res.data.id;
|
||||
} catch (err) {
|
||||
showToast(err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 평가 수행 페이지 ====================
|
||||
|
||||
let riskProject = null;
|
||||
|
||||
async function initRiskAssessPage() {
|
||||
if (!initAuth()) return;
|
||||
const id = new URLSearchParams(location.search).get('id');
|
||||
if (!id) { location.href = 'risk-projects.html'; return; }
|
||||
await loadProject(id);
|
||||
}
|
||||
|
||||
async function loadProject(id) {
|
||||
try {
|
||||
const res = await api('/risk/projects/' + id);
|
||||
riskProject = res.data;
|
||||
renderProjectHeader();
|
||||
renderProcesses();
|
||||
renderMitigations();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast('프로젝트를 불러올 수 없습니다', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderProjectHeader() {
|
||||
const el = document.getElementById('projectHeader');
|
||||
if (!el) return;
|
||||
const p = riskProject;
|
||||
el.innerHTML = `
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-gray-800">${escapeHtml(p.title)}</h2>
|
||||
<div class="flex flex-wrap gap-2 mt-1 text-sm text-gray-500">
|
||||
<span class="badge ${p.assessment_type === 'regular' ? 'badge-blue' : 'badge-amber'}">${TYPE_LABELS[p.assessment_type]}</span>
|
||||
<span>${p.product_type}</span>
|
||||
<span>${p.year}년${p.month ? ' ' + p.month + '월' : ''}</span>
|
||||
<span class="badge ${STATUS_BADGE[p.status]}">${STATUS_LABELS[p.status]}</span>
|
||||
${p.assessed_by ? `<span>평가자: ${escapeHtml(p.assessed_by)}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<select id="projectStatusSelect" onchange="changeProjectStatus(this.value)"
|
||||
class="input-field px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="draft" ${p.status==='draft'?'selected':''}>작성중</option>
|
||||
<option value="in_progress" ${p.status==='in_progress'?'selected':''}>진행중</option>
|
||||
<option value="completed" ${p.status==='completed'?'selected':''}>완료</option>
|
||||
</select>
|
||||
<a href="/api/risk/projects/${p.id}/export"
|
||||
class="px-4 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700 inline-flex items-center gap-1">
|
||||
<i class="fas fa-file-excel"></i> Excel
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function changeProjectStatus(status) {
|
||||
try {
|
||||
await api('/risk/projects/' + riskProject.id, {
|
||||
method: 'PATCH', body: JSON.stringify({ status })
|
||||
});
|
||||
riskProject.status = status;
|
||||
renderProjectHeader();
|
||||
showToast('상태가 변경되었습니다');
|
||||
} catch (err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
// ==================== 세부 공정 + 평가 항목 ====================
|
||||
|
||||
function renderProcesses() {
|
||||
const container = document.getElementById('processContainer');
|
||||
if (!container) return;
|
||||
const processes = riskProject.processes || [];
|
||||
|
||||
if (!processes.length) {
|
||||
container.innerHTML = `<div class="bg-white rounded-xl shadow-sm p-8 text-center text-gray-400">
|
||||
세부 공정이 없습니다. ${riskProject.assessment_type === 'adhoc' ? '<button onclick="openAddProcessModal()" class="text-blue-500 hover:underline ml-1">공정 추가</button>' : ''}
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = processes.map((proc, idx) => `
|
||||
<div class="bg-white rounded-xl shadow-sm mb-4 overflow-hidden">
|
||||
<button onclick="toggleProcess(${idx})" class="w-full risk-section-header px-4 py-3 flex items-center justify-between text-left">
|
||||
<span><i class="fas fa-caret-right mr-2 process-caret" id="caret-${idx}"></i>${escapeHtml(proc.process_name)}</span>
|
||||
<span class="text-xs text-gray-500">${(proc.items||[]).length}건</span>
|
||||
</button>
|
||||
<div id="process-${idx}" class="hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="risk-table w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-10">No</th><th>분류</th><th>원인(작업)</th><th>유해·위험요인</th>
|
||||
<th>관련법규</th><th>현재 안전조치</th><th class="w-12">가능성</th><th class="w-12">중대성</th>
|
||||
<th class="w-14">위험성</th><th>대책No</th><th>세부내용</th><th class="w-16">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="items-${proc.id}">
|
||||
${renderItems(proc.items || [], proc.id)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-3 border-t">
|
||||
<button onclick="openItemModal(${proc.id})" class="px-3 py-1.5 bg-blue-600 text-white rounded text-xs hover:bg-blue-700">
|
||||
<i class="fas fa-plus mr-1"></i>항목 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// 첫 번째 공정 자동 열기
|
||||
if (processes.length > 0) toggleProcess(0);
|
||||
}
|
||||
|
||||
function renderItems(items, processId) {
|
||||
if (!items.length) return '<tr><td colspan="12" class="text-center text-gray-400 py-4">항목이 없습니다</td></tr>';
|
||||
return items.map((item, idx) => `
|
||||
<tr>
|
||||
<td class="text-center">${idx + 1}</td>
|
||||
<td>${escapeHtml(item.category || '')}</td>
|
||||
<td>${escapeHtml(item.cause || '')}</td>
|
||||
<td>${escapeHtml(item.hazard || '')}</td>
|
||||
<td>${escapeHtml(item.regulation || '')}</td>
|
||||
<td>${escapeHtml(item.current_measure || '')}</td>
|
||||
<td class="text-center">${item.likelihood || ''}</td>
|
||||
<td class="text-center">${item.severity || ''}</td>
|
||||
<td class="text-center"><span class="badge ${riskLevelClass(item.risk_score)}">${item.risk_score || ''}</span></td>
|
||||
<td class="text-center">${escapeHtml(item.mitigation_no || '')}</td>
|
||||
<td>${escapeHtml(item.detail || '')}</td>
|
||||
<td class="text-center whitespace-nowrap">
|
||||
<button onclick="openItemModal(${processId}, ${item.id})" class="text-blue-500 hover:text-blue-700 mr-1" title="수정"><i class="fas fa-edit"></i></button>
|
||||
<button onclick="deleteItemConfirm(${item.id})" class="text-red-400 hover:text-red-600" title="삭제"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function toggleProcess(idx) {
|
||||
const el = document.getElementById('process-' + idx);
|
||||
const caret = document.getElementById('caret-' + idx);
|
||||
if (!el) return;
|
||||
const isOpen = !el.classList.contains('hidden');
|
||||
el.classList.toggle('hidden');
|
||||
if (caret) caret.style.transform = isOpen ? '' : 'rotate(90deg)';
|
||||
}
|
||||
|
||||
// ==================== 항목 모달 ====================
|
||||
|
||||
let editingItemId = null;
|
||||
let editingProcessId = null;
|
||||
|
||||
function openItemModal(processId, itemId) {
|
||||
editingProcessId = processId;
|
||||
editingItemId = itemId || null;
|
||||
const modal = document.getElementById('riskItemModal');
|
||||
modal.classList.remove('hidden');
|
||||
document.getElementById('riskItemModalTitle').textContent = itemId ? '항목 수정' : '항목 추가';
|
||||
const form = document.getElementById('riskItemForm');
|
||||
form.reset();
|
||||
|
||||
if (itemId) {
|
||||
// 기존 데이터 채우기
|
||||
let item = null;
|
||||
for (const proc of riskProject.processes) {
|
||||
item = (proc.items || []).find(i => i.id === itemId);
|
||||
if (item) break;
|
||||
}
|
||||
if (item) {
|
||||
document.getElementById('riCategory').value = item.category || '';
|
||||
document.getElementById('riCause').value = item.cause || '';
|
||||
document.getElementById('riHazard').value = item.hazard || '';
|
||||
document.getElementById('riRegulation').value = item.regulation || '';
|
||||
document.getElementById('riCurrentMeasure').value = item.current_measure || '';
|
||||
document.getElementById('riLikelihood').value = item.likelihood || '';
|
||||
document.getElementById('riSeverity').value = item.severity || '';
|
||||
document.getElementById('riMitigationNo').value = item.mitigation_no || '';
|
||||
document.getElementById('riDetail').value = item.detail || '';
|
||||
}
|
||||
}
|
||||
}
|
||||
function closeItemModal() {
|
||||
document.getElementById('riskItemModal').classList.add('hidden');
|
||||
editingItemId = null;
|
||||
editingProcessId = null;
|
||||
}
|
||||
|
||||
async function submitItem(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
category: document.getElementById('riCategory').value.trim() || null,
|
||||
cause: document.getElementById('riCause').value.trim() || null,
|
||||
hazard: document.getElementById('riHazard').value.trim() || null,
|
||||
regulation: document.getElementById('riRegulation').value.trim() || null,
|
||||
current_measure: document.getElementById('riCurrentMeasure').value.trim() || null,
|
||||
likelihood: document.getElementById('riLikelihood').value ? parseInt(document.getElementById('riLikelihood').value) : null,
|
||||
severity: document.getElementById('riSeverity').value ? parseInt(document.getElementById('riSeverity').value) : null,
|
||||
mitigation_no: document.getElementById('riMitigationNo').value.trim() || null,
|
||||
detail: document.getElementById('riDetail').value.trim() || null,
|
||||
};
|
||||
|
||||
try {
|
||||
if (editingItemId) {
|
||||
await api('/risk/items/' + editingItemId, { method: 'PATCH', body: JSON.stringify(data) });
|
||||
showToast('항목이 수정되었습니다');
|
||||
} else {
|
||||
await api('/risk/processes/' + editingProcessId + '/items', { method: 'POST', body: JSON.stringify(data) });
|
||||
showToast('항목이 추가되었습니다');
|
||||
}
|
||||
closeItemModal();
|
||||
await loadProject(riskProject.id);
|
||||
} catch (err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function deleteItemConfirm(itemId) {
|
||||
if (!confirm('이 항목을 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
await api('/risk/items/' + itemId, { method: 'DELETE' });
|
||||
showToast('항목이 삭제되었습니다');
|
||||
await loadProject(riskProject.id);
|
||||
} catch (err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
// ==================== 공정 추가 (수시 평가용) ====================
|
||||
|
||||
function openAddProcessModal() {
|
||||
const name = prompt('추가할 공정명을 입력하세요:');
|
||||
if (!name || !name.trim()) return;
|
||||
addProcessToProject(name.trim());
|
||||
}
|
||||
|
||||
async function addProcessToProject(processName) {
|
||||
try {
|
||||
await api('/risk/projects/' + riskProject.id + '/processes', {
|
||||
method: 'POST', body: JSON.stringify({ process_name: processName })
|
||||
});
|
||||
showToast('공정이 추가되었습니다');
|
||||
await loadProject(riskProject.id);
|
||||
} catch (err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
// ==================== 감소대책 ====================
|
||||
|
||||
function renderMitigations() {
|
||||
const container = document.getElementById('mitigationContainer');
|
||||
if (!container) return;
|
||||
const mitigations = riskProject.mitigations || [];
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-base font-semibold text-gray-800"><i class="fas fa-shield-alt text-blue-500 mr-2"></i>감소대책 수립 및 실행</h3>
|
||||
<button onclick="openMitigationModal()" class="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-xs hover:bg-blue-700">
|
||||
<i class="fas fa-plus mr-1"></i>대책 추가
|
||||
</button>
|
||||
</div>
|
||||
${mitigations.length === 0 ? '<div class="text-center text-gray-400 py-6">감소대책이 없습니다</div>' :
|
||||
`<div class="grid grid-cols-1 md:grid-cols-2 gap-4">${mitigations.map(m => renderMitigationCard(m)).join('')}</div>`}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderMitigationCard(m) {
|
||||
return `
|
||||
<div class="bg-white border rounded-xl p-4 shadow-sm">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-lg font-bold text-blue-600">#${escapeHtml(m.mitigation_no)}</span>
|
||||
<span class="badge ${STATUS_BADGE[m.status] || 'badge-gray'}">${MITIGATION_STATUS[m.status] || m.status}</span>
|
||||
</div>
|
||||
<button onclick="openMitigationModal(${m.id})" class="text-blue-500 hover:text-blue-700 text-sm"><i class="fas fa-edit"></i></button>
|
||||
</div>
|
||||
<div class="text-sm space-y-1 text-gray-600">
|
||||
<div><strong>유해·위험요인:</strong> ${escapeHtml(m.hazard_summary || '-')}</div>
|
||||
<div><strong>현재 위험성:</strong> <span class="badge ${riskLevelClass(m.current_risk_score)}">${m.current_risk_score || '-'}</span></div>
|
||||
<div><strong>개선계획:</strong> ${escapeHtml(m.improvement_plan || '-')}</div>
|
||||
<div class="flex gap-4">
|
||||
<span><strong>담당:</strong> ${escapeHtml(m.manager || '-')}</span>
|
||||
<span><strong>예산:</strong> ${escapeHtml(m.budget || '-')}</span>
|
||||
<span><strong>일정:</strong> ${escapeHtml(m.schedule || '-')}</span>
|
||||
</div>
|
||||
${m.completion_date ? `<div><strong>완료일:</strong> ${formatDate(m.completion_date)}</div>` : ''}
|
||||
${m.post_risk_score ? `<div><strong>대책 후 위험성:</strong> <span class="badge ${riskLevelClass(m.post_risk_score)}">${m.post_risk_score} (${m.post_likelihood}×${m.post_severity})</span></div>` : ''}
|
||||
</div>
|
||||
<div class="mt-3 pt-3 border-t">
|
||||
<form onsubmit="uploadMitigationPhoto(event, ${m.id})" class="flex items-center gap-2">
|
||||
<input type="file" accept="image/*" class="text-xs flex-1" id="photoInput-${m.id}">
|
||||
<button type="submit" class="px-2 py-1 bg-gray-100 rounded text-xs hover:bg-gray-200"><i class="fas fa-upload"></i></button>
|
||||
</form>
|
||||
${m.completion_photo ? `<img src="${m.completion_photo}" class="mt-2 rounded max-h-32 object-cover" alt="완료사진">` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ==================== 감소대책 모달 ====================
|
||||
|
||||
let editingMitigationId = null;
|
||||
|
||||
function openMitigationModal(mitigationId) {
|
||||
editingMitigationId = mitigationId || null;
|
||||
const modal = document.getElementById('mitigationModal');
|
||||
modal.classList.remove('hidden');
|
||||
document.getElementById('mitigationModalTitle').textContent = mitigationId ? '감소대책 수정' : '감소대책 추가';
|
||||
const form = document.getElementById('mitigationForm');
|
||||
form.reset();
|
||||
|
||||
if (mitigationId) {
|
||||
const m = (riskProject.mitigations || []).find(x => x.id === mitigationId);
|
||||
if (m) {
|
||||
document.getElementById('rmNo').value = m.mitigation_no || '';
|
||||
document.getElementById('rmHazard').value = m.hazard_summary || '';
|
||||
document.getElementById('rmRiskScore').value = m.current_risk_score || '';
|
||||
document.getElementById('rmPlan').value = m.improvement_plan || '';
|
||||
document.getElementById('rmManager').value = m.manager || '';
|
||||
document.getElementById('rmBudget').value = m.budget || '';
|
||||
document.getElementById('rmSchedule').value = m.schedule || '';
|
||||
document.getElementById('rmCompDate').value = m.completion_date ? String(m.completion_date).substring(0, 10) : '';
|
||||
document.getElementById('rmPostLikelihood').value = m.post_likelihood || '';
|
||||
document.getElementById('rmPostSeverity').value = m.post_severity || '';
|
||||
document.getElementById('rmStatus').value = m.status || 'planned';
|
||||
}
|
||||
} else {
|
||||
// 자동 번호 부여
|
||||
const existing = riskProject.mitigations || [];
|
||||
const maxNo = existing.reduce((max, m) => Math.max(max, parseInt(m.mitigation_no) || 0), 0);
|
||||
document.getElementById('rmNo').value = maxNo + 1;
|
||||
}
|
||||
}
|
||||
function closeMitigationModal() {
|
||||
document.getElementById('mitigationModal').classList.add('hidden');
|
||||
editingMitigationId = null;
|
||||
}
|
||||
|
||||
async function submitMitigation(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
mitigation_no: document.getElementById('rmNo').value.trim(),
|
||||
hazard_summary: document.getElementById('rmHazard').value.trim() || null,
|
||||
current_risk_score: document.getElementById('rmRiskScore').value ? parseInt(document.getElementById('rmRiskScore').value) : null,
|
||||
improvement_plan: document.getElementById('rmPlan').value.trim() || null,
|
||||
manager: document.getElementById('rmManager').value.trim() || null,
|
||||
budget: document.getElementById('rmBudget').value.trim() || null,
|
||||
schedule: document.getElementById('rmSchedule').value.trim() || null,
|
||||
completion_date: document.getElementById('rmCompDate').value || null,
|
||||
post_likelihood: document.getElementById('rmPostLikelihood').value ? parseInt(document.getElementById('rmPostLikelihood').value) : null,
|
||||
post_severity: document.getElementById('rmPostSeverity').value ? parseInt(document.getElementById('rmPostSeverity').value) : null,
|
||||
status: document.getElementById('rmStatus').value
|
||||
};
|
||||
if (!data.mitigation_no) return showToast('대책 번호를 입력하세요', 'error');
|
||||
|
||||
try {
|
||||
if (editingMitigationId) {
|
||||
await api('/risk/mitigations/' + editingMitigationId, { method: 'PATCH', body: JSON.stringify(data) });
|
||||
showToast('감소대책이 수정되었습니다');
|
||||
} else {
|
||||
await api('/risk/projects/' + riskProject.id + '/mitigations', { method: 'POST', body: JSON.stringify(data) });
|
||||
showToast('감소대책이 추가되었습니다');
|
||||
}
|
||||
closeMitigationModal();
|
||||
await loadProject(riskProject.id);
|
||||
} catch (err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function uploadMitigationPhoto(e, mitigationId) {
|
||||
e.preventDefault();
|
||||
const input = document.getElementById('photoInput-' + mitigationId);
|
||||
if (!input.files.length) return showToast('사진을 선택하세요', 'error');
|
||||
const formData = new FormData();
|
||||
formData.append('photo', input.files[0]);
|
||||
try {
|
||||
await api('/risk/mitigations/' + mitigationId + '/photo', {
|
||||
method: 'POST', body: formData, headers: {}
|
||||
});
|
||||
showToast('사진이 업로드되었습니다');
|
||||
await loadProject(riskProject.id);
|
||||
} catch (err) { showToast(err.message, 'error'); }
|
||||
}
|
||||
Reference in New Issue
Block a user