디시인사이드 갤러리에서 게시글 제목에 마우스를 올리면 미리보기 팝업을 표시합니다. (다크 모드 지원)
// ==UserScript==
// @name 디시인사이드 게시글 미리보기
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 디시인사이드 갤러리에서 게시글 제목에 마우스를 올리면 미리보기 팝업을 표시합니다. (다크 모드 지원)
// @author guvno
// @match https://gall.dcinside.com/*/board/lists*
// @match https://gall.dcinside.com/board/lists*
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 스타일 추가 (다크 모드 지원)
const style = document.createElement('style');
style.textContent = `
.preview-popup {
position: absolute;
width: 300px;
background-color: #252525; /* 다크 모드 배경색 */
border: 1px solid #666; /* 다크 모드 테두리 색상 */
color: #eee; /* 다크 모드 글자 색상 */
padding: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.5); /* 다크 모드 그림자 */
z-index: 9999;
max-height: 300px;
overflow-y: auto;
display: none;
transition: all 0.3s ease;
cursor: pointer;
box-sizing: border-box;
word-break: break-word;
}
.preview-popup.expanded {
width: 90vw;
max-height: 90vh;
overflow-y: auto;
}
.preview-popup img {
max-width: 100%;
max-height: 250px;
height: auto;
display: block;
margin: 10px 0;
pointer-events: none;
}
.preview-popup.expanded img {
max-height: 500px;
}
.preview-popup a {
color: #459aff;
}
`;
document.head.appendChild(style);
// 단일 팝업 요소 생성
const popup = document.createElement('div');
popup.className = 'preview-popup';
document.body.appendChild(popup);
let hideTimeout = null;
let currentLink = null;
let isExpanded = false;
// 캐싱을 위한 Map 객체
const contentCache = new Map();
// 동시에 진행되는 요청 수를 제한하기 위한 변수
const MAX_CONCURRENT_REQUESTS = 5;
let currentRequests = 0;
const requestQueue = [];
// 디바운스 타임 설정 (밀리초)
const DEBOUNCE_DELAY = 100;
// 게시글 내용 가져오기 함수
function fetchPostContent(url) {
return new Promise((resolve, reject) => {
if (contentCache.has(url)) {
resolve(contentCache.get(url));
return;
}
requestQueue.push({ url, resolve, reject });
processQueue();
});
}
// 요청 큐 처리 함수
function processQueue() {
if (currentRequests >= MAX_CONCURRENT_REQUESTS || requestQueue.length === 0) {
return;
}
const { url, resolve, reject } = requestQueue.shift();
currentRequests++;
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function(response) {
currentRequests--;
try {
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
const contentElement = doc.querySelector('.writing_view_box') || doc.querySelector('.view_content_wrap');
let content = contentElement ? contentElement.innerHTML : '내용을 불러올 수 없습니다.';
// 캐시에 저장
contentCache.set(url, content);
resolve(content);
} catch (error) {
reject('내용 파싱 오류: ' + error);
}
processQueue();
},
onerror: function(error) {
currentRequests--;
reject('오류: ' + error);
processQueue();
}
});
}
// 팝업 위치 설정
function positionPopup(link) {
const rect = link.getBoundingClientRect();
const scrollY = window.scrollY || window.pageYOffset;
const scrollX = window.scrollX || window.pageXOffset;
popup.style.top = `${scrollY + rect.top + 20}px`;
popup.style.left = `${scrollX + rect.left}px`;
}
// 디바운스를 위한 타이머 저장
const debounceTimers = new Map();
// 링크에 이벤트 리스너 추가
const titleLinks = document.querySelectorAll('.gall_tit a, .ub-content a.subject');
titleLinks.forEach(link => {
link.addEventListener('mouseenter', function(e) {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
if (isExpanded) {
return;
}
currentLink = this;
if (debounceTimers.has(this)) {
clearTimeout(debounceTimers.get(this));
}
const timer = setTimeout(async () => {
const url = this.href;
try {
const content = await fetchPostContent(url);
popup.innerHTML = content;
const images = popup.querySelectorAll('img');
let imagesLoaded = 0;
const totalImages = images.length;
if (totalImages === 0) {
positionPopup(this);
popup.style.display = 'block';
return;
}
images.forEach(img => {
if (img.complete) {
imagesLoaded++;
if (imagesLoaded === totalImages) {
positionPopup(this);
popup.style.display = 'block';
}
} else {
img.addEventListener('load', () => {
imagesLoaded++;
if (imagesLoaded === totalImages) {
positionPopup(this);
popup.style.display = 'block';
}
});
img.addEventListener('error', () => {
imagesLoaded++;
if (imagesLoaded === totalImages) {
positionPopup(this);
popup.style.display = 'block';
}
});
}
});
popup.style.display = 'block';
} catch (error) {
console.error('미리보기를 불러오는 중 오류 발생:', error);
popup.innerHTML = '내용을 불러올 수 없습니다.';
positionPopup(this);
popup.style.display = 'block';
isExpanded = false;
popup.classList.remove('expanded');
}
}, DEBOUNCE_DELAY);
debounceTimers.set(this, timer);
});
link.addEventListener('mouseleave', function(e) {
if (debounceTimers.has(this)) {
clearTimeout(debounceTimers.get(this));
debounceTimers.delete(this);
}
hideTimeout = setTimeout(() => {
if (!popup.matches(':hover') && !isExpanded) {
popup.style.display = 'none';
popup.innerHTML = '';
currentLink = null;
}
}, 300);
});
});
// 팝업에 이벤트 리스너 추가
popup.addEventListener('mouseenter', function() {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
});
popup.addEventListener('mouseleave', function() {
if (!isExpanded) {
popup.style.display = 'none';
popup.innerHTML = '';
currentLink = null;
}
});
// 팝업 클릭 시 크기 토글
popup.addEventListener('click', function(e) {
e.stopPropagation();
isExpanded = !isExpanded;
if (isExpanded) {
popup.classList.add('expanded');
if (currentLink) {
positionPopup(currentLink);
}
} else {
popup.classList.remove('expanded');
}
});
// 외부 클릭 시 팝업 숨기기 (확장된 상태에서도 동작)
document.addEventListener('click', function(e) {
if (isExpanded && currentLink && !popup.contains(e.target) && !currentLink.contains(e.target)) {
popup.style.display = 'none';
popup.innerHTML = '';
popup.classList.remove('expanded');
isExpanded = false;
currentLink = null;
}
});
// 팝업이 확장된 상태에서는 스크롤로 인해 팝업이 사라지지 않도록 수정
window.addEventListener('scroll', () => {
if (popup.style.display === 'block' && !isExpanded) {
popup.style.display = 'none';
popup.innerHTML = '';
currentLink = null;
}
});
window.addEventListener('resize', () => {
if (popup.style.display === 'block' && currentLink) {
positionPopup(currentLink);
}
});
popup.addEventListener('wheel', function(e) {
if (isExpanded) {
e.stopPropagation();
}
}, { passive: false });
})();