From e270e477f362dd68249bb4a013c66eab293bba82 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 7 Aug 2025 05:04:39 +0000 Subject: (최겸) PQ요청+기본계약 로직 수정(한글화 미적용) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/basic-contract/get-template/route.ts | 52 + lib/basic-contract/service.ts | 12 + .../template/template-editor-wrapper.tsx | 103 +- .../vendor-table/basic-contract-columns.tsx | 38 +- .../vendor-table/basic-contract-sign-dialog.tsx | 10 +- .../viewer/basic-contract-sign-viewer.tsx | 11 +- lib/pdftron/serverSDK/createBasicContractPdf.ts | 137 +++ lib/vendors/service.ts | 1132 ++++++++++++-------- lib/vendors/table/request-pq-dialog.tsx | 193 +++- pages/api/pdftron/createBasicContractPdf.ts | 359 +++++++ 10 files changed, 1553 insertions(+), 494 deletions(-) create mode 100644 app/api/basic-contract/get-template/route.ts create mode 100644 lib/pdftron/serverSDK/createBasicContractPdf.ts create mode 100644 pages/api/pdftron/createBasicContractPdf.ts diff --git a/app/api/basic-contract/get-template/route.ts b/app/api/basic-contract/get-template/route.ts new file mode 100644 index 00000000..111532f0 --- /dev/null +++ b/app/api/basic-contract/get-template/route.ts @@ -0,0 +1,52 @@ +import { NextRequest, NextResponse } from 'next/server'; +import fs from 'fs/promises'; +import path from 'path'; +import db from "@/db/db"; +import { basicContractTemplates } from '@/db/schema'; +import { eq } from 'drizzle-orm'; + +export async function POST(request: NextRequest) { + try { + const { templateId } = await request.json(); + + if (!templateId) { + return NextResponse.json({ error: '템플릿 ID가 누락되었습니다.' }, { status: 400 }); + } + + // 데이터베이스에서 템플릿 정보 가져오기 + const template = await db.query.basicContractTemplates.findFirst({ + where: eq(basicContractTemplates.id, templateId) + }); + + if (!template) { + return NextResponse.json({ error: '템플릿을 찾을 수 없습니다.' }, { status: 404 }); + } + + if (!template.filePath) { + return NextResponse.json({ error: '템플릿 파일 경로가 없습니다.' }, { status: 404 }); + } + + // 파일 시스템에서 파일 읽기 + const fullPath = path.join(process.cwd(), "public", template.filePath); + + try { + const fileBuffer = await fs.readFile(fullPath); + + // 파일을 Blob으로 반환 + return new NextResponse(fileBuffer, { + status: 200, + headers: { + 'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'Content-Disposition': `attachment; filename="${encodeURIComponent(template.fileName || 'template.docx')}"`, + }, + }); + } catch (fileError) { + console.error('파일 읽기 오류:', fileError); + return NextResponse.json({ error: '템플릿 파일을 읽을 수 없습니다.' }, { status: 500 }); + } + + } catch (error) { + console.error('템플릿 가져오기 오류:', error); + return NextResponse.json({ error: '서버 오류가 발생했습니다.' }, { status: 500 }); + } +} diff --git a/lib/basic-contract/service.ts b/lib/basic-contract/service.ts index 9b5505b5..03b27f96 100644 --- a/lib/basic-contract/service.ts +++ b/lib/basic-contract/service.ts @@ -1129,4 +1129,16 @@ export async function getExistingTemplateNames(): Promise { .groupBy(basicContractTemplates.templateName); return rows.map((r) => r.templateName); +} + +export async function getExistingTemplateNamesById(id:number): Promise { + const rows = await db + .select({ + templateName: basicContractTemplates.templateName, + }) + .from(basicContractTemplates) + .where(and(eq(basicContractTemplates.status,"ACTIVE"),eq(basicContractTemplates.id,id))) + .limit(1) + + return rows[0].templateName; } \ No newline at end of file diff --git a/lib/basic-contract/template/template-editor-wrapper.tsx b/lib/basic-contract/template/template-editor-wrapper.tsx index ea353b91..96e2330f 100644 --- a/lib/basic-contract/template/template-editor-wrapper.tsx +++ b/lib/basic-contract/template/template-editor-wrapper.tsx @@ -7,7 +7,7 @@ import { Save, RefreshCw, Type, FileText, AlertCircle } from "lucide-react"; import type { WebViewerInstance } from "@pdftron/webviewer"; import { Badge } from "@/components/ui/badge"; import { BasicContractTemplateViewer } from "./basic-contract-template-viewer"; -import { saveTemplateFile } from "../service"; +import { getExistingTemplateNamesById, saveTemplateFile } from "../service"; interface TemplateEditorWrapperProps { templateId: string | number; @@ -16,9 +16,27 @@ interface TemplateEditorWrapperProps { refreshAction?: () => Promise; } +// 템플릿 이름별 변수 매핑 (영문 변수명 사용) +const TEMPLATE_VARIABLES_MAP = { + "준법서약 (한글)": ["vendor_name", "address", "representative_name", "today_date"], + "준법서약 (영문)": ["vendor_name", "address", "representative_name", "today_date"], + "기술자료 요구서": ["vendor_name", "address", "representative_name", "today_date"], + "비밀유지 계약서": ["vendor_name", "address", "representative_name", "today_date"], + "표준하도급기본 계약서": ["vendor_name", "address", "representative_name", "today_date"], + "GTC": ["vendor_name", "address", "representative_name", "today_date"], + "안전보건관리 약정서": ["vendor_name", "address", "representative_name", "today_date"], + "동반성장": ["vendor_name", "address", "representative_name", "today_date"], + "윤리규범 준수 서약서": ["vendor_name", "address", "representative_name", "today_date"], + "기술자료 동의서": ["vendor_name", "address", "representative_name", "today_date"], + "내국신용장 미개설 합의서": ["vendor_name", "address", "representative_name", "today_date"], + "직납자재 하도급대급등 연동제 의향서": ["vendor_name", "address", "representative_name", "today_date"] +} as const; + // 변수 패턴 감지를 위한 정규식 const VARIABLE_PATTERN = /\{\{([^}]+)\}\}/g; + + export function TemplateEditorWrapper({ templateId, filePath, @@ -28,6 +46,35 @@ export function TemplateEditorWrapper({ const [instance, setInstance] = React.useState(null); const [isSaving, setIsSaving] = React.useState(false); const [documentVariables, setDocumentVariables] = React.useState([]); + const [templateName, setTemplateName] = React.useState(""); + const [predefinedVariables, setPredefinedVariables] = React.useState([]); + + console.log(templateId, "templateId"); + + // 템플릿 이름 로드 및 변수 설정 + React.useEffect(() => { + const loadTemplateInfo = async () => { + try { + const name = await getExistingTemplateNamesById(Number(templateId)); + setTemplateName(name); + + // 템플릿 이름에 따른 변수 설정 + const variables = TEMPLATE_VARIABLES_MAP[name as keyof typeof TEMPLATE_VARIABLES_MAP] || []; + setPredefinedVariables(variables); + + console.log("🏷️ 템플릿 이름:", name); + console.log("📝 할당된 변수들:", variables); + } catch (error) { + console.error("템플릿 정보 로드 오류:", error); + // 기본 변수 설정 + setPredefinedVariables(["회사명", "주소", "대표자명", "오늘날짜"]); + } + }; + + if (templateId) { + loadTemplateInfo(); + } + }, [templateId]); // 문서에서 변수 추출 const extractVariablesFromDocument = async () => { @@ -220,13 +267,6 @@ export function TemplateEditorWrapper({ window.location.reload(); }; - // 미리 정의된 변수들 - const predefinedVariables = [ - "회사명", "계약자명", "계약일자", "계약금액", - "납기일자", "담당자명", "담당자연락처", "프로젝트명", - "계약번호", "사업부", "부서명", "승인자명" - ]; - return (
{/* 상단 도구 모음 */} @@ -267,6 +307,12 @@ export function TemplateEditorWrapper({ {fileName} + {templateName && ( + + + {templateName} + + )} {documentVariables.length > 0 && ( @@ -283,6 +329,11 @@ export function TemplateEditorWrapper({

변수 관리 + {templateName && ( + + ({templateName}) + + )}

@@ -301,23 +352,27 @@ export function TemplateEditorWrapper({ )} - {/* 미리 정의된 변수들 */} -
-

자주 사용하는 변수 (클릭하여 복사):

-
- {predefinedVariables.map((variable, index) => ( - - ))} + {/* 템플릿별 미리 정의된 변수들 */} + {predefinedVariables.length > 0 && ( +
+

+ {templateName ? `${templateName}에 권장되는 변수` : "자주 사용하는 변수"} (클릭하여 복사): +

+
+ {predefinedVariables.map((variable, index) => ( + + ))} +
-
+ )}
)} diff --git a/lib/basic-contract/vendor-table/basic-contract-columns.tsx b/lib/basic-contract/vendor-table/basic-contract-columns.tsx index b79487d7..c9e8da53 100644 --- a/lib/basic-contract/vendor-table/basic-contract-columns.tsx +++ b/lib/basic-contract/vendor-table/basic-contract-columns.tsx @@ -8,6 +8,7 @@ import { toast } from "sonner" import { getErrorMessage } from "@/lib/handle-error" import { formatDate, formatDateTime } from "@/lib/utils" +import { downloadFile } from "@/lib/file-download" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" @@ -36,28 +37,24 @@ interface GetColumnsProps { /** * 파일 다운로드 함수 */ -/** - * 파일 다운로드 함수 - */ -const handleFileDownload = (filePath: string | null, fileName: string | null) => { +const handleFileDownload = async (filePath: string | null, fileName: string | null) => { if (!filePath || !fileName) { toast.error("파일 정보가 없습니다."); return; } try { - // 전체 URL 생성 - const fullUrl = `${window.location.origin}${filePath}`; + // /api/files/ 엔드포인트를 통한 안전한 다운로드 + const apiFilePath = `/api/files/${filePath.startsWith('/') ? filePath.substring(1) : filePath}`; - // a 태그를 생성하여 다운로드 실행 - const link = document.createElement('a'); - link.href = fullUrl; - link.download = fileName; // 다운로드될 파일명 설정 - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - toast.success("파일 다운로드를 시작합니다."); + const result = await downloadFile(apiFilePath, fileName, { + action: 'download', + showToast: true + }); + + if (!result.success) { + console.error("파일 다운로드 실패:", result.error); + } } catch (error) { console.error("파일 다운로드 오류:", error); toast.error("파일 다운로드 중 오류가 발생했습니다."); @@ -104,16 +101,19 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef { - const template = row.original; - const filePath = template.status === "PENDING" ? template.filePath : template.signedFilePath + const contract = row.original; + // PENDING 상태일 때는 원본 PDF 파일 (signedFilePath), COMPLETED일 때는 서명된 파일 (signedFilePath) + const filePath = contract.signedFilePath; + const fileName = contract.signedFileName; return (