diff options
Diffstat (limited to 'lib/forms')
| -rw-r--r-- | lib/forms/services.ts | 637 |
1 files changed, 488 insertions, 149 deletions
diff --git a/lib/forms/services.ts b/lib/forms/services.ts index d77f91d3..bd6e4bbc 100644 --- a/lib/forms/services.ts +++ b/lib/forms/services.ts @@ -10,74 +10,78 @@ import { formEntries, formMetas, forms, + tagClasses, tags, + tagSubfieldOptions, + tagSubfields, tagTypeClassFormMappings, + tagTypes, vendorDataReportTemps, VendorDataReportTemps, } from "@/db/schema/vendorData"; -import { eq, and, desc, sql, DrizzleError, or } from "drizzle-orm"; +import { eq, and, desc, sql, DrizzleError, 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, projects } from "@/db/schema"; +import { getSEDPToken } from "../sedp/sedp-token"; -export interface FormInfo { - id: number; - formCode: string; - formName: string; - // tagType: string -} -export async function getFormsByContractItemId(contractItemId: number | null) { +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}`; + // 고유 캐시 키 (모드 포함) + const cacheKey = `forms-${contractItemId}-${mode}`; try { return unstable_cache( - async () => { - console.log(contractItemId,"contractItemId") - console.log( - `[Forms Service] Fetching forms for contractItemId: ${contractItemId}` + `[Forms Service] Fetching forms for contractItemId: ${contractItemId}, mode: ${mode}` ); try { - // 데이터베이스에서 폼 조회 - const formRecords = await db - .select({ - id: forms.id, - formCode: forms.formCode, - formName: forms.formName, - // tagType: forms.tagType, - }) - .from(forms) - .where(eq(forms.contractItemId, contractItemId)); + // 쿼리 생성 + 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}` + `[Forms Service] Found ${formRecords.length} forms for contractItemId: ${contractItemId}, mode: ${mode}` ); - // 결과가 배열인지 확인 - if (!Array.isArray(formRecords)) { - getErrorMessage( - `Unexpected result format for contractItemId ${contractItemId} ${formRecords}` - ); - return { forms: [] }; - } - return { forms: formRecords }; } catch (error) { getErrorMessage( - `Database error for contractItemId ${contractItemId}: ${error}` + `Database error for contractItemId ${contractItemId}, mode: ${mode}: ${error}` ); throw error; // 캐시 함수에서 에러를 던져 캐싱이 발생하지 않도록 함 } @@ -91,29 +95,42 @@ export async function getFormsByContractItemId(contractItemId: number | null) { )(); } catch (error) { getErrorMessage( - `Cache operation failed for contractItemId ${contractItemId}: ${error}` + `Cache operation failed for contractItemId ${contractItemId}, mode: ${mode}: ${error}` ); // 캐시 문제 시 직접 쿼리 시도 try { console.log( - `[Forms Service] Fallback: Direct query for contractItemId: ${contractItemId}` + `[Forms Service] Fallback: Direct query for contractItemId: ${contractItemId}, mode: ${mode}` ); - const formRecords = await db - .select({ - id: forms.id, - formCode: forms.formCode, - formName: forms.formName, - // tagType: forms.tagType, - }) - .from(forms) - .where(eq(forms.contractItemId, contractItemId)); + // 쿼리 생성 + 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}:${dbError}` + `Fallback query failed for contractItemId ${contractItemId}, mode: ${mode}: ${dbError}` ); return { forms: [] }; } @@ -145,6 +162,7 @@ export async function revalidateForms(contractItemId: number) { export async function getFormData(formCode: string, contractItemId: number) { // 고유 캐시 키 (formCode + contractItemId) const cacheKey = `form-data-${formCode}-${contractItemId}`; + console.log(cacheKey, "getFormData") try { // 1) unstable_cache로 전체 로직을 감싼다 @@ -338,88 +356,6 @@ export async function getFormData(formCode: string, contractItemId: number) { } } -// export async function syncMissingTags(contractItemId: number, formCode: string) { - -// // (1) forms 테이블에서 (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}`) -// } - -// const { tagType, class: className } = formRow - -// // (2) tags 테이블에서 (contractItemId, tagType, class)인 태그 찾기 -// const tagRows = await db -// .select() -// .from(tags) -// .where( -// and( -// eq(tags.contractItemId, contractItemId), -// eq(tags.tagType, tagType), -// eq(tags.class, className), -// ) -// ) - -// if (tagRows.length === 0) { -// console.log("No matching tags found.") -// return { createdCount: 0 } -// } - -// // (3) formEntries에서 (contractItemId, formCode)인 row 1개 조회 -// let [entry] = await db -// .select() -// .from(formEntries) -// .where( -// and( -// eq(formEntries.contractItemId, contractItemId), -// eq(formEntries.formCode, formCode) -// ) -// ) -// .limit(1) - -// // (4) 만약 없다면 새로 insert: data = [] -// if (!entry) { -// const [inserted] = await db.insert(formEntries).values({ -// contractItemId, -// formCode, -// data: [], // 초기 상태는 빈 배열 -// }).returning() -// entry = inserted -// } - -// // entry.data는 배열이라고 가정 -// // Drizzle에서 jsonb는 JS object로 파싱되어 들어오므로, 타입 캐스팅 -// const existingData = entry.data as Array<{ tagNumber: string }> -// let createdCount = 0 - -// // (5) tagRows 각각에 대해, 이미 배열에 존재하는지 확인 후 없으면 push -// const updatedArray = [...existingData] -// for (const tagRow of tagRows) { -// const tagNo = tagRow.tagNo -// const found = updatedArray.some(item => item.tagNumber === tagNo) -// if (!found) { -// updatedArray.push({ tagNumber: tagNo }) -// createdCount++ -// } -// } - -// // (6) 변경이 있으면 UPDATE -// if (createdCount > 0) { -// await db -// .update(formEntries) -// .set({ data: updatedArray }) -// .where(eq(formEntries.id, entry.id)) -// } - -// revalidateTag(`form-data-${formCode}-${contractItemId}`); - -// return { createdCount } -// } export async function syncMissingTags( contractItemId: number, @@ -490,10 +426,10 @@ export async function syncMissingTags( entry = inserted; } - // entry.data는 [{ tagNumber: string, tagDescription?: string }, ...] 형태라고 가정 + // entry.data는 [{ TAG_NO: string, TAG_DESC?: string }, ...] 형태라고 가정 const existingData = entry.data as Array<{ - tagNumber: string; - tagDescription?: string; + TAG_NO: string; + TAG_DESC?: string; }>; // Create a Set of valid tagNumbers from tagRows for efficient lookup @@ -501,8 +437,8 @@ export async function syncMissingTags( // Copy existing data to work with let updatedData: Array<{ - tagNumber: string; - tagDescription?: string; + TAG_NO: string; + TAG_DESC?: string; }> = []; let createdCount = 0; @@ -511,7 +447,7 @@ export async function syncMissingTags( // First, filter out items that should be deleted (not in validTagNumbers) for (const item of existingData) { - if (validTagNumbers.has(item.tagNumber)) { + if (validTagNumbers.has(item.TAG_NO)) { updatedData.push(item); } else { deletedCount++; @@ -523,25 +459,25 @@ export async function syncMissingTags( for (const tagRow of tagRows) { const { tagNo, description } = tagRow; - // 5-1. 기존 데이터에서 tagNumber 매칭 + // 5-1. 기존 데이터에서 TAG_NO 매칭 const existingIndex = updatedData.findIndex( - (item) => item.tagNumber === tagNo + (item) => item.TAG_NO === tagNo ); // 5-2. 없다면 새로 추가 if (existingIndex === -1) { updatedData.push({ - tagNumber: tagNo, - tagDescription: description ?? "", + TAG_NO: tagNo, + TAG_DESC: description ?? "", }); createdCount++; } else { // 5-3. 이미 있으면, description이 다를 때만 업데이트(선택 사항) const existingItem = updatedData[existingIndex]; - if (existingItem.tagDescription !== description) { + if (existingItem.TAG_DESC !== description) { updatedData[existingIndex] = { ...existingItem, - tagDescription: description ?? "", + TAG_DESC: description ?? "", }; updatedCount++; } @@ -565,7 +501,7 @@ export async function syncMissingTags( /** * updateFormDataInDB: * (formCode, contractItemId)에 해당하는 "단 하나의" formEntries row를 가져와, - * data: [{ tagNumber, ...}, ...] 배열에서 tagNumber 매칭되는 항목을 업데이트 + * data: [{ TAG_NO, ...}, ...] 배열에서 TAG_NO 매칭되는 항목을 업데이트 * 업데이트 후, revalidateTag()로 캐시 무효화. */ type UpdateResponse = { @@ -581,8 +517,8 @@ export async function updateFormDataInDB( ): Promise<UpdateResponse> { try { // 1) tagNumber로 식별 - const tagNumber = newData.tagNumber; - if (!tagNumber) { + const TAG_NO = newData.TAG_NO; + if (!TAG_NO) { return { success: false, message: "tagNumber는 필수 항목입니다.", @@ -626,12 +562,12 @@ export async function updateFormDataInDB( }; } - // 4) tagNumber = newData.tagNumber 항목 찾기 - const idx = dataArray.findIndex((item) => item.tagNumber === tagNumber); + // 4) TAG_NO = newData.TAG_NO 항목 찾기 + const idx = dataArray.findIndex((item) => item.TAG_NO === TAG_NO); if (idx < 0) { return { success: false, - message: `태그 번호 "${tagNumber}"를 가진 항목을 찾을 수 없습니다.`, + message: `태그 번호 "${TAG_NO}"를 가진 항목을 찾을 수 없습니다.`, }; } @@ -640,7 +576,7 @@ export async function updateFormDataInDB( const updatedItem = { ...oldItem, ...newData, - tagNumber: oldItem.tagNumber, // tagNumber 변경 불가 시 유지 + TAG_NO: oldItem.TAG_NO, // TAG_NO 변경 불가 시 유지 }; const updatedArray = [...dataArray]; @@ -675,6 +611,7 @@ export async function updateFormDataInDB( 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); @@ -685,9 +622,9 @@ export async function updateFormDataInDB( success: true, message: "데이터가 성공적으로 업데이트되었습니다.", data: { - tagNumber, + TAG_NO, updatedFields: Object.keys(newData).filter( - (key) => key !== "tagNumber" + (key) => key !== "TAG_NO" ), }, }; @@ -922,3 +859,405 @@ export const deleteReportTempFile: deleteReportTempFile = async (id) => { 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; +} + +interface SEDPDataItem { + TAG_NO: string; + TAG_DESC: 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, + 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>(); + + // Transform each row + const transformedItems = []; + + for (const row of tableData) { + // Create base SEDP item with required fields + const sedpItem: SEDPDataItem = { + TAG_NO: row.TAG_NO || "", + TAG_DESC: row.TAG_DESC || "", + ATTRIBUTES: [], + SCOPE: objectCode, + 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, + 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 || "" + }; + + // 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, + designerNo: string = "253213" +): Promise<SEDPDataItem[]> { + // Use the utility function within the async Server Action + return transformDataToSEDPFormat( + tableData, + columnsJSON, + formCode, + objectCode, + projectNo, + 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; +} + +/** + * 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/Create`, + { + 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, + 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`); + } + + // 4. Transform data to SEDP format + const sedpData = await transformFormDataToSEDP( + formData, + columns, + formCode, + objectCode, + projectCode + ); + + // 5. Send to SEDP API + const result = await sendDataToSEDP(projectCode, sedpData); + + 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" + }; + } +}
\ No newline at end of file |
