From 92ddb4f13d48cbf344dc2bf63df4457b3c713608 Mon Sep 17 00:00:00 2001 From: rlaks5757 Date: Wed, 26 Mar 2025 16:51:54 +0900 Subject: feat: report batch download 기능 완료 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/docuSign/docuSignFns.ts | 7 +- lib/forms/services.ts | 476 +++++++++++++++++++++++----------- lib/pdftron/serverSDK/createReport.ts | 83 ++++++ lib/po/service.ts | 39 ++- 4 files changed, 428 insertions(+), 177 deletions(-) create mode 100644 lib/pdftron/serverSDK/createReport.ts (limited to 'lib') diff --git a/lib/docuSign/docuSignFns.ts b/lib/docuSign/docuSignFns.ts index 87977a0b..662ff23a 100644 --- a/lib/docuSign/docuSignFns.ts +++ b/lib/docuSign/docuSignFns.ts @@ -103,6 +103,7 @@ export async function requestContractSign( let accountInfo = await authenticate(); if (accountInfo) { const { accessToken, basePath, apiAccountId } = accountInfo; + console.log({ basePath }); const { email: subEmail, name: subConName, @@ -362,11 +363,11 @@ export async function getRecipients( result: false, message: "해당 Recipient id를 가진 서명자를 찾을 수 없습니다.", }; - } + } - const { autoRespondedReason, status } = signer; + const { autoRespondedReason } = signer; - if (autoRespondedReason || status === "status") { + if (autoRespondedReason) { return { result: false, message: autoRespondedReason, diff --git a/lib/forms/services.ts b/lib/forms/services.ts index e5fc8666..a1bbf003 100644 --- a/lib/forms/services.ts +++ b/lib/forms/services.ts @@ -1,18 +1,30 @@ // lib/forms/services.ts -"use server" +"use server"; +import { headers } from "next/headers"; +import path from "path"; +import fs from "fs/promises"; +import { v4 as uuidv4 } from "uuid"; import db from "@/db/db"; -import { formEntries, formMetas, forms, tags, tagTypeClassFormMappings } from "@/db/schema/vendorData" -import { eq, and, desc, sql, DrizzleError, or } from "drizzle-orm" -import { unstable_cache } from "next/cache" -import { revalidateTag } from "next/cache" +import { + formEntries, + formMetas, + forms, + tags, + tagTypeClassFormMappings, + vendorDataReportTemps, + VendorDataReportTemps, +} from "@/db/schema/vendorData"; +import { eq, and, desc, sql, DrizzleError, or } from "drizzle-orm"; +import { unstable_cache } from "next/cache"; +import { revalidateTag } from "next/cache"; import { getErrorMessage } from "../handle-error"; import { DataTableColumnJSON } from "@/components/form-data/form-data-table-columns"; export interface FormInfo { - id: number - formCode: string - formName: string + id: number; + formCode: string; + formName: string; // tagType: string } @@ -29,7 +41,9 @@ export async function getFormsByContractItemId(contractItemId: number | null) { try { return unstable_cache( async () => { - console.log(`[Forms Service] Fetching forms for contractItemId: ${contractItemId}`); + console.log( + `[Forms Service] Fetching forms for contractItemId: ${contractItemId}` + ); try { // 데이터베이스에서 폼 조회 @@ -38,38 +52,48 @@ export async function getFormsByContractItemId(contractItemId: number | null) { id: forms.id, formCode: forms.formCode, formName: forms.formName, - // tagType: forms.tagType, + // tagType: forms.tagType, }) .from(forms) .where(eq(forms.contractItemId, contractItemId)); - console.log(`[Forms Service] Found ${formRecords.length} forms for contractItemId: ${contractItemId}`); + console.log( + `[Forms Service] Found ${formRecords.length} forms for contractItemId: ${contractItemId}` + ); // 결과가 배열인지 확인 if (!Array.isArray(formRecords)) { - getErrorMessage(`Unexpected result format for contractItemId ${contractItemId} ${formRecords}`); + getErrorMessage( + `Unexpected result format for contractItemId ${contractItemId} ${formRecords}` + ); return { forms: [] }; } return { forms: formRecords }; } catch (error) { - getErrorMessage(`Database error for contractItemId ${contractItemId}: ${error}`); + getErrorMessage( + `Database error for contractItemId ${contractItemId}: ${error}` + ); throw error; // 캐시 함수에서 에러를 던져 캐싱이 발생하지 않도록 함 } }, [cacheKey], { // 캐시 시간 단축 - revalidate: 60, // 1분으로 줄임 - tags: [cacheKey] + revalidate: 60, // 1분으로 줄임 + tags: [cacheKey], } )(); } catch (error) { - getErrorMessage(`Cache operation failed for contractItemId ${contractItemId}: ${error}`); + getErrorMessage( + `Cache operation failed for contractItemId ${contractItemId}: ${error}` + ); // 캐시 문제 시 직접 쿼리 시도 try { - console.log(`[Forms Service] Fallback: Direct query for contractItemId: ${contractItemId}`); + console.log( + `[Forms Service] Fallback: Direct query for contractItemId: ${contractItemId}` + ); const formRecords = await db .select({ @@ -83,7 +107,9 @@ export async function getFormsByContractItemId(contractItemId: number | null) { return { forms: formRecords }; } catch (dbError) { - getErrorMessage(`Fallback query failed for contractItemId ${contractItemId}:${dbError}`); + getErrorMessage( + `Fallback query failed for contractItemId ${contractItemId}:${dbError}` + ); return { forms: [] }; } } @@ -113,7 +139,7 @@ export async function revalidateForms(contractItemId: number) { */ export async function getFormData(formCode: string, contractItemId: number) { // 고유 캐시 키 (formCode + contractItemId) - const cacheKey = `form-data-${formCode}-${contractItemId}` + const cacheKey = `form-data-${formCode}-${contractItemId}`; try { // 1) unstable_cache로 전체 로직을 감싼다 @@ -126,24 +152,29 @@ export async function getFormData(formCode: string, contractItemId: number) { .from(formMetas) .where(eq(formMetas.formCode, formCode)) .orderBy(desc(formMetas.updatedAt)) - .limit(1) + .limit(1); - const meta = metaRows[0] ?? null + const meta = metaRows[0] ?? null; if (!meta) { - return { columns: null, data: [] } + return { columns: null, data: [] }; } // (2) form_entries에서 (formCode, contractItemId)에 해당하는 "가장 최신" 한 행 const entryRows = await db .select() .from(formEntries) - .where(and(eq(formEntries.formCode, formCode), eq(formEntries.contractItemId, contractItemId))) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) .orderBy(desc(formEntries.updatedAt)) - .limit(1) + .limit(1); - const entry = entryRows[0] ?? null + const entry = entryRows[0] ?? null; // columns: DB에 저장된 JSON (DataTableColumnJSON[]) - const columns = meta.columns as DataTableColumnJSON[] + const columns = meta.columns as DataTableColumnJSON[]; columns.forEach((col) => { // 이미 displayLabel이 있으면 그대로 두고, @@ -151,40 +182,44 @@ export async function getFormData(formCode: string, contractItemId: number) { // 둘 다 없으면 label만 쓴다. if (!col.displayLabel) { if (col.uom) { - col.displayLabel = `${col.label} (${col.uom})` + col.displayLabel = `${col.label} (${col.uom})`; } else { - col.displayLabel = col.label + col.displayLabel = col.label; } } - }) - + }); + // data: 만약 entry가 없거나, data가 아닌 형태면 빈 배열 - let data: Array> = [] + let data: Array> = []; if (entry) { if (Array.isArray(entry.data)) { - data = entry.data + data = entry.data; } else { - console.warn("formEntries data was not an array. Using empty array.") + console.warn( + "formEntries data was not an array. Using empty array." + ); } } - return { columns, data } + return { columns, data }; // --- 기존 로직 끝 --- }, [cacheKey], // 캐시 키 의존성 { - revalidate: 60, // 1분 캐시 - tags: [cacheKey], // 캐시 태그 + revalidate: 60, // 1분 캐시 + tags: [cacheKey], // 캐시 태그 } - )() + )(); - return result + return result; } catch (cacheError) { - console.error(`[getFormData] Cache operation failed:`, cacheError) + console.error(`[getFormData] Cache operation failed:`, cacheError); // --- fallback: 캐시 문제 시 직접 쿼리 시도 --- try { - console.log(`[getFormData] Fallback DB query for (${formCode}, ${contractItemId})`) + console.log( + `[getFormData] Fallback DB query for (${formCode}, ${contractItemId})` + ); // (1) form_metas const metaRows = await db @@ -192,24 +227,29 @@ export async function getFormData(formCode: string, contractItemId: number) { .from(formMetas) .where(eq(formMetas.formCode, formCode)) .orderBy(desc(formMetas.updatedAt)) - .limit(1) + .limit(1); - const meta = metaRows[0] ?? null + const meta = metaRows[0] ?? null; if (!meta) { - return { columns: null, data: [] } + return { columns: null, data: [] }; } // (2) form_entries const entryRows = await db .select() .from(formEntries) - .where(and(eq(formEntries.formCode, formCode), eq(formEntries.contractItemId, contractItemId))) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) .orderBy(desc(formEntries.updatedAt)) - .limit(1) + .limit(1); - const entry = entryRows[0] ?? null + const entry = entryRows[0] ?? null; - const columns = meta.columns as DataTableColumnJSON[] + const columns = meta.columns as DataTableColumnJSON[]; columns.forEach((col) => { // 이미 displayLabel이 있으면 그대로 두고, @@ -217,33 +257,34 @@ export async function getFormData(formCode: string, contractItemId: number) { // 둘 다 없으면 label만 쓴다. if (!col.displayLabel) { if (col.uom) { - col.displayLabel = `${col.label} (${col.uom})` + col.displayLabel = `${col.label} (${col.uom})`; } else { - col.displayLabel = col.label + col.displayLabel = col.label; } } - }) + }); - let data: Array> = [] + let data: Array> = []; if (entry) { if (Array.isArray(entry.data)) { - data = entry.data + data = entry.data; } else { - console.warn("formEntries data was not an array. Using empty array (fallback).") + console.warn( + "formEntries data was not an array. Using empty array (fallback)." + ); } } - return { columns, data } + return { columns, data }; } catch (dbError) { - console.error(`[getFormData] Fallback DB query failed:`, dbError) - return { columns: null, data: [] } + console.error(`[getFormData] Fallback DB query failed:`, dbError); + return { columns: null, data: [] }; } } } // export async function syncMissingTags(contractItemId: number, formCode: string) { - // // (1) forms 테이블에서 (contractItemId, formCode) 찾기 // const [formRow] = await db // .select() @@ -320,50 +361,55 @@ export async function getFormData(formCode: string, contractItemId: number) { // .where(eq(formEntries.id, entry.id)) // } - // revalidateTag(`form-data-${formCode}-${contractItemId}`); // return { createdCount } // } -export async function syncMissingTags(contractItemId: number, formCode: string) { +export async function syncMissingTags( + contractItemId: number, + formCode: string +) { // (1) Ensure there's a row in `forms` matching (contractItemId, formCode). const [formRow] = await db .select() .from(forms) .where( - and(eq(forms.contractItemId, contractItemId), eq(forms.formCode, formCode)) + and( + eq(forms.contractItemId, contractItemId), + eq(forms.formCode, formCode) + ) ) - .limit(1) + .limit(1); if (!formRow) { throw new Error( `Form not found for contractItemId=${contractItemId}, formCode=${formCode}` - ) + ); } // (2) Get all mappings from `tagTypeClassFormMappings` for this formCode. const formMappings = await db .select() .from(tagTypeClassFormMappings) - .where(eq(tagTypeClassFormMappings.formCode, formCode)) + .where(eq(tagTypeClassFormMappings.formCode, formCode)); // If no mappings are found, there's nothing to sync. if (formMappings.length === 0) { - console.log(`No mappings found for formCode=${formCode}`) - return { createdCount: 0, updatedCount: 0, deletedCount: 0 } + console.log(`No mappings found for formCode=${formCode}`); + return { createdCount: 0, updatedCount: 0, deletedCount: 0 }; } // Build a dynamic OR clause to match (tagType, class) pairs from the mappings. const orConditions = formMappings.map((m) => and(eq(tags.tagType, m.tagTypeLabel), eq(tags.class, m.classLabel)) - ) + ); // (3) Fetch all matching `tags` for the contractItemId + any of the (tagType, class) pairs. const tagRows = await db .select() .from(tags) - .where(and(eq(tags.contractItemId, contractItemId), or(...orConditions))) + .where(and(eq(tags.contractItemId, contractItemId), or(...orConditions))); // (4) Fetch (or create) a single `formEntries` row for (contractItemId, formCode). let [entry] = await db @@ -375,7 +421,7 @@ export async function syncMissingTags(contractItemId: number, formCode: string) eq(formEntries.formCode, formCode) ) ) - .limit(1) + .limit(1); if (!entry) { const [inserted] = await db @@ -385,64 +431,64 @@ export async function syncMissingTags(contractItemId: number, formCode: string) formCode, data: [], // Initialize with empty array }) - .returning() - entry = inserted + .returning(); + entry = inserted; } // entry.data는 [{ tagNumber: string, tagDescription?: string }, ...] 형태라고 가정 const existingData = entry.data as Array<{ - tagNumber: string - tagDescription?: string - }> + tagNumber: string; + tagDescription?: string; + }>; // Create a Set of valid tagNumbers from tagRows for efficient lookup - const validTagNumbers = new Set(tagRows.map(tag => tag.tagNo)) + const validTagNumbers = new Set(tagRows.map((tag) => tag.tagNo)); // Copy existing data to work with let updatedData: Array<{ - tagNumber: string - tagDescription?: string - }> = [] - - let createdCount = 0 - let updatedCount = 0 - let deletedCount = 0 + tagNumber: string; + tagDescription?: string; + }> = []; + + let createdCount = 0; + let updatedCount = 0; + let deletedCount = 0; // First, filter out items that should be deleted (not in validTagNumbers) for (const item of existingData) { if (validTagNumbers.has(item.tagNumber)) { - updatedData.push(item) + updatedData.push(item); } else { - deletedCount++ + deletedCount++; } } // (5) For each tagRow, if it's missing in updatedData, push it in. // 이미 있는 경우에도 description이 달라지면 업데이트할 수 있음. for (const tagRow of tagRows) { - const { tagNo, description } = tagRow - + const { tagNo, description } = tagRow; + // 5-1. 기존 데이터에서 tagNumber 매칭 const existingIndex = updatedData.findIndex( (item) => item.tagNumber === tagNo - ) - + ); + // 5-2. 없다면 새로 추가 if (existingIndex === -1) { updatedData.push({ tagNumber: tagNo, tagDescription: description ?? "", - }) - createdCount++ + }); + createdCount++; } else { // 5-3. 이미 있으면, description이 다를 때만 업데이트(선택 사항) - const existingItem = updatedData[existingIndex] + const existingItem = updatedData[existingIndex]; if (existingItem.tagDescription !== description) { updatedData[existingIndex] = { ...existingItem, tagDescription: description ?? "", - } - updatedCount++ + }; + updatedCount++; } } } @@ -452,13 +498,13 @@ export async function syncMissingTags(contractItemId: number, formCode: string) await db .update(formEntries) .set({ data: updatedData }) - .where(eq(formEntries.id, entry.id)) + .where(eq(formEntries.id, entry.id)); } // 캐시 무효화 등 후처리 - revalidateTag(`form-data-${formCode}-${contractItemId}`) + revalidateTag(`form-data-${formCode}-${contractItemId}`); - return { createdCount, updatedCount, deletedCount } + return { createdCount, updatedCount, deletedCount }; } /** @@ -468,10 +514,10 @@ export async function syncMissingTags(contractItemId: number, formCode: string) * 업데이트 후, revalidateTag()로 캐시 무효화. */ type UpdateResponse = { - success: boolean - message: string - data?: any -} + success: boolean; + message: string; + data?: any; +}; export async function updateFormDataInDB( formCode: string, @@ -480,12 +526,12 @@ export async function updateFormDataInDB( ): Promise { try { // 1) tagNumber로 식별 - const tagNumber = newData.tagNumber + const tagNumber = newData.tagNumber; if (!tagNumber) { return { success: false, - message: "tagNumber는 필수 항목입니다." - } + message: "tagNumber는 필수 항목입니다.", + }; } // 2) row 찾기 (단 하나) @@ -498,52 +544,52 @@ export async function updateFormDataInDB( eq(formEntries.contractItemId, contractItemId) ) ) - .limit(1) + .limit(1); if (!entries || entries.length === 0) { return { success: false, - message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, contractItemId=${contractItemId})` - } + message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, contractItemId=${contractItemId})`, + }; } - const entry = entries[0] + const entry = entries[0]; // 3) data가 배열인지 확인 if (!entry.data) { return { success: false, - message: "폼 데이터가 없습니다." - } + message: "폼 데이터가 없습니다.", + }; } - const dataArray = entry.data as Array> + const dataArray = entry.data as Array>; if (!Array.isArray(dataArray)) { return { success: false, - message: "폼 데이터가 올바른 형식이 아닙니다. 배열 형식이어야 합니다." - } + message: "폼 데이터가 올바른 형식이 아닙니다. 배열 형식이어야 합니다.", + }; } // 4) tagNumber = newData.tagNumber 항목 찾기 - const idx = dataArray.findIndex((item) => item.tagNumber === tagNumber) + const idx = dataArray.findIndex((item) => item.tagNumber === tagNumber); if (idx < 0) { return { success: false, - message: `태그 번호 "${tagNumber}"를 가진 항목을 찾을 수 없습니다.` - } + message: `태그 번호 "${tagNumber}"를 가진 항목을 찾을 수 없습니다.`, + }; } // 5) 병합 - const oldItem = dataArray[idx] + const oldItem = dataArray[idx]; const updatedItem = { ...oldItem, ...newData, tagNumber: oldItem.tagNumber, // tagNumber 변경 불가 시 유지 - } + }; - const updatedArray = [...dataArray] - updatedArray[idx] = updatedItem + const updatedArray = [...dataArray]; + updatedArray[idx] = updatedItem; // 6) DB UPDATE try { @@ -551,67 +597,70 @@ export async function updateFormDataInDB( .update(formEntries) .set({ data: updatedArray, - updatedAt: new Date() // 업데이트 시간도 갱신 + updatedAt: new Date(), // 업데이트 시간도 갱신 }) - .where(eq(formEntries.id, entry.id)) + .where(eq(formEntries.id, entry.id)); } catch (dbError) { - console.error("Database update error:", dbError) + console.error("Database update error:", dbError); if (dbError instanceof DrizzleError) { return { success: false, - message: `데이터베이스 업데이트 오류: ${dbError.message}` - } + message: `데이터베이스 업데이트 오류: ${dbError.message}`, + }; } return { success: false, - message: "데이터베이스 업데이트 중 오류가 발생했습니다." - } + message: "데이터베이스 업데이트 중 오류가 발생했습니다.", + }; } // 7) Cache 무효화 try { // 캐시 태그를 form-data-${formCode}-${contractItemId} 형태로 가정 - const cacheTag = `form-data-${formCode}-${contractItemId}` - revalidateTag(cacheTag) + const cacheTag = `form-data-${formCode}-${contractItemId}`; + revalidateTag(cacheTag); } catch (cacheError) { - console.warn("Cache revalidation warning:", cacheError) + console.warn("Cache revalidation warning:", cacheError); // 캐시 무효화는 실패해도 업데이트 자체는 성공했으므로 경고만 로그로 남김 } return { success: true, - message: '데이터가 성공적으로 업데이트되었습니다.', + message: "데이터가 성공적으로 업데이트되었습니다.", data: { tagNumber, - updatedFields: Object.keys(newData).filter(key => key !== 'tagNumber') - } - } + updatedFields: Object.keys(newData).filter( + (key) => key !== "tagNumber" + ), + }, + }; } catch (error) { // 예상치 못한 오류 처리 - console.error("Unexpected error in updateFormDataInDB:", error) + console.error("Unexpected error in updateFormDataInDB:", error); return { success: false, - message: error instanceof Error - ? `예상치 못한 오류가 발생했습니다: ${error.message}` - : "알 수 없는 오류가 발생했습니다." - } + message: + error instanceof Error + ? `예상치 못한 오류가 발생했습니다: ${error.message}` + : "알 수 없는 오류가 발생했습니다.", + }; } } // FormColumn Type (동일) export interface FormColumn { - key: string - type: string - label: string - options?: string[] + key: string; + type: string; + label: string; + options?: string[]; } interface MetadataResult { - formName: string - formCode: string - columns: FormColumn[] + formName: string; + formCode: string; + columns: FormColumn[]; } /** @@ -620,26 +669,149 @@ interface MetadataResult { * { formName, formCode, columns } 형태로 반환. * 없으면 null. */ -export async function fetchFormMetadata(formCode: string): Promise { +export async function fetchFormMetadata( + formCode: string +): Promise { try { // 기존 방식: select().from().where() const rows = await db .select() .from(formMetas) .where(eq(formMetas.formCode, formCode)) - .limit(1) + .limit(1); // rows는 배열 - const metaData = rows[0] - if (!metaData) return null + const metaData = rows[0]; + if (!metaData) return null; return { formCode: metaData.formCode, formName: metaData.formName, - columns: metaData.columns as FormColumn[] + columns: metaData.columns as FormColumn[], + }; + } catch (err) { + console.error("Error in fetchFormMetadata:", err); + return null; + } +} + +type GetReportFileList = ( + packageId: string, + formCode: string +) => Promise<{ + formId: number; +}>; + +export const getFormId: GetReportFileList = async (packageId, formCode) => { + const result: { formId: number } = { + formId: 0, + }; + try { + const [targetForm] = await db + .select() + .from(forms) + .where( + and( + eq(forms.formCode, formCode), + eq(forms.contractItemId, Number(packageId)) + ) + ); + + if (!targetForm) { + throw new Error("Not Found Target Form"); } + + const { id: formId } = targetForm; + + result.formId = formId; + } catch (err) { + } finally { + return result; + } +}; + +type getReportTempList = ( + packageId: number, + formId: number +) => Promise; + +export const getReportTempList: getReportTempList = async ( + packageId, + formId +) => { + let result: VendorDataReportTemps[] = []; + + try { + result = await db + .select() + .from(vendorDataReportTemps) + .where( + and( + eq(vendorDataReportTemps.contractItemId, packageId), + eq(vendorDataReportTemps.formId, formId) + ) + ); } catch (err) { - console.error("Error in fetchFormMetadata:", err) - return null + } finally { + return result; + } +}; + +export async function uploadReportTemp( + packageId: number, + formId: number, + formData: FormData +) { + const file = formData.get("file") as File | null; + const customFileName = formData.get("customFileName") as string; + const uploaderType = (formData.get("uploaderType") as string) || "vendor"; + + if (!["vendor", "client", "shi"].includes(uploaderType)) { + throw new Error( + `Invalid uploaderType: ${uploaderType}. Must be one of: vendor, client, shi` + ); + } + if (file && file.size > 0) { + const originalName = customFileName; + const ext = path.extname(originalName); + const uniqueName = uuidv4() + ext; + const baseDir = path.join( + process.cwd(), + "public", + "vendorFormData", + packageId.toString(), + formId.toString() + ); + + const savePath = path.join(baseDir, uniqueName); + + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + await fs.mkdir(baseDir, { recursive: true }); + + await fs.writeFile(savePath, buffer); + + return db.transaction(async (tx) => { + // 파일 정보를 테이블에 저장 + await tx + .insert(vendorDataReportTemps) + .values({ + contractItemId: packageId, + formId: formId, + fileName: originalName, + filePath: `/vendorFormData/${packageId.toString()}/${formId.toString()}/${uniqueName}`, + }) + .returning(); + }); } -} \ No newline at end of file +} + +export const getOrigin = async ():Promise => { + 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 dc398201..0653c11a 100644 --- a/lib/po/service.ts +++ b/lib/po/service.ts @@ -1,5 +1,6 @@ "use server"; - +import path from "path"; +import { v4 as uuidv4 } from "uuid"; import { headers } from "next/headers"; import db from "@/db/db"; import { GetPOSchema } from "./validations"; @@ -323,13 +324,26 @@ Remarks:${contract.remarks}`, const { success: sendDocuSignResult, envelopeId } = sendDocuSign; + await tx + .update(contracts) + .set({ + status: sendDocuSignResult ? "PENDING_SIGNATURE" : "Docu Sign Failed", + }) + .where(eq(contracts.id, validatedData.contractId)); + if (!sendDocuSignResult) { return { success: false, - message: "DocuSign 전자 서명 발송에 실패하였습니다.", + message: "DocuSign Mail 발송에 실패하였습니다.", }; } + // Update contract status to indicate pending signatures + + const fileName = `${contractNo}-signature.pdf`; + const ext = path.extname(fileName); + const uniqueName = uuidv4() + ext; + // Create a single envelope for all signers const [newEnvelope] = await tx .insert(contractEnvelopes) @@ -338,7 +352,7 @@ Remarks:${contract.remarks}`, envelopeId: envelopeId, envelopeStatus: "sent", fileName: `${contractNo}-signature.pdf`, // Required field - filePath: `/contracts/${validatedData.contractId}/signatures`, // Required field + filePath: `/contracts/${validatedData.contractId}/signatures/${uniqueName}`, // Required field // Add any other required fields based on your schema }) .returning(); @@ -368,25 +382,6 @@ Remarks:${contract.remarks}`, }); } - // Update contract status to indicate pending signatures - await tx - .update(contracts) - .set({ status: "PENDING_SIGNATURE" }) - .where(eq(contracts.id, validatedData.contractId)); - - // In a real implementation, you would send the envelope to DocuSign or similar service - // For example: - // const docusignResult = await docusignClient.createEnvelope({ - // recipients: validatedData.signers.map(signer => ({ - // email: signer.signerEmail, - // name: signer.signerName, - // recipientType: signer.signerType === "REQUESTER" ? "signer" : "cc", - // routingOrder: signer.signerType === "REQUESTER" ? 1 : 2, - // })), - // documentId: `contract-${validatedData.contractId}`, - // // other DocuSign-specific parameters - // }); - // Revalidate the path to refresh the data revalidatePath("/po"); -- cgit v1.2.3 From c4c3f12b4a6d1a3c09b797e1a160747cac8761e9 Mon Sep 17 00:00:00 2001 From: rlaks5757 Date: Fri, 28 Mar 2025 11:27:25 +0900 Subject: template file download 개발 완료 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../form-data/form-data-report-batch-dialog.tsx | 13 +- components/form-data/form-data-report-dialog.tsx | 139 ++------- .../form-data-report-temp-upload-dialog.tsx | 340 ++++++++++++++++++--- components/form-data/form-data-table.tsx | 47 +-- db/schema/vendorData.ts | 19 -- lib/forms/services.ts | 53 +++- .../sample_template_file.docx | Bin 0 -> 24773 bytes 7 files changed, 399 insertions(+), 212 deletions(-) create mode 100644 public/vendorFormReportSample/sample_template_file.docx (limited to 'lib') diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx index 614f890e..6c690363 100644 --- a/components/form-data/form-data-report-batch-dialog.tsx +++ b/components/form-data/form-data-report-batch-dialog.tsx @@ -8,8 +8,10 @@ import React, { useEffect, } from "react"; import { useToast } from "@/hooks/use-toast"; +import { toast as toastMessage} from "sonner"; import prettyBytes from "pretty-bytes"; import { X, Loader2 } from "lucide-react"; +import { saveAs } from 'file-saver'; import { Badge } from "@/components/ui/badge"; import { Dialog, @@ -159,15 +161,14 @@ export const FormDataReportBatchDialog: FC = ({ 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); + + saveAs(blob, `${formCode}.pdf`); + + toastMessage.success("Report 다운로드 완료!") } else { const err = await reqeustCreateReport.json(); console.error("에러:", err); + throw new Error(err.message) } } catch (err) { console.error(err); diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx index deb0873b..e28b4345 100644 --- a/components/form-data/form-data-report-dialog.tsx +++ b/components/form-data/form-data-report-dialog.tsx @@ -8,11 +8,10 @@ import React, { useEffect, useRef, } from "react"; -import { WebViewerInstance, Core } from "@pdftron/webviewer"; -import { useToast } from "@/hooks/use-toast"; -import prettyBytes from "pretty-bytes"; -import { X, Loader2 } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; +import { WebViewerInstance } from "@pdftron/webviewer"; +import { Loader2 } from "lucide-react"; +import { saveAs } from "file-saver"; +import { toast } from "sonner"; import { Dialog, DialogContent, @@ -25,11 +24,11 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, - SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; + import { Button } from "@/components/ui/button"; import { getReportTempList } from "@/lib/forms/services"; import { DataTableColumnJSON } from "./form-data-table-columns"; @@ -58,7 +57,9 @@ export const FormDataReportDialog: FC = ({ setReportData, packageId, formId, + formCode, }) => { + const [tempList, setTempList] = useState([]); const [selectTemp, setSelectTemp] = useState(""); const [instance, setInstance] = useState(null); @@ -92,46 +93,9 @@ export const FormDataReportDialog: FC = ({ // }, }); - const blob = new Blob([fileData], { - type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - }); + saveAs(new Blob([fileData]), fileName); - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = fileName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - // const allTabs = UI.TabManager.getAllTabs() as { - // id: number; - // src: Core.Document; - // }[]; - - // for (const tab of allTabs) { - // // await UI.TabManager.setActiveTab(tab.id); - // await activateTabAndWaitForLoad(instance, tab.id); - // const tabDoc = tab.src; - // const fileName = tabDoc.getFilename(); - - // const fileData = await tabDoc.getFileData({ - // includeAnnotations: true, - // }); - - // console.log({ fileData }); - - // const blob = new Blob([fileData], { - // type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - // }); - - // // 다운로드 - // // const link = document.createElement("a"); - // // link.href = URL.createObjectURL(blob); - // // link.download = fileName; - // // document.body.appendChild(link); - // // link.click(); - // // document.body.removeChild(link); - // } + toast.success("Report 다운로드 완료!"); } }; @@ -175,6 +139,7 @@ export const FormDataReportDialog: FC = ({ instance={instance} setInstance={setInstance} setFileLoading={setFileLoading} + formCode={formCode} /> @@ -195,6 +160,7 @@ interface ReportWebViewerProps { instance: null | WebViewerInstance; setInstance: Dispatch>; setFileLoading: Dispatch>; + formCode: string; } const ReportWebViewer: FC = ({ @@ -204,6 +170,7 @@ const ReportWebViewer: FC = ({ instance, setInstance, setFileLoading, + formCode, }) => { const [viwerLoading, setViewerLoading] = useState(true); const viewer = useRef(null); @@ -234,12 +201,6 @@ const ReportWebViewer: FC = ({ viewer.current as HTMLDivElement ).then(async (instance: WebViewerInstance) => { setInstance(instance); - // //Tab 메뉴 사용 필요시 활성화 - // instance.UI.enableFeatures([instance.UI.Feature.MultiTab]); - // instance.UI.disableElements([ - // "addTabButton", - // "multiTabsEmptyPage", - // ]); setViewerLoading(false); }); }); @@ -262,9 +223,10 @@ const ReportWebViewer: FC = ({ instance, reportDatas, reportTempPath, - setFileLoading + setFileLoading, + formCode ); - }, [reportTempPath, reportDatas, instance, columnsJSON]); + }, [reportTempPath, reportDatas, instance, columnsJSON, formCode]); return (
@@ -319,7 +281,8 @@ type ImportReportData = ( instance: null | WebViewerInstance, reportDatas: ReportData[], reportTempPath: string, - setFileLoading: Dispatch> + setFileLoading: Dispatch>, + formCode: string ) => void; const importReportData: ImportReportData = async ( @@ -327,7 +290,8 @@ const importReportData: ImportReportData = async ( instance, reportDatas, reportTempPath, - setFileLoading + setFileLoading, + formCode ) => { setFileLoading(true); try { @@ -352,12 +316,13 @@ const importReportData: ImportReportData = async ( }); const doc = await createDocument(reportFileBlob, { + filename: `${formCode}_report.docx`, extension: "docx", }); await doc.applyTemplateValues(reportValueMapping); - documentViewer.loadDocument(doc, { + documentViewer.loadDocument(doc, { extension: "docx", enableOfficeEditing: true, officeOptions: { @@ -373,68 +338,6 @@ const importReportData: ImportReportData = async ( } }; -const importReportDataTab: ImportReportData = async ( - columnJSON, - instance, - reportDatas, - reportTempPath, - setFileLoading -) => { - setFileLoading(true); - try { - if (instance && reportDatas.length > 0 && reportTempPath.length > 0) { - const { UI, Core } = instance; - const { createDocument } = Core; - - const getFileData = await fetch(reportTempPath); - const reportFileBlob = await getFileData.blob(); - - const prevTab = UI.TabManager.getAllTabs(); - - (prevTab as object[] as { id: number }[]).forEach((c) => { - const { id } = c; - UI.TabManager.deleteTab(id); - }); - - const fileOptions = reportDatas.map((c) => { - const { tagNumber } = c; - - const options = { - filename: `${tagNumber}_report.docx`, - }; - - return { options, reportData: c }; - }); - - const tabIds = []; - - for (const fileOption of fileOptions) { - let doc = await createDocument(reportFileBlob, { - ...fileOption.options, - extension: "docx", - }); - - await doc.applyTemplateValues( - stringifyAllValues(fileOption.reportData) - ); - - const tab = await UI.TabManager.addTab(doc, { - ...fileOption.options, - }); - - tabIds.push(tab); // 탭 ID 저장 - } - - if (tabIds.length > 0) { - await UI.TabManager.setActiveTab(tabIds[0]); - } - } - } catch (err) { - } finally { - setFileLoading(false); - } -}; - type UpdateReportTempList = ( packageId: number, formId: number, 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 413c1e51..69df704e 100644 --- a/components/form-data/form-data-report-temp-upload-dialog.tsx +++ b/components/form-data/form-data-report-temp-upload-dialog.tsx @@ -8,8 +8,11 @@ import React, { useEffect, } from "react"; import { useToast } from "@/hooks/use-toast"; +import { toast as toastMessage } from "sonner"; import prettyBytes from "pretty-bytes"; -import { X, Loader2, Download } from "lucide-react"; +import { X, Loader2, Download, Delete, Trash2 } from "lucide-react"; +import ExcelJS from "exceljs"; +import { saveAs } from "file-saver"; import { Badge } from "@/components/ui/badge"; import { Dialog, @@ -40,10 +43,28 @@ import { FileListItem, FileListName, } from "@/components/ui/file-list"; -import { getReportTempList, uploadReportTemp } from "@/lib/forms/services"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { + getReportTempList, + uploadReportTemp, + getReportTempFileData, + deleteReportTempFile, +} from "@/lib/forms/services"; import { VendorDataReportTemps } from "@/db/schema/vendorData"; +import { DataTableColumnJSON } from "./form-data-table-columns"; interface FormDataReportTempUploadDialogProps { + columnsJSON: DataTableColumnJSON[]; open: boolean; setOpen: Dispatch>; packageId: number; @@ -57,7 +78,15 @@ const MAX_FILE_SIZE = 3000000; export const FormDataReportTempUploadDialog: FC< FormDataReportTempUploadDialogProps -> = ({ open, setOpen, packageId, formId, uploaderType }) => { +> = ({ + columnsJSON, + open, + setOpen, + packageId, + formId, + formCode, + uploaderType, +}) => { const { toast } = useToast(); const [selectedFiles, setSelectedFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); @@ -131,29 +160,106 @@ export const FormDataReportTempUploadDialog: FC< } }; + const downloadTempFile = async () => { + try { + const { fileName, fileType, base64 } = await getReportTempFileData(); + + saveAs(`data:${fileType};base64,${base64}`, fileName); + + toastMessage.success("Report Sample File 다운로드 완료!"); + } catch (err) { + console.log(err); + toast({ + title: "Error", + description: "Sample File을 찾을 수가 없습니다.", + variant: "destructive", + }); + } + }; + + const downloadReportVarList = async () => { + try { + // Create a new workbook + const workbook = new ExcelJS.Workbook(); + + // 데이터 시트 생성 + const worksheet = workbook.addWorksheet("Data"); + + // 유효성 검사용 숨김 시트 생성 + const validationSheet = workbook.addWorksheet("ValidationData"); + validationSheet.state = "hidden"; // 시트 숨김 처리 + + // 1. 데이터 시트에 헤더 추가 + const headers = ["Table Column Label", "Report Variable"]; + worksheet.addRow(headers); + + // 헤더 스타일 적용 + const headerRow = worksheet.getRow(1); + headerRow.font = { bold: true }; + headerRow.alignment = { horizontal: "center" }; + headerRow.eachCell((cell) => { + cell.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFCCCCCC" }, + }; + }); + + // 2. 데이터 행 추가 + columnsJSON.forEach((row) => { + const { displayLabel, label } = row; + + const labelConvert = label.replaceAll(" ", "_"); + + worksheet.addRow([displayLabel, labelConvert]); + }); + + // 3. 컬럼 너비 자동 조정 + headers.forEach((col, idx) => { + const column = worksheet.getColumn(idx + 1); + + // 최적 너비 계산 + let maxLength = col.length; + columnsJSON.forEach((row) => { + const valueKey = idx === 0 ? "displayLabel" : "label"; + + const value = row[valueKey]; + if (value !== undefined && value !== null) { + const valueLength = String(value).length; + if (valueLength > maxLength) { + maxLength = valueLength; + } + } + }); + + // 너비 설정 (최소 10, 최대 50) + column.width = Math.min(Math.max(maxLength + 2, 10), 50); + }); + + const buffer = await workbook.xlsx.writeBuffer(); + saveAs(new Blob([buffer]), `${formCode}_report_varible_list.xlsx`); + toastMessage.success("Report Varible List File 다운로드 완료!"); + } catch (err) { + console.log(err); + toast({ + title: "Error", + description: "Variable List 파일을 찾을 수가 없습니다.", + variant: "destructive", + }); + } + }; + return ( Report Template Upload - 사용하시고자 하는 Report Template(docx File)를 업로드 하여주시기 + 사용하시고자 하는 Report Template(.docx)를 업로드 하여주시기 바랍니다. - {/* {prevReportTemp.length > 0 && ( - <> - - - {prevReportTemp.map((c, i) => { - return
{i}
; - })} -
- - )} */} -
+
@@ -163,10 +269,19 @@ export const FormDataReportTempUploadDialog: FC< sample_template_file.docx - removeFile(index)} - // disabled={isUploading} - > + + + Download + + + + + + + + report_variable_list.xlsx + + Download @@ -174,36 +289,48 @@ export const FormDataReportTempUploadDialog: FC<
+
+ + + updateReportTempList(packageId, formId, setPrevReportTemp) + } + /> +
- - {({ maxSize }) => ( - <> - - -
- -
- 파일을 여기에 드롭하세요 - - 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "} - {maxSize ? prettyBytes(maxSize) : "무제한"} - +
+ + + {({ maxSize }) => ( + <> + + +
+ +
+ 파일을 여기에 드롭하세요 + + 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "} + {maxSize ? prettyBytes(maxSize) : "무제한"} + +
-
- - - - )} - + + + + )} + +
{selectedFiles.length > 0 && (
@@ -247,7 +374,7 @@ const UploadFileItem: FC = ({ isUploading, }) => { return ( - + {selectedFiles.map((file, index) => ( @@ -327,3 +454,116 @@ const updateReportTempList: UpdateReportTempList = async ( const tempList = await getReportTempList(packageId, formId); setPrevReportTemp(tempList); }; + +interface UploadedTempFiles { + prevReportTemp: VendorDataReportTemps[]; + updateReportTempList: () => void; +} + +const UploadedTempFiles: FC = ({ + prevReportTemp, + updateReportTempList, +}) => { + const { toast } = useToast(); + + const downloadTempFile = async (fileName: string, filePath: string) => { + try { + const getTempFile = await fetch(filePath); + + if (getTempFile.ok) { + const blob = await getTempFile.blob(); + + saveAs(blob, fileName); + + toastMessage.success("Report 다운로드 완료!"); + } else { + const err = await getTempFile.json(); + console.error("에러:", err); + throw new Error(err.message); + } + + toastMessage.success("Template File 다운로드 완료!"); + } catch (err) { + console.error(err) + toast({ + title: "Error", + description: "Template File 다운로드 중 오류가 발생했습니다.", + variant: "destructive", + }); + } + }; + + const deleteTempFile = async (id: number) => { + try { + const { result, error } = await deleteReportTempFile(id); + + if (result) { + updateReportTempList(); + toastMessage.success("Template File 삭제 완료!"); + } else { + throw new Error(error); + } + } catch (err) { + toast({ + title: "Error", + description: "Template File 삭제 중 오류가 발생했습니다.", + variant: "destructive", + }); + } + }; + + return ( + + + {prevReportTemp.map((c) => { + const { fileName, filePath, id } = c; + + return ( + + + + + + {fileName} + + { + downloadTempFile(fileName, filePath); + }} + > + + Download + + + + + Delete + + + + + + Report Templete File({fileName})을 삭제하시겠습니까? + + + + + 취소 + { + deleteTempFile(id); + }} + > + 삭제 + + + + + + + ); + })} + + + ); +}; diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx index 50c4f267..823416c1 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -11,19 +11,22 @@ import { DataTableColumnJSON, ColumnType, } from "./form-data-table-columns"; - import type { DataTableAdvancedFilterField } from "@/types/table"; import { Button } from "../ui/button"; import { Download, Loader, Save, Upload } from "lucide-react"; import { toast } from "sonner"; import { syncMissingTags, updateFormDataInDB } from "@/lib/forms/services"; import { UpdateTagSheet } from "./update-form-sheet"; - import ExcelJS from "exceljs"; import { saveAs } from "file-saver"; import { FormDataReportTempUploadDialog } from "./form-data-report-temp-upload-dialog"; import { FormDataReportDialog } from "./form-data-report-dialog"; import { FormDataReportBatchDialog } from "./form-data-report-batch-dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; interface GenericData { [key: string]: any; @@ -526,20 +529,29 @@ export default function DynamicTable({ {/* 버튼 그룹 */}
{/* 태그 불러오기 버튼 */} - - + + + + + + + + + - - - View Item Info - - - - - {/* Signature Request Button - only show if no signature exists */} - {!hasSignature && ( - - - - - - - Request Electronic Signature - - - - )} -
- ); - }, - size: 80, - }; - - // ---------------------------------------------------------------- - // 2) Regular columns grouped by group name - // ---------------------------------------------------------------- - // 2-1) groupMap: { [groupName]: ColumnDef[] } - const groupMap: Record[]> = {}; - - 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 = { - accessorKey: cfg.id, - enableResizing: true, - header: ({ column }) => ( - - ), - 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 ( -
- {formatDate(dateVal)} -
- ); - } - if (cfg.type === "number") { - const numVal = value as number; - return ( -
- {numVal ? numVal.toLocaleString() : "-"} -
- ); - } - return ( -
- {value ?? "-"} -
- ); - }, - }; - - groupMap[groupName].push(childCol); - }); - - // ---------------------------------------------------------------- - // 2-2) Create actual parent columns (groups) from the groupMap - // ---------------------------------------------------------------- - const nestedColumns: ColumnDef[] = []; - - // 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 -} - -export function PoaTableToolbarActions({ table }: ItemsTableToolbarActionsProps) { - return ( -
- {/** Refresh 버튼 */} - - - {/** Export 버튼 */} - -
- ) -} \ 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>, - ] - > -} - -export function ChangeOrderListsTable({ promises }: ItemsTableProps) { - const [result] = React.use(promises) - const { data, pageCount } = result - - const [rowAction, setRowAction] = - React.useState | 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[] = [ - { - id: "contractNo", - label: "계약번호", - }, - { - id: "originalContractName", - label: "계약명", - }, - { - id: "approvalStatus", - label: "승인 상태", - }, - ] - - const advancedFilterFields: DataTableAdvancedFilterField[] = [ - { - 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 ( - <> - - - - - - - ) -} \ 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().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> \ No newline at end of file -- cgit v1.2.3 From 54a8a851442e675afb13669509cd44ab33386987 Mon Sep 17 00:00:00 2001 From: rlaks5757 Date: Fri, 28 Mar 2025 14:04:38 +0900 Subject: features merge / upload 문구 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/form-data/form-data-report-batch-dialog.tsx | 2 +- components/form-data/form-data-table.tsx | 1 - lib/docuSign/docuSignFns.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx index 6c690363..f4efde64 100644 --- a/components/form-data/form-data-report-batch-dialog.tsx +++ b/components/form-data/form-data-report-batch-dialog.tsx @@ -213,7 +213,7 @@ export const FormDataReportBatchDialog: FC = ({
- +