Last active
May 18, 2025 15:06
-
-
Save lunamoth/27717e92ded97d0f5e820d8897ade053 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 Google AI Studio - 사이드바 자동 숨김 (v2.3 - 사용자 설정) | |
// @namespace http://tampermonkey.net/ | |
// @version 2.4 | |
// @description Google AI Studio 자동 숨김. 사용자가 직접 열었을 때 유지 시간 설정 가능. | |
// @author Gemini | |
// @match https://aistudio.google.com/* | |
// @grant none | |
// @run-at document-idle | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
// ======================================================================== | |
// ★★★ 사용자 설정 영역 시작 ★★★ | |
// ======================================================================== | |
const USER_SETTINGS = { | |
USER_ACTION_GRACE_PERIOD_MS: 10000, // 오른쪽 패널을 직접 열었을 때 유지 시간 (밀리초) | |
// 예: 5초 = 5000, 10초 = 10000, 30초 = 30000 | |
CHECK_INTERVAL_MS: 250, // (고급) 핵심 확인 주기 (밀리초) | |
INITIAL_CHECK_MAX_DURATION_MS: 5000, // (고급) 초기 로딩 시 확인 최대 지속 시간 (밀리초) | |
NAV_CLOSE_DELAY_MS: 50, // (고급) 페이지 이동 후 닫기 시도까지 지연 시간 (밀리초) | |
}; | |
// ======================================================================== | |
// ★★★ 사용자 설정 영역 끝 ★★★ | |
// ======================================================================== | |
const SELECTORS = { | |
LEFT_NAVBAR: 'ms-navbar .layout-navbar', | |
LEFT_COLLAPSE_BUTTON: 'ms-navbar button[aria-label="Expand or collapse navigation menu"], ms-navbar button[aria-label*="탐색 메뉴"]', | |
RIGHT_PANEL: 'ms-right-side-panel', | |
// 아래 선택자들은 RIGHT_PANEL 요소 하위에서 검색됩니다. | |
RIGHT_PANEL_CONTENT: '.content-container', | |
RIGHT_PANEL_CLOSE_BUTTON: 'ms-run-settings button[aria-label="Close run settings panel"], ms-run-settings button[aria-label*="닫기"]', | |
// 이 선택자는 페이지 전역에서 검색됩니다. | |
RIGHT_PANEL_OPEN_BUTTON: 'button[aria-label="Run settings"], button[aria-label*="실행 설정"]', | |
}; | |
const STATE = { | |
lastUserOpenTimestamp: 0, | |
checkIntervalId: null, | |
isInitialCheckPhase: true, | |
initialCheckStartTime: 0, | |
currentHref: document.location.href, | |
isClosingRightPanel: false, | |
isInitialized: false, | |
}; | |
const SCRIPT_NAME = 'AI Studio 자동 숨김'; | |
const SCRIPT_VERSION = 'v2.3'; // UserScript 메타데이터와 일치 | |
// --- 유틸리티 함수 --- | |
function debounce(func, wait) { | |
let timeout; | |
return function executedFunction(...args) { | |
const later = () => { | |
clearTimeout(timeout); | |
func.apply(this, args); | |
}; | |
clearTimeout(timeout); | |
timeout = setTimeout(later, wait); | |
}; | |
} | |
// --- 로깅 함수 --- | |
function logImportant(message) { | |
console.log(`%c${SCRIPT_NAME} (${SCRIPT_VERSION}): ${message}`, 'color: green; font-weight: bold;'); | |
} | |
function logError(message, error) { | |
console.error(`${SCRIPT_NAME} (${SCRIPT_VERSION}): ${message}`, error || ''); | |
} | |
// 디버그용 로그 (릴리스 시 주석 처리 상태 유지) | |
// function logDebug(message) { /* console.log(`${SCRIPT_NAME}: ${message}`); */ } | |
// --- DOM 요소 가져오기 헬퍼 --- | |
function getElement(selector, parent = document) { | |
try { | |
return parent.querySelector(selector); | |
} catch (e) { | |
logError(`요소(${selector}) 검색 중 오류 발생:`, e); | |
return null; | |
} | |
} | |
// --- 오른쪽 패널 열기 버튼 리스너 관리 --- | |
function attachRightPanelOpenButtonListener() { | |
try { | |
const openButton = getElement(SELECTORS.RIGHT_PANEL_OPEN_BUTTON); // 전역에서 검색 | |
if (openButton && openButton.getAttribute('data-autohide-listener') !== 'true') { | |
openButton.removeEventListener('click', handleRightPanelOpenButtonClick); // 중복 방지 | |
openButton.addEventListener('click', handleRightPanelOpenButtonClick); | |
openButton.setAttribute('data-autohide-listener', 'true'); | |
// logDebug('오른쪽 패널 열기 버튼 리스너 추가됨.'); | |
return true; | |
} | |
} catch (error) { | |
logError('오른쪽 패널 열기 버튼 리스너 부착 중 오류 발생:', error); | |
} | |
return false; | |
} | |
function handleRightPanelOpenButtonClick(event) { | |
if (event && event.isTrusted) { // 실제 사용자 클릭인지 확인 | |
// logDebug('[사용자 액션] 오른쪽 패널 열림 버튼 클릭됨. 타임스탬프 업데이트.'); | |
STATE.lastUserOpenTimestamp = Date.now(); | |
} | |
} | |
// --- 패널 제어 함수 --- | |
function closeLeftPanel() { | |
try { | |
const leftNavbar = getElement(SELECTORS.LEFT_NAVBAR); | |
const leftCollapseButton = getElement(SELECTORS.LEFT_COLLAPSE_BUTTON); | |
if (leftNavbar && leftNavbar.classList.contains('expanded') && leftCollapseButton) { | |
leftCollapseButton.click(); | |
// logDebug('왼쪽 패널 닫기 실행됨.'); | |
return true; | |
} | |
} catch (error) { | |
logError('왼쪽 패널 닫기 중 오류 발생:', error); | |
} | |
return false; | |
} | |
function isRightPanelRenderedAndOpen() { | |
const rightPanelElement = getElement(SELECTORS.RIGHT_PANEL); | |
if (!rightPanelElement) return false; // 패널 자체가 없으면 닫힌 상태 | |
return !!getElement(SELECTORS.RIGHT_PANEL_CONTENT, rightPanelElement); // 패널 내 컨텐츠 유무로 열림 상태 판단 | |
} | |
function tryCloseRightPanel(reason = "자동 숨김") { | |
if (STATE.isClosingRightPanel) return false; // 이미 닫기 진행 중이면 중복 실행 방지 | |
const rightPanelElement = getElement(SELECTORS.RIGHT_PANEL); | |
// 패널이 없거나, 패널은 있으나 컨텐츠가 없어 이미 닫힌 것으로 간주되면 실행 안함 | |
if (!rightPanelElement || !getElement(SELECTORS.RIGHT_PANEL_CONTENT, rightPanelElement)) { | |
return false; | |
} | |
const closeButton = getElement(SELECTORS.RIGHT_PANEL_CLOSE_BUTTON, rightPanelElement); | |
if (!closeButton) { | |
// logDebug('오른쪽 패널 닫기 버튼을 찾을 수 없습니다.'); | |
return false; // 닫기 버튼이 없으면 실행 불가 | |
} | |
STATE.isClosingRightPanel = true; | |
// logDebug(`오른쪽 패널 닫기 시도 (사유: ${reason})`); | |
try { | |
if (document.body.contains(closeButton)) { // 버튼이 실제로 DOM에 존재하는지 최종 확인 | |
closeButton.click(); | |
} else { | |
// 이 로그는 버튼이 사라지는 예외적인 상황을 파악하는 데 도움이 될 수 있습니다. | |
logError('오른쪽 패널 닫기 버튼이 DOM에서 사라져 클릭할 수 없습니다.', `사유: ${reason}`); | |
} | |
} catch (clickError) { | |
logError('오른쪽 패널 닫기 버튼 클릭 중 오류 발생:', clickError); | |
} finally { | |
// 클릭 후 실제 DOM 변경까지 시간이 걸릴 수 있으므로, 플래그 해제에 약간의 지연을 줌 | |
setTimeout(() => { STATE.isClosingRightPanel = false; }, 100); | |
} | |
return true; | |
} | |
function closeRightPanelIfNeeded(reason = "주기적 확인") { | |
if (STATE.isClosingRightPanel) return false; | |
const timeSinceLastUserOpen = Date.now() - STATE.lastUserOpenTimestamp; | |
// 사용자가 패널을 열었고, 설정된 유예 시간 이내인 경우 닫지 않음 | |
if (STATE.lastUserOpenTimestamp > 0 && timeSinceLastUserOpen < USER_SETTINGS.USER_ACTION_GRACE_PERIOD_MS) { | |
// logDebug(`오른쪽 패널 닫기 건너뜀 (사용자 유예 기간 ${Math.round((USER_SETTINGS.USER_ACTION_GRACE_PERIOD_MS - timeSinceLastUserOpen)/1000)}초 남음).`); | |
return false; | |
} | |
return tryCloseRightPanel(reason); | |
} | |
// --- 핵심 로직 --- | |
function periodicCheck() { | |
try { | |
closeLeftPanel(); | |
closeRightPanelIfNeeded("주기적 확인"); | |
attachRightPanelOpenButtonListener(); // 버튼이 동적으로 생성될 수 있으므로 주기적 리스너 부착 시도 | |
if (STATE.isInitialCheckPhase) { | |
const leftNavbar = getElement(SELECTORS.LEFT_NAVBAR); | |
const isLeftPanelClosed = !(leftNavbar && leftNavbar.classList.contains('expanded')); | |
const isRightEffectivelyClosed = !isRightPanelRenderedAndOpen(); // 오른쪽 패널 상태 확인 | |
const elapsedTime = Date.now() - STATE.initialCheckStartTime; | |
// 초기 0.5초 이후 양쪽 패널이 모두 닫혔거나, 최대 지속 시간을 초과하면 초기 단계 종료 | |
if ((isLeftPanelClosed && isRightEffectivelyClosed && elapsedTime > 500) || | |
(elapsedTime > USER_SETTINGS.INITIAL_CHECK_MAX_DURATION_MS)) { | |
STATE.isInitialCheckPhase = false; | |
logImportant(`초기 확인 단계 종료 (경과: ${elapsedTime}ms).`); | |
} | |
} | |
} catch (error) { | |
logError('주기적 확인 작업 중 오류 발생:', error); | |
} | |
} | |
// --- URL 변경 처리 --- | |
const debouncedHandleUrlChange = debounce(() => { | |
// logDebug(`[URL 변경 감지] 새 URL: ${document.location.href}. 즉시 닫기 시도.`); | |
STATE.currentHref = document.location.href; | |
STATE.lastUserOpenTimestamp = 0; // URL 변경 시 사용자 액션으로 인한 유예 시간 초기화 | |
STATE.isClosingRightPanel = false; // 닫기 플래그 초기화 | |
// URL 변경 후 DOM이 안정화될 시간을 약간 줌 | |
setTimeout(() => { | |
closeLeftPanel(); // 왼쪽 패널 즉시 닫기 | |
tryCloseRightPanel("URL 변경으로 인한 강제 닫기"); // 오른쪽 패널은 사용자 유예 시간 무시하고 닫기 | |
attachRightPanelOpenButtonListener(); // 새 페이지 요소에 리스너 재부착 | |
STATE.isInitialCheckPhase = true; // 초기화 확인 단계 재시작 | |
STATE.initialCheckStartTime = Date.now(); | |
}, USER_SETTINGS.NAV_CLOSE_DELAY_MS); | |
}, 150); // URL 변경 감지 디바운스 시간 | |
function setupUrlChangeListener() { | |
const checkForUrlChange = () => { | |
if (document.location.href !== STATE.currentHref) { | |
debouncedHandleUrlChange(); | |
} | |
}; | |
window.addEventListener('popstate', checkForUrlChange); | |
const originalPushState = history.pushState; | |
history.pushState = function(...args) { | |
originalPushState.apply(history, args); | |
checkForUrlChange(); | |
}; | |
const originalReplaceState = history.replaceState; | |
history.replaceState = function(...args) { | |
originalReplaceState.apply(history, args); | |
checkForUrlChange(); | |
}; | |
// logDebug('URL 변경 리스너 설정됨.'); | |
} | |
// --- 스크립트 초기화 --- | |
function initialize() { | |
if (STATE.isInitialized) { | |
// logDebug('스크립트가 이미 초기화되었습니다. 중복 실행 방지.'); | |
return; | |
} | |
STATE.isInitialized = true; // 초기화 플래그 설정 | |
logImportant(`스크립트 초기화 완료. 사용자 설정 유지 시간: ${USER_SETTINGS.USER_ACTION_GRACE_PERIOD_MS / 1000}초.`); | |
STATE.initialCheckStartTime = Date.now(); | |
STATE.isInitialCheckPhase = true; | |
STATE.lastUserOpenTimestamp = 0; | |
STATE.isClosingRightPanel = false; | |
STATE.currentHref = document.location.href; // 초기화 시 현재 href 정확히 설정 | |
if (STATE.checkIntervalId) clearInterval(STATE.checkIntervalId); // 만약을 위한 이전 인터벌 정리 | |
STATE.checkIntervalId = setInterval(periodicCheck, USER_SETTINGS.CHECK_INTERVAL_MS); | |
// logDebug(`핵심 확인 인터벌 시작 (간격: ${USER_SETTINGS.CHECK_INTERVAL_MS}ms).`); | |
setupUrlChangeListener(); | |
setTimeout(periodicCheck, 50); // 첫 확인은 DOM 렌더링 후 빠르게 시도 | |
} | |
// --- 스크립트 실행 시작점 --- | |
function main() { | |
// DOM 로드 상태에 따라 초기화 실행 | |
if (document.readyState === 'complete') { | |
setTimeout(initialize, 200); // 페이지 로드 완료 후 약간의 지연을 두어 안정성 확보 | |
} else { | |
const onReadyStateChangeCallback = () => { | |
if (document.readyState === 'complete') { | |
document.removeEventListener('readystatechange', onReadyStateChangeCallback); | |
if (!STATE.isInitialized) setTimeout(initialize, 200); | |
} | |
}; | |
document.addEventListener('readystatechange', onReadyStateChangeCallback); | |
// 안전 장치: readystatechange 이벤트가 예상대로 동작하지 않을 경우를 대비 | |
setTimeout(() => { | |
if (document.readyState === 'complete' && !STATE.isInitialized) { | |
logError('안전 장치 발동: 강제 초기화 시도.'); | |
initialize(); | |
} | |
}, 2000); // 안전 장치 발동 시간 | |
} | |
// 페이지를 떠나기 전 인터벌 정리 | |
window.addEventListener('beforeunload', () => { | |
if (STATE.checkIntervalId) { | |
clearInterval(STATE.checkIntervalId); | |
// logDebug('페이지 언로드. 인터벌 정리됨.'); | |
} | |
}); | |
} | |
// 스크립트 실행 | |
main(); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment