Greasy Fork

KoneGG 확장 검색 시스템 (제목/내용/작성자 검색 지원)

kone.gg 사이트에서 제목, 내용, 작성자명이 특정 키워드를 포함하는 게시글을 검색합니다 (새탭 열기 지원)

当前为 2025-05-22 提交的版本,查看 最新版本

// ==UserScript==
// @name         KoneGG 확장 검색 시스템 (제목/내용/작성자 검색 지원)
// @namespace    http://tampermonkey.net/
// @version      2.3
// @description  kone.gg 사이트에서 제목, 내용, 작성자명이 특정 키워드를 포함하는 게시글을 검색합니다 (새탭 열기 지원)
// @author       You
// @match        https://kone.gg/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_log
// ==/UserScript==

(function() {
    'use strict';

    // 디버그 로깅
    const DEBUG = true;
    // 검색 중단 플래그
    let searchCancelled = false;

    function log(...args) {
        if (DEBUG) {
            console.log('[KoneGG 검색]', ...args);
        }
    }

    // 현재 서브 이름 가져오기
    function getCurrentSubName() {
        const path = window.location.pathname;
        const matches = path.match(/\/s\/([^\/]+)/);
        const subName = matches ? matches[1] : null;
        log('현재 서브명:', subName);
        return subName;
    }

    // CSS 스타일 추가
    GM_addStyle(`
        .kone-search-button {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 50px;
            height: 50px;
            border-radius: 50%;
            background-color: #3b82f6;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            z-index: 9998;
            transition: all 0.3s ease;
        }

        .kone-search-button:hover {
            background-color: #2563eb;
            transform: scale(1.05);
        }

        .dark .kone-search-button {
            background-color: #4b5563;
        }

        .dark .kone-search-button:hover {
            background-color: #374151;
        }

        .kone-search-panel {
            position: fixed;
            bottom: 80px;
            right: 20px;
            width: 350px;
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
            z-index: 9997;
            font-family: 'Pretendard', sans-serif;
            display: none;
            overflow: hidden;
            border: 1px solid #e5e7eb;
        }

        .dark .kone-search-panel {
            background-color: #27272a;
            border-color: #3f3f46;
            color: #e4e4e7;
        }

        .kone-search-header {
            padding: 15px;
            border-bottom: 1px solid #e5e7eb;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .dark .kone-search-header {
            border-color: #3f3f46;
        }

        .kone-search-title {
            font-weight: 600;
            font-size: 16px;
        }

        .kone-search-close {
            cursor: pointer;
            opacity: 0.6;
        }

        .kone-search-close:hover {
            opacity: 1;
        }

        .kone-search-content {
            padding: 15px;
        }

        .kone-search-form {
            display: flex;
            flex-direction: column;
            gap: 12px;
        }

        .kone-search-input-container {
            position: relative;
        }

        .kone-search-input {
            width: 100%;
            padding: 10px 12px;
            padding-left: 35px;
            border: 1px solid #e5e7eb;
            border-radius: 6px;
            font-size: 14px;
            outline: none;
        }

        .dark .kone-search-input {
            background-color: #3f3f46;
            border-color: #52525b;
            color: #e4e4e7;
        }

        .kone-search-input:focus {
            border-color: #3b82f6;
        }

        .kone-search-icon {
            position: absolute;
            left: 10px;
            top: 50%;
            transform: translateY(-50%);
            color: #9ca3af;
        }

        .kone-search-settings {
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .kone-search-checkbox-container {
            display: flex;
            align-items: center;
            gap: 6px;
        }

        .kone-search-checkbox-label {
            font-size: 13px;
            user-select: none;
        }

        .kone-search-button-submit {
            padding: 8px 16px;
            background-color: #3b82f6;
            color: white;
            border: none;
            border-radius: 6px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .kone-search-button-submit:hover {
            background-color: #2563eb;
        }

        .dark .kone-search-button-submit {
            background-color: #4b5563;
        }

        .dark .kone-search-button-submit:hover {
            background-color: #374151;
        }

        .kone-search-results {
            margin-top: 15px;
            max-height: 350px;
            overflow-y: auto;
            display: none;
        }

        .kone-search-results-header {
            margin-bottom: 10px;
            font-size: 14px;
            font-weight: 600;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .kone-search-results-count {
            color: #6b7280;
            font-size: 13px;
            font-weight: normal;
        }

        .dark .kone-search-results-count {
            color: #a1a1aa;
        }

        .kone-search-results-list {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .kone-search-result-item {
            padding: 10px;
            border: 1px solid #e5e7eb;
            border-radius: 6px;
            cursor: pointer;
            transition: background-color 0.3s;
            position: relative;
        }

        .dark .kone-search-result-item {
            border-color: #3f3f46;
        }

        .kone-search-result-item:hover {
            background-color: #f9fafb;
        }

        .dark .kone-search-result-item:hover {
            background-color: #3f3f46;
        }

        .kone-search-result-item::after {
            content: '🔗 새 탭에서 열기';
            position: absolute;
            top: 50%;
            right: 10px;
            transform: translateY(-50%);
            background-color: rgba(59, 130, 246, 0.1);
            color: #3b82f6;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 11px;
            opacity: 0;
            transition: opacity 0.3s;
            pointer-events: none;
        }

        .kone-search-result-item:hover::after {
            opacity: 1;
        }

        .dark .kone-search-result-item::after {
            background-color: rgba(75, 85, 99, 0.3);
            color: #9ca3af;
        }

        .kone-search-result-title {
            font-weight: 500;
            font-size: 14px;
            margin-bottom: 5px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            padding-right: 80px;
        }

        .kone-search-result-content {
            font-size: 12px;
            margin-bottom: 5px;
            color: #6b7280;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            padding-right: 80px;
        }

        .dark .kone-search-result-content {
            color: #a1a1aa;
        }

        .kone-search-result-meta {
            display: flex;
            justify-content: space-between;
            font-size: 12px;
            color: #6b7280;
            padding-right: 80px;
        }

        .dark .kone-search-result-meta {
            color: #a1a1aa;
        }

        .kone-search-result-highlight {
            background-color: rgba(59, 130, 246, 0.2);
            padding: 0 2px;
            border-radius: 2px;
        }

        .kone-search-loading {
            display: none;
            justify-content: center;
            align-items: center;
            padding: 15px 0;
            flex-direction: column;
            gap: 10px;
        }

        .kone-search-spinner {
            width: 24px;
            height: 24px;
            border: 3px solid #f3f3f3;
            border-top: 3px solid #3b82f6;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }

        .dark .kone-search-spinner {
            border-color: #3f3f46;
            border-top-color: #4b5563;
        }

        .kone-search-progress {
            font-size: 13px;
            color: #6b7280;
            text-align: center;
        }

        .kone-search-debug {
            font-size: 11px;
            color: #9ca3af;
            margin-top: 5px;
            max-height: 60px;
            overflow-y: auto;
            background-color: rgba(0,0,0,0.05);
            padding: 5px;
            border-radius: 4px;
            display: none;
        }

        .dark .kone-search-debug {
            background-color: rgba(255,255,255,0.05);
            color: #a1a1aa;
        }

        .dark .kone-search-progress {
            color: #a1a1aa;
        }

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

        .kone-search-no-results {
            padding: 15px;
            text-align: center;
            color: #6b7280;
            font-size: 14px;
            display: none;
        }

        .dark .kone-search-no-results {
            color: #a1a1aa;
        }

        .kone-search-page-range {
            display: flex;
            gap: 5px;
            align-items: center;
            margin-top: 10px;
        }

        .kone-search-page-input {
            width: 50px;
            padding: 5px;
            border: 1px solid #e5e7eb;
            border-radius: 4px;
            text-align: center;
        }

        .dark .kone-search-page-input {
            background-color: #3f3f46;
            border-color: #52525b;
            color: #e4e4e7;
        }

        .kone-search-cancel-button {
            background-color: #ef4444;
            color: white;
            border: none;
            border-radius: 6px;
            padding: 8px 16px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            display: none;
            margin-top: 10px;
            width: 100%;
        }

        .kone-search-cancel-button:hover {
            background-color: #dc2626;
        }

        .kone-search-options {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            margin-top: 10px;
        }

        .kone-search-option {
            display: flex;
            align-items: center;
            gap: 5px;
        }

        .kone-search-option input[type="checkbox"] {
            margin: 0;
        }

        .kone-search-option label {
            font-size: 13px;
            user-select: none;
        }

        .kone-search-new-tab-info {
            padding: 8px 12px;
            background-color: rgba(59, 130, 246, 0.1);
            border: 1px solid rgba(59, 130, 246, 0.2);
            border-radius: 6px;
            font-size: 12px;
            color: #3b82f6;
            margin-top: 10px;
            text-align: center;
        }

        .dark .kone-search-new-tab-info {
            background-color: rgba(75, 85, 99, 0.2);
            border-color: rgba(75, 85, 99, 0.3);
            color: #9ca3af;
        }
    `);

    // DOM 요소 생성
    function createElements() {
        // 검색 버튼
        const searchButton = document.createElement('div');
        searchButton.className = 'kone-search-button';
        searchButton.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="11" cy="11" r="8"></circle>
                <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
            </svg>
        `;
        document.body.appendChild(searchButton);

        // 검색 패널
        const searchPanel = document.createElement('div');
        searchPanel.className = 'kone-search-panel';
        searchPanel.innerHTML = `
            <div class="kone-search-header">
                <div class="kone-search-title">확장 검색</div>
                <div class="kone-search-close">
                    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <line x1="18" y1="6" x2="6" y2="18"></line>
                        <line x1="6" y1="6" x2="18" y2="18"></line>
                    </svg>
                </div>
            </div>
            <div class="kone-search-content">
                <div class="kone-search-form">
                    <div class="kone-search-input-container">
                        <div class="kone-search-icon">
                            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                <circle cx="11" cy="11" r="8"></circle>
                                <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
                            </svg>
                        </div>
                        <input type="text" class="kone-search-input" placeholder="검색어를 입력하세요...">
                    </div>

                    <div class="kone-search-options">
                        <div class="kone-search-option">
                            <input type="checkbox" id="kone-search-title" class="kone-search-checkbox" checked>
                            <label for="kone-search-title" class="kone-search-option-label">제목</label>
                        </div>
                        <div class="kone-search-option">
                            <input type="checkbox" id="kone-search-content" class="kone-search-checkbox">
                            <label for="kone-search-content" class="kone-search-option-label">내용</label>
                        </div>
                        <div class="kone-search-option">
                            <input type="checkbox" id="kone-search-author" class="kone-search-checkbox">
                            <label for="kone-search-author" class="kone-search-option-label">작성자</label>
                        </div>
                    </div>

                    <div class="kone-search-page-range">
                        <span>페이지 범위:</span>
                        <input type="number" class="kone-search-page-input" id="kone-search-start-page" min="1" value="1" placeholder="시작">
                        <span>~</span>
                        <input type="number" class="kone-search-page-input" id="kone-search-end-page" min="1" value="10" placeholder="끝">
                    </div>
                    <div class="kone-search-settings">
                        <div class="kone-search-checkbox-container">
                            <input type="checkbox" id="kone-search-case-sensitive" class="kone-search-checkbox">
                            <label for="kone-search-case-sensitive" class="kone-search-checkbox-label">대소문자 구분</label>
                        </div>
                        <button class="kone-search-button-submit">검색</button>
                    </div>
                </div>

                <button class="kone-search-cancel-button">검색 중단</button>

                <div class="kone-search-loading">
                    <div class="kone-search-spinner"></div>
                    <div class="kone-search-progress">
                        검색 중... 페이지 <span id="kone-search-current-page">1</span>/<span id="kone-search-end-page-display">10</span>
                        <br>발견된 게시글: <span id="kone-search-found-count">0</span>개
                    </div>
                    <div class="kone-search-debug"></div>
                </div>

                <div class="kone-search-no-results">
                    검색 결과가 없습니다.
                </div>

                <div class="kone-search-new-tab-info" style="display: none;">
                    💡 검색 결과를 클릭하면 새 탭에서 열립니다
                </div>

                <div class="kone-search-results">
                    <div class="kone-search-results-header">
                        검색 결과 <span class="kone-search-results-count">0개</span>
                    </div>
                    <div class="kone-search-results-list">
                        <!-- 검색 결과 아이템 -->
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(searchPanel);

        return {
            searchButton,
            searchPanel,
            searchInput: searchPanel.querySelector('.kone-search-input'),
            searchSubmitButton: searchPanel.querySelector('.kone-search-button-submit'),
            searchCaseSensitive: searchPanel.querySelector('#kone-search-case-sensitive'),
            searchResults: searchPanel.querySelector('.kone-search-results'),
            searchResultsList: searchPanel.querySelector('.kone-search-results-list'),
            searchResultsCount: searchPanel.querySelector('.kone-search-results-count'),
            searchLoading: searchPanel.querySelector('.kone-search-loading'),
            searchCurrentPage: searchPanel.querySelector('#kone-search-current-page'),
            searchEndPageDisplay: searchPanel.querySelector('#kone-search-end-page-display'),
            searchFoundCount: searchPanel.querySelector('#kone-search-found-count'),
            searchNoResults: searchPanel.querySelector('.kone-search-no-results'),
            searchCloseButton: searchPanel.querySelector('.kone-search-close'),
            searchDebug: searchPanel.querySelector('.kone-search-debug'),
            searchStartPage: searchPanel.querySelector('#kone-search-start-page'),
            searchEndPage: searchPanel.querySelector('#kone-search-end-page'),
            searchCancelButton: searchPanel.querySelector('.kone-search-cancel-button'),
            searchTitle: searchPanel.querySelector('#kone-search-title'),
            searchContent: searchPanel.querySelector('#kone-search-content'),
            searchAuthor: searchPanel.querySelector('#kone-search-author'),
            searchNewTabInfo: searchPanel.querySelector('.kone-search-new-tab-info')
        };
    }

    // 디버그 메시지 추가
    function addDebugMessage(message) {
        if (DEBUG) {
            const { searchDebug } = elements;
            searchDebug.style.display = 'block';
            searchDebug.innerHTML += `${message}<br>`;
            searchDebug.scrollTop = searchDebug.scrollHeight;
        }
    }

    // JSON 문자열을 검증하고 수정하는 함수
    function validateAndFixJson(jsonStr) {
        try {
            // 기본 검증 시도
            JSON.parse(jsonStr);
            return jsonStr; // 유효한 경우 그대로 반환
        } catch (e) {
            addDebugMessage('JSON 검증 실패, 문제 해결 시도 중...');

            // 일반적인 JSON 문제 해결 시도
            let fixedJson = jsonStr;

            // 1. 쉼표 뒤에 빠진 공백 추가
            fixedJson = fixedJson.replace(/,(?!\s)/g, ', ');

            // 2. 콜론 뒤에 빠진 공백 추가
            fixedJson = fixedJson.replace(/:(?!\s)/g, ': ');

            // 3. 속성 이름에 큰따옴표 확인
            fixedJson = fixedJson.replace(/([{,]\s*)([a-zA-Z0-9_]+)(\s*:)/g, '$1"$2"$3');

            // 4. 마지막 속성 뒤에 콤마가 있는 경우 제거
            fixedJson = fixedJson.replace(/,\s*}/g, ' }');

            // 5. 문자열 내부의 이스케이프되지 않은 따옴표 처리
            fixedJson = fixedJson.replace(/"([^"\\]*(\\.[^"\\]*)*)"/g, function(match) {
                return match.replace(/(?<!\\)"/g, '\\"');
            });

            try {
                // 수정된 JSON이 유효한지 확인
                JSON.parse(fixedJson);
                addDebugMessage('JSON 수정 성공!');
                return fixedJson;
            } catch (fixError) {
                addDebugMessage('JSON 수정 실패: ' + fixError.message);

                // 최후의 수단: 정규식으로 필요한 필드만 직접 추출
                try {
                    const idMatch = jsonStr.match(/"id"\s*:\s*"([^"]+)"/);
                    const titleMatch = jsonStr.match(/"title"\s*:\s*"([^"]+)"/);
                    const contentMatch = jsonStr.match(/"content"\s*:\s*"([^"]+)"/);
                    const writerMatch = jsonStr.match(/"writer"\s*:\s*{([^}]+)}/);

                    if (idMatch && titleMatch) {
                        // 최소한의 필드로 간단한 JSON 객체 생성
                        const simpleJson = {
                            id: idMatch[1],
                            title: titleMatch[1],
                            content: contentMatch ? contentMatch[1] : "",
                            writer: { display_name: "알 수 없음" }
                        };

                        // writer 정보 추출 시도
                        if (writerMatch) {
                            const displayNameMatch = writerMatch[1].match(/"display_name"\s*:\s*"([^"]+)"/);
                            if (displayNameMatch) {
                                simpleJson.writer.display_name = displayNameMatch[1];
                            }
                        }

                        addDebugMessage('수동 필드 추출로 간단한 JSON 생성 성공');
                        return JSON.stringify(simpleJson);
                    }
                } catch (e) {
                    addDebugMessage('수동 필드 추출 실패: ' + e.message);
                }
            }

            return null; // 모든 시도 실패
        }
    }

    // 게시글 추출 함수: 특정 형식 추출
    function extractSpecificArticleFormat(html) {
        try {
            addDebugMessage('특정 형식의 게시글 데이터 추출 시도...');

            // HTML 콘텐츠 사용
            const htmlContent = html;

            // 예시 형식에 맞는 정규식 패턴
            const pattern = /\{(?:\\\"|\")id(?:\\\"|\"):(?:\\\"|\")[\w-]+(?:\\\"|\"),(?:\\\"|\")title(?:\\\"|\"):(?:\\\"|\")[^\"]*(?:\\\"|\"),(?:\\\"|\")content(?:\\\"|\"):(?:\\\"|\")[^\"]*(?:\\\"|\").*?(?:\\\"|\")created_at_formatted(?:\\\"|\"):(?:\\\"|\")[^\"]*(?:\\\"|\")}/g;

            const articles = [];
            let match;

            // 패턴에 맞는 문자열 찾기
            while ((match = pattern.exec(htmlContent)) !== null) {
                try {
                    // 추출한 문자열 확인 및 정리
                    let articleJson = match[0];
                    addDebugMessage('추출된 원본 문자열: ' + articleJson.substring(0, 50) + '...');

                    // 이스케이프된 따옴표를 정리
                    articleJson = articleJson
                        .replace(/\\\\"/g, '\\"')  // 이중 이스케이프된 따옴표 처리
                        .replace(/\\"/g, '"')      // 일반 이스케이프된 따옴표 처리
                        .replace(/"{/g, '{')       // JSON 객체 시작 부분의 따옴표 제거
                        .replace(/}"/g, '}');      // JSON 객체 끝 부분의 따옴표 제거

                    // 유효한 JSON 형식인지 확인
                    const validJson = validateAndFixJson(articleJson);

                    if (validJson) {
                        try {
                            const article = JSON.parse(validJson);
                            articles.push(article);
                            addDebugMessage('게시글 파싱 성공!');
                        } catch (parseError) {
                            addDebugMessage('최종 JSON 파싱 오류: ' + parseError.message);
                        }
                    }
                } catch (e) {
                    addDebugMessage('게시글 처리 오류: ' + e.message);
                }
            }

            addDebugMessage(`총 ${articles.length}개의 특정 형식 게시글을 추출했습니다.`);
            return articles;
        } catch (error) {
            addDebugMessage('특정 형식 추출 실패: ' + error.message);
            return [];
        }
    }

    // 페이지에서 게시글 가져오기
    async function getArticlesFromPage(subName, page) {
        try {
            const url = `https://kone.gg/s/${subName}${page ? `?p=${page}` : ''}`;
            addDebugMessage(`페이지 로드 중: ${url}`);

            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`서브 페이지 가져오기 실패: ${response.status}`);
            }

            const html = await response.text();
            return extractSpecificArticleFormat(html);
        } catch (error) {
            addDebugMessage(`오류: ${error.message}`);
            return [];
        }
    }

    async function performSearch(subName, keyword, options) {
        const { isCaseSensitive, startPage, endPage, searchTitle, searchContent, searchAuthor } = options;

        // 검색 중단 플래그 초기화
        searchCancelled = false;

        // 검색 버튼 숨기고 중단 버튼 표시
        elements.searchSubmitButton.style.display = 'none';
        elements.searchCancelButton.style.display = 'block';

        let allResults = [];

        // 검색어 처리
        const searchKeyword = isCaseSensitive ? keyword : keyword.toLowerCase();

        addDebugMessage(`검색 시작: '${keyword}' (${isCaseSensitive ? '대소문자 구분' : '대소문자 무시'}) - 페이지 범위: ${startPage}~${endPage}`);
        addDebugMessage(`검색 대상: ${searchTitle ? '제목 ' : ''}${searchContent ? '내용 ' : ''}${searchAuthor ? '작성자' : ''}`);

        // 페이지 범위 내에서 검색
        for (let currentPage = startPage; currentPage <= endPage; currentPage++) {
            // 검색 중단 확인
            if (searchCancelled) {
                addDebugMessage('사용자에 의해 검색이 중단되었습니다.');
                break;
            }

            // 현재 페이지 표시 업데이트
            elements.searchCurrentPage.textContent = currentPage;

            // 현재 페이지의 게시글 가져오기
            const articles = await getArticlesFromPage(subName, currentPage);

            // 빈 페이지 확인
            if (articles.length === 0) {
                addDebugMessage(`페이지 ${currentPage}: 게시글을 찾을 수 없음`);
                continue;
            }

            // 검색 결과 필터링
            const results = articles.filter(article => {
                // 검색 대상이 하나도 선택되지 않았을 경우
                if (!searchTitle && !searchContent && !searchAuthor) {
                    return false;
                }

                let found = false;

                // 제목 검색
                if (searchTitle && article.title) {
                    const titleForSearch = isCaseSensitive ? article.title : article.title.toLowerCase();
                    if (titleForSearch.includes(searchKeyword)) {
                        found = true;
                    }
                }

                // 내용 검색
                if (!found && searchContent && article.content) {
                    const contentForSearch = isCaseSensitive ? article.content : article.content.toLowerCase();
                    if (contentForSearch.includes(searchKeyword)) {
                        found = true;
                    }
                }

                // 작성자 검색
                if (!found && searchAuthor && article.writer && article.writer.display_name) {
                    const authorForSearch = isCaseSensitive ? article.writer.display_name : article.writer.display_name.toLowerCase();
                    if (authorForSearch.includes(searchKeyword)) {
                        found = true;
                    }
                }

                return found;
            });

            addDebugMessage(`페이지 ${currentPage}: ${articles.length}개 중 ${results.length}개 일치`);

            // 검색 결과를 표준 형식으로 변환
            const formattedResults = results.map(article => {
                // 어디에서 일치했는지 확인
                let matchType = '';

                if (searchTitle && article.title) {
                    const titleForSearch = isCaseSensitive ? article.title : article.title.toLowerCase();
                    if (titleForSearch.includes(searchKeyword)) {
                        matchType += '제목 ';
                    }
                }

                if (searchContent && article.content) {
                    const contentForSearch = isCaseSensitive ? article.content : article.content.toLowerCase();
                    if (contentForSearch.includes(searchKeyword)) {
                        matchType += '내용 ';
                    }
                }

                if (searchAuthor && article.writer && article.writer.display_name) {
                    const authorForSearch = isCaseSensitive ? article.writer.display_name : article.writer.display_name.toLowerCase();
                    if (authorForSearch.includes(searchKeyword)) {
                        matchType += '작성자 ';
                    }
                }

                // 내용 일부 가져오기 (최대 50자)
                let contentPreview = '';
                if (article.content) {
                    contentPreview = article.content.length > 50
                        ? article.content.substring(0, 50) + '...'
                        : article.content;
                }

                return {
                    article_id: article.id,
                    title: article.title || '제목 없음',
                    content: contentPreview,
                    url: `https://kone.gg/s/${subName}/${article.id}`,
                    author: article.writer ? (article.writer.display_name || "익명") : "익명",
                    date: article.created_at_formatted || '날짜 없음',
                    views: article.views,
                    comments: article.comments,
                    matchType: matchType.trim()
                };
            });

            // 검색 결과 추가
            allResults = [...allResults, ...formattedResults];

            // 발견된 게시글 수 업데이트
            elements.searchFoundCount.textContent = allResults.length;

            // 서버 부하 방지를 위한 지연
            await new Promise(resolve => setTimeout(resolve, 500));
        }

        // 검색 버튼 표시하고 중단 버튼 숨기기
        elements.searchSubmitButton.style.display = 'block';
        elements.searchCancelButton.style.display = 'none';

        addDebugMessage(`검색 완료: 총 ${allResults.length}개 게시글 발견`);
        return allResults;
    }

    // 검색 결과 표시 함수
    function displaySearchResults(results, keyword, options) {
        const { searchResults, searchResultsList, searchResultsCount, searchNoResults, searchNewTabInfo } = elements;
        const { isCaseSensitive, searchTitle, searchContent, searchAuthor } = options;

        // 검색 결과 목록 초기화
        searchResultsList.innerHTML = '';

        // 검색 결과가 없는 경우
        if (results.length === 0) {
            searchResults.style.display = 'none';
            searchNoResults.style.display = 'block';
            searchNewTabInfo.style.display = 'none';
            return;
        }

        // 검색 결과 카운트 업데이트
        searchResultsCount.textContent = `${results.length}개`;

        // 새 탭 정보 표시
        searchNewTabInfo.style.display = 'block';

        // 검색 결과 표시
        results.forEach(result => {
            const resultItem = document.createElement('div');
            resultItem.className = 'kone-search-result-item';

            // 하이라이트 함수
            const highlightKeyword = (text, keyword, isCaseSensitive) => {
                try {
                    if (!text) return '';

                    if (!isCaseSensitive) {
                        const regex = new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
                        return text.replace(regex, match => `<span class="kone-search-result-highlight">${match}</span>`);
                    } else {
                        return text.split(keyword).join(`<span class="kone-search-result-highlight">${keyword}</span>`);
                    }
                } catch (e) {
                    // 오류 발생 시 원래 텍스트 반환
                    return text;
                }
            };

            // 필요한 부분만 하이라이트 처리
            let title = result.title;
            let content = result.content;
            let author = result.author;

            if (searchTitle) {
                title = highlightKeyword(title, keyword, isCaseSensitive);
            }

            if (searchContent) {
                content = highlightKeyword(content, keyword, isCaseSensitive);
            }

            if (searchAuthor) {
                author = highlightKeyword(author, keyword, isCaseSensitive);
            }

            resultItem.innerHTML = `
                <div class="kone-search-result-title">${title}</div>
                <div class="kone-search-result-content">${content}</div>
                <div class="kone-search-result-meta">
                    <div>${author}</div>
                    <div>${result.date || '날짜 없음'} [${result.matchType}]</div>
                </div>
            `;

            // 클릭 이벤트 추가 - 새 탭에서 열기
            resultItem.addEventListener('click', (e) => {
                e.preventDefault();
                // 새 탭에서 열기
                window.open(result.url, '_blank');
                log(`새 탭에서 게시글 열기: ${result.url}`);
            });

            // 마우스 오버 시 커서 변경 및 시각적 피드백
            resultItem.style.cursor = 'pointer';

            searchResultsList.appendChild(resultItem);
        });

        // 검색 결과 표시
        searchResults.style.display = 'block';
        searchNoResults.style.display = 'none';
    }

    // 이벤트 핸들러 설정
    function setupEventHandlers() {
        const {
            searchButton,
            searchPanel,
            searchInput,
            searchSubmitButton,
            searchCaseSensitive,
            searchResults,
            searchLoading,
            searchNoResults,
            searchCloseButton,
            searchDebug,
            searchStartPage,
            searchEndPage,
            searchEndPageDisplay,
            searchCancelButton,
            searchTitle,
            searchContent,
            searchAuthor,
            searchNewTabInfo
        } = elements;

        // 검색 패널 토글
        let isPanelVisible = false;

        searchButton.addEventListener('click', () => {
            isPanelVisible = !isPanelVisible;
            searchPanel.style.display = isPanelVisible ? 'block' : 'none';

            if (isPanelVisible) {
                searchInput.focus();
            }
        });

        // 검색 패널 닫기
        searchCloseButton.addEventListener('click', () => {
            searchPanel.style.display = 'none';
            isPanelVisible = false;
        });

        // Enter 키로 검색 실행
        searchInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                searchSubmitButton.click();
            }
        });

        // 페이지 범위 입력 변경 시 끝 페이지 표시 업데이트
        searchEndPage.addEventListener('change', () => {
            searchEndPageDisplay.textContent = searchEndPage.value;
        });

        // 검색 중단 버튼 클릭
        searchCancelButton.addEventListener('click', () => {
            searchCancelled = true;
            searchCancelButton.textContent = "검색 중단 중...";
            searchCancelButton.disabled = true;
        });

        // 검색 버튼 클릭
        searchSubmitButton.addEventListener('click', async () => {
            const keyword = searchInput.value.trim();
            const isCaseSensitive = searchCaseSensitive.checked;
            const subName = getCurrentSubName();
            const startPage = parseInt(searchStartPage.value) || 1;
            const endPage = parseInt(searchEndPage.value) || 10;
            const searchInTitle = searchTitle.checked;
            const searchInContent = searchContent.checked;
            const searchInAuthor = searchAuthor.checked;

            // 끝 페이지 표시 업데이트
            searchEndPageDisplay.textContent = endPage;

            // 입력 검증
            if (!keyword) {
                alert('검색어를 입력해주세요.');
                return;
            }

            if (!subName) {
                alert('서브 페이지에서만 검색이 가능합니다.');
                return;
            }

            if (startPage > endPage) {
                alert('시작 페이지는 끝 페이지보다 작거나 같아야 합니다.');
                return;
            }

            if (!searchInTitle && !searchInContent && !searchInAuthor) {
                alert('제목, 내용, 작성자 중 하나 이상 선택해주세요.');
                return;
            }

            // UI 상태 업데이트
            searchResults.style.display = 'none';
            searchNoResults.style.display = 'none';
            searchNewTabInfo.style.display = 'none';
            searchLoading.style.display = 'flex';
            searchDebug.innerHTML = ''; // 디버그 초기화

            if (DEBUG) {
                searchDebug.style.display = 'block';
            }

            try {
                // 검색 옵션 설정
                const searchOptions = {
                    isCaseSensitive,
                    startPage,
                    endPage,
                    searchTitle: searchInTitle,
                    searchContent: searchInContent,
                    searchAuthor: searchInAuthor
                };

                // 검색 실행
                const results = await performSearch(subName, keyword, searchOptions);

                // 검색 결과 표시
                displaySearchResults(results, keyword, searchOptions);
            } catch (error) {
                console.error('검색 오류:', error);
                addDebugMessage(`심각한 오류: ${error.message}`);
                alert('검색 중 오류가 발생했습니다.');
            } finally {
                // 로딩 상태 종료
                searchLoading.style.display = 'none';
                // 중단 버튼 초기화
                searchCancelButton.textContent = "검색 중단";
                searchCancelButton.disabled = false;
                searchCancelButton.style.display = 'none';
                searchSubmitButton.style.display = 'block';
            }
        });

        // 문서 클릭 시 패널 닫기 (패널 외부 클릭)
        document.addEventListener('click', (e) => {
            if (isPanelVisible &&
                !searchPanel.contains(e.target) &&
                !searchButton.contains(e.target)) {
                searchPanel.style.display = 'none';
                isPanelVisible = false;
            }
        });
    }

    // 초기화 함수
    function init() {
        // DOM 요소 생성
        const createdElements = createElements();
        window.elements = createdElements;

        // 이벤트 핸들러 설정
        setupEventHandlers();

        log('KoneGG 확장 검색 시스템 (제목/내용/작성자 검색 지원, 새탭 열기)이 초기화되었습니다.');
    }

    // 페이지 로드 완료 후 초기화
    if (document.readyState === 'complete') {
        init();
    } else {
        window.addEventListener('load', init);
    }
})();