// ==UserScript== // @name Bing Plus // @version 6.2 // @description Add Gemini response, improve speed to search results, add Google search buttons // @author lanpod // @match https://www.bing.com/search* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @require https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js // @license MIT // @namespace http://tampermonkey.net/ // @downloadURL https://update.greasyfork.icu/scripts/530097/Bing%20Plus.user.js // @updateURL https://update.greasyfork.icu/scripts/530097/Bing%20Plus.meta.js // ==/UserScript== (function () { 'use strict'; // 설정 모듈: 전역 설정값 관리 (API, 스타일, 메시지 등) const Config = { API: { GEMINI_MODEL: 'gemini-2.0-flash', GEMINI_URL: 'https://generativelanguage.googleapis.com/v1beta/models/', MARKED_CDN_URL: 'https://api.cdnjs.com/libraries/marked' }, VERSIONS: { MARKED_VERSION: '15.0.7' }, CACHE: { PREFIX: 'gemini_cache_' }, STORAGE_KEYS: { CURRENT_VERSION: 'markedCurrentVersion', LATEST_VERSION: 'markedLatestVersion', LAST_NOTIFIED: 'markedLastNotifiedVersion' }, UI: { DEFAULT_MARGIN: 8, DEFAULT_PADDING: 16, Z_INDEX: 9999 }, STYLES: { COLORS: { BACKGROUND: '#fff', BORDER: '#e0e0e0', TEXT: '#000', TITLE: '#000', BUTTON_BG: '#f0f3ff', BUTTON_BORDER: '#ccc', DARK_BACKGROUND: '#202124', DARK_BORDER: '#5f6368', DARK_TEXT: '#fff', CODE_BLOCK_BG: '#f0f0f0', DARK_CODE_BLOCK_BG: '#555' }, BORDER: '1px solid #e0e0e0', BORDER_RADIUS: '4px', FONT_SIZE: { TEXT: '14px', TITLE: '18px' }, ICON_SIZE: '20px', LOGO_SIZE: '24px', SMALL_ICON_SIZE: '16px' }, ASSETS: { GOOGLE_LOGO: 'https://www.gstatic.com/marketing-cms/assets/images/bc/1a/a310779347afa1927672dc66a98d/g.png=s48-fcrop64=1,00000000ffffffff-rw', GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg', REFRESH_ICON: 'https://www.svgrepo.com/show/533704/refresh-cw-alt-3.svg' }, MESSAGE_KEYS: { PROMPT: 'prompt', ENTER_API_KEY: 'enterApiKey', GEMINI_EMPTY: 'geminiEmpty', PARSE_ERROR: 'parseError', NETWORK_ERROR: 'networkError', TIMEOUT: 'timeout', LOADING: 'loading', UPDATE_TITLE: 'updateTitle', UPDATE_NOW: 'updateNow', SEARCH_ON_GOOGLE: 'searchongoogle' } }; // 지역화 모듈: 다국어 메시지 처리 const Localization = { // 다국어 메시지 객체 MESSAGES: { [Config.MESSAGE_KEYS.PROMPT]: { ko: `"${'${query}'}"에 대한 정보를 찾아줘`, zh: `请以标记格式填写有关\"${'${query}'}\"的信息。`, default: `Please write information about \"${'${query}'}\" in markdown format` }, [Config.MESSAGE_KEYS.ENTER_API_KEY]: { ko: 'Gemini API 키를 입력하세요:', zh: '请输入 Gemini API 密钥:', default: 'Please enter your Gemini API key:' }, [Config.MESSAGE_KEYS.GEMINI_EMPTY]: { ko: '⚠️ Gemini 응답이 비어있습니다.', zh: '⚠️ Gemini 返回为空。', default: '⚠️ Gemini response is empty.' }, [Config.MESSAGE_KEYS.PARSE_ERROR]: { ko: '❌ 파싱 오류:', zh: '❌ 解析错误:', default: '❌ Parsing error:' }, [Config.MESSAGE_KEYS.NETWORK_ERROR]: { ko: '❌ 네트워크 오류:', zh: '❌ 网络错误:', default: '❌ Network error:' }, [Config.MESSAGE_KEYS.TIMEOUT]: { ko: '❌ 요청 시간이 초과되었습니다.', zh: '❌ 请求超时。', default: '❌ Request timeout' }, [Config.MESSAGE_KEYS.LOADING]: { ko: '불러오는 중...', zh: '加载中...', default: 'Loading...' }, [Config.MESSAGE_KEYS.UPDATE_TITLE]: { ko: 'marked.min.js 업데이트 필요', zh: '需要更新 marked.min.js', default: 'marked.min.js update required' }, [Config.MESSAGE_KEYS.UPDATE_NOW]: { ko: '확인', zh: '确认', default: 'OK' }, [Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE]: { ko: 'Google 에서 검색하기', zh: '在 Google 上搜索', default: 'Search on Google' } }, // 주어진 키와 변수를 사용하여 지역화된 메시지 반환 getMessage(key, vars = {}) { const lang = navigator.language; const langKey = lang.includes('ko') ? 'ko' : lang.includes('zh') ? 'zh' : 'default'; const template = this.MESSAGES[key]?.[langKey] || this.MESSAGES[key]?.default || ''; return template.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || ''); } }; // 디바이스 감지 모듈: 디바이스 타입 감지 (데스크톱, 모바일, 태블릿) const DeviceDetector = { // 캐싱 변수 _cache: { deviceType: null, isGeminiAvailable: null }, // 디바이스 타입 판별 (한 번에 처리) getDeviceType() { if (this._cache.deviceType !== null) { return this._cache.deviceType; } const userAgent = navigator.userAgent; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const width = window.innerWidth; let deviceType; // 1. UserAgent 기반으로 기본 판별 const isAndroid = /Android/i.test(userAgent); const isIPhone = /iPhone/i.test(userAgent); const hasMobileKeyword = /Mobile/i.test(userAgent); const isWindows = /Windows NT/i.test(userAgent); // 2. 디바이스 타입 판별 로직 if (isWindows && !isTouchDevice && width > 1024) { // Windows 기반이고 터치 디바이스가 아니며 화면이 넓으면 데스크톱으로 판별 deviceType = 'desktop'; } else if ((isAndroid || isIPhone) && hasMobileKeyword) { // Android 또는 iPhone이고 "Mobile" 키워드가 있으면 모바일 deviceType = 'mobile'; } else if (isAndroid && !hasMobileKeyword && width >= 768) { // Android이지만 "Mobile" 키워드가 없고 화면 크기가 태블릿 수준이면 태블릿 deviceType = 'tablet'; } else if (isTouchDevice && width <= 1024) { // 터치 디바이스이고 화면 크기가 작으면 모바일 deviceType = 'mobile'; } else { // 기본적으로 데스크톱으로 간주 deviceType = 'desktop'; } // 캐싱 및 로그 출력 this._cache.deviceType = deviceType; console.log(`Device Type: ${deviceType.charAt(0).toUpperCase() + deviceType.slice(1)}`); return deviceType; }, // 데스크톱 환경인지 확인 isDesktop() { return this.getDeviceType() === 'desktop'; }, // 모바일 환경인지 확인 isMobile() { return this.getDeviceType() === 'mobile'; }, // 태블릿 환경인지 확인 isTablet() { return this.getDeviceType() === 'tablet'; }, // Gemini UI를 표시할 수 있는 환경인지 확인 (데스크톱 환경에서만 가능) isGeminiAvailable() { if (this._cache.isGeminiAvailable === null) { const hasBContext = !!document.getElementById('b_context'); const hasBRight = !!document.querySelector('.b_right'); this._cache.isGeminiAvailable = this.isDesktop() && (hasBContext || hasBRight); } return this._cache.isGeminiAvailable; }, // 캐시 초기화 resetCache() { this._cache = { deviceType: null, isGeminiAvailable: null }; } }; // 스타일 생성 모듈: CSS 스타일 정의 const StyleGenerator = { // 공통 스타일 정의 commonStyles: { '#b_results > li.b_ad a': { 'color': 'green !important' }, '#b_context, .b_context, .b_right': { 'color': 'initial !important', 'border': 'none !important', 'border-width': '0 !important', 'border-style': 'none !important', 'border-collapse': 'separate !important', 'background': 'transparent !important' } }, // Gemini 박스 스타일 정의 geminiBoxStyles: { '#gemini-box': { 'width': '100%', 'max-width': '100%', 'background': `${Config.STYLES.COLORS.BACKGROUND} !important`, 'border': `${Config.STYLES.BORDER} !important`, 'border-style': 'solid !important', 'border-width': '1px !important', 'border-radius': Config.STYLES.BORDER_RADIUS, 'padding': `${Config.UI.DEFAULT_PADDING}px`, 'margin-bottom': `${Config.UI.DEFAULT_MARGIN * 2.5}px`, 'font-family': 'sans-serif', 'overflow-x': 'auto', 'position': 'relative', 'box-sizing': 'border-box', 'color': 'initial !important' } }, // 테마별 스타일 정의 (라이트/다크 모드) themeStyles: { '[data-theme="light"] #gemini-box, .light #gemini-box': { 'background': `${Config.STYLES.COLORS.BACKGROUND} !important`, 'border': `1px solid ${Config.STYLES.COLORS.BORDER} !important` }, '[data-theme="light"] #gemini-box h3, .light #gemini-box h3': { 'color': `${Config.STYLES.COLORS.TITLE} !important` }, '[data-theme="light"] #gemini-content, [data-theme="light"] #gemini-content *, .light #gemini-content, .light #gemini-content *': { 'color': `${Config.STYLES.COLORS.TEXT} !important`, 'background': 'transparent !important' }, '[data-theme="light"] #gemini-divider, .light #gemini-divider': { 'background': `${Config.STYLES.COLORS.BORDER} !important` }, '[data-theme="dark"] #gemini-box, .dark #gemini-box, .b_dark #gemini-box': { 'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`, 'border': `1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important` }, '@media (prefers-color-scheme: dark)': { '#gemini-box': { 'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`, 'border': `1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important` }, '#gemini-box h3': { 'color': `${Config.STYLES.COLORS.DARK_TEXT} !important` }, '#gemini-content, #gemini-content *': { 'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`, 'background': 'transparent !important' }, '#gemini-content pre': { 'background': `${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important` }, '#gemini-divider': { 'background': `${Config.STYLES.COLORS.DARK_BORDER} !important` }, '#marked-update-popup': { 'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`, 'color': `${Config.STYLES.COLORS.DARK_TEXT} !important` } } }, // Gemini 콘텐츠 스타일 정의 contentStyles: { '#gemini-content': { 'font-size': Config.STYLES.FONT_SIZE.TEXT, 'line-height': '1.6', 'white-space': 'pre-wrap', 'word-wrap': 'break-word', 'background': 'transparent !important' }, '#gemini-content pre': { 'background': `${Config.STYLES.COLORS.CODE_BLOCK_BG} !important`, 'padding': `${Config.UI.DEFAULT_MARGIN + 2}px`, 'border-radius': Config.STYLES.BORDER_RADIUS, 'overflow-x': 'auto' }, '[data-theme="dark"] #gemini-content pre, .dark #gemini-content pre, .b_dark #gemini-content pre': { 'background': `${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important` } }, // Gemini 헤더 스타일 정의 headerStyles: { '#gemini-header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'margin-bottom': `${Config.UI.DEFAULT_MARGIN}px` }, '#gemini-title-wrap': { 'display': 'flex', 'align-items': 'center' }, '#gemini-logo': { 'width': Config.STYLES.LOGO_SIZE, 'height': Config.STYLES.LOGO_SIZE, 'margin-right': `${Config.UI.DEFAULT_MARGIN}px` }, '#gemini-box h3': { 'margin': '0', 'font-size': Config.STYLES.FONT_SIZE.TITLE, 'font-weight': 'bold' }, '#gemini-refresh-btn': { 'width': Config.STYLES.ICON_SIZE, 'height': Config.STYLES.ICON_SIZE, 'cursor': 'pointer', 'opacity': '0.6', 'transition': 'transform 0.5s ease' }, '#gemini-refresh-btn:hover': { 'opacity': '1', 'transform': 'rotate(360deg)' }, '#gemini-divider': { 'height': '1px', 'margin': `${Config.UI.DEFAULT_MARGIN}px 0` } }, // Google 검색 버튼 스타일 정의 googleButtonStyles: { '#google-search-btn': { 'width': '100%', 'max-width': '100%', 'font-size': Config.STYLES.FONT_SIZE.TEXT, 'padding': `${Config.UI.DEFAULT_MARGIN}px`, 'margin-bottom': `${Config.UI.DEFAULT_MARGIN * 1.25}px`, 'cursor': 'pointer', 'border': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`, 'border-radius': Config.STYLES.BORDER_RADIUS, 'background-color': Config.STYLES.COLORS.BUTTON_BG, 'color': Config.STYLES.COLORS.TITLE, 'font-family': 'sans-serif', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'gap': `${Config.UI.DEFAULT_MARGIN}px`, 'transition': 'transform 0.2s ease' }, '#google-search-btn img': { 'width': Config.STYLES.SMALL_ICON_SIZE, 'height': Config.STYLES.SMALL_ICON_SIZE, 'vertical-align': 'middle', 'transition': 'transform 0.2s ease' }, '.desktop-useragent #google-search-btn:hover': { 'transform': 'scale(1.1)' }, '.desktop-useragent #google-search-btn:hover img': { 'transform': 'scale(1.1)' } }, // 업데이트 팝업 스타일 정의 popupStyles: { '#marked-update-popup': { 'position': 'fixed', 'top': '30%', 'left': '50%', 'transform': 'translate(-50%, -50%)', 'background': Config.STYLES.COLORS.BACKGROUND, 'padding': `${Config.UI.DEFAULT_PADDING * 1.25}px`, 'z-index': Config.UI.Z_INDEX, 'border': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`, 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)', 'text-align': 'center' }, '[data-theme="dark"] #marked-update-popup, .dark #marked-update-popup, .b_dark #marked-update-popup': { 'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`, 'color': `${Config.STYLES.COLORS.DARK_TEXT} !important` }, '#marked-update-popup button': { 'margin-top': `${Config.UI.DEFAULT_MARGIN * 1.25}px`, 'padding': `${Config.UI.DEFAULT_MARGIN}px ${Config.UI.DEFAULT_PADDING}px`, 'cursor': 'pointer', 'border': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`, 'border-radius': Config.STYLES.BORDER_RADIUS, 'background-color': Config.STYLES.COLORS.BUTTON_BG, 'color': Config.STYLES.COLORS.TITLE, 'font-family': 'sans-serif' } }, // 모바일 환경 스타일 정의 mobileStyles: { '.mobile-useragent #google-search-btn': { 'max-width': '100%', 'width': 'calc(100% - 16px)', // 좌우 마진 8px씩을 고려한 너비 계산 'margin-left': `${Config.UI.DEFAULT_MARGIN}px !important`, // 좌측 마진 8px 'margin-right': `${Config.UI.DEFAULT_MARGIN}px !important`, // 우측 마진 8px 'margin-top': `${Config.UI.DEFAULT_MARGIN}px`, 'margin-bottom': `${Config.UI.DEFAULT_MARGIN}px`, 'padding': `${Config.UI.DEFAULT_PADDING * 0.75}px`, 'border-radius': '16px', 'box-sizing': 'border-box' }, '.mobile-useragent #gemini-box': { 'padding': `${Config.UI.DEFAULT_PADDING * 0.75}px`, 'border-radius': '16px' }, '.mobile-useragent #b_content': { 'overflow': 'visible !important', // 부모 요소의 오버플로우로 인해 마진이 잘리지 않도록 설정 'position': 'relative' } }, // 모든 스타일을 하나의 CSS 문자열로 변환 generateStyles() { const styles = [ this.commonStyles, this.geminiBoxStyles, this.themeStyles, this.contentStyles, this.headerStyles, this.googleButtonStyles, this.popupStyles, this.mobileStyles ]; return styles.reduce((css, styleObj) => { for (const [selector, props] of Object.entries(styleObj)) { css += `${selector} {`; for (const [prop, value] of Object.entries(props)) { css += `${prop}: ${value};`; } css += '}'; } return css; }, ''); } }; // 테마 관리 모듈: 테마 변경 감지 및 적용 const ThemeManager = { // 테마 변경 감지 및 스타일 적용 applyTheme() { const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark' || document.documentElement.classList.contains('dark') || document.documentElement.classList.contains('b_dark') || window.matchMedia('(prefers-color-scheme: dark)').matches; const geminiBox = document.querySelector('#gemini-box'); if (geminiBox) { geminiBox.style.background = isDarkTheme ? Config.STYLES.COLORS.DARK_BACKGROUND : Config.STYLES.COLORS.BACKGROUND; geminiBox.style.borderColor = isDarkTheme ? Config.STYLES.COLORS.DARK_BORDER : Config.STYLES.COLORS.BORDER; } }, // 테마 변경 감지 설정 observeThemeChange() { const observer = new MutationObserver(() => this.applyTheme()); const targetElement = document.querySelector('#b_context') || document.querySelector('.b_right') || document.documentElement; observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] }); if (targetElement !== document.documentElement) { observer.observe(targetElement, { attributes: true, attributeFilter: ['style', 'class'] }); } window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => this.applyTheme()); } }; // 스타일 관리 모듈: 스타일 초기화 및 적용 const Styles = { // 스타일 초기화 initStyles() { const styleElement = document.createElement('style'); styleElement.id = 'bing-plus-styles'; styleElement.textContent = StyleGenerator.generateStyles(); document.head.appendChild(styleElement); this.applyMobileStyles(); }, // 모바일 환경 스타일 적용 applyMobileStyles() { if (DeviceDetector.isMobile()) { document.documentElement.classList.add('mobile-useragent'); } else if (DeviceDetector.isDesktop()) { document.documentElement.classList.add('desktop-useragent'); } } }; // 유틸리티 모듈: 공통 유틸리티 함수 const Utils = { // 검색 쿼리 추출 getQuery() { return new URLSearchParams(location.search).get('q'); }, // Gemini API 키 가져오기 또는 입력받기 getApiKey() { let key = localStorage.getItem('geminiApiKey'); if (!key) { key = prompt(Localization.getMessage(Config.MESSAGE_KEYS.ENTER_API_KEY)); if (key) localStorage.setItem('geminiApiKey', key); } return key; } }; // UI 생성 모듈: DOM 요소 생성 const UI = { // Google 검색 버튼 생성 createGoogleButton(query) { const btn = document.createElement('button'); btn.id = 'google-search-btn'; btn.innerHTML = ` Google Logo ${Localization.getMessage(Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE)} `; btn.onclick = () => window.open(`https://www.google.com/search?q=${encodeURIComponent(query)}`, '_blank'); return btn; }, // Gemini 박스 생성 createGeminiBox(query, apiKey) { const box = document.createElement('div'); box.id = 'gemini-box'; box.innerHTML = `

