Chzzk_L&V: Chatting Plus

파트너·지정 스트리머 채팅 강조 / 닉네임 각종 설정 / 드롭스 접고 펼치기 / 고정댓글, 미션 자동 제어 / 채팅창 접고 펼치기 단축키( ] ) / 채팅 새로고침 버튼

当前为 2025-07-28 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Chzzk_L&V: Chatting Plus
// @namespace    Chzzk_Live&VOD: Chatting Plus
// @version      2.1.0
// @description 파트너·지정 스트리머 채팅 강조 / 닉네임 각종 설정 / 드롭스 접고 펼치기 / 고정댓글, 미션 자동 제어 / 채팅창 접고 펼치기 단축키( ] ) / 채팅 새로고침 버튼
// @author       DOGJIP
// @match        https://chzzk.naver.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chzzk.naver.com
// ==/UserScript==

(function() {
  'use strict';

  // 기본 설정
  const DEFAULTS = {
      streamer: ['고수달','냐 미 Nyami','새 담','청 묘','침착맨','삼식123','레니아워 RenieHouR'],
      exception: ['인챈트 봇','픽셀봇','스텔라이브 봇'],
      fixUnreadable: true,
      removeHighlight: true,
      truncateName: true,
      dropsToggle: true,
      missionHover: true
  };

// chzzk_knife_tracker용 설정 객체
const KNIFE_CONFIG = {
chatContainerSelector: '.live_chatting_list_container__vwsbZ',
chatListSelector:      '.live_chatting_list_wrapper__a5XTV',
maxMessages:           100,
defaultStreamers:      DEFAULTS.streamer,
defaultExceptions:     DEFAULTS.exception,
};

  // 사용자 설정 불러오기(GM_getValue)
  let streamer       = GM_getValue('streamer', DEFAULTS.streamer);
  let exception      = GM_getValue('exception', DEFAULTS.exception);
  const ENABLE_FIX_UNREADABLE_COLOR = GM_getValue('fixUnreadable', DEFAULTS.fixUnreadable);
  const ENABLE_REMOVE_BG_COLOR      = GM_getValue('removeHighlight', DEFAULTS.removeHighlight);
  const ENABLE_TRUNCATE_NICKNAME    = GM_getValue('truncateName', DEFAULTS.truncateName);
  const ENABLE_DROPS_TOGGLE         = GM_getValue('dropsToggle',     DEFAULTS.dropsToggle);
  const ENABLE_MISSION_HOVER       = GM_getValue('missionHover', DEFAULTS.missionHover);

  let chatObserver = null;
  let pendingNodes = [];
  let processScheduled = false;
  let isChatOpen = true; // 초기 상태: 열림
  let refreshButton = null; // 채팅 리프레쉬 버튼

  function scheduleProcess() {
      if (processScheduled) return;
      processScheduled = true;
      window.requestAnimationFrame(() => {
          pendingNodes.forEach(processChatMessage);
          pendingNodes = [];
          processScheduled = false;
          });
  }

  const LIGHT_GREEN = "rgb(102, 200, 102)";
  const Background_SKYBLUE = 'rgba(173, 216, 230, 0.15)';
  const colorCache = new Map(); // key: CSS color string, value: 가시성(true=보임, false=지우기)

      GM_addStyle(`
/* 오버레이 */
#cp-settings-overlay {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0, 0, 0, 0.3);
  display: flex; align-items: center; justify-content: center;
  z-index: 9999;
  overflow: auto;
  pointer-events: none;
}

/* 패널: 연회색 배경 */
#cp-settings-panel {
  background: #b0b0b0;
  color: #111;
  padding: 1rem;
  border-radius: 8px;
  width: 480px;
  max-width: 90%;
  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
  font-family: sans-serif;
  pointer-events: auto;
}
#cp-settings-panel h3 {
  margin-top: 0;
  color: #111;
}

/* 입력창 */
#cp-settings-panel textarea {
  width: 100%;
  height: 80px;
  margin-bottom: 0.75rem;
  background: #fff;
  color: #111;
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 0.5rem;
  resize: vertical;
}

/* 버튼 컨테이너: flex layout */
#cp-settings-panel > div {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
}

/* 버튼 공통 */
#cp-settings-panel button {
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
  font-size: 0.9rem;
  cursor: pointer;
}

/* 저장 버튼 */
#cp-settings-panel button#cp-save-btn,
#cp-settings-panel button#cp-exc-save-btn {
  background: #007bff;
  color: #fff;
}

/* 취소 버튼 */
#cp-settings-panel button#cp-cancel-btn,
#cp-settings-panel button#cp-exc-cancel-btn {
  background: #ddd;
  color: #111;
  /* margin-left: auto; */
}

/* 버튼 호버 시 약간 어두워지기 */
#cp-settings-panel button:hover {
  opacity: 0.9;
}

/* Highlight 클래스 */
.cp-highlight {
  color: rgb(102, 200, 102) !important;
  font-weight: bold !important;
  text-transform: uppercase !important;
}

/* 설정 체크박스 레이아웃 */
.cp-setting-row {
  //display: flex;
  gap: 0.5rem;
  margin: 0.5rem 0;
  font-size: 0.8rem;
}
.cp-setting-label {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 0.2rem;
}

/* 백그라운드 색설정 */
.cp-bg {
  background-color: rgba(173, 216, 230, 0.15) !important;
}

/* 채팅 리프레쉬 버튼 스타일 */
@keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}
`);

  function showCombinedPanel() {
  if (document.getElementById('cp-settings-overlay')) return;
  // overlay & panel 기본 구조 재사용
  const overlay = document.createElement('div'); overlay.id = 'cp-settings-overlay';
  const panel   = document.createElement('div'); panel.id = 'cp-settings-panel';
  // 현재 저장된 값 불러오기
  const curStreamers = GM_getValue('streamer', DEFAULTS.streamer).join(', ');
  const curExceptions= GM_getValue('exception', DEFAULTS.exception).join(', ');
  panel.innerHTML = `
    <h3>강조/제외 닉네임 설정</h3>
    <label>연두색으로 강조할 닉네임 (콤마로 구분 //파트너 기본 지원):</label>
    <textarea id="cp-streamer-input">${curStreamers}</textarea>
    <label>배경색 강조 제외할 닉네임 (콤마로 구분 //매니저 봇등):</label>
    <textarea id="cp-exception-input">${curExceptions}</textarea>

    <label><h4>유틸 기능 (온/오프)------------------------------------------------------</h4></label>
    <div class="cp-setting-row">
        <label class="cp-setting-label">
              <input type="checkbox" id="cp-fix-unread" ${ENABLE_FIX_UNREADABLE_COLOR ? 'checked' : ''}> 투명 닉네임 제거</label>
        <label class="cp-setting-label">
              <input type="checkbox" id="cp-remove-hl" ${ENABLE_REMOVE_BG_COLOR ? 'checked' : ''}> 형광펜 제거)</label>
        <label class="cp-setting-label">
              <input type="checkbox" id="cp-truncate" ${ENABLE_TRUNCATE_NICKNAME ? 'checked' : ''}> 길이 제한 (최대:10자)</label>
    </div>

    <div class="cp-setting-row">
        <label class="cp-setting-label">
              <input type="checkbox" id="cp-drops-toggle" ${ENABLE_DROPS_TOGGLE ? 'checked' : ''}> 드롭스 토글 기능</label>
        <label class="cp-setting-label">
              <input type="checkbox" id="cp-mission-hover" ${ENABLE_MISSION_HOVER ? 'checked' : ''}> 고정 댓글, 미션 자동 펼치고 접기 <br>(처음 펼침, 마우스 지나가면 접힘)</label>
    </div>
          <label><h4>-----------------------------------------------------------------------------</h4></label>
          <label><h5>추가기능: 키보드 " ] " 버튼을 눌러 채팅창을 접고 펼칠 수 있습니다.</h4></label>
          <label><h5>추가기능: 채팅 입력창 옆에 새로고침 버튼으로 채팅창만 새로고침 가능합니다.</h4></label>
    <div>
      <button id="cp-save-btn">저장</button>
      <button id="cp-cancel-btn">취소</button>
    </div>
    <div style="font-size:0.75rem; text-align:right; margin-top:0.5rem;">
       Enter ↵: 저장 Esc : 취소 (저장시 새로고침 및 적용)
    </div>
  `;
      overlay.appendChild(panel);
      document.body.appendChild(overlay);
      panel.setAttribute('tabindex', '0');
      panel.focus();
      panel.addEventListener('keydown', e => {
          if (e.key === 'Enter') {
              e.preventDefault();
              panel.querySelector('#cp-save-btn').click();
          } else if (e.key === 'Escape') {
              e.preventDefault();
              panel.querySelector('#cp-cancel-btn').click();
          }
      });


  panel.querySelector('#cp-save-btn').addEventListener('click', () => {
      const s = panel.querySelector('#cp-streamer-input').value;
      const e = panel.querySelector('#cp-exception-input').value;
      const fixUnread      = panel.querySelector('#cp-fix-unread').checked;
      const removeHl       = panel.querySelector('#cp-remove-hl').checked;
      const truncateName   = panel.querySelector('#cp-truncate').checked;
      const dropsToggleVal = panel.querySelector('#cp-drops-toggle').checked;
      GM_setValue('streamer',
          Array.from(new Set(s.split(',').map(x=>x.trim()).filter(x=>x)))
      );
      GM_setValue('exception',
          Array.from(new Set(e.split(',').map(x=>x.trim()).filter(x=>x)))
      );
      GM_setValue('fixUnreadable',    fixUnread);
      GM_setValue('removeHighlight',  removeHl);
      GM_setValue('truncateName',     truncateName);
      GM_setValue('dropsToggle', dropsToggleVal);
      GM_setValue('missionHover', document.querySelector('#cp-mission-hover').checked);
      document.body.removeChild(overlay);
      location.reload();
  });
  panel.querySelector('#cp-cancel-btn').addEventListener('click', () => {
      document.body.removeChild(overlay);
  });
}

  // 유틸: 닉네임 색상이 너무 어두운 경우 스타일 제거
  function fixUnreadableNicknameColor(nicknameElem) {
      if (!nicknameElem) return;
      // 하이라이트 색상은 검사 제외
      const cssColor = window.getComputedStyle(nicknameElem).color;
      if (cssColor === LIGHT_GREEN) return;
      // 캐시 검사 (이미 검사한 값 제외)미
      if (colorCache.has(cssColor)) {
          if (colorCache.get(cssColor) === false) {
              nicknameElem.style.color = '';
          }
          return;
      }
      // 밝기 계산 로직
      const rgbaMatch = cssColor.match(/rgba?\((\d+), ?(\d+), ?(\d+)(?:, ?([0-9.]+))?\)/);
      if (!rgbaMatch) return;
      const r = parseInt(rgbaMatch[1], 10);
      const g = parseInt(rgbaMatch[2], 10);
      const b = parseInt(rgbaMatch[3], 10);
      const a = rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1;
      const brightness = (r * 299 + g * 587 + b * 114) / 1000;
      const visibility = brightness * a;
      if (visibility < 50) nicknameElem.style.color = '';
      colorCache.set(cssColor, visibility >= 50);
  }

  // 유틸: 닉네임 배경 제거
  function removeBackgroundColor(nicknameElem) {
      if (!nicknameElem) return;
      const bgTarget = nicknameElem.querySelector('[style*="background-color"]');
      if (bgTarget) bgTarget.style.removeProperty('background-color');
  }

  // 유틸: 닉네임 자르기
  function truncateNickname(nicknameElem, maxLen = 10) {
      if (!nicknameElem) return;
      const textSpan = nicknameElem.querySelector('.name_text__yQG50');
      if (!textSpan) return;
      const fullText = textSpan.textContent;
      if (fullText.length >= 13) {
          textSpan.textContent = fullText.slice(0, maxLen) + '...';
      }
  }

  // 채팅 메시지 처리
  function processChatMessage(messageElem) {
      if (messageElem.getAttribute('data-partner-processed') === 'true') return;
      const isPartner = !!messageElem.querySelector('[class*="name_icon__zdbVH"]');
      const badgeImg = messageElem.querySelector('.badge_container__a64XB img[src*="manager.png"], .badge_container__a64XB img[src*="streamer.png"]');
      const isManager = badgeImg?.src.includes('manager.png');
      const isStreamer = badgeImg?.src.includes('streamer.png');
      const nicknameElem = messageElem.querySelector('.live_chatting_username_nickname__dDbbj');
      const textElem = messageElem.querySelector('.live_chatting_message_text__DyleH');

      if (ENABLE_FIX_UNREADABLE_COLOR) fixUnreadableNicknameColor(nicknameElem);
      if (ENABLE_REMOVE_BG_COLOR)    removeBackgroundColor(nicknameElem);
      if (ENABLE_TRUNCATE_NICKNAME)  truncateNickname(nicknameElem);

      const nameText = nicknameElem?.querySelector('.name_text__yQG50')?.textContent.trim() || '';
      const isManualStreamer = streamer.includes(nameText);

      // 연두색 스타일
      if ((!isManager && !isStreamer) && (isPartner || isManualStreamer)) {
          nicknameElem && nicknameElem.classList.add('cp-highlight');
          textElem     && textElem.classList.add('cp-highlight');
          }
      // 배경 강조
      if ((isPartner || isStreamer || isManager || isManualStreamer) && !exception.includes(nameText)) {
          messageElem.classList.add('cp-bg');
          }
      messageElem.setAttribute('data-partner-processed', 'true');
  }

  // 채팅 옵저버 설정
  function setupChatObserver() {
      if (chatObserver) chatObserver.disconnect();
      const chatContainer = document.querySelector('[class*="live_chatting_list_wrapper__"], [class*="vod_chatting_list__"]');
      if (!chatContainer) return setTimeout(setupChatObserver, 500);
      chatContainer.querySelectorAll('[class^="live_chatting_message_chatting_message__"]').forEach(processChatMessage);

      chatObserver = new MutationObserver(mutations => {
          mutations.forEach(mutation => {
              mutation.addedNodes.forEach(node => {
                  if (node.nodeType !== 1) return;
                  if (node.className.includes('live_chatting_message_chatting_message__')) {
                      pendingNodes.push(node);
                  } else {
                      node.querySelectorAll('[class^="live_chatting_message_chatting_message__"]')
                             .forEach(n => pendingNodes.push(n));
                      }
                  });
              });
          scheduleProcess();
          });
      chatObserver.observe(chatContainer, { childList: true, subtree: false });
  }

    // 미션창 + 고정 채팅 자동 접고 펼치기 (영역 클릭하여 접고 펼치기 유지)
function setupMissionHover(retry = 0) {
  // 1) 미션창 wrapper
  const fixedWrapper = document.querySelector('.live_chatting_list_fixed__Wy3TT');
  if (!fixedWrapper) {
    if (retry < 10) {
      return setTimeout(() => setupMissionHover(retry + 1), 500);
    }
    return;
  }

  // 2) 토글 버튼을 찾아주는 유틸
  const getButtons = () => {
    const missionBtn = fixedWrapper.querySelector('.live_chatting_fixed_mission_folded_button__bBWS2');
    const chatContainer = document.querySelector('.live_chatting_fixed_container__2tQz6');
    const chatBtn       = chatContainer
      ?.querySelector('.live_chatting_fixed_control__FCHpN button:not([aria-haspopup])');
    return { missionBtn, chatContainer, chatBtn };
  };

  // 3) 모두 펼치기
  const openAll = () => {
    const { missionBtn, chatBtn } = getButtons();
    if (missionBtn && missionBtn.getAttribute('aria-expanded') === 'false') {
      missionBtn.click();
    }
    if (chatBtn && chatBtn.getAttribute('aria-expanded') === 'false') {
      chatBtn.click();
    }
  };

  // 4) 모두 접기
  const closeAll = () => {
    const { missionBtn, chatBtn } = getButtons();
    if (missionBtn && missionBtn.getAttribute('aria-expanded') === 'true') {
      missionBtn.click();
    }
    if (chatBtn && chatBtn.getAttribute('aria-expanded') === 'true') {
      chatBtn.click();
    }
  };

  // 5) 초기에는 무조건 펼친 상태로
  openAll();

  // 6) 한 번만 바인딩
  if (fixedWrapper._missionHoverBound) return;
  fixedWrapper._missionHoverBound = true;

  // --- 클릭 플래그 초기화 (짝수: 닫힌 상태, 홀수: 열린 상태) ---
  const clickState = {
    chat: 0,      // 채팅 영역 클릭 횟수
    mission: 0    // 미션 영역 클릭 횟수
  };

  // 7) 클릭 영역을 확대: fixedWrapper 내부 클릭 시 '미션 영역'으로, chatContainer 내부 클릭 시 '채팅 영역'으로 인식
  fixedWrapper.addEventListener('click', (e) => {
    if (!e.isTrusted) return; // 프로그램적 클릭 제외
    const { chatContainer } = getButtons();

    // (2-2~2-4 처리 위한 플래그 토글)
    if (chatContainer && chatContainer.contains(e.target)) {
      // 채팅 영역 내부 클릭
      clickState.chat += 1;
    } else {
      // fixedWrapper 내부이지만 chatContainer 외부 => 미션 영역 클릭
      clickState.mission += 1;
    }
  });

  // 8) 마우스 들어오면 모두 펼치기
  fixedWrapper.addEventListener('pointerenter', () => {
    openAll();
  });

  // 9) 마우스 나가면 클릭 플래그에 따라 상태 유지 또는 접기
  fixedWrapper.addEventListener('pointerleave', () => {
    const { missionBtn, chatContainer, chatBtn } = getButtons();
    const chatClickedOdd    = (clickState.chat % 2) === 1;
    const missionClickedOdd = (clickState.mission % 2) === 1;

    // 2-1. 클릭 없이 단순히 지나간 경우 (둘 다 닫기)
    if (!chatClickedOdd && !missionClickedOdd) {
      closeAll();
      return;
    }

    // 2-2. 채팅만 홀수번 클릭한 경우: 채팅 열리고, 미션 닫힘
    if (chatClickedOdd && !missionClickedOdd) {
      if (chatBtn && chatBtn.getAttribute('aria-expanded') === 'false') {
        chatBtn.click();
      }
      if (missionBtn && missionBtn.getAttribute('aria-expanded') === 'true') {
        missionBtn.click();
      }
      return;
    }

    // 2-3. 미션만 홀수번 클릭한 경우: 미션 열리고, 채팅 닫힘
    if (!chatClickedOdd && missionClickedOdd) {
      if (missionBtn && missionBtn.getAttribute('aria-expanded') === 'false') {
        missionBtn.click();
      }
      if (chatBtn && chatBtn.getAttribute('aria-expanded') === 'true') {
        chatBtn.click();
      }
      return;
    }

    // 2-4. 양쪽 모두 홀수번 클릭한 경우: 둘 다 열기
    if (chatClickedOdd && missionClickedOdd) {
      if (missionBtn && missionBtn.getAttribute('aria-expanded') === 'false') {
        missionBtn.click();
      }
      if (chatBtn && chatBtn.getAttribute('aria-expanded') === 'false') {
        chatBtn.click();
      }
    }
  });
}

      // ▽ 드롭스 토글용 CSS
  GM_addStyle(`
    #drops_info.drops-collapsed .live_information_drops_wrapper__gQBUq,
    #drops_info.drops-collapsed .live_information_drops_text__xRtWS,
    #drops_info.drops-collapsed .live_information_drops_default__jwWot,
    #drops_info.drops-collapsed .live_information_drops_area__7VJJr {
      display: none !important;
    }
    .live_information_drops_icon_drops__2YXie {
      transition: transform .2s;
    }
    #drops_info.drops-collapsed .live_information_drops_icon_drops__2YXie {
      transform: rotate(-90deg);
    }
    .live_information_drops_toggle_icon {
      margin-left: 10px;
      font-size: 18px;
      cursor: pointer;
      display: inline-block;
    }
  `);

  // === 키입력 ] 을 통해 채팅 접고 펼치기 ===
  function closeChat() {
      const btn = document.querySelector('.live_chatting_header_button__t2pa1');
      if (btn) {
          btn.click();
      } else {
          console.warn('채팅 접기 버튼을 찾을 수 없습니다.');
      }
  }

  function openChat() {
      const btn = document
          .querySelector('svg[viewBox="0 0 38 34"]')
          ?.closest('button');
      if (btn) {
          btn.click();
      } else {
          console.warn('기본 채팅 토글 버튼을 찾을 수 없습니다.');
      }
  }

  function onKeydown(e) {
      const tag = e.target.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || e.target.isContentEditable) return;
      if (e.key === ']') {
          if (isChatOpen) {
              closeChat();
              isChatOpen = false;
          } else {
              openChat();
              isChatOpen = true;
          }
      }
  }
      window.addEventListener('keydown', onKeydown);

function initDropsToggle() {
  const container = document.getElementById('drops_info');
  if (!container || container.classList.contains('drops-init')) return;

  const header = container.querySelector('.live_information_drops_header__920BX');
  if (!header) return;

  // 마크 표시 및 초기 숨김 상태
  const toggleIcon = document.createElement('span');
  toggleIcon.classList.add('live_information_drops_toggle_icon');
  toggleIcon.textContent = '▼';
  header.appendChild(toggleIcon);
  header.style.cursor = 'pointer';
  container.classList.add('drops-collapsed');
  container.classList.add('drops-init');

  header.addEventListener('click', () => {
    const collapsed = container.classList.toggle('drops-collapsed');
    toggleIcon.textContent = collapsed ? '▼' : '▲';
  });
}

  function setupDropsToggleObserver() {
  initDropsToggle();
  const obs = new MutationObserver(() => {
    initDropsToggle();
  });
  obs.observe(document.body, { childList: true, subtree: true });
}

// === 채팅 리프레쉬 기능 ===

// 채팅 리프레쉬 버튼 생성
function createRefreshButton() {
    const button = document.createElement('button');
    button.className = 'button_container__ppWwB button_only_icon__kahz5 button_not_disabled_style__+f4-T';
    button.type = 'button';
    button.title = '채팅 새로고침';
    button.style.cssText = `
        width: 28px;
        height: 28px;
        margin-right: 8px;
        background: transparent;
        border: none;
        cursor: pointer;
        border-radius: 4px;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: background-color 0.2s;
    `;

    // 새로고침 아이콘 SVG
    button.innerHTML = `
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C9.25022 4 6.82447 5.38734 5.38451 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
            <path d="M8 7.5L5.38451 7.5L5.38451 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>
    `;

    // 호버 효과
    button.addEventListener('mouseenter', () => {
        if (!button.disabled) {
            button.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
        }
    });

    button.addEventListener('mouseleave', () => {
        button.style.backgroundColor = 'transparent';
    });

    // 클릭 이벤트
    button.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        refreshChatAndReinit();
    });

    return button;
}

