// src/lib/cron/syncTagFormMappings.ts import db from "@/db/db"; import { projects, tagTypes, tagClasses, tagTypeClassFormMappings, formMetas, forms, contractItems, items, contracts } 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 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 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; } // 아이템 코드 추출 함수 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 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 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 }) .from(items) .where(inArray(items.itemCode, 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 itemCodeToContractItemId = new Map(); for (const item of itemRecords) { // itemCode가 null이 아닌 경우에만 처리 if (item.itemCode) { const matchedContractItems = contractItemRecords.filter(ci => ci.itemId === item.id); if (matchedContractItems.length > 0) { // 일치하는 첫 번째 contractItem 사용 itemCodeToContractItemId.set(item.itemCode, matchedContractItems[0].id); } } } return itemCodeToContractItemId; } catch (error) { console.error('ContractItems 조회 중 오류 발생:', error); return new Map(); } } // 데이터베이스에 태그 타입 클래스 폼 매핑 및 폼 메타 저장 async function saveFormMappingsAndMetas( projectId: number, projectCode: string, registers: Register[] ): Promise { try { // 프로젝트의 태그 타입과 클래스 가져오기 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(type => [type.code, type])); const tagClassMap = new Map(tagClassRecords.map(cls => [cls.code, cls])); // 모든 속성, 코드 리스트, UOM을 한 번에 가져와 반복 API 호출 방지 const attributeMap = await getAttributes(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을 찾았습니다`); // 저장할 데이터 준비 const mappingsToSave: TagTypeClassFormMapping[] = []; const formMetasToSave: FormMeta[] = []; const formsToSave: FormRecord[] = []; // 폼이 있는 contractItemId 트래킹 const contractItemIdsWithForms = new Set(); // 각 register 처리 for (const register of registers) { // 삭제된 register 건너뛰기 if (register.DELETED) continue; // REMARK에서 itemCodes 추출 // 폼 메타용 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; } // 컬럼 정보 생성 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" }; // 리스트 타입에 대한 옵션 추가 (기본 속성이 아닌 경우) 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; } } } // 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; } // 폼 메타 데이터 준비 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; } // 매핑 정보 저장 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() }); } 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); formsToSave.push({ contractItemId, formCode: register.TYPE_ID, formName: register.DESC, eng: true, createdAt: new Date(), updatedAt: new Date() }); } } // 트랜잭션으로 모든 작업 처리 let totalSaved = 0; await db.transaction(async (tx) => { // 기존 데이터 삭제 await tx.delete(tagTypeClassFormMappings).where(eq(tagTypeClassFormMappings.projectId, projectId)); await tx.delete(formMetas).where(eq(formMetas.projectId, projectId)); // 해당 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}개의 태그 타입-클래스-폼 매핑을 저장했습니다`); } // 폼 메타 저장 if (formMetasToSave.length > 0) { await tx.insert(formMetas).values(formMetasToSave); totalSaved += formMetasToSave.length; console.log(`프로젝트 ID ${projectId}에 대해 ${formMetasToSave.length}개의 폼 메타 레코드를 저장했습니다`); } // 폼 레코드 저장 if (formsToSave.length > 0) { await tx.insert(forms).values(formsToSave); totalSaved += formsToSave.length; console.log(`프로젝트 ID ${projectId}에 대해 ${formsToSave.length}개의 폼 레코드를 저장했습니다`); } }); return totalSaved; } catch (error) { console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID: ${projectId}):`, error); throw error; } } // 메인 동기화 함수 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 count = await saveFormMappingsAndMetas(project.id, project.code, registers); 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; } }