diff options
Diffstat (limited to 'lib/sedp')
| -rw-r--r-- | lib/sedp/get-form-tags.ts | 726 | ||||
| -rw-r--r-- | lib/sedp/get-tags.ts | 116 | ||||
| -rw-r--r-- | lib/sedp/sync-package.ts | 282 |
3 files changed, 919 insertions, 205 deletions
diff --git a/lib/sedp/get-form-tags.ts b/lib/sedp/get-form-tags.ts index 6911180b..4e819414 100644 --- a/lib/sedp/get-form-tags.ts +++ b/lib/sedp/get-form-tags.ts @@ -1,4 +1,3 @@ -// lib/sedp/get-tag.ts import db from "@/db/db"; import { contractItems, @@ -14,6 +13,8 @@ import { } from "@/db/schema"; import { eq, and, like, inArray } from "drizzle-orm"; import { getSEDPToken } from "./sedp-token"; +import { getFormMappingsByTagType } from "../tags/form-mapping-service"; + interface Attribute { ATT_ID: string; @@ -42,12 +43,7 @@ interface Column { /** * 태그 가져오기 서비스 함수 * contractItemId(packageId)를 기반으로 외부 시스템에서 태그 데이터를 가져와 DB에 저장 - * - * @param formCode 양식 코드 - * @param projectCode 프로젝트 코드 - * @param packageId 계약 아이템 ID (contractItemId) - * @param progressCallback 진행 상황을 보고하기 위한 콜백 함수 - * @returns 처리 결과 정보 (처리된 태그 수, 오류 목록 등) + * formEntries와 tags 테이블 모두에 데이터를 저장 */ export async function importTagsFromSEDP( formCode: string, @@ -58,6 +54,7 @@ export async function importTagsFromSEDP( processedCount: number; excludedCount: number; totalEntries: number; + formCreated?: boolean; // 새로 추가: form이 생성되었는지 여부 errors?: string[]; }> { try { @@ -89,243 +86,568 @@ export async function importTagsFromSEDP( // 진행 상황 보고 if (progressCallback) progressCallback(20); - // 프로젝트 ID 가져오기 - const projectRecord = await db.select({ id: projects.id }) - .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 formMetaRecord = await db.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 db.select({ id: formEntries.id, data: formEntries.data }) - .from(formEntries) - .where(and( - eq(formEntries.formCode, formCode), - eq(formEntries.contractItemId, packageId) - )); - - // 진행 상황 보고 - if (progressCallback) progressCallback(50); - - // 기존 데이터를 맵으로 변환하여 태그 번호로 빠르게 조회할 수 있게 함 - const existingTagMap = new Map(); - existingEntries.forEach(entry => { - const data = entry.data as any[]; - data.forEach(item => { - if (item.TAG_NO) { - existingTagMap.set(item.TAG_NO, { - entryId: entry.id, - data: item - }); - } - }); - }); - - // 진행 상황 보고 - if (progressCallback) progressCallback(60); - - // 처리 결과 카운터 - let processedCount = 0; - let excludedCount = 0; - - // 새로운 태그 데이터와 업데이트할 데이터 준비 - const newTagData: any[] = []; - const updateData: {entryId: number, tagNo: string, updates: any}[] = []; - - // SEDP 태그 데이터 처리 - for (const tagEntry of tagEntries) { - try { - if (!tagEntry.TAG_NO) { - excludedCount++; - errors.push(`Missing TAG_NO in tag entry`); - continue; + // 트랜잭션으로 모든 DB 작업 처리 + return await db.transaction(async (tx) => { + // 프로젝트 ID 가져오기 + const projectRecord = await tx.select({ id: projects.id }) + .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; + + // 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({ 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 getFormMappingsByTagType( + tagTypeDescription, + projectId, + tagClassLabel + ); + + // ep가 "IMEP"인 것만 필터링 + const formMappings = allFormMappings?.filter(mapping => mapping.ep === "IMEP") || []; + + // 현재 formCode와 일치하는 매핑 찾기 + const targetFormMapping = formMappings.find(mapping => mapping.formCode === formCode); + + if (targetFormMapping) { + console.log(`[IMPORT TAGS] Found IMEP 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 IMEP form mapping found for formCode: ${formCode}`); + console.log(`[IMPORT TAGS] Available IMEP mappings:`, formMappings.map(m => m.formCode)); + throw new Error(`Form ${formCode} not found and no IMEP 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); - // 기본 태그 데이터 객체 생성 - const tagObject: any = { - TAG_NO: tagEntry.TAG_NO, - TAG_DESC: tagEntry.TAG_DESC || "" - }; + // 기존 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); - // 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) { - // shi가 true인 컬럼이거나 필수 컬럼만 처리 - if (columnInfo.shi === true) { - // 값 타입에 따른 변환 + 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({ 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 getFormMappingsByTagType( + tagTypeDescription, + projectId, + tagClassLabel + ); + + // ep가 "IMEP"인 것만 필터링 + const formMappings = allFormMappings?.filter(mapping => mapping.ep === "IMEP") || []; + + // 현재 formCode와 일치하는 매핑 찾기 + const targetFormMapping = formMappings.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_NO) { + existingTagMap.set(item.TAG_NO, { + entryId: entry.id, + data: item + }); + } + }); + }); + + existingTags.forEach(tag => { + existingTagsMap.set(tag.tagNo, 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_NO) { + 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; // 기본값 + if (tagEntry.CLS_ID) { + const tagClassRecord = await tx.select({ 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; + } + } + + // 기본 태그 데이터 객체 생성 (formEntries용) + const tagObject: any = { + TAG_NO: tagEntry.TAG_NO, + TAG_DESC: tagEntry.TAG_DESC || "", + status: "From S-EDP" // SEDP에서 가져온 데이터임을 표시 + }; + + // tags 테이블용 데이터 (UPSERT용) + const tagRecord = { + contractItemId: packageId, + formId: formId, + tagNo: tagEntry.TAG_NO, + tagType: tagTypeDescription, + class: tagClassLabel, + 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 === true) { if (columnInfo.type === "NUMBER") { - // // 먼저 VALUE_DBL이 있는지 확인 - // if (attr.VALUE_DBL !== undefined && attr.VALUE_DBL !== null) { - // tagObject[attr.ATT_ID] = attr.VALUE_DBL; - // } - // VALUE_DBL이 없으면 VALUE 사용 시도 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) { - // 숫자 타입이 아닌 경우 VALUE 그대로 사용 tagObject[attr.ATT_ID] = attr.VALUE; } } } } - } - // 기존 태그가 있는지 확인하고 처리 - const existingTag = existingTagMap.get(tagEntry.TAG_NO); - if (existingTag) { - // 기존 태그가 있으면 업데이트할 필드 찾기 - const updates: any = {}; - let hasUpdates = false; - // shi=true인 필드만 업데이트 - for (const key of Object.keys(tagObject)) { - if (key === "TAG_NO") continue; // TAG_NO는 업데이트 안 함 - - // TAG_DESC는 항상 업데이트 - if (key === "TAG_DESC" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } + // 기존 태그가 있는지 확인하고 처리 + const existingTag = existingTagMap.get(tagEntry.TAG_NO); + + if (existingTag) { + // 기존 태그가 있으면 formEntries 업데이트 데이터 준비 + const updates: any = {}; + let hasUpdates = false; - // 그 외 필드는 컬럼 정보에서 shi=true인 것만 업데이트 - const columnInfo = columnsJSON.find(col => col.key === key); - if (columnInfo && columnInfo.shi === true) { - if (existingTag.data[key] !== tagObject[key]) { + for (const key of Object.keys(tagObject)) { + if (key === "TAG_NO") 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; + } + + const columnInfo = columnsJSON.find(col => col.key === key); + if (columnInfo && columnInfo.shi === true) { + if (existingTag.data[key] !== tagObject[key]) { + updates[key] = tagObject[key]; + hasUpdates = true; + } } } + + if (hasUpdates) { + updateData.push({ + entryId: existingTag.entryId, + tagNo: tagEntry.TAG_NO, + updates + }); + } + } else { + // 기존 태그가 없으면 새로 추가 + newTagData.push(tagObject); } - // 업데이트할 내용이 있으면 추가 - if (hasUpdates) { - updateData.push({ - entryId: existingTag.entryId, - tagNo: tagEntry.TAG_NO, - updates - }); - } + // tags 테이블에는 항상 upsert (새로 추가되거나 업데이트) + upsertTagRecords.push(tagRecord); + + processedCount++; + } catch (error) { + excludedCount++; + errors.push(`Error processing tag ${tagEntry.TAG_NO || 'unknown'}: ${error}`); + } + } + + // 진행 상황 보고 + if (progressCallback) progressCallback(80); + + // formEntries 업데이트 실행 + for (const update of updateData) { + try { + const entry = existingEntries.find(e => e.id === update.entryId); + if (!entry) continue; + + const data = entry.data as any[]; + const updatedData = data.map(item => { + if (item.TAG_NO === update.tagNo) { + return { ...item, ...update.updates }; + } + return item; + }); + + await tx.update(formEntries) + .set({ + data: updatedData, + updatedAt: new Date() + }) + .where(eq(formEntries.id, update.entryId)); + } catch (error) { + errors.push(`Error updating formEntry for tag ${update.tagNo}: ${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 { - // 기존 태그가 없으면 새로 추가 - newTagData.push(tagObject); + await tx.insert(formEntries) + .values({ + formCode, + contractItemId: packageId, + data: newTagData, + createdAt: new Date(), + updatedAt: new Date() + }); } - - processedCount++; - } catch (error) { - excludedCount++; - errors.push(`Error processing tag ${tagEntry.TAG_NO || 'unknown'}: ${error}`); } - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(80); - - // 업데이트 실행 - for (const update of updateData) { - try { - const entry = existingEntries.find(e => e.id === update.entryId); - if (!entry) continue; + + // tags 테이블 처리 (INSERT + UPDATE 분리) + if (upsertTagRecords.length > 0) { + const newTagRecords: any[] = []; + const updateTagRecords: {tagId: number, updates: any}[] = []; - const data = entry.data as any[]; - const updatedData = data.map(item => { - if (item.TAG_NO === update.tagNo) { - return { ...item, ...update.updates }; + // 각 태그를 확인하여 신규/업데이트 분류 + for (const tagRecord of upsertTagRecords) { + const existingTagRecord = existingTagsMap.get(tagRecord.tagNo); + + if (existingTagRecord) { + // 기존 태그가 있으면 업데이트 준비 + const tagUpdates: any = {}; + let hasTagUpdates = false; + + if (existingTagRecord.tagType !== tagRecord.tagType) { + tagUpdates.tagType = tagRecord.tagType; + hasTagUpdates = true; + } + if (existingTagRecord.class !== tagRecord.class) { + tagUpdates.class = tagRecord.class; + 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); } - return item; - }); + } - await db.update(formEntries) - .set({ - data: updatedData, - updatedAt: new Date() - }) - .where(eq(formEntries.id, update.entryId)); - } catch (error) { - errors.push(`Error updating tag ${update.tagNo}: ${error}`); - } - } - - // 새 태그 추가 - if (newTagData.length > 0) { - // 기존 엔트리가 있으면 첫 번째 것에 추가 - if (existingEntries.length > 0) { - const firstEntry = existingEntries[0]; - const existingData = firstEntry.data as any[]; - const updatedData = [...existingData, ...newTagData]; + // 새 태그 삽입 + if (newTagRecords.length > 0) { + try { + await tx.insert(tags) + .values(newTagRecords) + .onConflictDoNothing({ + target: [tags.contractItemId, tags.tagNo] + }); + } catch (error) { + // 개별 삽입으로 재시도 + for (const tagRecord of newTagRecords) { + try { + await tx.insert(tags) + .values(tagRecord) + .onConflictDoNothing({ + target: [tags.contractItemId, tags.tagNo] + }); + } catch (individualError) { + errors.push(`Error inserting tag ${tagRecord.tagNo}: ${individualError}`); + } + } + } + } - await db.update(formEntries) - .set({ - data: updatedData, - updatedAt: new Date() - }) - .where(eq(formEntries.id, firstEntry.id)); - } else { - // 기존 엔트리가 없으면 새로 생성 - await db.insert(formEntries) - .values({ - formCode, - contractItemId: packageId, - data: newTagData, - createdAt: new Date(), - updatedAt: new Date() - }); + // 기존 태그 업데이트 + 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); + + // 진행 상황 보고 + if (progressCallback) progressCallback(100); + + // 최종 결과 반환 + return { + processedCount, + excludedCount, + totalEntries: tagEntries.length, + formCreated, // 새로 추가: form이 생성되었는지 여부 + errors: errors.length > 0 ? errors : undefined + }; + }); - // 최종 결과 반환 - return { - processedCount, - excludedCount, - totalEntries: tagEntries.length, - errors: errors.length > 0 ? errors : undefined - }; } catch (error: any) { console.error("Tag import error:", error); throw error; diff --git a/lib/sedp/get-tags.ts b/lib/sedp/get-tags.ts index 7021d7d2..00916eb2 100644 --- a/lib/sedp/get-tags.ts +++ b/lib/sedp/get-tags.ts @@ -3,6 +3,7 @@ import { contractItems, tags, forms, + formEntries, // 추가 items, tagTypeClassFormMappings, projects, @@ -11,6 +12,7 @@ import { contracts } from "@/db/schema"; import { eq, and, like, inArray } from "drizzle-orm"; +import { revalidateTag } from "next/cache"; // 추가 import { getSEDPToken } from "./sedp-token"; /** @@ -240,6 +242,9 @@ export async function importTagsFromSEDP( insertValues.eng = true; } else if (mode === "IM") { insertValues.im = true; + if (mapping.remark && mapping.remark.includes("VD_")) { + insertValues.eng = true; + } } } @@ -289,8 +294,6 @@ export async function importTagsFromSEDP( try { // Step 5: Call the external API to get tag data - - const tagData = await fetchTagDataFromSEDP(projectCode, baseMappings[0].formCode); // 진행 상황 보고 @@ -309,13 +312,21 @@ export async function importTagsFromSEDP( const tagEntries = tagData[tableName]; if (!Array.isArray(tagEntries) || tagEntries.length === 0) { - allErrors.push(`No tag data found in the API response for formCode ${baseFormCode}`); + allErrors.push(`No tag data found in the API response for formCode ${baseMappings[0].formCode}`); continue; // 다음 매핑으로 진행 } const entriesCount = tagEntries.length; totalEntriesCount += entriesCount; + // formEntries를 위한 데이터 수집 + const newTagsForFormEntry: Array<{ + TAG_NO: string; + TAG_DESC: string | null; + status: string; + [key: string]: any; + }> = []; + // Process each tag entry for (let i = 0; i < tagEntries.length; i++) { try { @@ -371,6 +382,24 @@ export async function importTagsFromSEDP( } }); + // formEntries용 데이터 수집 + const tagDataForFormEntry = { + TAG_NO: entry.TAG_NO, + TAG_DESC: entry.TAG_DESC || null, + status:"From S-EDP" // SEDP에서 가져온 데이터임을 표시 + }; + + // ATTRIBUTES가 있으면 추가 (SHI 필드들) + if (Array.isArray(entry.ATTRIBUTES)) { + for (const attr of entry.ATTRIBUTES) { + if (attr.ATT_ID && attr.VALUE !== null && attr.VALUE !== undefined) { + tagDataForFormEntry[attr.ATT_ID] = attr.VALUE; + } + } + } + + newTagsForFormEntry.push(tagDataForFormEntry); + processedCount++; totalProcessedCount++; @@ -386,6 +415,87 @@ export async function importTagsFromSEDP( } } + // Step 7: formEntries 업데이트 + if (newTagsForFormEntry.length > 0) { + try { + // 기존 formEntry 가져오기 + const existingEntry = await db.query.formEntries.findFirst({ + where: and( + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, packageId) + ) + }); + + if (existingEntry && existingEntry.id) { + // 기존 formEntry가 있는 경우 + let existingData: Array<{ + TAG_NO: string; + TAG_DESC?: string; + status?: string; + [key: string]: any; + }> = []; + + if (Array.isArray(existingEntry.data)) { + existingData = existingEntry.data; + } + + // 기존 TAG_NO들 추출 + const existingTagNos = new Set(existingData.map(item => item.TAG_NO)); + + // 중복되지 않은 새 태그들만 필터링 + const newUniqueTagsData = newTagsForFormEntry.filter( + tagData => !existingTagNos.has(tagData.TAG_NO) + ); + + // 기존 태그들의 status와 ATTRIBUTES 업데이트 + const updatedExistingData = existingData.map(existingItem => { + const newTagData = newTagsForFormEntry.find( + newItem => newItem.TAG_NO === existingItem.TAG_NO + ); + + if (newTagData) { + // 기존 태그가 있으면 SEDP 데이터로 업데이트 + return { + ...existingItem, + ...newTagData, + TAG_NO: existingItem.TAG_NO // TAG_NO는 유지 + }; + } + + return existingItem; + }); + + const finalData = [...updatedExistingData, ...newUniqueTagsData]; + + await db + .update(formEntries) + .set({ + data: finalData, + updatedAt: new Date() + }) + .where(eq(formEntries.id, existingEntry.id)); + + console.log(`[IMPORT SEDP] Updated formEntry with ${newUniqueTagsData.length} new tags, updated ${updatedExistingData.length - newUniqueTagsData.length} existing tags for form ${formCode}`); + } else { + // formEntry가 없는 경우 새로 생성 + await db.insert(formEntries).values({ + formCode: formCode, + contractItemId: packageId, + data: newTagsForFormEntry, + createdAt: new Date(), + updatedAt: new Date(), + }); + + console.log(`[IMPORT SEDP] Created new formEntry with ${newTagsForFormEntry.length} tags for form ${formCode}`); + } + + // 캐시 무효화 + revalidateTag(`form-data-${formCode}-${packageId}`); + } catch (formEntryError) { + console.error(`[IMPORT SEDP] Error updating formEntry for form ${formCode}:`, formEntryError); + allErrors.push(`Error updating formEntry for form ${formCode}: ${formEntryError}`); + } + } } catch (error: any) { console.error(`Error processing mapping for formCode ${formCode}:`, error); diff --git a/lib/sedp/sync-package.ts b/lib/sedp/sync-package.ts new file mode 100644 index 00000000..c8f39ad8 --- /dev/null +++ b/lib/sedp/sync-package.ts @@ -0,0 +1,282 @@ +"use server" +// src/lib/cron/syncItemsFromCodeLists.ts +import db from "@/db/db"; +import { projects, items } from '@/db/schema'; +import { eq } 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'; + +async function getCodeLists(projectCode: string): Promise<Map<string, CodeList>> { + 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<string, CodeList>(); + 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(); + } + } + + +interface CodeValue { + VALUE: string; + DESCC: string; + ATTRIBUTES: Array<{ + ATT_ID: string; + VALUE: 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; +} + +export async function syncItemsFromCodeLists(): Promise<void> { + try { + console.log('아이템 동기화 시작...'); + + // 모든 프로젝트 가져오기 + const allProjects = await db.select().from(projects); + console.log(`총 ${allProjects.length}개의 프로젝트를 처리합니다.`); + + let totalItemsProcessed = 0; + let totalItemsInserted = 0; + let totalItemsUpdated = 0; + + for (const project of allProjects) { + try { + console.log(`프로젝트 ${project.code} (${project.name}) 처리 중...`); + + // 프로젝트의 코드리스트 가져오기 + const codeListMap = await getCodeLists(project.code); + + // PKG_NO 코드리스트 찾기 + const pkgNoCodeList = codeListMap.get('PKG_NO'); + + if (!pkgNoCodeList) { + console.log(`프로젝트 ${project.code}에서 PKG_NO 코드리스트를 찾을 수 없습니다.`); + continue; + } + + console.log(`프로젝트 ${project.code}에서 ${pkgNoCodeList.VALUES.length}개의 아이템을 처리합니다.`); + + // VALUES 배열 순회하며 items 테이블에 삽입/업데이트 + for (const codeValue of pkgNoCodeList.VALUES) { + try { + // ATTRIBUTES에서 필요한 값들 추출 + const packageCodeAttr = codeValue.ATTRIBUTES?.find(attr => attr.ATT_ID === 'SHI_PACK_NO'); + const smCodeAttr = codeValue.ATTRIBUTES?.find(attr => attr.ATT_ID === 'SM_code'); + + const itemData = { + ProjectNo: project.code, + itemCode: codeValue.VALUE, + itemName: codeValue.DESCC || '', + packageCode: packageCodeAttr?.VALUE || '', + smCode: smCodeAttr?.VALUE || null, + description: null, // 필요시 추가 매핑 + parentItemCode: null, // 필요시 추가 매핑 + itemLevel: null, // 필요시 추가 매핑 + deleteFlag: 'N', // 기본값 + unitOfMeasure: null, // 필요시 추가 매핑 + steelType: null, // 필요시 추가 매핑 + gradeMaterial: null, // 필요시 추가 매핑 + changeDate: null, // 필요시 추가 매핑 + baseUnitOfMeasure: null, // 필요시 추가 매핑 + updatedAt: new Date() + }; + + // 기존 아이템 확인 (itemCode로 조회) + const existingItem = await db.select() + .from(items) + .where(eq(items.itemCode, codeValue.VALUE)) + .limit(1); + + if (existingItem.length > 0) { + // 기존 아이템 업데이트 + await db.update(items) + .set(itemData) + .where(eq(items.itemCode, codeValue.VALUE)); + totalItemsUpdated++; + } else { + // 새 아이템 삽입 + await db.insert(items).values(itemData); + totalItemsInserted++; + } + + totalItemsProcessed++; + } catch (itemError) { + console.error(`아이템 ${codeValue.VALUE} 처리 중 오류:`, itemError); + } + } + + console.log(`프로젝트 ${project.code} 완료`); + } catch (projectError) { + console.error(`프로젝트 ${project.code} 처리 중 오류:`, projectError); + } + } + + console.log(`아이템 동기화 완료:`); + console.log(`- 총 처리된 아이템: ${totalItemsProcessed}개`); + console.log(`- 새로 삽입된 아이템: ${totalItemsInserted}개`); + console.log(`- 업데이트된 아이템: ${totalItemsUpdated}개`); + + } catch (error) { + console.error('아이템 동기화 중 전체 오류:', error); + throw error; + } +} + +// 특정 프로젝트만 동기화하는 함수 +export async function syncItemsForProject(projectCode: string): Promise<void> { + try { + console.log(`프로젝트 ${projectCode}의 아이템 동기화 시작...`); + + // 프로젝트 존재 확인 + const project = await db.select() + .from(projects) + .where(eq(projects.code, projectCode)) + .limit(1); + + if (project.length === 0) { + throw new Error(`프로젝트 ${projectCode}를 찾을 수 없습니다.`); + } + + // 프로젝트의 코드리스트 가져오기 + const codeListMap = await getCodeLists(projectCode); + + // PKG_NO 코드리스트 찾기 + const pkgNoCodeList = codeListMap.get('PKG_NO'); + + if (!pkgNoCodeList) { + console.log(`프로젝트 ${projectCode}에서 PKG_NO 코드리스트를 찾을 수 없습니다.`); + return; + } + + console.log(`${pkgNoCodeList.VALUES.length}개의 아이템을 처리합니다.`); + + let itemsProcessed = 0; + let itemsInserted = 0; + let itemsUpdated = 0; + + // VALUES 배열 순회하며 items 테이블에 삽입/업데이트 + for (const codeValue of pkgNoCodeList.VALUES) { + try { + // ATTRIBUTES에서 필요한 값들 추출 + const packageCodeAttr = codeValue.ATTRIBUTES?.find(attr => attr.ATT_ID === 'SHI_PACK_NO'); + const smCodeAttr = codeValue.ATTRIBUTES?.find(attr => attr.ATT_ID === 'SM_code'); + + const itemData = { + ProjectNo: projectCode, + itemCode: codeValue.VALUE, + itemName: codeValue.DESCC || '', + packageCode: packageCodeAttr?.VALUE || '', + smCode: smCodeAttr?.VALUE || null, + description: null, + parentItemCode: null, + itemLevel: null, + deleteFlag: 'N', + unitOfMeasure: null, + steelType: null, + gradeMaterial: null, + changeDate: null, + baseUnitOfMeasure: null, + updatedAt: new Date() + }; + + // 기존 아이템 확인 + const existingItem = await db.select() + .from(items) + .where(eq(items.itemCode, codeValue.VALUE)) + .limit(1); + + if (existingItem.length > 0) { + await db.update(items) + .set(itemData) + .where(eq(items.itemCode, codeValue.VALUE)); + itemsUpdated++; + } else { + await db.insert(items).values(itemData); + itemsInserted++; + } + + itemsProcessed++; + } catch (itemError) { + console.error(`아이템 ${codeValue.VALUE} 처리 중 오류:`, itemError); + } + } + + console.log(`프로젝트 ${projectCode} 아이템 동기화 완료:`); + console.log(`- 처리된 아이템: ${itemsProcessed}개`); + console.log(`- 새로 삽입된 아이템: ${itemsInserted}개`); + console.log(`- 업데이트된 아이템: ${itemsUpdated}개`); + + } catch (error) { + console.error(`프로젝트 ${projectCode} 아이템 동기화 중 오류:`, error); + throw error; + } +}
\ No newline at end of file |
