dcinside shortcut

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

2025-03-04 يوللانغان نەشرى. ئەڭ يېڭى نەشرىنى كۆرۈش.

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 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.0.7
// @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==
// ==UserScript==
// @name         dcinside shortcut
// @namespace    http://tampermonkey.net/
// @version      1.0.5
// @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
// @downloadURL https://update.greasyfork.org/scripts/528103/dcinside%20shortcut.user.js
// @updateURL https://update.greasyfork.org/scripts/528103/dcinside%20shortcut.meta.js
// ==/UserScript==
(function() {
    'use strict';

    // 즐겨찾는 갤러리 저장 키
    const FAVORITE_GALLERIES_KEY = 'dcinside_favorite_galleries';

    // Tampermonkey API 사용 가능 여부 확인
    const isTampermonkey = typeof GM_setValue !== 'undefined' && typeof GM_getValue !== 'undefined';

    // 즐겨찾는 갤러리 목록 가져오기
    async function getFavoriteGalleries() {
        let favorites = {};

        if (isTampermonkey) {
            // Tampermonkey API 사용
            favorites = GM_getValue(FAVORITE_GALLERIES_KEY, {});
        } else {
            try {
                // localStorage에서 확인
                const data = localStorage.getItem(FAVORITE_GALLERIES_KEY);
                if (data) {
                    favorites = JSON.parse(data);
                } else {
                    // 쿠키에서 확인
                    const cookieFavorites = document.cookie.split('; ').find(row => row.startsWith(FAVORITE_GALLERIES_KEY));
                    if (cookieFavorites) {
                        favorites = JSON.parse(decodeURIComponent(cookieFavorites.split('=')[1]));
                    }
                }
            } catch (error) {
                console.error('즐겨찾기 데이터를 가져오는데 실패했습니다:', error);
            }
        }

        return favorites;
    }

    // 즐겨찾는 갤러리 목록 저장
    function saveFavoriteGalleries(favorites) {
        try {
            if (isTampermonkey) {
                // Tampermonkey API 사용
                GM_setValue(FAVORITE_GALLERIES_KEY, favorites);
            } else {
                // localStorage에 저장
                localStorage.setItem(FAVORITE_GALLERIES_KEY, JSON.stringify(favorites));

                // 쿠키에도 저장 (도메인 간 공유를 위해)
                const expirationDate = new Date();
                expirationDate.setFullYear(expirationDate.getFullYear() + 1);
                document.cookie = `${FAVORITE_GALLERIES_KEY}=${encodeURIComponent(JSON.stringify(favorites))}; expires=${expirationDate.toUTCString()}; path=/; domain=.dcinside.com`;
            }
        } catch (error) {
            console.error('즐겨찾기 데이터를 저장하는데 실패했습니다:', error);
            alert('즐겨찾기 저장에 실패했습니다. 브라우저의 저장소 설정을 확인해주세요.');
        }
    }

    // 즐겨찾는 갤러리 목록 UI 표시
    async function showFavoriteGalleries() {
        const favorites = await getFavoriteGalleries();
        const container = document.createElement('div');
        container.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: #ffffff;
            padding: 20px;
            border-radius: 16px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.15);
            z-index: 10000;
            width: 360px;
            max-height: 80vh;
            overflow-y: auto;
            font-family: 'Roboto', sans-serif;
            border: 1px solid #e0e0e0;
            transition: opacity 0.2s ease-in-out;
            opacity: 0;
        `;
        setTimeout(() => container.style.opacity = '1', 10); // 페이드인 효과

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

        const title = document.createElement('h3');
        title.textContent = '즐겨찾는 갤러리';
        title.style.cssText = `
            font-size: 18px;
            font-weight: 700;
            color: #212121;
            margin: 0 0 15px 0;
            padding-bottom: 10px;
            border-bottom: 1px solid #e0e0e0;
        `;
        container.appendChild(title);

        const list = document.createElement('ul');
        list.style.cssText = `
            list-style: none;
            margin: 0;
            padding: 0;
            max-height: 50vh;
            overflow-y: auto;
        `;

        // Function to update the favorites list UI
        window.updateFavoritesList = async function() {
            list.innerHTML = '';
            const favorites = await getFavoriteGalleries();
            Object.entries(favorites).forEach(([key, gallery]) => {
                const item = document.createElement('li');
                item.style.cssText = `
                    display: flex;
                    align-items: center;
                    justify-content: space-between;
                    padding: 12px 15px;
                    margin: 5px 0;
                    background-color: #fafafa;
                    border-radius: 10px;
                    transition: background-color 0.2s ease;
                    cursor: pointer;
                `;
                item.onmouseenter = () => {
                    item.style.backgroundColor = '#f0f0f0';
                };
                item.onmouseleave = () => {
                    item.style.backgroundColor = '#fafafa';
                };

                const galleryName = gallery.name || gallery.galleryId || 'Unknown Gallery';
                const nameSpan = document.createElement('span');
                nameSpan.textContent = `${key}: ${galleryName}`;
                nameSpan.style.cssText = `
                    font-size: 15px;
                    font-weight: 400;
                    color: #424242;
                    flex-grow: 1;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                `;
                item.appendChild(nameSpan);

                const removeButton = document.createElement('button');
                removeButton.textContent = '✕';
                removeButton.style.cssText = `
                    background-color: transparent;
                    color: #757575;
                    border: none;
                    border-radius: 50%;
                    width: 24px;
                    height: 24px;
                    font-size: 16px;
                    line-height: 1;
                    cursor: pointer;
                    transition: color 0.2s ease, background-color 0.2s ease;
                `;
                removeButton.onmouseenter = () => {
                    removeButton.style.color = '#d32f2f';
                    removeButton.style.backgroundColor = '#ffebee';
                };
                removeButton.onmouseleave = () => {
                    removeButton.style.color = '#757575';
                    removeButton.style.backgroundColor = 'transparent';
                };
                removeButton.onclick = async (e) => {
                    e.stopPropagation();
                    const currentFavorites = await getFavoriteGalleries();
                    delete currentFavorites[key];
                    saveFavoriteGalleries(currentFavorites);
                    // Update the list after removal
                    updateFavoritesList();
                };
                item.appendChild(removeButton);

                item.onclick = () => {
                    const { galleryType, galleryId } = favorites[key];
                    const url = galleryType === 'board' ?
                          `https://gall.dcinside.com/board/lists?id=${galleryId}` :
                    `https://gall.dcinside.com/${galleryType}/board/lists?id=${galleryId}`;
                    window.location.href = url;
                };
                list.appendChild(item);
            });
        }

        // Initial population of favorites list
        await updateFavoritesList();
        container.appendChild(list);

        // 즐겨찾기 추가 UI 추가
        const addContainer = document.createElement('div');
        addContainer.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            margin: 15px 0;
            padding: 15px;
            background-color: #f5f5f5;
            border-radius: 10px;
        `;
        const numInput = document.createElement('input');
        numInput.type = 'text';
        numInput.placeholder = '0-9';
        numInput.style.cssText = `
            width: 45px;
            padding: 8px;
            border: 1px solid #e0e0e0;
            border-radius: 8px;
            font-size: 14px;
            text-align: center;
            outline: none;
            transition: border-color 0.2s ease;
            background-color: #ffffff;
        `;
        numInput.onfocus = () => {
            numInput.style.borderColor = '#1976d2';
        };
        numInput.onblur = () => {
            numInput.style.borderColor = '#e0e0e0';
        };
        const addButton = document.createElement('button');
        addButton.textContent = '즐겨찾기 추가';
        addButton.style.cssText = `
            padding: 8px 16px;
            background-color: #1976d2;
            color: #ffffff;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.2s ease;
            flex-grow: 1;
        `;
        addButton.onmouseenter = () => {
            addButton.style.backgroundColor = '#1565c0';
        };
        addButton.onmouseleave = () => {
            addButton.style.backgroundColor = '#1976d2';
        };
        addButton.onclick = function(e) {
            e.stopPropagation();
            const digit = numInput.value.trim();
            if (!/^[0-9]$/.test(digit)) {
                alert('0부터 9까지의 숫자를 입력해주세요.');
                return;
            }
            handleAltNumberKey(digit);
            numInput.value = '';
        };
        addContainer.appendChild(numInput);
        addContainer.appendChild(addButton);
        container.appendChild(addContainer);

        const closeButton = document.createElement('button');
        closeButton.textContent = '닫기';
        closeButton.style.cssText = `
            display: block;
            width: 100%;
            padding: 10px;
            margin-top: 15px;
            background-color: #1976d2;
            color: #ffffff;
            border: none;
            border-radius: 10px;
            font-size: 15px;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.2s ease;
        `;
        closeButton.onmouseenter = () => closeButton.style.backgroundColor = '#1565c0';
        closeButton.onmouseleave = () => closeButton.style.backgroundColor = '#1976d2';
        closeButton.onclick = () => {
            container.style.opacity = '0';
            setTimeout(() => document.body.removeChild(container), 200); // 페이드아웃 후 제거
        };
        container.appendChild(closeButton);

        document.body.appendChild(container);
    }

    // 현재 페이지가 갤러리 메인 페이지인지 확인
    function isGalleryMainPage() {
        return window.location.href.includes('/lists') && window.location.href.includes('id=');
    }

    // 현재 갤러리 정보 가져오기
    function getCurrentGalleryInfo() {
        const url = window.location.href;
        if (!isGalleryMainPage()) {
            return { galleryType: '', galleryId: '', galleryName: '' };
        }

        const galleryType = url.includes('/person/') ? 'person' :
        url.includes('mgallery') ? 'mgallery' :
        url.includes('mini') ? 'mini' : 'board';
        const galleryId = (url.match(/id=([^&]+)/) || [])[1] || '';

        // 갤러리 이름 추출
        const galleryNameElement = document.querySelector('div.fl.clear h2 a');
        let galleryName = galleryId; // 기본값으로 galleryId 사용

        if (galleryNameElement) {
            // <div class="pagehead_titicon ngall sp_img"> 같은 요소를 제외하고 텍스트만 추출
            galleryName = Array.from(galleryNameElement.childNodes)
                .filter(node => node.nodeType === Node.TEXT_NODE)
                .map(node => node.textContent.trim())
                .join('')
                .trim();
        }

        return { galleryType, galleryId, galleryName };
    }

    // ALT+숫자 키 처리
    async function handleAltNumberKey(key) {
        const favorites = await getFavoriteGalleries();
        const galleryInfo = getCurrentGalleryInfo();

        if (favorites[key]) {
            // 이미 등록된 경우 해당 갤러리로 이동
            const { galleryType, galleryId } = favorites[key];
            let url = '';
            if (galleryType === 'person') {
                url = `https://gall.dcinside.com/person/board/lists/?id=${galleryId}`;
            } else if (galleryType === 'board') {
                url = `https://gall.dcinside.com/board/lists?id=${galleryId}`;
            } else {
                url = `https://gall.dcinside.com/${galleryType}/board/lists?id=${galleryId}`;
            }
            window.location.href = url;
        } else if (isGalleryMainPage()) {
            favorites[key] = {
                galleryType: galleryInfo.galleryType,
                galleryId: galleryInfo.galleryId,
                name: galleryInfo.galleryName
            };
            saveFavoriteGalleries(favorites);

            // 커스텀 알림 표시
            const alertMessage = `${galleryInfo.galleryName}이(가) ${key}번에 등록되었습니다.`;
            const alertElement = document.createElement('div');
            alertElement.style.cssText = `
                position: fixed;
                top: 20px;
                left: 50%;
                transform: translateX(-50%);
                background-color: rgba(0, 0, 0, 0.8);
                color: white;
                padding: 15px 20px;
                border-radius: 8px;
                font-size: 14px;
                z-index: 10000;
                transition: opacity 0.3s ease;
            `;
            alertElement.textContent = alertMessage;
            document.body.appendChild(alertElement);

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

            // 즐겨찾기 목록 UI가 열려있다면 갱신
            const favoriteUI = document.querySelector('div[style*="position: fixed; top: 50%; left: 50%;"]');
            if (favoriteUI) {
                await updateFavoritesList();
            }
        } else {
            alert('즐겨찾기 등록은 갤러리 메인 페이지에서만 가능합니다.');
        }
    }

    // 키보드 이벤트 처리
    document.addEventListener('keydown', async event => {
        if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
            if (event.key >= '0' && event.key <= '9') {
                event.preventDefault();
                await handleAltNumberKey(event.key);
            } else if (event.key === '`') {
                event.preventDefault();
                const existingUI = document.querySelector('div[style*="position: fixed; top: 50%; left: 50%;"]');
                if (existingUI) {
                    document.body.removeChild(existingUI);
                } else {
                    await showFavoriteGalleries();
                }
            }
        }
    });

    // URL 및 갤러리 정보 추출
    const url = window.location.href;
    const galleryType = url.includes('mgallery') ? 'mgallery' :
    url.includes('mini') ? 'mini' :
    url.includes('person') ? 'person' : 'board';
    const galleryId = (url.match(/id=([^&]+)/) || [])[1] || '';
    if (!galleryId) {
        console.warn('갤러리 ID가 없습니다.');
        return;
    }
    console.log('Gallery type:', galleryType);

    // 현재 페이지 번호 및 개념글 모드 확인
    const currentPage = parseInt((url.match(/page=(\d+)/) || [])[1]) || 1;
    const isRecommendMode = url.includes('exception_mode=recommend');

    // 기본 URL 구성
    let baseUrl = (galleryType === 'board') ?
        `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}` :
    `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}`;
    if (isRecommendMode) {
        baseUrl += '&exception_mode=recommend';
    }

    // 숫자 입력 모드 관련 변수
    let numberInputMode = false, inputBuffer = '', numberInputTimeout = null, inputDisplay = null;

    // DOM 로드 후 안전하게 페이지 이동
    function navigateSafely(url) {
        const navigate = () => {
            console.log('페이지 이동:', url);
            window.location.href = url;
        };
        document.readyState === 'complete' ? navigate() : window.addEventListener('load', navigate, { once: true });
    }

    // 게시글 유효성 검사 함수들
    const isBlockedPost = numCell => {
        const row = numCell.closest('tr');
        return row && (row.classList.contains('block-disable') ||
                       row.classList.contains('list_trend') ||
                       row.style.display === 'none');
    };

    const isInvalidNumberCell = numCell => {
        const cleanedText = numCell.innerText.trim().replace(/\[\d+\]\s*|\[\+\d+\]\s*|\[\-\d+\]\s*/, '');
        return ['AD', '공지', '설문', 'Notice'].includes(cleanedText) || isNaN(cleanedText);
    };

    const isInvalidTitleCell = titleCell => !!titleCell.querySelector('em.icon_notice');
    const isInvalidSubjectCell = subjectCell => {
        const text = subjectCell.innerText.trim();
        return ['AD', '공지', '설문', '뉴스', '고정'].some(keyword => text.includes(keyword));
    };

    const isValidPost = (numCell, titleCell, subjectCell) => {
        if (!numCell || !titleCell) return false;
        if (isBlockedPost(numCell) || isInvalidNumberCell(numCell) || isInvalidTitleCell(titleCell)) return false;
        if (subjectCell && isInvalidSubjectCell(subjectCell)) return false;
        return true;
    };

    // 유효한 게시글 목록 및 현재 게시글 인덱스 구하기
    function getValidPosts() {
        const rows = document.querySelectorAll('table.gall_list tbody tr');
        const validPosts = [];
        let currentPostIndex = -1;
        rows.forEach(row => {
            const numCell = row.querySelector('td.gall_num');
            const titleCell = row.querySelector('td.gall_tit');
            const subjectCell = row.querySelector('td.gall_subject');
            if (!isValidPost(numCell, titleCell, subjectCell)) return;
            if (numCell.querySelector('.sp_img.crt_icon')) {
                currentPostIndex = validPosts.length;
            }
            const postLink = titleCell.querySelector('a:first-child');
            if (postLink) {
                validPosts.push({ row, link: postLink });
            }
        });
        return { validPosts, currentPostIndex };
    }

    // 글 목록에 번호표 추가
    function addNumberLabels() {
        // 이미 번호표가 추가된 경우 중복 실행 방지
        if (document.querySelector('.number-label')) {
            console.log('번호표가 이미 추가되어 있습니다.');
            return;
        }
        console.log('글 목록에 번호표 추가 중...');

        const allRows = document.querySelectorAll('table.gall_list tbody tr');
        const filteredRows = [];

        // 유효한 행만 필터링
        allRows.forEach(row => {
            const numCell = row.querySelector('td.gall_num');
            const titleCell = row.querySelector('td.gall_tit');
            const subjectCell = row.querySelector('td.gall_subject');

            // 유효성 검사
            if (!numCell || numCell.querySelector('.number-label') || numCell.querySelector('.sp_img.crt_icon')) return;
            if (!isValidPost(numCell, titleCell, subjectCell)) return;

            filteredRows.push(row);
        });

        // 현재 포스트 인덱스 확인
        const { validPosts, currentPostIndex } = getValidPosts();
        const maxPosts = Math.min(filteredRows.length, 100);

        // 번호표 추가
        for (let i = 0; i < maxPosts; i++) {
            const row = filteredRows[i];
            const numCell = row.querySelector('td.gall_num');
            const originalText = numCell.innerText.trim();

            // 번호 계산
            let labelNumber = currentPostIndex !== -1
            ? (validPosts.findIndex(post => post.row === row) + 1)
            : (i + 1);

            // 번호표 생성 및 추가
            const labelSpan = document.createElement('span');
            labelSpan.className = 'number-label';
            labelSpan.style.cssText = 'color: #ff6600; font-weight: bold;';
            labelSpan.textContent = `[${labelNumber}] `;

            // 기존 내용 유지하면서 번호표 추가
            numCell.innerHTML = '';
            numCell.appendChild(labelSpan);
            numCell.appendChild(document.createTextNode(originalText));
        }
        console.log(`${maxPosts}개의 글에 번호표를 추가했습니다.`);
    }

    // 숫자 입력 모드 UI 업데이트
    function updateInputDisplay(text) {
        if (!inputDisplay) {
            inputDisplay = document.createElement('div');
            inputDisplay.style.cssText = 'position: fixed; top: 10px; right: 10px; background-color: rgba(0,0,0,0.7); color: white; padding: 10px 15px; border-radius: 5px; font-size: 16px; font-weight: bold; z-index: 9999;';
            document.body.appendChild(inputDisplay);
        }
        inputDisplay.textContent = text;
    }

    function exitNumberInputMode() {
        numberInputMode = false;
        inputBuffer = '';
        if (numberInputTimeout) {
            clearTimeout(numberInputTimeout);
            numberInputTimeout = null;
        }
        if (inputDisplay) {
            document.body.removeChild(inputDisplay);
            inputDisplay = null;
        }
        console.log('숫자 입력 모드 종료');
    }

    function navigateToPost(number) {
        const { validPosts } = getValidPosts();
        const targetIndex = parseInt(number, 10) - 1;
        console.log(`입력된 숫자: ${number}, 유효한 글 수: ${validPosts.length}`);
        if (targetIndex >= 0 && targetIndex < validPosts.length) {
            console.log(`${targetIndex + 1}번 글 클릭:`, validPosts[targetIndex].link.href);
            validPosts[targetIndex].link.click();
            return true;
        }
        return false;
    }

    // 페이지 로드 시 번호표 추가
    if (document.readyState === 'complete') {
        addNumberLabels();
    } else {
        window.addEventListener('load', addNumberLabels);
    }

    // MutationObserver를 통해 동적 변화 감시 (번호표 재추가)
    function setupMutationObserver(target) {
        if (!target) return null;
        const observer = new MutationObserver(() => setTimeout(addNumberLabels, 100));
        observer.observe(target, { childList: true, subtree: true, characterData: true });
        return observer;
    }
    let observer = setupMutationObserver(document.querySelector('table.gall_list tbody'));
    const bodyObserver = new MutationObserver(() => {
        if (!document.querySelector('.number-label')) {
            addNumberLabels();
            if (observer) observer.disconnect();
            observer = setupMutationObserver(document.querySelector('table.gall_list tbody'));
        }
    });
    bodyObserver.observe(document.body, { childList: true, subtree: true });

    // 키보드 이벤트 처리 (숫자 입력 모드 및 단축키)
    document.addEventListener('keydown', async event => {
        if (!event || typeof event.key === 'undefined') return;
        const active = document.activeElement;
        if (active && (active.tagName === 'TEXTAREA' || active.tagName === 'INPUT' || active.isContentEditable)) return;
        if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) return;

        // 백틱(`) 또는 마침표(.)로 숫자 입력 모드 전환
        if (event.key === '`' || event.key === '.') {
            event.preventDefault();
            if (numberInputMode && inputBuffer.length > 0) {
                navigateToPost(inputBuffer);
                exitNumberInputMode();
                return;
            }
            numberInputMode = true;
            inputBuffer = '';
            if (numberInputTimeout) clearTimeout(numberInputTimeout);
            numberInputTimeout = setTimeout(exitNumberInputMode, 3000);
            updateInputDisplay('글 번호 입력: ');
            console.log('숫자 입력 모드 시작');
            return;
        }

        // 숫자 입력 모드: 숫자 키 처리
        if (numberInputMode && event.key >= '0' && event.key <= '9') {
            event.preventDefault();
            inputBuffer += event.key;
            updateInputDisplay(`글 번호 입력: ${inputBuffer}`);
            if (numberInputTimeout) clearTimeout(numberInputTimeout);
            numberInputTimeout = setTimeout(exitNumberInputMode, 3000);
            return;
        }

        // Enter: 입력 확정, Escape: 취소
        if (numberInputMode && event.key === 'Enter' && inputBuffer.length > 0) {
            event.preventDefault();
            navigateToPost(inputBuffer);
            exitNumberInputMode();
            return;
        }
        if (numberInputMode && event.key === 'Escape') {
            event.preventDefault();
            exitNumberInputMode();
            console.log('숫자 입력 모드 취소');
            return;
        }

        // 숫자 키 직접 입력 (숫자 입력 모드 아닐 때)
        if (!numberInputMode && event.key >= '0' && event.key <= '9') {
            const keyNumber = parseInt(event.key, 10);
            const targetIndex = keyNumber === 0 ? 9 : keyNumber - 1;
            const { validPosts } = getValidPosts();
            if (targetIndex >= 0 && targetIndex < validPosts.length) {
                validPosts[targetIndex].link.click();
            }
            return;
        }

        // 기타 단축키 처리
        switch (event.key.toUpperCase()) {
            case 'W': { // 글쓰기
                const btn = document.querySelector('button#btn_write');
                if (btn) btn.click();
                break;
            }
            case 'C': { // 댓글 입력창으로 이동
                event.preventDefault();
                const textarea = document.querySelector('textarea[id^="memo_"]');
                if (textarea) textarea.focus();
                break;
            }
            case 'D': { // 댓글 새로고침
                event.preventDefault();
                const refresh = document.querySelector('button.btn_cmt_refresh');
                if (refresh) refresh.click();
                break;
            }
            case 'R': { // 페이지 새로고침
                location.reload();
                break;
            }
            case 'Q': { // 최상단 스크롤
                window.scrollTo(0, 0);
                break;
            }
            case 'E': { // 글 목록으로 스크롤
                event.preventDefault();
                const list = document.querySelector('table.gall_list');
                if (list) list.scrollIntoView({ block: 'start' });
                break;
            }
            case 'F': { // 전체글 보기
                event.preventDefault();
                const fullUrl = (galleryType === 'board') ?
                      `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}` :
                `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}`;
                navigateSafely(fullUrl);
                break;
            }
            case 'G': { // 개념글 보기
                event.preventDefault();
                const recUrl = (galleryType === 'board') ?
                      `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}&exception_mode=recommend` :
                `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}&exception_mode=recommend`;
                navigateSafely(recUrl);
                break;
            }
            case 'A': { // 이전 페이지
                event.preventDefault();
                if (currentPage > 1) {
                    const urlObj = new URL(window.location.href);
                    urlObj.searchParams.set('page', currentPage - 1);
                    navigateSafely(urlObj.toString());
                }
                break;
            }
            case 'S': { // 다음 페이지
                event.preventDefault();
                const urlObj = new URL(window.location.href);
                urlObj.searchParams.set('page', currentPage + 1);
                navigateSafely(urlObj.toString());
                break;
            }
            case 'Z': { // 이전 글 또는 새 글로 이동
                event.preventDefault();
                const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
                if (!crtIcon) return;

                // 현재 글 번호 추출
                const currentPostNo = parseInt((window.location.href.match(/no=(\d+)/) || [])[1], 10);
                if (isNaN(currentPostNo)) {
                    console.error('현재 글 번호를 찾을 수 없습니다.');
                    return;
                }

                // 현재 페이지에서 이전 글 찾기
                let row = crtIcon.closest('tr')?.previousElementSibling;
                while (row) {
                    const numCell = row.querySelector('td.gall_num');
                    const titleCell = row.querySelector('td.gall_tit');
                    const subjectCell = row.querySelector('td.gall_subject');
                    if (isValidPost(numCell, titleCell, subjectCell)) {
                        break;
                    }
                    row = row.previousElementSibling;
                }

                if (row) {
                    // 현재 페이지 내에 이전 글이 있다면 이동
                    const prevLink = row.querySelector('td.gall_tit a:first-child');
                    if (prevLink) {
                        navigateSafely(prevLink.href);
                        return;
                    }
                } else {
                    // 현재 페이지에 이전 글이 없는 경우
                    if (currentPage === 1) {
                        // 1페이지인 경우, 새 글(현재 글 번호보다 큰 글)을 탐색
                        try {
                            const response = await fetch(window.location.href);
                            const text = await response.text();
                            const parser = new DOMParser();
                            const newPageDoc = parser.parseFromString(text, 'text/html');

                            const newRows = newPageDoc.querySelectorAll('table.gall_list tbody tr');
                            let newPosts = [];
                            let lastValidPostLink = null;
                            for (const newRow of newRows) {
                                const numCell = newRow.querySelector('td.gall_num');
                                const titleCell = newRow.querySelector('td.gall_tit');
                                const subjectCell = newRow.querySelector('td.gall_subject');

                                if (isValidPost(numCell, titleCell, subjectCell)) {
                                    const numText = numCell.innerText.trim();
                                    const num = parseInt(numText.replace(/\[\d+\]\s*/, ''), 10);
                                    // 새 글은 현재 글보다 번호가 큰 글이어야 함
                                    if (!isNaN(num) && num > currentPostNo) {
                                        const postLink = titleCell.querySelector('a:first-child');
                                        if (postLink) {
                                            newPosts.push({ num, link: postLink.href });
                                            lastValidPostLink = postLink.href;
                                        }
                                    }
                                }
                            }

                            if (newPosts.length > 0) {
                                // 만약 새 글이 여러 개라면 currentPostNo - 1 인 글을 우선 찾아 이동
                                const targetPost = newPosts.find(post => post.num === currentPostNo - 1);
                                navigateSafely(targetPost ? targetPost.link : lastValidPostLink);
                                return;
                            } else {
                                // 새 글이 없으면 알림 표시
                                const alertElement = document.createElement('div');
                                alertElement.style.cssText = `
                                    position: fixed;
                                    top: 20px;
                                    left: 50%;
                                    transform: translateX(-50%);
                                    background-color: rgba(0, 0, 0, 0.8);
                                    color: white;
                                    padding: 15px 20px;
                                    border-radius: 8px;
                                    font-size: 14px;
                                    z-index: 10000;
                                    transition: opacity 0.3s ease;
                                `;
                                alertElement.textContent = '첫 게시글입니다.';
                                document.body.appendChild(alertElement);

                                setTimeout(() => {
                                    alertElement.style.opacity = '0';
                                    setTimeout(() => {
                                        document.body.removeChild(alertElement);
                                    }, 300);
                                }, 2000);
                                return;
                            }
                        } catch (error) {
                            console.error('페이지 새로고침 실패:', error);
                        }
                    } else {
                        // 1페이지가 아니라면 이전 페이지의 마지막 유효 게시글을 찾습니다.
                        const prevPage = currentPage - 1;
                        const prevPageUrl = (galleryType === 'board') ?
                            `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}&page=${prevPage}` :
                            `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}&page=${prevPage}`;
                        try {
                            const response = await fetch(prevPageUrl);
                            const text = await response.text();
                            const parser = new DOMParser();
                            const prevPageDoc = parser.parseFromString(text, 'text/html');

                            const prevPageRows = Array.from(prevPageDoc.querySelectorAll('table.gall_list tbody tr'));
                            let lastValidPostLink = null;
                            // 단순히 DOM 순서상 마지막(하단에 위치한) 유효 게시글을 선택
                            for (let i = prevPageRows.length - 1; i >= 0; i--) {
                                const prevRow = prevPageRows[i];
                                const numCell = prevRow.querySelector('td.gall_num');
                                const titleCell = prevRow.querySelector('td.gall_tit');
                                const subjectCell = prevRow.querySelector('td.gall_subject');
                                if (isValidPost(numCell, titleCell, subjectCell)) {
                                    const postLink = titleCell.querySelector('a:first-child');
                                    if (postLink) {
                                        lastValidPostLink = postLink.href;
                                        break;
                                    }
                                }
                            }

                            if (lastValidPostLink) {
                                navigateSafely(lastValidPostLink);
                                return;
                            }
                        } catch (error) {
                            console.error('이전 페이지 로드 실패:', error);
                        }
                    }
                }
                break;
            }
            case 'X': { // 다음 글
                event.preventDefault();
                let nextLink = document.querySelector('a.next');
                if (!nextLink) {
                    const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
                    if (crtIcon) {
                        let row = crtIcon.closest('tr')?.nextElementSibling;

                        // 현재 페이지에서 다음 글 찾기
                        while (row && (row.classList.contains('block-disable') || row.classList.contains('list_trend') || row.style.display === 'none')) {
                            row = row.nextElementSibling;
                        }

                        // 현재 페이지에 다음 글이 없는 경우
                        if (!row) {
                            // 현재 보고 있는 글의 번호 추출 (URL의 no 파라미터)
                            const currentPostNo = parseInt((window.location.href.match(/no=(\d+)/) || [])[1], 10);
                            if (isNaN(currentPostNo)) {
                                console.error('현재 글 번호를 찾을 수 없습니다.');
                                return;
                            }

                            // 다음 페이지 URL 생성
                            const nextPage = currentPage + 1;
                            const nextPageUrl = (galleryType === 'board') ?
                                  `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}&page=${nextPage}` :
                            `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}&page=${nextPage}`;

                            // 다음 페이지 미리 로드
                            try {
                                const response = await fetch(nextPageUrl);
                                const text = await response.text();
                                const parser = new DOMParser();
                                const nextPageDoc = parser.parseFromString(text, 'text/html');

                                // 다음 페이지에서 유효한 글 찾기 (현재 글 번호보다 작은 글은 제외)
                                const nextPageRows = nextPageDoc.querySelectorAll('table.gall_list tbody tr');
                                for (const row of nextPageRows) {
                                    const numCell = row.querySelector('td.gall_num');
                                    const titleCell = row.querySelector('td.gall_tit');
                                    const subjectCell = row.querySelector('td.gall_subject');

                                    if (isValidPost(numCell, titleCell, subjectCell)) {
                                        const numText = numCell.innerText.trim();
                                        const num = parseInt(numText.replace(/\[\d+\]\s*/, ''), 10);
                                        if (!isNaN(num) && num < currentPostNo) {
                                            const postLink = titleCell.querySelector('a:first-child');
                                            if (postLink) {
                                                navigateSafely(postLink.href);
                                                return;
                                            }
                                        }
                                    }
                                }
                            } catch (error) {
                                console.error('다음 페이지 로드 실패:', error);
                            }
                        } else {
                            // 현재 페이지에 다음 글이 있는 경우
                            nextLink = row.querySelector('td.gall_tit a:first-child');
                        }
                    }
                }
                if (nextLink) navigateSafely(nextLink.href);
                break;
            }
        }
    });
})();