// 버튼 비활성화 (로딩 상태)
function disableRefreshButton() {
    if (refreshButton) {
        refreshButton.disabled = true;
        refreshButton.style.opacity = '0.5';
        refreshButton.style.pointerEvents = 'none';

        const svg = refreshButton.querySelector('svg');
        if (svg) {
            svg.style.animation = 'spin 1s linear infinite';
        }
    }
}

// 버튼 활성화 (정상 상태)
function enableRefreshButton() {
    if (refreshButton) {
        refreshButton.disabled = false;
        refreshButton.style.opacity = '1';
        refreshButton.style.pointerEvents = 'auto';

        const svg = refreshButton.querySelector('svg');
        if (svg) {
            svg.style.animation = '';
        }
    }
}

// 채팅 리프레쉬 및 재초기화 기능
function refreshChatAndReinit() {
    console.log('채팅 새로고침 및 재초기화 시작');
    disableRefreshButton();

    if (clickMoreMenuButton()) {
        clickChatPopupButton();
    } else {
        console.log('더보기 메뉴 버튼을 찾을 수 없음');
        // 실패 시 2초 후 버튼 복원
        setTimeout(enableRefreshButton, 2000);
    }
}

// UI 버튼을 채팅 입력창에 추가
function addRefreshButtonToUI() {
    const chatInputContainer = document.querySelector('.live_chatting_input_tools__OPA1R');
    const sendButton = document.querySelector('#send_chat_or_donate');

    if (chatInputContainer && sendButton && !refreshButton) {
        refreshButton = createRefreshButton();
        chatInputContainer.insertBefore(refreshButton, sendButton);
        console.log('새로고침 버튼이 UI에 추가됨');
    }
}

