// ==UserScript==
// @name dcinside shortcut
// @namespace http://tampermonkey.net/
// @version 1.1.3
// @description 디시인사이드(dcinside) 갤러리에서 사용할 수 있는 다양한 단축키를 제공합니다.
// - 글 목록에 번호 추가 (1~100번까지 표시)
// - 숫자 키 (1~9, 0)로 해당 번호의 글로 이동 (0은 10번 글)
// - ` or . + 숫자 입력 + ` or .으로 특정 번호의 글로 이동 (1~100번)
// - ALT + 숫자 (1~9, 0): 즐겨찾는 갤러리 등록/이동
// - ALT + `: 즐겨찾는 갤러리 목록 표시/숨기기
// - ALT + W: 글쓰기 등록
// - W: 글쓰기 페이지로 이동
// - C: 댓글 입력창으로 커서 이동
// - D: 댓글 새로고침 및 스크롤
// - R: 페이지 새로고침
// - Q: 페이지 최상단으로 스크롤
// - E: 글 목록으로 스크롤
// - F: 전체글 보기로 이동
// - G: 개념글 보기로 이동
// - A: 이전 페이지로 이동
// - S: 다음 페이지로 이동
// - Z: 이전 글로 이동
// - X: 다음 글로 이동
// @author 노노하꼬
// @match *://gall.dcinside.com/*
// @match *://www.dcinside.com/
// @icon https://www.google.com/s2/favicons?sz=64&domain=dcinside.com
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// @supportURL https://gallog.dcinside.com/nonohako/guestbook
// ==/UserScript==
(function() {
'use strict';
// Constants
const FAVORITE_GALLERIES_KEY = 'dcinside_favorite_galleries';
const isTampermonkey = typeof GM_setValue !== 'undefined' && typeof GM_getValue !== 'undefined';
// Storage Module
const Storage = {
async getFavorites() {
let favorites = {};
try {
if (isTampermonkey) {
favorites = GM_getValue(FAVORITE_GALLERIES_KEY, {});
} else {
const data = localStorage.getItem(FAVORITE_GALLERIES_KEY) ||
this.getCookie(FAVORITE_GALLERIES_KEY);
favorites = data ? JSON.parse(data) : {};
}
} catch (error) {
console.error('Failed to retrieve favorites:', error);
}
return favorites;
},
saveFavorites(favorites) {
try {
const data = JSON.stringify(favorites);
if (isTampermonkey) {
GM_setValue(FAVORITE_GALLERIES_KEY, favorites);
} else {
localStorage.setItem(FAVORITE_GALLERIES_KEY, data);
this.setCookie(FAVORITE_GALLERIES_KEY, data);
}
} catch (error) {
console.error('Failed to save favorites:', error);
alert('즐겨찾기 저장에 실패했습니다. 브라우저의 저장소 설정을 확인해주세요.');
}
},
getCookie(name) {
const value = document.cookie.match(`(^|;)\\s*${name}=([^;]+)`);
return value ? decodeURIComponent(value[2]) : null;
},
setCookie(name, value) {
const date = new Date();
date.setFullYear(date.getFullYear() + 1);
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; path=/; domain=.dcinside.com`;
},
async getAltNumberEnabled() {
if (isTampermonkey) {
return GM_getValue('altNumberEnabled', true); // 기본값: 활성화
} else {
const data = localStorage.getItem('altNumberEnabled') || this.getCookie('altNumberEnabled');
return data !== null ? JSON.parse(data) : true;
}
},
saveAltNumberEnabled(enabled) {
try {
const data = JSON.stringify(enabled);
if (isTampermonkey) {
GM_setValue('altNumberEnabled', enabled);
} else {
localStorage.setItem('altNumberEnabled', data);
this.setCookie('altNumberEnabled', data);
}
} catch (error) {
console.error('Failed to save altNumberEnabled:', error);
}
},
async getShortcutEnabled(key) {
if (isTampermonkey) {
return GM_getValue(key, true);
} else {
const data = localStorage.getItem(key) || this.getCookie(key);
return data !== null ? JSON.parse(data) : true;
}
},
saveShortcutEnabled(key, enabled) {
try {
const data = JSON.stringify(enabled);
if (isTampermonkey) {
GM_setValue(key, enabled);
} else {
localStorage.setItem(key, data);
this.setCookie(key, data);
}
} catch (error) {
console.error(`Failed to save ${key}:`, error);
}
},
async getShortcutKey(key) {
if (isTampermonkey) {
return GM_getValue(key, null);
} else {
const data = localStorage.getItem(key) || this.getCookie(key);
return data !== null ? data : null;
}
},
saveShortcutKey(key, value) {
try {
if (isTampermonkey) {
GM_setValue(key, value);
} else {
localStorage.setItem(key, value);
this.setCookie(key, value);
}
} catch (error) {
console.error(`Failed to save ${key}:`, error);
}
}
};
// UI Module
const UI = {
createElement(tag, styles, props = {}) {
const el = document.createElement(tag);
Object.assign(el.style, styles);
Object.assign(el, props);
return el;
},
async showFavorites() {
const container = this.createElement('div', {
position: 'fixed', top: '50%', left: '50%',
transform: 'translate(-50%, -50%)', backgroundColor: '#ffffff',
padding: '20px', borderRadius: '16px', boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
zIndex: '10000', width: '360px', maxHeight: '80vh', overflowY: 'auto',
fontFamily: "'Roboto', sans-serif", border: '1px solid #e0e0e0',
transition: 'opacity 0.2s ease-in-out', opacity: '0'
});
setTimeout(() => container.style.opacity = '1', 10);
this.loadRobotoFont();
container.appendChild(this.createTitle());
const list = this.createList();
container.appendChild(list);
container.appendChild(this.createAddContainer());
container.appendChild(this.createToggleAltNumber()); // 새로 추가: 토글 버튼
container.appendChild(this.createShortcutManagerButton());
container.appendChild(this.createCloseButton(container));
document.body.appendChild(container);
await this.updateFavoritesList(list);
},
loadRobotoFont() {
if (!document.querySelector('link[href*="Roboto"]')) {
document.head.appendChild(this.createElement('link', {}, {
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap'
}));
}
},
createToggleAltNumber() {
const container = this.createElement('div', {
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
margin: '15px 0', padding: '10px', backgroundColor: '#f5f5f5',
borderRadius: '10px'
});
const label = this.createElement('span', {
fontSize: '14px', fontWeight: '500', color: '#424242'
}, { textContent: 'ALT + 숫자 단축키 사용' });
const checkbox = this.createElement('input', {
marginLeft: 'auto'
}, { type: 'checkbox' });
Storage.getAltNumberEnabled().then(enabled => {
checkbox.checked = enabled;
});
checkbox.addEventListener('change', async () => {
await Storage.saveAltNumberEnabled(checkbox.checked);
UI.showAlert(`ALT + 숫자 단축키가 ${checkbox.checked ? '활성화' : '비활성화'}되었습니다.`);
});
container.appendChild(label);
container.appendChild(checkbox);
return container;
},
createShortcutManagerButton() {
const button = this.createElement('button', {
display: 'block', width: '100%', padding: '10px', marginTop: '15px',
backgroundColor: '#4caf50', color: '#ffffff', border: 'none',
borderRadius: '10px', fontSize: '15px', fontWeight: '500',
cursor: 'pointer', transition: 'background-color 0.2s ease'
}, { textContent: '단축키 관리' });
button.addEventListener('mouseenter', () => button.style.backgroundColor = '#388e3c');
button.addEventListener('mouseleave', () => button.style.backgroundColor = '#4caf50');
button.addEventListener('click', () => this.showShortcutManager());
return button;
},
showShortcutManager() {
const container = this.createElement('div', {
position: 'fixed', top: '50%', left: '50%',
transform: 'translate(-50%, -50%)', backgroundColor: '#ffffff',
padding: '20px', borderRadius: '16px', boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
zIndex: '10000', width: '400px', maxHeight: '80vh', overflowY: 'auto',
fontFamily: "'Roboto', sans-serif", border: '1px solid #e0e0e0',
transition: 'opacity 0.2s ease-in-out', opacity: '0'
});
setTimeout(() => container.style.opacity = '1', 10);
this.loadRobotoFont();
container.appendChild(this.createTitle('단축키 관리'));
// 단축키 활성화/비활성화 토글 추가
container.appendChild(this.createShortcutToggle('W - 글쓰기', 'shortcutWEnabled'));
container.appendChild(this.createShortcutToggle('C - 댓글 입력', 'shortcutCEnabled'));
container.appendChild(this.createShortcutToggle('D - 댓글 새로고침', 'shortcutDEnabled'));
container.appendChild(this.createShortcutToggle('R - 페이지 새로고침', 'shortcutREnabled'));
container.appendChild(this.createShortcutToggle('Q - 최상단 스크롤', 'shortcutQEnabled'));
container.appendChild(this.createShortcutToggle('E - 글 목록 스크롤', 'shortcutEEnabled'));
container.appendChild(this.createShortcutToggle('F - 전체글 보기', 'shortcutFEnabled'));
container.appendChild(this.createShortcutToggle('G - 개념글 보기', 'shortcutGEnabled'));
container.appendChild(this.createShortcutToggle('A - 이전 페이지', 'shortcutAEnabled'));
container.appendChild(this.createShortcutToggle('S - 다음 페이지', 'shortcutSEnabled'));
container.appendChild(this.createShortcutToggle('Z - 이전 글', 'shortcutZEnabled'));
container.appendChild(this.createShortcutToggle('X - 다음 글', 'shortcutXEnabled'));
container.appendChild(this.createCloseButton(container));
document.body.appendChild(container);
},
createShortcutToggle(label, storageKey) {
const container = this.createElement('div', {
display: 'flex', alignItems: 'center',
margin: '10px 0', padding: '10px', backgroundColor: '#f5f5f5',
borderRadius: '10px', gap: '10px'
});
const labelEl = this.createElement('span', {
fontSize: '14px', fontWeight: '500', color: '#424242',
width: '150px'
}, { textContent: label });
const checkbox = this.createElement('input', {
marginLeft: 'auto'
}, { type: 'checkbox' });
Storage.getShortcutEnabled(storageKey).then(enabled => {
checkbox.checked = enabled;
});
checkbox.addEventListener('change', async () => {
await Storage.saveShortcutEnabled(storageKey, checkbox.checked);
UI.showAlert(`${label} 단축키가 ${checkbox.checked ? '활성화' : '비활성화'}되었습니다.`);
});
// 단축키 변경 입력 필드 추가
const keyInput = this.createElement('input', {
width: '60px', padding: '5px',
border: '1px solid #e0e0e0', borderRadius: '4px',
fontSize: '12px', outline: 'none', textAlign: 'center'
}, { type: 'text', placeholder: '키 변경', maxLength: '1' });
// 기본 단축키 매핑
const defaultKeys = {
'shortcutWKey': 'W',
'shortcutCKey': 'C',
'shortcutDKey': 'D',
'shortcutRKey': 'R',
'shortcutQKey': 'Q',
'shortcutEKey': 'E',
'shortcutFKey': 'F',
'shortcutGKey': 'G',
'shortcutAKey': 'A',
'shortcutSKey': 'S',
'shortcutZKey': 'Z',
'shortcutXKey': 'X'
};
Storage.getShortcutKey(storageKey.replace('Enabled', 'Key')).then(savedKey => {
keyInput.placeholder = savedKey || defaultKeys[storageKey.replace('Enabled', 'Key')];
});
keyInput.addEventListener('keydown', (e) => {
e.stopPropagation();
const key = e.key.toUpperCase();
if (key.length === 1 && /^[A-Z]$/.test(key)) {
// 중복 단축키 검사
Storage.getShortcutKey(storageKey.replace('Enabled', 'Key')).then(savedKey => {
const allKeys = Object.values(defaultKeys);
if (allKeys.includes(key) && key !== defaultKeys[storageKey.replace('Enabled', 'Key')]) {
UI.showAlert(`'${key}' 단축키는 이미 사용 중입니다. 다른 키를 선택해주세요.`);
} else {
Storage.saveShortcutKey(storageKey.replace('Enabled', 'Key'), key);
UI.showAlert(`${label} 단축키가 ${key}로 변경되었습니다.`);
keyInput.placeholder = key;
keyInput.value = '';
}
});
}
});
container.appendChild(labelEl);
container.appendChild(checkbox);
container.appendChild(keyInput);
return container;
},
createTitle() {
return this.createElement('h3', {
fontSize: '18px', fontWeight: '700', color: '#212121',
margin: '0 0 15px 0', paddingBottom: '10px', borderBottom: '1px solid #e0e0e0'
}, { textContent: '즐겨찾는 갤러리' });
},
createList() {
return this.createElement('ul', {
listStyle: 'none', margin: '0', padding: '0',
maxHeight: '50vh', overflowY: 'auto'
});
},
async updateFavoritesList(list) {
list.innerHTML = '';
const favorites = await Storage.getFavorites();
Object.entries(favorites).forEach(([key, gallery]) => {
list.appendChild(this.createFavoriteItem(key, gallery));
});
},
createFavoriteItem(key, gallery) {
const item = this.createElement('li', {
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '12px 15px', margin: '5px 0', backgroundColor: '#fafafa',
borderRadius: '10px', transition: 'background-color 0.2s ease', cursor: 'pointer'
});
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f0f0f0');
item.addEventListener('mouseleave', () => item.style.backgroundColor = '#fafafa');
item.addEventListener('click', () => this.navigateToGallery(gallery));
// Ensure we display the gallery name properly
const name = gallery.name || gallery.galleryName || gallery.galleryId || 'Unknown Gallery';
item.appendChild(this.createElement('span', {
fontSize: '15px', fontWeight: '400', color: '#424242',
flexGrow: '1', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
}, { textContent: `${key}: ${name}` }));
item.appendChild(this.createRemoveButton(key));
return item;
},
createRemoveButton(key) {
const button = this.createElement('button', {
backgroundColor: 'transparent', color: '#757575', border: 'none',
borderRadius: '50%', width: '24px', height: '24px', fontSize: '16px',
lineHeight: '1', cursor: 'pointer', transition: 'color 0.2s ease, background-color 0.2s ease'
}, { textContent: '✕' });
button.addEventListener('mouseenter', () => {
button.style.color = '#d32f2f';
button.style.backgroundColor = '#ffebee';
});
button.addEventListener('mouseleave', () => {
button.style.color = '#757575';
button.style.backgroundColor = 'transparent';
});
button.addEventListener('click', async (e) => {
e.stopPropagation();
const favorites = await Storage.getFavorites();
delete favorites[key];
Storage.saveFavorites(favorites);
await this.updateFavoritesList(button.closest('ul'));
});
return button;
},
createAddContainer() {
const container = this.createElement('div', {
display: 'flex', alignItems: 'center', justifyContent: 'center',
gap: '8px', margin: '15px 0', padding: '15px', backgroundColor: '#f5f5f5',
borderRadius: '10px'
});
const input = this.createElement('input', {
width: '45px', padding: '8px', border: '1px solid #e0e0e0',
borderRadius: '8px', fontSize: '14px', textAlign: 'center',
outline: 'none', transition: 'border-color 0.2s ease', backgroundColor: '#ffffff'
}, { type: 'text', placeholder: '0-9' });
input.addEventListener('focus', () => input.style.borderColor = '#1976d2');
input.addEventListener('blur', () => input.style.borderColor = '#e0e0e0');
const button = this.createElement('button', {
padding: '8px 16px', backgroundColor: '#1976d2', color: '#ffffff',
border: 'none', borderRadius: '8px', fontSize: '14px', fontWeight: '500',
cursor: 'pointer', transition: 'background-color 0.2s ease', flexGrow: '1'
}, { textContent: '즐겨찾기 추가' });
button.addEventListener('mouseenter', () => button.style.backgroundColor = '#1565c0');
button.addEventListener('mouseleave', () => button.style.backgroundColor = '#1976d2');
button.addEventListener('click', (e) => {
e.stopPropagation();
const digit = input.value.trim();
if (!/^[0-9]$/.test(digit)) {
alert('0부터 9까지의 숫자를 입력해주세요.');
return;
}
Gallery.handleFavoriteKey(digit);
input.value = '';
});
container.appendChild(input);
container.appendChild(button);
return container;
},
createCloseButton(container) {
const button = this.createElement('button', {
display: 'block', width: '100%', padding: '10px', marginTop: '15px',
backgroundColor: '#1976d2', color: '#ffffff', border: 'none',
borderRadius: '10px', fontSize: '15px', fontWeight: '500',
cursor: 'pointer', transition: 'background-color 0.2s ease'
}, { textContent: 'Close' });
button.addEventListener('mouseenter', () => button.style.backgroundColor = '#1565c0');
button.addEventListener('mouseleave', () => button.style.backgroundColor = '#1976d2');
button.addEventListener('click', () => {
container.style.opacity = '0';
setTimeout(() => document.body.removeChild(container), 200);
});
return button;
},
navigateToGallery(gallery) {
const url = gallery.galleryType === 'board'
? `https://gall.dcinside.com/board/lists?id=${gallery.galleryId}`
: `https://gall.dcinside.com/${gallery.galleryType}/board/lists?id=${gallery.galleryId}`;
window.location.href = url;
},
showAlert(message) {
const alert = this.createElement('div', {
position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%)',
backgroundColor: 'rgba(0, 0, 0, 0.8)', color: 'white', padding: '15px 20px',
borderRadius: '8px', fontSize: '14px', zIndex: '10000', transition: 'opacity 0.3s ease'
}, { textContent: message });
document.body.appendChild(alert);
setTimeout(() => {
alert.style.opacity = '0';
setTimeout(() => document.body.removeChild(alert), 300);
}, 2000);
}
};
// Gallery Module
const Gallery = {
isMainPage() {
const { href } = window.location;
return href.includes('/lists') && href.includes('id=');
},
getInfo() {
if (!this.isMainPage()) return { galleryType: '', galleryId: '', galleryName: '' };
const { href } = window.location;
const galleryType = href.includes('/person/') ? 'person' :
href.includes('mgallery') ? 'mgallery' :
href.includes('mini') ? 'mini' : 'board';
const galleryId = href.match(/id=([^&]+)/)?.[1] || '';
const nameEl = document.querySelector('div.fl.clear h2 a');
const galleryName = nameEl
? Array.from(nameEl.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent.trim())
.join('') || galleryId
: galleryId;
return { galleryType, galleryId, galleryName };
},
async handleFavoriteKey(key) {
const favorites = await Storage.getFavorites();
const info = this.getInfo();
if (favorites[key]) {
UI.navigateToGallery(favorites[key]);
} else if (this.isMainPage()) {
// Ensure galleryName is saved as 'name' for UI compatibility
favorites[key] = {
galleryType: info.galleryType,
galleryId: info.galleryId,
name: info.galleryName
};
Storage.saveFavorites(favorites);
UI.showAlert(`${info.galleryName}이(가) ${key}번에 등록되었습니다.`);
const list = document.querySelector('ul[style*="max-height: 50vh"]');
if (list) await UI.updateFavoritesList(list);
} else {
alert('즐겨찾기 등록은 갤러리 메인 페이지에서만 가능합니다.');
}
},
getPageInfo() {
const { href } = window.location;
const galleryType = href.includes('mgallery') ? 'mgallery' :
href.includes('mini') ? 'mini' :
href.includes('person') ? 'person' : 'board';
const galleryId = href.match(/id=([^&]+)/)?.[1] || '';
const currentPage = parseInt(href.match(/page=(\d+)/)?.[1] || '1', 10);
const isRecommendMode = href.includes('exception_mode=recommend');
return { galleryType, galleryId, currentPage, isRecommendMode };
}
};
// Post Navigation Module
const Posts = {
isValidPost(numCell, titleCell, subjectCell) {
if (!numCell || !titleCell) return false;
const row = numCell.closest('tr');
if (row?.classList.contains('block-disable') ||
row?.classList.contains('list_trend') ||
row?.style.display === 'none') return false;
const numText = numCell.textContent.trim().replace(/\[\d+\]\s*|\[\+\d+\]\s*|\[\-\d+\]\s*/, '');
if (['AD', '공지', '설문', 'Notice'].includes(numText) || isNaN(numText)) return false;
if (titleCell.querySelector('em.icon_notice')) return false;
if (subjectCell?.textContent.trim().match(/AD|공지|설문|뉴스|고정|이슈/)) return false;
return true;
},
getValidPosts() {
const rows = document.querySelectorAll('table.gall_list tbody tr');
const validPosts = [];
let currentIndex = -1;
rows.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 (!this.isValidPost(numCell, titleCell, subjectCell)) return;
const link = titleCell.querySelector('a:first-child');
if (link) {
validPosts.push({ row, link });
if (numCell.querySelector('.sp_img.crt_icon')) currentIndex = validPosts.length - 1;
}
});
return { validPosts, currentIndex };
},
addNumberLabels() {
const tbody = document.querySelector('table.gall_list tbody');
if (!tbody || tbody.querySelector('.number-label')) return;
const { validPosts } = this.getValidPosts();
validPosts.slice(0, 100).forEach((post, i) => {
const numCell = post.row.querySelector('td.gall_num');
if (numCell.querySelector('.sp_img.crt_icon')) return;
const label = UI.createElement('span', {
color: '#ff6600', fontWeight: 'bold'
}, { className: 'number-label', textContent: `[${i + 1}] ` });
numCell.prepend(label);
});
},
navigate(number) {
const { validPosts } = this.getValidPosts();
const index = parseInt(number, 10) - 1;
if (index >= 0 && index < validPosts.length) {
validPosts[index].link.click();
return true;
}
return false;
}
};
// Event Handlers
const Events = {
numberInput: { mode: false, buffer: '', timeout: null, display: null },
async handleKeydown(event) {
if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
if (event.key === 'w' || event.key === 'W') {
event.preventDefault();
const writeButton = document.querySelector('button.btn_lightpurple.btn_svc.write[type="image"]');
if (writeButton) writeButton.click();
} else if (event.key >= '0' && event.key <= '9') {
event.preventDefault();
// 단축키 활성화 여부 확인
const enabled = await Storage.getAltNumberEnabled();
if (enabled) {
Gallery.handleFavoriteKey(event.key);
}
} else if (event.key === '`') {
event.preventDefault();
const ui = document.querySelector('div[style*="position: fixed; top: 50%"]');
ui ? ui.remove() : UI.showFavorites();
}
} else if (!event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
const enabled = await Storage.getShortcutEnabled(`shortcut${event.key.toUpperCase()}Enabled`);
if (enabled) {
this.handleNavigationKeys(event);
}
}
},
handleNavigationKeys(event) {
const active = document.activeElement;
if (active && ['TEXTAREA', 'INPUT'].includes(active.tagName) || active.isContentEditable) return;
if (['`', '.'].includes(event.key)) {
event.preventDefault();
this.toggleNumberInput(event.key);
return;
}
if (this.numberInput.mode) {
this.handleNumberInput(event);
return;
}
if (event.key >= '0' && event.key <= '9') {
const index = event.key === '0' ? 9 : parseInt(event.key, 10) - 1;
const { validPosts } = Posts.getValidPosts();
if (index < validPosts.length) validPosts[index].link.click();
return;
}
this.handleShortcuts(event.key.toUpperCase(), event);
},
toggleNumberInput(key) {
if (this.numberInput.mode && this.numberInput.buffer) {
Posts.navigate(this.numberInput.buffer);
this.exitNumberInput();
} else {
this.numberInput.mode = true;
this.numberInput.buffer = '';
this.updateNumberDisplay('Post number: ');
this.resetNumberTimeout();
}
},
handleNumberInput(event) {
event.preventDefault();
if (event.key >= '0' && event.key <= '9') {
this.numberInput.buffer += event.key;
this.updateNumberDisplay(`Post number: ${this.numberInput.buffer}`);
this.resetNumberTimeout();
} else if (event.key === 'Enter' && this.numberInput.buffer) {
Posts.navigate(this.numberInput.buffer);
this.exitNumberInput();
} else if (event.key === 'Escape') {
this.exitNumberInput();
}
},
updateNumberDisplay(text) {
if (!this.numberInput.display) {
this.numberInput.display = UI.createElement('div', {
position: 'fixed', top: '10px', right: '10px', backgroundColor: 'rgba(0,0,0,0.7)',
color: 'white', padding: '10px 15px', borderRadius: '5px', fontSize: '16px',
fontWeight: 'bold', zIndex: '9999'
});
document.body.appendChild(this.numberInput.display);
}
this.numberInput.display.textContent = text;
},
resetNumberTimeout() {
clearTimeout(this.numberInput.timeout);
this.numberInput.timeout = setTimeout(() => this.exitNumberInput(), 3000);
},
exitNumberInput() {
this.numberInput.mode = false;
this.numberInput.buffer = '';
clearTimeout(this.numberInput.timeout);
this.numberInput.timeout = null;
if (this.numberInput.display) {
this.numberInput.display.remove();
this.numberInput.display = null;
}
},
async handleShortcuts(key, event) {
const { galleryType, galleryId, currentPage, isRecommendMode } = Gallery.getPageInfo();
const baseUrl = `${galleryType === 'board' ? '' : galleryType}/board/lists/?id=${galleryId}`;
const recommendUrl = `${baseUrl}&exception_mode=recommend`;
const navigate = url => document.readyState === 'complete' ? window.location.href = url : window.addEventListener('load', () => window.location.href = url, { once: true });
// Check if we're on a post view page
const isViewPage = window.location.href.match(/\/board\/view\/?/) || window.location.href.match(/no=\d+/);
const currentPostNo = isViewPage ? window.location.href.match(/no=(\d+)/)?.[1] : null;
// Get saved shortcut keys
const savedKeys = {
'W': await Storage.getShortcutKey('shortcutWKey') || 'W',
'C': await Storage.getShortcutKey('shortcutCKey') || 'C',
'D': await Storage.getShortcutKey('shortcutDKey') || 'D',
'R': await Storage.getShortcutKey('shortcutRKey') || 'R',
'Q': await Storage.getShortcutKey('shortcutQKey') || 'Q',
'E': await Storage.getShortcutKey('shortcutEKey') || 'E',
'F': await Storage.getShortcutKey('shortcutFKey') || 'F',
'G': await Storage.getShortcutKey('shortcutGKey') || 'G',
'A': await Storage.getShortcutKey('shortcutAKey') || 'A',
'S': await Storage.getShortcutKey('shortcutSKey') || 'S',
'Z': await Storage.getShortcutKey('shortcutZKey') || 'Z',
'X': await Storage.getShortcutKey('shortcutXKey') || 'X'
};
switch (key) {
case savedKeys['W']: document.querySelector('button#btn_write')?.click(); break;
case savedKeys['C']:
// Prevent 'c' character from being entered when focusing on comment box
event.preventDefault();
document.querySelector('textarea[id^="memo_"]')?.focus();
break;
case savedKeys['D']: document.querySelector('button.btn_cmt_refresh')?.click(); break;
case savedKeys['R']: location.reload(); break;
case savedKeys['Q']: window.scrollTo(0, 0); break;
case savedKeys['E']: document.querySelector('table.gall_list')?.scrollIntoView({ block: 'start' }); break;
case savedKeys['F']: navigate(`https://gall.dcinside.com/${baseUrl}`); break; // 개념글 -> 일반 목록
case savedKeys['G']: navigate(`https://gall.dcinside.com/${recommendUrl}`); break; // 일반 -> 개념글
case savedKeys['A']:
if (currentPage > 1) {
// If we're on a post view page, maintain the post number when changing pages
if (isViewPage && currentPostNo) {
navigate(`${window.location.href.replace(/page=\d+/, `page=${currentPage - 1}`)}`);
} else {
navigate(`https://gall.dcinside.com/${isRecommendMode ? recommendUrl : baseUrl}&page=${currentPage - 1}`);
}
}
break;
case 'S':
// If we're on a post view page, maintain the post number when changing pages
if (isViewPage && currentPostNo) {
navigate(`${window.location.href.replace(/page=\d+/, `page=${currentPage + 1}`)}`);
} else {
navigate(`https://gall.dcinside.com/${isRecommendMode ? recommendUrl : baseUrl}&page=${currentPage + 1}`);
}
break;
case 'Z': await this.navigatePrevPost(galleryType, galleryId, currentPage); break;
case 'X': await this.navigateNextPost(galleryType, galleryId, currentPage); break;
}
},
async navigatePrevPost(galleryType, galleryId, currentPage) {
const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
if (!crtIcon) return;
const currentPostNo = parseInt(window.location.href.match(/no=(\d+)/)?.[1] || NaN, 10);
if (isNaN(currentPostNo)) return;
let row = crtIcon.closest('tr')?.previousElementSibling;
while (row && !Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
row = row.previousElementSibling;
}
if (row) {
row.querySelector('td.gall_tit a:first-child')?.click();
} else if (currentPage > 1) {
const { isRecommendMode } = Gallery.getPageInfo();
const baseUrl = `https://gall.dcinside.com/${galleryType === 'board' ? '' : galleryType}/board/lists/?id=${galleryId}&page=${currentPage - 1}`;
const prevUrl = isRecommendMode ? `${baseUrl}&exception_mode=recommend` : baseUrl;
const doc = await this.fetchPage(prevUrl);
const lastValidLink = this.getLastValidPostLink(doc);
if (lastValidLink) window.location.href = lastValidLink;
} else {
const doc = await this.fetchPage(window.location.href);
const newPosts = this.getNewerPosts(doc, currentPostNo);
if (newPosts.length) {
window.location.href = newPosts.find(p => p.num === currentPostNo - 1)?.link || newPosts[0].link;
} else {
UI.showAlert('첫 게시글입니다.');
}
}
},
async navigateNextPost(galleryType, galleryId, currentPage) {
const nextLink = document.querySelector('a.next') || this.getNextValidLink();
if (nextLink) {
window.location.href = nextLink.href;
} else {
const currentPostNo = parseInt(window.location.href.match(/no=(\d+)/)?.[1] || NaN, 10);
if (isNaN(currentPostNo)) return;
const { isRecommendMode } = Gallery.getPageInfo();
const baseUrl = `https://gall.dcinside.com/${galleryType === 'board' ? '' : galleryType}/board/lists/?id=${galleryId}&page=${currentPage + 1}`;
const nextUrl = isRecommendMode ? `${baseUrl}&exception_mode=recommend` : baseUrl;
const doc = await this.fetchPage(nextUrl);
const nextPosts = this.getValidPostsFromDoc(doc).filter(p => p.num < currentPostNo);
if (nextPosts.length) window.location.href = nextPosts[0].link;
}
},
getNextValidLink() {
const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
if (!crtIcon) return null;
let row = crtIcon.closest('tr')?.nextElementSibling;
while (row && !Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
row = row.nextElementSibling;
}
return row?.querySelector('td.gall_tit a:first-child');
},
async fetchPage(url) {
const response = await fetch(url);
const text = await response.text();
return new DOMParser().parseFromString(text, 'text/html');
},
getLastValidPostLink(doc) {
const rows = Array.from(doc.querySelectorAll('table.gall_list tbody tr'));
for (let i = rows.length - 1; i >= 0; i--) {
const row = rows[i];
if (Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject'))) {
return row.querySelector('td.gall_tit a:first-child')?.href;
}
}
return null;
},
getNewerPosts(doc, currentNo) {
const posts = this.getValidPostsFromDoc(doc);
return posts.filter(p => p.num > currentNo).sort((a, b) => a.num - b.num);
},
getValidPostsFromDoc(doc) {
return Array.from(doc.querySelectorAll('table.gall_list tbody tr'))
.filter(row => Posts.isValidPost(row.querySelector('td.gall_num'), row.querySelector('td.gall_tit'), row.querySelector('td.gall_subject')))
.map(row => {
const num = parseInt(row.querySelector('td.gall_num').textContent.trim().replace(/\[\d+\]\s*/, ''), 10);
return { num, link: row.querySelector('td.gall_tit a:first-child')?.href };
});
}
};
// Initialization
function init() {
document.addEventListener('keydown', e => Events.handleKeydown(e));
// ALT 키 단독 입력 차단
document.addEventListener('keydown', (e) => {
if (e.key === 'Alt' && !e.ctrlKey && !e.shiftKey && !e.metaKey) {
e.preventDefault();
}
});
document.readyState === 'complete' ? Posts.addNumberLabels() : window.addEventListener('load', Posts.addNumberLabels, { once: true });
const observer = new MutationObserver(() => setTimeout(Posts.addNumberLabels, 100));
const tbody = document.querySelector('table.gall_list tbody');
if (tbody) observer.observe(tbody, { childList: true, subtree: true, characterData: true });
const bodyObserver = new MutationObserver(() => {
if (!document.querySelector('.number-label')) Posts.addNumberLabels();
});
bodyObserver.observe(document.body, { childList: true, subtree: true });
}
init();
})();