import db from "@/db/db"; import { tagsPlant, formsPlant, formEntriesPlant, projects, tagTypes, tagClasses, } from "@/db/schema"; import { eq, and } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { getSEDPToken } from "./sedp-token"; const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; // ============ 타입 정의 ============ interface newRegister { PROJ_NO: string; MAP_ID: string; EP_ID: string; DESC: string; CATEGORY: string; BYPASS: boolean; REG_TYPE_ID: string; TOOL_ID: string; TOOL_TYPE: string; SCOPES: string[]; MAP_CLS: { TOOL_ATT_NAME: string; ITEMS: any[]; }; MAP_ATT: any[]; MAP_TMPLS: string[]; CRTER_NO: string; CRTE_DTM: string; CHGER_NO: string; _id: string; } 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: any[]; JOIN_TABLS: any[]; DELETED: boolean; CRTER_NO: string; CRTE_DTM: string; CHGER_NO: string | null; CHGE_DTM: string | null; _id: string; } interface FormInfo { formCode: string; formName: string; im: boolean; eng: boolean; } // ============ API 호출 함수들 ============ async function getNewRegisters(projectCode: string): Promise { const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/AdapterDataMapping/GetByToolID`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo: projectCode, TOOL_ID: "eVCP" }) } ); if (!response.ok) { throw new Error(`새 레지스터 요청 실패: ${response.status} ${response.statusText}`); } const data = await response.json(); const registers: newRegister[] = Array.isArray(data) ? data : [data]; console.log(`[getNewRegisters] 프로젝트 ${projectCode}에서 ${registers.length}개의 레지스터를 가져왔습니다.`); return registers; } async function getRegisters(projectCode: string): Promise { 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({ ProjectNo: projectCode, ContainDeleted: false }) } ); if (!response.ok) { throw new Error(`레지스터 요청 실패: ${response.status} ${response.statusText}`); } const data = await response.json(); const registers: Register[] = Array.isArray(data) ? data : [data]; console.log(`[getRegisters] 프로젝트 ${projectCode}에서 ${registers.length}개의 레지스터를 가져왔습니다.`); return registers; } async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise { const apiKey = await getSEDPToken(); 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 요청 실패: ${response.status} ${response.statusText} - ${errorText}`); } return await response.json(); } async function getRegisterDetail(projectCode: string, formCode: string): Promise { const apiKey = await getSEDPToken(); const response = await fetch( `${SEDP_API_BASE_URL}/Register/GetByID`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo: projectCode, TYPE_ID: formCode, ContainDeleted: false }) } ); if (!response.ok) { console.error(`Register detail 요청 실패: ${formCode}`); return null; } return await response.json(); } // ============ 메인 함수 ============ export async function importTagsFromSEDP( projectCode: string, packageCode: string, progressCallback?: (progress: number) => void ): Promise<{ processedCount: number; excludedCount: number; totalEntries: number; errors?: string[]; }> { const allErrors: string[] = []; let totalProcessedCount = 0; let totalExcludedCount = 0; let totalEntriesCount = 0; try { if (progressCallback) progressCallback(5); // Step 1: 프로젝트 ID 조회 const project = await db.query.projects.findFirst({ where: eq(projects.code, projectCode), columns: { id: true } }); if (!project) { throw new Error(`Project not found: ${projectCode}`); } const projectId = project.id; if (progressCallback) progressCallback(10); // Step 2: 두 API 동시 호출 const [newRegisters, registers] = await Promise.all([ getNewRegisters(projectCode), getRegisters(projectCode) ]); if (progressCallback) progressCallback(20); // ======== 서브클래스 매핑을 위한 태그 클래스 로드 ======== const allTagClasses = await db.query.tagClasses.findMany({ where: eq(tagClasses.projectId, projectId) }); // 클래스 코드로 빠른 조회를 위한 Map const tagClassByCode = new Map(allTagClasses.map(tc => [tc.code, tc])); // 서브클래스 코드로 부모 클래스 찾기 위한 Map const parentBySubclassCode = new Map(); for (const tc of allTagClasses) { if (tc.subclasses && Array.isArray(tc.subclasses)) { for (const sub of tc.subclasses as { id: string; desc: string }[]) { parentBySubclassCode.set(sub.id, tc); } } } console.log(`[importTagsFromSEDP] 태그 클래스 ${allTagClasses.length}개 로드, 서브클래스 매핑 ${parentBySubclassCode.size}개 생성`); // ======== 서브클래스 매핑 준비 완료 ======== // Step 3: packageCode에 해당하는 폼 정보 추출 const formsToProcess: FormInfo[] = []; // Register 정보를 Map으로 변환 (TYPE_ID로 빠른 조회) const registerMap = new Map(); for (const reg of registers) { registerMap.set(reg.TYPE_ID, reg); } // newRegisters에서 packageCode가 SCOPES에 포함된 것 필터링 for (const newReg of newRegisters) { if (newReg.SCOPES && newReg.SCOPES.includes(packageCode)) { const formCode = newReg.REG_TYPE_ID; const formName = newReg.DESC; // Register에서 EP_ID 확인하여 im/eng 결정 const register = registerMap.get(formCode); const isIM = register?.EP_ID === "IMEP"; formsToProcess.push({ formCode, formName, im: isIM, eng: !isIM }); } } if (formsToProcess.length === 0) { throw new Error(`No forms found for packageCode: ${packageCode}`); } console.log(`[importTagsFromSEDP] ${formsToProcess.length}개의 폼을 처리합니다.`); if (progressCallback) progressCallback(25); // Step 4: 각 폼에 대해 처리 for (let i = 0; i < formsToProcess.length; i++) { const formInfo = formsToProcess[i]; const { formCode, formName, im, eng } = formInfo; try { // 진행률 계산 const baseProgress = 25; const progressPerForm = 70 / formsToProcess.length; // Step 4-1: formsPlant upsert const existingForm = await db.query.formsPlant.findFirst({ where: and( eq(formsPlant.projectCode, projectCode), eq(formsPlant.packageCode, packageCode), eq(formsPlant.formCode, formCode) ) }); let formId: number; if (existingForm) { // 기존 폼 업데이트 await db.update(formsPlant) .set({ formName, im, eng, updatedAt: new Date() }) .where(eq(formsPlant.id, existingForm.id)); formId = existingForm.id; console.log(`[formsPlant] Updated form: ${formCode}`); } else { // 새 폼 생성 const insertResult = await db.insert(formsPlant) .values({ projectCode, packageCode, formCode, formName, im, eng }) .returning({ id: formsPlant.id }); formId = insertResult[0].id; console.log(`[formsPlant] Created form: ${formCode}`); } if (progressCallback) { progressCallback(baseProgress + progressPerForm * (i + 0.2)); } // Step 4-2: SEDP에서 태그 데이터 가져오기 const tagData = await fetchTagDataFromSEDP(projectCode, formCode); const tableName = Object.keys(tagData)[0]; const tagEntries = tagData[tableName]; if (!Array.isArray(tagEntries) || tagEntries.length === 0) { console.log(`[importTagsFromSEDP] No tag data for formCode: ${formCode}`); continue; } totalEntriesCount += tagEntries.length; if (progressCallback) { progressCallback(baseProgress + progressPerForm * (i + 0.4)); } // Step 4-3: Register detail에서 허용된 ATT_ID 추출 const registerDetail = await getRegisterDetail(projectCode, formCode); const allowedAttIds = new Set(); if (registerDetail?.MAP_ATT && Array.isArray(registerDetail.MAP_ATT)) { for (const mapAttr of registerDetail.MAP_ATT) { if (mapAttr.ATT_ID) { allowedAttIds.add(mapAttr.ATT_ID); } } } // Step 4-4: 태그 처리 const newTagsForFormEntry: Array> = []; let processedCount = 0; let excludedCount = 0; for (const entry of tagEntries) { // TAG_IDX 없으면 제외 if (!entry.TAG_IDX) { excludedCount++; continue; } // TAG_TYPE_ID 없으면 제외 if (!entry.TAG_TYPE_ID || entry.TAG_TYPE_ID === "") { excludedCount++; continue; } // attributes 추출 (허용된 ATT_ID만) const attributes: Record = {}; if (Array.isArray(entry.ATTRIBUTES)) { for (const attr of entry.ATTRIBUTES) { if (attr.ATT_ID && allowedAttIds.has(attr.ATT_ID)) { if (attr.VALUE !== null && attr.VALUE !== undefined) { attributes[attr.ATT_ID] = String(attr.VALUE); } } } } // tagType 조회 const tagType = await db.query.tagTypes.findFirst({ where: and( eq(tagTypes.code, entry.TAG_TYPE_ID), eq(tagTypes.projectId, projectId) ) }); // ======== 클래스 및 서브클래스 결정 로직 ======== let classLabel: string; let subclassValue: string | null = null; let tagClassId: number | null = null; // 1. 먼저 CLS_ID로 직접 tagClass 찾기 const tagClass = tagClassByCode.get(entry.CLS_ID); if (tagClass) { // 직접 찾은 경우 - 이게 메인 클래스 classLabel = tagClass.label || entry.CLS_ID; tagClassId = tagClass.id; } else { // 2. 서브클래스인지 확인 (부모 클래스의 subclasses 배열에 있는지) const parentClass = parentBySubclassCode.get(entry.CLS_ID); if (parentClass) { // 서브클래스인 경우 classLabel = parentClass.label || parentClass.code; subclassValue = entry.CLS_ID; tagClassId = parentClass.id; console.log(`[importTagsFromSEDP] 서브클래스 발견: ${entry.CLS_ID} -> 부모: ${parentClass.code}`); } else { // 어디에도 없는 경우 - 원본 값 사용 classLabel = entry.CLS_ID; console.log(`[importTagsFromSEDP] 클래스를 찾을 수 없음: ${entry.CLS_ID}`); } } // ======== 클래스/서브클래스 결정 완료 ======== // tagsPlant upsert (subclass 필드 추가) await db.insert(tagsPlant).values({ projectCode, packageCode, formId, tagIdx: entry.TAG_IDX, tagNo: entry.TAG_NO || entry.TAG_IDX, tagType: tagType?.description || entry.TAG_TYPE_ID, tagClassId: tagClassId, class: classLabel, subclass: subclassValue, description: entry.TAG_DESC, attributes, }).onConflictDoUpdate({ target: [tagsPlant.projectCode, tagsPlant.packageCode, tagsPlant.tagIdx], set: { formId, tagNo: entry.TAG_NO || entry.TAG_IDX, tagType: tagType?.description || entry.TAG_TYPE_ID, tagClassId: tagClassId, class: classLabel, subclass: subclassValue, description: entry.TAG_DESC, attributes, updatedAt: new Date() } }); // formEntriesPlant용 데이터 준비 const tagDataForFormEntry: Record = { TAG_IDX: entry.TAG_IDX, TAG_NO: entry.TAG_NO || entry.TAG_IDX, TAG_DESC: entry.TAG_DESC || null, status: "From S-EDP", source: "S-EDP" }; // ATTRIBUTES 추가 if (Array.isArray(entry.ATTRIBUTES)) { for (const attr of entry.ATTRIBUTES) { if (attr.ATT_ID && attr.VALUE !== null && attr.VALUE !== undefined) { tagDataForFormEntry[attr.ATT_ID] = attr.VALUE; } } } newTagsForFormEntry.push(tagDataForFormEntry); processedCount++; } totalProcessedCount += processedCount; totalExcludedCount += excludedCount; if (progressCallback) { progressCallback(baseProgress + progressPerForm * (i + 0.8)); } // Step 4-5: formEntriesPlant upsert if (newTagsForFormEntry.length > 0) { const existingEntry = await db.query.formEntriesPlant.findFirst({ where: and( eq(formEntriesPlant.formCode, formCode), eq(formEntriesPlant.projectCode, projectCode), eq(formEntriesPlant.packageCode, packageCode) ) }); if (existingEntry) { // 기존 데이터 병합 let existingData: Array> = []; if (Array.isArray(existingEntry.data)) { existingData = existingEntry.data; } const existingTagIdxs = new Set( existingData.map(item => item.TAG_IDX).filter(Boolean) ); // 기존 데이터 업데이트 + 새 데이터 추가 const updatedData = existingData.map(existingItem => { const newData = newTagsForFormEntry.find( n => n.TAG_IDX === existingItem.TAG_IDX ); return newData ? { ...existingItem, ...newData } : existingItem; }); const newUniqueData = newTagsForFormEntry.filter( n => !existingTagIdxs.has(n.TAG_IDX) ); await db.update(formEntriesPlant) .set({ data: [...updatedData, ...newUniqueData], updatedAt: new Date() }) .where(eq(formEntriesPlant.id, existingEntry.id)); console.log(`[formEntriesPlant] Updated: ${formCode} (${newUniqueData.length} new, ${updatedData.length} updated)`); } else { // 새로 생성 await db.insert(formEntriesPlant).values({ formCode, projectCode, packageCode, data: newTagsForFormEntry, createdAt: new Date(), updatedAt: new Date() }); console.log(`[formEntriesPlant] Created: ${formCode} (${newTagsForFormEntry.length} tags)`); } } if (progressCallback) { progressCallback(baseProgress + progressPerForm * (i + 1)); } } catch (error: any) { console.error(`Error processing form ${formCode}:`, error); allErrors.push(`Form ${formCode}: ${error.message}`); } } if (progressCallback) progressCallback(100); return { processedCount: totalProcessedCount, excludedCount: totalExcludedCount, totalEntries: totalEntriesCount, errors: allErrors.length > 0 ? allErrors : undefined }; } catch (error: any) { console.error("Tag import error:", error); throw error; } }