당신의 "Web Crawler Enhanced Pro Optimized v6.1.1" 사용자 스크립트는 매우 포괄적이고 다양한 기능을 갖추고 있습니다. 여러 검색 엔진을 지원하며, 사용자 경험을 향상시키기 위한 다양한 모듈들이 잘 분리되어 있습니다. 그러나 더 나은 성능과 유지 보수성을 위해 몇 가지 개선 사항과 최적화 방안을 제안드리고자 합니다.

1. 전반적인 구조 및 모듈화
긍정적인 점
모듈화: Config, Logger, Utils, UI, Download, DataVisualization, Crawling, Fetch, Robots, Extractor, Classifier, Evaluate, Scheduler 등 각 기능별로 모듈이 잘 분리되어 있어 코드의 가독성과 유지 보수성이 높습니다.
구성 설정: Config 모듈을 통해 기본 설정과 사용자 맞춤 설정을 관리하여 유연성이 뛰어납니다.
UI 구성: 사용자 인터페이스를 잘 구성하여 사용자 경험을 고려한 디자인이 돋보입니다.
개선이 필요한 점
중복 코드: 일부 모듈 간에 중복되는 코드가 존재할 수 있으며, 이를 함수화하거나 공통 모듈로 분리하면 더욱 효율적입니다.
코드 주석 및 문서화: 코드에 대한 설명이 부족하여 나중에 유지 보수할 때 어려움이 있을 수 있습니다. 주석을 추가하거나 문서화를 통해 코드를 이해하기 쉽게 만드는 것이 좋습니다.
2. 주요 모듈별 피드백 및 최적화 제안
a. Configuration Module (Config)
설정 초기화: 현재 getSavedSettings 함수는 GM_getValue를 사용하여 각 설정을 개별적으로 저장하고 불러옵니다. 이를 한 번에 저장하고 불러올 수 있는 구조로 변경하면 성능 향상과 코드 간결성을 도모할 수 있습니다.

// 예시: 설정을 하나의 객체로 저장
function getSavedSettings() {
    const savedSettings = GM_getValue('settings', DEFAULT_SETTINGS);
    return { ...DEFAULT_SETTINGS, ...savedSettings };
}

function saveSettings(newSettings) {
    GM_setValue('settings', newSettings);
}

function resetSettings() {
    GM_setValue('settings', DEFAULT_SETTINGS);
    Object.assign(settings, DEFAULT_SETTINGS);
}
b. Logger Module (Logger)
로그 저장 방식: 현재 logs를 Set으로 관리하고 있는데, 순서가 중요한 로그라면 배열로 관리하는 것이 더 적합합니다.

const logs = JSON.parse(GM_getValue('logs', '[]'));
성능 최적화: 로그가 많이 쌓일 경우 성능에 영향을 줄 수 있습니다. 일정 개수 이상일 경우 오래된 로그를 제거하는 로직을 추가하는 것이 좋습니다.
c. Utility Module (Utils)
함수 이름 일관성: 함수 이름을 모두 소문자로 작성하거나 카멜 케이스로 통일하면 일관성을 유지할 수 있습니다.
불필요한 함수 제거: 현재 escapeHTML, escapeCSV, escapeXML 등 비슷한 기능을 하는 함수들이 많습니다. 하나의 일반화된 이스케이프 함수로 통합할 수 있습니다.

