Greasy Fork

Greasy Fork is available in English.

아카라이브 듀얼스크린

아카라이브의 게시글을 게시글과 게시글 목록으로 나누어 듀얼스크린으로 변경합니다.

当前为 2025-01-03 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         아카라이브 듀얼스크린
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  아카라이브의 게시글을 게시글과 게시글 목록으로 나누어 듀얼스크린으로 변경합니다.
// @icon         https://www.google.com/s2/favicons?sz=64&domain=arca.live
// @author       스토커
// @match        *://*.arca.live/b/*/*
// @exclude      *://*.arca.live/b/*/write*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // write 페이지면 아예 실행하지 않음
    if (window.location.href.includes('/write') || window.location.href.includes('/edit')) {
        return;
    }

    const style = document.createElement('style');
    style.textContent = `
        :root {
            color-scheme: light dark;
        }

        body {
            padding: 0;
            overflow: hidden;
            background: var(--color-bg-body) !important;
        }

        html, html.theme-light {
            background: var(--color-bg-root) !important;
        }

        .dual-screen-container {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            display: flex;
            width: 100%;
            margin: 0;
            padding: 0;
            background: var(--color-bg-main);
            border: none;
            height: 100vh;
            min-height: 100vh;
            position: fixed;
            overflow: hidden;
        }

        .left-panel, .right-panel {
            height: 100%;
            position: relative;
            background: var(--color-bg-main);
            color: var(--color-text);
        }

        .left-panel {
            min-width: 200px;
            flex: 2;
            padding: 0;
            border-right: 1px solid var(--color-bd-inner);
            overflow-y: auto;
            overscroll-behavior-y: contain !important;
        }

        .left-panel .navbar {
            position: relative !important;
            width: 100%;
            background: var(--color-bg-navbar) !important;
            z-index: 100;
            color: var(--color-text-opposite) !important;
        }

        .left-panel .navbar a .nav-link {
            color: var(--color-text-opposite) !important;
        }

        .left-panel .board-title {
            position: relative !important;
            width: 100%;
            background: var(--color-bg-main) !important;
            margin: 0em 0rem 0rem 0rem !important;
            padding: 10px;
            border-bottom: 1px solid var(--color-bd-inner);
            z-index: 99;
        }

        .resize-handle {
            width: 6px;
            background: var(--color-bd-inner);
            cursor: col-resize;
            transition: background 0.3s;
            position: relative;
            min-height: 100vh;
            height: 100%;
            flex-shrink: 0;
            z-index: 1000;
        }

        .resize-handle:hover {
            background: var(--color-bd-outer);
        }

        .resize-handle.dragging {
            background: var(--color-bd-outer);
        }

        .left-panel .content-container {
            display: flex;
            flex-direction: column;
        }

        .left-panel .navbar {
            position: relative !important; /* 변경: fixed -> relative */
            width: 100%;
            background: #4f5464 !important;
            z-index: 100;
            color: #fff !important;
        }

        .left-panel .navbar a .nav-item dropdown:not {
            color: #fff !important;
        }

        .left-panel .navbar .nav-link {
            color: #fff !important;
        }

        .user-dropdown-menu {
            left: -50% !important;
        }

        .noti-dropdown-menu{
            left: -50% !important;
        }

        .left-panel .board-title {
            position: relative !important; /* 변경: fixed -> relative */
            width: 100%;
            background: #fff;
            margin: 0em 0rem 0rem 0rem !important; /* 변경: margin 값 조정 */
            padding: 10px;
            border-bottom: 1px solid #ddd;
            z-index: 99;
        }

        .left-panel .article-body {
            margin-top: 0 !important;
            padding: 10px;
        }

        .left-panel .navbar .navbar-brand svg {
            width: 21px;
            height: 21px;
        }

        .right-panel {
            min-width: 200px;
            flex: 1;
            display: flex;
            flex-direction: column;
        }

        .right-panel .right-sidebar {
            padding: 10px;
            border-bottom: 1px solid #ddd;
            margin: 0;
        }

        .right-panel .included-article-list-wrapper {
            flex: 1;
            overflow-y: auto;
            padding: 0px;
            overscroll-behavior: contain;
            touch-action: pan-y;
            margin-bottom: 3rem;
        }

        .right-panel .included-article-list {
            marin-top: 0 !important;
    }   

        .right-panel .footer {
            padding: 10px;
            border-top: 1px solid #ddd;
        }

        .reply-form__user-info__avatar{
            width: 1.4em !important;
        }
        
        .board-category-wrapper {
            overflow-x: auto !important;
            white-space: nowrap !important;
            cursor: grab !important;
            user-select: none !important;
        }

        .board-category-wrapper.dragging {
            cursor: grabbing !important;
        }

        .board-category {
            display: flex !important;
            flex-wrap: nowrap !important;
            padding-bottom: 5px !important;
        }

        .board-category a {
            pointer-events: auto !important;
        }

        .board-category.dragging a {
            pointer-events: none !important;
        }
        
    `;

    // Add dark theme support
    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        document.documentElement.classList.add('theme-dark');
    }

    // Watch for theme changes
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
        if (e.matches) {
            document.documentElement.classList.add('theme-dark');
        } else {
            document.documentElement.classList.remove('theme-dark');
        }
    });

    document.head.appendChild(style);

    // 설정 관리를 위한 객체 추가
    const Settings = {
        storageKey: 'arcalive_dualscreen_settings',
        defaults: {
            isSwapped: false,
            leftPanelWidth: '66.66%',
            rightPanelWidth: '33.33%'
        },

        scrollStorageKey: 'arcalive_dualscreen_scroll',

        // 스크롤 위치 저장
        saveScrollPosition(channelName, articleId, leftScroll, rightScroll) {
            try {
                const scrollData = this.loadScrollPositions();

                // 채널별 스크롤 위치 저장
                if (!scrollData.channels[channelName]) {
                    scrollData.channels[channelName] = {};
                }
                scrollData.channels[channelName].scroll = rightScroll;

                // 게시글별 스크롤 위치 저장
                if (articleId) {
                    if (!scrollData.articles) scrollData.articles = {};
                    scrollData.articles[articleId] = leftScroll;
                }

                localStorage.setItem(this.scrollStorageKey, JSON.stringify(scrollData));
            } catch (e) {
                console.error('스크롤 위치 저장 실패:', e);
            }
        },

        // 스크롤 위치 불러오기
        loadScrollPositions() {
            try {
                const saved = localStorage.getItem(this.scrollStorageKey);
                return saved ? JSON.parse(saved) : { channels: {}, articles: {} };
            } catch (e) {
                console.error('스크롤 위치 로드 실패:', e);
                return { channels: {}, articles: {} };
            }
        },

        // 특정 게시글/채널의 스크롤 위치 가져오기
        getScrollPosition(channelName, articleId) {
            const scrollData = this.loadScrollPositions();
            return {
                leftScroll: articleId ? scrollData.articles[articleId] || 0 : 0,
                rightScroll: scrollData.channels[channelName]?.scroll || 0
            };
        },

        // load() 함수 수정
        load() {
            try {
                const saved = localStorage.getItem(this.storageKey);
                if (!saved) {
                    return this.defaults;
                }
                const parsed = JSON.parse(saved);
                // 저장된 값이 있으면 기본값과 병합
                return {
                    ...this.defaults,
                    ...parsed
                };
            } catch (e) {
                console.error('설정 로드 실패:', e);
                return this.defaults;
            }
        },

        save(settings) {
            try {
                // 숫자를 백분율 문자열로 변환하여 저장
                const saveData = {
                    isSwapped: settings.isSwapped,
                    leftPanelWidth: typeof settings.leftPanelWidth === 'number' ?
                        settings.leftPanelWidth + '%' : settings.leftPanelWidth,
                    rightPanelWidth: typeof settings.rightPanelWidth === 'number' ?
                        settings.rightPanelWidth + '%' : settings.rightPanelWidth
                };
                localStorage.setItem(this.storageKey, JSON.stringify(saveData));
            } catch (e) {
                console.error('설정 저장 실패:', e);
            }
        },

        // 현재 레이아웃 상태 저장
        saveCurrentLayout(isSwapped, leftPanel, rightPanel) {
            const totalWidth = leftPanel.parentElement.offsetWidth - 6; // 핸들바 너비(6px) 제외
            const leftWidth = (leftPanel.offsetWidth / totalWidth) * 100;
            const rightWidth = (rightPanel.offsetWidth / totalWidth) * 100;

            this.save({
                isSwapped: isSwapped,
                leftPanelWidth: leftWidth,
                rightPanelWidth: rightWidth
            });
        }
    };

    function initializeDualScreen() {

        const navbar = document.querySelector('.navbar');
        const boardTitle = document.querySelector('.board-title');
        const articleWrapper = document.querySelector('.article-wrapper');
        const includedArticles = document.querySelector('.included-article-list');
        const rightSidebar = document.querySelector('.right-sidebar');
        const footer = document.querySelector('.footer');

        if (!articleWrapper || !includedArticles) return;

        // 컨테이너 생성
        const container = document.createElement('div');
        container.className = 'dual-screen-container';

        // 좌측 패널
        const leftPanel = document.createElement('div');
        leftPanel.className = 'left-panel';

        // 좌측 패널 내부 컨테이너 생성
        const contentContainer = document.createElement('div');
        contentContainer.className = 'content-container';

        // navbar와 board-title을 content-container에 추가
        if (navbar) {
            const navbarClone = navbar.cloneNode(true);
            contentContainer.appendChild(navbarClone);
        }

        if (boardTitle) {
            const boardTitleClone = boardTitle.cloneNode(true);
            contentContainer.appendChild(boardTitleClone);
        }

        // article-wrapper를 article-body div로 감싸서 추가
        const articleBody = document.createElement('div');
        articleBody.className = 'article-body';

        // DOM 구조와 이벤트를 모두 복제하는 함수
        function cloneWithEvents(originalElement) {
            const clone = originalElement.cloneNode(true);

            // 인라인 이벤트 속성 복사 (onclick 등)
            Array.from(originalElement.attributes).forEach(attr => {
                if (attr.name.startsWith('on')) {
                    clone.setAttribute(attr.name, attr.value);
                }
            });

            // 자식 요소들도 이벤트와 함께 복제
            Array.from(originalElement.getElementsByTagName('*')).forEach((element, index) => {
                const clonedElement = clone.getElementsByTagName('*')[index];
                if (clonedElement) {
                    // 모든 이벤트 리스너 복제
                    const eventTypes = ['click', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
                        'mousemove', 'submit', 'change', 'input'];

                    eventTypes.forEach(type => {
                        element.addEventListener(type, (event) => {
                            // 원본 요소에서 이벤트 발생시키기
                            const newEvent = new Event(type, {
                                bubbles: true,
                                cancelable: true,
                                composed: true
                            });
                            Object.defineProperty(newEvent, 'target', {
                                value: element,
                                enumerable: true
                            });
                            element.dispatchEvent(newEvent);
                        });
                    });

                    // 인라인 이벤트 핸들러 복사
                    Array.from(element.attributes).forEach(attr => {
                        if (attr.name.startsWith('on')) {
                            clonedElement.setAttribute(attr.name, attr.value);
                        }
                    });
                }
            });

            // 1. originalElement에 달린 모든 이벤트 리스너를 복제된 요소로 전달
            clone.addEventListener('click', (e) => {
                // 외부 스크립트 버튼 클릭은 원본에게 위임
                const isExtension = e.target.closest('[class*="MuiButton"], [class*="external-handler"], [id*="download"], [class*="css-"]');
                if (isExtension) {
                    e.preventDefault();
                    e.stopPropagation();

                    const originalTarget = findOriginalElement(e.target, originalElement, clone);
                    if (originalTarget) {
                        originalTarget.click();
                    }
                    return;
                }
            }, true);

            // 2. 모든 자식 요소들의 이벤트도 위임처리
            const processNode = (original, cloned) => {
                // data 속성 복사 
                [...original.attributes].forEach(attr => {
                    if (attr.name.startsWith('data-')) {
                        cloned.setAttribute(attr.name, attr.value);
                    }
                });

                // 자식요소들 재귀적으로 처리
                const originalChildren = original.children;
                const clonedChildren = cloned.children;

                for (let i = 0; i < originalChildren.length; i++) {
                    processNode(originalChildren[i], clonedChildren[i]);
                }
            };

            processNode(originalElement, clone);

            return clone;
        }

        // 동적 변경사항을 감시하고 처리하는 MutationObserver 설정
        function setupDynamicContentObserver(originalContainer, clonedContainer) {
            const observer = new MutationObserver((mutations) => {
                mutations.forEach(mutation => {
                    if (mutation.type === 'childList') {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === 1) { // Element node
                                // 새로 추가된 요소를 찾아서 복제
                                const clonedNode = cloneWithEvents(node);
                                const similarNodes = clonedContainer.querySelectorAll(`*[class="${node.className}"]`);
                                if (similarNodes.length > 0) {
                                    similarNodes[0].replaceWith(clonedNode);
                                } else {
                                    clonedContainer.appendChild(clonedNode);
                                }
                            }
                        });
                    }
                    else if (mutation.type === 'attributes') {
                        // 속성 변경 동기화
                        const target = mutation.target;
                        const clonedTarget = clonedContainer.querySelector(`*[class="${target.className}"]`);
                        if (clonedTarget) {
                            clonedTarget.setAttribute(mutation.attributeName,
                                target.getAttribute(mutation.attributeName));
                        }
                    }
                });
            });

            observer.observe(originalContainer, {
                childList: true,
                subtree: true,
                attributes: true,
                characterData: true
            });

            return observer;
        }

        // 원본 요소 찾기 함수 추가
        function findOriginalElement(clonedElement, originalRoot, clonedRoot) {
            const path = [];
            let element = clonedElement;

            while (element && element !== clonedRoot) {
                const parent = element.parentElement;
                if (!parent) break;

                const index = Array.from(parent.children).indexOf(element);
                path.unshift({
                    index,
                    className: element.className,
                    id: element.id,
                    tagName: element.tagName
                });
                element = parent;
            }

            return path.reduce((current, pathItem) => {
                if (!current) return null;

                // className, id, tagName으로 매칭
                const children = Array.from(current.children);
                return children.find(child =>
                    child.tagName === pathItem.tagName &&
                    child.className === pathItem.className &&
                    (!pathItem.id || child.id === pathItem.id)
                );
            }, originalRoot);
        }

        // article-wrapper 복제 시 이벤트도 함께 복제
        const clonedArticleWrapper = cloneWithEvents(articleWrapper);
        const articleObserver = setupDynamicContentObserver(articleWrapper, clonedArticleWrapper);

        // included articles 복제 시 이벤트도 함께 복제
        const clonedIncludedArticles = cloneWithEvents(includedArticles);
        const includedArticlesObserver = setupDynamicContentObserver(includedArticles, clonedIncludedArticles);

        // 페이지 언로드 시 옵저버 정리
        window.addEventListener('unload', () => {
            articleObserver.disconnect();
            includedArticlesObserver.disconnect();
        });

        // Deep clone the article wrapper with all event listeners
        // const clonedArticleWrapper = articleWrapper.cloneNode(true); <- 이 부분 제거

        // Re-attach event handlers for rating forms and share button
        const originalRateUpForm = articleWrapper.querySelector('#rateUpForm');
        const originalRateDownForm = articleWrapper.querySelector('#rateDownForm');
        const originalShareBtn = articleWrapper.querySelector('#articleShareBtn');
        const clonedRateUpForm = clonedArticleWrapper.querySelector('#rateUpForm');
        const clonedRateDownForm = clonedArticleWrapper.querySelector('#rateDownForm');
        const clonedShareBtn = clonedArticleWrapper.querySelector('#articleShareBtn');

        // 추천 버튼 이벤트 처리
        if (originalRateUpForm && clonedRateUpForm) {
            clonedRateUpForm.addEventListener('submit', (e) => {
                e.preventDefault();
                const submitEvent = new Event('submit', {
                    bubbles: true,
                    cancelable: true
                });
                originalRateUpForm.dispatchEvent(submitEvent);
                return false;
            });
        }

        // 비추천 버튼 이벤트 처리 
        if (originalRateDownForm && clonedRateDownForm) {
            clonedRateDownForm.addEventListener('submit', (e) => {
                e.preventDefault();
                const submitEvent = new Event('submit', {
                    bubbles: true,
                    cancelable: true
                });
                originalRateDownForm.dispatchEvent(submitEvent);
                return false;
            });
        }

        // 공유 버튼 이벤트 처리
        if (originalShareBtn && clonedShareBtn) {
            clonedShareBtn.addEventListener('click', (e) => {
                e.preventDefault();
                const clickEvent = new MouseEvent('click', {
                    bubbles: true,
                    cancelable: true,
                    view: window
                });
                originalShareBtn.dispatchEvent(clickEvent);
                return false;
            });
        }

        // 평가 결과 동기화를 위한 MutationObserver 설정
        const ratingObserver = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                const originalCount = mutation.target;
                const targetId = originalCount.id;
                const clonedCount = clonedArticleWrapper.querySelector(`#${targetId}`);
                if (clonedCount) {
                    clonedCount.textContent = originalCount.textContent;
                }
            });
        });

        // 모든 평가 관련 요소 감시
        const ratingElements = articleWrapper.querySelectorAll('#ratingUp, #ratingDown, #ratingUpIp, #ratingDownIp');
        ratingElements.forEach(element => {
            ratingObserver.observe(element, {
                childList: true,
                characterData: true,
                subtree: true
            });
        });

        articleBody.appendChild(clonedArticleWrapper);
        contentContainer.appendChild(articleBody);

        // content-container를 left-panel에 추가
        leftPanel.appendChild(contentContainer);

        // 리사이즈 핸들
        const resizeHandle = document.createElement('div');
        resizeHandle.className = 'resize-handle';

        // 우측 패널
        const rightPanel = document.createElement('div');
        rightPanel.className = 'right-panel';

        // 우측 사이드바 wrapper
        if (rightSidebar) {
            rightPanel.appendChild(rightSidebar);
        }

        // included articles wrapper
        const includedArticlesWrapper = document.createElement('div');
        includedArticlesWrapper.className = 'included-article-list-wrapper';

        // 여기서 중복 선언 제거
        // const clonedIncludedArticles = includedArticles.cloneNode(true); <- 이 줄 삭제

        // 이미 위에서 cloneWithEvents로 복제한 clonedIncludedArticles를 사용
        includedArticlesWrapper.appendChild(clonedIncludedArticles);
        rightPanel.appendChild(includedArticlesWrapper);

        // 카테고리 스크롤 기능을 위한 이벤트 핸들러 추가 
        const categoryWrappers = clonedIncludedArticles.querySelectorAll('.board-category');

        // 카테고리 드래그 상태 공유를 위한 전역 변수
        let isDragging = false;

        // 카테고리 드래그 이벤트 핸들러 수정
        if (categoryWrappers?.length) {
            categoryWrappers.forEach(category => {
                let startX;
                let startScrollLeft;
                let startTime;
                let lastMoveTime;

                category.addEventListener('mousedown', (e) => {
                    startX = e.pageX;
                    startScrollLeft = category.scrollLeft;
                    startTime = Date.now();
                    lastMoveTime = startTime;
                    isDragging = false; // 드래그 시작시 초기화
                });

                category.addEventListener('mousemove', (e) => {
                    if (!startX) return;

                    const walk = e.pageX - startX;
                    if (Math.abs(walk) > 5) {
                        isDragging = true;
                        category.scrollLeft = startScrollLeft - walk;
                        lastMoveTime = Date.now();
                    }
                });

                category.addEventListener('mouseup', () => {
                    startX = null;
                    // isDragging은 약간 지연 후 false로 설정
                    setTimeout(() => {
                        isDragging = false;
                    }, 50);
                });

                category.addEventListener('mouseleave', () => {
                    startX = null;
                    isDragging = false;
                });
            });
        }

        includedArticlesWrapper.appendChild(clonedIncludedArticles);
        rightPanel.appendChild(includedArticlesWrapper);

        // footer 이동
        if (footer) {
            rightPanel.appendChild(footer);
        }

        // 조립
        container.appendChild(leftPanel);
        container.appendChild(resizeHandle);
        container.appendChild(rightPanel);

        // 저장된 설정 로드하고 isPanelsSwapped 초기화
        const settings = Settings.load();
        let isPanelsSwapped = settings.isSwapped;

        // 초기 레이아웃 설정
        const applyLayout = () => {
            const settings = Settings.load();

            leftPanel.style.width = settings.leftPanelWidth;
            leftPanel.style.flex = 'none';
            rightPanel.style.width = settings.rightPanelWidth;
            rightPanel.style.flex = 'none';

            // container.innerHTML = ''; 대신 자식 요소들만 제거
            while (container.firstChild) {
                container.removeChild(container.firstChild);
            }

            // 스왑 상태에 따라 순서만 변경
            if (isPanelsSwapped) {
                container.appendChild(rightPanel);
                container.appendChild(resizeHandle);
                container.appendChild(leftPanel);
            } else {
                container.appendChild(leftPanel);
                container.appendChild(resizeHandle);
                container.appendChild(rightPanel);
            }
        };

        // 초기 레이아웃 적용
        applyLayout();

        // URL에서 채널명과 게시글 ID 추출
        const urlParts = window.location.pathname.split('/');
        const channelName = urlParts[2];
        const articleId = urlParts[3];

        // 스크롤 이벤트 처리 함수
        const handleScroll = () => {
            const leftScroll = leftPanel.scrollTop;
            const rightScroll = rightPanel.querySelector('.included-article-list-wrapper').scrollTop;
            Settings.saveScrollPosition(channelName, articleId, leftScroll, rightScroll);
        };

        // 스크롤 이벤트 리스너 등록 (디바운스 적용)
        let scrollTimeout;
        leftPanel.addEventListener('scroll', () => {
            clearTimeout(scrollTimeout);
            scrollTimeout = setTimeout(handleScroll, 100);
        });

        const rightScrollElement = rightPanel.querySelector('.included-article-list-wrapper');
        rightScrollElement.addEventListener('scroll', () => {
            clearTimeout(scrollTimeout);
            scrollTimeout = setTimeout(handleScroll, 100);
        });

        // 초기 스크롤 위치 복원
        const restoreScrollPosition = () => {
            const { leftScroll, rightScroll } = Settings.getScrollPosition(channelName, articleId);
            if (leftScroll) leftPanel.scrollTop = leftScroll;
            if (rightScroll) rightScrollElement.scrollTop = rightScroll;
        };

        // DOM이 완전히 로드된 후 스크롤 위치 복원
        setTimeout(restoreScrollPosition, 100);

        // 패널 스왑 함수 수정
        const swapPanels = () => {
            isPanelsSwapped = !isPanelsSwapped;

            // 현재 패널들의 너비를 백분율로 계산
            const totalWidth = container.offsetWidth - 6;
            const biggerWidth = Math.max(leftPanel.offsetWidth, rightPanel.offsetWidth);
            const smallerWidth = Math.min(leftPanel.offsetWidth, rightPanel.offsetWidth);

            // 더 큰 패널의 비율을 계산
            const biggerRatio = (biggerWidth / totalWidth) * 100;
            const smallerRatio = (smallerWidth / totalWidth) * 100;

            // 현재 왼쪽 패널이 더 큰지 여부 확인
            const isLeftBigger = leftPanel.offsetWidth > rightPanel.offsetWidth;

            // 스왑 후에는 비율을 반대로 적용
            if (isLeftBigger) {
                leftPanel.style.width = `${smallerRatio}%`;
                rightPanel.style.width = `${biggerRatio}%`;
            } else {
                leftPanel.style.width = `${biggerRatio}%`;
                rightPanel.style.width = `${smallerRatio}%`;
            }

            applyLayout();
            Settings.saveCurrentLayout(isPanelsSwapped, leftPanel, rightPanel);
        };

        // 더블클릭 이벤트 추가
        resizeHandle.addEventListener('dblclick', swapPanels);

        // 원래 요소 교체 및 제거
        articleWrapper.parentNode.replaceChild(container, articleWrapper);
        includedArticles.remove();
        if (rightSidebar) rightSidebar.remove();
        if (footer) footer.remove();
        if (navbar) navbar.remove();
        if (boardTitle) boardTitle.remove();

        // 리사이징 이벤트 설정
        let isResizing = false;
        let startX, startWidth;

        const handleResize = (e) => {
            if (!isResizing) return;

            const containerWidth = container.offsetWidth;
            const minWidth = 200;
            const maxWidth = containerWidth - minWidth;

            const currentX = e.pageX;
            const diffX = currentX - startX;
            const targetPanel = isPanelsSwapped ? rightPanel : leftPanel;
            const otherPanel = isPanelsSwapped ? leftPanel : rightPanel;

            let newWidth = startWidth + diffX;
            newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth));
            const otherWidth = containerWidth - newWidth - 6;

            // 너비 적용
            targetPanel.style.width = `${newWidth}px`;
            targetPanel.style.flex = 'none';
            otherPanel.style.width = `${otherWidth}px`;
            otherPanel.style.flex = 'none';

            // mousemove 이벤트에서 실시간으로 저장하도록 수정
            const totalWidth = container.offsetWidth - 6;
            const leftWidth = parseFloat((leftPanel.offsetWidth / totalWidth) * 100).toFixed(2);
            const rightWidth = parseFloat((rightPanel.offsetWidth / totalWidth) * 100).toFixed(2);

            Settings.save({
                isSwapped: isPanelsSwapped,
                leftPanelWidth: leftWidth + '%',
                rightPanelWidth: rightWidth + '%'
            });
        };

        resizeHandle.addEventListener('mousedown', (e) => {
            isResizing = true;
            resizeHandle.classList.add('dragging');
            startX = e.pageX;
            startWidth = (isPanelsSwapped ? rightPanel : leftPanel).offsetWidth;  // isSwapped -> isPanelsSwapped
        });

        document.addEventListener('mousemove', handleResize);

        // mouseup 이벤트 리스너에서는 저장 로직 제거
        document.addEventListener('mouseup', () => {
            if (isResizing) {
                isResizing = false;
                resizeHandle.classList.remove('dragging');
            }
        });

        // 윈도우 리사이즈 대응
        let resizeTimeout;
        window.addEventListener('resize', () => {
            clearTimeout(resizeTimeout);
            resizeTimeout = setTimeout(applyLayout, 100);
        });

        // 게시글 목록 비동기 로드 함수 수정
        async function loadArticleList(url, pushState = true) {
            try {
                // URL에 페이지 파라미터가 없으면 p=1 추가
                const targetUrl = new URL(url, window.location.origin);
                if (!targetUrl.searchParams.has('p')) {
                    targetUrl.searchParams.set('p', '1');
                }
                url = targetUrl.toString();

                const response = await fetch(url);
                const html = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');

                // list-table 부분만 교체되도록 수정
                const newListTable = doc.querySelector('.list-table.table');
                const currentListTable = rightPanel.querySelector('.list-table.table');

                if (!newListTable || !currentListTable) return;

                // URL과 현재 상태 파싱
                const currentUrl = new URL(url, window.location.origin);
                const category = currentUrl.searchParams.get('category');
                const page = currentUrl.searchParams.get('p');
                const mode = currentUrl.searchParams.get('mode');

                // 카테고리 활성화 상태 업데이트와 클릭 이벤트 처리 통합
                const categoryLinks = rightPanel.querySelectorAll('.board-category a');
                categoryLinks.forEach(link => {
                    // 활성화 상태 업데이트
                    const linkUrl = new URL(link.href);
                    const linkCategory = linkUrl.searchParams.get('category');
                    if ((!category && !linkCategory && link.classList.contains('active')) ||
                        (linkCategory === category)) {
                        link.classList.add('active');
                    } else {
                        link.classList.remove('active');
                    }

                    // 기존 이벤트 리스너 제거 및 새로운 이벤트 리스너 추가
                    const newLink = link.cloneNode(true);
                    newLink.addEventListener('click', (e) => {
                        if (!isDragging) { // 드래그 중이 아닐 때만 클릭 처리
                            e.preventDefault();
                            loadArticleList(newLink.href);
                        }
                    });
                    link.parentNode.replaceChild(newLink, link);
                });

                // 게시글 링크 수정
                newListTable.addEventListener('click', (e) => {
                    const rowLink = e.target.closest('a.vrow.column');
                    if (!rowLink) return;

                    // href 속성이 있는 링크인 경우에만 처리
                    const href = rowLink.getAttribute('href');
                    if (!href) return;

                    // 링크로 직접 이동
                    window.location.href = href;
                }, true);

                // 페이지네이션 활성화 상태 업데이트
                const pagination = rightPanel.querySelector('.pagination');
                if (pagination) {
                    pagination.querySelectorAll('.page-item').forEach(item => {
                        const pageLink = item.querySelector('a');
                        if (!pageLink) return;

                        const pageUrl = new URL(pageLink.href);
                        const thisPage = pageUrl.searchParams.get('p');

                        if ((!page && !thisPage) || (page === thisPage)) {
                            item.classList.add('active');
                        } else {
                            item.classList.remove('active');
                        }
                    });
                }

                // 모드(전체글/개념글) 활성화 상태 업데이트
                const modeButtons = rightPanel.querySelectorAll('.btns-board .btn-arca');
                modeButtons.forEach(btn => {
                    const btnUrl = new URL(btn.href);
                    const btnMode = btnUrl.searchParams.get('mode');
                    if ((!mode && !btnMode) || (mode === btnMode)) {
                        btn.classList.add('active');
                    } else {
                        btn.classList.remove('active');
                    }
                });

                // 목록만 교체
                currentListTable.replaceWith(newListTable);

                // URL 히스토리 업데이트
                if (pushState) {
                    history.pushState({}, '', url);
                }

            } catch (error) {
                console.error('게시글 목록 로드 실패:', error);
            }
        }

        // 게시글의 평가/공유 버튼 이벤트 설정 함수
        function setupArticleEventHandlers(articleWrapper) {
            const rateUpForm = articleWrapper.querySelector('#rateUpForm');
            const rateDownForm = articleWrapper.querySelector('#rateDownForm');
            const shareBtn = articleWrapper.querySelector('#articleShareBtn');

            if (rateUpForm) {
                rateUpForm.addEventListener('submit', (e) => {
                    e.preventDefault();
                    // 실제 평가 요청 보내기
                    fetch(rateUpForm.action, {
                        method: 'POST',
                        body: new FormData(rateUpForm)
                    }).then(response => response.json())
                        .then(data => {
                            // 평가 결과 업데이트
                            const ratingUp = articleWrapper.querySelector('#ratingUp');
                            const ratingUpIp = articleWrapper.querySelector('#ratingUpIp');
                            if (ratingUp) ratingUp.textContent = data.rating || '0';
                            if (ratingUpIp) ratingUpIp.textContent = data.ratingIp || '0';
                        });
                });
            }

            // 비추천 버튼도 동일한 방식으로 처리
            if (rateDownForm) {
                // ... 추천 버튼과 유사한 코드
            }

            // 공유 버튼
            if (shareBtn) {
                shareBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    // 원래 공유 기능 호출
                    window.articleShare();
                });
            }
        }

        // 검색 폼 제출 핸들러
        function handleSearchSubmit(e) {
            e.preventDefault();
            const formData = new FormData(e.target);
            const params = new URLSearchParams(formData);
            const url = `${e.target.action}?${params.toString()}`;
            loadArticleList(url);
        }

        // 정렬/필터 핸들러 설정
        function setupSortHandlers(btnsBoard) {
            // 정렬 선택
            const sortSelect = btnsBoard.querySelector('select[name="sort"]');
            if (sortSelect) {
                sortSelect.addEventListener('change', (e) => {
                    const currentUrl = new URL(window.location.href);
                    if (e.target.value) {
                        currentUrl.searchParams.set('sort', e.target.value);
                    } else {
                        currentUrl.searchParams.delete('sort');
                    }
                    loadArticleList(currentUrl.toString());
                });
            }

            // 추천컷 선택
            const cutSelect = btnsBoard.querySelector('select[name="cut"]');
            if (cutSelect) {
                cutSelect.addEventListener('change', (e) => {
                    const currentUrl = new URL(window.location.href);
                    if (e.target.value && e.target.value !== 'etc') {
                        currentUrl.searchParams.set('cut', e.target.value);
                    } else {
                        currentUrl.searchParams.delete('cut');
                    }
                    loadArticleList(currentUrl.toString());
                });
            }

            // 모드 버튼들 (전체글/개념글)
            btnsBoard.querySelectorAll('a.btn-arca').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    e.preventDefault();
                    loadArticleList(btn.href);
                });
            });
        }

        // 링크 클릭 이벤트 처리 수정
        function handleLinkClick(e) {
            const link = e.target.closest('a');
            if (!link) return;

            // 외부 확장프로그램이 추가한 버튼 클릭은 그대로 전달
            if (link.closest('[class*="MuiButton"]') ||
                link.closest('[class*="external-handler"]')) {
                return;
            }

            const channelPath = window.location.pathname.split('/')[2];
            const href = link.getAttribute('href');

            // 채널 내부 링크이거나 정렬/필터 관련 파라미터가 있는 경우
            if (href && (
                href.startsWith('/b/' + channelPath) ||
                href.includes('mode=') ||
                href.includes('sort=') ||
                href.includes('near=') ||
                href.includes('cut=')
            )) {
                e.preventDefault();
                loadArticleList(href);
            }
        }

        // 뒤로가기/앞으로가기 처리
        window.addEventListener('popstate', () => {
            loadArticleList(window.location.href, false);
        });

        // 카테고리, 검색, 정렬 등의 링크에 이벤트 핸들러 추가
        rightPanel.addEventListener('click', handleLinkClick);

        // 검색 폼 제출 이벤트 처리
        const searchForm = rightPanel.querySelector('.search-form');
        if (searchForm) {
            searchForm.addEventListener('submit', (e) => {
                e.preventDefault();
                const formData = new FormData(e.target);
                const params = new URLSearchParams(formData);
                const url = `${searchForm.action}?${params.toString()}`;
                loadArticleList(url);
            });
        }

        // 게시글 링크 처리 로직 수정
        const handleArticleClick = (e) => {
            e.preventDefault();
            const link = e.currentTarget.getAttribute('href');
            if (link) {
                window.location.href = link;
            }
        };

        // 게시글 미리보기 항목에 클릭 이벤트 추가
        const articles = document.querySelectorAll('.included-article-list .vrow.column');
        articles.forEach(article => {
            article.addEventListener('click', handleArticleClick);
            // 이미지 클릭 시 이벤트 중복 방지
            const preview = article.querySelector('.vrow-preview');
            if (preview) {
                preview.addEventListener('click', (e) => {
                    e.stopPropagation();
                    handleArticleClick(e);
                });
            }
        });

        // 2. 이벤트 위임을 처리할 핸들러 추가
        function delegateEvents(originalElement, clonedElement) {
            const eventTypes = [
                'click', 'submit', 'change', 'input',
                'mousedown', 'mouseup', 'mouseover', 'mouseout'
            ];

            eventTypes.forEach(type => {
                clonedElement.addEventListener(type, (e) => {
                    const targetElement = e.target;
                    const originalTarget = findMatchingElement(targetElement, originalElement, clonedElement);

                    if (originalTarget) {
                        const newEvent = new Event(type, {
                            bubbles: true,
                            cancelable: true,
                            composed: true
                        });
                        Object.defineProperty(newEvent, 'target', { value: originalTarget });
                        originalTarget.dispatchEvent(newEvent);
                    }
                });
            });
        }

        // 3. 원본-복제본 매칭 함수 수정
        function findMatchingElement(clonedTarget, originalRoot, clonedRoot) {
            const path = [];
            let element = clonedTarget;

            while (element && element !== clonedRoot) {
                const parent = element.parentElement;
                if (!parent) break;
                const siblings = Array.from(parent.children);
                const index = siblings.indexOf(element);
                path.unshift(index);
                element = parent;
            }

            return path.reduce((current, index) => {
                return current?.children?.[index] || null;
            }, originalRoot);
        }

        // 5. 외부 확장 프로그램 이벤트 처리
        const handleExtensionElements = () => {
            const observer = new MutationObserver((mutations) => {
                mutations.forEach(mutation => {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) { // Element node
                            const extensionButtons = node.querySelectorAll('[class*="MuiButton"], [class*="external-handler"]');
                            extensionButtons.forEach(btn => {
                                btn.addEventListener('click', (e) => {
                                    // 이벤트 버블링 허용
                                    e.stopPropagation();
                                });
                            });
                        }
                    });
                });
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        };

        // 6. 초기화 시 handleExtensionElements 호출 추가
        handleExtensionElements();

        // 7. 이미지 링크 이벤트 처리 수정
        const setupImageLinks = () => {
            document.querySelectorAll('.vrow-preview img').forEach(img => {
                img.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const link = img.closest('a');
                    if (link) {
                        window.location.href = link.href;
                    }
                });
            });
        };

        // 8. 동적으로 추가되는 이미지에 대한 이벤트 처리
        const imageObserver = new MutationObserver(() => {
            setupImageLinks();
        });

        imageObserver.observe(rightPanel.querySelector('.included-article-list-wrapper'), {
            childList: true,
            subtree: true
        });

        // 9. 초기 이미지 링크 설정
        setupImageLinks();
    }

    // 페이지 로드 완료 후 실행
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeDualScreen);
    } else {
        initializeDualScreen();
    }
})();