DC단축키

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

Per 26-02-2025. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         DC단축키
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  DC인사이드 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.
//               - 글 목록에 번호 추가 (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() {
    // 현재 URL에서 갤러리 타입(mgallery/mini/board/person)과 갤러리 ID 추출
    const urlParts = window.location.href.split('/');
    const galleryType = urlParts.includes('mgallery') ? 'mgallery' :
    urlParts.includes('mini') ? 'mini' :
    urlParts.includes('person') ? 'person' : 'board';
    const galleryIdMatch = window.location.href.match(/id=([^&]+)/);
    const galleryId = galleryIdMatch ? galleryIdMatch[1] : '';

    if (!galleryId) {
        console.log('갤러리 ID가 없습니다.');
    } else {
        // 갤러리 ID가 있을 때만 실행할 코드
        console.log('Gallery type:', galleryType);

        // 현재 페이지 번호 추출
        const pageMatch = window.location.href.match(/page=(\d+)/);
        const currentPage = pageMatch ? parseInt(pageMatch[1]) : 1;

        // 개념글 모드인지 확인
        const isRecommendMode = window.location.href.includes('exception_mode=recommend');

        // 기본 URL 구성
        let baseUrl = `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}`;
        if (galleryType === 'board') {
            baseUrl = `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}`;
        }
        if (isRecommendMode) {
            baseUrl += '&exception_mode=recommend';
        }

        // 숫자 입력 모드 관련 변수
        let numberInputMode = false;
        let inputBuffer = '';
        let numberInputTimeout = null;
        let inputDisplay = null;

        // 페이지 이동 함수 - DOM이 완전히 로드된 후 이동하도록 보장
        function navigateSafely(url) {
            if (document.readyState === 'complete') {
                console.log('페이지 이동:', url);
                window.location.href = url;
            } else {
                console.log('DOM 로딩 대기 중...');
                window.addEventListener('load', function() {
                    console.log('DOM 로드 완료, 페이지 이동:', url);
                    window.location.href = url;
                }, { once: true });
            }
        }

        // 유효한 게시글인지 확인하는 함수
        function isValidPost(numCell, titleCell, subjectCell) {
            if (!numCell || !titleCell) return false;

            const numText = numCell.innerText.trim();
            const cleanedNumText = numText.replace(/\[\d+\]\s*|\[\+\d+\]\s*|\[\-\d+\]\s*/, '');

            // 1. gall_num 셀에 'AD', '공지', '설문'이 있는 경우 제외
            if (cleanedNumText === 'AD' || cleanedNumText === '공지' || cleanedNumText === '설문') {
                return false;
            }

            // 2. 숫자가 아닌 경우 제외
            if (isNaN(cleanedNumText)) {
                return false;
            }

            // 3. gall_subject 셀에 'AD' 또는 '공지'가 포함된 경우 제외
            if (subjectCell) {
                const subjectText = subjectCell.innerText.trim();
                if (subjectText.includes('AD') || subjectText.includes('공지') || subjectText.includes('설문')) {
                    return false;
                }
            }

            // 4. 말머리에 공지 태그가 있는 경우 제외
            if (titleCell.querySelector('em.icon_notice')) {
                return false;
            }

            return true;
        }

        // 유효한 게시글 목록 가져오기
        function getValidPosts() {
            const allPosts = document.querySelectorAll('table.gall_list tbody tr');
            const validPosts = [];
            let currentPostIndex = -1;

            allPosts.forEach((row, index) => {
                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 allPosts = document.querySelectorAll('table.gall_list tbody tr');
            const filteredPosts = [];

            // 필터링된 글 목록 생성
            allPosts.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) return; // 번호 셀이 없으면 건너뛰기

                // 이미 번호표가 있는지 확인
                if (numCell.querySelector('.number-label')) return;

                // 현재 읽고 있는 글인 경우 제외 (crt_icon이 있는 경우)
                if (numCell.querySelector('.sp_img.crt_icon')) {
                    console.log('현재 읽고 있는 글은 건너뜁니다.');
                    return;
                }

                if (!isValidPost(numCell, titleCell, subjectCell)) return;

                // 모든 필터를 통과한 경우 목록에 추가
                filteredPosts.push(row);
            });

            // 현재 읽고 있는 글의 위치 찾기
            const { validPosts, currentPostIndex } = getValidPosts();

            console.log('현재 글 위치:', currentPostIndex);
            console.log('유효한 글 수:', validPosts.length);

            // 최대 100개까지 번호표 추가
            const maxPosts = Math.min(filteredPosts.length, 100);

            for (let i = 0; i < maxPosts; i++) {
                const row = filteredPosts[i];
                const numCell = row.querySelector('td.gall_num');
                const originalText = numCell.innerText.trim();

                // 현재 글 위치를 기준으로 번호 부여
                let labelNumber;

                // 현재 글이 있는 경우 (글 보기 페이지)
                if (currentPostIndex !== -1) {
                    // 현재 글 기준으로 몇 번째 글인지 계산
                    const rowIndex = validPosts.findIndex(post => post.row === row);
                    if (rowIndex === -1) continue; // 찾을 수 없는 경우 건너뛰기

                    // 현재 글 포함하여 순차적으로 번호 부여 (1부터 시작)
                    labelNumber = rowIndex + 1;
                } else {
                    // 글 목록 페이지인 경우 기존 방식대로 1부터 순차적으로 번호 부여
                    labelNumber = i + 1;
                }

                // 기존 번호표가 있는지 확인하고 없을 때만 추가
                if (!numCell.querySelector('.number-label')) {
                    // 번호표를 별도의 span으로 추가하여 식별 가능하게 함
                    const labelSpan = document.createElement('span');
                    labelSpan.className = 'number-label';
                    labelSpan.style.color = '#ff6600';
                    labelSpan.style.fontWeight = 'bold';

                    // 번호표 텍스트 설정
                    labelSpan.textContent = `[${labelNumber}] `;

                    // 기존 내용을 보존하기 위해 텍스트 노드 생성
                    const textNode = document.createTextNode(originalText);

                    // 셀 내용 초기화 후 새 요소 추가
                    numCell.innerHTML = '';
                    numCell.appendChild(labelSpan);
                    numCell.appendChild(textNode);
                }
            }

            console.log(`${maxPosts}개의 글에 번호표를 추가했습니다.`);
        }

        // 숫자 입력 모드 UI 생성/업데이트
        function updateInputDisplay(text) {
            if (!inputDisplay) {
                inputDisplay = document.createElement('div');
                inputDisplay.style.position = 'fixed';
                inputDisplay.style.top = '10px';
                inputDisplay.style.right = '10px';
                inputDisplay.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
                inputDisplay.style.color = 'white';
                inputDisplay.style.padding = '10px 15px';
                inputDisplay.style.borderRadius = '5px';
                inputDisplay.style.fontSize = '16px';
                inputDisplay.style.fontWeight = 'bold';
                inputDisplay.style.zIndex = '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 targetNumber = parseInt(number, 10);
            console.log(`입력된 숫자: ${targetNumber}, 유효한 글 수: ${validPosts.length}`);

            if (targetNumber > 0 && targetNumber <= validPosts.length) {
                console.log(`${targetNumber}번 글 클릭:`, validPosts[targetNumber - 1].link.href);
                validPosts[targetNumber - 1].link.click();
                return true;
            }

            return false;
        }

        // 페이지 로드 완료 시 번호표 추가
        if (document.readyState === 'complete') {
            addNumberLabels();
        } else {
            window.addEventListener('load', addNumberLabels);
        }

        // MutationObserver 설정 함수
        function setupMutationObserver() {
            const observeTarget = document.querySelector('table.gall_list tbody');
            if (observeTarget) {
                console.log('MutationObserver 설정 중...');
                const observer = new MutationObserver(() => {
                    console.log('DOM 변경 감지됨, 번호표 갱신 중...');
                    setTimeout(addNumberLabels, 100);
                });

                observer.observe(observeTarget, {
                    childList: true,
                    subtree: true,
                    characterData: true
                });

                return observer;
            }
            return null;
        }

        // 초기 MutationObserver 설정
        let observer = setupMutationObserver();

        // 페이지 변경 감지를 위한 추가 감시
        const bodyObserver = new MutationObserver(() => {
            // 테이블이 다시 로드되었는지 확인
            if (!document.querySelector('.number-label')) {
                console.log('번호표가 사라짐, 다시 추가 중...');
                addNumberLabels();

                // 기존 observer가 있으면 연결 해제
                if (observer) {
                    observer.disconnect();
                }

                // 새로운 테이블에 observer 다시 연결
                observer = setupMutationObserver();
            }
        });

        // body 전체를 감시하여 부분 새로고침 감지
        bodyObserver.observe(document.body, {
            childList: true,
            subtree: true
        });

        // 키보드 이벤트 처리
        document.addEventListener('keydown', function(event) {
            // 이벤트 객체나 key 속성이 없는 경우 처리 중단
            if (!event || typeof event.key === 'undefined') {
                console.log('유효하지 않은 키 이벤트:', event);
                return;
            }

            // 댓글 작성 중이나 글 작성 중에는 단축키 비활성화
            if (document.activeElement.tagName === 'TEXTAREA' ||
                document.activeElement.tagName === 'INPUT' ||
                document.activeElement.isContentEditable) {
                return;
            }

            // Ctrl, Alt, Shift 키가 함께 눌렸을 때는 단축키 기능 비활성화
            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);
                }

                // 3초 후 숫자 입력 모드 종료
                numberInputTimeout = setTimeout(exitNumberInputMode, 3000);

                // 입력 표시 UI 생성
                updateInputDisplay('글 번호 입력: ');

                console.log('숫자 입력 모드 시작');
                return;
            }

            // 숫자 입력 모드에서 숫자 키 입력 처리
            if (numberInputMode && event.key >= '0' && event.key <= '9') {
                event.preventDefault();
                inputBuffer += event.key;
                console.log('입력된 숫자:', inputBuffer);

                // 입력 표시 UI 업데이트
                updateInputDisplay(`글 번호 입력: ${inputBuffer}`);

                // 이전 타이머 재설정
                if (numberInputTimeout) {
                    clearTimeout(numberInputTimeout);
                }

                // 3초 후 숫자 입력 모드 종료
                numberInputTimeout = setTimeout(exitNumberInputMode, 3000);

                return;
            }

            // Enter 키로 숫자 입력 확정
            if (numberInputMode && event.key === 'Enter' && inputBuffer.length > 0) {
                event.preventDefault();
                navigateToPost(inputBuffer);
                exitNumberInputMode();
                return;
            }

            // 숫자 입력 모드에서 ESC 키로 취소
            if (numberInputMode && event.key === 'Escape') {
                event.preventDefault();
                exitNumberInputMode();
                console.log('숫자 입력 모드 취소');
                return;
            }

            // 숫자 입력 모드가 아닐 때 0-9 키를 눌렀을 때 (1-9는 해당 번호의 글로, 0은 10번 글로 이동)
            if (!numberInputMode && event.key >= '0' && event.key <= '9') {
                const keyNumber = parseInt(event.key, 10);
                const targetIndex = keyNumber === 0 ? 9 : keyNumber - 1; // 0 키는 10번으로 처리

                const { validPosts } = getValidPosts();

                // 유효한 범위인지 체크
                if (targetIndex >= 0 && targetIndex < validPosts.length) {
                    const displayNumber = keyNumber === 0 ? 10 : keyNumber;
                    console.log(`${displayNumber}번 글 클릭:`, validPosts[targetIndex].link.href);
                    validPosts[targetIndex].link.click();
                }
            }

            // 기타 단축키 처리
            if (event.key) {
                switch (event.key.toUpperCase()) {
                    case 'W': // 글쓰기
                        const writeBtn = document.querySelector('button#btn_write');
                        if (writeBtn) writeBtn.click();
                        break;

                    case 'C': // 댓글로 커서 이동
                        event.preventDefault();
                        const replyTextarea = document.querySelector('textarea[id^="memo_"]');
                        if (replyTextarea) replyTextarea.focus();
                        break;

                    case 'D': // 새로고침 버튼 클릭
                        event.preventDefault();
                        const refreshBtn = document.querySelector('button.btn_cmt_refresh');
                        if (refreshBtn) refreshBtn.click();
                        break;

                    case 'R': // 페이지 새로고침
                        location.reload();
                        break;

                    case 'Q': // 페이지 최상단으로 이동
                        window.scrollTo(0, 0);
                        break;

                    case 'E': // 글 목록으로 즉각적 스크롤 이동
                        event.preventDefault();
                        const gallList = document.querySelector('table.gall_list');
                        if (gallList) gallList.scrollIntoView({ block: 'start' });
                        break;

                    case 'F': // 전체글로 이동
                        event.preventDefault();
                        const fullListUrl = galleryType === 'board' ?
                              `https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}` :
                        `https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}`;
                        navigateSafely(fullListUrl);
                        break;

                    case 'G': // 개념글로 이동
                        event.preventDefault();
                        const recommendUrl = 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(recommendUrl);
                        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();
                        const prevPost = document.querySelector('a.prev');
                        if (!prevPost) {
                            // 현재 글 찾기
                            const currentPost = document.querySelector('td.gall_num .sp_img.crt_icon');
                            if (currentPost) {
                                const currentRow = currentPost.closest('tr');
                                if (currentRow && currentRow.previousElementSibling) {
                                    const prevLink = currentRow.previousElementSibling.querySelector('td.gall_tit a:first-child');
                                    if (prevLink) {
                                        navigateSafely(prevLink.href);
                                        return;
                                    }
                                }
                            }
                        } else {
                            navigateSafely(prevPost.href);
                        }
                        break;

                    case 'X': // 다음 글로 이동
                        event.preventDefault();
                        const nextPost = document.querySelector('a.next');
                        if (!nextPost) {
                            // 현재 글 찾기
                            const currentPost = document.querySelector('td.gall_num .sp_img.crt_icon');
                            if (currentPost) {
                                const currentRow = currentPost.closest('tr');
                                if (currentRow && currentRow.nextElementSibling) {
                                    const nextLink = currentRow.nextElementSibling.querySelector('td.gall_tit a:first-child');
                                    if (nextLink) {
                                        navigateSafely(nextLink.href);
                                        return;
                                    }
                                }
                            }
                        } else {
                            navigateSafely(nextPost.href);
                        }
                        break;
                }
            }
        });
    }
})();