summaryrefslogtreecommitdiff
path: root/lib/sedp
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-12-01 00:58:23 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-12-01 00:58:23 +0000
commit8440ac29c7dcbef992039678ecc0fabff2fd04ec (patch)
tree59092b8dcd22135009bf70d5863ffde444e5bed2 /lib/sedp
parent748f68bb7b5d02450664651ae5025c9a38fb71a5 (diff)
(대표님) S-EDP 관련 대표님 작업사항
Diffstat (limited to 'lib/sedp')
-rw-r--r--lib/sedp/get-form-tags-plant.ts933
-rw-r--r--lib/sedp/get-tags-plant.ts639
-rw-r--r--lib/sedp/sync-form.ts9
3 files changed, 1580 insertions, 1 deletions
diff --git a/lib/sedp/get-form-tags-plant.ts b/lib/sedp/get-form-tags-plant.ts
new file mode 100644
index 00000000..176f1b3f
--- /dev/null
+++ b/lib/sedp/get-form-tags-plant.ts
@@ -0,0 +1,933 @@
+import db from "@/db/db";
+import {
+ contractItems,
+ tagsPlant,
+ formsPlant,formEntriesPlant,
+ items,
+ tagTypeClassFormMappings,
+ projects,
+ tagTypes,
+ tagClasses,
+ formMetas,
+} 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<newRegister[]> {
+ 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;
+ }
+}
+
+
+/**
+ * 태그 가져오기 서비스 함수
+ * formEntries와 tags 테이블 모두에 데이터를 저장
+ */
+export async function importTagsFromSEDP(
+ formCode: string,
+ projectCode: string,
+ packageCode: string,
+ 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";
+
+
+
+
+ const targetPackageCode = 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: formsPlant.id })
+ .from(formsPlant)
+ .where(and(
+ eq(formsPlant.formCode, formCode),
+ eq(formsPlant.projectCode, projectCode),
+ eq(formsPlant.packageCode, packageCode)
+ ))
+ .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(formsPlant)
+ .values({
+ projectCode,
+ packageCode,
+ formCode: targetFormMapping.formCode,
+ formName: targetFormMapping.formName,
+ eng: true, // ENG 모드에서 가져오는 것이므로 eng: true
+ im: targetFormMapping.ep === "IMEP" ? true : false
+ })
+ .returning({ id: formsPlant.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, 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: formsPlant.eng,
+ im: formsPlant.im
+ })
+ .from(formsPlant)
+ .where(eq(formsPlant.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(formsPlant)
+ .set(updates)
+ .where(eq(formsPlant.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: formEntriesPlant.id, data: formEntriesPlant.data })
+ .from(formEntriesPlant)
+ .where(and(
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode)
+ ));
+
+ // 기존 tags 데이터 가져오기
+ const existingTags = await tx.select()
+ .from(tagsPlant)
+ .where(and(
+ eq(tagsPlant.projectCode, projectCode),
+ eq(tagsPlant.packageCode, packageCode),
+ )
+ );
+
+ // 진행 상황 보고
+ 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 = {
+ projectCode,
+ packageCode,
+ 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(formEntriesPlant)
+ .set({
+ data: updatedData,
+ updatedAt: new Date()
+ })
+ .where(eq(formEntriesPlant.id, entryId));
+
+ } catch (error) {
+ const tagNos = updates.map(u => u.tagNo || u.tagIdx).join(', ');
+ errors.push(`Error updating formEntry ${entryId} for tags ${tagNos}: ${error}`);
+ }
+ }
+
+ // 새 태그 추가 (formEntriesPlant)
+ 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(formEntriesPlant)
+ .set({
+ data: updatedData,
+ updatedAt: new Date()
+ })
+ .where(eq(formEntriesPlant.id, firstEntry.id));
+ } else {
+ await tx.insert(formEntriesPlant)
+ .values({
+ formCode,
+ projectCode,
+ packageCode,
+ 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(tagsPlant)
+ .values(newTagRecords)
+ .onConflictDoNothing({
+ target: [tagsPlant.projectCode,tagsPlant.packageCode, tagsPlant.tagIdx]
+ });
+ } catch (error) {
+ // 개별 삽입으로 재시도
+ for (const tagRecord of newTagRecords) {
+ try {
+ await tx.insert(tagsPlant)
+ .values(tagRecord)
+ .onConflictDoNothing({
+ target: [tagsPlant.projectCode,tagsPlant.packageCode, tagsPlant.tagIdx]
+ });
+ } catch (individualError) {
+ errors.push(`Error inserting tag ${tagRecord.tagIdx}: ${individualError}`);
+ }
+ }
+ }
+ }
+
+ // 기존 태그 업데이트
+ for (const update of updateTagRecords) {
+ try {
+ await tx.update(tagsPlant)
+ .set(update.updates)
+ .where(eq(tagsPlant.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<any> {
+ 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'}`);
+ }
+} \ No newline at end of file
diff --git a/lib/sedp/get-tags-plant.ts b/lib/sedp/get-tags-plant.ts
new file mode 100644
index 00000000..d1957db4
--- /dev/null
+++ b/lib/sedp/get-tags-plant.ts
@@ -0,0 +1,639 @@
+import db from "@/db/db";
+import {
+ tagsPlant,
+ formsPlant,
+ formEntriesPlant,
+ items,
+ tagTypeClassFormMappings,
+ projects,
+ tagTypes,
+ tagClasses,
+} from "@/db/schema";
+import { eq, and, like, inArray } from "drizzle-orm";
+import { revalidateTag } from "next/cache"; // 추가
+import { getSEDPToken } from "./sedp-token";
+
+/**
+ * 태그 가져오기 서비스 함수
+ * contractItemId(packageId)를 기반으로 외부 시스템에서 태그 데이터를 가져와 DB에 저장
+ * TAG_IDX를 기준으로 태그를 식별합니다.
+ *
+ * @param projectCode 계약 아이템 ID (contractItemId)
+ * @param packageCode 계약 아이템 ID (contractItemId)
+ * @param progressCallback 진행 상황을 보고하기 위한 콜백 함수
+ * @returns 처리 결과 정보 (처리된 태그 수, 오류 목록 등)
+ */
+export async function importTagsFromSEDP(
+ projectCode: string,
+ packageCode: string,
+ progressCallback?: (progress: number) => void,
+ mode?: string
+): Promise<{
+ processedCount: number;
+ excludedCount: number;
+ totalEntries: number;
+ errors?: string[];
+}> {
+ try {
+ // 진행 상황 보고
+ if (progressCallback) progressCallback(5);
+
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.code, projectCode),
+ columns: {
+ id: true
+ }
+ });
+
+
+ // 프로젝트 ID 획득
+ const projectId = project?.id;
+
+ // Step 1-2: Get the item using itemId from contractItem
+ const item = await db.query.items.findFirst({
+ where: and(eq(items.ProjectNo, projectCode), eq(items.packageCode, packageCode))
+ });
+
+ if (!item) {
+ throw new Error(`Item with ID ${item?.id} not found`);
+ }
+
+ const itemCode = item.itemCode;
+
+ // 진행 상황 보고
+ if (progressCallback) progressCallback(10);
+
+ // 기본 매핑 검색 - 모든 모드에서 사용
+ const baseMappings = await db.query.tagTypeClassFormMappings.findMany({
+ where: and(
+ like(tagTypeClassFormMappings.remark, `%${itemCode}%`),
+ eq(tagTypeClassFormMappings.projectId, projectId)
+ )
+ });
+
+ if (baseMappings.length === 0) {
+ throw new Error(`No mapping found for item code ${itemCode}`);
+ }
+
+ // Step 2: Find the mapping entries - 모드에 따라 다른 조건 적용
+ let mappings = [];
+
+ if (mode === 'IM') {
+ // IM 모드일 때는 먼저 SEDP에서 태그 데이터를 가져와 TAG_TYPE_ID 리스트 확보
+
+ // 프로젝트 코드 가져오기
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.id, projectId)
+ });
+
+ if (!project) {
+ throw new Error(`Project with ID ${projectId} not found`);
+ }
+
+ // 각 매핑의 formCode에 대해 태그 데이터 조회
+ const tagTypeIds = new Set<string>();
+
+ for (const mapping of baseMappings) {
+ try {
+ // SEDP에서 태그 데이터 가져오기
+ const tagData = await fetchTagDataFromSEDP(project.code, mapping.formCode);
+
+ // 첫 번째 키를 테이블 이름으로 사용
+ const tableName = Object.keys(tagData)[0];
+ const tagEntries = tagData[tableName];
+
+ if (Array.isArray(tagEntries)) {
+ // 모든 태그에서 TAG_TYPE_ID 수집
+ for (const entry of tagEntries) {
+ if (entry.TAG_TYPE_ID && entry.TAG_TYPE_ID !== "") {
+ tagTypeIds.add(entry.TAG_TYPE_ID);
+ }
+ }
+ }
+ } catch (error) {
+ console.error(`Error fetching tag data for formCode ${mapping.formCode}:`, error);
+ }
+ }
+
+ if (tagTypeIds.size === 0) {
+ throw new Error('No valid TAG_TYPE_ID found in SEDP tag data');
+ }
+
+ // 수집된 TAG_TYPE_ID로 tagTypes에서 정보 조회
+ const tagTypeInfo = await db.query.tagTypes.findMany({
+ where: and(
+ inArray(tagTypes.code, Array.from(tagTypeIds)),
+ eq(tagTypes.projectId, projectId)
+ )
+ });
+
+ if (tagTypeInfo.length === 0) {
+ throw new Error('No matching tag types found for the collected TAG_TYPE_IDs');
+ }
+
+ // 태그 타입 설명 수집
+ const tagLabels = tagTypeInfo.map(tt => tt.description);
+
+ // IM 모드에 맞는 매핑 조회 - ep가 "IMEP"인 항목만
+ mappings = await db.query.tagTypeClassFormMappings.findMany({
+ where: and(
+ inArray(tagTypeClassFormMappings.tagTypeLabel, tagLabels),
+ eq(tagTypeClassFormMappings.projectId, projectId),
+ eq(tagTypeClassFormMappings.ep, "IMEP")
+ )
+ });
+
+ } else {
+ // ENG 모드 또는 기본 모드일 때 - 기본 매핑 사용
+ mappings = [...baseMappings];
+
+ // ENG 모드에서는 ep 필드가 "IMEP"가 아닌 매핑만 필터링
+ if (mode === 'ENG') {
+ mappings = mappings.filter(mapping => mapping.ep !== "IMEP");
+ }
+ }
+
+ // 매핑이 없는 경우 모드에 따라 다른 오류 메시지 사용
+ if (mappings.length === 0) {
+ if (mode === 'IM') {
+ throw new Error('No suitable mappings found for IM mode');
+ } else {
+ throw new Error(`No mapping found for item code ${itemCode}`);
+ }
+ }
+
+ // 진행 상황 보고
+ if (progressCallback) progressCallback(15);
+
+ // 결과 누적을 위한 변수들 초기화
+ let totalProcessedCount = 0;
+ let totalExcludedCount = 0;
+ let totalEntriesCount = 0;
+ const allErrors: string[] = [];
+
+ // 각 매핑에 대해 처리
+ for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex++) {
+ const mapping = mappings[mappingIndex];
+
+ // Step 3: Get the project code
+ const project = await db.query.projects.findFirst({
+ where: eq(projects.id, mapping.projectId)
+ });
+
+ if (!project) {
+ allErrors.push(`Project with ID ${mapping.projectId} not found`);
+ continue; // 다음 매핑으로 진행
+ }
+
+ // IM 모드에서는 baseMappings에서 같은 formCode를 가진 매핑을 찾음
+ let formCode = mapping.formCode;
+ if (mode === 'IM') {
+ // baseMapping에서 동일한 formCode를 가진 매핑 찾기
+ const originalMapping = baseMappings.find(
+ baseMapping => baseMapping.formCode === mapping.formCode
+ );
+
+ // 찾았으면 해당 formCode 사용, 못 찾았으면 현재 매핑의 formCode 유지
+ if (originalMapping) {
+ formCode = originalMapping.formCode;
+ }
+ }
+
+ // 진행 상황 보고 - 매핑별 진행률 조정
+ if (progressCallback) {
+ const baseProgress = 15;
+ const mappingProgress = Math.floor(15 * (mappingIndex + 1) / mappings.length);
+ progressCallback(baseProgress + mappingProgress);
+ }
+
+ // Step 4: Find the form ID
+ const form = await db.query.formsPlant.findFirst({
+ where: and(
+ eq(formsPlant.projectCode, projectCode),
+ eq(formsPlant.packageCode, packageCode),
+ eq(formsPlant.formCode, formCode)
+ )
+ });
+
+ let formId;
+
+ // If form doesn't exist, create it
+ if (!form) {
+ // 폼이 없는 경우 새로 생성 - 모드에 따른 필드 설정
+ const insertValues: any = {
+ projectCode,
+ packageCode,
+ formCode: formCode,
+ formName: mapping.formName
+ };
+
+ // 모드 정보가 있으면 해당 필드 설정
+ if (mode) {
+ if (mode === "ENG") {
+ insertValues.eng = true;
+ } else if (mode === "IM") {
+ insertValues.im = true;
+ if (mapping.remark && mapping.remark.includes("VD_")) {
+ insertValues.eng = true;
+ }
+ }
+ }
+
+ const insertResult = await db.insert(formsPlant).values(insertValues).returning({ id: formsPlant.id });
+
+ if (insertResult.length === 0) {
+ allErrors.push(`Failed to create form record for formCode ${formCode}`);
+ continue; // 다음 매핑으로 진행
+ }
+
+ formId = insertResult[0].id;
+ } else {
+ // 폼이 이미 존재하는 경우 - 필요시 모드 필드 업데이트
+ formId = form.id;
+
+ if (mode) {
+ let shouldUpdate = false;
+ const updateValues: any = {};
+
+ if (mode === "ENG" && form.eng !== true) {
+ updateValues.eng = true;
+ shouldUpdate = true;
+ } else if (mode === "IM" && form.im !== true) {
+ updateValues.im = true;
+ shouldUpdate = true;
+ }
+
+ if (shouldUpdate) {
+ await db.update(formsPlant)
+ .set({
+ ...updateValues,
+ updatedAt: new Date()
+ })
+ .where(eq(formsPlant.id, formId));
+
+ console.log(`Updated form ${formId} with ${mode} mode enabled`);
+ }
+ }
+ }
+
+ // 진행 상황 보고 - 매핑별 진행률 조정
+ if (progressCallback) {
+ const baseProgress = 30;
+ const mappingProgress = Math.floor(20 * (mappingIndex + 1) / mappings.length);
+ progressCallback(baseProgress + mappingProgress);
+ }
+
+ try {
+ // Step 5: Call the external API to get tag data
+ const tagData = await fetchTagDataFromSEDP(projectCode, baseMappings[0].formCode);
+
+ // 진행 상황 보고
+ if (progressCallback) {
+ const baseProgress = 50;
+ const mappingProgress = Math.floor(10 * (mappingIndex + 1) / mappings.length);
+ progressCallback(baseProgress + mappingProgress);
+ }
+
+ // Step 6: Process the data and insert into the tags table
+ let processedCount = 0;
+ let excludedCount = 0;
+
+ // Get the first key from the response as the table name
+ const tableName = Object.keys(tagData)[0];
+ const tagEntries = tagData[tableName];
+
+ if (!Array.isArray(tagEntries) || tagEntries.length === 0) {
+ 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_IDX: string; // 변경: TAG_NO → TAG_IDX
+ TAG_NO?: string; // TAG_NO도 함께 저장 (편집 가능한 필드)
+ TAG_DESC: string | null;
+ status: string;
+ [key: string]: any;
+ }> = [];
+
+ const registerResponse = await fetch(
+ `${SEDP_API_BASE_URL}/Register/GetByID`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'accept': '*/*',
+ 'ApiKey': apiKey,
+ 'ProjectNo': projectCode
+ },
+ body: JSON.stringify({
+ ProjectNo: projectCode,
+ TYPE_ID: baseMappings[0].formCode, // 또는 mapping.formCode
+ ContainDeleted: false
+ })
+ }
+ )
+
+ if (!registerResponse.ok) {
+ allErrors.push(`Failed to fetch register details for ${baseMappings[0].formCode}`)
+ continue
+ }
+
+ const registerDetail: Register = await registerResponse.json()
+
+ // ✅ MAP_ATT에서 허용된 ATT_ID 목록 추출
+ const allowedAttIds = new Set<string>()
+ if (Array.isArray(registerDetail.MAP_ATT)) {
+ for (const mapAttr of registerDetail.MAP_ATT) {
+ if (mapAttr.ATT_ID) {
+ allowedAttIds.add(mapAttr.ATT_ID)
+ }
+ }
+ }
+
+
+ // Process each tag entry
+ for (let i = 0; i < tagEntries.length; i++) {
+ try {
+ const entry = tagEntries[i];
+
+ // TAG_IDX가 없는 경우 제외 (변경: TAG_NO → TAG_IDX 체크)
+ if (!entry.TAG_IDX) {
+ excludedCount++;
+ totalExcludedCount++;
+
+ // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트)
+ if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) {
+ const baseProgress = 60;
+ const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount)));
+ progressCallback(baseProgress + entryProgress);
+ }
+
+ continue; // 이 항목은 건너뜀
+ }
+
+ const attributes: Record<string, string> = {}
+ if (Array.isArray(entry.ATTRIBUTES)) {
+ for (const attr of entry.ATTRIBUTES) {
+ // MAP_ATT에 정의된 ATT_ID만 포함
+ if (attr.ATT_ID && allowedAttIds.has(attr.ATT_ID)) {
+ if (attr.VALUE !== null && attr.VALUE !== undefined) {
+ attributes[attr.ATT_ID] = String(attr.VALUE)
+ }
+ }
+ }
+ }
+
+
+ // TAG_TYPE_ID가 null이거나 빈 문자열인 경우 제외
+ if (entry.TAG_TYPE_ID === null || entry.TAG_TYPE_ID === "") {
+ excludedCount++;
+ totalExcludedCount++;
+
+ // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트)
+ if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) {
+ const baseProgress = 60;
+ const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount)));
+ progressCallback(baseProgress + entryProgress);
+ }
+
+ continue; // 이 항목은 건너뜀
+ }
+
+ // Get tag type description
+ const tagType = await db.query.tagTypes.findFirst({
+ where: and(
+ eq(tagTypes.code, entry.TAG_TYPE_ID),
+ eq(tagTypes.projectId, mapping.projectId)
+ )
+ });
+
+ // Get tag class label
+ const tagClass = await db.query.tagClasses.findFirst({
+ where: and(
+ eq(tagClasses.code, entry.CLS_ID),
+ eq(tagClasses.projectId, mapping.projectId)
+ )
+ });
+
+ // Insert or update the tag - tagIdx 필드 추가
+ await db.insert(tagsPlant).values({
+ projectCode,
+ packageCode,
+ formId: formId,
+ tagIdx: entry.TAG_IDX,
+ tagNo: entry.TAG_NO || entry.TAG_IDX,
+ tagType: tagType?.description || entry.TAG_TYPE_ID,
+ tagClassId: tagClass?.id,
+ class: tagClass?.label || entry.CLS_ID,
+ description: entry.TAG_DESC,
+ attributes: attributes, // JSONB로 저장
+ }).onConflictDoUpdate({
+ target: [tagsPlant.projectCode, tagsPlant.packageCode, tagsPlant.tagIdx],
+ set: {
+ formId: formId,
+ tagNo: entry.TAG_NO || entry.TAG_IDX,
+ tagType: tagType?.description || entry.TAG_TYPE_ID,
+ class: tagClass?.label || entry.CLS_ID,
+ description: entry.TAG_DESC,
+ attributes: attributes, // JSONB 업데이트
+ updatedAt: new Date()
+ }
+ })
+ // formEntries용 데이터 수집
+ const tagDataForFormEntry = {
+ TAG_IDX: entry.TAG_IDX, // 변경: TAG_NO → TAG_IDX
+ TAG_NO: entry.TAG_NO || entry.TAG_IDX, // TAG_NO도 함께 저장
+ TAG_DESC: entry.TAG_DESC || null,
+ status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시
+ source: "S-EDP" // 태그 출처 (불변) - S-EDP에서 가져옴
+ };
+
+ // 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++;
+
+ // 주기적으로 진행 상황 보고
+ if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) {
+ const baseProgress = 60;
+ const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount)));
+ progressCallback(baseProgress + entryProgress);
+ }
+ } catch (error: any) {
+ console.error(`Error processing tag entry:`, error);
+ allErrors.push(error.message || 'Unknown error');
+ }
+ }
+
+ // Step 7: formEntries 업데이트 - TAG_IDX 기준으로 변경
+ if (newTagsForFormEntry.length > 0) {
+ try {
+ // 기존 formEntry 가져오기
+ const existingEntry = await db.query.formEntriesPlant.findFirst({
+ where: and(
+ eq(formEntriesPlant.formCode, formCode),
+ eq(formEntriesPlant.projectCode, projectCode),
+ eq(formEntriesPlant.packageCode, packageCode)
+ )
+ });
+
+ if (existingEntry && existingEntry.id) {
+ // 기존 formEntry가 있는 경우
+ let existingData: Array<{
+ TAG_IDX?: string; // 추가: TAG_IDX 필드
+ TAG_NO?: string;
+ TAG_DESC?: string;
+ status?: string;
+ [key: string]: any;
+ }> = [];
+
+ if (Array.isArray(existingEntry.data)) {
+ existingData = existingEntry.data;
+ }
+
+ // 기존 TAG_IDX들 추출 (변경: TAG_NO → TAG_IDX)
+ const existingTagIdxs = new Set(
+ existingData
+ .map(item => item.TAG_IDX)
+ .filter(tagIdx => tagIdx !== undefined && tagIdx !== null)
+ );
+
+ // 중복되지 않은 새 태그들만 필터링 (변경: TAG_NO → TAG_IDX)
+ const newUniqueTagsData = newTagsForFormEntry.filter(
+ tagData => !existingTagIdxs.has(tagData.TAG_IDX)
+ );
+
+ // 기존 태그들의 status와 ATTRIBUTES 업데이트 (변경: TAG_NO → TAG_IDX)
+ const updatedExistingData = existingData.map(existingItem => {
+ const newTagData = newTagsForFormEntry.find(
+ newItem => newItem.TAG_IDX === existingItem.TAG_IDX
+ );
+
+ if (newTagData) {
+ // 기존 태그가 있으면 SEDP 데이터로 업데이트
+ return {
+ ...existingItem,
+ ...newTagData,
+ TAG_IDX: existingItem.TAG_IDX // TAG_IDX는 유지
+ };
+ }
+
+ return existingItem;
+ });
+
+ const finalData = [...updatedExistingData, ...newUniqueTagsData];
+
+ await db
+ .update(formEntriesPlant)
+ .set({
+ data: finalData,
+ updatedAt: new Date()
+ })
+ .where(eq(formEntriesPlant.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(formEntriesPlant).values({
+ formCode: formCode,
+ projectCode,
+ packageCode,
+ 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);
+ allErrors.push(`Error with formCode ${formCode}: ${error.message || 'Unknown error'}`);
+ }
+ }
+
+ // 모든 매핑 처리 완료 - 진행률 100%
+ if (progressCallback) {
+ progressCallback(100);
+ }
+
+ // 최종 결과 반환
+ return {
+ processedCount: totalProcessedCount,
+ excludedCount: totalExcludedCount,
+ totalEntries: totalEntriesCount,
+ errors: allErrors.length > 0 ? allErrors : 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<any> {
+ 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,
+ 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'}`);
+ }
+} \ No newline at end of file
diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts
index 904d27ba..a6d473ad 100644
--- a/lib/sedp/sync-form.ts
+++ b/lib/sedp/sync-form.ts
@@ -94,7 +94,7 @@ interface Register {
SEQ: number;
CMPLX_YN: boolean;
CMPL_SETT: any | null;
- MAP_ATT: any[];
+ MAP_ATT: MapAttribute2[];
MAP_CLS_ID: string[];
MAP_OPER: any | null;
LNK_ATT: LinkAttribute[];
@@ -157,6 +157,13 @@ interface MapAttribute {
INOUT: string | null;
}
+interface MapAttribute2 {
+ ATT_ID: string;
+ VALUE: string;
+ IS_PARA: boolean;
+ OPER: string | null;
+}
+
interface Attribute {
PROJ_NO: string;
ATT_ID: string;