';
+ html += '
' + catName + '
';
+
+ pages.forEach(page => {
+ // 프로필과 대시보드는 모든 사용자가 접근 가능하므로 체크박스 비활성화
+ const isAlwaysAccessible = page.page_key === 'dashboard' || page.page_key.startsWith('profile.');
+ const isChecked = userPageAccess.find(p => p.page_id === page.id && p.can_access === 1) || isAlwaysAccessible;
+
+ html += '
';
+ html += ' ';
+ html += '' + page.page_name + ' ';
+ html += '
';
+ });
+
+ html += '
';
+ });
+
+ pageAccessList.innerHTML = html;
+}
+
+// 페이지 권한 저장
+async function savePageAccess(userId) {
+ try {
+ const checkboxes = document.querySelectorAll('.page-access-checkbox:not([disabled])');
+ const pageAccessData = [];
+
+ checkboxes.forEach(checkbox => {
+ pageAccessData.push({
+ page_id: parseInt(checkbox.dataset.pageId),
+ can_access: checkbox.checked ? 1 : 0
+ });
+ });
+
+ console.log('📤 페이지 권한 저장:', userId, pageAccessData);
+
+ await apiCall(`/users/${userId}/page-access`, 'PUT', {
+ pageAccess: pageAccessData
+ });
+
+ console.log('✅ 페이지 권한 저장 완료');
+ } catch (error) {
+ console.error('❌ 페이지 권한 저장 오류:', error);
+ throw error;
+ }
+}
+
+// editUser 함수를 수정하여 페이지 권한 로드 추가
+const originalEditUser = window.editUser;
+window.editUser = async function(userId) {
+ // 페이지 목록이 없으면 로드
+ if (allPages.length === 0) {
+ await loadAllPages();
+ }
+
+ // 원래 editUser 함수 실행
+ if (originalEditUser) {
+ originalEditUser(userId);
+ }
+
+ // 사용자의 페이지 권한 로드
+ await loadUserPageAccess(userId);
+
+ // 사용자 정보 가져오기
+ const user = users.find(u => u.user_id === userId);
+ if (!user) return;
+
+ // 페이지 권한 체크박스 렌더링
+ const roleToValueMap = {
+ 'Admin': 'admin',
+ 'System Admin': 'admin',
+ 'User': 'user',
+ 'Guest': 'user'
+ };
+ const userRole = roleToValueMap[user.role] || 'user';
+ renderPageAccessList(userRole);
+};
+
+// saveUser 함수를 수정하여 페이지 권한 저장 추가
+const originalSaveUser = window.saveUser;
+window.saveUser = async function() {
+ try {
+ // 원래 saveUser 함수 실행
+ if (originalSaveUser) {
+ await originalSaveUser();
+ }
+
+ // 사용자 편집 시에만 페이지 권한 저장
+ if (currentEditingUser && currentEditingUser.user_id) {
+ const userRole = document.getElementById('userRole')?.value;
+
+ // Admin이 아닌 경우에만 페이지 권한 저장
+ if (userRole !== 'admin') {
+ await savePageAccess(currentEditingUser.user_id);
+ }
+ }
+
+ } catch (error) {
+ console.error('❌ 저장 오류:', error);
+ throw error;
+ }
+};
+
+
+
+// ========== 페이지 권한 관리 모달 ========== //
+let currentPageAccessUser = null;
+
+// 페이지 권한 관리 모달 열기
+async function managePageAccess(userId) {
+ try {
+ // 페이지 목록이 없으면 로드
+ if (allPages.length === 0) {
+ await loadAllPages();
+ }
+
+ // 사용자 정보 가져오기
+ const user = users.find(u => u.user_id === userId);
+ if (!user) {
+ showToast('사용자를 찾을 수 없습니다.', 'error');
+ return;
+ }
+
+ currentPageAccessUser = user;
+
+ // 사용자의 페이지 권한 로드
+ await loadUserPageAccess(userId);
+
+ // 모달 정보 업데이트
+ const userName = user.name || user.username;
+ document.getElementById('pageAccessModalTitle').textContent = userName + ' - 페이지 권한 관리';
+ document.getElementById('pageAccessUserName').textContent = userName;
+ document.getElementById('pageAccessUserRole').textContent = getRoleName(user.role);
+ document.getElementById('pageAccessUserAvatar').textContent = userName.charAt(0);
+
+ // 페이지 권한 체크박스 렌더링
+ renderPageAccessModalList();
+
+ // 모달 표시
+ document.getElementById('pageAccessModal').style.display = 'flex';
+ } catch (error) {
+ console.error('❌ 페이지 권한 관리 모달 오류:', error);
+ showToast('페이지 권한 관리를 열 수 없습니다.', 'error');
+ }
+}
+
+// 페이지 권한 모달 닫기
+function closePageAccessModal() {
+ document.getElementById('pageAccessModal').style.display = 'none';
+ currentPageAccessUser = null;
+}
+
+// 페이지 권한 체크박스 렌더링 (모달용)
+function renderPageAccessModalList() {
+ const pageAccessList = document.getElementById('pageAccessModalList');
+ if (!pageAccessList) return;
+
+ // 카테고리별로 페이지 그룹화
+ const pagesByCategory = {
+ 'work': [],
+ 'admin': [],
+ 'common': [],
+ 'profile': []
+ };
+
+ allPages.forEach(page => {
+ const category = page.category || 'common';
+ if (pagesByCategory[category]) {
+ pagesByCategory[category].push(page);
+ }
+ });
+
+ const categoryNames = {
+ 'common': '공통',
+ 'work': '작업',
+ 'admin': '관리',
+ 'profile': '프로필'
+ };
+
+ // HTML 생성
+ let html = '';
+
+ Object.keys(pagesByCategory).forEach(category => {
+ const pages = pagesByCategory[category];
+ if (pages.length === 0) return;
+
+ const catName = categoryNames[category] || category;
+ html += '';
+ html += '
' + catName + '
';
+
+ pages.forEach(page => {
+ // 프로필과 대시보드는 모든 사용자가 접근 가능
+ const isAlwaysAccessible = page.page_key === 'dashboard' || page.page_key.startsWith('profile.');
+ const isChecked = userPageAccess.find(p => p.page_id === page.id && p.can_access === 1) || isAlwaysAccessible;
+
+ html += '
';
+ html += ' ';
+ html += '' + page.page_name + ' ';
+ html += '
';
+ });
+
+ html += '
';
+ });
+
+ pageAccessList.innerHTML = html;
+}
+
+// 페이지 권한 저장 (모달용)
+async function savePageAccessFromModal() {
+ if (!currentPageAccessUser) {
+ showToast('사용자 정보가 없습니다.', 'error');
+ return;
+ }
+
+ try {
+ await savePageAccess(currentPageAccessUser.user_id);
+ showToast('페이지 권한이 저장되었습니다.', 'success');
+
+ // 캐시 삭제 (사용자가 다시 로그인하거나 페이지 새로고침 필요)
+ localStorage.removeItem('userPageAccess');
+
+ closePageAccessModal();
+ } catch (error) {
+ console.error('❌ 페이지 권한 저장 오류:', error);
+ showToast('페이지 권한 저장에 실패했습니다.', 'error');
+ }
+}
+
+// 전역 함수로 등록
+window.managePageAccess = managePageAccess;
+window.closePageAccessModal = closePageAccessModal;
+
+// 저장 버튼 이벤트 리스너
+document.addEventListener('DOMContentLoaded', () => {
+ const saveBtn = document.getElementById('savePageAccessBtn');
+ if (saveBtn) {
+ saveBtn.addEventListener('click', savePageAccessFromModal);
+ }
+});
diff --git a/web-ui/js/annual-vacation-overview.js b/web-ui/js/annual-vacation-overview.js
new file mode 100644
index 0000000..04b22ef
--- /dev/null
+++ b/web-ui/js/annual-vacation-overview.js
@@ -0,0 +1,412 @@
+/**
+ * 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 = `
+