summaryrefslogtreecommitdiff
path: root/pages/api/pdftron/createBasicContractPdf.ts
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-07 05:04:39 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-07 05:04:39 +0000
commite270e477f362dd68249bb4a013c66eab293bba82 (patch)
treeef29ec732fc62c220ca2f6ab12aa282b0db7c9ab /pages/api/pdftron/createBasicContractPdf.ts
parenta5cecbef039e29beaa616e3a36e7f15b8b35623c (diff)
(최겸) PQ요청+기본계약 로직 수정(한글화 미적용)
Diffstat (limited to 'pages/api/pdftron/createBasicContractPdf.ts')
-rw-r--r--pages/api/pdftron/createBasicContractPdf.ts359
1 files changed, 359 insertions, 0 deletions
diff --git a/pages/api/pdftron/createBasicContractPdf.ts b/pages/api/pdftron/createBasicContractPdf.ts
new file mode 100644
index 00000000..1122c022
--- /dev/null
+++ b/pages/api/pdftron/createBasicContractPdf.ts
@@ -0,0 +1,359 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import type { File as FormidableFile } from "formidable";
+import formidable from "formidable";
+import fs from "fs/promises";
+import path from "path";
+import { createBasicContractPdf } from "@/lib/pdftron/serverSDK/createBasicContractPdf";
+
+export const config = {
+ api: {
+ bodyParser: false,
+ },
+};
+
+// 보안 설정
+const SECURITY_CONFIG = {
+ ALLOWED_EXTENSIONS: new Set(['docx', 'doc']),
+ FORBIDDEN_EXTENSIONS: new Set([
+ 'exe', 'bat', 'cmd', 'scr', 'vbs', 'js', 'jar', 'com', 'pif',
+ 'msi', 'reg', 'ps1', 'sh', 'php', 'asp', 'jsp', 'py', 'pl',
+ 'html', 'htm', 'xhtml', 'xml', 'svg'
+ ]),
+ MAX_FILE_SIZE: 50 * 1024 * 1024, // 50MB
+ MAX_FILENAME_LENGTH: 255,
+};
+
+// 간단한 보안 검증 함수들
+function validateExtension(fileName: string): { valid: boolean; error?: string } {
+ const extension = path.extname(fileName).toLowerCase().substring(1);
+
+ if (!extension) {
+ return { valid: false, error: "파일 확장자가 없습니다" };
+ }
+
+ if (SECURITY_CONFIG.FORBIDDEN_EXTENSIONS.has(extension)) {
+ return { valid: false, error: `금지된 파일 형식입니다: .${extension}` };
+ }
+
+ if (!SECURITY_CONFIG.ALLOWED_EXTENSIONS.has(extension)) {
+ return { valid: false, error: `허용되지 않은 파일 형식입니다: .${extension}` };
+ }
+
+ return { valid: true };
+}
+
+function validateFileName(fileName: string): { valid: boolean; error?: string } {
+ if (fileName.length > SECURITY_CONFIG.MAX_FILENAME_LENGTH) {
+ return { valid: false, error: "파일명이 너무 깁니다" };
+ }
+
+ // 위험한 문자 체크
+ const dangerousPatterns = [
+ /[<>:"'|?*]/,
+ /[\x00-\x1f]/,
+ /^\./,
+ /\.\./,
+ /\/|\\$/,
+ /javascript:/i,
+ /data:/i,
+ /vbscript:/i,
+ /on\w+=/i,
+ /<script/i,
+ /<iframe/i,
+ ];
+
+ for (const pattern of dangerousPatterns) {
+ if (pattern.test(fileName)) {
+ return { valid: false, error: "안전하지 않은 파일명입니다" };
+ }
+ }
+
+ return { valid: true };
+}
+
+function validateFileSize(size: number): { valid: boolean; error?: string } {
+ if (size <= 0) {
+ return { valid: false, error: "파일이 비어있습니다" };
+ }
+
+ if (size > SECURITY_CONFIG.MAX_FILE_SIZE) {
+ const maxSizeMB = Math.round(SECURITY_CONFIG.MAX_FILE_SIZE / (1024 * 1024));
+ return { valid: false, error: `파일 크기가 너무 큽니다 (최대 ${maxSizeMB}MB)` };
+ }
+
+ return { valid: true };
+}
+
+function validateTemplateData(templateData: any): { valid: boolean; error?: string } {
+ try {
+ if (typeof templateData !== 'object' || templateData === null) {
+ return { valid: false, error: "템플릿 데이터는 객체여야 합니다" };
+ }
+
+ // 객체 크기 제한
+ const dataString = JSON.stringify(templateData);
+ if (dataString.length > 100000) { // 100KB
+ return { valid: false, error: "템플릿 데이터가 너무 큽니다" };
+ }
+
+ // XSS 방지 검증
+ const dangerousPatterns = [
+ /<script[\s\S]*?>/i,
+ /<iframe[\s\S]*?>/i,
+ /javascript\s*:/i,
+ /vbscript\s*:/i,
+ /on\w+\s*=/i,
+ ];
+
+ for (const pattern of dangerousPatterns) {
+ if (pattern.test(dataString)) {
+ return { valid: false, error: "템플릿 데이터에 잠재적으로 위험한 스크립트가 포함되어 있습니다" };
+ }
+ }
+
+ return { valid: true };
+ } catch (error) {
+ return { valid: false, error: "템플릿 데이터 검증 중 오류가 발생했습니다" };
+ }
+}
+
+async function validateFileContent(buffer: Buffer, fileName: string): Promise<{ valid: boolean; error?: string }> {
+ try {
+ // 실행 파일 패턴 검색
+ const executablePatterns = [
+ Buffer.from([0x4D, 0x5A]), // MZ (Windows executable)
+ Buffer.from([0x7F, 0x45, 0x4C, 0x46]), // ELF (Linux executable)
+ ];
+
+ for (const pattern of executablePatterns) {
+ if (buffer.subarray(0, pattern.length).equals(pattern)) {
+ return { valid: false, error: "실행 파일은 업로드할 수 없습니다" };
+ }
+ }
+
+ return { valid: true };
+ } catch (error) {
+ console.error("파일 내용 검증 오류:", error);
+ return { valid: false, error: "파일 내용을 검증할 수 없습니다" };
+ }
+}
+
+// DRM 복호화 함수 (create report 로직과 동일)
+async function decryptBufferWithDRM(buffer: Buffer, originalFileName: string): Promise<Buffer> {
+ try {
+ // 1. 기본 보안 검증
+ const extensionCheck = validateExtension(originalFileName);
+ if (!extensionCheck.valid) {
+ console.error(`🚨 파일 확장자 보안 위반: ${originalFileName} - ${extensionCheck.error}`);
+ throw new Error(extensionCheck.error);
+ }
+
+ const fileNameCheck = validateFileName(originalFileName);
+ if (!fileNameCheck.valid) {
+ console.error(`🚨 파일명 보안 위반: ${originalFileName} - ${fileNameCheck.error}`);
+ throw new Error(fileNameCheck.error);
+ }
+
+ const sizeCheck = validateFileSize(buffer.length);
+ if (!sizeCheck.valid) {
+ console.error(`🚨 파일 크기 보안 위반: ${originalFileName} - ${sizeCheck.error}`);
+ throw new Error(sizeCheck.error);
+ }
+
+ // 2. 파일 내용 기본 검증
+ const contentCheck = await validateFileContent(buffer, originalFileName);
+ if (!contentCheck.valid) {
+ console.error(`🚨 파일 내용 보안 위반: ${originalFileName} - ${contentCheck.error}`);
+ throw new Error(contentCheck.error);
+ }
+
+ console.log(`✅ 보안 검증 완료: ${originalFileName}`);
+
+ // 3. DRM 복호화 진행
+ const blob = new Blob([buffer]);
+ const file = new File([blob], originalFileName);
+
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const backendUrl = "http://localhost:6543/api/drm-proxy/decrypt";
+
+ console.log(`[DRM] 서버에서 파일 복호화 시도: ${originalFileName} (크기: ${buffer.length} bytes)`);
+
+ const response = await fetch(backendUrl, {
+ method: "POST",
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text().catch(() => '응답 텍스트를 가져올 수 없음');
+ throw new Error(`DRM 서버 응답 오류 [${response.status}]: ${errorText}`);
+ }
+
+ const arrayBuffer = await response.arrayBuffer();
+ const decryptedBuffer = Buffer.from(arrayBuffer);
+
+ console.log(`[DRM] 서버에서 파일 복호화 성공: ${originalFileName} (결과 크기: ${decryptedBuffer.length} bytes)`);
+
+ return decryptedBuffer;
+
+ } catch (error) {
+ const errorMessage = error instanceof Error
+ ? `${error.name}: ${error.message}`
+ : String(error);
+
+ console.error(`[DRM] 서버 복호화 오류: ${errorMessage}`, {
+ fileName: originalFileName,
+ fileSize: buffer.length,
+ error
+ });
+
+ // 보안 검증 실패인 경우 원본도 반환하지 않음
+ if (errorMessage.includes('보안') || errorMessage.includes('위반') || errorMessage.includes('금지된') || errorMessage.includes('허용되지')) {
+ throw error; // 보안 오류는 재throw
+ }
+
+ return buffer; // 일반적인 DRM 오류는 원본 버퍼 반환 (폴백)
+ }
+}
+
+export default async function handler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ if (req.method !== "POST") {
+ return res.status(405).json({ error: "Method not allowed" });
+ }
+
+ // 요청 ID 생성 (로깅용)
+ const requestId = `basiccontract_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+ console.log(`🚀 [${requestId}] 기본계약서 PDF 변환 요청 시작`);
+
+ try {
+ const form = formidable({
+ multiples: false,
+ maxFileSize: SECURITY_CONFIG.MAX_FILE_SIZE,
+ maxFieldsSize: 10 * 1024 * 1024, // 10MB 필드 크기 제한
+ maxFields: 20, // 최대 필드 수 제한
+ });
+
+ form.parse(req, async (err, fields, files) => {
+ if (err) {
+ console.error(`❌ [${requestId}] Form parsing 오류:`, err);
+ return res.status(500).json({
+ error: "파일 업로드 처리 중 오류가 발생했습니다",
+ requestId
+ });
+ }
+
+ try {
+ const outputFileName = fields?.outputFileName?.[0] ?? "";
+ const templateFile: FormidableFile | undefined = files?.templateFile?.[0];
+
+ // 1. 기본 필드 검증
+ if (!templateFile || outputFileName.length === 0) {
+ return res.status(400).json({
+ error: "필수 데이터가 누락되었습니다 (템플릿 파일, 출력 파일명)",
+ requestId
+ });
+ }
+
+ // 2. 파일명 보안 검증
+ const fileNameCheck = validateFileName(outputFileName);
+ if (!fileNameCheck.valid) {
+ console.error(`🚨 [${requestId}] 출력 파일명 보안 위반:`, fileNameCheck.error);
+ return res.status(400).json({
+ error: `출력 파일명 오류: ${fileNameCheck.error}`,
+ requestId
+ });
+ }
+
+ // 3. 템플릿 데이터 파싱 및 검증
+ let templateData: any = {};
+ try {
+ const templateDataString = fields?.templateData?.[0] ?? "{}";
+ templateData = JSON.parse(templateDataString);
+ } catch (parseError) {
+ console.error(`❌ [${requestId}] 템플릿 데이터 파싱 오류:`, parseError);
+ return res.status(400).json({
+ error: "템플릿 데이터 형식이 올바르지 않습니다",
+ requestId
+ });
+ }
+
+ const dataValidation = validateTemplateData(templateData);
+ if (!dataValidation.valid) {
+ console.error(`🚨 [${requestId}] 템플릿 데이터 보안 위반:`, dataValidation.error);
+ return res.status(400).json({
+ error: `템플릿 데이터 오류: ${dataValidation.error}`,
+ requestId
+ });
+ }
+
+ console.log(`✅ [${requestId}] 모든 보안 검증 완료 - 파일 처리 시작`);
+
+ // 4. 원본 파일 읽기
+ const originalBuffer = await fs.readFile(templateFile.filepath);
+
+ // 5. DRM 복호화 처리 (보안 검증 포함)
+ console.log(`🔐 [${requestId}] DRM 복호화 시작: ${templateFile.originalFilename || 'unknown'}`);
+ const decryptedBuffer = await decryptBufferWithDRM(
+ originalBuffer,
+ templateFile.originalFilename || 'template.docx'
+ );
+
+ // 6. 복호화된 버퍼로 기본계약서 PDF 생성
+ console.log(`📄 [${requestId}] 기본계약서 PDF 생성 시작`);
+ const {
+ result,
+ buffer: pdfBuffer,
+ error,
+ } = await createBasicContractPdf(decryptedBuffer, templateData);
+
+ if (result && pdfBuffer) {
+ console.log(`✅ [${requestId}] 기본계약서 PDF 생성 성공: ${outputFileName}`);
+
+ // 보안 헤더 설정
+ res.setHeader("Content-Type", "application/pdf");
+
+ // 한글 파일명을 위한 UTF-8 인코딩
+ const encodedFileName = encodeURIComponent(outputFileName);
+ res.setHeader("Content-Disposition", `attachment; filename*=UTF-8''${encodedFileName}`);
+
+ res.setHeader("X-Content-Type-Options", "nosniff");
+ res.setHeader("X-Frame-Options", "DENY");
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+
+ return res.send(Buffer.from(pdfBuffer));
+ }
+
+ console.error(`❌ [${requestId}] 기본계약서 PDF 생성 실패:`, error);
+ return res.status(500).json({
+ success: false,
+ message: "기본계약서 PDF 생성에 실패하였습니다.",
+ error,
+ requestId,
+ });
+
+ } catch (e) {
+ console.error(`❌ [${requestId}] 처리 중 오류:`, e);
+
+ // 보안 오류와 일반 오류 구분
+ const errorMessage = e instanceof Error ? e.message : "처리 중 오류가 발생했습니다";
+ const isSecurityError = errorMessage.includes('보안') || errorMessage.includes('위반') ||
+ errorMessage.includes('금지된') || errorMessage.includes('허용되지');
+
+ return res.status(isSecurityError ? 400 : 500).json({
+ error: isSecurityError ? errorMessage : "처리 중 오류가 발생했습니다",
+ requestId
+ });
+ }
+ });
+ } catch (err) {
+ console.error(`❌ [${requestId}] 전역 오류:`, err);
+ return res.status(500).json({
+ error: "서버 내부 오류가 발생했습니다",
+ requestId
+ });
+ }
+}