summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/forms/services.ts61
-rw-r--r--lib/pdftron/serverSDK/createReport.ts83
-rw-r--r--lib/po/service.ts4
-rw-r--r--lib/poa/service.ts132
-rw-r--r--lib/poa/table/poa-table-columns.tsx165
-rw-r--r--lib/poa/table/poa-table-toolbar-actions.tsx45
-rw-r--r--lib/poa/table/poa-table.tsx189
-rw-r--r--lib/poa/validations.ts66
8 files changed, 146 insertions, 599 deletions
diff --git a/lib/forms/services.ts b/lib/forms/services.ts
index ff21626c..22f10466 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,63 @@ 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;
+};
+
+export const getReportTempFileData = async (): Promise<{
+ fileName: string;
+ fileType: string;
+ base64: string;
+}> => {
+ const fileName = "sample_template_file.docx";
+
+ const tempFile = await fs.readFile(
+ `public/vendorFormReportSample/${fileName}`
+ );
+
+ return {
+ fileName,
+ fileType:
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ base64: tempFile.toString("base64"),
+ };
+};
+
+type deleteReportTempFile = (id: number) => Promise<{
+ result: boolean;
+ error?: any;
+}>;
+
+export const deleteReportTempFile: deleteReportTempFile = async (id) => {
+ try {
+ return db.transaction(async (tx) => {
+ const [targetTempFile] = await tx
+ .select()
+ .from(vendorDataReportTemps)
+ .where(eq(vendorDataReportTemps.id, id));
+
+ if (!targetTempFile) {
+ throw new Error("해당 Template File을 찾을 수 없습니다.");
+ }
+
+ await tx
+ .delete(vendorDataReportTemps)
+ .where(eq(vendorDataReportTemps.id, id));
+
+ const { filePath } = targetTempFile;
+
+ await fs.unlink("public" + filePath);
+
+ return { result: true };
+ });
+ } catch (err) {
+ return { result: false, error: (err as Error).message };
+ }
+};
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/lib/poa/service.ts b/lib/poa/service.ts
deleted file mode 100644
index a11cbdd8..00000000
--- a/lib/poa/service.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-"use server";
-
-import db from "@/db/db";
-import { GetChangeOrderSchema } from "./validations";
-import { unstable_cache } from "@/lib/unstable-cache";
-import { filterColumns } from "@/lib/filter-columns";
-import {
- asc,
- desc,
- ilike,
- and,
- or,
- count,
-} from "drizzle-orm";
-
-import {
- poaDetailView,
-} from "@/db/schema/contract";
-
-/**
- * POA 목록 조회
- */
-export async function getChangeOrders(input: GetChangeOrderSchema) {
- return unstable_cache(
- async () => {
- try {
- const offset = (input.page - 1) * input.perPage;
-
- // 1. Build where clause
- let advancedWhere;
- try {
- advancedWhere = filterColumns({
- table: poaDetailView,
- filters: input.filters,
- joinOperator: input.joinOperator,
- });
- } catch (whereErr) {
- console.error("Error building advanced where:", whereErr);
- advancedWhere = undefined;
- }
-
- let globalWhere;
- if (input.search) {
- try {
- const s = `%${input.search}%`;
- globalWhere = or(
- ilike(poaDetailView.contractNo, s),
- ilike(poaDetailView.originalContractName, s),
- ilike(poaDetailView.projectCode, s),
- ilike(poaDetailView.projectName, s),
- ilike(poaDetailView.vendorName, s)
- );
- } catch (searchErr) {
- console.error("Error building search where:", searchErr);
- globalWhere = undefined;
- }
- }
-
- // 2. Combine where clauses
- let finalWhere;
- if (advancedWhere && globalWhere) {
- finalWhere = and(advancedWhere, globalWhere);
- } else {
- finalWhere = advancedWhere || globalWhere;
- }
-
- // 3. Build order by
- let orderBy;
- try {
- orderBy =
- input.sort.length > 0
- ? input.sort.map((item) =>
- item.desc
- ? desc(poaDetailView[item.id])
- : asc(poaDetailView[item.id])
- )
- : [desc(poaDetailView.createdAt)];
- } catch (orderErr) {
- console.error("Error building order by:", orderErr);
- orderBy = [desc(poaDetailView.createdAt)];
- }
-
- // 4. Execute queries
- let data = [];
- let total = 0;
-
- try {
- const queryBuilder = db.select().from(poaDetailView);
-
- if (finalWhere) {
- queryBuilder.where(finalWhere);
- }
-
- queryBuilder.orderBy(...orderBy);
- queryBuilder.offset(offset).limit(input.perPage);
-
- data = await queryBuilder;
-
- const countBuilder = db
- .select({ count: count() })
- .from(poaDetailView);
-
- if (finalWhere) {
- countBuilder.where(finalWhere);
- }
-
- const countResult = await countBuilder;
- total = countResult[0]?.count || 0;
- } catch (queryErr) {
- console.error("Query execution failed:", queryErr);
- throw queryErr;
- }
-
- const pageCount = Math.ceil(total / input.perPage);
-
- return { data, pageCount };
- } catch (err) {
- console.error("Error in getChangeOrders:", err);
- if (err instanceof Error) {
- console.error("Error message:", err.message);
- console.error("Error stack:", err.stack);
- }
- return { data: [], pageCount: 0 };
- }
- },
- [JSON.stringify(input)],
- {
- revalidate: 3600,
- tags: [`poa`],
- }
- )();
-} \ No newline at end of file
diff --git a/lib/poa/table/poa-table-columns.tsx b/lib/poa/table/poa-table-columns.tsx
deleted file mode 100644
index b362e54c..00000000
--- a/lib/poa/table/poa-table-columns.tsx
+++ /dev/null
@@ -1,165 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type DataTableRowAction } from "@/types/table"
-import { type ColumnDef } from "@tanstack/react-table"
-import { InfoIcon, PenIcon } from "lucide-react"
-
-import { formatDate } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { POADetail } from "@/db/schema/contract"
-import { poaColumnsConfig } from "@/config/poaColumnsConfig"
-
-interface GetColumnsProps {
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<POADetail> | null>>
-}
-
-/**
- * tanstack table column definitions with nested headers
- */
-export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<POADetail>[] {
- // ----------------------------------------------------------------
- // 1) actions column (buttons for item info)
- // ----------------------------------------------------------------
- const actionsColumn: ColumnDef<POADetail> = {
- id: "actions",
- enableHiding: false,
- cell: function Cell({ row }) {
- const hasSignature = row.original.hasSignature;
-
- return (
- <div className="flex items-center space-x-1">
- {/* Item Info Button */}
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- size="icon"
- onClick={() => setRowAction({ row, type: "items" })}
- >
- <InfoIcon className="h-4 w-4" aria-hidden="true" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>
- View Item Info
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
-
- {/* Signature Request Button - only show if no signature exists */}
- {!hasSignature && (
- <TooltipProvider>
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- size="icon"
- onClick={() => setRowAction({ row, type: "signature" })}
- >
- <PenIcon className="h-4 w-4" aria-hidden="true" />
- </Button>
- </TooltipTrigger>
- <TooltipContent>
- Request Electronic Signature
- </TooltipContent>
- </Tooltip>
- </TooltipProvider>
- )}
- </div>
- );
- },
- size: 80,
- };
-
- // ----------------------------------------------------------------
- // 2) Regular columns grouped by group name
- // ----------------------------------------------------------------
- // 2-1) groupMap: { [groupName]: ColumnDef<POADetail>[] }
- const groupMap: Record<string, ColumnDef<POADetail>[]> = {};
-
- poaColumnsConfig.forEach((cfg) => {
- // Use "_noGroup" if no group is specified
- const groupName = cfg.group || "_noGroup";
-
- if (!groupMap[groupName]) {
- groupMap[groupName] = [];
- }
-
- // Child column definition
- const childCol: ColumnDef<POADetail> = {
- accessorKey: cfg.id,
- enableResizing: true,
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title={cfg.label} />
- ),
- meta: {
- excelHeader: cfg.excelHeader,
- group: cfg.group,
- type: cfg.type,
- },
- cell: ({ cell }) => {
- const value = cell.getValue();
-
- if (cfg.type === "date") {
- const dateVal = value as Date;
- return (
- <div className="text-sm">
- {formatDate(dateVal)}
- </div>
- );
- }
- if (cfg.type === "number") {
- const numVal = value as number;
- return (
- <div className="text-sm">
- {numVal ? numVal.toLocaleString() : "-"}
- </div>
- );
- }
- return (
- <div className="text-sm">
- {value ?? "-"}
- </div>
- );
- },
- };
-
- groupMap[groupName].push(childCol);
- });
-
- // ----------------------------------------------------------------
- // 2-2) Create actual parent columns (groups) from the groupMap
- // ----------------------------------------------------------------
- const nestedColumns: ColumnDef<POADetail>[] = [];
-
- // Order can be fixed by pre-defining group order or sorting
- Object.entries(groupMap).forEach(([groupName, colDefs]) => {
- if (groupName === "_noGroup") {
- // No group → Add as top-level columns
- nestedColumns.push(...colDefs);
- } else {
- // Parent column
- nestedColumns.push({
- id: groupName,
- header: groupName,
- columns: colDefs,
- });
- }
- });
-
- // ----------------------------------------------------------------
- // 3) Final column array: nestedColumns + actionsColumn
- // ----------------------------------------------------------------
- return [
- ...nestedColumns,
- actionsColumn,
- ];
-} \ No newline at end of file
diff --git a/lib/poa/table/poa-table-toolbar-actions.tsx b/lib/poa/table/poa-table-toolbar-actions.tsx
deleted file mode 100644
index 97a9cc55..00000000
--- a/lib/poa/table/poa-table-toolbar-actions.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { Download, RefreshCcw } from "lucide-react"
-
-import { exportTableToExcel } from "@/lib/export"
-import { Button } from "@/components/ui/button"
-import { POADetail } from "@/db/schema/contract"
-
-interface ItemsTableToolbarActionsProps {
- table: Table<POADetail>
-}
-
-export function PoaTableToolbarActions({ table }: ItemsTableToolbarActionsProps) {
- return (
- <div className="flex items-center gap-2">
- {/** Refresh 버튼 */}
- <Button
- variant="samsung"
- size="sm"
- className="gap-2"
- >
- <RefreshCcw className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Get POAs</span>
- </Button>
-
- {/** Export 버튼 */}
- <Button
- variant="outline"
- size="sm"
- onClick={() =>
- exportTableToExcel(table, {
- filename: "poa-list",
- excludeColumns: ["select", "actions"],
- })
- }
- className="gap-2"
- >
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">Export</span>
- </Button>
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/poa/table/poa-table.tsx b/lib/poa/table/poa-table.tsx
deleted file mode 100644
index a5cad02a..00000000
--- a/lib/poa/table/poa-table.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-"use client"
-
-import * as React from "react"
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} from "@/types/table"
-
-import { useDataTable } from "@/hooks/use-data-table"
-import { DataTable } from "@/components/data-table/data-table"
-import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-
-import { getChangeOrders } from "../service"
-import { POADetail } from "@/db/schema/contract"
-import { getColumns } from "./poa-table-columns"
-import { PoaTableToolbarActions } from "./poa-table-toolbar-actions"
-
-interface ItemsTableProps {
- promises: Promise<
- [
- Awaited<ReturnType<typeof getChangeOrders>>,
- ]
- >
-}
-
-export function ChangeOrderListsTable({ promises }: ItemsTableProps) {
- const [result] = React.use(promises)
- const { data, pageCount } = result
-
- const [rowAction, setRowAction] =
- React.useState<DataTableRowAction<POADetail> | null>(null)
-
- // Handle row actions
- React.useEffect(() => {
- if (!rowAction) return
-
- if (rowAction.type === "items") {
- // Handle items view action
- setRowAction(null)
- }
- }, [rowAction])
-
- const columns = React.useMemo(
- () => getColumns({ setRowAction }),
- [setRowAction]
- )
-
- const filterFields: DataTableFilterField<POADetail>[] = [
- {
- id: "contractNo",
- label: "계약번호",
- },
- {
- id: "originalContractName",
- label: "계약명",
- },
- {
- id: "approvalStatus",
- label: "승인 상태",
- },
- ]
-
- const advancedFilterFields: DataTableAdvancedFilterField<POADetail>[] = [
- {
- id: "contractNo",
- label: "계약번호",
- type: "text",
- },
- {
- id: "originalContractName",
- label: "계약명",
- type: "text",
- },
- {
- id: "projectId",
- label: "프로젝트 ID",
- type: "number",
- },
- {
- id: "vendorId",
- label: "벤더 ID",
- type: "number",
- },
- {
- id: "originalStatus",
- label: "상태",
- type: "text",
- },
- {
- id: "deliveryTerms",
- label: "납품조건",
- type: "text",
- },
- {
- id: "deliveryDate",
- label: "납품기한",
- type: "date",
- },
- {
- id: "deliveryLocation",
- label: "납품장소",
- type: "text",
- },
- {
- id: "currency",
- label: "통화",
- type: "text",
- },
- {
- id: "totalAmount",
- label: "총 금액",
- type: "number",
- },
- {
- id: "discount",
- label: "할인",
- type: "number",
- },
- {
- id: "tax",
- label: "세금",
- type: "number",
- },
- {
- id: "shippingFee",
- label: "배송비",
- type: "number",
- },
- {
- id: "netTotal",
- label: "최종 금액",
- type: "number",
- },
- {
- id: "changeReason",
- label: "변경 사유",
- type: "text",
- },
- {
- id: "approvalStatus",
- label: "승인 상태",
- type: "text",
- },
- {
- id: "createdAt",
- label: "생성일",
- type: "date",
- },
- {
- id: "updatedAt",
- label: "수정일",
- type: "date",
- },
- ]
-
- const { table } = useDataTable({
- data,
- columns,
- pageCount,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "createdAt", desc: true }],
- columnPinning: { right: ["actions"] },
- },
- getRowId: (originalRow) => String(originalRow.id),
- shallow: false,
- clearOnDefault: true,
- })
-
- return (
- <>
- <DataTable
- table={table}
- className="h-[calc(100vh-12rem)]"
- >
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- >
- <PoaTableToolbarActions table={table} />
- </DataTableAdvancedToolbar>
- </DataTable>
- </>
- )
-} \ No newline at end of file
diff --git a/lib/poa/validations.ts b/lib/poa/validations.ts
deleted file mode 100644
index eae1b5ab..00000000
--- a/lib/poa/validations.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import {
- createSearchParamsCache,
- parseAsArrayOf,
- parseAsInteger,
- parseAsString,
- parseAsStringEnum,
-} from "nuqs/server"
-import * as z from "zod"
-
-import { getFiltersStateParser, getSortingStateParser } from "@/lib/parsers"
-import { POADetail } from "@/db/schema/contract"
-
-export const searchParamsCache = createSearchParamsCache({
- // UI 모드나 플래그 관련
- flags: parseAsArrayOf(z.enum(["advancedTable", "floatingBar"])).withDefault([]),
-
- // 페이징
- page: parseAsInteger.withDefault(1),
- perPage: parseAsInteger.withDefault(10),
-
- // 정렬 (createdAt 기준 내림차순)
- sort: getSortingStateParser<POADetail>().withDefault([
- { id: "createdAt", desc: true },
- ]),
-
- // 원본 PO 관련
- contractNo: parseAsString.withDefault(""),
- originalContractName: parseAsString.withDefault(""),
- originalStatus: parseAsString.withDefault(""),
- originalStartDate: parseAsString.withDefault(""),
- originalEndDate: parseAsString.withDefault(""),
-
- // 프로젝트 정보
- projectId: parseAsString.withDefault(""),
- projectCode: parseAsString.withDefault(""),
- projectName: parseAsString.withDefault(""),
-
- // 벤더 정보
- vendorId: parseAsString.withDefault(""),
- vendorName: parseAsString.withDefault(""),
-
- // 납품 관련
- deliveryTerms: parseAsString.withDefault(""),
- deliveryDate: parseAsString.withDefault(""),
- deliveryLocation: parseAsString.withDefault(""),
-
- // 금액 관련
- currency: parseAsString.withDefault(""),
- totalAmount: parseAsString.withDefault(""),
- discount: parseAsString.withDefault(""),
- tax: parseAsString.withDefault(""),
- shippingFee: parseAsString.withDefault(""),
- netTotal: parseAsString.withDefault(""),
-
- // 변경 사유 및 승인 상태
- changeReason: parseAsString.withDefault(""),
- approvalStatus: parseAsString.withDefault(""),
-
- // 고급 필터(Advanced) & 검색
- filters: getFiltersStateParser().withDefault([]),
- joinOperator: parseAsStringEnum(["and", "or"]).withDefault("and"),
- search: parseAsString.withDefault(""),
-})
-
-// 최종 타입
-export type GetChangeOrderSchema = Awaited<ReturnType<typeof searchParamsCache.parse>> \ No newline at end of file