Files
TK-FB-Project/web-ui/js/worker-individual-report.js
Hyungi Ahn 746e09420b feat: 캘린더 기반 작업 현황 확인 시스템 구현
- 월별 캘린더 UI로 작업 현황을 한눈에 확인 가능
- 미입력(빨강), 부분입력(주황), 확인필요(보라), 이상없음(초록) 상태 표시
- 범례 아이콘(●)을 사용한 직관적인 상태 표시
- 날짜 클릭 시 해당일 작업자별 상세 현황 모달
- 작업자 클릭 시 개별 작업 입력/수정 모달
- 휴가 처리 기능 (연차, 반차, 반반차, 조퇴)
- 월별 집계 데이터 최적화로 API 호출 최소화

백엔드:
- monthly_worker_status, monthly_summary 테이블 추가
- 자동 집계 stored procedure 및 trigger 구현
- 확인필요(12시간 초과) 상태 감지 로직
- 출석 관리 시스템 확장

프론트엔드:
- 캘린더 그리드 UI 구현
- 상태별 색상 및 아이콘 표시
- 모달 기반 상세 정보 표시
- 반응형 디자인 적용
2025-11-04 10:12:07 +09:00

513 lines
16 KiB
JavaScript

// worker-individual-report.js - 작업자별 개별 보고서 관리
// 전역 변수
let currentWorkerId = null;
let currentWorkerName = '';
let selectedDate = '';
let currentUser = null;
let workTypes = [];
let workStatusTypes = [];
let errorTypes = [];
let projects = [];
let existingWork = [];
// URL 파라미터에서 정보 추출
function getUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
return {
worker_id: urlParams.get('worker_id'),
worker_name: decodeURIComponent(urlParams.get('worker_name') || ''),
date: urlParams.get('date') || new Date().toISOString().split('T')[0]
};
}
// 현재 로그인한 사용자 정보 가져오기
function getCurrentUser() {
try {
const token = localStorage.getItem('token');
if (!token) return null;
const payloadBase64 = token.split('.')[1];
if (payloadBase64) {
const payload = JSON.parse(atob(payloadBase64));
return payload;
}
} catch (error) {
console.log('토큰에서 사용자 정보 추출 실패:', error);
}
try {
const userInfo = localStorage.getItem('user');
if (userInfo) {
return JSON.parse(userInfo);
}
} catch (error) {
console.log('localStorage에서 사용자 정보 파싱 실패:', error);
}
return null;
}
// 메시지 표시 함수
function showMessage(msg, type = 'info') {
const container = document.getElementById('message-container');
if (container) {
container.innerHTML = `<div class="message ${type}">${msg}</div>`;
setTimeout(() => {
container.innerHTML = '';
}, 5000);
}
}
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
// API 함수가 로드될 때까지 기다림
let retryCount = 0;
const maxRetries = 50;
while (!window.apiCall && retryCount < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 100));
retryCount++;
}
if (!window.apiCall) {
console.error('❌ API 함수를 로드할 수 없습니다.');
showMessage('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
return;
}
try {
await initializePage();
} catch (error) {
console.error('페이지 초기화 오류:', error);
showMessage('페이지를 불러오는 중 오류가 발생했습니다.', 'error');
}
});
async function initializePage() {
console.log('🚀 개별 작업 보고서 페이지 초기화 시작');
// URL 파라미터 추출
const params = getUrlParams();
currentWorkerId = parseInt(params.worker_id);
currentWorkerName = params.worker_name;
selectedDate = params.date;
// 사용자 정보 설정
currentUser = getCurrentUser();
if (!currentWorkerId || !currentWorkerName) {
showMessage('잘못된 접근입니다. 작업자 정보가 없습니다.', 'error');
setTimeout(() => {
window.history.back();
}, 2000);
return;
}
// 페이지 제목 설정
updatePageHeader();
// 이벤트 리스너 설정
setupEventListeners();
// 초기 데이터 로드
await loadInitialData();
console.log('✅ 개별 작업 보고서 페이지 초기화 완료');
}
function updatePageHeader() {
document.getElementById('pageTitle').textContent = `👤 ${currentWorkerName} 작업 보고서`;
document.getElementById('pageSubtitle').textContent = `${selectedDate} 작업 내용을 관리합니다.`;
// 작업자 정보 카드 업데이트
document.getElementById('workerInitial').textContent = currentWorkerName.charAt(0);
document.getElementById('workerName').textContent = currentWorkerName;
document.getElementById('selectedDate').textContent = selectedDate;
}
function setupEventListeners() {
// 새 작업 추가 버튼
document.getElementById('addNewWorkBtn').addEventListener('click', showNewWorkForm);
document.getElementById('cancelNewWorkBtn').addEventListener('click', hideNewWorkForm);
document.getElementById('saveNewWorkBtn').addEventListener('click', saveNewWork);
// 업무 상태 변경 시 에러 유형 섹션 토글
document.getElementById('newWorkStatusSelect').addEventListener('change', toggleErrorTypeSection);
// 빠른 시간 버튼
document.querySelectorAll('.quick-time-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.getElementById('newWorkHours').value = e.target.dataset.hours;
});
});
// 휴가 처리 버튼들
document.querySelectorAll('.vacation-process-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const vacationType = e.target.dataset.type;
handleVacationProcess(vacationType);
});
});
}
async function loadInitialData() {
try {
showMessage('데이터를 불러오는 중...', 'loading');
// 병렬로 데이터 로드
await Promise.all([
loadWorkerInfo(),
loadExistingWork(),
loadProjects(),
loadWorkTypes(),
loadWorkStatusTypes(),
loadErrorTypes()
]);
// UI 업데이트
updateWorkerSummary();
renderExistingWork();
populateDropdowns();
showMessage('데이터 로드 완료', 'success');
} catch (error) {
console.error('초기 데이터 로드 실패:', error);
showMessage('데이터 로드 중 오류가 발생했습니다: ' + error.message, 'error');
}
}
async function loadWorkerInfo() {
try {
const response = await window.apiCall(`${window.API}/workers/${currentWorkerId}`);
const worker = response.data || response;
document.getElementById('workerJob').textContent = worker.job_type || '작업자';
} catch (error) {
console.error('작업자 정보 로드 오류:', error);
}
}
async function loadExistingWork() {
try {
const response = await window.apiCall(`${window.API}/daily-work-reports?date=${selectedDate}&worker_id=${currentWorkerId}`);
existingWork = Array.isArray(response) ? response : (response.data || []);
console.log(`✅ 기존 작업 ${existingWork.length}건 로드 완료`);
} catch (error) {
console.error('기존 작업 로드 오류:', error);
existingWork = [];
}
}
async function loadProjects() {
try {
const response = await window.apiCall(`${window.API}/projects`);
projects = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('프로젝트 로드 오류:', error);
projects = [];
}
}
async function loadWorkTypes() {
try {
const response = await window.apiCall(`${window.API}/daily-work-reports/work-types`);
workTypes = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('작업 유형 로드 오류:', error);
workTypes = [];
}
}
async function loadWorkStatusTypes() {
try {
const response = await window.apiCall(`${window.API}/daily-work-reports/work-status-types`);
workStatusTypes = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('작업 상태 유형 로드 오류:', error);
workStatusTypes = [];
}
}
async function loadErrorTypes() {
try {
const response = await window.apiCall(`${window.API}/daily-work-reports/error-types`);
errorTypes = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('에러 유형 로드 오류:', error);
errorTypes = [];
}
}
function updateWorkerSummary() {
const totalHours = existingWork.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
const workCount = existingWork.length;
document.getElementById('totalHours').textContent = `${totalHours.toFixed(1)}h`;
document.getElementById('workCount').textContent = `${workCount}`;
// 12시간 초과 경고
if (totalHours > 12) {
document.getElementById('totalHours').classList.add('warning');
showMessage(`⚠️ 총 작업시간이 ${totalHours.toFixed(1)}시간으로 12시간을 초과했습니다.`, 'warning');
}
}
function renderExistingWork() {
const container = document.getElementById('existingWorkList');
if (existingWork.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-icon">📭</div>
<h3>등록된 작업이 없습니다</h3>
<p>${selectedDate}${currentWorkerName}님의 작업이 등록되지 않았습니다.</p>
</div>
`;
return;
}
container.innerHTML = existingWork.map(work => `
<div class="existing-work-item" data-work-id="${work.id}">
<div class="work-item-header">
<div class="work-item-info">
<h4>${work.project_name || '미지정 프로젝트'}</h4>
<p>${work.work_type_name || '미지정 작업'}</p>
</div>
<div class="work-item-status">
<span class="status-badge ${work.work_status_id === 2 ? 'error' : 'normal'}">
${work.work_status_name || '정상'}
</span>
<span class="work-hours">${work.work_hours}h</span>
</div>
</div>
${work.work_status_id === 2 && work.error_type_name ? `
<div class="work-item-error">
<span class="error-label">오류:</span>
<span class="error-type">${work.error_type_name}</span>
</div>
` : ''}
<div class="work-item-actions">
<button class="btn btn-sm btn-primary" onclick="editWork(${work.id})">
✏️ 수정
</button>
<button class="btn btn-sm btn-danger" onclick="deleteWork(${work.id})">
🗑️ 삭제
</button>
</div>
</div>
`).join('');
}
function populateDropdowns() {
// 프로젝트 드롭다운
const projectSelect = document.getElementById('newProjectSelect');
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);
});
// 작업 유형 드롭다운
const workTypeSelect = document.getElementById('newWorkTypeSelect');
workTypeSelect.innerHTML = '<option value="">작업 유형을 선택하세요</option>';
workTypes.forEach(type => {
const option = document.createElement('option');
option.value = type.id;
option.textContent = type.name;
workTypeSelect.appendChild(option);
});
// 작업 상태 드롭다운
const workStatusSelect = document.getElementById('newWorkStatusSelect');
workStatusSelect.innerHTML = '<option value="">업무 상태를 선택하세요</option>';
workStatusTypes.forEach(status => {
const option = document.createElement('option');
option.value = status.id;
option.textContent = status.name;
workStatusSelect.appendChild(option);
});
// 에러 유형 드롭다운
const errorTypeSelect = document.getElementById('newErrorTypeSelect');
errorTypeSelect.innerHTML = '<option value="">에러 유형을 선택하세요</option>';
errorTypes.forEach(error => {
const option = document.createElement('option');
option.value = error.id;
option.textContent = error.name;
errorTypeSelect.appendChild(option);
});
}
function showNewWorkForm() {
document.getElementById('newWorkSection').style.display = 'block';
document.getElementById('addNewWorkBtn').style.display = 'none';
}
function hideNewWorkForm() {
document.getElementById('newWorkSection').style.display = 'none';
document.getElementById('addNewWorkBtn').style.display = 'block';
resetNewWorkForm();
}
function resetNewWorkForm() {
document.getElementById('newProjectSelect').value = '';
document.getElementById('newWorkTypeSelect').value = '';
document.getElementById('newWorkStatusSelect').value = '';
document.getElementById('newErrorTypeSelect').value = '';
document.getElementById('newWorkHours').value = '1.00';
document.getElementById('newErrorTypeSection').classList.remove('visible');
}
function toggleErrorTypeSection() {
const workStatusSelect = document.getElementById('newWorkStatusSelect');
const errorSection = document.getElementById('newErrorTypeSection');
const errorTypeSelect = document.getElementById('newErrorTypeSelect');
if (workStatusSelect.value === '2') { // 에러 상태
errorSection.classList.add('visible');
errorTypeSelect.setAttribute('required', 'true');
} else {
errorSection.classList.remove('visible');
errorTypeSelect.removeAttribute('required');
errorTypeSelect.value = '';
}
}
async function saveNewWork() {
try {
const projectId = document.getElementById('newProjectSelect').value;
const workTypeId = document.getElementById('newWorkTypeSelect').value;
const workStatusId = document.getElementById('newWorkStatusSelect').value;
const errorTypeId = document.getElementById('newErrorTypeSelect').value;
const workHours = document.getElementById('newWorkHours').value;
// 유효성 검사
if (!projectId || !workTypeId || !workStatusId || !workHours) {
showMessage('모든 필수 필드를 입력해주세요.', 'error');
return;
}
if (workStatusId === '2' && !errorTypeId) {
showMessage('에러 상태일 때는 에러 유형을 선택해야 합니다.', 'error');
return;
}
showMessage('작업을 저장하는 중...', 'loading');
const workData = {
report_date: selectedDate,
worker_id: currentWorkerId,
project_id: parseInt(projectId),
work_type_id: parseInt(workTypeId),
work_status_id: parseInt(workStatusId),
error_type_id: workStatusId === '2' ? parseInt(errorTypeId) : null,
work_hours: parseFloat(workHours),
created_by: currentUser?.user_id || 1
};
const response = await window.apiCall(`${window.API}/daily-work-reports`, {
method: 'POST',
body: JSON.stringify(workData)
});
showMessage('작업이 성공적으로 저장되었습니다.', 'success');
// 데이터 새로고침
await loadExistingWork();
updateWorkerSummary();
renderExistingWork();
hideNewWorkForm();
} catch (error) {
console.error('작업 저장 오류:', error);
showMessage(`작업 저장 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
async function editWork(workId) {
// TODO: 작업 수정 모달 또는 인라인 편집 구현
console.log(`작업 ${workId} 수정`);
showMessage('작업 수정 기능은 곧 구현될 예정입니다.', 'info');
}
async function deleteWork(workId) {
if (!confirm('이 작업을 삭제하시겠습니까?')) {
return;
}
try {
showMessage('작업을 삭제하는 중...', 'loading');
await window.apiCall(`${window.API}/daily-work-reports/${workId}`, {
method: 'DELETE'
});
showMessage('작업이 성공적으로 삭제되었습니다.', 'success');
// 데이터 새로고침
await loadExistingWork();
updateWorkerSummary();
renderExistingWork();
} catch (error) {
console.error('작업 삭제 오류:', error);
showMessage(`작업 삭제 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
async function handleVacationProcess(vacationType) {
const vacationNames = {
'full': '연차',
'half-half': '반반차',
'half': '반차'
};
const vacationHours = {
'full': 8,
'half-half': 6,
'half': 4
};
if (!confirm(`${vacationNames[vacationType]} 처리하시겠습니까?\n(${vacationHours[vacationType]}시간으로 자동 입력됩니다)`)) {
return;
}
try {
showMessage(`${vacationNames[vacationType]} 처리 중...`, 'loading');
// 휴가용 작업 보고서 생성
const vacationWork = {
report_date: selectedDate,
worker_id: currentWorkerId,
project_id: 1, // 기본 프로젝트 (휴가용)
work_type_id: 999, // 휴가 전용 작업 유형 (DB에 추가 필요)
work_status_id: 1, // 정상 상태
error_type_id: null,
work_hours: vacationHours[vacationType],
created_by: currentUser?.user_id || 1
};
const response = await window.apiCall(`${window.API}/daily-work-reports`, {
method: 'POST',
body: JSON.stringify(vacationWork)
});
showMessage(`${vacationNames[vacationType]} 처리가 완료되었습니다.`, 'success');
// 데이터 새로고침
await loadExistingWork();
updateWorkerSummary();
renderExistingWork();
} catch (error) {
console.error('휴가 처리 오류:', error);
showMessage(`휴가 처리 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
// 전역 함수로 등록
window.editWork = editWork;
window.deleteWork = deleteWork;