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;
|
||||
@@ -71,6 +71,75 @@ function hideMessage() {
|
||||
document.getElementById('message-container').innerHTML = '';
|
||||
}
|
||||
|
||||
// 저장 결과 모달 표시
|
||||
function showSaveResultModal(type, title, message, details = null) {
|
||||
const modal = document.getElementById('saveResultModal');
|
||||
const titleElement = document.getElementById('resultModalTitle');
|
||||
const contentElement = document.getElementById('resultModalContent');
|
||||
|
||||
// 아이콘 설정
|
||||
let icon = '';
|
||||
switch (type) {
|
||||
case 'success':
|
||||
icon = '✅';
|
||||
break;
|
||||
case 'error':
|
||||
icon = '❌';
|
||||
break;
|
||||
case 'warning':
|
||||
icon = '⚠️';
|
||||
break;
|
||||
default:
|
||||
icon = 'ℹ️';
|
||||
}
|
||||
|
||||
// 모달 내용 구성
|
||||
let content = `
|
||||
<div class="result-icon ${type}">${icon}</div>
|
||||
<h3 class="result-title ${type}">${title}</h3>
|
||||
<p class="result-message">${message}</p>
|
||||
`;
|
||||
|
||||
// 상세 정보가 있으면 추가
|
||||
if (details && details.length > 0) {
|
||||
content += `
|
||||
<div class="result-details">
|
||||
<h4>상세 정보:</h4>
|
||||
<ul>
|
||||
${details.map(detail => `<li>${detail}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
titleElement.textContent = '저장 결과';
|
||||
contentElement.innerHTML = content;
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// ESC 키로 닫기
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeSaveResultModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 배경 클릭으로 닫기
|
||||
modal.addEventListener('click', function(e) {
|
||||
if (e.target === modal) {
|
||||
closeSaveResultModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 저장 결과 모달 닫기
|
||||
function closeSaveResultModal() {
|
||||
const modal = document.getElementById('saveResultModal');
|
||||
modal.style.display = 'none';
|
||||
|
||||
// 이벤트 리스너 제거
|
||||
document.removeEventListener('keydown', closeSaveResultModal);
|
||||
}
|
||||
|
||||
// 단계 이동
|
||||
function goToStep(stepNumber) {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
@@ -148,10 +217,10 @@ async function loadWorkers() {
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
console.log('Projects API 호출 중... (통합 API 사용)');
|
||||
const data = await window.apiCall(`${window.API}/projects`);
|
||||
console.log('Projects API 호출 중... (활성 프로젝트만)');
|
||||
const data = await window.apiCall(`${window.API}/projects/active/list`);
|
||||
projects = Array.isArray(data) ? data : (data.data || data.projects || []);
|
||||
console.log('✅ Projects 로드 성공:', projects.length);
|
||||
console.log('✅ 활성 프로젝트 로드 성공:', projects.length);
|
||||
} catch (error) {
|
||||
console.error('프로젝트 로딩 오류:', error);
|
||||
throw error;
|
||||
@@ -251,12 +320,16 @@ function toggleWorkerSelection(workerId, btnElement) {
|
||||
|
||||
// 작업 항목 추가
|
||||
function addWorkEntry() {
|
||||
console.log('🔧 addWorkEntry 함수 호출됨');
|
||||
const container = document.getElementById('workEntriesList');
|
||||
console.log('🔧 컨테이너:', container);
|
||||
workEntryCounter++;
|
||||
console.log('🔧 작업 항목 카운터:', workEntryCounter);
|
||||
|
||||
const entryDiv = document.createElement('div');
|
||||
entryDiv.className = 'work-entry';
|
||||
entryDiv.dataset.id = workEntryCounter;
|
||||
console.log('🔧 생성된 작업 항목 div:', entryDiv);
|
||||
|
||||
entryDiv.innerHTML = `
|
||||
<div class="work-entry-header">
|
||||
@@ -337,7 +410,12 @@ function addWorkEntry() {
|
||||
`;
|
||||
|
||||
container.appendChild(entryDiv);
|
||||
console.log('🔧 작업 항목이 컨테이너에 추가됨');
|
||||
console.log('🔧 현재 컨테이너 내용:', container.innerHTML.length, '문자');
|
||||
console.log('🔧 현재 .work-entry 개수:', container.querySelectorAll('.work-entry').length);
|
||||
|
||||
setupWorkEntryEvents(entryDiv);
|
||||
console.log('🔧 이벤트 설정 완료');
|
||||
}
|
||||
|
||||
// 작업 항목 이벤트 설정
|
||||
@@ -432,43 +510,100 @@ async function saveWorkReport() {
|
||||
const reportDate = document.getElementById('reportDate').value;
|
||||
|
||||
if (!reportDate || selectedWorkers.size === 0) {
|
||||
showMessage('날짜와 작업자를 선택해주세요.', 'error');
|
||||
showSaveResultModal(
|
||||
'error',
|
||||
'입력 오류',
|
||||
'날짜와 작업자를 선택해주세요.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = document.querySelectorAll('.work-entry');
|
||||
console.log('🔍 찾은 작업 항목들:', entries);
|
||||
console.log('🔍 작업 항목 개수:', entries.length);
|
||||
|
||||
if (entries.length === 0) {
|
||||
showMessage('최소 하나의 작업을 추가해주세요.', 'error');
|
||||
showSaveResultModal(
|
||||
'error',
|
||||
'작업 항목 없음',
|
||||
'최소 하나의 작업을 추가해주세요.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const newWorkEntries = [];
|
||||
console.log('🔍 작업 항목 수집 시작...');
|
||||
|
||||
for (const entry of entries) {
|
||||
const projectId = entry.querySelector('.project-select').value;
|
||||
const workTypeId = entry.querySelector('.work-type-select').value;
|
||||
const workStatusId = entry.querySelector('.work-status-select').value;
|
||||
const errorTypeId = entry.querySelector('.error-type-select').value;
|
||||
const workHours = entry.querySelector('.time-input').value;
|
||||
console.log('🔍 작업 항목 처리 중:', entry);
|
||||
|
||||
const projectSelect = entry.querySelector('.project-select');
|
||||
const workTypeSelect = entry.querySelector('.work-type-select');
|
||||
const workStatusSelect = entry.querySelector('.work-status-select');
|
||||
const errorTypeSelect = entry.querySelector('.error-type-select');
|
||||
const timeInput = entry.querySelector('.time-input');
|
||||
|
||||
console.log('🔍 선택된 요소들:', {
|
||||
projectSelect,
|
||||
workTypeSelect,
|
||||
workStatusSelect,
|
||||
errorTypeSelect,
|
||||
timeInput
|
||||
});
|
||||
|
||||
const projectId = projectSelect?.value;
|
||||
const workTypeId = workTypeSelect?.value;
|
||||
const workStatusId = workStatusSelect?.value;
|
||||
const errorTypeId = errorTypeSelect?.value;
|
||||
const workHours = timeInput?.value;
|
||||
|
||||
console.log('🔍 수집된 값들:', {
|
||||
projectId,
|
||||
workTypeId,
|
||||
workStatusId,
|
||||
errorTypeId,
|
||||
workHours
|
||||
});
|
||||
|
||||
if (!projectId || !workTypeId || !workStatusId || !workHours) {
|
||||
showMessage('모든 작업 항목을 완성해주세요.', 'error');
|
||||
showSaveResultModal(
|
||||
'error',
|
||||
'입력 오류',
|
||||
'모든 작업 항목을 완성해주세요.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (workStatusId === '2' && !errorTypeId) {
|
||||
showMessage('에러 상태인 경우 에러 유형을 선택해주세요.', 'error');
|
||||
showSaveResultModal(
|
||||
'error',
|
||||
'입력 오류',
|
||||
'에러 상태인 경우 에러 유형을 선택해주세요.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
newWorkEntries.push({
|
||||
const workEntry = {
|
||||
project_id: parseInt(projectId),
|
||||
work_type_id: parseInt(workTypeId),
|
||||
work_status_id: parseInt(workStatusId),
|
||||
error_type_id: errorTypeId ? parseInt(errorTypeId) : null,
|
||||
work_hours: parseFloat(workHours)
|
||||
});
|
||||
};
|
||||
|
||||
console.log('🔍 생성된 작업 항목:', workEntry);
|
||||
console.log('🔍 작업 항목 상세:', {
|
||||
project_id: workEntry.project_id,
|
||||
work_type_id: workEntry.work_type_id,
|
||||
work_status_id: workEntry.work_status_id,
|
||||
error_type_id: workEntry.error_type_id,
|
||||
work_hours: workEntry.work_hours
|
||||
});
|
||||
newWorkEntries.push(workEntry);
|
||||
}
|
||||
|
||||
console.log('🔍 최종 수집된 작업 항목들:', newWorkEntries);
|
||||
console.log('🔍 총 작업 항목 개수:', newWorkEntries.length);
|
||||
|
||||
try {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
@@ -481,38 +616,61 @@ async function saveWorkReport() {
|
||||
const failureDetails = [];
|
||||
|
||||
for (const workerId of selectedWorkers) {
|
||||
const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음';
|
||||
|
||||
// 서버가 기대하는 work_entries 배열 형태로 전송
|
||||
const requestData = {
|
||||
report_date: reportDate,
|
||||
worker_id: parseInt(workerId),
|
||||
work_entries: newWorkEntries,
|
||||
work_entries: newWorkEntries.map(entry => ({
|
||||
project_id: entry.project_id,
|
||||
task_id: entry.work_type_id, // 서버에서 task_id로 기대
|
||||
work_hours: entry.work_hours,
|
||||
work_status_id: entry.work_status_id,
|
||||
error_type_id: entry.error_type_id
|
||||
})),
|
||||
created_by: currentUser?.user_id || currentUser?.id
|
||||
};
|
||||
|
||||
console.log('전송 데이터 (통합 API 사용):', requestData);
|
||||
console.log('🔄 배열 형태로 전송:', requestData);
|
||||
console.log('🔄 work_entries:', requestData.work_entries);
|
||||
console.log('🔄 work_entries[0] 상세:', requestData.work_entries[0]);
|
||||
console.log('🔄 전송 데이터 JSON:', JSON.stringify(requestData, null, 2));
|
||||
|
||||
try {
|
||||
const result = await window.apiCall(`${window.API}/daily-work-reports`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
const result = await window.apiCall(`${window.API}/daily-work-reports`, 'POST', requestData);
|
||||
|
||||
console.log('✅ 저장 성공 (통합 API):', result);
|
||||
console.log('✅ 저장 성공:', result);
|
||||
totalSaved++;
|
||||
} catch (error) {
|
||||
console.error('❌ 저장 실패:', error);
|
||||
totalFailed++;
|
||||
|
||||
const workerName = workers.find(w => w.worker_id == workerId)?.worker_name || '알 수 없음';
|
||||
failureDetails.push(`${workerName}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 결과 모달 표시
|
||||
if (totalSaved > 0 && totalFailed === 0) {
|
||||
showMessage(`✅ ${totalSaved}명의 작업보고서가 성공적으로 저장되었습니다!`, 'success');
|
||||
showSaveResultModal(
|
||||
'success',
|
||||
'저장 완료!',
|
||||
`${totalSaved}명의 작업보고서가 성공적으로 저장되었습니다.`
|
||||
);
|
||||
} else if (totalSaved > 0 && totalFailed > 0) {
|
||||
showMessage(`⚠️ ${totalSaved}명 성공, ${totalFailed}명 실패. 실패: ${failureDetails.join(', ')}`, 'warning');
|
||||
showSaveResultModal(
|
||||
'warning',
|
||||
'부분 저장 완료',
|
||||
`${totalSaved}명은 성공했지만 ${totalFailed}명은 실패했습니다.`,
|
||||
failureDetails
|
||||
);
|
||||
} else {
|
||||
showMessage(`❌ 모든 저장이 실패했습니다. 상세: ${failureDetails.join(', ')}`, 'error');
|
||||
showSaveResultModal(
|
||||
'error',
|
||||
'저장 실패',
|
||||
'모든 작업보고서 저장이 실패했습니다.',
|
||||
failureDetails
|
||||
);
|
||||
}
|
||||
|
||||
if (totalSaved > 0) {
|
||||
@@ -524,7 +682,12 @@ async function saveWorkReport() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('저장 오류:', error);
|
||||
showMessage('저장 중 예기치 못한 오류가 발생했습니다: ' + error.message, 'error');
|
||||
showSaveResultModal(
|
||||
'error',
|
||||
'저장 오류',
|
||||
'저장 중 예기치 못한 오류가 발생했습니다.',
|
||||
[error.message]
|
||||
);
|
||||
} finally {
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = false;
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
// /js/manage-task.js
|
||||
|
||||
import { API, getAuthHeaders, ensureAuthenticated } from '/js/api-config.js';
|
||||
|
||||
// 인증 확인
|
||||
ensureAuthenticated();
|
||||
|
||||
function createRow(item, cols, delHandler) {
|
||||
const tr = document.createElement('tr');
|
||||
cols.forEach(key => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = item[key];
|
||||
tr.appendChild(td);
|
||||
});
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.textContent = '삭제';
|
||||
delBtn.className = 'btn-delete';
|
||||
delBtn.onclick = () => delHandler(item);
|
||||
const td = document.createElement('td');
|
||||
td.appendChild(delBtn);
|
||||
tr.appendChild(td);
|
||||
return tr;
|
||||
}
|
||||
|
||||
const taskForm = document.getElementById('taskForm');
|
||||
taskForm?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
category: document.getElementById('category').value.trim(),
|
||||
subcategory: document.getElementById('subcategory').value.trim(),
|
||||
task_name: document.getElementById('task_name').value.trim(),
|
||||
description: document.getElementById('description').value.trim()
|
||||
};
|
||||
|
||||
if (!body.category || !body.task_name) {
|
||||
return alert('필수 항목을 입력하세요');
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/tasks`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(),
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (res.ok && result.success) {
|
||||
alert('✅ 등록 완료');
|
||||
taskForm.reset();
|
||||
loadTasks();
|
||||
} else {
|
||||
alert('❌ 실패: ' + (result.error || '알 수 없는 오류'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('🚨 서버 오류: ' + err.message);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadTasks() {
|
||||
const tbody = document.getElementById('taskTableBody');
|
||||
tbody.innerHTML = '<tr><td colspan="6">불러오는 중...</td></tr>';
|
||||
try {
|
||||
const res = await fetch(`${API}/tasks`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
|
||||
const list = await res.json();
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (Array.isArray(list)) {
|
||||
list.forEach(item => {
|
||||
const row = createRow(item, [
|
||||
'task_id', 'category', 'subcategory', 'task_name', 'description'
|
||||
], async t => {
|
||||
if (!confirm('삭제하시겠습니까?')) return;
|
||||
try {
|
||||
const delRes = await fetch(`${API}/tasks/${t.task_id}`, {
|
||||
method: 'DELETE',
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (delRes.ok) {
|
||||
alert('✅ 삭제 완료');
|
||||
loadTasks();
|
||||
} else {
|
||||
alert('❌ 삭제 실패');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('🚨 삭제 중 오류: ' + err.message);
|
||||
}
|
||||
});
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
} else {
|
||||
tbody.innerHTML = '<tr><td colspan="6">데이터 형식 오류</td></tr>';
|
||||
}
|
||||
} catch (err) {
|
||||
tbody.innerHTML = '<tr><td colspan="6">로드 실패: ' + err.message + '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', loadTasks);
|
||||
@@ -1009,7 +1009,7 @@ async function loadModalExistingWork() {
|
||||
async function loadModalDropdownData() {
|
||||
try {
|
||||
const [projectsRes, workTypesRes, workStatusRes, errorTypesRes] = await Promise.all([
|
||||
window.apiCall(`${window.API}/projects`),
|
||||
window.apiCall(`${window.API}/projects/active/list`),
|
||||
window.apiCall(`${window.API}/daily-work-reports/work-types`),
|
||||
window.apiCall(`${window.API}/daily-work-reports/work-status-types`),
|
||||
window.apiCall(`${window.API}/daily-work-reports/error-types`)
|
||||
|
||||
634
web-ui/js/project-management.js
Normal file
634
web-ui/js/project-management.js
Normal file
@@ -0,0 +1,634 @@
|
||||
// 프로젝트 관리 페이지 JavaScript
|
||||
|
||||
// 전역 변수
|
||||
let allProjects = [];
|
||||
let filteredProjects = [];
|
||||
let currentEditingProject = null;
|
||||
let currentStatusFilter = 'all'; // 'all', 'active', 'inactive'
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('📁 프로젝트 관리 페이지 초기화 시작');
|
||||
|
||||
initializePage();
|
||||
loadProjects();
|
||||
});
|
||||
|
||||
// 페이지 초기화
|
||||
function initializePage() {
|
||||
// 시간 업데이트 시작
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
|
||||
// 사용자 정보 업데이트
|
||||
updateUserInfo();
|
||||
|
||||
// 프로필 메뉴 토글
|
||||
setupProfileMenu();
|
||||
|
||||
// 로그아웃 버튼
|
||||
setupLogoutButton();
|
||||
|
||||
// 검색 입력 이벤트
|
||||
setupSearchInput();
|
||||
}
|
||||
|
||||
// 현재 시간 업데이트
|
||||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 검색 입력 설정
|
||||
function setupSearchInput() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
searchProjects();
|
||||
});
|
||||
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchProjects();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 목록 로드
|
||||
async function loadProjects() {
|
||||
try {
|
||||
console.log('📊 프로젝트 목록 로딩 시작');
|
||||
|
||||
const response = await apiCall('/projects', 'GET');
|
||||
|
||||
console.log('📊 API 응답 구조:', response);
|
||||
|
||||
// API 응답이 { success: true, data: [...] } 형태인 경우 처리
|
||||
let projectData = [];
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
projectData = response.data;
|
||||
} else if (Array.isArray(response)) {
|
||||
projectData = response;
|
||||
} else {
|
||||
console.warn('프로젝트 데이터가 배열이 아닙니다:', response);
|
||||
projectData = [];
|
||||
}
|
||||
|
||||
allProjects = projectData;
|
||||
|
||||
console.log(`✅ 프로젝트 ${allProjects.length}개 로드 완료`);
|
||||
|
||||
// 초기 필터 적용
|
||||
applyAllFilters();
|
||||
updateStatCardActiveState();
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트 로딩 오류:', error);
|
||||
showToast('프로젝트 목록을 불러오는데 실패했습니다.', 'error');
|
||||
allProjects = [];
|
||||
filteredProjects = [];
|
||||
renderProjects();
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 목록 렌더링
|
||||
function renderProjects() {
|
||||
const projectsGrid = document.getElementById('projectsGrid');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (!projectsGrid || !emptyState) return;
|
||||
|
||||
if (filteredProjects.length === 0) {
|
||||
projectsGrid.style.display = 'none';
|
||||
emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
projectsGrid.style.display = 'grid';
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
const projectsHtml = filteredProjects.map(project => {
|
||||
// 프로젝트 상태 아이콘 및 텍스트
|
||||
const statusMap = {
|
||||
'planning': { icon: '📋', text: '계획', color: '#6b7280' },
|
||||
'active': { icon: '🚀', text: '진행중', color: '#10b981' },
|
||||
'completed': { icon: '✅', text: '완료', color: '#3b82f6' },
|
||||
'cancelled': { icon: '❌', text: '취소', color: '#ef4444' }
|
||||
};
|
||||
|
||||
const status = statusMap[project.project_status] || statusMap['active'];
|
||||
// is_active 값 처리 (DB에서 0/1로 오는 경우 대비)
|
||||
const isInactive = project.is_active === 0 || project.is_active === false || project.is_active === 'false';
|
||||
|
||||
console.log('🎨 카드 렌더링:', {
|
||||
project_id: project.project_id,
|
||||
project_name: project.project_name,
|
||||
is_active_raw: project.is_active,
|
||||
isInactive: isInactive
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="project-card ${isInactive ? 'inactive' : ''}" onclick="editProject(${project.project_id})">
|
||||
${isInactive ? '<div class="inactive-overlay"><span class="inactive-badge">🚫 비활성화됨</span></div>' : ''}
|
||||
<div class="project-header">
|
||||
<div class="project-info">
|
||||
<div class="project-job-no">${project.job_no || 'Job No. 없음'}</div>
|
||||
<h3 class="project-name">
|
||||
${project.project_name}
|
||||
${isInactive ? '<span class="inactive-label">(비활성)</span>' : ''}
|
||||
</h3>
|
||||
<div class="project-meta">
|
||||
<span style="color: ${status.color}; font-weight: 500;">${status.icon} ${status.text}</span>
|
||||
${project.contract_date ? `<span>📅 계약일: ${formatDate(project.contract_date)}</span>` : ''}
|
||||
${project.due_date ? `<span>⏰ 납기일: ${formatDate(project.due_date)}</span>` : ''}
|
||||
${project.completed_date ? `<span>🎯 완료일: ${formatDate(project.completed_date)}</span>` : ''}
|
||||
${project.pm ? `<span>👤 PM: ${project.pm}</span>` : ''}
|
||||
${project.site ? `<span>📍 현장: ${project.site}</span>` : ''}
|
||||
${isInactive ? '<span class="inactive-notice">⚠️ 작업보고서에서 숨김</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-actions">
|
||||
<button class="btn-edit" onclick="event.stopPropagation(); editProject(${project.project_id})" title="수정">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-delete" onclick="event.stopPropagation(); confirmDeleteProject(${project.project_id})" title="삭제">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
projectsGrid.innerHTML = projectsHtml;
|
||||
}
|
||||
|
||||
// 프로젝트 통계 업데이트
|
||||
function updateProjectStats() {
|
||||
const activeProjects = filteredProjects.filter(p => p.is_active === 1 || p.is_active === true);
|
||||
const inactiveProjects = filteredProjects.filter(p => p.is_active === 0 || p.is_active === false);
|
||||
|
||||
const activeProjectsElement = document.getElementById('activeProjects');
|
||||
const inactiveProjectsElement = document.getElementById('inactiveProjects');
|
||||
const totalProjectsElement = document.getElementById('totalProjects');
|
||||
|
||||
if (activeProjectsElement) {
|
||||
activeProjectsElement.textContent = activeProjects.length;
|
||||
}
|
||||
|
||||
if (inactiveProjectsElement) {
|
||||
inactiveProjectsElement.textContent = inactiveProjects.length;
|
||||
}
|
||||
|
||||
if (totalProjectsElement) {
|
||||
totalProjectsElement.textContent = filteredProjects.length;
|
||||
}
|
||||
|
||||
console.log('📊 프로젝트 통계:', {
|
||||
전체: filteredProjects.length,
|
||||
활성: activeProjects.length,
|
||||
비활성: inactiveProjects.length
|
||||
});
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
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 filterByStatus(status) {
|
||||
currentStatusFilter = status;
|
||||
|
||||
// 통계 카드 활성화 상태 업데이트
|
||||
updateStatCardActiveState();
|
||||
|
||||
// 필터링 적용
|
||||
applyAllFilters();
|
||||
|
||||
console.log(`🔍 상태 필터 적용: ${status}`);
|
||||
}
|
||||
|
||||
// 통계 카드 활성화 상태 업데이트
|
||||
function updateStatCardActiveState() {
|
||||
// 모든 통계 카드에서 active 클래스 제거
|
||||
document.querySelectorAll('.stat-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// 현재 선택된 필터에 active 클래스 추가
|
||||
const activeCard = document.querySelector(`.${currentStatusFilter === 'active' ? 'active-stat' :
|
||||
currentStatusFilter === 'inactive' ? 'inactive-stat' : 'total-stat'}`);
|
||||
if (activeCard) {
|
||||
activeCard.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 필터 적용 (검색 + 상태)
|
||||
function applyAllFilters() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
||||
|
||||
// 1단계: 상태 필터링
|
||||
let statusFiltered = [...allProjects];
|
||||
if (currentStatusFilter === 'active') {
|
||||
statusFiltered = allProjects.filter(p => p.is_active === 1 || p.is_active === true);
|
||||
} else if (currentStatusFilter === 'inactive') {
|
||||
statusFiltered = allProjects.filter(p => p.is_active === 0 || p.is_active === false);
|
||||
}
|
||||
|
||||
// 2단계: 검색 필터링
|
||||
if (!searchTerm) {
|
||||
filteredProjects = statusFiltered;
|
||||
} else {
|
||||
filteredProjects = statusFiltered.filter(project =>
|
||||
project.project_name.toLowerCase().includes(searchTerm) ||
|
||||
(project.job_no && project.job_no.toLowerCase().includes(searchTerm)) ||
|
||||
(project.pm && project.pm.toLowerCase().includes(searchTerm)) ||
|
||||
(project.site && project.site.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
}
|
||||
|
||||
renderProjects();
|
||||
updateProjectStats();
|
||||
}
|
||||
|
||||
// 프로젝트 검색 (기존 함수 수정)
|
||||
function searchProjects() {
|
||||
applyAllFilters();
|
||||
}
|
||||
|
||||
// 프로젝트 필터링
|
||||
function filterProjects() {
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
const selectedStatus = statusFilter ? statusFilter.value : '';
|
||||
|
||||
// 현재는 상태 필드가 없으므로 기본 필터링만 적용
|
||||
searchProjects();
|
||||
}
|
||||
|
||||
// 프로젝트 정렬
|
||||
function sortProjects() {
|
||||
const sortBy = document.getElementById('sortBy');
|
||||
const sortField = sortBy ? sortBy.value : 'created_at';
|
||||
|
||||
filteredProjects.sort((a, b) => {
|
||||
switch (sortField) {
|
||||
case 'project_name':
|
||||
return a.project_name.localeCompare(b.project_name);
|
||||
case 'due_date':
|
||||
if (!a.due_date && !b.due_date) return 0;
|
||||
if (!a.due_date) return 1;
|
||||
if (!b.due_date) return -1;
|
||||
return new Date(a.due_date) - new Date(b.due_date);
|
||||
case 'created_at':
|
||||
default:
|
||||
return new Date(b.created_at || 0) - new Date(a.created_at || 0);
|
||||
}
|
||||
});
|
||||
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
// 프로젝트 목록 새로고침
|
||||
async function refreshProjectList() {
|
||||
const refreshBtn = document.querySelector('.btn-secondary');
|
||||
if (refreshBtn) {
|
||||
const originalText = refreshBtn.innerHTML;
|
||||
refreshBtn.innerHTML = '<span class="btn-icon">⏳</span>새로고침 중...';
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
await loadProjects();
|
||||
|
||||
refreshBtn.innerHTML = originalText;
|
||||
refreshBtn.disabled = false;
|
||||
} else {
|
||||
await loadProjects();
|
||||
}
|
||||
|
||||
showToast('프로젝트 목록이 새로고침되었습니다.', 'success');
|
||||
}
|
||||
|
||||
// 프로젝트 모달 열기
|
||||
function openProjectModal(project = null) {
|
||||
const modal = document.getElementById('projectModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const deleteBtn = document.getElementById('deleteProjectBtn');
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
currentEditingProject = project;
|
||||
|
||||
if (project) {
|
||||
// 수정 모드
|
||||
modalTitle.textContent = '프로젝트 수정';
|
||||
deleteBtn.style.display = 'inline-flex';
|
||||
|
||||
// 폼에 데이터 채우기
|
||||
document.getElementById('projectId').value = project.project_id;
|
||||
document.getElementById('jobNo').value = project.job_no || '';
|
||||
document.getElementById('projectName').value = project.project_name || '';
|
||||
document.getElementById('contractDate').value = project.contract_date || '';
|
||||
document.getElementById('dueDate').value = project.due_date || '';
|
||||
document.getElementById('deliveryMethod').value = project.delivery_method || '';
|
||||
document.getElementById('site').value = project.site || '';
|
||||
document.getElementById('pm').value = project.pm || '';
|
||||
document.getElementById('projectStatus').value = project.project_status || 'active';
|
||||
document.getElementById('completedDate').value = project.completed_date || '';
|
||||
// is_active 값 처리 (DB에서 0/1로 오는 경우 대비)
|
||||
const isActiveValue = project.is_active === 1 || project.is_active === true || project.is_active === 'true';
|
||||
document.getElementById('isActive').checked = isActiveValue;
|
||||
|
||||
console.log('🔧 프로젝트 로드:', {
|
||||
project_id: project.project_id,
|
||||
project_name: project.project_name,
|
||||
is_active_raw: project.is_active,
|
||||
is_active_processed: isActiveValue
|
||||
});
|
||||
} else {
|
||||
// 신규 등록 모드
|
||||
modalTitle.textContent = '새 프로젝트 등록';
|
||||
deleteBtn.style.display = 'none';
|
||||
|
||||
// 폼 초기화
|
||||
document.getElementById('projectForm').reset();
|
||||
document.getElementById('projectId').value = '';
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 첫 번째 입력 필드에 포커스
|
||||
setTimeout(() => {
|
||||
const firstInput = document.getElementById('jobNo');
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 프로젝트 모달 닫기
|
||||
function closeProjectModal() {
|
||||
const modal = document.getElementById('projectModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
currentEditingProject = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 편집
|
||||
function editProject(projectId) {
|
||||
const project = allProjects.find(p => p.project_id === projectId);
|
||||
if (project) {
|
||||
openProjectModal(project);
|
||||
} else {
|
||||
showToast('프로젝트를 찾을 수 없습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 저장
|
||||
async function saveProject() {
|
||||
try {
|
||||
const form = document.getElementById('projectForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const projectData = {
|
||||
job_no: document.getElementById('jobNo').value.trim(),
|
||||
project_name: document.getElementById('projectName').value.trim(),
|
||||
contract_date: document.getElementById('contractDate').value || null,
|
||||
due_date: document.getElementById('dueDate').value || null,
|
||||
delivery_method: document.getElementById('deliveryMethod').value || null,
|
||||
site: document.getElementById('site').value.trim() || null,
|
||||
pm: document.getElementById('pm').value.trim() || null,
|
||||
project_status: document.getElementById('projectStatus').value || 'active',
|
||||
completed_date: document.getElementById('completedDate').value || null,
|
||||
is_active: document.getElementById('isActive').checked ? 1 : 0
|
||||
};
|
||||
|
||||
console.log('💾 저장할 프로젝트 데이터:', projectData);
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!projectData.job_no || !projectData.project_name) {
|
||||
showToast('Job No.와 프로젝트명은 필수 입력 항목입니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId = document.getElementById('projectId').value;
|
||||
let response;
|
||||
|
||||
if (projectId) {
|
||||
// 수정
|
||||
response = await apiCall(`/projects/${projectId}`, 'PUT', projectData);
|
||||
} else {
|
||||
// 신규 등록
|
||||
response = await apiCall('/projects', 'POST', projectData);
|
||||
}
|
||||
|
||||
if (response && (response.success || response.project_id)) {
|
||||
const action = projectId ? '수정' : '등록';
|
||||
showToast(`프로젝트가 성공적으로 ${action}되었습니다.`, 'success');
|
||||
|
||||
closeProjectModal();
|
||||
await loadProjects();
|
||||
} else {
|
||||
throw new Error(response?.message || '저장에 실패했습니다.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트 저장 오류:', error);
|
||||
showToast(error.message || '프로젝트 저장 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 삭제 확인
|
||||
function confirmDeleteProject(projectId) {
|
||||
const project = allProjects.find(p => p.project_id === projectId);
|
||||
if (!project) {
|
||||
showToast('프로젝트를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`"${project.project_name}" 프로젝트를 정말 삭제하시겠습니까?\n\n⚠️ 삭제된 프로젝트는 복구할 수 없습니다.`)) {
|
||||
deleteProjectById(projectId);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 삭제 (수정 모드에서)
|
||||
function deleteProject() {
|
||||
if (currentEditingProject) {
|
||||
confirmDeleteProject(currentEditingProject.project_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 삭제 실행
|
||||
async function deleteProjectById(projectId) {
|
||||
try {
|
||||
const response = await apiCall(`/projects/${projectId}`, 'DELETE');
|
||||
|
||||
if (response && response.success) {
|
||||
showToast('프로젝트가 성공적으로 삭제되었습니다.', 'success');
|
||||
|
||||
closeProjectModal();
|
||||
await loadProjects();
|
||||
} else {
|
||||
throw new Error(response?.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트 삭제 오류:', error);
|
||||
showToast(error.message || '프로젝트 삭제 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 토스트 메시지 표시
|
||||
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.openProjectModal = openProjectModal;
|
||||
window.closeProjectModal = closeProjectModal;
|
||||
window.editProject = editProject;
|
||||
window.saveProject = saveProject;
|
||||
window.deleteProject = deleteProject;
|
||||
window.confirmDeleteProject = confirmDeleteProject;
|
||||
window.searchProjects = searchProjects;
|
||||
window.filterProjects = filterProjects;
|
||||
window.sortProjects = sortProjects;
|
||||
window.refreshProjectList = refreshProjectList;
|
||||
window.filterByStatus = filterByStatus;
|
||||
830
web-ui/js/work-analysis.js
Normal file
830
web-ui/js/work-analysis.js
Normal file
@@ -0,0 +1,830 @@
|
||||
// 작업 분석 페이지 JavaScript
|
||||
|
||||
// 전역 변수
|
||||
let currentMode = 'period';
|
||||
let currentTab = 'worker';
|
||||
let analysisData = null;
|
||||
let projectChart = null;
|
||||
let errorByProjectChart = null;
|
||||
let errorTimelineChart = null;
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('📈 작업 분석 페이지 초기화 시작');
|
||||
|
||||
initializePage();
|
||||
loadInitialData();
|
||||
});
|
||||
|
||||
// 페이지 초기화
|
||||
function initializePage() {
|
||||
// 시간 업데이트 시작
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
|
||||
// 사용자 정보 업데이트
|
||||
updateUserInfo();
|
||||
|
||||
// 프로필 메뉴 토글
|
||||
setupProfileMenu();
|
||||
|
||||
// 로그아웃 버튼
|
||||
setupLogoutButton();
|
||||
|
||||
// 기본 날짜 설정은 HTML에서 처리됨 (새로운 UI)
|
||||
console.log('✅ 작업 분석 페이지 초기화 완료');
|
||||
}
|
||||
|
||||
// 현재 시간 업데이트
|
||||
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 loadInitialData() {
|
||||
try {
|
||||
console.log('📊 초기 데이터 로딩 시작');
|
||||
|
||||
// 프로젝트 목록 로드
|
||||
const projects = await apiCall('/projects/active/list', 'GET');
|
||||
const projectData = Array.isArray(projects) ? projects : (projects.data || []);
|
||||
|
||||
// 프로젝트 필터 옵션 업데이트
|
||||
updateProjectFilters(projectData);
|
||||
|
||||
console.log('✅ 초기 데이터 로딩 완료');
|
||||
|
||||
} catch (error) {
|
||||
console.error('초기 데이터 로딩 오류:', error);
|
||||
showToast('초기 데이터를 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트 필터 업데이트
|
||||
function updateProjectFilters(projects) {
|
||||
const projectFilter = document.getElementById('projectFilter');
|
||||
const projectModeSelect = document.getElementById('projectModeSelect');
|
||||
|
||||
if (projectFilter) {
|
||||
projectFilter.innerHTML = '<option value="">전체 프로젝트</option>';
|
||||
projects.forEach(project => {
|
||||
projectFilter.innerHTML += `<option value="${project.project_id}">${project.project_name}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
if (projectModeSelect) {
|
||||
projectModeSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
||||
projects.forEach(project => {
|
||||
projectModeSelect.innerHTML += `<option value="${project.project_id}">${project.project_name}</option>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 분석 모드 전환
|
||||
function switchAnalysisMode(mode) {
|
||||
currentMode = mode;
|
||||
|
||||
// 탭 버튼 활성화 상태 변경
|
||||
document.querySelectorAll('.mode-tab').forEach(tab => {
|
||||
tab.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-mode="${mode}"]`).classList.add('active');
|
||||
|
||||
// 모드 콘텐츠 표시/숨김
|
||||
document.querySelectorAll('.analysis-mode').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${mode}-mode`).classList.add('active');
|
||||
|
||||
console.log(`🔄 분석 모드 전환: ${mode}`);
|
||||
}
|
||||
|
||||
// 분석 탭 전환
|
||||
function switchAnalysisTab(tab) {
|
||||
currentTab = tab;
|
||||
|
||||
// 탭 버튼 활성화 상태 변경
|
||||
document.querySelectorAll('.analysis-tab').forEach(tabBtn => {
|
||||
tabBtn.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-tab="${tab}"]`).classList.add('active');
|
||||
|
||||
// 탭 콘텐츠 표시/숨김
|
||||
document.querySelectorAll('.analysis-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${tab}-analysis`).classList.add('active');
|
||||
|
||||
console.log(`🔄 분석 탭 전환: ${tab}`);
|
||||
}
|
||||
|
||||
// 기간별 분석 로드
|
||||
async function loadPeriodAnalysis() {
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
const projectId = document.getElementById('projectFilter').value;
|
||||
|
||||
if (!startDate || !endDate) {
|
||||
showToast('시작일과 종료일을 모두 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (new Date(startDate) > new Date(endDate)) {
|
||||
showToast('시작일이 종료일보다 늦을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
|
||||
try {
|
||||
console.log('📊 기간별 분석 데이터 로딩 시작');
|
||||
|
||||
// API 호출 파라미터 구성
|
||||
const params = new URLSearchParams({
|
||||
start: startDate,
|
||||
end: endDate
|
||||
});
|
||||
|
||||
if (projectId) {
|
||||
params.append('project_id', projectId);
|
||||
}
|
||||
|
||||
// 여러 API를 병렬로 호출하여 종합 분석 데이터 구성
|
||||
console.log('📡 API 파라미터:', params.toString());
|
||||
|
||||
const [statsRes, workerStatsRes, projectStatsRes, errorAnalysisRes] = await Promise.all([
|
||||
apiCall(`/work-analysis/stats?${params}`, 'GET').catch(err => {
|
||||
console.error('❌ stats API 오류:', err);
|
||||
return { data: null };
|
||||
}),
|
||||
apiCall(`/work-analysis/worker-stats?${params}`, 'GET').catch(err => {
|
||||
console.error('❌ worker-stats API 오류:', err);
|
||||
return { data: [] };
|
||||
}),
|
||||
apiCall(`/work-analysis/project-stats?${params}`, 'GET').catch(err => {
|
||||
console.error('❌ project-stats API 오류:', err);
|
||||
return { data: [] };
|
||||
}),
|
||||
apiCall(`/work-analysis/error-analysis?${params}`, 'GET').catch(err => {
|
||||
console.error('❌ error-analysis API 오류:', err);
|
||||
return { data: {} };
|
||||
})
|
||||
]);
|
||||
|
||||
console.log('📊 개별 API 응답:');
|
||||
console.log(' - stats:', statsRes);
|
||||
console.log(' - worker-stats:', workerStatsRes);
|
||||
console.log(' - project-stats:', projectStatsRes);
|
||||
console.log(' - error-analysis:', errorAnalysisRes);
|
||||
|
||||
// 종합 분석 데이터 구성
|
||||
analysisData = {
|
||||
summary: statsRes.data || statsRes,
|
||||
workerStats: workerStatsRes.data || workerStatsRes,
|
||||
projectStats: projectStatsRes.data || projectStatsRes,
|
||||
errorStats: errorAnalysisRes.data || errorAnalysisRes
|
||||
};
|
||||
|
||||
console.log('📊 분석 데이터:', analysisData);
|
||||
console.log('📊 요약 통계:', analysisData.summary);
|
||||
console.log('👥 작업자 통계:', analysisData.workerStats);
|
||||
console.log('📁 프로젝트 통계:', analysisData.projectStats);
|
||||
console.log('⚠️ 오류 통계:', analysisData.errorStats);
|
||||
|
||||
// 결과 표시
|
||||
displayPeriodAnalysis(analysisData);
|
||||
|
||||
// 결과 섹션 표시
|
||||
document.getElementById('periodResults').style.display = 'block';
|
||||
|
||||
showToast('분석이 완료되었습니다.', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('기간별 분석 오류:', error);
|
||||
showToast('분석 중 오류가 발생했습니다.', 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 기간별 분석 결과 표시
|
||||
function displayPeriodAnalysis(data) {
|
||||
// 요약 통계 업데이트
|
||||
updateSummaryStats(data.summary || {});
|
||||
|
||||
// 작업자별 분석 표시
|
||||
displayWorkerAnalysis(data.workerStats || []);
|
||||
|
||||
// 프로젝트별 분석 표시
|
||||
displayProjectAnalysis(data.projectStats || []);
|
||||
|
||||
// 오류 분석 표시 (전체 분석 데이터도 함께 전달)
|
||||
displayErrorAnalysis(data.errorStats || {}, data);
|
||||
}
|
||||
|
||||
// 요약 통계 업데이트
|
||||
function updateSummaryStats(summary) {
|
||||
// API 응답 구조에 맞게 필드명 조정
|
||||
document.getElementById('totalHours').textContent = `${summary.totalHours || summary.total_hours || 0}h`;
|
||||
document.getElementById('totalWorkers').textContent = `${summary.activeworkers || summary.activeWorkers || summary.total_workers || 0}명`;
|
||||
document.getElementById('totalProjects').textContent = `${summary.activeProjects || summary.active_projects || summary.total_projects || 0}개`;
|
||||
document.getElementById('errorRate').textContent = `${summary.errorRate || summary.error_rate || 0}%`;
|
||||
}
|
||||
|
||||
// 작업자별 분석 표시
|
||||
function displayWorkerAnalysis(workerStats) {
|
||||
const grid = document.getElementById('workerAnalysisGrid');
|
||||
|
||||
console.log('👥 작업자 분석 데이터 확인:', workerStats);
|
||||
console.log('👥 데이터 타입:', typeof workerStats);
|
||||
console.log('👥 배열 여부:', Array.isArray(workerStats));
|
||||
console.log('👥 길이:', workerStats ? workerStats.length : 'undefined');
|
||||
|
||||
if (!workerStats || (Array.isArray(workerStats) && workerStats.length === 0)) {
|
||||
console.log('👥 빈 데이터로 인한 empty-state 표시');
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">👥</div>
|
||||
<h3>분석할 작업자 데이터가 없습니다.</h3>
|
||||
<p>선택한 기간에 등록된 작업이 없습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let gridHtml = '';
|
||||
|
||||
workerStats.forEach(worker => {
|
||||
const workerName = worker.worker_name || worker.name || '알 수 없음';
|
||||
const totalHours = worker.total_hours || worker.totalHours || 0;
|
||||
|
||||
gridHtml += `
|
||||
<div class="worker-card">
|
||||
<div class="worker-header">
|
||||
<div class="worker-info">
|
||||
<div class="worker-avatar">${workerName.charAt(0)}</div>
|
||||
<div class="worker-name">${workerName}</div>
|
||||
</div>
|
||||
<div class="worker-total-hours">${totalHours}h</div>
|
||||
</div>
|
||||
<div class="worker-projects">
|
||||
`;
|
||||
|
||||
// API 응답 구조에 따라 프로젝트 데이터 처리
|
||||
const projects = worker.projects || worker.project_details || [];
|
||||
if (projects.length > 0) {
|
||||
projects.forEach(project => {
|
||||
const projectName = project.project_name || project.name || '프로젝트';
|
||||
gridHtml += `
|
||||
<div class="project-item">
|
||||
<div class="project-name">${projectName}</div>
|
||||
<div class="work-items">
|
||||
`;
|
||||
|
||||
const works = project.works || project.work_details || project.tasks || [];
|
||||
if (works.length > 0) {
|
||||
works.forEach(work => {
|
||||
const workName = work.work_name || work.task_name || work.name || '작업';
|
||||
const workHours = work.hours || work.total_hours || work.work_hours || 0;
|
||||
gridHtml += `
|
||||
<div class="work-item">
|
||||
<div class="work-name">${workName}</div>
|
||||
<div class="work-hours">${workHours}h</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
gridHtml += `
|
||||
<div class="work-item">
|
||||
<div class="work-name">총 작업시간</div>
|
||||
<div class="work-hours">${project.total_hours || project.hours || 0}h</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
gridHtml += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
gridHtml += `
|
||||
<div class="project-item">
|
||||
<div class="project-name">전체 작업</div>
|
||||
<div class="work-items">
|
||||
<div class="work-item">
|
||||
<div class="work-name">총 작업시간</div>
|
||||
<div class="work-hours">${totalHours}h</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
gridHtml += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
grid.innerHTML = gridHtml;
|
||||
}
|
||||
|
||||
// 프로젝트별 분석 표시
|
||||
function displayProjectAnalysis(projectStats) {
|
||||
const detailsContainer = document.getElementById('projectDetails');
|
||||
|
||||
console.log('📁 프로젝트 분석 데이터 확인:', projectStats);
|
||||
console.log('📁 데이터 타입:', typeof projectStats);
|
||||
console.log('📁 배열 여부:', Array.isArray(projectStats));
|
||||
console.log('📁 길이:', projectStats ? projectStats.length : 'undefined');
|
||||
|
||||
if (projectStats && projectStats.length > 0) {
|
||||
console.log('📁 첫 번째 프로젝트 데이터:', projectStats[0]);
|
||||
}
|
||||
|
||||
if (!projectStats || projectStats.length === 0) {
|
||||
console.log('📁 빈 데이터로 인한 empty-state 표시');
|
||||
detailsContainer.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📁</div>
|
||||
<h3>분석할 프로젝트 데이터가 없습니다.</h3>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 프로젝트 상세 정보 표시
|
||||
let detailsHtml = '';
|
||||
|
||||
// 전체 시간 계산 (퍼센트 계산용)
|
||||
const totalAllHours = projectStats.reduce((sum, p) => {
|
||||
return sum + (p.totalHours || p.total_hours || p.hours || 0);
|
||||
}, 0);
|
||||
|
||||
projectStats.forEach(project => {
|
||||
console.log('📁 개별 프로젝트 처리:', project);
|
||||
|
||||
const projectName = project.project_name || project.name || project.projectName || '프로젝트';
|
||||
const totalHours = project.totalHours || project.total_hours || project.hours || 0;
|
||||
|
||||
// 퍼센트 계산
|
||||
let percentage = project.percentage || project.percent || 0;
|
||||
if (percentage === 0 && totalAllHours > 0) {
|
||||
percentage = Math.round((totalHours / totalAllHours) * 100);
|
||||
}
|
||||
|
||||
detailsHtml += `
|
||||
<div class="project-detail-card">
|
||||
<div class="project-detail-header">
|
||||
<div class="project-detail-name">${projectName}</div>
|
||||
<div class="project-percentage">${percentage}%</div>
|
||||
</div>
|
||||
<div class="project-hours">${totalHours}시간</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
detailsContainer.innerHTML = detailsHtml;
|
||||
|
||||
// 차트 업데이트
|
||||
updateProjectChart(projectStats);
|
||||
}
|
||||
|
||||
// 프로젝트 차트 업데이트
|
||||
function updateProjectChart(projectStats) {
|
||||
const ctx = document.getElementById('projectChart');
|
||||
|
||||
if (projectChart) {
|
||||
projectChart.destroy();
|
||||
}
|
||||
|
||||
const labels = projectStats.map(p => p.project_name || p.name || p.projectName || '프로젝트');
|
||||
const data = projectStats.map(p => p.totalHours || p.total_hours || p.hours || 0);
|
||||
|
||||
console.log('📊 차트 라벨:', labels);
|
||||
console.log('📊 차트 데이터:', data);
|
||||
const colors = [
|
||||
'#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6',
|
||||
'#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1'
|
||||
];
|
||||
|
||||
projectChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
backgroundColor: colors.slice(0, data.length),
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
padding: 20,
|
||||
usePointStyle: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 오류 분석 표시
|
||||
function displayErrorAnalysis(errorStats, allData) {
|
||||
console.log('⚠️ 오류 분석 데이터 확인:', errorStats);
|
||||
console.log('⚠️ 데이터 타입:', typeof errorStats);
|
||||
console.log('⚠️ 배열 여부:', Array.isArray(errorStats));
|
||||
|
||||
// errorStats가 배열인 경우 첫 번째 요소 사용
|
||||
let errorData = errorStats;
|
||||
if (Array.isArray(errorStats) && errorStats.length > 0) {
|
||||
errorData = errorStats[0];
|
||||
console.log('⚠️ 배열에서 첫 번째 요소 사용:', errorData);
|
||||
}
|
||||
|
||||
// 오류 요약 업데이트 - 실제 데이터 구조에 맞게 수정
|
||||
const errorHours = errorData.totalHours || errorData.total_hours || errorData.error_hours || 0;
|
||||
|
||||
// 전체 작업 시간에서 오류 시간을 빼서 정규 시간 계산
|
||||
// 요약 통계에서 전체 시간을 가져와서 계산
|
||||
const totalHours = allData && allData.summary ? allData.summary.totalHours : 0;
|
||||
const normalHours = Math.max(0, totalHours - errorHours);
|
||||
|
||||
console.log('⚠️ 정규 시간:', normalHours, '오류 시간:', errorHours);
|
||||
|
||||
document.getElementById('normalHours').textContent = `${normalHours}h`;
|
||||
document.getElementById('errorHours').textContent = `${errorHours}h`;
|
||||
|
||||
// 프로젝트별 에러율 차트
|
||||
if (errorStats.projectErrorRates) {
|
||||
updateErrorByProjectChart(errorStats.projectErrorRates);
|
||||
}
|
||||
|
||||
// 일별 오류 추이 차트
|
||||
if (errorStats.dailyErrorTrend) {
|
||||
updateErrorTimelineChart(errorStats.dailyErrorTrend);
|
||||
}
|
||||
|
||||
// 오류 유형별 분석
|
||||
if (errorStats.errorTypes) {
|
||||
displayErrorTypes(errorStats.errorTypes);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트별 에러율 차트 업데이트
|
||||
function updateErrorByProjectChart(projectErrorRates) {
|
||||
const ctx = document.getElementById('errorByProjectChart');
|
||||
|
||||
if (errorByProjectChart) {
|
||||
errorByProjectChart.destroy();
|
||||
}
|
||||
|
||||
const labels = projectErrorRates.map(p => p.project_name);
|
||||
const data = projectErrorRates.map(p => p.error_rate);
|
||||
|
||||
errorByProjectChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: '에러율 (%)',
|
||||
data: data,
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.8)',
|
||||
borderColor: 'rgba(239, 68, 68, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return value + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 일별 오류 추이 차트 업데이트
|
||||
function updateErrorTimelineChart(dailyErrorTrend) {
|
||||
const ctx = document.getElementById('errorTimelineChart');
|
||||
|
||||
if (errorTimelineChart) {
|
||||
errorTimelineChart.destroy();
|
||||
}
|
||||
|
||||
const labels = dailyErrorTrend.map(d => formatDate(new Date(d.date)));
|
||||
const errorData = dailyErrorTrend.map(d => d.error_count);
|
||||
const totalData = dailyErrorTrend.map(d => d.total_count);
|
||||
|
||||
errorTimelineChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: '총 작업',
|
||||
data: totalData,
|
||||
borderColor: 'rgba(59, 130, 246, 1)',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: '오류 작업',
|
||||
data: errorData,
|
||||
borderColor: 'rgba(239, 68, 68, 1)',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 오류 유형별 분석 표시
|
||||
function displayErrorTypes(errorTypes) {
|
||||
const container = document.getElementById('errorTypesAnalysis');
|
||||
|
||||
if (!errorTypes || errorTypes.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">⚠️</div>
|
||||
<h3>오류 유형 데이터가 없습니다.</h3>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<h4>🔍 오류 유형별 상세 분석</h4>';
|
||||
|
||||
errorTypes.forEach(errorType => {
|
||||
html += `
|
||||
<div class="error-type-item">
|
||||
<div class="error-type-info">
|
||||
<div class="error-type-icon">⚠️</div>
|
||||
<div class="error-type-name">${errorType.error_name}</div>
|
||||
</div>
|
||||
<div class="error-type-stats">
|
||||
<div class="error-type-count">${errorType.count}건</div>
|
||||
<div class="error-type-percentage">${errorType.percentage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 프로젝트별 분석 로드
|
||||
async function loadProjectAnalysis() {
|
||||
const projectId = document.getElementById('projectModeSelect').value;
|
||||
const startDate = document.getElementById('projectStartDate').value;
|
||||
const endDate = document.getElementById('projectEndDate').value;
|
||||
|
||||
if (!projectId) {
|
||||
showToast('프로젝트를 선택해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading(true);
|
||||
|
||||
try {
|
||||
console.log('📁 프로젝트별 분석 데이터 로딩 시작');
|
||||
|
||||
// API 호출 파라미터 구성
|
||||
const params = new URLSearchParams({
|
||||
project_id: projectId
|
||||
});
|
||||
|
||||
if (startDate) params.append('start', startDate);
|
||||
if (endDate) params.append('end', endDate);
|
||||
|
||||
// 프로젝트별 상세 분석 데이터 로드
|
||||
const response = await apiCall(`/work-analysis/project-worktype-analysis?${params}`, 'GET');
|
||||
const projectAnalysisData = response.data || response;
|
||||
|
||||
console.log('📁 프로젝트 분석 데이터:', projectAnalysisData);
|
||||
|
||||
// 결과 표시
|
||||
displayProjectModeAnalysis(projectAnalysisData);
|
||||
|
||||
// 결과 섹션 표시
|
||||
document.getElementById('projectModeResults').style.display = 'block';
|
||||
|
||||
showToast('프로젝트 분석이 완료되었습니다.', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('프로젝트별 분석 오류:', error);
|
||||
showToast('프로젝트 분석 중 오류가 발생했습니다.', 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 프로젝트별 분석 결과 표시
|
||||
function displayProjectModeAnalysis(data) {
|
||||
const container = document.getElementById('projectModeResults');
|
||||
|
||||
// 프로젝트별 분석 결과 HTML 생성
|
||||
let html = `
|
||||
<div class="project-mode-analysis">
|
||||
<h3>📁 ${data.project_name} 분석 결과</h3>
|
||||
<!-- 프로젝트별 상세 분석 내용 -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 로딩 상태 표시/숨김
|
||||
function showLoading(show) {
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = show ? 'flex' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
function formatDate(date) {
|
||||
if (!date) return '';
|
||||
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// 토스트 메시지 표시
|
||||
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: '10000',
|
||||
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.switchAnalysisMode = switchAnalysisMode;
|
||||
window.switchAnalysisTab = switchAnalysisTab;
|
||||
window.loadPeriodAnalysis = loadPeriodAnalysis;
|
||||
window.loadProjectAnalysis = loadProjectAnalysis;
|
||||
320
web-ui/js/work-management.js
Normal file
320
web-ui/js/work-management.js
Normal file
@@ -0,0 +1,320 @@
|
||||
// 작업 관리 페이지 JavaScript
|
||||
|
||||
// 전역 변수
|
||||
let statsData = {
|
||||
projects: 0,
|
||||
workers: 0,
|
||||
tasks: 0,
|
||||
codeTypes: 0
|
||||
};
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('🔧 작업 관리 페이지 초기화 시작');
|
||||
|
||||
initializePage();
|
||||
loadStatistics();
|
||||
loadRecentActivity();
|
||||
});
|
||||
|
||||
// 페이지 초기화
|
||||
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 loadStatistics() {
|
||||
try {
|
||||
console.log('📊 통계 데이터 로딩 시작');
|
||||
|
||||
// 프로젝트 수 조회
|
||||
try {
|
||||
const projectsResponse = await apiCall('/projects', 'GET');
|
||||
if (projectsResponse && Array.isArray(projectsResponse)) {
|
||||
statsData.projects = projectsResponse.length;
|
||||
updateStatDisplay('projectCount', statsData.projects);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('프로젝트 통계 로드 실패:', error);
|
||||
updateStatDisplay('projectCount', '오류');
|
||||
}
|
||||
|
||||
// 작업자 수 조회
|
||||
try {
|
||||
const workersResponse = await apiCall('/workers', 'GET');
|
||||
if (workersResponse && Array.isArray(workersResponse)) {
|
||||
const activeWorkers = workersResponse.filter(w => w.status === 'active');
|
||||
statsData.workers = activeWorkers.length;
|
||||
updateStatDisplay('workerCount', statsData.workers);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('작업자 통계 로드 실패:', error);
|
||||
updateStatDisplay('workerCount', '오류');
|
||||
}
|
||||
|
||||
// 작업 유형 수 조회
|
||||
try {
|
||||
const tasksResponse = await apiCall('/tasks', 'GET');
|
||||
if (tasksResponse && Array.isArray(tasksResponse)) {
|
||||
const activeTasks = tasksResponse.filter(t => t.is_active);
|
||||
statsData.tasks = activeTasks.length;
|
||||
updateStatDisplay('taskCount', statsData.tasks);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('작업 유형 통계 로드 실패:', error);
|
||||
updateStatDisplay('taskCount', '오류');
|
||||
}
|
||||
|
||||
// 코드 타입 수 조회 (임시로 고정값)
|
||||
statsData.codeTypes = 3; // ISSUE_TYPE, ERROR_TYPE, WORK_STATUS
|
||||
updateStatDisplay('codeTypeCount', statsData.codeTypes);
|
||||
|
||||
console.log('✅ 통계 데이터 로딩 완료:', statsData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('통계 데이터 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 표시 업데이트
|
||||
function updateStatDisplay(elementId, value) {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
|
||||
// 애니메이션 효과
|
||||
element.style.transform = 'scale(1.1)';
|
||||
setTimeout(() => {
|
||||
element.style.transform = 'scale(1)';
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// 최근 활동 로드
|
||||
async function loadRecentActivity() {
|
||||
try {
|
||||
console.log('📋 최근 활동 로딩 시작');
|
||||
|
||||
// 임시 데이터 (실제로는 API에서 가져와야 함)
|
||||
const activities = [
|
||||
{
|
||||
type: 'project',
|
||||
icon: '📁',
|
||||
title: '효성화학 에틸렌 탱크 건설공사 프로젝트가 수정되었습니다',
|
||||
user: '김두수',
|
||||
time: '2시간 전'
|
||||
},
|
||||
{
|
||||
type: 'worker',
|
||||
icon: '👥',
|
||||
title: '새로운 작업자가 등록되었습니다',
|
||||
user: '관리자',
|
||||
time: '1일 전'
|
||||
},
|
||||
{
|
||||
type: 'task',
|
||||
icon: '📋',
|
||||
title: '작업 유형이 업데이트되었습니다',
|
||||
user: '김두수',
|
||||
time: '2일 전'
|
||||
}
|
||||
];
|
||||
|
||||
renderActivityList(activities);
|
||||
|
||||
} catch (error) {
|
||||
console.error('최근 활동 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 활동 목록 렌더링
|
||||
function renderActivityList(activities) {
|
||||
const activityList = document.getElementById('activityList');
|
||||
if (!activityList) return;
|
||||
|
||||
const activitiesHtml = activities.map(activity => `
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon">${activity.icon}</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-title">${activity.title}</div>
|
||||
<div class="activity-meta">
|
||||
<span class="activity-user">${activity.user}</span>
|
||||
<span class="activity-time">${activity.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
activityList.innerHTML = activitiesHtml;
|
||||
}
|
||||
|
||||
// 페이지 네비게이션
|
||||
function navigateToPage(url) {
|
||||
console.log(`🔗 페이지 이동: ${url}`);
|
||||
|
||||
// 로딩 효과
|
||||
const card = event.currentTarget;
|
||||
const originalContent = card.innerHTML;
|
||||
|
||||
card.style.opacity = '0.7';
|
||||
card.style.pointerEvents = 'none';
|
||||
|
||||
// 잠시 후 페이지 이동
|
||||
setTimeout(() => {
|
||||
window.location.href = url;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// 토스트 메시지 표시
|
||||
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.navigateToPage = navigateToPage;
|
||||
window.loadRecentActivity = loadRecentActivity;
|
||||
@@ -5,6 +5,8 @@ let currentDate = new Date();
|
||||
let monthlyData = {}; // 월별 데이터 캐시
|
||||
// 작업자 데이터는 allWorkers 변수 사용
|
||||
let currentModalDate = null;
|
||||
let currentEditingWork = null;
|
||||
let existingWorks = [];
|
||||
|
||||
// DOM 요소
|
||||
const elements = {
|
||||
@@ -230,8 +232,8 @@ async function loadMonthlyWorkDataFallback(year, month) {
|
||||
console.log(`📋 ${monthKey} 로딩 진행률: ${loadedCount}/${totalDays}`);
|
||||
}
|
||||
|
||||
// API 부하 방지를 위한 지연 (100ms)
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
// API 부하 방지를 위한 지연 (500ms)
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`${dateStr} 데이터 로딩 실패:`, error.message);
|
||||
@@ -846,14 +848,20 @@ async function openWorkEntryModal(workerId, workerName, date) {
|
||||
}
|
||||
|
||||
// 모달 제목 및 정보 설정
|
||||
titleElement.textContent = `${workerName} - 작업 입력`;
|
||||
titleElement.textContent = `${workerName} - 작업 관리`;
|
||||
workerNameDisplay.value = workerName;
|
||||
workerIdInput.value = workerId;
|
||||
workDateInput.value = date;
|
||||
|
||||
// 기존 작업 데이터 로드
|
||||
await loadExistingWorks(workerId, date);
|
||||
|
||||
// 프로젝트 및 상태 데이터 로드
|
||||
await loadModalData();
|
||||
|
||||
// 기본적으로 기존 작업 탭 활성화
|
||||
switchTab('existing');
|
||||
|
||||
// 모달 표시
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
@@ -867,8 +875,8 @@ async function openWorkEntryModal(workerId, workerName, date) {
|
||||
// 모달 데이터 로드 (프로젝트, 작업 상태)
|
||||
async function loadModalData() {
|
||||
try {
|
||||
// 프로젝트 목록 로드
|
||||
const projectsResponse = await window.apiCall('/projects');
|
||||
// 활성 프로젝트 목록 로드
|
||||
const projectsResponse = await window.apiCall('/projects/active/list');
|
||||
const projects = Array.isArray(projectsResponse) ? projectsResponse : (projectsResponse.data || []);
|
||||
|
||||
const projectSelect = document.getElementById('projectSelect');
|
||||
@@ -960,24 +968,64 @@ async function saveWorkEntry() {
|
||||
const workData = {
|
||||
worker_id: document.getElementById('workerId').value,
|
||||
project_id: document.getElementById('projectSelect').value,
|
||||
work_type_id: document.getElementById('workTypeSelect').value, // 추가된 필드
|
||||
work_hours: document.getElementById('workHours').value,
|
||||
work_status_id: document.getElementById('workStatusSelect').value,
|
||||
error_type_id: document.getElementById('errorTypeSelect')?.value || null, // 추가된 필드
|
||||
description: document.getElementById('workDescription').value,
|
||||
report_date: document.getElementById('workDate').value
|
||||
};
|
||||
|
||||
const editingWorkId = document.getElementById('editingWorkId').value;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!workData.project_id || !workData.work_hours || !workData.work_status_id) {
|
||||
if (!workData.project_id || !workData.work_type_id || !workData.work_hours || !workData.work_status_id) {
|
||||
showToast('필수 항목을 모두 입력해주세요.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// API 호출
|
||||
const response = await window.apiCall('/daily-work-reports', 'POST', workData);
|
||||
// API 호출 (수정 또는 신규)
|
||||
let response;
|
||||
if (editingWorkId) {
|
||||
// 수정 모드 - 서버가 기대하는 형태로 데이터 변환
|
||||
const updateData = {
|
||||
project_id: workData.project_id,
|
||||
work_type_id: workData.work_type_id, // 실제 테이블 컬럼명 사용
|
||||
work_hours: workData.work_hours,
|
||||
work_status_id: workData.work_status_id, // 실제 테이블 컬럼명 사용
|
||||
error_type_id: workData.error_type_id // 실제 테이블 컬럼명 사용
|
||||
};
|
||||
|
||||
console.log('🔄 수정용 서버로 전송할 데이터:', updateData);
|
||||
response = await window.apiCall(`/daily-work-reports/${editingWorkId}`, 'PUT', updateData);
|
||||
} else {
|
||||
// 신규 추가 모드 - 서버가 기대하는 형태로 데이터 변환
|
||||
const serverData = {
|
||||
report_date: workData.report_date,
|
||||
worker_id: workData.worker_id,
|
||||
work_entries: [{
|
||||
project_id: workData.project_id,
|
||||
task_id: workData.work_type_id, // work_type_id를 task_id로 매핑
|
||||
work_hours: workData.work_hours,
|
||||
work_status_id: workData.work_status_id,
|
||||
error_type_id: workData.error_type_id,
|
||||
description: workData.description
|
||||
}]
|
||||
};
|
||||
|
||||
console.log('🔄 서버로 전송할 데이터:', serverData);
|
||||
response = await window.apiCall('/daily-work-reports', 'POST', serverData);
|
||||
}
|
||||
|
||||
if (response.success || response.id) {
|
||||
showToast('작업이 성공적으로 저장되었습니다.', 'success');
|
||||
closeWorkEntryModal();
|
||||
const action = editingWorkId ? '수정' : '저장';
|
||||
showToast(`작업이 성공적으로 ${action}되었습니다.`, 'success');
|
||||
|
||||
// 기존 작업 목록 새로고침
|
||||
await loadExistingWorks(workData.worker_id, workData.report_date);
|
||||
|
||||
// 기존 작업 탭으로 전환
|
||||
switchTab('existing');
|
||||
|
||||
// 캘린더 새로고침
|
||||
await renderCalendar();
|
||||
@@ -987,7 +1035,8 @@ async function saveWorkEntry() {
|
||||
await openDailyWorkModal(currentModalDate);
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.message || '저장에 실패했습니다.');
|
||||
const action = editingWorkId ? '수정' : '저장';
|
||||
throw new Error(response.message || `${action}에 실패했습니다.`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -1025,32 +1074,51 @@ function updateCurrentTime() {
|
||||
|
||||
// 사용자 정보 업데이트 함수
|
||||
function updateUserInfo() {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
// auth-check.js에서 사용하는 'user' 키와 기존 'userInfo' 키 모두 확인
|
||||
let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
let authUser = JSON.parse(localStorage.getItem('user') || '{}');
|
||||
|
||||
console.log('👤 localStorage userInfo:', userInfo);
|
||||
console.log('👤 localStorage user (auth):', authUser);
|
||||
|
||||
// 두 소스에서 사용자 정보 통합
|
||||
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
|
||||
};
|
||||
|
||||
console.log('👤 최종 사용자 정보:', finalUserInfo);
|
||||
|
||||
const userNameElement = document.getElementById('userName');
|
||||
const userRoleElement = document.getElementById('userRole');
|
||||
const userInitialElement = document.getElementById('userInitial');
|
||||
|
||||
if (userNameElement) {
|
||||
if (userInfo.worker_name) {
|
||||
userNameElement.textContent = userInfo.worker_name;
|
||||
if (finalUserInfo.worker_name) {
|
||||
userNameElement.textContent = finalUserInfo.worker_name;
|
||||
} else {
|
||||
userNameElement.textContent = '사용자';
|
||||
}
|
||||
}
|
||||
|
||||
if (userRoleElement) {
|
||||
if (userInfo.job_type) {
|
||||
userRoleElement.textContent = userInfo.job_type;
|
||||
if (finalUserInfo.job_type) {
|
||||
// role을 한글로 변환
|
||||
const roleMap = {
|
||||
'leader': '그룹장',
|
||||
'worker': '작업자',
|
||||
'admin': '관리자'
|
||||
};
|
||||
userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type;
|
||||
} else {
|
||||
userRoleElement.textContent = '작업자';
|
||||
}
|
||||
}
|
||||
|
||||
if (userInitialElement) {
|
||||
if (userInfo.worker_name) {
|
||||
userInitialElement.textContent = userInfo.worker_name.charAt(0);
|
||||
if (finalUserInfo.worker_name) {
|
||||
userInitialElement.textContent = finalUserInfo.worker_name.charAt(0);
|
||||
} else {
|
||||
userInitialElement.textContent = '사';
|
||||
}
|
||||
@@ -1098,7 +1166,494 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
initializePage();
|
||||
});
|
||||
|
||||
// ========== 작업 입력 모달 개선 기능들 ==========
|
||||
|
||||
// 탭 전환 함수
|
||||
function switchTab(tabName) {
|
||||
// 모든 탭 버튼 비활성화
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
// 모든 탭 콘텐츠 숨기기
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// 선택된 탭 활성화
|
||||
const selectedTabBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||||
const selectedTabContent = document.getElementById(`${tabName}WorkTab`);
|
||||
|
||||
if (selectedTabBtn) selectedTabBtn.classList.add('active');
|
||||
if (selectedTabContent) selectedTabContent.classList.add('active');
|
||||
|
||||
// 새 작업 탭으로 전환 시 폼 초기화
|
||||
if (tabName === 'new') {
|
||||
resetWorkForm();
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 작업 데이터 로드
|
||||
async function loadExistingWorks(workerId, date) {
|
||||
try {
|
||||
console.log(`📋 기존 작업 로드: 작업자 ${workerId}, 날짜 ${date}`);
|
||||
|
||||
let workerWorks = [];
|
||||
|
||||
try {
|
||||
// 방법 1: 날짜별 작업 보고서 조회 시도
|
||||
const response = await apiCall(`/daily-work-reports/date/${date}`, 'GET');
|
||||
|
||||
if (response && Array.isArray(response)) {
|
||||
console.log(`📊 방법1 - 전체 응답 데이터 (${response.length}건):`, response);
|
||||
|
||||
// 김두수(작업자 ID 1)의 모든 작업 확인
|
||||
const allWorkerOneWorks = response.filter(work => work.worker_id == 1);
|
||||
console.log(`🔍 김두수(ID=1)의 모든 작업 (${allWorkerOneWorks.length}건):`, allWorkerOneWorks);
|
||||
|
||||
// 해당 작업자의 작업만 필터링
|
||||
workerWorks = response.filter(work => {
|
||||
const isMatch = work.worker_id == workerId;
|
||||
console.log(`🔍 작업 필터링: ID=${work.id}, worker_id=${work.worker_id}, 대상=${workerId}, 일치=${isMatch}`);
|
||||
return isMatch;
|
||||
});
|
||||
|
||||
console.log(`✅ 방법1 성공: 작업자 ${workerId}의 ${date} 작업 ${workerWorks.length}건 로드`);
|
||||
console.log('📋 필터링된 작업 목록:', workerWorks);
|
||||
}
|
||||
} catch (dateApiError) {
|
||||
console.warn('📅 날짜별 API 실패, 범위 조회 시도:', dateApiError.message);
|
||||
|
||||
try {
|
||||
// 방법 2: 범위 조회로 fallback (해당 날짜만)
|
||||
const response = await apiCall(`/daily-work-reports?start=${date}&end=${date}`, 'GET');
|
||||
|
||||
if (response && Array.isArray(response)) {
|
||||
console.log(`📊 방법2 - 전체 응답 데이터 (${response.length}건):`, response);
|
||||
|
||||
workerWorks = response.filter(work => {
|
||||
const isMatch = work.worker_id == workerId;
|
||||
console.log(`🔍 작업 필터링: ID=${work.id}, worker_id=${work.worker_id}, 대상=${workerId}, 일치=${isMatch}`);
|
||||
return isMatch;
|
||||
});
|
||||
|
||||
console.log(`✅ 방법2 성공: 작업자 ${workerId}의 ${date} 작업 ${workerWorks.length}건 로드`);
|
||||
console.log('📋 필터링된 작업 목록:', workerWorks);
|
||||
}
|
||||
} catch (rangeApiError) {
|
||||
console.warn('📊 범위 조회도 실패:', rangeApiError.message);
|
||||
// 최종적으로 빈 배열로 처리
|
||||
workerWorks = [];
|
||||
}
|
||||
}
|
||||
|
||||
existingWorks = workerWorks;
|
||||
renderExistingWorks();
|
||||
updateTabCounter();
|
||||
|
||||
} catch (error) {
|
||||
console.error('기존 작업 로드 오류:', error);
|
||||
existingWorks = [];
|
||||
renderExistingWorks();
|
||||
updateTabCounter();
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 작업 목록 렌더링
|
||||
function renderExistingWorks() {
|
||||
console.log('🎨 작업 목록 렌더링 시작:', existingWorks);
|
||||
|
||||
const existingWorkList = document.getElementById('existingWorkList');
|
||||
const noExistingWork = document.getElementById('noExistingWork');
|
||||
const totalWorkCount = document.getElementById('totalWorkCount');
|
||||
const totalWorkHours = document.getElementById('totalWorkHours');
|
||||
|
||||
if (!existingWorkList) {
|
||||
console.error('❌ existingWorkList 요소를 찾을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 총 작업 시간 계산
|
||||
const totalHours = existingWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
|
||||
|
||||
console.log(`📊 작업 통계: ${existingWorks.length}건, 총 ${totalHours}시간`);
|
||||
|
||||
// 요약 정보 업데이트
|
||||
if (totalWorkCount) totalWorkCount.textContent = existingWorks.length;
|
||||
if (totalWorkHours) totalWorkHours.textContent = totalHours.toFixed(1);
|
||||
|
||||
if (existingWorks.length === 0) {
|
||||
existingWorkList.style.display = 'none';
|
||||
if (noExistingWork) noExistingWork.style.display = 'block';
|
||||
console.log('ℹ️ 작업이 없어서 빈 상태 표시');
|
||||
return;
|
||||
}
|
||||
|
||||
existingWorkList.style.display = 'block';
|
||||
if (noExistingWork) noExistingWork.style.display = 'none';
|
||||
|
||||
// 각 작업 데이터 상세 로그
|
||||
existingWorks.forEach((work, index) => {
|
||||
console.log(`📋 작업 ${index + 1}:`, {
|
||||
id: work.id,
|
||||
project_name: work.project_name,
|
||||
work_hours: work.work_hours,
|
||||
work_status_name: work.work_status_name,
|
||||
created_at: work.created_at,
|
||||
description: work.description
|
||||
});
|
||||
});
|
||||
|
||||
// 작업 목록 HTML 생성
|
||||
const worksHtml = existingWorks.map((work, index) => {
|
||||
const workItemHtml = `
|
||||
<div class="work-item" data-work-id="${work.id}">
|
||||
<div class="work-item-header">
|
||||
<div class="work-item-info">
|
||||
<div class="work-item-title">${work.project_name || '프로젝트 정보 없음'}</div>
|
||||
<div class="work-item-meta">
|
||||
<span>⏰ ${work.work_hours}시간</span>
|
||||
<span>📊 ${work.work_status_name || '상태 정보 없음'}</span>
|
||||
<span>📅 ${new Date(work.created_at).toLocaleString('ko-KR')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="work-item-actions">
|
||||
<button class="btn-edit" onclick="editWork(${work.id})" title="수정">
|
||||
✏️ 수정
|
||||
</button>
|
||||
<button class="btn-delete" onclick="confirmDeleteWork(${work.id})" title="삭제">
|
||||
🗑️ 삭제
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${work.description ? `<div class="work-item-description">${work.description}</div>` : ''}
|
||||
</div>`;
|
||||
|
||||
console.log(`🏗️ 작업 ${index + 1} HTML 생성 완료`);
|
||||
return workItemHtml;
|
||||
}).join('');
|
||||
|
||||
console.log(`📝 최종 HTML 길이: ${worksHtml.length} 문자`);
|
||||
console.log('🎯 HTML 내용 미리보기:', worksHtml.substring(0, 200) + '...');
|
||||
|
||||
existingWorkList.innerHTML = worksHtml;
|
||||
|
||||
// 렌더링 후 실제 DOM 요소 확인
|
||||
const renderedItems = existingWorkList.querySelectorAll('.work-item');
|
||||
console.log(`✅ 렌더링 완료: ${renderedItems.length}개 작업 아이템이 DOM에 추가됨`);
|
||||
|
||||
if (renderedItems.length !== existingWorks.length) {
|
||||
console.error(`⚠️ 렌더링 불일치: 데이터 ${existingWorks.length}건 vs DOM ${renderedItems.length}개`);
|
||||
}
|
||||
}
|
||||
|
||||
// 탭 카운터 업데이트
|
||||
function updateTabCounter() {
|
||||
const existingTabBtn = document.querySelector('[data-tab="existing"]');
|
||||
if (existingTabBtn) {
|
||||
existingTabBtn.innerHTML = `📋 기존 작업 (${existingWorks.length}건)`;
|
||||
}
|
||||
}
|
||||
|
||||
// 작업 수정
|
||||
function editWork(workId) {
|
||||
const work = existingWorks.find(w => w.id === workId);
|
||||
if (!work) {
|
||||
showToast('작업 정보를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 수정 모드로 전환
|
||||
currentEditingWork = work;
|
||||
|
||||
// 새 작업 탭으로 전환
|
||||
switchTab('new');
|
||||
|
||||
// 폼에 기존 데이터 채우기
|
||||
document.getElementById('editingWorkId').value = work.id;
|
||||
document.getElementById('projectSelect').value = work.project_id;
|
||||
document.getElementById('workHours').value = work.work_hours;
|
||||
document.getElementById('workStatusSelect').value = work.work_status_id;
|
||||
document.getElementById('workDescription').value = work.description || '';
|
||||
|
||||
// UI 업데이트
|
||||
document.getElementById('workContentTitle').textContent = '작업 내용 수정';
|
||||
document.getElementById('saveWorkBtn').innerHTML = '💾 수정 완료';
|
||||
document.getElementById('deleteWorkBtn').style.display = 'inline-block';
|
||||
|
||||
// 휴가 섹션 숨기기 (수정 시에는 휴가 처리 불가)
|
||||
document.getElementById('vacationSection').style.display = 'none';
|
||||
}
|
||||
|
||||
// 작업 삭제 확인
|
||||
function confirmDeleteWork(workId) {
|
||||
const work = existingWorks.find(w => w.id === workId);
|
||||
if (!work) {
|
||||
showToast('작업 정보를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm(`"${work.project_name}" 작업을 정말 삭제하시겠습니까?\n\n⚠️ 삭제된 작업은 복구할 수 없습니다.`)) {
|
||||
deleteWorkById(workId);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업 삭제 실행
|
||||
async function deleteWorkById(workId) {
|
||||
try {
|
||||
const response = await apiCall(`/daily-work-reports/${workId}`, 'DELETE');
|
||||
|
||||
if (response.success) {
|
||||
showToast('작업이 성공적으로 삭제되었습니다.', 'success');
|
||||
|
||||
// 기존 작업 목록 새로고침
|
||||
const workerId = document.getElementById('workerId').value;
|
||||
const date = document.getElementById('workDate').value;
|
||||
await loadExistingWorks(workerId, date);
|
||||
|
||||
// 현재 열린 모달이 있다면 새로고침
|
||||
if (currentModalDate) {
|
||||
await openDailyWorkModal(currentModalDate);
|
||||
}
|
||||
} else {
|
||||
showToast(response.message || '작업 삭제에 실패했습니다.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업 삭제 오류:', error);
|
||||
showToast('작업 삭제 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 작업 폼 초기화
|
||||
function resetWorkForm() {
|
||||
currentEditingWork = null;
|
||||
|
||||
// 폼 필드 초기화
|
||||
document.getElementById('editingWorkId').value = '';
|
||||
document.getElementById('projectSelect').value = '';
|
||||
document.getElementById('workHours').value = '';
|
||||
document.getElementById('workStatusSelect').value = '';
|
||||
document.getElementById('workDescription').value = '';
|
||||
|
||||
// UI 초기화
|
||||
document.getElementById('workContentTitle').textContent = '작업 내용';
|
||||
document.getElementById('saveWorkBtn').innerHTML = '💾 저장';
|
||||
document.getElementById('deleteWorkBtn').style.display = 'none';
|
||||
document.getElementById('vacationSection').style.display = 'block';
|
||||
}
|
||||
|
||||
// 작업 삭제 (수정 모드에서)
|
||||
function deleteWork() {
|
||||
if (currentEditingWork) {
|
||||
confirmDeleteWork(currentEditingWork.id);
|
||||
}
|
||||
}
|
||||
|
||||
// 휴가 처리 함수
|
||||
function handleVacation(vacationType) {
|
||||
const workHours = document.getElementById('workHours');
|
||||
const projectSelect = document.getElementById('projectSelect');
|
||||
const workTypeSelect = document.getElementById('workTypeSelect');
|
||||
const workStatusSelect = document.getElementById('workStatusSelect');
|
||||
const errorTypeSelect = document.getElementById('errorTypeSelect');
|
||||
const workDescription = document.getElementById('workDescription');
|
||||
|
||||
// 휴가 시간 설정
|
||||
const vacationHours = {
|
||||
'full': 8, // 연차
|
||||
'half': 4, // 반차
|
||||
'quarter': 2, // 반반차
|
||||
'early': 6 // 조퇴
|
||||
};
|
||||
|
||||
const vacationNames = {
|
||||
'full': '연차',
|
||||
'half': '반차',
|
||||
'quarter': '반반차',
|
||||
'early': '조퇴'
|
||||
};
|
||||
|
||||
// 시간 설정
|
||||
if (workHours) {
|
||||
workHours.value = vacationHours[vacationType] || 8;
|
||||
}
|
||||
|
||||
// 휴가용 기본값 설정 (휴가 관련 항목 찾아서 자동 선택)
|
||||
if (projectSelect && projectSelect.options.length > 1) {
|
||||
// "휴가", "연차", "관리" 등의 키워드가 포함된 프로젝트 찾기
|
||||
let vacationProjectFound = false;
|
||||
for (let i = 1; i < projectSelect.options.length; i++) {
|
||||
const optionText = projectSelect.options[i].textContent.toLowerCase();
|
||||
if (optionText.includes('휴가') || optionText.includes('연차') || optionText.includes('관리')) {
|
||||
projectSelect.selectedIndex = i;
|
||||
vacationProjectFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!vacationProjectFound) {
|
||||
projectSelect.selectedIndex = 1; // 첫 번째 프로젝트 선택
|
||||
}
|
||||
}
|
||||
|
||||
if (workTypeSelect && workTypeSelect.options.length > 1) {
|
||||
// "휴가", "연차", "관리" 등의 키워드가 포함된 작업 유형 찾기
|
||||
let vacationWorkTypeFound = false;
|
||||
for (let i = 1; i < workTypeSelect.options.length; i++) {
|
||||
const optionText = workTypeSelect.options[i].textContent.toLowerCase();
|
||||
if (optionText.includes('휴가') || optionText.includes('연차') || optionText.includes('관리')) {
|
||||
workTypeSelect.selectedIndex = i;
|
||||
vacationWorkTypeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!vacationWorkTypeFound) {
|
||||
workTypeSelect.selectedIndex = 1; // 첫 번째 작업 유형 선택
|
||||
}
|
||||
}
|
||||
|
||||
if (workStatusSelect && workStatusSelect.options.length > 1) {
|
||||
// "정상", "완료" 등의 키워드가 포함된 상태 찾기
|
||||
let normalStatusFound = false;
|
||||
for (let i = 1; i < workStatusSelect.options.length; i++) {
|
||||
const optionText = workStatusSelect.options[i].textContent.toLowerCase();
|
||||
if (optionText.includes('정상') || optionText.includes('완료') || optionText.includes('normal')) {
|
||||
workStatusSelect.selectedIndex = i;
|
||||
normalStatusFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!normalStatusFound) {
|
||||
workStatusSelect.selectedIndex = 1; // 첫 번째 상태 선택
|
||||
}
|
||||
}
|
||||
|
||||
// 오류 유형은 선택하지 않음
|
||||
if (errorTypeSelect) {
|
||||
errorTypeSelect.selectedIndex = 0;
|
||||
}
|
||||
|
||||
// 작업 설명에 휴가 정보 입력
|
||||
if (workDescription) {
|
||||
workDescription.value = `${vacationNames[vacationType]} (${vacationHours[vacationType]}시간)`;
|
||||
}
|
||||
|
||||
// 사용자에게 알림
|
||||
showToast(`${vacationNames[vacationType]} (${vacationHours[vacationType]}시간)이 설정되었습니다.`, 'success');
|
||||
}
|
||||
|
||||
// 탭 전환 함수
|
||||
function switchTab(tabName) {
|
||||
// 탭 버튼 활성화 상태 변경
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
||||
|
||||
// 탭 콘텐츠 표시/숨김
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
document.getElementById(`${tabName}WorkTab`).classList.add('active');
|
||||
|
||||
// 새 작업 탭으로 전환할 때 드롭다운 데이터 로드
|
||||
if (tabName === 'new') {
|
||||
loadDropdownData();
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 함수로 노출
|
||||
// 드롭다운 로딩 함수들
|
||||
async function loadDropdownData() {
|
||||
try {
|
||||
console.log('🔄 드롭다운 데이터 로딩 시작...');
|
||||
|
||||
// 프로젝트 로드
|
||||
console.log('📡 프로젝트 로딩 중...');
|
||||
const projectsRes = await window.apiCall('/projects/active/list');
|
||||
const projects = Array.isArray(projectsRes) ? projectsRes : (projectsRes.data || []);
|
||||
console.log('📁 로드된 프로젝트:', projects.length, '개');
|
||||
|
||||
const projectSelect = document.getElementById('projectSelect');
|
||||
if (projectSelect) {
|
||||
projectSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
||||
projects.forEach(project => {
|
||||
const option = document.createElement('option');
|
||||
option.value = project.project_id;
|
||||
option.textContent = project.project_name;
|
||||
projectSelect.appendChild(option);
|
||||
});
|
||||
console.log('✅ 프로젝트 드롭다운 업데이트 완료');
|
||||
} else {
|
||||
console.error('❌ projectSelect 요소를 찾을 수 없음');
|
||||
}
|
||||
|
||||
// 작업 유형 로드
|
||||
console.log('📡 작업 유형 로딩 중...');
|
||||
const workTypesRes = await window.apiCall('/daily-work-reports/work-types');
|
||||
const workTypes = Array.isArray(workTypesRes) ? workTypesRes : (workTypesRes.data || []);
|
||||
console.log('🔧 로드된 작업 유형:', workTypes.length, '개');
|
||||
|
||||
const workTypeSelect = document.getElementById('workTypeSelect');
|
||||
if (workTypeSelect) {
|
||||
workTypeSelect.innerHTML = '<option value="">작업 유형을 선택하세요</option>';
|
||||
workTypes.forEach(workType => {
|
||||
const option = document.createElement('option');
|
||||
option.value = workType.id; // work_type_id → id
|
||||
option.textContent = workType.name; // work_type_name → name
|
||||
workTypeSelect.appendChild(option);
|
||||
});
|
||||
console.log('✅ 작업 유형 드롭다운 업데이트 완료');
|
||||
} else {
|
||||
console.error('❌ workTypeSelect 요소를 찾을 수 없음');
|
||||
}
|
||||
|
||||
// 작업 상태 로드
|
||||
console.log('📡 작업 상태 로딩 중...');
|
||||
const workStatusRes = await window.apiCall('/daily-work-reports/work-status-types');
|
||||
const workStatuses = Array.isArray(workStatusRes) ? workStatusRes : (workStatusRes.data || []);
|
||||
console.log('📊 로드된 작업 상태:', workStatuses.length, '개');
|
||||
|
||||
const workStatusSelect = document.getElementById('workStatusSelect');
|
||||
if (workStatusSelect) {
|
||||
workStatusSelect.innerHTML = '<option value="">상태를 선택하세요</option>';
|
||||
workStatuses.forEach(status => {
|
||||
const option = document.createElement('option');
|
||||
option.value = status.id; // work_status_id → id
|
||||
option.textContent = status.name; // status_name → name
|
||||
workStatusSelect.appendChild(option);
|
||||
});
|
||||
console.log('✅ 작업 상태 드롭다운 업데이트 완료');
|
||||
} else {
|
||||
console.error('❌ workStatusSelect 요소를 찾을 수 없음');
|
||||
}
|
||||
|
||||
// 오류 유형 로드
|
||||
console.log('📡 오류 유형 로딩 중...');
|
||||
const errorTypesRes = await window.apiCall('/daily-work-reports/error-types');
|
||||
const errorTypes = Array.isArray(errorTypesRes) ? errorTypesRes : (errorTypesRes.data || []);
|
||||
console.log('⚠️ 로드된 오류 유형:', errorTypes.length, '개');
|
||||
|
||||
const errorTypeSelect = document.getElementById('errorTypeSelect');
|
||||
if (errorTypeSelect) {
|
||||
errorTypeSelect.innerHTML = '<option value="">오류 유형 (선택사항)</option>';
|
||||
errorTypes.forEach(errorType => {
|
||||
const option = document.createElement('option');
|
||||
option.value = errorType.id; // error_type_id → id
|
||||
option.textContent = errorType.name; // error_type_name → name
|
||||
errorTypeSelect.appendChild(option);
|
||||
});
|
||||
console.log('✅ 오류 유형 드롭다운 업데이트 완료');
|
||||
} else {
|
||||
console.error('❌ errorTypeSelect 요소를 찾을 수 없음');
|
||||
}
|
||||
|
||||
console.log('🎉 모든 드롭다운 데이터 로딩 완료!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 드롭다운 데이터 로딩 오류:', error);
|
||||
}
|
||||
}
|
||||
|
||||
window.openDailyWorkModal = openDailyWorkModal;
|
||||
window.closeDailyWorkModal = closeDailyWorkModal;
|
||||
window.openWorkerModal = openWorkerModal;
|
||||
@@ -1106,3 +1661,8 @@ window.openWorkEntryModal = openWorkEntryModal;
|
||||
window.closeWorkEntryModal = closeWorkEntryModal;
|
||||
window.handleVacation = handleVacation;
|
||||
window.saveWorkEntry = saveWorkEntry;
|
||||
window.switchTab = switchTab;
|
||||
window.editWork = editWork;
|
||||
window.confirmDeleteWork = confirmDeleteWork;
|
||||
window.deleteWork = deleteWork;
|
||||
window.loadDropdownData = loadDropdownData;
|
||||
|
||||
@@ -20,21 +20,20 @@ async function loadReports() {
|
||||
reportBody.innerHTML = '<tr><td colspan="8">불러오는 중...</td></tr>';
|
||||
|
||||
try {
|
||||
const [wRes, pRes, tRes, rRes] = await Promise.all([
|
||||
const [wRes, pRes, rRes] = await Promise.all([
|
||||
fetch(`${API}/workers`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/projects`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/tasks`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/projects/active/list`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/workreports?start=${selectedDate}&end=${selectedDate}`, { headers: getAuthHeaders() })
|
||||
]);
|
||||
|
||||
if (![wRes, pRes, tRes, rRes].every(res => res.ok)) throw new Error('불러오기 실패');
|
||||
if (![wRes, pRes, rRes].every(res => res.ok)) throw new Error('불러오기 실패');
|
||||
|
||||
const [workers, projects, tasks, reports] = await Promise.all([
|
||||
wRes.json(), pRes.json(), tRes.json(), rRes.json()
|
||||
const [workers, projects, reports] = await Promise.all([
|
||||
wRes.json(), pRes.json(), rRes.json()
|
||||
]);
|
||||
|
||||
// 배열 체크
|
||||
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(tasks) || !Array.isArray(reports)) {
|
||||
if (!Array.isArray(workers) || !Array.isArray(projects) || !Array.isArray(reports)) {
|
||||
throw new Error('잘못된 데이터 형식');
|
||||
}
|
||||
|
||||
@@ -45,7 +44,7 @@ async function loadReports() {
|
||||
|
||||
const nameMap = Object.fromEntries(workers.map(w => [w.worker_id, w.worker_name]));
|
||||
const projMap = Object.fromEntries(projects.map(p => [p.project_id, p.project_name]));
|
||||
const taskMap = Object.fromEntries(tasks.map(t => [t.task_id, `${t.category}:${t.subcategory}`]));
|
||||
// const taskMap = Object.fromEntries(tasks.map(t => [t.task_id, `${t.category}:${t.subcategory}`])); // tasks 테이블 삭제됨
|
||||
|
||||
reportBody.innerHTML = '';
|
||||
reports.forEach((r, i) => {
|
||||
@@ -57,10 +56,9 @@ async function loadReports() {
|
||||
${projects.map(p =>
|
||||
`<option value="${p.project_id}" ${p.project_id === r.project_id ? 'selected' : ''}>${p.project_name}</option>`
|
||||
).join('')}</select></td>
|
||||
<td><select data-id="task">
|
||||
${tasks.map(t =>
|
||||
`<option value="${t.task_id}" ${t.task_id === r.task_id ? 'selected' : ''}>${t.category}:${t.subcategory}</option>`
|
||||
).join('')}</select></td>
|
||||
<td><select data-id="task" disabled>
|
||||
<option>작업 유형 (삭제됨)</option>
|
||||
</select></td>
|
||||
<td><input type="number" min="0" step="0.5" value="${r.overtime_hours || ''}" data-id="overtime"></td>
|
||||
<td><select data-id="work_details">
|
||||
${['근무', '연차', '유급', '반차', '반반차', '조퇴', '휴무'].map(opt =>
|
||||
|
||||
@@ -45,7 +45,7 @@ class WorkReportReviewManager {
|
||||
// 기본 데이터 로딩
|
||||
const [workersRes, projectsRes, workTypesRes, workStatusRes, errorTypesRes] = await Promise.all([
|
||||
fetch(`${API}/workers`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/projects`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/projects/active/list`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/daily-work-reports/work-types`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/daily-work-reports/work-status-types`, { headers: getAuthHeaders() }),
|
||||
fetch(`${API}/daily-work-reports/error-types`, { headers: getAuthHeaders() })
|
||||
|
||||
@@ -641,8 +641,8 @@ async function loadBasicData() {
|
||||
console.log('🔗 통합 API 설정을 사용한 기본 데이터 로딩...');
|
||||
|
||||
const promises = [
|
||||
// 프로젝트 로드
|
||||
apiCall(`${API}/projects`)
|
||||
// 활성 프로젝트 로드
|
||||
apiCall(`${API}/projects/active/list`)
|
||||
.then(data => Array.isArray(data) ? data : (data.projects || []))
|
||||
.catch(() => []),
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ async function loadExistingWork() {
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const response = await window.apiCall(`${window.API}/projects`);
|
||||
const response = await window.apiCall(`${window.API}/projects/active/list`);
|
||||
projects = Array.isArray(response) ? response : (response.data || []);
|
||||
} catch (error) {
|
||||
console.error('프로젝트 로드 오류:', error);
|
||||
@@ -407,10 +407,7 @@ async function saveNewWork() {
|
||||
created_by: currentUser?.user_id || 1
|
||||
};
|
||||
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(workData)
|
||||
});
|
||||
const response = await window.apiCall(`${window.API}/daily-work-reports`, 'POST', workData);
|
||||
|
||||
showMessage('작업이 성공적으로 저장되었습니다.', 'success');
|
||||
|
||||
|
||||
734
web-ui/js/worker-management.js
Normal file
734
web-ui/js/worker-management.js
Normal file
@@ -0,0 +1,734 @@
|
||||
// 작업자 관리 페이지 JavaScript
|
||||
|
||||
// 전역 변수
|
||||
let allWorkers = [];
|
||||
let filteredWorkers = [];
|
||||
let currentEditingWorker = null;
|
||||
let currentStatusFilter = 'all'; // 'all', 'active', 'inactive'
|
||||
|
||||
// 페이지 초기화
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('👥 작업자 관리 페이지 초기화 시작');
|
||||
|
||||
initializePage();
|
||||
loadWorkers();
|
||||
});
|
||||
|
||||
// 페이지 초기화
|
||||
function initializePage() {
|
||||
// 시간 업데이트 시작
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
|
||||
// 사용자 정보 업데이트
|
||||
updateUserInfo();
|
||||
|
||||
// 프로필 메뉴 토글
|
||||
setupProfileMenu();
|
||||
|
||||
// 로그아웃 버튼
|
||||
setupLogoutButton();
|
||||
|
||||
// 검색 입력 이벤트
|
||||
setupSearchInput();
|
||||
}
|
||||
|
||||
// 현재 시간 업데이트
|
||||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 검색 입력 설정
|
||||
function setupSearchInput() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
searchWorkers();
|
||||
});
|
||||
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
searchWorkers();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 목록 로드
|
||||
async function loadWorkers() {
|
||||
try {
|
||||
console.log('📊 작업자 목록 로딩 시작');
|
||||
|
||||
const response = await apiCall('/workers?limit=1000', 'GET'); // 모든 작업자 조회
|
||||
|
||||
console.log('📊 API 응답 구조:', response);
|
||||
|
||||
// API 응답이 { success: true, data: [...] } 형태인 경우 처리
|
||||
let workerData = [];
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
workerData = response.data;
|
||||
} else if (Array.isArray(response)) {
|
||||
workerData = response;
|
||||
} else {
|
||||
console.warn('작업자 데이터가 배열이 아닙니다:', response);
|
||||
workerData = [];
|
||||
}
|
||||
|
||||
allWorkers = workerData;
|
||||
|
||||
console.log(`✅ 작업자 ${allWorkers.length}명 로드 완료`);
|
||||
|
||||
// 초기 필터 적용
|
||||
applyAllFilters();
|
||||
updateStatCardActiveState();
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업자 로딩 오류:', error);
|
||||
showToast('작업자 목록을 불러오는데 실패했습니다.', 'error');
|
||||
allWorkers = [];
|
||||
filteredWorkers = [];
|
||||
renderWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 목록 렌더링
|
||||
function renderWorkers() {
|
||||
const workersGrid = document.getElementById('workersGrid');
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
|
||||
if (!workersGrid || !emptyState) return;
|
||||
|
||||
if (filteredWorkers.length === 0) {
|
||||
workersGrid.style.display = 'none';
|
||||
emptyState.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
workersGrid.style.display = 'grid';
|
||||
emptyState.style.display = 'none';
|
||||
|
||||
const workersHtml = filteredWorkers.map(worker => {
|
||||
// 작업자 상태 및 직책 아이콘
|
||||
const jobTypeMap = {
|
||||
'worker': { icon: '👷', text: '작업자', color: '#6b7280' },
|
||||
'leader': { icon: '👨💼', text: '그룹장', color: '#3b82f6' },
|
||||
'admin': { icon: '👨💻', text: '관리자', color: '#8b5cf6' }
|
||||
};
|
||||
|
||||
const jobType = jobTypeMap[worker.job_type] || jobTypeMap['worker'];
|
||||
const isInactive = worker.status === 'inactive' || worker.is_active === 0 || worker.is_active === false;
|
||||
|
||||
console.log('🎨 카드 렌더링:', {
|
||||
worker_id: worker.worker_id,
|
||||
worker_name: worker.worker_name,
|
||||
status: worker.status,
|
||||
is_active: worker.is_active,
|
||||
isInactive: isInactive
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="project-card worker-card ${isInactive ? 'inactive' : ''}" onclick="editWorker(${worker.worker_id})">
|
||||
${isInactive ? '<div class="inactive-overlay"><span class="inactive-badge">🚫 비활성화됨</span></div>' : ''}
|
||||
<div class="project-header">
|
||||
<div class="project-info">
|
||||
<div class="worker-avatar">
|
||||
<span class="avatar-initial">${worker.worker_name.charAt(0)}</span>
|
||||
</div>
|
||||
<h3 class="project-name">
|
||||
${worker.worker_name}
|
||||
${isInactive ? '<span class="inactive-label">(비활성)</span>' : ''}
|
||||
</h3>
|
||||
<div class="project-meta">
|
||||
<span style="color: ${jobType.color}; font-weight: 500;">${jobType.icon} ${jobType.text}</span>
|
||||
${worker.phone_number ? `<span>📞 ${worker.phone_number}</span>` : ''}
|
||||
${worker.email ? `<span>📧 ${worker.email}</span>` : ''}
|
||||
${worker.department ? `<span>🏢 ${worker.department}</span>` : ''}
|
||||
${worker.hire_date ? `<span>📅 입사: ${formatDate(worker.hire_date)}</span>` : ''}
|
||||
${isInactive ? '<span class="inactive-notice">⚠️ 작업보고서에서 숨김</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-actions">
|
||||
<button class="btn-toggle ${isInactive ? 'btn-activate' : 'btn-deactivate'}"
|
||||
onclick="event.stopPropagation(); toggleWorkerStatus(${worker.worker_id})"
|
||||
title="${isInactive ? '활성화' : '비활성화'}">
|
||||
${isInactive ? '✅' : '❌'}
|
||||
</button>
|
||||
<button class="btn-edit" onclick="event.stopPropagation(); editWorker(${worker.worker_id})" title="수정">
|
||||
✏️
|
||||
</button>
|
||||
<button class="btn-delete" onclick="event.stopPropagation(); confirmDeleteWorker(${worker.worker_id})" title="삭제">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
workersGrid.innerHTML = workersHtml;
|
||||
}
|
||||
|
||||
// 작업자 통계 업데이트
|
||||
function updateWorkerStats() {
|
||||
const activeWorkers = filteredWorkers.filter(w => w.status !== 'inactive' && w.is_active !== 0 && w.is_active !== false);
|
||||
const inactiveWorkers = filteredWorkers.filter(w => w.status === 'inactive' || w.is_active === 0 || w.is_active === false);
|
||||
|
||||
const activeWorkersElement = document.getElementById('activeWorkers');
|
||||
const inactiveWorkersElement = document.getElementById('inactiveWorkers');
|
||||
const totalWorkersElement = document.getElementById('totalWorkers');
|
||||
|
||||
if (activeWorkersElement) {
|
||||
activeWorkersElement.textContent = activeWorkers.length;
|
||||
}
|
||||
|
||||
if (inactiveWorkersElement) {
|
||||
inactiveWorkersElement.textContent = inactiveWorkers.length;
|
||||
}
|
||||
|
||||
if (totalWorkersElement) {
|
||||
totalWorkersElement.textContent = filteredWorkers.length;
|
||||
}
|
||||
|
||||
console.log('📊 작업자 통계:', {
|
||||
전체: filteredWorkers.length,
|
||||
활성: activeWorkers.length,
|
||||
비활성: inactiveWorkers.length
|
||||
});
|
||||
}
|
||||
|
||||
// 날짜 포맷팅
|
||||
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 filterByStatus(status) {
|
||||
currentStatusFilter = status;
|
||||
|
||||
// 통계 카드 활성화 상태 업데이트
|
||||
updateStatCardActiveState();
|
||||
|
||||
// 필터링 적용
|
||||
applyAllFilters();
|
||||
|
||||
console.log(`🔍 상태 필터 적용: ${status}`);
|
||||
}
|
||||
|
||||
// 통계 카드 활성화 상태 업데이트
|
||||
function updateStatCardActiveState() {
|
||||
// 모든 통계 카드에서 active 클래스 제거
|
||||
document.querySelectorAll('.stat-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
});
|
||||
|
||||
// 현재 선택된 필터에 active 클래스 추가
|
||||
const activeCard = document.querySelector(`.${currentStatusFilter === 'active' ? 'active-stat' :
|
||||
currentStatusFilter === 'inactive' ? 'inactive-stat' : 'total-stat'}`);
|
||||
if (activeCard) {
|
||||
activeCard.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 필터 적용 (검색 + 상태 + 직책)
|
||||
function applyAllFilters() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const jobTypeFilter = document.getElementById('jobTypeFilter');
|
||||
const statusFilter = document.getElementById('statusFilter');
|
||||
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
||||
const jobTypeValue = jobTypeFilter ? jobTypeFilter.value : '';
|
||||
const statusValue = statusFilter ? statusFilter.value : '';
|
||||
|
||||
// 1단계: 상태 필터링 (통계 카드 클릭)
|
||||
let statusFiltered = [...allWorkers];
|
||||
if (currentStatusFilter === 'active') {
|
||||
statusFiltered = allWorkers.filter(w => w.status !== 'inactive' && w.is_active !== 0 && w.is_active !== false);
|
||||
} else if (currentStatusFilter === 'inactive') {
|
||||
statusFiltered = allWorkers.filter(w => w.status === 'inactive' || w.is_active === 0 || w.is_active === false);
|
||||
}
|
||||
|
||||
// 2단계: 드롭다운 상태 필터링
|
||||
if (statusValue) {
|
||||
if (statusValue === 'active') {
|
||||
statusFiltered = statusFiltered.filter(w => w.status !== 'inactive' && w.is_active !== 0 && w.is_active !== false);
|
||||
} else if (statusValue === 'inactive') {
|
||||
statusFiltered = statusFiltered.filter(w => w.status === 'inactive' || w.is_active === 0 || w.is_active === false);
|
||||
}
|
||||
}
|
||||
|
||||
// 3단계: 직책 필터링
|
||||
let jobTypeFiltered = statusFiltered;
|
||||
if (jobTypeValue) {
|
||||
jobTypeFiltered = statusFiltered.filter(w => w.job_type === jobTypeValue);
|
||||
}
|
||||
|
||||
// 4단계: 검색 필터링
|
||||
if (!searchTerm) {
|
||||
filteredWorkers = jobTypeFiltered;
|
||||
} else {
|
||||
filteredWorkers = jobTypeFiltered.filter(worker =>
|
||||
worker.worker_name.toLowerCase().includes(searchTerm) ||
|
||||
(worker.phone_number && worker.phone_number.toLowerCase().includes(searchTerm)) ||
|
||||
(worker.email && worker.email.toLowerCase().includes(searchTerm)) ||
|
||||
(worker.department && worker.department.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
}
|
||||
|
||||
renderWorkers();
|
||||
updateWorkerStats();
|
||||
}
|
||||
|
||||
// 작업자 검색
|
||||
function searchWorkers() {
|
||||
applyAllFilters();
|
||||
}
|
||||
|
||||
// 작업자 필터링
|
||||
function filterWorkers() {
|
||||
applyAllFilters();
|
||||
}
|
||||
|
||||
// 작업자 정렬
|
||||
function sortWorkers() {
|
||||
const sortBy = document.getElementById('sortBy');
|
||||
const sortField = sortBy ? sortBy.value : 'created_at';
|
||||
|
||||
filteredWorkers.sort((a, b) => {
|
||||
switch (sortField) {
|
||||
case 'worker_name':
|
||||
return a.worker_name.localeCompare(b.worker_name);
|
||||
case 'job_type':
|
||||
const jobOrder = { 'admin': 0, 'leader': 1, 'worker': 2 };
|
||||
return (jobOrder[a.job_type] || 3) - (jobOrder[b.job_type] || 3);
|
||||
case 'created_at':
|
||||
default:
|
||||
return new Date(b.created_at || 0) - new Date(a.created_at || 0);
|
||||
}
|
||||
});
|
||||
|
||||
renderWorkers();
|
||||
}
|
||||
|
||||
// 작업자 목록 새로고침
|
||||
async function refreshWorkerList() {
|
||||
const refreshBtn = document.querySelector('.btn-secondary');
|
||||
if (refreshBtn) {
|
||||
const originalText = refreshBtn.innerHTML;
|
||||
refreshBtn.innerHTML = '<span class="btn-icon">⏳</span>새로고침 중...';
|
||||
refreshBtn.disabled = true;
|
||||
|
||||
await loadWorkers();
|
||||
|
||||
refreshBtn.innerHTML = originalText;
|
||||
refreshBtn.disabled = false;
|
||||
} else {
|
||||
await loadWorkers();
|
||||
}
|
||||
|
||||
showToast('작업자 목록이 새로고침되었습니다.', 'success');
|
||||
}
|
||||
|
||||
// 작업자 모달 열기
|
||||
function openWorkerModal(worker = null) {
|
||||
const modal = document.getElementById('workerModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const deleteBtn = document.getElementById('deleteWorkerBtn');
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
currentEditingWorker = worker;
|
||||
|
||||
if (worker) {
|
||||
// 수정 모드
|
||||
modalTitle.textContent = '작업자 정보 수정';
|
||||
deleteBtn.style.display = 'inline-flex';
|
||||
|
||||
// 폼에 데이터 채우기
|
||||
document.getElementById('workerId').value = worker.worker_id;
|
||||
document.getElementById('workerName').value = worker.worker_name || '';
|
||||
document.getElementById('jobType').value = worker.job_type || 'worker';
|
||||
document.getElementById('phoneNumber').value = worker.phone_number || '';
|
||||
document.getElementById('email').value = worker.email || '';
|
||||
document.getElementById('hireDate').value = worker.hire_date || '';
|
||||
document.getElementById('department').value = worker.department || '';
|
||||
document.getElementById('notes').value = worker.notes || '';
|
||||
|
||||
// is_active 값 처리 (DB에서 0/1로 오는 경우 대비)
|
||||
const isActiveValue = worker.status !== 'inactive' && worker.is_active !== 0 && worker.is_active !== false;
|
||||
document.getElementById('isActive').checked = isActiveValue;
|
||||
|
||||
console.log('🔧 작업자 로드:', {
|
||||
worker_id: worker.worker_id,
|
||||
worker_name: worker.worker_name,
|
||||
status: worker.status,
|
||||
is_active_raw: worker.is_active,
|
||||
is_active_processed: isActiveValue
|
||||
});
|
||||
} else {
|
||||
// 신규 등록 모드
|
||||
modalTitle.textContent = '새 작업자 등록';
|
||||
deleteBtn.style.display = 'none';
|
||||
|
||||
// 폼 초기화
|
||||
document.getElementById('workerForm').reset();
|
||||
document.getElementById('workerId').value = '';
|
||||
document.getElementById('isActive').checked = true;
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
// 첫 번째 입력 필드에 포커스
|
||||
setTimeout(() => {
|
||||
const firstInput = document.getElementById('workerName');
|
||||
if (firstInput) firstInput.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 작업자 모달 닫기
|
||||
function closeWorkerModal() {
|
||||
const modal = document.getElementById('workerModal');
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
currentEditingWorker = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 편집
|
||||
function editWorker(workerId) {
|
||||
const worker = allWorkers.find(w => w.worker_id === workerId);
|
||||
if (worker) {
|
||||
openWorkerModal(worker);
|
||||
} else {
|
||||
showToast('작업자를 찾을 수 없습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 저장
|
||||
async function saveWorker() {
|
||||
try {
|
||||
const form = document.getElementById('workerForm');
|
||||
|
||||
const workerData = {
|
||||
worker_name: document.getElementById('workerName').value.trim(),
|
||||
job_type: document.getElementById('jobType').value || 'worker',
|
||||
phone_number: document.getElementById('phoneNumber').value.trim() || null,
|
||||
email: document.getElementById('email').value.trim() || null,
|
||||
hire_date: document.getElementById('hireDate').value || null,
|
||||
department: document.getElementById('department').value.trim() || null,
|
||||
notes: document.getElementById('notes').value.trim() || null,
|
||||
status: document.getElementById('isActive').checked ? 'active' : 'inactive'
|
||||
};
|
||||
|
||||
console.log('💾 저장할 작업자 데이터:', workerData);
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!workerData.worker_name) {
|
||||
showToast('작업자명은 필수 입력 항목입니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const workerId = document.getElementById('workerId').value;
|
||||
let response;
|
||||
|
||||
if (workerId) {
|
||||
// 수정
|
||||
response = await apiCall(`/workers/${workerId}`, 'PUT', workerData);
|
||||
} else {
|
||||
// 신규 등록
|
||||
response = await apiCall('/workers', 'POST', workerData);
|
||||
}
|
||||
|
||||
if (response && (response.success || response.worker_id)) {
|
||||
const action = workerId ? '수정' : '등록';
|
||||
showToast(`작업자가 성공적으로 ${action}되었습니다.`, 'success');
|
||||
|
||||
closeWorkerModal();
|
||||
await loadWorkers();
|
||||
} else {
|
||||
throw new Error(response?.message || '저장에 실패했습니다.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('작업자 저장 오류:', error);
|
||||
showToast(error.message || '작업자 저장 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 상태 토글 (활성화/비활성화)
|
||||
async function toggleWorkerStatus(workerId) {
|
||||
const worker = allWorkers.find(w => w.worker_id === workerId);
|
||||
if (!worker) {
|
||||
showToast('작업자를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const isCurrentlyInactive = worker.status === 'inactive' || worker.is_active === 0 || worker.is_active === false;
|
||||
const newStatus = isCurrentlyInactive ? 'active' : 'inactive';
|
||||
const actionText = isCurrentlyInactive ? '활성화' : '비활성화';
|
||||
|
||||
if (!confirm(`"${worker.worker_name}" 작업자를 ${actionText}하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🔄 작업자 상태 변경: ${worker.worker_name} → ${newStatus}`);
|
||||
|
||||
try {
|
||||
const updateData = {
|
||||
...worker,
|
||||
status: newStatus,
|
||||
is_active: newStatus === 'active' ? 1 : 0
|
||||
};
|
||||
|
||||
const response = await window.apiCall(`${window.API}/workers/${workerId}`, 'PUT', updateData);
|
||||
|
||||
if (response) {
|
||||
// 로컬 데이터 업데이트
|
||||
const workerIndex = allWorkers.findIndex(w => w.worker_id === workerId);
|
||||
if (workerIndex !== -1) {
|
||||
allWorkers[workerIndex].status = newStatus;
|
||||
allWorkers[workerIndex].is_active = newStatus === 'active' ? 1 : 0;
|
||||
}
|
||||
|
||||
// UI 새로고침
|
||||
applyAllFilters();
|
||||
updateWorkerStats();
|
||||
|
||||
showToast(`${worker.worker_name} 작업자가 ${actionText}되었습니다.`, 'success');
|
||||
console.log(`✅ 작업자 상태 변경 완료: ${worker.worker_name} → ${newStatus}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('작업자 상태 변경 오류:', error);
|
||||
showToast(`작업자 상태 변경에 실패했습니다: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 삭제 확인
|
||||
function confirmDeleteWorker(workerId) {
|
||||
console.log('🔍 삭제 요청된 작업자 ID:', workerId);
|
||||
|
||||
const worker = allWorkers.find(w => w.worker_id === workerId);
|
||||
if (!worker) {
|
||||
console.error('❌ 작업자를 찾을 수 없음:', workerId);
|
||||
showToast('작업자를 찾을 수 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('👤 삭제 대상 작업자:', {
|
||||
worker_id: worker.worker_id,
|
||||
worker_name: worker.worker_name,
|
||||
job_type: worker.job_type
|
||||
});
|
||||
|
||||
// 더 명확한 확인 메시지
|
||||
const confirmMessage = `⚠️ 작업자 삭제 확인 ⚠️
|
||||
|
||||
삭제할 작업자: ${worker.worker_name} (ID: ${worker.worker_id})
|
||||
직책: ${worker.job_type || '미지정'}
|
||||
|
||||
정말로 이 작업자를 삭제하시겠습니까?
|
||||
|
||||
⚠️ 주의: 삭제된 작업자와 관련된 모든 데이터가 함께 삭제됩니다.
|
||||
- 작업 보고서
|
||||
- 이슈 보고서
|
||||
- 월별 통계
|
||||
- 그룹 소속 정보
|
||||
|
||||
이 작업은 되돌릴 수 없습니다!`;
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
console.log('✅ 사용자가 삭제를 확인함');
|
||||
deleteWorkerById(workerId);
|
||||
} else {
|
||||
console.log('❌ 사용자가 삭제를 취소함');
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 삭제 (수정 모드에서)
|
||||
function deleteWorker() {
|
||||
if (currentEditingWorker) {
|
||||
confirmDeleteWorker(currentEditingWorker.worker_id);
|
||||
}
|
||||
}
|
||||
|
||||
// 작업자 삭제 실행
|
||||
async function deleteWorkerById(workerId) {
|
||||
console.log('🗑️ 작업자 삭제 실행 시작:', workerId);
|
||||
|
||||
try {
|
||||
const worker = allWorkers.find(w => w.worker_id === workerId);
|
||||
console.log('🔍 삭제 실행 전 작업자 정보:', worker);
|
||||
|
||||
const response = await window.apiCall(`${window.API}/workers/${workerId}`, 'DELETE');
|
||||
console.log('📡 삭제 API 응답:', response);
|
||||
|
||||
if (response && (response.success || response.message)) {
|
||||
console.log('✅ 작업자 삭제 성공');
|
||||
showToast(`작업자 "${worker?.worker_name || workerId}"가 성공적으로 삭제되었습니다.`, 'success');
|
||||
|
||||
closeWorkerModal();
|
||||
await loadWorkers();
|
||||
} else {
|
||||
throw new Error(response?.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 작업자 삭제 오류:', error);
|
||||
showToast(error.message || '작업자 삭제 중 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 토스트 메시지 표시
|
||||
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.openWorkerModal = openWorkerModal;
|
||||
window.closeWorkerModal = closeWorkerModal;
|
||||
window.editWorker = editWorker;
|
||||
window.saveWorker = saveWorker;
|
||||
window.deleteWorker = deleteWorker;
|
||||
window.confirmDeleteWorker = confirmDeleteWorker;
|
||||
window.searchWorkers = searchWorkers;
|
||||
window.filterWorkers = filterWorkers;
|
||||
window.sortWorkers = sortWorkers;
|
||||
window.refreshWorkerList = refreshWorkerList;
|
||||
window.filterByStatus = filterByStatus;
|
||||
Reference in New Issue
Block a user