diff options
Diffstat (limited to 'lib/forms-plant')
| -rw-r--r-- | lib/forms-plant/sedp-actions.ts | 222 | ||||
| -rw-r--r-- | lib/forms-plant/services.ts | 2076 | ||||
| -rw-r--r-- | lib/forms-plant/stat.ts | 375 |
3 files changed, 2673 insertions, 0 deletions
diff --git a/lib/forms-plant/sedp-actions.ts b/lib/forms-plant/sedp-actions.ts new file mode 100644 index 00000000..4883a33f --- /dev/null +++ b/lib/forms-plant/sedp-actions.ts @@ -0,0 +1,222 @@ +"use server"; + +import { getSEDPToken } from "@/lib/sedp/sedp-token"; + +interface SEDPTagData { + [tableName: string]: Array<{ + TAG_NO: string; + TAG_DESC: string; + ATTRIBUTES: Array<{ + ATT_ID: string; + VALUE: string; + }>; + }>; +} + +interface SEDPTemplateData { + templateId: string; + content: string; + projectNo: string; + regTypeId: string; + [key: string]: any; +} + +// ๐ ์ค์ SEDP API ์๋ต ๊ตฌ์กฐ (๋๋ฌธ์) +interface SEDPTemplateResponse { + TMPL_ID: string; + NAME: string; + TMPL_TYPE: string; + SPR_LST_SETUP?: { + ACT_SHEET: string; + HIDN_SHEETS: string[]; + CONTENT?: string; + DATA_SHEETS: Array<{ + SHEET_NAME: string; + REG_TYPE_ID: string; + MAP_CELL_ATT: Array<{ + ATT_ID: string; + IN: string; + }>; + }>; + }; + GRD_LST_SETUP?: { + REG_TYPE_ID: string; + SPR_ITM_IDS: string[]; + ATTS: any[]; + }; + SPR_ITM_LST_SETUP?: { + ACT_SHEET: string; + HIDN_SHEETS: string[]; + CONTENT?: string; + DATA_SHEETS: Array<{ + SHEET_NAME: string; + REG_TYPE_ID: string; + MAP_CELL_ATT: Array<{ + ATT_ID: string; + IN: string; + }>; + }>; + }; + [key: string]: any; +} + +/** + * SEDP์์ ํ๊ทธ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์๋ฒ ์ก์
+ */ +export async function fetchTagDataFromSEDP( + projectCode: string, + formCode: string +): Promise<SEDPTagData> { + try { + // Get the token + const apiKey = await getSEDPToken(); + + // Define the API base URL + const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; + + // Make the API call + const response = await fetch( + `${SEDP_API_BASE_URL}/Data/GetPubData`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ProjectNo: projectCode, + REG_TYPE_ID: formCode, + ContainDeleted: false + }) + } + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`); + } + + const data = await response.json(); + return data as SEDPTagData; + } catch (error: unknown) { + console.error('Error calling SEDP API:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(`Failed to fetch data from SEDP API: ${errorMessage}`); + } +} + +/** + * SEDP์์ ํ
ํ๋ฆฟ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์๋ฒ ์ก์
+ */ +export async function fetchTemplateFromSEDP( + projectCode: string, + formCode: string +): Promise<SEDPTemplateResponse[]> { + try { + // Get the token + const apiKey = await getSEDPToken(); + + // Define the API base URL + const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; + + const responseAdapter = await fetch( + `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ProjectNo: projectCode, + "TOOL_ID": "eVCP" + }) + } + ); + + if (!responseAdapter.ok) { + throw new Error(`์ ๋ ์ง์คํฐ ์์ฒญ ์คํจ: ${responseAdapter.status} ${responseAdapter.statusText}`); + } + + const dataAdapter = await responseAdapter.json(); + const templateList = dataAdapter.find(v => v.REG_TYPE_ID === formCode)?.MAP_TMPLS || []; + + // ๊ฐ TMPL_ID์ ๋ํด API ํธ์ถ + const templatePromises = templateList.map(async (tmplId: string) => { + const response = await fetch( + `${SEDP_API_BASE_URL}/Template/GetByID`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + WithContent: true, + ProjectNo: projectCode, + TMPL_ID: tmplId + }) + } + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`SEDP Template API request failed for TMPL_ID ${tmplId}: ${response.status} ${response.statusText} - ${errorText}`); + } + + const data = await response.json(); + + // ๐ API ์๋ต ๋ฐ์ดํฐ ๊ตฌ์กฐ ํ์ธ ๋ฐ ๋ก๊น
+ console.log('๐ SEDP Template API Response for', tmplId, ':', { + hasTMPL_ID: !!data.TMPL_ID, + hasTemplateId: !!(data as any).templateId, + keys: Object.keys(data), + sample: data + }); + + // ๐ TMPL_ID ํ๋ ๊ฒ์ฆ + if (!data.TMPL_ID) { + console.error('โ Missing TMPL_ID in API response:', data); + // templateId๊ฐ ์๋ค๋ฉด ๋ณํ ์๋ + if ((data as any).templateId) { + console.warn('โ ๏ธ Found templateId instead of TMPL_ID, converting...'); + data.TMPL_ID = (data as any).templateId; + } + } + + return data as SEDPTemplateResponse; + }); + + // ๋ชจ๋ API ํธ์ถ์ ๋ณ๋ ฌ๋ก ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์์ง + const templates = await Promise.all(templatePromises); + + // ๐ null์ด๋ undefined๊ฐ ์๋ ๊ฐ๋ค๋ง ํํฐ๋งํ๊ณ TMPL_ID ๊ฒ์ฆ + const validTemplates = templates.filter(template => { + if (!template) { + console.warn('โ ๏ธ Null or undefined template received'); + return false; + } + if (!template.TMPL_ID) { + console.error('โ Template missing TMPL_ID:', template); + return false; + } + return true; + }); + + console.log(`โ
fetchTemplateFromSEDP completed: ${validTemplates.length} valid templates`); + validTemplates.forEach(t => console.log(` - ${t.TMPL_ID}: ${t.NAME} (${t.TMPL_TYPE})`)); + + return validTemplates; + + } catch (error: unknown) { + console.error('Error calling SEDP Template API:', error); + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + throw new Error(`Failed to fetch template from SEDP API: ${errorMessage}`); + } +}
\ No newline at end of file diff --git a/lib/forms-plant/services.ts b/lib/forms-plant/services.ts new file mode 100644 index 00000000..99e7c35b --- /dev/null +++ b/lib/forms-plant/services.ts @@ -0,0 +1,2076 @@ +// 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"; +import db from "@/db/db"; +import { + formEntries, + formMetas, + forms, + tagClassAttributes, + tagClasses, + tags, + tagSubfieldOptions, + tagSubfields, + tagTypeClassFormMappings, + tagTypes, + vendorDataReportTemps, + VendorDataReportTemps, +} from "@/db/schema/vendorData"; +import { eq, and, desc, sql, DrizzleError, inArray, or, type SQL, type InferSelectModel } 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"; +import { contractItems, contracts, items, projects } from "@/db/schema"; +import { getSEDPToken } from "../sedp/sedp-token"; +import { decryptWithServerAction } from "@/components/drm/drmUtils"; +import { deleteFile, saveFile } from "@/lib/file-stroage"; + + +export type FormInfo = InferSelectModel<typeof forms>; + +export async function getFormsByContractItemId( + contractItemId: number | null, + mode: "ENG" | "IM" | "ALL" = "ALL" +): Promise<{ forms: FormInfo[] }> { + // ์ ํจ์ฑ ๊ฒ์ฌ + if (!contractItemId || contractItemId <= 0) { + console.warn(`Invalid contractItemId: ${contractItemId}`); + return { forms: [] }; + } + + // ๊ณ ์ ์บ์ ํค (๋ชจ๋ ํฌํจ) + const cacheKey = `forms-${contractItemId}-${mode}`; + + try { + // return unstable_cache( + // async () => { + // console.log( + // `[Forms Service] Fetching forms for contractItemId: ${contractItemId}, mode: ${mode}` + // ); + + try { + // ์ฟผ๋ฆฌ ์์ฑ + let query = db.select().from(forms).where(eq(forms.contractItemId, contractItemId)); + + // ๋ชจ๋์ ๋ฐ๋ฅธ ์ถ๊ฐ ํํฐ + if (mode === "ENG") { + query = db.select().from(forms).where( + and( + eq(forms.contractItemId, contractItemId), + eq(forms.eng, true) + ) + ); + } else if (mode === "IM") { + query = db.select().from(forms).where( + and( + eq(forms.contractItemId, contractItemId), + eq(forms.im, true) + ) + ); + } + + // ์ฟผ๋ฆฌ ์คํ + const formRecords = await query; + + console.log( + `[Forms Service] Found ${formRecords.length} forms for contractItemId: ${contractItemId}, mode: ${mode}` + ); + + return { forms: formRecords }; + } catch (error) { + getErrorMessage( + `Database error for contractItemId ${contractItemId}, mode: ${mode}: ${error}` + ); + throw error; // ์บ์ ํจ์์์ ์๋ฌ๋ฅผ ๋์ ธ ์บ์ฑ์ด ๋ฐ์ํ์ง ์๋๋ก ํจ + } + // }, + // [cacheKey], + // { + // // ์บ์ ์๊ฐ ๋จ์ถ + // revalidate: 60, // 1๋ถ์ผ๋ก ์ค์ + // tags: [cacheKey], + // } + // )(); + } catch (error) { + getErrorMessage( + `Cache operation failed for contractItemId ${contractItemId}, mode: ${mode}: ${error}` + ); + + // ์บ์ ๋ฌธ์ ์ ์ง์ ์ฟผ๋ฆฌ ์๋ + try { + console.log( + `[Forms Service] Fallback: Direct query for contractItemId: ${contractItemId}, mode: ${mode}` + ); + + // ์ฟผ๋ฆฌ ์์ฑ + let query = db.select().from(forms).where(eq(forms.contractItemId, contractItemId)); + + // ๋ชจ๋์ ๋ฐ๋ฅธ ์ถ๊ฐ ํํฐ + if (mode === "ENG") { + query = db.select().from(forms).where( + and( + eq(forms.contractItemId, contractItemId), + eq(forms.eng, true) + ) + ); + } else if (mode === "IM") { + query = db.select().from(forms).where( + and( + eq(forms.contractItemId, contractItemId), + eq(forms.im, true) + ) + ); + } + + // ์ฟผ๋ฆฌ ์คํ + const formRecords = await query; + + return { forms: formRecords }; + } catch (dbError) { + getErrorMessage( + `Fallback query failed for contractItemId ${contractItemId}, mode: ${mode}: ${dbError}` + ); + return { forms: [] }; + } + } +} + +/** + * ํผ ์บ์๋ฅผ ๊ฐฑ์ ํ๋ ์๋ฒ ์ก์
+ */ +export async function revalidateForms(contractItemId: number) { + if (!contractItemId) return; + + const cacheKey = `forms-${contractItemId}`; + console.log(`[Forms Service] Invalidating cache for ${cacheKey}`); + + try { + revalidateTag(cacheKey); + console.log(`[Forms Service] Cache invalidated for ${cacheKey}`); + } catch (error) { + getErrorMessage(`Failed to invalidate cache for ${cacheKey}: ${error}`); + } +} + +export interface EditableFieldsInfo { + tagNo: string; + editableFields: string[]; // ํธ์ง ๊ฐ๋ฅํ ํ๋ ํค ๋ชฉ๋ก +} + +// TAG๋ณ ํธ์ง ๊ฐ๋ฅ ํ๋ ์กฐํ ํจ์ +export async function getEditableFieldsByTag( + contractItemId: number, + projectId: number +): Promise<Map<string, string[]>> { + try { + // 1. ํด๋น contractItemId์ ๋ชจ๋ ํ๊ทธ ์กฐํ + const tagList = await db + .select({ + tagNo: tags.tagNo, + tagClass: tags.class + }) + .from(tags) + .where(eq(tags.contractItemId, contractItemId)); + + const editableFieldsMap = new Map<string, string[]>(); + + // 2. ๊ฐ ํ๊ทธ๋ณ๋ก ํธ์ง ๊ฐ๋ฅ ํ๋ ๊ณ์ฐ + for (const tag of tagList) { + try { + // 2-1. tagClasses์์ ํด๋น class(label)์ projectId๋ก tagClass ์ฐพ๊ธฐ + const tagClassResult = await db + .select({ id: tagClasses.id }) + .from(tagClasses) + .where( + and( + eq(tagClasses.label, tag.tagClass), + eq(tagClasses.projectId, projectId) + ) + ) + .limit(1); + + if (tagClassResult.length === 0) { + console.warn(`No tagClass found for class: ${tag.tagClass}, projectId: ${projectId}`); + editableFieldsMap.set(tag.tagNo, []); // ํธ์ง ๋ถ๊ฐ๋ฅ + continue; + } + + // 2-2. tagClassAttributes์์ ํธ์ง ๊ฐ๋ฅํ ํ๋ ๋ชฉ๋ก ์กฐํ + const editableAttributes = await db + .select({ attId: tagClassAttributes.attId }) + .from(tagClassAttributes) + .where(eq(tagClassAttributes.tagClassId, tagClassResult[0].id)) + .orderBy(tagClassAttributes.seq); + + // 2-3. attId ๋ชฉ๋ก ์ ์ฅ + const editableFields = editableAttributes.map(attr => attr.attId); + editableFieldsMap.set(tag.tagNo, editableFields); + + } catch (error) { + console.error(`Error processing tag ${tag.tagNo}:`, error); + editableFieldsMap.set(tag.tagNo, []); // ์๋ฌ ์ ํธ์ง ๋ถ๊ฐ๋ฅ + } + } + + return editableFieldsMap; + } catch (error) { + console.error('Error getting editable fields by tag:', error); + return new Map(); + } +} +/** + * "๊ฐ์ฅ ์ต์ 1๊ฐ row"๋ฅผ ๊ฐ์ ธ์ค๊ณ , + * data๊ฐ ๋ฐฐ์ด์ด๋ฉด ๊ทธ ๋ฐฐ์ด์ ๋ฐํ, + * ๊ทธ๋ฆฌ๊ณ ์ด ๋ก์ง ์ ์ฒด๋ฅผ unstable_cache๋ก ๊ฐ์ธ ์บ์ฑ. + */ +export async function getFormData(formCode: string, contractItemId: number) { + try { + + // ๊ธฐ์กด ๋ก์ง์ผ๋ก projectId, columns, data ๊ฐ์ ธ์ค๊ธฐ + const contractItemResult = await db + .select({ + projectId: projects.id + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .where(eq(contractItems.id, contractItemId)) + .limit(1); + + if (contractItemResult.length === 0) { + console.warn(`[getFormData] No contract item found with ID: ${contractItemId}`); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } + + const projectId = contractItemResult[0].projectId; + + const metaRows = await db + .select() + .from(formMetas) + .where( + and( + eq(formMetas.formCode, formCode), + eq(formMetas.projectId, projectId) + ) + ) + .orderBy(desc(formMetas.updatedAt)) + .limit(1); + + const meta = metaRows[0] ?? null; + if (!meta) { + console.warn(`[getFormData] No form meta found for formCode: ${formCode} and projectId: ${projectId}`); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } + + const entryRows = await db + .select() + .from(formEntries) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + .orderBy(desc(formEntries.updatedAt)) + .limit(1); + + const entry = entryRows[0] ?? null; + + let columns = meta.columns as DataTableColumnJSON[]; + const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO']; + columns = columns.filter(col => !excludeKeys.includes(col.key)); + + + + columns.forEach((col) => { + if (!col.displayLabel) { + if (col.uom) { + col.displayLabel = `${col.label} (${col.uom})`; + } else { + col.displayLabel = col.label; + } + } + }); + + columns.push({ + key: "status", + label: "status", + displayLabel: "Status", + type: "STRING" + }) + + let data: Array<Record<string, any>> = []; + if (entry) { + if (Array.isArray(entry.data)) { + data = entry.data; + + data.sort((a, b) => { + const statusA = a.status || ''; + const statusB = b.status || ''; + return statusB.localeCompare(statusA) + }) + + } else { + console.warn("formEntries data was not an array. Using empty array."); + } + } + + // *** ์๋ก ์ถ๊ฐ: ํธ์ง ๊ฐ๋ฅ ํ๋ ์ ๋ณด ๊ณ์ฐ *** + const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId); + + return { columns, data, editableFieldsMap }; + + + } catch (cacheError) { + console.error(`[getFormData] Cache operation failed:`, cacheError); + + // Fallback logic (๊ธฐ์กด๊ณผ ๋์ผํ๊ฒ editableFieldsMap ์ถ๊ฐ) + try { + console.log(`[getFormData] Fallback DB query for (${formCode}, ${contractItemId})`); + + const contractItemResult = await db + .select({ + projectId: projects.id + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .where(eq(contractItems.id, contractItemId)) + .limit(1); + + if (contractItemResult.length === 0) { + console.warn(`[getFormData] Fallback: No contract item found with ID: ${contractItemId}`); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } + + const projectId = contractItemResult[0].projectId; + + const metaRows = await db + .select() + .from(formMetas) + .where( + and( + eq(formMetas.formCode, formCode), + eq(formMetas.projectId, projectId) + ) + ) + .orderBy(desc(formMetas.updatedAt)) + .limit(1); + + const meta = metaRows[0] ?? null; + if (!meta) { + console.warn(`[getFormData] Fallback: No form meta found for formCode: ${formCode} and projectId: ${projectId}`); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } + + const entryRows = await db + .select() + .from(formEntries) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + .orderBy(desc(formEntries.updatedAt)) + .limit(1); + + const entry = entryRows[0] ?? null; + + let columns = meta.columns as DataTableColumnJSON[]; + const excludeKeys = ['BF_TAG_NO', 'TAG_TYPE_ID', 'PIC_NO']; + columns = columns.filter(col => !excludeKeys.includes(col.key)); + + columns.forEach((col) => { + if (!col.displayLabel) { + if (col.uom) { + col.displayLabel = `${col.label} (${col.uom})`; + } else { + col.displayLabel = col.label; + } + } + }); + + let data: Array<Record<string, any>> = []; + if (entry) { + if (Array.isArray(entry.data)) { + data = entry.data; + } else { + console.warn("formEntries data was not an array. Using empty array (fallback)."); + } + } + + // Fallback์์๋ ํธ์ง ๊ฐ๋ฅ ํ๋ ์ ๋ณด ๊ณ์ฐ + const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId); + + return { columns, data, projectId, editableFieldsMap }; + } catch (dbError) { + console.error(`[getFormData] Fallback DB query failed:`, dbError); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } + } +} +/**1 + * contractId์ formCode(itemCode)๋ฅผ ์ฌ์ฉํ์ฌ contractItemId๋ฅผ ์ฐพ๋ ์๋ฒ ์ก์
+ * + * @param contractId - ๊ณ์ฝ ID + * @param formCode - ํผ ์ฝ๋ (itemCode์ ๋์ผ) + * @returns ์ฐพ์ contractItemId ๋๋ null + */ +export async function findContractItemId(contractId: number, formCode: string): Promise<number | null> { + try { + console.log(`[findContractItemId] ๊ณ์ฝ ID ${contractId}์ formCode ${formCode}์ ๋ํ contractItem ์กฐํ ์์`); + + // 1. forms ํ
์ด๋ธ์์ formCode์ ํด๋นํ๋ ๋ชจ๋ ๋ ์ฝ๋ ์กฐํ + const formsResult = await db + .select({ + contractItemId: forms.contractItemId + }) + .from(forms) + .where(eq(forms.formCode, formCode)); + + if (formsResult.length === 0) { + console.warn(`[findContractItemId] formCode ${formCode}์ ํด๋นํ๋ form์ ์ฐพ์ ์ ์์ต๋๋ค.`); + return null; + } + + // ๋ชจ๋ contractItemId ์ถ์ถ + const contractItemIds = formsResult.map(form => form.contractItemId); + console.log(`[findContractItemId] formCode ${formCode}์ ํด๋นํ๋ ${contractItemIds.length}๊ฐ์ contractItemId ๋ฐ๊ฒฌ`); + + // 2. contractItems ํ
์ด๋ธ์์ ์ถ์ถํ contractItemId ์ค์์ + // contractId๊ฐ ์ผ์นํ๋ ํญ๋ชฉ ์ฐพ๊ธฐ + const contractItemResult = await db + .select({ + id: contractItems.id + }) + .from(contractItems) + .where( + and( + inArray(contractItems.id, contractItemIds), + eq(contractItems.contractId, contractId) + ) + ) + .limit(1); + + if (contractItemResult.length === 0) { + console.warn(`[findContractItemId] ๊ณ์ฝ ID ${contractId}์ ์ผ์นํ๋ contractItemId๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.`); + return null; + } + + const contractItemId = contractItemResult[0].id; + console.log(`[findContractItemId] ๊ณ์ฝ ์์ดํ
ID ${contractItemId} ๋ฐ๊ฒฌ`); + + return contractItemId; + } catch (error) { + console.error(`[findContractItemId] contractItem ์กฐํ ์ค ์ค๋ฅ ๋ฐ์:`, error); + return null; + } +} + +export async function getPackageCodeById(contractItemId: number): Promise<string | null> { + try { + + // 1. forms ํ
์ด๋ธ์์ formCode์ ํด๋นํ๋ ๋ชจ๋ ๋ ์ฝ๋ ์กฐํ + const contractItemsResult = await db + .select({ + itemId: contractItems.itemId + }) + .from(contractItems) + .where(eq(contractItems.id, contractItemId)) + .limit(1) + ; + + if (contractItemsResult.length === 0) { + console.warn(`[contractItemId]์ ํด๋นํ๋ item์ ์ฐพ์ ์ ์์ต๋๋ค.`); + return null; + } + + const itemId = contractItemsResult[0].itemId + + const packageCodeResult = await db + .select({ + packageCode: items.packageCode + }) + .from(items) + .where(eq(items.id, itemId)) + .limit(1); + + if (packageCodeResult.length === 0) { + console.warn(`${itemId}์ ์ผ์นํ๋ ํจํค์ง ์ฝ๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.`); + return null; + } + + const packageCode = packageCodeResult[0].packageCode; + + return packageCode; + } catch (error) { + console.error(`ํจํค์ง ์ฝ๋ ์กฐํ ์ค ์ค๋ฅ ๋ฐ์:`, error); + return null; + } +} + + +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) + ) + ) + .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)); + + // 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 }; + } + + // 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))); + + // (4) Fetch (or create) a single `formEntries` row for (contractItemId, formCode). + let [entry] = await db + .select() + .from(formEntries) + .where( + and( + eq(formEntries.contractItemId, contractItemId), + eq(formEntries.formCode, formCode) + ) + ) + .limit(1); + + if (!entry) { + const [inserted] = await db + .insert(formEntries) + .values({ + contractItemId, + formCode, + data: [], // Initialize with empty array + }) + .returning(); + entry = inserted; + } + + // entry.data๋ [{ TAG_NO: string, TAG_DESC?: string }, ...] ํํ๋ผ๊ณ ๊ฐ์ + const existingData = entry.data as Array<{ + TAG_NO: string; + TAG_DESC?: string; + }>; + + // Create a Set of valid tagNumbers from tagRows for efficient lookup + const validTagNumbers = new Set(tagRows.map((tag) => tag.tagNo)); + + // Copy existing data to work with + let updatedData: Array<{ + TAG_NO: string; + TAG_DESC?: 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.TAG_NO)) { + updatedData.push(item); + } else { + deletedCount++; + } + } + + // (5) For each tagRow, if it's missing in updatedData, push it in. + // ์ด๋ฏธ ์๋ ๊ฒฝ์ฐ์๋ description์ด ๋ฌ๋ผ์ง๋ฉด ์
๋ฐ์ดํธํ ์ ์์. + for (const tagRow of tagRows) { + const { tagNo, description } = tagRow; + + // 5-1. ๊ธฐ์กด ๋ฐ์ดํฐ์์ TAG_NO ๋งค์นญ + const existingIndex = updatedData.findIndex( + (item) => item.TAG_NO === tagNo + ); + + // 5-2. ์๋ค๋ฉด ์๋ก ์ถ๊ฐ + if (existingIndex === -1) { + updatedData.push({ + TAG_NO: tagNo, + TAG_DESC: description ?? "", + }); + createdCount++; + } else { + // 5-3. ์ด๋ฏธ ์์ผ๋ฉด, description์ด ๋ค๋ฅผ ๋๋ง ์
๋ฐ์ดํธ(์ ํ ์ฌํญ) + const existingItem = updatedData[existingIndex]; + if (existingItem.TAG_DESC !== description) { + updatedData[existingIndex] = { + ...existingItem, + TAG_DESC: description ?? "", + }; + updatedCount++; + } + } + } + + // (6) ์ค์ ๋ก ์ถ๊ฐ๋๊ฑฐ๋ ์์ ๋๊ฑฐ๋ ์ญ์ ๋ ๊ฒ ์๋ค๋ฉด DB์ ๋ฐ์ + if (createdCount > 0 || updatedCount > 0 || deletedCount > 0) { + await db + .update(formEntries) + .set({ data: updatedData }) + .where(eq(formEntries.id, entry.id)); + } + + // ์บ์ ๋ฌดํจํ ๋ฑ ํ์ฒ๋ฆฌ + revalidateTag(`form-data-${formCode}-${contractItemId}`); + + return { createdCount, updatedCount, deletedCount }; +} + +/** + * updateFormDataInDB: + * (formCode, contractItemId)์ ํด๋นํ๋ "๋จ ํ๋์" formEntries row๋ฅผ ๊ฐ์ ธ์, + * data: [{ TAG_NO, ...}, ...] ๋ฐฐ์ด์์ TAG_NO ๋งค์นญ๋๋ ํญ๋ชฉ์ ์
๋ฐ์ดํธ + * ์
๋ฐ์ดํธ ํ, revalidateTag()๋ก ์บ์ ๋ฌดํจํ. + */ +export interface UpdateResponse { + success: boolean; + message: string; + data?: { + updatedCount?: number; + failedCount?: number; + updatedTags?: string[]; + notFoundTags?: string[]; + updateTimestamp?: string; + error?: any; + invalidRows?: any[]; + TAG_NO?: string; + updatedFields?: string[]; + }; +} + +export async function updateFormDataInDB( + formCode: string, + contractItemId: number, + newData: Record<string, any> +): Promise<UpdateResponse> { + try { + // 1) tagNumber๋ก ์๋ณ + const TAG_NO = newData.TAG_NO; + if (!TAG_NO) { + return { + success: false, + message: "tagNumber๋ ํ์ ํญ๋ชฉ์
๋๋ค.", + }; + } + + // 2) row ์ฐพ๊ธฐ (๋จ ํ๋) + const entries = await db + .select() + .from(formEntries) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + .limit(1); + + if (!entries || entries.length === 0) { + return { + success: false, + message: `ํผ ๋ฐ์ดํฐ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. (formCode=${formCode}, contractItemId=${contractItemId})`, + }; + } + + const entry = entries[0]; + + // 3) data๊ฐ ๋ฐฐ์ด์ธ์ง ํ์ธ + if (!entry.data) { + return { + success: false, + message: "ํผ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.", + }; + } + + const dataArray = entry.data as Array<Record<string, any>>; + if (!Array.isArray(dataArray)) { + return { + success: false, + message: "ํผ ๋ฐ์ดํฐ๊ฐ ์ฌ๋ฐ๋ฅธ ํ์์ด ์๋๋๋ค. ๋ฐฐ์ด ํ์์ด์ด์ผ ํฉ๋๋ค.", + }; + } + + // 4) TAG_NO = newData.TAG_NO ํญ๋ชฉ ์ฐพ๊ธฐ + const idx = dataArray.findIndex((item) => item.TAG_NO === TAG_NO); + if (idx < 0) { + return { + success: false, + message: `ํ๊ทธ ๋ฒํธ "${TAG_NO}"๋ฅผ ๊ฐ์ง ํญ๋ชฉ์ ์ฐพ์ ์ ์์ต๋๋ค.`, + }; + } + + // 5) ๋ณํฉ (status ํ๋ ์ถ๊ฐ) + const oldItem = dataArray[idx]; + const updatedItem = { + ...oldItem, + ...newData, + TAG_NO: oldItem.TAG_NO, // TAG_NO ๋ณ๊ฒฝ ๋ถ๊ฐ ์ ์ ์ง + status: "Updated" // Excel์์ ๊ฐ์ ธ์จ ๋ฐ์ดํฐ์์ ํ์ + }; + + const updatedArray = [...dataArray]; + updatedArray[idx] = updatedItem; + + // 6) DB UPDATE + try { + await db + .update(formEntries) + .set({ + data: updatedArray, + updatedAt: new Date(), // ์
๋ฐ์ดํธ ์๊ฐ๋ ๊ฐฑ์ + }) + .where(eq(formEntries.id, entry.id)); + } catch (dbError) { + console.error("Database update error:", dbError); + + if (dbError instanceof DrizzleError) { + return { + success: false, + message: `๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ ์ค๋ฅ: ${dbError.message}`, + }; + } + + return { + success: false, + message: "๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", + }; + } + + // 7) Cache ๋ฌดํจํ + try { + // ์บ์ ํ๊ทธ๋ฅผ form-data-${formCode}-${contractItemId} ํํ๋ก ๊ฐ์ + const cacheTag = `form-data-${formCode}-${contractItemId}`; + console.log(cacheTag, "update") + revalidateTag(cacheTag); + } catch (cacheError) { + console.warn("Cache revalidation warning:", cacheError); + // ์บ์ ๋ฌดํจํ๋ ์คํจํด๋ ์
๋ฐ์ดํธ ์์ฒด๋ ์ฑ๊ณตํ์ผ๋ฏ๋ก ๊ฒฝ๊ณ ๋ง ๋ก๊ทธ๋ก ๋จ๊น + } + + return { + success: true, + message: "๋ฐ์ดํฐ๊ฐ ์ฑ๊ณต์ ์ผ๋ก ์
๋ฐ์ดํธ๋์์ต๋๋ค.", + data: { + TAG_NO, + updatedFields: Object.keys(newData).filter( + (key) => key !== "TAG_NO" + ), + }, + }; + } catch (error) { + // ์์์น ๋ชปํ ์ค๋ฅ ์ฒ๋ฆฌ + console.error("Unexpected error in updateFormDataInDB:", error); + return { + success: false, + message: + error instanceof Error + ? `์์์น ๋ชปํ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: ${error.message}` + : "์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", + }; + } +} + +export async function updateFormDataBatchInDB( + formCode: string, + contractItemId: number, + newDataArray: Record<string, any>[] +): Promise<UpdateResponse> { + try { + // ์
๋ ฅ ์ ํจ์ฑ ๊ฒ์ฌ + if (!newDataArray || newDataArray.length === 0) { + return { + success: false, + message: "์
๋ฐ์ดํธํ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.", + }; + } + + // TAG_NO ์ ํจ์ฑ ๊ฒ์ฌ + const invalidRows = newDataArray.filter(row => !row.TAG_NO); + if (invalidRows.length > 0) { + return { + success: false, + message: `${invalidRows.length}๊ฐ ํ์ TAG_NO๊ฐ ์์ต๋๋ค.`, + data: { invalidRows } + }; + } + + // 1) DB์์ ํ์ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ + const entries = await db + .select() + .from(formEntries) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + .limit(1); + + if (!entries || entries.length === 0) { + return { + success: false, + message: `ํผ ๋ฐ์ดํฐ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. (formCode=${formCode}, contractItemId=${contractItemId})`, + }; + } + + const entry = entries[0]; + + // ๋ฐ์ดํฐ ํ์ ๊ฒ์ฆ + if (!entry.data) { + return { + success: false, + message: "ํผ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.", + }; + } + + const dataArray = entry.data as Array<Record<string, any>>; + if (!Array.isArray(dataArray)) { + return { + success: false, + message: "ํผ ๋ฐ์ดํฐ๊ฐ ์ฌ๋ฐ๋ฅธ ํ์์ด ์๋๋๋ค. ๋ฐฐ์ด ํ์์ด์ด์ผ ํฉ๋๋ค.", + }; + } + + // 2) ๋ชจ๋ ๋ณ๊ฒฝ์ฌํญ์ ํ๋ฒ์ ์ ์ฉ + const updatedArray = [...dataArray]; + const updatedTags: string[] = []; + const notFoundTags: string[] = []; + const updateTimestamp = new Date().toISOString(); + + // ๊ฐ import row์ ๋ํด ์
๋ฐ์ดํธ ์ํ + for (const newData of newDataArray) { + const TAG_NO = newData.TAG_NO; + const idx = updatedArray.findIndex(item => item.TAG_NO === TAG_NO); + + if (idx >= 0) { + // ๊ธฐ์กด ๋ฐ์ดํฐ์ ๋ณํฉ + const oldItem = updatedArray[idx]; + updatedArray[idx] = { + ...oldItem, + ...newData, + TAG_NO: oldItem.TAG_NO, // TAG_NO๋ ๋ณ๊ฒฝ ๋ถ๊ฐ + TAG_DESC: oldItem.TAG_DESC, // TAG_DESC๋ ๋ณด์กด + status: "Updated", // Excel import ํ์ + lastUpdated: updateTimestamp // ์
๋ฐ์ดํธ ์๊ฐ ์ถ๊ฐ + }; + updatedTags.push(TAG_NO); + } else { + // TAG๋ฅผ ์ฐพ์ ์ ์๋ ๊ฒฝ์ฐ + notFoundTags.push(TAG_NO); + } + } + + // ํ๋๋ ์
๋ฐ์ดํธํ ํญ๋ชฉ์ด ์๋ ๊ฒฝ์ฐ + if (updatedTags.length === 0) { + return { + success: false, + message: `์
๋ฐ์ดํธํ ์ ์๋ TAG๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค. ๋ชจ๋ ${notFoundTags.length}๊ฐ TAG๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์์ต๋๋ค.`, + data: { + updatedCount: 0, + failedCount: notFoundTags.length, + notFoundTags + } + }; + } + + // 3) DB์ ํ ๋ฒ๋ง ์ ์ฅ + try { + await db + .update(formEntries) + .set({ + data: updatedArray, + updatedAt: new Date(), + }) + .where(eq(formEntries.id, entry.id)); + + } catch (dbError) { + console.error("Database update error:", dbError); + + if (dbError instanceof DrizzleError) { + return { + success: false, + message: `๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ ์ค๋ฅ: ${dbError.message}`, + data: { + updatedCount: 0, + failedCount: newDataArray.length, + error: dbError + } + }; + } + + return { + success: false, + message: "๋ฐ์ดํฐ๋ฒ ์ด์ค ์
๋ฐ์ดํธ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", + data: { + updatedCount: 0, + failedCount: newDataArray.length + } + }; + } + + // 4) ์บ์ ๋ฌดํจํ + try { + const cacheTag = `form-data-${formCode}-${contractItemId}`; + console.log(`Cache invalidated: ${cacheTag}`); + revalidateTag(cacheTag); + } catch (cacheError) { + // ์บ์ ๋ฌดํจํ ์คํจ๋ ๊ฒฝ๊ณ ๋ง + console.warn("Cache revalidation warning:", cacheError); + } + + // 5) ์ฑ๊ณต ์๋ต + const message = notFoundTags.length > 0 + ? `${updatedTags.length}๊ฐ ํญ๋ชฉ์ด ์
๋ฐ์ดํธ๋์์ต๋๋ค. (${notFoundTags.length}๊ฐ TAG๋ ์ฐพ์ ์ ์์)` + : `${updatedTags.length}๊ฐ ํญ๋ชฉ์ด ์ฑ๊ณต์ ์ผ๋ก ์
๋ฐ์ดํธ๋์์ต๋๋ค.`; + + return { + success: true, + message: message, + data: { + updatedCount: updatedTags.length, + updatedTags, + notFoundTags: notFoundTags.length > 0 ? notFoundTags : undefined, + failedCount: notFoundTags.length, + updateTimestamp + }, + }; + + } catch (error) { + // ์์์น ๋ชปํ ์ค๋ฅ ์ฒ๋ฆฌ + console.error("Unexpected error in updateFormDataBatchInDB:", error); + + return { + success: false, + message: error instanceof Error + ? `์์์น ๋ชปํ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค: ${error.message}` + : "์ ์ ์๋ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", + data: { + updatedCount: 0, + failedCount: newDataArray.length, + error: error + } + }; + } +} + +// FormColumn Type (๋์ผ) +export interface FormColumn { + key: string; + type: string; + label: string; + options?: string[]; +} + +interface MetadataResult { + formName: string; + formCode: string; + columns: FormColumn[]; +} + +/** + * ์๋ฒ ์ก์
: + * ์ฃผ์ด์ง formCode์ ํด๋นํ๋ form_metas ๋ ์ฝ๋ 1๊ฐ๋ฅผ ์ฐพ์์ + * { formName, formCode, columns } ํํ๋ก ๋ฐํ. + * ์์ผ๋ฉด null. + */ +export async function fetchFormMetadata( + formCode: string, + projectId: number +): Promise<MetadataResult | null> { + try { + // ๊ธฐ์กด ๋ฐฉ์: select().from().where() + const rows = await db + .select() + .from(formMetas) + .where(and(eq(formMetas.formCode, formCode), eq(formMetas.projectId, projectId))) + .limit(1); + + // rows๋ ๋ฐฐ์ด + const metaData = rows[0]; + if (!metaData) return null; + + return { + formCode: metaData.formCode, + formName: metaData.formName, + 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<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 saveResult = await saveFile({ file, directory: "vendorFormData", originalName: customFileName }); + if (!saveResult.success) { + return { success: false, error: saveResult.error }; + } + + return db.transaction(async (tx) => { + // ํ์ผ ์ ๋ณด๋ฅผ ํ
์ด๋ธ์ ์ ์ฅ + await tx + .insert(vendorDataReportTemps) + .values({ + contractItemId: packageId, + formId: formId, + fileName: customFileName, + filePath: saveResult.publicPath!, + }) + .returning(); + }); + } +} + +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; +}; + + +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 deleteFile(filePath); + + return { result: true }; + }); + } catch (err) { + return { result: false, error: (err as Error).message }; + } +}; + + +/** + * Get tag type mappings specific to a form + * @param formCode The form code to filter mappings + * @param projectId The project ID + * @returns Array of tag type-class mappings for the form + */ +export async function getFormTagTypeMappings(formCode: string, projectId: number) { + + try { + const mappings = await db.query.tagTypeClassFormMappings.findMany({ + where: and( + eq(tagTypeClassFormMappings.formCode, formCode), + eq(tagTypeClassFormMappings.projectId, projectId) + ) + }); + + return mappings; + } catch (error) { + console.error("Error fetching form tag type mappings:", error); + throw new Error("Failed to load form tag type mappings"); + } +} + +/** + * Get tag type by its description + * @param description The tag type description (used as tagTypeLabel in mappings) + * @param projectId The project ID + * @returns The tag type object + */ +export async function getTagTypeByDescription(description: string, projectId: number) { + try { + const tagType = await db.query.tagTypes.findFirst({ + where: and( + eq(tagTypes.description, description), + eq(tagTypes.projectId, projectId) + ) + }); + + return tagType; + } catch (error) { + console.error("Error fetching tag type by description:", error); + throw new Error("Failed to load tag type"); + } +} + +/** + * Get subfields for a specific tag type + * @param tagTypeCode The tag type code + * @param projectId The project ID + * @returns Object containing subfields with their options + */ +export async function getSubfieldsByTagTypeForForm(tagTypeCode: string, projectId: number) { + try { + const subfields = await db.query.tagSubfields.findMany({ + where: and( + eq(tagSubfields.tagTypeCode, tagTypeCode), + eq(tagSubfields.projectId, projectId) + ), + orderBy: tagSubfields.sortOrder + }); + + const subfieldsWithOptions = await Promise.all( + subfields.map(async (subfield) => { + const options = await db.query.tagSubfieldOptions.findMany({ + where: and( + eq(tagSubfieldOptions.attributesId, subfield.attributesId), + eq(tagSubfieldOptions.projectId, projectId) + ) + }); + + return { + name: subfield.attributesId, + label: subfield.attributesDescription, + type: options.length > 0 ? "select" : "text", + options: options.map(opt => ({ value: opt.code, label: opt.label })), + expression: subfield.expression || undefined, + delimiter: subfield.delimiter || undefined + }; + }) + ); + + return { subFields: subfieldsWithOptions }; + } catch (error) { + console.error("Error fetching subfields for form:", error); + throw new Error("Failed to load subfields"); + } +} + +interface GenericData { + [key: string]: any; +} + +interface SEDPAttribute { + NAME: string; + VALUE: any; + UOM: string; + UOM_ID?: string; + CLS_ID?:string; +} + +interface SEDPDataItem { + TAG_NO: string; + TAG_DESC: string; + CLS_ID: string; + ATTRIBUTES: SEDPAttribute[]; + SCOPE: string; + TOOLID: string; + ITM_NO: string; + OP_DELETE: boolean; + MAIN_YN: boolean; + LAST_REV_YN: boolean; + CRTER_NO: string; + CHGER_NO: string; + TYPE: string; + PROJ_NO: string; + REV_NO: string; + CRTE_DTM?: string; + CHGE_DTM?: string; + _id?: string; +} + +async function transformDataToSEDPFormat( + tableData: GenericData[], + columnsJSON: DataTableColumnJSON[], + formCode: string, + objectCode: string, + projectNo: string, + contractItemId: number, // Add contractItemId parameter + designerNo: string = "253213" +): Promise<SEDPDataItem[]> { + // Create a map for quick column lookup + const columnsMap = new Map<string, DataTableColumnJSON>(); + columnsJSON.forEach(col => { + columnsMap.set(col.key, col); + }); + + // Current timestamp for CRTE_DTM and CHGE_DTM + const currentTimestamp = new Date().toISOString(); + + // Define the API base URL + const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; + + // Get the token + const apiKey = await getSEDPToken(); + + // Cache for UOM factors to avoid duplicate API calls + const uomFactorCache = new Map<string, number>(); + + // Cache for packageCode to avoid duplicate DB queries for same tag + const packageCodeCache = new Map<string, string>(); + + // Cache for tagClass code to avoid duplicate DB queries for same tag + const tagClassCodeCache = new Map<string, string>(); + + // Transform each row + const transformedItems = []; + + for (const row of tableData) { + + const cotractItem = await db.query.contractItems.findFirst({ + where: + eq(contractItems.id, contractItemId), + }); + + const item = await db.query.items.findFirst({ + where: + eq(items.id, cotractItem.itemId), + }); + + // Get packageCode for this specific tag + let packageCode = item.packageCode; // fallback to formCode + let tagClassCode = ""; // for CLS_ID + + if (row.TAG_NO && contractItemId) { + // Check cache first + const cacheKey = `${contractItemId}-${row.TAG_NO}`; + + if (packageCodeCache.has(cacheKey)) { + packageCode = packageCodeCache.get(cacheKey)!; + } else { + try { + // Query to get packageCode for this specific tag + const tagResult = await db.query.tags.findFirst({ + where: and( + eq(tags.contractItemId, contractItemId), + eq(tags.tagNo, row.TAG_NO) + ) + }); + + if (tagResult) { + // Get tagClass code if tagClassId exists + if (tagResult.tagClassId) { + // Check tagClass cache first + if (tagClassCodeCache.has(cacheKey)) { + tagClassCode = tagClassCodeCache.get(cacheKey)!; + } else { + const tagClassResult = await db.query.tagClasses.findFirst({ + where: eq(tagClasses.id, tagResult.tagClassId) + }); + + if (tagClassResult) { + tagClassCode = tagClassResult.code; + console.log(`Found tagClass code for tag ${row.TAG_NO}: ${tagClassCode}`); + } else { + console.warn(`No tagClass found for tagClassId: ${tagResult.tagClassId}`); + } + + // Cache the tagClass code result + tagClassCodeCache.set(cacheKey, tagClassCode); + } + } + + // Get the contract item + const contractItemResult = await db.query.contractItems.findFirst({ + where: eq(contractItems.id, tagResult.contractItemId) + }); + + if (contractItemResult) { + // Get the first item with this itemId + const itemResult = await db.query.items.findFirst({ + where: eq(items.id, contractItemResult.itemId) + }); + + if (itemResult && itemResult.packageCode) { + packageCode = itemResult.packageCode; + console.log(`Found packageCode for tag ${row.TAG_NO}: ${packageCode}`); + } else { + console.warn(`No item found for contractItem.itemId: ${contractItemResult.itemId}, using fallback`); + } + } else { + console.warn(`No contractItem found for tag ${row.TAG_NO}, using fallback`); + } + } else { + console.warn(`No tag found for contractItemId: ${contractItemId}, tagNo: ${row.TAG_NO}, using fallback`); + } + + // Cache the result (even if it's the fallback value) + packageCodeCache.set(cacheKey, packageCode); + } catch (error) { + console.error(`Error fetching packageCode for tag ${row.TAG_NO}:`, error); + // Use fallback value and cache it + packageCodeCache.set(cacheKey, packageCode); + } + } + + // Get tagClass code if not already retrieved above + if (!tagClassCode && tagClassCodeCache.has(cacheKey)) { + tagClassCode = tagClassCodeCache.get(cacheKey)!; + } else if (!tagClassCode) { + try { + const tagResult = await db.query.tags.findFirst({ + where: and( + eq(tags.contractItemId, contractItemId), + eq(tags.tagNo, row.TAG_NO) + ) + }); + + if (tagResult && tagResult.tagClassId) { + const tagClassResult = await db.query.tagClasses.findFirst({ + where: eq(tagClasses.id, tagResult.tagClassId) + }); + + if (tagClassResult) { + tagClassCode = tagClassResult.code; + console.log(`Found tagClass code for tag ${row.TAG_NO}: ${tagClassCode}`); + } + } + + // Cache the tagClass code result + tagClassCodeCache.set(cacheKey, tagClassCode); + } catch (error) { + console.error(`Error fetching tagClass code for tag ${row.TAG_NO}:`, error); + // Cache empty string as fallback + tagClassCodeCache.set(cacheKey, ""); + } + } + } + + // Create base SEDP item with required fields + const sedpItem: SEDPDataItem = { + TAG_NO: row.TAG_NO || "", + TAG_DESC: row.TAG_DESC || "", + ATTRIBUTES: [], + // SCOPE: objectCode, + SCOPE: packageCode, + TOOLID: "eVCP", // Changed from VDCS + ITM_NO: row.TAG_NO || "", + OP_DELETE: false, + MAIN_YN: true, + LAST_REV_YN: true, + CRTER_NO: designerNo, + CHGER_NO: designerNo, + TYPE: formCode, // Use packageCode instead of formCode + CLS_ID: tagClassCode, // Add CLS_ID with tagClass code + PROJ_NO: projectNo, + REV_NO: "00", + CRTE_DTM: currentTimestamp, + CHGE_DTM: currentTimestamp, + _id: "" + }; + + // Convert all other fields (except TAG_NO and TAG_DESC) to ATTRIBUTES + for (const key in row) { + if (key !== "TAG_NO" && key !== "TAG_DESC") { + const column = columnsMap.get(key); + let value = row[key]; + + // Only process non-empty values + if (value !== undefined && value !== null && value !== "") { + // Check if we need to apply UOM conversion + if (column?.uomId) { + // First check cache to avoid duplicate API calls + let factor = uomFactorCache.get(column.uomId); + + // If not in cache, make API call to get the factor + if (factor === undefined) { + try { + const response = await fetch( + `${SEDP_API_BASE_URL}/UOM/GetByID`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectNo + }, + body: JSON.stringify({ + 'ProjectNo': projectNo, + 'UOMID': column.uomId, + 'ContainDeleted': false + }) + } + ); + + if (response.ok) { + const uomData = await response.json(); + if (uomData && uomData.FACTOR !== undefined && uomData.FACTOR !== null) { + factor = Number(uomData.FACTOR); + // Store in cache for future use (type assertion to ensure it's a number) + uomFactorCache.set(column.uomId, factor); + } + } else { + console.warn(`Failed to get UOM data for ${column.uomId}: ${response.statusText}`); + } + } catch (error) { + console.error(`Error fetching UOM data for ${column.uomId}:`, error); + } + } + + // Apply the factor if we got one + // if (factor !== undefined && typeof value === 'number') { + // value = value * factor; + // } + } + + const attribute: SEDPAttribute = { + NAME: key, + VALUE: String(value), // ๋ชจ๋ ๊ฐ์ ๋ฌธ์์ด๋ก ๋ณํ + UOM: column?.uom || "", + CLS_ID: tagClassCode || "", + }; + + // Add UOM_ID if present in column definition + if (column?.uomId) { + attribute.UOM_ID = column.uomId; + } + + sedpItem.ATTRIBUTES.push(attribute); + } + } + } + + transformedItems.push(sedpItem); + } + + return transformedItems; +} + +// Server Action wrapper (async) +export async function transformFormDataToSEDP( + tableData: GenericData[], + columnsJSON: DataTableColumnJSON[], + formCode: string, + objectCode: string, + projectNo: string, + contractItemId: number, // Add contractItemId parameter + designerNo: string = "253213" +): Promise<SEDPDataItem[]> { + return transformDataToSEDPFormat( + tableData, + columnsJSON, + formCode, + objectCode, + projectNo, + contractItemId, // Pass contractItemId + designerNo + ); +} +/** + * Get project code by project ID + */ +export async function getProjectCodeById(projectId: number): Promise<string> { + const projectRecord = await db + .select({ code: projects.code }) + .from(projects) + .where(eq(projects.id, projectId)) + .limit(1); + + if (!projectRecord || projectRecord.length === 0) { + throw new Error(`Project not found with ID: ${projectId}`); + } + + return projectRecord[0].code; +} + +export async function getProjectById(projectId: number): Promise<{ code: string; type: string; }> { + const projectRecord = await db + .select({ code: projects.code , type:projects.type}) + .from(projects) + .where(eq(projects.id, projectId)) + .limit(1); + + if (!projectRecord || projectRecord.length === 0) { + throw new Error(`Project not found with ID: ${projectId}`); + } + + return projectRecord[0]; +} + + +/** + * Send data to SEDP + */ +export async function sendDataToSEDP( + projectCode: string, + sedpData: SEDPDataItem[] +): Promise<any> { + try { + // Get the token + const apiKey = await getSEDPToken(); + + // Define the API base URL + const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; + + console.log("Sending data to SEDP:", JSON.stringify(sedpData, null, 2)); + + // Make the API call + const response = await fetch( + `${SEDP_API_BASE_URL}/AdapterData/Overwrite`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify(sedpData) + } + ); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`); + } + + const data = await response.json(); + return data; + } catch (error: any) { + console.error('Error calling SEDP API:', error); + throw new Error(`Failed to send data to SEDP API: ${error.message || 'Unknown error'}`); + } +} + +/** + * Server action to send form data to SEDP + */ +export async function sendFormDataToSEDP( + formCode: string, + projectId: number, + contractItemId: number, // contractItemId ํ๋ผ๋ฏธํฐ ์ถ๊ฐ + formData: GenericData[], + columns: DataTableColumnJSON[] +): Promise<{ success: boolean; message: string; data?: any }> { + try { + // 1. Get project code + const projectCode = await getProjectCodeById(projectId); + + // 2. Get class mapping + const mappingsResult = await db.query.tagTypeClassFormMappings.findFirst({ + where: and( + eq(tagTypeClassFormMappings.formCode, formCode), + eq(tagTypeClassFormMappings.projectId, projectId) + ) + }); + + // Check if mappings is an array or a single object and handle accordingly + const mappings = Array.isArray(mappingsResult) ? mappingsResult[0] : mappingsResult; + + // Default object code to fallback value if we can't find it + let objectCode = ""; // Default fallback + + if (mappings && mappings.classLabel) { + const objectCodeResult = await db.query.tagClasses.findFirst({ + where: and( + eq(tagClasses.label, mappings.classLabel), + eq(tagClasses.projectId, projectId) + ) + }); + + // Check if result is an array or a single object + const objectCodeRecord = Array.isArray(objectCodeResult) ? objectCodeResult[0] : objectCodeResult; + + if (objectCodeRecord && objectCodeRecord.code) { + objectCode = objectCodeRecord.code; + } else { + console.warn(`No tag class found for label ${mappings.classLabel} in project ${projectId}, using default`); + } + } else { + console.warn(`No mapping found for formCode ${formCode} in project ${projectId}, using default object code`); + } + + // 3. Transform data to SEDP format + const sedpData = await transformFormDataToSEDP( + formData, + columns, + formCode, + objectCode, + projectCode, + contractItemId // Add contractItemId parameter + ); + + // 4. Send to SEDP API + const result = await sendDataToSEDP(projectCode, sedpData); + + // 5. SEDP ์ ์ก ์ฑ๊ณต ํ formEntries์ status ์
๋ฐ์ดํธ + try { + // Get the current formEntries data + const entries = await db + .select() + .from(formEntries) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + .limit(1); + + if (entries && entries.length > 0) { + const entry = entries[0]; + const dataArray = entry.data as Array<Record<string, any>>; + + if (Array.isArray(dataArray)) { + // Extract TAG_NO list from formData + const sentTagNumbers = new Set( + formData + .map(item => item.TAG_NO) + .filter(tagNo => tagNo) // Remove null/undefined values + ); + + // Update status for sent tags + const updatedDataArray = dataArray.map(item => { + if (item.TAG_NO && sentTagNumbers.has(item.TAG_NO)) { + return { + ...item, + status: "Sent to S-EDP" // SEDP๋ก ์ ์ก๋ ๋ฐ์ดํฐ์์ ํ์ + }; + } + return item; + }); + + // Update the database + await db + .update(formEntries) + .set({ + data: updatedDataArray, + updatedAt: new Date() + }) + .where(eq(formEntries.id, entry.id)); + + console.log(`Updated status for ${sentTagNumbers.size} tags to "Sent to S-EDP"`); + } + } else { + console.warn(`No formEntries found for formCode: ${formCode}, contractItemId: ${contractItemId}`); + } + } catch (statusUpdateError) { + // Status ์
๋ฐ์ดํธ ์คํจ๋ ๊ฒฝ๊ณ ๋ก๋ง ์ฒ๋ฆฌ (SEDP ์ ์ก์ ์ฑ๊ณตํ์ผ๋ฏ๋ก) + console.warn("Failed to update status after SEDP send:", statusUpdateError); + } + + return { + success: true, + message: "Data successfully sent to SEDP", + data: result + }; + } catch (error: any) { + console.error("Error sending data to SEDP:", error); + return { + success: false, + message: error.message || "Failed to send data to SEDP" + }; + } +} + + +export async function deleteFormDataByTags({ + formCode, + contractItemId, + tagIdxs, +}: { + formCode: string + contractItemId: number + tagIdxs: string[] +}): Promise<{ + error?: string + success?: boolean + deletedCount?: number + deletedTagsCount?: number +}> { + try { + // ์
๋ ฅ ๊ฒ์ฆ + if (!formCode || !contractItemId || !Array.isArray(tagIdxs) || tagIdxs.length === 0) { + return { + error: "Missing required parameters: formCode, contractItemId, tagIdxs", + } + } + + console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagNos:`, tagIdxs) + + // ํธ๋์ญ์
์ผ๋ก ์์ ํ๊ฒ ์ฒ๋ฆฌ + const result = await db.transaction(async (tx) => { + // 1. ํ์ฌ formEntry ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ + const currentEntryResult = await tx + .select() + .from(formEntries) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + .orderBy(desc(formEntries.updatedAt)) + .limit(1) + + if (currentEntryResult.length === 0) { + throw new Error("Form entry not found") + } + + const currentEntry = currentEntryResult[0] + let currentData = Array.isArray(currentEntry.data) ? currentEntry.data : [] + + console.log(`[DELETE ACTION] Current data count: ${currentData.length}`) + + // 2. ์ญ์ ํ ํญ๋ชฉ๋ค ํํฐ๋ง (formEntries์์) + const updatedData = currentData.filter((item: any) => + !tagIdxs.includes(item.TAG_IDX) + ) + + const deletedFromFormEntries = currentData.length - updatedData.length + + console.log(`[DELETE ACTION] Updated data count: ${updatedData.length}`) + console.log(`[DELETE ACTION] Deleted ${deletedFromFormEntries} items from formEntries`) + + if (deletedFromFormEntries === 0) { + throw new Error("No items were found to delete in formEntries") + } + + // 3. tags ํ
์ด๋ธ์์ ํด๋น ํ๊ทธ๋ค ์ญ์ + const deletedTagsResult = await tx + .delete(tags) + .where( + and( + eq(tags.contractItemId, contractItemId), + inArray(tags.tagIdx, tagIdxs) + ) + ) + .returning({ tagNo: tags.tagNo }) + + const deletedTagsCount = deletedTagsResult.length + + console.log(`[DELETE ACTION] Deleted ${deletedTagsCount} items from tags table`) + console.log(`[DELETE ACTION] Deleted tag numbers:`, deletedTagsResult.map(t => t.tagNo)) + + // 4. formEntries ๋ฐ์ดํฐ ์
๋ฐ์ดํธ + await tx + .update(formEntries) + .set({ + data: updatedData, + updatedAt: new Date(), + }) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + + return { + deletedFromFormEntries, + deletedTagsCount, + deletedTagNumbers: deletedTagsResult.map(t => t.tagNo) + } + }) + + // 5. ์บ์ ๋ฌดํจํ + const cacheKey = `form-data-${formCode}-${contractItemId}` + revalidateTag(cacheKey) + revalidateTag(`tags-${contractItemId}`) + + // ํ์ด์ง ์ฌ๊ฒ์ฆ (ํ์ํ ๊ฒฝ์ฐ) + + console.log(`[DELETE ACTION] Transaction completed successfully`) + console.log(`[DELETE ACTION] FormEntries deleted: ${result.deletedFromFormEntries}`) + console.log(`[DELETE ACTION] Tags deleted: ${result.deletedTagsCount}`) + + return { + success: true, + deletedCount: result.deletedFromFormEntries, + deletedTagsCount: result.deletedTagsCount, + } + + } catch (error) { + console.error("[DELETE ACTION] Error deleting form data:", error) + return { + error: error instanceof Error ? error.message : "An unexpected error occurred", + } + } +} + +/** + * Server action to exclude selected tags by updating their status + */ +export async function excludeFormDataByTags({ + formCode, + contractItemId, + tagNumbers, +}: { + formCode: string + contractItemId: number + tagNumbers: string[] +}): Promise<{ + error?: string + success?: boolean + excludedCount?: number +}> { + try { + // ์
๋ ฅ ๊ฒ์ฆ + if (!formCode || !contractItemId || !Array.isArray(tagNumbers) || tagNumbers.length === 0) { + return { + error: "Missing required parameters: formCode, contractItemId, tagNumbers", + } + } + + console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagNumbers:`, tagNumbers) + + // ํธ๋์ญ์
์ผ๋ก ์์ ํ๊ฒ ์ฒ๋ฆฌ + const result = await db.transaction(async (tx) => { + // 1. ํ์ฌ formEntry ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ + const currentEntryResult = await tx + .select() + .from(formEntries) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + .orderBy(desc(formEntries.updatedAt)) + .limit(1) + + if (currentEntryResult.length === 0) { + throw new Error("Form entry not found") + } + + const currentEntry = currentEntryResult[0] + let currentData = Array.isArray(currentEntry.data) ? currentEntry.data : [] + + console.log(`[EXCLUDE ACTION] Current data count: ${currentData.length}`) + + // 2. TAG_NO๊ฐ ์ผ์นํ๋ ํญ๋ชฉ๋ค์ status๋ฅผ 'excluded'๋ก ์
๋ฐ์ดํธ + let excludedCount = 0 + const updatedData = currentData.map((item: any) => { + if (tagNumbers.includes(item.TAG_NO)) { + excludedCount++ + return { + ...item, + status: 'excluded', + excludedAt: new Date().toISOString() // ์ ์ธ ์๊ฐ ์ถ๊ฐ (์ ํ์ฌํญ) + } + } + return item + }) + + console.log(`[EXCLUDE ACTION] Excluded ${excludedCount} items`) + + if (excludedCount === 0) { + throw new Error("No items were found to exclude") + } + + // 3. formEntries ๋ฐ์ดํฐ ์
๋ฐ์ดํธ + await tx + .update(formEntries) + .set({ + data: updatedData, + updatedAt: new Date(), + }) + .where( + and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) + ) + ) + + return { + excludedCount, + excludedTagNumbers: tagNumbers + } + }) + + // 4. ์บ์ ๋ฌดํจํ + const cacheKey = `form-data-${formCode}-${contractItemId}` + revalidateTag(cacheKey) + + console.log(`[EXCLUDE ACTION] Transaction completed successfully`) + console.log(`[EXCLUDE ACTION] Tags excluded: ${result.excludedCount}`) + + return { + success: true, + excludedCount: result.excludedCount, + } + + } catch (error) { + console.error("[EXCLUDE ACTION] Error excluding form data:", error) + return { + error: error instanceof Error ? error.message : "An unexpected error occurred", + } + } +} + + + +export async function getRegisters(projectCode: string): Promise<Register[]> { + try { + // ํ ํฐ(API ํค) ๊ฐ์ ธ์ค๊ธฐ + const apiKey = await getSEDPToken(); + const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; + + const response = await fetch( + `${SEDP_API_BASE_URL}/Register/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ProjectNo: projectCode, + ContainDeleted: false + }) + } + ); + + if (!response.ok) { + throw new Error(`๋ ์ง์คํฐ ์์ฒญ ์คํจ: ${response.status} ${response.statusText}`); + } + + // ์์ ํ๊ฒ JSON ํ์ฑ + let data; + try { + data = await response.json(); + } catch (parseError) { + console.error(`ํ๋ก์ ํธ ${projectCode}์ ๋ ์ง์คํฐ ์๋ต ํ์ฑ ์คํจ:`, parseError); + // ์๋ต ๋ด์ฉ ๋ก๊น
+ const text = await response.clone().text(); + console.log(`์๋ต ๋ด์ฉ: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + throw new Error(`๋ ์ง์คํฐ ์๋ต ํ์ฑ ์คํจ: ${parseError instanceof Error ? parseError.message : String(parseError)}`); + } + + // ๊ฒฐ๊ณผ๋ฅผ ๋ฐฐ์ด๋ก ๋ณํ (๋จ์ผ ๊ฐ์ฒด์ธ ๊ฒฝ์ฐ ๋ฐฐ์ด๋ก ๋ํ) + const registers: Register[] = Array.isArray(data) ? data : [data]; + + console.log(`ํ๋ก์ ํธ ${projectCode}์์ ${registers.length}๊ฐ์ ์ ํจํ ๋ ์ง์คํฐ๋ฅผ ๊ฐ์ ธ์์ต๋๋ค.`); + return registers; + } catch (error) { + console.error(`ํ๋ก์ ํธ ${projectCode}์ ๋ ์ง์คํฐ ๊ฐ์ ธ์ค๊ธฐ ์คํจ:`, error); + throw error; + } +}
\ No newline at end of file diff --git a/lib/forms-plant/stat.ts b/lib/forms-plant/stat.ts new file mode 100644 index 00000000..f13bab61 --- /dev/null +++ b/lib/forms-plant/stat.ts @@ -0,0 +1,375 @@ +"use server" + +import db from "@/db/db" +import { vendors, contracts, contractItems, forms, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema" +import { eq, and, inArray } from "drizzle-orm" +import { getEditableFieldsByTag } from "./services" +import { getServerSession } from "next-auth/next" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" + +interface VendorFormStatus { + vendorId: number + vendorName: string + formCount: number // ๋ฒค๋๊ฐ ๊ฐ์ง form ๊ฐ์ + tagCount: number // ๋ฒค๋๊ฐ ๊ฐ์ง tag ๊ฐ์ + totalFields: number // ์
๋ ฅํด์ผ ํ๋ ์ด ํ๋ ๊ฐ์ + completedFields: number // ์
๋ ฅ ์๋ฃ๋ ํ๋ ๊ฐ์ + completionRate: number // ์๋ฃ์จ (%) +} + +export interface FormStatusByVendor { + tagCount: number; + totalFields: number; + completedFields: number; + completionRate: number; + upcomingCount: number; // 7์ผ ์ด๋ด ์๋ฐํ ๊ฐ์ + overdueCount: number; // ์ง์ฐ๋ ๊ฐ์ +} + +export async function getProjectsWithContracts() { + try { + const projectList = await db + .selectDistinct({ + id: projects.id, + projectCode: projects.code, + projectName: projects.name, + }) + .from(projects) + .innerJoin(contracts, eq(contracts.projectId, projects.id)) + .orderBy(projects.code) + + return projectList + } catch (error) { + console.error('Error getting projects with contracts:', error) + throw new Error('๊ณ์ฝ์ด ์๋ ํ๋ก์ ํธ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.') + } +} + + + +export async function getVendorFormStatus(projectId?: number): Promise<VendorFormStatus[]> { + try { + // 1. ๋ฒค๋ ์กฐํ ์ฟผ๋ฆฌ ์์ + const vendorList = projectId + ? await db + .selectDistinct({ + vendorId: vendors.id, + vendorName: vendors.vendorName, + }) + .from(vendors) + .innerJoin(contracts, eq(contracts.vendorId, vendors.id)) + .where(eq(contracts.projectId, projectId)) + : await db + .selectDistinct({ + vendorId: vendors.id, + vendorName: vendors.vendorName, + }) + .from(vendors) + .innerJoin(contracts, eq(contracts.vendorId, vendors.id)) + + + const vendorStatusList: VendorFormStatus[] = [] + + for (const vendor of vendorList) { + let vendorFormCount = 0 + let vendorTagCount = 0 + let vendorTotalFields = 0 + let vendorCompletedFields = 0 + const uniqueTags = new Set<string>() + + // 2. ๊ณ์ฝ ์กฐํ ์ projectId ํํฐ ์ถ๊ฐ + const vendorContracts = projectId + ? await db + .select({ + id: contracts.id, + projectId: contracts.projectId + }) + .from(contracts) + .where( + and( + eq(contracts.vendorId, vendor.vendorId), + eq(contracts.projectId, projectId) + ) + ) + : await db + .select({ + id: contracts.id, + projectId: contracts.projectId + }) + .from(contracts) + .where(eq(contracts.vendorId, vendor.vendorId)) + + + for (const contract of vendorContracts) { + // 3. ๊ณ์ฝ๋ณ contractItems ์กฐํ + const contractItemsList = await db + .select({ + id: contractItems.id + }) + .from(contractItems) + .where(eq(contractItems.contractId, contract.id)) + + for (const contractItem of contractItemsList) { + // 4. contractItem๋ณ forms ์กฐํ + const formsList = await db + .select({ + id: forms.id, + formCode: forms.formCode, + contractItemId: forms.contractItemId + }) + .from(forms) + .where(eq(forms.contractItemId, contractItem.id)) + + vendorFormCount += formsList.length + + // 5. formEntries ์กฐํ + const entriesList = await db + .select({ + id: formEntries.id, + formCode: formEntries.formCode, + data: formEntries.data + }) + .from(formEntries) + .where(eq(formEntries.contractItemId, contractItem.id)) + + // 6. TAG๋ณ ํธ์ง ๊ฐ๋ฅ ํ๋ ์กฐํ + const editableFieldsByTag = await getEditableFieldsByTag(contractItem.id, contract.projectId) + + for (const entry of entriesList) { + // formMetas์์ ํด๋น formCode์ columns ์กฐํ + const metaResult = await db + .select({ + columns: formMetas.columns + }) + .from(formMetas) + .where( + and( + eq(formMetas.formCode, entry.formCode), + eq(formMetas.projectId, contract.projectId) + ) + ) + .limit(1) + + if (metaResult.length === 0) continue + + const metaColumns = metaResult[0].columns as any[] + + // shi๊ฐ 'IN' ๋๋ 'BOTH'์ธ ํ๋ ์ฐพ๊ธฐ + const inputRequiredFields = metaColumns + .filter(col => col.shi === 'IN' || col.shi === 'BOTH') + .map(col => col.key) + + // entry.data ๋ถ์ (๋ฐฐ์ด๋ก ๊ฐ์ ) + const dataArray = Array.isArray(entry.data) ? entry.data : [] + + for (const dataItem of dataArray) { + if (typeof dataItem !== 'object' || !dataItem) continue + + const tagNo = dataItem.TAG_NO + if (tagNo) { + uniqueTags.add(tagNo) + + // TAG๋ณ ํธ์ง ๊ฐ๋ฅ ํ๋ ๊ฐ์ ธ์ค๊ธฐ + const tagEditableFields = editableFieldsByTag.get(tagNo) || [] + + // ์ต์ข
์
๋ ฅ ํ์ ํ๋ = shi ๊ธฐ๋ฐ ํ๋ + TAG ๊ธฐ๋ฐ ํธ์ง ๊ฐ๋ฅ ํ๋ + const allRequiredFields = inputRequiredFields.filter(field => + tagEditableFields.includes(field) + ) + // ๊ฐ ํ๋๋ณ ์
๋ ฅ ์ํ ์ฒดํฌ + for (const fieldKey of allRequiredFields) { + vendorTotalFields++ + + const fieldValue = dataItem[fieldKey] + // ๊ฐ์ด ์๊ณ , ๋น ๋ฌธ์์ด์ด ์๋๊ณ , null์ด ์๋๋ฉด ์
๋ ฅ ์๋ฃ + if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { + vendorCompletedFields++ + } + } + } + } + } + } + } + + // ์๋ฃ์จ ๊ณ์ฐ + const completionRate = vendorTotalFields > 0 + ? Math.round((vendorCompletedFields / vendorTotalFields) * 100 * 10) / 10 + : 0 + + vendorStatusList.push({ + vendorId: vendor.vendorId, + vendorName: vendor.vendorName || '์ด๋ฆ ์์', + formCount: vendorFormCount, + tagCount: uniqueTags.size, + totalFields: vendorTotalFields, + completedFields: vendorCompletedFields, + completionRate + }) + } + + return vendorStatusList + + } catch (error) { + console.error('Error getting vendor form status:', error) + throw new Error('๋ฒค๋๋ณ Form ์
๋ ฅ ํํฉ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.') + } +} + + + +export async function getFormStatusByVendor(projectId: number, contractItemId: number, formCode: string): Promise<FormStatusByVendor[]> { + try { + const session = await getServerSession(authOptions) + if (!session?.user?.id) { + throw new Error("์ธ์ฆ์ด ํ์ํฉ๋๋ค.") + } + + + let vendorFormCount = 0 + let vendorTagCount = 0 + let vendorTotalFields = 0 + let vendorCompletedFields = 0 + let vendorUpcomingCount = 0 // 7์ผ ์ด๋ด ์๋ฐํ ๊ฐ์ + let vendorOverdueCount = 0 // ์ง์ฐ๋ ๊ฐ์ + const uniqueTags = new Set<string>() + const processedTags = new Set<string>() // ์ค๋ณต ์ฒ๋ฆฌ ๋ฐฉ์ง์ฉ + + // ํ์ฌ ๋ ์ง์ 7์ผ ํ ๋ ์ง ๊ณ์ฐ + const today = new Date() + today.setHours(0, 0, 0, 0) // ์๊ฐ ๋ถ๋ถ ์ ๊ฑฐ + const sevenDaysLater = new Date(today) + sevenDaysLater.setDate(sevenDaysLater.getDate() + 7) + + // 4. contractItem๋ณ forms ์กฐํ + const formsList = await db + .select({ + id: forms.id, + formCode: forms.formCode, + contractItemId: forms.contractItemId + }) + .from(forms) + .where( + and( + eq(forms.contractItemId, contractItemId), + eq(forms.formCode, formCode) + ) + ) + + vendorFormCount += formsList.length + + // 5. formEntries ์กฐํ + const entriesList = await db + .select({ + id: formEntries.id, + formCode: formEntries.formCode, + data: formEntries.data + }) + .from(formEntries) + .where( + and( + eq(formEntries.contractItemId, contractItemId), + eq(formEntries.formCode, formCode) + ) + ) + + // 6. TAG๋ณ ํธ์ง ๊ฐ๋ฅ ํ๋ ์กฐํ + const editableFieldsByTag = await getEditableFieldsByTag(contractItemId, projectId) + + const vendorStatusList: VendorFormStatus[] = [] + + for (const entry of entriesList) { + const metaResult = await db + .select({ + columns: formMetas.columns + }) + .from(formMetas) + .where( + and( + eq(formMetas.formCode, entry.formCode), + eq(formMetas.projectId, projectId) + ) + ) + .limit(1) + + if (metaResult.length === 0) continue + + const metaColumns = metaResult[0].columns as any[] + + const inputRequiredFields = metaColumns + .filter(col => col.shi === 'IN' || col.shi === 'BOTH') + .map(col => col.key) + + const dataArray = Array.isArray(entry.data) ? entry.data : [] + + for (const dataItem of dataArray) { + if (typeof dataItem !== 'object' || !dataItem) continue + + const tagNo = dataItem.TAG_NO + if (tagNo) { + uniqueTags.add(tagNo) + + // TAG๋ณ ํธ์ง ๊ฐ๋ฅ ํ๋ ๊ฐ์ ธ์ค๊ธฐ + const tagEditableFields = editableFieldsByTag.get(tagNo) || [] + + const allRequiredFields = inputRequiredFields.filter(field => + tagEditableFields.includes(field) + ) + + // ํด๋น TAG์ ํ๋ ์๋ฃ ์ํ ์ฒดํฌ + let tagHasIncompleteFields = false + + for (const fieldKey of allRequiredFields) { + vendorTotalFields++ + + const fieldValue = dataItem[fieldKey] + if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') { + vendorCompletedFields++ + } else { + tagHasIncompleteFields = true + } + } + + // ๋ฏธ์๋ฃ TAG์ ๋ํด์๋ง ๋ ์ง ์ฒดํฌ (TAG๋น ํ ๋ฒ๋ง ์ฒ๋ฆฌ) + if (!processedTags.has(tagNo) && tagHasIncompleteFields) { + processedTags.add(tagNo) + + const targetDate = dataItem.DUE_DATE + if (targetDate) { + const target = new Date(targetDate) + target.setHours(0, 0, 0, 0) // ์๊ฐ ๋ถ๋ถ ์ ๊ฑฐ + + if (target < today) { + // ๋ฏธ์๋ฃ์ด๋ฉด์ ์ง์ฐ๋ ๊ฒฝ์ฐ (์ค๋๋ณด๋ค ์ด์ ) + vendorOverdueCount++ + } else if (target >= today && target <= sevenDaysLater) { + // ๋ฏธ์๋ฃ์ด๋ฉด์ 7์ผ ์ด๋ด ์๋ฐํ ๊ฒฝ์ฐ + vendorUpcomingCount++ + } + } + } + } + } + } + + // ์๋ฃ์จ ๊ณ์ฐ + const completionRate = vendorTotalFields > 0 + ? Math.round((vendorCompletedFields / vendorTotalFields) * 100 * 10) / 10 + : 0 + + vendorStatusList.push({ + tagCount: uniqueTags.size, + totalFields: vendorTotalFields, + completedFields: vendorCompletedFields, + completionRate, + upcomingCount: vendorUpcomingCount, + overdueCount: vendorOverdueCount + }) + + return vendorStatusList + + } catch (error) { + console.error('Error getting vendor form status:', error) + throw new Error('๋ฒค๋๋ณ Form ์
๋ ฅ ํํฉ ์กฐํ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.') + } +}
\ No newline at end of file |
