diff options
| -rw-r--r-- | components/documents/StageList.tsx | 2 | ||||
| -rw-r--r-- | components/documents/view-document-dialog.tsx | 221 | ||||
| -rw-r--r-- | components/form-data/form-data-report-batch-dialog.tsx | 73 | ||||
| -rw-r--r-- | components/form-data/form-data-report-temp-upload-dialog.tsx | 28 | ||||
| -rw-r--r-- | components/form-data/form-data-table.tsx | 4 | ||||
| -rw-r--r-- | db/schema/vendorData.ts | 320 | ||||
| -rw-r--r-- | lib/forms/services.ts | 10 | ||||
| -rw-r--r-- | lib/pdftron/serverSDK/createReport.ts | 83 | ||||
| -rw-r--r-- | lib/po/service.ts | 4 | ||||
| -rw-r--r-- | pages/api/pdftron/createVendorDataReports.ts | 78 | ||||
| -rw-r--r-- | pages/api/po/sendDocuSign.ts | 8 | ||||
| -rw-r--r-- | pages/api/po/webhook.ts | 29 |
12 files changed, 456 insertions, 404 deletions
diff --git a/components/documents/StageList.tsx b/components/documents/StageList.tsx index 81f8a5ca..6df448df 100644 --- a/components/documents/StageList.tsx +++ b/components/documents/StageList.tsx @@ -55,7 +55,7 @@ interface Version { approvedDate: string | null DocumentSubmitDate: Date attachments: Attachment[] - selected?: boolean + selected?: boolean; } export default function StageList({ document }: StageListProps) { diff --git a/components/documents/view-document-dialog.tsx b/components/documents/view-document-dialog.tsx index 7603fdc0..9711c4be 100644 --- a/components/documents/view-document-dialog.tsx +++ b/components/documents/view-document-dialog.tsx @@ -1,4 +1,3 @@ -<<<<<<< HEAD "use client"; import * as React from "react"; @@ -14,7 +13,6 @@ import { } from "@/components/ui/dialog"; import { Building2, FileIcon, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; -import fs from "fs"; // 인터페이스 interface Attachment { @@ -37,7 +35,7 @@ interface Version { approvedDate: string | null; DocumentSubmitDate: Date; attachments: Attachment[]; - selected: boolean; + selected?: boolean; } type ViewDocumentDialogProps = { @@ -74,95 +72,22 @@ const DocumentViewer: React.FC<{ ); const [viwerLoading, setViewerLoading] = React.useState<boolean>(true); const [fileSetLoading, setFileSetLoading] = React.useState<boolean>(true); -======= -"use client" - -import * as React from "react" -import { WebViewerInstance } from "@pdftron/webviewer"; -import { - Dialog, DialogTrigger, DialogContent, DialogHeader, - DialogTitle, DialogDescription, DialogFooter -} from "@/components/ui/dialog" -import { Building2, FileIcon, Loader2 } from "lucide-react" -import { Button } from "@/components/ui/button" -import fs from "fs" - -interface Version { - id: number - stage: string - revision: string - uploaderType: string - uploaderName: string | null - comment: string | null - status: string | null - planDate: string | null - actualDate: string | null - approvedDate: string | null - DocumentSubmitDate: Date - attachments: Attachment[] - selected: boolean -} - -type ViewDocumentDialogProps = { - versions: Version[] -} - -export function ViewDocumentDialog({versions}: ViewDocumentDialogProps){ - const [open, setOpen] = React.useState(false) - - - return ( - <> - <Button - size="sm" - className="border-blue-200" - variant="outline" - onClick={() => setOpen(prev => !prev)} - > - 문서 보기 - </Button> - {open && <DocumentViewer - open={open} - setOpen={setOpen} - versions={versions} - /> - } - </> - ); -} - -function DocumentViewer({open, setOpen, versions}){ - const [instance, setInstance] = React.useState<null | WebViewerInstance>(null) - const [viwerLoading, setViewerLoading] = React.useState<boolean>(true) - const [fileSetLoading, setFileSetLoading] = React.useState<boolean>(true) ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d const viewer = React.useRef<HTMLDivElement>(null); const initialized = React.useRef(false); const isCancelled = React.useRef(false); // 초기화 중단용 flag const cleanupHtmlStyle = () => { const htmlElement = document.documentElement; -<<<<<<< HEAD // 기존 style 속성 가져오기 const originalStyle = htmlElement.getAttribute("style") || ""; -======= - - // 기존 style 속성 가져오기 - const originalStyle = htmlElement.getAttribute("style") || ""; - ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d // "color-scheme: light" 또는 "color-scheme: dark" 찾기 const colorSchemeStyle = originalStyle .split(";") .map((s) => s.trim()) .find((s) => s.startsWith("color-scheme:")); -<<<<<<< HEAD -======= - ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d // 새로운 스타일 적용 (color-scheme만 유지) if (colorSchemeStyle) { htmlElement.setAttribute("style", colorSchemeStyle + ";"); @@ -170,18 +95,13 @@ function DocumentViewer({open, setOpen, versions}){ htmlElement.removeAttribute("style"); // color-scheme도 없으면 style 속성 자체 삭제 } -<<<<<<< HEAD console.log("html style 삭제"); -======= - console.log("html style 삭제") ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }; React.useEffect(() => { if (open && !initialized.current) { initialized.current = true; isCancelled.current = false; // 다시 열릴 때는 false로 리셋 -<<<<<<< HEAD requestAnimationFrame(() => { if (viewer.current) { @@ -196,7 +116,8 @@ function DocumentViewer({open, setOpen, versions}){ WebViewer( { path: "/pdftronWeb", - licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + licenseKey: + "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd", fullAPI: true, css: "/globals.css", }, @@ -209,41 +130,11 @@ function DocumentViewer({open, setOpen, versions}){ "multiTabsEmptyPage", ]); setViewerLoading(false); -======= - - requestAnimationFrame(() => { - if (viewer.current) { - import("@pdftron/webviewer").then(({ default: WebViewer }) => { - console.log(isCancelled.current) - if (isCancelled.current) { - console.log("📛 WebViewer 초기화 취소됨 (Dialog 닫힘)"); - - return; - } - - WebViewer( - { - path: "/pdftronWeb", - licenseKey: "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd", - fullAPI: true, - css:"/globals.css" - }, - viewer.current as HTMLDivElement - ).then(async (instance: WebViewerInstance) => { - - - setInstance(instance); - instance.UI.enableFeatures([instance.UI.Feature.MultiTab]); - instance.UI.disableElements(["addTabButton", "multiTabsEmptyPage"]); - setViewerLoading(false); - ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }); }); } }); } -<<<<<<< HEAD return () => { // cleanup 시에는 중단 flag 세움 @@ -251,20 +142,10 @@ function DocumentViewer({open, setOpen, versions}){ instance.UI.dispose(); } setTimeout(() => cleanupHtmlStyle(), 500); -======= - - return async () => { - // cleanup 시에는 중단 flag 세움 - if(instance){ - await instance.UI.dispose() - } - await setTimeout(() => cleanupHtmlStyle(), 500) ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }; }, [open]); React.useEffect(() => { -<<<<<<< HEAD const loadDocument = async () => { if (instance && versions.length > 0) { const { UI } = instance; @@ -330,77 +211,10 @@ function DocumentViewer({open, setOpen, versions}){ try { await instance.UI.dispose(); setInstance(null); // 상태도 초기화 -======= - const loadDocument = async () => { - - if(instance && versions.length > 0){ - const { UI } = instance; - - const optionsArray = [] - - versions.forEach(c => { - const {attachments} = c - attachments.forEach(c2 => { - const {fileName, filePath, fileType} = c2 - - const options = { - filename: fileName, - ...(fileType.includes("xlsx") && { - officeOptions: { - formatOptions: { - applyPageBreaksToSheet: true, - }, - }, - }), - }; - - optionsArray.push({ - filePath, - options - }) - }) - }) - - const tabIds = []; - - for (const option of optionsArray) { - const { filePath, options } = option; - const response = await fetch(filePath); - const blob = await response.blob(); - - const tab = await UI.TabManager.addTab(blob, options); - tabIds.push(tab); // 탭 ID 저장 - } - - if (tabIds.length > 0) { - await UI.TabManager.setActiveTab(tabIds[0]); - } - - setFileSetLoading(false) - } - } - loadDocument(); - }, [instance, versions]) - - - return ( - <Dialog open={open} onOpenChange={async (val) => { - console.log({val, fileSetLoading}) - if(!val && fileSetLoading){ - return; - } - - if (instance) { - try { - await instance.UI.dispose(); - setInstance(null); // 상태도 초기화 - ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d } catch (e) { console.warn("dispose error", e); } } -<<<<<<< HEAD // cleanupHtmlStyle() setViewerLoading(false); @@ -408,7 +222,7 @@ function DocumentViewer({open, setOpen, versions}){ await setTimeout(() => cleanupHtmlStyle(), 1000); }} > - <DialogContent className="w-[70vw] h-[90vh]" style={{ maxWidth: "none" }}> + <DialogContent className="w-[90vw] h-[90vh]" style={{ maxWidth: "none" }}> <DialogHeader className="h-[38px]"> <DialogTitle>문서 미리보기</DialogTitle> <DialogDescription>첨부파일 미리보기</DialogDescription> @@ -430,30 +244,3 @@ function DocumentViewer({open, setOpen, versions}){ </Dialog> ); }; -======= - - // cleanupHtmlStyle() - setViewerLoading(false); - setOpen(prev => !prev) - await setTimeout(() => cleanupHtmlStyle(), 1000) - }}> - <DialogContent className="w-[90vw] h-[90vh]" style={{maxWidth: "none"}}> - <DialogHeader className="h-[38px]"> - <DialogTitle> - 문서 미리보기 - </DialogTitle> - <DialogDescription> - 첨부파일 미리보기 - </DialogDescription> - </DialogHeader> - <div ref={viewer} style={{height: "calc(90vh - 20px - 38px - 1rem - 48px)"}}> - {viwerLoading && <div className="flex flex-col items-center justify-center py-12"> - <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> - <p className="text-sm text-muted-foreground">문서 뷰어 로딩 중...</p> - </div>} - </div> - </DialogContent> - </Dialog> - ); -} ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx index e3fd7ea2..614f890e 100644 --- a/components/form-data/form-data-report-batch-dialog.tsx +++ b/components/form-data/form-data-report-batch-dialog.tsx @@ -6,7 +6,6 @@ import React, { SetStateAction, useState, useEffect, - useRef, } from "react"; import { useToast } from "@/hooks/use-toast"; import prettyBytes from "pretty-bytes"; @@ -25,7 +24,6 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, - SelectGroup, SelectItem, SelectTrigger, SelectValue, @@ -49,7 +47,7 @@ import { FileListName, } from "@/components/ui/file-list"; import { Button } from "@/components/ui/button"; -import { getReportTempList } from "@/lib/forms/services"; +import { getReportTempList, getOrigin } from "@/lib/forms/services"; import { DataTableColumnJSON } from "./form-data-table-columns"; const MAX_FILE_SIZE = 3000000; @@ -129,30 +127,59 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ setIsUploading(true); try { - const totalFiles = selectedFiles.length; - let successCount = 0; + const origin = await getOrigin() - for (let i = 0; i < totalFiles; i++) { - const file = selectedFiles[i]; + const targetFiles = selectedFiles[0]; - const formData = new FormData(); - formData.append("file", file); - formData.append("customFileName", file.name); + const reportDatas = reportData.map((c) => { + const reportValue = stringifyAllValues(c); - // await uploadReportTemp(packageId, formId, formData); + const reportValueMapping: { [key: string]: any } = {}; - successCount++; + columnsJSON.forEach((c2) => { + const { key, label } = c2; + + const objKey = label.split(" ").join("_"); + + reportValueMapping[objKey] = reportValue?.[key] ?? ""; + }); + + return reportValueMapping; + }); + const formData = new FormData(); + formData.append("file", targetFiles); + formData.append("customFileName", `${formCode}.pdf`); + formData.append("reportDatas", JSON.stringify(reportDatas)); + formData.append("reportTempPath", selectTemp); + + const reqeustCreateReport = await fetch( + `${origin}/api/pdftron/createVendorDataReports`, + { method: "POST", body: formData } + ); + + if (reqeustCreateReport.ok) { + const blob = await reqeustCreateReport.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `${formCode}.pdf`; + a.click(); + window.URL.revokeObjectURL(url); + } else { + const err = await reqeustCreateReport.json(); + console.error("에러:", err); } } catch (err) { console.error(err); toast({ title: "Error", - description: "파일 업로드 중 오류가 발생했습니다.", + description: "Report 생성 중 오류가 발생했습니다.", variant: "destructive", }); } finally { setIsUploading(false); - setOpen(false); + setSelectedFiles([]) + setOpen(false) } }; @@ -237,10 +264,10 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ <DialogFooter> <Button - disabled={selectedFiles.length === 0 || selectTemp.length === 0} + disabled={selectedFiles.length === 0 || selectTemp.length === 0 || isUploading} onClick={submitData} > - 다운로드 + {isUploading && <Loader2 />}다운로드 </Button> </DialogFooter> </DialogContent> @@ -305,3 +332,17 @@ const updateReportTempList: UpdateReportTempList = async ( }) ); }; + +const stringifyAllValues = (obj: any): any => { + if (Array.isArray(obj)) { + return obj.map((item) => stringifyAllValues(item)); + } else if (typeof obj === "object" && obj !== null) { + const result: any = {}; + for (const key in obj) { + result[key] = stringifyAllValues(obj[key]); + } + return result; + } else { + return obj !== null && obj !== undefined ? String(obj) : ""; + } +}; diff --git a/components/form-data/form-data-report-temp-upload-dialog.tsx b/components/form-data/form-data-report-temp-upload-dialog.tsx index b646c3e6..413c1e51 100644 --- a/components/form-data/form-data-report-temp-upload-dialog.tsx +++ b/components/form-data/form-data-report-temp-upload-dialog.tsx @@ -9,7 +9,7 @@ import React, { } from "react"; import { useToast } from "@/hooks/use-toast"; import prettyBytes from "pretty-bytes"; -import { X, Loader2 } from "lucide-react"; +import { X, Loader2, Download } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Dialog, @@ -137,7 +137,8 @@ export const FormDataReportTempUploadDialog: FC< <DialogHeader> <DialogTitle>Report Template Upload</DialogTitle> <DialogDescription> - 사용하시고자 하는 Report Template를 업로드 하여주시기 바랍니다. + 사용하시고자 하는 Report Template(docx File)를 업로드 하여주시기 + 바랍니다. </DialogDescription> </DialogHeader> {/* {prevReportTemp.length > 0 && ( @@ -150,6 +151,29 @@ export const FormDataReportTempUploadDialog: FC< </ScrollArea> </> )} */} + <div + // className="flex flex-col gap-2" + > + <Label>Sample Template Download</Label> + + <FileList className="max-h-[200px] gap-3"> + <FileListItem className="p-3"> + <FileListHeader> + <FileListIcon /> + <FileListInfo> + <FileListName>sample_template_file.docx</FileListName> + </FileListInfo> + <FileListAction + // onClick={() => removeFile(index)} + // disabled={isUploading} + > + <Download className="h-4 w-4" /> + <span className="sr-only">Download</span> + </FileListAction> + </FileListHeader> + </FileListItem> + </FileList> + </div> <Dropzone maxSize={MAX_FILE_SIZE} diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx index 9feaf3b2..50c4f267 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -531,14 +531,14 @@ export default function DynamicTable({ size="sm" onClick={() => setBatchDownDialog(true)} > - Report Batch + Report Download </Button> <Button variant="default" size="sm" onClick={() => setTempUpDialog(true)} > - Temp Upload + Template Upload </Button> <Button variant="default" diff --git a/db/schema/vendorData.ts b/db/schema/vendorData.ts index 92a92c8e..2739e8eb 100644 --- a/db/schema/vendorData.ts +++ b/db/schema/vendorData.ts @@ -3,32 +3,41 @@ import { text, varchar, timestamp, - integer, numeric, date, unique, serial, jsonb, uniqueIndex -} from "drizzle-orm/pg-core" -import { contractItems } from "./contract" - -export const forms = pgTable("forms", { - id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - contractItemId: integer("contract_item_id") + integer, + numeric, + date, + unique, + serial, + jsonb, + uniqueIndex, + } from "drizzle-orm/pg-core"; + import { contractItems } from "./contract"; + + export const forms = pgTable( + "forms", + { + id: integer("id").primaryKey().generatedAlwaysAsIdentity(), + contractItemId: integer("contract_item_id") .notNull() .references(() => contractItems.id, { onDelete: "cascade" }), - formCode: varchar("form_code", { length: 100 }).notNull(), - formName: varchar("form_name", { length: 255 }).notNull(), - // tagType: varchar("tag_type", { length: 50 }).notNull(), - // class: varchar("class", { length: 100 }).notNull(), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), -}, (table) => { - return { + formCode: varchar("form_code", { length: 100 }).notNull(), + formName: varchar("form_name", { length: 255 }).notNull(), + // tagType: varchar("tag_type", { length: 50 }).notNull(), + // class: varchar("class", { length: 100 }).notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (table) => { + return { // contractItemId와 formCode의 조합을 유니크하게 설정 - contractItemFormCodeUnique: uniqueIndex("contract_item_form_code_unique").on( - table.contractItemId, - table.formCode - ), + contractItemFormCodeUnique: uniqueIndex( + "contract_item_form_code_unique" + ).on(table.contractItemId, table.formCode), + }; } -}) - -export const rfqAttachments = pgTable("form_templates", { + ); + + export const rfqAttachments = pgTable("form_templates", { id: serial("id").primaryKey(), formId: integer("form_id").references(() => forms.id), fileName: varchar("file_name", { length: 255 }).notNull(), @@ -36,175 +45,212 @@ export const rfqAttachments = pgTable("form_templates", { createdAt: timestamp("created_at").defaultNow().notNull(), udpatedAt: timestamp("updated_at").defaultNow().notNull(), + }); -}); - - -export const formMetas = pgTable("form_metas", { + export const formMetas = pgTable("form_metas", { id: serial("id").primaryKey(), formCode: varchar("form_code", { length: 50 }).notNull(), formName: varchar("form_name", { length: 255 }).notNull(), columns: jsonb("columns").notNull(), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), -}) - -export const formEntries = pgTable("form_entries", { + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }); + + export const formEntries = pgTable("form_entries", { id: serial("id").primaryKey(), formCode: varchar("form_code", { length: 50 }).notNull(), data: jsonb("data").notNull(), contractItemId: integer("contract_item_id") - .notNull() - .references(() => contractItems.id, { onDelete: "cascade" }), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), -}) - - -// ============ tags (각 계약 아이템에 대한 Tag) ============ -// "어느 계약의 어느 아이템에 대한 태그"임을 나타내려면 contract_items를 참조 -export const tags = pgTable("tags", { + .notNull() + .references(() => contractItems.id, { onDelete: "cascade" }), + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }); + + // ============ tags (각 계약 아이템에 대한 Tag) ============ + // "어느 계약의 어느 아이템에 대한 태그"임을 나타내려면 contract_items를 참조 + export const tags = pgTable("tags", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - + // 이 Tag가 속한 "계약 내 아이템" (즉 contract_items.id) contractItemId: integer("contract_item_id") - .notNull() - .references(() => contractItems.id, { onDelete: "cascade" }), - - formId: integer("form_id") - .references(() => forms.id, { onDelete: "set null" }), - + .notNull() + .references(() => contractItems.id, { onDelete: "cascade" }), + + formId: integer("form_id").references(() => forms.id, { + onDelete: "set null", + }), + tagNo: varchar("tag_no", { length: 100 }).notNull(), tagType: varchar("tag_type", { length: 50 }).notNull(), class: varchar("class", { length: 100 }).notNull(), description: text("description"), - + createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), -}) - -export type Tag = typeof tags.$inferSelect -export type Form = typeof forms.$inferSelect -export type NewTag = typeof tags.$inferInsert - -export const tagTypes = pgTable("tag_types", { + }); + + export type Tag = typeof tags.$inferSelect; + export type Form = typeof forms.$inferSelect; + export type NewTag = typeof tags.$inferInsert; + + export const tagTypes = pgTable("tag_types", { code: varchar("code", { length: 50 }).primaryKey(), description: text("description").notNull(), - - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), -}) - -export const tagSubfields = pgTable("tag_subfields", { - id: serial("id").primaryKey(), - - // 외래키: tagTypeCode -> tagTypes.code - tagTypeCode: varchar("tag_type_code", { length: 50 }) + + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }); + + export const tagSubfields = pgTable( + "tag_subfields", + { + id: serial("id").primaryKey(), + + // 외래키: tagTypeCode -> tagTypes.code + tagTypeCode: varchar("tag_type_code", { length: 50 }) .notNull() .references(() => tagTypes.code, { onDelete: "cascade" }), - - /** - * 나머지 필드 - */ - // tagTypeDescription: -> 이제 불필요. tagTypes.description로 join - attributesId: varchar("attributes_id", { length: 50 }).notNull(), - attributesDescription: text("attributes_description").notNull(), - - expression: text("expression"), - delimiter: varchar("delimiter", { length: 10 }), - - sortOrder: integer("sort_order").default(0).notNull(), - - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), -}, (table) => { - return { + + /** + * 나머지 필드 + */ + // tagTypeDescription: -> 이제 불필요. tagTypes.description로 join + attributesId: varchar("attributes_id", { length: 50 }).notNull(), + attributesDescription: text("attributes_description").notNull(), + + expression: text("expression"), + delimiter: varchar("delimiter", { length: 10 }), + + sortOrder: integer("sort_order").default(0).notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }, + (table) => { + return { uniqTagTypeAttribute: unique("uniq_tag_type_attribute").on( - table.tagTypeCode, - table.attributesId + table.tagTypeCode, + table.attributesId ), - }; -}); - -export const tagSubfieldOptions = pgTable("tag_subfield_options", { + }; + } + ); + + export const tagSubfieldOptions = pgTable("tag_subfield_options", { id: serial("id").primaryKey(), - + // 어떤 subfield에 속하는 옵션인지 attributesId: varchar("attributes_id", { length: 50 }) - .notNull() - .references(() => tagSubfields.attributesId, { onDelete: "cascade" }), - + .notNull() + .references(() => tagSubfields.attributesId, { onDelete: "cascade" }), + /** * 실제 코드 (예: "PM", "AA", "VB", "VAR", "01", "02" ...) */ code: varchar("code", { length: 50 }).notNull(), - + /** * 사용자에게 보여줄 레이블 (예: "Pump", "Pneumatic Motor", "Ball Valve", ...) */ label: text("label").notNull(), - + /** * 생성/수정 시각 (선택) */ - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), -}) - -export const tagClasses = pgTable("tag_classes", { + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }); + + export const tagClasses = pgTable("tag_classes", { id: integer("id").primaryKey().generatedAlwaysAsIdentity(), - + // 기존 code/label code: varchar("code", { length: 100 }).notNull(), label: text("label").notNull(), - + // 새 필드: tagTypeCode -> references tagTypes.code tagTypeCode: varchar("tag_type_code", { length: 50 }) - .notNull() - .references(() => tagTypes.code, { onDelete: "cascade" }), - + .notNull() + .references(() => tagTypes.code, { onDelete: "cascade" }), + createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), -}) - -export const tagTypeClassFormMappings = pgTable("tag_type_class_form_mappings", { - id: serial("id").primaryKey(), - - tagTypeLabel: varchar("tag_type_label", { length: 255 }).notNull(), - classLabel: varchar("class_label", { length: 255 }).notNull(), - - formCode: varchar("form_code", { length: 50 }).notNull(), - formName: varchar("form_name", { length: 255 }).notNull(), - - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), -}) - -export type TagTypeClassFormMappings = typeof tagTypeClassFormMappings.$inferSelect -export type TagSubfields = typeof tagSubfields.$inferSelect -export type TagSubfieldOption = typeof tagSubfieldOptions.$inferSelect -export type TagClasses = typeof tagClasses.$inferSelect - - -export const viewTagSubfields = pgTable("view_tag_subfields", { + }); + + export const tagTypeClassFormMappings = pgTable( + "tag_type_class_form_mappings", + { + id: serial("id").primaryKey(), + + tagTypeLabel: varchar("tag_type_label", { length: 255 }).notNull(), + classLabel: varchar("class_label", { length: 255 }).notNull(), + + formCode: varchar("form_code", { length: 50 }).notNull(), + formName: varchar("form_name", { length: 255 }).notNull(), + + createdAt: timestamp("created_at", { withTimezone: true }) + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + } + ); + + export type TagTypeClassFormMappings = + typeof tagTypeClassFormMappings.$inferSelect; + export type TagSubfields = typeof tagSubfields.$inferSelect; + export type TagSubfieldOption = typeof tagSubfieldOptions.$inferSelect; + export type TagClasses = typeof tagClasses.$inferSelect; + + export const viewTagSubfields = pgTable("view_tag_subfields", { id: integer("id").primaryKey(), - + tagTypeCode: varchar("tag_type_code", { length: 50 }).notNull(), tagTypeDescription: text("tag_type_description"), attributesId: varchar("attributes_id", { length: 50 }).notNull(), attributesDescription: text("attributes_description").notNull(), - + expression: text("expression"), delimiter: varchar("delimiter", { length: 10 }), sortOrder: integer("sort_order").default(0).notNull(), - + createdAt: timestamp("created_at", { withTimezone: true }), updatedAt: timestamp("updated_at", { withTimezone: true }), +<<<<<<< HEAD + }); + + export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect; + + export const vendorDataReportTemps = pgTable("vendor_data_report_temps", { +======= }) export type ViewTagSubfields = typeof viewTagSubfields.$inferSelect export const vendorDataReportTemps = pgTable("vendor_data_report_temps", { +>>>>>>> dev id: serial("id").primaryKey(), contractItemId: integer("contract_item_id") .notNull() @@ -215,6 +261,17 @@ export const vendorDataReportTemps = pgTable("vendor_data_report_temps", { fileName: varchar("file_name", { length: 255 }).notNull(), filePath: varchar("file_path", { length: 1024 }).notNull(), createdAt: timestamp("created_at", { withTimezone: true }) +<<<<<<< HEAD + .defaultNow() + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }) + .defaultNow() + .notNull(), + }); + + export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect; + +======= .defaultNow() .notNull(), updatedAt: timestamp("updated_at", { withTimezone: true }) @@ -222,4 +279,5 @@ export const vendorDataReportTemps = pgTable("vendor_data_report_temps", { .notNull(), }); - export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect;
\ No newline at end of file + export type VendorDataReportTemps = typeof vendorDataReportTemps.$inferSelect; +>>>>>>> dev diff --git a/lib/forms/services.ts b/lib/forms/services.ts index ff21626c..a1bbf003 100644 --- a/lib/forms/services.ts +++ b/lib/forms/services.ts @@ -1,6 +1,7 @@ // lib/forms/services.ts "use server"; +import { headers } from "next/headers"; import path from "path"; import fs from "fs/promises"; import { v4 as uuidv4 } from "uuid"; @@ -805,3 +806,12 @@ export async function uploadReportTemp( }); } } + +export const getOrigin = async ():Promise<string> => { + const headersList = await headers(); + const host = headersList.get("host"); + const proto = headersList.get("x-forwarded-proto") || "http"; // 기본값은 http + const origin = `${proto}://${host}`; + + return origin; +}; diff --git a/lib/pdftron/serverSDK/createReport.ts b/lib/pdftron/serverSDK/createReport.ts new file mode 100644 index 00000000..412ada87 --- /dev/null +++ b/lib/pdftron/serverSDK/createReport.ts @@ -0,0 +1,83 @@ +const { PDFNet } = require("@pdftron/pdfnet-node"); + +type CreateReport = ( + coverPage: Buffer, + reportTempPath: string, + reportDatas: { + [key: string]: any; + }[] +) => Promise<{ + result: boolean; + buffer?: ArrayBuffer; + error?: any; +}>; + +export const createReport: CreateReport = async ( + coverPage, + reportTempPath, + reportDatas +) => { + const main = async () => { + await PDFNet.initialize(process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY); + + const mainDoc = await PDFNet.PDFDoc.create(); + const buf = await PDFNet.Convert.office2PDFBuffer(coverPage); + const coverPDFDoc = await PDFNet.PDFDoc.createFromBuffer(buf); + + const options = new PDFNet.Convert.OfficeToPDFOptions(); + + await mainDoc.insertPages( + (await mainDoc.getPageCount()) + 1, + coverPDFDoc, + 1, + await coverPDFDoc.getPageCount(), + PDFNet.PDFDoc.InsertFlag.e_none + ); + + for (const reportData of reportDatas) { + const resportDataJson = JSON.stringify(reportData); + + const templateDoc = await PDFNet.Convert.createOfficeTemplateWithPath( + "public" + reportTempPath, + options + ); + + const pdfdoc = await templateDoc.fillTemplateJson(resportDataJson); + + await mainDoc.insertPages( + (await mainDoc.getPageCount()) + 1, + pdfdoc, + 1, + await pdfdoc.getPageCount(), + PDFNet.PDFDoc.InsertFlag.e_none + ); + } + + // await mainDoc.save("test1.pdf", PDFNet.SDFDoc.SaveOptions.e_linearized); + + const buffer = await mainDoc.saveMemoryBuffer( + PDFNet.SDFDoc.SaveOptions.e_linearized + ); + + return { + result: true, + buffer, + }; + }; + + const result = await PDFNet.runWithCleanup( + main, + process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY + ) + .catch((err: any) => { + return { + result: false, + error: err, + }; + }) + .then(async (data: any) => { + return data; + }); + + return result; +}; diff --git a/lib/po/service.ts b/lib/po/service.ts index f697bd58..5f2e4f35 100644 --- a/lib/po/service.ts +++ b/lib/po/service.ts @@ -324,7 +324,6 @@ Remarks:${contract.remarks}`, const { success: sendDocuSignResult, envelopeId } = sendDocuSign; - await tx .update(contracts) .set({ @@ -344,7 +343,8 @@ Remarks:${contract.remarks}`, const fileName = `${contractNo}-signature.pdf`; const ext = path.extname(fileName); const uniqueName = uuidv4() + ext; - // Create a single envelope for all signers + + // Create a single envelope for all signers const [newEnvelope] = await tx .insert(contractEnvelopes) .values({ diff --git a/pages/api/pdftron/createVendorDataReports.ts b/pages/api/pdftron/createVendorDataReports.ts index 47f6055d..f461a7fb 100644 --- a/pages/api/pdftron/createVendorDataReports.ts +++ b/pages/api/pdftron/createVendorDataReports.ts @@ -1,5 +1,8 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import type { File as FormidableFile } from "formidable"; import formidable from "formidable"; +import fs from "fs/promises"; +import { createReport } from "@/lib/pdftron/serverSDK/createReport"; export const config = { api: { @@ -15,22 +18,61 @@ export default async function handler( return res.status(405).end(); } - const form = formidable({ multiples: true }); - - form.parse(req, async (err, fields, files) => { - if (err) { - console.error(err); - return res.status(500).json({ error: "Error parsing form" }); - } - - try { - const additionalData = JSON.parse((fields?.additionalData ?? "") as string); - console.log("📦 additionalData:", additionalData); - console.log("📎 files:", files.files); // files.files는 array or single file - - return res.status(200).json({ success: true }); - } catch (e) { - return res.status(400).json({ error: "Invalid additionalData" }); - } - }); + try { + const form = formidable({ multiples: false }); + + form.parse(req, async (err, fields, files) => { + if (err) { + console.error(err); + return res.status(500).json({ error: "Error parsing form" }); + } + + try { + const fileName = fields?.customFileName?.[0] ?? ""; + const reportDatas = JSON.parse(fields?.reportDatas?.[0] ?? "[]") as { + [key: string]: any; + }[]; + const reportTempPath = fields?.reportTempPath?.[0] ?? ""; + const reportCoverPage: FormidableFile | undefined = files?.file?.[0]; + + if ( + !reportCoverPage || + fileName.length === 0 || + reportDatas.length === 0 || + reportTempPath.length === 0 + ) { + return res.status(400).json({ error: "Invalid Report Data" }); + } + + const buffer = await fs.readFile(reportCoverPage.filepath); + + const { + result, + buffer: pdfBuffer, + error, + } = await createReport(buffer, reportTempPath, reportDatas); + + if (result && pdfBuffer) { + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename="${fileName}"` + ); + + return res.send(Buffer.from(pdfBuffer)); + } + + return res.status(200).json({ + success: false, + message: "Report 생성에 실패하였습니다.", + error, + }); + } catch (e) { + console.log(e); + return res.status(400).json({ error: "Invalid additionalData" }); + } + }); + } catch (err) { + return res.status(401).end(); + } } diff --git a/pages/api/po/sendDocuSign.ts b/pages/api/po/sendDocuSign.ts index ccb83733..eb218c2c 100644 --- a/pages/api/po/sendDocuSign.ts +++ b/pages/api/po/sendDocuSign.ts @@ -5,7 +5,7 @@ export const config = { }; import type { NextApiRequest, NextApiResponse } from "next"; -import { requestContractSign } from "@/lib/docuSign/docuSignFns"; +import { requestContractSign, getRecipients } from "@/lib/docuSign/docuSignFns"; export default async function handler( req: NextApiRequest, @@ -36,11 +36,13 @@ export default async function handler( const { result, envelopeId, error } = docuSignStart; - res.status(200).json({ + const sendResult = { success: result, envelopeId, message: error?.message, - }); + }; + + res.status(200).json(sendResult); } catch (error: any) { res .status(500) diff --git a/pages/api/po/webhook.ts b/pages/api/po/webhook.ts index e61246a2..39a6931e 100644 --- a/pages/api/po/webhook.ts +++ b/pages/api/po/webhook.ts @@ -3,15 +3,11 @@ export const config = { bodyParser: true, // ✅ 이게 false면 안 됨! }, }; - import type { NextApiRequest, NextApiResponse } from "next"; import path from "path"; import fs from "fs"; import * as z from "zod"; import db from "@/db/db"; -import { GetPOSchema } from "@/lib/po/validations"; -import { unstable_cache } from "@/lib/unstable-cache"; -import { filterColumns } from "@/lib/filter-columns"; import { asc, desc, @@ -25,18 +21,15 @@ import { eq, count, } from "drizzle-orm"; -import { countPos, selectPos } from "@/lib/po/repository"; import { contractEnvelopes, - contractsDetailView, contractSigners, contracts, } from "@/db/schema/contract"; -import { vendors, vendorContacts } from "@/db/schema/vendors"; -import dayjs from "dayjs"; - -import { POContent } from "@/lib/docuSign/types"; -import { downloadContractFile, getRecipients } from "@/lib/docuSign/docuSignFns"; +import { + downloadContractFile, + getRecipients, +} from "@/lib/docuSign/docuSignFns"; export default async function handler( req: NextApiRequest, @@ -108,7 +101,7 @@ export default async function handler( await tx .update(contracts) - .set({ status: `$FAILED_${safeRole}_SEND_MAIL(${message})` }) + .set({ status: `FAILED_${safeRole}_SEND_MAIL(${message})` }) .where(eq(contracts.id, contractId)); await tx @@ -154,6 +147,10 @@ export default async function handler( .where(eq(dbSchma.envelopeId, envelopeId)) .limit(1); + if (!targetContract) { + continue; + } + const { id, contractId } = targetContract; if (contractId === null || contractId === undefined) { @@ -210,6 +207,10 @@ export default async function handler( .where(eq(dbSchma.envelopeId, envelopeId)) .limit(1); + if (!targetContract) { + continue; + } + const { id, contractId, fileName, filePath } = targetContract; if (contractId === null || contractId === undefined) { @@ -277,6 +278,10 @@ export default async function handler( .where(eq(dbSchma.envelopeId, envelopeId)) .limit(1); + if (!targetContract) { + continue; + } + const { contractId, fileName, filePath } = targetContract; if (contractId === null || contractId === undefined) { |
