사주 풀이 고도화, NAS 배포 자동화
This commit is contained in:
@@ -1,10 +1,78 @@
|
||||
import jsPDF from 'jspdf';
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
/**
|
||||
* html2canvas가 지원하지 않는 CSS 색상 함수(lab, oklch, oklab)를
|
||||
* 클론된 문서에서 안전한 값으로 치환한다.
|
||||
*
|
||||
* 접근 방식:
|
||||
* 1) <style> 태그의 텍스트에서 직접 regex 치환 (가장 확실)
|
||||
* 2) CSSStyleSheet.cssRules에서 문제 속성 제거 (외부 스타일시트 대응)
|
||||
* 3) 개별 요소의 인라인 style 정리
|
||||
*/
|
||||
function sanitizeUnsupportedColors(clonedDoc: Document) {
|
||||
const unsafeRe = /(?:oklch|oklab|lab)\([^)]*\)/gi;
|
||||
|
||||
// 1단계: 모든 <style> 태그 텍스트에서 lab()/oklch()/oklab() → 안전한 색상으로 치환
|
||||
const styleTags = clonedDoc.querySelectorAll('style');
|
||||
styleTags.forEach((tag) => {
|
||||
const text = tag.textContent;
|
||||
if (text && unsafeRe.test(text)) {
|
||||
tag.textContent = text.replace(unsafeRe, 'rgba(128,128,128,1)');
|
||||
}
|
||||
// reset lastIndex because we reuse the regex
|
||||
unsafeRe.lastIndex = 0;
|
||||
});
|
||||
|
||||
// 2단계: <link> 등 외부 스타일시트의 cssRules에서 문제 속성 제거
|
||||
try {
|
||||
for (const sheet of Array.from(clonedDoc.styleSheets)) {
|
||||
try {
|
||||
// <style> 태그는 이미 1단계에서 처리했으므로 skip
|
||||
if (sheet.ownerNode && (sheet.ownerNode as HTMLElement).tagName === 'STYLE') continue;
|
||||
|
||||
const rules = sheet.cssRules;
|
||||
if (!rules) continue;
|
||||
for (let i = rules.length - 1; i >= 0; i--) {
|
||||
const rule = rules[i];
|
||||
if (rule instanceof CSSStyleRule) {
|
||||
for (let j = rule.style.length - 1; j >= 0; j--) {
|
||||
const prop = rule.style.item(j);
|
||||
const val = rule.style.getPropertyValue(prop);
|
||||
unsafeRe.lastIndex = 0;
|
||||
if (unsafeRe.test(val)) {
|
||||
rule.style.removeProperty(prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 크로스 오리진 스타일시트는 접근 불가 - 무시
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// styleSheets 접근 실패 시 무시
|
||||
}
|
||||
|
||||
// 3단계: 인라인 style 속성에 남아있는 lab() 정리
|
||||
const allElements = clonedDoc.querySelectorAll('*');
|
||||
allElements.forEach((el) => {
|
||||
const htmlEl = el as HTMLElement;
|
||||
const inlineStyle = htmlEl.getAttribute('style');
|
||||
if (inlineStyle) {
|
||||
unsafeRe.lastIndex = 0;
|
||||
if (unsafeRe.test(inlineStyle)) {
|
||||
htmlEl.setAttribute(
|
||||
'style',
|
||||
inlineStyle.replace(unsafeRe, 'rgba(128,128,128,1)')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML 요소를 PDF로 변환하여 다운로드
|
||||
* @param elementId - PDF로 변환할 HTML 요소의 ID
|
||||
* @param filename - 다운로드될 PDF 파일명
|
||||
*/
|
||||
export async function downloadPDF(elementId: string, filename: string) {
|
||||
try {
|
||||
@@ -13,38 +81,33 @@ export async function downloadPDF(elementId: string, filename: string) {
|
||||
throw new Error(`Element with id "${elementId}" not found`);
|
||||
}
|
||||
|
||||
// 로딩 표시
|
||||
const originalContent = element.innerHTML;
|
||||
|
||||
// HTML을 캔버스로 변환
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2, // 해상도 향상
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
logging: false,
|
||||
backgroundColor: '#ffffff'
|
||||
backgroundColor: '#ffffff',
|
||||
onclone: (_doc: Document, clonedEl: HTMLElement) => {
|
||||
sanitizeUnsupportedColors(clonedEl.ownerDocument);
|
||||
}
|
||||
});
|
||||
|
||||
// 캔버스를 이미지로 변환
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
|
||||
// PDF 생성
|
||||
const pdf = new jsPDF({
|
||||
orientation: 'portrait',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
const imgWidth = 210; // A4 width in mm
|
||||
const pageHeight = 297; // A4 height in mm
|
||||
const imgWidth = 210;
|
||||
const pageHeight = 297;
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
let heightLeft = imgHeight;
|
||||
let position = 0;
|
||||
|
||||
// 첫 페이지 추가
|
||||
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
|
||||
heightLeft -= pageHeight;
|
||||
|
||||
// 여러 페이지가 필요한 경우
|
||||
while (heightLeft > 0) {
|
||||
position = heightLeft - imgHeight;
|
||||
pdf.addPage();
|
||||
@@ -52,9 +115,7 @@ export async function downloadPDF(elementId: string, filename: string) {
|
||||
heightLeft -= pageHeight;
|
||||
}
|
||||
|
||||
// PDF 다운로드
|
||||
pdf.save(filename);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('PDF 생성 중 오류 발생:', error);
|
||||
@@ -63,10 +124,6 @@ export async function downloadPDF(elementId: string, filename: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 페이지를 PDF로 다운로드
|
||||
* @param filename - 다운로드될 PDF 파일명
|
||||
*/
|
||||
export async function downloadCurrentPageAsPDF(filename: string) {
|
||||
return downloadPDF('pdf-content', filename);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user