Compare commits

...

3 Commits

Author SHA1 Message Date
156e983454 feat: 지붕재적합성 pdf 다운로드 중 다량데이터 요청 시 서버 shutdown 케이스 방어처리 추가 2025-06-04 18:02:33 +09:00
64a643e619 feat: add builderId to session management
- Updated session data structure to include builderId in session state.
- Modified authentication and partner API routes to handle builderId.
- Adjusted default session and initial state to initialize builderId as null.
2025-06-04 17:10:22 +09:00
b62c859a11 Merge pull request 'feat: 지붕재적합성 pdf 다운로드 api (ssr) 방식 추가' (#57) from feature/suitable into dev
Reviewed-on: #57
2025-06-04 16:56:17 +09:00
9 changed files with 122 additions and 36 deletions

49
package-lock.json generated
View File

@ -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",
@ -2643,9 +2662,9 @@
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -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",
@ -4413,9 +4450,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"

View File

@ -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",

View File

@ -56,6 +56,7 @@ export async function POST(request: Request) {
session.groupId = result.data.data.groupId
session.storeLvl = result.data.data.storeLvl
session.custCd = result.data.data.custCd
session.builderId = result.data.data.builderId
session.builderNo = result.data.data.builderNo
session.builderNm = result.data.data.builderNm
session.isLoggedIn = true
@ -104,6 +105,7 @@ export async function POST(request: Request) {
GROUP_ID: result.data.data.groupId,
STORE_LVL: result.data.data.storeLvl,
CUST_CD: result.data.data.custCd,
BUILDER_ID: result.data.data.builderId,
BUILDER_NO: result.data.data.builderNo,
BUILDER_NM: result.data.data.builderNm,
IS_LOGGED_IN: true,

View File

@ -84,6 +84,7 @@ export async function POST(request: Request) {
session.groupId = null
session.storeLvl = null
session.custCd = null
session.builderId = data[0].user_seko_id
session.builderNo = data[0].user_seko_id
session.builderNm = data[0].supplier_name
session.isLoggedIn = true
@ -123,6 +124,7 @@ export async function POST(request: Request) {
GROUP_ID: null,
STORE_LVL: null,
CUST_CD: null,
BUILDER_ID: data[0].user_seko_id,
BUILDER_NO: data[0].user_seko_id,
BUILDER_NM: data[0].supplier_name,
IS_LOGGED_IN: true,

View File

@ -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
}

View File

@ -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>

View File

@ -44,6 +44,7 @@ export const defaultSession: SessionData = {
groupId: null,
storeLvl: null,
custCd: null,
builderId: null,
builderNo: null,
builderNm: null,
isLoggedIn: false,

View File

@ -40,6 +40,7 @@ const initialState: InitialState = {
groupId: null,
storeLvl: null,
custCd: null,
builderId: null,
builderNo: null,
builderNm: null,
isLoggedIn: false,

View File

@ -26,6 +26,7 @@ export interface SessionData {
groupId: null
storeLvl: null
custCd: null
builderId: null
builderNo: null
builderNm: null | string
isLoggedIn: boolean