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", "mysql2": "^3.14.1",
"next": "15.2.4", "next": "15.2.4",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"pdf-lib": "^1.17.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-to-pdf": "^2.0.0", "react-to-pdf": "^2.0.0",
@ -1555,6 +1556,24 @@
"node": ">=0.10" "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": { "node_modules/@prisma/client": {
"version": "6.7.0", "version": "6.7.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz",
@ -2643,9 +2662,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ms": "^2.1.3" "ms": "^2.1.3"
@ -4120,6 +4139,24 @@
"node": ">=8" "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": { "node_modules/performance-now": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
@ -4413,9 +4450,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"

View File

@ -29,6 +29,7 @@
"mysql2": "^3.14.1", "mysql2": "^3.14.1",
"next": "15.2.4", "next": "15.2.4",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"pdf-lib": "^1.17.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-to-pdf": "^2.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.groupId = result.data.data.groupId
session.storeLvl = result.data.data.storeLvl session.storeLvl = result.data.data.storeLvl
session.custCd = result.data.data.custCd session.custCd = result.data.data.custCd
session.builderId = result.data.data.builderId
session.builderNo = result.data.data.builderNo session.builderNo = result.data.data.builderNo
session.builderNm = result.data.data.builderNm session.builderNm = result.data.data.builderNm
session.isLoggedIn = true session.isLoggedIn = true
@ -104,6 +105,7 @@ export async function POST(request: Request) {
GROUP_ID: result.data.data.groupId, GROUP_ID: result.data.data.groupId,
STORE_LVL: result.data.data.storeLvl, STORE_LVL: result.data.data.storeLvl,
CUST_CD: result.data.data.custCd, CUST_CD: result.data.data.custCd,
BUILDER_ID: result.data.data.builderId,
BUILDER_NO: result.data.data.builderNo, BUILDER_NO: result.data.data.builderNo,
BUILDER_NM: result.data.data.builderNm, BUILDER_NM: result.data.data.builderNm,
IS_LOGGED_IN: true, IS_LOGGED_IN: true,

View File

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

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
import { pdf, Document } from '@react-pdf/renderer' import { pdf, Document } from '@react-pdf/renderer'
import { PDFDocument } from 'pdf-lib'
import { prisma } from '@/libs/prisma' import { prisma } from '@/libs/prisma'
import { type Suitable } from '@/types/Suitable' import { type Suitable } from '@/types/Suitable'
import SuitablePdf from '@/components/pdf/SuitablePdf' import SuitablePdf from '@/components/pdf/SuitablePdf'
@ -65,14 +66,32 @@ export async function POST(request: NextRequest) {
// 데이터 조회 // 데이터 조회
const suitable: Suitable[] = await prisma.$queryRawUnsafe(query) const suitable: Suitable[] = await prisma.$queryRawUnsafe(query)
// pdf 생성 // pdf 생성 : mainId 100개씩 청크로 나누기
const content = React.createElement(Document, null, React.createElement(SuitablePdf, { data: suitable, fileTitle: fileTitle })) const CHUNK_SIZE = 100
const pdfBlob = await pdf(content).toBlob() 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 fileName = `${fileTitle.replace(' ', '_')}.pdf`
const encodedFileName = encodeURIComponent(fileName) const encodedFileName = encodeURIComponent(fileName)
return new Response(pdfBlob, { return new NextResponse(Buffer.from(mergedPdfBytes), {
headers: { headers: {
'Content-Type': 'application/pdf', 'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="suitable.pdf"; filename*=UTF-8''${encodedFileName}`, '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 }) 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')}` 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() const createTime = formatDateString()
return ( return (
<Page size="A4" orientation="landscape" style={styles.page}> <Page size="A4" orientation="landscape" style={styles.page}>
{/* Intro Section */} {/* Intro Section */}
<View> {firstPage && (
<Text style={[styles.text]}></Text> <View>
<Text style={[styles.text]}>{fileTitle}</Text> <View>
<Text style={[styles.text]}>{createTime}</Text> <Text style={styles.text}></Text>
</View> <Text style={styles.text}>{fileTitle}</Text>
<Text style={styles.text}>{createTime}</Text>
<View> </View>
<Text style={[styles.text]}>使</Text> <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>
</View> <Text style={styles.text}></Text>
</View>
</View>
)}
<View> <View>
{/* Cards Section */} {/* Cards Section */}
{data?.map((item: Suitable) => ( {data?.map((item: Suitable) => (
<View key={item.id}> <View key={item.id}>
<Text style={styles.text}>{item.productName}</Text> {/* Table Title */}
<Text style={styles.text}>{item.manuFtCd}</Text> <View>
<Text style={styles.text}>{item.roofMtCd}</Text> <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}> <View style={styles.table}>
{/* Table Header */} {/* Table Header */}
<View style={styles.tableRow}> <View style={styles.tableRow}>
@ -102,14 +109,16 @@ export default function SuitablePdf({ data, fileTitle }: { data: Suitable[]; fil
</View> </View>
{/* Table Body */} {/* Table Body */}
{JSON.parse(item.detail)?.map((subItem: SuitableDetail) => ( <View>
<View key={subItem.id} style={styles.tableRow}> {JSON.parse(item.detail)?.map((subItem: SuitableDetail) => (
<Text style={[styles.tableCol, styles.text]}>{item.roofShCd}</Text> <View key={subItem.id} style={styles.tableRow}>
<Text style={[styles.tableCol, styles.text]}>{subItem.trestleMfpcCd}</Text> <Text style={[styles.tableCol, styles.text]}>{item.roofShCd}</Text>
<Text style={[styles.tableCol, styles.text]}>{subItem.trestleManufacturerProductName}</Text> <Text style={[styles.tableCol, styles.text]}>{subItem.trestleMfpcCd}</Text>
<Text style={[styles.tableCol, styles.text]}>{subItem.memo}</Text> <Text style={[styles.tableCol, styles.text]}>{subItem.trestleManufacturerProductName}</Text>
</View> <Text style={[styles.tableCol, styles.text]}>{subItem.memo}</Text>
))} </View>
))}
</View>
</View> </View>
</View> </View>
))} ))}

View File

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

View File

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

View File

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