dcinside shortcut

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

As of 17. 03. 2025. See the latest version.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         dcinside shortcut
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  디시인사이드(dcinside) 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.
//               - 글 목록에 번호 추가 (1~100번까지 표시)
//               - 숫자 키 (1~9, 0)로 해당 번호의 글로 이동 (0은 10번 글)
//               - ` or . + 숫자 입력 + ` or .으로 특정 번호의 글로 이동 (1~100번)
//               - ALT + 숫자 (1~9, 0): 즐겨찾는 갤러리 등록/이동
//               - ALT + `: 즐겨찾는 갤러리 목록 표시/숨기기
//               - 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);
            }
        }
    };

    // 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.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: '10px'
            }, { 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;
        },

        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 },

        handleKeydown(event) {
            if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
                if (event.key >= '0' && event.key <= '9') {
                    event.preventDefault();
                    // 단축키 활성화 여부 확인
                    Storage.getAltNumberEnabled().then(enabled => {
                        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) {
                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;

            switch (key) {
                case 'W': document.querySelector('button#btn_write')?.click(); break;
                case 'C':
                    // Prevent 'c' character from being entered when focusing on comment box
                    event.preventDefault();
                    document.querySelector('textarea[id^="memo_"]')?.focus();
                    break;
                case 'D': document.querySelector('button.btn_cmt_refresh')?.click(); break;
                case 'R': location.reload(); break;
                case 'Q': window.scrollTo(0, 0); break;
                case 'E': document.querySelector('table.gall_list')?.scrollIntoView({ block: 'start' }); break;
                case 'F': navigate(`https://gall.dcinside.com/${baseUrl}`); break; // 개념글 -> 일반 목록
                case 'G': navigate(`https://gall.dcinside.com/${recommendUrl}`); break; // 일반 -> 개념글
                case '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 prevUrl = `https://gall.dcinside.com/${galleryType === 'board' ? '' : galleryType}/board/lists/?id=${galleryId}&page=${currentPage - 1}`;
                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 nextUrl = `https://gall.dcinside.com/${galleryType === 'board' ? '' : galleryType}/board/lists/?id=${galleryId}&page=${currentPage + 1}`;
                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));
        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();
})();