您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
디시인사이드 갤러리에서 게시글 제목에 마우스를 올리면 미리보기 팝업을 표시합니다. (다크 모드 지원)
// ==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 }); })();