Greasy Fork

Greasy Fork is available in English.

Chzzk_Live SpecialChatView

파트너 스트리머(방송주인 포함)와 채팅관리자 채팅을 추출하여 채팅창 상단에 고정(8초)으로 표시

当前为 2025-04-30 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Chzzk_Live SpecialChatView
// @namespace    Chzzk_Live SpecialChatView
// @version      1.1
// @description  파트너 스트리머(방송주인 포함)와 채팅관리자 채팅을 추출하여 채팅창 상단에 고정(8초)으로 표시
// @author      주말쾌락주의        //DOGJIP 재업로드드
// @match        https://chzzk.naver.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let lastUrl = location.href;

    // ===============================
    // 1. 특수 채팅 고정 영역 생성 함수
    // ===============================
    function createSpecialChatContainer() {
        const chatWrapper = document.querySelector('[class*="live_chatting_list_wrapper__"]');
        if (!chatWrapper) return null;

        let specialContainer = document.getElementById('special-chat-container');
        if (!specialContainer) {
            specialContainer = document.createElement('div');
            specialContainer.id = 'special-chat-container';

            specialContainer.style.position = 'absolute';
            specialContainer.style.top = '50';
            specialContainer.style.left = '0';
            specialContainer.style.width = '100%';
            specialContainer.style.zIndex = '9999';
            specialContainer.style.backgroundColor = 'rgba(0,0,0,0.1)';
            specialContainer.style.color = '#fff';
            specialContainer.style.padding = '5px';
            specialContainer.style.boxSizing = 'border-box';

            chatWrapper.parentElement.insertBefore(specialContainer, chatWrapper);
            chatWrapper.style.marginTop = specialContainer.offsetHeight + 'px';
        }
        return specialContainer;
    }

    // ====================================
    // 2. 특수 채팅 메시지 추가 함수 (n초 후 제거)
    // ====================================
    function addSpecialChatMessage(nickname, message) {
        const container = createSpecialChatContainer();
        if (!container) return;

        const chatElem = document.createElement('div');
        chatElem.className = 'special-chat-message';
        chatElem.style.borderBottom = '1px solid #fff';
        chatElem.style.padding = '10px 5px';
        chatElem.textContent = `${nickname}: ${message}`;

        container.appendChild(chatElem);
        updateContainerHeight();

        setTimeout(() => {
            if (container.contains(chatElem)) {
                container.removeChild(chatElem);
                updateContainerHeight();
            }
        }, 20000); //n초 후 사라짐(1000=1초)
    }

    function updateContainerHeight() {
        const container = document.getElementById('special-chat-container');
        if (!container) return;

        const messages = container.querySelectorAll('.special-chat-message');
        let totalHeight = 0;

        messages.forEach(msg => {
            totalHeight += msg.offsetHeight;
        });

        if (messages.length > 0) {
            container.style.height = `${totalHeight}px`;
            container.style.backgroundColor = 'rgba(0, 0, 0, 0.9)';
        } else {
            container.style.height = '0px';
            container.style.backgroundColor = 'transparent';
        }
    }

    // ============================================
    // 3. 특정 클래스의 하위요소 존재 여부 체크 함수
    // ============================================
    function hasDescendantWithClass(element, classSubstring) {
        return element.querySelector(`[class*="${classSubstring}"]`) !== null;
    }

    // ===================================
    // 4. 새 채팅 메시지 처리 함수 (스트리머 등 특정 유저 확인)
    // ===================================
    function processChatMessage(node) {
        if (node.nodeType !== 1) return;
        if (!node.className.includes('live_chatting_list_item__')) return;

        const Badge = node.querySelector('[class*="badge_container__"] img');
        const isStreamer = Badge && Badge.src.includes("streamer.png");
        const isPartner = hasDescendantWithClass(node, 'name_icon__zdbVH');
        const isManager = Badge && Badge.src.includes("manager.png");
        //const isFan = Badge && Badge.src.includes("fan_03.png"); //테스트용 후원뱃지 유저

        if (!(isStreamer || isPartner || isManager)) return;
        //if (!(isStreamer || isPartner || isManager || isFan)) return; //테스트용 후원 뱃지 유저 포함

        const nicknameElem = node.querySelector('span[class^="name_text__"]');
        const nickname = nicknameElem ? nicknameElem.innerText.trim() : 'Unknown';


        const messageElem = node.querySelector('[class^="live_chatting_message_text__"]');
        const message = messageElem ? messageElem.innerText.trim() : '';

        if (nickname && message) {
            addSpecialChatMessage(nickname, message);
        }
    }

    // ===================================
    // 5. 채팅 메시지 MutationObserver (진입시 마지막 채팅 저장 후 최신 채팅만 감시)
    // ===================================
    let lastProcessedChat = null; // ✅ 마지막으로 처리한 채팅을 저장할 변수

    function startChatObserver() {
        const chatList = document.querySelector('[class*="live_chatting_list_wrapper__"]');
        if (chatList) {
            chatObserver.disconnect(); // 중복 방지

            // ✅ 처음 실행 시, 가장 마지막(최신) 채팅 메시지를 기억해둠
            if (!lastProcessedChat) {
                const chatItems = chatList.querySelectorAll('[class^="live_chatting_list_item__"]');
                if (chatItems.length > 0) {
                    lastProcessedChat = chatItems[chatItems.length - 1]; // 가장 아래(최신) 메시지를 저장
                    console.log(`[Chzzk Script] 초기 마지막 채팅 저장 완료.`);
                }
            }

            chatObserver.observe(chatList, { childList: true, subtree: true });

            chatList.addEventListener('scroll', function() {
                if (chatList.scrollTop + chatList.clientHeight < chatList.scrollHeight) {
                    const container = document.getElementById('special-chat-container');
                    if (container) {
                        container.innerHTML = "";
                    }
                }
            });
        } else {
            setTimeout(startChatObserver, 1000);
        }
    }

    // ✅ MutationObserver 수정 (마지막 채팅 이후 새로 추가된 것만 감지)
    const chatObserver = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType !== 1 || !node.className.includes('live_chatting_list_item__')) return;

                // ✅ 마지막으로 저장된 채팅 이후에 등장한 메시지만 처리
                if (!lastProcessedChat || node.compareDocumentPosition(lastProcessedChat) & Node.DOCUMENT_POSITION_FOLLOWING) {
                    processChatMessage(node);
                    lastProcessedChat = node; // ✅ 최신 채팅으로 업데이트
                }
            });
        });
    });

    function observeSpecialChatContainer() {
        const container = document.getElementById('special-chat-container');
        if (!container) return;

        const observer = new MutationObserver(() => {
            updateContainerHeight();
        });

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

    // ===================================
    // 6. 안전한 초기화 루프 (채팅창을 찾지 못하는 등 DOM생성 이전일 경우 재시도)
    // ===================================
    function safeInit(retryCount = 0) {
        const chatList = document.querySelector('[class*="live_chatting_list_wrapper__"]');
        if (chatList) {
            console.log('[Chzzk Script] 채팅 리스트 탐색 성공. 초기화 시작.');
            startChatObserver();
            observeSpecialChatContainer();
        } else {
            if (retryCount < 20) {
                console.log('[Chzzk Script] 채팅 리스트 미탐색. 재시도 중...', retryCount);
                setTimeout(() => safeInit(retryCount + 1), 500);
            } else {
                console.warn('[Chzzk Script] 채팅 리스트 탐색 실패. 초기화 중단.');
            }
        }
    }

    // ===================================
    // 7. SPA 대응: history.pushState / replaceState / popstate 감지 (타 방송인 화면으로 이동해도 정상적인 코드 실행)
    // ===================================
    (function() {
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        const onUrlChange = () => {
            if (location.href !== lastUrl) {
                console.log('[Chzzk Script] URL 변경 감지 (SPA)', location.href);
                lastUrl = location.href;
                setTimeout(() => safeInit(), 500);
            }
        };

        history.pushState = function (...args) {
            originalPushState.apply(this, args);
            onUrlChange();
        };

        history.replaceState = function (...args) {
            originalReplaceState.apply(this, args);
            onUrlChange();
        };

        window.addEventListener('popstate', onUrlChange);
    })();

    // ✅ 최초 진입 시 실행
    safeInit();

})();