// src/lib/cron/syncTagFormMappings.ts import db from "@/db/db"; 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"; // 환경 변수 const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; // 인터페이스 정의 interface TagTypeClassFormMapping { projectId: number; tagTypeLabel: string; classLabel: string; formCode: string; formName: string; remark: string | null; ep: string; createdAt: Date; updatedAt: Date; } interface FormMeta { projectId: number; formCode: string; formName: string; columns: string; // JSON 문자열 createdAt: Date; updatedAt: Date; } interface FormRecord { contractItemId: number; formCode: string; formName: string; eng: boolean; createdAt: Date; updatedAt: Date; } interface TemplateItem { TMPL_ID: string; NAME: string; TMPL_TYPE: string; SPR_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; 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; 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; 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; EP_ID: string; DESC: string; REMARK: string | null; NEW_TAG_YN: boolean; ALL_TAG_YN: boolean; VND_YN: boolean; SEQ: number; CMPLX_YN: boolean; CMPL_SETT: any | null; MAP_ATT: any[]; MAP_CLS_ID: string[]; MAP_OPER: any | null; LNK_ATT: LinkAttribute[]; JOIN_TABLS: any[]; DELETED: boolean; CRTER_NO: string; CRTE_DTM: string; CHGER_NO: string | null; CHGE_DTM: string | null; _id: string; } interface LinkAttribute { ATT_ID: string; CPY_DESC: string; JOIN_KEY_ATT_ID: string | null; JOIN_VAL_ATT_ID: string | null; KEY_YN: boolean; EDIT_YN: boolean; PUB_YN: boolean; VND_YN: boolean; DEF_VAL: string | null; 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; DESC: string; GROUP: string | null; REMARK: string | null; VAL_TYPE: string; IGN_LIST_VAL: boolean; CL_ID: string | null; UOM_ID: string | null; DEF_VAL: string | null; MIN_VAL: number; MAX_VAL: number; ESS_YN: boolean; SEQ: number; FORMAT: string | null; REG_EXPS: string | null; ATTRIBUTES: any[]; DELETED: boolean; CRTER_NO: string; CRTE_DTM: string; CHGER_NO: string | null; CHGE_DTM: string | null; _id: string; } interface CodeList { PROJ_NO: string; CL_ID: string; DESC: string; REMARK: string | null; PRNT_CD_ID: string | null; REG_TYPE_ID: string | null; VAL_ATT_ID: string | null; VALUES: CodeValue[]; LNK_ATT: any[]; DELETED: boolean; CRTER_NO: string; CRTE_DTM: string; CHGER_NO: string | null; CHGE_DTM: string | null; _id: string; } interface CodeValue { PRNT_VALUE: string | null; VALUE: string; DESC: string; REMARK: string; USE_YN: boolean; SEQ: number; ATTRIBUTES: any[]; } interface UOM { PROJ_NO: string; UOM_ID: string; DESC: string; SYMBOL: string; CONV_RATE: number; DELETED: boolean; CRTER_NO: string; CRTE_DTM: string; CHGER_NO: string | null; CHGE_DTM: string | null; _id: string; } interface Project { id: number; code: string; name: string; type?: string; createdAt?: Date; updatedAt?: Date; } interface SyncResult { project: string; success: boolean; count?: number; error?: string; } interface FormColumn { key: string; label: string; type: string; options?: string[]; uom?: string; uomId?: string; shi?: Boolean; hidden?: boolean; seq?: number; head?: string; } // 아이템 코드 추출 함수 function extractItemCodes(remark: string | null): string[] { if (!remark) return []; // 검색용으로만 소문자로 변환 const remarkLower = remark.toLowerCase(); // 'vd_' 접두사 확인 const hasVD_ = remarkLower.includes("vd_"); if (!hasVD_) return []; let vdPart = ""; // 'vd_'가 있으면 원본 문자열에서 추출 (소문자 버전이 아님) if (hasVD_) { const vdIndex = remarkLower.indexOf("vd_"); vdPart = remark.substring(vdIndex + 3); // 원본 문자열에서 추출 } if (!vdPart) return []; // 쉼표로 구분된 여러 itemCode 처리 return vdPart.split(",").map(code => code.trim()); } async function getDefaulTAttributes(): Promise { try { const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/Dictionary/GetByKey`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, }, body: JSON.stringify({ Key: "DefaultAttributesToCompare", }) } ); if (!response.ok) { if (response.status === 404) { console.warn(`디폴트 속성 찾을 수 없음`); return []; } throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`); } // 안전하게 JSON 파싱 try { const data = await response.json(); // 데이터가 배열인지 확인하고 문자열 배열로 변환 if (Array.isArray(data)) { return data as string[]; } else { console.warn('응답이 배열 형식이 아닙니다'); return []; } } 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) { console.error(`디폴트 어트리뷰트 가져오기 실패:`, error); throw error; } } async function fetchTemplateFromSEDP(projectCode: string, formCode: string): Promise { 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 { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); 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)}`); } // 결과를 배열로 변환 (단일 객체인 경우 배열로 래핑) let registers: Register[] = Array.isArray(data) ? data : [data]; // MAP_CLS_ID가 비어있지 않고 REMARK가 vd, VD, vD, Vd 중 하나인 레지스터만 필터링 registers = registers.filter(register => { // 삭제된 레지스터 제외 if (register.DELETED) return false; // MAP_CLS_ID 배열이 존재하고 요소가 하나 이상 있는지 확인 const hasValidMapClsId = Array.isArray(register.MAP_CLS_ID) && register.MAP_CLS_ID.length > 0; // REMARK가 'vd_' 또는 'vd' 포함 확인 (대소문자 구분 없이) const remarkLower = register.REMARK && register.REMARK.toLowerCase(); const hasValidRemark = remarkLower && (remarkLower.includes('vd')); // 두 조건 모두 충족해야 함 return hasValidMapClsId && hasValidRemark; }); console.log(`프로젝트 ${projectCode}에서 ${registers.length}개의 유효한 레지스터를 가져왔습니다.`); return registers; } catch (error) { console.error(`프로젝트 ${projectCode}의 레지스터 가져오기 실패:`, error); throw error; } } async function getNewRegisters(projectCode: string): Promise { 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> { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/Attributes/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 파싱 try { const data = await response.json(); // 데이터가 배열인지 확인 const attributes: Attribute[] = Array.isArray(data) ? data : [data]; // ATT_ID로 효율적인 조회를 위한 맵 생성 const attributeMap = new Map(); for (const attribute of attributes) { if (!attribute.DELETED) { attributeMap.set(attribute.ATT_ID, attribute); } } console.log(`프로젝트 ${projectCode}에서 ${attributeMap.size}개의 속성을 가져왔습니다`); return attributeMap; } catch (parseError) { console.error(`프로젝트 ${projectCode}의 속성 응답 파싱 실패:`, parseError); // 응답 내용 로깅 try { const text = await response.clone().text(); console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); } catch (textError) { console.error('응답 내용 로깅 실패:', textError); } return new Map(); } } catch (error) { console.error(`프로젝트 ${projectCode}의 속성 가져오기 실패:`, error); return new Map(); } } // 특정 속성 가져오기 (하위 호환성을 위해 유지) async function getAttributeById(projectCode: string, attributeId: string, register: string): Promise { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/Attributes/GetByID`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo: projectCode, ATT_ID: attributeId, ContainDeleted: false }) } ); if (!response.ok) { if (response.status === 404) { console.warn(`속성 ID ${attributeId}를 찾을 수 없음`); return null; } throw new Error(`속성 요청 실패: ${response.status} ${response.statusText}`); } // 안전하게 JSON 파싱 try { const data = await response.json(); return data; } catch (parseError) { console.error(`속성 ID ${attributeId} ${register} ${projectCode} 응답 파싱 실패:`, parseError); // 응답 내용 로깅 try { const text = await response.clone().text(); console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); } catch (textError) { console.error('응답 내용 로깅 실패:', textError); } return null; } } catch (error) { console.error(`속성 ID ${attributeId} 가져오기 실패:`, error); return null; } } // 프로젝트의 모든 코드 리스트를 가져와 맵으로 반환 async function getCodeLists(projectCode: string): Promise> { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/CodeList/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 파싱 try { const data = await response.json(); // 데이터가 배열인지 확인 const codeLists: CodeList[] = Array.isArray(data) ? data : [data]; // CL_ID로 효율적인 조회를 위한 맵 생성 const codeListMap = new Map(); for (const codeList of codeLists) { if (!codeList.DELETED) { codeListMap.set(codeList.CL_ID, codeList); } } console.log(`프로젝트 ${projectCode}에서 ${codeListMap.size}개의 코드 리스트를 가져왔습니다`); return codeListMap; } catch (parseError) { console.error(`프로젝트 ${projectCode}의 코드 리스트 응답 파싱 실패:`, parseError); // 응답 내용 로깅 try { const text = await response.clone().text(); console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); } catch (textError) { console.error('응답 내용 로깅 실패:', textError); } return new Map(); } } catch (error) { console.error(`프로젝트 ${projectCode}의 코드 리스트 가져오기 실패:`, error); return new Map(); } } // 특정 코드 리스트 가져오기 (하위 호환성을 위해 유지) async function getCodeListById(projectCode: string, codeListId: string): Promise { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/CodeList/GetByID`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo: projectCode, CL_ID: codeListId, ContainDeleted: false }) } ); if (!response.ok) { if (response.status === 404) { console.warn(`코드 리스트 ID ${codeListId}를 찾을 수 없음`); return null; } throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`); } // 안전하게 JSON 파싱 try { const data = await response.json(); return data; } catch (parseError) { console.error(`코드 리스트 ID ${codeListId} 응답 파싱 실패:`, parseError); // 응답 내용 로깅 try { const text = await response.clone().text(); console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); } catch (textError) { console.error('응답 내용 로깅 실패:', textError); } return null; } } catch (error) { console.error(`코드 리스트 ID ${codeListId} 가져오기 실패:`, error); return null; } } // 프로젝트의 모든 UOM을 가져와 맵으로 반환 async function getUOMs(projectCode: string): Promise> { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/UOM/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(`UOM 요청 실패: ${response.status} ${response.statusText}`); } // 안전하게 JSON 파싱 try { const data = await response.json(); // 데이터가 배열인지 확인 const uoms: UOM[] = Array.isArray(data) ? data : [data]; // UOM_ID로 효율적인 조회를 위한 맵 생성 const uomMap = new Map(); for (const uom of uoms) { if (!uom.DELETED) { uomMap.set(uom.UOM_ID, uom); } } console.log(`프로젝트 ${projectCode}에서 ${uomMap.size}개의 UOM을 가져왔습니다`); return uomMap; } catch (parseError) { console.error(`프로젝트 ${projectCode}의 UOM 응답 파싱 실패:`, parseError); // 응답 내용 로깅 try { const text = await response.clone().text(); console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); } catch (textError) { console.error('응답 내용 로깅 실패:', textError); } return new Map(); } } catch (error) { console.error(`프로젝트 ${projectCode}의 UOM 가져오기 실패:`, error); return new Map(); } } // UOM 가져오기 (하위 호환성을 위해 유지) async function getUomById(projectCode: string, uomId: string): Promise { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/UOM/GetByID`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, 'ProjectNo': projectCode }, body: JSON.stringify({ UOMID: uomId, // API 명세서에 따라 UOMID 사용 ProjectNo: projectCode, ContainDeleted: false }) } ); if (!response.ok) { if (response.status === 404) { console.warn(`UOM ID ${uomId}를 찾을 수 없음`); return null; } throw new Error(`UOM 요청 실패: ${response.status} ${response.statusText}`); } // 안전하게 JSON 파싱 try { const data = await response.json(); return data; } catch (parseError) { console.error(`UOM ID ${uomId} 응답 파싱 실패:`, parseError); // 응답 내용 로깅 try { const text = await response.clone().text(); console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); } catch (textError) { console.error('응답 내용 로깅 실패:', textError); } return null; } } catch (error) { console.error(`UOM ID ${uomId} 가져오기 실패:`, error); return null; } } // contractItemId 조회 함수 async function getContractItemsByItemCodes(itemCodes: string[], projectId: number): Promise> { try { if (!itemCodes.length) return new Map(); // 먼저 itemCodes에 해당하는 item 레코드를 조회 const itemRecords = await db.select({ id: items.id, itemCode: items.itemCode, packageCode: items.packageCode }) .from(items) .where(inArray(items.packageCode, itemCodes)); if (!itemRecords.length) { console.log(`No items found for itemCodes: ${itemCodes.join(', ')}`); return new Map(); } // item ID 목록 추출 const itemIds = itemRecords.map(item => item.id); // contracts와 join하여 projectId로 필터링하면서 contractItems 조회 const contractItemRecords = await db.select({ id: contractItems.id, itemId: contractItems.itemId }) .from(contractItems) .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) .where( and( inArray(contractItems.itemId, itemIds), eq(contracts.projectId, projectId) ) ); // itemCode와 contractItemId 배열의 매핑 생성 const itemCodeToContractItemIds = new Map(); for (const item of itemRecords) { // itemCode가 null이 아닌 경우에만 처리 if (item.packageCode) { const matchedContractItems = contractItemRecords.filter(ci => ci.itemId === item.id); if (matchedContractItems.length > 0) { // 일치하는 모든 contractItem을 배열로 저장 const contractItemIds = matchedContractItems.map(ci => ci.id); itemCodeToContractItemIds.set(item.packageCode, contractItemIds); } } } return itemCodeToContractItemIds; } catch (error) { console.error('ContractItems 조회 중 오류 발생:', error); return new Map(); } } // 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[], // legacy SEDP Register list (supplemental) newRegisters: newRegister[] // AdapterDataMapping list (primary) ): Promise { try { /* ------------------------------------------------------------------ */ /* 1. Prepare look‑up structures & common data */ /* ------------------------------------------------------------------ */ 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 registerMap = new Map(registers.map(r => [r.TYPE_ID, r])); const attributeMap = await getAttributes(projectCode); const codeListMap = await getCodeLists(projectCode); const uomMap = await getUOMs(projectCode); const defaultAttributes = await getDefaulTAttributes(); /* ------------------------------------------------------------------ */ /* 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 itemCodeToContractItemIds = await getContractItemsByItemCodes(uniqueItemCodes, projectId); /* ------------------------------------------------------------------ */ /* 3. Buffers for bulk insert */ /* ------------------------------------------------------------------ */ const mappingsToSave: TagTypeClassFormMapping[] = []; const formMetasToSave: FormMeta[] = []; const formsToSave: FormRecord[] = []; const contractItemIdsWithForms = new Set(); const templateDataByFormCode: Map = new Map(); /* ------------------------------------------------------------------ */ /* 4. Iterate over newRegisters */ /* ------------------------------------------------------------------ */ for (const newReg of newRegisters) { const formCode = newReg.REG_TYPE_ID; const legacy = registerMap.get(formCode); /* ---------- 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(); 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 }); })); /* ---------- 4‑b. columns -------------------------------------- */ const columns: FormColumn[] = []; 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 : 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 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(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))]; } columns.push(col); } if (!columns.length) { console.log(`폼 ${formCode} 건너뜀 (컬럼 없음)`); continue; } columns.sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999)); formMetasToSave.push({ projectId, formCode, formName: legacy?.DESC || formCode, columns: JSON.stringify(columns), createdAt: new Date(), updatedAt: new Date() }); /* ---------- 4‑c. class mappings -------------------------------- */ const classIds = new Set(); if (newReg.MAP_CLS?.ITEMS?.length) { newReg.MAP_CLS.ITEMS.forEach(it => classIds.add(it.SEDP_OBJ_CLS_ID)); } 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() }); }); /* ---------- 4‑d. contractItem ↔ form - 수정된 부분 -------------- */ if (newReg.TOOL_TYPE) { const contractItemIds = itemCodeToContractItemIds.get(newReg.TOOL_TYPE); if (contractItemIds && contractItemIds.length > 0) { // 모든 contractItemId에 대해 form 생성 contractItemIds.forEach(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 => { 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])); const savedMappings = mappingsToSave.length ? await tx.insert(tagTypeClassFormMappings).values(mappingsToSave).onConflictDoNothing().returning({ id: tagTypeClassFormMappings.id, formCode: tagTypeClassFormMappings.formCode }) : []; totalSaved += mappingsToSave.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).onConflictDoNothing(); totalSaved += rows.length; } } if (formMetasToSave.length) { await tx.insert(formMetas).values(formMetasToSave).onConflictDoNothing(); totalSaved += formMetasToSave.length; } if (formsToSave.length) { await tx.insert(forms).values(formsToSave).onConflictDoNothing(); totalSaved += formsToSave.length; } }); return totalSaved; } catch (err) { console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID:${projectId})`, err); throw err; } } // 메인 동기화 함수 export async function syncTagFormMappings() { try { console.log('태그 폼 매핑 동기화 시작:', new Date().toISOString()); // 모든 프로젝트 가져오기 const allProjects = await db.select().from(projects); // 각 프로젝트에 대해 폼 매핑 동기화 const results = await Promise.allSettled( allProjects.map(async (project: Project) => { try { // 레지스터 데이터 가져오기 const registers = await getRegisters(project.code); const newRegisters = await getNewRegisters(project.code); // 데이터베이스에 저장 const count = await saveFormMappingsAndMetas(project.id, project.code, registers, newRegisters); return { project: project.code, success: true, count } as SyncResult; } catch (error) { console.error(`프로젝트 ${project.code} 폼 매핑 동기화 실패:`, error); return { project: project.code, success: false, error: error instanceof Error ? error.message : String(error) } as SyncResult; } }) ); // 결과 처리를 위한 배열 준비 const successfulResults: SyncResult[] = []; const failedResults: SyncResult[] = []; // 결과 분류 results.forEach((result) => { if (result.status === 'fulfilled') { if (result.value.success) { successfulResults.push(result.value); } else { failedResults.push(result.value); } } else { // 거부된 프로미스는 실패로 간주 failedResults.push({ project: 'unknown', success: false, error: result.reason?.toString() || 'Unknown error' }); } }); const successCount = successfulResults.length; const failCount = failedResults.length; // 이제 안전하게 count 속성에 접근 가능 const totalItems = successfulResults.reduce((sum, result) => sum + (result.count || 0), 0 ); console.log(`태그 폼 매핑 동기화 완료: ${successCount}개 프로젝트 성공 (총 ${totalItems}개 항목), ${failCount}개 프로젝트 실패`); return { success: successCount, failed: failCount, items: totalItems, timestamp: new Date().toISOString() }; } catch (error) { console.error('태그 폼 매핑 동기화 중 오류 발생:', error); throw error; } }