Merge pull request 'feat: 지붕재적합성 pdf 다운로드 중 다량데이터 요청 시 서버 shutdown 케이스 방어처리 추가' (#58) from feature/suitable into dev
Reviewed-on: #58
This commit is contained in:
commit
99c3d12925
37
package-lock.json
generated
37
package-lock.json
generated
@ -22,6 +22,7 @@
|
||||
"mysql2": "^3.14.1",
|
||||
"next": "15.2.4",
|
||||
"nodemailer": "^7.0.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-to-pdf": "^2.0.0",
|
||||
@ -1555,6 +1556,24 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/standard-fonts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/upng": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "6.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz",
|
||||
@ -4120,6 +4139,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||
"@pdf-lib/upng": "^1.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"mysql2": "^3.14.1",
|
||||
"next": "15.2.4",
|
||||
"nodemailer": "^7.0.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-to-pdf": "^2.0.0",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { pdf, Document } from '@react-pdf/renderer'
|
||||
import { PDFDocument } from 'pdf-lib'
|
||||
import { prisma } from '@/libs/prisma'
|
||||
import { type Suitable } from '@/types/Suitable'
|
||||
import SuitablePdf from '@/components/pdf/SuitablePdf'
|
||||
@ -65,14 +66,32 @@ export async function POST(request: NextRequest) {
|
||||
// 데이터 조회
|
||||
const suitable: Suitable[] = await prisma.$queryRawUnsafe(query)
|
||||
|
||||
// pdf 생성
|
||||
const content = React.createElement(Document, null, React.createElement(SuitablePdf, { data: suitable, fileTitle: fileTitle }))
|
||||
// pdf 생성 : mainId 100개씩 청크로 나누기
|
||||
const CHUNK_SIZE = 100
|
||||
const pdfBuffers: Uint8Array[] = []
|
||||
for (let i = 0; i < suitable.length; i += CHUNK_SIZE) {
|
||||
const chunk = suitable.slice(i, i + CHUNK_SIZE)
|
||||
const content = React.createElement(
|
||||
Document,
|
||||
null,
|
||||
React.createElement(SuitablePdf, {
|
||||
data: chunk,
|
||||
fileTitle: fileTitle,
|
||||
firstPage: i === 0,
|
||||
}),
|
||||
)
|
||||
const pdfBlob = await pdf(content).toBlob()
|
||||
const arrayBuffer = await pdfBlob.arrayBuffer()
|
||||
pdfBuffers.push(new Uint8Array(arrayBuffer))
|
||||
}
|
||||
|
||||
// 모든 PDF 버퍼 병합
|
||||
const mergedPdfBytes = await mergePdfBuffers(pdfBuffers)
|
||||
|
||||
const fileName = `${fileTitle.replace(' ', '_')}.pdf`
|
||||
const encodedFileName = encodeURIComponent(fileName)
|
||||
|
||||
return new Response(pdfBlob, {
|
||||
return new NextResponse(Buffer.from(mergedPdfBytes), {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="suitable.pdf"; filename*=UTF-8''${encodedFileName}`,
|
||||
@ -83,3 +102,16 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: '데이터 조회 중 오류가 발생했습니다' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
async function mergePdfBuffers(buffers: Uint8Array[]) {
|
||||
const mergedPdf = await PDFDocument.create()
|
||||
|
||||
for (const buf of buffers) {
|
||||
const pdf = await PDFDocument.load(buf)
|
||||
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices())
|
||||
copiedPages.forEach((page) => mergedPdf.addPage(page))
|
||||
}
|
||||
|
||||
const mergedPdfBytes = await mergedPdf.save()
|
||||
return mergedPdfBytes
|
||||
}
|
||||
|
||||
@ -63,35 +63,42 @@ const formatDateString = () => {
|
||||
return `${year}年${month}月${day}日 ${hours}:${minutes.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
export default function SuitablePdf({ data, fileTitle }: { data: Suitable[]; fileTitle: string }) {
|
||||
export default function SuitablePdf({ data, fileTitle, firstPage }: { data: Suitable[]; fileTitle: string; firstPage: boolean }) {
|
||||
const createTime = formatDateString()
|
||||
|
||||
return (
|
||||
<Page size="A4" orientation="landscape" style={styles.page}>
|
||||
{/* Intro Section */}
|
||||
{firstPage && (
|
||||
<View>
|
||||
<Text style={[styles.text]}>ハンファジャパン株式会社</Text>
|
||||
<Text style={[styles.text]}>{fileTitle}</Text>
|
||||
<Text style={[styles.text]}>{createTime}</Text>
|
||||
<View>
|
||||
<Text style={styles.text}>ハンファジャパン株式会社</Text>
|
||||
<Text style={styles.text}>{fileTitle}</Text>
|
||||
<Text style={styles.text}>{createTime}</Text>
|
||||
</View>
|
||||
|
||||
<View>
|
||||
<Text style={[styles.text]}>本適合表は参考資料としてご使用下さい。</Text>
|
||||
<Text style={[styles.text]}>屋根材製品の形状・仕様はメーカーより変更される場合が御座います。</Text>
|
||||
<Text style={[styles.text]}>
|
||||
<Text style={styles.text}>本適合表は参考資料としてご使用下さい。</Text>
|
||||
<Text style={styles.text}>屋根材製品の形状・仕様はメーカーより変更される場合が御座います。</Text>
|
||||
<Text style={styles.text}>
|
||||
又、現場環境(働き、勾配、瓦桟木条件等)により本適合表と異なる適合結果となる場合が御座います。予めご了承下さい。
|
||||
</Text>
|
||||
<Text style={[styles.text]}>屋根材以外の設置条件(垂木、野地板等の設置基準)も必ずご確認下さい。</Text>
|
||||
<Text style={styles.text}>屋根材以外の設置条件(垂木、野地板等の設置基準)も必ずご確認下さい。</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View>
|
||||
{/* Cards Section */}
|
||||
{data?.map((item: Suitable) => (
|
||||
<View key={item.id}>
|
||||
{/* Table Title */}
|
||||
<View>
|
||||
<Text style={styles.text}>{item.productName}</Text>
|
||||
<Text style={styles.text}>{item.manuFtCd}</Text>
|
||||
<Text style={styles.text}>{item.roofMtCd}</Text>
|
||||
</View>
|
||||
|
||||
{/* Table */}
|
||||
<View style={styles.table}>
|
||||
{/* Table Header */}
|
||||
<View style={styles.tableRow}>
|
||||
@ -102,6 +109,7 @@ export default function SuitablePdf({ data, fileTitle }: { data: Suitable[]; fil
|
||||
</View>
|
||||
|
||||
{/* Table Body */}
|
||||
<View>
|
||||
{JSON.parse(item.detail)?.map((subItem: SuitableDetail) => (
|
||||
<View key={subItem.id} style={styles.tableRow}>
|
||||
<Text style={[styles.tableCol, styles.text]}>{item.roofShCd}</Text>
|
||||
@ -112,6 +120,7 @@ export default function SuitablePdf({ data, fileTitle }: { data: Suitable[]; fil
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user