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