-
- 작업 유형:
- ${entry.work_type_name || '-'}
-
- ${entry.work_status_id === 2 ? `
-
- 에러 유형:
- ${entry.error_type_name || '에러'}
-
` : ''}
-
- `;
- return entryDiv;
-}
-
-function displayWorkersDetails(workers) {
- const workersListEl = document.getElementById('workersList');
- workersListEl.innerHTML = '';
- workers.forEach(worker => {
- const workerCard = document.createElement('div');
- workerCard.className = 'worker-card';
- workerCard.innerHTML = `
-
- `;
- const entriesContainer = document.createElement('div');
- entriesContainer.className = 'work-entries';
- worker.entries.forEach(entry => entriesContainer.appendChild(createWorkEntryElement(entry)));
- workerCard.appendChild(entriesContainer);
- workersListEl.appendChild(workerCard);
- });
- document.getElementById('workersReport').style.display = 'block';
-}
-
-const hideElement = (id) => {
- const el = document.getElementById(id);
- if (el) el.style.display = 'none';
-};
-
-/**
- * 가공된 데이터를 받아 화면 전체를 렌더링합니다.
- * @param {object|null} processedData - 가공된 데이터 또는 데이터가 없을 경우 null
- */
-export function renderReport(processedData) {
- hideElement('loadingSpinner');
- hideElement('errorMessage');
- hideElement('noDataMessage');
- hideElement('reportSummary');
- hideElement('workersReport');
- hideElement('exportSection');
-
- if (!processedData) {
- document.getElementById('noDataMessage').style.display = 'block';
- return;
- }
- displaySummary(processedData.summary);
- displayWorkersDetails(processedData.workers);
- document.getElementById('exportSection').style.display = 'block';
-}
-
-export function showLoading(isLoading) {
- document.getElementById('loadingSpinner').style.display = isLoading ? 'flex' : 'none';
- if(isLoading) {
- hideElement('errorMessage');
- hideElement('noDataMessage');
- }
-}
-
-export function showError(message) {
- const errorEl = document.getElementById('errorMessage');
- errorEl.querySelector('.error-text').textContent = message;
- errorEl.style.display = 'block';
- hideElement('loadingSpinner');
-}
\ No newline at end of file
diff --git a/web-ui/js/work-report-calendar.js b/web-ui/js/work-report-calendar.js
deleted file mode 100644
index bfeadb3..0000000
--- a/web-ui/js/work-report-calendar.js
+++ /dev/null
@@ -1,1336 +0,0 @@
-// 작업 현황 캘린더 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 요소 초기화 (기존 + CalendarView)
- initializeElements();
- CalendarView.initializeElements();
-
- // 이벤트 리스너 등록
- setupEventListeners();
-
- // 작업자 데이터 로드 (한 번만)
- await loadWorkersData();
-
- // 현재 월 캘린더 렌더링
- await CalendarView.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);
- CalendarView.renderCalendar();
- });
-
- elements.nextMonthBtn.addEventListener('click', () => {
- CalendarState.currentDate.setMonth(CalendarState.currentDate.getMonth() + 1);
- CalendarView.renderCalendar();
- });
-
- elements.todayBtn.addEventListener('click', () => {
- CalendarState.currentDate = new Date();
- CalendarView.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 = '등록된 작업자가 없습니다.
';
- 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 ? `
-
-
-
-
${worker.workerName}
-
${worker.jobType || '일반'}
-
-
-
-
- 작업시간
- ${worker.actualWorkHours.toFixed(1)}h
-
-
- 정규
- ${worker.regularWorkCount}건
- 에러
- ${worker.errorWorkCount}건
-
-
-
- ${deleteBtn}
-
-
-
- `;
- }).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 `
-
-
-
- ${workerGroup.worker_name.charAt(0)}
-
-
-
${workerGroup.worker_name}
-
${workerGroup.job_type || '작업자'}
-
-
-
-
- ${statusBadge}
-
-
-
-
- 작업시간
- ${totalHours.toFixed(1)}h
-
-
- 정규
- ${regularWorkCount}건
-
- ${errorWorkCount > 0 ? `
-
- 에러
- ${errorWorkCount}건
-
- ` : ''}
-
-
- `;
- }).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 CalendarView.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 = '