/* ===== 위험성평가 모듈 ===== */ 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 = '프로젝트가 없습니다'; return; } tbody.innerHTML = projects.map(p => ` ${escapeHtml(p.title)} ${TYPE_LABELS[p.assessment_type]} ${p.product_type} ${p.year}${p.month ? '/' + p.month : ''} ${STATUS_LABELS[p.status]} ${p.total_items || 0} ${p.high_risk_count ? `${p.high_risk_count}` : '-'} `).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 = `

${escapeHtml(p.title)}

${TYPE_LABELS[p.assessment_type]} ${p.product_type} ${p.year}년${p.month ? ' ' + p.month + '월' : ''} ${STATUS_LABELS[p.status]} ${p.assessed_by ? `평가자: ${escapeHtml(p.assessed_by)}` : ''}
Excel
`; } 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 = `
세부 공정이 없습니다. ${riskProject.assessment_type === 'adhoc' ? '' : ''}
`; return; } container.innerHTML = processes.map((proc, idx) => `
`).join(''); // 첫 번째 공정 자동 열기 if (processes.length > 0) toggleProcess(0); } function renderItems(items, processId) { if (!items.length) return '항목이 없습니다'; return items.map((item, idx) => ` ${idx + 1} ${escapeHtml(item.category || '')} ${escapeHtml(item.cause || '')} ${escapeHtml(item.hazard || '')} ${escapeHtml(item.regulation || '')} ${escapeHtml(item.current_measure || '')} ${item.likelihood || ''} ${item.severity || ''} ${item.risk_score || ''} ${escapeHtml(item.mitigation_no || '')} ${escapeHtml(item.detail || '')} `).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 = `

감소대책 수립 및 실행

${mitigations.length === 0 ? '
감소대책이 없습니다
' : `
${mitigations.map(m => renderMitigationCard(m)).join('')}
`} `; } function renderMitigationCard(m) { return `
#${escapeHtml(m.mitigation_no)} ${MITIGATION_STATUS[m.status] || m.status}
유해·위험요인: ${escapeHtml(m.hazard_summary || '-')}
현재 위험성: ${m.current_risk_score || '-'}
개선계획: ${escapeHtml(m.improvement_plan || '-')}
담당: ${escapeHtml(m.manager || '-')} 예산: ${escapeHtml(m.budget || '-')} 일정: ${escapeHtml(m.schedule || '-')}
${m.completion_date ? `
완료일: ${formatDate(m.completion_date)}
` : ''} ${m.post_risk_score ? `
대책 후 위험성: ${m.post_risk_score} (${m.post_likelihood}×${m.post_severity})
` : ''}
${m.completion_photo ? `완료사진` : ''}
`; } // ==================== 감소대책 모달 ==================== 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'); } }