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.
This commit is contained in:
121
web-ui/js/modules/calendar/CalendarView.js
Normal file
121
web-ui/js/modules/calendar/CalendarView.js
Normal file
@@ -0,0 +1,121 @@
|
||||
// web-ui/js/modules/calendar/CalendarView.js
|
||||
|
||||
/**
|
||||
* 캘린더 UI 렌더링 및 DOM 조작을 담당하는 전역 객체입니다.
|
||||
*/
|
||||
(function(window) {
|
||||
'use strict';
|
||||
|
||||
const CalendarView = {
|
||||
elements: {},
|
||||
|
||||
initializeElements: function() {
|
||||
this.elements.monthYearTitle = document.getElementById('monthYearTitle');
|
||||
this.elements.calendarDays = document.getElementById('calendarDays');
|
||||
this.elements.prevMonthBtn = document.getElementById('prevMonthBtn');
|
||||
this.elements.nextMonthBtn = document.getElementById('nextMonthBtn');
|
||||
this.elements.todayBtn = document.getElementById('todayBtn');
|
||||
this.elements.dailyWorkModal = document.getElementById('dailyWorkModal');
|
||||
this.elements.modalTitle = document.getElementById('modalTitle');
|
||||
this.elements.modalSummary = document.querySelector('.daily-summary');
|
||||
this.elements.modalTotalWorkers = document.getElementById('modalTotalWorkers');
|
||||
this.elements.modalTotalHours = document.getElementById('modalTotalHours');
|
||||
this.elements.modalTotalTasks = document.getElementById('modalTotalTasks');
|
||||
this.elements.modalErrorCount = document.getElementById('modalErrorCount');
|
||||
this.elements.modalWorkersList = document.getElementById('modalWorkersList');
|
||||
this.elements.modalNoData = document.getElementById('modalNoData');
|
||||
this.elements.statusFilter = document.getElementById('statusFilter');
|
||||
this.elements.loadingSpinner = document.getElementById('loadingSpinner');
|
||||
},
|
||||
|
||||
showLoading: function(show) {
|
||||
if (this.elements.loadingSpinner) {
|
||||
this.elements.loadingSpinner.style.display = show ? 'flex' : 'none';
|
||||
}
|
||||
},
|
||||
|
||||
showToast: function(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;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => toast.remove(), 3000);
|
||||
},
|
||||
|
||||
renderCalendar: async function() {
|
||||
const year = CalendarState.currentDate.getFullYear();
|
||||
const month = CalendarState.currentDate.getMonth();
|
||||
|
||||
const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'];
|
||||
this.elements.monthYearTitle.textContent = `${year}년 ${monthNames[month]}`;
|
||||
|
||||
this.showLoading(true);
|
||||
try {
|
||||
const monthData = await CalendarAPI.getMonthlyCalendarData(year, month);
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const startDate = new Date(firstDay);
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay());
|
||||
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
let calendarHTML = '';
|
||||
let currentDay = new Date(startDate);
|
||||
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const dateStr = `${currentDay.getFullYear()}-${String(currentDay.getMonth() + 1).padStart(2, '0')}-${String(currentDay.getDate()).padStart(2, '0')}`;
|
||||
const dayWorkData = monthData[dateStr] || { hasData: false, hasIssues: false, hasErrors: false, workerCount: 0 };
|
||||
const dayStatus = this.analyzeDayStatus(dayWorkData);
|
||||
|
||||
let dayClasses = ['calendar-day'];
|
||||
if (currentDay.getMonth() !== month) dayClasses.push('other-month');
|
||||
if (dateStr === todayStr) dayClasses.push('today');
|
||||
if (currentDay.getDay() === 0) dayClasses.push('sunday');
|
||||
if (currentDay.getDay() === 6) dayClasses.push('saturday');
|
||||
|
||||
const hasAnyProblem = dayStatus.hasOvertimeWarning || dayStatus.hasIncomplete || dayStatus.hasIssues;
|
||||
if (dayStatus.hasData && !hasAnyProblem) dayClasses.push('has-normal');
|
||||
|
||||
let statusIcons = '';
|
||||
if (hasAnyProblem) {
|
||||
if (dayStatus.hasOvertimeWarning) statusIcons += '<div class="legend-icon purple">●</div>';
|
||||
if (dayStatus.hasIncomplete) statusIcons += '<div class="legend-icon red">●</div>';
|
||||
if (dayStatus.hasIssues) statusIcons += '<div class="legend-icon orange">●</div>';
|
||||
}
|
||||
|
||||
calendarHTML += `<div class="${dayClasses.join(' ')}" onclick="openDailyWorkModal('${dateStr}')"><div class="day-number">${currentDay.getDate()}</div>${statusIcons}</div>`;
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
}
|
||||
this.elements.calendarDays.innerHTML = calendarHTML;
|
||||
} catch (error) {
|
||||
console.error('캘린더 렌더링 오류:', error);
|
||||
this.showToast('캘린더를 불러오는데 실패했습니다.', 'error');
|
||||
} finally {
|
||||
this.showLoading(false);
|
||||
}
|
||||
},
|
||||
|
||||
analyzeDayStatus: function(dayData) {
|
||||
if (dayData && typeof dayData === 'object' && 'totalWorkers' in dayData) {
|
||||
const totalRegisteredWorkers = CalendarState.allWorkers ? CalendarState.allWorkers.length : 0;
|
||||
const actualIncompleteWorkers = Math.max(0, totalRegisteredWorkers - dayData.workingWorkers);
|
||||
return {
|
||||
hasData: dayData.totalWorkers > 0,
|
||||
hasIssues: dayData.partialWorkers > 0,
|
||||
hasIncomplete: actualIncompleteWorkers > 0 || dayData.incompleteWorkers > 0,
|
||||
hasOvertimeWarning: dayData.hasOvertimeWarning || dayData.overtimeWarningWorkers > 0,
|
||||
workerCount: dayData.totalWorkers || 0
|
||||
};
|
||||
}
|
||||
return { hasData: false, hasIssues: false, hasErrors: false, workerCount: 0 };
|
||||
}
|
||||
};
|
||||
|
||||
window.CalendarView = CalendarView;
|
||||
|
||||
})(window);
|
||||
@@ -138,213 +138,6 @@ async function loadMonthlyWorkData(year, month) {
|
||||
}
|
||||
}
|
||||
|
||||
// 캘린더 렌더링
|
||||
async function renderCalendar() {
|
||||
const year = CalendarState.currentDate.getFullYear();
|
||||
const month = CalendarState.currentDate.getMonth();
|
||||
|
||||
// 헤더 업데이트
|
||||
const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월',
|
||||
'7월', '8월', '9월', '10월', '11월', '12월'];
|
||||
const monthText = `${year}년 ${monthNames[month]}`;
|
||||
|
||||
elements.monthYearTitle.textContent = monthText;
|
||||
|
||||
// 로딩 표시
|
||||
showLoading(true);
|
||||
|
||||
try {
|
||||
// 월별 데이터 로드
|
||||
const monthData = await loadMonthlyWorkData(year, month);
|
||||
|
||||
// 캘린더 날짜 생성
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const startDate = new Date(firstDay);
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay()); // 주의 시작일 (일요일)
|
||||
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
let calendarHTML = '';
|
||||
const currentDay = new Date(startDate);
|
||||
|
||||
// 6주 * 7일 = 42일 렌더링
|
||||
for (let i = 0; i < 42; i++) {
|
||||
// 로컬 시간대로 날짜 문자열 생성 (UTC 변환 문제 방지)
|
||||
const year = currentDay.getFullYear();
|
||||
const month_num = String(currentDay.getMonth() + 1).padStart(2, '0');
|
||||
const day_num = String(currentDay.getDate()).padStart(2, '0');
|
||||
const dateStr = `${year}-${month_num}-${day_num}`;
|
||||
|
||||
const dayNumber = currentDay.getDate();
|
||||
const isCurrentMonth = currentDay.getMonth() === month;
|
||||
const isToday = dateStr === todayStr;
|
||||
const isSunday = currentDay.getDay() === 0;
|
||||
const isSaturday = currentDay.getDay() === 6;
|
||||
|
||||
// 해당 날짜의 작업 데이터 (집계 데이터 구조)
|
||||
let dayWorkData = monthData[dateStr] || {
|
||||
hasData: false,
|
||||
hasIssues: false,
|
||||
hasErrors: false,
|
||||
workerCount: 0
|
||||
};
|
||||
|
||||
// 실제 데이터 사용 (테스트 데이터 제거)
|
||||
const dayStatus = analyzeDayStatus(dayWorkData);
|
||||
|
||||
// 디버깅: 상태가 있는 날짜만 로그
|
||||
if (dayStatus.hasData || dayStatus.hasIssues || dayStatus.hasIncomplete || dayStatus.hasOvertimeWarning) {
|
||||
let statusText = '이상없음';
|
||||
if (dayStatus.hasOvertimeWarning) statusText = '확인필요';
|
||||
else if (dayStatus.hasIncomplete) statusText = '미입력';
|
||||
else if (dayStatus.hasIssues) statusText = '부분입력';
|
||||
|
||||
console.log(`📅 ${dateStr} (${dayNumber}일):`, {
|
||||
상태: statusText,
|
||||
작업자수: dayStatus.workerCount,
|
||||
dayStatus,
|
||||
원본데이터: dayWorkData
|
||||
});
|
||||
}
|
||||
|
||||
let dayClasses = ['calendar-day'];
|
||||
if (!isCurrentMonth) dayClasses.push('other-month');
|
||||
if (isToday) dayClasses.push('today');
|
||||
if (isSunday) dayClasses.push('sunday');
|
||||
if (isSaturday) dayClasses.push('saturday');
|
||||
if (isSunday || isSaturday) dayClasses.push('weekend');
|
||||
|
||||
// 문제가 있는지 확인
|
||||
const hasAnyProblem = dayStatus.hasOvertimeWarning || dayStatus.hasIncomplete || dayStatus.hasIssues;
|
||||
|
||||
// 문제가 없으면 초록색 배경
|
||||
if (dayStatus.hasData && !hasAnyProblem) {
|
||||
dayClasses.push('has-normal'); // 이상없음 (초록)
|
||||
}
|
||||
|
||||
// 문제가 있으면 범례 아이콘들을 그대로 표시
|
||||
let statusIcons = '';
|
||||
if (hasAnyProblem) {
|
||||
// 범례와 동일한 아이콘들 표시
|
||||
if (dayStatus.hasOvertimeWarning) {
|
||||
statusIcons += '<div class="legend-icon purple">●</div>';
|
||||
}
|
||||
if (dayStatus.hasIncomplete) {
|
||||
statusIcons += '<div class="legend-icon red">●</div>';
|
||||
}
|
||||
if (dayStatus.hasIssues) {
|
||||
statusIcons += '<div class="legend-icon orange">●</div>';
|
||||
}
|
||||
}
|
||||
|
||||
calendarHTML += `
|
||||
<div class="${dayClasses.join(' ')}" onclick="openDailyWorkModal('${dateStr}')">
|
||||
<div class="day-number">${dayNumber}</div>
|
||||
${statusIcons}
|
||||
</div>
|
||||
`;
|
||||
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
}
|
||||
|
||||
elements.calendarDays.innerHTML = calendarHTML;
|
||||
|
||||
} catch (error) {
|
||||
console.error('캘린더 렌더링 오류:', error);
|
||||
showToast('캘린더를 불러오는데 실패했습니다.', 'error');
|
||||
} finally {
|
||||
showLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 일별 상태 분석 (집계 데이터 또는 원본 데이터 처리)
|
||||
function analyzeDayStatus(dayData) {
|
||||
// 새로운 집계 데이터 구조인지 확인 (monthly_summary에서 온 데이터)
|
||||
if (dayData && typeof dayData === 'object' && 'totalWorkers' in dayData) {
|
||||
// 미입력 판단: allWorkers 배열 길이와 실제 작업한 작업자 수 비교
|
||||
const totalRegisteredWorkers = CalendarState.allWorkers ? CalendarState.allWorkers.length : 10; // 실제 등록된 작업자 수
|
||||
const actualIncompleteWorkers = Math.max(0, totalRegisteredWorkers - dayData.workingWorkers);
|
||||
|
||||
const result = {
|
||||
hasData: dayData.totalWorkers > 0,
|
||||
hasIssues: dayData.partialWorkers > 0, // 부분입력 작업자가 있으면 true
|
||||
hasIncomplete: actualIncompleteWorkers > 0 || dayData.incompleteWorkers > 0, // 실제 미입력 작업자가 있으면 true
|
||||
hasOvertimeWarning: dayData.hasOvertimeWarning || dayData.overtimeWarningWorkers > 0, // 12시간 초과
|
||||
workerCount: dayData.totalWorkers || 0
|
||||
};
|
||||
|
||||
// 디버깅: 모든 데이터 로그 (미입력 문제 해결용)
|
||||
console.log('📊 analyzeDayStatus 결과:', {
|
||||
dayData,
|
||||
result,
|
||||
actualIncompleteWorkers,
|
||||
workingWorkers: dayData.workingWorkers,
|
||||
totalRegisteredWorkers: totalRegisteredWorkers,
|
||||
allWorkersLength: CalendarState.allWorkers ? CalendarState.allWorkers.length : 'undefined'
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 기존 hasData 구조 확인
|
||||
if (dayData && typeof dayData === 'object' && dayData.hasData !== undefined) {
|
||||
return {
|
||||
hasData: dayData.hasData,
|
||||
hasIssues: dayData.hasIssues,
|
||||
hasErrors: dayData.hasErrors,
|
||||
workerCount: dayData.workerCount || 0
|
||||
};
|
||||
}
|
||||
|
||||
// 폴백: 기존 방식으로 분석 (원본 작업 데이터 배열)
|
||||
if (!Array.isArray(dayData) || dayData.length === 0) {
|
||||
return {
|
||||
hasData: false,
|
||||
hasIssues: false,
|
||||
hasErrors: false,
|
||||
workerCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 작업자별로 그룹화
|
||||
const workerGroups = {};
|
||||
dayData.forEach(work => {
|
||||
if (!workerGroups[work.worker_id]) {
|
||||
workerGroups[work.worker_id] = [];
|
||||
}
|
||||
workerGroups[work.worker_id].push(work);
|
||||
});
|
||||
|
||||
const workerCount = Object.keys(workerGroups).length;
|
||||
let hasIssues = false;
|
||||
let hasErrors = false;
|
||||
|
||||
// 각 작업자의 상태 분석 - 문제가 있는지만 확인
|
||||
Object.values(workerGroups).forEach(workerWork => {
|
||||
const totalHours = workerWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||||
const hasError = workerWork.some(w => w.work_status_id === 2);
|
||||
const hasVacation = workerWork.some(w => w.project_id === 13);
|
||||
|
||||
// 오류가 있는 경우
|
||||
if (hasError) {
|
||||
hasErrors = true;
|
||||
}
|
||||
// 휴가가 아닌데 미입력이거나 부분입력인 경우
|
||||
else if (!hasVacation && (totalHours === 0 || totalHours < 8)) {
|
||||
hasIssues = true;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hasData: true,
|
||||
hasIssues,
|
||||
hasErrors,
|
||||
workerCount
|
||||
};
|
||||
}
|
||||
|
||||
// 일일 작업 현황 모달 열기
|
||||
async function openDailyWorkModal(dateStr) {
|
||||
console.log(`🗓️ 클릭된 날짜: ${dateStr}`);
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
<script src="/js/load-navbar.js?v=4"></script>
|
||||
<script src="/js/modules/calendar/CalendarState.js?v=1"></script>
|
||||
<script src="/js/modules/calendar/CalendarAPI.js?v=1"></script>
|
||||
<script src="/js/modules/calendar/CalendarView.js?v=1"></script>
|
||||
<script src="/js/work-report-calendar.js?v=41"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user