您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
kone.gg 사이트에서 제목, 내용, 작성자명이 특정 키워드를 포함하는 게시글을 검색합니다 (새탭 열기 지원)
当前为
// ==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); } })();