Eroscript scroll Pagination conversion

Eroscript 무한 스크롤 방식을 페이지 방식으로 전환하는 확장프로그램 스크립트. 뒤로가기 시 DOM 교체로 페이지네이션이 사라지는 문제 수정 (자가 치유 재초기화).

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Advertisement:

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

Advertisement:

// ==UserScript==
// @name         Eroscript scroll Pagination conversion
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Eroscript 무한 스크롤 방식을 페이지 방식으로 전환하는 확장프로그램 스크립트. 뒤로가기 시 DOM 교체로 페이지네이션이 사라지는 문제 수정 (자가 치유 재초기화).
// @author       Irukai
// @match        https://discuss.eroscripts.com/*
// @grant        none
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 환경 설정
    const itemsPerPage = 15;
    const maxPages = 10;
    const checkInterval = 1000;
    const safeMode = true;
    const refreshTimeout = 8000;     // 갱신 시 새 아이템 대기 최대 시간(ms)
    const restoreTimeout = 4000;     // 복원 시 배치당 대기 최대 시간(ms)
    const refreshPollMs = 250;       // 폴링 간격(ms)
    const settleWaitMs = 400;        // 아이템 증가 확인 후 정착 대기(ms)
    const maxRestoreAttempts = 15;   // 복원 시 최대 배치 로드 횟수
    const multiRefreshBatches = 5;   // 연속 갱신 시 로드할 배치 수
    const spacerViewportRatio = 1.5; // 스페이서 고정 높이 (뷰포트 대비 배율)
    const stateKeyPrefix = 'epc_state:'; // sessionStorage 키 접두사

    let currentPage = 1;
    let contentContainer = null;
    let allItems = [];
    let knownIds = new Set();      // 페이지네이션에 반영된 topic ID 집합
    let paginationContainer = null;
    let spacerEl = null;           // 연쇄 로딩 차단용 고정 높이 스페이서
    let refreshBtn = null;         // 갱신 버튼 (뱃지 갱신용 참조)
    let multiRefreshBtn = null;    // 연속 갱신 버튼
    let isInitialized = false;
    let observer = null;
    let lastUrl = location.href;
    let isLoading = false;

    // 페이지네이션 업데이트 중복방지 플래그
    let isUpdatingPagination = false;

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startScriptSafely);
    } else {
        startScriptSafely();
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // ===== 상태 저장/복원 (뒤로가기 대응, 같은 탭 세션 내 유지) =====

    function stateKey() {
        return stateKeyPrefix + location.pathname;
    }

    function saveState() {
        try {
            sessionStorage.setItem(stateKey(), JSON.stringify({
                itemCount: allItems.length,
                currentPage: currentPage
            }));
        } catch (e) {
            // sessionStorage 사용 불가 환경이면 조용히 무시
        }
    }

    function loadSavedState() {
        try {
            const raw = sessionStorage.getItem(stateKey());
            if (!raw) return null;
            const state = JSON.parse(raw);
            if (typeof state.itemCount !== 'number' || typeof state.currentPage !== 'number') return null;
            return state;
        } catch (e) {
            return null;
        }
    }

    function startScriptSafely() {
        console.log("Discourse 페이지네이션: 시작 준비 중...");

        setupKeyboardShortcuts();

        checkPageAndInitialize();
        setInterval(checkPageAndInitialize, checkInterval);

        function onUrlChange() {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                console.log("URL 변경 감지됨, 초기화 상태 리셋 및 재초기화 시작");
                resetPaginationState();
                checkPageAndInitialize();
            }
        }

        window.addEventListener('popstate', onUrlChange);

        const originalPushState = history.pushState;
        history.pushState = function() {
            originalPushState.apply(this, arguments);
            setTimeout(onUrlChange, 500);
        };
    }

    // 초기화 상태 리셋 (URL 변경 또는 DOM 교체 감지 시)
    function resetPaginationState() {
        isInitialized = false;
        knownIds = new Set();
        allItems = [];
        if (paginationContainer) {
            paginationContainer.remove();
            paginationContainer = null;
        }
        refreshBtn = null;
        multiRefreshBtn = null;
        if (spacerEl) {
            spacerEl.remove();
            spacerEl = null;
        }
        if (observer) {
            observer.disconnect();
            observer = null;
        }
        contentContainer = null;
    }

    // ===== 키보드 단축키: ←/→ 페이지 이동 =====

    function setupKeyboardShortcuts() {
        document.addEventListener('keydown', (e) => {
            if (!isInitialized || isLoading) return;
            if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return;

            // 입력창/에디터에 포커스가 있으면 무시
            const t = e.target;
            if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;

            if (e.key === 'ArrowLeft') {
                if (currentPage > 1) {
                    e.preventDefault();
                    showPage(currentPage - 1);
                }
            } else if (e.key === 'ArrowRight') {
                const totalPages = Math.ceil(allItems.length / itemsPerPage);
                if (currentPage < totalPages) {
                    e.preventDefault();
                    showPage(currentPage + 1);
                }
            }
        });
    }

    function checkPageAndInitialize() {
        if (location.pathname.startsWith('/t/')) {
            if (isInitialized) {
                resetPaginationState();
            }
            return;
        }

        // 자가 치유: 뒤로가기 등으로 Discourse가 DOM을 통째로 교체하면
        // 기존 컨테이너/페이지네이션 바가 문서에서 분리(detached)됨 → 리셋 후 재초기화
        if (isInitialized) {
            const containerDetached = contentContainer && !contentContainer.isConnected;
            const paginationDetached = paginationContainer && !paginationContainer.isConnected;
            if (containerDetached || paginationDetached) {
                console.log("DOM 교체 감지 (detached), 재초기화 시작");
                resetPaginationState();
            }
        }

        if ((document.body) && !isInitialized) {
            try {
                console.log("페이지네이션 초기화 시작 (/c/ 경로)");
                initialize();
            } catch (error) {
                console.error("페이지네이션 초기화 오류:", error);
            }
        } else if (isInitialized) {
            hideNewItemsOnly(); // 자동 갱신 대신 새 아이템 숨김 + 스페이서 갱신만 수행
        }
    }

    // ===== 아이템 식별 (Ember 재렌더링에 안전하도록 topic ID 기반) =====

    function itemKey(item) {
        if (!item || !item.getAttribute) return null;
        return item.getAttribute('data-topic-id');
    }

    function isKnownItem(item) {
        const key = itemKey(item);
        if (key !== null) return knownIds.has(key);
        // topic ID가 없는 레이아웃이면 마킹 방식으로 대체
        return item.dataset && item.dataset.customKnown === '1';
    }

    // DOM 순서를 유지하면서 knownIds에 등록된 아이템만 allItems로 재구성
    // (재렌더링으로 노드가 교체돼도 항상 살아있는 노드를 참조하게 됨)
    function syncAllItemsFromDom() {
        const domItems = getDiscourseItems();
        allItems = domItems.filter(isKnownItem);
    }

    // 아직 페이지네이션에 반영되지 않은(숨겨진 채 대기 중인) 아이템 수
    function getPendingCount() {
        return getDiscourseItems().filter(item => !isKnownItem(item)).length;
    }

    // ===== 새 아이템 처리 =====

    // 무한 스크롤로 새로 로드된 아이템은 숨기기만 하고 페이지네이션에 반영하지 않음
    // (갱신 버튼을 눌렀을 때만 collectAllItems()로 반영됨)
    function hideNewItemsOnly() {
        if (!contentContainer) return;
        const domItems = getDiscourseItems();
        let hiddenNew = 0;
        domItems.forEach(item => {
            if (!isKnownItem(item)) {
                if (item.style.display !== 'none') {
                    item.style.display = 'none';
                    hiddenNew++;
                }
            }
        });
        if (hiddenNew > 0) {
            console.log(`새 아이템 ${hiddenNew}개 숨김 처리 (수동 갱신 대기)`);
        }
        syncAllItemsFromDom();
        updateSpacer();
        updateRefreshBadge();
    }

    // 현재 DOM의 모든 아이템(숨겨져 있던 새 아이템 포함)을 페이지네이션에 흡수
    function collectAllItems() {
        const domItems = getDiscourseItems();
        domItems.forEach(item => {
            const key = itemKey(item);
            if (key !== null) {
                knownIds.add(key);
            } else if (item.dataset) {
                item.dataset.customKnown = '1';
            }
        });
        allItems = domItems;
        console.log(`${allItems.length}개의 아이템을 찾았습니다.`);
        if (isInitialized) {
            allItems.forEach(item => item.style.display = 'none');
        }
    }

    // ===== 스페이서: 고정 높이(뷰포트 150%)로 연쇄 로딩 차단 + 스크롤바 길이 유지 =====

    function getSpacerHost() {
        if (!contentContainer) return null;
        if (contentContainer.tagName === 'TBODY') return contentContainer;
        const tbody = contentContainer.querySelector('tbody');
        return tbody || contentContainer;
    }

    function updateSpacer() {
        const host = getSpacerHost();
        if (!host) return;

        // 스페이서 생성 (테이블이면 tr, 아니면 div)
        if (!spacerEl || !spacerEl.isConnected) {
            if (spacerEl) spacerEl.remove();
            if (host.tagName === 'TBODY') {
                spacerEl = document.createElement('tr');
                spacerEl.className = 'custom-pagination-spacer';
                const td = document.createElement('td');
                td.colSpan = 99;
                td.style.padding = '0';
                td.style.border = 'none';
                const inner = document.createElement('div');
                inner.style.height = '0px';
                td.appendChild(inner);
                spacerEl.appendChild(td);
            } else {
                spacerEl = document.createElement('div');
                spacerEl.className = 'custom-pagination-spacer';
                spacerEl.style.height = '0px';
            }
            host.appendChild(spacerEl);
        }

        // 항상 리스트 맨 끝에 위치하도록 유지 (새 행이 뒤에 붙었을 경우)
        if (host.lastElementChild !== spacerEl) {
            host.appendChild(spacerEl);
        }

        // 고정 높이: 숨겨진 아이템이 하나라도 있으면 뷰포트의 150%, 없으면 0
        const domItems = getDiscourseItems();
        const hiddenCount = domItems.filter(i => i.style.display === 'none').length;
        const h = hiddenCount > 0 ? Math.round(window.innerHeight * spacerViewportRatio) : 0;

        const target = spacerEl.tagName === 'TR' ? spacerEl.querySelector('div') : spacerEl;
        if (target) target.style.height = h + 'px';
    }

    // ===== 폴링 대기 =====

    // 고정 대기 대신 아이템 증가를 감시해서 오판 방지
    async function waitForNewItems(prevCount, timeoutMs) {
        const timeout = timeoutMs || refreshTimeout;
        const start = Date.now();
        while (Date.now() - start < timeout) {
            await sleep(refreshPollMs);
            if (getDiscourseItems().length > prevCount) {
                await sleep(settleWaitMs); // 같은 배치의 나머지 행 도착 대기
                return true;
            }
        }
        return false;
    }

    // ===== 바닥 로딩 트리거 =====

    // 이미 바닥에 있으면 scrollTo가 no-op이라 scroll 이벤트가 발생하지 않고,
    // Discourse가 바닥 체크를 다시 하지 않아 로딩이 멈춘다.
    // 위로 살짝 올렸다가 다시 바닥으로 이동해 이벤트를 강제로 발생시킨다.
    async function triggerBottomLoad() {
        const upTarget = Math.max(0, document.body.scrollHeight - window.innerHeight * 2);
        window.scrollTo(0, upTarget);
        await sleep(60);
        window.scrollTo(0, document.body.scrollHeight);
        // 보조: 위치 변화가 없었을 경우를 대비한 합성 scroll 이벤트
        window.dispatchEvent(new Event('scroll'));
    }

    // ===== 뒤로가기 복원: 저장된 아이템 수까지 배치를 자동 재로드 =====

    async function restoreLoadedItems(targetCount) {
        if (isLoading) return;
        isLoading = true;
        console.log(`복원 시작: 목표 ${targetCount}개 (현재 ${getDiscourseItems().length}개)`);
        try {
            let attempts = 0;
            let cur = getDiscourseItems().length;
            while (cur < targetCount && attempts < maxRestoreAttempts) {
                await triggerBottomLoad();
                const gotNew = await waitForNewItems(cur, restoreTimeout);
                const next = getDiscourseItems().length;
                if (!gotNew || next === cur) {
                    console.log("더 이상 로드되지 않음, 복원 중단");
                    break;
                }
                cur = next;
                attempts++;
                console.log(`복원 진행: ${cur}/${targetCount} (시도 ${attempts})`);
            }
            window.scrollTo(0, 0);
            console.log(`복원 완료: ${cur}개 로드됨`);
        } finally {
            isLoading = false;
        }
    }

    // ===== 초기화 =====

    async function initialize() {
        if (isInitialized) return;
        console.log("Discourse 페이지네이션: 초기화 중...");
        try {
            contentContainer = findDiscourseContentContainer();
            if (!contentContainer) {
                console.log("콘텐츠 컨테이너를 아직 찾을 수 없습니다. 나중에 다시 시도합니다.");
                return;
            }
            console.log("콘텐츠 컨테이너 찾음:", contentContainer);

            if (!safeMode) {
                disableInfiniteScrollSafely();
            }

            collectAllItems();

            if (allItems.length === 0) {
                console.warn("표시할 아이템을 찾을 수 없습니다.");
                return;
            }

            createPaginationInterface();
            // 복원 중 인터벌/옵저버 재진입을 막기 위해 먼저 초기화 완료로 표시
            isInitialized = true;
            setupContentObserver();

            // 저장된 기록이 있고 DOM 아이템이 부족하면 자동 재로드
            const saved = loadSavedState();
            if (saved && saved.itemCount > getDiscourseItems().length) {
                await restoreLoadedItems(saved.itemCount);
                collectAllItems();
            }

            checkUrlForPageNumber(saved ? saved.currentPage : null);
            console.log("페이지네이션 초기화 완료!");
        } catch (error) {
            console.error("초기화 중 오류 발생:", error);
        }
    }

    function disableInfiniteScrollSafely() {
        if (window._discourse_scroll_trackers) {
            try {
                window._discourse_scroll_trackers.forEach((tracker, i) => {
                    if (tracker && typeof tracker === 'function') {
                        window._discourse_scroll_trackers[i] = function() {};
                    }
                });
            } catch (e) {
                console.error("스크롤 트래커 비활성화 오류:", e);
            }
        }
        if (window.MessageBus && safeMode === false) {
            try {
                if (typeof window.MessageBus.baseInterval === 'number') {
                    window.MessageBus.baseInterval = 120000;
                }
                if (typeof window.MessageBus.unsubscribe === 'function') {
                    window.MessageBus.unsubscribe('/latest');
                    window.MessageBus.unsubscribe('/new');
                }
                console.log("MessageBus 설정 조정됨");
            } catch (e) {
                console.error("MessageBus 조정 오류:", e);
            }
        }
    }

    function findDiscourseContentContainer() {
        const candidates = [
            document.querySelector('.topic-list tbody'),
            document.querySelector('table.topic-list'),
            document.querySelector('.topic-list'),
            document.querySelector('.latest-topic-list'),
            document.querySelector('.category-list'),
            document.querySelector('#list-area')
        ];
        for (const c of candidates) {
            if (!c) continue;
            // 토픽 페이지 하단의 추천/관련 토픽 목록은 콘텐츠 컨테이너로 오인하지 않음
            if (c.closest('.suggested-topics, #suggested-topics, .more-topics__list, .related-topics')) continue;
            return c;
        }
        return null;
    }

    function getDiscourseItems() {
        if (!contentContainer) return [];
        const rows = contentContainer.querySelectorAll('tr.topic-list-item');
        if (rows.length > 0) return Array.from(rows);
        const topicItems = contentContainer.querySelectorAll('.latest-topic-list-item');
        if (topicItems.length > 0) return Array.from(topicItems);
        const alternateItems = contentContainer.querySelectorAll('.topic-list-item');
        if (alternateItems.length > 0) return Array.from(alternateItems);
        if (contentContainer.children.length > 0) {
            // 스페이서는 아이템에서 제외
            return Array.from(contentContainer.children)
                .filter(el => !el.classList.contains('custom-pagination-spacer'));
        }
        return [];
    }

    // ===== 페이지네이션 UI =====

    function createPaginationInterface() {
        if (paginationContainer) paginationContainer.remove();

        paginationContainer = document.createElement('div');
        paginationContainer.className = 'custom-discourse-pagination';
        paginationContainer.style.cssText = `
            display: flex;
            justify-content: center;
            align-items: center;
            margin: 20px 0;
            padding: 10px 15px;
            background-color: #ffffff;
            border-radius: 5px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            position: sticky;
            bottom: 10px;
            z-index: 1000;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            width: 100%;
            max-width: none;
            font-size: 12px;
            overflow-x: auto;
            white-space: nowrap;
            flex-wrap: nowrap;
        `;

        updatePaginationInterface();

        let inserted = false;
        const candidates = [
            document.querySelector('.topic-list'),
            document.querySelector('table.topic-list'),
            contentContainer,
            document.querySelector('.list-controls'),
            document.querySelector('#main-outlet'),
            document.querySelector('main .container'),
            document.body
        ];

        for (const c of candidates) {
            if (c && !inserted) {
                if (c === contentContainer) {
                    c.parentNode.insertBefore(paginationContainer, c.nextSibling);
                } else {
                    c.appendChild(paginationContainer);
                }
                inserted = true;
                break;
            }
        }
    }

    function updatePaginationInterface() {
        if (!paginationContainer) return;

        const totalItems = allItems.length;
        const totalPages = Math.ceil(totalItems / itemsPerPage);

        paginationContainer.innerHTML = '';
        refreshBtn = null;
        multiRefreshBtn = null;

        if (currentPage > 1) addPageButton('<<', 1);
        if (currentPage > 1) addPageButton('<', currentPage - 1);

        const half = Math.floor(maxPages / 2);
        let startPage = currentPage - half;
        let endPage = currentPage + half;

        if (maxPages % 2 === 0) endPage -= 1;

        if (startPage < 1) {
            endPage += 1 - startPage;
            startPage = 1;
        }
        if (endPage > totalPages) {
            startPage -= (endPage - totalPages);
            endPage = totalPages;
            if (startPage < 1) startPage = 1;
        }

        let displayedPages = endPage - startPage + 1;
        if (displayedPages > maxPages) {
            endPage = startPage + maxPages - 1;
            if (endPage > totalPages) endPage = totalPages;
        }

        for (let i = startPage; i <= endPage; i++) {
            addPageButton(i.toString(), i, i === currentPage);
        }

        if (currentPage < totalPages) addPageButton('>', currentPage + 1);
        if (currentPage < totalPages) addPageButton('>>', totalPages);

        addRefreshButtons();
        updateRefreshBadge();
    }

    // 갱신 버튼 뱃지: 숨겨진 채 대기 중인 아이템 수 표시
    function updateRefreshBadge() {
        if (!refreshBtn || isLoading) return;
        const pending = getPendingCount();
        refreshBtn.textContent = pending > 0 ? `갱신 (+${pending})` : '갱신';
    }

    // ===== 갱신 공통 로직: batches개의 배치를 로드하고 새 아이템 첫 페이지로 이동 =====

    async function performRefresh(batches, progressBtn) {
        if (isLoading) return;
        isLoading = true;

        const buttons = [refreshBtn, multiRefreshBtn].filter(Boolean);
        buttons.forEach(b => { b.disabled = true; });

        try {
            const savedScrollY = window.scrollY;
            const prevTotal = allItems.length;

            // 이미 숨겨진 채 대기 중인 아이템은 배치 하나로 간주
            let loadedAny = getPendingCount() > 0;
            let remaining = batches - (loadedAny ? 1 : 0);

            for (let i = 0; i < remaining; i++) {
                if (progressBtn) progressBtn.textContent = `로딩 ${i + 1}/${remaining}`;
                const cur = getDiscourseItems().length;
                await triggerBottomLoad();
                const gotNew = await waitForNewItems(cur);
                if (!gotNew) break;
                loadedAny = true;
            }

            if (!loadedAny) {
                window.scrollTo(0, savedScrollY);
                alert('추가로 로드할 항목이 없습니다.');
                return;
            }

            collectAllItems();
            updateSpacer();

            // 새로 추가된 아이템이 시작되는 페이지로 자동 이동
            const firstNewPage = Math.floor(prevTotal / itemsPerPage) + 1;
            showPage(firstNewPage); // 내부에서 updatePaginationInterface() 호출 → 버튼 상태 복구
        } finally {
            isLoading = false;
            // 실패 경로에서도 버튼 상태 복구
            if (refreshBtn) { refreshBtn.disabled = false; }
            if (multiRefreshBtn) { multiRefreshBtn.disabled = false; multiRefreshBtn.textContent = `+${multiRefreshBatches}배치`; }
            updateRefreshBadge();
        }
    }

    function addRefreshButtons() {
        const baseStyle = `
            margin-left: 6px;
            padding: 5px 12px;
            color: #fff;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            white-space: nowrap;
        `;

        // 갱신 (1배치)
        refreshBtn = document.createElement('button');
        refreshBtn.textContent = '갱신';
        refreshBtn.style.cssText = baseStyle + `
            margin-left: 10px;
            background-color: #28a745;
            border: 1px solid #28a745;
        `;
        refreshBtn.addEventListener('click', async () => {
            refreshBtn.textContent = '로딩...';
            await performRefresh(1, null);
        });
        paginationContainer.appendChild(refreshBtn);

        // 연속 갱신 (N배치)
        multiRefreshBtn = document.createElement('button');
        multiRefreshBtn.textContent = `+${multiRefreshBatches}배치`;
        multiRefreshBtn.style.cssText = baseStyle + `
            background-color: #17a2b8;
            border: 1px solid #17a2b8;
        `;
        multiRefreshBtn.addEventListener('click', async () => {
            await performRefresh(multiRefreshBatches, multiRefreshBtn);
        });
        paginationContainer.appendChild(multiRefreshBtn);
    }


    function addPageButton(label, pageNum, isActive = false) {
        const button = document.createElement('button');
        button.textContent = label;
        button.dataset.page = pageNum;
        button.style.cssText = `
            margin: 0 3px;
            padding: 5px 10px;
            background-color: ${isActive ? '#0088cc' : '#f8f9fa'};
            color: ${isActive ? '#fff' : '#0088cc'};
            border: 1px solid ${isActive ? '#0088cc' : '#ddd'};
            border-radius: 3px;
            cursor: pointer;
            font-weight: ${isActive ? 'bold' : 'normal'};
            transition: all 0.2s ease;
            font-size: 12px;
            white-space: nowrap;
        `;
        button.addEventListener('click', () => {
            showPage(pageNum);
        });
        button.addEventListener('mouseover', () => {
            if (!isActive) button.style.backgroundColor = '#e9ecef';
        });
        button.addEventListener('mouseout', () => {
            if (!isActive) button.style.backgroundColor = '#f8f9fa';
        });
        paginationContainer.appendChild(button);
    }

    function showPage(pageNum) {
        // 재렌더링 대비: 표시 직전에 살아있는 DOM 노드로 재동기화
        syncAllItemsFromDom();
        if (!allItems.length) return;

        const totalPages = Math.ceil(allItems.length / itemsPerPage);
        if (pageNum < 1) pageNum = 1;
        if (pageNum > totalPages) pageNum = totalPages;

        currentPage = pageNum;
        const startIdx = (pageNum - 1) * itemsPerPage;
        const endIdx = Math.min(startIdx + itemsPerPage, allItems.length);
        allItems.forEach(item => item.style.display = 'none');
        for (let i = startIdx; i < endIdx; i++) {
            if (allItems[i]) allItems[i].style.display = '';
        }
        console.log(`페이지 ${pageNum} 표시: ${endIdx - startIdx}개 아이템`);
        updateSpacer();
        updatePaginationInterface();
        saveState(); // 뒤로가기 복원용 상태 저장
        const scrollTarget = contentContainer.offsetTop - 100;
        window.scrollTo({top: Math.max(0, scrollTarget), behavior: 'smooth'});
        if (history.pushState) {
            try {
                const pageParam = new URLSearchParams(window.location.search);
                pageParam.set('custom_page', pageNum);
                const newUrl = window.location.pathname + '?' + pageParam.toString();
                window.history.replaceState({path: newUrl}, '', newUrl);
            } catch (e) {
                console.error("URL 업데이트 오류:", e);
            }
        }
    }

    function isInsideMedia(node) {
        if (!node) return false;
        let el = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
        while (el) {
            const tag = el.tagName && el.tagName.toLowerCase();
            if (tag === 'video' || tag === 'audio' || tag === 'img' || tag === 'iframe') {
                return true;
            }
            el = el.parentElement;
        }
        return false;
    }

    function setupContentObserver() {
        if (observer) observer.disconnect();

        observer = new MutationObserver((mutations) => {
            let needsUpdate = false;
            for (const mutation of mutations) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    if (paginationContainer && (paginationContainer.contains(mutation.target) || mutation.target === paginationContainer)) {
                        continue;
                    }
                    if (spacerEl && (mutation.target === spacerEl || spacerEl.contains(mutation.target))) {
                        continue;
                    }
                    if (isInsideMedia(mutation.target)) {
                        continue;
                    }
                    if (contentContainer.contains(mutation.target) || mutation.target === contentContainer) {
                        needsUpdate = true;
                        break;
                    }
                }
            }
            // 자동 갱신 제거: 새 콘텐츠는 숨김 + 스페이서 갱신 + 뱃지 갱신만 하고
            // 페이지네이션 반영은 갱신 버튼에서만 수행
            if (needsUpdate && !isUpdatingPagination) {
                isUpdatingPagination = true;
                setTimeout(() => {
                    hideNewItemsOnly();
                    isUpdatingPagination = false;
                }, 300);
            }
        });

        observer.observe(contentContainer, {childList: true, subtree: true});
        console.log("콘텐츠 변화 감지 옵저버 설정됨");
    }

    function checkUrlForPageNumber(savedPage) {
        try {
            const urlParams = new URLSearchParams(window.location.search);
            let pageParam = urlParams.get('custom_page');
            const totalItems = allItems.length;
            const totalPages = Math.ceil(totalItems / itemsPerPage);
            let pageNum = 1;
            if (pageParam !== null) {
                pageNum = parseInt(pageParam);
            } else if (savedPage !== null && savedPage !== undefined) {
                // URL에 파라미터가 없으면 저장된 페이지로 복원
                pageNum = savedPage;
            }
            if (isNaN(pageNum) || pageNum < 1) {
                pageNum = 1;
            } else if (pageNum > totalPages) {
                pageNum = totalPages > 0 ? totalPages : 1;
            }
            console.log(`페이지 번호 결정: ${pageNum}`);
            currentPage = pageNum;
            showPage(pageNum);
        } catch (e) {
            console.error("URL 파라미터 확인 오류:", e);
            showPage(1);
        }
    }

})();