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:
Hyungi Ahn
2025-12-19 12:42:24 +09:00
parent 8a8307edfc
commit 05843da1c4
19 changed files with 826 additions and 381 deletions

View 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);

View File

@@ -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}`);