디시인사이드 게시글 미리보기

디시인사이드 갤러리에서 게시글 제목에 마우스를 올리면 미리보기 팝업을 표시합니다. (다크 모드 지원)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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

})();