Files
TK-FB-Project/web-ui/js/code-management.js
Hyungi Ahn 90d3e32992 feat: 일일순회점검 시스템 구축 및 관리 기능 개선
- 일일순회점검 시스템 신규 구현
  - 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>
2026-02-04 11:41:41 +09:00

767 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 코드 관리 페이지 JavaScript
// 전역 변수
let workStatusTypes = [];
let errorTypes = [];
let workTypes = [];
let currentCodeType = 'work-status';
let currentEditingCode = null;
// 페이지 초기화
document.addEventListener('DOMContentLoaded', function() {
console.log('🏷️ 코드 관리 페이지 초기화 시작');
initializePage();
loadAllCodes();
});
// 페이지 초기화
function initializePage() {
// 시간 업데이트 시작
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
// 사용자 정보 업데이트
updateUserInfo();
// 프로필 메뉴 토글
setupProfileMenu();
// 로그아웃 버튼
setupLogoutButton();
}
// 현재 시간 업데이트
function updateCurrentTime() {
const now = new Date();
const timeString = now.toLocaleTimeString('ko-KR', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
const timeElement = document.getElementById('timeValue');
if (timeElement) {
timeElement.textContent = timeString;
}
}
// 사용자 정보 업데이트
// navbar/sidebar는 app-init.js에서 공통 처리
function updateUserInfo() {
// app-init.js가 navbar 사용자 정보를 처리
}
// 프로필 메뉴 설정
function setupProfileMenu() {
const userProfile = document.getElementById('userProfile');
const profileMenu = document.getElementById('profileMenu');
if (userProfile && profileMenu) {
userProfile.addEventListener('click', function(e) {
e.stopPropagation();
const isVisible = profileMenu.style.display === 'block';
profileMenu.style.display = isVisible ? 'none' : 'block';
});
// 외부 클릭 시 메뉴 닫기
document.addEventListener('click', function() {
profileMenu.style.display = 'none';
});
}
}
// 로그아웃 버튼 설정
function setupLogoutButton() {
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', function() {
if (confirm('로그아웃 하시겠습니까?')) {
localStorage.removeItem('token');
localStorage.removeItem('user');
localStorage.removeItem('userInfo');
window.location.href = '/index.html';
}
});
}
}
// 모든 코드 데이터 로드
async function loadAllCodes() {
try {
console.log('📊 모든 코드 데이터 로딩 시작');
await Promise.all([
loadWorkStatusTypes(),
// loadErrorTypes(), // 오류 유형은 신고 시스템으로 대체됨
loadWorkTypes()
]);
// 현재 활성 탭 렌더링
renderCurrentTab();
} catch (error) {
console.error('코드 데이터 로딩 오류:', error);
showToast('코드 데이터를 불러오는데 실패했습니다.', 'error');
}
}
// 작업 상태 유형 로드
async function loadWorkStatusTypes() {
try {
console.log('📊 작업 상태 유형 로딩...');
const response = await apiCall('/daily-work-reports/work-status-types', 'GET');
let statusData = [];
if (response && response.success && Array.isArray(response.data)) {
statusData = response.data;
} else if (Array.isArray(response)) {
statusData = response;
}
workStatusTypes = statusData;
console.log(`✅ 작업 상태 유형 ${workStatusTypes.length}개 로드 완료`);
} catch (error) {
console.error('작업 상태 유형 로딩 오류:', error);
workStatusTypes = [];
}
}
// 오류 유형 로드
async function loadErrorTypes() {
try {
console.log('⚠️ 오류 유형 로딩...');
const response = await apiCall('/daily-work-reports/error-types', 'GET');
let errorData = [];
if (response && response.success && Array.isArray(response.data)) {
errorData = response.data;
} else if (Array.isArray(response)) {
errorData = response;
}
errorTypes = errorData;
console.log(`✅ 오류 유형 ${errorTypes.length}개 로드 완료`);
} catch (error) {
console.error('오류 유형 로딩 오류:', error);
errorTypes = [];
}
}
// 작업 유형 로드
async function loadWorkTypes() {
try {
console.log('🔧 작업 유형 로딩...');
const response = await apiCall('/daily-work-reports/work-types', 'GET');
let typeData = [];
if (response && response.success && Array.isArray(response.data)) {
typeData = response.data;
} else if (Array.isArray(response)) {
typeData = response;
}
workTypes = typeData;
console.log(`✅ 작업 유형 ${workTypes.length}개 로드 완료`);
} catch (error) {
console.error('작업 유형 로딩 오류:', error);
workTypes = [];
}
}
// 코드 탭 전환
function switchCodeTab(tabName) {
// 탭 버튼 활성화 상태 변경
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
// 탭 콘텐츠 표시/숨김
document.querySelectorAll('.code-tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
currentCodeType = tabName;
renderCurrentTab();
}
// 현재 탭 렌더링
function renderCurrentTab() {
switch (currentCodeType) {
case 'work-status':
renderWorkStatusTypes();
break;
// case 'error-types': // 오류 유형은 신고 시스템으로 대체됨
// renderErrorTypes();
// break;
case 'work-types':
renderWorkTypes();
break;
}
}
// 작업 상태 유형 렌더링
function renderWorkStatusTypes() {
const grid = document.getElementById('workStatusGrid');
if (!grid) return;
if (workStatusTypes.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">📊</div>
<h3>등록된 작업 상태 유형이 없습니다.</h3>
<p>"새 상태 추가" 버튼을 눌러 작업 상태를 등록해보세요.</p>
<button class="btn btn-primary" onclick="openCodeModal('work-status')">
첫 상태 추가하기
</button>
</div>
`;
updateWorkStatusStats();
return;
}
let gridHtml = '';
workStatusTypes.forEach(status => {
const isError = status.is_error === 1 || status.is_error === true;
const statusClass = isError ? 'error-status' : 'normal-status';
const statusIcon = isError ? '❌' : '✅';
const statusLabel = isError ? '오류' : '정상';
gridHtml += `
<div class="code-card ${statusClass}" onclick="editCode('work-status', ${status.id})">
<div class="code-header">
<div class="code-icon">${statusIcon}</div>
<div class="code-info">
<h3 class="code-name">${status.name}</h3>
<span class="code-label">${statusLabel}</span>
</div>
<div class="code-actions">
<button class="btn-small btn-edit" onclick="event.stopPropagation(); editCode('work-status', ${status.id})" title="수정">
✏️
</button>
<button class="btn-small btn-delete" onclick="event.stopPropagation(); confirmDeleteCode('work-status', ${status.id})" title="삭제">
🗑️
</button>
</div>
</div>
${status.description ? `<p class="code-description">${status.description}</p>` : ''}
<div class="code-meta">
<span class="code-date">등록: ${formatDate(status.created_at)}</span>
</div>
</div>
`;
});
grid.innerHTML = gridHtml;
updateWorkStatusStats();
}
// 오류 유형 렌더링
function renderErrorTypes() {
const grid = document.getElementById('errorTypesGrid');
if (!grid) return;
if (errorTypes.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">⚠️</div>
<h3>등록된 오류 유형이 없습니다.</h3>
<p>"새 오류 유형 추가" 버튼을 눌러 오류 유형을 등록해보세요.</p>
<button class="btn btn-primary" onclick="openCodeModal('error-types')">
첫 오류 유형 추가하기
</button>
</div>
`;
updateErrorTypesStats();
return;
}
let gridHtml = '';
errorTypes.forEach(error => {
const severityMap = {
'low': { icon: '🟢', label: '낮음', class: 'severity-low' },
'medium': { icon: '🟡', label: '보통', class: 'severity-medium' },
'high': { icon: '🟠', label: '높음', class: 'severity-high' },
'critical': { icon: '🔴', label: '심각', class: 'severity-critical' }
};
const severity = severityMap[error.severity] || severityMap.medium;
gridHtml += `
<div class="code-card error-type-card ${severity.class}" onclick="editCode('error-types', ${error.id})">
<div class="code-header">
<div class="code-icon">⚠️</div>
<div class="code-info">
<h3 class="code-name">${error.name}</h3>
<span class="code-label">${severity.icon} ${severity.label}</span>
</div>
<div class="code-actions">
<button class="btn-small btn-edit" onclick="event.stopPropagation(); editCode('error-types', ${error.id})" title="수정">
✏️
</button>
<button class="btn-small btn-delete" onclick="event.stopPropagation(); confirmDeleteCode('error-types', ${error.id})" title="삭제">
🗑️
</button>
</div>
</div>
${error.description ? `<p class="code-description">${error.description}</p>` : ''}
${error.solution_guide ? `<div class="solution-guide"><strong>해결 가이드:</strong><br>${error.solution_guide}</div>` : ''}
<div class="code-meta">
<span class="code-date">등록: ${formatDate(error.created_at)}</span>
${error.updated_at !== error.created_at ? `<span class="code-date">수정: ${formatDate(error.updated_at)}</span>` : ''}
</div>
</div>
`;
});
grid.innerHTML = gridHtml;
updateErrorTypesStats();
}
// 작업 유형 렌더링
function renderWorkTypes() {
const grid = document.getElementById('workTypesGrid');
if (!grid) return;
if (workTypes.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🔧</div>
<h3>등록된 작업 유형이 없습니다.</h3>
<p>"새 작업 유형 추가" 버튼을 눌러 작업 유형을 등록해보세요.</p>
<button class="btn btn-primary" onclick="openCodeModal('work-types')">
첫 작업 유형 추가하기
</button>
</div>
`;
updateWorkTypesStats();
return;
}
let gridHtml = '';
workTypes.forEach(type => {
gridHtml += `
<div class="code-card work-type-card" onclick="editCode('work-types', ${type.id})">
<div class="code-header">
<div class="code-icon">🔧</div>
<div class="code-info">
<h3 class="code-name">${type.name}</h3>
${type.category ? `<span class="code-label">📁 ${type.category}</span>` : ''}
</div>
<div class="code-actions">
<button class="btn-small btn-edit" onclick="event.stopPropagation(); editCode('work-types', ${type.id})" title="수정">
✏️
</button>
<button class="btn-small btn-delete" onclick="event.stopPropagation(); confirmDeleteCode('work-types', ${type.id})" title="삭제">
🗑️
</button>
</div>
</div>
${type.description ? `<p class="code-description">${type.description}</p>` : ''}
<div class="code-meta">
<span class="code-date">등록: ${formatDate(type.created_at)}</span>
${type.updated_at !== type.created_at ? `<span class="code-date">수정: ${formatDate(type.updated_at)}</span>` : ''}
</div>
</div>
`;
});
grid.innerHTML = gridHtml;
updateWorkTypesStats();
}
// 작업 상태 통계 업데이트
function updateWorkStatusStats() {
const total = workStatusTypes.length;
const normal = workStatusTypes.filter(s => !s.is_error).length;
const error = workStatusTypes.filter(s => s.is_error).length;
document.getElementById('workStatusCount').textContent = total;
document.getElementById('normalStatusCount').textContent = normal;
document.getElementById('errorStatusCount').textContent = error;
}
// 오류 유형 통계 업데이트
function updateErrorTypesStats() {
const total = errorTypes.length;
const critical = errorTypes.filter(e => e.severity === 'critical').length;
const high = errorTypes.filter(e => e.severity === 'high').length;
const medium = errorTypes.filter(e => e.severity === 'medium').length;
const low = errorTypes.filter(e => e.severity === 'low').length;
document.getElementById('errorTypesCount').textContent = total;
document.getElementById('criticalErrorsCount').textContent = critical;
document.getElementById('highErrorsCount').textContent = high;
document.getElementById('mediumErrorsCount').textContent = medium;
document.getElementById('lowErrorsCount').textContent = low;
}
// 작업 유형 통계 업데이트
function updateWorkTypesStats() {
const total = workTypes.length;
const categories = new Set(workTypes.map(t => t.category).filter(Boolean)).size;
document.getElementById('workTypesCount').textContent = total;
document.getElementById('workCategoriesCount').textContent = categories;
}
// 코드 모달 열기
function openCodeModal(codeType, codeData = null) {
const modal = document.getElementById('codeModal');
const modalTitle = document.getElementById('modalTitle');
const deleteBtn = document.getElementById('deleteCodeBtn');
if (!modal) return;
currentEditingCode = codeData;
// 모든 전용 필드 숨기기
document.getElementById('isErrorGroup').style.display = 'none';
document.getElementById('severityGroup').style.display = 'none';
document.getElementById('solutionGuideGroup').style.display = 'none';
document.getElementById('categoryGroup').style.display = 'none';
// 코드 유형별 설정
switch (codeType) {
case 'work-status':
modalTitle.textContent = codeData ? '작업 상태 수정' : '새 작업 상태 추가';
document.getElementById('isErrorGroup').style.display = 'block';
break;
case 'error-types':
modalTitle.textContent = codeData ? '오류 유형 수정' : '새 오류 유형 추가';
document.getElementById('severityGroup').style.display = 'block';
document.getElementById('solutionGuideGroup').style.display = 'block';
break;
case 'work-types':
modalTitle.textContent = codeData ? '작업 유형 수정' : '새 작업 유형 추가';
document.getElementById('categoryGroup').style.display = 'block';
updateCategoryList();
break;
}
document.getElementById('codeType').value = codeType;
if (codeData) {
// 수정 모드
deleteBtn.style.display = 'inline-flex';
// 폼에 데이터 채우기
document.getElementById('codeId').value = codeData.id;
document.getElementById('codeName').value = codeData.name || '';
document.getElementById('codeDescription').value = codeData.description || '';
// 코드 유형별 필드 채우기
if (codeType === 'work-status') {
document.getElementById('isError').checked = codeData.is_error === 1 || codeData.is_error === true;
} else if (codeType === 'error-types') {
document.getElementById('severity').value = codeData.severity || 'medium';
document.getElementById('solutionGuide').value = codeData.solution_guide || '';
} else if (codeType === 'work-types') {
document.getElementById('category').value = codeData.category || '';
}
} else {
// 신규 등록 모드
deleteBtn.style.display = 'none';
// 폼 초기화
document.getElementById('codeForm').reset();
document.getElementById('codeId').value = '';
document.getElementById('codeType').value = codeType;
}
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
// 첫 번째 입력 필드에 포커스
setTimeout(() => {
document.getElementById('codeName').focus();
}, 100);
}
// 카테고리 목록 업데이트
function updateCategoryList() {
const categoryList = document.getElementById('categoryList');
if (categoryList) {
const categories = [...new Set(workTypes.map(t => t.category).filter(Boolean))].sort();
categoryList.innerHTML = categories.map(cat => `<option value="${cat}">`).join('');
}
}
// 코드 모달 닫기
function closeCodeModal() {
const modal = document.getElementById('codeModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
currentEditingCode = null;
}
}
// 코드 편집
function editCode(codeType, codeId) {
let codeData = null;
switch (codeType) {
case 'work-status':
codeData = workStatusTypes.find(s => s.id === codeId);
break;
case 'error-types':
codeData = errorTypes.find(e => e.id === codeId);
break;
case 'work-types':
codeData = workTypes.find(t => t.id === codeId);
break;
}
if (codeData) {
openCodeModal(codeType, codeData);
} else {
showToast('코드를 찾을 수 없습니다.', 'error');
}
}
// 코드 저장
async function saveCode() {
try {
const codeType = document.getElementById('codeType').value;
const codeId = document.getElementById('codeId').value;
const codeData = {
name: document.getElementById('codeName').value.trim(),
description: document.getElementById('codeDescription').value.trim() || null
};
// 필수 필드 검증
if (!codeData.name) {
showToast('이름은 필수 입력 항목입니다.', 'error');
return;
}
// 코드 유형별 추가 필드
if (codeType === 'work-status') {
codeData.is_error = document.getElementById('isError').checked ? 1 : 0;
} else if (codeType === 'error-types') {
codeData.severity = document.getElementById('severity').value;
codeData.solution_guide = document.getElementById('solutionGuide').value.trim() || null;
} else if (codeType === 'work-types') {
codeData.category = document.getElementById('category').value.trim() || null;
}
console.log('💾 저장할 코드 데이터:', codeData);
let endpoint = '';
switch (codeType) {
case 'work-status':
endpoint = '/daily-work-reports/work-status-types';
break;
case 'error-types':
endpoint = '/daily-work-reports/error-types';
break;
case 'work-types':
endpoint = '/daily-work-reports/work-types';
break;
}
let response;
if (codeId) {
// 수정
response = await apiCall(`${endpoint}/${codeId}`, 'PUT', codeData);
} else {
// 신규 등록
response = await apiCall(endpoint, 'POST', codeData);
}
if (response && (response.success || response.id)) {
const action = codeId ? '수정' : '등록';
showToast(`코드가 성공적으로 ${action}되었습니다.`, 'success');
closeCodeModal();
await loadAllCodes();
} else {
throw new Error(response?.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('코드 저장 오류:', error);
showToast(error.message || '코드 저장 중 오류가 발생했습니다.', 'error');
}
}
// 코드 삭제 확인
function confirmDeleteCode(codeType, codeId) {
let codeData = null;
let typeName = '';
switch (codeType) {
case 'work-status':
codeData = workStatusTypes.find(s => s.id === codeId);
typeName = '작업 상태';
break;
case 'error-types':
codeData = errorTypes.find(e => e.id === codeId);
typeName = '오류 유형';
break;
case 'work-types':
codeData = workTypes.find(t => t.id === codeId);
typeName = '작업 유형';
break;
}
if (!codeData) {
showToast('코드를 찾을 수 없습니다.', 'error');
return;
}
if (confirm(`"${codeData.name}" ${typeName}을 정말 삭제하시겠습니까?\n\n⚠️ 삭제된 코드는 복구할 수 없습니다.`)) {
deleteCodeById(codeType, codeId);
}
}
// 코드 삭제 (수정 모드에서)
function deleteCode() {
if (currentEditingCode) {
const codeType = document.getElementById('codeType').value;
confirmDeleteCode(codeType, currentEditingCode.id);
}
}
// 코드 삭제 실행
async function deleteCodeById(codeType, codeId) {
try {
let endpoint = '';
switch (codeType) {
case 'work-status':
endpoint = '/daily-work-reports/work-status-types';
break;
case 'error-types':
endpoint = '/daily-work-reports/error-types';
break;
case 'work-types':
endpoint = '/daily-work-reports/work-types';
break;
}
const response = await apiCall(`${endpoint}/${codeId}`, 'DELETE');
if (response && response.success) {
showToast('코드가 성공적으로 삭제되었습니다.', 'success');
closeCodeModal();
await loadAllCodes();
} else {
throw new Error(response?.message || '삭제에 실패했습니다.');
}
} catch (error) {
console.error('코드 삭제 오류:', error);
showToast(error.message || '코드 삭제 중 오류가 발생했습니다.', 'error');
}
}
// 전체 새로고침
async function refreshAllCodes() {
const refreshBtn = document.querySelector('.btn-secondary');
if (refreshBtn) {
const originalText = refreshBtn.innerHTML;
refreshBtn.innerHTML = '<span class="btn-icon">⏳</span>새로고침 중...';
refreshBtn.disabled = true;
await loadAllCodes();
refreshBtn.innerHTML = originalText;
refreshBtn.disabled = false;
} else {
await loadAllCodes();
}
showToast('모든 코드 데이터가 새로고침되었습니다.', 'success');
}
// 날짜 포맷팅
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
// 토스트 메시지 표시
function showToast(message, type = 'info') {
// 기존 토스트 제거
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
// 새 토스트 생성
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
// 스타일 적용
Object.assign(toast.style, {
position: 'fixed',
top: '20px',
right: '20px',
padding: '12px 24px',
borderRadius: '8px',
color: 'white',
fontWeight: '500',
zIndex: '1000',
transform: 'translateX(100%)',
transition: 'transform 0.3s ease'
});
// 타입별 배경색
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6'
};
toast.style.backgroundColor = colors[type] || colors.info;
document.body.appendChild(toast);
// 애니메이션
setTimeout(() => {
toast.style.transform = 'translateX(0)';
}, 100);
// 자동 제거
setTimeout(() => {
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 300);
}, 3000);
}
// 전역 함수로 노출
window.switchCodeTab = switchCodeTab;
window.openCodeModal = openCodeModal;
window.closeCodeModal = closeCodeModal;
window.editCode = editCode;
window.saveCode = saveCode;
window.deleteCode = deleteCode;
window.confirmDeleteCode = confirmDeleteCode;
window.refreshAllCodes = refreshAllCodes;