feat: 작업 분석 시스템 및 관리 기능 대폭 개선
✨ 새로운 기능: - 작업 분석 페이지 구현 (기간별, 프로젝트별, 작업자별, 오류별) - 개별 분석 실행 버튼으로 API 부하 최적화 - 연차/휴무 집계 방식 개선 (주말 제외, 작업내용 통합) - 프로젝트 관리 시스템 (활성화/비활성화) - 작업자 관리 시스템 (CRUD 기능) - 코드 관리 시스템 (작업유형, 작업상태, 오류유형) 🎨 UI/UX 개선: - 기간별 작업 현황을 테이블 형태로 변경 - 작업자별 rowspan 그룹화로 가독성 향상 - 연차/휴무 프로젝트 하단 배치 및 시각적 구분 - 기간 확정 시스템으로 사용자 경험 개선 - 반응형 디자인 적용 🔧 기술적 개선: - Rate Limiting 제거 (내부 시스템 최적화) - 주말 연차/휴무 자동 제외 로직 - 작업공수 계산 정확도 향상 - 데이터베이스 마이그레이션 추가 - API 엔드포인트 확장 및 최적화 🐛 버그 수정: - projectSelect 요소 참조 오류 해결 - 차트 높이 무한 증가 문제 해결 - 날짜 표시 형식 단순화 - 작업보고서 저장 validation 오류 수정
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user