diff --git a/package-lock.json b/package-lock.json index 39a0822..619656f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@types/react-dom": "18.2.6", "@uiw/react-markdown-preview": "^4.1.15", "async-mutex": "^0.4.0", + "comlink": "^4.4.1", "dayjs": "^1.11.9", "easymde": "^2.18.0", "encoding": "^0.1.13", @@ -4492,6 +4493,11 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "dev": true }, + "node_modules/comlink": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.1.tgz", + "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==" + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", diff --git a/package.json b/package.json index a843321..0ab9dc2 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/react-dom": "18.2.6", "@uiw/react-markdown-preview": "^4.1.15", "async-mutex": "^0.4.0", + "comlink": "^4.4.1", "dayjs": "^1.11.9", "easymde": "^2.18.0", "encoding": "^0.1.13", diff --git a/src/components/classroom/pdf/MaterialExportModal.tsx b/src/components/classroom/pdf/MaterialExportModal.tsx index 9886f2d..f57fdda 100644 --- a/src/components/classroom/pdf/MaterialExportModal.tsx +++ b/src/components/classroom/pdf/MaterialExportModal.tsx @@ -2,64 +2,11 @@ import { ModalIDs } from '@/src/constants/modal/modal'; import Modal from '../../ui/Modal'; import { closeModalHandler } from '@/src/util/modal/modalHandler'; import MaterialPDFRenderer from './MaterialPDFRenderer'; -import { Font } from '@react-pdf/renderer'; type Props = { courseId: number | null; courseTitle: string | null; }; -Font.register({ - family: 'MonoplexKR', - fonts: [ - { - src: '/fonts/MonoplexKR-Regular.ttf', - fontWeight: 'normal', - fontStyle: 'normal' - }, - { - src: '/fonts/MonoplexKR-Italic.ttf', - fontWeight: 'normal', - fontStyle: 'italic' - }, - { - src: '/fonts/MonoplexKR-Bold.ttf', - fontWeight: 'bold', - fontStyle: 'normal' - }, - { - src: '/fonts/MonoplexKR-BoldItalic.ttf', - fontWeight: 'bold', - fontStyle: 'italic' - } - ] -}); - -Font.register({ - family: 'Pretendard', - fonts: [ - { - fontWeight: 'normal', - src: '/fonts/Pretendard-Regular.woff' - }, - { - fontWeight: 'medium', - src: '/fonts/Pretendard-Medium.woff' - }, - { - fontWeight: 'semibold', - src: '/fonts/Pretendard-SemiBold.woff' - }, - { - fontWeight: 'bold', - src: '/fonts/Pretendard-Bold.woff' - } - ] -}); - -Font.registerEmojiSource({ - format: 'png', - url: 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/' -}); export default function MaterialExportModal({ courseId, courseTitle }: Props) { return ( diff --git a/src/components/classroom/pdf/MaterialPDFDocument.tsx b/src/components/classroom/pdf/MaterialPDFDocument.tsx index 56f0299..f4ec010 100644 --- a/src/components/classroom/pdf/MaterialPDFDocument.tsx +++ b/src/components/classroom/pdf/MaterialPDFDocument.tsx @@ -1,11 +1,11 @@ -'use client'; import { Document, Page, Text, StyleSheet, Image, - View + View, + Font } from '@react-pdf/renderer'; import { Token, marked } from 'marked'; @@ -13,6 +13,59 @@ type Props = { materials: CourseMaterialWorkbook }; const TimestampRegex = /]*class="timestamp"[^>]*>.*?<\/button>/g; +Font.register({ + family: 'MonoplexKR', + fonts: [ + { + src: '/fonts/MonoplexKR-Regular.ttf', + fontWeight: 'normal', + fontStyle: 'normal' + }, + { + src: '/fonts/MonoplexKR-Italic.ttf', + fontWeight: 'normal', + fontStyle: 'italic' + }, + { + src: '/fonts/MonoplexKR-Bold.ttf', + fontWeight: 'bold', + fontStyle: 'normal' + }, + { + src: '/fonts/MonoplexKR-BoldItalic.ttf', + fontWeight: 'bold', + fontStyle: 'italic' + } + ] +}); + +Font.register({ + family: 'Pretendard', + fonts: [ + { + fontWeight: 'normal', + src: '/fonts/Pretendard-Regular.woff' + }, + { + fontWeight: 'medium', + src: '/fonts/Pretendard-Medium.woff' + }, + { + fontWeight: 'semibold', + src: '/fonts/Pretendard-SemiBold.woff' + }, + { + fontWeight: 'bold', + src: '/fonts/Pretendard-Bold.woff' + } + ] +}); + +Font.registerEmojiSource({ + format: 'png', + url: 'https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/72x72/' +}); + const styles = StyleSheet.create({ body: { paddingTop: 35, diff --git a/src/components/classroom/pdf/MaterialPDFRenderer.tsx b/src/components/classroom/pdf/MaterialPDFRenderer.tsx index eaa25ac..cb83155 100644 --- a/src/components/classroom/pdf/MaterialPDFRenderer.tsx +++ b/src/components/classroom/pdf/MaterialPDFRenderer.tsx @@ -1,7 +1,4 @@ 'use client'; -import { PDFDownloadLink, PDFViewer } from '@react-pdf/renderer'; -import MaterialPDFDocument from './MaterialPDFDocument'; -import LoadingSpinner from '../../ui/LoadingSpinner'; import DownloadSVG from '@/public/icon/Download'; import Button from '../../ui/button/Button'; import { useQuery } from '@tanstack/react-query'; @@ -9,6 +6,9 @@ import { QueryKeys } from '@/src/api/queryKeys'; import { fetchCourseMaterialWorkbook } from '@/src/api/courses/courses'; import { ONE_SECOND_IN_MS } from '@/src/constants/time/time'; import CourseMaterialLoading from '../../course/drawer/courseMaterial/CourseMaterialLoading'; +import { useEffect, useState } from 'react'; +import { wrap } from 'comlink'; +import { GeneratePDF } from '@/src/util/pdf/pdfWorker'; type Props = { courseId: string; @@ -23,50 +23,60 @@ const STATUS = { const REFETCH_INTERVAL = 10 * ONE_SECOND_IN_MS; export default function MaterialPDFRenderer({ courseId, courseTitle }: Props) { + const [url, setUrl] = useState(); + const [loading, setLoading] = useState(false); + const { data, status } = useQuery( [QueryKeys.MATERIAL_EXPORT, courseId], () => fetchCourseMaterialWorkbook(parseInt(courseId)), { refetchInterval(data) { return data?.status === STATUS.PENDING ? REFETCH_INTERVAL : false; - }, - refetchIntervalInBackground: true + } } ); + + useEffect(() => { + if (data && data.status === STATUS.SUCCESS) { + const worker = new Worker( + new URL('@/src/util/pdf/pdfWorker.ts', import.meta.url) + ); + const generatePDF = wrap(worker); + + setLoading(true); + generatePDF(data as CourseMaterialWorkbook) + .then(setUrl) + .finally(() => setLoading(false)); + + return () => { + if (url) { + URL.revokeObjectURL(url); + worker.terminate(); + } + }; + } + }, [data]); + return (
- {status === 'loading' ? ( - + {status === 'loading' || loading ? ( + ) : ( <> - {data && data.status === STATUS.SUCCESS ? ( + {url ? ( <> - } - fileName={`${courseTitle}.pdf`} - className='flex w-full' - > - {({ loading }) => - loading ? ( - - ) : ( -
- -
- ) - } -
- - - + + + +