전반적인 구조 및 모듈화
장점:
모듈화: 코드가 여러 개의 즉시 실행 함수 (IIFE)로 잘 나누어져 있으며, 각각의 기능별로 Config, DB, Logger, Utils, UI, Download, DataVisualization, Crawling, Fetch, Robots, Extractor, Classifier, Evaluate, Scheduler 등의 모듈로 구분되어 있습니다. 이는 유지보수성과 가독성을 높이는 좋은 접근 방식입니다.
비동기 처리: async/await를 활용하여 비동기 작업을 효율적으로 처리하고 있습니다.
설정 저장: GM_setValue와 GM_getValue를 이용한 설정 저장 및 불러오기가 잘 구현되어 있습니다.
개선할 점:
모듈화 심화: 각 모듈을 별도의 파일로 분리하거나, ES6 모듈을 활용하여 더욱 명확하게 분리하면 코드의 관리가 더 용이해질 수 있습니다. Tampermonkey에서는 이 기능이 제한적일 수 있지만, 가능하다면 시도해보시는 것도 좋습니다.
2. UI 구현
장점:
풍부한 UI: 사용자 인터페이스가 풍부하게 구현되어 있어 사용자 경험이 좋습니다. 설정, 로그, 데이터 보기, 이전 크롤링 기록 등을 탭으로 구분하여 직관적으로 접근할 수 있게 했습니다.
스타일링: CSS가 잘 적용되어 있어 UI가 깔끔하고 사용하기 편리합니다.
드래그 가능: UI 컨테이너가 드래그 가능하게 구현되어 있어 사용자가 원하는 위치로 쉽게 이동할 수 있습니다.
개선할 점:
반응형 디자인: 현재 고정된 크기와 위치로 설정되어 있어서 다양한 화면 크기에서의 최적화가 부족할 수 있습니다. 미디어 쿼리를 추가하거나 유연한 레이아웃을 고려해보세요.
접근성: 폰트 크기, 대비 비율, 키보드 접근성 등을 고려하여 접근성을 향상시킬 수 있습니다.
3. 크롤링 로직 및 기능
장점:
다양한 검색 엔진 지원: 구글, 빙, 야후, 덕덕고 등 다양한 검색 엔진을 지원하여 유연한 크롤링이 가능합니다.
로봇 배제 표준(robots.txt) 준수: Robots 모듈을 통해 robots.txt를 확인하고 준수하는 기능이 포함되어 있습니다.
CAPTCHA 감지 및 처리: CAPTCHA가 감지되면 사용자에게 알림을 주는 기능이 구현되어 있습니다.
재시도 메커니즘: 요청 실패 시 재시도할 수 있는 로직이 포함되어 있어 안정성을 높였습니다.
개선할 점:
검색 엔진 핸들러 확장성: 현재 구글, 빙, 덕덕고만 핸들링하는 것으로 보입니다. 추가적인 검색 엔진 핸들러를 구현할 때 모듈화된 구조를 더 강화하여 쉽게 확장할 수 있도록 고려해보세요.
링크 추출 최적화: 현재는 특정 CSS 셀렉터를 사용하여 링크를 추출하고 있습니다. 검색 엔진의 DOM 구조 변경 시 핸들러가 작동하지 않을 수 있으므로, 이를 더 동적으로 처리하거나, 검색 엔진별로 DOM 구조 변화에 대응할 수 있는 유연성을 추가하는 것이 좋습니다.
동시 요청 제한: maxConcurrentRequests를 설정하여 동시 요청 수를 제한하고 있지만, 실제로 큐 관리가 최적화되었는지 확인이 필요합니다. 특히 비동기 요청이 많아질 경우 큐 관리 로직을 더욱 견고하게 만들 필요가 있습니다.
4. 데이터 저장 및 관리
장점:
IndexedDB 활용: idb 라이브러리를 사용하여 브라우저 내 IndexedDB에 데이터를 효율적으로 저장하고 관리합니다. 이는 대용량 데이터를 다루기에 적합합니다.
데이터 시각화: Chart.js를 활용하여 수집된 데이터를 시각화하는 기능이 구현되어 있어 데이터 분석에 유용합니다.
데이터 내보내기 기능: CSV, JSON, XML, XLS 등 다양한 형식으로 데이터를 내보낼 수 있어 유용합니다.
개선할 점:
데이터 중복 방지: visited 스토어를 통해 방문한 URL을 추적하고 있지만, 데이터 스토어에도 중복 데이터가 추가되지 않도록 추가적인 검증 로직을 고려해보세요.
데이터 쿼리 최적화: 데이터가 많아질 경우, IndexedDB의 인덱스를 효과적으로 활용하여 쿼리 성능을 최적화할 필요가 있습니다.
5. 설정 및 사용자 경험
장점:
풍부한 설정 옵션: 크롤링 딜레이, 사용자 에이전트, 콘텐츠 유형 필터링, 키워드 포함/제외, 언어 필터링, 크롤링 깊이 등 다양한 설정을 제공하여 사용자가 원하는 대로 크롤러를 맞춤 설정할 수 있습니다.
설정 저장 및 초기화: 설정을 저장하고 초기화할 수 있는 기능이 잘 구현되어 있습니다.
개선할 점:
설정 유효성 검사: 일부 설정 입력값에 대해 유효성 검사를 하고 있지만, 더 세밀한 검사가 필요할 수 있습니다. 예를 들어, 프록시 입력값이 올바른 URL 형식인지 확인하는 등의 검증을 강화하세요.
설정 사용자 경험 향상: 사용자 인터페이스 상에서 설정을 보다 직관적으로 조작할 수 있도록, 예를 들어 슬라이더, 토글 스위치 등을 활용하여 설정 변경을 쉽게 할 수 있도록 고려해보세요.
6. 로깅 및 피드백
장점:
로깅 시스템: Logger 모듈을 통해 로그를 기록하고, 사용자에게 실시간으로 피드백을 제공합니다. 이는 디버깅과 모니터링에 유용합니다.
고급 로깅 옵션: 고급 로깅을 활성화할 수 있는 옵션이 있어, 필요에 따라 상세 로그를 확인할 수 있습니다.
개선할 점:
로그 보관 및 정리: 로그가 계속 축적될 경우 성능 저하나 저장 공간 문제를 일으킬 수 있습니다. 일정 기간이 지난 로그를 자동으로 삭제하거나, 로그의 최대 개수를 제한하는 기능을 고려해보세요.
로깅 레벨 세분화: 현재 info, warning, error, success 정도로 분류되어 있는데, 필요에 따라 더 세분화된 로깅 레벨을 추가하거나, 사용자에게 로깅 레벨을 설정할 수 있는 옵션을 제공할 수 있습니다.
7. 보안 및 프라이버시
장점:
프록시 지원: proxy 설정을 통해 IP 차단을 우회할 수 있어 유용합니다.
개선할 점:
개인 정보 보호: 크롤링한 데이터를 저장하고 다운로드할 수 있는 기능이 있으므로, 민감한 정보가 포함되지 않도록 주의가 필요합니다. 특히, 사용자 에이전트나 프록시 설정 시 보안에 취약해질 수 있는 점을 고려해야 합니다.
CORS 및 보안 헤더: Tampermonkey 스크립트는 브라우저의 CORS 정책을 우회할 수 있지만, 이는 잠재적인 보안 위험을 초래할 수 있습니다. @connect 메타 태그와 함께 어떤 도메인과 통신할 수 있는지 명확히 설정하여 보안을 강화하세요.
8. 최적화 및 성능
장점:
성능 최적화: 크롤링 딜레이, 동시 요청 제한 등을 통해 서버에 과부하를 걸지 않도록 조절할 수 있습니다.
개선할 점:
메모리 관리: 대용량 데이터를 다룰 경우, 메모리 누수나 성능 저하를 방지하기 위한 추가적인 최적화가 필요할 수 있습니다. 예를 들어, 대용량 데이터를 처리할 때는 스트림 처리나 배치 처리를 고려해보세요.
비동기 흐름 관리: processQueue 함수에서 큐를 처리하는 로직이 잘 구현되어 있지만, 예외 상황이나 비정상적인 종료 시 동기화 문제가 발생하지 않도록 철저히 테스트해야 합니다.
9. 기타 고려 사항
국제화 및 현지화:
현재 UI 텍스트가 주로 한국어로 작성되어 있습니다. 다양한 언어 사용자에게 제공할 계획이라면, 국제화(i18n) 지원을 고려할 수 있습니다.
테스트 및 디버깅:
크롤러가 다양한 상황에서 안정적으로 동작하는지, 각 모듈 간의 상호작용이 원활한지 철저한 테스트가 필요합니다. 특히, 검색 엔진 DOM 구조 변화에 따른 대응 여부를 주기적으로 점검하세요.
결론
전체적으로 매우 포괄적이고 기능이 풍부한 웹 크롤러 사용자 스크립트입니다. 모듈화와 비동기 처리를 통해 유지보수성과 성능을 고려한 점이 인상적입니다. 다만, 추가적인 최적화와 보안 강화, 사용자 경험 개선을 통해 더욱 완성도 높은 스크립트로 발전시킬 수 있을 것입니다. 지속적인 테스트와 사용자 피드백을 통해 개선해나가시면 좋겠습니다.

