Google Plus & Bing Plus

Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons

目前為 2025-06-18 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Google Plus & Bing Plus
// @version      7.0.2
// @description  Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons
// @author       monit8280
// @match        https://www.bing.com/search*
// @match        https://www.google.com/search*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      generativelanguage.googleapis.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.12/marked.min.js
// @license      MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    // 설정 모듈: 전역 설정값 관리 (API, 스타일, 메시지 등)
    const Config = {
        API: {
            GEMINI_MODEL: 'gemini-2.5-flash',
            GEMINI_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
            MARKED_CDN_URL: 'https://api.cdnjs.com/libraries/marked'
        },
        VERSIONS: {
            MARKED_VERSION: '15.0.12'
        },
        CACHE: {
            PREFIX: 'gemini_cache_'
        },
        STORAGE_KEYS: {
            CURRENT_VERSION: 'markedCurrentVersion',
            LATEST_VERSION: 'markedLatestVersion',
            LAST_NOTIFIED: 'markedLastNotifiedVersion',
            THEME_MODE: 'themeMode' // 테마 모드 저장을 위한 새 키 추가
        },
        UI: {
            DEFAULT_MARGIN: 8,
            DEFAULT_PADDING: 16,
            Z_INDEX: 9999
        },
        STYLES: {
            COLORS: {
                BACKGROUND_LIGHT: '#fff', // 라이트 모드 배경색
                BACKGROUND_DARK: '#282c34', // 다크 모드 배경색
                BORDER_LIGHT: '#e0e0e0', // 라이트 모드 테두리색
                BORDER_DARK: '#444', // 다크 모드 테두리색
                TEXT_LIGHT: '#000', // 라이트 모드 텍스트색
                TEXT_DARK: '#e0e0e0', // 다크 모드 텍스트색
                TITLE_LIGHT: '#000', // 라이트 모드 제목색
                TITLE_DARK: '#ffffff', // 다크 모드 제목색
                BUTTON_BG_LIGHT: '#f0f3ff', // 라이트 모드 버튼 배경
                BUTTON_BG_DARK: '#3a3f4b', // 다크 모드 버튼 배경
                BUTTON_BORDER_LIGHT: '#ccc', // 라이트 모드 버튼 테두리
                BUTTON_BORDER_DARK: '#555', // 다크 모드 버튼 테두리
                CODE_BLOCK_BG_LIGHT: '#f0f0f0', // 라이트 모드 코드 블록 배경
                CODE_BLOCK_BG_DARK: '#3b3b3b', // 다크 모드 코드 블록 배경
            },
            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',
            BING_LOGO: 'https://th.bing.com/th/id/ODF.EuPayFgGHQiAI7K9SOL6lg?w=32&h=32&qlt=91&pcl=fffffa&o=6&pid=1.2',
            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',
            LIGHT_MODE_ICON: 'https://www.svgrepo.com/show/503805/sun.svg', // 라이트 모드 아이콘
            DARK_MODE_ICON: 'https://www.svgrepo.com/show/526043/moon-stars.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',
            SEARCH_ON_BING: 'searchonbing'
        }
    };
    // 지역화 모듈: 다국어 메시지 처리
    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'
            },
            [Config.MESSAGE_KEYS.SEARCH_ON_BING]: {
                ko: 'Bing 에서 검색하기',
                zh: '在 Bing 上搜索',
                default: 'Search on Bing'
            }
        },

        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;
            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);
            if (isWindows && !isTouchDevice && width > 1024) {
                deviceType = 'desktop';
            } else if ((isAndroid || isIPhone) && hasMobileKeyword) {
                deviceType = 'mobile';
            } else if (isAndroid && !hasMobileKeyword && width >= 768) {
                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';
        },

        isGeminiAvailable() {
            if (this._cache.isGeminiAvailable === null) {
                const hasRHS = !!document.getElementById('rhs') ||
                    !!document.getElementById('b_context') || !!document.querySelector('.b_right');
                this._cache.isGeminiAvailable = this.isDesktop() && hasRHS;
            }
            return this._cache.isGeminiAvailable;
        },

        resetCache() {
            this._cache = {
                deviceType: null,
                isGeminiAvailable: null
            };
        },

        isGoogle() {
            return window.location.hostname.includes('google.com');
        },

        isBing() {
            return window.location.hostname.includes('bing.com');
        }
    };

    // 스타일 생성 모듈: 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'
            },
            '#rhs': {
                'float': 'right',
                'padding-left': '16px',
                'width': '432px',
                'margin-top': '20px'
            },
            '#rhs #gemini-wrapper': {
                'margin-bottom': '20px'
            },
            // Google 모바일 페이지의 gsr 요소 배경색 추가
            '.mobile-useragent #gsr': {
                'background-color': '#ffffff !important'
            }
        },

        // Gemini 박스 스타일 정의
        geminiBoxStyles: {
            '#gemini-box': {
                'width': '100%',
                'max-width': '100%',
                'border-width': '1px',
                'border-style': 'solid',
                '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: {
            '#gemini-box': {
                'background': `var(--gemini-background-color) !important`,
                'border-color': `var(--gemini-border-color) !important`
            },
            '#gemini-box h3': {
                'color': `var(--gemini-title-color) !important`
            },
            '#gemini-content, #gemini-content *': {
                'color': `var(--gemini-text-color) !important`,
                'background': 'transparent !important'
            },
            '#gemini-divider': {
                'background': `var(--gemini-border-color) !important`
            },
            '#gemini-content pre': {
                'background': `var(--gemini-code-block-bg) !important`,
                'padding': `${Config.UI.DEFAULT_MARGIN + 2}px`,
                'border-radius': Config.STYLES.BORDER_RADIUS,
                'overflow-x': 'auto'
            },
            '#google-search-btn, #bing-search-btn': {
                'border-color': `var(--gemini-button-border)`,
                'background-color': `var(--gemini-button-bg)`,
                'color': `var(--gemini-title-color)`,
            },
            '#marked-update-popup': {
                'background': `var(--gemini-background-color)`,
                'border-color': `var(--gemini-button-border)`,
            },
            '#marked-update-popup button': {
                'border-color': `var(--gemini-button-border)`,
                'background-color': `var(--gemini-button-bg)`,
                'color': `var(--gemini-title-color)`,
            }
        },

        // Gemini 콘텐츠 스타일 정의
        contentStyles: {
            '#gemini-content': {
                'font-size': Config.STYLES.FONT_SIZE.TEXT,
                'line-height': '1.6',
                'white-space': 'pre-wrap',
                'word-wrap': 'break-word',
                'overflow-wrap': 'break-word',
                'background': 'transparent !important'
            },
            '#gemini-content ul, #gemini-content ol': {
                'list-style-type': 'none'
            }
        },

        // 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',
                'margin-left': `${Config.UI.DEFAULT_MARGIN}px`
            },
            '#gemini-theme-toggle-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, #gemini-theme-toggle-btn:hover': {
                'opacity': '1',
                'transform': 'rotate(360deg)'
            },
            '#gemini-divider': {
                'height': '1px',
                'margin': `${Config.UI.DEFAULT_MARGIN}px 0`
            }
        },

        // Google/Bing 검색 버튼 스타일 정의
        searchButtonStyles: {
            '#google-search-btn, #bing-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-width': '1px',
                'border-style': 'solid',
                'border-radius': Config.STYLES.BORDER_RADIUS,
                '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, #bing-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, .desktop-useragent #bing-search-btn:hover': {
                'transform': 'scale(1.1)'
            },
            '.desktop-useragent #google-search-btn:hover img, .desktop-useragent #bing-search-btn:hover img': {
                'transform': 'scale(1.1)'
            }
        },

        // 업데이트 팝업 스타일 정의
        popupStyles: {
            '#marked-update-popup': {
                'position': 'fixed',
                'top': '30%',
                'left': '50%',
                'transform': 'translate(-50%, -50%)',
                'padding': `${Config.UI.DEFAULT_PADDING * 1.25}px`,
                'z-index': Config.UI.Z_INDEX,
                'border-width': '1px',
                'border-style': 'solid',
                'box-shadow': '0 2px 10px rgba(0,0,0,0.1)',
                'text-align': 'center'
            },
            '#marked-update-popup button': {
                'margin-top': `${Config.UI.DEFAULT_MARGIN * 1.25}px`,
                'padding': `${Config.UI.DEFAULT_PADDING}px ${Config.UI.DEFAULT_PADDING}px`,
                'cursor': 'pointer',
                'border-width': '1px',
                'border-style': 'solid',
                'border-radius': Config.STYLES.BORDER_RADIUS,
                'font-family': 'sans-serif'
            }
        },

        // 모바일 환경 스타일 정의
        mobileStyles: {
            '.mobile-useragent #google-search-btn, .mobile-useragent #bing-search-btn': {
                'max-width': '100%',
                'width': 'calc(100% - 16px)',
                'margin-left': `${Config.UI.DEFAULT_MARGIN}px !important`,
                'margin-right': `${Config.UI.DEFAULT_MARGIN}px !important`,
                '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.searchButtonStyles,
                this.popupStyles,
                this.mobileStyles
            ];

            // CSS 변수 정의 (root에 추가하여 테마 변경에 활용)
            const cssVariables = `
                :root {
                    --gemini-background-color: ${Config.STYLES.COLORS.BACKGROUND_LIGHT};
                    --gemini-border-color: ${Config.STYLES.COLORS.BORDER_LIGHT};
                    --gemini-text-color: ${Config.STYLES.COLORS.TEXT_LIGHT};
                    --gemini-title-color: ${Config.STYLES.COLORS.TITLE_LIGHT};
                    --gemini-button-bg: ${Config.STYLES.COLORS.BUTTON_BG_LIGHT};
                    --gemini-button-border: ${Config.STYLES.COLORS.BUTTON_BORDER_LIGHT};
                    --gemini-code-block-bg: ${Config.STYLES.COLORS.CODE_BLOCK_BG_LIGHT};
                }
                .dark-mode {
                    --gemini-background-color: ${Config.STYLES.COLORS.BACKGROUND_DARK};
                    --gemini-border-color: ${Config.STYLES.COLORS.BORDER_DARK};
                    --gemini-text-color: ${Config.STYLES.COLORS.TEXT_DARK};
                    --gemini-title-color: ${Config.STYLES.COLORS.TITLE_DARK};
                    --gemini-button-bg: ${Config.STYLES.COLORS.BUTTON_BG_DARK};
                    --gemini-button-border: ${Config.STYLES.COLORS.BUTTON_BORDER_DARK};
                    --gemini-code-block-bg: ${Config.STYLES.COLORS.CODE_BLOCK_BG_DARK};
                }
            `;

            return cssVariables + 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 = {
        currentTheme: 'light', // 초기 테마 설정

        init() {
            const savedTheme = localStorage.getItem(Config.STORAGE_KEYS.THEME_MODE);
            if (savedTheme) {
                this.currentTheme = savedTheme;
            } else {
                // 시스템 테마 감지 (선택 사항)
                if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
                    this.currentTheme = 'dark';
                }
            }
            this.applyTheme();
        },

        applyTheme() {
            if (this.currentTheme === 'dark') {
                document.documentElement.classList.add('dark-mode');
            } else {
                document.documentElement.classList.remove('dark-mode');
            }
        },

        toggleTheme() {
            this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light';
            localStorage.setItem(Config.STORAGE_KEYS.THEME_MODE, this.currentTheme);
            this.applyTheme();
            this.updateThemeToggleButtonIcon();
        },

        getThemeToggleButtonIcon() {
            return this.currentTheme === 'light' ? Config.ASSETS.DARK_MODE_ICON : Config.ASSETS.LIGHT_MODE_ICON;
        },

        updateThemeToggleButtonIcon() {
            const themeToggleButton = document.getElementById('gemini-theme-toggle-btn');
            if (themeToggleButton) {
                themeToggleButton.src = this.getThemeToggleButtonIcon();
                themeToggleButton.title = this.currentTheme === 'light' ? 'Dark Mode' : 'Light Mode';
            }
        },

        observeThemeChange() {
            // 외부 테마 변경 감지 (예: 시스템 설정 변경) - 필요에 따라 추가
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
                const newTheme = e.matches ? 'dark' : 'light';
                if (this.currentTheme !== newTheme) {
                    this.currentTheme = newTheme;
                    localStorage.setItem(Config.STORAGE_KEYS.THEME_MODE, this.currentTheme);
                    this.applyTheme();
                    this.updateThemeToggleButtonIcon();
                }
            });
        }
    };
    // 스타일 관리 모듈: 스타일 초기화 및 적용
    const Styles = {
        initStyles() {
            const styleElement = document.createElement('style');
            styleElement.id = 'bing-plus-styles';
            styleElement.textContent = StyleGenerator.generateStyles(); // CSS 문자열 생성
            document.head.appendChild(styleElement);
            // <style> 요소 추가
            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');
        },

        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 = {
        createSearchButton(query) {
            const btn = document.createElement('button');
            if (DeviceDetector.isGoogle()) {
                btn.id = 'bing-search-btn';
                btn.innerHTML = `
                    <img src="${Config.ASSETS.BING_LOGO}" alt="Bing Logo" style="width: ${Config.STYLES.SMALL_ICON_SIZE}; height: ${Config.STYLES.SMALL_ICON_SIZE}; vertical-align: middle;">
                    ${Localization.getMessage(Config.MESSAGE_KEYS.SEARCH_ON_BING)}
                `;
                btn.onclick = () => window.open(`https://www.bing.com/search?q=${encodeURIComponent(query)}`, '_blank');
            } else {
                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;
        },

        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>
                    <div style="display: flex; align-items: center;">
                        <img id="gemini-theme-toggle-btn" title="${ThemeManager.currentTheme === 'light' ? 'Dark Mode' : 'Light Mode'}" src="${ThemeManager.getThemeToggleButtonIcon()}" />
                        <img id="gemini-refresh-btn" title="Refresh" src="${Config.ASSETS.REFRESH_ICON}" />
                    </div>
                </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);
            box.querySelector('#gemini-theme-toggle-btn').onclick = () => ThemeManager.toggleTheme(); // 토글 버튼 클릭 이벤트

            if (DeviceDetector.isDesktop()) {
                VersionChecker.checkMarkedJsVersion();
            }

            return box;
        },

        createGeminiUI(query, apiKey) {
            const wrapper = document.createElement('div');
            wrapper.id = 'gemini-wrapper';
            wrapper.appendChild(this.createSearchButton(query));
            wrapper.appendChild(this.createGeminiBox(query, apiKey));
            return wrapper;
        },

        removeExistingElements() {
            document.querySelectorAll('#gemini-wrapper, #google-search-btn, #bing-search-btn').forEach(el => el.remove());
        },

        createRHSIfNeeded() {
            if (DeviceDetector.isGoogle() && !document.getElementById('rhs')) {
                const mainContent = document.getElementById('rcnt');
                if (mainContent) {
                    const rhsDiv = document.createElement('div');
                    rhsDiv.id = 'rhs';
                    rhsDiv.setAttribute('jsname', 'Iclw3');
                    rhsDiv.style.cssText = `
                        float: right;
                        padding-left: 16px;
                        width: 432px;
                        margin-top: 20px;
                    `;
                    mainContent.appendChild(rhsDiv);
                }
            }
        }
    };
    // Gemini API 모듈: Gemini API 호출 및 응답 처리
    const GeminiAPI = {
        fetch(query, container, apiKey, force = false) {
            console.log('GeminiAPI.fetch 호출됨. 쿼리:', { query, apiKey, force });
            const cacheKey = `${Config.CACHE.PREFIX}${query}`;
            const cached = force ? null : sessionStorage.getItem(cacheKey);
            if (cached) {
                console.log('캐시된 응답 사용:', query);
                if (container) container.innerHTML = marked.parse(cached);
                return;
            }

            if (!apiKey) {
                console.error('Gemini API 키가 누락되었습니다!');
                if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.ENTER_API_KEY);
                return;
            }

            if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.LOADING);
            const promptText = Localization.getMessage(Config.MESSAGE_KEYS.PROMPT, { query });
            console.log('Gemini API 요청 프롬프트:', promptText);
            console.log('API URL:', `${Config.API.GEMINI_URL}${Config.API.GEMINI_MODEL}:generateContent?key=${apiKey}`);
            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: promptText }] }]
                }),
                onload({ status, responseText }) {
                    console.log('GM_xmlhttpRequest onload. 상태:', status, '응답 텍스트 (일부):', responseText.substring(0, 200));
                    try {
                        const parsedResponse = JSON.parse(responseText);
                        const text = parsedResponse?.candidates?.[0]?.content?.parts?.[0]?.text;
                        if (text) {
                            sessionStorage.setItem(cacheKey, text);
                            if (container) container.innerHTML = marked.parse(text);
                            console.log('Gemini 응답 성공적으로 파싱 및 표시됨.');
                        } else {
                            if (container) {
                                if (parsedResponse.error) {
                                    container.textContent = `❌ Gemini API 오류: ${parsedResponse.error.message ||
                                        JSON.stringify(parsedResponse.error)}`;
                                    console.error('Gemini API에서 오류를 반환했습니다:', parsedResponse.error);
                                } else {
                                    container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.GEMINI_EMPTY);
                                    console.warn('Gemini 응답이 비어있거나 예상된 텍스트를 포함하지 않습니다.');
                                }
                            }
                        }
                    } catch (e) {
                        if (container) container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.PARSE_ERROR)} ${e.message}`;
                        console.error('Gemini API 응답 파싱 오류:', e, '응답 텍스트:', responseText);
                    }
                },
                onerror(err) {
                    if (container) container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.NETWORK_ERROR)} ${err.finalUrl ||
                        err.statusText || JSON.stringify(err)}`;
                    console.error('GM_xmlhttpRequest onerror:', err);
                },
                ontimeout() {
                    if (container) container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.TIMEOUT);
                    console.warn('GM_xmlhttpRequest ontimeout 쿼리:', query);
                }
            });
        }
    };

    // 링크 정리 모듈: 중간 URL 제거
    const LinkCleaner = {
        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;
            }
        },

        resolveRealUrl(url) {
            const rules = [
                { pattern: /bing\.com\/(ck\/a|aclick)/, key: 'u' },
                { pattern: /so\.com\/search\/eclk/, key: 'aurl' },
                { pattern: /google\.com\/url/, key: 'url' }
            ];
            for (const { pattern, key } of rules) {
                if (pattern.test(url)) {
                    const real = this.decodeRealUrl(url, key);
                    if (real && real !== url) return real;
                }
            }
            return 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;
        },

        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 버전 확인 오류:', e.message);
                    }
                },
                onerror: () => console.warn('marked.min.js 버전 확인 요청 실패')
            });
        }
    };

    // 이벤트 핸들러 모듈: URL 및 DOM 변경 감지
    const EventHandler = {
        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,
        geminiBoxExists: false,

        startRendering() {
            if (this.isRendering) return false;
            this.isRendering = true;
            return true;
        },

        finishRendering() {
            this.isRendering = false;
        },

        maintainGeminiBoxPosition(wrapper) {
            const existingGeminiWrapper = document.getElementById('gemini-wrapper');
            if (existingGeminiWrapper) {
                existingGeminiWrapper.remove();
            }

            if (DeviceDetector.isGoogle()) {
                UI.createRHSIfNeeded();
                const rhsTarget = document.getElementById('rhs');
                if (rhsTarget) {
                    rhsTarget.prepend(wrapper);
                    this.geminiBoxExists = true;
                } else {
                    console.warn('Google: #rhs 요소가 생성 시도 후에도 발견되지 않았습니다.');
                    this.geminiBoxExists = false;
                }
            } else if (DeviceDetector.isBing()) {
                const bingContextTarget = document.getElementById('b_context') ||
                    document.querySelector('.b_right');
                if (bingContextTarget) {
                    bingContextTarget.prepend(wrapper);
                    this.geminiBoxExists = true;
                } else {
                    console.warn('Bing: #b_context 또는 .b_right 요소가 발견되지 않았습니다.');
                    this.geminiBoxExists = false;
                }
            }
        }
    };
    // UI 렌더링 모듈: UI 렌더링 로직
    const UIRenderer = {
        renderDesktop(query, apiKey) {
            const wrapper = UI.createGeminiUI(query, apiKey);
            RenderState.maintainGeminiBoxPosition(wrapper);
            if (RenderState.geminiBoxExists) {
                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;
            }
            RenderState.finishRendering();
            return false;
        },

        renderMobile(query) {
            const contentTarget = document.getElementById('b_content') ||
                document.getElementById('main');
            if (!contentTarget) {
                RenderState.finishRendering();
                return false;
            }

            requestAnimationFrame(() => {
                const searchBtn = UI.createSearchButton(query);
                if (contentTarget.parentNode) {
                    contentTarget.parentNode.style.overflow = 'visible';
                    contentTarget.parentNode.style.position = 'relative';
                    contentTarget.parentNode.insertBefore(searchBtn, contentTarget);
                } else {
                    document.body.prepend(searchBtn);
                }
                RenderState.finishRendering();
            });
            return true;
        },

        renderTablet() {
            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 = () => {
                ThemeManager.init(); // 테마 관리 초기화
                Styles.initStyles();
                LinkCleaner.convertLinksToReal(document);

                const checkAndRender = () => {
                    const targetElement = document.getElementById('rhs') ||
                        document.getElementById('b_context') || document.querySelector('.b_right');
                    if (targetElement || DeviceDetector.isMobile() || DeviceDetector.isTablet()) {
                        UIRenderer.render();
                    } else {
                        if (DeviceDetector.isGoogle()) {
                            UI.createRHSIfNeeded();
                            setTimeout(checkAndRender, 100);
                        } else {
                            setTimeout(checkAndRender, 100);
                        }
                    }
                };
                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();
})();