feat: 작업 분석 시스템 및 관리 기능 대폭 개선
✨ 새로운 기능: - 작업 분석 페이지 구현 (기간별, 프로젝트별, 작업자별, 오류별) - 개별 분석 실행 버튼으로 API 부하 최적화 - 연차/휴무 집계 방식 개선 (주말 제외, 작업내용 통합) - 프로젝트 관리 시스템 (활성화/비활성화) - 작업자 관리 시스템 (CRUD 기능) - 코드 관리 시스템 (작업유형, 작업상태, 오류유형) 🎨 UI/UX 개선: - 기간별 작업 현황을 테이블 형태로 변경 - 작업자별 rowspan 그룹화로 가독성 향상 - 연차/휴무 프로젝트 하단 배치 및 시각적 구분 - 기간 확정 시스템으로 사용자 경험 개선 - 반응형 디자인 적용 🔧 기술적 개선: - Rate Limiting 제거 (내부 시스템 최적화) - 주말 연차/휴무 자동 제외 로직 - 작업공수 계산 정확도 향상 - 데이터베이스 마이그레이션 추가 - API 엔드포인트 확장 및 최적화 🐛 버그 수정: - projectSelect 요소 참조 오류 해결 - 차트 높이 무한 증가 문제 해결 - 날짜 표시 형식 단순화 - 작업보고서 저장 validation 오류 수정
This commit is contained in:
795
web-ui/js/code-management.js
Normal file
795
web-ui/js/code-management.js
Normal file
@@ -0,0 +1,795 @@
|
||||
// 코드 관리 페이지 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 사용자 정보 업데이트
|
||||
function updateUserInfo() {
|
||||
let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
let authUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
|
||||
const finalUserInfo = {
|
||||
worker_name: userInfo.worker_name || authUser.username || authUser.worker_name,
|
||||
job_type: userInfo.job_type || authUser.role || authUser.job_type,
|
||||
username: authUser.username || userInfo.username
|
||||
};
|
||||
|
||||
const userNameElement = document.getElementById('userName');
|
||||
const userRoleElement = document.getElementById('userRole');
|
||||
const userInitialElement = document.getElementById('userInitial');
|
||||
|
||||
if (userNameElement) {
|
||||
userNameElement.textContent = finalUserInfo.worker_name || '사용자';
|
||||
}
|
||||
|
||||
if (userRoleElement) {
|
||||
const roleMap = {
|
||||
'leader': '그룹장',
|
||||
'worker': '작업자',
|
||||
'admin': '관리자',
|
||||
'system': '시스템 관리자'
|
||||
};
|
||||
userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type || '작업자';
|
||||
}
|
||||
|
||||
if (userInitialElement) {
|
||||
const name = finalUserInfo.worker_name || '사용자';
|
||||
userInitialElement.textContent = name.charAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로필 메뉴 설정
|
||||
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;
|
||||
Reference in New Issue
Block a user