Greasy Fork

아카라이브 듀얼스크린

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

// ==UserScript==
// @name         아카라이브 듀얼스크린
// @namespace    http://tampermonkey.net/
// @version      1.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;
            flex-shrink: 0; // 추가: 패널이 가로로 축소되지 않도록 함
        }

        .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;
            padding-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, .list-table.hybrid');
                const currentListTable = rightPanel.querySelector('.list-table.table, .list-table.hybrid');

                if (!newListTable || !currentListTable) return;

                // 카테고리 관련 요소도 업데이트
                const newBoardCategory = doc.querySelector('.board-category-wrapper');
                const currentBoardCategory = rightPanel.querySelector('.board-category-wrapper');

                if (newBoardCategory && currentBoardCategory) {
                    currentBoardCategory.replaceWith(cloneWithEvents(newBoardCategory));
                }

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

                // 게시글 링크 처리 - hybrid와 일반 구조 모두 지원
                const handleRowClick = (e) => {
                    const rowLink = e.target.closest('a.vrow.column');
                    if (!rowLink) return;

                    const href = rowLink.getAttribute('href');
                    if (!href) return;

                    window.location.href = href;
                };

                newListTable.addEventListener('click', handleRowClick, true);

                // 모드(전체글/개념글) 활성화 상태 업데이트 - 선택자 수정
                const modeButtons = rightPanel.querySelectorAll('.btns-board a.btn-arca, .btns-board a.btn-danger');
                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');
                    }
                });

                // 페이지네이션 교체
                const newPagination = doc.querySelector('.pagination-wrapper');
                const currentPagination = rightPanel.querySelector('.pagination-wrapper');
                if (newPagination && currentPagination) {
                    currentPagination.replaceWith(cloneWithEvents(newPagination));
                }

                // 목록 교체
                currentListTable.replaceWith(cloneWithEvents(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();
    }
})();