import db from "@/db/db"; import { contractItems, tags, forms, formEntries, // 추가 items, tagTypeClassFormMappings, projects, tagTypes, tagClasses, contracts } from "@/db/schema"; import { eq, and, like, inArray } from "drizzle-orm"; import { revalidateTag } from "next/cache"; // 추가 import { getSEDPToken } from "./sedp-token"; /** * 태그 가져오기 서비스 함수 * contractItemId(packageId)를 기반으로 외부 시스템에서 태그 데이터를 가져와 DB에 저장 * TAG_IDX를 기준으로 태그를 식별합니다. * * @param packageId 계약 아이템 ID (contractItemId) * @param progressCallback 진행 상황을 보고하기 위한 콜백 함수 * @returns 처리 결과 정보 (처리된 태그 수, 오류 목록 등) */ export async function importTagsFromSEDP( packageId: number, progressCallback?: (progress: number) => void, mode?: string ): Promise<{ processedCount: number; excludedCount: number; totalEntries: number; errors?: string[]; }> { try { // 진행 상황 보고 if (progressCallback) progressCallback(5); // Step 1: Get the contract item to find relevant data const contractItem = await db.query.contractItems.findFirst({ where: eq(contractItems.id, packageId) }); if (!contractItem) { throw new Error(`Contract item with ID ${packageId} not found`); } // Step 1-2: 계약 아이템에서 계약 정보를 가져와 프로젝트 ID 획득 const contract = await db.query.contracts.findFirst({ where: eq(contracts.id, contractItem.contractId) }); if (!contract) { throw new Error(`Contract with ID ${contractItem.contractId} not found`); } // 프로젝트 ID 획득 const projectId = contract.projectId; // Step 1-2: Get the item using itemId from contractItem const item = await db.query.items.findFirst({ where: eq(items.id, contractItem.itemId) }); if (!item) { throw new Error(`Item with ID ${contractItem.itemId} not found`); } const itemCode = item.itemCode; // 진행 상황 보고 if (progressCallback) progressCallback(10); // 기본 매핑 검색 - 모든 모드에서 사용 const baseMappings = await db.query.tagTypeClassFormMappings.findMany({ where: and( like(tagTypeClassFormMappings.remark, `%${itemCode}%`), eq(tagTypeClassFormMappings.projectId, projectId) ) }); if (baseMappings.length === 0) { throw new Error(`No mapping found for item code ${itemCode}`); } // Step 2: Find the mapping entries - 모드에 따라 다른 조건 적용 let mappings = []; if (mode === 'IM') { // IM 모드일 때는 먼저 SEDP에서 태그 데이터를 가져와 TAG_TYPE_ID 리스트 확보 // 프로젝트 코드 가져오기 const project = await db.query.projects.findFirst({ where: eq(projects.id, projectId) }); if (!project) { throw new Error(`Project with ID ${projectId} not found`); } // 각 매핑의 formCode에 대해 태그 데이터 조회 const tagTypeIds = new Set(); for (const mapping of baseMappings) { try { // SEDP에서 태그 데이터 가져오기 const tagData = await fetchTagDataFromSEDP(project.code, mapping.formCode); // 첫 번째 키를 테이블 이름으로 사용 const tableName = Object.keys(tagData)[0]; const tagEntries = tagData[tableName]; if (Array.isArray(tagEntries)) { // 모든 태그에서 TAG_TYPE_ID 수집 for (const entry of tagEntries) { if (entry.TAG_TYPE_ID && entry.TAG_TYPE_ID !== "") { tagTypeIds.add(entry.TAG_TYPE_ID); } } } } catch (error) { console.error(`Error fetching tag data for formCode ${mapping.formCode}:`, error); } } if (tagTypeIds.size === 0) { throw new Error('No valid TAG_TYPE_ID found in SEDP tag data'); } // 수집된 TAG_TYPE_ID로 tagTypes에서 정보 조회 const tagTypeInfo = await db.query.tagTypes.findMany({ where: and( inArray(tagTypes.code, Array.from(tagTypeIds)), eq(tagTypes.projectId, projectId) ) }); if (tagTypeInfo.length === 0) { throw new Error('No matching tag types found for the collected TAG_TYPE_IDs'); } // 태그 타입 설명 수집 const tagLabels = tagTypeInfo.map(tt => tt.description); // IM 모드에 맞는 매핑 조회 - ep가 "IMEP"인 항목만 mappings = await db.query.tagTypeClassFormMappings.findMany({ where: and( inArray(tagTypeClassFormMappings.tagTypeLabel, tagLabels), eq(tagTypeClassFormMappings.projectId, projectId), eq(tagTypeClassFormMappings.ep, "IMEP") ) }); } else { // ENG 모드 또는 기본 모드일 때 - 기본 매핑 사용 mappings = [...baseMappings]; // ENG 모드에서는 ep 필드가 "IMEP"가 아닌 매핑만 필터링 if (mode === 'ENG') { mappings = mappings.filter(mapping => mapping.ep !== "IMEP"); } } // 매핑이 없는 경우 모드에 따라 다른 오류 메시지 사용 if (mappings.length === 0) { if (mode === 'IM') { throw new Error('No suitable mappings found for IM mode'); } else { throw new Error(`No mapping found for item code ${itemCode}`); } } // 진행 상황 보고 if (progressCallback) progressCallback(15); // 결과 누적을 위한 변수들 초기화 let totalProcessedCount = 0; let totalExcludedCount = 0; let totalEntriesCount = 0; const allErrors: string[] = []; // 각 매핑에 대해 처리 for (let mappingIndex = 0; mappingIndex < mappings.length; mappingIndex++) { const mapping = mappings[mappingIndex]; // Step 3: Get the project code const project = await db.query.projects.findFirst({ where: eq(projects.id, mapping.projectId) }); if (!project) { allErrors.push(`Project with ID ${mapping.projectId} not found`); continue; // 다음 매핑으로 진행 } const projectCode = project.code; // IM 모드에서는 baseMappings에서 같은 formCode를 가진 매핑을 찾음 let formCode = mapping.formCode; if (mode === 'IM') { // baseMapping에서 동일한 formCode를 가진 매핑 찾기 const originalMapping = baseMappings.find( baseMapping => baseMapping.formCode === mapping.formCode ); // 찾았으면 해당 formCode 사용, 못 찾았으면 현재 매핑의 formCode 유지 if (originalMapping) { formCode = originalMapping.formCode; } } // 진행 상황 보고 - 매핑별 진행률 조정 if (progressCallback) { const baseProgress = 15; const mappingProgress = Math.floor(15 * (mappingIndex + 1) / mappings.length); progressCallback(baseProgress + mappingProgress); } // Step 4: Find the form ID const form = await db.query.forms.findFirst({ where: and( eq(forms.contractItemId, packageId), eq(forms.formCode, formCode) ) }); let formId; // If form doesn't exist, create it if (!form) { // 폼이 없는 경우 새로 생성 - 모드에 따른 필드 설정 const insertValues: any = { contractItemId: packageId, formCode: formCode, formName: mapping.formName }; // 모드 정보가 있으면 해당 필드 설정 if (mode) { if (mode === "ENG") { insertValues.eng = true; } else if (mode === "IM") { insertValues.im = true; if (mapping.remark && mapping.remark.includes("VD_")) { insertValues.eng = true; } } } const insertResult = await db.insert(forms).values(insertValues).returning({ id: forms.id }); if (insertResult.length === 0) { allErrors.push(`Failed to create form record for formCode ${formCode}`); continue; // 다음 매핑으로 진행 } formId = insertResult[0].id; } else { // 폼이 이미 존재하는 경우 - 필요시 모드 필드 업데이트 formId = form.id; if (mode) { let shouldUpdate = false; const updateValues: any = {}; if (mode === "ENG" && form.eng !== true) { updateValues.eng = true; shouldUpdate = true; } else if (mode === "IM" && form.im !== true) { updateValues.im = true; shouldUpdate = true; } if (shouldUpdate) { await db.update(forms) .set({ ...updateValues, updatedAt: new Date() }) .where(eq(forms.id, formId)); console.log(`Updated form ${formId} with ${mode} mode enabled`); } } } // 진행 상황 보고 - 매핑별 진행률 조정 if (progressCallback) { const baseProgress = 30; const mappingProgress = Math.floor(20 * (mappingIndex + 1) / mappings.length); progressCallback(baseProgress + mappingProgress); } try { // Step 5: Call the external API to get tag data const tagData = await fetchTagDataFromSEDP(projectCode, baseMappings[0].formCode); // 진행 상황 보고 if (progressCallback) { const baseProgress = 50; const mappingProgress = Math.floor(10 * (mappingIndex + 1) / mappings.length); progressCallback(baseProgress + mappingProgress); } // Step 6: Process the data and insert into the tags table let processedCount = 0; let excludedCount = 0; // Get the first key from the response as the table name const tableName = Object.keys(tagData)[0]; const tagEntries = tagData[tableName]; if (!Array.isArray(tagEntries) || tagEntries.length === 0) { allErrors.push(`No tag data found in the API response for formCode ${baseMappings[0].formCode}`); continue; // 다음 매핑으로 진행 } const entriesCount = tagEntries.length; totalEntriesCount += entriesCount; // formEntries를 위한 데이터 수집 const newTagsForFormEntry: Array<{ TAG_IDX: string; // 변경: TAG_NO → TAG_IDX TAG_NO?: string; // TAG_NO도 함께 저장 (편집 가능한 필드) TAG_DESC: string | null; status: string; [key: string]: any; }> = []; // Process each tag entry for (let i = 0; i < tagEntries.length; i++) { try { const entry = tagEntries[i]; // TAG_IDX가 없는 경우 제외 (변경: TAG_NO → TAG_IDX 체크) if (!entry.TAG_IDX) { excludedCount++; totalExcludedCount++; // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트) if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) { const baseProgress = 60; const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount))); progressCallback(baseProgress + entryProgress); } continue; // 이 항목은 건너뜀 } // TAG_TYPE_ID가 null이거나 빈 문자열인 경우 제외 if (entry.TAG_TYPE_ID === null || entry.TAG_TYPE_ID === "") { excludedCount++; totalExcludedCount++; // 주기적으로 진행 상황 보고 (건너뛰어도 진행률은 업데이트) if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) { const baseProgress = 60; const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount))); progressCallback(baseProgress + entryProgress); } continue; // 이 항목은 건너뜀 } // Get tag type description const tagType = await db.query.tagTypes.findFirst({ where: and( eq(tagTypes.code, entry.TAG_TYPE_ID), eq(tagTypes.projectId, mapping.projectId) ) }); // Get tag class label const tagClass = await db.query.tagClasses.findFirst({ where: and( eq(tagClasses.code, entry.CLS_ID), eq(tagClasses.projectId, mapping.projectId) ) }); // Insert or update the tag - tagIdx 필드 추가 await db.insert(tags).values({ contractItemId: packageId, formId: formId, tagIdx: entry.TAG_IDX, // 추가: SEDP 고유 식별자 tagNo: entry.TAG_NO || entry.TAG_IDX, // TAG_NO가 없으면 TAG_IDX 사용 tagType: tagType?.description || entry.TAG_TYPE_ID, tagClassId: tagClass?.id, class: tagClass?.label || entry.CLS_ID, description: entry.TAG_DESC }).onConflictDoUpdate({ target: [tags.contractItemId, tags.tagIdx], // 변경: tagNo → tagIdx set: { formId: formId, tagNo: entry.TAG_NO || entry.TAG_IDX, // tagNo도 업데이트 가능 tagType: tagType?.description || entry.TAG_TYPE_ID, class: tagClass?.label || entry.CLS_ID, description: entry.TAG_DESC, updatedAt: new Date() } }); // formEntries용 데이터 수집 const tagDataForFormEntry = { TAG_IDX: entry.TAG_IDX, // 변경: TAG_NO → TAG_IDX TAG_NO: entry.TAG_NO || entry.TAG_IDX, // TAG_NO도 함께 저장 TAG_DESC: entry.TAG_DESC || null, status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시 source: "S-EDP" // 태그 출처 (불변) - S-EDP에서 가져옴 }; // ATTRIBUTES가 있으면 추가 (SHI 필드들) 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++; // 주기적으로 진행 상황 보고 if (progressCallback && (i % 10 === 0 || i === tagEntries.length - 1)) { const baseProgress = 60; const entryProgress = Math.floor(30 * ((mappingIndex * entriesCount + i) / (mappings.length * entriesCount))); progressCallback(baseProgress + entryProgress); } } catch (error: any) { console.error(`Error processing tag entry:`, error); allErrors.push(error.message || 'Unknown error'); } } // Step 7: formEntries 업데이트 - TAG_IDX 기준으로 변경 if (newTagsForFormEntry.length > 0) { try { // 기존 formEntry 가져오기 const existingEntry = await db.query.formEntries.findFirst({ where: and( eq(formEntries.formCode, formCode), eq(formEntries.contractItemId, packageId) ) }); if (existingEntry && existingEntry.id) { // 기존 formEntry가 있는 경우 let existingData: Array<{ TAG_IDX?: string; // 추가: TAG_IDX 필드 TAG_NO?: string; TAG_DESC?: string; status?: string; [key: string]: any; }> = []; if (Array.isArray(existingEntry.data)) { existingData = existingEntry.data; } // 기존 TAG_IDX들 추출 (변경: TAG_NO → TAG_IDX) const existingTagIdxs = new Set( existingData .map(item => item.TAG_IDX) .filter(tagIdx => tagIdx !== undefined && tagIdx !== null) ); // 중복되지 않은 새 태그들만 필터링 (변경: TAG_NO → TAG_IDX) const newUniqueTagsData = newTagsForFormEntry.filter( tagData => !existingTagIdxs.has(tagData.TAG_IDX) ); // 기존 태그들의 status와 ATTRIBUTES 업데이트 (변경: TAG_NO → TAG_IDX) const updatedExistingData = existingData.map(existingItem => { const newTagData = newTagsForFormEntry.find( newItem => newItem.TAG_IDX === existingItem.TAG_IDX ); if (newTagData) { // 기존 태그가 있으면 SEDP 데이터로 업데이트 return { ...existingItem, ...newTagData, TAG_IDX: existingItem.TAG_IDX // TAG_IDX는 유지 }; } return existingItem; }); const finalData = [...updatedExistingData, ...newUniqueTagsData]; await db .update(formEntries) .set({ data: finalData, updatedAt: new Date() }) .where(eq(formEntries.id, existingEntry.id)); console.log(`[IMPORT SEDP] Updated formEntry with ${newUniqueTagsData.length} new tags, updated ${updatedExistingData.length - newUniqueTagsData.length} existing tags for form ${formCode}`); } else { // formEntry가 없는 경우 새로 생성 await db.insert(formEntries).values({ formCode: formCode, contractItemId: packageId, data: newTagsForFormEntry, createdAt: new Date(), updatedAt: new Date(), }); console.log(`[IMPORT SEDP] Created new formEntry with ${newTagsForFormEntry.length} tags for form ${formCode}`); } // 캐시 무효화 revalidateTag(`form-data-${formCode}-${packageId}`); } catch (formEntryError) { console.error(`[IMPORT SEDP] Error updating formEntry for form ${formCode}:`, formEntryError); allErrors.push(`Error updating formEntry for form ${formCode}: ${formEntryError}`); } } } catch (error: any) { console.error(`Error processing mapping for formCode ${formCode}:`, error); allErrors.push(`Error with formCode ${formCode}: ${error.message || 'Unknown error'}`); } } // 모든 매핑 처리 완료 - 진행률 100% 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; } } /** * SEDP API에서 태그 데이터 가져오기 * * @param projectCode 프로젝트 코드 * @param formCode 양식 코드 * @returns API 응답 데이터 */ async function fetchTagDataFromSEDP(projectCode: string, formCode: string): Promise { try { // Get the token const apiKey = await getSEDPToken(); // Define the API base URL const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; // Make the API call const response = await fetch( `${SEDP_API_BASE_URL}/Data/GetPubData`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'accept': '*/*', 'ApiKey': apiKey, 'ProjectNo': projectCode }, body: JSON.stringify({ ProjectNo: projectCode, REG_TYPE_ID: formCode, ContainDeleted: false }) } ); if (!response.ok) { const errorText = await response.text(); throw new Error(`SEDP API request failed: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json(); return data; } catch (error: any) { console.error('Error calling SEDP API:', error); throw new Error(`Failed to fetch data from SEDP API: ${error.message || 'Unknown error'}`); } }