// ==UserScript==
// @name         Advanced Multi-Engine Web Crawler
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  여러 검색 엔진을 지원하는 고급 웹 크롤링 및 데이터 수집 도구
// @author       Your Name
// @match        *://www.google.com/search*
// @match        *://www.bing.com/search*
// @match        *://duckduckgo.com/*
// @match        *://search.yahoo.com/*
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js
// ==/UserScript==

(function() {
    'use strict';

    // 기본 설정
    const defaultSettings = {
        delay: 2000, // 요청 간 딜레이 (밀리초)
        maxPages: 5, // 최대 페이지 수
        blacklist: ['ads', 'sponsor', 'advertisement'], // 블랙리스트 도메인
        whitelist: [], // 화이트리스트 도메인
    };

    // 설정 불러오기 또는 기본 설정 저장
    const settings = GM_getValue('crawlerSettings', defaultSettings);

    // 상태 변수 불러오기
    let currentPage = GM_getValue('currentPage', 1);
    let results = GM_getValue('results', []);
    let visited = new Set(GM_getValue('visited', []));

    // 검색 엔진 식별
    const searchEngine = identifySearchEngine();

    // UI 생성
    function createUI() {
        if ($('#crawler-ui').length) return; // UI가 이미 존재하면 생성하지 않음

        const ui = $(`
            <div id="crawler-ui" style="position: fixed; top: 10px; right: 10px; width: 400px; background: #fff; border: 2px solid #444; border-radius: 8px; z-index: 10000; font-family: Arial, sans-serif; box-shadow: 0 0 15px rgba(0,0,0,0.3);">
                <div style="display: flex; border-bottom: 2px solid #444; background: #f0f0f0; border-top-left-radius: 8px; border-top-right-radius: 8px;">
                    <div class="tab active" data-tab="log" style="flex: 1; padding: 10px; cursor: pointer; text-align: center; font-weight: bold;">📜 로그</div>
                    <div class="tab" data-tab="data" style="flex: 1; padding: 10px; cursor: pointer; text-align: center; font-weight: bold;">📊 데이터</div>
                    <div class="tab" data-tab="settings" style="flex: 1; padding: 10px; cursor: pointer; text-align: center; font-weight: bold;">⚙️ 설정</div>
                </div>
                <div class="tab-content" id="log" style="padding: 10px; display: block; max-height: 200px; overflow-y: auto; font-size: 12px; background: #fafafa;">
                    <h3 style="margin-top: 0;">로그</h3>
                    <div id="log-content" style="max-height: 160px; overflow-y: auto;"></div>
                </div>
                <div class="tab-content" id="data" style="padding: 10px; display: none; background: #fafafa;">
                    <h3 style="margin-top: 0;">데이터</h3>
                    <input type="text" id="search-filter" placeholder="실시간 검색..." style="width: 100%; padding: 5px; margin-bottom: 10px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px;">
                    <div style="overflow-x: auto;">
                        <table id="data-table" border="1" width="100%" style="border-collapse: collapse; font-size: 12px; width: 100%;">
                            <thead style="background: #444; color: #fff;">
                                <tr>
                                    <th style="padding: 8px; cursor: pointer;" data-sort="name">사이트 이름 ▲▼</th>
                                    <th style="padding: 8px; cursor: pointer;" data-sort="url">주소 ▲▼</th>
                                    <th style="padding: 8px; cursor: pointer;" data-sort="description">설명 ▲▼</th>
                                    <th style="padding: 8px; cursor: pointer;" data-sort="category">분류 ▲▼</th>
                                    <th style="padding: 8px; cursor: pointer;" data-sort="quality">품질 점수 ▲▼</th>
                                    <th style="padding: 8px;">HTML 보기</th>
                                </tr>
                            </thead>
                            <tbody></tbody>
                        </table>
                    </div>
                    <div id="pagination" style="margin-top: 10px; text-align: center;">
                        <button id="prev-page" style="padding: 5px 10px; margin-right: 5px;">이전</button>
                        <span id="page-info">1 / 1</span>
                        <button id="next-page" style="padding: 5px 10px; margin-left: 5px;">다음</button>
                    </div>
                </div>
                <div class="tab-content" id="settings" style="padding: 10px; display: none; background: #fafafa;">
                    <h3 style="margin-top: 0;">설정</h3>
                    <label style="display: block; margin-bottom: 10px;">딜레이 (ms): 
                        <input type="number" id="delay" value="${settings.delay}" style="width: 100%; padding: 5px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px;">
                    </label>
                    <label style="display: block; margin-bottom: 10px;">최대 페이지: 
                        <input type="number" id="maxPages" value="${settings.maxPages}" style="width: 100%; padding: 5px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px;">
                    </label>
                    <label style="display: block; margin-bottom: 10px;">블랙리스트 (콤마로 구분): 
                        <input type="text" id="blacklist" value="${settings.blacklist.join(', ')}" style="width: 100%; padding: 5px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px;">
                    </label>
                    <label style="display: block; margin-bottom: 10px;">화이트리스트 (콤마로 구분): 
                        <input type="text" id="whitelist" value="${settings.whitelist.join(', ')}" style="width: 100%; padding: 5px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px;">
                    </label>
                    <button id="save-settings" style="width: 100%; padding: 8px; background: #444; color: #fff; border: none; border-radius: 4px; cursor: pointer;">저장</button>
                </div>
                <div style="padding: 10px; border-top: 2px solid #444; background: #f9f9f9; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px;">
                    <button id="start-crawl" style="width: 48%; padding: 8px; background: #28a745; color: #fff; border: none; border-radius: 4px; cursor: pointer;">크롤링 시작</button>
                    <button id="stop-crawl" style="width: 48%; padding: 8px; background: #dc3545; color: #fff; border: none; border-radius: 4px; margin-left: 4%; cursor: pointer;">크롤링 중지</button><br><br>
                    <button id="download-csv" style="width: 48%; padding: 8px; background: #007bff; color: #fff; border: none; border-radius: 4px; cursor: pointer;">CSV 다운로드</button>
                    <button id="download-json" style="width: 48%; padding: 8px; background: #17a2b8; color: #fff; border: none; border-radius: 4px; margin-left: 4%; cursor: pointer;">JSON 다운로드</button><br><br>
                    <div>진행률: <span id="progress">0%</span></div>
                </div>
            </div>

            <!-- Modal 창 -->
            <div id="html-modal" style="display:none; position: fixed; top: 0; left: 0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index:10001;">
                <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 80%; height: 80%; background: white; padding: 20px; overflow: auto; border-radius: 8px;">
                    <h3>HTML 내용</h3>
                    <pre id="modal-content" style="white-space: pre-wrap; word-wrap: break-word; max-height: 70%; overflow-y: auto; background: #f8f8f8; padding: 10px; border: 1px solid #ccc; border-radius: 4px;"></pre>
                    <button id="close-modal" style="padding: 8px 16px; background: #dc3545; color: #fff; border: none; border-radius: 4px; cursor: pointer; margin-top: 10px;">닫기</button>
                </div>
            </div>
        `);

        $('body').append(ui);

        // 탭 전환 이벤트
        $('.tab').on('click', function() {
            const tab = $(this).data('tab');
            $('.tab').removeClass('active').css('background', '');
            $(this).addClass('active').css('background', '#e0e0e0');
            $('.tab-content').hide();
            $(`#${tab}`).show();
        });

        // 설정 저장 이벤트
        $('#save-settings').on('click', function() {
            settings.delay = parseInt($('#delay').val()) || defaultSettings.delay;
            settings.maxPages = parseInt($('#maxPages').val()) || defaultSettings.maxPages;
            settings.blacklist = $('#blacklist').val().split(',').map(s => s.trim()).filter(s => s);
            settings.whitelist = $('#whitelist').val().split(',').map(s => s.trim()).filter(s => s);
            GM_setValue('crawlerSettings', settings);
            log('설정이 저장되었습니다.');
        });

        // 크롤링 시작 버튼
        $('#start-crawl').on('click', startCrawling);

        // 크롤링 중지 버튼
        $('#stop-crawl').on('click', stopCrawling);

        // CSV 다운로드 버튼
        $('#download-csv').on('click', downloadCSV);

        // JSON 다운로드 버튼
        $('#download-json').on('click', downloadJSON);

        // HTML 모달 닫기
        $('#close-modal').on('click', function() {
            $('#html-modal').hide();
        });

        // HTML 내용 보기 이벤트
        $('body').on('click', '.view-html', function() {
            const htmlContent = $(this).data('html');
            $('#modal-content').text(htmlContent);
            $('#html-modal').show();
        });

        // 데이터 검색 필터링
        $('#search-filter').on('input', function() {
            const filter = $(this).val().toLowerCase();
            currentDataPage = 1;
            renderTable();
        });

        // 테이블 헤더 클릭 시 정렬
        $('#data-table thead th[data-sort]').on('click', function() {
            const sortKey = $(this).data('sort');
            if (currentSort.key === sortKey) {
                currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
            } else {
                currentSort.key = sortKey;
                currentSort.order = 'asc';
            }
            sortResults();
            renderTable();
        });

        // 페이징 버튼 이벤트
        $('#prev-page').on('click', function() {
            if (currentDataPage > 1) {
                currentDataPage--;
                renderTable();
            }
        });

        $('#next-page').on('click', function() {
            if (currentDataPage < totalDataPages) {
                currentDataPage++;
                renderTable();
            }
        });
    }

    // 로그 추가 함수
    function log(message) {
        const timestamp = new Date().toLocaleTimeString();
        $('#log-content').append(`<div>[${timestamp}] ${message}</div>`);
        $('#log-content').scrollTop($('#log-content')[0].scrollHeight);
        console.log(`[Crawler] ${message}`);
    }

    // 크롤링 제어 변수
    let crawling = false;

    // 크롤링 시작
    async function startCrawling() {
        if (crawling) {
            log('이미 크롤링이 진행 중입니다.');
            return;
        }
        crawling = true;
        log('크롤링을 시작합니다.');
        await crawlPage();
    }

    // 크롤링 중지
    function stopCrawling() {
        if (!crawling) {
            log('크롤링이 진행 중이지 않습니다.');
            return;
        }
        crawling = false;
        log('크롤링이 중지되었습니다.');
    }

    // 페이지 크롤링 함수
    async function crawlPage() {
        try {
            while (crawling && currentPage <= settings.maxPages) {
                log(`페이지 ${currentPage} 크롤링 중...`);
                const items = extractSearchResults();

                for (let item of items) {
                    if (!crawling) break;

                    const { name, url, description, html } = item;

                    // 중복 검사
                    if (visited.has(url)) {
                        log(`이미 방문한 사이트: ${url}`);
                        continue;
                    }

                    // 화이트리스트/블랙리스트 검사
                    if (settings.whitelist.length > 0) {
                        const isWhitelisted = settings.whitelist.some(domain => url.includes(domain));
                        if (!isWhitelisted) {
                            log(`화이트리스트에 없는 사이트: ${url}`);
                            continue;
                        }
                    }
                    if (settings.blacklist.some(domain => url.includes(domain))) {
                        log(`블랙리스트에 있는 사이트: ${url}`);
                        continue;
                    }

                    visited.add(url);
                    GM_setValue('visited', Array.from(visited));

                    // 콘텐츠 품질 평가 (단어 수 기준)
                    const wordCount = description.split(/\s+/).length;
                    const qualityScore = wordCount > 100 ? '높음' : '낮음';

                    // 키워드 분류
                    const categories = classifyKeywords(description);

                    // 결과 저장
                    results.push({
                        name,
                        url,
                        description,
                        category: categories.join(', '),
                        quality: qualityScore,
                        html
                    });

                    GM_setValue('results', results);
                    updateDataTable();
                    log(`수집 완료: ${name} (${url})`);

                    // 진행률 업데이트
                    const progress = Math.min((results.length / (settings.maxPages * items.length)) * 100, 100);
                    $('#progress').text(`${progress.toFixed(2)}%`);

                    await delay(settings.delay);
                }

                // 다음 페이지로 이동
                if (currentPage < settings.maxPages) {
                    const nextButton = getNextButton();
                    if (nextButton) {
                        nextButton.click();
                        currentPage++;
                        GM_setValue('currentPage', currentPage);
                        log(`다음 페이지로 이동: 페이지 ${currentPage}`);
                        await delay(settings.delay * 2);
                    } else {
                        log('더 이상 페이지가 없습니다.');
                        break;
                    }
                } else {
                    log('최대 페이지 수에 도달했습니다.');
                    break;
                }
            }
        } catch (error) {
            log(`에러 발생: ${error.message}`);
        } finally {
            crawling = false;
            log('크롤링이 완료되었습니다.');
        }
    }

    // 검색 결과 추출 함수
    function extractSearchResults() {
        let items = [];
        try {
            switch(searchEngine) {
                case 'Google':
                    $('.g').each(function() {
                        const name = $(this).find('h3').text() || 'No Title';
                        let url = $(this).find('a').attr('href') || 'No URL';
                        // 구글의 경우 URL이 `/url?q=...` 형식일 수 있으므로 정제
                        if (url.startsWith('/url?q=')) {
                            url = url.split('/url?q=')[1].split('&')[0];
                        }
                        const description = $(this).find('.VwiC3b').text() || 'No Description';
                        const html = $('<div>').append($(this).clone()).html();
                        items.push({ name, url, description, html });
                    });
                    break;
                case 'Bing':
                    $('.b_algo').each(function() {
                        const name = $(this).find('h2').text() || 'No Title';
                        const url = $(this).find('a').attr('href') || 'No URL';
                        const description = $(this).find('.b_caption p').text() || 'No Description';
                        const html = $('<div>').append($(this).clone()).html();
                        items.push({ name, url, description, html });
                    });
                    break;
                case 'DuckDuckGo':
                    $('.result').each(function() {
                        const name = $(this).find('.result__title').text() || 'No Title';
                        let url = $(this).find('a.result__a').attr('href') || 'No URL';
                        // DuckDuckGo의 경우 중간 리디렉션 URL일 수 있으므로 정제 필요
                        const urlParam = new URLSearchParams(url.split('?')[1]);
                        url = urlParam.get('uddg') || url;
                        const description = $(this).find('.result__snippet').text() || 'No Description';
                        const html = $('<div>').append($(this).clone()).html();
                        items.push({ name, url, description, html });
                    });
                    break;
                case 'Yahoo':
                    $('.dd.algo').each(function() {
                        const name = $(this).find('h3').text() || 'No Title';
                        const url = $(this).find('a').attr('href') || 'No URL';
                        const description = $(this).find('.compText p').text() || 'No Description';
                        const html = $('<div>').append($(this).clone()).html();
                        items.push({ name, url, description, html });
                    });
                    break;
                default:
                    log('지원하지 않는 검색 엔진입니다.');
            }
        } catch (error) {
            log(`검색 결과 추출 중 에러 발생: ${error.message}`);
        }
        return items;
    }

    // 다음 페이지 버튼 가져오기
    function getNextButton() {
        switch(searchEngine) {
            case 'Google':
                return document.querySelector('#pnnext');
            case 'Bing':
                return document.querySelector('a.sb_pagN');
            case 'DuckDuckGo':
                return document.querySelector('a.result--more__btn');
            case 'Yahoo':
                return document.querySelector('a.next');
            default:
                return null;
        }
    }

    // 데이터 테이블 업데이트
    function updateDataTable() {
        renderTable();
    }

    // HTML 이스케이프 함수
    function escapeHtml(text) {
        return text.replace(/&/g, "&amp;")
                   .replace(/</g, "&lt;")
                   .replace(/>/g, "&gt;")
                   .replace(/"/g, "&quot;")
                   .replace(/'/g, "&#039;");
    }

    // 키워드 분류 함수 (예시)
    function classifyKeywords(text) {
        const categories = [];
        const keywords = {
            '교육': ['학교', '교육', '강의', '학습'],
            '테크': ['기술', '소프트웨어', '프로그래밍', '개발'],
            '비즈니스': ['비즈니스', '마케팅', '스타트업', '기업'],
            '엔터테인먼트': ['영화', '음악', '게임', '연예'],
            '건강': ['건강', '병원', '의료', '운동']
        };
        for (let [category, keys] of Object.entries(keywords)) {
            if (keys.some(key => text.includes(key))) {
                categories.push(category);
            }
        }
        return categories.length > 0 ? categories : ['기타'];
    }

    // 검색 엔진 식별 함수
    function identifySearchEngine() {
        const url = window.location.hostname;
        if (url.includes('google.com')) return 'Google';
        if (url.includes('bing.com')) return 'Bing';
        if (url.includes('duckduckgo.com')) return 'DuckDuckGo';
        if (url.includes('yahoo.com')) return 'Yahoo';
        return 'Unknown';
    }

    // 초기 UI 생성 및 데이터 로드
    $(document).ready(function() {
        createUI();
        renderTable();
        updateProgress();
    });

    // 단축키 지원
    $(document).on('keydown', function(e) {
        if (e.ctrlKey) {
            switch (e.key.toLowerCase()) {
                case '1':
                    $('.tab[data-tab="log"]').click();
                    break;
                case '2':
                    $('.tab[data-tab="data"]').click();
                    break;
                case '3':
                    $('.tab[data-tab="settings"]').click();
                    break;
                case 's':
                    $('#save-settings').click();
                    break;
                case 'p':
                    $('#start-crawl').click();
                    break;
                case 'q':
                    $('#stop-crawl').click();
                    break;
                case 'd':
                    if (e.shiftKey) {
                        downloadJSON();
                    } else {
                        downloadCSV();
                    }
                    break;
            }
        }
    });

    // 데이터 정렬 상태
    let currentSort = { key: null, order: 'asc' };

    // 데이터 페이지 상태
    let currentDataPage = 1;
    const itemsPerPage = 10;
    let totalDataPages = 1;

    // 테이블 렌더링 함수
    function renderTable() {
        const tbody = $('#data-table tbody');
        tbody.empty();

        let filteredResults = results.filter(item => {
            const filter = $('#search-filter').val().toLowerCase();
            return item.name.toLowerCase().includes(filter) ||
                   item.url.toLowerCase().includes(filter) ||
                   item.description.toLowerCase().includes(filter) ||
                   item.category.toLowerCase().includes(filter) ||
                   item.quality.toLowerCase().includes(filter);
        });

        totalDataPages = Math.ceil(filteredResults.length / itemsPerPage) || 1;
        if (currentDataPage > totalDataPages) currentDataPage = totalDataPages;

        const start = (currentDataPage - 1) * itemsPerPage;
        const end = start + itemsPerPage;
        const pageItems = filteredResults.slice(start, end);

        for (let item of pageItems) {
            tbody.append(`
                <tr>
                    <td style="padding: 8px; border: 1px solid #ddd;">${escapeHtml(item.name)}</td>
                    <td style="padding: 8px; border: 1px solid #ddd;"><a href="${item.url}" target="_blank" style="color: #007bff; text-decoration: none;">${escapeHtml(item.url)}</a></td>
                    <td style="padding: 8px; border: 1px solid #ddd;">${escapeHtml(item.description)}</td>
                    <td style="padding: 8px; border: 1px solid #ddd;">${escapeHtml(item.category)}</td>
                    <td style="padding: 8px; border: 1px solid #ddd;">${escapeHtml(item.quality)}</td>
                    <td style="padding: 8px; border: 1px solid #ddd; text-align: center;">
                        <button class="view-html" data-html="${escapeHtml(item.html)}" style="padding: 4px 8px; background: #17a2b8; color: #fff; border: none; border-radius: 4px; cursor: pointer;">보기</button>
                    </td>
                </tr>
            `);
        }

        $('#page-info').text(`${currentDataPage} / ${totalDataPages}`);
    }

    // 데이터 정렬 함수
    function sortResults() {
        if (!currentSort.key) return;
        results.sort((a, b) => {
            let valA = a[currentSort.key].toLowerCase();
            let valB = b[currentSort.key].toLowerCase();
            if (valA < valB) return currentSort.order === 'asc' ? -1 : 1;
            if (valA > valB) return currentSort.order === 'asc' ? 1 : -1;
            return 0;
        });
    }

    // 진행률 업데이트
    function updateProgress() {
        const totalItems = settings.maxPages * 10; // 각 페이지당 10개 결과 가정
        const progress = Math.min((results.length / totalItems) * 100, 100);
        $('#progress').text(`${progress.toFixed(2)}%`);
    }

    // CSV 다운로드
    function downloadCSV() {
        if (results.length === 0) {
            log('다운로드할 데이터가 없습니다.');
            return;
        }
        const ws = XLSX.utils.json_to_sheet(results);
        const wb = XLSX.utils.book_new();
        XLSX.utils.book_append_sheet(wb, ws, 'Results');
        const csvData = XLSX.write(wb, {bookType: 'csv', type: 'array'});
        const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'results.csv';
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        log('CSV 파일이 다운로드되었습니다.');
    }

    // JSON 다운로드
    function downloadJSON() {
        if (results.length === 0) {
            log('다운로드할 데이터가 없습니다.');
            return;
        }
        const blob = new Blob([JSON.stringify(results, null, 2)], {type: 'application/json'});
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'results.json';
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        log('JSON 파일이 다운로드되었습니다.');
    }

    // 지연 함수
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

})();


데이터 칸을 더 효율성 좋게 그리고 가독성 좋게 그리고 디자인 좋게 다시 해줘. 또한 명령문을 더 정확하게 이행해. 알겟찌? 그리고 가독성 좋게 스크롤바나 그런 것들을 추가해!