Initiated the process of refactoring the monolithic `work-report-calendar.js` file as outlined in the Phase 2 frontend modernization plan. - Created `CalendarAPI.js` to encapsulate all API calls related to the calendar, centralizing data fetching logic. - Created `CalendarState.js` to manage the component's state, removing global variables from the main script. - Refactored `work-report-calendar.js` to use the new state and API modules. - Refactored `manage-project.js` to use the existing global API helpers, providing a consistent example for API usage.
1591 lines
56 KiB
JavaScript
1591 lines
56 KiB
JavaScript
// 작업 현황 캘린더 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 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}`);
|
||
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;
|