// ==UserScript==
// @name Chzzk_L&V: Chatting Plus
// @namespace Chzzk_Live&VOD: Chatting Plus
// @version 1.9.0
// @description "파트너이며 매니저가 아닌 유저" 또는 지정한 streamer 닉네임의 메시지를 연두색으로 표시, 채팅 닉네임 꾸미기 효과 중 스텔스모드 무력화 및 형광펜 제거, 긴 닉네임 10자 초과 시 생략 처리, 타임머신 기능 안내 및 치트키 구매 팝업 클릭하여 닫기
// @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 CURRENT_VERSION = '1.9.0';
// 기본 설정(하드코딩)
const DEFAULTS = {
streamer: ['고수달','냐 미 Nyami','새 담','청 묘','침착맨','삼식123','레니아워 RenieHouR'],
exception: ['인챈트 봇','픽셀 봇','스텔라이브 봇','뚜팔봇'],
fixUnreadable: true,
removeHighlight: true,
truncateName: true,
autoCloseTooltip: true
};
// 이전 저장된 버전 정보 가져오기
const savedVersion = GM_getValue('scriptVersion', '0.0.0');
// 버전이 변경되었을 때 기본값 덮어쓰기
if (savedVersion !== CURRENT_VERSION) {
GM_setValue('streamer', DEFAULTS.streamer);
GM_setValue('exception', DEFAULTS.exception);
GM_setValue('fixUnreadable', DEFAULTS.fixUnreadable);
GM_setValue('removeHighlight', DEFAULTS.removeHighlight);
GM_setValue('truncateName', DEFAULTS.truncateName);
GM_setValue('autoCloseTooltip', DEFAULTS.autoCloseTooltip);
GM_setValue('scriptVersion', CURRENT_VERSION);
}
// 사용자 설정 불러오기(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_TOOLTIP_AUTO_CLOSE = GM_getValue('autoCloseTooltip', DEFAULTS.autoCloseTooltip);
let chatObserver = null;
let tooltipClosed = false;
const LIGHT_GREEN = "rgb(102, 200, 102)";
const Background_SKYBLUE = 'rgba(173, 216, 230, 0.15)';
GM_addStyle(`
/* 오버레이 */
#cp-settings-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex; align-items: center; justify-content: center;
z-index: 9999;
}
/* 패널: 연회색 배경 */
#cp-settings-panel {
background: #b0b0b0;
color: #111;
padding: 1rem;
border-radius: 8px;
width: 320px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
font-family: sans-serif;
}
#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;
}
/* 버튼 공통 */
#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 {
background: #007bff;
color: #fff;
margin-right: 0.5rem;
}
/* 취소 버튼(오른쪽): 연회색 배경, 검은 글씨 */
#cp-settings-panel button#cp-cancel-btn {
background: #ddd;
color: #111;
float: right;
}
/* 버튼 호버 시 약간 어두워지기 */
#cp-settings-panel button:hover {
opacity: 0.9;
}
`);
function showSettingsPanel() {
// 이미 열려 있으면 열리지 않게
if (document.getElementById('cp-settings-overlay')) return;
// 오버레이
const overlay = document.createElement('div');
overlay.id = 'cp-settings-overlay';
// 패널
const panel = document.createElement('div');
panel.id = 'cp-settings-panel';
panel.innerHTML = `
<h3>스트리머 목록 편집</h3>
<textarea id="cp-streamer-input">${GM_getValue('streamer', DEFAULTS.streamer).join(', ')}</textarea>
<div>
<button id="cp-save-btn">저장</button>
<button id="cp-cancel-btn">취소</button>
</div>
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
// 버튼 이벤트
panel.querySelector('#cp-save-btn').addEventListener('click', () => {
const val = panel.querySelector('#cp-streamer-input').value;
const arr = val.split(',').map(s => s.trim()).filter(s=>s);
GM_setValue('streamer', arr);
alert('저장되었습니다: ' + arr.join(', '));
document.body.removeChild(overlay);
location.reload();
});
panel.querySelector('#cp-cancel-btn').addEventListener('click', () => {
document.body.removeChild(overlay);
});
}
function showExceptionPanel() {
if (document.getElementById('cp-settings-overlay')) return;
const overlay = document.createElement('div');
overlay.id = 'cp-settings-overlay';
const panel = document.createElement('div');
panel.id = 'cp-settings-panel';
panel.innerHTML = `
<h3>제외 대상 닉네임 편집</h3>
<textarea id="cp-exception-input">${GM_getValue('exception', DEFAULTS.exception).join(', ')}</textarea>
<div>
<button id="cp-exc-save-btn">저장</button>
<button id="cp-exc-cancel-btn">취소</button>
</div>
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
panel.querySelector('#cp-exc-save-btn').addEventListener('click', () => {
const val = panel.querySelector('#cp-exception-input').value;
const arr = val.split(',').map(s => s.trim()).filter(s=>s);
GM_setValue('exception', arr);
document.body.removeChild(overlay);
location.reload();
});
panel.querySelector('#cp-exc-cancel-btn').addEventListener('click', () => {
document.body.removeChild(overlay);
});
}
// 유틸: 닉네임 색상이 너무 어두운 경우 스타일 제거
function fixUnreadableNicknameColor(nicknameElem) {
if (!nicknameElem) return;
const computedColor = window.getComputedStyle(nicknameElem).color;
const rgbaMatch = computedColor.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 = '';
}
// 유틸: 닉네임 배경 제거
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 > maxLen) textSpan.textContent = fullText.slice(0, maxLen) + '...';
}
// 유틸: 치트키 팝업 자동 닫기
function autoClickTooltipCloseButton() {
if (tooltipClosed) return;
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
const closeBtn = node.tagName === 'BUTTON' && node.className.includes('cheat_key_tooltip_button_close__')
? node : node.querySelector('button[class*="cheat_key_tooltip_button_close__"]');
if (closeBtn) {
closeBtn.click(); tooltipClosed = true; observer.disconnect(); return;
}
}
}
});
observer.observe(document.body, { childList: true, subtree: false });
}
// 채팅 메시지 처리
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 && Object.assign(nicknameElem.style, { color: LIGHT_GREEN, fontWeight: 'bold', textTransform: 'uppercase' });
textElem && Object.assign(textElem.style, { color: LIGHT_GREEN, fontWeight: 'bold', textTransform: 'uppercase' });
}
// 배경 강조
if ((isPartner || isStreamer || isManager || isManualStreamer) && !exception.includes(nameText)) {
messageElem.style.backgroundColor = Background_SKYBLUE;
}
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(muts => muts.forEach(m => m.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
if (node.className.includes('live_chatting_message_chatting_message__')) processChatMessage(node);
else node.querySelectorAll('[class^="live_chatting_message_chatting_message__"]').forEach(processChatMessage);
})));
chatObserver.observe(chatContainer, { childList: true, subtree: true });
}
// SPA 탐지
function setupSPADetection() {
let lastUrl = location.href;
const onUrlChange = () => {
if (location.href !== lastUrl) {
lastUrl = location.href; tooltipClosed = false;
setTimeout(() => { setupChatObserver(); autoClickTooltipCloseButton(); }, 1000);
}
};
['pushState','replaceState'].forEach(m => {
const orig = history[m];
history[m] = function(...args) { orig.apply(this,args); onUrlChange(); };
});
window.addEventListener('popstate', onUrlChange);
}
// 설정 메뉴 추가
GM_registerMenuCommand("🤖 강조할 닉네임 설정(파트너아닌경우))", showSettingsPanel);
GM_registerMenuCommand("🛡️ 배경 강조 제외 닉네임 설정(매니저 봇등)", showExceptionPanel);
GM_registerMenuCommand(`🛠️ 닉네임 은신 제거(2티어): ${ENABLE_FIX_UNREADABLE_COLOR?'✔️':'❌'}`, () => {
GM_setValue('fixUnreadable', !ENABLE_FIX_UNREADABLE_COLOR);
location.reload();
});
GM_registerMenuCommand(`🖍️ 형광펜 제거(2티어): ${ENABLE_REMOVE_BG_COLOR?'✔️':'❌'}`, () => {
GM_setValue('removeHighlight', !ENABLE_REMOVE_BG_COLOR);
location.reload();
});
GM_registerMenuCommand(`✂️ 닉네임 자르기(Max:10): ${ENABLE_TRUNCATE_NICKNAME?'✔️':'❌'}`, () => {
GM_setValue('truncateName', !ENABLE_TRUNCATE_NICKNAME);
location.reload();
});
GM_registerMenuCommand(`❎ 툴팁 자동 닫기(타임머신관련): ${ENABLE_TOOLTIP_AUTO_CLOSE?'✔️':'❌'}`, () => {
GM_setValue('autoCloseTooltip', !ENABLE_TOOLTIP_AUTO_CLOSE);
location.reload();
});
// 초기화
function init() {
setupChatObserver();
setupSPADetection();
if (ENABLE_TOOLTIP_AUTO_CLOSE) autoClickTooltipCloseButton();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();