NSFW 채널은 ㅅㅂ 돔 파싱을 하려면 로드 다 되고 나서 해야 하더라
그래서 그냥 script에서 직접 따다가 검색하도록 수정함.
좀 불완전하게 검색될 수도 있음
// ==UserScript==
// @name KoneGG 확장 검색 시스템 (제목/내용/작성자 검색 지원)
// @namespace http://tampermonkey.net/
// @version 2.2
// @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;
}
.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-title {
font-weight: 500;
font-size: 14px;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.kone-search-result-content {
font-size: 12px;
margin-bottom: 5px;
color: #6b7280;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dark .kone-search-result-content {
color: #a1a1aa;
}
.kone-search-result-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #6b7280;
}
.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;
}
`);
// 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-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')
};
}
// 디버그 메시지 추가
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 } = elements;
const { isCaseSensitive, searchTitle, searchContent, searchAuthor } = options;
// 검색 결과 목록 초기화
searchResultsList.innerHTML = '';
// 검색 결과가 없는 경우
if (results.length === 0) {
searchResults.style.display = 'none';
searchNoResults.style.display = 'block';
return;
}
// 검색 결과 카운트 업데이트
searchResultsCount.textContent = `${results.length}개`;
// 검색 결과 표시
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', () => {
window.location.href = result.url;
});
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
} = 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';
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);
}
})();
직접 페이지 하나하나에 요청 날려서 리스트를 긁고, 거기에서 검색하는거라 속도 뒤지게 느림
10페이지 이상으로 검색하지 않는것을 추천