// 더보기 메뉴 버튼 클릭
function clickMoreMenuButton() {
    const moreButton = document.querySelector('button.live_chatting_header_button__t2pa1[aria-label="더보기 메뉴"]');
    if (moreButton) {
        moreButton.click();
        console.log('더보기 메뉴 클릭됨');
        return true;
    }
    return false;
}

// 채팅창 팝업 버튼 클릭
function clickChatPopupButton() {
    let attempts = 0;
    const maxAttempts = 20;

    const findPopupButton = setInterval(() => {
        attempts++;
        console.log(`채팅창 팝업 버튼 찾는 중... (${attempts}/${maxAttempts})`);

        const popupButtons = document.querySelectorAll('button.layer_button__fFPB8');

        for (let popupButton of popupButtons) {
            const spans = popupButton.querySelectorAll('span');
            for (let span of spans) {
                if (span.textContent.includes('채팅창 팝업')) {
                    console.log('채팅창 팝업 버튼 찾음, 클릭 시도');
                    popupButton.click();
                    console.log('채팅창 팝업 버튼 클릭됨');
                    clearInterval(findPopupButton);

                    setTimeout(() => {
                        findAndClickChatViewButton();
                    }, 300);
                    return;
                }
            }
        }

        if (attempts >= maxAttempts) {
            console.log('채팅창 팝업 버튼을 찾을 수 없음');
            clearInterval(findPopupButton);
            setTimeout(enableRefreshButton, 2000);
        }
    }, 100);
}

