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