Skip to content

Commit

Permalink
Feat : pdfWorker for PDF conversion (#96)
Browse files Browse the repository at this point in the history
Feat : pdfWorker for PDF conversion
  • Loading branch information
oikkoikk authored Nov 24, 2023
2 parents bb24c8f + 9436c31 commit 782468b
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 93 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 0 additions & 53 deletions src/components/classroom/pdf/MaterialExportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
57 changes: 55 additions & 2 deletions src/components/classroom/pdf/MaterialPDFDocument.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,71 @@
'use client';
import {
Document,
Page,
Text,
StyleSheet,
Image,
View
View,
Font
} from '@react-pdf/renderer';
import { Token, marked } from 'marked';

type Props = { materials: CourseMaterialWorkbook };

const TimestampRegex = /<button[^>]*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,
Expand Down
78 changes: 44 additions & 34 deletions src/components/classroom/pdf/MaterialPDFRenderer.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'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';
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;
Expand All @@ -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<string>();
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<GeneratePDF>(worker);

setLoading(true);
generatePDF(data as CourseMaterialWorkbook)
.then(setUrl)
.finally(() => setLoading(false));

return () => {
if (url) {
URL.revokeObjectURL(url);
worker.terminate();
}
};
}
}, [data]);

return (
<div className='flex flex-col items-center justify-center w-full h-full gap-10'>
{status === 'loading' ? (
<LoadingSpinner className='text-sroom-brand' />
{status === 'loading' || loading ? (
<CourseMaterialLoading
title='강의 자료를 PDF로 변환 중이에요!'
description='강의 자료가 너무 많을 경우, 시간이 오래 걸릴 수 있어요'
/>
) : (
<>
{data && data.status === STATUS.SUCCESS ? (
{url ? (
<>
<PDFDownloadLink
document={<MaterialPDFDocument materials={data} />}
fileName={`${courseTitle}.pdf`}
className='flex w-full'
>
{({ loading }) =>
loading ? (
<LoadingSpinner className='text-sroom-brand' />
) : (
<div className='flex flex-col items-center justify-center w-full h-full gap-10'>
<Button
className='flex text-sroom-brand'
hoverEffect={true}
>
<p className='text-base font-semibold'>저장하기</p>
<span className='w-6 h-6'>
<DownloadSVG />
</span>
</Button>
</div>
)
}
</PDFDownloadLink>
<PDFViewer width={100} height={100} className='w-full h-full'>
<MaterialPDFDocument materials={data} />
</PDFViewer>
<a href={url} download={`${courseTitle}.pdf`}>
<Button className='flex text-sroom-brand' hoverEffect={true}>
<p className='text-base font-semibold'>저장하기</p>
<span className='w-6 h-6'>
<DownloadSVG />
</span>
</Button>
</a>
<iframe src={url} width='100%' height='100%' />
</>
) : (
<CourseMaterialLoading />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import LoadingSpinnerSVG from '@/public/icon/LoadingSpinner';

type Props = {};
type Props = {
title?: string;
description?: string;
};

export default function CourseMaterialLoading({}: Props) {
export default function CourseMaterialLoading({ title, description }: Props) {
return (
<div className='flex flex-col items-center justify-center h-[calc(100%-5rem)] gap-7'>
<div className='flex items-center justify-center w-12 h-12 animate-spin'>
<LoadingSpinnerSVG />
</div>
<div className='flex flex-col items-center justify-center'>
<p className='mb-1 text-lg font-semibold text-sroom-black-300'>
강의 자료를 생성 중이에요!
{title ? title : '강의 자료를 생성 중이에요!'}
</p>
<p className='font-normal text-sroom-black-100'>
{description ? description : '조금만 기다려 주세요'}
</p>
<p className='font-normal text-sroom-black-100'>조금만 기다려 주세요</p>
</div>
</div>
);
Expand Down
15 changes: 15 additions & 0 deletions src/util/pdf/pdfWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { pdf } from '@react-pdf/renderer';
import { createElement } from 'react';
import MaterialPDFDocument from '../../components/classroom/pdf/MaterialPDFDocument';
import { expose } from 'comlink';

const generatePDF = async (materials: CourseMaterialWorkbook) => {
const blob = await pdf(
createElement(MaterialPDFDocument as any, { materials }) as any
).toBlob();
return URL.createObjectURL(blob);
};

expose(generatePDF);

export type GeneratePDF = (material: CourseMaterialWorkbook) => Promise<string>;

0 comments on commit 782468b

Please sign in to comment.