refactor: 네비게이션 헤더 최신 디자인으로 전면 개편 및 로그인 버그 수정
- fix: 로그인 API에서 user.role_name 필드 올바르게 사용 (auth.service.js) - refactor: navbar 컴포넌트를 최신 dashboard-header 스타일로 전환 - refactor: 구버전 work-report-header 제거 (6개 페이지) - refactor: load-navbar.js를 최신 헤더 구조에 맞게 업데이트 - style: 파란색 그라데이션 헤더, 실시간 시계, 향상된 프로필 메뉴 - docs: 2026-01-20 개발 로그 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -28,14 +28,15 @@ function clearAuthData() {
|
||||
const currentUser = getUser();
|
||||
|
||||
// 사용자 정보가 유효한지 확인 (토큰은 있지만 유저 정보가 깨졌을 경우)
|
||||
if (!currentUser || !currentUser.username || !currentUser.role) {
|
||||
if (!currentUser || !currentUser.username) {
|
||||
console.error('🚨 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.');
|
||||
clearAuthData();
|
||||
window.location.href = '/index.html';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`✅ ${currentUser.username}(${currentUser.role})님 인증 성공.`);
|
||||
const userRole = currentUser.role || currentUser.access_level || '사용자';
|
||||
console.log(`✅ ${currentUser.username}(${userRole})님 인증 성공.`);
|
||||
|
||||
// 역할 기반 메뉴 제어 로직은 각 컴포넌트 로더(load-navbar.js 등)로 이전함.
|
||||
// 전역 변수 할당(window.currentUser) 제거.
|
||||
|
||||
@@ -57,39 +57,22 @@ function populateUserInfo(doc, user) {
|
||||
const roleName = ROLE_NAMES[user.role] || ROLE_NAMES.default;
|
||||
|
||||
const elements = {
|
||||
'user-name': displayName,
|
||||
'user-role': roleName,
|
||||
'dropdown-user-fullname': displayName,
|
||||
'dropdown-user-id': `@${user.username}`,
|
||||
'userName': displayName,
|
||||
'userRole': roleName,
|
||||
'userInitial': displayName.charAt(0),
|
||||
};
|
||||
|
||||
for (const id in elements) {
|
||||
const el = doc.getElementById(id);
|
||||
if (el) el.textContent = elements[id];
|
||||
}
|
||||
|
||||
const systemBtn = doc.getElementById('systemBtn');
|
||||
if (systemBtn && user.role === 'system') {
|
||||
systemBtn.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 네비게이션 바와 관련된 모든 이벤트를 설정합니다.
|
||||
*/
|
||||
function setupNavbarEvents() {
|
||||
const userInfoDropdown = document.getElementById('user-info-dropdown');
|
||||
const profileDropdownMenu = document.getElementById('profile-dropdown-menu');
|
||||
|
||||
if (userInfoDropdown && profileDropdownMenu) {
|
||||
userInfoDropdown.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
profileDropdownMenu.classList.toggle('show');
|
||||
userInfoDropdown.classList.toggle('active');
|
||||
});
|
||||
}
|
||||
|
||||
const logoutButton = document.getElementById('dropdown-logout');
|
||||
const logoutButton = document.getElementById('logoutBtn');
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener('click', () => {
|
||||
if (confirm('로그아웃 하시겠습니까?')) {
|
||||
@@ -98,34 +81,13 @@ function setupNavbarEvents() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const systemButton = document.getElementById('systemBtn');
|
||||
if (systemButton) {
|
||||
systemButton.addEventListener('click', () => {
|
||||
window.location.href = config.paths.systemDashboard;
|
||||
});
|
||||
}
|
||||
|
||||
const dashboardButton = document.querySelector('.dashboard-btn');
|
||||
if (dashboardButton) {
|
||||
dashboardButton.addEventListener('click', () => {
|
||||
window.location.href = config.paths.groupLeaderDashboard;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (profileDropdownMenu && !userInfoDropdown.contains(e.target) && !profileDropdownMenu.contains(e.target)) {
|
||||
profileDropdownMenu.classList.remove('show');
|
||||
userInfoDropdown.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 시간을 업데이트하는 함수
|
||||
*/
|
||||
function updateTime() {
|
||||
const timeElement = document.getElementById('current-time');
|
||||
const timeElement = document.getElementById('timeValue');
|
||||
if (timeElement) {
|
||||
const now = new Date();
|
||||
timeElement.textContent = now.toLocaleTimeString('ko-KR', { hour12: false });
|
||||
|
||||
390
web-ui/js/my-attendance.js
Normal file
390
web-ui/js/my-attendance.js
Normal file
@@ -0,0 +1,390 @@
|
||||
/**
|
||||
* 나의 출근 현황 페이지
|
||||
* 본인의 출근 기록과 근태 현황을 조회하고 표시합니다
|
||||
*/
|
||||
|
||||
// 전역 상태
|
||||
let currentYear = new Date().getFullYear();
|
||||
let currentMonth = new Date().getMonth() + 1;
|
||||
let attendanceData = [];
|
||||
let vacationBalance = null;
|
||||
let monthlyStats = null;
|
||||
|
||||
// 페이지 로드 시 초기화
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializePage();
|
||||
});
|
||||
|
||||
/**
|
||||
* 페이지 초기화
|
||||
*/
|
||||
function initializePage() {
|
||||
initializeYearMonthSelects();
|
||||
setupEventListeners();
|
||||
loadAttendanceData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 년도/월 선택 옵션 초기화
|
||||
*/
|
||||
function initializeYearMonthSelects() {
|
||||
const yearSelect = document.getElementById('yearSelect');
|
||||
const monthSelect = document.getElementById('monthSelect');
|
||||
|
||||
// 년도 옵션 (현재 년도 기준 ±2년)
|
||||
const currentYearValue = new Date().getFullYear();
|
||||
for (let year = currentYearValue - 2; year <= currentYearValue + 2; year++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = year;
|
||||
option.textContent = `${year}년`;
|
||||
if (year === currentYear) option.selected = true;
|
||||
yearSelect.appendChild(option);
|
||||
}
|
||||
|
||||
// 월 옵션
|
||||
for (let month = 1; month <= 12; month++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = month;
|
||||
option.textContent = `${month}월`;
|
||||
if (month === currentMonth) option.selected = true;
|
||||
monthSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 설정
|
||||
*/
|
||||
function setupEventListeners() {
|
||||
// 조회 버튼
|
||||
document.getElementById('loadAttendance').addEventListener('click', () => {
|
||||
currentYear = parseInt(document.getElementById('yearSelect').value);
|
||||
currentMonth = parseInt(document.getElementById('monthSelect').value);
|
||||
loadAttendanceData();
|
||||
});
|
||||
|
||||
// 탭 전환
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const tabName = e.currentTarget.dataset.tab;
|
||||
switchTab(tabName);
|
||||
});
|
||||
});
|
||||
|
||||
// 달력 네비게이션
|
||||
document.getElementById('prevMonth').addEventListener('click', () => {
|
||||
changeMonth(-1);
|
||||
});
|
||||
|
||||
document.getElementById('nextMonth').addEventListener('click', () => {
|
||||
changeMonth(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 출근 데이터 로드
|
||||
*/
|
||||
async function loadAttendanceData() {
|
||||
try {
|
||||
showLoading();
|
||||
|
||||
// 병렬로 데이터 로드
|
||||
const [attendanceRes, vacationRes, statsRes] = await Promise.all([
|
||||
window.apiGet(`/users/me/attendance-records?year=${currentYear}&month=${currentMonth}`),
|
||||
window.apiGet(`/users/me/vacation-balance?year=${currentYear}`),
|
||||
window.apiGet(`/users/me/monthly-stats?year=${currentYear}&month=${currentMonth}`)
|
||||
]);
|
||||
|
||||
attendanceData = attendanceRes.data || attendanceRes || [];
|
||||
vacationBalance = vacationRes.data || vacationRes;
|
||||
monthlyStats = statsRes.data || statsRes;
|
||||
|
||||
// UI 업데이트
|
||||
updateStats();
|
||||
renderTable();
|
||||
renderCalendar();
|
||||
|
||||
} catch (error) {
|
||||
console.error('출근 데이터 로드 실패:', error);
|
||||
showError('출근 데이터를 불러오는데 실패했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 업데이트
|
||||
*/
|
||||
function updateStats() {
|
||||
// 총 근무시간 (API는 month_hours 반환)
|
||||
const totalHours = monthlyStats?.month_hours || monthlyStats?.total_work_hours || 0;
|
||||
document.getElementById('totalHours').textContent = `${totalHours}시간`;
|
||||
|
||||
// 근무일수
|
||||
const totalDays = monthlyStats?.work_days || 0;
|
||||
document.getElementById('totalDays').textContent = `${totalDays}일`;
|
||||
|
||||
// 잔여 연차
|
||||
const remaining = vacationBalance?.remaining_annual_leave ||
|
||||
(vacationBalance?.total_annual_leave || 0) - (vacationBalance?.used_annual_leave || 0);
|
||||
document.getElementById('remainingLeave').textContent = `${remaining}일`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 렌더링
|
||||
*/
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('attendanceTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!attendanceData || attendanceData.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="empty-cell">출근 기록이 없습니다.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
attendanceData.forEach(record => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = `attendance-row ${getStatusClass(record.attendance_type_code || record.type_code)}`;
|
||||
tr.onclick = () => showDetailModal(record);
|
||||
|
||||
const date = new Date(record.record_date);
|
||||
const dayOfWeek = ['일', '월', '화', '수', '목', '금', '토'][date.getDay()];
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${formatDate(record.record_date)}</td>
|
||||
<td>${dayOfWeek}</td>
|
||||
<td>${record.check_in_time || '-'}</td>
|
||||
<td>${record.check_out_time || '-'}</td>
|
||||
<td>${record.total_work_hours ? `${record.total_work_hours}h` : '-'}</td>
|
||||
<td><span class="status-badge ${getStatusClass(record.attendance_type_code || record.type_code)}">${getStatusText(record)}</span></td>
|
||||
<td class="notes-cell">${record.notes || '-'}</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 달력 렌더링
|
||||
*/
|
||||
function renderCalendar() {
|
||||
const calendarTitle = document.getElementById('calendarTitle');
|
||||
const calendarGrid = document.getElementById('calendarGrid');
|
||||
|
||||
calendarTitle.textContent = `${currentYear}년 ${currentMonth}월`;
|
||||
|
||||
// 달력 그리드 초기화
|
||||
calendarGrid.innerHTML = '';
|
||||
|
||||
// 요일 헤더
|
||||
const weekdays = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
weekdays.forEach(day => {
|
||||
const dayHeader = document.createElement('div');
|
||||
dayHeader.className = 'calendar-day-header';
|
||||
dayHeader.textContent = day;
|
||||
calendarGrid.appendChild(dayHeader);
|
||||
});
|
||||
|
||||
// 해당 월의 첫날과 마지막 날
|
||||
const firstDay = new Date(currentYear, currentMonth - 1, 1);
|
||||
const lastDay = new Date(currentYear, currentMonth, 0);
|
||||
const daysInMonth = lastDay.getDate();
|
||||
const startDayOfWeek = firstDay.getDay();
|
||||
|
||||
// 출근 데이터를 날짜별로 매핑
|
||||
const attendanceMap = {};
|
||||
if (attendanceData) {
|
||||
attendanceData.forEach(record => {
|
||||
const date = new Date(record.record_date);
|
||||
const day = date.getDate();
|
||||
attendanceMap[day] = record;
|
||||
});
|
||||
}
|
||||
|
||||
// 빈 칸 (이전 달)
|
||||
for (let i = 0; i < startDayOfWeek; i++) {
|
||||
const emptyCell = document.createElement('div');
|
||||
emptyCell.className = 'calendar-day empty';
|
||||
calendarGrid.appendChild(emptyCell);
|
||||
}
|
||||
|
||||
// 날짜 칸
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const dayCell = document.createElement('div');
|
||||
dayCell.className = 'calendar-day';
|
||||
|
||||
const record = attendanceMap[day];
|
||||
if (record) {
|
||||
dayCell.classList.add('has-record', getStatusClass(record.attendance_type_code || record.type_code));
|
||||
dayCell.onclick = () => showDetailModal(record);
|
||||
}
|
||||
|
||||
dayCell.innerHTML = `
|
||||
<div class="calendar-day-number">${day}</div>
|
||||
${record ? `<div class="calendar-day-status">${getStatusIcon(record)}</div>` : ''}
|
||||
`;
|
||||
|
||||
calendarGrid.appendChild(dayCell);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 탭 전환
|
||||
*/
|
||||
function switchTab(tabName) {
|
||||
// 탭 버튼 활성화 토글
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tab === tabName);
|
||||
});
|
||||
|
||||
// 탭 컨텐츠 토글
|
||||
document.querySelectorAll('.tab-content').forEach(content => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
if (tabName === 'list') {
|
||||
document.getElementById('listView').classList.add('active');
|
||||
} else if (tabName === 'calendar') {
|
||||
document.getElementById('calendarView').classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 월 변경
|
||||
*/
|
||||
function changeMonth(offset) {
|
||||
currentMonth += offset;
|
||||
|
||||
if (currentMonth < 1) {
|
||||
currentMonth = 12;
|
||||
currentYear--;
|
||||
} else if (currentMonth > 12) {
|
||||
currentMonth = 1;
|
||||
currentYear++;
|
||||
}
|
||||
|
||||
// Select 박스 업데이트
|
||||
document.getElementById('yearSelect').value = currentYear;
|
||||
document.getElementById('monthSelect').value = currentMonth;
|
||||
|
||||
loadAttendanceData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 상세 모달 표시
|
||||
*/
|
||||
function showDetailModal(record) {
|
||||
const modal = document.getElementById('detailModal');
|
||||
const modalBody = document.getElementById('modalBody');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
|
||||
const date = new Date(record.record_date);
|
||||
modalTitle.textContent = `${formatDate(record.record_date)} 출근 상세`;
|
||||
|
||||
modalBody.innerHTML = `
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<label>날짜</label>
|
||||
<div>${formatDate(record.record_date)}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>출근 상태</label>
|
||||
<div><span class="status-badge ${getStatusClass(record.attendance_type_code || record.type_code)}">${getStatusText(record)}</span></div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>출근 시간</label>
|
||||
<div>${record.check_in_time || '기록 없음'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>퇴근 시간</label>
|
||||
<div>${record.check_out_time || '기록 없음'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>총 근무 시간</label>
|
||||
<div>${record.total_work_hours ? `${record.total_work_hours} 시간` : '계산 불가'}</div>
|
||||
</div>
|
||||
${record.vacation_type_name ? `
|
||||
<div class="detail-item">
|
||||
<label>휴가 유형</label>
|
||||
<div>${record.vacation_type_name}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${record.notes ? `
|
||||
<div class="detail-item full-width">
|
||||
<label>비고</label>
|
||||
<div>${record.notes}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* 모달 닫기
|
||||
*/
|
||||
function closeDetailModal() {
|
||||
document.getElementById('detailModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 모달 외부 클릭 시 닫기
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('detailModal');
|
||||
if (event.target === modal) {
|
||||
closeDetailModal();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 유틸리티 함수들
|
||||
*/
|
||||
|
||||
function formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${month}/${day}`;
|
||||
}
|
||||
|
||||
function getStatusClass(typeCode) {
|
||||
const typeMap = {
|
||||
'NORMAL': 'normal',
|
||||
'LATE': 'late',
|
||||
'EARLY_LEAVE': 'early',
|
||||
'ABSENT': 'absent',
|
||||
'VACATION': 'vacation'
|
||||
};
|
||||
return typeMap[typeCode] || 'normal';
|
||||
}
|
||||
|
||||
function getStatusText(record) {
|
||||
if (record.vacation_type_name) {
|
||||
return record.vacation_type_name;
|
||||
}
|
||||
return record.attendance_type_name || record.type_name || '정상';
|
||||
}
|
||||
|
||||
function getStatusIcon(record) {
|
||||
const typeCode = record.attendance_type_code || record.type_code;
|
||||
const iconMap = {
|
||||
'NORMAL': '✓',
|
||||
'LATE': '⚠',
|
||||
'EARLY_LEAVE': '⏰',
|
||||
'ABSENT': '✗',
|
||||
'VACATION': '🌴'
|
||||
};
|
||||
return iconMap[typeCode] || '✓';
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
const tbody = document.getElementById('attendanceTableBody');
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="loading-cell">데이터를 불러오는 중...</td></tr>';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const tbody = document.getElementById('attendanceTableBody');
|
||||
tbody.innerHTML = `<tr><td colspan="7" class="error-cell">${message}</td></tr>`;
|
||||
|
||||
// 통계 초기화
|
||||
document.getElementById('totalHours').textContent = '-';
|
||||
document.getElementById('totalDays').textContent = '-';
|
||||
document.getElementById('remainingLeave').textContent = '-';
|
||||
}
|
||||
Reference in New Issue
Block a user