// 채팅보기 버튼 찾아서 클릭 및 재초기화
function findAndClickChatViewButton() {
    console.log('채팅보기 버튼 찾기 시작');

    let attempts = 0;
    const maxAttempts = 50;

    const findChatViewButton = setInterval(() => {
        attempts++;
        console.log(`채팅보기 버튼 찾는 중... (${attempts}/${maxAttempts})`);

        const chatViewButtons = document.querySelectorAll('button.no_content_button__fFsAz');

        for (let button of chatViewButtons) {
            if (button.textContent.includes('채팅보기')) {
                console.log('메인 창에서 채팅보기 버튼 발견 - 즉시 클릭');
                button.click();
                console.log('채팅보기 버튼 클릭됨 (채팅 새로고침 완료)');
                clearInterval(findChatViewButton);

                // 채팅 새로고침 후 모든 기능 재초기화
                setTimeout(() => {
                    console.log('채팅 리프레쉬 후 재초기화 시작');

                    // 설정값 새로 불러오기
                    streamer = GM_getValue('streamer', DEFAULTS.streamer);
                    exception = GM_getValue('exception', DEFAULTS.exception);

                    // 모든 기능 재설정
                    setupChatObserver();
                    if (ENABLE_MISSION_HOVER) setupMissionHover();
                    if (ENABLE_DROPS_TOGGLE) setupDropsToggleObserver();
                    initKnifeTracker(KNIFE_CONFIG);

                    // 리프레쉬 버튼 재추가 (기존 버튼이 사라질 수 있음)
                    refreshButton = null;
                    setTimeout(addRefreshButtonToUI, 200);

                    console.log('채팅 리프레쉬 후 재초기화 완료');
                    enableRefreshButton();
                }, 1000);
                return;
            }
        }

        if (attempts >= maxAttempts) {
            console.log('채팅보기 버튼을 찾을 수 없음');
            clearInterval(findChatViewButton);
            setTimeout(enableRefreshButton, 2000);
        }
    }, 50);
}