Gemini Search Results


${Localization.getMessage(Config.MESSAGE_KEYS.LOADING)}
`; box.querySelector('#gemini-refresh-btn').onclick = () => GeminiAPI.fetch(query, box.querySelector('#gemini-content'), apiKey, true); if (DeviceDetector.isDesktop()) { VersionChecker.checkMarkedJsVersion(); } return box; }, // Gemini UI 전체 생성 (Google 버튼 + Gemini 박스) createGeminiUI(query, apiKey) { const wrapper = document.createElement('div'); wrapper.id = 'gemini-wrapper'; wrapper.appendChild(this.createGoogleButton(query)); wrapper.appendChild(this.createGeminiBox(query, apiKey)); return wrapper; }, // 기존 UI 요소 제거 removeExistingElements() { document.querySelectorAll('#gemini-wrapper, #google-search-btn').forEach(el => el.remove()); } }; // Gemini API 모듈: Gemini API 호출 및 응답 처리 const GeminiAPI = { // Gemini API 호출 fetch(query, container, apiKey, force = false) { const cacheKey = `${Config.CACHE.PREFIX}${query}`; const cached = force ? null : sessionStorage.getItem(cacheKey); if (cached) { if (container) container.innerHTML = marked.parse(cached); return; } if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.LOADING); GM_xmlhttpRequest({ method: 'POST', url: `${Config.API.GEMINI_URL}${Config.API.GEMINI_MODEL}:generateContent?key=${apiKey}`, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ contents: [{ parts: [{ text: Localization.getMessage(Config.MESSAGE_KEYS.PROMPT, { query }) }] }] }), onload({ responseText }) { try { const text = JSON.parse(responseText)?.candidates?.[0]?.content?.parts?.[0]?.text; if (text) { sessionStorage.setItem(cacheKey, text); if (container) container.innerHTML = marked.parse(text); } else { if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.GEMINI_EMPTY); } } catch (e) { if (container) container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.PARSE_ERROR)} ${e.message}`; } }, onerror(err) { if (container) container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.NETWORK_ERROR)} ${err.finalUrl}`; }, ontimeout() { if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.TIMEOUT); } }); } }; // 링크 정리 모듈: 중간 URL 제거 const LinkCleaner = { // URL 디코딩 decodeRealUrl(url, key) { const param = new URL(url).searchParams.get(key)?.replace(/^a1/, ''); if (!param) return null; try { const decoded = decodeURIComponent(atob(param.replace(/_/g, '/').replace(/-/g, '+'))); return decoded.startsWith('/') ? location.origin + decoded : decoded; } catch { return null; } }, // 실제 URL로 변환 resolveRealUrl(url) { const rules = [ { pattern: /bing\.com\/(ck\/a|aclick)/, key: 'u' }, { pattern: /so\.com\/search\/eclk/, key: 'aurl' } ]; for (const { pattern, key } of rules) { if (pattern.test(url)) { const real = this.decodeRealUrl(url, key); if (real && real !== url) return real; } } return url; }, // 모든 링크를 실제 URL로 변환 convertLinksToReal(root) { root.querySelectorAll('a[href]').forEach(a => { const realUrl = this.resolveRealUrl(a.href); if (realUrl && realUrl !== a.href) a.href = realUrl; }); } }; // 버전 확인 모듈: marked.js 버전 체크 const VersionChecker = { // 버전 비교 compareVersions(current, latest) { const currentParts = current.split('.').map(Number); const latestParts = latest.split('.').map(Number); for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { const c = currentParts[i] || 0; const l = latestParts[i] || 0; if (c < l) return -1; if (c > l) return 1; } return 0; }, // marked.js 버전 체크 및 업데이트 알림 표시 checkMarkedJsVersion() { localStorage.setItem(Config.STORAGE_KEYS.CURRENT_VERSION, Config.VERSIONS.MARKED_VERSION); GM_xmlhttpRequest({ method: 'GET', url: Config.API.MARKED_CDN_URL, onload: ({ responseText }) => { try { const latest = JSON.parse(responseText).version; localStorage.setItem(Config.STORAGE_KEYS.LATEST_VERSION, latest); const lastNotified = localStorage.getItem(Config.STORAGE_KEYS.LAST_NOTIFIED); if (this.compareVersions(Config.VERSIONS.MARKED_VERSION, latest) < 0 && (!lastNotified || this.compareVersions(lastNotified, latest) < 0)) { const existingPopup = document.getElementById('marked-update-popup'); if (existingPopup) existingPopup.remove(); const popup = document.createElement('div'); popup.id = 'marked-update-popup'; popup.innerHTML = `

