- 공통 템플릿 타입 수정 (IntegratedDetailTemplate, UniversalListPage) - 페이지(app/[locale]) 타입 호환성 수정 (80개) - 재고/자재 모듈 타입 수정 (StockStatus, ReceivingManagement) - 생산 모듈 타입 수정 (WorkOrders, WorkerScreen, WorkResults) - 주문/출고 모듈 타입 수정 (ShipmentManagement, Orders) - 견적/단가 모듈 타입 수정 (Quotes, Pricing) - 건설 모듈 타입 수정 (49개, 17개 하위 모듈) - HR 모듈 타입 수정 (CardManagement, VacationManagement 등) - 설정 모듈 타입 수정 (PermissionManagement, AccountManagement 등) - 게시판 모듈 타입 수정 (BoardManagement, BoardList 등) - 회계 모듈 타입 수정 (VendorManagement, BadDebtCollection 등) - 기타 모듈 타입 수정 (CEODashboard, clients, vehicle 등) - 유틸/훅/API 타입 수정 (hooks, contexts, lib) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
183 lines
5.2 KiB
TypeScript
183 lines
5.2 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import puppeteer from 'puppeteer';
|
|
|
|
/**
|
|
* PDF 생성 API
|
|
* POST /api/pdf/generate
|
|
*
|
|
* Body: {
|
|
* html: string,
|
|
* styles: string,
|
|
* title?: string,
|
|
* orientation?: 'portrait' | 'landscape',
|
|
* documentNumber?: string,
|
|
* createdDate?: string,
|
|
* showHeaderFooter?: boolean
|
|
* }
|
|
* Response: PDF blob
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const {
|
|
html,
|
|
styles = '',
|
|
title = '문서',
|
|
orientation = 'portrait',
|
|
documentNumber = '',
|
|
createdDate = '',
|
|
showHeaderFooter = true,
|
|
} = await request.json();
|
|
|
|
if (!html) {
|
|
return NextResponse.json(
|
|
{ error: 'HTML content is required' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// Puppeteer 브라우저 실행 (Docker Alpine에서는 시스템 Chromium 사용)
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-gpu',
|
|
'--disable-software-rasterizer',
|
|
],
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
|
|
// 전체 HTML 문서 구성 (인라인 스타일 포함)
|
|
const fullHtml = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${title}</title>
|
|
<style>
|
|
/* 기본 리셋 */
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
color: #000;
|
|
background: #fff;
|
|
-webkit-print-color-adjust: exact;
|
|
print-color-adjust: exact;
|
|
}
|
|
|
|
/* 문서 컨테이너 - A4에 맞게 조정 */
|
|
.document-container {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
padding: 0;
|
|
}
|
|
|
|
/* 페이지 나눔 설정 */
|
|
table { page-break-inside: auto; }
|
|
tr { page-break-inside: avoid; page-break-after: auto; }
|
|
thead { display: table-header-group; }
|
|
tfoot { display: table-footer-group; }
|
|
|
|
/* 이미지/SVG 크기 제한 */
|
|
img, svg {
|
|
max-width: 100%;
|
|
height: auto;
|
|
}
|
|
|
|
/* 클라이언트에서 추출한 computed styles */
|
|
${styles}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="document-container">
|
|
${html}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
// 뷰포트 설정 (문서 전체가 보이도록 넓게)
|
|
await page.setViewport({
|
|
width: 1200,
|
|
height: 1600,
|
|
deviceScaleFactor: 2,
|
|
});
|
|
|
|
// HTML 설정
|
|
await page.setContent(fullHtml, {
|
|
waitUntil: 'networkidle0',
|
|
});
|
|
|
|
// 헤더 템플릿 (문서번호, 생성일)
|
|
const headerTemplate = showHeaderFooter
|
|
? `
|
|
<div style="width: 100%; font-size: 9px; font-family: 'Pretendard', sans-serif; padding: 0 15mm;">
|
|
<div style="border-bottom: 1px solid #ddd; padding-bottom: 5px; display: flex; justify-content: space-between; color: #666;">
|
|
<span>${documentNumber ? `문서번호: ${documentNumber}` : ''}</span>
|
|
<span>${createdDate ? `생성일: ${createdDate}` : ''}</span>
|
|
</div>
|
|
</div>
|
|
`
|
|
: '';
|
|
|
|
// 푸터 템플릿 (라인 + 페이지번호)
|
|
const footerTemplate = showHeaderFooter
|
|
? `
|
|
<div style="width: 100%; font-size: 9px; font-family: 'Pretendard', sans-serif; padding: 0 15mm;">
|
|
<div style="border-top: 1px solid #ddd; padding-top: 5px; display: flex; justify-content: space-between; color: #666;">
|
|
<span>${title}</span>
|
|
<span>Page <span class="pageNumber"></span> / <span class="totalPages"></span></span>
|
|
</div>
|
|
</div>
|
|
`
|
|
: '';
|
|
|
|
// PDF 생성 (자동 스케일로 A4에 맞춤)
|
|
const pdfBuffer = await page.pdf({
|
|
format: 'A4',
|
|
landscape: orientation === 'landscape',
|
|
printBackground: true,
|
|
preferCSSPageSize: false,
|
|
scale: 0.75, // 문서를 75%로 축소하여 A4에 맞춤
|
|
displayHeaderFooter: showHeaderFooter,
|
|
headerTemplate: headerTemplate,
|
|
footerTemplate: footerTemplate,
|
|
margin: {
|
|
top: showHeaderFooter ? '20mm' : '10mm',
|
|
right: '10mm',
|
|
bottom: showHeaderFooter ? '20mm' : '10mm',
|
|
left: '10mm',
|
|
},
|
|
});
|
|
|
|
// 브라우저 종료
|
|
await browser.close();
|
|
|
|
// PDF 응답
|
|
return new NextResponse(Buffer.from(pdfBuffer), {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/pdf',
|
|
'Content-Disposition': `attachment; filename="${encodeURIComponent(title)}.pdf"`,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('PDF 생성 오류:', error);
|
|
return NextResponse.json(
|
|
{ error: 'PDF 생성 중 오류가 발생했습니다.' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|