import db from "@/db/db"; import { contractItems, tags, forms, items, tagTypeClassFormMappings, projects, tagTypes, tagClasses, formMetas, formEntries, contracts, vendors } from "@/db/schema"; import { eq, and, like, inArray } from "drizzle-orm"; import { getSEDPToken } from "./sedp-token"; import { getFormMappingsByTagTypebyProeject } from "../tags/form-mapping-service"; interface Attribute { ATT_ID: string; VALUE: any; VALUE_DBL: number; UOM_ID: string | null; } interface TagEntry { TAG_IDX: string; TAG_NO: string; BF_TAG_NO: string; TAG_DESC: string; EP_ID: string; TAG_TYPE_ID: string; CLS_ID: string; ATTRIBUTES: Attribute[]; [key: string]: any; } interface Column { key: string; label: string; type: string; shi?: 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; SCOPES: 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; DUE_DATE: string; //"YYYY-MM-DDTHH:mm:ssZ" INOUT: string | null; } async function getNewRegisters(projectCode: string): Promise { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; const response = await fetch( `${SEDP_API_BASE_URL}/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; } } /** * 태그 가져오기 서비스 함수 * contractItemId(packageId)를 기반으로 외부 시스템에서 태그 데이터를 가져와 DB에 저장 * formEntries와 tags 테이블 모두에 데이터를 저장 */ export async function importTagsFromSEDP( formCode: string, projectCode: string, packageId: number, progressCallback?: (progress: number) => void ): Promise<{ processedCount: number; excludedCount: number; totalEntries: number; formCreated?: boolean; errors?: string[]; }> { try { // 진행 상황 보고 if (progressCallback) progressCallback(5); // 에러 수집 배열 const errors: string[] = []; // SEDP API에서 태그 데이터 가져오기 const tagData = await fetchTagDataFromSEDP(projectCode, formCode); const newRegisters = await getNewRegisters(projectCode); const registerMatched = newRegisters.find(v => v.REG_TYPE_ID === formCode).MAP_ATT // 트랜잭션으로 모든 DB 작업 처리 return await db.transaction(async (tx) => { // 프로젝트 정보 가져오기 (type 포함) const projectRecord = await tx.select({ id: projects.id, type: projects.type }) .from(projects) .where(eq(projects.code, projectCode)) .limit(1); if (!projectRecord || projectRecord.length === 0) { throw new Error(`Project not found for code: ${projectCode}`); } const projectId = projectRecord[0].id; const projectType = projectRecord[0].type; // 프로젝트 타입에 따라 packageCode를 찾을 ATT_ID 결정 const packageCodeAttId = projectType === "ship" ? "CM3003" : "ME5074"; // packageId로 contractItem과 item 정보 가져오기 const contractItemRecord = await tx.select({ itemId: contractItems.itemId, contractId: contractItems.contractId }) .from(contractItems) .where(eq(contractItems.id, packageId)) .limit(1); if (!contractItemRecord || contractItemRecord.length === 0) { throw new Error(`Contract item not found for packageId: ${packageId}`); } const contractRecord = await tx.select({ vendorId: contracts.vendorId }) .from(contracts) .where(eq(contracts.id, contractItemRecord[0].contractId)) .limit(1); const vendorRecord = await tx.select({ vendorCode: vendors.vendorCode, vendorName: vendors.vendorName }) .from(vendors) .where(eq(vendors.id, contractRecord[0].vendorId)) .limit(1); const itemRecord = await tx.select({ packageCode: items.packageCode }) .from(items) .where(eq(items.id, contractItemRecord[0].itemId)) .limit(1); if (!itemRecord || itemRecord.length === 0) { throw new Error(`Item not found for itemId: ${contractItemRecord[0].itemId}`); } const targetPackageCode = itemRecord[0].packageCode; // 데이터 형식 처리 - tagData의 첫 번째 키 사용 const tableName = Object.keys(tagData)[0]; if (!tableName || !tagData[tableName]) { throw new Error("Invalid tag data format from SEDP API"); } const allTagEntries: TagEntry[] = tagData[tableName]; if (!Array.isArray(allTagEntries) || allTagEntries.length === 0) { return { processedCount: 0, excludedCount: 0, totalEntries: 0, errors: ["No tag entries found in API response"] }; } // packageCode로 필터링 - ATTRIBUTES에서 지정된 ATT_ID의 VALUE와 packageCode 비교 const tagEntries = allTagEntries.filter(entry => { if (Array.isArray(entry.ATTRIBUTES)) { const packageCodeAttr = entry.ATTRIBUTES.find(attr => attr.ATT_ID === packageCodeAttId); if (packageCodeAttr && packageCodeAttr.VALUE === targetPackageCode) { return true; } } return false; }); if (tagEntries.length === 0) { return { processedCount: 0, excludedCount: 0, totalEntries: allTagEntries.length, errors: [`No tag entries found with ${packageCodeAttId} attribute value matching packageCode: ${targetPackageCode}`] }; } // 진행 상황 보고 if (progressCallback) progressCallback(20); // 나머지 코드는 기존과 동일... // form ID 가져오기 - 없으면 생성 let formRecord = await tx.select({ id: forms.id }) .from(forms) .where(and( eq(forms.formCode, formCode), eq(forms.contractItemId, packageId) )) .limit(1); let formCreated = false; // form이 없으면 생성 if (!formRecord || formRecord.length === 0) { console.log(`[IMPORT TAGS] Form ${formCode} not found, attempting to create...`); // 첫 번째 태그의 정보를 사용해서 form mapping을 찾습니다 // 모든 태그가 같은 formCode를 사용한다고 가정 if (tagEntries.length > 0) { const firstTag = tagEntries[0]; // tagType 조회 (TAG_TYPE_ID -> description) let tagTypeDescription = firstTag.TAG_TYPE_ID; // 기본값 if (firstTag.TAG_TYPE_ID) { const tagTypeRecord = await tx.select({ description: tagTypes.description }) .from(tagTypes) .where(and( eq(tagTypes.code, firstTag.TAG_TYPE_ID), eq(tagTypes.projectId, projectId) )) .limit(1); if (tagTypeRecord && tagTypeRecord.length > 0) { tagTypeDescription = tagTypeRecord[0].description; } } // tagClass 조회 (CLS_ID -> label) let tagClassLabel = firstTag.CLS_ID; // 기본값 if (firstTag.CLS_ID) { const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) .from(tagClasses) .where(and( eq(tagClasses.code, firstTag.CLS_ID), eq(tagClasses.projectId, projectId) )) .limit(1); if (tagClassRecord && tagClassRecord.length > 0) { tagClassLabel = tagClassRecord[0].label; } } // 태그 타입에 따른 폼 정보 가져오기 const allFormMappings = await getFormMappingsByTagTypebyProeject( projectId, ); // 현재 formCode와 일치하는 매핑 찾기 const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode); if (targetFormMapping) { console.log(`[IMPORT TAGS] Found form mapping for ${formCode}, creating form...`); // form 생성 const insertResult = await tx .insert(forms) .values({ contractItemId: packageId, formCode: targetFormMapping.formCode, formName: targetFormMapping.formName, eng: true, // ENG 모드에서 가져오는 것이므로 eng: true im: targetFormMapping.ep === "IMEP" ? true : false }) .returning({ id: forms.id }); formRecord = insertResult; formCreated = true; console.log(`[IMPORT TAGS] Successfully created form:`, insertResult[0]); } else { console.log(`[IMPORT TAGS] No form mapping found for formCode: ${formCode}`); console.log(`[IMPORT TAGS] Available mappings:`, allFormMappings.map(m => m.formCode)); throw new Error(`Form ${formCode} not found and no mapping available for tag type ${tagTypeDescription}`); } } else { throw new Error(`Form not found for formCode: ${formCode} and contractItemId: ${packageId}, and no tags to derive form mapping`); } } else { console.log(`[IMPORT TAGS] Found existing form:`, formRecord[0].id); // 기존 form이 있는 경우 eng와 im 필드를 체크하고 업데이트 const existingForm = await tx.select({ eng: forms.eng, im: forms.im }) .from(forms) .where(eq(forms.id, formRecord[0].id)) .limit(1); if (existingForm.length > 0) { // form mapping 정보 가져오기 (im 필드 업데이트를 위해) let shouldUpdateIm = false; let targetImValue = false; // 첫 번째 태그의 정보를 사용해서 form mapping을 확인 if (tagEntries.length > 0) { const firstTag = tagEntries[0]; // tagType 조회 let tagTypeDescription = firstTag.TAG_TYPE_ID; if (firstTag.TAG_TYPE_ID) { const tagTypeRecord = await tx.select({ description: tagTypes.description }) .from(tagTypes) .where(and( eq(tagTypes.code, firstTag.TAG_TYPE_ID), eq(tagTypes.projectId, projectId) )) .limit(1); if (tagTypeRecord && tagTypeRecord.length > 0) { tagTypeDescription = tagTypeRecord[0].description; } } // tagClass 조회 let tagClassLabel = firstTag.CLS_ID; if (firstTag.CLS_ID) { const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) .from(tagClasses) .where(and( eq(tagClasses.code, firstTag.CLS_ID), eq(tagClasses.projectId, projectId) )) .limit(1); if (tagClassRecord && tagClassRecord.length > 0) { tagClassLabel = tagClassRecord[0].label; } } // form mapping 정보 가져오기 const allFormMappings = await getFormMappingsByTagTypebyProeject( projectId, ); // 현재 formCode와 일치하는 매핑 찾기 const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode); if (targetFormMapping) { targetImValue = targetFormMapping.ep === "IMEP"; shouldUpdateIm = existingForm[0].im !== targetImValue; } } // 업데이트할 필드들 준비 const updates: any = {}; let hasUpdates = false; // eng 필드 체크 if (existingForm[0].eng !== true) { updates.eng = true; hasUpdates = true; } // im 필드 체크 if (shouldUpdateIm) { updates.im = targetImValue; hasUpdates = true; } // 업데이트 실행 if (hasUpdates) { await tx .update(forms) .set(updates) .where(eq(forms.id, formRecord[0].id)); console.log(`[IMPORT TAGS] Form ${formRecord[0].id} updated with:`, updates); } } } const formId = formRecord[0].id; // 나머지 처리 로직은 기존과 동일... // (양식 메타데이터 가져오기, 태그 처리 등) // 양식 메타데이터 가져오기 const formMetaRecord = await tx.select({ columns: formMetas.columns }) .from(formMetas) .where(and( eq(formMetas.projectId, projectId), eq(formMetas.formCode, formCode) )) .limit(1); if (!formMetaRecord || formMetaRecord.length === 0) { throw new Error(`Form metadata not found for formCode: ${formCode} and projectId: ${projectId}`); } // 진행 상황 보고 if (progressCallback) progressCallback(30); // 컬럼 정보 파싱 const columnsJSON: Column[] = (formMetaRecord[0].columns); // 현재 formEntries 데이터 가져오기 const existingEntries = await tx.select({ id: formEntries.id, data: formEntries.data }) .from(formEntries) .where(and( eq(formEntries.formCode, formCode), eq(formEntries.contractItemId, packageId) )); // 기존 tags 데이터 가져오기 const existingTags = await tx.select() .from(tags) .where(eq(tags.contractItemId, packageId)); // 진행 상황 보고 if (progressCallback) progressCallback(50); // 기존 데이터를 맵으로 변환 const existingTagMap = new Map(); const existingTagsMap = new Map(); existingEntries.forEach(entry => { const data = entry.data as any[]; data.forEach(item => { if (item.TAG_IDX) { existingTagMap.set(item.TAG_IDX, { entryId: entry.id, data: item }); } }); }); existingTags.forEach(tag => { existingTagsMap.set(tag.tagIdx, tag); }); // 진행 상황 보고 if (progressCallback) progressCallback(60); // 처리 결과 카운터 let processedCount = 0; let excludedCount = 0; // 새로운 태그 데이터와 업데이트할 데이터 준비 const newTagData: any[] = []; const upsertTagRecords: any[] = []; // 새로 추가되거나 업데이트될 태그들 const updateData: { entryId: number, tagNo: string, updates: any }[] = []; // SEDP 태그 데이터 처리 for (const tagEntry of tagEntries) { try { if (!tagEntry.TAG_IDX) { excludedCount++; errors.push(`Missing TAG_NO in tag entry`); continue; } // tagType 조회 (TAG_TYPE_ID -> description) let tagTypeDescription = tagEntry.TAG_TYPE_ID; // 기본값 if (tagEntry.TAG_TYPE_ID) { const tagTypeRecord = await tx.select({ description: tagTypes.description }) .from(tagTypes) .where(and( eq(tagTypes.code, tagEntry.TAG_TYPE_ID), eq(tagTypes.projectId, projectId) )) .limit(1); if (tagTypeRecord && tagTypeRecord.length > 0) { tagTypeDescription = tagTypeRecord[0].description; } } // tagClass 조회 (CLS_ID -> label) let tagClassLabel = tagEntry.CLS_ID; // 기본값 let tagClassId = null; // 기본값 if (tagEntry.CLS_ID) { const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) .from(tagClasses) .where(and( eq(tagClasses.code, tagEntry.CLS_ID), eq(tagClasses.projectId, projectId) )) .limit(1); if (tagClassRecord && tagClassRecord.length > 0) { tagClassLabel = tagClassRecord[0].label; tagClassId = tagClassRecord[0].id; } } const packageCode = projectType === "ship" ? tagEntry.ATTRIBUTES.find(v => v.ATT_ID === "CM3003")?.VALUE : tagEntry.ATTRIBUTES.find(v => v.ATT_ID === "ME5074")?.VALUE // 기본 태그 데이터 객체 생성 (formEntries용) const tagObject: any = { TAG_IDX: tagEntry.TAG_IDX, // SEDP 고유 식별자 TAG_NO: tagEntry.TAG_NO, TAG_DESC: tagEntry.TAG_DESC || "", CLS_ID: tagEntry.CLS_ID || "", VNDRCD: vendorRecord[0].vendorCode, VNDRNM_1: vendorRecord[0].vendorName, status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시 source: "S-EDP", // 태그 출처 (불변) - S-EDP에서 가져옴 ...(projectType === "ship" ? { CM3003: packageCode } : { ME5074: packageCode }) } let latestDueDate: Date | null = null; // tags 테이블용 데이터 (UPSERT용) const tagRecord = { contractItemId: packageId, formId: formId, tagIdx: tagEntry.TAG_IDX, // SEDP 고유 식별자 tagNo: tagEntry.TAG_NO, tagType: tagTypeDescription || "", class: tagClassLabel, tagClassId: tagClassId, description: tagEntry.TAG_DESC || null, createdAt: new Date(), updatedAt: new Date() }; // ATTRIBUTES 필드에서 shi=true인 컬럼의 값 추출 if (Array.isArray(tagEntry.ATTRIBUTES)) { for (const attr of tagEntry.ATTRIBUTES) { const columnInfo = columnsJSON.find(col => col.key === attr.ATT_ID); if (columnInfo && (columnInfo.shi === "BOTH" || columnInfo.shi === "OUT" || columnInfo.shi === null)) { if (columnInfo.type === "NUMBER") { if (attr.VALUE !== undefined && attr.VALUE !== null) { if (typeof attr.VALUE === 'string') { const numberMatch = attr.VALUE.match(/(-?\d+(\.\d+)?)/); if (numberMatch) { tagObject[attr.ATT_ID] = parseFloat(numberMatch[0]); } else { const parsed = parseFloat(attr.VALUE); if (!isNaN(parsed)) { tagObject[attr.ATT_ID] = parsed; } } } else if (typeof attr.VALUE === 'number') { tagObject[attr.ATT_ID] = attr.VALUE; } } } else if (attr.VALUE !== null && attr.VALUE !== undefined) { tagObject[attr.ATT_ID] = attr.VALUE; } } // registerMatched에서 해당 SEDP_ATT_ID의 DUE_DATE 찾기 if (registerMatched && Array.isArray(registerMatched)) { const matchedAttribute = registerMatched.find( regAttr => regAttr.SEDP_ATT_ID === attr.ATT_ID ); if (matchedAttribute && matchedAttribute.DUE_DATE) { try { const dueDate = new Date(matchedAttribute.DUE_DATE); // 유효한 날짜인지 확인 if (!isNaN(dueDate.getTime())) { // 첫 번째 DUE_DATE이거나 현재까지 찾은 것보다 더 늦은 날짜인 경우 업데이트 if (!latestDueDate || dueDate > latestDueDate) { latestDueDate = dueDate; } } } catch (dateError) { console.warn(`Invalid DUE_DATE format for ${attr.ATT_ID}: ${matchedAttribute.DUE_DATE}`); } } } } } if (latestDueDate) { // ISO 형식의 문자열로 저장 (또는 원하는 형식으로 변경 가능) tagObject.DUE_DATE = latestDueDate.toISOString(); // 또는 YYYY-MM-DD 형식을 원한다면: // tagObject.DUE_DATE = latestDueDate.toISOString().split('T')[0]; // 또는 원본 형식 그대로 유지하려면: // tagObject.DUE_DATE = latestDueDate.toISOString().replace('Z', ''); } // 기존 태그가 있는지 확인하고 처리 const existingTag = existingTagMap.get(tagEntry.TAG_IDX); if (existingTag) { // 기존 태그가 있으면 formEntries 업데이트 데이터 준비 const updates: any = {}; let hasUpdates = false; for (const key of Object.keys(tagObject)) { if (key === "TAG_IDX") continue; if (key === "TAG_NO" && tagObject[key] !== existingTag.data[key]) { updates[key] = tagObject[key]; hasUpdates = true; continue; } if (key === "TAG_DESC" && tagObject[key] !== existingTag.data[key]) { updates[key] = tagObject[key]; hasUpdates = true; continue; } if (key === "status" && tagObject[key] !== existingTag.data[key]) { updates[key] = tagObject[key]; hasUpdates = true; continue; } if (key === "CLS_ID" && tagObject[key] !== existingTag.data[key]) { updates[key] = tagObject[key]; hasUpdates = true; continue; } if (key === "DUE_DATE" && tagObject[key] !== existingTag.data[key]) { updates[key] = tagObject[key]; hasUpdates = true; continue; } const columnInfo = columnsJSON.find(col => col.key === key); if (columnInfo && (columnInfo.shi === "BOTH" || columnInfo.shi === "OUT" || columnInfo.shi === null)) { if (existingTag.data[key] !== tagObject[key]) { updates[key] = tagObject[key]; hasUpdates = true; } } } if (hasUpdates) { updateData.push({ entryId: existingTag.entryId, tagIdx: tagEntry.TAG_IDX, // TAG_IDX로 변경 updates }); } } else { // 기존 태그가 없으면 새로 추가 newTagData.push(tagObject); } // tags 테이블에는 항상 upsert (새로 추가되거나 업데이트) upsertTagRecords.push(tagRecord); processedCount++; } catch (error) { excludedCount++; errors.push(`Error processing tag ${tagEntry.TAG_IDX || 'unknown'}: ${error}`); } } // 진행 상황 보고 if (progressCallback) progressCallback(80); // formEntries 업데이트 실행 // entryId별로 업데이트를 그룹화 const updatesByEntryId = new Map(); for (const update of updateData) { if (!updatesByEntryId.has(update.entryId)) { updatesByEntryId.set(update.entryId, []); } updatesByEntryId.get(update.entryId).push(update); } // 그룹화된 업데이트를 처리 for (const [entryId, updates] of updatesByEntryId) { try { const entry = existingEntries.find(e => e.id === entryId); if (!entry) continue; const data = entry.data as any[]; // 해당 entryId의 모든 업데이트를 한 번에 적용 const updatedData = data.map(item => { let updatedItem = { ...item }; // 현재 item에 적용할 모든 업데이트를 찾아서 적용 for (const update of updates) { if (item.TAG_IDX === update.tagIdx) { updatedItem = { ...updatedItem, ...update.updates }; } } return updatedItem; }); // entryId별로 한 번만 DB 업데이트 await tx.update(formEntries) .set({ data: updatedData, updatedAt: new Date() }) .where(eq(formEntries.id, entryId)); } catch (error) { const tagNos = updates.map(u => u.tagNo || u.tagIdx).join(', '); errors.push(`Error updating formEntry ${entryId} for tags ${tagNos}: ${error}`); } } // 새 태그 추가 (formEntries) if (newTagData.length > 0) { if (existingEntries.length > 0) { const firstEntry = existingEntries[0]; const existingData = firstEntry.data as any[]; const updatedData = [...existingData, ...newTagData]; await tx.update(formEntries) .set({ data: updatedData, updatedAt: new Date() }) .where(eq(formEntries.id, firstEntry.id)); } else { await tx.insert(formEntries) .values({ formCode, contractItemId: packageId, data: newTagData, createdAt: new Date(), updatedAt: new Date() }); } } // tags 테이블 처리 (INSERT + UPDATE 분리) if (upsertTagRecords.length > 0) { const newTagRecords: any[] = []; const updateTagRecords: { tagId: number, updates: any }[] = []; // 각 태그를 확인하여 신규/업데이트 분류 for (const tagRecord of upsertTagRecords) { const existingTagRecord = existingTagsMap.get(tagRecord.tagIdx); if (existingTagRecord) { // 기존 태그가 있으면 업데이트 준비 const tagUpdates: any = {}; let hasTagUpdates = false; // tagNo도 업데이트 가능 (편집된 경우) if (existingTagRecord.tagNo !== tagRecord.tagNo) { tagUpdates.tagNo = tagRecord.tagNo; hasTagUpdates = true; } if (existingTagRecord.tagType !== tagRecord.tagType) { tagUpdates.tagType = tagRecord.tagType; hasTagUpdates = true; } if (existingTagRecord.class !== tagRecord.class) { tagUpdates.class = tagRecord.class; hasTagUpdates = true; } if (existingTagRecord.tagClassId !== tagRecord.tagClassId) { tagUpdates.tagClassId = tagRecord.tagClassId; hasTagUpdates = true; } if (existingTagRecord.description !== tagRecord.description) { tagUpdates.description = tagRecord.description; hasTagUpdates = true; } if (existingTagRecord.formId !== tagRecord.formId) { tagUpdates.formId = tagRecord.formId; hasTagUpdates = true; } if (hasTagUpdates) { updateTagRecords.push({ tagId: existingTagRecord.id, updates: { ...tagUpdates, updatedAt: new Date() } }); } } else { // 새로운 태그 newTagRecords.push(tagRecord); } } // 새 태그 삽입 if (newTagRecords.length > 0) { try { await tx.insert(tags) .values(newTagRecords) .onConflictDoNothing({ target: [tags.contractItemId, tags.tagIdx] }); } catch (error) { // 개별 삽입으로 재시도 for (const tagRecord of newTagRecords) { try { await tx.insert(tags) .values(tagRecord) .onConflictDoNothing({ target: [tags.contractItemId, tags.tagIdx] }); } catch (individualError) { errors.push(`Error inserting tag ${tagRecord.tagIdx}: ${individualError}`); } } } } // 기존 태그 업데이트 for (const update of updateTagRecords) { try { await tx.update(tags) .set(update.updates) .where(eq(tags.id, update.tagId)); } catch (error) { errors.push(`Error updating tag record ${update.tagId}: ${error}`); } } } // 진행 상황 보고 if (progressCallback) progressCallback(100); // 최종 결과 반환 return { processedCount, excludedCount, totalEntries: tagEntries.length, formCreated, errors: errors.length > 0 ? errors : undefined }; }); } catch (error: any) { console.error("Tag import error:", error); throw error; } } /** * SEDP API에서 태그 데이터 가져오기 * * @param projectCode 프로젝트 코드 * @param formCode 양식 코드 * @returns API 응답 데이터 */ async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise { try { // Get the token const apiKey = await getSEDPToken(); // Define the API base URL const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; // Make the API call const response = await fetch( `${SEDP_API_BASE_URL}/Data/GetPubData`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo: projectCode, REG_TYPE_ID: formCode, // TODO: 이창국 프로 요청으로, ContainDeleted: true로 변경예정, EDP에서 삭제된 데이터도 가져올 수 있어야 한다고 함. // 삭제된 게 들어오면 eVCP내에서 지우거나, 비활성화 하는 등의 처리를 해야 할 걸로 보임 ContainDeleted: false }) } ); if (!response.ok) { const errorText = await response.text(); throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json(); return data; } catch (error: any) { console.error('Error calling SEDP API:', error); throw new Error(`Failed to fetch data from SEDP API: ${error.message || 'Unknown error'}`); } }