summaryrefslogtreecommitdiff
path: root/lib/sedp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sedp')
-rw-r--r--lib/sedp/sedp-token.ts91
-rw-r--r--lib/sedp/sync-form.ts512
-rw-r--r--lib/sedp/sync-object-class.ts304
-rw-r--r--lib/sedp/sync-projects.ts194
-rw-r--r--lib/sedp/sync-tag-types.ts567
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