필요하시다면 특정 부분에 대한 자세한 리뷰나 개선 방안에 대해서도 추가로 논의할 수 있습니다!             이걸 바탕으로 밑에걸 개선해 일단 최대한 니가 쓸 수 있는데 까지는 아무말없이 써봐. 그냥 코드만 써. ㅇㅇ                                                 // ==UserScript==
// @name         Advanced Web Crawler Enhanced Pro Optimized v6.2.0
// @namespace    http://tampermonkey.net/
// @version      6.2.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
// @require      https://cdnjs.cloudflare.com/ajax/libs/idb/7.0.1/idb.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.2 (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: [],
            regexInclude: [],
            regexExclude: [],
            language: [],
            minWordCount: 100,
            maxWordCount: 10000,
            maxDepth: 2,
            proxy: '',
            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: {},
            currentTab: 'logs',
            isCrawling: false,
            progress: 0,
            currentPage: 1,
            currentDepth: 0
        };

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

        function getSavedSettings() {
            return GM_getValue('settings', DEFAULT_SETTINGS);
        }

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

        function resetSettings() {
            GM_setValue('settings', DEFAULT_SETTINGS);
            Object.assign(settings, DEFAULT_SETTINGS);
        }

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

    // IndexedDB Setup
    const DB = (function() {
        const dbPromise = idb.openDB('crawler-db', 1, {
            upgrade(db) {
                if (!db.objectStoreNames.contains('data')) {
                    const store = db.createObjectStore('data', { keyPath: 'url' });
                    store.createIndex('category', 'category');
                }
                if (!db.objectStoreNames.contains('logs')) {
                    const store = db.createObjectStore('logs', { autoIncrement: true });
                    store.createIndex('type', 'type');
                }
                if (!db.objectStoreNames.contains('visited')) {
                    db.createObjectStore('visited', { keyPath: 'url' });
                }
                if (!db.objectStoreNames.contains('previousCrawls')) {
                    db.createObjectStore('previousCrawls', { autoIncrement: true });
                }
            }
        });

        async function addData(entry) {
            const db = await dbPromise;
            await db.put('data', entry);
        }

        async function getAllData() {
            const db = await dbPromise;
            return await db.getAll('data');
        }

        async function clearData() {
            const db = await dbPromise;
            await db.clear('data');
        }

        async function addLog(log) {
            const db = await dbPromise;
            await db.add('logs', log);
        }

        async function getAllLogs() {
            const db = await dbPromise;
            return await db.getAll('logs');
        }

        async function clearLogs() {
            const db = await dbPromise;
            await db.clear('logs');
        }

        async function addVisited(url) {
            const db = await dbPromise;
            await db.put('visited', { url });
        }

        async function isVisited(url) {
            const db = await dbPromise;
            const result = await db.get('visited', url);
            return !!result;
        }

        async function clearVisited() {
            const db = await dbPromise;
            await db.clear('visited');
        }

        async function addPreviousCrawl(crawl) {
            const db = await dbPromise;
            await db.add('previousCrawls', crawl);
        }

        async function getPreviousCrawls() {
            const db = await dbPromise;
            return await db.getAll('previousCrawls');
        }

        async function clearPreviousCrawls() {
            const db = await dbPromise;
            await db.clear('previousCrawls');
        }

        return {
            addData,
            getAllData,
            clearData,
            addLog,
            getAllLogs,
            clearLogs,
            addVisited,
            isVisited,
            clearVisited,
            addPreviousCrawl,
            getPreviousCrawls,
            clearPreviousCrawls
        };
    })();

    // Logger Module
    const Logger = (function() {
        async function log(message, type = 'info') {
            const timestamp = new Date().toLocaleString();
            const logEntry = { timestamp, message, type };
            await DB.addLog(logEntry);
            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] || '');
            }
        }

        async function getLogs() {
            return await DB.getAllLogs();
        }

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

    // Utility Module
    const Utils = (function() {
        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]);
        }

        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 {
            escape,
            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: hidden;
                    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; }
                .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 {
                    color: red;
                    font-weight: bold;
                    margin-top: 10px;
                }
                .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', saveSettingsHandler);
            $('#reset-settings').on('click', resetSettingsHandler);
            $('#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() {
            (async () => {
                const currentTab = Config.settings.currentTab || 'logs';
                $(`.crawler-tab[data-tab="${currentTab}"]`).addClass('active');
                $('#crawler-content > div').hide();
                $(`#tab-${currentTab}`).show();
                await UI.updateLogs();
                await UI.updateDataTable();
                await UI.updatePreviousCrawlsUI();
                if (Config.settings.dataVisualization) DataVisualization.renderChart();
            })();
        }

        function updateLogs() {
            return new Promise(async (resolve) => {
                const logs = await Logger.getLogs();
                $('#log-messages').html(logs.map(l => `<div>[${l.timestamp}] [${l.type.toUpperCase()}] ${Utils.escape(l.message)}</div>`).join(''));
                $('.progress-bar-fill').css('width', `${Config.settings.progress.toFixed(2)}%`);
                $('.status-text').text(`진행률: ${Config.settings.progress.toFixed(2)}% (${Config.settings.currentPage}/${Config.settings.maxPages} 페이지)`);
                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);
                    }
                }
                resolve();
            });
        }

        async function updateDataTable() {
            const data = await DB.getAllData();
            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.escape(item.name)}</td>` : ''}
                    ${Config.settings.dataFields.url ? `<td><a href="${Utils.escape(item.url)}" target="_blank">${Utils.escape(item.url)}</a></td>` : ''}
                    ${Config.settings.dataFields.description ? `<td>${Utils.escape(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.escape(item.category)}</td>` : ''}
                    ${Config.settings.dataFields.quality ? `<td>${Utils.escape(item.quality.toString())}</td>` : ''}
                    ${Config.settings.dataFields.language ? `<td>${Utils.escape(item.language)}</td>` : ''}
                    ${Config.settings.dataFields.wordCount ? `<td>${Utils.escape(item.wordCount.toString())}</td>` : ''}
                    ${Config.settings.dataFields.metaTags ? `<td>${Utils.escape(JSON.stringify(item.metaTags))}</td>` : ''}
                    ${Config.settings.dataFields.headings ? `<td>${Utils.escape(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();
        }

        async function updatePreviousCrawlsUI() {
            const previousCrawls = await DB.getPreviousCrawls();
            $('#previous-crawls-body').html(previousCrawls.map(crawl => `
                <tr>
                    <td>${Utils.escape(crawl.date)}</td>
                    <td>${Utils.escape(crawl.count.toString())}</td>
                    <td>${Utils.escape(crawl.status)}</td>
                </tr>
            `).join(''));
        }

        function addUserAgent() {
            const ua = prompt("새 사용자 에이전트를 입력하세요:");
            if (ua) {
                Config.settings.userAgents.push(ua);
                Config.saveSettings({ 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); });
                Config.settings.userAgentIndex = 0;
                Config.saveSettings({
                    userAgents: Config.settings.userAgents,
                    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 saveSettingsHandler() {
            (async () => {
                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.settings.contentTypes.exclude = $('#content-exclude-input').val().split(',').map(ct => ct.trim()).filter(ct => ct);
                Config.settings.timeout = timeout;
                Config.settings.includeKeywords = $('#include-keywords-input').val().split('\n').map(kw => kw.trim()).filter(kw => kw);
                Config.settings.excludeKeywords = $('#exclude-keywords-input').val().split('\n').map(kw => kw.trim()).filter(kw => kw);
                Config.settings.regexInclude = $('#regex-include-input').val().split('\n').map(rx => rx.trim()).filter(rx => rx);
                Config.settings.regexExclude = $('#regex-exclude-input').val().split('\n').map(rx => rx.trim()).filter(rx => rx);
                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.settings.whitelist = $('#whitelist-input').val().split('\n').map(d => d.trim()).filter(d => d);
                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();
                } else {
                    Scheduler.clearSchedule();
                }

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

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

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

        function toggleDataVisualization() {
            Config.settings.dataVisualization = $('#data-visualization-checkbox').is(':checked');
            Config.saveSettings({ 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');
            Config.saveSettings({ enableCaptchas: Config.settings.enableCaptchas });
            Logger.log('CAPTCHA 처리 설정이 변경되었습니다.', 'success');
        }

        function filterData() {
            updateDataTable();
        }

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

        async function resetData() {
            if (confirm("모든 크롤링 데이터를 초기화하시겠습니까?")) {
                await DB.clearData();
                await DB.clearVisited();
                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': saveSettingsHandler(); break;
                        case 'reset': resetSettingsHandler(); 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;
            }
        }

        function getFieldName(field) {
            const names = {
                name: '이름',
                url: '주소',
                description: '설명',
                html: 'HTML 내용',
                category: '분류',
                quality: '품질점수',
                language: '언어',
                wordCount: '단어 수',
                metaTags: '메타 태그',
                headings: '헤딩'
            };
            return names[field] || field;
        }

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

    // Download Module
    const Download = (function() {
        async function downloadCSV() {
            const data = await DB.getAllData();
            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 csvRows = [headers.join(",")];
            data.forEach(item => {
                let row = [];
                Object.keys(Config.settings.dataFields).forEach(field => {
                    if (Config.settings.dataFields[field]) row.push(`"${Utils.escape(item[field] || '', 'csv')}"`);
                });
                csvRows.push(row.join(","));
            });
            const csvContent = csvRows.join("\n");
            const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
            const link = document.createElement("a");
            const url = URL.createObjectURL(blob);
            link.href = url;
            link.download = "data.csv";
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
            Logger.log('CSV 파일이 다운로드되었습니다.', 'success');
        }

        async function downloadJSON() {
            const data = await DB.getAllData();
            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");
            const url = URL.createObjectURL(blob);
            link.href = url;
            link.download = "data.json";
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
            Logger.log('JSON 파일이 다운로드되었습니다.', 'success');
        }

        async function downloadXML() {
            const data = await DB.getAllData();
            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.escape(item.name || 'No Title', 'xml')}</name>`;
                if (Config.settings.dataFields.url) xml += `<url>${Utils.escape(item.url || '', 'xml')}</url>`;
                if (Config.settings.dataFields.description) xml += `<description>${Utils.escape(item.description || 'No Description', 'xml')}</description>`;
                if (Config.settings.dataFields.html) xml += `<html><![CDATA[${item.html || ''}]]></html>`;
                if (Config.settings.dataFields.category) xml += `<category>${Utils.escape(item.category || '', 'xml')}</category>`;
                if (Config.settings.dataFields.quality) xml += `<quality>${Utils.escape(item.quality)}</quality>`;
                if (Config.settings.dataFields.language) xml += `<language>${Utils.escape(item.language || 'unknown', 'xml')}</language>`;
                if (Config.settings.dataFields.wordCount) xml += `<wordCount>${Utils.escape(item.wordCount)}</wordCount>`;
                if (Config.settings.dataFields.metaTags) xml += `<metaTags>${Utils.escape(JSON.stringify(item.metaTags), 'xml')}</metaTags>`;
                if (Config.settings.dataFields.headings) xml += `<headings>${Utils.escape(item.headings.join('; '), 'xml')}</headings>`;
                xml += '</entry>';
            });
            xml += '</data>';
            const blob = new Blob([xml], { type: "application/xml" });
            const link = document.createElement("a");
            const url = URL.createObjectURL(blob);
            link.href = url;
            link.download = "data.xml";
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
            Logger.log('XML 파일이 다운로드되었습니다.', 'success');
        }

        async function downloadXLS() {
            const data = await DB.getAllData();
            if (!data.length) { Logger.log('다운로드할 데이터가 없습니다.', 'warning'); return; }
            const sheetData = [];
            const headers = Object.keys(Config.settings.dataFields).filter(f => Config.settings.dataFields[f]).map(f => Utils.capitalize(getFieldName(f)));
            sheetData.push(headers);
            data.forEach(item => {
                let row = [];
                Object.keys(Config.settings.dataFields).forEach(field => {
                    if (Config.settings.dataFields[field]) {
                        if (field === 'metaTags') row.push(JSON.stringify(item[field]));
                        else if (field === 'headings') row.push(item[field].join('; '));
                        else row.push(item[field] || '');
                    }
                });
                sheetData.push(row);
            });
            const sheet = XLSX.utils.aoa_to_sheet(sheetData);
            const wb = XLSX.utils.book_new();
            XLSX.utils.book_append_sheet(wb, sheet, "Data");
            XLSX.writeFile(wb, "data.xlsx");
            Logger.log('XLS 파일이 다운로드되었습니다.', 'success');
        }

        function getFieldName(field) {
            const names = {
                name: '이름',
                url: '주소',
                description: '설명',
                html: 'HTML 내용',
                category: '분류',
                quality: '품질점수',
                language: '언어',
                wordCount: '단어 수',
                metaTags: '메타 태그',
                headings: '헤딩'
            };
            return names[field] || field;
        }

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

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

        async function renderChart() {
            if (!document.getElementById('data-chart').getContext) return;
            const ctx = document.getElementById('data-chart').getContext('2d');
            if (chartInstance) chartInstance.destroy();
            const data = await DB.getAllData();
            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 = Config.settings.isCrawling;
        let currentPage = Config.settings.currentPage;
        let totalPages = Config.settings.maxPages;
        let progress = Config.settings.progress;
        let currentDepth = Config.settings.currentDepth;
        let activeRequests = 0;
        let requestQueue = [];
        let previousCrawls = await DB.getPreviousCrawls();

        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;
                }
            },
            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;
            Config.saveSettings({ isCrawling });
            await DB.addPreviousCrawl({ date: new Date().toLocaleString(), count: 0, status: '진행 중' });
            previousCrawls = await DB.getPreviousCrawls();
            Logger.log('크롤링을 시작합니다.', 'info');
            UI.updateLogs();
            currentPage = Config.settings.currentPage;
            totalPages = Config.settings.maxPages;
            currentDepth = Config.settings.currentDepth;
            setButtonStates();
            await crawlPage(currentDepth);
        }

        async function stopCrawling() {
            if (!isCrawling) {
                Logger.log('크롤링이 진행 중이지 않습니다.', 'warning');
                return;
            }
            isCrawling = false;
            Config.saveSettings({ isCrawling });
            Logger.log('크롤링이 중지되었습니다.', 'warning');
            await 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; }
                    const validResults = results.filter(result => {
                        const { 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 false;
                        }
                        return true;
                    });
                    const uniqueResults = [...new Set(validResults.map(r => r.url))].map(url => validResults.find(r => r.url === url));
                    requestQueue.push(...uniqueResults.map(r => ({ url: r.url, depth })));
                    for (const result of uniqueResults) {
                        await DB.addVisited(result.url);
                    }
                    if (previousCrawls.length && previousCrawls[previousCrawls.length -1].status === '진행 중') {
                        previousCrawls[previousCrawls.length -1].count += uniqueResults.length;
                        await DB.addPreviousCrawl(previousCrawls[previousCrawls.length -1]);
                        UI.updatePreviousCrawlsUI();
                    }
                    progress = (currentPage / totalPages) * 100;
                    Config.saveSettings({ progress });
                    UI.updateLogs();
                    await processQueue();
                    if (currentPage < totalPages && isCrawling) {
                        const nextUrl = handler.getNextPageUrl(doc);
                        if (nextUrl) {
                            currentPage++;
                            Config.saveSettings({ currentPage });
                            depth++;
                            Config.saveSettings({ currentDepth: depth });
                            Logger.log(`다음 페이지로 이동: ${currentPage}`, 'info');
                            const adaptiveDelay = calculateAdaptiveDelay();
                            await Utils.delay(adaptiveDelay);
                            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');
                    await finalizeCrawl('완료됨');
                    isCrawling = false;
                    Config.saveSettings({ isCrawling });
                    setButtonStates();
                    Config.saveSettings({ progress: 100 });
                    UI.updateLogs();
                }
            } catch (error) {
                Logger.log(`크롤링 중 오류 발생: ${error.message}`, 'error');
                await 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 || await DB.isVisited(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;
                    await DB.addData(entry);
                    UI.updateDataTable();
                    Logger.log(`데이터 수집 완료: ${url}`, 'success');
                    if (Config.settings.dataVisualization) DataVisualization.renderChart();
                }
                if (depth < Config.settings.maxDepth) {
                    const links = Extractor.extractLinks(doc);
                    requestQueue.push(...links.map(link => ({ url: link, depth: depth + 1 })));
                }
            } catch (error) {
                Logger.log(`데이터 수집 실패: ${url} - ${error.message}`, 'error');
            }
        }

        async function finalizeCrawl(status) {
            if (previousCrawls.length) {
                previousCrawls[previousCrawls.length -1].status = status;
                await DB.addPreviousCrawl(previousCrawls[previousCrawls.length -1]);
                UI.updatePreviousCrawlsUI();
            }
        }

        function calculateAdaptiveDelay() {
            // Implement adaptive delay based on response times and error rates
            // Placeholder implementation
            return Config.settings.delay + Math.random() * Config.settings.delayRandom;
        }

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

        function updatePageContent(content) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(content, 'text/html');
            document.documentElement.innerHTML = doc.documentElement.innerHTML;
        }

        function isValidURL(url) {
            try { new URL(url); return true; }
            catch { return false; }
        }

        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)}` : enforceHTTPS(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;
            }
        }

        function enforceHTTPS(url) {
            try {
                const parsed = new URL(url);
                parsed.protocol = 'https:';
                return parsed.toString();
            } catch {
                return url;
            }
        }

        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');
            alert(`CAPTCHA가 감지되었습니다. 수동으로 해결해주세요: ${url}`);
        }

        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
        };
    })();

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

        function clearSchedule() {
            clearInterval(scheduleInterval);
            Logger.log('크롤링 스케줄이 해제되었습니다.', 'info');
        }

        return {
            scheduleCrawling,
            clearSchedule
        };
    })();

    // Fetch Module
    // (Already defined above)

    // Robots Module
    // (Already defined above)

    // Extractor Module
    // (Already defined above)

    // Classifier Module
    // (Already defined above)

    // Evaluate Module
    // (Already defined above)

    // Download Module
    // (Already defined above)

    // Data Visualization Module
    // (Already defined above)

    // UI Module
    // (Already defined above)

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

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