dcinside shortcut

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

Från och med 2025-02-28. Se den senaste versionen.

// ==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;
            }
        }
    });
})();