${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_TITLE)}

Current: ${Config.VERSIONS.MARKED_VERSION}
Latest: ${latest}

`; popup.querySelector('button').onclick = () => { localStorage.setItem(Config.STORAGE_KEYS.LAST_NOTIFIED, latest); popup.remove(); }; document.body.appendChild(popup); } } catch (e) { console.warn('marked.min.js version check error:', e.message); } }, onerror: () => console.warn('marked.min.js version check request failed') }); } }; // 이벤트 핸들러 모듈: URL 및 DOM 변경 감지 const EventHandler = { // URL 변경 감지 및 처리 observeUrlChange(onChangeCallback) { let lastUrl = location.href; const checkUrlChange = () => { if (location.href !== lastUrl) { lastUrl = location.href; onChangeCallback(); } }; const originalPushState = history.pushState; history.pushState = function (...args) { originalPushState.apply(this, args); checkUrlChange(); }; const originalReplaceState = history.replaceState; history.replaceState = function (...args) { originalReplaceState.apply(this, args); checkUrlChange(); }; window.addEventListener('popstate', checkUrlChange); const observer = new MutationObserver(checkUrlChange); const targetNode = document.querySelector('head > title') || document.body; observer.observe(targetNode, { childList: true, subtree: true }); } }; // 렌더링 상태 관리 모듈: UI 렌더링 상태 관리 const RenderState = { isRendering: false, // 렌더링 시작 startRendering() { if (this.isRendering) return false; this.isRendering = true; return true; }, // 렌더링 완료 finishRendering() { this.isRendering = false; } }; // UI 렌더링 모듈: UI 렌더링 로직 const UIRenderer = { // 데스크톱 환경 렌더링 renderDesktop(query, apiKey) { // Gemini UI가 표시될 수 있는지 확인 (데스크톱에서 강제로 렌더링 보장) const contextTarget = document.getElementById('b_context') || document.querySelector('.b_right'); if (!contextTarget) return false; requestAnimationFrame(() => { const wrapper = UI.createGeminiUI(query, apiKey); contextTarget.prepend(wrapper); window.requestIdleCallback(() => { const content = wrapper.querySelector('#gemini-content'); if (content) { const cache = sessionStorage.getItem(`${Config.CACHE.PREFIX}${query}`); if (cache) { content.innerHTML = marked.parse(cache); } else { window.requestIdleCallback(() => GeminiAPI.fetch(query, content, apiKey)); } } RenderState.finishRendering(); }); }); return true; }, // 모바일 환경 렌더링 renderMobile(query) { const contentTarget = document.getElementById('b_content'); if (!contentTarget) return false; requestAnimationFrame(() => { const googleBtn = UI.createGoogleButton(query); // 부모 요소에 스타일 적용 (필요 시) contentTarget.parentNode.style.overflow = 'visible'; contentTarget.parentNode.style.position = 'relative'; contentTarget.parentNode.insertBefore(googleBtn, contentTarget); RenderState.finishRendering(); }); return true; }, // 태블릿 환경 렌더링 (아무 UI도 표시하지 않음, Google 검색 버튼 포함) renderTablet() { // 태블릿에서는 Gemini UI와 Google 검색 버튼 모두 표시하지 않음 RenderState.finishRendering(); return true; }, // 메인 렌더링 함수 render() { if (!RenderState.startRendering()) return; const query = Utils.getQuery(); if (!query) { RenderState.finishRendering(); return; } UI.removeExistingElements(); const deviceType = DeviceDetector.getDeviceType(); if (deviceType === 'desktop') { const apiKey = Utils.getApiKey(); if (!apiKey) { RenderState.finishRendering(); return; } this.renderDesktop(query, apiKey); } else if (deviceType === 'mobile') { this.renderMobile(query); } else if (deviceType === 'tablet') { this.renderTablet(); } else { RenderState.finishRendering(); } } }; // 초기화 모듈: 스크립트 초기화 const Initializer = { // 초기화 실행 init() { const initialize = () => { Styles.initStyles(); ThemeManager.applyTheme(); LinkCleaner.convertLinksToReal(document); // DOM 준비 완료 후 렌더링 const checkAndRender = () => { if (document.getElementById('b_context') || document.querySelector('.b_right')) { UIRenderer.render(); } else { setTimeout(checkAndRender, 100); // 100ms 후 재시도 } }; checkAndRender(); EventHandler.observeUrlChange(() => { UIRenderer.render(); LinkCleaner.convertLinksToReal(document); }); ThemeManager.observeThemeChange(); }; if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(initialize, 1); } else { document.addEventListener('DOMContentLoaded', initialize); } } }; // 스크립트 실행 Initializer.init(); })();