// ==UserScript==
// @name         Advanced Educational Web Scraper
// @namespace    http://tampermonkey.net/
// @version      3.3
// @description  구글, 얀덱스, 빙, 덕덕고 검색 결과를 자동으로 크롤링하고 다양한 기능을 제공하는 교육용 스크립트 (robots.txt 검사 제거)
// @author       Your Name
// @match        https://www.google.com/search*
// @match        https://www.yandex.com/search/*
// @match        https://www.yandex.ru/search/*
// @match        https://www.bing.com/search*
// @match        https://duckduckgo.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @connect      *
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    /*** 1. 설정 및 상태 변수 초기화 ***/
    const DEFAULT_STATE = {
        isCrawling: false,
        currentPage: 1,
        maxPages: 5,
        delay: 3000, // ms
        collectedData: [],
        visitedURLs: [],
        filterKeywords: ['광고', 'promotion', 'sponsored', '광고주'],
        excludedDomains: ['facebook.com', 'twitter.com'],
        retryCount: 0,
        maxRetries: 3,
        targetSearchEngine: detectSearchEngine(),
        log: '',
    };

    const STORAGE_KEY = 'webScraperState';
    let state = GM_getValue(STORAGE_KEY, DEFAULT_STATE);

    /*** 2. 검색 엔진 감지 ***/
    function detectSearchEngine() {
        if (window.location.hostname.includes('google')) {
            return 'google';
        } else if (window.location.hostname.includes('yandex')) {
            return 'yandex';
        } else if (window.location.hostname.includes('bing')) {
            return 'bing';
        } else if (window.location.hostname.includes('duckduckgo')) {
            return 'duckduckgo';
        }
        return 'unknown';
    }

    /*** 3. 사용자 알림 함수 ***/
    function notify(message) {
        if (Notification.permission === "granted") {
            new Notification(message);
        } else if (Notification.permission !== "denied") {
            Notification.requestPermission().then(permission => {
                if (permission === "granted") {
                    new Notification(message);
                }
            });
        }
    }

    /*** 4. UI 요소 생성 및 스타일링 ***/
    function createUI() {
        // 기존 UI 제거 (페이지 이동 시 초기화 방지)
        let existingUI = document.getElementById('advanced-web-scraper-ui');
        if (existingUI) existingUI.remove();

        // UI 컨테이너 생성
        const uiContainer = document.createElement('div');
        uiContainer.id = 'advanced-web-scraper-ui';
        uiContainer.style.position = 'fixed';
        uiContainer.style.top = '10px';
        uiContainer.style.right = '10px';
        uiContainer.style.width = '400px';
        uiContainer.style.height = '80vh'; // 반응형 높이
        uiContainer.style.backgroundColor = '#2c3e50';
        uiContainer.style.border = '1px solid #34495e';
        uiContainer.style.padding = '15px';
        uiContainer.style.overflowY = 'auto';
        uiContainer.style.zIndex = '10000';
        uiContainer.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
        uiContainer.style.borderRadius = '8px';
        uiContainer.style.fontFamily = 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif';
        uiContainer.style.color = '#ecf0f1';

        uiContainer.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
                <h2 style="margin: 0; font-size: 18px;">🕸️ 크롤러 대시보드</h2>
                <button id="closeUI" style="background: transparent; border: none; color: #ecf0f1; font-size: 18px; cursor: pointer;">✖️</button>
            </div>
            <div style="margin-bottom: 15px;">
                <div id="progress" style="background-color: #34495e; border-radius: 4px; overflow: hidden; height: 20px; margin-bottom: 5px;">
                    <div id="progressBar" style="height: 100%; width: 0%; background-color: #1abc9c; transition: width 0.3s;"></div>
                </div>
                <div style="font-size: 14px;">진행률: <span id="progressText">0%</span> (<span id="progressCount">0</span>/<span id="progressTotal">${state.maxPages * 10}</span>)</div>
                <div style="font-size: 14px; margin-top: 5px;"><strong>현재 페이지:</strong> <span id="currentPage">${state.currentPage}</span></div>
                <div style="font-size: 14px;"><strong>현재 상태:</strong> <span id="currentStatus">대기 중</span></div>
                <div style="font-size: 14px;"><strong>예상 완료 시간:</strong> <span id="estimatedTime">계산 중...</span></div>
                <div id="spinner" style="display: none; margin-top: 10px;">
                    <div class="loader"></div>
                </div>
            </div>
            <div style="display: flex; gap: 10px; margin-bottom: 15px;">
                <button id="startCrawl" style="flex: 1; padding: 10px; background-color: #27ae60; border: none; border-radius: 4px; color: #ecf0f1; cursor: pointer; font-size: 14px;">
                    ▶️ 크롤링 시작
                </button>
                <button id="stopCrawl" style="flex: 1; padding: 10px; background-color: #c0392b; border: none; border-radius: 4px; color: #ecf0f1; cursor: pointer; font-size: 14px;">
                    ⏹️ 크롤링 중지
                </button>
            </div>
            <div style="display: flex; gap: 10px; margin-bottom: 15px;">
                <button id="downloadCSV" style="flex: 1; padding: 10px; background-color: #2980b9; border: none; border-radius: 4px; color: #ecf0f1; cursor: pointer; font-size: 14px;">
                    📥 CSV 다운로드
                </button>
                <button id="downloadJSON" style="flex: 1; padding: 10px; background-color: #8e44ad; border: none; border-radius: 4px; color: #ecf0f1; cursor: pointer; font-size: 14px;">
                    📥 JSON 다운로드
                </button>
            </div>
            <h3 style="border-bottom: 1px solid #34495e; padding-bottom: 5px; margin-bottom: 15px;">📜 로그</h3>
            <div id="log" style="background-color: #34495e; padding: 10px; border-radius: 4px; height: 150px; overflow-y: auto; font-size: 12px;"></div>
            <h3 style="border-bottom: 1px solid #34495e; padding-bottom: 5px; margin: 15px 0 15px 0;">📊 수집된 데이터</h3>
            <input type="text" id="tableFilter" placeholder="검색어 입력..." style="width: 100%; padding: 8px; border: none; border-radius: 4px; margin-bottom: 10px;">
            <div style="max-height: 200px; overflow-y: auto; margin-bottom: 15px;">
                <table id="dataTable" style="width: 100%; border-collapse: collapse; background-color: #2c3e50;">
                    <thead>
                        <tr>
                            <th style="padding: 8px; border-bottom: 1px solid #34495e; cursor: pointer;">📝 사이트 이름</th>
                            <th style="padding: 8px; border-bottom: 1px solid #34495e; cursor: pointer;">🔗 주소</th>
                            <th style="padding: 8px; border-bottom: 1px solid #34495e; cursor: pointer;">📄 설명</th>
                            <th style="padding: 8px; border-bottom: 1px solid #34495e; cursor: pointer;">🖼️ 이미지 URL</th>
                            <th style="padding: 8px; border-bottom: 1px solid #34495e; cursor: pointer;">🎥 비디오 링크</th>
                        </tr>
                    </thead>
                    <tbody></tbody>
                </table>
            </div>
            <h3 style="border-bottom: 1px solid #34495e; padding-bottom: 5px; margin: 15px 0 15px 0;">⚙️ 설정</h3>
            <div style="margin-bottom: 10px;">
                <label for="pageSelector" style="font-size: 14px;">🔢 크롤링할 페이지 수:</label>
                <select id="pageSelector" style="width: 100%; padding: 8px; border: none; border-radius: 4px; margin-top: 5px;">
                    ${generatePageOptions(state.maxPages)}
                </select>
            </div>
            <div style="margin-bottom: 10px;">
                <label for="delay" style="font-size: 14px;">⏱️ 요청 간 지연 시간 (ms):</label>
                <input type="number" id="delay" value="${state.delay}" min="500" max="10000" style="width: 100%; padding: 8px; border: none; border-radius: 4px; margin-top: 5px;">
            </div>
            <div style="margin-bottom: 10px;">
                <label for="filterKeywords" style="font-size: 14px;">🔍 필터링 키워드 (쉼표로 구분):</label>
                <input type="text" id="filterKeywords" value="${state.filterKeywords.join(', ')}" style="width: 100%; padding: 8px; border: none; border-radius: 4px; margin-top: 5px;">
            </div>
            <div style="margin-bottom: 10px;">
                <label for="excludedDomains" style="font-size: 14px;">🚫 제외할 도메인 (쉼표로 구분):</label>
                <input type="text" id="excludedDomains" value="${state.excludedDomains.join(', ')}" style="width: 100%; padding: 8px; border: none; border-radius: 4px; margin-top: 5px;">
            </div>
            <button id="saveSettings" style="width: 100%; padding: 10px; background-color: #e67e22; border: none; border-radius: 4px; color: #ecf0f1; cursor: pointer; font-size: 14px;">
                💾 설정 저장
            </button>
            <h3 style="border-bottom: 1px solid #34495e; padding-bottom: 5px; margin: 15px 0 15px 0;">📈 데이터 시각화</h3>
            <canvas id="dataChart" width="350" height="150"></canvas>
        `;
        document.body.appendChild(uiContainer);

        // 버튼 스타일링을 CSS로 이동
        const style = document.createElement('style');
        style.innerHTML = `
            /* 로딩 스피너 스타일 */
            .loader {
                border: 4px solid #f3f3f3;
                border-top: 4px solid #1abc9c;
                border-radius: 50%;
                width: 24px;
                height: 24px;
                animation: spin 1s linear infinite;
                margin: auto;
            }

            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }

            /* 테이블 스트라이프 및 호버 효과 */
            #dataTable tbody tr:nth-child(odd) {
                background-color: #34495e;
            }

            #dataTable tbody tr:hover {
                background-color: #1abc9c;
                color: #2c3e50;
            }

            #dataTable th, #dataTable td {
                text-align: left;
            }

            /* 인풋 필드 포커스 효과 */
            input[type="text"], input[type="number"], select {
                background-color: #34495e;
                color: #ecf0f1;
            }

            input::placeholder {
                color: #95a5a6;
            }
        `;
        document.head.appendChild(style);

        /*** 차트 초기화 및 업데이트 ***/
        if (!window.Chart) {
            const chartScript = document.createElement('script');
            chartScript.src = 'https://cdn.jsdelivr.net/npm/chart.js';
            chartScript.onload = () => {
                initializeChart();
                if (state.isCrawling) {
                    crawlPage();
                }
            };
            chartScript.onerror = () => {
                log('Chart.js 로드에 실패했습니다. 데이터 시각화를 사용할 수 없습니다.', 'error');
            };
            document.head.appendChild(chartScript);
        } else {
            initializeChart();
        }
    }

    /*** Helper 함수: 페이지 옵션 생성 ***/
    function generatePageOptions(selectedPage) {
        let options = '';
        for (let i = 1; i <= 20; i++) { // 최대 20페이지까지 선택 가능
            options += `<option value="${i}" ${i === selectedPage ? 'selected' : ''}>${i} 페이지</option>`;
        }
        return options;
    }

    /*** 5. 차트 초기화 및 업데이트 ***/
    let dataChart;
    function initializeChart() {
        const ctx = document.getElementById('dataChart').getContext('2d');
        dataChart = new Chart(ctx, {
            type: 'bar',
            data: {
                labels: ['수집된 사이트 수'],
                datasets: [{
                    label: '사이트 수',
                    data: [state.collectedData.length],
                    backgroundColor: 'rgba(46, 204, 113, 0.6)',
                    borderColor: 'rgba(46, 204, 113, 1)',
                    borderWidth: 1
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: true,
                        ticks: {
                            precision:0
                        }
                    }
                },
                plugins: {
                    legend: {
                        display: false
                    }
                }
            }
        });
    }

    function updateChart() {
        if (!dataChart) {
            log('Chart.js가 초기화되지 않았습니다. 데이터를 시각화할 수 없습니다.', 'error');
            return;
        }
        dataChart.data.datasets[0].data = [state.collectedData.length];
        dataChart.update();
    }

    /*** 6. 로그 함수 ***/
    function log(message, type = 'info') {
        const time = new Date().toLocaleTimeString();
        let fullMessage = `[${time}] ${message}`;
        if (type === 'error') {
            fullMessage = `[${time}] <span style="color: #e74c3c;">${message}</span>`;
        } else if (type === 'warning') {
            fullMessage = `[${time}] <span style="color: #f1c40f;">${message}</span>`;
        }
        const logElement = document.getElementById('log');
        logElement.innerHTML += fullMessage + '<br>';
        logElement.scrollTop = logElement.scrollHeight;
        state.log += fullMessage + '<br>';
        GM_setValue(STORAGE_KEY, state);
    }

    /*** 7. 진행률 및 상태 업데이트 함수 ***/
    function updateProgress(current, total) {
        const percent = total === 0 ? 0 : Math.floor((current / total) * 100);
        document.getElementById('progressBar').style.width = `${percent}%`;
        document.getElementById('progressText').innerText = `${percent}%`;
        document.getElementById('progressCount').innerText = current;
        document.getElementById('progressTotal').innerText = total;
    }

    function updateStatus() {
        const currentPageElement = document.getElementById('currentPage');
        const estimatedTimeElement = document.getElementById('estimatedTime');
        currentPageElement.innerText = `${state.currentPage}`;

        // 예상 완료 시간 계산
        const remainingPages = state.maxPages - state.currentPage;
        const estimatedSeconds = Math.floor((remainingPages * state.delay) / 1000);
        estimatedTimeElement.innerText = `${estimatedSeconds}초`;
    }

    /*** 8. 데이터 추가 함수 (중복 제거 포함) ***/
    async function addData(name, url, description, imageUrls = [], videoLinks = []) {
        // 중복 데이터 확인
        if (state.visitedURLs.includes(url)) {
            log(`이미 존재하는 데이터: ${url}`, 'warning');
            return;
        }

        // 광고 및 제외 도메인 필터링
        if (isAdSite(description, url)) {
            log(`광고 또는 제외 도메인 사이트 발견 및 필터링: ${url}`, 'warning');
            return;
        }

        // 링크 유효성 검사
        const isValid = await validateLink(url);
        if (!isValid) {
            log(`유효하지 않은 링크 발견 및 제외: ${url}`, 'warning');
            return;
        }

        // 데이터 수집
        state.visitedURLs.push(url);
        state.collectedData.push({ name, url, description, imageUrls, videoLinks });
        GM_setValue(STORAGE_KEY, state);

        // 테이블에 데이터 추가
        const dataTableBody = document.querySelector('#dataTable tbody');
        const row = document.createElement('tr');
        row.innerHTML = `
            <td style="padding: 8px; border-bottom: 1px solid #34495e;">${name}</td>
            <td style="padding: 8px; border-bottom: 1px solid #34495e;"><a href="${url}" target="_blank" style="color: #1abc9c; text-decoration: none;">🔗 보기</a></td>
            <td style="padding: 8px; border-bottom: 1px solid #34495e;">${description}</td>
            <td style="padding: 8px; border-bottom: 1px solid #34495e;">${imageUrls.length > 0 ? `<a href="${imageUrls[0]}" target="_blank" style="color: #e67e22; text-decoration: none;">🖼️ 이미지</a>` : 'N/A'}</td>
            <td style="padding: 8px; border-bottom: 1px solid #34495e;">${videoLinks.length > 0 ? `<a href="${videoLinks[0]}" target="_blank" style="color: #9b59b6; text-decoration: none;">🎥 비디오</a>` : 'N/A'}</td>
        `;
        dataTableBody.appendChild(row);

        // 로그 및 진행률 업데이트
        log(`데이터 수집: ${name}`);
        updateProgress(state.collectedData.length, state.maxPages * 10);
        updateChart();
        updateStatus();
    }

    /*** 9. 광고 및 제외 도메인 필터링 함수 ***/
    function isAdSite(description, url) {
        // 키워드 필터링
        const keywords = state.filterKeywords.map(k => k.toLowerCase());
        const isAd = keywords.some(keyword => description.toLowerCase().includes(keyword));

        // 도메인 제외 필터링
        const isExcluded = state.excludedDomains.some(domain => url.includes(domain));

        return isAd || isExcluded;
    }

    /*** 10. 검색 결과 추출 함수 (검색 엔진별 지원 확장) ***/
    function extractSearchResults() {
        let results = [];
        switch (state.targetSearchEngine) {
            case 'google':
                const googleResults = document.querySelectorAll('div.g');
                googleResults.forEach(block => {
                    const titleElement = block.querySelector('h3');
                    const linkElement = block.querySelector('a');
                    const descriptionElement = block.querySelector('.IsZvec, .VwiC3b');
                    const imageElements = block.querySelectorAll('img');
                    const videoElements = block.querySelectorAll('a[href*="video"]'); // Simplistic video link detection
                    if (titleElement && linkElement && descriptionElement) {
                        const name = titleElement.innerText;
                        const url = linkElement.href;
                        const description = descriptionElement.innerText;
                        const imageUrls = Array.from(imageElements).map(img => img.src).filter(src => src);
                        const videoLinks = Array.from(videoElements).map(a => a.href).filter(href => href);
                        results.push({ name, url, description, imageUrls, videoLinks });
                    }
                });
                break;
            case 'yandex':
                const yandexResults = document.querySelectorAll('.serp-item');
                yandexResults.forEach(block => {
                    const titleElement = block.querySelector('.organic__url-text');
                    const linkElement = block.querySelector('.organic__url');
                    const descriptionElement = block.querySelector('.organic__text');
                    const imageElements = block.querySelectorAll('img');
                    const videoElements = block.querySelectorAll('a[href*="video"]'); // Simplistic video link detection
                    if (titleElement && linkElement && descriptionElement) {
                        const name = titleElement.innerText;
                        const url = linkElement.href;
                        const description = descriptionElement.innerText;
                        const imageUrls = Array.from(imageElements).map(img => img.src).filter(src => src);
                        const videoLinks = Array.from(videoElements).map(a => a.href).filter(href => href);
                        results.push({ name, url, description, imageUrls, videoLinks });
                    }
                });
                break;
            case 'bing':
                const bingResults = document.querySelectorAll('.b_algo');
                bingResults.forEach(block => {
                    const titleElement = block.querySelector('h2');
                    const linkElement = block.querySelector('a');
                    const descriptionElement = block.querySelector('.b_caption p');
                    const imageElements = block.querySelectorAll('img');
                    const videoElements = block.querySelectorAll('a[href*="video"]'); // Simplistic video link detection
                    if (titleElement && linkElement && descriptionElement) {
                        const name = titleElement.innerText;
                        const url = linkElement.href;
                        const description = descriptionElement.innerText;
                        const imageUrls = Array.from(imageElements).map(img => img.src).filter(src => src);
                        const videoLinks = Array.from(videoElements).map(a => a.href).filter(href => href);
                        results.push({ name, url, description, imageUrls, videoLinks });
                    }
                });
                break;
            case 'duckduckgo':
                const ddgResults = document.querySelectorAll('.result');
                ddgResults.forEach(block => {
                    const titleElement = block.querySelector('.result__title a');
                    const linkElement = block.querySelector('.result__url');
                    const descriptionElement = block.querySelector('.result__snippet');
                    const imageElements = block.querySelectorAll('img');
                    const videoElements = block.querySelectorAll('a[href*="video"]'); // Simplistic video link detection
                    if (titleElement && linkElement && descriptionElement) {
                        const name = titleElement.innerText;
                        const url = linkElement.href;
                        const description = descriptionElement.innerText;
                        const imageUrls = Array.from(imageElements).map(img => img.src).filter(src => src);
                        const videoLinks = Array.from(videoElements).map(a => a.href).filter(href => href);
                        results.push({ name, url, description, imageUrls, videoLinks });
                    }
                });
                break;
            default:
                console.warn('지원되지 않는 검색 엔진입니다.');
        }
        return results;
    }

    /*** 11. 다음 페이지로 이동 함수 (검색 엔진별 지원 확장) ***/
    function goToNextPage() {
        switch (state.targetSearchEngine) {
            case 'google':
                const googleNext = document.querySelector('#pnnext');
                if (googleNext) {
                    googleNext.click();
                    return true;
                }
                break;
            case 'yandex':
                const yandexNext = document.querySelector('a.button_theme_clear.button_size_m.button_pagination_next');
                if (yandexNext) {
                    yandexNext.click();
                    return true;
                }
                break;
            case 'bing':
                const bingNext = document.querySelector('.sb_pagN');
                if (bingNext) {
                    bingNext.click();
                    return true;
                }
                break;
            case 'duckduckgo':
                const ddgNext = document.querySelector('.result--more__btn');
                if (ddgNext) {
                    ddgNext.click();
                    return true;
                }
                break;
            default:
                return false;
        }
        return false;
    }

    /*** 12. 설정 저장 함수 ***/
    function saveSettings() {
        const pageSelector = document.getElementById('pageSelector');
        const maxPages = parseInt(pageSelector.value);
        const delay = parseInt(document.getElementById('delay').value);
        const filterKeywords = document.getElementById('filterKeywords').value.split(',').map(k => k.trim());
        const excludedDomains = document.getElementById('excludedDomains').value.split(',').map(d => d.trim());

        state.maxPages = maxPages;
        state.delay = delay;
        state.filterKeywords = filterKeywords;
        state.excludedDomains = excludedDomains;
        GM_setValue(STORAGE_KEY, state);

        log('설정이 저장되었습니다.', 'info');
        updateProgress(state.collectedData.length, state.maxPages * 10);
        updateStatus();
    }

    /*** 13. 링크 검증 함수 ***/
    function validateLink(url) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "HEAD",
                url: url,
                onload: function(response) {
                    if (response.status >= 200 && response.status < 400) {
                        resolve(true);
                    } else {
                        resolve(false);
                    }
                },
                onerror: function() {
                    resolve(false);
                }
            });
        });
    }

    /*** 14. 크롤링 함수 (에러 처리 및 재시도 로직 포함) ***/
    async function crawlPage() {
        if (!state.isCrawling) return;

        try {
            setStatus(`페이지 ${state.currentPage} 크롤링 중...`);
            log(`페이지 ${state.currentPage} 크롤링 시작.`);
            updateProgress(state.collectedData.length, state.maxPages * 10);
            toggleSpinner(true);

            // 검색 결과 추출
            const searchResults = extractSearchResults();

            log(`검색 결과 추출: ${searchResults.length}개 항목 발견.`, 'info');

            // 데이터 처리
            for (let result of searchResults) {
                log(`데이터 처리 중: ${result.name}`, 'info');
                await addData(result.name, result.url, result.description, result.imageUrls, result.videoLinks);
            }

            toggleSpinner(false);

            // 최대 페이지 수 도달 여부 확인
            if (state.currentPage >= state.maxPages) {
                log('크롤링 완료!', 'info');
                notify('크롤링이 완료되었습니다.');
                state.isCrawling = false;
                GM_setValue(STORAGE_KEY, state);
                updateStatus();
                setStatus('크롤링 완료');
                return;
            }

            // 다음 페이지로 이동
            const navigated = goToNextPage();
            if (navigated) {
                state.currentPage++;
                log(`다음 페이지(${state.currentPage})로 이동.`, 'info');
                GM_setValue(STORAGE_KEY, state);
                updateStatus();

                // 단계별 진행 표시
                setStatus(`페이지 ${state.currentPage} 로드 중...`);

                // 페이지 로딩 대기 (DELAY)
                setTimeout(() => {
                    crawlPage();
                }, state.delay);
            } else {
                log('다음 페이지를 찾을 수 없습니다. 크롤링을 종료합니다.', 'warning');
                notify('다음 페이지를 찾을 수 없어 크롤링을 종료합니다.');
                state.isCrawling = false;
                GM_setValue(STORAGE_KEY, state);
                updateStatus();
                setStatus('다음 페이지 없음');
            }
        } catch (error) {
            log(`에러 발생: ${error.message}`, 'error');
            notify(`에러 발생: ${error.message}`);
            toggleSpinner(false);
            // 재시도 로직
            if (state.retryCount < state.maxRetries) {
                state.retryCount++;
                log(`재시도 시도 (${state.retryCount}/${state.maxRetries})...`, 'warning');
                GM_setValue(STORAGE_KEY, state);
                setTimeout(crawlPage, state.delay);
            } else {
                log('재시도 한계를 초과했습니다. 크롤링을 중지합니다.', 'error');
                notify('재시도 한계를 초과하여 크롤링을 중지했습니다.');
                state.isCrawling = false;
                GM_setValue(STORAGE_KEY, state);
                updateStatus();
                setStatus('크롤링 중지');
            }
        }
    }

    /*** 15. 크롤링 제어 함수 ***/
    function startCrawling() {
        if (state.isCrawling) {
            log('이미 크롤링 중입니다.', 'warning');
            return;
        }

        state.isCrawling = true;
        state.currentPage = 1;
        state.collectedData = [];
        state.visitedURLs = [];
        state.retryCount = 0;
        GM_setValue(STORAGE_KEY, state);
        log('크롤링 시작.', 'info');
        setStatus('크롤링 시작');
        updateProgress(0, state.maxPages * 10);
        createUI();
        updateChart();
        crawlPage();
    }

    function stopCrawling() {
        if (!state.isCrawling) {
            log('크롤링이 진행되고 있지 않습니다.', 'warning');
            return;
        }
        state.isCrawling = false;
        GM_setValue(STORAGE_KEY, state);
        log('크롤링 중지.', 'info');
        notify('크롤링이 중지되었습니다.');
        setStatus('크롤링 중지');
        updateStatus();
    }

    /*** 16. 상태 표시 함수 ***/
    function setStatus(status) {
        const statusElement = document.getElementById('currentStatus');
        if (statusElement) {
            statusElement.innerText = `${status}`;
        } else {
            const uiContainer = document.getElementById('advanced-web-scraper-ui');
            const newStatus = document.createElement('div');
            newStatus.id = 'currentStatus';
            newStatus.style.marginTop = '5px';
            newStatus.innerText = `${status}`;
            uiContainer.appendChild(newStatus);
        }
    }

    /*** 17. 데이터 정렬 및 필터링 ***/
    function sortTable(columnIndex) {
        const dataTable = document.getElementById('dataTable');
        const tbody = dataTable.querySelector('tbody');
        const rows = Array.from(tbody.querySelectorAll('tr'));

        rows.sort((a, b) => {
            const aText = a.children[columnIndex].innerText.toLowerCase();
            const bText = b.children[columnIndex].innerText.toLowerCase();
            if (aText < bText) return -1;
            if (aText > bText) return 1;
            return 0;
        });

        // 테이블 재정렬
        tbody.innerHTML = '';
        rows.forEach(row => tbody.appendChild(row));
        log(`테이블이 ${dataTable.querySelectorAll('th')[columnIndex].innerText} 기준으로 정렬되었습니다.`, 'info');
    }

    function filterTable() {
        const filter = document.getElementById('tableFilter').value.toLowerCase();
        const rows = document.querySelectorAll('#dataTable tbody tr');
        rows.forEach(row => {
            const cells = row.querySelectorAll('td');
            const match = Array.from(cells).some(cell => cell.innerText.toLowerCase().includes(filter));
            row.style.display = match ? '' : 'none';
        });
    }

    /*** 18. 데이터 내보내기 및 가져오기 ***/
    function downloadCSV() {
        if (state.collectedData.length === 0) {
            log('다운로드할 데이터가 없습니다.', 'warning');
            return;
        }

        let csvContent = "data:text/csv;charset=utf-8,";
        csvContent += "사이트 이름,주소,설명,이미지 URL,비디오 링크\n";
        state.collectedData.forEach(row => {
            const rowData = `"${row.name.replace(/"/g, '""')}", "${row.url}", "${row.description.replace(/"/g, '""')}", "${row.imageUrls.length > 0 ? row.imageUrls[0] : 'N/A'}", "${row.videoLinks.length > 0 ? row.videoLinks[0] : 'N/A'}"`;
            csvContent += rowData + "\n";
        });

        const encodedUri = encodeURI(csvContent);
        const link = document.createElement('a');
        link.setAttribute('href', encodedUri);
        link.setAttribute('download', 'search_results.csv');
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        log('CSV 파일이 다운로드되었습니다.', 'info');
    }

    function downloadJSON() {
        if (state.collectedData.length === 0) {
            log('다운로드할 데이터가 없습니다.', 'warning');
            return;
        }

        const jsonContent = JSON.stringify(state.collectedData, null, 2);
        const blob = new Blob([jsonContent], { type: "application/json" });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = 'search_results.json';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        log('JSON 파일이 다운로드되었습니다.', 'info');
    }

    /*** 19. 테이블 정렬 이벤트 리스너 추가 ***/
    function addTableSortListeners() {
        const headers = document.querySelectorAll('#dataTable th');
        headers.forEach((header, index) => {
            header.style.cursor = 'pointer';
            header.addEventListener('click', () => sortTable(index));
        });
    }

    /*** 20. 테이블 필터 이벤트 리스너 추가 ***/
    function addTableFilterListener() {
        const filterInput = document.getElementById('tableFilter');
        filterInput.addEventListener('input', filterTable);
    }

    /*** 21. 반응형 디자인 (CSS 미디어 쿼리) ***/
    function addResponsiveStyles() {
        // 이미 스타일이 createUI에서 추가됨
    }

    /*** 22. 자동 저장 및 복구 ***/
    // 이미 GM_setValue와 GM_getValue로 구현됨. 또한 주기적으로 상태를 저장함.

    /*** 23. 초기화 및 상태 복원 함수 ***/
    function initializeUIComponents() {
        createUI();
        addResponsiveStyles();
        addTableSortListeners();
        addTableFilterListener();
        updateProgress(state.collectedData.length, state.maxPages * 10);
        updateStatus();

        // 기존 데이터 표시
        const dataTableBody = document.querySelector('#dataTable tbody');
        state.collectedData.forEach(item => {
            const row = document.createElement('tr');
            row.innerHTML = `
                <td style="padding: 8px; border-bottom: 1px solid #34495e;">${item.name}</td>
                <td style="padding: 8px; border-bottom: 1px solid #34495e;"><a href="${item.url}" target="_blank" style="color: #1abc9c; text-decoration: none;">🔗 보기</a></td>
                <td style="padding: 8px; border-bottom: 1px solid #34495e;">${item.description}</td>
                <td style="padding: 8px; border-bottom: 1px solid #34495e;">${item.imageUrls.length > 0 ? `<a href="${item.imageUrls[0]}" target="_blank" style="color: #e67e22; text-decoration: none;">🖼️ 이미지</a>` : 'N/A'}</td>
                <td style="padding: 8px; border-bottom: 1px solid #34495e;">${item.videoLinks.length > 0 ? `<a href="${item.videoLinks[0]}" target="_blank" style="color: #9b59b6; text-decoration: none;">🎥 비디오</a>` : 'N/A'}</td>
            `;
            dataTableBody.appendChild(row);
        });

        // 로그 복원
        if (state.log) {
            const logElement = document.getElementById('log');
            logElement.innerHTML = state.log;
        }

        // 크롤링이 진행 중인 경우 계속 진행
        if (state.isCrawling) {
            crawlPage();
        }

        // UI 창 닫기 기능 추가
        const closeButton = document.getElementById('closeUI');
        closeButton.addEventListener('click', () => {
            uiContainer.style.display = 'none';
        });
    }

    /*** 24. 자동 저장 타이머 설정 ***/
    setInterval(() => {
        GM_setValue(STORAGE_KEY, state);
    }, 5000); // 5초마다 저장

    /*** 25. 이벤트 리스너 설정 ***/
    function setupEventListeners() {
        const startButton = document.getElementById('startCrawl');
        const stopButton = document.getElementById('stopCrawl');
        const saveSettingsButton = document.getElementById('saveSettings');
        const downloadCSVButton = document.getElementById('downloadCSV');
        const downloadJSONButton = document.getElementById('downloadJSON');

        startButton.addEventListener('click', startCrawling);
        stopButton.addEventListener('click', stopCrawling);
        saveSettingsButton.addEventListener('click', saveSettings);
        downloadCSVButton.addEventListener('click', downloadCSV);
        downloadJSONButton.addEventListener('click', downloadJSON);
    }

    /*** 26. 스피너 토글 함수 ***/
    function toggleSpinner(show) {
        const spinner = document.getElementById('spinner');
        if (show) {
            spinner.style.display = 'block';
        } else {
            spinner.style.display = 'none';
        }
    }

    /*** 27. 초기 실행 ***/
    window.addEventListener('load', () => {
        initializeUIComponents();
        setupEventListeners();
    });

})();
(추가해야 하는 기능)
                               탭 인터페이스
탭 메뉴: 📜 로그, 📊 데이터, ⚙️ 설정의 세 가지 탭으로 구성되어 있습니다.
탭 이벤트: 탭을 클릭하거나 Ctrl+1, Ctrl+2, Ctrl+3 단축키를 사용하여 각 탭으로 전환할 수 있습니다.
스타일: 활성화된 탭은 색상이 다르며, 마우스 호버 시 색상이 변경됩니다.
HTML 내용 수집 및 텍스트 추출
각 검색 결과의 전체 HTML 내용과 주요 텍스트 내용을 비동기로 수집합니다.
수집된 내용을 데이터 테이블의 🗨️ 텍스트 내용과 📄 HTML 내용 열에 표시합니다.
📄 HTML 내용 셀을 클릭하면 모달 창으로 전체 HTML 내용을 확인할 수 있습니다.
툴팁 추가
버튼과 입력 필드에 title 속성을 추가하여 툴팁을 제공합니다. 이를 통해 사용자가 각 요소의 기능을 직관적으로 이해할 수 있습니다.
키보드 단축키 지원
기본 단축키:
Ctrl+S: 설정 저장
Ctrl+P: 크롤링 시작
Ctrl+Q: 크롤링 중지
Ctrl+D: CSV 다운로드
Ctrl+J: JSON 다운로드
Ctrl+1: 로그 탭 활성화
Ctrl+2: 데이터 탭 활성화
Ctrl+3: 설정 탭 활성화
검색 기능 강화
데이터 테이블 상단의 검색 입력 필드에 키워드를 입력하면 실시간으로 데이터를 필터링할 수 있습니다.
filterTable 함수가 입력된 키워드에 따라 관련 없는 행을 숨기거나 표시합니다.
실시간 데이터 업데이트 표시
데이터를 수집할 때마다 테이블과 차트가 실시간으로 업데이트됩니다.
높은 품질의 콘텐츠는 테이블에 더 높은 점수로 표시됩니다.
콘텐츠 품질 평가
evaluateContentQuality 함수는 페이지의 텍스트 단어 수를 기준으로 간단한 품질 점수를 계산합니다.
점수는 🗨️ 텍스트 내용 열 아래에 표시되며, info 유형의 로그로 출력됩니다.
자동 키워드 분류
classifyKeywords 함수는 설명 내용에 포함된 특정 키워드를 기반으로 콘텐츠를 자동으로 카테고리 분류합니다.
카테고리는 🗂️ 분류 열에 표시됩니다.
사용자 정의 블랙리스트/화이트리스트
설정 탭에서 🚫 블랙리스트 도메인과 ✅ 화이트리스트 도메인을 관리할 수 있습니다.
화이트리스트가 설정된 경우, 화이트리스트 도메인에 속한 사이트만 크롤링이 허용됩니다.
블랙리스트 도메인은 추가로 제한되며, 해당 도메인에서 수집된 데이터는 필터링됩니다.                      기존 코드를 확인하면서 추가해야 하는 코드들을 확인하면서 최대한으로  호환이 되는지 버그가 안 나는지 , 이걸 하면 버그가 날것 같은 지 등등 확인해서 스크립트를 작성해줘