Greasy Fork

Greasy Fork is available in English.

Bing Plus

Bing 검색 결과 옆에 Gemini 응답을 표시하고 중간 URL을 제거하여 검색 속도를 향상시킵니다.

当前为 2025-04-24 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bing Plus
// @version      5.6
// @description  Bing 검색 결과 옆에 Gemini 응답을 표시하고 중간 URL을 제거하여 검색 속도를 향상시킵니다.
// @author       lanpod
// @match        https://www.bing.com/search*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @require      https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js
// @license      MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    // 설정 모듈: API, 스타일, 메시지 등 전역 설정값 관리
    const Config = {
        API: {
            GEMINI_MODEL: 'gemini-2.0-flash',
            GEMINI_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
            MARKED_CDN_URL: 'https://api.cdnjs.com/libraries/marked'
        },
        VERSIONS: {
            MARKED_VERSION: '15.0.7' // 버전을 한 곳에서 관리
        },
        CACHE: {
            PREFIX: 'gemini_cache_'
        },
        STORAGE_KEYS: {
            CURRENT_VERSION: 'markedCurrentVersion',
            LATEST_VERSION: 'markedLatestVersion',
            LAST_NOTIFIED: 'markedLastNotifiedVersion'
        },
        UI: {
            DEFAULT_MARGIN: 8,
            DEFAULT_PADDING: 16,
            Z_INDEX: 9999
        },
        STYLES: {
            COLORS: {
                BACKGROUND: '#fff',
                BORDER: '#e0e0e0',
                TEXT: '#000',
                TITLE: '#000',
                BUTTON_BG: '#f0f3ff',
                BUTTON_BORDER: '#ccc',
                DARK_BACKGROUND: '#202124',
                DARK_BORDER: '#5f6368',
                DARK_TEXT: '#fff',
                CODE_BLOCK_BG: '#f0f0f0',
                DARK_CODE_BLOCK_BG: '#555'
            },
            BORDER: '1px solid #e0e0e0',
            BORDER_RADIUS: '4px',
            FONT_SIZE: {
                TEXT: '14px',
                TITLE: '18px'
            },
            ICON_SIZE: '20px',
            LOGO_SIZE: '24px',
            SMALL_ICON_SIZE: '16px'
        },
        ASSETS: {
            GOOGLE_LOGO: 'https://www.gstatic.com/marketing-cms/assets/images/bc/1a/a310779347afa1927672dc66a98d/g.png=s48-fcrop64=1,00000000ffffffff-rw',
            GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg',
            REFRESH_ICON: 'https://www.svgrepo.com/show/533704/refresh-cw-alt-3.svg'
        },
        MESSAGE_KEYS: {
            PROMPT: 'prompt',
            ENTER_API_KEY: 'enterApiKey',
            GEMINI_EMPTY: 'geminiEmpty',
            PARSE_ERROR: 'parseError',
            NETWORK_ERROR: 'networkError',
            TIMEOUT: 'timeout',
            LOADING: 'loading',
            UPDATE_TITLE: 'updateTitle',
            UPDATE_NOW: 'updateNow',
            SEARCH_ON_GOOGLE: 'searchongoogle'
        }
    };

    // 지역화 모듈: 다국어 메시지 처리
    const Localization = {
        MESSAGES: {
            [Config.MESSAGE_KEYS.PROMPT]: {
                ko: `"${'${query}'}"에 대한 정보를 찾아줘`,
                zh: `请以标记格式填写有关\"${'${query}'}\"的信息。`,
                default: `Please write information about \"${'${query}'}\" in markdown format`
            },
            [Config.MESSAGE_KEYS.ENTER_API_KEY]: {
                ko: 'Gemini API 키를 입력하세요:',
                zh: '请输入 Gemini API 密钥:',
                default: 'Please enter your Gemini API key:'
            },
            [Config.MESSAGE_KEYS.GEMINI_EMPTY]: {
                ko: '⚠️ Gemini 응답이 비어있습니다.',
                zh: '⚠️ Gemini 返回为空。',
                default: '⚠️ Gemini response is empty.'
            },
            [Config.MESSAGE_KEYS.PARSE_ERROR]: {
                ko: '❌ 파싱 오류:',
                zh: '❌ 解析错误:',
                default: '❌ Parsing error:'
            },
            [Config.MESSAGE_KEYS.NETWORK_ERROR]: {
                ko: '❌ 네트워크 오류:',
                zh: '❌ 网络错误:',
                default: '❌ Network error:'
            },
            [Config.MESSAGE_KEYS.TIMEOUT]: {
                ko: '❌ 요청 시간이 초과되었습니다.',
                zh: '❌ 请求超时。',
                default: '❌ Request timeout'
            },
            [Config.MESSAGE_KEYS.LOADING]: {
                ko: '불러오는 중...',
                zh: '加载中...',
                default: 'Loading...'
            },
            [Config.MESSAGE_KEYS.UPDATE_TITLE]: {
                ko: 'marked.min.js 업데이트 필요',
                zh: '需要更新 marked.min.js',
                default: 'marked.min.js update required'
            },
            [Config.MESSAGE_KEYS.UPDATE_NOW]: {
                ko: '확인',
                zh: '确认',
                default: 'OK'
            },
            [Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE]: {
                ko: 'Google 에서 검색하기',
                zh: '在 Google 上搜索',
                default: 'Search on Google'
            }
        },
        // 메시지를 언어에 맞게 반환
        getMessage(key, vars = {}) {
            const lang = navigator.language;
            const langKey = lang.includes('ko') ? 'ko' : lang.includes('zh') ? 'zh' : 'default';
            const template = this.MESSAGES[key]?.[langKey] || this.MESSAGES[key]?.default || '';
            return template.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || '');
        }
    };

    // 스타일 모듈: 스타일 생성 및 테마 관리
    const Styles = {
        // 스타일 동적으로 생성
        generateStyles() {
            return `
                /* 광고 링크 초록색으로 설정 */
                #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 박스 스타일 */
                #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;
                }

                /* 라이트 테마 스타일 */
                [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;
                    }
                }

                [data-theme="dark"] #gemini-box h3,
                .dark #gemini-box h3,
                .b_dark #gemini-box h3 {
                    color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
                }

                @media (prefers-color-scheme: dark) {
                    #gemini-box h3 {
                        color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
                    }
                }

                [data-theme="dark"] #gemini-content,
                [data-theme="dark"] #gemini-content *,
                .dark #gemini-content,
                .dark #gemini-content *,
                .b_dark #gemini-content,
                .b_dark #gemini-content * {
                    color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
                    background: transparent !important;
                }

                @media (prefers-color-scheme: dark) {
                    #gemini-content,
                    #gemini-content * {
                        color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
                        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;
                }

                @media (prefers-color-scheme: dark) {
                    #gemini-content pre {
                        background: ${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important;
                    }
                }

                [data-theme="dark"] #gemini-divider,
                .dark #gemini-divider,
                .b_dark #gemini-divider {
                    background: ${Config.STYLES.COLORS.DARK_BORDER} !important;
                }

                @media (prefers-color-scheme: dark) {
                    #gemini-divider {
                        background: ${Config.STYLES.COLORS.DARK_BORDER} !important;
                    }
                }

                /* Gemini UI 헤더 스타일 */
                #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;
                }

                #gemini-content {
                    font-size: ${Config.STYLES.FONT_SIZE.TEXT};
                    line-height: 1.6;
                    white-space: pre-wrap;
                    word-wrap: break-word;
                    background: transparent !important;
                }

                /* Google 검색 버튼 스타일 */
                #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;
                }

                /* 데스크톱 환경에서만 호버 효과 적용 */
                @media (hover: hover) and (pointer: fine) {
                    #google-search-btn:hover {
                        transform: scale(1.1);
                    }
                    #google-search-btn:hover img {
                        transform: scale(1.1);
                    }
                }

                /* 업데이트 팝업 스타일 */
                #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;
                }

                @media (prefers-color-scheme: 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;
                }

                /* 모바일 반응형 스타일 */
                @media (max-width: 768px) {
                    #google-search-btn {
                        max-width: 96%;
                        margin: ${Config.UI.DEFAULT_MARGIN}px auto;
                        padding: ${Config.UI.DEFAULT_PADDING * 0.75}px;
                        border-radius: 16px;
                    }
                    #gemini-box {
                        padding: ${Config.UI.DEFAULT_PADDING * 0.75}px;
                        border-radius: 16px;
                    }
                }
            `;
        },
        // 스타일 초기화 및 적용
        initStyles() {
            const styleElement = document.createElement('style');
            styleElement.id = 'bing-plus-styles';
            styleElement.textContent = this.generateStyles();
            document.head.appendChild(styleElement);
        },
        // 현재 테마 감지 및 적용
        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;
            }
        }
    };

    // 유틸리티 모듈: 공통 유틸리티 함수
    const Utils = {
        // 캐싱 변수
        _isDesktop: null,
        _isGeminiAvailable: null,
        // 데스크톱 환경인지 확인 (UserAgent 기준으로만 판단)
        isDesktop() {
            if (this._isDesktop === null) {
                this._isDesktop = !/Mobi|Android|iPhone|iPad/i.test(navigator.userAgent);
            }
            return this._isDesktop;
        },
        // Gemini UI를 표시할 수 있는 환경인지 확인
        isGeminiAvailable() {
            if (this._isGeminiAvailable === null) {
                const hasBContext = !!document.getElementById('b_context');
                const hasBRight = !!document.querySelector('.b_right');
                this._isGeminiAvailable = this.isDesktop() && (hasBContext || hasBRight);
            }
            return this._isGeminiAvailable;
        },
        // 검색 쿼리 추출
        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 모듈: UI 요소 생성
    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);

            // 데스크톱 환경에서만 marked.js 버전 체크
            if (Utils.isDesktop()) {
                VersionChecker.checkMarkedJsVersion();
            }

            return box;
        },
        // Gemini UI 전체 생성 (버튼 + 박스)
        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;
        }
    };

    // 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);

            // VersionChecker를 변수로 저장하여 콜백 내에서 사용
            const self = this;

            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);
                        console.log(`Current version: ${Config.VERSIONS.MARKED_VERSION}, Latest version: ${latest}`); // 디버깅 로그
                        console.log(`Compare result: ${self.compareVersions(Config.VERSIONS.MARKED_VERSION, latest)}`); // 비교 결과 로그

                        const lastNotified = localStorage.getItem(Config.STORAGE_KEYS.LAST_NOTIFIED);
                        console.log(`Last notified: ${lastNotified}`); // lastNotified 값 확인

                        if (self.compareVersions(Config.VERSIONS.MARKED_VERSION, latest) < 0 &&
                            (!lastNotified || self.compareVersions(lastNotified, latest) < 0)) {
                            console.log('Popup should be displayed'); // 팝업 표시 조건 충족 여부 확인
                            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);
                        } else {
                            console.log('Popup not displayed due to version or lastNotified condition');
                        }
                    } catch (e) {
                        console.warn('marked.min.js version check error:', e.message);
                    }
                },
                onerror: () => console.warn('marked.min.js version check request failed')
            });
        }
    };

    // 메인 모듈: 전체 기능 초기화 및 관리
    const Main = {
        isRendering: false, // 렌더링 중복 방지 플래그

        // Gemini UI 렌더링
        renderGemini() {
            if (this.isRendering) return; // 중복 렌더링 방지
            this.isRendering = true;

            const query = Utils.getQuery();
            if (!query) {
                this.isRendering = false;
                return;
            }

            // 기존 UI 요소 제거
            const existingElements = document.querySelectorAll('#gemini-wrapper, #google-search-btn');
            existingElements.forEach(el => el.remove());

            if (Utils.isDesktop()) {
                // 데스크톱 환경: Gemini UI 표시
                if (!Utils.isGeminiAvailable()) {
                    this.isRendering = false;
                    return;
                }

                const apiKey = Utils.getApiKey();
                if (!apiKey) {
                    this.isRendering = false;
                    return;
                }

                const contextTarget = document.getElementById('b_context') || document.querySelector('.b_right');
                if (!contextTarget) {
                    this.isRendering = false;
                    return;
                }

                // UI 렌더링
                requestAnimationFrame(() => {
                    const wrapper = UI.createGeminiUI(query, apiKey);
                    contextTarget.prepend(wrapper);

                    // Gemini 응답 비동기 로드
                    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));
                            }
                        }
                        this.isRendering = false;
                    });
                });
            } else {
                // 모바일 환경: Google 검색 버튼만 표시
                const contentTarget = document.getElementById('b_content');
                if (!contentTarget) {
                    this.isRendering = false;
                    return;
                }

                requestAnimationFrame(() => {
                    const googleBtn = UI.createGoogleButton(query);
                    contentTarget.parentNode.insertBefore(googleBtn, contentTarget);
                    this.isRendering = false;
                });
            }
        },

        // URL 변경 감지
        observeUrlChange() {
            let lastUrl = location.href;

            const checkUrlChange = () => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    this.renderGemini();
                    LinkCleaner.convertLinksToReal(document);
                }
            };

            // History API 이벤트 감지
            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);

            // DOM 변경 감지 (title 태그만 감시)
            const observer = new MutationObserver(checkUrlChange);
            const targetNode = document.querySelector('head > title') || document.body;
            observer.observe(targetNode, { childList: true, subtree: true });
        },

        // 테마 변경 감지
        observeThemeChange() {
            const observer = new MutationObserver(() => {
                Styles.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', () => {
                Styles.applyTheme();
            });
        },

        // 초기화 함수
        init() {
            const initialize = () => {
                Styles.initStyles(); // 스타일 초기화
                Styles.applyTheme(); // 테마 적용
                LinkCleaner.convertLinksToReal(document); // 링크 정리
                this.renderGemini(); // Gemini UI 렌더링
                this.observeUrlChange(); // URL 변경 감지
                this.observeThemeChange(); // 테마 변경 감지
            };

            if (document.readyState === 'complete' || document.readyState === 'interactive') {
                setTimeout(initialize, 1);
            } else {
                document.addEventListener('DOMContentLoaded', initialize);
            }
        }
    };

    // 스크립트 실행
    Main.init();
})();