// ==UserScript==
// @name dcinside 단축키
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @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;
}
}
});
}
})();