您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
dcinside 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.
当前为
// ==UserScript== // @name dcinside shortcut // @namespace http://tampermonkey.net/ // @version 1.0.2 // @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() { // 현재 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; } } }); } })();