feat: 프론트엔드 모듈화 및 공통 헤더 시스템 구현
- 권한 기반 공통 헤더 컴포넌트 구현 - 모바일 친화적 캘린더 날짜 필터 추가 - 페이지 프리로더 및 키보드 단축키 시스템 구현 - Service Worker 기반 캐싱 시스템 추가 Frontend Changes: - components/common-header.js: 권한 기반 동적 메뉴 생성 - components/mobile-calendar.js: 터치/스와이프 지원 캘린더 - core/permissions.js: 페이지 접근 권한 관리 - core/page-manager.js: 페이지 라이프사이클 관리 - core/page-preloader.js: 페이지 프리로딩 최적화 - core/keyboard-shortcuts.js: 키보드 네비게이션 - css/mobile-calendar.css: 모바일 최적화 캘린더 스타일 - sw.js: 3단계 캐싱 전략 서비스 워커 Removed: - auth-common.js, common-header.js (구버전 파일들)
This commit is contained in:
621
frontend/static/js/core/keyboard-shortcuts.js
Normal file
621
frontend/static/js/core/keyboard-shortcuts.js
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* 키보드 단축키 관리자
|
||||
* 전역 키보드 단축키를 관리하고 사용자 경험을 향상시킵니다.
|
||||
*/
|
||||
|
||||
class KeyboardShortcutManager {
|
||||
constructor() {
|
||||
this.shortcuts = new Map();
|
||||
this.isEnabled = true;
|
||||
this.helpModalVisible = false;
|
||||
this.currentUser = null;
|
||||
|
||||
// 기본 단축키 등록
|
||||
this.registerDefaultShortcuts();
|
||||
|
||||
// 이벤트 리스너 등록
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 단축키 등록
|
||||
*/
|
||||
registerDefaultShortcuts() {
|
||||
// 전역 단축키
|
||||
this.register('?', () => this.showHelpModal(), '도움말 표시');
|
||||
this.register('Escape', () => this.handleEscape(), '모달/메뉴 닫기');
|
||||
|
||||
// 네비게이션 단축키
|
||||
this.register('g h', () => this.navigateToPage('/index.html', 'issues_create'), '홈 (부적합 등록)');
|
||||
this.register('g v', () => this.navigateToPage('/issue-view.html', 'issues_view'), '부적합 조회');
|
||||
this.register('g d', () => this.navigateToPage('/daily-work.html', 'daily_work'), '일일 공수');
|
||||
this.register('g p', () => this.navigateToPage('/project-management.html', 'projects_manage'), '프로젝트 관리');
|
||||
this.register('g r', () => this.navigateToPage('/reports.html', 'reports'), '보고서');
|
||||
this.register('g a', () => this.navigateToPage('/admin.html', 'users_manage'), '관리자');
|
||||
|
||||
// 액션 단축키
|
||||
this.register('n', () => this.triggerNewAction(), '새 항목 생성');
|
||||
this.register('s', () => this.triggerSaveAction(), '저장');
|
||||
this.register('r', () => this.triggerRefreshAction(), '새로고침');
|
||||
this.register('f', () => this.focusSearchField(), '검색 포커스');
|
||||
|
||||
// 관리자 전용 단축키
|
||||
this.register('ctrl+shift+u', () => this.navigateToPage('/admin.html', 'users_manage'), '사용자 관리 (관리자)');
|
||||
|
||||
console.log('⌨️ 키보드 단축키 등록 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 단축키 등록
|
||||
* @param {string} combination - 키 조합 (예: 'ctrl+s', 'g h')
|
||||
* @param {function} callback - 실행할 함수
|
||||
* @param {string} description - 설명
|
||||
* @param {object} options - 옵션
|
||||
*/
|
||||
register(combination, callback, description, options = {}) {
|
||||
const normalizedCombo = this.normalizeKeyCombination(combination);
|
||||
|
||||
this.shortcuts.set(normalizedCombo, {
|
||||
callback,
|
||||
description,
|
||||
requiresAuth: options.requiresAuth !== false,
|
||||
adminOnly: options.adminOnly || false,
|
||||
pageSpecific: options.pageSpecific || null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 키 조합 정규화
|
||||
*/
|
||||
normalizeKeyCombination(combination) {
|
||||
return combination
|
||||
.toLowerCase()
|
||||
.split(' ')
|
||||
.map(part => part.trim())
|
||||
.filter(part => part.length > 0)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 바인딩
|
||||
*/
|
||||
bindEvents() {
|
||||
let keySequence = [];
|
||||
let sequenceTimer = null;
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!this.isEnabled) return;
|
||||
|
||||
// 입력 필드에서는 일부 단축키만 허용
|
||||
if (this.isInputField(e.target)) {
|
||||
this.handleInputFieldShortcuts(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 키 조합 생성
|
||||
const keyCombo = this.createKeyCombo(e);
|
||||
|
||||
// 시퀀스 타이머 리셋
|
||||
if (sequenceTimer) {
|
||||
clearTimeout(sequenceTimer);
|
||||
}
|
||||
|
||||
// 단일 키 단축키 확인
|
||||
if (this.handleShortcut(keyCombo, e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 시퀀스 키 처리
|
||||
keySequence.push(keyCombo);
|
||||
|
||||
// 시퀀스 단축키 확인
|
||||
const sequenceCombo = keySequence.join(' ');
|
||||
if (this.handleShortcut(sequenceCombo, e)) {
|
||||
keySequence = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// 시퀀스 타이머 설정 (1초 후 리셋)
|
||||
sequenceTimer = setTimeout(() => {
|
||||
keySequence = [];
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 키 조합 생성
|
||||
*/
|
||||
createKeyCombo(event) {
|
||||
const parts = [];
|
||||
|
||||
if (event.ctrlKey) parts.push('ctrl');
|
||||
if (event.altKey) parts.push('alt');
|
||||
if (event.shiftKey) parts.push('shift');
|
||||
if (event.metaKey) parts.push('meta');
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
|
||||
// 특수 키 처리
|
||||
const specialKeys = {
|
||||
' ': 'space',
|
||||
'enter': 'enter',
|
||||
'escape': 'escape',
|
||||
'tab': 'tab',
|
||||
'backspace': 'backspace',
|
||||
'delete': 'delete',
|
||||
'arrowup': 'up',
|
||||
'arrowdown': 'down',
|
||||
'arrowleft': 'left',
|
||||
'arrowright': 'right'
|
||||
};
|
||||
|
||||
const normalizedKey = specialKeys[key] || key;
|
||||
parts.push(normalizedKey);
|
||||
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
/**
|
||||
* 단축키 처리
|
||||
*/
|
||||
handleShortcut(combination, event) {
|
||||
const shortcut = this.shortcuts.get(combination);
|
||||
|
||||
if (!shortcut) return false;
|
||||
|
||||
// 권한 확인
|
||||
if (shortcut.requiresAuth && !this.currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shortcut.adminOnly && this.currentUser?.role !== 'admin') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 페이지별 단축키 확인
|
||||
if (shortcut.pageSpecific && !this.isCurrentPage(shortcut.pageSpecific)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 기본 동작 방지
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// 콜백 실행
|
||||
try {
|
||||
shortcut.callback(event);
|
||||
console.log(`⌨️ 단축키 실행: ${combination}`);
|
||||
} catch (error) {
|
||||
console.error('단축키 실행 실패:', combination, error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력 필드 확인
|
||||
*/
|
||||
isInputField(element) {
|
||||
const inputTypes = ['input', 'textarea', 'select'];
|
||||
const contentEditable = element.contentEditable === 'true';
|
||||
|
||||
return inputTypes.includes(element.tagName.toLowerCase()) || contentEditable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 입력 필드에서의 단축키 처리
|
||||
*/
|
||||
handleInputFieldShortcuts(event) {
|
||||
const keyCombo = this.createKeyCombo(event);
|
||||
|
||||
// 입력 필드에서 허용되는 단축키
|
||||
const allowedInInput = ['escape', 'ctrl+s', 'ctrl+enter'];
|
||||
|
||||
if (allowedInInput.includes(keyCombo)) {
|
||||
this.handleShortcut(keyCombo, event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 페이지 확인
|
||||
*/
|
||||
isCurrentPage(pageId) {
|
||||
return window.commonHeader?.currentPage === pageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 네비게이션
|
||||
*/
|
||||
navigateToPage(url, pageId) {
|
||||
// 권한 확인
|
||||
if (pageId && window.canAccessPage && !window.canAccessPage(pageId)) {
|
||||
this.showNotification('해당 페이지에 접근할 권한이 없습니다.', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 페이지와 같으면 무시
|
||||
if (window.location.pathname === url) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 부드러운 전환
|
||||
if (window.CommonHeader) {
|
||||
window.CommonHeader.navigateToPage(
|
||||
{ preventDefault: () => {}, stopPropagation: () => {} },
|
||||
url,
|
||||
pageId
|
||||
);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 새 항목 생성 액션
|
||||
*/
|
||||
triggerNewAction() {
|
||||
const newButtons = [
|
||||
'button[onclick*="showAddModal"]',
|
||||
'button[onclick*="addNew"]',
|
||||
'#addBtn',
|
||||
'#add-btn',
|
||||
'.btn-add',
|
||||
'button:contains("추가")',
|
||||
'button:contains("등록")',
|
||||
'button:contains("새")'
|
||||
];
|
||||
|
||||
for (const selector of newButtons) {
|
||||
const button = document.querySelector(selector);
|
||||
if (button && !button.disabled) {
|
||||
button.click();
|
||||
this.showNotification('새 항목 생성', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.showNotification('새 항목 생성 버튼을 찾을 수 없습니다.', 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* 저장 액션
|
||||
*/
|
||||
triggerSaveAction() {
|
||||
const saveButtons = [
|
||||
'button[type="submit"]',
|
||||
'button[onclick*="save"]',
|
||||
'#saveBtn',
|
||||
'#save-btn',
|
||||
'.btn-save',
|
||||
'button:contains("저장")',
|
||||
'button:contains("등록")'
|
||||
];
|
||||
|
||||
for (const selector of saveButtons) {
|
||||
const button = document.querySelector(selector);
|
||||
if (button && !button.disabled) {
|
||||
button.click();
|
||||
this.showNotification('저장 실행', 'success');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.showNotification('저장 버튼을 찾을 수 없습니다.', 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* 새로고침 액션
|
||||
*/
|
||||
triggerRefreshAction() {
|
||||
const refreshButtons = [
|
||||
'button[onclick*="load"]',
|
||||
'button[onclick*="refresh"]',
|
||||
'#refreshBtn',
|
||||
'#refresh-btn',
|
||||
'.btn-refresh'
|
||||
];
|
||||
|
||||
for (const selector of refreshButtons) {
|
||||
const button = document.querySelector(selector);
|
||||
if (button && !button.disabled) {
|
||||
button.click();
|
||||
this.showNotification('새로고침 실행', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 기본 새로고침
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 검색 필드 포커스
|
||||
*/
|
||||
focusSearchField() {
|
||||
const searchFields = [
|
||||
'input[type="search"]',
|
||||
'input[placeholder*="검색"]',
|
||||
'input[placeholder*="찾기"]',
|
||||
'#searchInput',
|
||||
'#search',
|
||||
'.search-input'
|
||||
];
|
||||
|
||||
for (const selector of searchFields) {
|
||||
const field = document.querySelector(selector);
|
||||
if (field) {
|
||||
field.focus();
|
||||
field.select();
|
||||
this.showNotification('검색 필드 포커스', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.showNotification('검색 필드를 찾을 수 없습니다.', 'warning');
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape 키 처리
|
||||
*/
|
||||
handleEscape() {
|
||||
// 모달 닫기
|
||||
const modals = document.querySelectorAll('.modal, [id*="modal"], [class*="modal"]');
|
||||
for (const modal of modals) {
|
||||
if (!modal.classList.contains('hidden') && modal.style.display !== 'none') {
|
||||
modal.classList.add('hidden');
|
||||
this.showNotification('모달 닫기', 'info');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 드롭다운 메뉴 닫기
|
||||
const dropdowns = document.querySelectorAll('[id*="menu"], [class*="dropdown"]');
|
||||
for (const dropdown of dropdowns) {
|
||||
if (!dropdown.classList.contains('hidden')) {
|
||||
dropdown.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 포커스 해제
|
||||
if (document.activeElement && document.activeElement !== document.body) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 도움말 모달 표시
|
||||
*/
|
||||
showHelpModal() {
|
||||
if (this.helpModalVisible) {
|
||||
this.hideHelpModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = this.createHelpModal();
|
||||
document.body.appendChild(modal);
|
||||
this.helpModalVisible = true;
|
||||
|
||||
// 외부 클릭으로 닫기
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.hideHelpModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 도움말 모달 생성
|
||||
*/
|
||||
createHelpModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'keyboard-shortcuts-modal';
|
||||
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
||||
|
||||
const shortcuts = this.getAvailableShortcuts();
|
||||
const shortcutGroups = this.groupShortcuts(shortcuts);
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full mx-4 max-h-[80vh] overflow-y-auto">
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
<i class="fas fa-keyboard mr-3 text-blue-600"></i>
|
||||
키보드 단축키
|
||||
</h2>
|
||||
<button onclick="keyboardShortcuts.hideHelpModal()"
|
||||
class="text-gray-400 hover:text-gray-600 text-2xl">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
${Object.entries(shortcutGroups).map(([group, items]) => `
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 mb-4 border-b pb-2">
|
||||
${group}
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
${items.map(item => `
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-gray-600">${item.description}</span>
|
||||
<div class="flex space-x-1">
|
||||
${item.keys.map(key => `
|
||||
<kbd class="px-2 py-1 bg-gray-100 border border-gray-300 rounded text-sm font-mono">
|
||||
${key}
|
||||
</kbd>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 bg-blue-50 rounded-lg">
|
||||
<div class="flex items-start">
|
||||
<i class="fas fa-info-circle text-blue-600 mt-1 mr-3"></i>
|
||||
<div>
|
||||
<h4 class="font-semibold text-blue-900 mb-2">사용 팁</h4>
|
||||
<ul class="text-blue-800 text-sm space-y-1">
|
||||
<li>• 입력 필드에서는 일부 단축키만 작동합니다.</li>
|
||||
<li>• 'g' 키를 누른 후 다른 키를 눌러 페이지를 이동할 수 있습니다.</li>
|
||||
<li>• ESC 키로 모달이나 메뉴를 닫을 수 있습니다.</li>
|
||||
<li>• '?' 키로 언제든 이 도움말을 볼 수 있습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용 가능한 단축키 가져오기
|
||||
*/
|
||||
getAvailableShortcuts() {
|
||||
const available = [];
|
||||
|
||||
for (const [combination, shortcut] of this.shortcuts) {
|
||||
// 권한 확인
|
||||
if (shortcut.requiresAuth && !this.currentUser) continue;
|
||||
if (shortcut.adminOnly && this.currentUser?.role !== 'admin') continue;
|
||||
|
||||
available.push({
|
||||
combination,
|
||||
description: shortcut.description,
|
||||
keys: this.formatKeyCombo(combination)
|
||||
});
|
||||
}
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
/**
|
||||
* 단축키 그룹화
|
||||
*/
|
||||
groupShortcuts(shortcuts) {
|
||||
const groups = {
|
||||
'네비게이션': [],
|
||||
'액션': [],
|
||||
'전역': []
|
||||
};
|
||||
|
||||
shortcuts.forEach(shortcut => {
|
||||
if (shortcut.combination.startsWith('g ')) {
|
||||
groups['네비게이션'].push(shortcut);
|
||||
} else if (['n', 's', 'r', 'f'].includes(shortcut.combination)) {
|
||||
groups['액션'].push(shortcut);
|
||||
} else {
|
||||
groups['전역'].push(shortcut);
|
||||
}
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 키 조합 포맷팅
|
||||
*/
|
||||
formatKeyCombo(combination) {
|
||||
return combination
|
||||
.split(' ')
|
||||
.map(part => {
|
||||
return part
|
||||
.split('+')
|
||||
.map(key => {
|
||||
const keyNames = {
|
||||
'ctrl': 'Ctrl',
|
||||
'alt': 'Alt',
|
||||
'shift': 'Shift',
|
||||
'meta': 'Cmd',
|
||||
'space': 'Space',
|
||||
'enter': 'Enter',
|
||||
'escape': 'Esc',
|
||||
'tab': 'Tab'
|
||||
};
|
||||
return keyNames[key] || key.toUpperCase();
|
||||
})
|
||||
.join(' + ');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 도움말 모달 숨기기
|
||||
*/
|
||||
hideHelpModal() {
|
||||
const modal = document.getElementById('keyboard-shortcuts-modal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
this.helpModalVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 표시
|
||||
*/
|
||||
showNotification(message, type = 'info') {
|
||||
// 기존 알림 제거
|
||||
const existing = document.getElementById('shortcut-notification');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.id = 'shortcut-notification';
|
||||
notification.className = `fixed top-4 right-4 px-4 py-2 rounded-lg shadow-lg z-50 transition-all duration-300 ${this.getNotificationClass(type)}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 3초 후 자동 제거
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 클래스 가져오기
|
||||
*/
|
||||
getNotificationClass(type) {
|
||||
const classes = {
|
||||
'info': 'bg-blue-600 text-white',
|
||||
'success': 'bg-green-600 text-white',
|
||||
'warning': 'bg-yellow-600 text-white',
|
||||
'error': 'bg-red-600 text-white'
|
||||
};
|
||||
return classes[type] || classes.info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 설정
|
||||
*/
|
||||
setUser(user) {
|
||||
this.currentUser = user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 단축키 활성화/비활성화
|
||||
*/
|
||||
setEnabled(enabled) {
|
||||
this.isEnabled = enabled;
|
||||
console.log(`⌨️ 키보드 단축키 ${enabled ? '활성화' : '비활성화'}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 단축키 제거
|
||||
*/
|
||||
unregister(combination) {
|
||||
const normalizedCombo = this.normalizeKeyCombination(combination);
|
||||
return this.shortcuts.delete(normalizedCombo);
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스
|
||||
window.keyboardShortcuts = new KeyboardShortcutManager();
|
||||
368
frontend/static/js/core/page-manager.js
Normal file
368
frontend/static/js/core/page-manager.js
Normal file
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* 페이지 관리자
|
||||
* 모듈화된 페이지들의 생명주기를 관리하고 부드러운 전환을 제공
|
||||
*/
|
||||
|
||||
class PageManager {
|
||||
constructor() {
|
||||
this.currentPage = null;
|
||||
this.loadedModules = new Map();
|
||||
this.pageHistory = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 초기화
|
||||
* @param {string} pageId - 페이지 식별자
|
||||
* @param {Object} options - 초기화 옵션
|
||||
*/
|
||||
async initializePage(pageId, options = {}) {
|
||||
try {
|
||||
// 로딩 표시
|
||||
this.showPageLoader();
|
||||
|
||||
// 사용자 인증 확인
|
||||
const user = await this.checkAuthentication();
|
||||
if (!user) return;
|
||||
|
||||
// 공통 헤더 초기화
|
||||
await this.initializeCommonHeader(user, pageId);
|
||||
|
||||
// 페이지별 권한 체크
|
||||
if (!this.checkPagePermission(pageId, user)) {
|
||||
this.redirectToAccessiblePage();
|
||||
return;
|
||||
}
|
||||
|
||||
// 페이지 모듈 로드 및 초기화
|
||||
await this.loadPageModule(pageId, options);
|
||||
|
||||
// 페이지 히스토리 업데이트
|
||||
this.updatePageHistory(pageId);
|
||||
|
||||
// 로딩 숨기기
|
||||
this.hidePageLoader();
|
||||
|
||||
} catch (error) {
|
||||
console.error('페이지 초기화 실패:', error);
|
||||
this.showErrorPage(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 인증 확인
|
||||
*/
|
||||
async checkAuthentication() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
window.location.href = '/index.html';
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// API가 로드될 때까지 대기
|
||||
await this.waitForAPI();
|
||||
|
||||
const user = await AuthAPI.getCurrentUser();
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error('인증 실패:', error);
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('currentUser');
|
||||
window.location.href = '/index.html';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 로드 대기
|
||||
*/
|
||||
async waitForAPI() {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
while (!window.AuthAPI && attempts < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!window.AuthAPI) {
|
||||
throw new Error('API를 로드할 수 없습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 헤더 초기화
|
||||
*/
|
||||
async initializeCommonHeader(user, pageId) {
|
||||
// 권한 시스템 초기화
|
||||
if (window.pagePermissionManager) {
|
||||
window.pagePermissionManager.setUser(user);
|
||||
}
|
||||
|
||||
// 공통 헤더 초기화
|
||||
if (window.commonHeader) {
|
||||
await window.commonHeader.init(user, pageId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 권한 체크
|
||||
*/
|
||||
checkPagePermission(pageId, user) {
|
||||
// admin은 모든 페이지 접근 가능
|
||||
if (user.role === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 권한 시스템이 로드되지 않았으면 기본 페이지만 허용
|
||||
if (!window.canAccessPage) {
|
||||
return ['issues_create', 'issues_view'].includes(pageId);
|
||||
}
|
||||
|
||||
return window.canAccessPage(pageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 접근 가능한 페이지로 리다이렉트
|
||||
*/
|
||||
redirectToAccessiblePage() {
|
||||
alert('이 페이지에 접근할 권한이 없습니다.');
|
||||
|
||||
// 기본적으로 접근 가능한 페이지로 이동
|
||||
if (window.canAccessPage && window.canAccessPage('issues_view')) {
|
||||
window.location.href = '/issue-view.html';
|
||||
} else {
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 모듈 로드
|
||||
*/
|
||||
async loadPageModule(pageId, options) {
|
||||
// 이미 로드된 모듈이 있으면 재사용
|
||||
if (this.loadedModules.has(pageId)) {
|
||||
const module = this.loadedModules.get(pageId);
|
||||
if (module.reinitialize) {
|
||||
await module.reinitialize(options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 페이지별 모듈 로드
|
||||
const module = await this.createPageModule(pageId, options);
|
||||
if (module) {
|
||||
this.loadedModules.set(pageId, module);
|
||||
this.currentPage = pageId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 모듈 생성
|
||||
*/
|
||||
async createPageModule(pageId, options) {
|
||||
switch (pageId) {
|
||||
case 'issues_create':
|
||||
return new IssuesCreateModule(options);
|
||||
case 'issues_view':
|
||||
return new IssuesViewModule(options);
|
||||
case 'issues_manage':
|
||||
return new IssuesManageModule(options);
|
||||
case 'projects_manage':
|
||||
return new ProjectsManageModule(options);
|
||||
case 'daily_work':
|
||||
return new DailyWorkModule(options);
|
||||
case 'reports':
|
||||
return new ReportsModule(options);
|
||||
case 'users_manage':
|
||||
return new UsersManageModule(options);
|
||||
default:
|
||||
console.warn(`알 수 없는 페이지 ID: ${pageId}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 히스토리 업데이트
|
||||
*/
|
||||
updatePageHistory(pageId) {
|
||||
this.pageHistory.push({
|
||||
pageId,
|
||||
timestamp: new Date(),
|
||||
url: window.location.href
|
||||
});
|
||||
|
||||
// 히스토리 크기 제한 (최대 10개)
|
||||
if (this.pageHistory.length > 10) {
|
||||
this.pageHistory.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 로더 표시
|
||||
*/
|
||||
showPageLoader() {
|
||||
const existingLoader = document.getElementById('page-loader');
|
||||
if (existingLoader) return;
|
||||
|
||||
const loader = document.createElement('div');
|
||||
loader.id = 'page-loader';
|
||||
loader.className = 'fixed inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50';
|
||||
loader.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div>
|
||||
<p class="text-lg font-medium text-gray-700">페이지를 로드하는 중...</p>
|
||||
<p class="text-sm text-gray-500 mt-1">잠시만 기다려주세요</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 로더 숨기기
|
||||
*/
|
||||
hidePageLoader() {
|
||||
const loader = document.getElementById('page-loader');
|
||||
if (loader) {
|
||||
loader.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 페이지 표시
|
||||
*/
|
||||
showErrorPage(error) {
|
||||
this.hidePageLoader();
|
||||
|
||||
const errorContainer = document.createElement('div');
|
||||
errorContainer.className = 'fixed inset-0 bg-gray-50 flex items-center justify-center z-50';
|
||||
errorContainer.innerHTML = `
|
||||
<div class="text-center max-w-md mx-auto p-8">
|
||||
<div class="mb-6">
|
||||
<i class="fas fa-exclamation-triangle text-6xl text-red-500"></i>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">페이지 로드 실패</h2>
|
||||
<p class="text-gray-600 mb-6">${error.message || '알 수 없는 오류가 발생했습니다.'}</p>
|
||||
<div class="space-x-4">
|
||||
<button onclick="window.location.reload()"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
다시 시도
|
||||
</button>
|
||||
<button onclick="window.location.href='/index.html'"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700">
|
||||
홈으로
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(errorContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 정리
|
||||
*/
|
||||
cleanup() {
|
||||
if (this.currentPage && this.loadedModules.has(this.currentPage)) {
|
||||
const module = this.loadedModules.get(this.currentPage);
|
||||
if (module.cleanup) {
|
||||
module.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 페이지 모듈 클래스
|
||||
* 모든 페이지 모듈이 상속받아야 하는 기본 클래스
|
||||
*/
|
||||
class BasePageModule {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.initialized = false;
|
||||
this.eventListeners = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 초기화 (하위 클래스에서 구현)
|
||||
*/
|
||||
async initialize() {
|
||||
throw new Error('initialize 메서드를 구현해야 합니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 재초기화
|
||||
*/
|
||||
async reinitialize(options = {}) {
|
||||
this.cleanup();
|
||||
this.options = { ...this.options, ...options };
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 등록 (자동 정리를 위해)
|
||||
*/
|
||||
addEventListener(element, event, handler) {
|
||||
element.addEventListener(event, handler);
|
||||
this.eventListeners.push({ element, event, handler });
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 정리
|
||||
*/
|
||||
cleanup() {
|
||||
// 등록된 이벤트 리스너 제거
|
||||
this.eventListeners.forEach(({ element, event, handler }) => {
|
||||
element.removeEventListener(event, handler);
|
||||
});
|
||||
this.eventListeners = [];
|
||||
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 표시
|
||||
*/
|
||||
showLoading(container, message = '로딩 중...') {
|
||||
if (typeof container === 'string') {
|
||||
container = document.getElementById(container);
|
||||
}
|
||||
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="text-center">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mb-3"></div>
|
||||
<p class="text-gray-600">${message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 표시
|
||||
*/
|
||||
showError(container, message = '오류가 발생했습니다.') {
|
||||
if (typeof container === 'string') {
|
||||
container = document.getElementById(container);
|
||||
}
|
||||
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-exclamation-triangle text-4xl text-red-500 mb-3"></i>
|
||||
<p class="text-gray-600">${message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스
|
||||
window.pageManager = new PageManager();
|
||||
window.BasePageModule = BasePageModule;
|
||||
317
frontend/static/js/core/page-preloader.js
Normal file
317
frontend/static/js/core/page-preloader.js
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 페이지 프리로더
|
||||
* 사용자가 방문할 가능성이 높은 페이지들을 미리 로드하여 성능 향상
|
||||
*/
|
||||
|
||||
class PagePreloader {
|
||||
constructor() {
|
||||
this.preloadedPages = new Set();
|
||||
this.preloadQueue = [];
|
||||
this.isPreloading = false;
|
||||
this.preloadCache = new Map();
|
||||
this.resourceCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 프리로더 초기화
|
||||
*/
|
||||
init() {
|
||||
// 유휴 시간에 프리로딩 시작
|
||||
this.schedulePreloading();
|
||||
|
||||
// 링크 호버 시 프리로딩
|
||||
this.setupHoverPreloading();
|
||||
|
||||
// 서비스 워커 등록 (캐싱용)
|
||||
this.registerServiceWorker();
|
||||
}
|
||||
|
||||
/**
|
||||
* 우선순위 기반 프리로딩 스케줄링
|
||||
*/
|
||||
schedulePreloading() {
|
||||
// 현재 사용자 권한에 따른 접근 가능한 페이지들
|
||||
const accessiblePages = this.getAccessiblePages();
|
||||
|
||||
// 우선순위 설정
|
||||
const priorityPages = this.getPriorityPages(accessiblePages);
|
||||
|
||||
// 유휴 시간에 프리로딩 시작
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
this.startPreloading(priorityPages);
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
// requestIdleCallback 미지원 브라우저
|
||||
setTimeout(() => {
|
||||
this.startPreloading(priorityPages);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 접근 가능한 페이지 목록 가져오기
|
||||
*/
|
||||
getAccessiblePages() {
|
||||
const allPages = [
|
||||
{ id: 'issues_create', url: '/index.html', priority: 1 },
|
||||
{ id: 'issues_view', url: '/issue-view.html', priority: 1 },
|
||||
{ id: 'issues_manage', url: '/issue-view.html#manage', priority: 2 },
|
||||
{ id: 'projects_manage', url: '/project-management.html', priority: 3 },
|
||||
{ id: 'daily_work', url: '/daily-work.html', priority: 2 },
|
||||
{ id: 'reports', url: '/reports.html', priority: 3 },
|
||||
{ id: 'users_manage', url: '/admin.html', priority: 4 }
|
||||
];
|
||||
|
||||
// 권한 체크
|
||||
return allPages.filter(page => {
|
||||
if (!window.canAccessPage) return false;
|
||||
return window.canAccessPage(page.id);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 우선순위 기반 페이지 정렬
|
||||
*/
|
||||
getPriorityPages(pages) {
|
||||
return pages
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
.slice(0, 3); // 최대 3개 페이지만 프리로드
|
||||
}
|
||||
|
||||
/**
|
||||
* 프리로딩 시작
|
||||
*/
|
||||
async startPreloading(pages) {
|
||||
if (this.isPreloading) return;
|
||||
|
||||
this.isPreloading = true;
|
||||
console.log('🚀 페이지 프리로딩 시작:', pages.map(p => p.id));
|
||||
|
||||
for (const page of pages) {
|
||||
if (this.preloadedPages.has(page.url)) continue;
|
||||
|
||||
try {
|
||||
await this.preloadPage(page);
|
||||
|
||||
// 네트워크 상태 확인 (느린 연결에서는 중단)
|
||||
if (this.isSlowConnection()) {
|
||||
console.log('⚠️ 느린 연결 감지, 프리로딩 중단');
|
||||
break;
|
||||
}
|
||||
|
||||
// CPU 부하 방지를 위한 딜레이
|
||||
await this.delay(500);
|
||||
|
||||
} catch (error) {
|
||||
console.warn('프리로딩 실패:', page.id, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.isPreloading = false;
|
||||
console.log('✅ 페이지 프리로딩 완료');
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 페이지 프리로드
|
||||
*/
|
||||
async preloadPage(page) {
|
||||
try {
|
||||
// HTML 프리로드
|
||||
const htmlResponse = await fetch(page.url, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'text/html' }
|
||||
});
|
||||
|
||||
if (htmlResponse.ok) {
|
||||
const html = await htmlResponse.text();
|
||||
this.preloadCache.set(page.url, html);
|
||||
|
||||
// 페이지 내 리소스 추출 및 프리로드
|
||||
await this.preloadPageResources(html, page.url);
|
||||
|
||||
this.preloadedPages.add(page.url);
|
||||
console.log(`📄 프리로드 완료: ${page.id}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`프리로드 실패: ${page.id}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 리소스 프리로드 (CSS, JS)
|
||||
*/
|
||||
async preloadPageResources(html, baseUrl) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
// CSS 파일 프리로드
|
||||
const cssLinks = doc.querySelectorAll('link[rel="stylesheet"]');
|
||||
for (const link of cssLinks) {
|
||||
const href = this.resolveUrl(link.href, baseUrl);
|
||||
if (!this.resourceCache.has(href)) {
|
||||
this.preloadResource(href, 'style');
|
||||
}
|
||||
}
|
||||
|
||||
// JS 파일 프리로드 (중요한 것만)
|
||||
const scriptTags = doc.querySelectorAll('script[src]');
|
||||
for (const script of scriptTags) {
|
||||
const src = this.resolveUrl(script.src, baseUrl);
|
||||
if (this.isImportantScript(src) && !this.resourceCache.has(src)) {
|
||||
this.preloadResource(src, 'script');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 리소스 프리로드
|
||||
*/
|
||||
preloadResource(url, type) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'preload';
|
||||
link.href = url;
|
||||
link.as = type;
|
||||
|
||||
link.onload = () => {
|
||||
this.resourceCache.set(url, true);
|
||||
};
|
||||
|
||||
link.onerror = () => {
|
||||
console.warn('리소스 프리로드 실패:', url);
|
||||
};
|
||||
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* 중요한 스크립트 판별
|
||||
*/
|
||||
isImportantScript(src) {
|
||||
const importantScripts = [
|
||||
'api.js',
|
||||
'permissions.js',
|
||||
'common-header.js',
|
||||
'page-manager.js'
|
||||
];
|
||||
|
||||
return importantScripts.some(script => src.includes(script));
|
||||
}
|
||||
|
||||
/**
|
||||
* URL 해결
|
||||
*/
|
||||
resolveUrl(url, baseUrl) {
|
||||
if (url.startsWith('http') || url.startsWith('//')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
const base = new URL(baseUrl, window.location.origin);
|
||||
return new URL(url, base).href;
|
||||
}
|
||||
|
||||
/**
|
||||
* 호버 시 프리로딩 설정
|
||||
*/
|
||||
setupHoverPreloading() {
|
||||
let hoverTimeout;
|
||||
|
||||
document.addEventListener('mouseover', (e) => {
|
||||
const link = e.target.closest('a[href]');
|
||||
if (!link) return;
|
||||
|
||||
const href = link.getAttribute('href');
|
||||
if (!href || href.startsWith('#') || href.startsWith('javascript:')) return;
|
||||
|
||||
// 300ms 후 프리로드 (실제 클릭 의도 확인)
|
||||
hoverTimeout = setTimeout(() => {
|
||||
this.preloadOnHover(href);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', (e) => {
|
||||
if (hoverTimeout) {
|
||||
clearTimeout(hoverTimeout);
|
||||
hoverTimeout = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 호버 시 프리로드
|
||||
*/
|
||||
async preloadOnHover(url) {
|
||||
if (this.preloadedPages.has(url)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'text/html' }
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
this.preloadCache.set(url, html);
|
||||
this.preloadedPages.add(url);
|
||||
console.log('🖱️ 호버 프리로드 완료:', url);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('호버 프리로드 실패:', url, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 느린 연결 감지
|
||||
*/
|
||||
isSlowConnection() {
|
||||
if ('connection' in navigator) {
|
||||
const connection = navigator.connection;
|
||||
return connection.effectiveType === 'slow-2g' ||
|
||||
connection.effectiveType === '2g' ||
|
||||
connection.saveData === true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 딜레이 유틸리티
|
||||
*/
|
||||
delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스 워커 등록
|
||||
*/
|
||||
async registerServiceWorker() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/sw.js');
|
||||
console.log('🔧 서비스 워커 등록 완료:', registration);
|
||||
} catch (error) {
|
||||
console.log('서비스 워커 등록 실패:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 프리로드된 페이지 가져오기
|
||||
*/
|
||||
getPreloadedPage(url) {
|
||||
return this.preloadCache.get(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 정리
|
||||
*/
|
||||
clearCache() {
|
||||
this.preloadCache.clear();
|
||||
this.resourceCache.clear();
|
||||
this.preloadedPages.clear();
|
||||
console.log('🗑️ 프리로드 캐시 정리 완료');
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스
|
||||
window.pagePreloader = new PagePreloader();
|
||||
260
frontend/static/js/core/permissions.js
Normal file
260
frontend/static/js/core/permissions.js
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* 단순화된 페이지 권한 관리 시스템
|
||||
* admin/user 구조에서 페이지별 접근 권한을 관리
|
||||
*/
|
||||
|
||||
class PagePermissionManager {
|
||||
constructor() {
|
||||
this.currentUser = null;
|
||||
this.pagePermissions = new Map();
|
||||
this.defaultPages = this.initDefaultPages();
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 페이지 목록 초기화
|
||||
*/
|
||||
initDefaultPages() {
|
||||
return {
|
||||
'issues_create': { title: '부적합 등록', defaultAccess: true },
|
||||
'issues_view': { title: '부적합 조회', defaultAccess: true },
|
||||
'issues_manage': { title: '부적합 관리', defaultAccess: false },
|
||||
'projects_manage': { title: '프로젝트 관리', defaultAccess: false },
|
||||
'daily_work': { title: '일일 공수', defaultAccess: false },
|
||||
'reports': { title: '보고서', defaultAccess: false },
|
||||
'users_manage': { title: '사용자 관리', defaultAccess: false }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 설정
|
||||
* @param {Object} user - 사용자 객체
|
||||
*/
|
||||
setUser(user) {
|
||||
this.currentUser = user;
|
||||
this.loadPagePermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자별 페이지 권한 로드
|
||||
*/
|
||||
async loadPagePermissions() {
|
||||
if (!this.currentUser) return;
|
||||
|
||||
try {
|
||||
// API에서 사용자별 페이지 권한 가져오기
|
||||
const response = await fetch(`/api/users/${this.currentUser.id}/page-permissions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const pagePermissions = await response.json();
|
||||
this.pagePermissions.clear(); // 기존 권한 초기화
|
||||
pagePermissions.forEach(perm => {
|
||||
this.pagePermissions.set(perm.page_name, perm.can_access);
|
||||
});
|
||||
console.log('페이지 권한 로드 완료:', this.pagePermissions);
|
||||
} else {
|
||||
console.warn('페이지 권한 로드 실패, 기본 권한 사용');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('페이지 권한 로드 실패, 기본 권한 사용:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 접근 권한 체크
|
||||
* @param {string} pageName - 체크할 페이지명
|
||||
* @returns {boolean} 접근 권한 여부
|
||||
*/
|
||||
canAccessPage(pageName) {
|
||||
if (!this.currentUser) return false;
|
||||
|
||||
// admin은 모든 페이지 접근 가능
|
||||
if (this.currentUser.role === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 개별 페이지 권한이 설정되어 있으면 우선 적용
|
||||
if (this.pagePermissions.has(pageName)) {
|
||||
return this.pagePermissions.get(pageName);
|
||||
}
|
||||
|
||||
// 기본 권한 확인
|
||||
const pageConfig = this.defaultPages[pageName];
|
||||
return pageConfig ? pageConfig.defaultAccess : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* UI 요소 페이지 권한 제어
|
||||
* @param {string} selector - CSS 선택자
|
||||
* @param {string} pageName - 필요한 페이지 권한
|
||||
* @param {string} action - 'show'|'hide'|'disable'|'enable'
|
||||
*/
|
||||
controlElement(selector, pageName, action = 'show') {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
const hasAccess = this.canAccessPage(pageName);
|
||||
|
||||
elements.forEach(element => {
|
||||
switch (action) {
|
||||
case 'show':
|
||||
element.style.display = hasAccess ? '' : 'none';
|
||||
break;
|
||||
case 'hide':
|
||||
element.style.display = hasAccess ? 'none' : '';
|
||||
break;
|
||||
case 'disable':
|
||||
element.disabled = !hasAccess;
|
||||
if (!hasAccess) {
|
||||
element.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
break;
|
||||
case 'enable':
|
||||
element.disabled = hasAccess;
|
||||
if (hasAccess) {
|
||||
element.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 메뉴 구성 생성
|
||||
* @returns {Array} 페이지 권한에 따른 메뉴 구성
|
||||
*/
|
||||
getMenuConfig() {
|
||||
const menuItems = [
|
||||
{
|
||||
id: 'issues_create',
|
||||
title: '부적합 등록',
|
||||
icon: 'fas fa-plus-circle',
|
||||
path: '#issues/create',
|
||||
pageName: 'issues_create'
|
||||
},
|
||||
{
|
||||
id: 'issues_view',
|
||||
title: '부적합 조회',
|
||||
icon: 'fas fa-search',
|
||||
path: '#issues/view',
|
||||
pageName: 'issues_view'
|
||||
},
|
||||
{
|
||||
id: 'issues_manage',
|
||||
title: '부적합 관리',
|
||||
icon: 'fas fa-tasks',
|
||||
path: '#issues/manage',
|
||||
pageName: 'issues_manage'
|
||||
},
|
||||
{
|
||||
id: 'projects_manage',
|
||||
title: '프로젝트 관리',
|
||||
icon: 'fas fa-folder-open',
|
||||
path: '#projects/manage',
|
||||
pageName: 'projects_manage'
|
||||
},
|
||||
{
|
||||
id: 'daily_work',
|
||||
title: '일일 공수',
|
||||
icon: 'fas fa-calendar-check',
|
||||
path: '#daily-work',
|
||||
pageName: 'daily_work'
|
||||
},
|
||||
{
|
||||
id: 'reports',
|
||||
title: '보고서',
|
||||
icon: 'fas fa-chart-bar',
|
||||
path: '#reports',
|
||||
pageName: 'reports'
|
||||
},
|
||||
{
|
||||
id: 'users_manage',
|
||||
title: '사용자 관리',
|
||||
icon: 'fas fa-users-cog',
|
||||
path: '#users/manage',
|
||||
pageName: 'users_manage'
|
||||
}
|
||||
];
|
||||
|
||||
// 페이지 권한에 따라 메뉴 필터링
|
||||
return menuItems.filter(item => this.canAccessPage(item.pageName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 권한 부여
|
||||
* @param {number} userId - 사용자 ID
|
||||
* @param {string} pageName - 페이지명
|
||||
* @param {boolean} canAccess - 접근 허용 여부
|
||||
* @param {string} notes - 메모
|
||||
*/
|
||||
async grantPageAccess(userId, pageName, canAccess, notes = '') {
|
||||
if (this.currentUser.role !== 'admin') {
|
||||
throw new Error('관리자만 권한을 설정할 수 있습니다.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/page-permissions/grant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
page_name: pageName,
|
||||
can_access: canAccess,
|
||||
notes: notes
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('페이지 권한 설정 실패');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('페이지 권한 설정 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 페이지 권한 목록 조회
|
||||
* @param {number} userId - 사용자 ID
|
||||
* @returns {Array} 페이지 권한 목록
|
||||
*/
|
||||
async getUserPagePermissions(userId) {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${userId}/page-permissions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('페이지 권한 목록 조회 실패');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('페이지 권한 목록 조회 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 페이지 목록과 설명 가져오기
|
||||
* @returns {Object} 페이지 목록
|
||||
*/
|
||||
getAllPages() {
|
||||
return this.defaultPages;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 페이지 권한 관리자 인스턴스
|
||||
window.pagePermissionManager = new PagePermissionManager();
|
||||
|
||||
// 편의 함수들
|
||||
window.canAccessPage = (pageName) => window.pagePermissionManager.canAccessPage(pageName);
|
||||
window.controlElement = (selector, pageName, action) => window.pagePermissionManager.controlElement(selector, pageName, action);
|
||||
Reference in New Issue
Block a user