// ==UserScript==
// @name         Enhanced Multi Search Engine Web Crawler
// @namespace    http://tampermonkey.net/
// @version      7.0
// @description  여러 검색 엔진에서 검색 결과를 크롤링하고 엑셀 형태로 표시합니다. (Google, Bing, Naver, Yandex 지원) - 에러 처리 및 취소 기능 추가
// @match        https://www.google.com/search*
// @match        https://www.bing.com/search*
// @match        https://search.naver.com/search.naver*
// @match        https://yandex.com/search/*
// @match        https://yandex.ru/search/*
// @grant        GM_download
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_log
// @connect      *
// ==/UserScript==

(function() {
    'use strict';

    // 상태 저장 키
    const STATE_KEY = 'crawlerState_v7';

    // 기본 상태 설정
    const defaultState = {
        crawlData: [],
        crawledURLs: [],
        currentPage: 1,
        totalPages: 5,
        isCrawling: false,
        isCancelled: false,
        startTime: null,
        currentEngineKey: null
    };

    // 현재 상태
    let state = { ...defaultState };

    // 지원하는 검색 엔진 설정
    const searchEngines = {
        'https://www.google.com/search': {
            name: 'Google',
            resultSelector: 'div.g',
            titleSelector: 'h3',
            linkSelector: 'a',
            descriptionSelector: '.IsZvec'
        },
        'https://www.bing.com/search': {
            name: 'Bing',
            resultSelector: 'li.b_algo',
            titleSelector: 'h2',
            linkSelector: 'a',
            descriptionSelector: '.b_caption p'
        },
        'https://search.naver.com/search.naver': {
            name: 'Naver',
            resultSelector: 'div.group_news ul.list_news li',
            titleSelector: 'a.news_tit',
            linkSelector: 'a.news_tit',
            descriptionSelector: 'div.dsc_wrap'
        },
        'https://yandex.com/search/': {  // Yandex International
            name: 'Yandex',
            resultSelector: 'li.serp-item',
            titleSelector: 'h2 a',
            linkSelector: 'h2 a',
            descriptionSelector: 'div.Text span'
        },
        'https://yandex.ru/search/': {  // Yandex Russia
            name: 'Yandex.ru',
            resultSelector: 'li.serp-item',
            titleSelector: 'h2 a',
            linkSelector: 'h2 a',
            descriptionSelector: 'div.Text span'
        }
    };

    let currentEngine = null;
    const MAX_RETRIES = 3;
    let retryCount = 0;

    // UI 생성 함수
    function createUI() {
        if (document.getElementById('crawlerContainer')) return; // UI가 이미 존재하면 생성하지 않음

        const container = document.createElement('div');
        container.id = 'crawlerContainer';
        container.style.position = 'fixed';
        container.style.top = '10px';
        container.style.right = '10px';
        container.style.width = '500px';
        container.style.height = '800px';
        container.style.backgroundColor = 'white';
        container.style.border = '1px solid #ccc';
        container.style.zIndex = 10000;
        container.style.padding = '10px';
        container.style.overflowY = 'scroll';
        container.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
        container.style.fontFamily = 'Arial, sans-serif';
        container.style.fontSize = '14px';

        // 제목
        const title = document.createElement('h3');
        title.innerText = '웹 크롤러 UI';
        container.appendChild(title);

        // 검색 엔진 선택
        const engineLabel = document.createElement('label');
        engineLabel.innerText = '검색 엔진: ';
        engineLabel.htmlFor = 'searchEngineSelect';
        container.appendChild(engineLabel);

        const engineSelect = document.createElement('select');
        engineSelect.id = 'searchEngineSelect';
        for (let key in searchEngines) {
            const option = document.createElement('option');
            option.value = key;
            option.innerText = searchEngines[key].name;
            engineSelect.appendChild(option);
        }
        container.appendChild(engineSelect);
        container.appendChild(document.createElement('br'));
        container.appendChild(document.createElement('br'));

        // 크롤링 속도 조절
        const speedLabel = document.createElement('label');
        speedLabel.innerText = '크롤링 속도(초): ';
        speedLabel.htmlFor = 'crawlSpeedInput';
        container.appendChild(speedLabel);

        const speedInput = document.createElement('input');
        speedInput.type = 'number';
        speedInput.id = 'crawlSpeedInput';
        speedInput.value = 2; // 기본 2초
        speedInput.min = 1;
        speedInput.max = 10;
        container.appendChild(speedInput);
        container.appendChild(document.createElement('br'));
        container.appendChild(document.createElement('br'));

        // 진행률 표시
        const progress = document.createElement('div');
        progress.id = 'crawlProgress';
        progress.innerText = '진행률: 0%';
        container.appendChild(progress);

        // 남은 예상 시간 표시
        const timeRemaining = document.createElement('div');
        timeRemaining.id = 'timeRemaining';
        timeRemaining.innerText = '남은 예상 시간: 계산 중...';
        container.appendChild(timeRemaining);
        container.appendChild(document.createElement('br'));

        // 데이터 표
        const table = document.createElement('table');
        table.id = 'crawlTable';
        table.border = '1';
        table.style.width = '100%';
        table.style.borderCollapse = 'collapse';
        table.style.marginBottom = '10px';

        const header = table.insertRow();
        ['사이트 이름', '주소', '설명'].forEach(text => {
            const th = document.createElement('th');
            th.innerText = text;
            th.style.padding = '5px';
            th.style.backgroundColor = '#f2f2f2';
            header.appendChild(th);
        });

        container.appendChild(table);

        // 시작 버튼
        const startButton = document.createElement('button');
        startButton.innerText = '크롤링 시작';
        startButton.onclick = startCrawling;
        container.appendChild(startButton);

        // 취소 버튼
        const cancelButton = document.createElement('button');
        cancelButton.innerText = '크롤링 취소';
        cancelButton.style.marginLeft = '10px';
        cancelButton.onclick = cancelCrawling;
        container.appendChild(cancelButton);
        container.appendChild(document.createElement('br'));
        container.appendChild(document.createElement('br'));

        // 다운로드 버튼
        const downloadButton = document.createElement('button');
        downloadButton.innerText = 'CSV 다운로드';
        downloadButton.onclick = downloadCSV;
        container.appendChild(downloadButton);
        container.appendChild(document.createElement('br'));
        container.appendChild(document.createElement('br'));

        // 로그 영역
        const log = document.createElement('div');
        log.id = 'crawlLog';
        log.style.marginTop = '10px';
        log.style.height = '300px';
        log.style.overflowY = 'scroll';
        log.style.border = '1px solid #ccc';
        log.style.padding = '5px';
        log.style.backgroundColor = '#f9f9f9';
        container.appendChild(log);

        document.body.appendChild(container);

        // 검색 엔진 초기 설정
        currentEngine = searchEngines[engineSelect.value];
        engineSelect.onchange = () => {
            currentEngine = searchEngines[engineSelect.value];
            addLog(`검색 엔진 변경: ${currentEngine.name}`);
            saveState(); // 상태 저장
        };
    }

    // 로그 추가 함수
    function addLog(message) {
        const log = document.getElementById('crawlLog');
        const p = document.createElement('p');
        p.innerText = `[${new Date().toLocaleTimeString()}] ${message}`;
        log.appendChild(p);
        log.scrollTop = log.scrollHeight;
    }

    // 진행률 및 남은 시간 업데이트 함수
    function updateProgress() {
        const progress = document.getElementById('crawlProgress');
        const percent = Math.floor((state.currentPage / state.totalPages) * 100);
        progress.innerText = `진행률: ${percent}%`;

        // 남은 시간 계산
        const elapsed = (Date.now() - state.startTime) / 1000; // 초 단위
        const pagesLeft = state.totalPages - state.currentPage;
        const speed = parseInt(document.getElementById('crawlSpeedInput').value) || 2;
        const estimatedRemaining = pagesLeft * speed - elapsed;
        const remainingTime = estimatedRemaining > 0 ? estimatedRemaining : 0;
        const minutes = Math.floor(remainingTime / 60);
        const seconds = Math.floor(remainingTime % 60);
        const timeRemaining = document.getElementById('timeRemaining');
        timeRemaining.innerText = `남은 예상 시간: ${minutes}분 ${seconds}초`;
    }

    // 데이터 추가 및 UI 갱신 함수
    function addData(name, url, description) {
        if (state.crawledURLs.includes(url)) {
            addLog(`중복된 URL 발견: ${url}`);
            return;
        }
        state.crawledURLs.push(url);
        state.crawlData.push({ name, url, description });

        const table = document.getElementById('crawlTable');
        const row = table.insertRow();
        const cell1 = row.insertCell(0);
        const cell2 = row.insertCell(1);
        const cell3 = row.insertCell(2);
        cell1.innerText = name;
        cell2.innerHTML = `<a href="${url}" target="_blank">${url}</a>`;
        cell3.innerText = description;
    }

    // 상태 저장 함수
    function saveState() {
        GM_setValue(STATE_KEY, JSON.stringify(state));
    }

    // 상태 불러오기 함수
    function loadState() {
        const savedState = GM_getValue(STATE_KEY, JSON.stringify(defaultState));
        state = JSON.parse(savedState);
    }

    // 상태 초기화 함수
    function clearState() {
        GM_deleteValue(STATE_KEY);
    }

    // CSV 다운로드 함수
    function downloadCSV() {
        if (state.crawlData.length === 0) {
            addLog('다운로드할 데이터가 없습니다.');
            return;
        }

        const headers = ['사이트 이름', '주소', '설명'];
        const rows = state.crawlData.map(item => [item.name, item.url, item.description]);

        let csvContent = 'data:text/csv;charset=utf-8,';
        csvContent += headers.join(',') + '\n';
        rows.forEach(row => {
            const formattedRow = row.map(field => `"${field.replace(/"/g, '""')}"`).join(',');
            csvContent += formattedRow + '\n';
        });

        const encodedUri = encodeURI(csvContent);
        const link = document.createElement('a');
        link.setAttribute('href', encodedUri);
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        link.setAttribute('download', `crawl_data_${timestamp}.csv`);
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        addLog('CSV 파일이 다운로드되었습니다.');
    }

    // 크롤링 취소 함수
    function cancelCrawling() {
        if (!state.isCrawling) {
            addLog('현재 크롤링이 진행 중이지 않습니다.');
            return;
        }
        state.isCancelled = true;
        saveState();
        addLog('크롤링이 취소되었습니다.');
    }

    // 크롤링 함수
    async function crawlCurrentPage() {
        try {
            const results = document.querySelectorAll(currentEngine.resultSelector);
            if (results.length === 0) {
                throw new Error('검색 결과를 찾을 수 없습니다.');
            }

            results.forEach(result => {
                let name, url, description;

                // 제목과 링크 추출
                const titleElement = result.querySelector(currentEngine.titleSelector);
                const linkElement = result.querySelector(currentEngine.linkSelector);
                if (titleElement && linkElement) {
                    name = titleElement.innerText.trim();
                    url = linkElement.href.trim();
                } else {
                    return; // 필수 요소가 없으면 건너뜀
                }

                // 설명 추출
                const descriptionElement = result.querySelector(currentEngine.descriptionSelector);
                description = descriptionElement ? descriptionElement.innerText.trim() : '설명 없음';

                // 광고 사이트 필터링
                const adKeywords = ['ad', 'advertisement', 'sponsored', 'promo', 'promotion', 'buy now', 'click here'];
                const isAd = adKeywords.some(keyword =>
                    (name && name.toLowerCase().includes(keyword)) ||
                    (url && url.toLowerCase().includes(keyword))
                );
                if (isAd) {
                    addLog(`광고 사이트 제외됨: ${url}`);
                    return;
                }

                addData(name, url, description);
            });

        } catch (error) {
            throw error;
        }
    }

    // 다음 페이지로 이동하는 함수
    async function goToNextPage() {
        return new Promise((resolve, reject) => {
            try {
                let nextButton = null;
                if (currentEngine.name === 'Google') {
                    nextButton = document.querySelector('#pnnext');
                } else if (currentEngine.name === 'Bing') {
                    nextButton = document.querySelector('a.sb_pagN');
                } else if (currentEngine.name === 'Naver') {
                    nextButton = document.querySelector('a.btn_next');
                } else if (currentEngine.name === 'Yandex' || currentEngine.name === 'Yandex.ru') {
                    // Yandex의 다음 페이지 버튼
                    nextButton = document.querySelector('a.Button2_view_default'); // 기본 다음 버튼 선택자
                }

                if (nextButton) {
                    nextButton.click();
                    addLog('다음 페이지로 이동 중...');

                    // 페이지 로드 대기
                    const onLoad = () => {
                        window.removeEventListener('load', onLoad);
                        resolve();
                    };
                    window.addEventListener('load', onLoad, { once: true });
                } else {
                    throw new Error('다음 페이지 버튼을 찾을 수 없습니다.');
                }
            } catch (error) {
                reject(error);
            }
        });
    }

    // 에러 처리 및 재시도 로직 포함 크롤링 함수
    async function crawlWithRetry() {
        try {
            await crawlCurrentPage();
            await goToNextPage();
        } catch (error) {
            retryCount++;
            addLog(`오류 발생: ${error.message}`);
            if (retryCount <= MAX_RETRIES) {
                addLog(`재시도 중 (${retryCount}/${MAX_RETRIES})...`);
                await delay(2000); // 2초 대기 후 재시도
                await crawlWithRetry(); // 재귀 호출
            } else {
                addLog(`크롤링을 중단합니다. 최대 재시도 횟수 (${MAX_RETRIES}) 초과.`);
                state.isCrawling = false;
                saveState();
                throw error;
            }
        }
    }

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

    // 크롤링 시작 함수
    async function startCrawling() {
        if (state.isCrawling) {
            addLog('현재 크롤링이 진행 중입니다.');
            return;
        }

        // 현재 선택된 검색 엔진 설정
        const engineSelect = document.getElementById('searchEngineSelect');
        const selectedKey = engineSelect.value;
        state.currentEngineKey = selectedKey;
        currentEngine = searchEngines[selectedKey];

        // 상태 초기화
        state.isCrawling = true;
        state.isCancelled = false;
        state.currentPage = 1;
        state.crawlData = [];
        state.crawledURLs = [];
        state.startTime = Date.now();

        // 테이블 초기화
        const table = document.getElementById('crawlTable');
        table.innerHTML = '';
        const header = table.insertRow();
        ['사이트 이름', '주소', '설명'].forEach(text => {
            const th = document.createElement('th');
            th.innerText = text;
            th.style.padding = '5px';
            th.style.backgroundColor = '#f2f2f2';
            header.appendChild(th);
        });

        saveState();
        addLog('크롤링 시작');

        try {
            while (state.currentPage <= state.totalPages && !state.isCancelled) {
                addLog(`페이지 ${state.currentPage} 크롤링 중...`);
                updateProgress();
                await crawlWithRetry();

                // 사용자 지정 속도에 따라 대기
                const speed = parseInt(document.getElementById('crawlSpeedInput').value) || 2;
                addLog(`딜레이: ${speed}초`);
                await delay(speed * 1000);

                state.currentPage++;
                saveState();

                if (state.currentPage > state.totalPages) {
                    break;
                }
            }

            if (!state.isCancelled) {
                addLog('크롤링 완료');
                state.isCrawling = false;
                saveState();
            }
        } catch (error) {
            addLog(`크롤링 중 오류 발생: ${error.message}`);
        }
    }

    // 상태 저장 전에 실행되는 함수
    function saveStateBeforeNavigation() {
        saveState();
    }

    // 상태 불러오기 및 크롤링 재개 함수
    async function loadAndResume() {
        loadState();

        // 데이터가 존재하면 테이블에 추가
        if (state.crawlData.length > 0) {
            const table = document.getElementById('crawlTable');
            // 테이블 헤더가 없을 경우 초기화
            if (table.rows.length <= 1) { // 헤더만 있거나 비어있을 때
                table.innerHTML = '';
                const header = table.insertRow();
                ['사이트 이름', '주소', '설명'].forEach(text => {
                    const th = document.createElement('th');
                    th.innerText = text;
                    th.style.padding = '5px';
                    th.style.backgroundColor = '#f2f2f2';
                    header.appendChild(th);
                });
            }

            // 데이터 추가
            state.crawlData.forEach(item => {
                const row = table.insertRow();
                const cell1 = row.insertCell(0);
                const cell2 = row.insertCell(1);
                const cell3 = row.insertCell(2);
                cell1.innerText = item.name;
                cell2.innerHTML = `<a href="${item.url}" target="_blank">${item.url}</a>`;
                cell3.innerText = item.description;
            });
        }

        // 검색 엔진 선택 복원
        if (state.currentEngineKey && searchEngines[state.currentEngineKey]) {
            const engineSelect = document.getElementById('searchEngineSelect');
            engineSelect.value = state.currentEngineKey;
            currentEngine = searchEngines[state.currentEngineKey];
            addLog(`검색 엔진 설정: ${currentEngine.name}`);
        }

        // 크롤링 중이라면 계속 진행
        if (state.isCrawling && !state.isCancelled) {
            addLog('이전 크롤링 상태에서 재개합니다.');
            await startCrawling();
        }
    }

    // 스크립트 초기화
    function init() {
        createUI();
        loadAndResume();
    }

    // 페이지 로드 시 스크립트 실행
    window.addEventListener('load', () => {
        init();
    });

    // 페이지 전환 시 상태 저장
    window.addEventListener('beforeunload', saveStateBeforeNavigation);

})();                       이 코드에서 첫 번쨰 페이지를 크롤링한 뒤에 자동으로 다음 페이지로 넘어간다음에는 크롤링을 안 함 . 수정해줘 + UI 개선 ㄱㄱ