Google Plus & Bing Plus

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

// ==UserScript==
// @name         Google Plus & Bing Plus
// @version      7.0.3
// @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.google.com/favicon.ico',
            BING_LOGO: 'https://account.microsoft.com/favicon.ico',
            GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png',
            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();
})();