diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/forms/services.ts | 61 | ||||
| -rw-r--r-- | lib/pdftron/serverSDK/createReport.ts | 83 | ||||
| -rw-r--r-- | lib/po/service.ts | 4 | ||||
| -rw-r--r-- | lib/poa/service.ts | 132 | ||||
| -rw-r--r-- | lib/poa/table/poa-table-columns.tsx | 165 | ||||
| -rw-r--r-- | lib/poa/table/poa-table-toolbar-actions.tsx | 45 | ||||
| -rw-r--r-- | lib/poa/table/poa-table.tsx | 189 | ||||
| -rw-r--r-- | lib/poa/validations.ts | 66 |
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 |
