feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
469
deploy/tkfb-package/web-ui/js/equipment-management.js
Normal file
469
deploy/tkfb-package/web-ui/js/equipment-management.js
Normal file
@@ -0,0 +1,469 @@
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user