// ==UserScript==
// @name dcinside shortcut
// @namespace http://tampermonkey.net/
// @version 1.0.8
// @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/*
// @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';
// 즐겨찾는 갤러리 저장 키
const FAVORITE_GALLERIES_KEY = 'dcinside_favorite_galleries';
// Tampermonkey API 사용 가능 여부 확인
const isTampermonkey = typeof GM_setValue !== 'undefined' && typeof GM_getValue !== 'undefined';
// 즐겨찾는 갤러리 목록 가져오기
async function getFavoriteGalleries() {
let favorites = {};
if (isTampermonkey) {
// Tampermonkey API 사용
favorites = GM_getValue(FAVORITE_GALLERIES_KEY, {});
} else {
try {
// localStorage에서 확인
const data = localStorage.getItem(FAVORITE_GALLERIES_KEY);
if (data) {
favorites = JSON.parse(data);
} else {
// 쿠키에서 확인
const cookieFavorites = document.cookie.split('; ').find(row => row.startsWith(FAVORITE_GALLERIES_KEY));
if (cookieFavorites) {
favorites = JSON.parse(decodeURIComponent(cookieFavorites.split('=')[1]));
}
}
} catch (error) {
console.error('즐겨찾기 데이터를 가져오는데 실패했습니다:', error);
}
}
return favorites;
}
// 즐겨찾는 갤러리 목록 저장
function saveFavoriteGalleries(favorites) {
try {
if (isTampermonkey) {
// Tampermonkey API 사용
GM_setValue(FAVORITE_GALLERIES_KEY, favorites);
} else {
// localStorage에 저장
localStorage.setItem(FAVORITE_GALLERIES_KEY, JSON.stringify(favorites));
// 쿠키에도 저장 (도메인 간 공유를 위해)
const expirationDate = new Date();
expirationDate.setFullYear(expirationDate.getFullYear() + 1);
document.cookie = `${FAVORITE_GALLERIES_KEY}=${encodeURIComponent(JSON.stringify(favorites))}; expires=${expirationDate.toUTCString()}; path=/; domain=.dcinside.com`;
}
} catch (error) {
console.error('즐겨찾기 데이터를 저장하는데 실패했습니다:', error);
alert('즐겨찾기 저장에 실패했습니다. 브라우저의 저장소 설정을 확인해주세요.');
}
}
// 즐겨찾는 갤러리 목록 UI 표시
async function showFavoriteGalleries() {
const favorites = await 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;
`;
// Function to update the favorites list UI
window.updateFavoritesList = async function() {
list.innerHTML = '';
const favorites = await getFavoriteGalleries();
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;
cursor: pointer;
`;
item.onmouseenter = () => {
item.style.backgroundColor = '#f0f0f0';
};
item.onmouseleave = () => {
item.style.backgroundColor = '#fafafa';
};
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 = async (e) => {
e.stopPropagation();
const currentFavorites = await getFavoriteGalleries();
delete currentFavorites[key];
saveFavoriteGalleries(currentFavorites);
// Update the list after removal
updateFavoritesList();
};
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);
});
}
// Initial population of favorites list
await updateFavoritesList();
container.appendChild(list);
// 즐겨찾기 추가 UI 추가
const addContainer = document.createElement('div');
addContainer.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin: 15px 0;
padding: 15px;
background-color: #f5f5f5;
border-radius: 10px;
`;
const numInput = document.createElement('input');
numInput.type = 'text';
numInput.placeholder = '0-9';
numInput.style.cssText = `
width: 45px;
padding: 8px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
text-align: center;
outline: none;
transition: border-color 0.2s ease;
background-color: #ffffff;
`;
numInput.onfocus = () => {
numInput.style.borderColor = '#1976d2';
};
numInput.onblur = () => {
numInput.style.borderColor = '#e0e0e0';
};
const addButton = document.createElement('button');
addButton.textContent = '즐겨찾기 추가';
addButton.style.cssText = `
padding: 8px 16px;
background-color: #1976d2;
color: #ffffff;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
flex-grow: 1;
`;
addButton.onmouseenter = () => {
addButton.style.backgroundColor = '#1565c0';
};
addButton.onmouseleave = () => {
addButton.style.backgroundColor = '#1976d2';
};
addButton.onclick = function(e) {
e.stopPropagation();
const digit = numInput.value.trim();
if (!/^[0-9]$/.test(digit)) {
alert('0부터 9까지의 숫자를 입력해주세요.');
return;
}
handleAltNumberKey(digit);
numInput.value = '';
};
addContainer.appendChild(numInput);
addContainer.appendChild(addButton);
container.appendChild(addContainer);
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;
if (!isGalleryMainPage()) {
return { galleryType: '', galleryId: '', galleryName: '' };
}
const galleryType = url.includes('/person/') ? 'person' :
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+숫자 키 처리
async function handleAltNumberKey(key) {
const favorites = await getFavoriteGalleries();
const galleryInfo = getCurrentGalleryInfo();
if (favorites[key]) {
// 이미 등록된 경우 해당 갤러리로 이동
const { galleryType, galleryId } = favorites[key];
let url = '';
if (galleryType === 'person') {
url = `https://gall.dcinside.com/person/board/lists/?id=${galleryId}`;
} else if (galleryType === 'board') {
url = `https://gall.dcinside.com/board/lists?id=${galleryId}`;
} else {
url = `https://gall.dcinside.com/${galleryType}/board/lists?id=${galleryId}`;
}
window.location.href = url;
} else if (isGalleryMainPage()) {
favorites[key] = {
galleryType: galleryInfo.galleryType,
galleryId: galleryInfo.galleryId,
name: galleryInfo.galleryName
};
saveFavoriteGalleries(favorites);
// 커스텀 알림 표시
const alertMessage = `${galleryInfo.galleryName}이(가) ${key}번에 등록되었습니다.`;
const alertElement = document.createElement('div');
alertElement.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px 20px;
border-radius: 8px;
font-size: 14px;
z-index: 10000;
transition: opacity 0.3s ease;
`;
alertElement.textContent = alertMessage;
document.body.appendChild(alertElement);
setTimeout(() => {
alertElement.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(alertElement);
}, 300);
}, 2000);
// 즐겨찾기 목록 UI가 열려있다면 갱신
const favoriteUI = document.querySelector('div[style*="position: fixed; top: 50%; left: 50%;"]');
if (favoriteUI) {
await updateFavoritesList();
}
} else {
alert('즐겨찾기 등록은 갤러리 메인 페이지에서만 가능합니다.');
}
}
// 키보드 이벤트 처리
document.addEventListener('keydown', async event => {
if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) {
if (event.key >= '0' && event.key <= '9') {
event.preventDefault();
await 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 {
await 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', '공지', '설문', 'Notice'].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);
// 번호표 생성 및 추가
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', async 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) {
const urlObj = new URL(window.location.href);
urlObj.searchParams.set('page', currentPage - 1);
navigateSafely(urlObj.toString());
}
break;
}
case 'S': { // 다음 페이지
event.preventDefault();
const urlObj = new URL(window.location.href);
urlObj.searchParams.set('page', currentPage + 1);
navigateSafely(urlObj.toString());
break;
}
case 'Z': { // 이전 글 또는 새 글로 이동
event.preventDefault();
const crtIcon = document.querySelector('td.gall_num .sp_img.crt_icon');
if (!crtIcon) return;
// 현재 글 번호 추출
const currentPostNo = parseInt((window.location.href.match(/no=(\d+)/) || [])[1], 10);
if (isNaN(currentPostNo)) {
console.error('현재 글 번호를 찾을 수 없습니다.');
return;
}
// 현재 페이지에서 이전 글 찾기
let row = crtIcon.closest('tr')?.previousElementSibling;
while (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)) {
break;
}
row = row.previousElementSibling;
}
if (row) {
// 현재 페이지 내에 이전 글이 있다면 이동
const prevLink = row.querySelector('td.gall_tit a:first-child');
if (prevLink) {
navigateSafely(prevLink.href);
return;
}
} else {
// 현재 페이지에 이전 글이 없는 경우
if (currentPage === 1) {
// 1페이지인 경우, 새 글(현재 글 번호보다 큰 글)을 탐색
try {
const response = await fetch(window.location.href);
const text = await response.text();
const parser = new DOMParser();
const newPageDoc = parser.parseFromString(text, 'text/html');
const newRows = newPageDoc.querySelectorAll('table.gall_list tbody tr');
let newPosts = [];
let lastValidPostLink = null;
for (const newRow of newRows) {
const numCell = newRow.querySelector('td.gall_num');
const titleCell = newRow.querySelector('td.gall_tit');
const subjectCell = newRow.querySelector('td.gall_subject');
if (isValidPost(numCell, titleCell, subjectCell)) {
const numText = numCell.innerText.trim();
const num = parseInt(numText.replace(/\[\d+\]\s*/, ''), 10);
// 새 글은 현재 글보다 번호가 큰 글이어야 함
if (!isNaN(num) && num > currentPostNo) {
const postLink = titleCell.querySelector('a:first-child');
if (postLink) {
newPosts.push({ num, link: postLink.href });
lastValidPostLink = postLink.href;
}
}
}
}
if (newPosts.length > 0) {
// 만약 새 글이 여러 개라면 currentPostNo - 1 인 글을 우선 찾아 이동
const targetPost = newPosts.find(post => post.num === currentPostNo - 1);
navigateSafely(targetPost ? targetPost.link : lastValidPostLink);
return;
} else {
// 새 글이 없으면 알림 표시
const alertElement = document.createElement('div');
alertElement.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px 20px;
border-radius: 8px;
font-size: 14px;
z-index: 10000;
transition: opacity 0.3s ease;
`;
alertElement.textContent = '첫 게시글입니다.';
document.body.appendChild(alertElement);
setTimeout(() => {
alertElement.style.opacity = '0';
setTimeout(() => {
document.body.removeChild(alertElement);
}, 300);
}, 2000);
return;
}
} catch (error) {
console.error('페이지 새로고침 실패:', error);
}
} else {
// 1페이지가 아니라면 이전 페이지의 마지막 유효 게시글을 찾습니다.
const prevPage = currentPage - 1;
const prevPageUrl = (galleryType === 'board') ?
`https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}&page=${prevPage}` :
`https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}&page=${prevPage}`;
try {
const response = await fetch(prevPageUrl);
const text = await response.text();
const parser = new DOMParser();
const prevPageDoc = parser.parseFromString(text, 'text/html');
const prevPageRows = Array.from(prevPageDoc.querySelectorAll('table.gall_list tbody tr'));
let lastValidPostLink = null;
// 단순히 DOM 순서상 마지막(하단에 위치한) 유효 게시글을 선택
for (let i = prevPageRows.length - 1; i >= 0; i--) {
const prevRow = prevPageRows[i];
const numCell = prevRow.querySelector('td.gall_num');
const titleCell = prevRow.querySelector('td.gall_tit');
const subjectCell = prevRow.querySelector('td.gall_subject');
if (isValidPost(numCell, titleCell, subjectCell)) {
const postLink = titleCell.querySelector('a:first-child');
if (postLink) {
lastValidPostLink = postLink.href;
break;
}
}
}
if (lastValidPostLink) {
navigateSafely(lastValidPostLink);
return;
}
} catch (error) {
console.error('이전 페이지 로드 실패:', error);
}
}
}
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) {
// 현재 보고 있는 글의 번호 추출 (URL의 no 파라미터)
const currentPostNo = parseInt((window.location.href.match(/no=(\d+)/) || [])[1], 10);
if (isNaN(currentPostNo)) {
console.error('현재 글 번호를 찾을 수 없습니다.');
return;
}
// 다음 페이지 URL 생성
const nextPage = currentPage + 1;
const nextPageUrl = (galleryType === 'board') ?
`https://gall.dcinside.com/${galleryType}/lists/?id=${galleryId}&page=${nextPage}` :
`https://gall.dcinside.com/${galleryType}/board/lists/?id=${galleryId}&page=${nextPage}`;
// 다음 페이지 미리 로드
try {
const response = await fetch(nextPageUrl);
const text = await response.text();
const parser = new DOMParser();
const nextPageDoc = parser.parseFromString(text, 'text/html');
// 다음 페이지에서 유효한 글 찾기 (현재 글 번호보다 작은 글은 제외)
const nextPageRows = nextPageDoc.querySelectorAll('table.gall_list tbody tr');
for (const row of nextPageRows) {
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)) {
const numText = numCell.innerText.trim();
const num = parseInt(numText.replace(/\[\d+\]\s*/, ''), 10);
if (!isNaN(num) && num < currentPostNo) {
const postLink = titleCell.querySelector('a:first-child');
if (postLink) {
navigateSafely(postLink.href);
return;
}
}
}
}
} catch (error) {
console.error('다음 페이지 로드 실패:', error);
}
} else {
// 현재 페이지에 다음 글이 있는 경우
nextLink = row.querySelector('td.gall_tit a:first-child');
}
}
}
if (nextLink) navigateSafely(nextLink.href);
break;
}
}
});
})();