Greasy Fork

Greasy Fork is available in English.

Bing Plus

Display Gemini response results next to Bing search results and speed up searches by eliminating intermediate URLs.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bing Plus
// @version      4.0
// @description  Display Gemini response results next to Bing search results and speed up searches by eliminating intermediate URLs.
// @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';

    // 설정 모듈: 스크립트 전반에 걸쳐 사용되는 상수와 설정 값을 중앙에서 관리
    const Config = {
        // API 관련 설정: Gemini API와 외부 리소스에 접근하기 위한 URL 및 모델 정보
        API: {
            GEMINI_MODEL: 'gemini-2.0-flash', // 사용할 Gemini 모델 이름
            GEMINI_URL: 'https://generativelanguage.googleapis.com/v1beta/models/', // Gemini API 기본 URL
            MARKED_CDN_URL: 'https://api.cdnjs.com/libraries/marked' // marked.js 라이브러리 버전 확인용 CDN URL
        },

        // 버전 및 캐시 설정: 외부 라이브러리 버전과 캐시 접두사 관리
        VERSIONS: {
            MARKED_VERSION: '15.0.7' // 현재 사용하는 marked.js 버전
        },
        CACHE: {
            PREFIX: 'gemini_cache_' // Gemini API 응답 캐싱 시 사용할 키 접두사
        },
        STORAGE_KEYS: {
            CURRENT_VERSION: 'markedCurrentVersion', // 현재 marked.js 버전 저장 키
            LATEST_VERSION: 'markedLatestVersion', // 최신 marked.js 버전 저장 키
            LAST_NOTIFIED: 'markedLastNotifiedVersion' // 마지막으로 사용자에게 알림을 표시한 버전 저장 키
        },

        // UI 관련 설정: Gemini UI 요소의 크기와 여백 등 관리
        UI: {
            GEMINI_WIDTH: 400, // Gemini 박스 너비 (단위: px)
            DEFAULT_MARGIN: 8, // 기본 여백 크기 (단위: px)
            DEFAULT_PADDING: 16, // 기본 패딩 크기 (단위: px)
            Z_INDEX: 9999 // 팝업 및 UI 요소의 z-index 값
        },

        // 스타일 관련 설정: 색상, 폰트 크기, 테두리 등 스타일 속성 관리
        STYLES: {
            COLORS: {
                BACKGROUND: '#fff', // Gemini 박스 배경색
                BORDER: '#e0e0e0', // 테두리 색상
                TEXT: '#333', // 기본 텍스트 색상
                TITLE: '#202124', // 제목 텍스트 색상
                BUTTON_BG: '#f0f3ff', // 버튼 배경색
                CODE_BG: '#f5f5f5', // 코드 블록 배경색
                BUTTON_BORDER: '#ccc' // 버튼 테두리 색상
            },
            BORDER: '1px solid #e0e0e0', // 기본 테두리 스타일
            BORDER_RADIUS: '4px', // 테두리 둥글기 반경
            FONT_SIZE: {
                TEXT: '14px', // 기본 텍스트 크기
                TITLE: '18px' // 제목 텍스트 크기
            },
            ICON_SIZE: '20px', // 새로고침 아이콘 크기
            LOGO_SIZE: '24px', // 로고 이미지 크기
            SMALL_ICON_SIZE: '16px' // Google 버튼 내 작은 아이콘 크기
        },

        // 아이콘 및 이미지 URL: UI에 사용할 이미지 리소스 경로
        ASSETS: {
            GOOGLE_LOGO: 'https://www.gstatic.com/marketing-cms/assets/images/bc/1a/a310779347afa1927672dc66a98d/g.png=s48-fcrop64=1,00000000ffffffff-rw', // Google 로고 이미지 URL
            GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg', // Gemini 로고 이미지 URL
            REFRESH_ICON: 'https://www.svgrepo.com/show/533704/refresh-cw-alt-3.svg' // 새로고침 아이콘 이미지 URL
        },

        // 로컬라이제이션 메시지 키: 지역화 메시지 접근 시 사용할 키
        MESSAGE_KEYS: {
            PROMPT: 'prompt', // Gemini API 요청 프롬프트 메시지 키
            ENTER_API_KEY: 'enterApiKey', // API 키 입력 요청 메시지 키
            GEMINI_EMPTY: 'geminiEmpty', // Gemini 응답이 비었을 때 메시지 키
            PARSE_ERROR: 'parseError', // JSON 파싱 오류 메시지 키
            NETWORK_ERROR: 'networkError', // 네트워크 오류 메시지 키
            TIMEOUT: 'timeout', // 요청 시간 초과 메시지 키
            LOADING: 'loading', // 로딩 중 메시지 키
            UPDATE_TITLE: 'updateTitle', // marked.js 업데이트 알림 제목 메시지 키
            UPDATE_NOW: 'updateNow', // 업데이트 확인 버튼 메시지 키
            SEARCH_ON_GOOGLE: 'searchongoogle' // Google 검색 버튼 메시지 키
        }
    };

    // 지역화 모듈: 다국어 지원을 위한 메시지 관리 및 반환
    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'
            }
        },

        // 사용자의 언어에 따라 적절한 메시지를 반환
        // @param {string} key - 메시지 키 (Config.MESSAGE_KEYS에서 참조)
        // @param {Object} vars - 메시지 내 변수 치환용 객체 (예: { query: '검색어' })
        // @returns {string} - 사용자의 언어에 맞는 메시지 문자열
        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] || ''); // 변수 치환
        }
    };

    // 스타일 모듈: 페이지에 CSS 스타일을 동적으로 삽입
    const Styles = {
        // CSS 스타일을 정의하고 페이지에 삽입 (GM_addStyle 사용)
        // Gemini UI, Google 검색 버튼, 버전 업데이트 팝업 등의 스타일 포함
        inject() {
            console.log('Injecting styles...'); // 스타일 삽입 시작 로그
            GM_addStyle(`
                #b_results > li.b_ad a { color: green !important; } /* Bing 광고 링크를 초록색으로 표시 */

                #gemini-box { /* Gemini 결과 박스 스타일 */
                    max-width: ${Config.UI.GEMINI_WIDTH}px; /* 최대 너비 */
                    width: ${Config.UI.GEMINI_WIDTH}px; /* 너비 */
                    min-width: ${Config.UI.GEMINI_WIDTH}px; /* 최소 너비 */
                    background: ${Config.STYLES.COLORS.BACKGROUND}; /* 배경색 */
                    border: ${Config.STYLES.BORDER}; /* 테두리 스타일 */
                    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; /* 테두리와 패딩 포함한 크기 계산 */
                }

                #gemini-header { /* Gemini 박스 헤더 스타일 */
                    display: flex; /* 플렉스 레이아웃 */
                    align-items: center; /* 세로 중앙 정렬 */
                    justify-content: space-between; /* 양쪽 끝 정렬 */
                    margin-bottom: ${Config.UI.DEFAULT_MARGIN}px; /* 하단 여백 */
                }

                #gemini-title-wrap { /* Gemini 제목과 로고를 감싸는 컨테이너 */
                    display: flex; /* 플렉스 레이아웃 */
                    align-items: center; /* 세로 중앙 정렬 */
                }

                #gemini-logo { /* Gemini 로고 스타일 */
                    width: ${Config.STYLES.LOGO_SIZE}; /* 로고 너비 */
                    height: ${Config.STYLES.LOGO_SIZE}; /* 로고 높이 */
                    margin-right: ${Config.UI.DEFAULT_MARGIN}px; /* 오른쪽 여백 */
                }

                #gemini-box h3 { /* Gemini 박스 제목 스타일 */
                    margin: 0; /* 기본 여백 제거 */
                    font-size: ${Config.STYLES.FONT_SIZE.TITLE}; /* 폰트 크기 */
                    color: ${Config.STYLES.COLORS.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); /* 360도 회전 */
                }

                #gemini-divider { /* 구분선 스타일 */
                    height: 1px; /* 높이 */
                    background: ${Config.STYLES.COLORS.BORDER}; /* 배경색 (테두리 색상과 동일) */
                    margin: ${Config.UI.DEFAULT_MARGIN}px 0; /* 상하 여백 */
                }

                #gemini-content { /* Gemini 콘텐츠 스타일 */
                    font-size: ${Config.STYLES.FONT_SIZE.TEXT}; /* 폰트 크기 */
                    line-height: 1.6; /* 줄 간격 */
                    color: ${Config.STYLES.COLORS.TEXT}; /* 텍스트 색상 */
                    white-space: pre-wrap; /* 줄바꿈 유지 */
                    word-wrap: break-word; /* 긴 단어 줄바꿈 */
                }

                #gemini-content pre { /* 코드 블록 스타일 */
                    background: ${Config.STYLES.COLORS.CODE_BG}; /* 배경색 */
                    padding: ${Config.UI.DEFAULT_MARGIN + 2}px; /* 내부 여백 */
                    border-radius: ${Config.STYLES.BORDER_RADIUS}; /* 테두리 둥글기 */
                    overflow-x: auto; /* 가로 스크롤 허용 */
                }

                #google-search-btn { /* Google 검색 버튼 스타일 */
                    width: ${Config.UI.GEMINI_WIDTH}px; /* 버튼 너비 */
                    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; /* 아이콘과 텍스트 간격 */
                }

                #google-search-btn img { /* Google 버튼 내 이미지 스타일 */
                    width: ${Config.STYLES.SMALL_ICON_SIZE}; /* 이미지 너비 */
                    height: ${Config.STYLES.SMALL_ICON_SIZE}; /* 이미지 높이 */
                    vertical-align: middle; /* 세로 중앙 정렬 */
                }

                #marked-update-popup { /* 버전 업데이트 팝업 스타일 */
                    position: fixed; /* 고정 위치 */
                    top: 30%; /* 상단에서 30% 위치 */
                    left: 50%; /* 좌측에서 50% 위치 */
                    transform: translate(-50%, -50%); /* 중앙 정렬 */
                    background: ${Config.STYLES.COLORS.BACKGROUND}; /* 배경색 */
                    padding: ${Config.UI.DEFAULT_PADDING * 1.25}px; /* 내부 여백 */
                    z-index: ${Config.UI.Z_INDEX}; /* z-index 값 */
                    border: 1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}; /* 테두리 스타일 */
                    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_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; /* 폰트 설정 */
                }
            `);
            console.log('Styles injected'); // 스타일 삽입 완료 로그
        }
    };

    // 유틸리티 모듈: 자주 사용하는 도구 함수 모음
    const Utils = {
        // 디바이스가 데스크톱인지 확인
        // 화면 너비가 768px 이상이고, 모바일 디바이스가 아닌 경우 true 반환
        // @returns {boolean} - 데스크톱 여부
        isDesktop() {
            const isDesktop = window.innerWidth > 768 && !/Mobi|Android/i.test(navigator.userAgent); // 너비 및 유저 에이전트 확인
            console.log('isDesktop:', { width: window.innerWidth, userAgent: navigator.userAgent, result: isDesktop }); // 디버깅 로그
            return isDesktop;
        },

        // Gemini UI를 표시할 수 있는 환경인지 확인
        // 데스크톱 환경이고 Bing 페이지의 b_context 요소가 존재하는 경우 true 반환
        // @returns {boolean} - Gemini UI 표시 가능 여부
        isGeminiAvailable() {
            const hasBContext = !!document.getElementById('b_context'); // b_context 요소 존재 여부
            console.log('Bing isGeminiAvailable:', { isDesktop: this.isDesktop(), hasBContext }); // 디버깅 로그
            return this.isDesktop() && hasBContext;
        },

        // URL 파라미터에서 검색 쿼리를 추출
        // @returns {string|null} - 검색 쿼리 문자열 또는 null
        getQuery() {
            const query = new URLSearchParams(location.search).get('q'); // URL에서 'q' 파라미터 추출
            console.log('getQuery:', { query, search: location.search }); // 디버깅 로그
            return query;
        },

        // Gemini API 키를 로컬 스토리지에서 가져오거나 사용자 입력으로 획득
        // @returns {string|null} - API 키 문자열 또는 null
        getApiKey() {
            let key = localStorage.getItem('geminiApiKey'); // 로컬 스토리지에서 API 키 확인
            if (!key) { // API 키가 없으면 사용자 입력 요청
                key = prompt(Localization.getMessage(Config.MESSAGE_KEYS.ENTER_API_KEY)); // 사용자 입력 프롬프트
                if (key) localStorage.setItem('geminiApiKey', key); // 입력된 키 저장
                console.log('API key:', key ? 'stored' : 'prompt failed'); // 디버깅 로그
            } else {
                console.log('API key retrieved'); // API 키가 이미 존재하는 경우 로그
            }
            return key;
        }
    };

    // UI 모듈: Gemini UI와 Google 검색 버튼 생성 및 관리
    const UI = {
        // Google 검색 버튼 생성
        // 클릭 시 동일한 검색 쿼리로 Google 검색 페이지로 이동
        // @param {string} query - 검색 쿼리 문자열
        // @returns {HTMLElement} - 생성된 버튼 요소
        createGoogleButton(query) {
            const btn = document.createElement('button'); // 버튼 요소 생성
            btn.id = 'google-search-btn'; // 버튼 ID 설정
            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'); // 클릭 이벤트: Google 검색 페이지로 이동
            return btn;
        },

        // Gemini 결과 박스 생성
        // 헤더, 로고, 새로고침 버튼, 콘텐츠 영역 포함
        // @param {string} query - 검색 쿼리 문자열
        // @param {string} apiKey - Gemini API 키
        // @returns {HTMLElement} - 생성된 Gemini 박스 요소
        createGeminiBox(query, apiKey) {
            const box = document.createElement('div'); // 박스 요소 생성
            box.id = 'gemini-box'; // 박스 ID 설정
            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); // 새로고침 버튼 클릭 이벤트
            return box;
        },

        // 전체 Gemini UI 생성 (Google 버튼 + Gemini 박스)
        // @param {string} query - 검색 쿼리 문자열
        // @param {string} apiKey - Gemini API 키
        // @returns {HTMLElement} - 생성된 UI 래퍼 요소
        createGeminiUI(query, apiKey) {
            const wrapper = document.createElement('div'); // UI 래퍼 요소 생성
            wrapper.appendChild(this.createGoogleButton(query)); // Google 버튼 추가
            wrapper.appendChild(this.createGeminiBox(query, apiKey)); // Gemini 박스 추가
            console.log('Gemini UI created:', { query, hasApiKey: !!apiKey }); // 디버깅 로그
            return wrapper;
        }
    };

    // Gemini API 모듈: Gemini API 요청 및 응답 처리
    const GeminiAPI = {
        // Gemini API를 호출하여 검색 결과를 가져오고 콘텐츠를 업데이트
        // @param {string} query - 검색 쿼리 문자열
        // @param {HTMLElement} container - 콘텐츠를 표시할 요소
        // @param {string} apiKey - Gemini API 키
        // @param {boolean} force - 캐시 무시하고 새로 요청할지 여부 (기본값: false)
        fetch(query, container, apiKey, force = false) {
            console.log('Fetching Gemini API:', { query, force }); // API 요청 시작 로그
            VersionChecker.checkMarkedJsVersion(); // marked.js 버전 확인 호출

            const cacheKey = `${Config.CACHE.PREFIX}${query}`; // 캐시 키 생성
            if (!force) { // 강제 새로고침이 아닌 경우 캐시 확인
                const cached = sessionStorage.getItem(cacheKey); // 캐시에서 데이터 조회
                if (cached) {
                    container.innerHTML = marked.parse(cached); // 캐시 데이터로 콘텐츠 업데이트
                    console.log('Loaded from cache:', { query }); // 캐시 로드 로그
                    return;
                }
            }

            container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.LOADING); // 로딩 메시지 표시

            // Gemini API 요청
            GM_xmlhttpRequest({
                method: 'POST', // POST 요청
                url: `${Config.API.GEMINI_URL}${Config.API.GEMINI_MODEL}:generateContent?key=${apiKey}`, // API 엔드포인트
                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); // 캐시에 저장
                            container.innerHTML = marked.parse(text); // 마크다운 파싱 후 콘텐츠 업데이트
                            console.log('Gemini API success:', { query }); // 성공 로그
                        } else {
                            container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.GEMINI_EMPTY); // 응답이 비었을 경우 메시지 표시
                            console.log('Gemini API empty response'); // 비어있는 응답 로그
                        }
                    } catch (e) {
                        container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.PARSE_ERROR)} ${e.message}`; // 파싱 오류 메시지 표시
                        console.error('Gemini API parse error:', e.message); // 오류 로그
                    }
                },
                onerror: err => { // 네트워크 오류 시
                    container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.NETWORK_ERROR)} ${err.finalUrl}`; // 오류 메시지 표시
                    console.error('Gemini API network error:', err); // 오류 로그
                },
                ontimeout: () => { // 타임아웃 시
                    container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.TIMEOUT); // 타임아웃 메시지 표시
                    console.error('Gemini API timeout'); // 타임아웃 로그
                }
            });
        }
    };

    // 링크 정리 모듈: Bing 페이지의 추적 링크를 실제 URL로 변환
    const LinkCleaner = {
        // URL 파라미터를 디코딩하여 실제 목적지 URL 추출
        // @param {string} url - 디코딩할 URL
        // @param {string} key - 파라미터 키 (예: 'u', 'aurl')
        // @returns {string|null} - 디코딩된 URL 또는 null
        decodeRealUrl(url, key) {
            const param = new URL(url).searchParams.get(key)?.replace(/^a1/, ''); // 파라미터 추출 및 'a1' 접두사 제거
            if (!param) return null;
            try {
                const decoded = decodeURIComponent(atob(param.replace(/_/g, '/').replace(/-/g, '+'))); // Base64 디코딩 후 URL 디코딩
                return decoded.startsWith('/') ? location.origin + decoded : decoded; // 상대 경로일 경우 도메인 추가
            } catch {
                return null; // 디코딩 실패 시 null 반환
            }
        },

        // 추적 URL을 실제 목적지 URL로 변환
        // @param {string} url - 변환할 URL
        // @returns {string} - 실제 URL 또는 원본 URL
        resolveRealUrl(url) {
            const rules = [
                { pattern: /bing\.com\/(ck\/a|aclick)/, key: 'u' }, // Bing 추적 링크 규칙
                { pattern: /so\.com\/search\/eclk/, key: 'aurl' } // So.com 추적 링크 규칙
            ];
            for (const { pattern, key } of rules) {
                if (pattern.test(url)) { // URL이 규칙에 맞는지 확인
                    const real = this.decodeRealUrl(url, key); // 실제 URL 디코딩
                    if (real && real !== url) return real; // 디코딩 성공 시 실제 URL 반환
                }
            }
            return url; // 변환 실패 시 원본 URL 반환
        },

        // 페이지 내 모든 추적 링크를 실제 URL로 변환
        // @param {HTMLElement} root - 링크를 검색할 루트 요소
        convertLinksToReal(root) {
            root.querySelectorAll('a[href]').forEach(a => { // 모든 링크 요소 순회
                const realUrl = this.resolveRealUrl(a.href); // 실제 URL로 변환
                if (realUrl && realUrl !== a.href) a.href = realUrl; // 변환된 URL로 업데이트
            });
            console.log('Links converted'); // 링크 변환 완료 로그
        }
    };

    // 버전 확인 모듈: marked.js 라이브러리 버전 확인 및 업데이트 알림
    const VersionChecker = {
        // 두 버전 문자열을 비교
        // @param {string} current - 현재 버전 (예: '15.0.7')
        // @param {string} latest - 최신 버전 (예: '15.1.0')
        // @returns {number} - -1 (current < latest), 0 (equal), 1 (current > latest)
        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; // 없는 부분은 0으로 처리
                const l = latestParts[i] || 0;
                if (c < l) return -1; // 현재 버전이 더 낮음
                if (c > l) return 1; // 현재 버전이 더 높음
            }
            return 0; // 버전 동일
        },

        // marked.js의 최신 버전을 확인하고 업데이트가 필요하면 팝업 표시
        checkMarkedJsVersion() {
            localStorage.setItem(Config.STORAGE_KEYS.CURRENT_VERSION, Config.VERSIONS.MARKED_VERSION); // 현재 버전 저장

            GM_xmlhttpRequest({
                method: 'GET', // GET 요청
                url: Config.API.MARKED_CDN_URL, // marked.js CDN URL
                onload({ responseText }) { // 요청 성공 시
                    try {
                        const latest = JSON.parse(responseText).version; // 최신 버전 추출
                        console.log(`marked.js version: current=${Config.VERSIONS.MARKED_VERSION}, latest=${latest}`); // 버전 비교 로그

                        localStorage.setItem(Config.STORAGE_KEYS.LATEST_VERSION, latest); // 최신 버전 저장

                        const lastNotified = localStorage.getItem(Config.STORAGE_KEYS.LAST_NOTIFIED); // 마지막 알림 버전 확인
                        console.log(`Last notified version: ${lastNotified || 'none'}`); // 알림 로그

                        // 팝업 표시 조건: 현재 버전이 최신 버전보다 낮고, 이전에 알림을 받지 않았거나 최신 버전이 더 높은 경우
                        if (this.compareVersions(Config.VERSIONS.MARKED_VERSION, latest) < 0 &&
                            (!lastNotified || this.compareVersions(lastNotified, latest) < 0)) {
                            console.log('Popup display condition met'); // 팝업 조건 충족 로그

                            const existingPopup = document.getElementById('marked-update-popup'); // 기존 팝업 확인
                            if (existingPopup) {
                                existingPopup.remove(); // 기존 팝업 제거
                                console.log('Existing popup removed'); // 제거 로그
                            }

                            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); // 알림 버전 기록
                                console.log(`Notified version recorded: ${latest}`); // 기록 로그
                                popup.remove(); // 팝업 제거
                            };
                            document.body.appendChild(popup); // 팝업 표시
                            console.log('New popup displayed'); // 표시 로그
                        } else {
                            console.log('Popup display condition not met'); // 팝업 조건 미충족 로그
                        }
                    } catch (e) {
                        console.warn('marked.min.js version check error:', e.message); // 오류 로그
                    }
                },
                onerror: () => console.warn('marked.min.js version check request failed') // 요청 실패 로그
            });
        }
    };

    // 메인 모듈: 스크립트의 주요 기능 실행 및 관리
    const Main = {
        // Gemini UI를 조건에 따라 렌더링
        renderGemini() {
            console.log('renderGemini called'); // 렌더링 시작 로그
            if (!Utils.isGeminiAvailable()) { // Gemini UI 표시 조건 확인
                console.log('Skipped: isGeminiAvailable false'); // 조건 불충족 로그
                return;
            }

            const query = Utils.getQuery(); // 검색 쿼리 가져오기
            if (!query || document.getElementById('gemini-box')) { // 쿼리 없거나 이미 UI 존재 시 스킵
                console.log('Skipped:', { queryExists: !!query, geminiBoxExists: !!document.getElementById('gemini-box') }); // 스킵 로그
                return;
            }

            const apiKey = Utils.getApiKey(); // API 키 가져오기
            if (!apiKey) { // API 키 없으면 스킵
                console.log('Skipped: No API key'); // 스킵 로그
                return;
            }

            const ui = UI.createGeminiUI(query, apiKey); // Gemini UI 생성
            const target = document.getElementById('b_context'); // 삽입 대상 요소
            if (target) {
                target.prepend(ui); // UI 삽입
                console.log('Gemini UI inserted into b_context'); // 삽입 성공 로그
            } else {
                console.error('b_context not found for insertion'); // 삽입 실패 로그
                return;
            }

            const content = ui.querySelector('#gemini-content'); // 콘텐츠 영역 선택
            const cache = sessionStorage.getItem(`${Config.CACHE.PREFIX}${query}`); // 캐시 확인
            content.innerHTML = cache ? marked.parse(cache) : Localization.getMessage(Config.MESSAGE_KEYS.LOADING); // 캐시 있으면 표시, 없으면 로딩 메시지
            if (!cache) GeminiAPI.fetch(query, content, apiKey); // 캐시 없으면 API 호출
        },

        // URL 변경 감지 및 UI 갱신
        observeUrlChange() {
            let lastUrl = location.href; // 현재 URL 저장
            const observer = new MutationObserver(() => { // DOM 변경 감지
                if (location.href !== lastUrl) { // URL 변경 시
                    lastUrl = location.href; // 새로운 URL 저장
                    console.log('MutationObserver triggered: URL changed'); // URL 변경 로그
                    this.renderGemini(); // UI 갱신
                    LinkCleaner.convertLinksToReal(document); // 링크 정리
                }
            });
            observer.observe(document.body, { childList: true, subtree: true }); // DOM 변경 감시 설정
            console.log('Observing URL changes on document.body'); // 감시 시작 로그
        },

        // 스크립트 초기화
        init() {
            console.log('Bing Plus init:', { hostname: location.hostname, url: location.href }); // 초기화 시작 로그
            try {
                Styles.inject(); // 스타일 삽입
                LinkCleaner.convertLinksToReal(document); // 링크 정리
                this.renderGemini(); // Gemini UI 렌더링
                this.observeUrlChange(); // URL 변경 감시 시작
            } catch (e) {
                console.error('Init error:', e.message); // 초기화 오류 로그
            }
        }
    };

    // 스크립트 실행 시작
    console.log('Bing Plus script loaded'); // 스크립트 로드 완료 로그
    Main.init(); // 초기화 함수 호출
})();