dcinside shortcut

dcinside 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.

Stan na 28-02-2025. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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

(function() {
    'use strict';

    // 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', '공지', '설문'].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);
            if (!numCell.querySelector('.number-label')) {
                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', 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) navigateSafely(`${baseUrl}&page=${currentPage - 1}`);
                break;
            }
            case 'S': { // 다음 페이지
                event.preventDefault();
                navigateSafely(`${baseUrl}&page=${currentPage + 1}`);
                break;
            }
            case 'Z': { // 이전 글
                event.preventDefault();
                let prevLink = document.querySelector('a.prev');
                if (!prevLink) {
                    const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
                    if (crtIcon) {
                        let row = crtIcon.closest('tr')?.previousElementSibling;
                        while (row && (row.classList.contains('block-disable') || row.classList.contains('list_trend') || row.style.display === 'none')) {
                            row = row.previousElementSibling;
                        }
                        if (row) prevLink = row.querySelector('td.gall_tit a:first-child');
                    }
                }
                if (prevLink) navigateSafely(prevLink.href);
                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) nextLink = row.querySelector('td.gall_tit a:first-child');
                    }
                }
                if (nextLink) navigateSafely(nextLink.href);
                break;
            }
        }
    });
})();