dcinside shortcut

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

目前為 2025-02-28 提交的版本,檢視 最新版本

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

QingJ © 2025

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