Files
TK-FB-Project/web-ui/js/equipment-management.js
Hyungi Ahn 36f110c90a fix: 보안 취약점 수정 및 XSS 방지 적용
## 백엔드 보안 수정
- 하드코딩된 비밀번호 및 JWT 시크릿 폴백 제거
- SQL Injection 방지를 위한 화이트리스트 검증 추가
- 인증 미적용 API 라우트에 requireAuth 미들웨어 적용
- CSRF 보호 미들웨어 구현 (csrf.js)
- 파일 업로드 보안 유틸리티 추가 (fileUploadSecurity.js)
- 비밀번호 정책 검증 유틸리티 추가 (passwordValidator.js)

## 프론트엔드 XSS 방지
- api-base.js에 전역 escapeHtml() 함수 추가
- 17개 주요 JS 파일에 escapeHtml 적용:
  - tbm.js, daily-patrol.js, daily-work-report.js
  - task-management.js, workplace-status.js
  - equipment-detail.js, equipment-management.js
  - issue-detail.js, issue-report.js
  - vacation-common.js, worker-management.js
  - safety-report-list.js, nonconformity-list.js
  - project-management.js, workplace-management.js

## 정리
- 백업 폴더 및 빈 파일 삭제
- SECURITY_GUIDE.md 문서 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 06:33:10 +09:00

470 lines
16 KiB
JavaScript

