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/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; } interface TagType { TYPE_ID: string; DESC: string; PROJ_NO: string; // 기타 필드들... } // 오브젝트 클래스 데이터 가져오기 async function getObjectClasses(projectCode: string, token:string): Promise { 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({ 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(`프로젝트 ${projectCode}의 오브젝트 클래스 가져오기 실패:`, error); throw error; } } // 태그 타입 존재 확인 async function verifyTagTypes(projectId: number, tagTypeCodes: string[]): Promise> { 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; } } async function saveTagTypesToDatabase(allTagTypes: TagType[], projectCode: string): Promise { 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 { 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 { try { // null이 아닌 TAG_TYPE_ID만 필터링 const validClasses = classes.filter(cls => cls.TAG_TYPE_ID !== null && cls.TAG_TYPE_ID !== "") ; if (validClasses.length === 0) { console.log(`프로젝트 ID ${projectId}에 저장할 유효한 오브젝트 클래스가 없습니다.`); return 0; } // 모든 태그 타입 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); // 태그 타입이 존재하는 오브젝트 클래스만 필터링 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; } } // 5. 메인 동기화 함수 수정 export async function syncObjectClasses() { try { console.log('오브젝트 클래스 동기화 시작:', new Date().toISOString()); // 1. 토큰 가져오기 const token = await getSEDPToken(); // 2. 모든 프로젝트 가져오기 const allProjects = await db.select().from(projects); // 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); // 데이터베이스에 저장 (skipTagTypeSync를 true로 설정하여 태그 타입 동기화 건너뜀) const count = await saveObjectClassesToDatabase(project.id, objectClasses, project.code, token, true); 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 { tagTypeSync: { success: tagTypeSuccessCount, failed: tagTypeFailCount }, objectClassSync: { success: successCount, failed: failCount, items: totalItems }, timestamp: new Date().toISOString() }; } catch (error) { console.error('오브젝트 클래스 동기화 중 오류 발생:', error); throw error; } }