Last active
May 18, 2025 15:10
-
-
Save lunamoth/9fe3255283371f3c6370159b5dcbee0f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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