Bing Plus

Add Gemini response, improve speed to search results, add Google search buttons

当前为 2025-06-17 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Bing Plus
// @version      6.3
// @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.9/marked.min.js
// @license      MIT
// @namespace http://tampermonkey.net/
// ==/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.9'
        },
        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 = `
                <img src="${Config.ASSETS.GOOGLE_LOGO}" alt="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 = `
                <div id="gemini-header">
                    <div id="gemini-title-wrap">
                        <img id="gemini-logo" src="${Config.ASSETS.GEMINI_LOGO}" alt="Gemini Logo">
                        <h3>Gemini Search Results</h3>
                    </div>
                    <img id="gemini-refresh-btn" title="Refresh" src="${Config.ASSETS.REFRESH_ICON}" />
                </div>
                <hr id="gemini-divider">
                <div id="gemini-content">${Localization.getMessage(Config.MESSAGE_KEYS.LOADING)}</div>
            `;
            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 = `
                                <p><b>${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_TITLE)}</b></p>
                                <p>Current: ${Config.VERSIONS.MARKED_VERSION}<br>Latest: ${latest}</p>
                                <button>${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_NOW)}</button>
                            `;
                            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();
})();