Files
TK-FB-Project/web-ui/js/annual-vacation-overview.js
Hyungi Ahn b6485e3140 feat: 대시보드 작업장 현황 지도 구현
- 실시간 작업장 현황을 지도로 시각화
- 작업장 관리 페이지에서 정의한 구역 정보 활용
- TBM 작업자 및 방문자 현황 표시

주요 변경사항:
- dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거)
- workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현
- modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가

시각화 방식:
- 인원 없음: 회색 테두리 + 작업장 이름
- 내부 작업자: 파란색 영역 + 인원 수
- 외부 방문자: 보라색 영역 + 인원 수
- 둘 다: 초록색 영역 + 총 인원 수

기술 구현:
- Canvas API 기반 사각형 영역 렌더링
- map-regions API를 통한 데이터 일관성 보장
- 클릭 이벤트로 상세 정보 모달 표시

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-29 15:46:47 +09:00

413 lines
11 KiB
JavaScript

/**
* annual-vacation-overview.js
* 연간 연차 현황 페이지 로직 (2-탭 구조)
*/
import { API_BASE_URL } from './api-config.js';
// 전역 변수
let annualUsageChart = null;
let currentYear = new Date().getFullYear();
let vacationRequests = [];
/**
* 페이지 초기화
*/
document.addEventListener('DOMContentLoaded', async () => {
// 관리자 권한 체크
const user = JSON.parse(localStorage.getItem('user') || '{}');
const isAdmin = user.role === 'Admin' || [1, 2].includes(user.role_id);
if (!isAdmin) {
alert('관리자만 접근할 수 있습니다');
window.location.href = '/pages/dashboard.html';
return;
}
initializeYearSelector();
initializeMonthSelector();
initializeEventListeners();
await loadAnnualUsageData();
});
/**
* 연도 선택 초기화
*/
function initializeYearSelector() {
const yearSelect = document.getElementById('yearSelect');
const currentYear = new Date().getFullYear();
// 최근 5년, 현재 연도, 다음 연도
for (let year = currentYear - 5; year <= currentYear + 1; year++) {
const option = document.createElement('option');
option.value = year;
option.textContent = `${year}`;
if (year === currentYear) {
option.selected = true;
}
yearSelect.appendChild(option);
}
}
/**
* 월 선택 초기화
*/
function initializeMonthSelector() {
const monthSelect = document.getElementById('monthSelect');
const currentMonth = new Date().getMonth() + 1;
// 현재 월을 기본 선택
monthSelect.value = currentMonth;
}
/**
* 이벤트 리스너 초기화
*/
function initializeEventListeners() {
// 탭 전환
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const tabName = e.target.dataset.tab;
switchTab(tabName);
});
});
// 조회 버튼
document.getElementById('refreshBtn').addEventListener('click', async () => {
await loadAnnualUsageData();
const activeTab = document.querySelector('.tab-btn.active').dataset.tab;
if (activeTab === 'monthlyDetails') {
await loadMonthlyDetails();
}
});
// 연도 변경 시 자동 조회
document.getElementById('yearSelect').addEventListener('change', async () => {
await loadAnnualUsageData();
const activeTab = document.querySelector('.tab-btn.active').dataset.tab;
if (activeTab === 'monthlyDetails') {
await loadMonthlyDetails();
}
});
// 월 선택 변경 시
document.getElementById('monthSelect').addEventListener('change', loadMonthlyDetails);
// 엑셀 다운로드
document.getElementById('exportExcelBtn').addEventListener('click', exportToExcel);
}
/**
* 탭 전환
*/
function switchTab(tabName) {
// 탭 버튼 활성화
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.tab === tabName) {
btn.classList.add('active');
}
});
// 탭 콘텐츠 활성화
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
if (tabName === 'annualUsage') {
document.getElementById('annualUsageTab').classList.add('active');
} else if (tabName === 'monthlyDetails') {
document.getElementById('monthlyDetailsTab').classList.add('active');
loadMonthlyDetails();
}
}
/**
* 연간 사용 데이터 로드 (탭 1)
*/
async function loadAnnualUsageData() {
const year = document.getElementById('yearSelect').value;
try {
const token = localStorage.getItem('token');
// 해당 연도의 모든 승인된 휴가 신청 조회
const response = await fetch(
`${API_BASE_URL}/api/vacation-requests?start_date=${year}-01-01&end_date=${year}-12-31&status=approved`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
if (!response.ok) {
throw new Error('휴가 데이터를 불러오는데 실패했습니다');
}
const result = await response.json();
vacationRequests = result.data || [];
// 월별로 집계
const monthlyData = aggregateMonthlyUsage(vacationRequests);
// 잔여 일수 계산 (올해 총 부여 - 사용)
const remainingDays = await calculateRemainingDays(year);
updateAnnualUsageChart(monthlyData, remainingDays);
} catch (error) {
console.error('연간 사용 데이터 로드 오류:', error);
showToast('데이터를 불러오는데 실패했습니다', 'error');
}
}
/**
* 월별 사용 일수 집계
*/
function aggregateMonthlyUsage(requests) {
const monthlyUsage = Array(12).fill(0); // 1월~12월
requests.forEach(req => {
const startDate = new Date(req.start_date);
const endDate = new Date(req.end_date);
const daysUsed = req.days_used || 0;
// 간단한 집계: 시작일의 월에 모든 일수를 할당
// (더 정교한 계산이 필요하면 일자별로 쪼개야 함)
const month = startDate.getMonth(); // 0-11
monthlyUsage[month] += daysUsed;
});
return monthlyUsage;
}
/**
* 잔여 일수 계산
*/
async function calculateRemainingDays(year) {
try {
const token = localStorage.getItem('token');
// 전체 작업자의 휴가 잔액 조회
const response = await fetch(`${API_BASE_URL}/api/vacation-balances/year/${year}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
return 0;
}
const result = await response.json();
const balances = result.data || [];
// 전체 잔여 일수 합계
const totalRemaining = balances.reduce((sum, item) => sum + (item.remaining_days || 0), 0);
return totalRemaining;
} catch (error) {
console.error('잔여 일수 계산 오류:', error);
return 0;
}
}
/**
* 연간 사용 차트 업데이트
*/
function updateAnnualUsageChart(monthlyData, remainingDays) {
const ctx = document.getElementById('annualUsageChart');
// 기존 차트 삭제
if (annualUsageChart) {
annualUsageChart.destroy();
}
const labels = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월', '잔여'];
const data = [...monthlyData, remainingDays];
annualUsageChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '일수',
data: data,
backgroundColor: data.map((_, idx) =>
idx === 12 ? 'rgba(16, 185, 129, 0.8)' : 'rgba(59, 130, 246, 0.8)'
),
borderColor: data.map((_, idx) =>
idx === 12 ? 'rgba(16, 185, 129, 1)' : 'rgba(59, 130, 246, 1)'
),
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
return `${context.parsed.y}`;
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 5
}
}
}
}
});
}
/**
* 월별 상세 기록 로드 (탭 2)
*/
async function loadMonthlyDetails() {
const year = document.getElementById('yearSelect').value;
const month = document.getElementById('monthSelect').value;
try {
const token = localStorage.getItem('token');
// 해당 월의 모든 휴가 신청 조회 (승인된 것만)
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
const lastDay = new Date(year, month, 0).getDate();
const endDate = `${year}-${String(month).padStart(2, '0')}-${lastDay}`;
const response = await fetch(
`${API_BASE_URL}/api/vacation-requests?start_date=${startDate}&end_date=${endDate}&status=approved`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
if (!response.ok) {
throw new Error('월별 데이터를 불러오는데 실패했습니다');
}
const result = await response.json();
const monthlyRequests = result.data || [];
updateMonthlyTable(monthlyRequests);
} catch (error) {
console.error('월별 상세 기록 로드 오류:', error);
showToast('데이터를 불러오는데 실패했습니다', 'error');
}
}
/**
* 월별 테이블 업데이트
*/
function updateMonthlyTable(requests) {
const tbody = document.getElementById('monthlyTableBody');
if (requests.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="7" class="loading-state">
<p>데이터가 없습니다</p>
</td>
</tr>
`;
return;
}
tbody.innerHTML = requests.map(req => {
const statusText = req.status === 'approved' ? '승인' : req.status === 'pending' ? '대기' : '거부';
const statusClass = req.status === 'approved' ? 'success' : req.status === 'pending' ? 'warning' : 'danger';
return `
<tr>
<td>${req.worker_name}</td>
<td>${req.vacation_type_name}</td>
<td>${req.start_date}</td>
<td>${req.end_date}</td>
<td>${req.days_used}일</td>
<td>${req.reason || '-'}</td>
<td><span class="badge badge-${statusClass}">${statusText}</span></td>
</tr>
`;
}).join('');
}
/**
* 엑셀 다운로드
*/
function exportToExcel() {
const year = document.getElementById('yearSelect').value;
const month = document.getElementById('monthSelect').value;
const tbody = document.getElementById('monthlyTableBody');
// 테이블에 데이터가 없으면 중단
if (!tbody.querySelector('tr:not(.loading-state)')) {
showToast('다운로드할 데이터가 없습니다', 'warning');
return;
}
// CSV 형식으로 데이터 생성
const headers = ['작업자명', '휴가유형', '시작일', '종료일', '사용일수', '사유', '상태'];
const rows = Array.from(tbody.querySelectorAll('tr:not(.loading-state)')).map(tr => {
const cells = tr.querySelectorAll('td');
return Array.from(cells).map(cell => {
// badge 클래스가 있으면 텍스트만 추출
const badge = cell.querySelector('.badge');
return badge ? badge.textContent : cell.textContent;
});
});
const csvContent = [
headers.join(','),
...rows.map(row => row.join(','))
].join('\n');
// BOM 추가 (한글 깨짐 방지)
const BOM = '\uFEFF';
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', `월별_연차_상세_${year}_${month}월.csv`);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showToast('엑셀 파일이 다운로드되었습니다', 'success');
}
/**
* 토스트 메시지 표시
*/
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
container.removeChild(toast);
}, 300);
}, 3000);
}