Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save lunamoth/9fe3255283371f3c6370159b5dcbee0f to your computer and use it in GitHub Desktop.
Save lunamoth/9fe3255283371f3c6370159b5dcbee0f to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Chzzk Sort by Viewers (Following Live) v1.7 (Refactored)
// @name:ko 치지직 팔로잉 라이브 시청자 순 자동 정렬 v1.7 (리팩토링)
// @namespace http://tampermonkey.net/
// @version 1.7
// @description Automatically sorts by "Most Viewers" on Chzzk Following Live page, hiding the dropdown flicker.
// @description:ko 치지직 팔로잉 라이브 페이지의 정렬 순서를 '시청자 많은 순'으로 자동 변경하고, 드롭다운 깜빡임을 숨깁니다.
// @author Your Name (or AI Refactored)
// @match https://chzzk.naver.com/following?tab=LIVE
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const DEBUG = false; // 디버그 로그를 보려면 true로 변경
const TARGET_SORT_TEXT = "시청자 많은 순";
const DROPDOWN_BUTTON_SELECTOR = "button#live-order";
const CURRENT_SORT_TEXT_SELECTOR = '.selectbox_name__fdZrs'; // 드롭다운 버튼 내 현재 정렬 텍스트 요소
const OPTION_XPATH = `//button[contains(@class, 'selectbox_option__lsIDs') and normalize-space(.)='${TARGET_SORT_TEXT}']`;
const DROPDOWN_LIST_ID = "selectbox-listbox"; // 드롭다운 메뉴 리스트의 ID (button의 aria-controls 값과 일치)
const POLLING_INTERVAL_MS = 500; // 기본 폴링 간격
const ELEMENT_WAIT_TIMEOUT_MS = 10000; // 요소 기다리는 최대 시간 (메인 버튼용)
const OPTION_WAIT_TIMEOUT_MS = 2500; // 옵션 요소 기다리는 최대 시간
const HIDE_STYLE_ID = "chzzk-hide-dropdown-style-refactored"; // 중복 방지를 위한 ID 변경
// --- State ---
let scriptHasRunSuccessfully = false;
let styleAdded = false;
// --- Helper Functions ---
const log = (level, ...args) => {
if (level === 'error' || level === 'warn' || DEBUG) {
console[level](`[ChzzkSortScript]`, ...args);
}
};
// 드롭다운 리스트를 숨기는 스타일을 적용하는 함수
function applyHideStyle() {
if (styleAdded || document.getElementById(HIDE_STYLE_ID)) return;
GM_addStyle(`
#${DROPDOWN_LIST_ID}.selectbox_layer__Wsk6t {
display: none !important;
opacity: 0 !important;
visibility: hidden !important;
}
`);
// GM_addStyle은 ID를 직접 할당하는 표준 방법이 없으므로,
// 이 스크립트 내에서 추가되었음을 나타내는 플래그를 사용합니다.
// 또는, 직접 style 태그를 만들고 ID를 할당하여 document.head에 추가할 수 있습니다.
// 여기서는 간단하게 GM_addStyle을 사용합니다.
styleAdded = true;
log('info', '드롭다운 숨김 스타일 적용됨.');
}
// 특정 요소가 나타날 때까지 기다리는 Promise 반환 함수
function waitForElement(selectorOrXpath, timeout, isXpath = false) {
return new Promise((resolve) => {
let elapsedTime = 0;
const interval = POLLING_INTERVAL_MS / 2; // 더 자주 체크
const check = () => {
const element = isXpath ?
document.evaluate(selectorOrXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue :
document.querySelector(selectorOrXpath);
if (element) {
resolve(element);
} else {
elapsedTime += interval;
if (elapsedTime >= timeout) {
resolve(null); // 타임아웃 시 null 반환
} else {
setTimeout(check, interval);
}
}
};
check();
});
}
// 메인 로직 실행 함수
async function initSortProcess() {
if (scriptHasRunSuccessfully) {
log('info', '스크립트가 이미 성공적으로 실행되었습니다.');
return;
}
log('info', '스크립트 시작. 자동 정렬 시도.');
applyHideStyle(); // 클릭 전에 스타일을 먼저 적용하여 깜빡임 최소화
const dropdownButton = await waitForElement(DROPDOWN_BUTTON_SELECTOR, ELEMENT_WAIT_TIMEOUT_MS);
if (!dropdownButton) {
log('warn', `드롭다운 버튼 (${DROPDOWN_BUTTON_SELECTOR})을 찾지 못했습니다. 페이지 로드가 늦거나 UI가 변경되었을 수 있습니다.`);
return;
}
const currentSortElement = dropdownButton.querySelector(CURRENT_SORT_TEXT_SELECTOR);
const currentSortText = currentSortElement ? currentSortElement.textContent.trim() : null;
if (!currentSortText) {
log('warn', '현재 정렬 상태 텍스트를 읽을 수 없습니다.');
// 이 경우에도 일단 진행해 볼 수 있지만, 이미 정렬된 상태인지 확인할 수 없음.
}
if (currentSortText === TARGET_SORT_TEXT) {
log('info', `이미 '${TARGET_SORT_TEXT}'으로 정렬되어 있습니다.`);
scriptHasRunSuccessfully = true;
return;
}
log('info', `현재 정렬: '${currentSortText || "알 수 없음"}'. '${TARGET_SORT_TEXT}'으로 변경 시작.`);
// 드롭다운 버튼 클릭하여 옵션 목록 열기 (숨겨진 상태에서)
dropdownButton.click();
const sortOption = await waitForElement(OPTION_XPATH, OPTION_WAIT_TIMEOUT_MS, true);
if (sortOption) {
log('info', `'${TARGET_SORT_TEXT}' 옵션 찾음. 클릭합니다.`);
sortOption.click();
scriptHasRunSuccessfully = true;
log('info', '정렬 순서 변경 완료.');
// 드롭다운이 자연스럽게 닫히도록 잠시 대기 후 확인
// (페이지 반응성이 느릴 경우 aria-expanded가 바로 false가 안될 수 있음)
setTimeout(() => {
if (dropdownButton.getAttribute('aria-expanded') === 'true') {
log('info', '옵션 클릭 후 드롭다운이 아직 열려있어 강제로 닫습니다.');
dropdownButton.click(); // 만약 열려있다면 닫기
}
}, 100); // 짧은 지연
} else {
log('warn', `'${TARGET_SORT_TEXT}' 옵션을 시간 내에 찾지 못했습니다.`);
// 옵션을 못찾았을 경우, 열려있을 수 있는 드롭다운을 닫아준다.
if (dropdownButton.getAttribute('aria-expanded') === 'true') {
log('info', '옵션 찾기 실패 후 드롭다운이 열려있어 닫습니다.');
dropdownButton.click();
}
}
}
// 페이지 로드 시 또는 DOM 변경 감지 시 유연하게 대응하기 위해
// MutationObserver를 사용하거나, 단순하게 setInterval로 주기적 확인 가능.
// 이 스크립트는 특정 페이지(@match)에서 한 번만 실행되면 되므로,
// DOMContentLoaded 이후 또는 적절한 지연 후 실행하는 것이 좋음.
// 여기서는 기존 방식과 유사하게, 하지만 async/await로 개선된 로직을 사용합니다.
// 스크립트가 여러 번 실행되는 것을 방지하기 위해 (예: SPA 페이지 이동 착각)
if (window.chzzkSortScriptHasInitiated) {
log('info', '스크립트 초기화 로직이 이미 실행되었습니다.');
return;
}
window.chzzkSortScriptHasInitiated = true;
// 페이지가 완전히 로드된 후 스크립트 실행을 시도하는 것이 안정적일 수 있습니다.
// 간단하게 setTimeout으로 시작 지연을 주거나, DOMContentLoaded 이벤트를 사용할 수 있습니다.
// Chzzk 페이지는 동적으로 컨텐츠를 로드하므로, 적절한 시점에 시작하는 것이 중요.
// 여기서는 기존의 polling 방식을 유지하되, 타임아웃을 명확히 합니다.
// mainIntervalId 같은 전역 인터벌 대신, initSortProcess 내부에서 비동기적으로 처리.
// MutationObserver를 사용하여 #live-order 버튼이 생길 때까지 기다리는 것이 더 효율적일 수 있으나,
// 여기서는 주어진 코드의 구조를 최대한 활용하여 리팩토링합니다.
// 간단하게 페이지 로드 후 일정 시간 뒤에 시작하거나, 짧은 인터벌로 버튼을 찾고,
// 찾으면 메인 로직을 실행하고 인터벌을 중지하는 방식으로 구현.
const initialCheckInterval = setInterval(() => {
if (document.querySelector(DROPDOWN_BUTTON_SELECTOR) || document.readyState === 'complete') {
clearInterval(initialCheckInterval);
initSortProcess().catch(err => { // async 함수의 에러는 여기서 처리
console.error('[ChzzkSortScript] 스크립트 실행 중 예외 발생:', err);
});
}
}, POLLING_INTERVAL_MS);
// 만약 30초 후에도 시작 안되면 강제 중지 (안전장치)
setTimeout(() => {
clearInterval(initialCheckInterval);
if (!scriptHasRunSuccessfully && !window.chzzkSortScriptHasInitiatedMainLogic) { // initSortProcess가 시작되지 않았다면
log('warn', '스크립트 초기화 시도가 너무 오래 걸려 중단합니다.');
}
}, 30000);
// initSortProcess 함수 내부에서 실제 로직이 시작될 때 이 플래그를 true로 설정
// (initSortProcess가 한 번 호출되면 window.chzzkSortScriptHasInitiatedMainLogic = true; 추가 필요)
// 또는, initSortProcess의 첫 줄에 설정하여 중복 실행 방지
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment