/** * 키보드 단축키 관리자 * 전역 키보드 단축키를 관리하고 사용자 경험을 향상시킵니다. */ 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 = `

키보드 단축키

${Object.entries(shortcutGroups).map(([group, items]) => `

${group}

${items.map(item => `
${item.description}
${item.keys.map(key => ` ${key} `).join('')}
`).join('')}
`).join('')}

사용 팁

  • • 입력 필드에서는 일부 단축키만 작동합니다.
  • • 'g' 키를 누른 후 다른 키를 눌러 페이지를 이동할 수 있습니다.
  • • ESC 키로 모달이나 메뉴를 닫을 수 있습니다.
  • • '?' 키로 언제든 이 도움말을 볼 수 있습니다.
`; 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();