diff options
Diffstat (limited to 'lib/sedp/sync-form.ts')
| -rw-r--r-- | lib/sedp/sync-form.ts | 503 |
1 files changed, 306 insertions, 197 deletions
diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts index 2cb677b7..c293c98e 100644 --- a/lib/sedp/sync-form.ts +++ b/lib/sedp/sync-form.ts @@ -1,6 +1,6 @@ // src/lib/cron/syncTagFormMappings.ts import db from "@/db/db"; -import { projects, tagTypes, tagClasses, tagTypeClassFormMappings, formMetas, forms, contractItems, items, contracts } from '@/db/schema'; +import { projects, tagTypes, tagClasses, tagTypeClassFormMappings, formMetas, forms, contractItems, items, contracts, templateItems } from '@/db/schema'; import { eq, and, inArray, ilike } from 'drizzle-orm'; import { getSEDPToken } from "./sedp-token"; @@ -37,6 +37,51 @@ interface FormRecord { createdAt: Date; updatedAt: Date; } + +interface TemplateItem { + TMPL_ID: string; + NAME: string; + TMPL_TYPE: string; + SPR_LST_SETUP: { + ACT_SHEET: string; + HIDN_SHEETS: Array<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: Array<string>; + ATTS: Array<{ + ATT_ID: string; + ALIAS: string; + HEAD_TEXT: string; + HIDN_YN: boolean; + SEQ:number; + DFLT_YN:boolean; + }>; + }; + SPR_ITM_LST_SETUP: { + ACT_SHEET: string; + HIDN_SHEETS: Array<string>; + CONTENT?: string; + DATA_SHEETS: Array<{ + SHEET_NAME: string; + REG_TYPE_ID: string; + MAP_CELL_ATT: Array<{ + ATT_ID: string; + IN: string; + }>; + }>; + }; +} + interface Register { PROJ_NO: string; TYPE_ID: string; @@ -75,6 +120,42 @@ interface LinkAttribute { UOM_ID: string | null; } +interface newRegister { + PROJ_NO: string; + MAP_ID: string; + EP_ID: string; + CATEGORY: string; + BYPASS: boolean; + REG_TYPE_ID: string; + TOOL_ID: string; + TOOL_TYPE: string; + MAP_CLS: { + TOOL_ATT_NAME: string; + ITEMS: ClassItmes[]; + }; + MAP_ATT: MapAttribute[]; + MAP_TMPLS: string[]; + CRTER_NO: string; + CRTE_DTM: string; + CHGER_NO: string; + _id: string; +} + + + +interface ClassItmes { + SEDP_OBJ_CLS_ID: string; + TOOL_VALS: string; + ISDEFALUT: boolean; +} + +interface MapAttribute { + SEDP_ATT_ID: string; + TOOL_ATT_NAME: string; + KEY_YN: boolean; + INOUT: string | null; +} + interface Attribute { PROJ_NO: string; ATT_ID: string; @@ -167,6 +248,9 @@ interface FormColumn { uom?: string; uomId?: string; shi?: Boolean; + hidden?: boolean; + seq?: number; + head?: string; } // 아이템 코드 추출 함수 @@ -249,6 +333,63 @@ async function getDefaulTAttributes(): Promise<string[]> { } } +async function fetchTemplateFromSEDP(projectCode: string, formCode: string): Promise<TemplateItem[]> { + try { + // Get the token + const apiKey = await getSEDPToken(); + + // Make the API call + const response = await fetch( + `${SEDP_API_BASE_URL}/Template/GetByRegisterID`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + WithContent: true, + ProjectNo: projectCode, + REG_TYPE_ID: formCode + }) + } + ); + + if (!response.ok) { + if (response.status === 404) { + console.warn(`템플릿을 찾을 수 없음: ${formCode}`); + return []; + } + const errorText = await response.text(); + throw new Error(`SEDP Template API request failed: ${response.status} ${response.statusText} - ${errorText}`); + } + + // 안전하게 JSON 파싱 + try { + const data = await response.json(); + // 데이터가 배열인지 확인 + const templates: TemplateItem[] = Array.isArray(data) ? data : [data]; + return templates.filter(template => template && template.TMPL_ID); + } catch (parseError) { + console.error(`템플릿 응답 파싱 실패:`, parseError); + // 응답 내용 로깅 + try { + const text = await response.clone().text(); + console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + } catch (textError) { + console.error('응답 내용 로깅 실패:', textError); + } + return []; + } + } catch (error: any) { + console.error('Error calling SEDP Template API:', error); + console.warn(`템플릿 가져오기 실패 (${formCode}): ${error.message || 'Unknown error'}`); + return []; + } +} + // 레지스터 데이터 가져오기 async function getRegisters(projectCode: string): Promise<Register[]> { try { @@ -315,6 +456,55 @@ async function getRegisters(projectCode: string): Promise<Register[]> { } } +async function getNewRegisters(projectCode: string): Promise<newRegister[]> { + try { + // 토큰(API 키) 가져오기 + const apiKey = await getSEDPToken(); + + const response = 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 (!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)}`); + } + + // 결과를 배열로 변환 (단일 객체인 경우 배열로 래핑) + let registers: newRegister[] = Array.isArray(data) ? data : [data]; + + console.log(`프로젝트 ${projectCode}에서 ${registers.length}개의 새 레지스터를 가져왔습니다.`); + return registers; + } catch (error) { + console.error(`프로젝트 ${projectCode}의 새 레지스터 가져오기 실패:`, error); + throw error; + } +} + // 프로젝트의 모든 속성을 가져와 맵으로 반환 async function getAttributes(projectCode: string): Promise<Map<string, Attribute>> { try { @@ -716,249 +906,167 @@ async function getContractItemsByItemCodes(itemCodes: string[], projectId: numbe } } -// 데이터베이스에 태그 타입 클래스 폼 매핑 및 폼 메타 저장 -async function saveFormMappingsAndMetas( +// UPDATED: saveFormMappingsAndMetas() +// ------------------------------------ +// Primary loop is **newRegisters**‑first; legacy **registers** are consulted +// only for supplemental details. +// +// 2025‑07‑09 fix: newRegister.MAP_CLS is a **single object**, not an array. +// Updated class‑ID extraction accordingly. +// +export async function saveFormMappingsAndMetas( projectId: number, projectCode: string, - registers: Register[] + registers: Register[], // legacy SEDP Register list (supplemental) + newRegisters: newRegister[] // AdapterDataMapping list (primary) ): Promise<number> { try { - // 프로젝트의 태그 타입과 클래스 가져오기 - const tagTypeRecords = await db.select() - .from(tagTypes) - .where(eq(tagTypes.projectId, projectId)); + /* ------------------------------------------------------------------ */ + /* 1. Prepare look‑up structures & common data */ + /* ------------------------------------------------------------------ */ - const tagClassRecords = await db.select() - .from(tagClasses) - .where(eq(tagClasses.projectId, projectId)); + const tagTypeRecords = await db.select().from(tagTypes).where(eq(tagTypes.projectId, projectId)); + const tagClassRecords = await db.select().from(tagClasses).where(eq(tagClasses.projectId, projectId)); + const tagTypeMap = new Map(tagTypeRecords.map(t => [t.code, t])); + const tagClassMap = new Map(tagClassRecords.map(c => [c.code, c])); - // 태그 타입과 클래스 매핑 - const tagTypeMap = new Map(tagTypeRecords.map(type => [type.code, type])); - const tagClassMap = new Map(tagClassRecords.map(cls => [cls.code, cls])); + const registerMap = new Map(registers.map(r => [r.TYPE_ID, r])); - // 모든 속성, 코드 리스트, UOM을 한 번에 가져와 반복 API 호출 방지 const attributeMap = await getAttributes(projectCode); - const codeListMap = await getCodeLists(projectCode); - const uomMap = await getUOMs(projectCode); - - // 기본 속성 가져오기 + const codeListMap = await getCodeLists(projectCode); + const uomMap = await getUOMs(projectCode); const defaultAttributes = await getDefaulTAttributes(); - // 모든 register에서 itemCode를 추출하여 한 번에 조회 - const allItemCodes: string[] = []; - registers.forEach(register => { - if (register.REMARK) { - const itemCodes = extractItemCodes(register.REMARK); - allItemCodes.push(...itemCodes); - } - }); - - // 중복 제거 - const uniqueItemCodes = [...new Set(allItemCodes)]; - - // 모든 itemCode에 대한 contractItemId 조회 - const itemCodeToContractItemId = await getContractItemsByItemCodes(uniqueItemCodes , projectId); - - console.log(`${uniqueItemCodes.length}개의 고유 itemCode 중 ${itemCodeToContractItemId.size}개의 contractItem을 찾았습니다`); + /* ------------------------------------------------------------------ */ + /* 2. Contract‑item look‑up (TOOL_TYPE) */ + /* ------------------------------------------------------------------ */ + const uniqueItemCodes = [...new Set(newRegisters.filter(nr => nr.TOOL_TYPE).map(nr => nr.TOOL_TYPE as string))]; + const itemCodeToContractItemId = await getContractItemsByItemCodes(uniqueItemCodes, projectId); - // 저장할 데이터 준비 + /* ------------------------------------------------------------------ */ + /* 3. Buffers for bulk insert */ + /* ------------------------------------------------------------------ */ const mappingsToSave: TagTypeClassFormMapping[] = []; const formMetasToSave: FormMeta[] = []; const formsToSave: FormRecord[] = []; - - // 폼이 있는 contractItemId 트래킹 const contractItemIdsWithForms = new Set<number>(); + const templateDataByFormCode: Map<string, TemplateItem[]> = new Map(); - // 각 register 처리 - for (const register of registers) { - // 삭제된 register 건너뛰기 - if (register.DELETED) continue; + /* ------------------------------------------------------------------ */ + /* 4. Iterate over newRegisters */ + /* ------------------------------------------------------------------ */ + for (const newReg of newRegisters) { + const formCode = newReg.REG_TYPE_ID; + const legacy = registerMap.get(formCode); - // REMARK에서 itemCodes 추출 + /* ---------- 4‑a. templates ------------------------------------ */ + let templates: TemplateItem[] = []; + try { + const fetched = await fetchTemplateFromSEDP(projectCode, formCode); + templates = fetched.filter(t => (newReg.MAP_TMPLS?.length ? newReg.MAP_TMPLS.includes(t.TMPL_ID) : true)); + if (templates.length) templateDataByFormCode.set(formCode, templates); + } catch (e) { + console.warn(`템플릿 가져오기 실패 (${formCode})`, e); + } + const templateAttrMap = new Map<string, { hidden: boolean; seq: number; head: string }>(); + templates.forEach(t => t.GRD_LST_SETUP?.ATTS?.forEach(att => { + if (!templateAttrMap.has(att.ATT_ID)) templateAttrMap.set(att.ATT_ID, { hidden: att.HIDN_YN, seq: att.SEQ, head: att.HEAD_TEXT }); + })); - // 폼 메타용 columns 구성 + /* ---------- 4‑b. columns -------------------------------------- */ const columns: FormColumn[] = []; - - for (const linkAtt of register.LNK_ATT) { - let attribute = null; - - // 기본 속성인지 확인 - if (defaultAttributes && defaultAttributes.includes(linkAtt.ATT_ID)) { - // 기본 속성에 대한 기본 attribute 객체 생성 - attribute = { - DESC: linkAtt.ATT_ID, - VAL_TYPE: 'STRING' - }; - } else { - // 맵에서 속성 조회 - attribute = attributeMap.get(linkAtt.ATT_ID); - - // 속성을 찾지 못한 경우 다음으로 넘어감 - if (!attribute) continue; + for (const mapAtt of newReg.MAP_ATT) { + const attId = mapAtt.SEDP_ATT_ID; + const attribute = defaultAttributes.includes(attId) ? { DESC: attId, VAL_TYPE: "STRING" } as Partial<Attribute> : attributeMap.get(attId); + if (!attribute) continue; + + const tmplMeta = templateAttrMap.get(attId); + const isShi = mapAtt.INOUT === "OUT"; + + let uomSymbol: string | undefined; let uomId: string | undefined; + if (legacy?.LNK_ATT) { + const l = legacy.LNK_ATT.find(a => a.ATT_ID === attId); + if (l?.UOM_ID) { const u = uomMap.get(l.UOM_ID); if (u) { uomSymbol = u.SYMBOL; uomId = u.UOM_ID; } } } - // 컬럼 정보 생성 - const column: FormColumn = { - key: linkAtt.ATT_ID, - label: attribute.DESC, - type: (attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST') - ? 'LIST' - : (attribute.VAL_TYPE || 'STRING'), - shi: attribute.REMARK?.toLocaleLowerCase() === "shi" + const col: FormColumn = { + key: attId, + label: attribute.DESC as string, + type: (attribute.VAL_TYPE === "LIST" || attribute.VAL_TYPE === "DYNAMICLIST") ? "LIST" : (attribute.VAL_TYPE || "STRING"), + shi: isShi, + hidden: tmplMeta?.hidden ?? false, + seq: tmplMeta?.seq ?? 0, + head: tmplMeta?.head ?? "", + ...(uomSymbol ? { uom: uomSymbol, uomId } : {}) }; - // 리스트 타입에 대한 옵션 추가 (기본 속성이 아닌 경우) - if (!defaultAttributes.includes(linkAtt.ATT_ID) && - (attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST') && - attribute.CL_ID) { - - // 맵에서 코드 리스트 조회 - const codeList = codeListMap.get(attribute.CL_ID); - - if (codeList && codeList.VALUES) { - const options = [...new Set( - codeList.VALUES - .filter(value => value.USE_YN) - .map(value => value.VALUE) - )]; - - if (options.length > 0) { - column.options = options; - } - } + if (!defaultAttributes.includes(attId) && (attribute.VAL_TYPE === "LIST" || attribute.VAL_TYPE === "DYNAMICLIST") && attribute.CL_ID) { + const cl = codeListMap.get(attribute.CL_ID); + if (cl?.VALUES?.length) col.options = [...new Set(cl.VALUES.filter(v => v.USE_YN).map(v => v.VALUE))]; } - // UOM 정보 추가 - if (linkAtt.UOM_ID) { - const uom = uomMap.get(linkAtt.UOM_ID); - - if (uom) { - column.uom = uom.SYMBOL; - column.uomId = uom.UOM_ID; - } - } - - columns.push(column); - } - - // 컬럼이 없으면 건너뛰기 - if (columns.length === 0) { - console.log(`폼 ${register.TYPE_ID} (${register.DESC})에 컬럼이 없어 건너뜁니다`); - continue; + columns.push(col); } + if (!columns.length) { console.log(`폼 ${formCode} 건너뜀 (컬럼 없음)`); continue; } + columns.sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999)); - // 폼 메타 데이터 준비 - formMetasToSave.push({ - projectId, - formCode: register.TYPE_ID, - formName: register.DESC, - columns: JSON.stringify(columns), - createdAt: new Date(), - updatedAt: new Date() - }); - - // 클래스 매핑 처리 - for (const classId of register.MAP_CLS_ID) { - const tagClass = tagClassMap.get(classId); - - if (!tagClass) { - console.warn(`프로젝트 ID ${projectId}에서 클래스 ID ${classId}를 찾을 수 없습니다`); - continue; - } - - const tagTypeCode = tagClass.tagTypeCode; - const tagType = tagTypeMap.get(tagTypeCode); - - if (!tagType) { - console.warn(`프로젝트 ID ${projectId}에서 태그 타입 ${tagTypeCode}를 찾을 수 없습니다`); - continue; - } + formMetasToSave.push({ projectId, formCode, formName: legacy?.DESC || formCode, columns: JSON.stringify(columns), createdAt: new Date(), updatedAt: new Date() }); - // 매핑 정보 저장 - mappingsToSave.push({ - projectId, - tagTypeLabel: tagType.description, - classLabel: tagClass.label, - formCode: register.TYPE_ID, - formName: register.DESC, - remark: register.REMARK, - ep: register.EP_ID, - createdAt: new Date(), - updatedAt: new Date() - }); + /* ---------- 4‑c. class mappings -------------------------------- */ + const classIds = new Set<string>(); + if (newReg.MAP_CLS?.ITEMS?.length) { + newReg.MAP_CLS.ITEMS.forEach(it => classIds.add(it.SEDP_OBJ_CLS_ID)); } - const itemCodes = extractItemCodes(register.REMARK || ''); - if (!itemCodes.length) { - console.log(`Register ${register.TYPE_ID} (${register.DESC})의 REMARK에 유효한 itemCode가 없습니다`); - continue; - } - // 폼 레코드 준비 - for (const itemCode of itemCodes) { - const contractItemId = itemCodeToContractItemId.get(itemCode); - - if (!contractItemId) { - console.warn(`itemCode: ${itemCode}에 대한 contractItemId를 찾을 수 없습니다`); - continue; - } - - // 폼이 있는 contractItemId 추적 - contractItemIdsWithForms.add(contractItemId); + classIds.forEach(classId => { + const cls = tagClassMap.get(classId); + if (!cls) { console.warn(`클래스 ${classId} 없음`); return; } + const tp = tagTypeMap.get(cls.tagTypeCode); + if (!tp) { console.warn(`태그 타입 ${cls.tagTypeCode} 없음`); return; } + mappingsToSave.push({ projectId, tagTypeLabel: tp.description, classLabel: cls.label, formCode, formName: legacy?.DESC || formCode, remark: newReg.TOOL_TYPE || null, ep: newReg.EP_ID || legacy?.EP_ID || "", createdAt: new Date(), updatedAt: new Date() }); + }); - formsToSave.push({ - contractItemId, - formCode: register.TYPE_ID, - formName: register.DESC, - eng: true, - createdAt: new Date(), - updatedAt: new Date() - }); + /* ---------- 4‑d. contractItem ↔ form --------------------------- */ + if (newReg.TOOL_TYPE) { + const cId = itemCodeToContractItemId.get(newReg.TOOL_TYPE); + if (cId) { contractItemIdsWithForms.add(cId); formsToSave.push({ contractItemId: cId, formCode, formName: legacy?.DESC || formCode, eng: true, createdAt: new Date(), updatedAt: new Date() }); } + else console.warn(`itemCode ${newReg.TOOL_TYPE} 의 contractItemId 없음`); } } - // 트랜잭션으로 모든 작업 처리 + /* ------------------------------------------------------------------ */ + /* 5. DB transaction */ + /* ------------------------------------------------------------------ */ let totalSaved = 0; - - await db.transaction(async (tx) => { - // 기존 데이터 삭제 + await db.transaction(async tx => { + const old = await tx.select({ id: tagTypeClassFormMappings.id }).from(tagTypeClassFormMappings).where(eq(tagTypeClassFormMappings.projectId, projectId)); + if (old.length) await tx.delete(templateItems).where(inArray(templateItems.formMappingId, old.map(o => o.id))); await tx.delete(tagTypeClassFormMappings).where(eq(tagTypeClassFormMappings.projectId, projectId)); await tx.delete(formMetas).where(eq(formMetas.projectId, projectId)); + if (contractItemIdsWithForms.size) await tx.delete(forms).where(inArray(forms.contractItemId, [...contractItemIdsWithForms])); - // 해당 contractItemId에 대한 기존 폼 삭제 - if (contractItemIdsWithForms.size > 0) { - await tx.delete(forms).where(inArray(forms.contractItemId, [...contractItemIdsWithForms])); - } - - // 매핑 저장 - if (mappingsToSave.length > 0) { - await tx.insert(tagTypeClassFormMappings).values(mappingsToSave); - totalSaved += mappingsToSave.length; - console.log(`프로젝트 ID ${projectId}에 대해 ${mappingsToSave.length}개의 태그 타입-클래스-폼 매핑을 저장했습니다`); - } + const savedMappings = mappingsToSave.length ? await tx.insert(tagTypeClassFormMappings).values(mappingsToSave).returning({ id: tagTypeClassFormMappings.id, formCode: tagTypeClassFormMappings.formCode }) : []; + totalSaved += mappingsToSave.length; - // 폼 메타 저장 - if (formMetasToSave.length > 0) { - await tx.insert(formMetas).values(formMetasToSave); - totalSaved += formMetasToSave.length; - console.log(`프로젝트 ID ${projectId}에 대해 ${formMetasToSave.length}개의 폼 메타 레코드를 저장했습니다`); + if (savedMappings.length) { + const rows: any[] = []; + savedMappings.forEach(m => (templateDataByFormCode.get(m.formCode) || []).forEach(t => rows.push({ formMappingId: m.id, tmplId: t.TMPL_ID, name: t.NAME, tmplType: t.TMPL_TYPE, sprLstSetup: t.SPR_LST_SETUP, grdLstSetup: t.GRD_LST_SETUP, sprItmLstSetup: t.SPR_ITM_LST_SETUP, description: `Template for form ${m.formCode}`, isActive: true, createdAt: new Date(), updatedAt: new Date() }))); + if (rows.length) { await tx.insert(templateItems).values(rows); totalSaved += rows.length; } } - // 폼 레코드 저장 - if (formsToSave.length > 0) { - await tx.insert(forms).values(formsToSave); - totalSaved += formsToSave.length; - console.log(`프로젝트 ID ${projectId}에 대해 ${formsToSave.length}개의 폼 레코드를 저장했습니다`); - } + if (formMetasToSave.length) { await tx.insert(formMetas).values(formMetasToSave); totalSaved += formMetasToSave.length; } + if (formsToSave.length) { await tx.insert(forms).values(formsToSave); totalSaved += formsToSave.length; } }); return totalSaved; - } catch (error) { - console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID: ${projectId}):`, error); - throw error; + } catch (err) { + console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID:${projectId})`, err); + throw err; } } + // 메인 동기화 함수 export async function syncTagFormMappings() { try { @@ -973,9 +1081,10 @@ export async function syncTagFormMappings() { try { // 레지스터 데이터 가져오기 const registers = await getRegisters(project.code); + const newRegisters = await getNewRegisters(project.code); // 데이터베이스에 저장 - const count = await saveFormMappingsAndMetas(project.id, project.code, registers); + const count = await saveFormMappingsAndMetas(project.id, project.code, registers, newRegisters); return { project: project.code, success: true, |
