dcinside shortcut

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

目前为 2025-03-04 提交的版本。查看 最新版本

// ==UserScript==
// @name         dcinside shortcut
// @namespace    http://tampermonkey.net/
// @version      1.0.8
// @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';

    // 즐겨찾는 갤러리 저장 키
    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;
            }
        }
    });
})();

QingJ © 2025

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