summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/documents/StageList.tsx2
-rw-r--r--components/documents/view-document-dialog.tsx221
-rw-r--r--components/form-data/form-data-report-batch-dialog.tsx73
-rw-r--r--components/form-data/form-data-report-temp-upload-dialog.tsx28
-rw-r--r--components/form-data/form-data-table.tsx4
-rw-r--r--db/schema/vendorData.ts320
-rw-r--r--lib/forms/services.ts10
-rw-r--r--lib/pdftron/serverSDK/createReport.ts83
-rw-r--r--lib/po/service.ts4
-rw-r--r--pages/api/pdftron/createVendorDataReports.ts78
-rw-r--r--pages/api/po/sendDocuSign.ts8
-rw-r--r--pages/api/po/webhook.ts29
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) {