diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-08 03:08:19 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-04-08 03:08:19 +0000 |
| commit | 9ceed79cf32c896f8a998399bf1b296506b2cd4a (patch) | |
| tree | f84750fa6cac954d5e31221fc47a54c655fc06a9 /lib/sedp | |
| parent | 230ce796836c25df26c130dbcd616ef97d12b2ec (diff) | |
로그인 및 미들웨어 처리. 구조 변경
Diffstat (limited to 'lib/sedp')
| -rw-r--r-- | lib/sedp/sedp-token.ts | 91 | ||||
| -rw-r--r-- | lib/sedp/sync-form.ts | 512 | ||||
| -rw-r--r-- | lib/sedp/sync-object-class.ts | 304 | ||||
| -rw-r--r-- | lib/sedp/sync-projects.ts | 194 | ||||
| -rw-r--r-- | lib/sedp/sync-tag-types.ts | 567 |
5 files changed, 1668 insertions, 0 deletions
diff --git a/lib/sedp/sedp-token.ts b/lib/sedp/sedp-token.ts new file mode 100644 index 00000000..bac6bdca --- /dev/null +++ b/lib/sedp/sedp-token.ts @@ -0,0 +1,91 @@ +// 환경 변수 +const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/dev/api'; +const SEDP_API_USER_ID = process.env.SEDP_API_USER_ID || 'EVCPUSER'; +const SEDP_API_PASSWORD = process.env.SEDP_API_PASSWORD || 'evcpuser@2025'; + +/** + * SEDP API에서 인증 토큰을 가져옵니다. + * 매 호출 시마다 새로운 토큰을 발급받습니다. + */ +export async function getSEDPToken(): Promise<string> { + try { + const response = await fetch( + `${SEDP_API_BASE_URL}/Security/RequestToken`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*' + }, + body: JSON.stringify({ + UserID: SEDP_API_USER_ID, + Password: SEDP_API_PASSWORD + }) + } + ); + + if (!response.ok) { + throw new Error(`SEDP 토큰 요청 실패: ${response.status} ${response.statusText}`); + } + + // 응답이 직접 토큰 문자열인 경우 + const tokenData = await response.text(); + + // 응답이 JSON 형식이면 파싱 + try { + const jsonData = JSON.parse(tokenData); + if (typeof jsonData === 'string') { + return jsonData; // JSON 문자열이지만 내용물이 토큰 문자열인 경우 + } else if (jsonData.token) { + return jsonData.token; // { token: "..." } 형태인 경우 + } else { + console.warn('예상치 못한 토큰 응답 형식:', jsonData); + // 가장 가능성 있는 필드를 찾아봄 + for (const key of ['token', 'accessToken', 'access_token', 'Token', 'jwt']) { + if (jsonData[key]) return jsonData[key]; + } + // 그래도 없으면 문자열로 변환 + return JSON.stringify(jsonData); + } + } catch (e) { + // 파싱 실패 = 응답이 JSON이 아닌 순수 토큰 문자열 + return tokenData.trim(); + } + } catch (error) { + console.error('SEDP 토큰 가져오기 실패:', error); + throw error; + } +} + +/** + * SEDP API에 인증된 요청을 보냅니다. + */ +export async function fetchSEDP(endpoint: string, options: RequestInit = {}): Promise<any> { + try { + // 토큰 가져오기 + const token = await getSEDPToken(); + + // 헤더 준비 + const headers = { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': token, + ...(options.headers || {}) + }; + + // 요청 보내기 + const response = await fetch(`${SEDP_API_BASE_URL}${endpoint}`, { + ...options, + headers + }); + + if (!response.ok) { + throw new Error(`SEDP API 요청 실패 (${endpoint}): ${response.status} ${response.statusText}`); + } + + return response.json(); + } catch (error) { + console.error(`SEDP API 오류 (${endpoint}):`, error); + throw error; + } +}
\ No newline at end of file diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts new file mode 100644 index 00000000..b9e6fa90 --- /dev/null +++ b/lib/sedp/sync-form.ts @@ -0,0 +1,512 @@ +// 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 { getSEDPToken } from "./sedp-token"; + +// 환경 변수 +const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/dev/api'; + +// 인터페이스 정의 +interface Register { + PROJ_NO: string; + TYPE_ID: string; + EP_ID: string; + DESC: string; + REMARK: string | null; + NEW_TAG_YN: boolean; + ALL_TAG_YN: boolean; + VND_YN: boolean; + SEQ: number; + CMPLX_YN: boolean; + CMPL_SETT: any | null; + MAP_ATT: any[]; + MAP_CLS_ID: string[]; + MAP_OPER: any | null; + LNK_ATT: LinkAttribute[]; + JOIN_TABLS: any[]; + DELETED: boolean; + CRTER_NO: string; + CRTE_DTM: string; + CHGER_NO: string | null; + CHGE_DTM: string | null; + _id: string; +} + +interface LinkAttribute { + ATT_ID: string; + CPY_DESC: string; + JOIN_KEY_ATT_ID: string | null; + JOIN_VAL_ATT_ID: string | null; + KEY_YN: boolean; + EDIT_YN: boolean; + PUB_YN: boolean; + VND_YN: boolean; + DEF_VAL: string | null; + UOM_ID: string | null; +} + +interface Attribute { + PROJ_NO: string; + ATT_ID: string; + DESC: string; + GROUP: string | null; + REMARK: string | null; + VAL_TYPE: string; + IGN_LIST_VAL: boolean; + CL_ID: string | null; + UOM_ID: string | null; + DEF_VAL: string | null; + MIN_VAL: number; + MAX_VAL: number; + ESS_YN: boolean; + SEQ: number; + FORMAT: string | null; + REG_EXPS: string | null; + ATTRIBUTES: any[]; + DELETED: boolean; + CRTER_NO: string; + CRTE_DTM: string; + CHGER_NO: string | null; + CHGE_DTM: string | null; + _id: string; +} + +interface CodeList { + PROJ_NO: string; + CL_ID: string; + DESC: string; + REMARK: string | null; + PRNT_CD_ID: string | null; + REG_TYPE_ID: string | null; + VAL_ATT_ID: string | null; + VALUES: CodeValue[]; + LNK_ATT: any[]; + DELETED: boolean; + CRTER_NO: string; + CRTE_DTM: string; + CHGER_NO: string | null; + CHGE_DTM: string | null; + _id: string; +} + +interface CodeValue { + PRNT_VALUE: string | null; + VALUE: string; + DESC: string; + REMARK: string; + USE_YN: boolean; + SEQ: number; + ATTRIBUTES: any[]; +} + +interface UOM { + PROJ_NO: string; + UOM_ID: string; + DESC: string; + SYMBOL: string; + CONV_RATE: number; + DELETED: boolean; + CRTER_NO: string; + CRTE_DTM: string; + CHGER_NO: string | null; + CHGE_DTM: string | null; + _id: string; +} + +interface Project { + id: number; + code: string; + name: string; + type?: string; + createdAt?: Date; + updatedAt?: Date; +} + +interface SyncResult { + project: string; + success: boolean; + count?: number; + error?: string; +} + +interface FormColumn { + key: string; + label: string; + type: string; + options?: string[]; + uom?: string; + uomId?: string; +} + +// 레지스터 데이터 가져오기 +async function getRegisters(projectCode: string): Promise<Register[]> { + try { + // 토큰(API 키) 가져오기 + const apiKey = await getSEDPToken(); + + const response = await fetch( + `${SEDP_API_BASE_URL}/Register/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ContainDeleted: true + }) + } + ); + + 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(`프로젝트 ${projectCode}의 레지스터 가져오기 실패:`, error); + throw error; + } +} + +// 특정 속성 가져오기 +async function getAttributeById(projectCode: string, attributeId: string): Promise<Attribute | null> { + try { + // 토큰(API 키) 가져오기 + const apiKey = await getSEDPToken(); + + const response = await fetch( + `${SEDP_API_BASE_URL}/Attributes/GetByID`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ATT_ID: attributeId + }) + } + ); + + if (!response.ok) { + if (response.status === 404) { + console.warn(`속성 ID ${attributeId}를 찾을 수 없음`); + return null; + } + throw new Error(`속성 요청 실패: ${response.status} ${response.statusText}`); + } + + return response.json(); + } catch (error) { + console.error(`속성 ID ${attributeId} 가져오기 실패:`, error); + return null; + } +} + +// 특정 코드 리스트 가져오기 +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`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + CL_ID: codeListId + }) + } + ); + + if (!response.ok) { + if (response.status === 404) { + console.warn(`코드 리스트 ID ${codeListId}를 찾을 수 없음`); + return null; + } + throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`); + } + + return response.json(); + } catch (error) { + console.error(`코드 리스트 ID ${codeListId} 가져오기 실패:`, error); + return null; + } +} + +// 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`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + UOM_ID: uomId + }) + } + ); + + if (!response.ok) { + if (response.status === 404) { + console.warn(`UOM ID ${uomId}를 찾을 수 없음`); + return null; + } + throw new Error(`UOM 요청 실패: ${response.status} ${response.statusText}`); + } + + return response.json(); + } catch (error) { + console.error(`UOM ID ${uomId} 가져오기 실패:`, error); + return null; + } +} + +// 데이터베이스에 태그 타입 클래스 폼 매핑 및 폼 메타 저장 +async function saveFormMappingsAndMetas( + 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])); + + // 저장할 매핑 목록과 폼 메타 정보 + const mappingsToSave = []; + const formMetasToSave = []; + + // 각 레지스터 처리 + for (const register of registers) { + // 삭제된 레지스터는 건너뜀 + if (register.DELETED) continue; + + // 폼 메타 데이터를 위한 컬럼 정보 구성 + const columns: FormColumn[] = []; + + // 각 속성 정보 수집 + for (const linkAtt of register.LNK_ATT) { + // 속성 가져오기 + const attribute = await getAttributeById(projectCode, linkAtt.ATT_ID); + + if (!attribute) continue; + + // 기본 컬럼 정보 + const column: FormColumn = { + key: linkAtt.ATT_ID, + label: linkAtt.CPY_DESC, + type: attribute.VAL_TYPE || 'STRING' + }; + + // 리스트 타입인 경우 옵션 추가 + if ((attribute.VAL_TYPE === 'LIST' || attribute.VAL_TYPE === 'DYNAMICLIST') && attribute.CL_ID) { + const codeList = await getCodeListById(projectCode, attribute.CL_ID); + + if (codeList && codeList.VALUES) { + // 유효한 옵션만 필터링 + const options = codeList.VALUES + .filter(value => value.USE_YN) + .map(value => value.DESC); + + if (options.length > 0) { + column.options = options; + } + } + } + + // UOM 정보 추가 + if (linkAtt.UOM_ID) { + const uom = await getUomById(projectCode, linkAtt.UOM_ID); + + if (uom) { + column.uom = uom.SYMBOL; + column.uomId = uom.UOM_ID; + } + } + + columns.push(column); + } + + // 폼 메타 정보 저장 + formMetasToSave.push({ + projectId, + formCode: register.TYPE_ID, + formName: register.DESC, + columns: JSON.stringify(columns), + createdAt: new Date(), + updatedAt: new Date() + }); + + // 관련된 클래스 매핑 처리 + for (const classId of register.MAP_CLS_ID) { + // 해당 클래스와 태그 타입 확인 + const tagClass = tagClassMap.get(classId); + + if (!tagClass) { + console.warn(`클래스 ID ${classId}를 프로젝트 ID ${projectId}에서 찾을 수 없음`); + continue; + } + + const tagTypeCode = tagClass.tagTypeCode; + const tagType = tagTypeMap.get(tagTypeCode); + + if (!tagType) { + console.warn(`태그 타입 ${tagTypeCode}를 프로젝트 ID ${projectId}에서 찾을 수 없음`); + continue; + } + + // 매핑 정보 저장 + mappingsToSave.push({ + projectId, + tagTypeLabel: tagType.description, + classLabel: tagClass.label, + formCode: register.TYPE_ID, + formName: register.DESC, + 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}개의 폼 메타 정보 저장 완료`); + } + + return totalSaved; + } catch (error) { + console.error(`폼 매핑 및 메타 저장 실패 (프로젝트 ID: ${projectId}):`, error); + throw error; + } +} + +// 메인 동기화 함수 +export async function syncTagFormMappings() { + try { + console.log('태그 폼 매핑 동기화 시작:', new Date().toISOString()); + + // 모든 프로젝트 가져오기 + const allProjects = await db.select().from(projects); + + // 각 프로젝트에 대해 폼 매핑 동기화 + const results = await Promise.allSettled( + allProjects.map(async (project: Project) => { + try { + // 레지스터 데이터 가져오기 + const registers = await getRegisters(project.code); + + // 데이터베이스에 저장 + const count = await saveFormMappingsAndMetas(project.id, project.code, registers); + return { + project: project.code, + success: true, + count + } as SyncResult; + } catch (error) { + console.error(`프로젝트 ${project.code} 폼 매핑 동기화 실패:`, error); + return { + project: project.code, + success: false, + error: error instanceof Error ? error.message : String(error) + } as SyncResult; + } + }) + ); + + // 결과 처리를 위한 배열 준비 + const successfulResults: SyncResult[] = []; + const failedResults: SyncResult[] = []; + + // 결과 분류 + results.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value.success) { + successfulResults.push(result.value); + } else { + failedResults.push(result.value); + } + } else { + // 거부된 프로미스는 실패로 간주 + failedResults.push({ + project: 'unknown', + success: false, + error: result.reason?.toString() || 'Unknown error' + }); + } + }); + + const successCount = successfulResults.length; + const failCount = failedResults.length; + + // 이제 안전하게 count 속성에 접근 가능 + const totalItems = successfulResults.reduce((sum, result) => + sum + (result.count || 0), 0 + ); + + console.log(`태그 폼 매핑 동기화 완료: ${successCount}개 프로젝트 성공 (총 ${totalItems}개 항목), ${failCount}개 프로젝트 실패`); + + return { + success: successCount, + failed: failCount, + items: totalItems, + timestamp: new Date().toISOString() + }; + } catch (error) { + console.error('태그 폼 매핑 동기화 중 오류 발생:', error); + throw error; + } +}
\ No newline at end of file diff --git a/lib/sedp/sync-object-class.ts b/lib/sedp/sync-object-class.ts new file mode 100644 index 00000000..1cf0c23b --- /dev/null +++ b/lib/sedp/sync-object-class.ts @@ -0,0 +1,304 @@ +import db from "@/db/db"; +import { projects, tagClasses, tagTypes } from '@/db/schema'; +import { eq, and } 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'; + +// ObjectClass 인터페이스 정의 +interface ObjectClass { + PROJ_NO: string; + CLS_ID: string; + DESC: string; + TAG_TYPE_ID: string | null; + PRT_CLS_ID: string | null; + LNK_ATT: any[]; + DELETED: boolean; + DEL_USER: string | null; + DEL_DTM: string | null; + CRTER_NO: string; + CRTE_DTM: string; + CHGER_NO: string | null; + CHGE_DTM: string | null; + _id: string; +} + +interface Project { + id: number; + code: string; + name: string; + type?: string; + createdAt?: Date; + updatedAt?: Date; +} + +// 동기화 결과 인터페이스 +interface SyncResult { + project: string; + success: boolean; + count?: number; + error?: string; +} + +// 오브젝트 클래스 데이터 가져오기 +async function getObjectClasses(projectCode: string, token:string): Promise<ObjectClass[]> { + try { + const response = await fetch( + `${SEDP_API_BASE_URL}/ObjectClass/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': token, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ContainDeleted: true + }) + } + ); + + 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(`프로젝트 ${projectCode}의 오브젝트 클래스 가져오기 실패:`, error); + throw error; + } +} + +// 태그 타입 존재 확인 +async function verifyTagTypes(projectId: number, tagTypeCodes: string[]): Promise<Set<string>> { + try { + // 프로젝트에 있는 태그 타입 코드 조회 + const existingTagTypes = await db.select({ code: tagTypes.code }) + .from(tagTypes) + .where(eq(tagTypes.projectId, projectId)); + + // 존재하는 태그 타입 코드 Set으로 반환 + return new Set(existingTagTypes.map(type => type.code)); + } catch (error) { + console.error(`프로젝트 ID ${projectId}의 태그 타입 확인 실패:`, error); + throw error; + } +} + +// 데이터베이스에 오브젝트 클래스 저장 (upsert 사용) +async function saveObjectClassesToDatabase(projectId: number, classes: ObjectClass[]): Promise<number> { + try { + // null이 아닌 TAG_TYPE_ID만 필터링 + const validClasses = classes.filter(cls => cls.TAG_TYPE_ID !== null); + + if (validClasses.length === 0) { + console.log(`프로젝트 ID ${projectId}에 저장할 유효한 오브젝트 클래스가 없습니다.`); + return 0; + } + + // 모든 태그 타입 ID 목록 추출 + const tagTypeCodes = validClasses.map(cls => cls.TAG_TYPE_ID!); + + // 존재하는 태그 타입 확인 + const existingTagTypeCodes = await verifyTagTypes(projectId, tagTypeCodes); + + // 태그 타입이 존재하는 오브젝트 클래스만 필터링 + const classesToSave = validClasses.filter(cls => + cls.TAG_TYPE_ID !== null && existingTagTypeCodes.has(cls.TAG_TYPE_ID) + ); + + if (classesToSave.length === 0) { + console.log(`프로젝트 ID ${projectId}에 저장할 유효한 오브젝트 클래스가 없습니다 (태그 타입 존재하지 않음).`); + return 0; + } + + // 현재 프로젝트의 오브젝트 클래스 코드 가져오기 + const existingClasses = await db.select() + .from(tagClasses) + .where(eq(tagClasses.projectId, projectId)); + + // 코드 기준으로 맵 생성 + const existingClassMap = new Map( + existingClasses.map(cls => [cls.code, cls]) + ); + + // 새로 추가할 항목 + const toInsert = []; + + // 업데이트할 항목 + const toUpdate = []; + + // API에 있는 코드 목록 + const apiClassCodes = new Set(classesToSave.map(cls => cls.CLS_ID)); + + // 삭제할 코드 목록 + const codesToDelete = existingClasses + .map(cls => cls.code) + .filter(code => !apiClassCodes.has(code)); + + // 클래스 데이터 처리 + for (const cls of classesToSave) { + // 데이터베이스 레코드 준비 + const record = { + code: cls.CLS_ID, + projectId: projectId, + label: cls.DESC, + tagTypeCode: cls.TAG_TYPE_ID!, + updatedAt: new Date() + }; + + // 이미 존재하는 코드인지 확인 + if (existingClassMap.has(cls.CLS_ID)) { + // 업데이트 항목에 추가 + toUpdate.push(record); + } else { + // 새로 추가할 항목에 추가 (createdAt 필드 추가) + toInsert.push({ + ...record, + createdAt: new Date() + }); + } + } + + // 트랜잭션 실행 + let totalChanged = 0; + + // 1. 새 항목 삽입 + if (toInsert.length > 0) { + await db.insert(tagClasses).values(toInsert); + totalChanged += toInsert.length; + console.log(`프로젝트 ID ${projectId}에 ${toInsert.length}개의 새 오브젝트 클래스 추가 완료`); + } + + // 2. 기존 항목 업데이트 + for (const item of toUpdate) { + await db.update(tagClasses) + .set({ + label: item.label, + tagTypeCode: item.tagTypeCode, + updatedAt: item.updatedAt + }) + .where( + and( + eq(tagClasses.code, item.code), + eq(tagClasses.projectId, item.projectId) + ) + ); + totalChanged += 1; + } + + if (toUpdate.length > 0) { + console.log(`프로젝트 ID ${projectId}의 ${toUpdate.length}개 오브젝트 클래스 업데이트 완료`); + } + + // 3. 더 이상 존재하지 않는 항목 삭제 + if (codesToDelete.length > 0) { + for (const code of codesToDelete) { + await db.delete(tagClasses) + .where( + and( + eq(tagClasses.code, code), + eq(tagClasses.projectId, projectId) + ) + ); + } + console.log(`프로젝트 ID ${projectId}에서 ${codesToDelete.length}개의 오브젝트 클래스 삭제 완료`); + totalChanged += codesToDelete.length; + } + + return totalChanged; + } catch (error) { + console.error(`오브젝트 클래스 저장 실패 (프로젝트 ID: ${projectId}):`, error); + throw error; + } +} + +// 메인 동기화 함수 +export async function syncObjectClasses() { + try { + console.log('오브젝트 클래스 동기화 시작:', new Date().toISOString()); + + // 1. 토큰 가져오기 + const token = await getSEDPToken(); + + // 2. 모든 프로젝트 가져오기 + const allProjects = await db.select().from(projects); + + // 3. 각 프로젝트에 대해 오브젝트 클래스 동기화 + 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); + return { + project: project.code, + success: true, + count + } as SyncResult; + } catch (error) { + console.error(`프로젝트 ${project.code} 동기화 실패:`, error); + return { + project: project.code, + success: false, + error: error instanceof Error ? error.message : String(error) + } as SyncResult; + } + }) + ); + + // 결과 처리를 위한 배열 준비 + const successfulResults: SyncResult[] = []; + const failedResults: SyncResult[] = []; + + // 결과 분류 + results.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value.success) { + successfulResults.push(result.value); + } else { + failedResults.push(result.value); + } + } else { + // 거부된 프로미스는 실패로 간주 + failedResults.push({ + project: 'unknown', + success: false, + error: result.reason?.toString() || 'Unknown error' + }); + } + }); + + const successCount = successfulResults.length; + const failCount = failedResults.length; + + // 이제 안전하게 count 속성에 접근 가능 + const totalItems = successfulResults.reduce((sum, result) => + sum + (result.count || 0), 0 + ); + + console.log(`오브젝트 클래스 동기화 완료: ${successCount}개 프로젝트 성공 (총 ${totalItems}개 항목), ${failCount}개 프로젝트 실패`); + + return { + success: successCount, + failed: failCount, + items: totalItems, + timestamp: new Date().toISOString() + }; + } catch (error) { + console.error('오브젝트 클래스 동기화 중 오류 발생:', error); + throw error; + } +}
\ No newline at end of file diff --git a/lib/sedp/sync-projects.ts b/lib/sedp/sync-projects.ts new file mode 100644 index 00000000..1094b55f --- /dev/null +++ b/lib/sedp/sync-projects.ts @@ -0,0 +1,194 @@ +// src/lib/cron/syncProjects.ts +import db from "@/db/db"; +import { projects } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { getSEDPToken } from "./sedp-token"; + +// 환경 변수 +const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/dev/api'; + +// 인터페이스 정의 +interface Project { + PROJ_NO: string; + DESC: string; + TYPE?: string; + DELETED?: boolean; + DEL_USER?: string | null; + DEL_DTM?: string | null; + CRTER_NO?: string; + CRTE_DTM?: string; + CHGER_NO?: string | null; + CHGE_DTM?: string | null; + _id?: string; +} + +interface SyncResult { + success: number; + failed: number; + items: number; + timestamp: string; +} + +// 프로젝트 데이터 가져오기 +async function getProjects(): Promise<Project[]> { + try { + // 토큰(API 키) 가져오기 + const apiKey = await getSEDPToken(); + + const response = await fetch( + `${SEDP_API_BASE_URL}/Project/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': apiKey + }, + body: JSON.stringify({ + ContainDeleted: true + }) + } + ); + + 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; + } +} + +// 데이터베이스에 프로젝트 저장 +async function saveProjectsToDatabase(projectsData: Project[]): Promise<number> { + try { + // 기존 프로젝트 조회 + const existingProjects = await db.select().from(projects); + + // 코드 기준으로 맵 생성 + const existingProjectMap = new Map( + existingProjects.map(project => [project.code, project]) + ); + + // 새로 추가할 항목 + const toInsert = []; + + // 업데이트할 항목 + const toUpdate = []; + + // API에 있는 코드 목록 + const apiProjectCodes = new Set(projectsData.map(project => project.PROJ_NO)); + + // 삭제할 코드 목록 + const codesToDelete = [...existingProjectMap.keys()] + .filter(code => !apiProjectCodes.has(code)); + + // 프로젝트 데이터 처리 + for (const project of projectsData) { + // 삭제된 프로젝트는 건너뜀 + if (project.DELETED) continue; + + // 프로젝트 레코드 준비 + const projectRecord = { + code: project.PROJ_NO, + name: project.DESC || project.PROJ_NO, + type: project.TYPE || 'ship', + updatedAt: new Date() + }; + + // 이미 존재하는 코드인지 확인 + if (existingProjectMap.has(project.PROJ_NO)) { + // 업데이트 항목에 추가 + toUpdate.push(projectRecord); + } else { + // 새로 추가할 항목에 추가 + toInsert.push({ + ...projectRecord, + createdAt: new Date() + }); + } + } + + // 트랜잭션 실행 + let totalChanged = 0; + + // 1. 새 프로젝트 삽입 + if (toInsert.length > 0) { + await db.insert(projects).values(toInsert); + totalChanged += toInsert.length; + console.log(`${toInsert.length}개의 새 프로젝트 추가 완료`); + } + + // 2. 기존 프로젝트 업데이트 + for (const item of toUpdate) { + await db.update(projects) + .set({ + name: item.name, + type: item.type, + updatedAt: item.updatedAt + }) + .where(eq(projects.code, item.code)); + totalChanged += 1; + } + + if (toUpdate.length > 0) { + console.log(`${toUpdate.length}개 프로젝트 업데이트 완료`); + } + + // 3. 더 이상 존재하지 않는 프로젝트 삭제 + if (codesToDelete.length > 0) { + for (const code of codesToDelete) { + await db.delete(projects) + .where(eq(projects.code, code)); + } + console.log(`${codesToDelete.length}개의 프로젝트 삭제 완료`); + totalChanged += codesToDelete.length; + } + + return totalChanged; + } catch (error) { + console.error('프로젝트 저장 실패:', error); + throw error; + } +} + +// 메인 동기화 함수 +export async function syncProjects(): Promise<SyncResult> { + try { + console.log('프로젝트 동기화 시작:', new Date().toISOString()); + + // 1. 프로젝트 데이터 가져오기 + const projectsData = await getProjects(); + console.log(`${projectsData.length}개의 프로젝트 정보를 가져왔습니다.`); + + // 2. 데이터베이스에 저장 + const totalItems = await saveProjectsToDatabase(projectsData); + + console.log(`프로젝트 동기화 완료: 총 ${totalItems}개 항목 처리됨`); + + return { + success: 1, // 단일 작업이므로 성공은 1 + failed: 0, + items: totalItems, + timestamp: new Date().toISOString() + }; + } catch (error) { + console.error('프로젝트 동기화 중 오류 발생:', error); + return { + success: 0, + failed: 1, + items: 0, + timestamp: new Date().toISOString() + }; + } +}
\ No newline at end of file diff --git a/lib/sedp/sync-tag-types.ts b/lib/sedp/sync-tag-types.ts new file mode 100644 index 00000000..2d19fc19 --- /dev/null +++ b/lib/sedp/sync-tag-types.ts @@ -0,0 +1,567 @@ +// src/lib/cron/syncTagSubfields.ts +import db from "@/db/db"; +import { projects, tagTypes, tagSubfields, tagSubfieldOptions } from '@/db/schema'; +import { eq, and, inArray } 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 TagType { + PROJ_NO: string; + TYPE_ID: string; + DESC: string | null; + REMARK?: string | null; + SEQ?: number; + LNK_CODE: LinkCode[]; + DELETED?: boolean; + CRTER_NO?: string; + CRTE_DTM?: string; + CHGER_NO?: string | null; + CHGE_DTM?: string | null; + _id?: string; +} + +interface LinkCode { + SEQ: number; + ATT_ID: string; + DL_VAL: string; + REPR_YN: boolean; + START: number; + LENGTH: number; + IS_SEQ: boolean; +} + +interface Attribute { + PROJ_NO: string; + ATT_ID: string; + DESC: string; + GROUP?: string | null; + REMARK?: string | null; + VAL_TYPE?: string; + IGN_LIST_VAL?: boolean; + CL_ID?: string | null; + UOM_ID?: string | null; + DEF_VAL?: string | null; + MIN_VAL?: number; + MAX_VAL?: number; + ESS_YN?: boolean; + SEQ?: number; + FORMAT?: string | null; + REG_EXPS?: string | null; + ATTRIBUTES?: any[]; + DELETED?: boolean; + CRTER_NO?: string; + CRTE_DTM?: string; + CHGER_NO?: string | null; + CHGE_DTM?: string | null; + _id?: string; +} + +interface CodeList { + PROJ_NO: string; + CL_ID: string; + DESC: string; + REMARK?: string | null; + PRNT_CD_ID?: string | null; + REG_TYPE_ID?: string | null; + VAL_ATT_ID?: string | null; + VALUES: CodeValue[]; + LNK_ATT?: any[]; + DELETED?: boolean; + CRTER_NO?: string; + CRTE_DTM?: string; + CHGER_NO?: string | null; + CHGE_DTM?: string | null; + _id?: string; +} + +interface CodeValue { + PRNT_VALUE?: string | null; + VALUE: string; + DESC: string; + REMARK?: string; + USE_YN: boolean; + SEQ: number; + ATTRIBUTES?: any[]; +} + +interface Project { + id: number; + code: string; + name: string; + type?: string; + createdAt?: Date; + updatedAt?: Date; +} + +interface SyncResult { + project: string; + success: boolean; + count?: number; + error?: string; +} + +// 태그 타입 데이터 가져오기 +async function getTagTypes(projectCode: string, token: string): Promise<TagType[] | TagType> { + try { + 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: true + }) + } + ); + + if (!response.ok) { + throw new Error(`태그 타입 요청 실패: ${response.status} ${response.statusText}`); + } + + return response.json(); + } catch (error) { + console.error(`프로젝트 ${projectCode}의 태그 타입 가져오기 실패:`, error); + throw error; + } +} + +// 속성 데이터 가져오기 +async function getAttributes(projectCode: string, token: string): Promise<Attribute[]> { + try { + const response = await fetch( + `${SEDP_API_BASE_URL}/Attributes/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': token, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ProjectNo: projectCode, + ContainDeleted: true + }) + } + ); + + if (!response.ok) { + throw new Error(`속성 요청 실패: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + return Array.isArray(data) ? data : [data]; + } catch (error) { + console.error(`프로젝트 ${projectCode}의 속성 가져오기 실패:`, error); + throw error; + } +} + +// 코드 리스트 가져오기 +async function getCodeList(projectCode: string, codeListId: string, token: string): Promise<CodeList | null> { + try { + const response = await fetch( + `${SEDP_API_BASE_URL}/CodeList/Get`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'accept': '*/*', + 'ApiKey': token, + 'ProjectNo': projectCode + }, + body: JSON.stringify({ + ProjectNo: projectCode, + CL_ID: codeListId, + ContainDeleted: true + }) + } + ); + + if (!response.ok) { + throw new Error(`코드 리스트 요청 실패: ${response.status} ${response.statusText}`); + } + + return response.json(); + } catch (error) { + console.error(`프로젝트 ${projectCode}의 코드 리스트 가져오기 실패:`, error); + return null; // 코드 리스트를 가져오지 못해도 전체 프로세스는 계속 진행 + } +} + +// 태그 서브필드 처리 및 저장 +async function processAndSaveTagSubfields( + projectId: number, + projectCode: string, + tagTypesData: TagType[], + attributesData: Attribute[], + token: string +): Promise<number> { + try { + // 속성 ID를 키로 하는 맵 생성 + const attributesMap = new Map<string, Attribute>(); + attributesData.forEach(attr => { + attributesMap.set(attr.ATT_ID, attr); + }); + + // 현재 DB에 있는 태그 서브필드 가져오기 + const existingSubfields = await db.select().from(tagSubfields) + .where(eq(tagSubfields.projectId, projectId)); + + // 서브필드 키 생성 함수 + const createSubfieldKey = (tagTypeCode: string, attributeId: string) => + `${tagTypeCode}:${attributeId}`; + + // 현재 DB에 있는 서브필드를 키-값 맵으로 변환 + const existingSubfieldsMap = new Map(); + existingSubfields.forEach(subfield => { + const key = createSubfieldKey(subfield.tagTypeCode, subfield.attributesId); + existingSubfieldsMap.set(key, subfield); + }); + + // 새로 추가할 서브필드 + const toInsert = []; + + // 업데이트할 서브필드 + const toUpdate = []; + + // API에서 가져온 서브필드 키 목록 + const apiSubfieldKeys = new Set<string>(); + + // 코드 리스트 ID 목록 (나중에 코드 리스트 옵션을 가져오기 위함) + const codeListsToFetch = new Map<string, { attributeId: string, clId: string }>(); + + // 태그 타입별로 처리 + for (const tagType of tagTypesData) { + // 링크 코드가 있는 경우만 처리 + if (tagType.LNK_CODE && tagType.LNK_CODE.length > 0) { + // 각 링크 코드에 대해 서브필드 생성 + for (const linkCode of tagType.LNK_CODE) { + const attributeId = linkCode.ATT_ID; + const attribute = attributesMap.get(attributeId); + + // 해당 속성이 있는 경우만 처리 + if (attribute) { + const subFieldKey = createSubfieldKey(tagType.TYPE_ID, attributeId); + apiSubfieldKeys.add(subFieldKey); + + // 서브필드 데이터 준비 + const subfieldData = { + projectId: projectId, + tagTypeCode: tagType.TYPE_ID, + attributesId: attributeId, + attributesDescription: attribute.DESC || attributeId, + expression: attribute.REG_EXPS || null, + delimiter: linkCode.DL_VAL || null, + sortOrder: linkCode.SEQ || 0, + updatedAt: new Date() + }; + + // 이미 존재하는 서브필드인지 확인 + if (existingSubfieldsMap.has(subFieldKey)) { + // 업데이트 항목에 추가 + toUpdate.push(subfieldData); + } else { + // 새로 추가할 항목에 추가 + toInsert.push({ + ...subfieldData, + createdAt: new Date() + }); + } + + // 코드 리스트가 있으면 나중에 가져올 목록에 추가 + if (attribute.CL_ID) { + codeListsToFetch.set(attribute.CL_ID, { + attributeId: attributeId, + clId: attribute.CL_ID + }); + } + } + } + } + } + + // 삭제할 서브필드 키 목록 + const keysToDelete = [...existingSubfieldsMap.keys()] + .filter(key => !apiSubfieldKeys.has(key)); + + // 트랜잭션 실행 + let totalChanged = 0; + + // 1. 새 서브필드 삽입 + if (toInsert.length > 0) { + await db.insert(tagSubfields).values(toInsert); + totalChanged += toInsert.length; + console.log(`프로젝트 ID ${projectId}에 ${toInsert.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}개 태그 서브필드 업데이트 완료`); + } + + // 3. 더 이상 존재하지 않는 서브필드 삭제 + if (keysToDelete.length > 0) { + for (const key of keysToDelete) { + const [tagTypeCode, attributeId] = key.split(':'); + await db.delete(tagSubfields) + .where( + and( + eq(tagSubfields.projectId, projectId), + eq(tagSubfields.tagTypeCode, tagTypeCode), + eq(tagSubfields.attributesId, attributeId) + ) + ); + } + console.log(`프로젝트 ID ${projectId}에서 ${keysToDelete.length}개의 태그 서브필드 삭제 완료`); + totalChanged += keysToDelete.length; + } + + // 4. 코드 리스트 옵션 가져와서 저장 + let optionsChanged = 0; + + if (codeListsToFetch.size > 0) { + console.log(`프로젝트 ID ${projectId}의 ${codeListsToFetch.size}개 코드 리스트에 대한 옵션 처리 시작`); + + for (const [clId, { attributeId }] of codeListsToFetch.entries()) { + try { + // 코드 리스트 가져오기 + const codeList = await getCodeList(projectCode, clId, token); + + if (codeList && codeList.VALUES && codeList.VALUES.length > 0) { + // 현재 DB에 있는 옵션 가져오기 + const existingOptions = await db.select().from(tagSubfieldOptions) + .where( + and( + eq(tagSubfieldOptions.projectId, projectId), + eq(tagSubfieldOptions.attributesId, attributeId) + ) + ); + + // 현재 DB에 있는 옵션 맵 + const existingOptionsMap = new Map(); + existingOptions.forEach(option => { + existingOptionsMap.set(option.code, option); + }); + + // 새로 추가할 옵션 + const optionsToInsert = []; + + // 업데이트할 옵션 + const optionsToUpdate = []; + + // API에서 가져온 코드 목록 + const apiOptionCodes = new Set<string>(); + + // 각 코드 값을 옵션으로 추가 + for (const value of codeList.VALUES) { + // 사용 가능한 코드만 추가 + if (value.USE_YN) { + const code = value.VALUE; + apiOptionCodes.add(code); + + // 옵션 데이터 준비 + const optionData = { + projectId: projectId, + attributesId: attributeId, + code: code, + label: value.DESC || code, + updatedAt: new Date() + }; + + // 이미 존재하는 옵션인지 확인 + if (existingOptionsMap.has(code)) { + // 업데이트 항목에 추가 + optionsToUpdate.push(optionData); + } else { + // 새로 추가할 항목에 추가 + optionsToInsert.push({ + ...optionData, + createdAt: new Date() + }); + } + } + } + + // 삭제할 옵션 코드 목록 + const optionCodesToDelete = [...existingOptionsMap.keys()] + .filter(code => !apiOptionCodes.has(code)); + + // a. 새 옵션 삽입 + if (optionsToInsert.length > 0) { + await db.insert(tagSubfieldOptions).values(optionsToInsert); + optionsChanged += optionsToInsert.length; + console.log(`속성 ${attributeId}에 ${optionsToInsert.length}개의 새 옵션 추가 완료`); + } + + // b. 기존 옵션 업데이트 + for (const option of optionsToUpdate) { + await db.update(tagSubfieldOptions) + .set({ + label: option.label, + updatedAt: option.updatedAt + }) + .where( + and( + eq(tagSubfieldOptions.projectId, option.projectId), + eq(tagSubfieldOptions.attributesId, option.attributesId), + eq(tagSubfieldOptions.code, option.code) + ) + ); + optionsChanged += 1; + } + + if (optionsToUpdate.length > 0) { + console.log(`속성 ${attributeId}의 ${optionsToUpdate.length}개 옵션 업데이트 완료`); + } + + // c. 더 이상 존재하지 않는 옵션 삭제 + if (optionCodesToDelete.length > 0) { + for (const code of optionCodesToDelete) { + await db.delete(tagSubfieldOptions) + .where( + and( + eq(tagSubfieldOptions.projectId, projectId), + eq(tagSubfieldOptions.attributesId, attributeId), + eq(tagSubfieldOptions.code, code) + ) + ); + } + console.log(`속성 ${attributeId}에서 ${optionCodesToDelete.length}개의 옵션 삭제 완료`); + optionsChanged += optionCodesToDelete.length; + } + } + } catch (error) { + console.error(`코드 리스트 ${clId} 처리 중 오류:`, error); + // 특정 코드 리스트 처리 실패해도 계속 진행 + } + } + + console.log(`프로젝트 ID ${projectId}의 코드 리스트 옵션 처리 완료: 총 ${optionsChanged}개 변경됨`); + } + + return totalChanged + optionsChanged; + } catch (error) { + console.error(`태그 서브필드 처리 실패 (프로젝트 ID: ${projectId}):`, error); + throw error; + } +} + +// 메인 동기화 함수 +export async function syncTagSubfields() { + try { + console.log('태그 서브필드 동기화 시작:', new Date().toISOString()); + + // 1. 토큰 가져오기 + const token = await getSEDPToken(); + + // 2. 모든 프로젝트 가져오기 + const allProjects = await db.select().from(projects); + + // 3. 각 프로젝트에 대해 태그 서브필드 동기화 + const results = await Promise.allSettled( + allProjects.map(async (project: Project) => { + try { + // 태그 타입 데이터 가져오기 + const tagTypesData = await getTagTypes(project.code, token); + const tagTypesArray = Array.isArray(tagTypesData) ? tagTypesData : [tagTypesData]; + + // 속성 데이터 가져오기 + const attributesData = await getAttributes(project.code, token); + + // 서브필드 처리 및 저장 + const count = await processAndSaveTagSubfields( + project.id, + project.code, + tagTypesArray, + attributesData, + token + ); + + return { + project: project.code, + success: true, + count + } as SyncResult; + } catch (error) { + console.error(`프로젝트 ${project.code} 서브필드 동기화 실패:`, error); + return { + project: project.code, + success: false, + error: error instanceof Error ? error.message : String(error) + } as SyncResult; + } + }) + ); + + // 결과 처리를 위한 배열 준비 + const successfulResults: SyncResult[] = []; + const failedResults: SyncResult[] = []; + + // 결과 분류 + results.forEach((result) => { + if (result.status === 'fulfilled') { + if (result.value.success) { + successfulResults.push(result.value); + } else { + failedResults.push(result.value); + } + } else { + // 거부된 프로미스는 실패로 간주 + failedResults.push({ + project: 'unknown', + success: false, + error: result.reason?.toString() || 'Unknown error' + }); + } + }); + + const successCount = successfulResults.length; + const failCount = failedResults.length; + + // 이제 안전하게 count 속성에 접근 가능 + const totalItems = successfulResults.reduce((sum, result) => + sum + (result.count || 0), 0 + ); + + console.log(`태그 서브필드 동기화 완료: ${successCount}개 프로젝트 성공 (총 ${totalItems}개 항목), ${failCount}개 프로젝트 실패`); + + return { + success: successCount, + failed: failCount, + items: totalItems, + timestamp: new Date().toISOString() + }; + } catch (error) { + console.error('태그 서브필드 동기화 중 오류 발생:', error); + throw error; + } +}
\ No newline at end of file |
