dcinside shortcut

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

От 28.02.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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

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

    // 즐겨찾는 갤러리 목록 가져오기
    function getFavoriteGalleries() {
        const favorites = localStorage.getItem(FAVORITE_GALLERIES_KEY);
        return favorites ? JSON.parse(favorites) : {};
    }

    // 즐겨찾는 갤러리 목록 저장
    function saveFavoriteGalleries(favorites) {
        localStorage.setItem(FAVORITE_GALLERIES_KEY, JSON.stringify(favorites));
    }

    // 즐겨찾는 갤러리 목록 UI 표시
    function showFavoriteGalleries() {
        const favorites = 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;
    `;

    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, transform 0.1s ease;
            cursor: pointer;
        `;
        item.onmouseenter = () => {
            item.style.backgroundColor = '#f0f0f0';
            item.style.transform = 'translateX(5px)';
        };
        item.onmouseleave = () => {
            item.style.backgroundColor = '#fafafa';
            item.style.transform = 'translateX(0)';
        };

        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 = (e) => {
            e.stopPropagation(); // 목록 클릭과 분리
            delete favorites[key];
            saveFavoriteGalleries(favorites);
            item.style.opacity = '0';
            setTimeout(() => item.remove(), 200); // 페이드아웃 후 제거
        };
        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);
    });
    container.appendChild(list);

    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;
        const galleryType = 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+숫자 키 처리
    function handleAltNumberKey(key) {
        const favorites = getFavoriteGalleries();
        const galleryInfo = getCurrentGalleryInfo();

        if (favorites[key]) {
            // 이미 등록된 경우 해당 갤러리로 이동
            const { galleryType, galleryId } = favorites[key];
            const url = galleryType === 'board' ?
                  `https://gall.dcinside.com/board/lists?id=${galleryId}` : // board 타입은 /board/lists?id= 형식
            `https://gall.dcinside.com/${galleryType}/board/lists?id=${galleryId}`;
            window.location.href = url;
        } else {
            // 등록되지 않은 경우 현재 갤러리 등록
            favorites[key] = {
                galleryType: galleryInfo.galleryType,
                galleryId: galleryInfo.galleryId,
                name: galleryInfo.galleryName // 갤러리 이름을 명시적으로 저장
            };
            saveFavoriteGalleries(favorites);
            alert(`${galleryInfo.galleryName}이(가) ${key}번에 등록되었습니다.`);
        }
    }

    // 키보드 이벤트 처리
    document.addEventListener('keydown', event => {
        if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
            if (event.key >= '0' && event.key <= '9' && isGalleryMainPage()) {
                event.preventDefault();
                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 {
                    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', '공지', '설문'].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;
                }
            }
        });
})();