- 일일순회점검 시스템 신규 구현 - DB 테이블: patrol_checklist_items, daily_patrol_sessions, patrol_check_records, workplace_items, item_types - API: /api/patrol/* 엔드포인트 - 프론트엔드: 지도 기반 작업장 점검 UI - 설비 관리 기능 개선 - 구매 관련 필드 추가 (구매일, 가격, 공급업체 등) - 설비 코드 자동 생성 (TKP-XXX 형식) - 작업장 관리 개선 - 레이아웃 이미지 업로드 기능 - 마커 위치 저장 기능 - 부서 관리 기능 추가 - 사이드바 네비게이션 카테고리 재구성 - 이미지 401 오류 수정 (정적 파일 경로 처리) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
454 lines
15 KiB
JavaScript
454 lines
15 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 =>
|
|
`<option value="${w.workplace_id}">${w.category_name ? w.category_name + ' - ' : ''}${w.workplace_name}</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 =>
|
|
`<option value="${type}">${type}</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 => `
|
|
<tr>
|
|
<td class="eq-col-code">${eq.equipment_code || '-'}</td>
|
|
<td class="eq-col-name" title="${eq.equipment_name || ''}">${eq.equipment_name || '-'}</td>
|
|
<td class="eq-col-model" title="${eq.model_name || ''}">${eq.model_name || '-'}</td>
|
|
<td class="eq-col-spec" title="${eq.specifications || ''}">${eq.specifications || '-'}</td>
|
|
<td>${eq.manufacturer || '-'}</td>
|
|
<td>${eq.supplier || '-'}</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-${eq.status}">
|
|
${getStatusText(eq.status)}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<div class="eq-actions">
|
|
<button class="eq-btn-action eq-btn-edit" onclick="editEquipment(${eq.equipment_id})" title="수정">
|
|
✏️
|
|
</button>
|
|
<button class="eq-btn-action eq-btn-delete" onclick="deleteEquipment(${eq.equipment_id})" 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();
|
|
}
|
|
});
|