From bf391ece642a6cd96969e59dad947e22c9a95dcc Mon Sep 17 00:00:00 2001 From: lunaticbum Date: Fri, 16 Oct 2026 17:10:31 +0900 Subject: [PATCH] ... --- .../my_extension/Novel_Scraper/content.js | 86 +++++++++++++------ .../my_extension/Novel_Scraper/inject.js | 7 ++ .../my_extension/Novel_Scraper/manifest.json | 13 ++- .../my_extension/Novel_Scraper/popup.js | 8 +- 4 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 app/src/main/assets/extensions/my_extension/Novel_Scraper/inject.js diff --git a/app/src/main/assets/extensions/my_extension/Novel_Scraper/content.js b/app/src/main/assets/extensions/my_extension/Novel_Scraper/content.js index 5031877a..4b4a26ae 100644 --- a/app/src/main/assets/extensions/my_extension/Novel_Scraper/content.js +++ b/app/src/main/assets/extensions/my_extension/Novel_Scraper/content.js @@ -1,12 +1,21 @@ -// 페이지 로딩 완료 후 실행 -window.addEventListener('load', async () => { +let isRunning = false; +let currentUrl = location.href; + +// 메인 스크래핑 로직을 별도의 함수로 분리 +async function runScraper() { + if (isRunning) return; // 중복 실행 방지 + isRunning = true; + // 1. URL 패턴 확인 const match = window.location.pathname.match(/\/novel\/(\d+)\/(\d+)/); - if (!match) return; + if (!match) { + isRunning = false; + return; + } const bookId = match[1]; const chapterId = match[2]; - console.log(`웹소설 스크래퍼: 책 ID [${bookId}] 감지됨.`); + console.log(`웹소설 스크래퍼: 책 ID [${bookId}], 챕터 ID [${chapterId}] 감지됨.`); // ⏳ 2. 1~2초 동안 '페이지 다운(80%)' 스크롤 실행 const initialScrollTime = Math.floor(Math.random() * (10000 - 1000 + 1)) + 8000; @@ -15,7 +24,10 @@ window.addEventListener('load', async () => { // 3. 데이터 스크래핑 const scrapedData = scrapePage(); - if (!scrapedData) return; + if (!scrapedData) { + isRunning = false; + return; + } // 4. 책 ID별 Storage 저장 chrome.storage.local.get(['scrapedBooks'], async (result) => { @@ -39,6 +51,7 @@ window.addEventListener('load', async () => { const isDuplicate = bookData.chapters.some(chap => chap.chapterId === chapterId); if (isDuplicate) { console.log(`⚠️ 이미 저장된 챕터입니다 (ID: ${chapterId}). 자동 진행을 중지합니다.`); + isRunning = false; return; } @@ -57,72 +70,91 @@ window.addEventListener('load', async () => { }, async () => { console.log(`✅ [${scrapedData.novelTitle} - ${scrapedData.chapterTitle}] 저장 완료!`); - // ⏳ 5. 저장 완료 후 2~3초 랜덤 대기 + // ⏳ 5. 저장 완료 후 대기 const waitDelay = Math.floor(Math.random() * (5000 - 2000 + 1)) + 4000; console.log(`${(waitDelay / 1000).toFixed(1)}초 대기 후 다음 챕터로 이동합니다... ⏳`); await new Promise(resolve => setTimeout(resolve, waitDelay)); - // '다음화' 버튼 클릭 - const buttons = Array.from(document.querySelectorAll('a.btn.btn-black.btn-sm')); - const nextButton = buttons.find(btn => btn.innerText.trim().includes('다음화')); + // '다음화' 버튼 클릭 (업데이트된 구조 호환) + const buttons = Array.from(document.querySelectorAll('a.btn.btn-black.btn-sm, a.btn.btn--primary')); + let nextButton = buttons.find(btn => btn.innerText.trim().includes('다음화')); + + if (!nextButton) { + nextButton = document.querySelector('a[data-ntk-next-link]'); + } if (nextButton) { console.log("👉 다음 화 버튼 클릭 진행"); + isRunning = false; // 다음화를 위해 플래그 해제 nextButton.click(); } else { console.log("⚠️ '다음화' 버튼을 찾을 수 없거나 마지막 화입니다."); + isRunning = false; } }); }); -}); +} // ========================================== -// 🚀 화면 높이의 약 80%씩 큼직하게 스크롤하는 함수 +// 🚀 초기 로드 및 URL 변경 감지 +// ========================================== + +// 1. 처음 페이지 로드 시 실행 +window.addEventListener('load', runScraper); + +// 2. DOM 변화를 감지하여 URL이 변경되었는지 확인 (SPA 동작 대응) +const observer = new MutationObserver(() => { + if (location.href !== currentUrl) { + console.log(`🔄 URL 변경 감지됨: ${currentUrl} -> ${location.href}`); + currentUrl = location.href; + + // 새로운 페이지의 DOM이 렌더링될 시간을 벌기 위해 2초 대기 후 실행 + setTimeout(runScraper, 2000); + } +}); + +// 문서 전체의 자식 노드 변화 관찰 시작 +observer.observe(document.body, { childList: true, subtree: true }); + + +// ========================================== +// 🚀 화면 스크롤 함수 (기존과 동일) // ========================================== function pageSkimScroll(duration) { return new Promise((resolve) => { let elapsed = 0; - const scrollLoop = () => { if (elapsed >= duration) { resolve(); return; } - - // 400ms ~ 700ms 사이의 랜덤한 대기 시간 (큼직하게 내리기 때문에 간격을 조금 더 줌) const stepTime = Math.floor(Math.random() * (1700 - 400 + 1)) + 1400; - - // 현재 브라우저 창 높이(Viewport)를 구함 const screenHeight = window.innerHeight; - - // 75% ~ 85% 사이의 랜덤한 비율 설정 (평균 80%) const scrollRatio = 0.75 + (Math.random() * 0.1); const stepAmount = screenHeight * scrollRatio; - - // 부드럽게 스크롤 window.scrollBy({ top: stepAmount, behavior: 'smooth' }); - elapsed += stepTime; setTimeout(scrollLoop, stepTime); }; - - // 루프 시작 scrollLoop(); }); } // ========================================== -// 스크래핑 헬퍼 함수 +// 스크래핑 헬퍼 함수 (최신화된 선택자 적용) // ========================================== function scrapePage() { - const titleEl = document.querySelector('.page-title h2'); + const titleEl = document.querySelector('.page-title h2') || document.querySelectorAll(".page-top")[0]?.querySelectorAll("a")[2]; let novelTitle = titleEl ? titleEl.innerText.trim() : '제목없음'; - let chapterTitle = document.querySelector('.theme-novel-title'); + let chapterTitle = document.querySelector('.theme-novel-title') || document.querySelectorAll(".page-top")[0]?.querySelector("strong"); chapterTitle = chapterTitle ? chapterTitle.innerText.trim() : '1화'; let novelContent = ''; - const contentHost = document.querySelector('.theme-novel-content'); + // Shadow DOM을 포함한 컨테이너 찾기 + const contentHost = document.querySelector('.theme-novel-content') || + document.querySelector('.novel-viewer > div[style*="--novel-font-size"]') || + document.querySelectorAll(".novel-viewer")[0]?.querySelectorAll("div")[1]; if (contentHost) { if (contentHost.shadowRoot) { diff --git a/app/src/main/assets/extensions/my_extension/Novel_Scraper/inject.js b/app/src/main/assets/extensions/my_extension/Novel_Scraper/inject.js new file mode 100644 index 00000000..c4087cf1 --- /dev/null +++ b/app/src/main/assets/extensions/my_extension/Novel_Scraper/inject.js @@ -0,0 +1,7 @@ +const originalAttachShadow = Element.prototype.attachShadow; +Element.prototype.attachShadow = function(options) { + if (options && options.mode === 'closed') { + options.mode = 'open'; + } + return originalAttachShadow.call(this, options); +}; \ No newline at end of file diff --git a/app/src/main/assets/extensions/my_extension/Novel_Scraper/manifest.json b/app/src/main/assets/extensions/my_extension/Novel_Scraper/manifest.json index d4dc5134..23f5b2b4 100644 --- a/app/src/main/assets/extensions/my_extension/Novel_Scraper/manifest.json +++ b/app/src/main/assets/extensions/my_extension/Novel_Scraper/manifest.json @@ -20,7 +20,18 @@ { "matches": [ "*://newtoki1.org/novel/*", - "*://*.newtoki1.org/novel/*" + "*://*.newtoki1.org/novel/*", + "*://toki35.com/novel/*" + ], + "js": ["inject.js"], + "run_at": "document_start", + "world": "MAIN" + }, + { + "matches": [ + "*://newtoki1.org/novel/*", + "*://*.newtoki1.org/novel/*", + "*://toki35.com/novel/*" ], "js": ["content.js"], "run_at": "document_end" diff --git a/app/src/main/assets/extensions/my_extension/Novel_Scraper/popup.js b/app/src/main/assets/extensions/my_extension/Novel_Scraper/popup.js index 14d02ad7..672c8aa4 100644 --- a/app/src/main/assets/extensions/my_extension/Novel_Scraper/popup.js +++ b/app/src/main/assets/extensions/my_extension/Novel_Scraper/popup.js @@ -124,14 +124,16 @@ function sendToTelegram() { // 스크래핑 함수 (수동 저장용) function scrapePage() { - const titleEl = document.querySelector('.page-title h2'); + const titleEl = document.querySelector('.page-title h2') || document.querySelectorAll(".page-top")[0].querySelectorAll("a")[2]; let novelTitle = titleEl ? titleEl.innerText.trim() : '제목없음'; - let chapterTitle = document.querySelector('.theme-novel-title'); + let chapterTitle = document.querySelector('.theme-novel-title') || document.querySelectorAll(".page-top")[0].querySelector("strong"); chapterTitle = chapterTitle ? chapterTitle.innerText.trim() : '1화'; let novelContent = ''; - const contentHost = document.querySelector('.theme-novel-content'); + // content.js 및 popup.js의 scrapePage() 내부 수정 + const contentHost = document.querySelector('.theme-novel-content') || + document.querySelector('.novel-viewer > div[style*="--novel-font-size"]'); if (contentHost) { if (contentHost.shadowRoot) {