Files
TK-FB-Project/web-ui/js/work-report-calendar.js
Hyungi Ahn 05843da1c4 refactor(db,frontend): Improve queries and modularize frontend
- Replaced SELECT* queries in 8 models with explicit columns.
- Began modularizing work-report-calendar.js by creating CalendarAPI.js, CalendarState.js, and CalendarView.js.
- Refactored manage-project.js to use global API helpers.
- Fixed API container crash by adding missing volume mounts to docker-compose.yml.
- Added new migration for missing columns in the projects table.
- Documented current DB schema and deployment notes.
2025-12-19 12:42:24 +09:00

1384 lines
49 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 작업 현황 캘린더 JavaScript
// 전역 변수 대신 CalendarState 사용
// let currentDate = new Date();
// let monthlyData = {}; // 월별 데이터 캐시
// let allWorkers = []; // 작업자 데이터는 allWorkers 변수 사용
// let currentModalDate = null;
// let currentEditingWork = null;
// let existingWorks = [];
// DOM 요소
const elements = {
monthYearTitle: null,
calendarDays: null,
prevMonthBtn: null,
nextMonthBtn: null,
todayBtn: null,
dailyWorkModal: null,
modalTitle: null,
modalTotalWorkers: null,
modalTotalHours: null,
modalTotalTasks: null,
modalErrorCount: null,
modalWorkersList: null,
statusFilter: null,
loadingSpinner: null
};
// 초기화
document.addEventListener('DOMContentLoaded', async function() {
console.log('🚀 작업 현황 캘린더 초기화 시작');
// DOM 요소 초기화
initializeElements();
// 이벤트 리스너 등록
setupEventListeners();
// 작업자 데이터 로드 (한 번만)
await loadWorkersData();
// 현재 월 캘린더 렌더링
await renderCalendar();
console.log('✅ 작업 현황 캘린더 초기화 완료');
});
// DOM 요소 초기화
function initializeElements() {
elements.monthYearTitle = document.getElementById('monthYearTitle');
elements.calendarDays = document.getElementById('calendarDays');
elements.prevMonthBtn = document.getElementById('prevMonthBtn');
elements.nextMonthBtn = document.getElementById('nextMonthBtn');
elements.todayBtn = document.getElementById('todayBtn');
elements.dailyWorkModal = document.getElementById('dailyWorkModal');
elements.modalTitle = document.getElementById('modalTitle');
elements.modalSummary = document.querySelector('.daily-summary'); // 요약 섹션
elements.modalTotalWorkers = document.getElementById('modalTotalWorkers');
elements.modalTotalHours = document.getElementById('modalTotalHours');
elements.modalTotalTasks = document.getElementById('modalTotalTasks');
elements.modalErrorCount = document.getElementById('modalErrorCount');
elements.modalWorkersList = document.getElementById('modalWorkersList');
elements.modalNoData = document.getElementById('modalNoData');
elements.statusFilter = document.getElementById('statusFilter');
elements.loadingSpinner = document.getElementById('loadingSpinner');
}
// 이벤트 리스너 설정
function setupEventListeners() {
elements.prevMonthBtn.addEventListener('click', () => {
CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() - 1);
renderCalendar();
});
elements.nextMonthBtn.addEventListener('click', () => {
CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() + 1);
renderCalendar();
});
elements.todayBtn.addEventListener('click', () => {
CalendarState.currentDate = new Date();
renderCalendar();
});
elements.statusFilter.addEventListener('change', filterWorkersList);
// 모달 외부 클릭 시 닫기
elements.dailyWorkModal.addEventListener('click', (e) => {
if (e.target === elements.dailyWorkModal) {
closeDailyWorkModal();
}
});
// ESC 키로 모달 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && elements.dailyWorkModal.style.display !== 'none') {
closeDailyWorkModal();
}
});
}
// 작업자 데이터 로드 (캐시)
async function loadWorkersData() {
if (CalendarState.allWorkers.length > 0) return CalendarState.allWorkers;
try {
console.log('👥 작업자 데이터 로딩 (from CalendarAPI)...');
// The new API function already filters for active workers
const activeWorkers = await CalendarAPI.getWorkers();
CalendarState.allWorkers = activeWorkers;
console.log(`✅ 작업자 ${CalendarState.allWorkers.length}명 로드 완료`);
return CalendarState.allWorkers;
} catch (error) {
console.error('작업자 데이터 로딩 오류:', error);
showToast(error.message, 'error');
return [];
}
}
// 월별 작업 데이터 로드 (집계 테이블 사용으로 최적화)
async function loadMonthlyWorkData(year, month) {
const monthKey = `${year}-${String(month + 1).padStart(2, '0')}`;
if (CalendarState.monthlyData[monthKey]) {
console.log(`📋 캐시된 ${monthKey} 데이터 사용`);
return CalendarState.monthlyData[monthKey];
}
try {
const data = await CalendarAPI.getMonthlyCalendarData(year, month);
CalendarState.monthlyData[monthKey] = data; // Cache the data
return data;
} catch (error) {
console.error(`${monthKey} 데이터 로딩 오류:`, error);
showToast(error.message, 'error');
return {}; // Return empty object on failure
}
}
// 일일 작업 현황 모달 열기
async function openDailyWorkModal(dateStr) {
console.log(`🗓️ 클릭된 날짜: ${dateStr}`);
CalendarState.currentModalDate = dateStr;
// 날짜 포맷팅
const date = new Date(dateStr + 'T00:00:00');
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
console.log(`📅 파싱된 날짜: ${year}${month}${day}`);
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
const dayName = dayNames[date.getDay()];
elements.modalTitle.textContent = `${year}${month}${day}일 (${dayName}) 작업 현황`;
try {
const response = await CalendarAPI.getDailyDetails(dateStr);
if (response.workers) { // New API structure
renderModalDataFromSummary(response.workers, response.summary);
} else { // Fallback structure
renderModalData(response);
}
// 모달 표시
elements.dailyWorkModal.style.display = 'flex';
document.body.style.overflow = 'hidden';
} catch (error) {
console.error('일일 작업 데이터 로딩 오류:', error);
showToast('해당 날짜의 작업 데이터를 불러오는데 실패했습니다.', 'error');
}
}
// 집계 데이터로 모달 렌더링 (최적화된 버전)
async function renderModalDataFromSummary(workers, summary) {
// 전체 작업자 목록 가져오기
const allWorkersList = await loadWorkersData();
// 작업한 작업자 ID 목록
const workedWorkerIds = new Set(workers.map(w => w.workerId));
// 미기입 작업자 추가 (대시보드와 동일한 상태 판단 로직 적용)
const missingWorkers = allWorkersList
.filter(worker => !workedWorkerIds.has(worker.worker_id))
.map(worker => {
return {
workerId: worker.worker_id,
workerName: worker.worker_name,
jobType: worker.job_type,
totalHours: 0,
actualWorkHours: 0,
vacationHours: 0,
totalWorkCount: 0,
regularWorkCount: 0,
errorWorkCount: 0,
status: 'incomplete',
hasVacation: false,
hasError: false,
hasIssues: true
};
});
// 전체 작업자 목록 (작업한 사람 + 미기입 사람)
const allModalWorkers = [...workers, ...missingWorkers];
// 요약 정보 업데이트 (전체 작업자 수 포함)
if (elements.modalTotalWorkers) {
elements.modalTotalWorkers.textContent = `${allModalWorkers.length}`;
}
if (elements.modalTotalHours) {
elements.modalTotalHours.textContent = `${summary.totalHours.toFixed(1)}h`;
}
if (elements.modalTotalTasks) {
elements.modalTotalTasks.textContent = `${summary.totalTasks}`;
}
if (elements.modalErrorCount) {
elements.modalErrorCount.textContent = `${summary.errorCount}`;
elements.modalErrorCount.className = summary.errorCount > 0 ? 'summary-value error' : 'summary-value';
}
// 작업자 리스트 렌더링
if (allModalWorkers.length === 0) {
elements.modalWorkersList.innerHTML = '<div class="empty-state">등록된 작업자가 없습니다.</div>';
return;
}
const workersHtml = allModalWorkers.map(worker => {
// 상태 텍스트 및 색상 결정 (에러가 있어도 작업시간 기준으로 판단)
let statusText = '미입력';
let statusClass = 'incomplete';
// 에러 여부와 관계없이 작업시간 기준으로 상태 결정
const totalHours = worker.totalHours || 0;
const hasVacation = worker.hasVacation || false;
const vacationHours = worker.vacationHours || 0;
if (totalHours > 12) {
statusText = '확인필요'; statusClass = 'overtime-warning';
} else if (hasVacation && vacationHours > 0) {
switch (vacationHours) {
case 8: statusText = '연차'; statusClass = 'vacation-full'; break;
case 6: statusText = '조퇴'; statusClass = 'vacation-half-half'; break;
case 4: statusText = '반차'; statusClass = 'vacation-half'; break;
case 2: statusText = '반반차'; statusClass = 'vacation-quarter'; break;
default: statusText = '연차'; statusClass = 'vacation-full';
}
} else if (totalHours > 8) {
statusText = '연장근로'; statusClass = 'overtime';
} else if (totalHours === 8) {
statusText = '정시근로'; statusClass = 'complete';
} else if (totalHours > 0) {
statusText = '부분입력'; statusClass = 'partial';
} else {
statusText = '미입력'; statusClass = 'incomplete';
}
// 작업자 이름의 첫 글자 추출
const initial = worker.workerName ? worker.workerName.charAt(0) : '?';
// 관리자/그룹장 권한 확인
const currentUser = JSON.parse(localStorage.getItem('user') || '{}');
const isAdmin = ['admin', 'system', 'group_leader'].includes(currentUser.access_level || currentUser.role);
// 삭제 버튼 (관리자/그룹장만 표시, 작업이 있는 경우에만)
const deleteBtn = isAdmin && worker.totalWorkCount > 0 ? `
<button class="btn-delete-worker-work" onclick="event.stopPropagation(); deleteWorkerDayWork(${worker.workerId}, '${CalendarState.currentModalDate}', '${worker.workerName}')" title="이 작업자의 해당 날짜 작업 전체 삭제">
🗑️
</button>
` : '';
return `
<div class="worker-card ${statusClass}" onclick="openWorkerModal(${worker.workerId}, '${CalendarState.currentModalDate}')">
<div class="worker-avatar">
<div class="avatar-circle">
<span class="avatar-text">${initial}</span>
</div>
</div>
<div class="worker-info">
<div class="worker-name">${worker.workerName}</div>
<div class="worker-job">${worker.jobType || '일반'}</div>
</div>
<div class="worker-status">
<div class="status-badge ${statusClass}">${statusText}</div>
</div>
<div class="worker-stats">
<div class="stat-row">
<span class="stat-label">작업시간</span>
<span class="stat-value">${worker.actualWorkHours.toFixed(1)}h</span>
</div>
<div class="stat-row">
<span class="stat-label">정규</span>
<span class="stat-value">${worker.regularWorkCount}건</span>
<span class="stat-label">에러</span>
<span class="stat-value ${worker.errorWorkCount > 0 ? 'error' : ''}">${worker.errorWorkCount}건</span>
</div>
</div>
<div class="worker-actions">
${deleteBtn}
<button class="btn-work-entry" onclick="event.stopPropagation(); openWorkerModal(${worker.workerId}, '${CalendarState.currentModalDate}')" title="작업입력">
작업입력
</button>
</div>
</div>
`;
}).join('');
elements.modalWorkersList.innerHTML = workersHtml;
}
// 모달 데이터 렌더링 (폴백용 - 기존 방식)
function renderModalData(workData) {
// 작업자별로 그룹화
const workerGroups = {};
workData.forEach(work => {
if (!workerGroups[work.worker_id]) {
workerGroups[work.worker_id] = {
worker_id: work.worker_id,
worker_name: work.worker_name,
job_type: work.job_type,
works: []
};
}
workerGroups[work.worker_id].works.push(work);
});
// 요약 정보 계산
const totalWorkers = Object.keys(workerGroups).length;
const totalHours = workData.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const totalTasks = workData.length;
const errorCount = workData.filter(w => w.work_status_id === 2).length;
// 요약 정보 업데이트
elements.modalTotalWorkers.textContent = `${totalWorkers}`;
elements.modalTotalHours.textContent = `${totalHours.toFixed(1)}h`;
elements.modalTotalTasks.textContent = `${totalTasks}`;
elements.modalErrorCount.textContent = `${errorCount}`;
// 작업자 리스트 렌더링
renderWorkersList(Object.values(workerGroups));
}
// 작업자 리스트 렌더링
function renderWorkersList(workerGroups) {
const workersHTML = workerGroups.map(workerGroup => {
const totalHours = workerGroup.works.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
const hasError = workerGroup.works.some(w => w.work_status_id === 2);
const hasVacation = workerGroup.works.some(w => w.project_id === 13);
const regularWorkCount = workerGroup.works.filter(w => w.project_id !== 13 && w.work_status_id !== 2).length;
const errorWorkCount = workerGroup.works.filter(w => w.project_id !== 13 && w.work_status_id === 2).length;
// 상태 결정
let status, statusText, statusBadge;
if (hasVacation) {
const vacationHours = workerGroup.works
.filter(w => w.project_id === 13)
.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
if (vacationHours === 8) {
status = 'vacation-full';
statusText = '연차';
statusBadge = '연차';
} else if (vacationHours === 6) {
status = 'vacation-half-half';
statusText = '조퇴';
statusBadge = '조퇴';
} else if (vacationHours === 4) {
status = 'vacation-half';
statusText = '반차';
statusBadge = '반차';
} else if (vacationHours === 2) {
status = 'vacation-quarter';
statusText = '반반차';
statusBadge = '반반차';
}
} else if (totalHours > 8) {
status = 'overtime';
statusText = '연장근로';
statusBadge = '연장근로';
} else if (totalHours === 8) {
status = 'complete';
statusText = '정시근로';
statusBadge = '정시근로';
} else if (totalHours > 0) {
status = 'partial';
statusText = '부분입력';
statusBadge = '부분입력';
} else {
status = 'incomplete';
statusText = '미입력';
statusBadge = '미입력';
}
return `
<div class="worker-status-row ${status}" data-status="${status}">
<div class="worker-basic-info">
<div class="worker-avatar">
<span>${workerGroup.worker_name.charAt(0)}</span>
</div>
<div class="worker-details">
<h4 class="worker-name">${workerGroup.worker_name}</h4>
<p class="worker-job">${workerGroup.job_type || '작업자'}</p>
</div>
</div>
<div class="worker-status-indicator">
<span class="status-badge status-${status}">${statusBadge}</span>
</div>
<div class="worker-stats-inline">
<div class="stat-item">
<span class="stat-label">작업시간</span>
<span class="stat-value">${totalHours.toFixed(1)}h</span>
</div>
<div class="stat-item">
<span class="stat-label">정규</span>
<span class="stat-value">${regularWorkCount}건</span>
</div>
${errorWorkCount > 0 ? `
<div class="stat-item error">
<span class="stat-label">에러</span>
<span class="stat-value">${errorWorkCount}건</span>
</div>
` : ''}
</div>
</div>
`;
}).join('');
elements.modalWorkersList.innerHTML = workersHTML;
}
// 작업자 리스트 필터링
function filterWorkersList() {
const filterValue = elements.statusFilter.value;
const workerRows = elements.modalWorkersList.querySelectorAll('.worker-status-row');
workerRows.forEach(row => {
const status = row.dataset.status;
if (filterValue === 'all' || status === filterValue ||
(filterValue === 'vacation' && status.startsWith('vacation'))) {
row.style.display = 'flex';
} else {
row.style.display = 'none';
}
});
}
// 모달 닫기
function closeDailyWorkModal() {
elements.dailyWorkModal.style.display = 'none';
document.body.style.overflow = '';
CalendarState.currentModalDate = null;
}
// 로딩 표시
function showLoading(show) {
if (elements.loadingSpinner) {
elements.loadingSpinner.style.display = show ? 'flex' : 'none';
}
}
// 토스트 메시지 (간단한 구현)
function showToast(message, type = 'info') {
// 기존 토스트가 있으면 제거
const existingToast = document.querySelector('.toast-message');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = `toast-message toast-${type}`;
toast.textContent = message;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 24px;
background: ${type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#3b82f6'};
color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
font-weight: 500;
max-width: 400px;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// 작업자의 해당 날짜 작업 전체 삭제 (관리자/그룹장용)
async function deleteWorkerDayWork(workerId, date, workerName) {
// 확인 대화상자
const confirmed = confirm(
`⚠️ 정말로 삭제하시겠습니까?\n\n` +
`작업자: ${workerName}\n` +
`날짜: ${date}\n\n` +
`이 작업자의 해당 날짜 모든 작업이 삭제됩니다.\n` +
`삭제된 작업은 복구할 수 없습니다.`
);
if (!confirmed) return;
try {
showToast('작업을 삭제하는 중...', 'info');
// 날짜+작업자별 전체 삭제 API 호출
const result = await CalendarAPI.deleteWorkerDayWork(workerId, date);
console.log('✅ 작업 삭제 성공:', result);
showToast(`${workerName}${date} 작업이 삭제되었습니다.`, 'success');
// 모달 데이터 새로고침
await openDailyWorkModal(CalendarState.currentModalDate);
// 캘린더도 새로고침
await renderCalendar();
} catch (error) {
console.error('❌ 작업 삭제 실패:', error);
showToast(`작업 삭제 실패: ${error.message}`, 'error');
}
}
// 작업자 개별 작업 모달 열기
async function openWorkerModal(workerId, date) {
try {
// 작업자 정보 찾기
const worker = CalendarState.allWorkers.find(w => w.worker_id === workerId);
if (!worker) {
showToast('작업자 정보를 찾을 수 없습니다.', 'error');
return;
}
// 작업 입력 모달 열기
await openWorkEntryModal(workerId, worker.worker_name, date);
} catch (error) {
console.error('작업자 모달 열기 오류:', error);
showToast('작업 입력 모달을 여는데 실패했습니다.', 'error');
}
}
// 작업 입력 모달 열기
async function openWorkEntryModal(workerId, workerName, date) {
try {
// 모달 요소들 가져오기
const modal = document.getElementById('workEntryModal');
const titleElement = document.getElementById('workEntryModalTitle');
const workerNameDisplay = document.getElementById('workerNameDisplay');
const workerIdInput = document.getElementById('workerId');
const workDateInput = document.getElementById('workDate');
if (!modal) {
showToast('작업 입력 모달을 찾을 수 없습니다.', 'error');
return;
}
// 모달 제목 및 정보 설정
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';
} catch (error) {
console.error('작업 입력 모달 열기 오류:', error);
showToast('작업 입력 모달을 여는데 실패했습니다.', 'error');
}
}
// 모달 데이터 로드 (프로젝트, 작업 상태)
async function loadModalData() {
try {
// 활성 프로젝트 목록 로드
const projectsResponse = await window.apiCall('/projects/active/list');
const projects = Array.isArray(projectsResponse) ? projectsResponse : (projectsResponse.data || []);
const projectSelect = document.getElementById('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);
});
// 작업 상태 목록 로드 (하드코딩으로 대체)
const statuses = [
{ status_id: 1, status_name: '완료' },
{ status_id: 2, status_name: '오류' },
{ status_id: 3, status_name: '진행중' }
];
const statusSelect = document.getElementById('workStatusSelect');
statusSelect.innerHTML = '<option value="">상태를 선택하세요</option>';
statuses.forEach(status => {
const option = document.createElement('option');
option.value = status.status_id;
option.textContent = status.status_name;
statusSelect.appendChild(option);
});
} catch (error) {
console.error('모달 데이터 로드 오류:', error);
showToast('데이터를 불러오는데 실패했습니다.', 'error');
}
}
// 작업 입력 모달 닫기
function closeWorkEntryModal() {
const modal = document.getElementById('workEntryModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = 'auto';
// 폼 초기화
const form = document.getElementById('workEntryForm');
if (form) {
form.reset();
}
}
}
// 휴가 처리
function handleVacation(type) {
const projectSelect = document.getElementById('projectSelect');
const workHours = document.getElementById('workHours');
const workStatusSelect = document.getElementById('workStatusSelect');
const workDescription = document.getElementById('workDescription');
// 연차/휴무 프로젝트 선택 (project_id: 13)
projectSelect.value = '13';
// 휴가 유형에 따른 시간 설정
switch (type) {
case 'full': // 연차
workHours.value = '8';
workDescription.value = '연차';
break;
case 'half': // 반차
workHours.value = '4';
workDescription.value = '반차';
break;
case 'quarter': // 반반차
workHours.value = '2';
workDescription.value = '반반차';
break;
case 'early': // 조퇴
workHours.value = '6';
workDescription.value = '조퇴';
break;
}
// 완료 상태로 설정 (status_id: 1)
workStatusSelect.value = '1';
}
// 작업 저장
async function saveWorkEntry() {
try {
const form = document.getElementById('workEntryForm');
const formData = new FormData(form);
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_type_id || !workData.work_hours || !workData.work_status_id) {
showToast('필수 항목을 모두 입력해주세요.', 'error');
return;
}
// 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) {
const action = editingWorkId ? '수정' : '저장';
showToast(`작업이 성공적으로 ${action}되었습니다.`, 'success');
// 기존 작업 목록 새로고침
await loadExistingWorks(workData.worker_id, workData.report_date);
// 기존 작업 탭으로 전환
switchTab('existing');
// 캘린더 새로고침
await renderCalendar();
// 현재 열린 모달이 있다면 새로고침
if (CalendarState.currentModalDate) {
await openDailyWorkModal(CalendarState.currentModalDate);
}
} else {
const action = editingWorkId ? '수정' : '저장';
throw new Error(response.message || `${action}에 실패했습니다.`);
}
} catch (error) {
console.error('작업 저장 오류:', error);
showToast(error.message || '작업 저장에 실패했습니다.', 'error');
}
}
// 모달 닫기 함수
function closeDailyWorkModal() {
if (elements.dailyWorkModal) {
elements.dailyWorkModal.style.display = 'none';
document.body.style.overflow = 'auto';
}
}
// 전역 변수로 작업자 목록 저장
// let allWorkers = []; // Now in CalendarState
// 시간 업데이트 함수
function updateCurrentTime() {
const now = new Date();
const timeString = now.toLocaleTimeString('ko-KR', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
const timeValueElement = document.getElementById('timeValue');
if (timeValueElement) {
timeValueElement.textContent = timeString;
}
}
// 사용자 정보 업데이트 함수
function updateUserInfo() {
// 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 (finalUserInfo.worker_name) {
userNameElement.textContent = finalUserInfo.worker_name;
} else {
userNameElement.textContent = '사용자';
}
}
if (userRoleElement) {
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 (finalUserInfo.worker_name) {
userInitialElement.textContent = finalUserInfo.worker_name.charAt(0);
} else {
userInitialElement.textContent = '사';
}
}
}
// 페이지 초기화 개선
function initializePage() {
// 시간 업데이트 시작
updateCurrentTime();
setInterval(updateCurrentTime, 1000);
// 사용자 정보 업데이트
updateUserInfo();
// 프로필 메뉴 토글
const userProfile = document.getElementById('userProfile');
const profileMenu = document.getElementById('profileMenu');
if (userProfile && profileMenu) {
userProfile.addEventListener('click', (e) => {
e.stopPropagation();
profileMenu.style.display = profileMenu.style.display === 'block' ? 'none' : 'block';
});
// 외부 클릭 시 메뉴 닫기
document.addEventListener('click', () => {
profileMenu.style.display = 'none';
});
}
// 로그아웃 버튼
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', () => {
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
window.location.href = '/pages/auth/login.html';
});
}
}
// DOMContentLoaded 이벤트에 초기화 함수 추가
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 = [];
}
}
CalendarState.existingWorks = workerWorks;
renderExistingWorks();
updateTabCounter();
} catch (error) {
console.error('기존 작업 로드 오류:', error);
CalendarState.existingWorks = [];
renderExistingWorks();
updateTabCounter();
}
}
// 기존 작업 목록 렌더링
function renderExistingWorks() {
console.log('🎨 작업 목록 렌더링 시작:', CalendarState.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 = CalendarState.existingWorks.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
console.log(`📊 작업 통계: ${CalendarState.existingWorks.length}건, 총 ${totalHours}시간`);
// 요약 정보 업데이트
if (totalWorkCount) totalWorkCount.textContent = CalendarState.existingWorks.length;
if (totalWorkHours) totalWorkHours.textContent = totalHours.toFixed(1);
if (CalendarState.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';
// 각 작업 데이터 상세 로그
CalendarState.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 = CalendarState.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 !== CalendarState.existingWorks.length) {
console.error(`⚠️ 렌더링 불일치: 데이터 ${CalendarState.existingWorks.length}건 vs DOM ${renderedItems.length}`);
}
}
// 탭 카운터 업데이트
function updateTabCounter() {
const existingTabBtn = document.querySelector('[data-tab="existing"]');
if (existingTabBtn) {
existingTabBtn.innerHTML = `📋 기존 작업 (${CalendarState.existingWorks.length}건)`;
}
}
// 작업 수정
function editWork(workId) {
const work = CalendarState.existingWorks.find(w => w.id === workId);
if (!work) {
showToast('작업 정보를 찾을 수 없습니다.', 'error');
return;
}
// 수정 모드로 전환
CalendarState.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 = CalendarState.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 (CalendarState.currentModalDate) {
await openDailyWorkModal(CalendarState.currentModalDate);
}
} else {
showToast(response.message || '작업 삭제에 실패했습니다.', 'error');
}
} catch (error) {
console.error('작업 삭제 오류:', error);
showToast('작업 삭제 중 오류가 발생했습니다.', 'error');
}
}
// 작업 폼 초기화
function resetWorkForm() {
CalendarState.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 (CalendarState.currentEditingWork) {
confirmDeleteWork(CalendarState.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;
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;