sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거, department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러, 4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
510 lines
16 KiB
JavaScript
510 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 {
|
|
user_id: urlParams.get('user_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('sso_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('sso_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.user_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(`/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(`/daily-work-reports?date=${selectedDate}&user_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(`/projects/active/list`);
|
|
projects = Array.isArray(response) ? response : (response.data || []);
|
|
} catch (error) {
|
|
console.error('프로젝트 로드 오류:', error);
|
|
projects = [];
|
|
}
|
|
}
|
|
|
|
async function loadWorkTypes() {
|
|
try {
|
|
const response = await window.apiCall(`/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(`/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(`/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,
|
|
user_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(`/daily-work-reports`, 'POST', 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(`/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,
|
|
user_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(`/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;
|