// DOM 변화 감지하여 리프레쉬 버튼 추가 (SPA 대응)
function observeForRefreshButton() {
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.type === 'childList') {
                // 채팅 입력창이 나타나면 버튼 추가 중복으로 버튼이 생겨서 제외처리함
                if (document.querySelector('.live_chatting_input_tools__OPA1R') && !refreshButton) {
                    //setTimeout(addRefreshButtonToUI, 100);
                }

                // 버튼이 사라진 경우 재생성
                if (refreshButton && !document.body.contains(refreshButton)) {
                    console.log('리프레쉬 버튼이 사라져서 재생성');
                    refreshButton = null;
                    setTimeout(addRefreshButtonToUI, 100);
                }
            }
        });
    });

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

  function setupSPADetection() {
  let lastUrl = location.href;
  const onUrlChange = () => {
    if (location.href !== lastUrl) {
      lastUrl = location.href;
      setTimeout(() => {
        setupChatObserver();
        if (ENABLE_MISSION_HOVER) setupMissionHover();
        if (ENABLE_DROPS_TOGGLE) setupDropsToggleObserver();
        initKnifeTracker(KNIFE_CONFIG);

        // SPA 페이지 변경 시 리프레쉬 버튼도 재추가
        refreshButton = null;
        setTimeout(addRefreshButtonToUI, 300);
      }, 500);
    }
  };
  ['pushState', 'replaceState'].forEach(method => {
    const orig = history[method];
    history[method] = function(...args) {
      orig.apply(this, args);
      onUrlChange();
    };
  });
  window.addEventListener('popstate', onUrlChange);
}