// equipment-management.js
// 설비 관리 페이지 JavaScript
let equipments = [];
let allEquipments = []; // 필터링 전 전체 데이터
let workplaces = [];
let equipmentTypes = [];
let currentEquipment = null;
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', async () => {
await waitForAxiosConfig();
await loadInitialData();
});
// axios 설정 대기 함수
function waitForAxiosConfig() {
return new Promise((resolve) => {
const check = setInterval(() => {
if (axios.defaults.baseURL) {
clearInterval(check);
resolve();
}
}, 50);
setTimeout(() => {
clearInterval(check);
if (!axios.defaults.baseURL) {
console.error('Axios 설정 시간 초과');
}
resolve();
}, 5000);
});
}
// 초기 데이터 로드
async function loadInitialData() {
try {
await Promise.all([
loadEquipments(),
loadWorkplaces(),
loadEquipmentTypes()
]);
} catch (error) {
console.error('초기 데이터 로드 실패:', error);
alert('데이터를 불러오는데 실패했습니다.');
}
}
// 설비 목록 로드
async function loadEquipments() {
try {
const response = await axios.get('/equipments');
if (response.data.success) {
allEquipments = response.data.data;
equipments = [...allEquipments];
renderStats();
renderEquipmentList();
}
} catch (error) {
console.error('설비 목록 로드 실패:', error);
throw error;
}
}
// 작업장 목록 로드
async function loadWorkplaces() {
try {
const response = await axios.get('/workplaces');
if (response.data.success) {
workplaces = response.data.data;
populateWorkplaceFilters();
}
} catch (error) {
console.error('작업장 목록 로드 실패:', error);
}
}
// 설비 유형 목록 로드
async function loadEquipmentTypes() {
try {
const response = await axios.get('/equipments/types');
if (response.data.success) {
equipmentTypes = response.data.data;
populateTypeFilter();
}
} catch (error) {
console.error('설비 유형 로드 실패:', error);
}
}
// 통계 렌더링
function renderStats() {
const container = document.getElementById('statsSection');
if (!container) return;
const totalCount = allEquipments.length;
const activeCount = allEquipments.filter(e => e.status === 'active').length;
const maintenanceCount = allEquipments.filter(e => e.status === 'maintenance').length;
const inactiveCount = allEquipments.filter(e => e.status === 'inactive').length;
const totalValue = allEquipments.reduce((sum, e) => sum + (Number(e.purchase_price) || 0), 0);
const avgValue = totalCount > 0 ? totalValue / totalCount : 0;
container.innerHTML = `
<div class="eq-stat-card highlight">
<div class="eq-stat-label">전체 설비</div>
<div class="eq-stat-value">${totalCount}대</div>
<div class="eq-stat-sub">총 자산가치 ${formatPriceShort(totalValue)}</div>
</div>
<div class="eq-stat-card">
<div class="eq-stat-label">활성</div>
<div class="eq-stat-value" style="color: #16a34a;">${activeCount}대</div>
<div class="eq-stat-sub">${totalCount > 0 ? Math.round(activeCount / totalCount * 100) : 0}%</div>
</div>
<div class="eq-stat-card">
<div class="eq-stat-label">정비중</div>
<div class="eq-stat-value" style="color: #d97706;">${maintenanceCount}대</div>
<div class="eq-stat-sub">${totalCount > 0 ? Math.round(maintenanceCount / totalCount * 100) : 0}%</div>
</div>
<div class="eq-stat-card">
<div class="eq-stat-label">비활성</div>
<div class="eq-stat-value" style="color: #dc2626;">${inactiveCount}대</div>
<div class="eq-stat-sub">${totalCount > 0 ? Math.round(inactiveCount / totalCount * 100) : 0}%</div>
</div>
<div class="eq-stat-card">
<div class="eq-stat-label">평균 구입가</div>
<div class="eq-stat-value">${formatPriceShort(avgValue)}</div>
<div class="eq-stat-sub">설비당 평균</div>
</div>
`;
}
// 작업장 필터 채우기
function populateWorkplaceFilters() {
const filterWorkplace = document.getElementById('filterWorkplace');
const modalWorkplace = document.getElementById('workplaceId');
const workplaceOptions = workplaces.map(w => {
const safeId = parseInt(w.workplace_id) || 0;
const categoryName = escapeHtml(w.category_name || '');
const workplaceName = escapeHtml(w.workplace_name || '');
const label = categoryName ? categoryName + ' - ' + workplaceName : workplaceName;
return `<option value="${safeId}">${label}</option>`;
}).join('');
if (filterWorkplace) filterWorkplace.innerHTML = '<option value="">전체</option>' + workplaceOptions;
if (modalWorkplace) modalWorkplace.innerHTML = '<option value="">선택 안함</option>' + workplaceOptions;
}
// 설비 유형 필터 채우기
function populateTypeFilter() {
const filterType = document.getElementById('filterType');
if (!filterType) return;
const typeOptions = equipmentTypes.map(type => {
const safeType = escapeHtml(type || '');
return `<option value="${safeType}">${safeType}</option>`;
}).join('');
filterType.innerHTML = '<option value="">전체</option>' + typeOptions;
}
// 설비 목록 렌더링
function renderEquipmentList() {
const container = document.getElementById('equipmentList');
if (equipments.length === 0) {
container.innerHTML = `
<div class="eq-empty-state">
<p>등록된 설비가 없습니다.</p>
<button class="btn btn-primary" onclick="openEquipmentModal()">설비 추가하기</button>
</div>
`;
return;
}
const tableHTML = `
<div class="eq-result-count">
<span>검색 결과 <strong>${equipments.length}건</strong></span>
</div>
<div class="eq-table-wrapper">
<table class="eq-table">
<thead>
<tr>
<th>관리번호</th>
<th>설비명</th>
<th>모델명</th>
<th>규격</th>
<th>제조사</th>
<th>구입처</th>
<th style="text-align:right">구입가격</th>
<th>구입일자</th>
<th>상태</th>
<th style="width:80px">관리</th>
</tr>
</thead>
<tbody>
${equipments.map(eq => {
const safeId = parseInt(eq.equipment_id) || 0;
const safeCode = escapeHtml(eq.equipment_code || '-');
const safeName = escapeHtml(eq.equipment_name || '-');
const safeModel = escapeHtml(eq.model_name || '-');
const safeSpec = escapeHtml(eq.specifications || '-');
const safeManufacturer = escapeHtml(eq.manufacturer || '-');
const safeSupplier = escapeHtml(eq.supplier || '-');
const validStatuses = ['active', 'maintenance', 'inactive'];
const safeStatus = validStatuses.includes(eq.status) ? eq.status : 'inactive';
return `
<tr>
<td class="eq-col-code">${safeCode}</td>
<td class="eq-col-name" title="${safeName}">${safeName}</td>
<td class="eq-col-model" title="${safeModel}">${safeModel}</td>
<td class="eq-col-spec" title="${safeSpec}">${safeSpec}</td>
<td>${safeManufacturer}</td>
<td>${safeSupplier}</td>
<td class="eq-col-price">${eq.purchase_price ? formatPrice(eq.purchase_price) : '-'}</td>
<td class="eq-col-date">${eq.installation_date ? formatDate(eq.installation_date) : '-'}</td>
<td>
<span class="eq-status eq-status-${safeStatus}">
${getStatusText(eq.status)}
</span>
</td>
<td>
<div class="eq-actions">
<button class="eq-btn-action eq-btn-edit" onclick="editEquipment(${safeId})" title="수정">
✏️
</button>
<button class="eq-btn-action eq-btn-delete" onclick="deleteEquipment(${safeId})" title="삭제">
🗑️
</button>
</div>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = tableHTML;
}
// 상태 텍스트 변환
function getStatusText(status) {
const statusMap = {
'active': '활성',
'maintenance': '정비중',
'inactive': '비활성'
};
return statusMap[status] || status || '-';
}
// 가격 포맷팅 (전체)
function formatPrice(price) {
if (!price) return '-';
return Number(price).toLocaleString('ko-KR') + '원';
}
// 가격 포맷팅 (축약)
function formatPriceShort(price) {
if (!price) return '0원';
const num = Number(price);
if (num >= 100000000) {
return (num / 100000000).toFixed(1).replace(/\.0$/, '') + '억원';
} else if (num >= 10000) {
return (num / 10000).toFixed(0) + '만원';
}
return num.toLocaleString('ko-KR') + '원';
}
// 날짜 포맷팅
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' });
}
// 필터링
function filterEquipments() {
const workplaceFilter = document.getElementById('filterWorkplace').value;
const typeFilter = document.getElementById('filterType').value;
const statusFilter = document.getElementById('filterStatus').value;
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
equipments = allEquipments.filter(e => {
if (workplaceFilter && e.workplace_id != workplaceFilter) return false;
if (typeFilter && e.equipment_type !== typeFilter) return false;
if (statusFilter && e.status !== statusFilter) return false;
if (searchTerm) {
const searchFields = [
e.equipment_name,
e.equipment_code,
e.manufacturer,
e.supplier,
e.model_name
].map(f => (f || '').toLowerCase());
if (!searchFields.some(f => f.includes(searchTerm))) return false;
}
return true;
});
renderEquipmentList();
}
// 설비 추가 모달 열기
async function openEquipmentModal(equipmentId = null) {
currentEquipment = equipmentId;
const modal = document.getElementById('equipmentModal');
const modalTitle = document.getElementById('modalTitle');
const form = document.getElementById('equipmentForm');
form.reset();
document.getElementById('equipmentId').value = '';
if (equipmentId) {
modalTitle.textContent = '설비 수정';
loadEquipmentData(equipmentId);
} else {
modalTitle.textContent = '설비 추가';
// 새 설비일 경우 다음 관리번호 자동 생성
await loadNextEquipmentCode();
}
modal.style.display = 'flex';
}
// 다음 관리번호 로드
async function loadNextEquipmentCode() {
try {
console.log('📋 다음 관리번호 조회 중...');
const response = await axios.get('/equipments/next-code');
console.log('📋 다음 관리번호 응답:', response.data);
if (response.data.success) {
document.getElementById('equipmentCode').value = response.data.data.next_code;
console.log('✅ 다음 관리번호 설정:', response.data.data.next_code);
}
} catch (error) {
console.error('❌ 다음 관리번호 조회 실패:', error);
console.error('❌ 에러 상세:', error.response?.data || error.message);
// 오류 시 기본값으로 빈 값 유지 (사용자가 직접 입력)
}
}
// 설비 데이터 로드 (수정용)
async function loadEquipmentData(equipmentId) {
try {
const response = await axios.get(`/equipments/${equipmentId}`);
if (response.data.success) {
const eq = response.data.data;
document.getElementById('equipmentId').value = eq.equipment_id;
document.getElementById('equipmentCode').value = eq.equipment_code || '';
document.getElementById('equipmentName').value = eq.equipment_name || '';
document.getElementById('equipmentType').value = eq.equipment_type || '';
document.getElementById('workplaceId').value = eq.workplace_id || '';
document.getElementById('manufacturer').value = eq.manufacturer || '';
document.getElementById('supplier').value = eq.supplier || '';
document.getElementById('purchasePrice').value = eq.purchase_price || '';
document.getElementById('modelName').value = eq.model_name || '';
document.getElementById('serialNumber').value = eq.serial_number || '';
document.getElementById('installationDate').value = eq.installation_date ? eq.installation_date.split('T')[0] : '';
document.getElementById('equipmentStatus').value = eq.status || 'active';
document.getElementById('specifications').value = eq.specifications || '';
document.getElementById('notes').value = eq.notes || '';
}
} catch (error) {
console.error('설비 데이터 로드 실패:', error);
alert('설비 정보를 불러오는데 실패했습니다.');
}
}
// 설비 모달 닫기
function closeEquipmentModal() {
document.getElementById('equipmentModal').style.display = 'none';
currentEquipment = null;
}
// 설비 저장
async function saveEquipment() {
const equipmentId = document.getElementById('equipmentId').value;
const equipmentData = {
equipment_code: document.getElementById('equipmentCode').value.trim(),
equipment_name: document.getElementById('equipmentName').value.trim(),
equipment_type: document.getElementById('equipmentType').value.trim() || null,
workplace_id: document.getElementById('workplaceId').value || null,
manufacturer: document.getElementById('manufacturer').value.trim() || null,
supplier: document.getElementById('supplier').value.trim() || null,
purchase_price: document.getElementById('purchasePrice').value || null,
model_name: document.getElementById('modelName').value.trim() || null,
serial_number: document.getElementById('serialNumber').value.trim() || null,
installation_date: document.getElementById('installationDate').value || null,
status: document.getElementById('equipmentStatus').value,
specifications: document.getElementById('specifications').value.trim() || null,
notes: document.getElementById('notes').value.trim() || null
};
if (!equipmentData.equipment_code) {
alert('관리번호를 입력해주세요.');
return;
}
if (!equipmentData.equipment_name) {
alert('설비명을 입력해주세요.');
return;
}
try {
let response;
if (equipmentId) {
response = await axios.put(`/equipments/${equipmentId}`, equipmentData);
} else {
response = await axios.post('/equipments', equipmentData);
}
if (response.data.success) {
alert(equipmentId ? '설비가 수정되었습니다.' : '설비가 추가되었습니다.');
closeEquipmentModal();
await loadEquipments();
await loadEquipmentTypes();
}
} catch (error) {
console.error('설비 저장 실패:', error);
if (error.response?.data?.message) {
alert(error.response.data.message);
} else {
alert('설비 저장 중 오류가 발생했습니다.');
}
}
}
// 설비 수정
function editEquipment(equipmentId) {
openEquipmentModal(equipmentId);
}
// 설비 삭제
async function deleteEquipment(equipmentId) {
const equipment = allEquipments.find(e => e.equipment_id === equipmentId);
if (!equipment) return;
if (!confirm(`'${equipment.equipment_name}' 설비를 삭제하시겠습니까?`)) {
return;
}
try {
const response = await axios.delete(`/equipments/${equipmentId}`);
if (response.data.success) {
alert('설비가 삭제되었습니다.');
await loadEquipments();
}
} catch (error) {
console.error('설비 삭제 실패:', error);
alert('설비 삭제 중 오류가 발생했습니다.');
}
}
// ESC 키로 모달 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeEquipmentModal();
}
});
// 모달 외부 클릭 시 닫기
document.getElementById('equipmentModal')?.addEventListener('click', (e) => {
if (e.target.id === 'equipmentModal') {
closeEquipmentModal();
}
});