...
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// 페이지 로딩 완료 후 실행
|
||||
window.addEventListener('load', async () => {
|
||||
// 1. URL 패턴 확인
|
||||
const match = window.location.pathname.match(/\/novel\/(\d+)\/(\d+)/);
|
||||
if (!match) return;
|
||||
|
||||
const bookId = match[1];
|
||||
const chapterId = match[2];
|
||||
console.log(`웹소설 스크래퍼: 책 ID [${bookId}] 감지됨.`);
|
||||
|
||||
// ⏳ 2. 1~2초 동안 '페이지 다운(80%)' 스크롤 실행
|
||||
const initialScrollTime = Math.floor(Math.random() * (10000 - 1000 + 1)) + 8000;
|
||||
console.log(`${(initialScrollTime / 1000).toFixed(1)}초 동안 큼직하게 훑어보며 대기합니다... 👀`);
|
||||
await pageSkimScroll(initialScrollTime);
|
||||
|
||||
// 3. 데이터 스크래핑
|
||||
const scrapedData = scrapePage();
|
||||
if (!scrapedData) return;
|
||||
|
||||
// 4. 책 ID별 Storage 저장
|
||||
chrome.storage.local.get(['scrapedBooks'], async (result) => {
|
||||
let scrapedBooks = result.scrapedBooks || {};
|
||||
|
||||
if (!scrapedBooks[bookId]) {
|
||||
scrapedBooks[bookId] = {
|
||||
id: 'book_' + bookId,
|
||||
fileName: scrapedData.novelTitle,
|
||||
chapters: []
|
||||
};
|
||||
}
|
||||
|
||||
let bookData = scrapedBooks[bookId];
|
||||
|
||||
if (scrapedData.novelTitle !== '제목없음') {
|
||||
bookData.fileName = scrapedData.novelTitle;
|
||||
}
|
||||
|
||||
// 중복 챕터 검사
|
||||
const isDuplicate = bookData.chapters.some(chap => chap.chapterId === chapterId);
|
||||
if (isDuplicate) {
|
||||
console.log(`⚠️ 이미 저장된 챕터입니다 (ID: ${chapterId}). 자동 진행을 중지합니다.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 챕터 추가
|
||||
bookData.chapters.push({
|
||||
chapterId: chapterId,
|
||||
title: scrapedData.chapterTitle,
|
||||
content: scrapedData.content
|
||||
});
|
||||
|
||||
// Storage에 저장
|
||||
scrapedBooks[bookId] = bookData;
|
||||
chrome.storage.local.set({
|
||||
scrapedBooks: scrapedBooks,
|
||||
currentBookId: bookId
|
||||
}, async () => {
|
||||
console.log(`✅ [${scrapedData.novelTitle} - ${scrapedData.chapterTitle}] 저장 완료!`);
|
||||
|
||||
// ⏳ 5. 저장 완료 후 2~3초 랜덤 대기
|
||||
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('다음화'));
|
||||
|
||||
if (nextButton) {
|
||||
console.log("👉 다음 화 버튼 클릭 진행");
|
||||
nextButton.click();
|
||||
} else {
|
||||
console.log("⚠️ '다음화' 버튼을 찾을 수 없거나 마지막 화입니다.");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ==========================================
|
||||
// 🚀 화면 높이의 약 80%씩 큼직하게 스크롤하는 함수
|
||||
// ==========================================
|
||||
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');
|
||||
let novelTitle = titleEl ? titleEl.innerText.trim() : '제목없음';
|
||||
|
||||
let chapterTitle = document.querySelector('.theme-novel-title');
|
||||
chapterTitle = chapterTitle ? chapterTitle.innerText.trim() : '1화';
|
||||
|
||||
let novelContent = '';
|
||||
const contentHost = document.querySelector('.theme-novel-content');
|
||||
|
||||
if (contentHost) {
|
||||
if (contentHost.shadowRoot) {
|
||||
const pTags = contentHost.shadowRoot.querySelectorAll('p');
|
||||
const textArray = Array.from(pTags).map(p => p.innerText.trim());
|
||||
novelContent = textArray.join('\n\n');
|
||||
} else {
|
||||
const template = contentHost.querySelector('template[shadowrootmode="open"]');
|
||||
if (template && template.content) {
|
||||
const pTags = template.content.querySelectorAll('p');
|
||||
const textArray = Array.from(pTags).map(p => p.textContent.trim());
|
||||
novelContent = textArray.join('\n\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!novelContent) {
|
||||
console.warn('본문을 추출할 수 없습니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
novelTitle: novelTitle,
|
||||
chapterTitle: chapterTitle,
|
||||
content: novelContent
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "웹소설 스크래퍼 (나만의 서재용)",
|
||||
"version": "1.2",
|
||||
"description": "웹소설 챕터를 수집하여 서재 리더기용 JSON으로 다운로드합니다.",
|
||||
"action": {
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"scripting",
|
||||
"storage",
|
||||
"downloads",
|
||||
"unlimitedStorage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://api.telegram.org/*"
|
||||
],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"*://newtoki1.org/novel/*",
|
||||
"*://*.newtoki1.org/novel/*"
|
||||
],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body { width: 280px; padding: 15px; font-family: 'Malgun Gothic', sans-serif; background: #f9f9f9; }
|
||||
h3 { margin-top: 0; text-align: center; color: #333; }
|
||||
button { width: 100%; padding: 10px; margin-bottom: 8px; cursor: pointer; border: none; border-radius: 6px; font-weight: bold; font-size: 13px; transition: 0.2s; }
|
||||
button:hover { opacity: 0.9; }
|
||||
#btn-save { background: #3498db; color: white; }
|
||||
#btn-download { background: #27ae60; color: white; }
|
||||
#btn-telegram { background: #0088cc; color: white; } /* 텔레그램 시그니처 색상 */
|
||||
#btn-clear { background: #e74c3c; color: white; }
|
||||
|
||||
.tg-config { background: #fff; padding: 10px; border-radius: 6px; margin-bottom: 10px; border: 1px solid #ddd; }
|
||||
.tg-config input { width: 92%; padding: 6px; margin-top: 4px; margin-bottom: 8px; font-size: 11px; border: 1px solid #ccc; border-radius: 4px; }
|
||||
.tg-config label { font-size: 11px; font-weight: bold; color: #555; }
|
||||
|
||||
#status { font-size: 12px; color: #666; text-align: center; margin-top: 5px; padding: 10px; background: #eee; border-radius: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h3>📖 소설 스크래퍼</h3>
|
||||
|
||||
<div class="tg-config">
|
||||
<label>Bot Token:</label>
|
||||
<input type="password" id="tg-token" placeholder="BotFather에게 받은 토큰">
|
||||
<label>Chat ID:</label>
|
||||
<input type="text" id="tg-chatid" placeholder="숫자로 된 Chat ID">
|
||||
<button id="btn-save-config" style="background:#666; color:#fff; padding:5px; font-size:11px; margin-bottom:0;">⚙️ 텔레그램 설정 저장</button>
|
||||
</div>
|
||||
|
||||
<button id="btn-save">➕ 현재 화면(챕터) 저장하기</button>
|
||||
<button id="btn-download">📥 리더기용 JSON 다운로드</button>
|
||||
<button id="btn-telegram">🚀 텔레그램으로 JSON 전송</button>
|
||||
<button id="btn-clear">🗑️ 현재 소설 데이터 초기화</button>
|
||||
|
||||
<div id="status">대기 중...</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,267 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateStatus();
|
||||
loadTelegramConfig();
|
||||
|
||||
// 1. [설정 저장] 버튼
|
||||
document.getElementById('btn-save-config').addEventListener('click', saveTelegramConfig);
|
||||
|
||||
// 2. [저장하기] 버튼
|
||||
// 2. [저장하기] 버튼
|
||||
document.getElementById('btn-save').addEventListener('click', async () => {
|
||||
let [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
chrome.scripting.executeScript({
|
||||
target: { tabId: tab.id },
|
||||
function: scrapePage
|
||||
}, (results) => {
|
||||
if (results && results[0] && results[0].result) {
|
||||
const data = results[0].result;
|
||||
const match = tab.url ? tab.url.match(/\/novel\/(\d+)\/(\d+)/) : null;
|
||||
const bookId = match ? match[1] : 'manual_' + Date.now();
|
||||
const chapterId = match ? match[2] : 'manual_chap_' + Date.now(); // 챕터 ID 추가
|
||||
|
||||
// chapterId를 같이 넘겨줍니다.
|
||||
saveToExtensionStorage(data, bookId, chapterId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. [다운로드] 버튼
|
||||
document.getElementById('btn-download').addEventListener('click', downloadJson);
|
||||
|
||||
// 4. [텔레그램 전송] 버튼
|
||||
document.getElementById('btn-telegram').addEventListener('click', sendToTelegram);
|
||||
|
||||
// 5. [초기화] 버튼
|
||||
document.getElementById('btn-clear').addEventListener('click', clearStorage);
|
||||
});
|
||||
|
||||
// 텔레그램 설정 불러오기
|
||||
function loadTelegramConfig() {
|
||||
chrome.storage.local.get(['tgToken', 'tgChatId'], (result) => {
|
||||
if (result.tgToken) document.getElementById('tg-token').value = result.tgToken;
|
||||
if (result.tgChatId) document.getElementById('tg-chatid').value = result.tgChatId;
|
||||
});
|
||||
}
|
||||
|
||||
// 텔레그램 설정 저장
|
||||
function saveTelegramConfig() {
|
||||
const token = document.getElementById('tg-token').value.trim();
|
||||
const chatId = document.getElementById('tg-chatid').value.trim();
|
||||
|
||||
chrome.storage.local.set({ tgToken: token, tgChatId: chatId }, () => {
|
||||
alert("✅ 텔레그램 설정이 저장 되었습니다!");
|
||||
});
|
||||
}
|
||||
|
||||
// 🚀 텔레그램 API로 JSON 파일 전송 함수
|
||||
function sendToTelegram() {
|
||||
chrome.storage.local.get(['scrapedBooks', 'currentBookId', 'tgToken', 'tgChatId'], async (result) => {
|
||||
const token = result.tgToken;
|
||||
const chatId = result.tgChatId;
|
||||
|
||||
if (!token || !chatId) {
|
||||
alert("⚠️ 먼저 텔레그램 Bot Token과 Chat ID를 설정하고 저장해주세요!");
|
||||
return;
|
||||
}
|
||||
|
||||
const books = result.scrapedBooks || {};
|
||||
const currentBookId = result.currentBookId;
|
||||
const bookData = currentBookId ? books[currentBookId] : null;
|
||||
|
||||
if (!bookData || bookData.chapters.length === 0) {
|
||||
alert("전송할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 리더기 포맷 페이로드 생성
|
||||
const payload = {
|
||||
type: 'single_book',
|
||||
book: bookData,
|
||||
progress: { chapter: 0, page: 0, tags: [], memo: "스크래퍼 확장프로그램으로 수집됨", isFavorite: false, isCompleted: false }
|
||||
};
|
||||
|
||||
const jsonString = JSON.stringify(payload, null, 2);
|
||||
const safeFileName = bookData.fileName.replace(/[\\/:*?"<>|]/g, '_');
|
||||
|
||||
// JSON 문자열을 Blob 파일 객체로 변환
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
|
||||
// Multipart FormData 생성 (텔레그램 API 전송용)
|
||||
const formData = new FormData();
|
||||
formData.append('chat_id', chatId);
|
||||
formData.append('document', blob, `book_${safeFileName}.json`);
|
||||
formData.append('caption', `📖 [${bookData.fileName}]\n총 ${bookData.chapters.length}화 수집 완료 JSON 파일입니다.`);
|
||||
|
||||
// 버튼 상태 변경
|
||||
const btnTg = document.getElementById('btn-telegram');
|
||||
btnTg.innerText = "⏳ 전송 중...";
|
||||
btnTg.disabled = true;
|
||||
|
||||
try {
|
||||
// Telegram sendDocument API 호출
|
||||
const response = await fetch(`https://api.telegram.org/bot${token}/sendDocument`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const resData = await response.json();
|
||||
|
||||
if (resData.ok) {
|
||||
alert(`🚀 [${bookData.fileName}] 텔레그램으로 전송 성공!`);
|
||||
} else {
|
||||
alert(`❌ 전송 실패: ${resData.description}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
alert("❌ 네트워크 오류가 발생했습니다.");
|
||||
} finally {
|
||||
btnTg.innerText = "🚀 텔레그램으로 JSON 전송";
|
||||
btnTg.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 스크래핑 함수 (수동 저장용)
|
||||
function scrapePage() {
|
||||
const titleEl = document.querySelector('.page-title h2');
|
||||
let novelTitle = titleEl ? titleEl.innerText.trim() : '제목없음';
|
||||
|
||||
let chapterTitle = document.querySelector('.theme-novel-title');
|
||||
chapterTitle = chapterTitle ? chapterTitle.innerText.trim() : '1화';
|
||||
|
||||
let novelContent = '';
|
||||
const contentHost = document.querySelector('.theme-novel-content');
|
||||
|
||||
if (contentHost) {
|
||||
if (contentHost.shadowRoot) {
|
||||
const pTags = contentHost.shadowRoot.querySelectorAll('p');
|
||||
const textArray = Array.from(pTags).map(p => p.innerText.trim());
|
||||
novelContent = textArray.join('\n\n');
|
||||
} else {
|
||||
const template = contentHost.querySelector('template[shadowrootmode="open"]');
|
||||
if (template && template.content) {
|
||||
const pTags = template.content.querySelectorAll('p');
|
||||
const textArray = Array.from(pTags).map(p => p.textContent.trim());
|
||||
novelContent = textArray.join('\n\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!novelContent) {
|
||||
alert('본문을 추출할 수 없습니다.');
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
novelTitle: novelTitle,
|
||||
chapterTitle: chapterTitle,
|
||||
content: novelContent
|
||||
};
|
||||
}
|
||||
|
||||
// 수동 저장 함수
|
||||
function saveToExtensionStorage(scrapedData, bookId) {
|
||||
chrome.storage.local.get(['scrapedBooks'], (result) => {
|
||||
let scrapedBooks = result.scrapedBooks || {};
|
||||
|
||||
if (!scrapedBooks[bookId]) {
|
||||
scrapedBooks[bookId] = {
|
||||
id: 'book_' + bookId,
|
||||
fileName: scrapedData.novelTitle,
|
||||
chapters: []
|
||||
};
|
||||
}
|
||||
|
||||
let bookData = scrapedBooks[bookId];
|
||||
|
||||
// 챕터 ID 기준으로 중복 검사
|
||||
const isDuplicate = bookData.chapters.some(chap => chap.chapterId === chapterId);
|
||||
if (isDuplicate) {
|
||||
alert('⚠️ 이미 저장된 챕터입니다!');
|
||||
return;
|
||||
}
|
||||
|
||||
bookData.chapters.push({
|
||||
chapterId: chapterId,
|
||||
title: scrapedData.chapterTitle,
|
||||
content: scrapedData.content
|
||||
});
|
||||
scrapedBooks[bookId] = bookData;
|
||||
|
||||
chrome.storage.local.set({ scrapedBooks: scrapedBooks, currentBookId: bookId }, () => {
|
||||
updateStatus();
|
||||
alert(`✅ [${scrapedData.chapterTitle}] 저장 완료!`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 다운로드 함수
|
||||
function downloadJson() {
|
||||
chrome.storage.local.get(['scrapedBooks', 'currentBookId'], (result) => {
|
||||
const books = result.scrapedBooks || {};
|
||||
const currentBookId = result.currentBookId;
|
||||
const bookData = currentBookId ? books[currentBookId] : null;
|
||||
|
||||
if (!bookData || bookData.chapters.length === 0) {
|
||||
alert("다운로드할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type: 'single_book',
|
||||
book: bookData,
|
||||
progress: { chapter: 0, page: 0, tags: [], memo: "스크래퍼 확장프로그램으로 수집됨", isFavorite: false, isCompleted: false }
|
||||
};
|
||||
|
||||
const jsonString = JSON.stringify(payload, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const safeFileName = bookData.fileName.replace(/[\\/:*?"<>|]/g, '_');
|
||||
|
||||
chrome.downloads.download({
|
||||
url: url,
|
||||
filename: `book_${safeFileName}.json`,
|
||||
saveAs: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 초기화 함수
|
||||
function clearStorage() {
|
||||
chrome.storage.local.get(['scrapedBooks', 'currentBookId'], (result) => {
|
||||
const books = result.scrapedBooks || {};
|
||||
const currentBookId = result.currentBookId;
|
||||
|
||||
if (!currentBookId || !books[currentBookId]) {
|
||||
alert("삭제할 수집 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const bookTitle = books[currentBookId].fileName;
|
||||
if(confirm(`[${bookTitle}] 수집 데이터를 삭제하시겠습니까?`)) {
|
||||
delete books[currentBookId];
|
||||
chrome.storage.local.set({ scrapedBooks: books, currentBookId: null }, () => {
|
||||
updateStatus();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 상태창 업데이트
|
||||
function updateStatus() {
|
||||
chrome.storage.local.get(['scrapedBooks', 'currentBookId'], (result) => {
|
||||
const books = result.scrapedBooks || {};
|
||||
const currentBookId = result.currentBookId;
|
||||
const book = currentBookId ? books[currentBookId] : null;
|
||||
|
||||
const count = book ? book.chapters.length : 0;
|
||||
const title = book ? book.fileName : '없음';
|
||||
|
||||
document.getElementById('status').innerHTML = `
|
||||
<strong>현재 선택 소설:</strong> ${title}<br>
|
||||
<strong style="color:#27ae60; font-size:14px;">모인 챕터: ${count}화</strong>
|
||||
`;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// ==========================================
|
||||
// 1. 챕터를 로컬 스토리지에 누적 저장하는 함수
|
||||
// ==========================================
|
||||
function saveChapterToLocal(bookTitle, chapterTitle, chapterContent) {
|
||||
const STORAGE_KEY = 'webReader_scraper_temp';
|
||||
let bookData = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||||
|
||||
// 기존 데이터가 없거나, 다른 책을 스크랩하기 시작한 경우 초기화
|
||||
if (!bookData || bookData.fileName !== bookTitle) {
|
||||
bookData = {
|
||||
id: 'book_scraped_' + Date.now(), // 고유 ID 생성
|
||||
fileName: bookTitle,
|
||||
chapters: []
|
||||
};
|
||||
console.log(`새로운 책 [${bookTitle}] 스크랩을 시작합니다.`);
|
||||
}
|
||||
|
||||
// 중복 저장 방지 로직 (동일한 챕터 제목이 있는지 검사)
|
||||
const isDuplicate = bookData.chapters.some(chap => chap.title === chapterTitle);
|
||||
if (isDuplicate) {
|
||||
console.warn(`⚠️ 이미 저장된 챕터입니다: ${chapterTitle}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 챕터 데이터 추가
|
||||
bookData.chapters.push({
|
||||
title: chapterTitle,
|
||||
content: chapterContent
|
||||
});
|
||||
|
||||
// 변경된 데이터를 다시 스토리지에 저장
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(bookData));
|
||||
console.log(`✅ [${bookTitle}] ${chapterTitle} 저장 완료! (현재 총 ${bookData.chapters.length}화)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 2. 누적된 데이터를 리더기 호환 JSON으로 다운로드하는 함수
|
||||
// ==========================================
|
||||
function downloadBookAsJson() {
|
||||
const STORAGE_KEY = 'webReader_scraper_temp';
|
||||
const bookData = JSON.parse(localStorage.getItem(STORAGE_KEY));
|
||||
|
||||
if (!bookData || !bookData.chapters || bookData.chapters.length === 0) {
|
||||
alert("다운로드할 챕터 데이터가 없습니다. 먼저 챕터를 저장해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 리더기의 '개별 책 복원' 형식과 완벽히 일치하도록 페이로드 구성
|
||||
const payload = {
|
||||
type: 'single_book',
|
||||
book: bookData,
|
||||
progress: {
|
||||
chapter: 0,
|
||||
page: 0,
|
||||
tags: [], // 리더기에서 불러올 때 재분석 기능으로 태그를 달 수 있습니다.
|
||||
memo: "스크랩을 통해 추가된 소설입니다.",
|
||||
isFavorite: false,
|
||||
isCompleted: false
|
||||
}
|
||||
};
|
||||
|
||||
// JSON 파일로 변환 및 다운로드 처리
|
||||
const jsonString = JSON.stringify(payload, null, 2);
|
||||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
// 파일명에 사용할 수 없는 특수문자 제거
|
||||
const safeFileName = bookData.fileName.replace(/[\\/:*?"<>|]/g, '_');
|
||||
a.download = `book_${safeFileName}.json`;
|
||||
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// 다운로드 후 임시 데이터 정리
|
||||
if (confirm("파일 다운로드가 완료되었습니다.\n다음 스크랩을 위해 임시 저장된 데이터를 초기화할까요?")) {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
console.log("임시 데이터가 초기화되었습니다.");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+491
-566
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user