IntersectionObserver 후킹으로 백그라운드 탭 이미지 로딩 문제 해결

- @run-at document-start로 변경하여 페이지 스크립트보다 먼저 실행
- IntersectionObserver.observe() 후킹: observe() 즉시 콜백 실행하도록 하여
  백그라운드 탭에서도 IntersectionObserver가 동작하게 함
- 기존 로직은 DOMContentLoaded 후 실행되도록 main() 함수로 분리
- scrollHeight 동적 대응 로직 유지
This commit is contained in:
user01
2026-06-14 03:31:09 +09:00
parent 36d41457f5
commit c6bc10fbeb

View File

@@ -13,394 +13,446 @@
// @grant GM_download
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.js
// @run-at document-end
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 사이트가 console을 모니터링/차단하므로 자체 로거 사용
const log = (...args) => {
try { console.log(...args); } catch (_) {}
};
// ---------------------------------------------------------------
// Phase 1: IntersectionObserver 후킹 (document-start 에서 즉시 실행)
// 백그라운드 탭에서 IO가 동작하지 않아도 observe() 즉시 콜백 실행
// ---------------------------------------------------------------
const OrigIO = window.IntersectionObserver;
if (OrigIO) {
const HookedIO = function (callback, options) {
const instance = new OrigIO(callback, options);
const origObserve = instance.observe.bind(instance);
instance.observe = function (target) {
origObserve(target);
setTimeout(() => {
try {
callback([{
target: target,
isIntersecting: true,
intersectionRatio: 1,
boundingClientRect: target.getBoundingClientRect(),
intersectionRect: target.getBoundingClientRect(),
rootBounds: null,
time: performance.now()
}], instance);
} catch (_) { }
}, 50);
};
return instance;
};
HookedIO.prototype = OrigIO.prototype;
window.IntersectionObserver = HookedIO;
}
// 메뉴 명령 ID 관리 (중복 등록 방지 → 배지 숫자 증가 방지)
let menuCmdIds = [];
// ---------------------------------------------------------------
// Phase 2: DOM 로딩 후 나머지 로직
// ---------------------------------------------------------------
const main = () => {
// 사이트가 console을 모니터링/차단하므로 자체 로거 사용
const log = (...args) => {
try { console.log(...args); } catch (_) {}
};
const registerMenu = () => {
menuCmdIds.forEach(id => {
try { GM_unregisterMenuCommand(id); } catch (_) {}
});
menuCmdIds = [];
// 메뉴 명령 ID 관리 (중복 등록 방지 → 배지 숫자 증가 방지)
let menuCmdIds = [];
const { isListPage, isEpisodePage } = getListPageInfo();
if (isListPage) {
menuCmdIds.push(GM_registerMenuCommand('[목록] 전체 에피소드 다운로드', () => downloadAllEpisodes()));
} else if (isEpisodePage) {
menuCmdIds.push(GM_registerMenuCommand('현재 에피소드 다운로드', () => downloadCurrentEpisode()));
const registerMenu = () => {
menuCmdIds.forEach(id => {
try { GM_unregisterMenuCommand(id); } catch (_) {}
});
menuCmdIds = [];
const { isListPage, isEpisodePage } = getListPageInfo();
if (isListPage) {
menuCmdIds.push(GM_registerMenuCommand('[목록] 전체 에피소드 다운로드', () => downloadAllEpisodes()));
} else if (isEpisodePage) {
menuCmdIds.push(GM_registerMenuCommand('현재 에피소드 다운로드', () => downloadCurrentEpisode()));
}
};
const downloadZip = (blob, filename) => {
const url = URL.createObjectURL(blob);
if (typeof GM_download !== "undefined") {
GM_download({
url: url,
name: filename,
onload: () => URL.revokeObjectURL(url),
onerror: () => {
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
});
} else {
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
};
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
};
const downloadZip = (blob, filename) => {
const url = URL.createObjectURL(blob);
if (typeof GM_download !== "undefined") {
GM_download({
url: url,
name: filename,
onload: () => URL.revokeObjectURL(url),
onerror: () => {
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
// 자동 연속 다운로드 큐 (sessionStorage 기반)
const QUEUE_KEY = 'sbxh6_q';
const IDX_KEY = 'sbxh6_i';
const SERIES_KEY = 'sbxh6_s';
const LIST_KEY = 'sbxh6_list';
const getQueue = () => {
try { return JSON.parse(sessionStorage.getItem(QUEUE_KEY) || 'null'); } catch (_) { return null; }
};
const getQueueIndex = () => {
try { return parseInt(sessionStorage.getItem(IDX_KEY) || '0', 10); } catch (_) { return 0; }
};
const cleanupQueue = () => {
sessionStorage.removeItem(QUEUE_KEY);
sessionStorage.removeItem(IDX_KEY);
sessionStorage.removeItem(SERIES_KEY);
sessionStorage.removeItem(LIST_KEY);
};
function generateComicInfo(title, series, writer, number, pageCount, imageCount = 0) {
let pagesXml = '';
if (imageCount > 0) {
for (let i = 0; i < imageCount; i++) {
pagesXml += ` <Page Image="${i}" Type="Story"/>\n`;
}
}
const comicInfo = `<?xml version="1.0" encoding="utf-8"?>
<ComicInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Title>${title}</Title>
<Series>${series}</Series>
<Number>${number}</Number>
<Count>-1</Count>
<Volume>-1</Volume>
<Summary></Summary>
<Notes></Notes>
<Year>-1</Year>
<Month>-1</Month>
<Writer>${writer}</Writer>
<Penciller></Penciller>
<Inker></Inker>
<Colorist></Colorist>
<Letterer></Letterer>
<CoverArtist></CoverArtist>
<Editor></Editor>
<Publisher></Publisher>
<Imprint></Imprint>
<Genre></Genre>
<Web></Web>
<PageCount>${pageCount}</PageCount>
<LanguageISO>ko</LanguageISO>
<Format></Format>
<BlackAndWhite>Unknown</BlackAndWhite>
<Manga>Yes</Manga>
${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}
</ComicInfo>`;
return comicInfo;
}
function getEpisodeImages(container) {
const root = container || document;
const imgs = root.querySelectorAll(
'.vw-imgs img, .vw-imgs--double img, .vw-imgs--single img, ' +
'.webtoon-page img, .manhwa-page img, ' +
'[class*="vw-imgs"] img'
);
const urls = [];
const seen = new Set();
imgs.forEach(img => {
const src = img.src || img.getAttribute('src') || '';
if (src && src.startsWith('http') && !seen.has(src)) {
seen.add(src);
urls.push(src);
}
});
} else {
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
return urls;
}
};
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getSeriesName() {
const metaEl = document.querySelector('div.vw-bot-meta strong');
if (metaEl) return metaEl.textContent.trim();
// 자동 연속 다운로드 큐 (sessionStorage 기반)
const QUEUE_KEY = 'sbxh6_q';
const IDX_KEY = 'sbxh6_i';
const SERIES_KEY = 'sbxh6_s';
const LIST_KEY = 'sbxh6_list';
const getQueue = () => {
try { return JSON.parse(sessionStorage.getItem(QUEUE_KEY) || 'null'); } catch (_) { return null; }
};
const getQueueIndex = () => {
try { return parseInt(sessionStorage.getItem(IDX_KEY) || '0', 10); } catch (_) { return 0; }
};
const cleanupQueue = () => {
sessionStorage.removeItem(QUEUE_KEY);
sessionStorage.removeItem(IDX_KEY);
sessionStorage.removeItem(SERIES_KEY);
sessionStorage.removeItem(LIST_KEY);
};
function generateComicInfo(title, series, writer, number, pageCount, imageCount = 0) {
let pagesXml = '';
if (imageCount > 0) {
for (let i = 0; i < imageCount; i++) {
pagesXml += ` <Page Image="${i}" Type="Story"/>\n`;
const titleEl = document.querySelector('h1');
if (titleEl && !titleEl.closest('.hero-v2')) {
return titleEl.textContent.trim();
}
}
const comicInfo = `<?xml version="1.0" encoding="utf-8"?>
<ComicInfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Title>${title}</Title>
<Series>${series}</Series>
<Number>${number}</Number>
<Count>-1</Count>
<Volume>-1</Volume>
<Summary></Summary>
<Notes></Notes>
<Year>-1</Year>
<Month>-1</Month>
<Writer>${writer}</Writer>
<Penciller></Penciller>
<Inker></Inker>
<Colorist></Colorist>
<Letterer></Letterer>
<CoverArtist></CoverArtist>
<Editor></Editor>
<Publisher></Publisher>
<Imprint></Imprint>
<Genre></Genre>
<Web></Web>
<PageCount>${pageCount}</PageCount>
<LanguageISO>ko</LanguageISO>
<Format></Format>
<BlackAndWhite>Unknown</BlackAndWhite>
<Manga>Yes</Manga>
${pagesXml ? ` <Pages>\n${pagesXml} </Pages>\n` : ''}
</ComicInfo>`;
return comicInfo;
}
function getEpisodeImages(container) {
const root = container || document;
const imgs = root.querySelectorAll(
'.vw-imgs img, .vw-imgs--double img, .vw-imgs--single img, ' +
'.webtoon-page img, .manhwa-page img, ' +
'[class*="vw-imgs"] img'
);
const urls = [];
const seen = new Set();
imgs.forEach(img => {
const src = img.src || img.getAttribute('src') || '';
if (src && src.startsWith('http') && !seen.has(src)) {
seen.add(src);
urls.push(src);
const ogTitle = document.querySelector('meta[property="og:title"]');
if (ogTitle) {
const content = ogTitle.getAttribute('content') || '';
return content.split('|')[0].trim() || content;
}
});
return urls;
}
function getSeriesName() {
const metaEl = document.querySelector('div.vw-bot-meta strong');
if (metaEl) return metaEl.textContent.trim();
const titleEl = document.querySelector('h1');
if (titleEl && !titleEl.closest('.hero-v2')) {
return titleEl.textContent.trim();
return document.title.split('|')[0].trim() || '제목없음';
}
const ogTitle = document.querySelector('meta[property="og:title"]');
if (ogTitle) {
const content = ogTitle.getAttribute('content') || '';
return content.split('|')[0].trim() || content;
}
function getEpisodeTitle() {
const metaEl = document.querySelector('div.vw-bot-meta span');
if (metaEl) return metaEl.textContent.trim();
return document.title.split('|')[0].trim() || '제목없음';
}
function getEpisodeTitle() {
const metaEl = document.querySelector('div.vw-bot-meta span');
if (metaEl) return metaEl.textContent.trim();
const titleEl = document.querySelector('h1');
if (titleEl && !titleEl.closest('.hero-v2')) {
return titleEl.textContent.trim();
}
return document.title.split('|')[0].trim() || '제목없음';
}
function getListPageInfo() {
const currentURL = window.location.href;
const pathParts = window.location.pathname.split('/').filter(Boolean);
const isListPage = pathParts.length === 2;
const isEpisodePage = pathParts.length >= 3;
return { isListPage, isEpisodePage };
}
// lazy loading 트리거: data-src 복사 + loading=eager + 스크롤
const triggerLazyLoading = async () => {
const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"]') || document;
const imgs = viewer.querySelectorAll('img');
imgs.forEach(img => {
img.setAttribute('loading', 'eager');
['data-src', 'data-original', 'data-lazy-src', 'data-lazy'].forEach(attr => {
const val = img.getAttribute(attr);
if (val && !img.src) img.src = val;
});
});
// 동적 스크롤: 새 콘텐츠가 계속 로드되면 scrollHeight도 계속 증가
let prevHeight = 0;
let stuckCount = 0;
while (stuckCount < 3) {
const maxScroll = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight, 0
);
if (maxScroll === prevHeight) {
stuckCount++;
} else {
stuckCount = 0;
const titleEl = document.querySelector('h1');
if (titleEl && !titleEl.closest('.hero-v2')) {
return titleEl.textContent.trim();
}
prevHeight = maxScroll;
// 현재 위치에서 끝까지 step 단위로 스크롤 (느리게)
const step = Math.min(300, maxScroll);
for (let s = window.scrollY; s <= maxScroll; s += step) {
window.scrollTo(0, Math.min(s, maxScroll));
await sleep(300);
}
window.scrollTo(0, maxScroll);
await sleep(1500);
return document.title.split('|')[0].trim() || '제목없음';
}
// 끝까지 다 내려간 상태에서 여유 대기
await sleep(2000);
window.scrollTo(0, 0);
await sleep(1000);
};
// 공통: 이미지 URL 목록을 받아 ZIP 생성 + 다운로드
const downloadImagesAsZip = async (imageUrls, seriesName, episodeTitle, episodeNumber) => {
const zip = new JSZip();
const protocolDomain = window.location.origin;
function getListPageInfo() {
const currentURL = window.location.href;
const pathParts = window.location.pathname.split('/').filter(Boolean);
const isListPage = pathParts.length === 2;
const isEpisodePage = pathParts.length >= 3;
for (let i = 0; i < imageUrls.length; i++) {
const url = imageUrls[i];
await new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET', url, responseType: 'arraybuffer',
headers: {
'Referer': protocolDomain,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
onload: (resp) => {
if (resp.status === 200) {
const ext = url.split('.').pop().split('?')[0] || 'jpg';
zip.file(`image${String(i).padStart(4, '0')}.${ext}`, new Blob([resp.response], { type: 'image/' + ext }));
}
resolve();
},
onerror: () => resolve(),
ontimeout: () => resolve()
return { isListPage, isEpisodePage };
}
// 이미지의 data-* 속성을 src로 강제 복사
const forceLoadImages = (root) => {
const container = root || document;
container.querySelectorAll('img').forEach(img => {
img.setAttribute('loading', 'eager');
['data-src', 'data-original', 'data-lazy-src', 'data-lazy', 'data-srcset'].forEach(attr => {
const val = img.getAttribute(attr);
if (val && !img.src) img.src = val;
});
});
if ((i + 1) % 5 === 0) await sleep(1000);
}
};
const comicInfo = generateComicInfo(episodeTitle, seriesName, '', episodeNumber, imageUrls.length, imageUrls.length);
zip.file('ComicInfo.xml', comicInfo);
const blob = await zip.generateAsync({ type: "blob" });
const filename = `${seriesName} - ${episodeTitle}.zip`.replace(/[/\\:*?"<>|]/g, '_');
downloadZip(blob, filename);
log(`저장: ${filename} (${imageUrls.length}개)`);
};
// lazy loading 트리거: data-src 복사 + loading=eager + 스크롤
const triggerLazyLoading = async () => {
const viewer = document.querySelector('.vw-imgs, [class*="vw-imgs"]') || document;
// 우선 현재 img들 강제 로딩
forceLoadImages(viewer);
// 동적 스크롤: 새 콘텐츠가 계속 로드되면 scrollHeight도 계속 증가
let prevHeight = 0;
let stuckCount = 0;
while (stuckCount < 3) {
const maxScroll = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight, 0
);
if (maxScroll === prevHeight) {
stuckCount++;
} else {
stuckCount = 0;
}
prevHeight = maxScroll;
// 현재 위치에서 끝까지 step 단위로 스크롤 (느리게)
const step = Math.min(300, maxScroll);
for (let s = window.scrollY; s <= maxScroll; s += step) {
window.scrollTo(0, Math.min(s, maxScroll));
await sleep(300);
}
window.scrollTo(0, maxScroll);
// 스크롤 후 추가된 img들도 강제 로딩 (백그라운드 탭 IO 대체)
forceLoadImages(viewer);
await sleep(1500);
}
// 끝까지 다 내려간 상태에서 여유 대기 + 최종 강제 로딩
forceLoadImages(viewer);
await sleep(2000);
window.scrollTo(0, 0);
await sleep(1000);
};
// 공통: 현재 페이지에서 이미지 획득 + lazy 로딩 트리거
const getImagesWithLazyLoad = async () => {
let urls = getEpisodeImages();
if (urls.length === 0) {
for (let i = 0; i < 10; i++) {
await sleep(500);
// 공통: 이미지 URL 목록을 받아 ZIP 생성 + 다운로드
const downloadImagesAsZip = async (imageUrls, seriesName, episodeTitle, episodeNumber) => {
const zip = new JSZip();
const protocolDomain = window.location.origin;
for (let i = 0; i < imageUrls.length; i++) {
const url = imageUrls[i];
await new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET', url, responseType: 'arraybuffer',
headers: {
'Referer': protocolDomain,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
onload: (resp) => {
if (resp.status === 200) {
const ext = url.split('.').pop().split('?')[0] || 'jpg';
zip.file(`image${String(i).padStart(4, '0')}.${ext}`, new Blob([resp.response], { type: 'image/' + ext }));
}
resolve();
},
onerror: () => resolve(),
ontimeout: () => resolve()
});
});
if ((i + 1) % 5 === 0) await sleep(1000);
}
const comicInfo = generateComicInfo(episodeTitle, seriesName, '', episodeNumber, imageUrls.length, imageUrls.length);
zip.file('ComicInfo.xml', comicInfo);
const blob = await zip.generateAsync({ type: "blob" });
const filename = `${seriesName} - ${episodeTitle}.zip`.replace(/[/\\:*?"<>|]/g, '_');
downloadZip(blob, filename);
log(`저장: ${filename} (${imageUrls.length}개)`);
};
// 공통: 현재 페이지에서 이미지 획득 + lazy 로딩 트리거
const getImagesWithLazyLoad = async () => {
let urls = getEpisodeImages();
if (urls.length === 0) {
for (let i = 0; i < 10; i++) {
await sleep(500);
urls = getEpisodeImages();
if (urls.length > 0) break;
}
}
if (urls.length <= 3) {
log(`lazy loading 트리거 (${urls.length}개 → 전체)...`);
await triggerLazyLoading();
urls = getEpisodeImages();
if (urls.length > 0) break;
}
return urls;
};
async function downloadCurrentEpisode() {
try {
log('에피소드 다운로드 시작...');
const imageUrls = await getImagesWithLazyLoad();
if (imageUrls.length === 0) {
alert('이미지를 찾을 수 없습니다.');
return;
}
const seriesName = getSeriesName();
const episodeTitle = getEpisodeTitle();
log(`시리즈: ${seriesName}, 에피소드: ${episodeTitle}, 이미지: ${imageUrls.length}`);
await downloadImagesAsZip(imageUrls, seriesName, episodeTitle, '1');
log('완료');
} catch (error) {
alert('다운로드 중 오류 발생:\n' + error);
log(error);
}
}
if (urls.length <= 3) {
log(`lazy loading 트리거 (${urls.length}개 → 전체)...`);
await triggerLazyLoading();
urls = getEpisodeImages();
}
return urls;
};
async function downloadCurrentEpisode() {
try {
log('에피소드 다운로드 시작...');
const imageUrls = await getImagesWithLazyLoad();
async function downloadAllEpisodes() {
log('목록 페이지 처리 시작');
if (imageUrls.length === 0) {
alert('이미지를 찾을 수 없습니다.');
return;
}
const episodeList = document.querySelector('ul.ep-list-v2');
if (!episodeList) { alert('에피소드 목록을 찾을 수 없습니다.'); return; }
const episodes = [];
episodeList.querySelectorAll('li.ep-row-v2').forEach((li, index) => {
const link = li.querySelector('a.ep-row-v2-link');
if (link) {
const href = link.getAttribute('href');
const absoluteUrl = href.startsWith('http') ? href : new URL(href, window.location.href).href;
const epNoEl = li.querySelector('.ep-row-v2-no');
const epTitleEl = li.querySelector('.ep-row-v2-title strong');
episodes.push({
index,
number: epNoEl ? epNoEl.textContent.trim() : String(index + 1),
title: epTitleEl ? epTitleEl.textContent.trim() : String(index + 1),
url: absoluteUrl
});
}
});
if (episodes.length === 0) { alert('에피소드 링크를 추출할 수 없습니다.'); return; }
const seriesName = getSeriesName();
const episodeTitle = getEpisodeTitle();
log(`시리즈: ${seriesName}, 에피소드: ${episodeTitle}, 이미지: ${imageUrls.length}`);
log(`${episodes.length}개 에피소드를 큐에 저장, 첫 화로 이동합니다...`);
await downloadImagesAsZip(imageUrls, seriesName, episodeTitle, '1');
log('완료');
} catch (error) {
alert('다운로드 중 오류 발생:\n' + error);
log(error);
}
}
async function downloadAllEpisodes() {
log('목록 페이지 처리 시작');
const episodeList = document.querySelector('ul.ep-list-v2');
if (!episodeList) { alert('에피소드 목록을 찾을 수 없습니다.'); return; }
const episodes = [];
episodeList.querySelectorAll('li.ep-row-v2').forEach((li, index) => {
const link = li.querySelector('a.ep-row-v2-link');
if (link) {
const href = link.getAttribute('href');
const absoluteUrl = href.startsWith('http') ? href : new URL(href, window.location.href).href;
const epNoEl = li.querySelector('.ep-row-v2-no');
const epTitleEl = li.querySelector('.ep-row-v2-title strong');
episodes.push({
index,
number: epNoEl ? epNoEl.textContent.trim() : String(index + 1),
title: epTitleEl ? epTitleEl.textContent.trim() : String(index + 1),
url: absoluteUrl
});
}
});
if (episodes.length === 0) { alert('에피소드 링크를 추출할 수 없습니다.'); return; }
const seriesName = getSeriesName();
log(`${episodes.length}개 에피소드를 큐에 저장, 첫 화로 이동합니다...`);
// 큐 저장 후 첫 번째 에피소드로 이동
sessionStorage.setItem(QUEUE_KEY, JSON.stringify(episodes));
sessionStorage.setItem(SERIES_KEY, seriesName);
sessionStorage.setItem(IDX_KEY, '0');
sessionStorage.setItem(LIST_KEY, window.location.href);
window.location.href = episodes[0].url;
}
// 자동 큐 처리: sessionStorage에 큐가 있으면 현재 화면을 다운로드 후 다음으로 이동
const processAutoQueue = async () => {
const queue = getQueue();
if (!queue) return;
const { isEpisodePage } = getListPageInfo();
if (!isEpisodePage) return; // 목록 페이지에서는 큐 처리 안 함
const idx = getQueueIndex();
if (idx >= queue.length) { cleanupQueue(); return; }
log(`자동 다운로드: ${idx + 1}/${queue.length} - ${queue[idx].title}`);
await sleep(7000);
try {
const imageUrls = await getImagesWithLazyLoad();
if (imageUrls.length > 0) {
const seriesName = sessionStorage.getItem(SERIES_KEY) || getSeriesName();
const episodeTitle = getEpisodeTitle() || queue[idx].title;
await downloadImagesAsZip(imageUrls, seriesName, episodeTitle, queue[idx].number);
} else {
log(`이미지 없음 - 스킵`);
}
} catch (e) {
log('자동 다운로드 오류:', e);
// 큐 저장 후 첫 번째 에피소드로 이동
sessionStorage.setItem(QUEUE_KEY, JSON.stringify(episodes));
sessionStorage.setItem(SERIES_KEY, seriesName);
sessionStorage.setItem(IDX_KEY, '0');
sessionStorage.setItem(LIST_KEY, window.location.href);
window.location.href = episodes[0].url;
}
const next = idx + 1;
sessionStorage.setItem(IDX_KEY, String(next));
// 자동 큐 처리: sessionStorage에 큐가 있으면 현재 화면을 다운로드 후 다음으로 이동
const processAutoQueue = async () => {
const queue = getQueue();
if (!queue) return;
const { isEpisodePage } = getListPageInfo();
if (!isEpisodePage) return; // 목록 페이지에서는 큐 처리 안 함
if (next < queue.length) {
log(`${next + 1}/${queue.length}번째로 이동...`);
await sleep(2000);
window.location.href = queue[next].url;
} else {
const listUrl = sessionStorage.getItem(LIST_KEY);
cleanupQueue();
log('모든 에피소드 다운로드 완료!');
if (listUrl) {
log('목록 페이지로 복귀합니다...');
const idx = getQueueIndex();
if (idx >= queue.length) { cleanupQueue(); return; }
log(`자동 다운로드: ${idx + 1}/${queue.length} - ${queue[idx].title}`);
await sleep(7000);
try {
const imageUrls = await getImagesWithLazyLoad();
if (imageUrls.length > 0) {
const seriesName = sessionStorage.getItem(SERIES_KEY) || getSeriesName();
const episodeTitle = getEpisodeTitle() || queue[idx].title;
await downloadImagesAsZip(imageUrls, seriesName, episodeTitle, queue[idx].number);
} else {
log(`이미지 없음 - 스킵`);
}
} catch (e) {
log('자동 다운로드 오류:', e);
}
const next = idx + 1;
sessionStorage.setItem(IDX_KEY, String(next));
if (next < queue.length) {
log(`${next + 1}/${queue.length}번째로 이동...`);
await sleep(2000);
window.location.href = listUrl;
window.location.href = queue[next].url;
} else {
const listUrl = sessionStorage.getItem(LIST_KEY);
cleanupQueue();
log('모든 에피소드 다운로드 완료!');
if (listUrl) {
log('목록 페이지로 복귀합니다...');
await sleep(2000);
window.location.href = listUrl;
}
}
}
};
registerMenu();
// 자동 다운로드 큐 확인 (목록→에피소드 이동 시)
setTimeout(processAutoQueue, 1000);
// Next.js SPA 네비게이션 감지: URL 변경 시 메뉴 재등록
const origPushState = history.pushState;
const origReplaceState = history.replaceState;
history.pushState = function () {
origPushState.apply(this, arguments);
setTimeout(registerMenu, 500);
};
history.replaceState = function () {
origReplaceState.apply(this, arguments);
setTimeout(registerMenu, 500);
};
window.addEventListener('popstate', () => setTimeout(registerMenu, 500));
log('sbxh6Downloader loaded');
};
registerMenu();
// 자동 다운로드 큐 확인 (목록→에피소드 이동 시)
setTimeout(processAutoQueue, 1000);
// Next.js SPA 네비게이션 감지: URL 변경 시 메뉴 재등록
const origPushState = history.pushState;
const origReplaceState = history.replaceState;
history.pushState = function () {
origPushState.apply(this, arguments);
setTimeout(registerMenu, 500);
};
history.replaceState = function () {
origReplaceState.apply(this, arguments);
setTimeout(registerMenu, 500);
};
window.addEventListener('popstate', () => setTimeout(registerMenu, 500));
log('sbxh6Downloader loaded');
// DOM 로딩 완료 후 main() 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', main);
} else {
main();
}
})();