dcinside shortcut

디시인사이드(dcinside) 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.

// ==UserScript==
// @name         dcinside shortcut
// @namespace    http://tampermonkey.net/
// @version      1.1.3
// @description  디시인사이드(dcinside) 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.
//               - 글 목록에 번호 추가 (1~100번까지 표시)
//               - 숫자 키 (1~9, 0)로 해당 번호의 글로 이동 (0은 10번 글)
//               - ` or . + 숫자 입력 + ` or .으로 특정 번호의 글로 이동 (1~100번)
//               - ALT + 숫자 (1~9, 0): 즐겨찾는 갤러리 등록/이동
//               - ALT + `: 즐겨찾는 갤러리 목록 표시/숨기기
//               - ALT + W: 글쓰기 등록
//               - W: 글쓰기 페이지로 이동
//               - C: 댓글 입력창으로 커서 이동
//               - D: 댓글 새로고침 및 스크롤
//               - R: 페이지 새로고침
//               - Q: 페이지 최상단으로 스크롤
//               - E: 글 목록으로 스크롤
//               - F: 전체글 보기로 이동
//               - G: 개념글 보기로 이동
//               - A: 이전 페이지로 이동
//               - S: 다음 페이지로 이동
//               - Z: 이전 글로 이동
//               - X: 다음 글로 이동
// @author       노노하꼬
// @match        *://gall.dcinside.com/*
// @match        *://www.dcinside.com/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=dcinside.com
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// @supportURL   https://gallog.dcinside.com/nonohako/guestbook
// ==/UserScript==
(function() {
    'use strict';

    // Constants
    const FAVORITE_GALLERIES_KEY = 'dcinside_favorite_galleries';
    const isTampermonkey = typeof GM_setValue !== 'undefined' && typeof GM_getValue !== 'undefined';

    // Storage Module
    const Storage = {
        async getFavorites() {
            let favorites = {};
            try {
                if (isTampermonkey) {
                    favorites = GM_getValue(FAVORITE_GALLERIES_KEY, {});
                } else {
                    const data = localStorage.getItem(FAVORITE_GALLERIES_KEY) ||
                          this.getCookie(FAVORITE_GALLERIES_KEY);
                    favorites = data ? JSON.parse(data) : {};
                }
            } catch (error) {
                console.error('Failed to retrieve favorites:', error);
            }
            return favorites;
        },

        saveFavorites(favorites) {
            try {
                const data = JSON.stringify(favorites);
                if (isTampermonkey) {
                    GM_setValue(FAVORITE_GALLERIES_KEY, favorites);
                } else {
                    localStorage.setItem(FAVORITE_GALLERIES_KEY, data);
                    this.setCookie(FAVORITE_GALLERIES_KEY, data);
                }
            } catch (error) {
                console.error('Failed to save favorites:', error);
                alert('즐겨찾기 저장에 실패했습니다. 브라우저의 저장소 설정을 확인해주세요.');
            }
        },

        getCookie(name) {
            const value = document.cookie.match(`(^|;)\\s*${name}=([^;]+)`);
            return value ? decodeURIComponent(value[2]) : null;
        },

        setCookie(name, value) {
            const date = new Date();
            date.setFullYear(date.getFullYear() + 1);
            document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; path=/; domain=.dcinside.com`;
        },

        async getAltNumberEnabled() {
            if (isTampermonkey) {
                return GM_getValue('altNumberEnabled', true); // 기본값: 활성화
            } else {
                const data = localStorage.getItem('altNumberEnabled') || this.getCookie('altNumberEnabled');
                return data !== null ? JSON.parse(data) : true;
            }
        },

        saveAltNumberEnabled(enabled) {
            try {
                const data = JSON.stringify(enabled);
                if (isTampermonkey) {
                    GM_setValue('altNumberEnabled', enabled);
                } else {
                    localStorage.setItem('altNumberEnabled', data);
                    this.setCookie('altNumberEnabled', data);
                }
            } catch (error) {
                console.error('Failed to save altNumberEnabled:', error);
            }
        },

        async getShortcutEnabled(key) {
            if (isTampermonkey) {
                return GM_getValue(key, true);
            } else {
                const data = localStorage.getItem(key) || this.getCookie(key);
                return data !== null ? JSON.parse(data) : true;
            }
        },

        saveShortcutEnabled(key, enabled) {
            try {
                const data = JSON.stringify(enabled);
                if (isTampermonkey) {
                    GM_setValue(key, enabled);
                } else {
                    localStorage.setItem(key, data);
                    this.setCookie(key, data);
                }
            } catch (error) {
                console.error(`Failed to save ${key}:`, error);
            }
        },

        async getShortcutKey(key) {
            if (isTampermonkey) {
                return GM_getValue(key, null);
            } else {
                const data = localStorage.getItem(key) || this.getCookie(key);
                return data !== null ? data : null;
            }
        },

        saveShortcutKey(key, value) {
            try {
                if (isTampermonkey) {
                    GM_setValue(key, value);
                } else {
                    localStorage.setItem(key, value);
                    this.setCookie(key, value);
                }
            } catch (error) {
                console.error(`Failed to save ${key}:`, error);
            }
        }
    };

    // UI Module
    const UI = {
        createElement(tag, styles, props = {}) {
            const el = document.createElement(tag);
            Object.assign(el.style, styles);
            Object.assign(el, props);
            return el;
        },

        async showFavorites() {
            const container = this.createElement('div', {
                position: 'fixed', top: '50%', left: '50%',
                transform: 'translate(-50%, -50%)', backgroundColor: '#ffffff',
                padding: '20px', borderRadius: '16px', boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
                zIndex: '10000', width: '360px', maxHeight: '80vh', overflowY: 'auto',
                fontFamily: "'Roboto', sans-serif", border: '1px solid #e0e0e0',
                transition: 'opacity 0.2s ease-in-out', opacity: '0'
            });
            setTimeout(() => container.style.opacity = '1', 10);

            this.loadRobotoFont();
            container.appendChild(this.createTitle());
            const list = this.createList();
            container.appendChild(list);
            container.appendChild(this.createAddContainer());
            container.appendChild(this.createToggleAltNumber()); // 새로 추가: 토글 버튼
            container.appendChild(this.createShortcutManagerButton());
            container.appendChild(this.createCloseButton(container));

            document.body.appendChild(container);
            await this.updateFavoritesList(list);
        },

        loadRobotoFont() {
            if (!document.querySelector('link[href*="Roboto"]')) {
                document.head.appendChild(this.createElement('link', {}, {
                    rel: 'stylesheet',
                    href: 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'
                }));
            }
        },

        createToggleAltNumber() {
            const container = this.createElement('div', {
                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                margin: '15px 0', padding: '10px', backgroundColor: '#f5f5f5',
                borderRadius: '10px'
            });

            const label = this.createElement('span', {
                fontSize: '14px', fontWeight: '500', color: '#424242'
            }, { textContent: 'ALT + 숫자 단축키 사용' });

            const checkbox = this.createElement('input', {
                marginLeft: 'auto'
            }, { type: 'checkbox' });

            Storage.getAltNumberEnabled().then(enabled => {
                checkbox.checked = enabled;
            });

            checkbox.addEventListener('change', async () => {
                await Storage.saveAltNumberEnabled(checkbox.checked);
                UI.showAlert(`ALT + 숫자 단축키가 ${checkbox.checked ? '활성화' : '비활성화'}되었습니다.`);
            });

            container.appendChild(label);
            container.appendChild(checkbox);
            return container;
        },

        createShortcutManagerButton() {
            const button = this.createElement('button', {
                display: 'block', width: '100%', padding: '10px', marginTop: '15px',
                backgroundColor: '#4caf50', color: '#ffffff', border: 'none',
                borderRadius: '10px', fontSize: '15px', fontWeight: '500',
                cursor: 'pointer', transition: 'background-color 0.2s ease'
            }, { textContent: '단축키 관리' });

            button.addEventListener('mouseenter', () => button.style.backgroundColor = '#388e3c');
            button.addEventListener('mouseleave', () => button.style.backgroundColor = '#4caf50');
            button.addEventListener('click', () => this.showShortcutManager());
            return button;
        },

        showShortcutManager() {
            const container = this.createElement('div', {
                position: 'fixed', top: '50%', left: '50%',
                transform: 'translate(-50%, -50%)', backgroundColor: '#ffffff',
                padding: '20px', borderRadius: '16px', boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
                zIndex: '10000', width: '400px', maxHeight: '80vh', overflowY: 'auto',
                fontFamily: "'Roboto', sans-serif", border: '1px solid #e0e0e0',
                transition: 'opacity 0.2s ease-in-out', opacity: '0'
            });
            setTimeout(() => container.style.opacity = '1', 10);

            this.loadRobotoFont();
            container.appendChild(this.createTitle('단축키 관리'));

            // 단축키 활성화/비활성화 토글 추가
            container.appendChild(this.createShortcutToggle('W - 글쓰기', 'shortcutWEnabled'));
            container.appendChild(this.createShortcutToggle('C - 댓글 입력', 'shortcutCEnabled'));
            container.appendChild(this.createShortcutToggle('D - 댓글 새로고침', 'shortcutDEnabled'));
            container.appendChild(this.createShortcutToggle('R - 페이지 새로고침', 'shortcutREnabled'));
            container.appendChild(this.createShortcutToggle('Q - 최상단 스크롤', 'shortcutQEnabled'));
            container.appendChild(this.createShortcutToggle('E - 글 목록 스크롤', 'shortcutEEnabled'));
            container.appendChild(this.createShortcutToggle('F - 전체글 보기', 'shortcutFEnabled'));
            container.appendChild(this.createShortcutToggle('G - 개념글 보기', 'shortcutGEnabled'));
            container.appendChild(this.createShortcutToggle('A - 이전 페이지', 'shortcutAEnabled'));
            container.appendChild(this.createShortcutToggle('S - 다음 페이지', 'shortcutSEnabled'));
            container.appendChild(this.createShortcutToggle('Z - 이전 글', 'shortcutZEnabled'));
            container.appendChild(this.createShortcutToggle('X - 다음 글', 'shortcutXEnabled'));

            container.appendChild(this.createCloseButton(container));
            document.body.appendChild(container);
        },

        createShortcutToggle(label, storageKey) {
            const container = this.createElement('div', {
                display: 'flex', alignItems: 'center',
                margin: '10px 0', padding: '10px', backgroundColor: '#f5f5f5',
                borderRadius: '10px', gap: '10px'
            });

            const labelEl = this.createElement('span', {
                fontSize: '14px', fontWeight: '500', color: '#424242',
                width: '150px'
            }, { textContent: label });

            const checkbox = this.createElement('input', {
                marginLeft: 'auto'
            }, { type: 'checkbox' });

            Storage.getShortcutEnabled(storageKey).then(enabled => {
                checkbox.checked = enabled;
            });

            checkbox.addEventListener('change', async () => {
                await Storage.saveShortcutEnabled(storageKey, checkbox.checked);
                UI.showAlert(`${label} 단축키가 ${checkbox.checked ? '활성화' : '비활성화'}되었습니다.`);
            });

            // 단축키 변경 입력 필드 추가
            const keyInput = this.createElement('input', {
                width: '60px', padding: '5px',
                border: '1px solid #e0e0e0', borderRadius: '4px',
                fontSize: '12px', outline: 'none', textAlign: 'center'
            }, { type: 'text', placeholder: '키 변경', maxLength: '1' });

            // 기본 단축키 매핑
            const defaultKeys = {
                'shortcutWKey': 'W',
                'shortcutCKey': 'C',
                'shortcutDKey': 'D',
                'shortcutRKey': 'R',
                'shortcutQKey': 'Q',
                'shortcutEKey': 'E',
                'shortcutFKey': 'F',
                'shortcutGKey': 'G',
                'shortcutAKey': 'A',
                'shortcutSKey': 'S',
                'shortcutZKey': 'Z',
                'shortcutXKey': 'X'
            };

            Storage.getShortcutKey(storageKey.replace('Enabled', 'Key')).then(savedKey => {
                keyInput.placeholder = savedKey || defaultKeys[storageKey.replace('Enabled', 'Key')];
            });

            keyInput.addEventListener('keydown', (e) => {
                e.stopPropagation();
                const key = e.key.toUpperCase();
                if (key.length === 1 && /^[A-Z]$/.test(key)) {
                    // 중복 단축키 검사
                    Storage.getShortcutKey(storageKey.replace('Enabled', 'Key')).then(savedKey => {
                        const allKeys = Object.values(defaultKeys);
                        if (allKeys.includes(key) && key !== defaultKeys[storageKey.replace('Enabled', 'Key')]) {
                            UI.showAlert(`'${key}' 단축키는 이미 사용 중입니다. 다른 키를 선택해주세요.`);
                        } else {
                            Storage.saveShortcutKey(storageKey.replace('Enabled', 'Key'), key);
                            UI.showAlert(`${label} 단축키가 ${key}로 변경되었습니다.`);
                            keyInput.placeholder = key;
                            keyInput.value = '';
                        }
                    });
                }
            });

            container.appendChild(labelEl);
            container.appendChild(checkbox);
            container.appendChild(keyInput);
            return container;
        },

        createTitle() {
            return this.createElement('h3', {
                fontSize: '18px', fontWeight: '700', color: '#212121',
                margin: '0 0 15px 0', paddingBottom: '10px', borderBottom: '1px solid #e0e0e0'
            }, { textContent: '즐겨찾는 갤러리' });
        },

        createList() {
            return this.createElement('ul', {
                listStyle: 'none', margin: '0', padding: '0',
                maxHeight: '50vh', overflowY: 'auto'
            });
        },

        async updateFavoritesList(list) {
            list.innerHTML = '';
            const favorites = await Storage.getFavorites();
            Object.entries(favorites).forEach(([key, gallery]) => {
                list.appendChild(this.createFavoriteItem(key, gallery));
            });
        },

        createFavoriteItem(key, gallery) {
            const item = this.createElement('li', {
                display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                padding: '12px 15px', margin: '5px 0', backgroundColor: '#fafafa',
                borderRadius: '10px', transition: 'background-color 0.2s ease', cursor: 'pointer'
            });

            item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f0f0f0');
            item.addEventListener('mouseleave', () => item.style.backgroundColor = '#fafafa');
            item.addEventListener('click', () => this.navigateToGallery(gallery));

            // Ensure we display the gallery name properly
            const name = gallery.name || gallery.galleryName || gallery.galleryId || 'Unknown Gallery';
            item.appendChild(this.createElement('span', {
                fontSize: '15px', fontWeight: '400', color: '#424242',
                flexGrow: '1', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
            }, { textContent: `${key}: ${name}` }));

            item.appendChild(this.createRemoveButton(key));
            return item;
        },

        createRemoveButton(key) {
            const button = this.createElement('button', {
                backgroundColor: 'transparent', color: '#757575', border: 'none',
                borderRadius: '50%', width: '24px', height: '24px', fontSize: '16px',
                lineHeight: '1', cursor: 'pointer', transition: 'color 0.2s ease, background-color 0.2s ease'
            }, { textContent: '✕' });

            button.addEventListener('mouseenter', () => {
                button.style.color = '#d32f2f';
                button.style.backgroundColor = '#ffebee';
            });
            button.addEventListener('mouseleave', () => {
                button.style.color = '#757575';
                button.style.backgroundColor = 'transparent';
            });
            button.addEventListener('click', async (e) => {
                e.stopPropagation();
                const favorites = await Storage.getFavorites();
                delete favorites[key];
                Storage.saveFavorites(favorites);
                await this.updateFavoritesList(button.closest('ul'));
            });
            return button;
        },

        createAddContainer() {
            const container = this.createElement('div', {
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                gap: '8px', margin: '15px 0', padding: '15px', backgroundColor: '#f5f5f5',
                borderRadius: '10px'
            });

            const input = this.createElement('input', {
                width: '45px', padding: '8px', border: '1px solid #e0e0e0',
                borderRadius: '8px', fontSize: '14px', textAlign: 'center',
                outline: 'none', transition: 'border-color 0.2s ease', backgroundColor: '#ffffff'
            }, { type: 'text', placeholder: '0-9' });

            input.addEventListener('focus', () => input.style.borderColor = '#1976d2');
            input.addEventListener('blur', () => input.style.borderColor = '#e0e0e0');

            const button = this.createElement('button', {
                padding: '8px 16px', backgroundColor: '#1976d2', color: '#ffffff',
                border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '500',
                cursor: 'pointer', transition: 'background-color 0.2s ease', flexGrow: '1'
            }, { textContent: '즐겨찾기 추가' });

            button.addEventListener('mouseenter', () => button.style.backgroundColor = '#1565c0');
            button.addEventListener('mouseleave', () => button.style.backgroundColor = '#1976d2');
            button.addEventListener('click', (e) => {
                e.stopPropagation();
                const digit = input.value.trim();
                if (!/^[0-9]$/.test(digit)) {
                    alert('0부터 9까지의 숫자를 입력해주세요.');
                    return;
                }
                Gallery.handleFavoriteKey(digit);
                input.value = '';
            });

            container.appendChild(input);
            container.appendChild(button);
            return container;
        },

        createCloseButton(container) {
            const button = this.createElement('button', {
                display: 'block', width: '100%', padding: '10px', marginTop: '15px',
                backgroundColor: '#1976d2', color: '#ffffff', border: 'none',
                borderRadius: '10px', fontSize: '15px', fontWeight: '500',
                cursor: 'pointer', transition: 'background-color 0.2s ease'
            }, { textContent: 'Close' });

            button.addEventListener('mouseenter', () => button.style.backgroundColor = '#1565c0');
            button.addEventListener('mouseleave', () => button.style.backgroundColor = '#1976d2');
            button.addEventListener('click', () => {
                container.style.opacity = '0';
                setTimeout(() => document.body.removeChild(container), 200);
            });
            return button;
        },

        navigateToGallery(gallery) {
            const url = gallery.galleryType === 'board'
            ? `https://gall.dcinside.com/board/lists?id=${gallery.galleryId}`
                : `https://gall.dcinside.com/${gallery.galleryType}/board/lists?id=${gallery.galleryId}`;
            window.location.href = url;
        },

        showAlert(message) {
            const alert = this.createElement('div', {
                position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%)',
                backgroundColor: 'rgba(0, 0, 0, 0.8)', color: 'white', padding: '15px 20px',
                borderRadius: '8px', fontSize: '14px', zIndex: '10000', transition: 'opacity 0.3s ease'
            }, { textContent: message });

            document.body.appendChild(alert);
            setTimeout(() => {
                alert.style.opacity = '0';
                setTimeout(() => document.body.removeChild(alert), 300);
            }, 2000);
        }
    };

    // Gallery Module
    const Gallery = {
        isMainPage() {
            const { href } = window.location;
            return href.includes('/lists') && href.includes('id=');
        },

        getInfo() {
            if (!this.isMainPage()) return { galleryType: '', galleryId: '', galleryName: '' };

            const { href } = window.location;
            const galleryType = href.includes('/person/') ? 'person' :
            href.includes('mgallery') ? 'mgallery' :
            href.includes('mini') ? 'mini' : 'board';
            const galleryId = href.match(/id=([^&]+)/)?.[1] || '';
            const nameEl = document.querySelector('div.fl.clear h2 a');
            const galleryName = nameEl
            ? Array.from(nameEl.childNodes)
            .filter(node => node.nodeType === Node.TEXT_NODE)
            .map(node => node.textContent.trim())
            .join('') || galleryId
            : galleryId;

            return { galleryType, galleryId, galleryName };
        },

        async handleFavoriteKey(key) {
            const favorites = await Storage.getFavorites();
            const info = this.getInfo();

            if (favorites[key]) {
                UI.navigateToGallery(favorites[key]);
            } else if (this.isMainPage()) {
                // Ensure galleryName is saved as 'name' for UI compatibility
                favorites[key] = {
                    galleryType: info.galleryType,
                    galleryId: info.galleryId,
                    name: info.galleryName
                };
                Storage.saveFavorites(favorites);
                UI.showAlert(`${info.galleryName}이(가) ${key}번에 등록되었습니다.`);
                const list = document.querySelector('ul[style*="max-height: 50vh"]');
                if (list) await UI.updateFavoritesList(list);
            } else {
                alert('즐겨찾기 등록은 갤러리 메인 페이지에서만 가능합니다.');
            }
        },

        getPageInfo() {
            const { href } = window.location;
            const galleryType = href.includes('mgallery') ? 'mgallery' :
            href.includes('mini') ? 'mini' :
            href.includes('person') ? 'person' : 'board';
            const galleryId = href.match(/id=([^&]+)/)?.[1] || '';
            const currentPage = parseInt(href.match(/page=(\d+)/)?.[1] || '1', 10);
            const isRecommendMode = href.includes('exception_mode=recommend');

            return { galleryType, galleryId, currentPage, isRecommendMode };
        }
    };

    // Post Navigation Module
    const Posts = {
        isValidPost(numCell, titleCell, subjectCell) {
            if (!numCell || !titleCell) return false;
            const row = numCell.closest('tr');
            if (row?.classList.contains('block-disable') ||
                row?.classList.contains('list_trend') ||
                row?.style.display === 'none') return false;

            const numText = numCell.textContent.trim().replace(/\[\d+\]\s*|\[\+\d+\]\s*|\[\-\d+\]\s*/, '');
            if (['AD', '공지', '설문', 'Notice'].includes(numText) || isNaN(numText)) return false;
            if (titleCell.querySelector('em.icon_notice')) return false;
            if (subjectCell?.textContent.trim().match(/AD|공지|설문|뉴스|고정|이슈/)) return false;
            return true;
        },

        getValidPosts() {
            const rows = document.querySelectorAll('table.gall_list tbody tr');
            const validPosts = [];
            let currentIndex = -1;

            rows.forEach((row, index) => {
                const numCell = row.querySelector('td.gall_num');
                const titleCell = row.querySelector('td.gall_tit');
                const subjectCell = row.querySelector('td.gall_subject');
                if (!this.isValidPost(numCell, titleCell, subjectCell)) return;

                const link = titleCell.querySelector('a:first-child');
                if (link) {
                    validPosts.push({ row, link });
                    if (numCell.querySelector('.sp_img.crt_icon')) currentIndex = validPosts.length - 1;
                }
            });

            return { validPosts, currentIndex };
        },

        addNumberLabels() {
            const tbody = document.querySelector('table.gall_list tbody');
            if (!tbody || tbody.querySelector('.number-label')) return;

            const { validPosts } = this.getValidPosts();
            validPosts.slice(0, 100).forEach((post, i) => {
                const numCell = post.row.querySelector('td.gall_num');
                if (numCell.querySelector('.sp_img.crt_icon')) return;

                const label = UI.createElement('span', {
                    color: '#ff6600', fontWeight: 'bold'
                }, { className: 'number-label', textContent: `[${i + 1}] ` });
                numCell.prepend(label);
            });
        },

        navigate(number) {
            const { validPosts } = this.getValidPosts();
            const index = parseInt(number, 10) - 1;
            if (index >= 0 && index < validPosts.length) {
                validPosts[index].link.click();
                return true;
            }
            return false;
        }
    };

    // Event Handlers
    const Events = {
        numberInput: { mode: false, buffer: '', timeout: null, display: null },

        async handleKeydown(event) {
            if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
                if (event.key === 'w' || event.key === 'W') {
                    event.preventDefault();
                    const writeButton = document.querySelector('button.btn_lightpurple.btn_svc.write[type="image"]');
                    if (writeButton) writeButton.click();
                } else if (event.key >= '0' && event.key <= '9') {
                    event.preventDefault();
                    // 단축키 활성화 여부 확인
                    const enabled = await Storage.getAltNumberEnabled();
                    if (enabled) {
                        Gallery.handleFavoriteKey(event.key);
                    }
                } else if (event.key === '`') {
                    event.preventDefault();
                    const ui = document.querySelector('div[style*="position: fixed; top: 50%"]');
                    ui ? ui.remove() : UI.showFavorites();
                }
            } else if (!event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
                const enabled = await Storage.getShortcutEnabled(`shortcut${event.key.toUpperCase()}Enabled`);
                if (enabled) {
                    this.handleNavigationKeys(event);
                }
            }
        },

        handleNavigationKeys(event) {
            const active = document.activeElement;
            if (active && ['TEXTAREA', 'INPUT'].includes(active.tagName) || active.isContentEditable) return;

            if (['`', '.'].includes(event.key)) {
                event.preventDefault();
                this.toggleNumberInput(event.key);
                return;
            }

            if (this.numberInput.mode) {
                this.handleNumberInput(event);
                return;
            }

            if (event.key >= '0' && event.key <= '9') {
                const index = event.key === '0' ? 9 : parseInt(event.key, 10) - 1;
                const { validPosts } = Posts.getValidPosts();
                if (index < validPosts.length) validPosts[index].link.click();
                return;
            }

            this.handleShortcuts(event.key.toUpperCase(), event);
        },

        toggleNumberInput(key) {
            if (this.numberInput.mode && this.numberInput.buffer) {
                Posts.navigate(this.numberInput.buffer);
                this.exitNumberInput();
            } else {
                this.numberInput.mode = true;
                this.numberInput.buffer = '';
                this.updateNumberDisplay('Post number: ');
                this.resetNumberTimeout();
            }
        },

        handleNumberInput(event) {
            event.preventDefault();
            if (event.key >= '0' && event.key <= '9') {
                this.numberInput.buffer += event.key;
                this.updateNumberDisplay(`Post number: ${this.numberInput.buffer}`);
                this.resetNumberTimeout();
            } else if (event.key === 'Enter' && this.numberInput.buffer) {
                Posts.navigate(this.numberInput.buffer);
                this.exitNumberInput();
            } else if (event.key === 'Escape') {
                this.exitNumberInput();
            }
        },

        updateNumberDisplay(text) {
            if (!this.numberInput.display) {
                this.numberInput.display = UI.createElement('div', {
                    position: 'fixed', top: '10px', right: '10px', backgroundColor: 'rgba(0,0,0,0.7)',
                    color: 'white', padding: '10px 15px', borderRadius: '5px', fontSize: '16px',
                    fontWeight: 'bold', zIndex: '9999'
                });
                document.body.appendChild(this.numberInput.display);
            }
            this.numberInput.display.textContent = text;
        },

        resetNumberTimeout() {
            clearTimeout(this.numberInput.timeout);
            this.numberInput.timeout = setTimeout(() => this.exitNumberInput(), 3000);
        },

        exitNumberInput() {
            this.numberInput.mode = false;
            this.numberInput.buffer = '';
            clearTimeout(this.numberInput.timeout);
            this.numberInput.timeout = null;
            if (this.numberInput.display) {
                this.numberInput.display.remove();
                this.numberInput.display = null;
            }
        },

        async handleShortcuts(key, event) {
            const { galleryType, galleryId, currentPage, isRecommendMode } = Gallery.getPageInfo();
            const baseUrl = `${galleryType === 'board' ? '' : galleryType}/board/lists/?id=${galleryId}`;
            const recommendUrl = `${baseUrl}&exception_mode=recommend`;
            const navigate = url => document.readyState === 'complete' ? window.location.href = url : window.addEventListener('load', () => window.location.href = url, { once: true });

            // Check if we're on a post view page
            const isViewPage = window.location.href.match(/\/board\/view\/?/) || window.location.href.match(/no=\d+/);
            const currentPostNo = isViewPage ? window.location.href.match(/no=(\d+)/)?.[1] : null;

            // Get saved shortcut keys
            const savedKeys = {
                'W': await Storage.getShortcutKey('shortcutWKey') || 'W',
                'C': await Storage.getShortcutKey('shortcutCKey') || 'C',
                'D': await Storage.getShortcutKey('shortcutDKey') || 'D',
                'R': await Storage.getShortcutKey('shortcutRKey') || 'R',
                'Q': await Storage.getShortcutKey('shortcutQKey') || 'Q',
                'E': await Storage.getShortcutKey('shortcutEKey') || 'E',
                'F': await Storage.getShortcutKey('shortcutFKey') || 'F',
                'G': await Storage.getShortcutKey('shortcutGKey') || 'G',
                'A': await Storage.getShortcutKey('shortcutAKey') || 'A',
                'S': await Storage.getShortcutKey('shortcutSKey') || 'S',
                'Z': await Storage.getShortcutKey('shortcutZKey') || 'Z',
                'X': await Storage.getShortcutKey('shortcutXKey') || 'X'
            };

            switch (key) {
                case savedKeys['W']: document.querySelector('button#btn_write')?.click(); break;
                case savedKeys['C']:
                    // Prevent 'c' character from being entered when focusing on comment box
                    event.preventDefault();
                    document.querySelector('textarea[id^="memo_"]')?.focus();
                    break;
                case savedKeys['D']: document.querySelector('button.btn_cmt_refresh')?.click(); break;
                case savedKeys['R']: location.reload(); break;
                case savedKeys['Q']: window.scrollTo(0, 0); break;
                case savedKeys['E']: document.querySelector('table.gall_list')?.scrollIntoView({ block: 'start' }); break;
                case savedKeys['F']: navigate(`https://gall.dcinside.com/${baseUrl}`); break; // 개념글 -> 일반 목록
                case savedKeys['G']: navigate(`https://gall.dcinside.com/${recommendUrl}`); break; // 일반 -> 개념글
                case savedKeys['A']:
                    if (currentPage > 1) {
                        // If we're on a post view page, maintain the post number when changing pages
                        if (isViewPage && currentPostNo) {
                            navigate(`${window.location.href.replace(/page=\d+/, `page=${currentPage - 1}`)}`);
                        } else {
                            navigate(`https://gall.dcinside.com/${isRecommendMode ? recommendUrl : baseUrl}&page=${currentPage - 1}`);
                        }
                    }
                    break;
                case 'S':
                    // If we're on a post view page, maintain the post number when changing pages
                    if (isViewPage && currentPostNo) {
                        navigate(`${window.location.href.replace(/page=\d+/, `page=${currentPage + 1}`)}`);
                    } else {
                        navigate(`https://gall.dcinside.com/${isRecommendMode ? recommendUrl : baseUrl}&page=${currentPage + 1}`);
                    }
                    break;
                case 'Z': await this.navigatePrevPost(galleryType, galleryId, currentPage); break;
                case 'X': await this.navigateNextPost(galleryType, galleryId, currentPage); break;
            }
        },

        async navigatePrevPost(galleryType, galleryId, currentPage) {
            const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
            if (!crtIcon) return;

            const currentPostNo = parseInt(window.location.href.match(/no=(\d+)/)?.[1] || NaN, 10);
            if (isNaN(currentPostNo)) return;

            let row = crtIcon.closest('tr')?.previousElementSibling;
            while (row && !Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
                row = row.previousElementSibling;
            }

            if (row) {
                row.querySelector('td.gall_tit a:first-child')?.click();
            } else if (currentPage > 1) {
                const { isRecommendMode } = Gallery.getPageInfo();
                const baseUrl = `https://gall.dcinside.com/${galleryType === 'board' ? '' : galleryType}/board/lists/?id=${galleryId}&page=${currentPage - 1}`;
                const prevUrl = isRecommendMode ? `${baseUrl}&exception_mode=recommend` : baseUrl;
                const doc = await this.fetchPage(prevUrl);
                const lastValidLink = this.getLastValidPostLink(doc);
                if (lastValidLink) window.location.href = lastValidLink;
            } else {
                const doc = await this.fetchPage(window.location.href);
                const newPosts = this.getNewerPosts(doc, currentPostNo);
                if (newPosts.length) {
                    window.location.href = newPosts.find(p => p.num === currentPostNo - 1)?.link || newPosts[0].link;
                } else {
                    UI.showAlert('첫 게시글입니다.');
                }
            }
        },

        async navigateNextPost(galleryType, galleryId, currentPage) {
            const nextLink = document.querySelector('a.next') || this.getNextValidLink();
            if (nextLink) {
                window.location.href = nextLink.href;
            } else {
                const currentPostNo = parseInt(window.location.href.match(/no=(\d+)/)?.[1] || NaN, 10);
                if (isNaN(currentPostNo)) return;

                const { isRecommendMode } = Gallery.getPageInfo();
                const baseUrl = `https://gall.dcinside.com/${galleryType === 'board' ? '' : galleryType}/board/lists/?id=${galleryId}&page=${currentPage + 1}`;
                const nextUrl = isRecommendMode ? `${baseUrl}&exception_mode=recommend` : baseUrl;
                const doc = await this.fetchPage(nextUrl);
                const nextPosts = this.getValidPostsFromDoc(doc).filter(p => p.num < currentPostNo);
                if (nextPosts.length) window.location.href = nextPosts[0].link;
            }
        },

        getNextValidLink() {
            const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
            if (!crtIcon) return null;
            let row = crtIcon.closest('tr')?.nextElementSibling;
            while (row && !Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
                row = row.nextElementSibling;
            }
            return row?.querySelector('td.gall_tit a:first-child');
        },

        async fetchPage(url) {
            const response = await fetch(url);
            const text = await response.text();
            return new DOMParser().parseFromString(text, 'text/html');
        },

        getLastValidPostLink(doc) {
            const rows = Array.from(doc.querySelectorAll('table.gall_list tbody tr'));
            for (let i = rows.length - 1; i >= 0; i--) {
                const row = rows[i];
                if (Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
                    return row.querySelector('td.gall_tit a:first-child')?.href;
                }
            }
            return null;
        },

        getNewerPosts(doc, currentNo) {
            const posts = this.getValidPostsFromDoc(doc);
            return posts.filter(p => p.num > currentNo).sort((a, b) => a.num - b.num);
        },

        getValidPostsFromDoc(doc) {
            return Array.from(doc.querySelectorAll('table.gall_list tbody tr'))
                .filter(row => Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject')))
                .map(row => {
                const num = parseInt(row.querySelector('td.gall_num').textContent.trim().replace(/\[\d+\]\s*/, ''), 10);
                return { num, link: row.querySelector('td.gall_tit a:first-child')?.href };
            });
        }
    };

    // Initialization
    function init() {
        document.addEventListener('keydown', e => Events.handleKeydown(e));

        // ALT 키 단독 입력 차단
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Alt' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
                e.preventDefault();
            }
        });
        document.readyState === 'complete' ? Posts.addNumberLabels() : window.addEventListener('load', Posts.addNumberLabels, { once: true });

        const observer = new MutationObserver(() => setTimeout(Posts.addNumberLabels, 100));
        const tbody = document.querySelector('table.gall_list tbody');
        if (tbody) observer.observe(tbody, { childList: true, subtree: true, characterData: true });

        const bodyObserver = new MutationObserver(() => {
            if (!document.querySelector('.number-label')) Posts.addNumberLabels();
        });
        bodyObserver.observe(document.body, { childList: true, subtree: true });
    }

    init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址