This commit is contained in:
2026-10-16 17:10:31 +09:00
parent d73a1dda56
commit bf391ece64
4 changed files with 83 additions and 31 deletions
@@ -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) {
@@ -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);
};
@@ -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"
@@ -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) {