diff options
Diffstat (limited to 'lib/sedp')
| -rw-r--r-- | lib/sedp/get-form-tags.ts | 380 | ||||
| -rw-r--r-- | lib/sedp/get-tags.ts | 263 | ||||
| -rw-r--r-- | lib/sedp/sync-form.ts | 756 | ||||
| -rw-r--r-- | lib/sedp/sync-object-class.ts | 257 | ||||
| -rw-r--r-- | lib/sedp/sync-projects.ts | 7 | ||||
| -rw-r--r-- | lib/sedp/sync-tag-types.ts | 88 |
6 files changed, 1586 insertions, 165 deletions
diff --git a/lib/sedp/get-form-tags.ts b/lib/sedp/get-form-tags.ts new file mode 100644 index 00000000..b488bfad --- /dev/null +++ b/lib/sedp/get-form-tags.ts @@ -0,0 +1,380 @@ +// lib/sedp/get-tag.ts +import db from "@/db/db"; +import { + contractItems, + tags, + forms, + items, + tagTypeClassFormMappings, + projects, + tagTypes, + tagClasses, + formMetas, + formEntries +} from "@/db/schema"; +import { eq, and, like, inArray } from "drizzle-orm"; +import { getSEDPToken } from "./sedp-token"; + +interface Attribute { + ATT_ID: string; + VALUE: any; + VALUE_DBL: number; + UOM_ID: string | null; +} + +interface TagEntry { + 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?: boolean; +} + +/** + * 태그 가져오기 서비스 함수 + * contractItemId(packageId)를 기반으로 외부 시스템에서 태그 데이터를 가져와 DB에 저장 + * + * @param formCode 양식 코드 + * @param projectCode 프로젝트 코드 + * @param packageId 계약 아이템 ID (contractItemId) + * @param progressCallback 진행 상황을 보고하기 위한 콜백 함수 + * @returns 처리 결과 정보 (처리된 태그 수, 오류 목록 등) + */ +export async function importTagsFromSEDP( + formCode: string, + projectCode: string, + packageId: number, + progressCallback?: (progress: number) => void +): Promise<{ + processedCount: number; + excludedCount: number; + totalEntries: number; + errors?: string[]; +}> { + try { + // 진행 상황 보고 + if (progressCallback) progressCallback(5); + + // 에러 수집 배열 + const errors: string[] = []; + + // SEDP API에서 태그 데이터 가져오기 + const tagData = await fetchTagDataFromSEDP(projectCode, formCode); + + // 데이터 형식 처리 + const tableName = Object.keys(tagData)[0]; + if (!tableName || !tagData[tableName]) { + throw new Error("Invalid tag data format from SEDP API"); + } + + const tagEntries: TagEntry[] = tagData[tableName]; + if (!Array.isArray(tagEntries) || tagEntries.length === 0) { + return { + processedCount: 0, + excludedCount: 0, + totalEntries: 0, + errors: ["No tag entries found in API response"] + }; + } + + // 진행 상황 보고 + 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[] = JSON.parse(formMetaRecord[0].columns as string); + + // 현재 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; + } + + // 기본 태그 데이터 객체 생성 + const tagObject: any = { + TAG_NO: tagEntry.TAG_NO, + TAG_DESC: tagEntry.TAG_DESC || "" + }; + + // 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 (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; + } + + // 그 외 필드는 컬럼 정보에서 shi=true인 것만 업데이트 + 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); + } + + 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; + + const data = entry.data as any[]; + const updatedData = data.map(item => { + if (item.TAG_NO === update.tagNo) { + return { ...item, ...update.updates }; + } + 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]; + + 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() + }); + } + } + + // 진행 상황 보고 + if (progressCallback) progressCallback(100); + + // 최종 결과 반환 + return { + processedCount, + excludedCount, + totalEntries: tagEntries.length, + 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, + 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.ts b/lib/sedp/get-tags.ts new file mode 100644 index 00000000..7c5661c3 --- /dev/null +++ b/lib/sedp/get-tags.ts @@ -0,0 +1,263 @@ +// lib/sedp/get-tag.ts +import db from "@/db/db"; +import { + contractItems, + tags, + forms, + items, + tagTypeClassFormMappings, + projects, + tagTypes, + tagClasses +} from "@/db/schema"; +import { eq, and, like } from "drizzle-orm"; +import { getSEDPToken } from "./sedp-token"; + +/** + * 태그 가져오기 서비스 함수 + * contractItemId(packageId)를 기반으로 외부 시스템에서 태그 데이터를 가져와 DB에 저장 + * + * @param packageId 계약 아이템 ID (contractItemId) + * @param progressCallback 진행 상황을 보고하기 위한 콜백 함수 + * @returns 처리 결과 정보 (처리된 태그 수, 오류 목록 등) + */ +// 함수 반환 타입 업데이트 +export async function importTagsFromSEDP( + packageId: number, + progressCallback?: (progress: number) => void +): Promise<{ + processedCount: number; + excludedCount: number; + totalEntries: number; + errors?: string[]; +}> { + try { + // 진행 상황 보고 + if (progressCallback) progressCallback(5); + + // Step 1: Get the contract item to find relevant data + const contractItem = await db.query.contractItems.findFirst({ + where: eq(contractItems.id, packageId) + }); + + if (!contractItem) { + throw new Error(`Contract item with ID ${packageId} not found`); + } + + // 진행 상황 보고 + if (progressCallback) progressCallback(5); + + // Step 1-2: Get the item using itemId from contractItem + const item = await db.query.items.findFirst({ + where: eq(items.id, contractItem.itemId) + }); + + if (!item) { + throw new Error(`Item with ID ${contractItem.itemId} not found`); + } + + const itemCode = item.itemCode; + + // 진행 상황 보고 + if (progressCallback) progressCallback(10); + + // Step 2: Find the mapping entry with the item code in remark field + // 더 유연한 검색 패턴 사용 (%itemCode%) + const mapping = await db.query.tagTypeClassFormMappings.findFirst({ + where: like(tagTypeClassFormMappings.remark, `%${itemCode}%`) + }); + + if (!mapping) { + throw new Error(`No mapping found for item code ${itemCode}`); + } + + // 진행 상황 보고 + if (progressCallback) progressCallback(15); + + // Step 3: Get the project code + const project = await db.query.projects.findFirst({ + where: eq(projects.id, mapping.projectId) + }); + + if (!project) { + throw new Error(`Project with ID ${mapping.projectId} not found`); + } + + const projectCode = project.code; + const formCode = mapping.formCode; + + // 진행 상황 보고 + if (progressCallback) progressCallback(20); + + // Step 4: Find the form ID + const form = await db.query.forms.findFirst({ + where: and( + eq(forms.contractItemId, packageId), + eq(forms.formCode, formCode) + ) + }); + + let formId = form?.id; + + // If form doesn't exist, create it + if (!form) { + const insertResult = await db.insert(forms).values({ + contractItemId: packageId, + formCode: formCode, + formName: mapping.formName + }).returning({ id: forms.id }); + + if (insertResult.length === 0) { + throw new Error('Failed to create form record'); + } + + formId = insertResult[0].id; + } + + // 진행 상황 보고 + if (progressCallback) progressCallback(30); + + // Step 5: Call the external API to get tag data + const tagData = await fetchTagDataFromSEDP(projectCode, formCode); + + // 진행 상황 보고 + if (progressCallback) progressCallback(50); + + // Step 6: Process the data and insert into the tags table + let processedCount = 0; + let excludedCount = 0; + const errors: string[] = []; + + // 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) { + throw new Error('No tag data found in the API response'); + } + + const totalEntries = tagEntries.length; + + // Process each tag entry + for (let i = 0; i < tagEntries.length; i++) { + try { + const entry = tagEntries[i]; + + // TAG_TYPE_ID가 null이거나 빈 문자열인 경우 제외 + if (entry.TAG_TYPE_ID === null || entry.TAG_TYPE_ID === "") { + excludedCount++; + + // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트) + if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) { + progressCallback(Math.floor(50 + (i / tagEntries.length) * 50)); + } + + 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 + await db.insert(tags).values({ + contractItemId: packageId, + formId: formId, + tagNo: entry.TAG_NO, + tagType: tagType?.description || entry.TAG_TYPE_ID, + class: tagClass?.label || entry.CLS_ID, + description: entry.TAG_DESC + }).onConflictDoUpdate({ + target: [tags.contractItemId, tags.tagNo], + set: { + formId: formId, + tagType: tagType?.description || entry.TAG_TYPE_ID, + class: tagClass?.label || entry.CLS_ID, + description: entry.TAG_DESC, + updatedAt: new Date() + } + }); + + processedCount++; + + // 주기적으로 진행 상황 보고 + if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) { + progressCallback(Math.floor(50 + (i / tagEntries.length) * 50)); + } + } catch (error: any) { + console.error(`Error processing tag entry:`, error); + errors.push(error.message || 'Unknown error'); + } + } + + // 최종 결과 반환 + return { + processedCount, + excludedCount, + totalEntries, + 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, + 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 b9e6fa90..a3caa809 100644 --- a/lib/sedp/sync-form.ts +++ b/lib/sedp/sync-form.ts @@ -1,13 +1,42 @@ // src/lib/cron/syncTagFormMappings.ts import db from "@/db/db"; -import { projects, tagTypes, tagClasses, tagTypeClassFormMappings, formMetas } from '@/db/schema'; -import { eq, and, inArray } from 'drizzle-orm'; +import { projects, tagTypes, tagClasses, tagTypeClassFormMappings, formMetas, forms, contractItems, items } from '@/db/schema'; +import { eq, and, inArray, ilike } from 'drizzle-orm'; import { getSEDPToken } from "./sedp-token"; // 환경 변수 const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/dev/api'; // 인터페이스 정의 +interface TagTypeClassFormMapping { + projectId: number; + tagTypeLabel: string; + classLabel: string; + formCode: string; + formName: string; + remark: string | null; + ep: string; + createdAt: Date; + updatedAt: Date; +} + +interface FormMeta { + projectId: number; + formCode: string; + formName: string; + columns: string; // JSON 문자열 + createdAt: Date; + updatedAt: Date; +} + +interface FormRecord { + contractItemId: number; + formCode: string; + formName: string; + eng: boolean; + createdAt: Date; + updatedAt: Date; +} interface Register { PROJ_NO: string; TYPE_ID: string; @@ -137,6 +166,87 @@ interface FormColumn { options?: string[]; uom?: string; uomId?: string; + shi?: Boolean; +} + +// 아이템 코드 추출 함수 +function extractItemCodes(remark: string | null): string[] { + if (!remark) return []; + + // 검색용으로만 소문자로 변환 + const remarkLower = remark.toLowerCase(); + + // 'vd_' 접두사 확인 + const hasVD_ = remarkLower.includes("vd_"); + + if (!hasVD_) return []; + + let vdPart = ""; + + // 'vd_'가 있으면 원본 문자열에서 추출 (소문자 버전이 아님) + if (hasVD_) { + const vdIndex = remarkLower.indexOf("vd_"); + vdPart = remark.substring(vdIndex + 3); // 원본 문자열에서 추출 + } + + if (!vdPart) return []; + + // 쉼표로 구분된 여러 itemCode 처리 + return vdPart.split(",").map(code => code.trim()); +} + +async function getDefaulTAttributes(): Promise<string[]> { + try { + const apiKey = await getSEDPToken(); + + const response = await fetch( + `${SEDP_API_BASE_URL}/Dictionary/GetByKey`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + }, + body: JSON.stringify({ + Key: "DefaultAttributesToCompare", + }) + } + ); + + if (!response.ok) { + if (response.status === 404) { + console.warn(`디폴트 속성 찾을 수 없음`); + return []; + } + throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`); + } + + // 안전하게 JSON 파싱 + try { + const data = await response.json(); + // 데이터가 배열인지 확인하고 문자열 배열로 변환 + if (Array.isArray(data)) { + return data as string[]; + } else { + console.warn('응답이 배열 형식이 아닙니다'); + return []; + } + } catch (parseError) { + console.error(`디폴트 속성 응답 파싱 실패:`, parseError); + // 응답 내용 로깅 + try { + const text = await response.clone().text(); + console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + } catch (textError) { + console.error('응답 내용 로깅 실패:', textError); + } + return []; + } + } catch (error) { + console.error(`디폴트 어트리뷰트 가져오기 실패:`, error); + throw error; + } } // 레지스터 데이터 가져오기 @@ -144,7 +254,7 @@ async function getRegisters(projectCode: string): Promise<Register[]> { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); - + const response = await fetch( `${SEDP_API_BASE_URL}/Register/Get`, { @@ -156,36 +266,123 @@ async function getRegisters(projectCode: string): Promise<Register[]> { 'ProjectNo': projectCode }, body: JSON.stringify({ - ContainDeleted: true + ProjectNo: projectCode, + ContainDeleted: false }) } ); - + if (!response.ok) { throw new Error(`레지스터 요청 실패: ${response.status} ${response.statusText}`); } - - const data = await response.json(); - - // 결과가 배열인지 확인 - if (Array.isArray(data)) { - return data; - } else { - // 단일 객체인 경우 배열로 변환 - return [data]; + + // 안전하게 JSON 파싱 + let data; + try { + data = await response.json(); + } catch (parseError) { + console.error(`프로젝트 ${projectCode}의 레지스터 응답 파싱 실패:`, parseError); + // 응답 내용 로깅 + const text = await response.clone().text(); + console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + throw new Error(`레지스터 응답 파싱 실패: ${parseError instanceof Error ? parseError.message : String(parseError)}`); } + + // 결과를 배열로 변환 (단일 객체인 경우 배열로 래핑) + let registers: Register[] = Array.isArray(data) ? data : [data]; + + // MAP_CLS_ID가 비어있지 않고 REMARK가 vd, VD, vD, Vd 중 하나인 레지스터만 필터링 + registers = registers.filter(register => { + // 삭제된 레지스터 제외 + if (register.DELETED) return false; + + // MAP_CLS_ID 배열이 존재하고 요소가 하나 이상 있는지 확인 + const hasValidMapClsId = Array.isArray(register.MAP_CLS_ID) && register.MAP_CLS_ID.length > 0; + + // REMARK가 'vd_' 또는 'vd' 포함 확인 (대소문자 구분 없이) + const remarkLower = register.REMARK && register.REMARK.toLowerCase(); + const hasValidRemark = remarkLower && (remarkLower.includes('vd')); + + // 두 조건 모두 충족해야 함 + return hasValidMapClsId && hasValidRemark; + }); + + console.log(`프로젝트 ${projectCode}에서 ${registers.length}개의 유효한 레지스터를 가져왔습니다.`); + return registers; } catch (error) { console.error(`프로젝트 ${projectCode}의 레지스터 가져오기 실패:`, error); throw error; } } -// 특정 속성 가져오기 -async function getAttributeById(projectCode: string, attributeId: string): Promise<Attribute | null> { +// 프로젝트의 모든 속성을 가져와 맵으로 반환 +async function getAttributes(projectCode: string): Promise<Map<string, Attribute>> { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); - + + const response = await fetch( + `${SEDP_API_BASE_URL}/Attributes/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ProjectNo: projectCode, + ContainDeleted: false + }) + } + ); + + if (!response.ok) { + throw new Error(`속성 요청 실패: ${response.status} ${response.statusText}`); + } + + // 안전하게 JSON 파싱 + try { + const data = await response.json(); + + // 데이터가 배열인지 확인 + const attributes: Attribute[] = Array.isArray(data) ? data : [data]; + + // ATT_ID로 효율적인 조회를 위한 맵 생성 + const attributeMap = new Map<string, Attribute>(); + for (const attribute of attributes) { + if (!attribute.DELETED) { + attributeMap.set(attribute.ATT_ID, attribute); + } + } + + console.log(`프로젝트 ${projectCode}에서 ${attributeMap.size}개의 속성을 가져왔습니다`); + return attributeMap; + + } catch (parseError) { + console.error(`프로젝트 ${projectCode}의 속성 응답 파싱 실패:`, parseError); + // 응답 내용 로깅 + try { + const text = await response.clone().text(); + console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + } catch (textError) { + console.error('응답 내용 로깅 실패:', textError); + } + return new Map(); + } + } catch (error) { + console.error(`프로젝트 ${projectCode}의 속성 가져오기 실패:`, error); + return new Map(); + } +} + +// 특정 속성 가져오기 (하위 호환성을 위해 유지) +async function getAttributeById(projectCode: string, attributeId: string, register: string): Promise<Attribute | null> { + try { + // 토큰(API 키) 가져오기 + const apiKey = await getSEDPToken(); + const response = await fetch( `${SEDP_API_BASE_URL}/Attributes/GetByID`, { @@ -197,11 +394,13 @@ async function getAttributeById(projectCode: string, attributeId: string): Promi 'ProjectNo': projectCode }, body: JSON.stringify({ - ATT_ID: attributeId + ProjectNo: projectCode, + ATT_ID: attributeId, + ContainDeleted: false }) } ); - + if (!response.ok) { if (response.status === 404) { console.warn(`속성 ID ${attributeId}를 찾을 수 없음`); @@ -209,20 +408,96 @@ async function getAttributeById(projectCode: string, attributeId: string): Promi } throw new Error(`속성 요청 실패: ${response.status} ${response.statusText}`); } - - return response.json(); + + // 안전하게 JSON 파싱 + try { + const data = await response.json(); + return data; + } catch (parseError) { + console.error(`속성 ID ${attributeId} ${register} ${projectCode} 응답 파싱 실패:`, parseError); + // 응답 내용 로깅 + try { + const text = await response.clone().text(); + console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + } catch (textError) { + console.error('응답 내용 로깅 실패:', textError); + } + return null; + } } catch (error) { console.error(`속성 ID ${attributeId} 가져오기 실패:`, error); return null; } } -// 특정 코드 리스트 가져오기 +// 프로젝트의 모든 코드 리스트를 가져와 맵으로 반환 +async function getCodeLists(projectCode: string): Promise<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(); + } +} + +// 특정 코드 리스트 가져오기 (하위 호환성을 위해 유지) async function getCodeListById(projectCode: string, codeListId: string): Promise<CodeList | null> { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); - + const response = await fetch( `${SEDP_API_BASE_URL}/CodeList/GetByID`, { @@ -234,11 +509,13 @@ async function getCodeListById(projectCode: string, codeListId: string): Promise 'ProjectNo': projectCode }, body: JSON.stringify({ - CL_ID: codeListId + ProjectNo: projectCode, + CL_ID: codeListId, + ContainDeleted: false }) } ); - + if (!response.ok) { if (response.status === 404) { console.warn(`코드 리스트 ID ${codeListId}를 찾을 수 없음`); @@ -246,20 +523,96 @@ async function getCodeListById(projectCode: string, codeListId: string): Promise } throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`); } - - return response.json(); + + // 안전하게 JSON 파싱 + try { + const data = await response.json(); + return data; + } catch (parseError) { + console.error(`코드 리스트 ID ${codeListId} 응답 파싱 실패:`, parseError); + // 응답 내용 로깅 + try { + const text = await response.clone().text(); + console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + } catch (textError) { + console.error('응답 내용 로깅 실패:', textError); + } + return null; + } } catch (error) { console.error(`코드 리스트 ID ${codeListId} 가져오기 실패:`, error); return null; } } -// UOM 가져오기 +// 프로젝트의 모든 UOM을 가져와 맵으로 반환 +async function getUOMs(projectCode: string): Promise<Map<string, UOM>> { + try { + // 토큰(API 키) 가져오기 + const apiKey = await getSEDPToken(); + + const response = await fetch( + `${SEDP_API_BASE_URL}/UOM/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ProjectNo: projectCode, + ContainDeleted: false + }) + } + ); + + if (!response.ok) { + throw new Error(`UOM 요청 실패: ${response.status} ${response.statusText}`); + } + + // 안전하게 JSON 파싱 + try { + const data = await response.json(); + + // 데이터가 배열인지 확인 + const uoms: UOM[] = Array.isArray(data) ? data : [data]; + + // UOM_ID로 효율적인 조회를 위한 맵 생성 + const uomMap = new Map<string, UOM>(); + for (const uom of uoms) { + if (!uom.DELETED) { + uomMap.set(uom.UOM_ID, uom); + } + } + + console.log(`프로젝트 ${projectCode}에서 ${uomMap.size}개의 UOM을 가져왔습니다`); + return uomMap; + + } catch (parseError) { + console.error(`프로젝트 ${projectCode}의 UOM 응답 파싱 실패:`, parseError); + // 응답 내용 로깅 + try { + const text = await response.clone().text(); + console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + } catch (textError) { + console.error('응답 내용 로깅 실패:', textError); + } + return new Map(); + } + } catch (error) { + console.error(`프로젝트 ${projectCode}의 UOM 가져오기 실패:`, error); + return new Map(); + } +} + +// UOM 가져오기 (하위 호환성을 위해 유지) async function getUomById(projectCode: string, uomId: string): Promise<UOM | null> { try { // 토큰(API 키) 가져오기 const apiKey = await getSEDPToken(); - + const response = await fetch( `${SEDP_API_BASE_URL}/UOM/GetByID`, { @@ -271,11 +624,13 @@ async function getUomById(projectCode: string, uomId: string): Promise<UOM | nul 'ProjectNo': projectCode }, body: JSON.stringify({ - UOM_ID: uomId + UOMID: uomId, // API 명세서에 따라 UOMID 사용 + ProjectNo: projectCode, + ContainDeleted: false }) } ); - + if (!response.ok) { if (response.status === 404) { console.warn(`UOM ID ${uomId}를 찾을 수 없음`); @@ -283,90 +638,215 @@ async function getUomById(projectCode: string, uomId: string): Promise<UOM | nul } throw new Error(`UOM 요청 실패: ${response.status} ${response.statusText}`); } - - return response.json(); + + // 안전하게 JSON 파싱 + try { + const data = await response.json(); + return data; + } catch (parseError) { + console.error(`UOM ID ${uomId} 응답 파싱 실패:`, parseError); + // 응답 내용 로깅 + try { + const text = await response.clone().text(); + console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); + } catch (textError) { + console.error('응답 내용 로깅 실패:', textError); + } + return null; + } } catch (error) { console.error(`UOM ID ${uomId} 가져오기 실패:`, error); return null; } } +// contractItemId 조회 함수 +async function getContractItemsByItemCodes(itemCodes: string[]): Promise<Map<string, number>> { + try { + if (!itemCodes.length) return new Map(); + + // 먼저 itemCodes에 해당하는 item 레코드를 조회 + const itemRecords = await db.select({ + id: items.id, + itemCode: items.itemCode + }) + .from(items) + .where(inArray(items.itemCode, itemCodes)); + + if (!itemRecords.length) { + console.log(`No items found for itemCodes: ${itemCodes.join(', ')}`); + return new Map(); + } + + // item ID 목록 추출 + const itemIds = itemRecords.map(item => item.id); + + // contractItems 조회 + const contractItemRecords = await db.select({ + id: contractItems.id, + itemId: contractItems.itemId + }) + .from(contractItems) + .where(inArray(contractItems.itemId, itemIds)); + + // itemCode와 contractItemId의 매핑 생성 + const itemCodeToContractItemId = new Map<string, number>(); + + for (const item of itemRecords) { + // itemCode가 null이 아닌 경우에만 처리 + if (item.itemCode) { + const matchedContractItems = contractItemRecords.filter(ci => ci.itemId === item.id); + if (matchedContractItems.length > 0) { + // 일치하는 첫 번째 contractItem 사용 + itemCodeToContractItemId.set(item.itemCode, matchedContractItems[0].id); + } + } + } + + return itemCodeToContractItemId; + } catch (error) { + console.error('ContractItems 조회 중 오류 발생:', error); + return new Map(); + } +} + // 데이터베이스에 태그 타입 클래스 폼 매핑 및 폼 메타 저장 async function saveFormMappingsAndMetas( - projectId: number, + projectId: number, projectCode: string, registers: Register[] ): Promise<number> { try { - // 프로젝트와 관련된 태그 타입 및 클래스 가져오기 + // 프로젝트의 태그 타입과 클래스 가져오기 const tagTypeRecords = await db.select() .from(tagTypes) .where(eq(tagTypes.projectId, projectId)); - + const tagClassRecords = await db.select() .from(tagClasses) .where(eq(tagClasses.projectId, projectId)); - - // 태그 타입과 클래스를 매핑 + + // 태그 타입과 클래스 매핑 const tagTypeMap = new Map(tagTypeRecords.map(type => [type.code, type])); const tagClassMap = new Map(tagClassRecords.map(cls => [cls.code, cls])); + + // 모든 속성, 코드 리스트, UOM을 한 번에 가져와 반복 API 호출 방지 + const attributeMap = await getAttributes(projectCode); + const codeListMap = await getCodeLists(projectCode); + const uomMap = await getUOMs(projectCode); - // 저장할 매핑 목록과 폼 메타 정보 - const mappingsToSave = []; - const formMetasToSave = []; + // 기본 속성 가져오기 + const defaultAttributes = await getDefaulTAttributes(); + + // 모든 register에서 itemCode를 추출하여 한 번에 조회 + const allItemCodes: string[] = []; + registers.forEach(register => { + if (register.REMARK) { + const itemCodes = extractItemCodes(register.REMARK); + allItemCodes.push(...itemCodes); + } + }); + + // 중복 제거 + const uniqueItemCodes = [...new Set(allItemCodes)]; - // 각 레지스터 처리 + // 모든 itemCode에 대한 contractItemId 조회 + const itemCodeToContractItemId = await getContractItemsByItemCodes(uniqueItemCodes); + + console.log(`${uniqueItemCodes.length}개의 고유 itemCode 중 ${itemCodeToContractItemId.size}개의 contractItem을 찾았습니다`); + + // 저장할 데이터 준비 + const mappingsToSave: TagTypeClassFormMapping[] = []; + const formMetasToSave: FormMeta[] = []; + const formsToSave: FormRecord[] = []; + + // 폼이 있는 contractItemId 트래킹 + const contractItemIdsWithForms = new Set<number>(); + + // 각 register 처리 for (const register of registers) { - // 삭제된 레지스터는 건너뜀 + // 삭제된 register 건너뛰기 if (register.DELETED) continue; - - // 폼 메타 데이터를 위한 컬럼 정보 구성 + + // REMARK에서 itemCodes 추출 + const itemCodes = extractItemCodes(register.REMARK || ''); + if (!itemCodes.length) { + console.log(`Register ${register.TYPE_ID} (${register.DESC})의 REMARK에 유효한 itemCode가 없습니다`); + continue; + } + + // 폼 메타용 columns 구성 const columns: FormColumn[] = []; - - // 각 속성 정보 수집 + for (const linkAtt of register.LNK_ATT) { - // 속성 가져오기 - const attribute = await getAttributeById(projectCode, linkAtt.ATT_ID); - - if (!attribute) continue; - - // 기본 컬럼 정보 + let attribute = null; + + // 기본 속성인지 확인 + if (defaultAttributes && defaultAttributes.includes(linkAtt.ATT_ID)) { + // 기본 속성에 대한 기본 attribute 객체 생성 + attribute = { + DESC: linkAtt.ATT_ID, + VAL_TYPE: 'STRING' + }; + } else { + // 맵에서 속성 조회 + attribute = attributeMap.get(linkAtt.ATT_ID); + + // 속성을 찾지 못한 경우 다음으로 넘어감 + if (!attribute) continue; + } + + // 컬럼 정보 생성 const column: FormColumn = { key: linkAtt.ATT_ID, - label: linkAtt.CPY_DESC, - type: attribute.VAL_TYPE || 'STRING' + label: attribute.DESC, + type: (attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST') + ? 'LIST' + : (attribute.VAL_TYPE || 'STRING'), + shi: attribute.REMARK?.toLocaleLowerCase() === "shi" }; - - // 리스트 타입인 경우 옵션 추가 - if ((attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST') && attribute.CL_ID) { - const codeList = await getCodeListById(projectCode, attribute.CL_ID); + + // 리스트 타입에 대한 옵션 추가 (기본 속성이 아닌 경우) + if (!defaultAttributes.includes(linkAtt.ATT_ID) && + (attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST') && + attribute.CL_ID) { + // 맵에서 코드 리스트 조회 + const codeList = codeListMap.get(attribute.CL_ID); + if (codeList && codeList.VALUES) { - // 유효한 옵션만 필터링 - const options = codeList.VALUES - .filter(value => value.USE_YN) - .map(value => value.DESC); - + const options = [...new Set( + codeList.VALUES + .filter(value => value.USE_YN) + .map(value => value.VALUE) + )]; + if (options.length > 0) { column.options = options; } } } - + // UOM 정보 추가 if (linkAtt.UOM_ID) { - const uom = await getUomById(projectCode, linkAtt.UOM_ID); - + const uom = uomMap.get(linkAtt.UOM_ID); + if (uom) { column.uom = uom.SYMBOL; column.uomId = uom.UOM_ID; } } - + columns.push(column); } - - // 폼 메타 정보 저장 + + // 컬럼이 없으면 건너뛰기 + if (columns.length === 0) { + console.log(`폼 ${register.TYPE_ID} (${register.DESC})에 컬럼이 없어 건너뜁니다`); + continue; + } + + // 폼 메타 데이터 준비 formMetasToSave.push({ projectId, formCode: register.TYPE_ID, @@ -375,25 +855,24 @@ async function saveFormMappingsAndMetas( createdAt: new Date(), updatedAt: new Date() }); - - // 관련된 클래스 매핑 처리 + + // 클래스 매핑 처리 for (const classId of register.MAP_CLS_ID) { - // 해당 클래스와 태그 타입 확인 const tagClass = tagClassMap.get(classId); - + if (!tagClass) { - console.warn(`클래스 ID ${classId}를 프로젝트 ID ${projectId}에서 찾을 수 없음`); + console.warn(`프로젝트 ID ${projectId}에서 클래스 ID ${classId}를 찾을 수 없습니다`); continue; } - + const tagTypeCode = tagClass.tagTypeCode; const tagType = tagTypeMap.get(tagTypeCode); - + if (!tagType) { - console.warn(`태그 타입 ${tagTypeCode}를 프로젝트 ID ${projectId}에서 찾을 수 없음`); + console.warn(`프로젝트 ID ${projectId}에서 태그 타입 ${tagTypeCode}를 찾을 수 없습니다`); continue; } - + // 매핑 정보 저장 mappingsToSave.push({ projectId, @@ -401,32 +880,71 @@ async function saveFormMappingsAndMetas( classLabel: tagClass.label, formCode: register.TYPE_ID, formName: register.DESC, + remark: register.REMARK, + ep: register.EP_ID, + createdAt: new Date(), + updatedAt: new Date() + }); + } + + // 폼 레코드 준비 + for (const itemCode of itemCodes) { + const contractItemId = itemCodeToContractItemId.get(itemCode); + + if (!contractItemId) { + console.warn(`itemCode: ${itemCode}에 대한 contractItemId를 찾을 수 없습니다`); + continue; + } + + // 폼이 있는 contractItemId 추적 + contractItemIdsWithForms.add(contractItemId); + + formsToSave.push({ + contractItemId, + formCode: register.TYPE_ID, + formName: register.DESC, + eng: true, createdAt: new Date(), updatedAt: new Date() }); } } - - // 기존 데이터 삭제 후 새로 저장 - await db.delete(tagTypeClassFormMappings).where(eq(tagTypeClassFormMappings.projectId, projectId)); - await db.delete(formMetas).where(eq(formMetas.projectId, projectId)); - + + // 트랜잭션으로 모든 작업 처리 let totalSaved = 0; - - // 매핑 정보 저장 - if (mappingsToSave.length > 0) { - await db.insert(tagTypeClassFormMappings).values(mappingsToSave); - totalSaved += mappingsToSave.length; - console.log(`프로젝트 ID ${projectId}에 ${mappingsToSave.length}개의 태그 타입-클래스-폼 매핑 저장 완료`); - } - - // 폼 메타 정보 저장 - if (formMetasToSave.length > 0) { - await db.insert(formMetas).values(formMetasToSave); - totalSaved += formMetasToSave.length; - console.log(`프로젝트 ID ${projectId}에 ${formMetasToSave.length}개의 폼 메타 정보 저장 완료`); - } - + + await db.transaction(async (tx) => { + // 기존 데이터 삭제 + await tx.delete(tagTypeClassFormMappings).where(eq(tagTypeClassFormMappings.projectId, projectId)); + await tx.delete(formMetas).where(eq(formMetas.projectId, projectId)); + + // 해당 contractItemId에 대한 기존 폼 삭제 + if (contractItemIdsWithForms.size > 0) { + await tx.delete(forms).where(inArray(forms.contractItemId, [...contractItemIdsWithForms])); + } + + // 매핑 저장 + if (mappingsToSave.length > 0) { + await tx.insert(tagTypeClassFormMappings).values(mappingsToSave); + totalSaved += mappingsToSave.length; + console.log(`프로젝트 ID ${projectId}에 대해 ${mappingsToSave.length}개의 태그 타입-클래스-폼 매핑을 저장했습니다`); + } + + // 폼 메타 저장 + if (formMetasToSave.length > 0) { + await tx.insert(formMetas).values(formMetasToSave); + totalSaved += formMetasToSave.length; + console.log(`프로젝트 ID ${projectId}에 대해 ${formMetasToSave.length}개의 폼 메타 레코드를 저장했습니다`); + } + + // 폼 레코드 저장 + if (formsToSave.length > 0) { + await tx.insert(forms).values(formsToSave); + totalSaved += formsToSave.length; + console.log(`프로젝트 ID ${projectId}에 대해 ${formsToSave.length}개의 폼 레코드를 저장했습니다`); + } + }); + return totalSaved; } catch (error) { console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID: ${projectId}):`, error); @@ -438,39 +956,39 @@ async function saveFormMappingsAndMetas( export async function syncTagFormMappings() { try { console.log('태그 폼 매핑 동기화 시작:', new Date().toISOString()); - + // 모든 프로젝트 가져오기 const allProjects = await db.select().from(projects); - + // 각 프로젝트에 대해 폼 매핑 동기화 const results = await Promise.allSettled( allProjects.map(async (project: Project) => { try { // 레지스터 데이터 가져오기 const registers = await getRegisters(project.code); - + // 데이터베이스에 저장 const count = await saveFormMappingsAndMetas(project.id, project.code, registers); - return { - project: project.code, - success: true, - count + return { + project: project.code, + success: true, + count } as SyncResult; } catch (error) { console.error(`프로젝트 ${project.code} 폼 매핑 동기화 실패:`, error); - return { - project: project.code, - success: false, - error: error instanceof Error ? error.message : String(error) + return { + project: project.code, + success: false, + error: error instanceof Error ? error.message : String(error) } as SyncResult; } }) ); - + // 결과 처리를 위한 배열 준비 const successfulResults: SyncResult[] = []; const failedResults: SyncResult[] = []; - + // 결과 분류 results.forEach((result) => { if (result.status === 'fulfilled') { @@ -488,19 +1006,19 @@ export async function syncTagFormMappings() { }); } }); - + const successCount = successfulResults.length; const failCount = failedResults.length; - + // 이제 안전하게 count 속성에 접근 가능 - const totalItems = successfulResults.reduce((sum, result) => + const totalItems = successfulResults.reduce((sum, result) => sum + (result.count || 0), 0 ); - + console.log(`태그 폼 매핑 동기화 완료: ${successCount}개 프로젝트 성공 (총 ${totalItems}개 항목), ${failCount}개 프로젝트 실패`); - - return { - success: successCount, + + return { + success: successCount, failed: failCount, items: totalItems, timestamp: new Date().toISOString() diff --git a/lib/sedp/sync-object-class.ts b/lib/sedp/sync-object-class.ts index 1cf0c23b..0a76c592 100644 --- a/lib/sedp/sync-object-class.ts +++ b/lib/sedp/sync-object-class.ts @@ -40,7 +40,12 @@ interface SyncResult { count?: number; error?: string; } - +interface TagType { + TYPE_ID: string; + DESC: string; + PROJ_NO: string; + // 기타 필드들... +} // 오브젝트 클래스 데이터 가져오기 async function getObjectClasses(projectCode: string, token:string): Promise<ObjectClass[]> { try { @@ -55,7 +60,8 @@ async function getObjectClasses(projectCode: string, token:string): Promise<Obje 'ProjectNo': projectCode }, body: JSON.stringify({ - ContainDeleted: true + ProjectNo:projectCode, + ContainDeleted: false }) } ); @@ -95,11 +101,171 @@ async function verifyTagTypes(projectId: number, tagTypeCodes: string[]): Promis } } -// 데이터베이스에 오브젝트 클래스 저장 (upsert 사용) -async function saveObjectClassesToDatabase(projectId: number, classes: ObjectClass[]): Promise<number> { +async function saveTagTypesToDatabase(allTagTypes: TagType[], projectCode: string): Promise<void> { + try { + if (allTagTypes.length === 0) { + console.log(`프로젝트 ${projectCode}에 저장할 태그 타입이 없습니다.`); + return; + } + + // 프로젝트 코드로 프로젝트 ID 조회 + const projectResult = await db.select({ id: projects.id }) + .from(projects) + .where(eq(projects.code, projectCode)) + .limit(1); + + if (projectResult.length === 0) { + throw new Error(`프로젝트 코드 ${projectCode}에 해당하는 프로젝트를 찾을 수 없습니다.`); + } + + const projectId = projectResult[0].id; + + // 현재 프로젝트의 모든 태그 타입 조회 + const existingTagTypes = await db.select() + .from(tagTypes) + .where(eq(tagTypes.projectId, projectId)); + + // 코드 기준으로 맵 생성 + const existingTagTypeMap = new Map( + existingTagTypes.map(type => [type.code, type]) + ); + + // API에 있는 코드 목록 + const apiTagTypeCodes = new Set(allTagTypes.map(type => type.TYPE_ID)); + + // 삭제할 코드 목록 + const codesToDelete = existingTagTypes + .map(type => type.code) + .filter(code => !apiTagTypeCodes.has(code)); + + // 새로 추가할 항목 + const toInsert = []; + + // 업데이트할 항목 + const toUpdate = []; + + // 태그 타입 데이터 처리 + for (const tagType of allTagTypes) { + // 데이터베이스 레코드 준비 + const record = { + code: tagType.TYPE_ID, + projectId: projectId, + description: tagType.DESC, + updatedAt: new Date() + }; + + // 이미 존재하는 코드인지 확인 + if (existingTagTypeMap.has(tagType.TYPE_ID)) { + // 업데이트 항목에 추가 + toUpdate.push(record); + } else { + // 새로 추가할 항목에 추가 (createdAt 필드 추가) + toInsert.push({ + ...record, + createdAt: new Date() + }); + } + } + + // 트랜잭션 실행 + + // 1. 새 항목 삽입 + if (toInsert.length > 0) { + await db.insert(tagTypes).values(toInsert); + console.log(`프로젝트 ID ${projectId}에 ${toInsert.length}개의 새 태그 타입 추가 완료`); + } + + // 2. 기존 항목 업데이트 + for (const item of toUpdate) { + await db.update(tagTypes) + .set({ + description: item.description, + updatedAt: item.updatedAt + }) + .where( + and( + eq(tagTypes.code, item.code), + eq(tagTypes.projectId, item.projectId) + ) + ); + } + + if (toUpdate.length > 0) { + console.log(`프로젝트 ID ${projectId}의 ${toUpdate.length}개 태그 타입 업데이트 완료`); + } + + // 3. 더 이상 존재하지 않는 항목 삭제 + if (codesToDelete.length > 0) { + for (const code of codesToDelete) { + await db.delete(tagTypes) + .where( + and( + eq(tagTypes.code, code), + eq(tagTypes.projectId, projectId) + ) + ); + } + console.log(`프로젝트 ID ${projectId}에서 ${codesToDelete.length}개의 태그 타입 삭제 완료`); + } + + console.log(`프로젝트 ${projectCode}(ID: ${projectId})의 태그 타입 동기화 완료`); + } catch (error) { + console.error(`태그 타입 저장 실패 (프로젝트: ${projectCode}):`, error); + throw error; + } +} + +async function getAllTagTypes(projectCode: string, token: string): Promise<TagType[]> { + try { + console.log(`프로젝트 ${projectCode}의 모든 태그 타입 가져오기 시작`); + + const response = await fetch( + `${SEDP_API_BASE_URL}/TagType/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': token, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ProjectNo: projectCode, + ContainDeleted: false + }) + } + ); + + if (!response.ok) { + throw new Error(`태그 타입 요청 실패: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + // 결과가 배열인지 확인 + if (Array.isArray(data)) { + return data; + } else { + // 단일 객체인 경우 배열로 변환 + return [data]; + } + } catch (error) { + console.error('태그 타입 목록 가져오기 실패:', error); + throw error; + } +} + +// 4. 기존 함수 수정 - saveObjectClassesToDatabase +async function saveObjectClassesToDatabase( + projectId: number, + classes: ObjectClass[], + projectCode: string, + token: string, + skipTagTypeSync: boolean = false // 태그 타입 동기화를 건너뛸지 여부 +): Promise<number> { try { // null이 아닌 TAG_TYPE_ID만 필터링 - const validClasses = classes.filter(cls => cls.TAG_TYPE_ID !== null); + const validClasses = classes.filter(cls => cls.TAG_TYPE_ID !== null && cls.TAG_TYPE_ID !== "") ; if (validClasses.length === 0) { console.log(`프로젝트 ID ${projectId}에 저장할 유효한 오브젝트 클래스가 없습니다.`); @@ -109,6 +275,25 @@ async function saveObjectClassesToDatabase(projectId: number, classes: ObjectCla // 모든 태그 타입 ID 목록 추출 const tagTypeCodes = validClasses.map(cls => cls.TAG_TYPE_ID!); + // skipTagTypeSync가 true인 경우 태그 타입 동기화 단계 건너뜀 + if (!skipTagTypeSync) { + // 태그 타입이 없는 경우를 대비해 태그 타입 정보 먼저 가져와서 저장 + console.log(`프로젝트 ID ${projectId}의 태그 타입 동기화 시작...`); + + try { + // 프로젝트의 모든 태그 타입 가져오기 + const allTagTypes = await getAllTagTypes(projectCode, token); + + // 태그 타입 저장 + await saveTagTypesToDatabase(allTagTypes, projectCode); + } catch (error) { + console.error(`프로젝트 ${projectCode}의 태그 타입 동기화 실패:`, error); + // 에러가 발생해도 계속 진행 + } + + console.log(`프로젝트 ID ${projectId}의 태그 타입 동기화 완료`); + } + // 존재하는 태그 타입 확인 const existingTagTypeCodes = await verifyTagTypes(projectId, tagTypeCodes); @@ -122,6 +307,7 @@ async function saveObjectClassesToDatabase(projectId: number, classes: ObjectCla return 0; } + // 이하 기존 코드와 동일 // 현재 프로젝트의 오브젝트 클래스 코드 가져오기 const existingClasses = await db.select() .from(tagClasses) @@ -223,7 +409,7 @@ async function saveObjectClassesToDatabase(projectId: number, classes: ObjectCla } } -// 메인 동기화 함수 +// 5. 메인 동기화 함수 수정 export async function syncObjectClasses() { try { console.log('오브젝트 클래스 동기화 시작:', new Date().toISOString()); @@ -234,15 +420,55 @@ export async function syncObjectClasses() { // 2. 모든 프로젝트 가져오기 const allProjects = await db.select().from(projects); - // 3. 각 프로젝트에 대해 오브젝트 클래스 동기화 + // 3. 모든 프로젝트에 대해 먼저 태그 타입 동기화 (바로 이 부분이 추가됨) + console.log('모든 프로젝트의 태그 타입 동기화 시작...'); + + const tagTypeResults = await Promise.allSettled( + allProjects.map(async (project: Project) => { + try { + console.log(`프로젝트 ${project.code}의 태그 타입 동기화 시작...`); + // 프로젝트의 모든 태그 타입 가져오기 + const allTagTypes = await getAllTagTypes(project.code, token); + + // 태그 타입 저장 + await saveTagTypesToDatabase(allTagTypes, project.code); + console.log(`프로젝트 ${project.code}의 태그 타입 동기화 완료`); + + return { + project: project.code, + success: true, + count: allTagTypes.length + }; + } catch (error) { + console.error(`프로젝트 ${project.code}의 태그 타입 동기화 실패:`, error); + return { + project: project.code, + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + }) + ); + + // 태그 타입 동기화 결과 집계 + const tagTypeSuccessCount = tagTypeResults.filter( + result => result.status === 'fulfilled' && result.value.success + ).length; + + const tagTypeFailCount = tagTypeResults.length - tagTypeSuccessCount; + + console.log(`모든 프로젝트의 태그 타입 동기화 완료: ${tagTypeSuccessCount}개 성공, ${tagTypeFailCount}개 실패`); + + // 4. 각 프로젝트에 대해 오브젝트 클래스 동기화 (태그 타입 동기화는 건너뜀) const results = await Promise.allSettled( allProjects.map(async (project: Project) => { try { // 오브젝트 클래스 데이터 가져오기 const objectClasses = await getObjectClasses(project.code, token); - // 데이터베이스에 저장 - const count = await saveObjectClassesToDatabase(project.id, objectClasses); + // 데이터베이스에 저장 (skipTagTypeSync를 true로 설정하여 태그 타입 동기화 건너뜀) + const count = await saveObjectClassesToDatabase(project.id, objectClasses, project.code, token, true); + return { project: project.code, success: true, @@ -291,10 +517,17 @@ export async function syncObjectClasses() { console.log(`오브젝트 클래스 동기화 완료: ${successCount}개 프로젝트 성공 (총 ${totalItems}개 항목), ${failCount}개 프로젝트 실패`); + // 전체 결과에 태그 타입 동기화 결과도 포함 return { - success: successCount, - failed: failCount, - items: totalItems, + tagTypeSync: { + success: tagTypeSuccessCount, + failed: tagTypeFailCount + }, + objectClassSync: { + success: successCount, + failed: failCount, + items: totalItems + }, timestamp: new Date().toISOString() }; } catch (error) { diff --git a/lib/sedp/sync-projects.ts b/lib/sedp/sync-projects.ts index 1094b55f..0f5ed2a8 100644 --- a/lib/sedp/sync-projects.ts +++ b/lib/sedp/sync-projects.ts @@ -38,15 +38,12 @@ async function getProjects(): Promise<Project[]> { const response = await fetch( `${SEDP_API_BASE_URL}/Project/Get`, { - method: 'POST', + method: 'GET', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey - }, - body: JSON.stringify({ - ContainDeleted: true - }) + } } ); diff --git a/lib/sedp/sync-tag-types.ts b/lib/sedp/sync-tag-types.ts index 2d19fc19..8233badd 100644 --- a/lib/sedp/sync-tag-types.ts +++ b/lib/sedp/sync-tag-types.ts @@ -118,7 +118,7 @@ async function getTagTypes(projectCode: string, token: string): Promise<TagType[ }, body: JSON.stringify({ ProjectNo: projectCode, - ContainDeleted: true + ContainDeleted: false }) } ); @@ -149,7 +149,7 @@ async function getAttributes(projectCode: string, token: string): Promise<Attrib }, body: JSON.stringify({ ProjectNo: projectCode, - ContainDeleted: true + ContainDeleted: false }) } ); @@ -170,7 +170,7 @@ async function getAttributes(projectCode: string, token: string): Promise<Attrib async function getCodeList(projectCode: string, codeListId: string, token: string): Promise<CodeList | null> { try { const response = await fetch( - `${SEDP_API_BASE_URL}/CodeList/Get`, + `${SEDP_API_BASE_URL}/CodeList/GetByID`, { method: 'POST', headers: { @@ -182,7 +182,7 @@ async function getCodeList(projectCode: string, codeListId: string, token: strin body: JSON.stringify({ ProjectNo: projectCode, CL_ID: codeListId, - ContainDeleted: true + ContainDeleted: false }) } ); @@ -299,34 +299,64 @@ async function processAndSaveTagSubfields( // 1. 새 서브필드 삽입 if (toInsert.length > 0) { - await db.insert(tagSubfields).values(toInsert); - totalChanged += toInsert.length; - console.log(`프로젝트 ID ${projectId}에 ${toInsert.length}개의 새 태그 서브필드 추가 완료`); + // 중복 제거를 위한 Map 생성 (마지막 항목만 유지) + const uniqueInsertMap = new Map(); + + for (const item of toInsert) { + const compositeKey = `${item.projectId}:${item.tagTypeCode}:${item.attributesId}`; + uniqueInsertMap.set(compositeKey, item); + } + + // 중복이 제거된 배열 생성 + const deduplicatedInserts = Array.from(uniqueInsertMap.values()); + + // 중복 제거된 항목만 삽입 + await db.insert(tagSubfields).values(deduplicatedInserts); + + // 중복 제거 전후 개수 로그 + console.log(`프로젝트 ID ${projectId}에 ${deduplicatedInserts.length}개의 새 태그 서브필드 추가 완료 (중복 제거 전: ${toInsert.length}개)`); + totalChanged += deduplicatedInserts.length; } - // 2. 기존 서브필드 업데이트 - for (const item of toUpdate) { - await db.update(tagSubfields) - .set({ - attributesDescription: item.attributesDescription, - expression: item.expression, - delimiter: item.delimiter, - sortOrder: item.sortOrder, - updatedAt: item.updatedAt - }) - .where( - and( - eq(tagSubfields.projectId, item.projectId), - eq(tagSubfields.tagTypeCode, item.tagTypeCode), - eq(tagSubfields.attributesId, item.attributesId) - ) - ); - totalChanged += 1; - } - if (toUpdate.length > 0) { - console.log(`프로젝트 ID ${projectId}의 ${toUpdate.length}개 태그 서브필드 업데이트 완료`); - } + // 2. 기존 서브필드 업데이트 +if (toUpdate.length > 0) { + // 중복 제거를 위한 Map 생성 (마지막 항목만 유지) + const uniqueUpdateMap = new Map(); + + for (const item of toUpdate) { + const compositeKey = `${item.projectId}:${item.tagTypeCode}:${item.attributesId}`; + uniqueUpdateMap.set(compositeKey, item); + } + + // 중복이 제거된 배열 생성 + const deduplicatedUpdates = Array.from(uniqueUpdateMap.values()); + + // 중복 제거 전후 개수 로그 + console.log(`프로젝트 ID ${projectId}의 ${deduplicatedUpdates.length}개 태그 서브필드 업데이트 시작 (중복 제거 전: ${toUpdate.length}개)`); + + // 각 항목 개별 업데이트 + for (const item of deduplicatedUpdates) { + await db.update(tagSubfields) + .set({ + attributesDescription: item.attributesDescription, + expression: item.expression, + delimiter: item.delimiter, + sortOrder: item.sortOrder, + updatedAt: item.updatedAt + }) + .where( + and( + eq(tagSubfields.projectId, item.projectId), + eq(tagSubfields.tagTypeCode, item.tagTypeCode), + eq(tagSubfields.attributesId, item.attributesId) + ) + ); + totalChanged += 1; + } + + console.log(`프로젝트 ID ${projectId}의 ${deduplicatedUpdates.length}개 태그 서브필드 업데이트 완료`); +} // 3. 더 이상 존재하지 않는 서브필드 삭제 if (keysToDelete.length > 0) { |