// ==== chzzk_knife_tracker 함수화 버전 ==== //
/**
* 채팅창 상단에 주요 유저 메시지만 모아 보여주는 기능을 초기화합니다.
* @param {Object} options
* @param {string}   options.chatContainerSelector  – 채팅 컨테이너 셀렉터
* @param {string}   options.chatListSelector       – 채팅 리스트 셀렉터
* @param {number}   options.maxMessages            – 보관할 최대 메시지 개수
* @param {string[]} options.defaultStreamers       – 기본 스트리머 닉네임 목록
* @param {string[]} options.defaultExceptions      – 기본 예외 닉네임 목록
*/

// ==== chzzk_knife_tracker 함수화 버전 (수정됨) ==== //
/**
* 채팅창 상단에 주요 유저 메시지만 모아 보여주는 기능을 초기화합니다.
* @param {Object} options
* @param {string}   options.chatContainerSelector  – 채팅 컨테이너 셀렉터
* @param {string}   options.chatListSelector       – 채팅 리스트 셀렉터
* @param {number}   options.maxMessages            – 보관할 최대 메시지 개수
* @param {string[]} options.defaultStreamers       – 기본 스트리머 닉네임 목록
* @param {string[]} options.defaultExceptions      – 기본 예외 닉네임 목록
*/
function initKnifeTracker({
chatContainerSelector,
chatListSelector,
maxMessages = 100,
defaultStreamers = [],
defaultExceptions = [],
}) {
const styleId = 'knifeTracker';
const filteredMessages = [];
let knifeObserver  = null;

// — GM 저장소에서 실제 설정 불러오기 —
const manualStreamers = GM_getValue('streamer', defaultStreamers);
const exceptions     = GM_getValue('exception', defaultExceptions);

// 1) 스타일 주입 (수정됨: column-reverse 제거)
const css = `
  #filtered-chat-box {
    display: flex;
    flex-direction: column;
    height: 70px;
    overflow-y: auto;
    padding: 8px 8px 0 8px;
    margin: 0;
    border-bottom: 2px solid #444;
    border-radius: 0 0 6px 6px;
    background-color: rgba(30, 30, 30, 0.8);
    scrollbar-width: none;
    resize: vertical;
    min-height: 38px;
    max-height: 350px;
    position: relative;
  }
  .live_chatting_list_wrapper__a5XTV,
  .live_chatting_list_container__vwsbZ {
    margin-top: 0 !important;
    padding-top: 0 !important;
  }
  .live_chatting_list_fixed__Wy3TT {
    top: 0 !important;
  }
`;

function injectStyles() {
  if (document.head.querySelector(`#${styleId}`)) return;
  const s = document.createElement('style');
  s.id = styleId;
  s.textContent = css;
  document.head.appendChild(s);
}

function shouldTrackUser(node) {
  // 1) 닉네임 텍스트
  const nicknameElem = node.querySelector('.live_chatting_username_nickname__dDbbj');
  const nameText = nicknameElem
    ?.querySelector('.name_text__yQG50')
    ?.textContent.trim() || '';

  // 2) 파트너 아이콘 (원본과 동일한 클래스)
  const isPartner = !!node.querySelector('[class*="name_icon__zdbVH"]');

  // 3) 매니저/스트리머 뱃지 (원본과 동일한 selector)
  const badgeImg = node.querySelector(
    '.badge_container__a64XB img[src*="manager.png"], ' +
    '.badge_container__a64XB img[src*="streamer.png"]'
  );
  const isManager = badgeImg?.src.includes('manager.png');
  const isStreamer = badgeImg?.src.includes('streamer.png');

  // 4) 수동 지정 스트리머
  const isManualStreamer = manualStreamers.includes(nameText);

  // 5) 예외 닉네임
  const isException = exceptions.includes(nameText);

  // — 원본 CP-bg 조건과 동일하게 —
  return !isException && (
    isPartner ||
    isStreamer ||
    isManager ||
    isManualStreamer
  );
}

// 3) 박스 만들기 (수정됨)
function createFilteredBox() {
  const container = document.querySelector(chatContainerSelector);
  if (!container || document.getElementById('filtered-chat-box')) return;

  const box = document.createElement('div');
  box.id = 'filtered-chat-box';
  container.parentElement.insertBefore(box, container);
  injectStyles();

  // 기존 메시지들을 시간순으로 표시 (최신 것부터 위에)
  filteredMessages.forEach(m => {
    const clone = m.cloneNode(true);
    resizeVerificationMark(clone);
    box.appendChild(clone);
  });

  // 자동 스크롤을 맨 위로 (최신 메시지가 보이도록)
  box.scrollTop = 0;
}

// 4) 새 메시지 감시 (수정됨)
const collectedMessages = new Set();
let lastKnownMessageCount = 0;

function observeNewMessages() {
  const list = document.querySelector(chatListSelector);
  if (!list) return;

  // 초기 메시지 개수 저장
  lastKnownMessageCount = list.children.length;

  if (knifeObserver) knifeObserver.disconnect();
  knifeObserver = new MutationObserver(mutations => {
    mutations.forEach(m => {
      for (const node of m.addedNodes) {
        if (!(node instanceof HTMLElement)) continue;

        if (!node.matches('.live_chatting_list_item__0SGhw')) continue;

        const nickname = node.querySelector('.name_text__yQG50')?.textContent?.trim() || '';
        const message = node.querySelector('.live_chatting_message_chatting_message__7TKns')?.textContent?.trim() || '';
        const key = `${nickname}:${message}`;

        if (collectedMessages.has(key)) continue;
        collectedMessages.add(key);

        if (node._knifeProcessed) continue;
        node._knifeProcessed = true;

        if (!node.querySelector('[class^="live_chatting_message_container__"]')) continue;
        if (!shouldTrackUser(node)) continue;

        const box = document.getElementById('filtered-chat-box');
        if (!box) return;

        const clone = node.cloneNode(true);
        replaceBlockWithInline(clone);
        resizeVerificationMark(clone);

        // 새로운 메시지가 DOM의 어느 위치에 추가되었는지 확인
        const chatList = document.querySelector(chatListSelector);
        const currentMessageCount = chatList.children.length;
        const nodeIndex = Array.from(chatList.children).indexOf(node);

        // 스크롤로 인해 맨 위에 추가된 과거 메시지인지 판단
        // (전체 메시지 수가 증가했지만 새 메시지가 맨 위쪽에 위치한 경우)
        const isScrollLoadedMessage = nodeIndex < Math.min(10, lastKnownMessageCount);

        if (isScrollLoadedMessage) {
          // 스크롤로 로드된 과거 메시지는 맨 아래에 추가
          box.appendChild(clone);
          filteredMessages.push(clone);

          // 개수 제한 (맨 위에서 제거)
          if (filteredMessages.length > maxMessages) {
            const removed = filteredMessages.shift();
            const firstChild = box.firstChild;
            if (firstChild) box.removeChild(firstChild);
          }
        } else {
          // 실시간 메시지는 맨 위에 추가
          box.insertBefore(clone, box.firstChild);
          filteredMessages.unshift(clone);

          // 개수 제한 (맨 아래에서 제거)
          if (filteredMessages.length > maxMessages) {
            const removed = filteredMessages.pop();
            const lastChild = box.lastChild;
            if (lastChild) box.removeChild(lastChild);
          }

          // 새로 추가된 메시지로 포커스/스크롤 이동 (최신 메시지가 보이도록)
          try {
            // 방법 1: 새로 추가된 요소로 스크롤
            clone.scrollIntoView({ behavior: 'instant', block: 'start' });

            // 방법 2: 강제 스크롤 (즉시 실행)
            box.scrollTop = 0;
            box.scrollTo(0, 0);

            // 방법 3: DOM 업데이트 후 스크롤
            setTimeout(() => {
              box.scrollTop = 0;
              box.scrollTo(0, 0);
            }, 0);

            // 방법 4: requestAnimationFrame을 사용한 정확한 타이밍 스크롤
            requestAnimationFrame(() => {
              box.scrollTop = 0;
              box.scrollTo(0, 0);
            });

            // 방법 5: 임시 포커스와 스크롤
            setTimeout(() => {
              if (clone.tabIndex === undefined) clone.tabIndex = -1;
              clone.focus();
              clone.blur();
              box.scrollTop = 0;
            }, 50);

            // 방법 6: 마지막으로 스크롤 강제 적용
            setTimeout(() => {
              box.scrollTop = 0;
              box.scrollTo(0, 0);
            }, 100);

            // 방법 7: 스크롤 이벤트 리스너 추가
            const observer = new MutationObserver(() => {
              box.scrollTop = 0;
              box.scrollTo(0, 0);
            });
            observer.observe(box, { childList: true });

            // 메시지가 완전히 추가된 후에 스크롤 이벤트 리스너 제거
            setTimeout(() => {
              observer.disconnect();
              // 마지막으로 스크롤 위치 강제 설정
              box.scrollTop = box.scrollHeight;
              box.scrollTo(0, box.scrollHeight);
            }, 200);

          } catch (e) {
            console.error('스크롤 처리 중 오류:', e);
            console.log('스크롤 이동 실패:', e);
          }
        }

        // 메시지 개수 업데이트
        lastKnownMessageCount = currentMessageCount;

        // 스크롤 위치를 최신 메시지로 강제 설정
        setTimeout(() => {
          const box = document.getElementById('filtered-chat-box');
          if (box) {
            // 스크롤 위치를 마지막 메시지로 이동
            const lastMessage = box.lastChild;
            if (lastMessage) {
              lastMessage.scrollIntoView({ behavior: 'instant', block: 'start' });
            }
            box.scrollTop = box.scrollHeight;
            box.scrollTo(0, box.scrollHeight);
          }
        }, 0);
      }
    });
  });

  knifeObserver.observe(list, { childList: true, subtree: true });
}

// 페이지 로드 시 기존 채팅 메시지들을 처리하는 함수 (완전 수정)
function processExistingMessages() {
  const list = document.querySelector(chatListSelector);
  if (!list) return;

  const existingMessages = Array.from(list.querySelectorAll('.live_chatting_list_item__0SGhw'));

  // 기존 메시지들을 최신순으로 처리 (최신 것부터)
  existingMessages.reverse().forEach(node => {
    if (node._knifeProcessed) return;
    node._knifeProcessed = true;

    if (!node.querySelector('[class^="live_chatting_message_container__"]')) return;
    if (!shouldTrackUser(node)) return;

    const clone = node.cloneNode(true);
    replaceBlockWithInline(clone);
    resizeVerificationMark(clone);

    // 기존 메시지들은 최신순으로 배열에 추가
    filteredMessages.unshift(clone);
    if (filteredMessages.length > maxMessages) filteredMessages.pop();
  });
}

function replaceBlockWithInline(node) {
  const messageElement = node.querySelector('.live_chatting_message_chatting_message__7TKns');
  if (!messageElement || messageElement.tagName !== 'DIV') return;

  const span = document.createElement('span');
  span.className = messageElement.className;
  span.innerHTML = messageElement.innerHTML;
  span.style.paddingLeft = '0px';
  messageElement.replaceWith(span);
}

// 복사된 메시지에서 .blind(인증마크) 폰트 크기 등 조정
function resizeVerificationMark(node) {
  // 인증마크 (.blind) 뿐만 아니라 다른 아이콘들도 처리
  const verified = node.querySelector('.live_chatting_username_nickname__dDbbj .blind');
  if (verified) {
    // 폰트 크기 줄이고 위치·투명도 조절
    verified.style.fontSize       = '10px';
    verified.style.lineHeight     = '1';
    verified.style.verticalAlign  = 'middle';
    verified.style.marginLeft     = '4px';
    verified.style.opacity        = '0.8';
  }

  // name_icon__zdbVH 클래스를 가진 모든 아이콘들 크기 조정
  const nameIcons = node.querySelectorAll('[class*="name_icon__zdbVH"]');
  nameIcons.forEach(icon => {
    icon.style.width = '14px';
    icon.style.height = '14px';
    icon.style.marginTop = '1px';
    // 배경 이미지가 있는 경우 backgroundSize도 조정
    if (icon.style.backgroundImage) {
      icon.style.backgroundSize = '14px 14px';
    }
  });

  // 뱃지 이미지들도 크기 조정
  const badgeImages = node.querySelectorAll('.badge_container__a64XB img');
  badgeImages.forEach(img => {
    img.style.width = '14px';
    img.style.height = '14px';
    img.style.marginRight = '2px';
  });
}

// 5) 채팅 준비 완료 후 초기화
function waitForChatThenInit() {
  const obs = new MutationObserver((_, o) => {
    const c = document.querySelector(chatContainerSelector);
    const l = document.querySelector(chatListSelector);
    if (c && l) {
      o.disconnect();
      injectStyles();
      processExistingMessages(); // 기존 메시지 처리 먼저 실행
      createFilteredBox();
      observeNewMessages();
    }
  });
  obs.observe(document.body, { childList: true, subtree: true });
}

waitForChatThenInit();
}

  // 설정 메뉴 추가
  GM_registerMenuCommand("⚙️ Chzzk: Chatting Plus 설정 변경", showCombinedPanel);

  // 초기화
  function init() {
      setupChatObserver();
      setupSPADetection();
      initKnifeTracker(KNIFE_CONFIG);
      if (ENABLE_MISSION_HOVER) setupMissionHover();
      if (ENABLE_DROPS_TOGGLE) setupDropsToggleObserver();

      // 리프레쉬 버튼 관련 초기화
      observeForRefreshButton();
      setTimeout(addRefreshButtonToUI, 1000);
  }
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
  else init();
})();