function escape(str, type = 'html') {
    const map = {
        html: { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' },
        csv: { '"': '""' },
        xml: { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' },
    };
    return String(str).replace(new RegExp(`[${Object.keys(map[type]).join('')}]`, 'g'), match => map[type][match]);
}
d. UI Module (UI)
DOM 조작 최적화: 현재 jQuery를 이용한 다수의 DOM 조작이 발생합니다. 가능한 한 최소화하고, 필요 시 한 번에 업데이트하도록 개선할 수 있습니다.
이벤트 핸들러 정리: 이벤트 핸들러를 동적으로 추가하거나 제거하는 부분이 많으므로, 이벤트 위임(Event Delegation)을 활용하면 더 효율적일 수 있습니다.
반응형 디자인 고려: 다양한 화면 크기에서도 UI가 잘 동작하도록 반응형 디자인을 고려하면 사용자 경험이 더욱 향상될 것입니다.
e. Crawling Module (Crawling)
재귀 깊이 관리: 현재 crawlPage 함수에서 재귀적으로 호출되는 구조인데, 깊이 제한이나 스택 오버플로우 방지 로직을 추가하면 더욱 안정적입니다.
에러 핸들링 강화: 크롤링 중 발생할 수 있는 다양한 에러 상황에 대한 핸들링을 강화하여 크롤러의 안정성을 높일 수 있습니다.
비동기 처리: 현재 비동기 처리가 어느 정도 이루어지고 있지만, 모든 비동기 함수가 제대로 await되고 있는지 확인하고, 가능한 모든 비동기 로직을 Promise 체인으로 관리하면 더욱 효율적입니다.
f. Fetch Module (Fetch)
헤더 관리: User-Agent 등을 현재 랜덤하게 선택하고 있는데, 필요에 따라 동적으로 변경할 수 있는 로직을 추가하면 크롤링의 유연성을 높일 수 있습니다.
캐싱 고려: 동일한 URL에 대한 반복적인 요청을 줄이기 위해 캐싱 메커니즘을 도입하면 성능 향상에 도움이 됩니다.
g. Robots Module (Robots)
Robots.txt 파싱 라이브러리 사용: 직접 구현한 RobotsParser보다는 검증된 라이브러리를 사용하는 것이 안정성과 유지 보수성 측면에서 유리합니다.
비동기 처리: checkRobotsTxt 함수가 동기식으로 작성된 것 같은데, 이를 비동기로 변경하여 크롤링 성능을 저해하지 않도록 합니다.
h. Scheduler Module (Scheduler)
타이머 관리: 현재 setInterval을 사용하여 스케줄링을 하고 있는데, setTimeout을 재귀적으로 호출하는 방식으로 변경하면 보다 정확한 타이밍 관리가 가능합니다.
스케줄 상태 저장: 스케줄링 상태를 GM_setValue로 저장하여 브라우저 재시작 후에도 계속 진행될 수 있도록 하면 더 유용할 것입니다.
3. 기타 개선 사항
a. 국제화 및 지역화 (i18n/l10n)
현재 로그 메시지와 일부 UI 텍스트가 한글로 작성되어 있습니다. 다국어 지원을 고려하여 텍스트를 외부 파일이나 객체로 분리하고, 사용자 설정에 따라 언어를 선택할 수 있도록 하면 다양한 사용자에게 유연하게 대응할 수 있습니다.
b. 보안 고려
사용자 입력을 처리할 때 XSS 공격을 방지하기 위해 철저한 검증과 이스케이프 처리가 필요합니다. UI 모듈 내에서 사용자 입력을 직접적으로 삽입하는 부분이 있는지 검토하고, escapeHTML 등의 유틸리티 함수를 적절히 사용하세요.
@connect * 권한은 보안상 위험할 수 있으므로, 필요한 호스트만 명시적으로 허용하는 것이 좋습니다.
c. 성능 최적화
현재 크롤러는 여러 요청을 동시에 처리할 수 있도록 설정되어 있으나, 네트워크 대역폭과 서버 부하를 고려하여 maxConcurrentRequests 값을 적절하게 설정하는 것이 중요합니다.
DOM 조작을 최소화하고, 데이터를 메모리 내에서 처리한 후 한 번에 업데이트하는 방식으로 성능을 향상시킬 수 있습니다.
d. 코드 일관성 및 스타일
코드 전반에 걸쳐 일관된 들여쓰기와 코드 스타일을 유지하면 가독성이 향상됩니다. 예를 들어, 중괄호 {} 사용 여부, 세미콜론 ; 사용 여부 등을 일관되게 유지하세요.
ESLint와 같은 도구를 사용하여 코드 린팅을 자동화하면 스타일과 오류를 사전에 방지할 수 있습니다.
e. 테스트 및 디버깅
현재 크롤러의 각 기능에 대한 테스트 케이스가 보이지 않습니다. 단위 테스트나 통합 테스트를 작성하여 기능이 의도대로 동작하는지 검증하는 것이 좋습니다.
디버깅을 용이하게 하기 위해 로그의 수준(info, warning, error, success)을 세분화하고, 필터링 기능을 추가하면 유용합니다.
4. 코드 최적화 예시
a. 비동기 함수의 적절한 활용
현재 몇몇 비동기 함수들이 제대로 await되고 있지 않거나, 비동기 로직이 묶여 있지 않은 경우가 있을 수 있습니다. 이를 해결하기 위해 모든 비동기 함수가 async/await를 통해 처리되도록 합니다.

b. Fetch 모듈의 단순화 및 최적화

const Fetch = (function() {
    async function fetchWithRetry(url, attempts, delayMs) {
        for (let i = 1; i <= attempts; i++) {
            try {
                return await GM_xmlhttpRequestPromise(url);
            } catch (e) {
                Logger.log(`요청 실패 (${i}/${attempts}): ${url} - ${e.message}`, 'warning');
                if (i < attempts) await Utils.delay(delayMs);
            }
        }
        throw new Error('모든 재시도 시도가 실패했습니다.');
    }

    function GM_xmlhttpRequestPromise(url) {
        return new Promise((resolve, reject) => {
            let reqUrl = Config.settings.proxy ? `${Config.settings.proxy}?url=${encodeURIComponent(url)}` : url;
            GM_xmlhttpRequest({
                method: "GET",
                url: reqUrl,
                headers: { 'User-Agent': getCurrentUserAgent(), 'Accept': '*/*' },
                timeout: Config.settings.timeout,
                onload: res => (res.status >= 200 && res.status < 300) ? resolve(res) : reject(new Error(`HTTP ${res.status}`)),
                onerror: () => reject(new Error('Request failed')),
                ontimeout: () => reject(new Error('Request timed out'))
            });
        });
    }

    function getCurrentUserAgent() {
        const index = Config.settings.userAgentIndex % Config.settings.userAgents.length;
        return Config.settings.userAgents[index];
    }

    function getContentType(res) {
        return res.responseHeaders.split('\n').find(h => h.toLowerCase().startsWith('content-type'))?.split(':')[1].trim().split(';')[0] || '';
    }

    function isContentTypeAllowed(ct, contentTypes) {
        if (contentTypes.include.length && !contentTypes.include.includes(ct)) return false;
        if (contentTypes.exclude.includes(ct)) return false;
        return true;
    }

    async function fetchPageContent(url) {
        try {
            const response = await fetchWithRetry(url, Config.settings.retryAttempts, Config.settings.retryDelay);
            return response.responseText;
        } catch (error) {
            Logger.log(`페이지 내용 가져오기 실패: ${url} - ${error.message}`, 'error');
            return null;
        }
    }

    return {
        fetchWithRetry,
        getCurrentUserAgent,
        getContentType,
        isContentTypeAllowed,
        fetchPageContent
    };
})();
c. Logger 모듈의 개선

const Logger = (function() {
    const logs = JSON.parse(GM_getValue('logs', '[]'));

    function log(message, type = 'info') {
        const timestamp = new Date().toLocaleString();
        logs.push({ timestamp, message, type });
        // 최대 로그 개수 제한 (예: 1000개)
        if (logs.length > 1000) logs.shift();
        GM_setValue('logs', JSON.stringify(logs));
        UI.updateLogs();
        if (Config.settings.advancedLogging) {
            const styles = { 
                'info': 'color: blue;', 
                'warning': 'color: orange;', 
                'error': 'color: red;', 
                'success': 'color: green; font-weight: bold;' 
            };
            console.log(`%c[${timestamp}] [${type.toUpperCase()}] ${message}`, styles[type] || '');
        }
    }

    function getLogs() {
        return logs;
    }

    return {
        log,
        getLogs
    };
})();
d. Crawling 모듈의 비동기 큐 관리 최적화

const Crawling = (function() {
    let isCrawling = GM_getValue('isCrawling', false);
    let currentPage = parseInt(GM_getValue('currentPage', 1), 10);
    let totalPages = parseInt(GM_getValue('totalPages', Config.settings.maxPages), 10);
    let progress = parseFloat(GM_getValue('progress', 0));
    let currentDepth = parseInt(GM_getValue('currentDepth', 0), 10);
    let activeRequests = 0;
    const requestQueue = [];
    const previousCrawls = JSON.parse(GM_getValue('previousCrawls', '[]'));
    const visitedUrls = new Set(JSON.parse(GM_getValue('visitedUrls', '[]')));
    let data = JSON.parse(GM_getValue('data', '[]'));

    // ... 기존 코드 유지 ...

    async function processQueue() {
        while (requestQueue.length && activeRequests < Config.settings.maxConcurrentRequests && isCrawling) {
            const { url, depth } = requestQueue.shift();
            activeRequests++;
            handleRequest(url, depth).finally(() => { activeRequests--; processQueue(); });
        }
    }

    async function handleRequest(url, depth) {
        if (!isCrawling || visitedUrls.has(url)) return;
        try {
            const response = await Fetch.fetchWithRetry(url, Config.settings.retryAttempts, Config.settings.retryDelay);
            const contentType = Fetch.getContentType(response);
            if (!Fetch.isContentTypeAllowed(contentType, Config.settings.contentTypes)) {
                Logger.log(`허용되지 않은 콘텐츠 유형(${contentType})의 URL: ${url}`, 'warning');
                return;
            }
            const parser = new DOMParser();
            const doc = parser.parseFromString(response.responseText, 'text/html');
            const text = doc.body ? doc.body.innerText : '';
            if (Config.settings.enableCaptchas && Robots.detectCaptcha(doc)) { 
                await Robots.handleCaptcha(url); 
                return; 
            }
            const wc = text.split(/\s+/).length;
            if (wc < Config.settings.minWordCount || wc > Config.settings.maxWordCount) {
                Logger.log(`단어 수 조건에 맞지 않는 URL: ${url} (${wc} 단어)`, 'warning');
                return;
            }
            const quality = Evaluate.evaluateContentQuality(text);
            const lang = doc.documentElement.lang || 'unknown';
            const category = Classifier.classifyKeywords(text);
            const meta = Extractor.extractMetaTags(doc);
            const headings = Extractor.extractHeadings(doc);
            if (category !== '기타' || quality > 100) {
                const entry = { url, quality, language: lang, wordCount: wc, category };
                if (Config.settings.dataFields.name) entry.name = Extractor.extractName(doc) || 'No Title';
                if (Config.settings.dataFields.description) entry.description = Extractor.extractDescription(doc) || 'No Description';
                if (Config.settings.dataFields.html) entry.html = response.responseText;
                if (Config.settings.dataFields.metaTags) entry.metaTags = meta;
                if (Config.settings.dataFields.headings) entry.headings = headings;
                data.push(entry);
                GM_setValue('data', JSON.stringify(data));
                UI.updateDataTable();
                Logger.log(`데이터 수집 완료: ${url}`, 'success');
                if (Config.settings.dataVisualization) DataVisualization.renderChart();
            }
            if (depth < Config.settings.maxDepth) {
                const links = Extractor.extractLinks(doc);
                links.forEach(link => { 
                    if (!visitedUrls.has(link) && isValidURL(link)) requestQueue.push({ url: link, depth: depth + 1 }); 
                });
            }
        } catch (error) {
            Logger.log(`데이터 수집 실패: ${url} - ${error.message}`, 'error');
        }
        visitedUrls.add(url);
        GM_setValue('visitedUrls', JSON.stringify([...visitedUrls]));
    }

    // ... 나머지 코드 유지 ...
})();
5. 기타 권장 사항
a. 버전 관리 및 변경 사항 기록
스크립트의 각 버전별로 변경 사항을 CHANGELOG 파일이나 주석에 기록하여 사용자와 개발자가 어떤 변경이 있었는지 쉽게 파악할 수 있도록 합니다.
b. 사용자 피드백 수집
사용자들이 스크립트를 사용하는 동안 발생하는 문제나 개선 사항을 쉽게 보고할 수 있는 피드백 메커니즘을 도입하면 더 나은 제품을 개발하는 데 도움이 됩니다.
c. 성능 모니터링
크롤링 속도, 성공률, 실패률 등을 실시간으로 모니터링할 수 있는 대시보드를 제공하면 크롤러의 성능을 지속적으로 개선할 수 있습니다.
결론
전반적으로 매우 잘 설계된 웹 크롤러 스크립트입니다. 모듈화된 구조와 풍부한 기능들은 사용자에게 많은 가치를 제공합니다. 위에서 제시한 피드백과 최적화 방안을 통해 더욱 안정적이고 효율적인 스크립트로 발전시킬 수 있을 것입니다. 개발하시면서 지속적으로 코드의 품질을 높이고, 사용자 경험을 개선하기 위한 노력을 계속해 나가시길 바랍니다. 성공적인 개발을 기원합니다!                                  이걸 바탕으로 밑에걸 고쳐 알겠지? 일단 최대한 니가 쓸 수 있는데 까지는 아무말없이 써봐. 그냥 코드만 써. ㅇㅇ                // ==UserScript==
// @name         Advanced Web Crawler Enhanced Pro Optimized v6.1.0
// @namespace    http://tampermonkey.net/
// @version      6.1.0
// @description  Optimized and enhanced version of Advanced Web Crawler Enhanced Pro with improved error handling, asynchronous processing, performance optimizations, and enhanced user experience.
// @author       Your Name
// @match        https://www.google.com/search*
// @match        https://www.bing.com/search*
// @match        https://search.yahoo.com/search*
// @match        https://duckduckgo.com/*
// @match        https://www.baidu.com/s*
// @match        https://www.yandex.com/search/*
// @match        https://search.naver.com/search*
// @match        https://search.daum.net/search*
// @match        https://www.ask.com/*
// @match        https://www.ecosia.org/search*
// @match        https://www.startpage.com/*
// @match        https://searx.me/*
// @match        https://www.dogpile.com/*
// @match        https://www.bing.com/images*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @connect      *
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/chart.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js
// ==/UserScript==

(function() {
    'use strict';

    // Configuration Module
    const Config = (function() {
        const DEFAULT_SETTINGS = {
            delay: 2000,
            delayRandom: 500,
            maxPages: 3,
            userAgents: [
                'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)',
                'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
                'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)',
                'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15',
                'Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15',
                'AdvancedWebCrawler/6.1 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)'
            ],
            userAgentIndex: 0,
            blacklist: [],
            whitelist: [],
            contentTypes: {
                include: ['text/html'],
                exclude: ['application/pdf', 'image/jpeg', 'image/png', 'video/mp4']
            },
            timeout: 8000,
            includeKeywords: [],
            excludeKeywords: [],
            language: [],
            minWordCount: 100,
            maxWordCount: 10000,
            maxDepth: 2,
            proxy: '',
            regexInclude: [],
            regexExclude: [],
            dataFields: {
                name: true,
                url: true,
                description: true,
                html: true,
                category: true,
                quality: true,
                language: true,
                wordCount: true,
                metaTags: false,
                headings: false
            },
            schedule: {
                enabled: false,
                startTime: '00:00',
                interval: 3600000
            },
            searchEngines: {
                google: true,
                bing: true,
                yahoo: true,
                duckduckgo: true,
                baidu: true,
                yandex: true,
                naver: true,
                daum: true,
                ask: true,
                ecosia: true,
                startpage: true,
                searx: true,
                dogpile: true,
                bingimages: true
            },
            retryAttempts: 3,
            retryDelay: 2000,
            respectRobots: true,
            maxConcurrentRequests: 3,
            dataVisualization: true,
            exportFormats: {
                csv: true,
                json: true,
                xml: true,
                xls: true
            },
            advancedLogging: true,
            enableCaptchas: false,
            customKeywords: {}
        };

        const settings = Object.assign({}, DEFAULT_SETTINGS, getSavedSettings());

        function getSavedSettings() {
            const savedSettings = {};
            Object.keys(DEFAULT_SETTINGS).forEach(key => {
                savedSettings[key] = GM_getValue(key, DEFAULT_SETTINGS[key]);
            });
            return savedSettings;
        }

        function saveSettings(newSettings) {
            Object.keys(newSettings).forEach(key => {
                GM_setValue(key, newSettings[key]);
            });
        }

        function resetSettings() {
            Object.keys(DEFAULT_SETTINGS).forEach(key => {
                GM_setValue(key, DEFAULT_SETTINGS[key]);
            });
            Object.assign(settings, DEFAULT_SETTINGS);
        }

        return {
            DEFAULT_SETTINGS,
            settings,
            saveSettings,
            resetSettings
        };
    })();

    // Logger Module
    const Logger = (function() {
        const logs = new Set(JSON.parse(GM_getValue('logs', '[]')));

        function log(message, type = 'info') {
            const timestamp = new Date().toLocaleString();
            logs.add(JSON.stringify({ timestamp, message, type }));
            GM_setValue('logs', JSON.stringify([...logs]));
            UI.updateLogs();
            if (Config.settings.advancedLogging) {
                const styles = { 'info': 'color: blue;', 'warning': 'color: orange;', 'error': 'color: red;', 'success': 'color: green; font-weight: bold;' };
                console.log(`%c[${timestamp}] [${type.toUpperCase()}] ${message}`, styles[type] || '');
            }
        }

        function getLogs() {
            return Array.from(logs).map(log => JSON.parse(log));
        }

        return {
            log,
            getLogs
        };
    })();

    // Utility Module
    const Utils = (function() {
        function escapeHTML(str) {
            return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;")
                             .replace(/>/g, "&gt;").replace(/"/g, "&quot;")
                             .replace(/'/g, "&#039;");
        }

        function escapeCSV(str) {
            return String(str).replace(/"/g, '""');
        }

        function escapeXML(str) {
            return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;")
                             .replace(/>/g, "&gt;").replace(/"/g, "&quot;")
                             .replace(/'/g, "&#039;");
        }

        function capitalize(str) {
            return str.charAt(0).toUpperCase() + str.slice(1);
        }

        function delay(ms) {
            return new Promise(res => setTimeout(res, ms));
        }

        function generateColors(count) {
            return Array.from({length: count}, (_, i) => `hsl(${i * (360 / count)}, 70%, 50%)`);
        }

        function parseCustomKeywords(text) {
            const lines = text.split('\n').map(l => l.trim()).filter(l => l);
            const ck = {};
            lines.forEach(line => {
                const [cat, kws] = line.split(':');
                if (cat && kws) ck[cat.trim()] = kws.split(',').map(kw => kw.trim()).filter(kw => kw);
            });
            return ck;
        }

        function formatCustomKeywords(ck) {
            return Object.entries(ck).map(([c, kws]) => `${c}:${kws.join(',')}`).join('\n');
        }

        return {
            escapeHTML,
            escapeCSV,
            escapeXML,
            capitalize,
            delay,
            generateColors,
            parseCustomKeywords,
            formatCustomKeywords
        };
    })();

    // UI Module
    const UI = (function() {
        function createUI() {
            const style = `
                #crawler-container {
                    position: fixed;
                    top: 20px;
                    right: 20px;
                    width: 400px;
                    height: 500px;
                    background: #fff;
                    border: 1px solid #ccc;
                    z-index: 10000;
                    font-family: Arial, sans-serif;
                    box-shadow: 0 4px 8px rgba(0,0,0,0.2);
                    overflow: auto;
                    resize: both;
                    border-radius: 8px;
                    min-width: 300px;
                    min-height: 300px;
                }
                #crawler-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    background: #f1f1f1;
                    border-bottom: 1px solid #ccc;
                    padding: 5px 10px;
                    cursor: move;
                }
                #crawler-minimize {
                    background: none;
                    border: none;
                    font-size: 18px;
                    cursor: pointer;
                }
                #crawler-tabs { display: flex; background: #f1f1f1; border-bottom: 1px solid #ccc; }
                .crawler-tab { flex: 1; padding: 10px; text-align: center; cursor: pointer; font-weight: bold; }
                .crawler-tab.active { background: #ddd; }
                #crawler-content { padding: 10px; overflow: auto; height: calc(100% - 50px); }
                table { width: 100%; border-collapse: collapse; }
                th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
                th { background-color: #f2f2f2; cursor: pointer; }
                tr:hover { background-color: #f5f5f5; }
                .progress-bar { width: 100%; background-color: #f3f3f3; border: 1px solid #ccc; border-radius: 5px; overflow: hidden; margin-bottom: 10px; }
                .progress-bar-fill { height: 20px; background-color: #4caf50; width: 0%; transition: width 0.2s; }
                .modal { display: none; position: fixed; z-index: 10001; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4); }
                .modal-content { background-color: #fff; margin: 5% auto; padding: 20px; border: 1px solid #888; width: 80%; max-height: 80%; overflow-y: auto; border-radius: 8px; }
                .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; }
                .control-buttons { margin: 10px 0; }
                .control-buttons button { padding: 8px 12px; margin-right: 8px; cursor: pointer; border: none; background-color: #4caf50; color: white; border-radius: 4px; font-size: 14px; }
                .control-buttons button:disabled { background-color: #a5d6a7; cursor: not-allowed; }
                .status-text { margin-top: 10px; font-weight: bold; font-size: 14px; }
                .settings-section { margin-bottom: 15px; }
                .settings-section label { display: block; margin-bottom: 5px; font-weight: bold; }
                .settings-section input[type="text"], .settings-section input[type="number"], .settings-section textarea, .settings-section select { width: 100%; padding: 5px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
                .settings-section .checkbox-group { display: flex; flex-wrap: wrap; }
                .settings-section .checkbox-group label { margin-right: 10px; font-size: 14px; }
                .settings-section .radio-group { display: flex; flex-wrap: wrap; }
                .settings-section .radio-group label { margin-right: 10px; font-size: 14px; }
                .tooltip { position: relative; display: inline-block; cursor: help; font-size: 14px; }
                .tooltip .tooltiptext { visibility: hidden; width: 220px; background-color: #555; color: #fff; text-align: center; border-radius: 6px; padding: 5px; position: absolute; z-index: 1; bottom: 125%; left: 50%; margin-left: -110px; opacity: 0; transition: opacity 0.3s; }
                .tooltip:hover .tooltiptext { visibility: visible; opacity: 1; }
                #crawler-container::-webkit-resizer { background: #f1f1f1; border: none; }
                #data-visualization { margin-top: 20px; }
                #data-chart { width: 100%; height: 300px; }
                #modal-html-content { white-space: pre-wrap; word-wrap: break-word; background-color: #f9f9f9; padding: 10px; border-radius: 4px; }
                #crawler-tabs .crawler-tab:last-child { flex: 1; }
                #previous-crawls { margin-top: 20px; }
                #previous-crawls table { width: 100%; }
                #previous-crawls th, #previous-crawls td { padding: 5px; }
                #reset-data { background-color: #f44336; }
                /* Loading Spinner */
                .spinner {
                    border: 4px solid #f3f3f3;
                    border-top: 4px solid #3498db;
                    border-radius: 50%;
                    width: 24px;
                    height: 24px;
                    animation: spin 2s linear infinite;
                    display: inline-block;
                    vertical-align: middle;
                }
                @keyframes spin {
                    0% { transform: rotate(0deg); }
                    100% { transform: rotate(360deg); }
                }
                /* Error Message Styling */
                .error-message {
                    color: red;
                    font-weight: bold;
                    margin-top: 10px;
                }
                /* Success Message Styling */
                .success-message {
                    color: green;
                    font-weight: bold;
                    margin-top: 10px;
                }
            `;
            $('head').append(`<style>${style}</style>`);
            const container = $(`
                <div id="crawler-container">
                    <div id="crawler-header">
                        <div id="crawler-tabs">
                            <div class="crawler-tab" data-tab="logs" title="로그 보기">📜 로그</div>
                            <div class="crawler-tab" data-tab="data" title="수집된 데이터 보기">📊 데이터</div>
                            <div class="crawler-tab" data-tab="settings" title="설정 변경">⚙️ 설정</div>
                            <div class="crawler-tab" data-tab="previous-crawls" title="이전 크롤링 기록">📁 기록</div>
                        </div>
                        <button id="crawler-minimize" title="창 최소화">─</button>
                    </div>
                    <div id="crawler-content">
                        <div id="tab-logs" style="display: none;">
                            <div class="progress-bar">
                                <div class="progress-bar-fill" style="width: 0%;"></div>
                            </div>
                            <div class="status-text">진행률: 0% (0/0 페이지)</div>
                            <div class="control-buttons">
                                <button id="start-crawling" title="크롤링을 시작합니다.">▶ 시작</button>
                                <button id="stop-crawling" title="크롤링을 중지합니다." disabled>■ 중지</button>
                                ${Config.settings.exportFormats.csv ? `<button id="download-csv" title="CSV 다운로드">📥 CSV</button>` : ''}
                                ${Config.settings.exportFormats.json ? `<button id="download-json" title="JSON 다운로드">📥 JSON</button>` : ''}
                                ${Config.settings.exportFormats.xml ? `<button id="download-xml" title="XML 다운로드">📥 XML</button>` : ''}
                                ${Config.settings.exportFormats.xls ? `<button id="download-xls" title="XLS 다운로드">📥 XLS</button>` : ''}
                                <button id="reset-data" title="모든 크롤링 데이터를 초기화합니다.">🗑️ 초기화</button>
                            </div>
                            <div id="log-messages" style="height: 200px; overflow-y: auto; background: #f9f9f9; padding: 10px; border: 1px solid #ccc; border-radius: 4px;">
                                로그 내용이 여기에 표시됩니다.
                            </div>
                            <div id="error-message" class="error-message" style="display: none;"></div>
                            <div id="success-message" class="success-message" style="display: none;"></div>
                        </div>
                        <div id="tab-data" style="display: none;">
                            <input type="text" id="search-filter" placeholder="검색 필터..." style="width: 100%; padding: 5px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px;">
                            <table>
                                <thead>
                                    <tr>
                                        ${Config.settings.dataFields.name ? '<th data-sort="name">사이트 이름</th>' : ''}
                                        ${Config.settings.dataFields.url ? '<th data-sort="url">주소</th>' : ''}
                                        ${Config.settings.dataFields.description ? '<th data-sort="description">설명</th>' : ''}
                                        ${Config.settings.dataFields.html ? '<th>📄 HTML 내용</th>' : ''}
                                        ${Config.settings.dataFields.category ? '<th data-sort="category">분류</th>' : ''}
                                        ${Config.settings.dataFields.quality ? '<th data-sort="quality">품질점수</th>' : ''}
                                        ${Config.settings.dataFields.language ? '<th data-sort="language">언어</th>' : ''}
                                        ${Config.settings.dataFields.wordCount ? '<th data-sort="wordCount">단어 수</th>' : ''}
                                        ${Config.settings.dataFields.metaTags ? '<th>메타 태그</th>' : ''}
                                        ${Config.settings.dataFields.headings ? '<th>헤딩</th>' : ''}
                                    </tr>
                                </thead>
                                <tbody id="data-table-body"></tbody>
                            </table>
                            ${Config.settings.dataVisualization ? `
                                <div id="data-visualization">
                                    <h3>데이터 시각화</h3>
                                    <canvas id="data-chart"></canvas>
                                </div>
                            ` : ''}
                        </div>
                        <div id="tab-settings" style="display: none;">
                            <h3>설정</h3>
                            <div class="settings-section">
                                <label for="delay-input">크롤링 딜레이 (ms):
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">기본 딜레이과 딜레이 랜덤 값을 조합하여 요청 사이의 딜레이를 설정합니다.</span>
                                    </span>
                                </label>
                                <input type="number" id="delay-input" value="${Config.settings.delay}" min="500" step="500">
                            </div>
                            <div class="settings-section">
                                <label for="delay-random-input">딜레이 랜덤 범위 (ms):
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">딜레이에 무작위 요소를 추가하여 보다 자연스러운 요청을 합니다.</span>
                                    </span>
                                </label>
                                <input type="number" id="delay-random-input" value="${Config.settings.delayRandom}" min="0" step="500">
                            </div>
                            <div class="settings-section">
                                <label for="max-pages-input">최대 크롤링 페이지 수:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">크롤링할 최대 페이지 수를 설정하세요.</span>
                                    </span>
                                </label>
                                <input type="number" id="max-pages-input" value="${Config.settings.maxPages}" min="1">
                            </div>
                            <div class="settings-section">
                                <label for="user-agent-select">사용자 에이전트(User-Agent):
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">크롤링에 사용할 사용자 에이전트를 선택하거나 추가할 수 있습니다.</span>
                                    </span>
                                </label>
                                <select id="user-agent-select" multiple size="6">
                                    ${Config.settings.userAgents.map((ua, idx) => `<option value="${idx}" ${idx === Config.settings.userAgentIndex ? 'selected' : ''}>${ua}</option>`).join('')}
                                </select>
                                <button id="add-user-agent" style="margin-top: 5px; padding: 5px 10px; background-color: #2196F3; color: white; border-radius: 4px; font-size: 12px;">추가</button>
                                <button id="remove-user-agent" style="margin-top: 5px; padding: 5px 10px; background-color: #f44336; color: white; border-radius: 4px; font-size: 12px;">제거</button>
                            </div>
                            <div class="settings-section">
                                <label>콘텐츠 유형:</label>
                                <div>
                                    <strong>포함할 유형:</strong>
                                    <textarea id="content-include-input" rows="3">${Config.settings.contentTypes.include.join(', ')}</textarea>
                                </div>
                                <div>
                                    <strong>제외할 유형:</strong>
                                    <textarea id="content-exclude-input" rows="3">${Config.settings.contentTypes.exclude.join(', ')}</textarea>
                                </div>
                            </div>
                            <div class="settings-section">
                                <label for="timeout-input">크롤링 타임아웃 (ms):
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">요청이 완료되기까지 기다릴 최대 시간을 밀리초 단위로 설정합니다.</span>
                                    </span>
                                </label>
                                <input type="number" id="timeout-input" value="${Config.settings.timeout}" min="1000" step="1000">
                            </div>
                            <div class="settings-section">
                                <label for="include-keywords-input">포함할 키워드:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">크롤링할 페이지에 반드시 포함되어야 하는 키워드를 입력하세요. 각 키워드는 줄바꿈으로 구분됩니다.</span>
                                    </span>
                                </label>
                                <textarea id="include-keywords-input" rows="3">${Config.settings.includeKeywords.join('\n')}</textarea>
                            </div>
                            <div class="settings-section">
                                <label for="exclude-keywords-input">제외할 키워드:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">크롤링할 페이지에 포함되면 안 되는 키워드를 입력하세요. 각 키워드는 줄바꿈으로 구분됩니다.</span>
                                    </span>
                                </label>
                                <textarea id="exclude-keywords-input" rows="3">${Config.settings.excludeKeywords.join('\n')}</textarea>
                            </div>
                            <div class="settings-section">
                                <label for="regex-include-input">포함할 키워드 (정규 표현식):
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">포함할 키워드의 정규 표현식을 입력하세요. 각 패턴은 줄바꿈으로 구분됩니다.</span>
                                    </span>
                                </label>
                                <textarea id="regex-include-input" rows="3">${Config.settings.regexInclude.join('\n')}</textarea>
                            </div>
                            <div class="settings-section">
                                <label for="regex-exclude-input">제외할 키워드 (정규 표현식):
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">제외할 키워드의 정규 표현식을 입력하세요. 각 패턴은 줄바꿈으로 구분됩니다.</span>
                                    </span>
                                </label>
                                <textarea id="regex-exclude-input" rows="3">${Config.settings.regexExclude.join('\n')}</textarea>
                            </div>
                            <div class="settings-section">
                                <label for="language-select">언어 필터링:</label>
                                <select id="language-select" multiple size="10">
                                    ${[
                                        { value: 'en', text: '영어 (en)' }, { value: 'ko', text: '한국어 (ko)' },
                                        { value: 'zh', text: '중국어 (zh)' }, { value: 'ja', text: '일본어 (ja)' },
                                        { value: 'ru', text: '러시아어 (ru)' }, { value: 'es', text: '스페인어 (es)' },
                                        { value: 'fr', text: '프랑스어 (fr)' }, { value: 'de', text: '독일어 (de)' },
                                        { value: 'it', text: '이탈리아어 (it)' }, { value: 'pt', text: '포르투갈어 (pt)' },
                                        { value: 'ar', text: '아랍어 (ar)' }, { value: 'hi', text: '힌디어 (hi)' },
                                        { value: 'bn', text: '벵골어 (bn)' }, { value: 'pa', text: '펀자브어 (pa)' },
                                        { value: 'nl', text: '네덜란드어 (nl)' }, { value: 'sv', text: '스웨덴어 (sv)' },
                                        { value: 'tr', text: '터키어 (tr)' }, { value: 'vi', text: '베트남어 (vi)' },
                                        { value: 'pl', text: '폴란드어 (pl)' }, { value: 'th', text: '태국어 (th)' }
                                    ].map(lang => `<option value="${lang.value}" ${Config.settings.language.includes(lang.value) ? 'selected' : ''}>${lang.text}</option>`).join('')}
                                </select>
                            </div>
                            <div class="settings-section">
                                <label for="min-word-count-input">최소 단어 수:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">수집할 페이지의 최소 단어 수를 설정하세요.</span>
                                    </span>
                                </label>
                                <input type="number" id="min-word-count-input" value="${Config.settings.minWordCount}" min="10" step="10">
                                <label for="max-word-count-input">최대 단어 수:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">수집할 페이지의 최대 단어 수를 설정하세요.</span>
                                    </span>
                                </label>
                                <input type="number" id="max-word-count-input" value="${Config.settings.maxWordCount}" min="100" step="100">
                            </div>
                            <div class="settings-section">
                                <label for="max-depth-input">최대 크롤링 깊이:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">링크를 추적할 최대 깊이를 설정하세요.</span>
                                    </span>
                                </label>
                                <input type="number" id="max-depth-input" value="${Config.settings.maxDepth}" min="1" step="1">
                            </div>
                            <div class="settings-section">
                                <label for="proxy-input">프록시 서버:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">프록시 서버 주소를 입력하세요. 예: http://proxyserver:port</span>
                                    </span>
                                </label>
                                <input type="text" id="proxy-input" value="${Config.settings.proxy}" placeholder="http://proxyserver:port">
                            </div>
                            <div class="settings-section">
                                <label>수집할 데이터 필드:</label>
                                <div class="checkbox-group">
                                    ${Object.keys(Config.settings.dataFields).map(field => `<label><input type="checkbox" id="field-${field}" ${Config.settings.dataFields[field] ? 'checked' : ''}> ${Utils.capitalize(getFieldName(field))}</label>`).join('')}
                                </div>
                            </div>
                            <div class="settings-section">
                                <label for="retry-attempts-input">재시도 횟수:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">요청 실패 시 재시도할 횟수를 설정하세요.</span>
                                    </span>
                                </label>
                                <input type="number" id="retry-attempts-input" value="${Config.settings.retryAttempts}" min="0" step="1">
                                <label for="retry-delay-input">재시도 딜레이 (ms):
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">재시도 시 대기할 시간을 밀리초 단위로 설정하세요.</span>
                                    </span>
                                </label>
                                <input type="number" id="retry-delay-input" value="${Config.settings.retryDelay}" min="1000" step="500">
                            </div>
                            <div class="settings-section">
                                <label>크롤링 스케줄링:</label>
                                <div class="checkbox-group">
                                    <label><input type="checkbox" id="schedule-enabled" ${Config.settings.schedule.enabled ? 'checked' : ''}> 스케줄링 활성화</label>
                                </div>
                                <div id="schedule-options" style="display: ${Config.settings.schedule.enabled ? 'block' : 'none'}; margin-top: 10px;">
                                    <label for="start-time-input">시작 시간 (HH:MM):
                                        <span class="tooltip">ℹ️
                                            <span class="tooltiptext">크롤링을 시작할 시간을 설정하세요.</span>
                                        </span>
                                    </label>
                                    <input type="time" id="start-time-input" value="${Config.settings.schedule.startTime}">
                                    <label for="interval-input">크롤링 간격 (분):
                                        <span class="tooltip">ℹ️
                                            <span class="tooltiptext">크롤링 작업 간의 간격을 분 단위로 설정하세요.</span>
                                        </span>
                                    </label>
                                    <input type="number" id="interval-input" value="${Config.settings.schedule.interval / 60000}" min="1" step="1">
                                </div>
                            </div>
                            <div class="settings-section">
                                <label>크롤링할 검색 엔진 선택:</label>
                                <div class="checkbox-group">
                                    ${Object.keys(Config.settings.searchEngines).map(engine => `<label><input type="checkbox" id="engine-${engine}" ${Config.settings.searchEngines[engine] ? 'checked' : ''}> ${Utils.capitalize(engine)}</label>`).join('')}
                                </div>
                            </div>
                            <div class="settings-section">
                                <label for="blacklist-input">블랙리스트 도메인:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">크롤링에서 제외할 도메인을 입력하세요. 각 도메인은 줄바꿈으로 구분됩니다.</span>
                                    </span>
                                </label>
                                <textarea id="blacklist-input" rows="5">${Config.settings.blacklist.join('\n')}</textarea>
                            </div>
                            <div class="settings-section">
                                <label for="whitelist-input">화이트리스트 도메인:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">크롤링을 허용할 도메인을 입력하세요. 각 도메인은 줄바꿈으로 구분됩니다.</span>
                                    </span>
                                </label>
                                <textarea id="whitelist-input" rows="5">${Config.settings.whitelist.join('\n')}</textarea>
                            </div>
                            <div class="settings-section">
                                <label for="respect-robots-checkbox">robots.txt 준수:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">대상 사이트의 robots.txt 규칙을 준수하여 크롤링을 진행합니다.</span>
                                    </span>
                                </label>
                                <input type="checkbox" id="respect-robots-checkbox" ${Config.settings.respectRobots ? 'checked' : ''}>
                            </div>
                            <div class="settings-section">
                                <label for="max-concurrent-requests-input">동시 요청 최대 수:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">한 번에 요청할 수 있는 최대 동시 요청 수를 설정하세요.</span>
                                    </span>
                                </label>
                                <input type="number" id="max-concurrent-requests-input" value="${Config.settings.maxConcurrentRequests}" min="1" step="1">
                            </div>
                            <div class="settings-section">
                                <label for="data-visualization-checkbox">데이터 시각화 활성화:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">수집된 데이터를 시각화하여 그래프로 표시합니다.</span>
                                    </span>
                                </label>
                                <input type="checkbox" id="data-visualization-checkbox" ${Config.settings.dataVisualization ? 'checked' : ''}>
                            </div>
                            <div class="settings-section">
                                <label for="enable-captchas-checkbox">CAPTCHA 처리 활성화:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">CAPTCHA가 감지되었을 때 자동 처리를 활성화합니다. (제한적)</span>
                                    </span>
                                </label>
                                <input type="checkbox" id="enable-captchas-checkbox" ${Config.settings.enableCaptchas ? 'checked' : ''}>
                            </div>
                            <div class="settings-section">
                                <label>데이터 내보내기 형식:</label>
                                <div class="checkbox-group">
                                    ${Object.keys(Config.settings.exportFormats).map(format => `<label><input type="checkbox" id="export-${format}" ${Config.settings.exportFormats[format] ? 'checked' : ''}> ${format.toUpperCase()}</label>`).join('')}
                                </div>
                            </div>
                            <div class="settings-section">
                                <label for="advanced-logging-checkbox">고급 로깅 활성화:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">추가적인 로깅 정보를 활성화합니다.</span>
                                    </span>
                                </label>
                                <input type="checkbox" id="advanced-logging-checkbox" ${Config.settings.advancedLogging ? 'checked' : ''}>
                            </div>
                            <div class="settings-section">
                                <label for="custom-keywords-input">사용자 지정 키워드 분류:
                                    <span class="tooltip">ℹ️
                                        <span class="tooltiptext">사용자 지정 키워드를 추가하여 데이터 분류를 향상시킵니다. 형식: 카테고리:키워드1,키워드2,...</span>
                                    </span>
                                </label>
                                <textarea id="custom-keywords-input" rows="5">${Utils.formatCustomKeywords(Config.settings.customKeywords)}</textarea>
                            </div>
                            <button id="save-settings" style="margin-top: 10px; padding: 10px 20px; background-color: #4caf50; color: white; border-radius: 4px; font-size: 14px;">저장</button>
                            <button id="reset-settings" style="margin-top: 10px; padding: 10px 20px; background-color: #f44336; color: white; border-radius: 4px; font-size: 14px;">초기화</button>
                        </div>
                        <div id="tab-previous-crawls" style="display: none;">
                            <h3>이전 크롤링 기록</h3>
                            <button id="clear-previous-crawls" style="padding: 8px 12px; background-color: #f44336; color: white; border-radius: 4px; font-size: 14px; margin-bottom: 10px;">기록 초기화</button>
                            <table>
                                <thead>
                                    <tr>
                                        <th>날짜</th>
                                        <th>크롤링 수</th>
                                        <th>상태</th>
                                    </tr>
                                </thead>
                                <tbody id="previous-crawls-body"></tbody>
                            </table>
                        </div>
                    </div>
                    <div id="html-modal" class="modal">
                        <div class="modal-content">
                            <span class="close">&times;</span>
                            <pre id="modal-html-content"></pre>
                        </div>
                    </div>
                </div>
            `);
            $('body').append(container);

            $('#crawler-minimize').on('click', toggleMinimize);
            $('.crawler-tab').on('click', switchTab);
            $('#save-settings').on('click', saveSettings);
            $('#reset-settings').on('click', resetSettings);
            $('#add-user-agent').on('click', addUserAgent);
            $('#remove-user-agent').on('click', removeUserAgent);
            $('#download-csv').on('click', Download.downloadCSV);
            $('#download-json').on('click', Download.downloadJSON);
            $('#download-xml').on('click', Download.downloadXML);
            $('#download-xls').on('click', Download.downloadXLS);
            $('#search-filter').on('input', filterData);
            $('#schedule-enabled').on('change', toggleScheduleOptions);
            $('#data-visualization-checkbox').on('change', toggleDataVisualization);
            $('#enable-captchas-checkbox').on('change', toggleCaptchaHandling);
            $('.close').on('click', () => $('#html-modal').hide());
            $(window).on('click', function(e) { if (e.target.id === 'html-modal') $('#html-modal').hide(); });
            $('#start-crawling').on('click', Crawling.startCrawling);
            $('#stop-crawling').on('click', Crawling.stopCrawling);
            $('#clear-previous-crawls').on('click', clearPreviousCrawls);
            $('#reset-data').on('click', resetData);
            $(document).on('keydown', handleShortcuts);
            updateUI();
            makeDraggable();
        }

        function toggleMinimize() {
            $('#crawler-content').toggle();
            const isHidden = $('#crawler-content').is(':hidden');
            $('#crawler-minimize').text(isHidden ? '☐' : '─');
            Logger.log(`크롤러 창이 ${isHidden ? '최소화' : '확대'}되었습니다.`, 'info');
        }

        function switchTab() {
            const tab = $(this).data('tab');
            $('.crawler-tab').removeClass('active');
            $(this).addClass('active');
            $('#crawler-content > div').hide();
            $(`#tab-${tab}`).show();
            Config.saveSettings({ currentTab: tab });
        }

        function updateUI() {
            const currentTab = GM_getValue('currentTab', 'logs');
            $(`.crawler-tab[data-tab="${currentTab}"]`).addClass('active');
            $('#crawler-content > div').hide();
            $(`#tab-${currentTab}`).show();
            updateLogs();
            updateDataTable();
            updatePreviousCrawlsUI();
            if (Config.settings.dataVisualization) DataVisualization.renderChart();
        }

        function updateLogs() {
            const logs = Logger.getLogs();
            $('#log-messages').html(logs.map(l => `<div>[${l.timestamp}] [${l.type.toUpperCase()}] ${Utils.escapeHTML(l.message)}</div>`).join(''));
            const progress = GM_getValue('progress', 0);
            const currentPage = GM_getValue('currentPage', 0);
            const totalPages = GM_getValue('totalPages', Config.settings.maxPages);
            $('.progress-bar-fill').css('width', `${progress.toFixed(2)}%`);
            $('.status-text').text(`진행률: ${progress.toFixed(2)}% (${currentPage}/${totalPages} 페이지)`);
            const latestLog = logs[logs.length -1];
            if (latestLog) {
                if (latestLog.type === 'error') {
                    $('#error-message').text(latestLog.message).show();
                    setTimeout(() => { $('#error-message').fadeOut(); }, 5000);
                }
                if (latestLog.type === 'success') {
                    $('#success-message').text(latestLog.message).show();
                    setTimeout(() => { $('#success-message').fadeOut(); }, 5000);
                }
            }
        }

        function updateDataTable() {
            const data = JSON.parse(GM_getValue('data', '[]'));
            const filter = $('#search-filter').val().toLowerCase();
            const filtered = filter ? data.filter(item => Object.keys(Config.settings.dataFields).some(field => Config.settings.dataFields[field] && String(item[field]).toLowerCase().includes(filter))) : data;
            const rows = filtered.map(item => `
                <tr>
                    ${Config.settings.dataFields.name ? `<td>${Utils.escapeHTML(item.name)}</td>` : ''}
                    ${Config.settings.dataFields.url ? `<td><a href="${Utils.escapeHTML(item.url)}" target="_blank">${Utils.escapeHTML(item.url)}</a></td>` : ''}
                    ${Config.settings.dataFields.description ? `<td>${Utils.escapeHTML(item.description)}</td>` : ''}
                    ${Config.settings.dataFields.html ? `<td><button class="view-html" data-html="${encodeURIComponent(item.html)}">📄 보기</button></td>` : ''}
                    ${Config.settings.dataFields.category ? `<td>${Utils.escapeHTML(item.category)}</td>` : ''}
                    ${Config.settings.dataFields.quality ? `<td>${Utils.escapeHTML(item.quality.toString())}</td>` : ''}
                    ${Config.settings.dataFields.language ? `<td>${Utils.escapeHTML(item.language)}</td>` : ''}
                    ${Config.settings.dataFields.wordCount ? `<td>${Utils.escapeHTML(item.wordCount.toString())}</td>` : ''}
                    ${Config.settings.dataFields.metaTags ? `<td>${Utils.escapeHTML(JSON.stringify(item.metaTags))}</td>` : ''}
                    ${Config.settings.dataFields.headings ? `<td>${Utils.escapeHTML(item.headings.join(', '))}</td>` : ''}
                </tr>
            `).join('');
            $('#data-table-body').html(rows);
            $('.view-html').off('click').on('click', function() {
                $('#modal-html-content').text(decodeURIComponent($(this).data('html')));
                $('#html-modal').show();
            });
            if (Config.settings.dataVisualization) DataVisualization.renderChart();
        }

        function updatePreviousCrawlsUI() {
            const previousCrawls = JSON.parse(GM_getValue('previousCrawls', '[]'));
            $('#previous-crawls-body').html(previousCrawls.map(crawl => `
                <tr>
                    <td>${Utils.escapeHTML(crawl.date)}</td>
                    <td>${Utils.escapeHTML(crawl.count.toString())}</td>
                    <td>${Utils.escapeHTML(crawl.status)}</td>
                </tr>
            `).join(''));
        }

        function addUserAgent() {
            const ua = prompt("새 사용자 에이전트를 입력하세요:");
            if (ua) {
                Config.settings.userAgents.push(ua);
                GM_setValue('userAgents', Config.settings.userAgents);
                updateUserAgentSelect();
                Logger.log('새 사용자 에이전트가 추가되었습니다.', 'success');
            }
        }

        function removeUserAgent() {
            const selected = $('#user-agent-select').val();
            if (selected && selected.length) {
                selected.sort((a, b) => b - a).forEach(idx => { Config.settings.userAgents.splice(idx, 1); });
                GM_setValue('userAgents', Config.settings.userAgents);
                Config.settings.userAgentIndex = 0;
                GM_setValue('userAgentIndex', Config.settings.userAgentIndex);
                updateUserAgentSelect();
                Logger.log('선택한 사용자 에이전트가 제거되었습니다.', 'success');
            } else {
                alert('제거할 사용자 에이전트를 선택하세요.');
            }
        }

        function updateUserAgentSelect() {
            $('#user-agent-select').empty();
            Config.settings.userAgents.forEach((ua, idx) => {
                $('#user-agent-select').append(`<option value="${idx}" ${idx === Config.settings.userAgentIndex ? 'selected' : ''}>${ua}</option>`);
            });
        }

        function saveSettings() {
            const delay = parseInt($('#delay-input').val(), 10);
            const delayRandom = parseInt($('#delay-random-input').val(), 10);
            const maxPages = parseInt($('#max-pages-input').val(), 10);
            const timeout = parseInt($('#timeout-input').val(), 10);
            const minWordCount = parseInt($('#min-word-count-input').val(), 10);
            const maxWordCount = parseInt($('#max-word-count-input').val(), 10);
            const maxDepth = parseInt($('#max-depth-input').val(), 10);
            const retryAttempts = parseInt($('#retry-attempts-input').val(), 10);
            const retryDelay = parseInt($('#retry-delay-input').val(), 10);
            const interval = parseInt($('#interval-input').val(), 10) * 60000;
            const isValidSettings = !isNaN(delay) && delay >= 500 &&
                                    !isNaN(delayRandom) && delayRandom >= 0 &&
                                    !isNaN(maxPages) && maxPages >= 1 &&
                                    !isNaN(timeout) && timeout >= 1000 &&
                                    !isNaN(minWordCount) && minWordCount >= 10 &&
                                    !isNaN(maxWordCount) && maxWordCount >= 100 &&
                                    !isNaN(maxDepth) && maxDepth >= 1 &&
                                    !isNaN(retryAttempts) && retryAttempts >= 0 &&
                                    !isNaN(retryDelay) && retryDelay >= 1000 &&
                                    (!Config.settings.schedule.enabled || (!isNaN(interval) && interval >= 60000));

            if (!isValidSettings) {
                $('#error-message').text('설정값이 유효하지 않습니다. 올바른 값을 입력하세요.').show();
                setTimeout(() => { $('#error-message').fadeOut(); }, 5000);
                return;
            }

            Config.settings.delay = delay;
            Config.settings.delayRandom = delayRandom;
            Config.settings.maxPages = maxPages;
            const selectedUA = parseInt($('#user-agent-select').val(), 10);
            Config.settings.userAgentIndex = isNaN(selectedUA) ? 0 : selectedUA;
            Config.settings.contentTypes.include = $('#content-include-input').val().split(',').map(ct => ct.trim()).filter(ct => ct) || Config.DEFAULT_SETTINGS.contentTypes.include;
            Config.settings.contentTypes.exclude = $('#content-exclude-input').val().split(',').map(ct => ct.trim()).filter(ct => ct) || Config.DEFAULT_SETTINGS.contentTypes.exclude;
            Config.settings.timeout = timeout;
            Config.settings.includeKeywords = $('#include-keywords-input').val().split('\n').map(kw => kw.trim()).filter(kw => kw) || Config.DEFAULT_SETTINGS.includeKeywords;
            Config.settings.excludeKeywords = $('#exclude-keywords-input').val().split('\n').map(kw => kw.trim()).filter(kw => kw) || Config.DEFAULT_SETTINGS.excludeKeywords;
            Config.settings.regexInclude = $('#regex-include-input').val().split('\n').map(rx => rx.trim()).filter(rx => rx) || Config.DEFAULT_SETTINGS.regexInclude;
            Config.settings.regexExclude = $('#regex-exclude-input').val().split('\n').map(rx => rx.trim()).filter(rx => rx) || Config.DEFAULT_SETTINGS.regexExclude;
            Config.settings.language = $('#language-select').val() || [];
            Config.settings.minWordCount = minWordCount;
            Config.settings.maxWordCount = maxWordCount;
            Config.settings.maxDepth = maxDepth;
            Config.settings.proxy = $('#proxy-input').val().trim() || Config.DEFAULT_SETTINGS.proxy;
            ['name','url','description','html','category','quality','language','wordCount','metaTags','headings'].forEach(field => {
                Config.settings.dataFields[field] = $(`#field-${field}`).is(':checked');
            });
            Config.settings.retryAttempts = retryAttempts;
            Config.settings.retryDelay = retryDelay;
            Config.settings.schedule.enabled = $('#schedule-enabled').is(':checked');
            Config.settings.schedule.startTime = $('#start-time-input').val() || Config.DEFAULT_SETTINGS.schedule.startTime;
            Config.settings.schedule.interval = Config.settings.schedule.enabled ? (isNaN(interval) ? Config.DEFAULT_SETTINGS.schedule.interval : interval) : Config.DEFAULT_SETTINGS.schedule.interval;
            Object.keys(Config.settings.searchEngines).forEach(engine => { Config.settings.searchEngines[engine] = $(`#engine-${engine}`).is(':checked'); });
            Config.settings.blacklist = $('#blacklist-input').val().split('\n').map(d => d.trim()).filter(d => d) || Config.DEFAULT_SETTINGS.blacklist;
            Config.settings.whitelist = $('#whitelist-input').val().split('\n').map(d => d.trim()).filter(d => d) || Config.DEFAULT_SETTINGS.whitelist;
            Config.settings.respectRobots = $('#respect-robots-checkbox').is(':checked');
            Config.settings.maxConcurrentRequests = parseInt($('#max-concurrent-requests-input').val(), 10) || Config.DEFAULT_SETTINGS.maxConcurrentRequests;
            Config.settings.dataVisualization = $('#data-visualization-checkbox').is(':checked');
            Config.settings.exportFormats.csv = $('#export-csv').is(':checked');
            Config.settings.exportFormats.json = $('#export-json').is(':checked');
            Config.settings.exportFormats.xml = $('#export-xml').is(':checked');
            Config.settings.exportFormats.xls = $('#export-xls').is(':checked');
            Config.settings.advancedLogging = $('#advanced-logging-checkbox').is(':checked');
            Config.settings.enableCaptchas = $('#enable-captchas-checkbox').is(':checked');
            Config.settings.customKeywords = Utils.parseCustomKeywords($('#custom-keywords-input').val());

            Config.saveSettings(Config.settings);
            Crawling.updateCrawlSettings();
            Logger.log('설정이 저장되었습니다.', 'success');

            if (Config.settings.schedule.enabled) {
                Scheduler.scheduleCrawling();
            }

            updateDataTable();
            if (Config.settings.dataVisualization) DataVisualization.renderChart();
        }

        function resetSettings() {
            if (confirm("기본 설정으로 초기화하시겠습니까?")) {
                Config.resetSettings();
                createUI();
                Logger.log('기본 설정으로 초기화되었습니다.', 'success');
            }
        }

        function toggleScheduleOptions() {
            $('#schedule-options').toggle($('#schedule-enabled').is(':checked'));
        }

        function toggleDataVisualization() {
            Config.settings.dataVisualization = $('#data-visualization-checkbox').is(':checked');
            GM_setValue('dataVisualization', Config.settings.dataVisualization);
            $('#data-visualization').toggle(Config.settings.dataVisualization);
            Logger.log('데이터 시각화 설정이 변경되었습니다.', 'success');
            if (Config.settings.dataVisualization) DataVisualization.renderChart();
        }

        function toggleCaptchaHandling() {
            Config.settings.enableCaptchas = $('#enable-captchas-checkbox').is(':checked');
            GM_setValue('enableCaptchas', Config.settings.enableCaptchas);
            Logger.log('CAPTCHA 처리 설정이 변경되었습니다.', 'success');
        }

        function filterData() {
            updateDataTable();
        }

        function clearPreviousCrawls() {
            if (confirm("이전 크롤링 기록을 모두 초기화하시겠습니까?")) {
                GM_setValue('previousCrawls', JSON.stringify([]));
                updatePreviousCrawlsUI();
                Logger.log('이전 크롤링 기록이 초기화되었습니다.', 'success');
            }
        }

        function resetData() {
            if (confirm("모든 크롤링 데이터를 초기화하시겠습니까?")) {
                GM_setValue('data', JSON.stringify([]));
                GM_setValue('visitedUrls', JSON.stringify([]));
                updateDataTable();
                Logger.log('크롤링 데이터가 초기화되었습니다.', 'success');
            }
        }

        function handleShortcuts(e) {
            if (e.ctrlKey) {
                const actions = { '1': 'logs', '2': 'data', '3': 'settings', '4': 'previous-crawls', 's': 'save', 'r': 'reset', 'p': 'start', 'q': 'stop', 'c': 'csv', 'j': 'json', 'x': 'xml', 'l': 'xls' };
                const action = actions[e.key];
                if (action) {
                    e.preventDefault();
                    switch(action) {
                        case 'logs': $('.crawler-tab[data-tab="logs"]').click(); break;
                        case 'data': $('.crawler-tab[data-tab="data"]').click(); break;
                        case 'settings': $('.crawler-tab[data-tab="settings"]').click(); break;
                        case 'previous-crawls': $('.crawler-tab[data-tab="previous-crawls"]').click(); break;
                        case 'save': saveSettings(); break;
                        case 'reset': resetSettings(); break;
                        case 'start': if (!Crawling.isCrawling) Crawling.startCrawling(); break;
                        case 'stop': if (Crawling.isCrawling) Crawling.stopCrawling(); break;
                        case 'csv': Config.settings.exportFormats.csv && Download.downloadCSV(); break;
                        case 'json': Config.settings.exportFormats.json && Download.downloadJSON(); break;
                        case 'xml': Config.settings.exportFormats.xml && Download.downloadXML(); break;
                        case 'xls': Config.settings.exportFormats.xls && Download.downloadXLS(); break;
                    }
                }
            }
        }

        function makeDraggable() {
            const container = document.getElementById('crawler-container');
            const header = document.getElementById('crawler-header');
            let isDragging = false;
            let startX, startY, initialX, initialY;
            header.onmousedown = function(e) {
                isDragging = true;
                startX = e.clientX;
                startY = e.clientY;
                initialX = container.offsetLeft;
                initialY = container.offsetTop;
                document.onmousemove = drag;
                document.onmouseup = stopDrag;
                return false;
            };
            function drag(e) {
                if (isDragging) {
                    let dx = e.clientX - startX;
                    let dy = e.clientY - startY;
                    container.style.left = (initialX + dx) + 'px';
                    container.style.top = (initialY + dy) + 'px';
                }
            }
            function stopDrag() {
                isDragging = false;
                document.onmousemove = null;
                document.onmouseup = null;
            }
        }

        return {
            createUI,
            updateLogs,
            updateDataTable,
            updatePreviousCrawlsUI
        };
    })();

    // Download Module
    const Download = (function() {
        function downloadCSV() {
            const data = JSON.parse(GM_getValue('data', '[]'));
            if (!data.length) { Logger.log('다운로드할 데이터가 없습니다.', 'warning'); return; }
            const headers = Object.keys(Config.settings.dataFields).filter(f => Config.settings.dataFields[f]).map(f => Utils.capitalize(getFieldName(f)));
            const csv = [headers.join(",")].concat(data.map(item => Object.keys(Config.settings.dataFields).filter(f => Config.settings.dataFields[f]).map(f => `"${Utils.escapeCSV(item[f] || '')}"`).join(","))).join("\n");
            const link = document.createElement("a");
            link.href = `data:text/csv;charset=utf-8,${encodeURIComponent(csv)}`;
            link.download = "data.csv";
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            Logger.log('CSV 파일이 다운로드되었습니다.', 'success');
        }

        function downloadJSON() {
            const data = JSON.parse(GM_getValue('data', '[]'));
            if (!data.length) { Logger.log('다운로드할 데이터가 없습니다.', 'warning'); return; }
            const json = JSON.stringify(data, null, 2);
            const blob = new Blob([json], { type: "application/json" });
            const link = document.createElement("a");
            link.href = URL.createObjectURL(blob);
            link.download = "data.json";
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            Logger.log('JSON 파일이 다운로드되었습니다.', 'success');
        }

        function downloadXML() {
            const data = JSON.parse(GM_getValue('data', '[]'));
            if (!data.length) { Logger.log('다운로드할 데이터가 없습니다.', 'warning'); return; }
            let xml = '<?xml version="1.0" encoding="UTF-8"?><data>';
            data.forEach(item => {
                xml += '<entry>';
                if (Config.settings.dataFields.name) xml += `<name>${Utils.escapeXML(item.name || 'No Title')}</name>`;
                if (Config.settings.dataFields.url) xml += `<url>${Utils.escapeXML(item.url || '')}</url>`;
                if (Config.settings.dataFields.description) xml += `<description>${Utils.escapeXML(item.description || 'No Description')}</description>`;
                if (Config.settings.dataFields.html) xml += `<html><![CDATA[${item.html || ''}]]></html>`;
                if (Config.settings.dataFields.category) xml += `<category>${Utils.escapeXML(item.category || '')}</category>`;
                if (Config.settings.dataFields.quality) xml += `<quality>${Utils.escapeXML(item.quality)}</quality>`;
                if (Config.settings.dataFields.language) xml += `<language>${Utils.escapeXML(item.language || 'unknown')}</language>`;
                if (Config.settings.dataFields.wordCount) xml += `<wordCount>${Utils.escapeXML(item.wordCount)}</wordCount>`;
                if (Config.settings.dataFields.metaTags) xml += `<metaTags>${Utils.escapeXML(JSON.stringify(item.metaTags))}</metaTags>`;
                if (Config.settings.dataFields.headings) xml += `<headings>${Utils.escapeXML(item.headings.join('; '))}</headings>`;
                xml += '</entry>';
            });
            xml += '</data>';
            const blob = new Blob([xml], { type: "application/xml" });
            const link = document.createElement("a");
            link.href = URL.createObjectURL(blob);
            link.download = "data.xml";
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            Logger.log('XML 파일이 다운로드되었습니다.', 'success');
        }

        function downloadXLS() {
            const data = JSON.parse(GM_getValue('data', '[]'));
            if (!data.length) { Logger.log('다운로드할 데이터가 없습니다.', 'warning'); return; }
            const sheet = XLSX.utils.json_to_sheet(data.map(item => {
                const entry = {};
                if (Config.settings.dataFields.name) entry['사이트 이름'] = item.name;
                if (Config.settings.dataFields.url) entry['주소'] = item.url;
                if (Config.settings.dataFields.description) entry['설명'] = item.description;
                if (Config.settings.dataFields.html) entry['📄 HTML 내용'] = item.html;
                if (Config.settings.dataFields.category) entry['분류'] = item.category;
                if (Config.settings.dataFields.quality) entry['품질점수'] = item.quality;
                if (Config.settings.dataFields.language) entry['언어'] = item.language;
                if (Config.settings.dataFields.wordCount) entry['단어 수'] = item.wordCount;
                if (Config.settings.dataFields.metaTags) entry['메타 태그'] = JSON.stringify(item.metaTags);
                if (Config.settings.dataFields.headings) entry['헤딩'] = item.headings.join('; ');
                return entry;
            }));
            const wb = XLSX.utils.book_new();
            XLSX.utils.book_append_sheet(wb, sheet, "Data");
            XLSX.writeFile(wb, "data.xlsx");
            Logger.log('XLS 파일이 다운로드되었습니다.', 'success');
        }

        return {
            downloadCSV,
            downloadJSON,
            downloadXML,
            downloadXLS
        };
    })();

    // Data Visualization Module
    const DataVisualization = (function() {
        let chartInstance = null;

        function renderChart() {
            if (!document.getElementById('data-chart').getContext) return;
            const ctx = document.getElementById('data-chart').getContext('2d');
            if (chartInstance) chartInstance.destroy();
            const data = JSON.parse(GM_getValue('data', '[]'));
            const categories = {};
            data.forEach(item => { const cat = item.category || '기타'; categories[cat] = (categories[cat] || 0) + 1; });
            const chartData = { labels: Object.keys(categories), datasets: [{ label: '카테고리별 데이터 수', data: Object.values(categories), backgroundColor: Utils.generateColors(Object.keys(categories).length) }] };
            chartInstance = new Chart(ctx, { type: 'pie', data: chartData, options: { responsive: true } });
        }

        return {
            renderChart
        };
    })();

    // Crawling Module
    const Crawling = (function() {
        let isCrawling = GM_getValue('isCrawling', false);
        let currentPage = parseInt(GM_getValue('currentPage', 1), 10);
        let totalPages = parseInt(GM_getValue('totalPages', Config.settings.maxPages), 10);
        let progress = parseFloat(GM_getValue('progress', 0));
        let currentDepth = parseInt(GM_getValue('currentDepth', 0), 10);
        let activeRequests = 0;
        let requestQueue = [];
        let previousCrawls = JSON.parse(GM_getValue('previousCrawls', '[]'));
        const visitedUrls = new Set(JSON.parse(GM_getValue('visitedUrls', '[]')));
        let data = JSON.parse(GM_getValue('data', '[]'));

        const searchEngineHandlers = {
            google: {
                extractResults: function(doc) {
                    const results = [];
                    doc.querySelectorAll('div.g, div[data-hveid]').forEach(item => {
                        const name = item.querySelector('h3')?.innerText.trim() || 'No Title';
                        const url = item.querySelector('a')?.href || '';
                        const desc = item.querySelector('.aCOpRe, .VwiC3b')?.innerText.trim() || 'No Description';
                        if (isValidURL(url)) results.push({ name, url, description: desc });
                    });
                    return results;
                },
                getNextPageUrl: function(doc) {
                    return doc.querySelector('a#pnnext')?.href || null;
                }
            },
            bing: {
                extractResults: function(doc) {
                    const results = [];
                    doc.querySelectorAll('.b_algo').forEach(item => {
                        const name = item.querySelector('h2')?.innerText.trim() || 'No Title';
                        const url = item.querySelector('h2 a')?.href || '';
                        const desc = item.querySelector('.b_caption p')?.innerText.trim() || 'No Description';
                        if (isValidURL(url)) results.push({ name, url, description: desc });
                    });
                    return results;
                },
                getNextPageUrl: function(doc) {
                    return doc.querySelector('a.sb_pagN')?.href || null;
                }
            },
            yahoo: {
                extractResults: function(doc) {
                    const results = [];
                    doc.querySelectorAll('.Sr').forEach(item => {
                        const name = item.querySelector('h3')?.innerText.trim() || 'No Title';
                        const url = item.querySelector('a')?.href || '';
                        const desc = item.querySelector('.compText a')?.innerText.trim() || 'No Description';
                        if (isValidURL(url)) results.push({ name, url, description: desc });
                    });
                    return results;
                },
                getNextPageUrl: function(doc) {
                    return doc.querySelector('a.next')?.href || null;
                }
            },
            duckduckgo: {
                extractResults: function(doc) {
                    const results = [];
                    doc.querySelectorAll('.result').forEach(item => {
                        const name = item.querySelector('.result__title')?.innerText.trim() || 'No Title';
                        const url = item.querySelector('.result__url')?.href || '';
                        const desc = item.querySelector('.result__snippet')?.innerText.trim() || 'No Description';
                        if (isValidURL(url)) results.push({ name, url, description: desc });
                    });
                    return results;
                },
                getNextPageUrl: function(doc) {
                    return doc.querySelector('a.result--more__btn')?.href || null;
                }
            },
            // 추가 검색 엔진 핸들러...
        };

        function identifySearchEngine() {
            const host = window.location.hostname;
            if (host.includes('google.com')) return 'google';
            if (host.includes('bing.com')) return 'bing';
            if (host.includes('yahoo.com')) return 'yahoo';
            if (host.includes('duckduckgo.com')) return 'duckduckgo';
            // 다른 검색 엔진 추가
            return null;
        }

        async function startCrawling() {
            if (isCrawling) {
                Logger.log('이미 크롤링이 진행 중입니다.', 'warning');
                return;
            }
            if (!Object.values(Config.settings.searchEngines).some(v => v)) {
                Logger.log('사용할 검색 엔진이 하나도 선택되지 않았습니다.', 'error');
                return;
            }
            isCrawling = true;
            GM_setValue('isCrawling', isCrawling);
            Logger.log('크롤링을 시작합니다.', 'info');
            UI.updateLogs();
            currentPage = parseInt(GM_getValue('currentPage', 1), 10);
            totalPages = parseInt(GM_getValue('totalPages', Config.settings.maxPages), 10);
            GM_setValue('totalPages', totalPages);
            currentDepth = parseInt(GM_getValue('currentDepth', 0), 10);
            if (!previousCrawls.some(crawl => crawl.status === '진행 중')) {
                previousCrawls.push({ date: new Date().toLocaleString(), count: 0, status: '진행 중' });
                GM_setValue('previousCrawls', JSON.stringify(previousCrawls));
                UI.updatePreviousCrawlsUI();
            }
            await crawlPage(currentDepth);
        }

        function stopCrawling() {
            if (!isCrawling) {
                Logger.log('크롤링이 진행 중이지 않습니다.', 'warning');
                return;
            }
            isCrawling = false;
            GM_setValue('isCrawling', isCrawling);
            Logger.log('크롤링이 중지되었습니다.', 'warning');
            finalizeCrawl('중지됨');
            UI.updateLogs();
            setButtonStates();
        }

        function setButtonStates() {
            $('#start-crawling').prop('disabled', isCrawling);
            $('#stop-crawling').prop('disabled', !isCrawling);
        }

        async function crawlPage(depth) {
            try {
                while (isCrawling && currentPage <= totalPages && depth <= Config.settings.maxDepth) {
                    Logger.log(`페이지 ${currentPage} 크롤링 시작. (깊이: ${depth})`, 'info');
                    if (Config.settings.respectRobots && !(await Robots.checkRobotsTxt(window.location.href))) {
                        Logger.log(`robots.txt에 의해 크롤링이 금지된 페이지: ${window.location.href}`, 'warning');
                        break;
                    }
                    const searchEngine = identifySearchEngine();
                    if (!searchEngine || !searchEngineHandlers[searchEngine]) {
                        Logger.log(`지원되지 않는 검색 엔진: ${window.location.hostname}`, 'error');
                        break;
                    }
                    const doc = document;
                    const handler = searchEngineHandlers[searchEngine];
                    const results = handler.extractResults(doc);
                    if (!results) { Logger.log('검색 결과를 찾을 수 없습니다.', 'error'); break; }
                    results.forEach(result => {
                        if (!isCrawling) return;
                        const { name, url, description } = result;
                        if (visitedUrls.has(url) || !isValidURL(url) ||
                            (Config.settings.whitelist.length && !Config.settings.whitelist.some(d => url.includes(d))) ||
                            Config.settings.blacklist.some(d => url.includes(d)) ||
                            (Config.settings.includeKeywords.length && !Config.settings.includeKeywords.some(k => url.includes(k) || description.includes(k))) ||
                            (Config.settings.excludeKeywords.some(k => url.includes(k) || description.includes(k))) ||
                            (Config.settings.regexInclude.length && !Config.settings.regexInclude.some(r => new RegExp(r).test(url) || new RegExp(r).test(description))) ||
                            (Config.settings.regexExclude.length && Config.settings.regexExclude.some(r => new RegExp(r).test(url) || new RegExp(r).test(description)))) {
                            return;
                        }
                        requestQueue.push({ url, depth });
                        if (activeRequests >= Config.settings.maxConcurrentRequests) return;
                    });
                    if (previousCrawls.length && previousCrawls[previousCrawls.length -1].status === '진행 중') {
                        previousCrawls[previousCrawls.length -1].count += results.length;
                        GM_setValue('previousCrawls', JSON.stringify(previousCrawls));
                        UI.updatePreviousCrawlsUI();
                    }
                    await processQueue();
                    progress = (currentPage / totalPages) * 100;
                    GM_setValue('progress', progress);
                    UI.updateLogs();
                    if (currentPage < totalPages && isCrawling) {
                        const nextUrl = handler.getNextPageUrl(doc);
                        if (nextUrl) {
                            currentPage++;
                            GM_setValue('currentPage', currentPage);
                            depth++;
                            GM_setValue('currentDepth', depth);
                            Logger.log(`다음 페이지로 이동: ${currentPage}`, 'info');
                            await Utils.delay(Config.settings.delay + Math.random() * Config.settings.delayRandom);
                            const nextPageContent = await Fetch.fetchPageContent(nextUrl);
                            if (nextPageContent) {
                                updatePageContent(nextPageContent);
                                continue;
                            } else {
                                Logger.log('다음 페이지 내용을 가져올 수 없습니다.', 'error');
                                break;
                            }
                        } else { Logger.log('다음 페이지 링크를 찾을 수 없습니다.', 'error'); break; }
                    } else { break; }
                }
                if (isCrawling && currentPage > totalPages && activeRequests === 0 && requestQueue.length === 0) {
                    Logger.log('크롤링이 완료되었습니다.', 'success');
                    finalizeCrawl('완료됨');
                    isCrawling = false;
                    GM_setValue('isCrawling', isCrawling);
                    setButtonStates();
                    GM_setValue('progress', 100);
                    UI.updateLogs();
                }
            } catch (error) {
                Logger.log(`크롤링 중 오류 발생: ${error.message}`, 'error');
                finalizeCrawl('오류 발생');
            }
        }

        async function processQueue() {
            while (requestQueue.length && activeRequests < Config.settings.maxConcurrentRequests && isCrawling) {
                const { url, depth } = requestQueue.shift();
                activeRequests++;
                handleRequest(url, depth).finally(() => { activeRequests--; processQueue(); });
            }
        }

        async function handleRequest(url, depth) {
            if (!isCrawling || visitedUrls.has(url)) return;
            try {
                const response = await Fetch.fetchWithRetry(url, Config.settings.retryAttempts, Config.settings.retryDelay);
                const contentType = Fetch.getContentType(response);
                if (!Fetch.isContentTypeAllowed(contentType, Config.settings.contentTypes)) {
                    Logger.log(`허용되지 않은 콘텐츠 유형(${contentType})의 URL: ${url}`, 'warning');
                    return;
                }
                const parser = new DOMParser();
                const doc = parser.parseFromString(response.responseText, 'text/html');
                const text = doc.body ? doc.body.innerText : '';
                if (Config.settings.enableCaptchas && Robots.detectCaptcha(doc)) { await Robots.handleCaptcha(url); return; }
                const wc = text.split(/\s+/).length;
                if (wc < Config.settings.minWordCount || wc > Config.settings.maxWordCount) {
                    Logger.log(`단어 수 조건에 맞지 않는 URL: ${url} (${wc} 단어)`, 'warning');
                    return;
                }
                const quality = Evaluate.evaluateContentQuality(text);
                const lang = doc.documentElement.lang || 'unknown';
                const category = Classifier.classifyKeywords(text);
                const meta = Extractor.extractMetaTags(doc);
                const headings = Extractor.extractHeadings(doc);
                if (category !== '기타' || quality > 100) {
                    const entry = { url, quality, language: lang, wordCount: wc, category };
                    if (Config.settings.dataFields.name) entry.name = Extractor.extractName(doc) || 'No Title';
                    if (Config.settings.dataFields.description) entry.description = Extractor.extractDescription(doc) || 'No Description';
                    if (Config.settings.dataFields.html) entry.html = response.responseText;
                    if (Config.settings.dataFields.metaTags) entry.metaTags = meta;
                    if (Config.settings.dataFields.headings) entry.headings = headings;
                    data.push(entry);
                    GM_setValue('data', JSON.stringify(data));
                    UI.updateDataTable();
                    Logger.log(`데이터 수집 완료: ${url}`, 'success');
                    if (Config.settings.dataVisualization) DataVisualization.renderChart();
                }
                if (depth < Config.settings.maxDepth) {
                    const links = Extractor.extractLinks(doc);
                    links.forEach(link => { if (!visitedUrls.has(link) && isValidURL(link)) requestQueue.push({ url: link, depth: depth + 1 }); });
                }
            } catch (error) {
                Logger.log(`데이터 수집 실패: ${url} - ${error.message}`, 'error');
            }
            visitedUrls.add(url);
            GM_setValue('visitedUrls', JSON.stringify([...visitedUrls]));
        }

        function finalizeCrawl(status) {
            if (previousCrawls.length) {
                previousCrawls[previousCrawls.length -1].status = status;
                GM_setValue('previousCrawls', JSON.stringify(previousCrawls));
                UI.updatePreviousCrawlsUI();
            }
        }

        function updateCrawlSettings() {
            // Placeholder for any settings update needed during crawling
        }

        return {
            startCrawling,
            stopCrawling,
            isCrawling
        };
    })();

    // Fetch Module
    const Fetch = (function() {
        async function fetchWithRetry(url, attempts, delayMs) {
            for (let i = 1; i <= attempts; i++) {
                try {
                    return await GM_xmlhttpRequestPromise(url);
                } catch (e) {
                    Logger.log(`요청 실패 (${i}/${attempts}): ${url} - ${e.message}`, 'warning');
                    if (i < attempts) await Utils.delay(delayMs);
                }
            }
            throw new Error('모든 재시도 시도가 실패했습니다.');
        }

        function GM_xmlhttpRequestPromise(url) {
            return new Promise((resolve, reject) => {
                let reqUrl = Config.settings.proxy ? `${Config.settings.proxy}?url=${encodeURIComponent(url)}` : url;
                GM_xmlhttpRequest({
                    method: "GET",
                    url: reqUrl,
                    headers: { 'User-Agent': getCurrentUserAgent(), 'Accept': '*/*' },
                    timeout: Config.settings.timeout,
                    onload: res => (res.status >= 200 && res.status < 300) ? resolve(res) : reject(new Error(`HTTP ${res.status}`)),
                    onerror: () => reject(new Error('Request failed')),
                    ontimeout: () => reject(new Error('Request timed out'))
                });
            });
        }

        function getCurrentUserAgent() {
            return Config.settings.userAgents[Math.floor(Math.random() * Config.settings.userAgents.length)];
        }

        function getContentType(res) {
            return res.responseHeaders.split('\n').find(h => h.toLowerCase().startsWith('content-type'))?.split(':')[1].trim().split(';')[0] || '';
        }

        function isContentTypeAllowed(ct, contentTypes) {
            if (contentTypes.include.length && !contentTypes.include.includes(ct)) return false;
            if (contentTypes.exclude.includes(ct)) return false;
            return true;
        }

        async function fetchPageContent(url) {
            try {
                const response = await fetchWithRetry(url, Config.settings.retryAttempts, Config.settings.retryDelay);
                return response.responseText;
            } catch (error) {
                Logger.log(`페이지 내용 가져오기 실패: ${url} - ${error.message}`, 'error');
                return null;
            }
        }

        return {
            fetchWithRetry,
            getCurrentUserAgent,
            getContentType,
            isContentTypeAllowed,
            fetchPageContent
        };
    })();

    // Robots Module
    const Robots = (function() {
        async function checkRobotsTxt(url) {
            try {
                const parsed = new URL(url);
                const robots = `${parsed.origin}/robots.txt`;
                const res = await Fetch.fetchWithRetry(robots, 2, 2000);
                return new RobotsParser(res.responseText).isAllowed(url, Fetch.getCurrentUserAgent());
            } catch {
                Logger.log('robots.txt 확인 실패, 기본 허용.', 'warning');
                return true;
            }
        }

        function detectCaptcha(doc) {
            return doc.querySelector('iframe[src*="captcha"], form[action*="captcha"]') !== null;
        }

        async function handleCaptcha(url) {
            Logger.log(`CAPTCHA 감지됨: ${url}`, 'warning');
            // CAPTCHA 처리 로직 추가
        }

        class RobotsParser {
            constructor(text) { this.rules = []; this.parse(text); }
            parse(text) {
                let agents = [];
                text.split('\n').forEach(line => {
                    const [key, ...val] = line.split(':').map(s => s.trim());
                    if (!key) return;
                    if (key.toLowerCase() === 'user-agent') { agents = [val.join(':').toLowerCase()]; }
                    else if (['disallow','allow'].includes(key.toLowerCase())) this.rules.push({ agents, type: key.toLowerCase(), path: val.join(':') });
                });
            }
            isAllowed(url, ua) {
                const path = new URL(url).pathname;
                let allowed = true;
                this.rules.forEach(rule => {
                    if (rule.agents.includes('*') || rule.agents.includes(ua.toLowerCase())) {
                        if (rule.type === 'disallow' && path.startsWith(rule.path)) allowed = false;
                        if (rule.type === 'allow' && path.startsWith(rule.path)) allowed = true;
                    }
                });
                return allowed;
            }
        }

        return {
            checkRobotsTxt,
            detectCaptcha,
            handleCaptcha
        };
    })();

    // Extractor Module
    const Extractor = (function() {
        function extractMetaTags(doc) {
            const metas = {};
            doc.querySelectorAll('meta[name], meta[property]').forEach(meta => {
                const name = meta.getAttribute('name') || meta.getAttribute('property');
                const content = meta.getAttribute('content');
                if (name && content) metas[name.toLowerCase()] = content;
            });
            return metas;
        }

        function extractHeadings(doc) {
            const hs = [];
            for (let i = 1; i <= 6; i++) doc.querySelectorAll(`h${i}`).forEach(h => hs.push(h.innerText.trim()));
            return hs;
        }

        function extractName(doc) { return doc.querySelector('title')?.innerText || 'No Title'; }
        function extractDescription(doc) { return doc.querySelector('meta[name="description"]')?.getAttribute('content') || 'No Description'; }
        function extractLinks(doc) { return Array.from(doc.querySelectorAll('a[href]')).map(a => a.href).filter(isValidURL); }

        return {
            extractMetaTags,
            extractHeadings,
            extractName,
            extractDescription,
            extractLinks
        };
    })();

    // Classifier Module
    const Classifier = (function() {
        function classifyKeywords(text) {
            const kws = combineKeywords();
            for (const [cat, keywords] of Object.entries(kws)) {
                if (keywords.some(kw => text.includes(kw))) return cat;
            }
            return '기타';
        }

        function combineKeywords() {
            const standard = {
                '교육': ['학교', '교육', '학생', '교수', '강의'],
                '기술': ['기술', '소프트웨어', '하드웨어', '프로그래밍', '개발'],
                '뉴스': ['뉴스', '속보', '기사', '보도'],
                '엔터테인먼트': ['영화', '음악', '연예', '게임', '드라마'],
                '스포츠': ['축구', '야구', '농구', '경기', '토너먼트'],
                '비즈니스': ['비즈니스', '경제', '주식', '투자', '시장'],
                '건강': ['건강', '의료', '헬스', '운동', '영양'],
                '과학': ['과학', '연구', '실험', '발견', '이론'],
                '문학': ['소설', '시', '문학', '작가', '책'],
                '여행': ['여행', '관광', '숙박', '비행기', '여권']
            };
            const custom = Config.settings.customKeywords || {};
            for (const [cat, kws] of Object.entries(custom)) {
                standard[cat] = standard[cat] ? [...new Set([...standard[cat], ...kws])] : kws;
            }
            return standard;
        }

        return {
            classifyKeywords
        };
    })();

    // Evaluate Module
    const Evaluate = (function() {
        function evaluateContentQuality(text) { return text.split(/\s+/).length; }

        return {
            evaluateContentQuality
        };
    })();

    // Robots Check Module
    const Robots = (function() {
        return {};
    })();

    // Scheduler Module
    const Scheduler = (function() {
        let scheduleInterval = null;

        function scheduleCrawling() {
            clearInterval(scheduleInterval);
            const now = new Date();
            const [startHour, startMinute] = Config.settings.schedule.startTime.split(':').map(Number);
            const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), startHour, startMinute, 0);
            if (startTime < now) startTime.setDate(startTime.getDate() + 1);
            const initialDelay = startTime - now;
            setTimeout(() => {
                Crawling.startCrawling();
                scheduleInterval = setInterval(Crawling.startCrawling, Config.settings.schedule.interval);
            }, initialDelay);
            Logger.log(`크롤링 스케줄이 설정되었습니다. 시작 시간: ${startTime.toLocaleString()}, 간격: ${Config.settings.schedule.interval / 60000}분`, 'info');
        }

        return {
            scheduleCrawling
        };
    })();

    // Initialization
    function init() {
        UI.createUI();
        UI.updateLogs();
        UI.updateDataTable();
        UI.updatePreviousCrawlsUI();
        if (Config.settings.dataVisualization) DataVisualization.renderChart();
        if (Crawling.isCrawling) {
            Logger.log('크롤링 재개됨.', 'info');
            Crawling.startCrawling();
        } else {
            Logger.log('크롤러 초기화됨.', 'info');
        }
    }

    window.addEventListener('load', init);
})();