Files
sam-react-prod/src/app/api/pdf/generate/route.ts
유병철 a1f4c82cec fix: 프로젝트 전체 TypeScript 타입에러 408개 수정 (tsc --noEmit 0 errors)
- 공통 템플릿 타입 수정 (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>
2026-01-30 10:07:58 +09:00

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 }
);
}
}