diff options
| author | dtsdujinkim <141381630+dtsdujinkim@users.noreply.github.com> | 2025-03-27 11:58:14 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-27 11:58:14 +0900 |
| commit | a2bb2de8aa7534b7b89993c395808b4b2b0b9f5d (patch) | |
| tree | 932095e15eedbcf726b407470b98e6e332256a8b /lib/forms/services.ts | |
| parent | 2ca4c91514feadb5edd0c9411670c7d9964d21e3 (diff) | |
| parent | 23db698279eb8ea5f73f678ce6deb93267c4705e (diff) | |
Merge pull request #3 from DTS-Development/kiman
Vendor Data Report Viewer, PO DocuSign
Diffstat (limited to 'lib/forms/services.ts')
| -rw-r--r-- | lib/forms/services.ts | 466 |
1 files changed, 314 insertions, 152 deletions
diff --git a/lib/forms/services.ts b/lib/forms/services.ts index e5fc8666..ff21626c 100644 --- a/lib/forms/services.ts +++ b/lib/forms/services.ts @@ -1,18 +1,29 @@ // lib/forms/services.ts -"use server" +"use server"; +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 +40,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 +51,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 +106,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 +138,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 +151,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 +181,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<Record<string, any>> = [] + let data: Array<Record<string, any>> = []; 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 +226,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 +256,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<Record<string, any>> = [] + let data: Array<Record<string, any>> = []; 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 +360,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 +420,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 +430,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 +497,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 +513,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 +525,12 @@ export async function updateFormDataInDB( ): Promise<UpdateResponse> { try { // 1) tagNumber로 식별 - const tagNumber = newData.tagNumber + const tagNumber = newData.tagNumber; if (!tagNumber) { return { success: false, - message: "tagNumber는 필수 항목입니다." - } + message: "tagNumber는 필수 항목입니다.", + }; } // 2) row 찾기 (단 하나) @@ -498,52 +543,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<Record<string, any>> + const dataArray = entry.data as Array<Record<string, any>>; 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 +596,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 +668,140 @@ interface MetadataResult { * { formName, formCode, columns } 형태로 반환. * 없으면 null. */ -export async function fetchFormMetadata(formCode: string): Promise<MetadataResult | null> { +export async function fetchFormMetadata( + formCode: string +): Promise<MetadataResult | null> { 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) { - console.error("Error in fetchFormMetadata:", err) - return null + } finally { + return result; } -}
\ No newline at end of file +}; + +type getReportTempList = ( + packageId: number, + formId: number +) => Promise<VendorDataReportTemps[]>; + +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) { + } 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(); + }); + } +} |
