...
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user