diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-27 17:53:34 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-27 17:53:34 +0900 |
| commit | 5870b73785715d1585531e655c06d8c068eb64ac (patch) | |
| tree | 1d19e1482f5210cc56e778158b51e810f9717c46 /lib | |
| parent | 95984e67b8d57fbe1431fcfedf3bb682f28416b3 (diff) | |
(김준회) Revert "(대표님) EDP 작업사항"
태그 가져오기 실패 등 에러로 인한 Revert 처리
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/forms-plant/services.ts | 458 | ||||
| -rw-r--r-- | lib/forms-plant/stat.ts | 32 | ||||
| -rw-r--r-- | lib/sedp/get-form-tags-plant.ts | 933 | ||||
| -rw-r--r-- | lib/sedp/get-tags-plant.ts | 639 | ||||
| -rw-r--r-- | lib/sedp/sync-form.ts | 9 | ||||
| -rw-r--r-- | lib/tags-plant/column-builder.service.ts | 34 | ||||
| -rw-r--r-- | lib/tags-plant/queries.ts | 68 | ||||
| -rw-r--r-- | lib/tags-plant/repository.ts | 42 | ||||
| -rw-r--r-- | lib/tags-plant/service.ts | 729 | ||||
| -rw-r--r-- | lib/tags-plant/table/add-tag-dialog.tsx | 18 | ||||
| -rw-r--r-- | lib/tags-plant/table/delete-tags-dialog.tsx | 12 | ||||
| -rw-r--r-- | lib/tags-plant/table/tag-table.tsx | 775 | ||||
| -rw-r--r-- | lib/tags-plant/table/tags-export.tsx | 5 | ||||
| -rw-r--r-- | lib/tags-plant/table/tags-table-floating-bar.tsx | 5 | ||||
| -rw-r--r-- | lib/tags-plant/table/tags-table-toolbar-actions.tsx | 42 | ||||
| -rw-r--r-- | lib/tags-plant/table/update-tag-sheet.tsx | 13 | ||||
| -rw-r--r-- | lib/vendor-data/services.ts | 93 | ||||
| -rw-r--r-- | lib/vendor-document/service.ts | 706 |
18 files changed, 632 insertions, 3981 deletions
diff --git a/lib/forms-plant/services.ts b/lib/forms-plant/services.ts index 7e1976e6..219f36e4 100644 --- a/lib/forms-plant/services.ts +++ b/lib/forms-plant/services.ts @@ -7,18 +7,18 @@ import fs from "fs/promises"; import { v4 as uuidv4 } from "uuid"; import db from "@/db/db"; import { - formEntries,formEntriesPlant, - formMetas,formsPlant, + formEntries, + formMetas, forms, tagClassAttributes, tagClasses, - tags,tagsPlant, + tags, tagSubfieldOptions, tagSubfields, tagTypeClassFormMappings, tagTypes, - vendorDataReportTempsPlant, - VendorDataReportTempsPlant, + vendorDataReportTemps, + VendorDataReportTemps, } from "@/db/schema/vendorData"; import { eq, and, desc, sql, DrizzleError, inArray, or, type SQL, type InferSelectModel } from "drizzle-orm"; import { unstable_cache } from "next/cache"; @@ -29,7 +29,6 @@ import { contractItems, contracts, items, projects } from "@/db/schema"; import { getSEDPToken } from "../sedp/sedp-token"; import { decryptWithServerAction } from "@/components/drm/drmUtils"; import { deleteFile, saveFile } from "@/lib/file-stroage"; -import { Register } from "@/components/form-data-plant/form-data-table-columns"; export type FormInfo = InferSelectModel<typeof forms>; @@ -165,8 +164,7 @@ export interface EditableFieldsInfo { // TAG별 편집 가능 필드 조회 함수 export async function getEditableFieldsByTag( - projectCode: string, - packageCode: string, + contractItemId: number, projectId: number ): Promise<Map<string, string[]>> { try { @@ -176,11 +174,8 @@ export async function getEditableFieldsByTag( tagNo: tags.tagNo, tagClass: tags.class }) - .from(tagsPlant) - .where( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode), - ); + .from(tags) + .where(eq(tags.contractItemId, contractItemId)); const editableFieldsMap = new Map<string, string[]>(); @@ -233,17 +228,26 @@ export async function getEditableFieldsByTag( * data가 배열이면 그 배열을 반환, * 그리고 이 로직 전체를 unstable_cache로 감싸 캐싱. */ -export async function getFormData(formCode: string, projectCode: string, packageCode:string) { +export async function getFormData(formCode: string, contractItemId: number) { try { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 기존 로직으로 projectId, columns, data 가져오기 + const contractItemResult = await db + .select({ + projectId: projects.id + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .where(eq(contractItems.id, contractItemId)) + .limit(1); + + if (contractItemResult.length === 0) { + console.warn(`[getFormData] No contract item found with ID: ${contractItemId}`); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } - const projectId = project.id; + const projectId = contractItemResult[0].projectId; const metaRows = await db .select() @@ -265,15 +269,14 @@ export async function getFormData(formCode: string, projectCode: string, package const entryRows = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1); const entry = entryRows[0] ?? null; @@ -318,7 +321,7 @@ export async function getFormData(formCode: string, projectCode: string, package } // *** 새로 추가: 편집 가능 필드 정보 계산 *** - const editableFieldsMap = await getEditableFieldsByTag(projectCode,packageCode ,projectId); + const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId); return { columns, data, editableFieldsMap }; @@ -328,16 +331,24 @@ export async function getFormData(formCode: string, projectCode: string, package // Fallback logic (기존과 동일하게 editableFieldsMap 추가) try { - console.log(`[getFormData] Fallback DB query for (${formCode}, ${packageCode})`); + console.log(`[getFormData] Fallback DB query for (${formCode}, ${contractItemId})`); - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + const contractItemResult = await db + .select({ + projectId: projects.id + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .innerJoin(projects, eq(contracts.projectId, projects.id)) + .where(eq(contractItems.id, contractItemId)) + .limit(1); + + if (contractItemResult.length === 0) { + console.warn(`[getFormData] Fallback: No contract item found with ID: ${contractItemId}`); + return { columns: null, data: [], editableFieldsMap: new Map() }; + } - const projectId = project.id; + const projectId = contractItemResult[0].projectId; const metaRows = await db .select() @@ -359,15 +370,14 @@ export async function getFormData(formCode: string, projectCode: string, package const entryRows = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1); const entry = entryRows[0] ?? null; @@ -396,7 +406,7 @@ export async function getFormData(formCode: string, projectCode: string, package } // Fallback에서도 편집 가능 필드 정보 계산 - const editableFieldsMap = await getEditableFieldsByTag(projectCode, packageCode, projectId); + const editableFieldsMap = await getEditableFieldsByTag(contractItemId, projectId); return { columns, data, projectId, editableFieldsMap }; } catch (dbError) { @@ -405,7 +415,7 @@ export async function getFormData(formCode: string, projectCode: string, package } } } -/** +/**1 * contractId와 formCode(itemCode)를 사용하여 contractItemId를 찾는 서버 액션 * * @param contractId - 계약 ID @@ -507,26 +517,24 @@ export async function getPackageCodeById(contractItemId: number): Promise<string export async function syncMissingTags( - projectCode: string, - packageCode: string, + contractItemId: number, formCode: string ) { // (1) Ensure there's a row in `forms` matching (contractItemId, formCode). const [formRow] = await db .select() - .from(formsPlant) + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formCode) + eq(forms.contractItemId, contractItemId), + eq(forms.formCode, formCode) ) ) .limit(1); if (!formRow) { throw new Error( - `Form not found for projectCode=${projectCode}, formCode=${formCode}` + `Form not found for contractItemId=${contractItemId}, formCode=${formCode}` ); } @@ -550,28 +558,26 @@ export async function syncMissingTags( // (3) Fetch all matching `tags` for the contractItemId + any of the (tagType, class) pairs. const tagRows = await db .select() - .from(tagsPlant) - .where(and(eq(tagsPlant.packageCode, packageCode),eq(tagsPlant.projectCode, projectCode), or(...orConditions))); + .from(tags) + .where(and(eq(tags.contractItemId, contractItemId), or(...orConditions))); // (4) Fetch (or create) a single `formEntries` row for (contractItemId, formCode). let [entry] = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.formCode, formCode) + eq(formEntries.contractItemId, contractItemId), + eq(formEntries.formCode, formCode) ) ) .limit(1); if (!entry) { const [inserted] = await db - .insert(formEntriesPlant) + .insert(formEntries) .values({ - projectCode, - packageCode, + contractItemId, formCode, data: [], // Initialize with empty array }) @@ -640,13 +646,13 @@ export async function syncMissingTags( // (6) 실제로 추가되거나 수정되거나 삭제된 게 있다면 DB에 반영 if (createdCount > 0 || updatedCount > 0 || deletedCount > 0) { await db - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData }) - .where(eq(formEntriesPlant.id, entry.id)); + .where(eq(formEntries.id, entry.id)); } // 캐시 무효화 등 후처리 - // revalidateTag(`form-data-${formCode}-${projectCode}`); + revalidateTag(`form-data-${formCode}-${contractItemId}`); return { createdCount, updatedCount, deletedCount }; } @@ -675,8 +681,7 @@ export interface UpdateResponse { export async function updateFormDataInDB( formCode: string, - projectCode: string, - packageCode: string, + contractItemId: number, newData: Record<string, any> ): Promise<UpdateResponse> { try { @@ -692,12 +697,11 @@ export async function updateFormDataInDB( // 2) row 찾기 (단 하나) const entries = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( eq(formEntries.formCode, formCode), - eq(formEntries.projectCode, projectCode), - eq(formEntries.packageCode, packageCode), + eq(formEntries.contractItemId, contractItemId) ) ) .limit(1); @@ -752,12 +756,12 @@ export async function updateFormDataInDB( // 6) DB UPDATE try { await db - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedArray, updatedAt: new Date(), // 업데이트 시간도 갱신 }) - .where(eq(formEntriesPlant.id, entry.id)); + .where(eq(formEntries.id, entry.id)); } catch (dbError) { console.error("Database update error:", dbError); @@ -777,7 +781,7 @@ export async function updateFormDataInDB( // 7) Cache 무효화 try { // 캐시 태그를 form-data-${formCode}-${contractItemId} 형태로 가정 - const cacheTag = `form-data-${formCode}-${projectCode}`; + const cacheTag = `form-data-${formCode}-${contractItemId}`; console.log(cacheTag, "update") revalidateTag(cacheTag); } catch (cacheError) { @@ -810,8 +814,7 @@ export async function updateFormDataInDB( export async function updateFormDataBatchInDB( formCode: string, - projectCode: string, - packageCode: string, + contractItemId: number, newDataArray: Record<string, any>[] ): Promise<UpdateResponse> { try { @@ -836,12 +839,11 @@ export async function updateFormDataBatchInDB( // 1) DB에서 현재 데이터 가져오기 const entries = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) .limit(1); @@ -849,7 +851,7 @@ export async function updateFormDataBatchInDB( if (!entries || entries.length === 0) { return { success: false, - message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, projectCode=${projectCode})`, + message: `폼 데이터를 찾을 수 없습니다. (formCode=${formCode}, contractItemId=${contractItemId})`, }; } @@ -916,12 +918,12 @@ export async function updateFormDataBatchInDB( // 3) DB에 한 번만 저장 try { await db - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedArray, updatedAt: new Date(), }) - .where(eq(formEntriesPlant.id, entry.id)); + .where(eq(formEntries.id, entry.id)); } catch (dbError) { console.error("Database update error:", dbError); @@ -950,7 +952,7 @@ export async function updateFormDataBatchInDB( // 4) 캐시 무효화 try { - const cacheTag = `form-data-${formCode}-${projectCode}`; + const cacheTag = `form-data-${formCode}-${contractItemId}`; console.log(`Cache invalidated: ${cacheTag}`); revalidateTag(cacheTag); } catch (cacheError) { @@ -1041,37 +1043,26 @@ export async function fetchFormMetadata( } type GetReportFileList = ( - projectCode: string, - packageCode: string, - formCode: string, - mode: string + packageId: string, + formCode: string ) => Promise<{ formId: number; }>; -export const getFormId: GetReportFileList = async (projectCode, packageCode, formCode, mode) => { +export const getFormId: GetReportFileList = async (packageId, formCode) => { const result: { formId: number } = { formId: 0, }; try { - // mode에 따른 조건 배열 생성 - const conditions = [ - eq(formsPlant.formCode, formCode), - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - ]; - - // mode에 따라 추가 조건 설정 - if (mode === "IM") { - conditions.push(eq(formsPlant.im, true)); - } else if (mode === "ENG") { - conditions.push(eq(formsPlant.eng, true)); - } - const [targetForm] = await db .select() - .from(formsPlant) - .where(and(...conditions)); + .from(forms) + .where( + and( + eq(forms.formCode, formCode), + eq(forms.contractItemId, Number(packageId)) + ) + ); if (!targetForm) { throw new Error("Not Found Target Form"); @@ -1081,34 +1072,30 @@ export const getFormId: GetReportFileList = async (projectCode, packageCode, for result.formId = formId; } catch (err) { - console.error("Error getting form ID:", err); } finally { return result; } }; type getReportTempList = ( - projectCode: string, - packageCode: string, + packageId: number, formId: number -) => Promise<VendorDataReportTempsPlant[]>; +) => Promise<VendorDataReportTemps[]>; export const getReportTempList: getReportTempList = async ( - projectCode, - packageCode, + packageId, formId ) => { - let result: VendorDataReportTempsPlant[] = []; + let result: VendorDataReportTemps[] = []; try { result = await db .select() - .from(vendorDataReportTempsPlant) + .from(vendorDataReportTemps) .where( and( - eq(vendorDataReportTempsPlant.projectCode, projectCode), - eq(vendorDataReportTempsPlant.packageCode, packageCode), - eq(vendorDataReportTempsPlant.formId, formId) + eq(vendorDataReportTemps.contractItemId, packageId), + eq(vendorDataReportTemps.formId, formId) ) ); } catch (err) { @@ -1118,8 +1105,7 @@ export const getReportTempList: getReportTempList = async ( }; export async function uploadReportTemp( - projectCode: string, - packageCode: string, + packageId: number, formId: number, formData: FormData ) { @@ -1142,10 +1128,9 @@ export async function uploadReportTemp( return db.transaction(async (tx) => { // 파일 정보를 테이블에 저장 await tx - .insert(vendorDataReportTempsPlant) + .insert(vendorDataReportTemps) .values({ - projectCode, - packageCode, + contractItemId: packageId, formId: formId, fileName: customFileName, filePath: saveResult.publicPath!, @@ -1175,16 +1160,16 @@ export const deleteReportTempFile: deleteReportTempFile = async (id) => { return db.transaction(async (tx) => { const [targetTempFile] = await tx .select() - .from(vendorDataReportTempsPlant) - .where(eq(vendorDataReportTempsPlant.id, id)); + .from(vendorDataReportTemps) + .where(eq(vendorDataReportTemps.id, id)); if (!targetTempFile) { throw new Error("해당 Template File을 찾을 수 없습니다."); } await tx - .delete(vendorDataReportTempsPlant) - .where(eq(vendorDataReportTempsPlant.id, id)); + .delete(vendorDataReportTemps) + .where(eq(vendorDataReportTemps.id, id)); const { filePath } = targetTempFile; @@ -1325,8 +1310,7 @@ async function transformDataToSEDPFormat( formCode: string, objectCode: string, projectNo: string, - packageCode: string, - contractItemId: string, + contractItemId: number, // Add contractItemId parameter designerNo: string = "253213" ): Promise<SEDPDataItem[]> { // Create a map for quick column lookup @@ -1347,6 +1331,9 @@ async function transformDataToSEDPFormat( // Cache for UOM factors to avoid duplicate API calls const uomFactorCache = new Map<string, number>(); + // Cache for packageCode to avoid duplicate DB queries for same tag + const packageCodeCache = new Map<string, string>(); + // Cache for tagClass code to avoid duplicate DB queries for same tag const tagClassCodeCache = new Map<string, string>(); @@ -1354,16 +1341,98 @@ async function transformDataToSEDPFormat( const transformedItems = []; for (const row of tableData) { - let tagClassCode = ""; - // Get tagClass code if TAG_NO exists + const cotractItem = await db.query.contractItems.findFirst({ + where: + eq(contractItems.id, contractItemId), + }); + + const item = await db.query.items.findFirst({ + where: + eq(items.id, cotractItem.itemId), + }); + + // Get packageCode for this specific tag + let packageCode = item.packageCode; // fallback to formCode + let tagClassCode = ""; // for CLS_ID + if (row.TAG_NO && contractItemId) { + // Check cache first const cacheKey = `${contractItemId}-${row.TAG_NO}`; - if (tagClassCodeCache.has(cacheKey)) { - tagClassCode = tagClassCodeCache.get(cacheKey)!; + if (packageCodeCache.has(cacheKey)) { + packageCode = packageCodeCache.get(cacheKey)!; } else { try { + // Query to get packageCode for this specific tag + const tagResult = await db.query.tags.findFirst({ + where: and( + eq(tags.contractItemId, contractItemId), + eq(tags.tagNo, row.TAG_NO) + ) + }); + + if (tagResult) { + // Get tagClass code if tagClassId exists + if (tagResult.tagClassId) { + // Check tagClass cache first + if (tagClassCodeCache.has(cacheKey)) { + tagClassCode = tagClassCodeCache.get(cacheKey)!; + } else { + const tagClassResult = await db.query.tagClasses.findFirst({ + where: eq(tagClasses.id, tagResult.tagClassId) + }); + + if (tagClassResult) { + tagClassCode = tagClassResult.code; + console.log(`Found tagClass code for tag ${row.TAG_NO}: ${tagClassCode}`); + } else { + console.warn(`No tagClass found for tagClassId: ${tagResult.tagClassId}`); + } + + // Cache the tagClass code result + tagClassCodeCache.set(cacheKey, tagClassCode); + } + } + + // Get the contract item + const contractItemResult = await db.query.contractItems.findFirst({ + where: eq(contractItems.id, tagResult.contractItemId) + }); + + if (contractItemResult) { + // Get the first item with this itemId + const itemResult = await db.query.items.findFirst({ + where: eq(items.id, contractItemResult.itemId) + }); + + if (itemResult && itemResult.packageCode) { + packageCode = itemResult.packageCode; + console.log(`Found packageCode for tag ${row.TAG_NO}: ${packageCode}`); + } else { + console.warn(`No item found for contractItem.itemId: ${contractItemResult.itemId}, using fallback`); + } + } else { + console.warn(`No contractItem found for tag ${row.TAG_NO}, using fallback`); + } + } else { + console.warn(`No tag found for contractItemId: ${contractItemId}, tagNo: ${row.TAG_NO}, using fallback`); + } + + // Cache the result (even if it's the fallback value) + packageCodeCache.set(cacheKey, packageCode); + } catch (error) { + console.error(`Error fetching packageCode for tag ${row.TAG_NO}:`, error); + // Use fallback value and cache it + packageCodeCache.set(cacheKey, packageCode); + } + } + + // Get tagClass code if not already retrieved above + if (!tagClassCode && tagClassCodeCache.has(cacheKey)) { + tagClassCode = tagClassCodeCache.get(cacheKey)!; + } else if (!tagClassCode) { + try { const tagResult = await db.query.tags.findFirst({ where: and( eq(tags.contractItemId, contractItemId), @@ -1371,20 +1440,22 @@ async function transformDataToSEDPFormat( ) }); - if (tagResult?.tagClassId) { + if (tagResult && tagResult.tagClassId) { const tagClassResult = await db.query.tagClasses.findFirst({ where: eq(tagClasses.id, tagResult.tagClassId) }); - + if (tagClassResult) { tagClassCode = tagClassResult.code; console.log(`Found tagClass code for tag ${row.TAG_NO}: ${tagClassCode}`); } } + // Cache the tagClass code result tagClassCodeCache.set(cacheKey, tagClassCode); } catch (error) { - console.error(`Error fetching tagClass for tag ${row.TAG_NO}:`, error); + console.error(`Error fetching tagClass code for tag ${row.TAG_NO}:`, error); + // Cache empty string as fallback tagClassCodeCache.set(cacheKey, ""); } } @@ -1395,16 +1466,17 @@ async function transformDataToSEDPFormat( TAG_NO: row.TAG_NO || "", TAG_DESC: row.TAG_DESC || "", ATTRIBUTES: [], + // SCOPE: objectCode, SCOPE: packageCode, - TOOLID: "eVCP", + TOOLID: "eVCP", // Changed from VDCS ITM_NO: row.TAG_NO || "", - OP_DELETE: row.status === "Deleted", + OP_DELETE: row.status === "Deleted", // Set OP_DELETE based on status MAIN_YN: true, LAST_REV_YN: true, CRTER_NO: designerNo, CHGER_NO: designerNo, - TYPE: formCode, - CLS_ID: tagClassCode, + TYPE: formCode, // Use packageCode instead of formCode + CLS_ID: tagClassCode, // Add CLS_ID with tagClass code PROJ_NO: projectNo, REV_NO: "00", CRTE_DTM: currentTimestamp, @@ -1450,7 +1522,7 @@ async function transformDataToSEDPFormat( const uomData = await response.json(); if (uomData && uomData.FACTOR !== undefined && uomData.FACTOR !== null) { factor = Number(uomData.FACTOR); - // Store in cache for future use + // Store in cache for future use (type assertion to ensure it's a number) uomFactorCache.set(column.uomId, factor); } } else { @@ -1461,7 +1533,7 @@ async function transformDataToSEDPFormat( } } - // Apply the factor if needed (currently commented out) + // Apply the factor if we got one // if (factor !== undefined && typeof value === 'number') { // value = value * factor; // } @@ -1469,7 +1541,7 @@ async function transformDataToSEDPFormat( const attribute: SEDPAttribute = { NAME: key, - VALUE: String(value), + VALUE: String(value), // 모든 값을 문자열로 변환 UOM: column?.uom || "", CLS_ID: tagClassCode || "", }; @@ -1497,7 +1569,7 @@ export async function transformFormDataToSEDP( formCode: string, objectCode: string, projectNo: string, - packageCode: string, // Add contractItemId parameter + contractItemId: number, // Add contractItemId parameter designerNo: string = "253213" ): Promise<SEDPDataItem[]> { return transformDataToSEDPFormat( @@ -1506,7 +1578,7 @@ export async function transformFormDataToSEDP( formCode, objectCode, projectNo, - packageCode, + contractItemId, // Pass contractItemId designerNo ); } @@ -1527,20 +1599,6 @@ export async function getProjectCodeById(projectId: number): Promise<string> { return projectRecord[0].code; } -export async function getProjectIdByCode(projectCode: string): Promise<number> { - const projectRecord = await db - .select({ id: projects.id }) - .from(projects) - .where(eq(projects.code, projectCode)) - .limit(1); - - if (!projectRecord || projectRecord.length === 0) { - throw new Error(`Project not found with ID: ${projectId}`); - } - - return projectRecord[0].id; -} - export async function getProjectById(projectId: number): Promise<{ code: string; type: string; }> { const projectRecord = await db .select({ code: projects.code , type:projects.type}) @@ -1621,13 +1679,13 @@ export async function sendDataToSEDP( export async function sendFormDataToSEDP( formCode: string, projectId: number, - projectCode: string, // contractItemId 파라미터 추가 - packageCode: string, // contractItemId 파라미터 추가 + contractItemId: number, // contractItemId 파라미터 추가 formData: GenericData[], columns: DataTableColumnJSON[] ): Promise<{ success: boolean; message: string; data?: any }> { try { // 1. Get project code + const projectCode = await getProjectCodeById(projectId); // 2. Get class mapping const mappingsResult = await db.query.tagTypeClassFormMappings.findFirst({ @@ -1670,7 +1728,7 @@ export async function sendFormDataToSEDP( formCode, objectCode, projectCode, - packageCode // Add contractItemId parameter + contractItemId // Add contractItemId parameter ); // 4. Send to SEDP API @@ -1681,12 +1739,11 @@ export async function sendFormDataToSEDP( // Get the current formEntries data const entries = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) .limit(1); @@ -1721,17 +1778,17 @@ export async function sendFormDataToSEDP( // Update the database await db - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedDataArray, updatedAt: new Date() }) - .where(eq(formEntriesPlant.id, entry.id)); + .where(eq(formEntries.id, entry.id)); console.log(`Updated status for ${sentTagNumbers.size} tags to "Sent to S-EDP"`); } } else { - console.warn(`No formEntriesPlant found for formCode: ${formCode}`); + console.warn(`No formEntries found for formCode: ${formCode}, contractItemId: ${contractItemId}`); } } catch (statusUpdateError) { // Status 업데이트 실패는 경고로만 처리 (SEDP 전송은 성공했으므로) @@ -1755,14 +1812,12 @@ export async function sendFormDataToSEDP( export async function deleteFormDataByTags({ formCode, - projectCode, - packageCode, + contractItemId, tagIdxs, projectId, }: { formCode: string - projectCode: string - packageCode: string + contractItemId: number tagIdxs: string[] projectId?: number }): Promise<{ @@ -1775,26 +1830,25 @@ export async function deleteFormDataByTags({ }> { try { // 입력 검증 - if (!formCode || !projectCode || !Array.isArray(tagIdxs) || tagIdxs.length === 0) { + if (!formCode || !contractItemId || !Array.isArray(tagIdxs) || tagIdxs.length === 0) { return { error: "Missing required parameters: formCode, contractItemId, tagIdxs", } } - console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, projectCode: ${projectCode}, tagIdxs:`, tagIdxs) + console.log(`[DELETE ACTION] Deleting tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagIdxs:`, tagIdxs) // 1. 트랜잭션 전에 삭제할 항목들을 미리 조회하여 저장 (S-EDP 전송용) const entryForSedp = await db .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1) let itemsToSendToSedp: Record<string, unknown>[] = [] @@ -1814,15 +1868,14 @@ export async function deleteFormDataByTags({ // 2-1. 현재 formEntry 데이터 가져오기 const currentEntryResult = await tx .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1) if (currentEntryResult.length === 0) { @@ -1850,15 +1903,14 @@ export async function deleteFormDataByTags({ // 2-3. tags 테이블에서 해당 태그들 삭제 const deletedTagsResult = await tx - .delete(tagsPlant) + .delete(tags) .where( and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode), - inArray(tagsPlant.tagIdx, tagIdxs) + eq(tags.contractItemId, contractItemId), + inArray(tags.tagIdx, tagIdxs) ) ) - .returning({ tagNo: tagsPlant.tagNo }) + .returning({ tagNo: tags.tagNo }) const deletedTagsCount = deletedTagsResult.length @@ -1867,16 +1919,15 @@ export async function deleteFormDataByTags({ // 2-4. formEntries 데이터 업데이트 (삭제된 항목 제외) await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date(), }) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) @@ -1969,8 +2020,7 @@ export async function deleteFormDataByTags({ const sedpResult = await sendFormDataToSEDP( formCode, projectId, - projectCode, - packageCode, + contractItemId, uniqueDeletedItems as GenericData[], formMetaResult.columns as DataTableColumnJSON[] ) @@ -2029,13 +2079,11 @@ export async function deleteFormDataByTags({ */ export async function excludeFormDataByTags({ formCode, - projectCode, - packageCode, + contractItemId, tagNumbers, }: { formCode: string - projectCode: string - packageCode: string + contractItemId: number tagNumbers: string[] }): Promise<{ error?: string @@ -2044,28 +2092,27 @@ export async function excludeFormDataByTags({ }> { try { // 입력 검증 - if (!formCode || !projectCode || !Array.isArray(tagNumbers) || tagNumbers.length === 0) { + if (!formCode || !contractItemId || !Array.isArray(tagNumbers) || tagNumbers.length === 0) { return { - error: "Missing required parameters: formCode, projectCode, tagNumbers", + error: "Missing required parameters: formCode, contractItemId, tagNumbers", } } - console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, projectCode: ${projectCode}, tagNumbers:`, tagNumbers) + console.log(`[EXCLUDE ACTION] Excluding tags for formCode: ${formCode}, contractItemId: ${contractItemId}, tagNumbers:`, tagNumbers) // 트랜잭션으로 안전하게 처리 const result = await db.transaction(async (tx) => { // 1. 현재 formEntry 데이터 가져오기 const currentEntryResult = await tx .select() - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) - .orderBy(desc(formEntriesPlant.updatedAt)) + .orderBy(desc(formEntries.updatedAt)) .limit(1) if (currentEntryResult.length === 0) { @@ -2099,16 +2146,15 @@ export async function excludeFormDataByTags({ // 3. formEntries 데이터 업데이트 await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date(), }) .where( and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, contractItemId) ) ) @@ -2119,7 +2165,7 @@ export async function excludeFormDataByTags({ }) // 4. 캐시 무효화 - const cacheKey = `form-data-${formCode}-${packageCode}` + const cacheKey = `form-data-${formCode}-${contractItemId}` revalidateTag(cacheKey) console.log(`[EXCLUDE ACTION] Transaction completed successfully`) diff --git a/lib/forms-plant/stat.ts b/lib/forms-plant/stat.ts index f734e782..f13bab61 100644 --- a/lib/forms-plant/stat.ts +++ b/lib/forms-plant/stat.ts @@ -1,7 +1,7 @@ "use server" import db from "@/db/db" -import { vendors, contracts, contractItems, forms,formsPlant,formEntriesPlant, formEntries, formMetas, tags,tagsPlant, tagClasses, tagClassAttributes, projects } from "@/db/schema" +import { vendors, contracts, contractItems, forms, formEntries, formMetas, tags, tagClasses, tagClassAttributes, projects } from "@/db/schema" import { eq, and, inArray } from "drizzle-orm" import { getEditableFieldsByTag } from "./services" import { getServerSession } from "next-auth/next" @@ -218,7 +218,7 @@ export async function getVendorFormStatus(projectId?: number): Promise<VendorFor -export async function getFormStatusByVendor(projectId: number, projectCode: string, packageCode: string, formCode: string): Promise<FormStatusByVendor[]> { +export async function getFormStatusByVendor(projectId: number, contractItemId: number, formCode: string): Promise<FormStatusByVendor[]> { try { const session = await getServerSession(authOptions) if (!session?.user?.id) { @@ -244,16 +244,15 @@ export async function getFormStatusByVendor(projectId: number, projectCode: stri // 4. contractItem별 forms 조회 const formsList = await db .select({ - id: formsPlant.id, - formCode: formsPlant.formCode, - contractItemId: formsPlant.contractItemId + id: forms.id, + formCode: forms.formCode, + contractItemId: forms.contractItemId }) - .from(formsPlant) + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formCode) + eq(forms.contractItemId, contractItemId), + eq(forms.formCode, formCode) ) ) @@ -262,21 +261,20 @@ export async function getFormStatusByVendor(projectId: number, projectCode: stri // 5. formEntries 조회 const entriesList = await db .select({ - id: formEntriesPlant.id, - formCode: formEntriesPlant.formCode, - data: formEntriesPlant.data + id: formEntries.id, + formCode: formEntries.formCode, + data: formEntries.data }) - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.formCode, formCode) + eq(formEntries.contractItemId, contractItemId), + eq(formEntries.formCode, formCode) ) ) // 6. TAG별 편집 가능 필드 조회 - const editableFieldsByTag = await getEditableFieldsByTag(projectCode,packageCode, projectId) + const editableFieldsByTag = await getEditableFieldsByTag(contractItemId, projectId) const vendorStatusList: VendorFormStatus[] = [] diff --git a/lib/sedp/get-form-tags-plant.ts b/lib/sedp/get-form-tags-plant.ts deleted file mode 100644 index 176f1b3f..00000000 --- a/lib/sedp/get-form-tags-plant.ts +++ /dev/null @@ -1,933 +0,0 @@ -import db from "@/db/db"; -import { - contractItems, - tagsPlant, - formsPlant,formEntriesPlant, - items, - tagTypeClassFormMappings, - projects, - tagTypes, - tagClasses, - formMetas, -} from "@/db/schema"; -import { eq, and, like, inArray } from "drizzle-orm"; -import { getSEDPToken } from "./sedp-token"; -import { getFormMappingsByTagTypebyProeject } from "../tags/form-mapping-service"; - - -interface Attribute { - ATT_ID: string; - VALUE: any; - VALUE_DBL: number; - UOM_ID: string | null; -} - -interface TagEntry { - TAG_IDX: string; - TAG_NO: string; - BF_TAG_NO: string; - TAG_DESC: string; - EP_ID: string; - TAG_TYPE_ID: string; - CLS_ID: string; - ATTRIBUTES: Attribute[]; - [key: string]: any; -} - -interface Column { - key: string; - label: string; - type: string; - shi?: string | null; -} - -interface newRegister { - PROJ_NO: string; - MAP_ID: string; - EP_ID: string; - CATEGORY: string; - BYPASS: boolean; - REG_TYPE_ID: string; - TOOL_ID: string; - TOOL_TYPE: string; - SCOPES: string[]; - MAP_CLS: { - TOOL_ATT_NAME: string; - ITEMS: ClassItmes[]; - }; - MAP_ATT: MapAttribute[]; - MAP_TMPLS: string[]; - CRTER_NO: string; - CRTE_DTM: string; - CHGER_NO: string; - _id: string; -} - -interface ClassItmes { - SEDP_OBJ_CLS_ID: string; - TOOL_VALS: string; - ISDEFALUT: boolean; -} - -interface MapAttribute { - SEDP_ATT_ID: string; - TOOL_ATT_NAME: string; - KEY_YN: boolean; - DUE_DATE: string; //"YYYY-MM-DDTHH:mm:ssZ" - INOUT: string | null; -} - - - -async function getNewRegisters(projectCode: string): Promise<newRegister[]> { - try { - // 토큰(API 키) 가져오기 - const apiKey = await getSEDPToken(); - - const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; - - 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}`); - } - - // 안전하게 JSON 파싱 - let data; - try { - data = await response.json(); - } catch (parseError) { - console.error(`프로젝트 ${projectCode}의 새 레지스터 응답 파싱 실패:`, parseError); - // 응답 내용 로깅 - const text = await response.clone().text(); - console.log(`응답 내용: ${text.substring(0, 200)}${text.length > 200 ? '...' : ''}`); - throw new Error(`새 레지스터 응답 파싱 실패: ${parseError instanceof Error ? parseError.message : String(parseError)}`); - } - - // 결과를 배열로 변환 (단일 객체인 경우 배열로 래핑) - let registers: newRegister[] = Array.isArray(data) ? data : [data]; - - console.log(`프로젝트 ${projectCode}에서 ${registers.length}개의 새 레지스터를 가져왔습니다.`); - return registers; - } catch (error) { - console.error(`프로젝트 ${projectCode}의 새 레지스터 가져오기 실패:`, error); - throw error; - } -} - - -/** - * 태그 가져오기 서비스 함수 - * formEntries와 tags 테이블 모두에 데이터를 저장 - */ -export async function importTagsFromSEDP( - formCode: string, - projectCode: string, - packageCode: string, - progressCallback?: (progress: number) => void -): Promise<{ - processedCount: number; - excludedCount: number; - totalEntries: number; - formCreated?: boolean; - errors?: string[]; -}> { - try { - // 진행 상황 보고 - if (progressCallback) progressCallback(5); - - // 에러 수집 배열 - const errors: string[] = []; - - // SEDP API에서 태그 데이터 가져오기 - const tagData = await fetchTagDataFromSEDP(projectCode, formCode); - const newRegisters = await getNewRegisters(projectCode); - - const registerMatched = newRegisters.find(v => v.REG_TYPE_ID === formCode).MAP_ATT - - - // 트랜잭션으로 모든 DB 작업 처리 - return await db.transaction(async (tx) => { - // 프로젝트 정보 가져오기 (type 포함) - const projectRecord = await tx.select({ id: projects.id, type: projects.type }) - .from(projects) - .where(eq(projects.code, projectCode)) - .limit(1); - - if (!projectRecord || projectRecord.length === 0) { - throw new Error(`Project not found for code: ${projectCode}`); - } - - const projectId = projectRecord[0].id; - const projectType = projectRecord[0].type; - - // 프로젝트 타입에 따라 packageCode를 찾을 ATT_ID 결정 - const packageCodeAttId = projectType === "ship" ? "CM3003" : "ME5074"; - - - - - const targetPackageCode = packageCode; - - // 데이터 형식 처리 - tagData의 첫 번째 키 사용 - const tableName = Object.keys(tagData)[0]; - - if (!tableName || !tagData[tableName]) { - throw new Error("Invalid tag data format from SEDP API"); - } - - const allTagEntries: TagEntry[] = tagData[tableName]; - - if (!Array.isArray(allTagEntries) || allTagEntries.length === 0) { - return { - processedCount: 0, - excludedCount: 0, - totalEntries: 0, - errors: ["No tag entries found in API response"] - }; - } - - // packageCode로 필터링 - ATTRIBUTES에서 지정된 ATT_ID의 VALUE와 packageCode 비교 - const tagEntries = allTagEntries.filter(entry => { - if (Array.isArray(entry.ATTRIBUTES)) { - const packageCodeAttr = entry.ATTRIBUTES.find(attr => attr.ATT_ID === packageCodeAttId); - if (packageCodeAttr && packageCodeAttr.VALUE === targetPackageCode) { - return true; - } - } - return false; - }); - - if (tagEntries.length === 0) { - return { - processedCount: 0, - excludedCount: 0, - totalEntries: allTagEntries.length, - errors: [`No tag entries found with ${packageCodeAttId} attribute value matching packageCode: ${targetPackageCode}`] - }; - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(20); - - // 나머지 코드는 기존과 동일... - // form ID 가져오기 - 없으면 생성 - let formRecord = await tx.select({ id: formsPlant.id }) - .from(formsPlant) - .where(and( - eq(formsPlant.formCode, formCode), - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode) - )) - .limit(1); - - let formCreated = false; - - // form이 없으면 생성 - if (!formRecord || formRecord.length === 0) { - console.log(`[IMPORT TAGS] Form ${formCode} not found, attempting to create...`); - - // 첫 번째 태그의 정보를 사용해서 form mapping을 찾습니다 - // 모든 태그가 같은 formCode를 사용한다고 가정 - if (tagEntries.length > 0) { - const firstTag = tagEntries[0]; - - // tagType 조회 (TAG_TYPE_ID -> description) - let tagTypeDescription = firstTag.TAG_TYPE_ID; // 기본값 - if (firstTag.TAG_TYPE_ID) { - const tagTypeRecord = await tx.select({ description: tagTypes.description }) - .from(tagTypes) - .where(and( - eq(tagTypes.code, firstTag.TAG_TYPE_ID), - eq(tagTypes.projectId, projectId) - )) - .limit(1); - - if (tagTypeRecord && tagTypeRecord.length > 0) { - tagTypeDescription = tagTypeRecord[0].description; - } - } - - // tagClass 조회 (CLS_ID -> label) - let tagClassLabel = firstTag.CLS_ID; // 기본값 - if (firstTag.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) - .from(tagClasses) - .where(and( - eq(tagClasses.code, firstTag.CLS_ID), - eq(tagClasses.projectId, projectId) - )) - .limit(1); - - if (tagClassRecord && tagClassRecord.length > 0) { - tagClassLabel = tagClassRecord[0].label; - } - } - - // 태그 타입에 따른 폼 정보 가져오기 - const allFormMappings = await getFormMappingsByTagTypebyProeject( - projectId, - ); - - // 현재 formCode와 일치하는 매핑 찾기 - const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode); - - if (targetFormMapping) { - console.log(`[IMPORT TAGS] Found form mapping for ${formCode}, creating form...`); - - // form 생성 - const insertResult = await tx - .insert(formsPlant) - .values({ - projectCode, - packageCode, - formCode: targetFormMapping.formCode, - formName: targetFormMapping.formName, - eng: true, // ENG 모드에서 가져오는 것이므로 eng: true - im: targetFormMapping.ep === "IMEP" ? true : false - }) - .returning({ id: formsPlant.id }); - - formRecord = insertResult; - formCreated = true; - - console.log(`[IMPORT TAGS] Successfully created form:`, insertResult[0]); - } else { - console.log(`[IMPORT TAGS] No form mapping found for formCode: ${formCode}`); - console.log(`[IMPORT TAGS] Available mappings:`, allFormMappings.map(m => m.formCode)); - throw new Error(`Form ${formCode} not found and no mapping available for tag type ${tagTypeDescription}`); - } - } else { - throw new Error(`Form not found for formCode: ${formCode} and, and no tags to derive form mapping`); - } - } else { - console.log(`[IMPORT TAGS] Found existing form:`, formRecord[0].id); - - // 기존 form이 있는 경우 eng와 im 필드를 체크하고 업데이트 - const existingForm = await tx.select({ - eng: formsPlant.eng, - im: formsPlant.im - }) - .from(formsPlant) - .where(eq(formsPlant.id, formRecord[0].id)) - .limit(1); - - if (existingForm.length > 0) { - // form mapping 정보 가져오기 (im 필드 업데이트를 위해) - let shouldUpdateIm = false; - let targetImValue = false; - - // 첫 번째 태그의 정보를 사용해서 form mapping을 확인 - if (tagEntries.length > 0) { - const firstTag = tagEntries[0]; - - // tagType 조회 - let tagTypeDescription = firstTag.TAG_TYPE_ID; - if (firstTag.TAG_TYPE_ID) { - const tagTypeRecord = await tx.select({ description: tagTypes.description }) - .from(tagTypes) - .where(and( - eq(tagTypes.code, firstTag.TAG_TYPE_ID), - eq(tagTypes.projectId, projectId) - )) - .limit(1); - - if (tagTypeRecord && tagTypeRecord.length > 0) { - tagTypeDescription = tagTypeRecord[0].description; - } - } - - // tagClass 조회 - let tagClassLabel = firstTag.CLS_ID; - if (firstTag.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) - .from(tagClasses) - .where(and( - eq(tagClasses.code, firstTag.CLS_ID), - eq(tagClasses.projectId, projectId) - )) - .limit(1); - - if (tagClassRecord && tagClassRecord.length > 0) { - tagClassLabel = tagClassRecord[0].label; - } - } - - // form mapping 정보 가져오기 - const allFormMappings = await getFormMappingsByTagTypebyProeject( - projectId, - ); - - // 현재 formCode와 일치하는 매핑 찾기 - const targetFormMapping = allFormMappings.find(mapping => mapping.formCode === formCode); - - if (targetFormMapping) { - targetImValue = targetFormMapping.ep === "IMEP"; - shouldUpdateIm = existingForm[0].im !== targetImValue; - } - } - - // 업데이트할 필드들 준비 - const updates: any = {}; - let hasUpdates = false; - - // eng 필드 체크 - if (existingForm[0].eng !== true) { - updates.eng = true; - hasUpdates = true; - } - - // im 필드 체크 - if (shouldUpdateIm) { - updates.im = targetImValue; - hasUpdates = true; - } - - // 업데이트 실행 - if (hasUpdates) { - await tx - .update(formsPlant) - .set(updates) - .where(eq(formsPlant.id, formRecord[0].id)); - - console.log(`[IMPORT TAGS] Form ${formRecord[0].id} updated with:`, updates); - } - } - } - - const formId = formRecord[0].id; - - // 나머지 처리 로직은 기존과 동일... - // (양식 메타데이터 가져오기, 태그 처리 등) - - // 양식 메타데이터 가져오기 - const formMetaRecord = await tx.select({ columns: formMetas.columns }) - .from(formMetas) - .where(and( - eq(formMetas.projectId, projectId), - eq(formMetas.formCode, formCode) - )) - .limit(1); - - if (!formMetaRecord || formMetaRecord.length === 0) { - throw new Error(`Form metadata not found for formCode: ${formCode} and projectId: ${projectId}`); - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(30); - - // 컬럼 정보 파싱 - const columnsJSON: Column[] = (formMetaRecord[0].columns); - - // 현재 formEntries 데이터 가져오기 - const existingEntries = await tx.select({ id: formEntriesPlant.id, data: formEntriesPlant.data }) - .from(formEntriesPlant) - .where(and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) - )); - - // 기존 tags 데이터 가져오기 - const existingTags = await tx.select() - .from(tagsPlant) - .where(and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode), - ) - ); - - // 진행 상황 보고 - if (progressCallback) progressCallback(50); - - // 기존 데이터를 맵으로 변환 - const existingTagMap = new Map(); - const existingTagsMap = new Map(); - - existingEntries.forEach(entry => { - const data = entry.data as any[]; - data.forEach(item => { - if (item.TAG_IDX) { - existingTagMap.set(item.TAG_IDX, { - entryId: entry.id, - data: item - }); - } - }); - }); - - existingTags.forEach(tag => { - existingTagsMap.set(tag.tagIdx, tag); - }); - - // 진행 상황 보고 - if (progressCallback) progressCallback(60); - - // 처리 결과 카운터 - let processedCount = 0; - let excludedCount = 0; - - // 새로운 태그 데이터와 업데이트할 데이터 준비 - const newTagData: any[] = []; - const upsertTagRecords: any[] = []; // 새로 추가되거나 업데이트될 태그들 - const updateData: { entryId: number, tagNo: string, updates: any }[] = []; - - // SEDP 태그 데이터 처리 - for (const tagEntry of tagEntries) { - try { - if (!tagEntry.TAG_IDX) { - excludedCount++; - errors.push(`Missing TAG_NO in tag entry`); - continue; - } - - // tagType 조회 (TAG_TYPE_ID -> description) - let tagTypeDescription = tagEntry.TAG_TYPE_ID; // 기본값 - if (tagEntry.TAG_TYPE_ID) { - const tagTypeRecord = await tx.select({ description: tagTypes.description }) - .from(tagTypes) - .where(and( - eq(tagTypes.code, tagEntry.TAG_TYPE_ID), - eq(tagTypes.projectId, projectId) - )) - .limit(1); - - if (tagTypeRecord && tagTypeRecord.length > 0) { - tagTypeDescription = tagTypeRecord[0].description; - } - } - - // tagClass 조회 (CLS_ID -> label) - let tagClassLabel = tagEntry.CLS_ID; // 기본값 - let tagClassId = null; // 기본값 - if (tagEntry.CLS_ID) { - const tagClassRecord = await tx.select({ id: tagClasses.id, label: tagClasses.label }) - .from(tagClasses) - .where(and( - eq(tagClasses.code, tagEntry.CLS_ID), - eq(tagClasses.projectId, projectId) - )) - .limit(1); - - if (tagClassRecord && tagClassRecord.length > 0) { - tagClassLabel = tagClassRecord[0].label; - tagClassId = tagClassRecord[0].id; - } - } - - const packageCode = projectType === "ship" ? tagEntry.ATTRIBUTES.find(v => v.ATT_ID === "CM3003")?.VALUE : tagEntry.ATTRIBUTES.find(v => v.ATT_ID === "ME5074")?.VALUE - - // 기본 태그 데이터 객체 생성 (formEntries용) - const tagObject: any = { - TAG_IDX: tagEntry.TAG_IDX, // SEDP 고유 식별자 - TAG_NO: tagEntry.TAG_NO, - TAG_DESC: tagEntry.TAG_DESC || "", - CLS_ID: tagEntry.CLS_ID || "", - VNDRCD: vendorRecord[0].vendorCode, - VNDRNM_1: vendorRecord[0].vendorName, - status: "From S-EDP", // SEDP에서 가져온 데이터임을 표시 - source: "S-EDP", // 태그 출처 (불변) - S-EDP에서 가져옴 - ...(projectType === "ship" ? { CM3003: packageCode } : { ME5074: packageCode }) - } - - let latestDueDate: Date | null = null; - - // tags 테이블용 데이터 (UPSERT용) - const tagRecord = { - projectCode, - packageCode, - formId: formId, - tagIdx: tagEntry.TAG_IDX, // SEDP 고유 식별자 - tagNo: tagEntry.TAG_NO, - tagType: tagTypeDescription || "", - class: tagClassLabel, - tagClassId: tagClassId, - description: tagEntry.TAG_DESC || null, - createdAt: new Date(), - updatedAt: new Date() - }; - - // ATTRIBUTES 필드에서 shi=true인 컬럼의 값 추출 - if (Array.isArray(tagEntry.ATTRIBUTES)) { - for (const attr of tagEntry.ATTRIBUTES) { - const columnInfo = columnsJSON.find(col => col.key === attr.ATT_ID); - if (columnInfo && (columnInfo.shi === "BOTH" || columnInfo.shi === "OUT" || columnInfo.shi === null)) { - if (columnInfo.type === "NUMBER") { - if (attr.VALUE !== undefined && attr.VALUE !== null) { - if (typeof attr.VALUE === 'string') { - const numberMatch = attr.VALUE.match(/(-?\d+(\.\d+)?)/); - if (numberMatch) { - tagObject[attr.ATT_ID] = parseFloat(numberMatch[0]); - } else { - const parsed = parseFloat(attr.VALUE); - if (!isNaN(parsed)) { - tagObject[attr.ATT_ID] = parsed; - } - } - } else if (typeof attr.VALUE === 'number') { - tagObject[attr.ATT_ID] = attr.VALUE; - } - } - } else if (attr.VALUE !== null && attr.VALUE !== undefined) { - tagObject[attr.ATT_ID] = attr.VALUE; - } - } - - // registerMatched에서 해당 SEDP_ATT_ID의 DUE_DATE 찾기 - if (registerMatched && Array.isArray(registerMatched)) { - const matchedAttribute = registerMatched.find( - regAttr => regAttr.SEDP_ATT_ID === attr.ATT_ID - ); - - if (matchedAttribute && matchedAttribute.DUE_DATE) { - try { - const dueDate = new Date(matchedAttribute.DUE_DATE); - - // 유효한 날짜인지 확인 - if (!isNaN(dueDate.getTime())) { - // 첫 번째 DUE_DATE이거나 현재까지 찾은 것보다 더 늦은 날짜인 경우 업데이트 - if (!latestDueDate || dueDate > latestDueDate) { - latestDueDate = dueDate; - } - } - } catch (dateError) { - console.warn(`Invalid DUE_DATE format for ${attr.ATT_ID}: ${matchedAttribute.DUE_DATE}`); - } - } - } - - } - } - - if (latestDueDate) { - // ISO 형식의 문자열로 저장 (또는 원하는 형식으로 변경 가능) - tagObject.DUE_DATE = latestDueDate.toISOString(); - - // 또는 YYYY-MM-DD 형식을 원한다면: - // tagObject.DUE_DATE = latestDueDate.toISOString().split('T')[0]; - - // 또는 원본 형식 그대로 유지하려면: - // tagObject.DUE_DATE = latestDueDate.toISOString().replace('Z', ''); - } - - - - // 기존 태그가 있는지 확인하고 처리 - const existingTag = existingTagMap.get(tagEntry.TAG_IDX); - - if (existingTag) { - // 기존 태그가 있으면 formEntries 업데이트 데이터 준비 - const updates: any = {}; - let hasUpdates = false; - - for (const key of Object.keys(tagObject)) { - if (key === "TAG_IDX") continue; - - if (key === "TAG_NO" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - - - if (key === "TAG_DESC" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - - if (key === "status" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - if (key === "CLS_ID" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - - if (key === "DUE_DATE" && tagObject[key] !== existingTag.data[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - continue; - } - - const columnInfo = columnsJSON.find(col => col.key === key); - if (columnInfo && (columnInfo.shi === "BOTH" || columnInfo.shi === "OUT" || columnInfo.shi === null)) { - if (existingTag.data[key] !== tagObject[key]) { - updates[key] = tagObject[key]; - hasUpdates = true; - } - } - } - - if (hasUpdates) { - updateData.push({ - entryId: existingTag.entryId, - tagIdx: tagEntry.TAG_IDX, // TAG_IDX로 변경 - updates - }); - } - } else { - // 기존 태그가 없으면 새로 추가 - newTagData.push(tagObject); - } - - // tags 테이블에는 항상 upsert (새로 추가되거나 업데이트) - upsertTagRecords.push(tagRecord); - - processedCount++; - } catch (error) { - excludedCount++; - errors.push(`Error processing tag ${tagEntry.TAG_IDX || 'unknown'}: ${error}`); - } - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(80); - - // formEntries 업데이트 실행 - // entryId별로 업데이트를 그룹화 - const updatesByEntryId = new Map(); - - for (const update of updateData) { - if (!updatesByEntryId.has(update.entryId)) { - updatesByEntryId.set(update.entryId, []); - } - updatesByEntryId.get(update.entryId).push(update); - } - - // 그룹화된 업데이트를 처리 - for (const [entryId, updates] of updatesByEntryId) { - try { - const entry = existingEntries.find(e => e.id === entryId); - if (!entry) continue; - - const data = entry.data as any[]; - - // 해당 entryId의 모든 업데이트를 한 번에 적용 - const updatedData = data.map(item => { - let updatedItem = { ...item }; - - // 현재 item에 적용할 모든 업데이트를 찾아서 적용 - for (const update of updates) { - if (item.TAG_IDX === update.tagIdx) { - updatedItem = { ...updatedItem, ...update.updates }; - } - } - - return updatedItem; - }); - - // entryId별로 한 번만 DB 업데이트 - await tx.update(formEntriesPlant) - .set({ - data: updatedData, - updatedAt: new Date() - }) - .where(eq(formEntriesPlant.id, entryId)); - - } catch (error) { - const tagNos = updates.map(u => u.tagNo || u.tagIdx).join(', '); - errors.push(`Error updating formEntry ${entryId} for tags ${tagNos}: ${error}`); - } - } - - // 새 태그 추가 (formEntriesPlant) - if (newTagData.length > 0) { - if (existingEntries.length > 0) { - const firstEntry = existingEntries[0]; - const existingData = firstEntry.data as any[]; - const updatedData = [...existingData, ...newTagData]; - - await tx.update(formEntriesPlant) - .set({ - data: updatedData, - updatedAt: new Date() - }) - .where(eq(formEntriesPlant.id, firstEntry.id)); - } else { - await tx.insert(formEntriesPlant) - .values({ - formCode, - projectCode, - packageCode, - data: newTagData, - createdAt: new Date(), - updatedAt: new Date() - }); - } - } - - // tags 테이블 처리 (INSERT + UPDATE 분리) - if (upsertTagRecords.length > 0) { - const newTagRecords: any[] = []; - const updateTagRecords: { tagId: number, updates: any }[] = []; - - // 각 태그를 확인하여 신규/업데이트 분류 - for (const tagRecord of upsertTagRecords) { - const existingTagRecord = existingTagsMap.get(tagRecord.tagIdx); - - if (existingTagRecord) { - // 기존 태그가 있으면 업데이트 준비 - const tagUpdates: any = {}; - let hasTagUpdates = false; - - // tagNo도 업데이트 가능 (편집된 경우) - if (existingTagRecord.tagNo !== tagRecord.tagNo) { - tagUpdates.tagNo = tagRecord.tagNo; - hasTagUpdates = true; - } - - if (existingTagRecord.tagType !== tagRecord.tagType) { - tagUpdates.tagType = tagRecord.tagType; - hasTagUpdates = true; - } - if (existingTagRecord.class !== tagRecord.class) { - tagUpdates.class = tagRecord.class; - hasTagUpdates = true; - } - if (existingTagRecord.tagClassId !== tagRecord.tagClassId) { - tagUpdates.tagClassId = tagRecord.tagClassId; - hasTagUpdates = true; - } - - if (existingTagRecord.description !== tagRecord.description) { - tagUpdates.description = tagRecord.description; - hasTagUpdates = true; - } - if (existingTagRecord.formId !== tagRecord.formId) { - tagUpdates.formId = tagRecord.formId; - hasTagUpdates = true; - } - - if (hasTagUpdates) { - updateTagRecords.push({ - tagId: existingTagRecord.id, - updates: { ...tagUpdates, updatedAt: new Date() } - }); - } - } else { - // 새로운 태그 - newTagRecords.push(tagRecord); - } - } - - // 새 태그 삽입 - if (newTagRecords.length > 0) { - try { - await tx.insert(tagsPlant) - .values(newTagRecords) - .onConflictDoNothing({ - target: [tagsPlant.projectCode,tagsPlant.packageCode, tagsPlant.tagIdx] - }); - } catch (error) { - // 개별 삽입으로 재시도 - for (const tagRecord of newTagRecords) { - try { - await tx.insert(tagsPlant) - .values(tagRecord) - .onConflictDoNothing({ - target: [tagsPlant.projectCode,tagsPlant.packageCode, tagsPlant.tagIdx] - }); - } catch (individualError) { - errors.push(`Error inserting tag ${tagRecord.tagIdx}: ${individualError}`); - } - } - } - } - - // 기존 태그 업데이트 - for (const update of updateTagRecords) { - try { - await tx.update(tagsPlant) - .set(update.updates) - .where(eq(tagsPlant.id, update.tagId)); - } catch (error) { - errors.push(`Error updating tag record ${update.tagId}: ${error}`); - } - } - } - - // 진행 상황 보고 - if (progressCallback) progressCallback(100); - - // 최종 결과 반환 - return { - processedCount, - excludedCount, - totalEntries: tagEntries.length, - formCreated, - errors: errors.length > 0 ? errors : 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<any> { - 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, - // TODO: 이창국 프로 요청으로, ContainDeleted: true로 변경예정, EDP에서 삭제된 데이터도 가져올 수 있어야 한다고 함. - // 삭제된 게 들어오면 eVCP내에서 지우거나, 비활성화 하는 등의 처리를 해야 할 걸로 보임 - 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'}`); - } -}
\ No newline at end of file diff --git a/lib/sedp/get-tags-plant.ts b/lib/sedp/get-tags-plant.ts deleted file mode 100644 index d1957db4..00000000 --- a/lib/sedp/get-tags-plant.ts +++ /dev/null @@ -1,639 +0,0 @@ -import db from "@/db/db"; -import { - tagsPlant, - formsPlant, - formEntriesPlant, - items, - tagTypeClassFormMappings, - projects, - tagTypes, - tagClasses, -} 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 projectCode 계약 아이템 ID (contractItemId) - * @param packageCode 계약 아이템 ID (contractItemId) - * @param progressCallback 진행 상황을 보고하기 위한 콜백 함수 - * @returns 처리 결과 정보 (처리된 태그 수, 오류 목록 등) - */ -export async function importTagsFromSEDP( - projectCode: string, - packageCode: string, - progressCallback?: (progress: number) => void, - mode?: string -): Promise<{ - processedCount: number; - excludedCount: number; - totalEntries: number; - errors?: string[]; -}> { - try { - // 진행 상황 보고 - if (progressCallback) progressCallback(5); - - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); - - - // 프로젝트 ID 획득 - const projectId = project?.id; - - // Step 1-2: Get the item using itemId from contractItem - const item = await db.query.items.findFirst({ - where: and(eq(items.ProjectNo, projectCode), eq(items.packageCode, packageCode)) - }); - - if (!item) { - throw new Error(`Item with ID ${item?.id} 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<string>(); - - 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; // 다음 매핑으로 진행 - } - - // 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.formsPlant.findFirst({ - where: and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formCode) - ) - }); - - let formId; - - // If form doesn't exist, create it - if (!form) { - // 폼이 없는 경우 새로 생성 - 모드에 따른 필드 설정 - const insertValues: any = { - projectCode, - packageCode, - 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(formsPlant).values(insertValues).returning({ id: formsPlant.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(formsPlant) - .set({ - ...updateValues, - updatedAt: new Date() - }) - .where(eq(formsPlant.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; - }> = []; - - const registerResponse = 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: baseMappings[0].formCode, // 또는 mapping.formCode - ContainDeleted: false - }) - } - ) - - if (!registerResponse.ok) { - allErrors.push(`Failed to fetch register details for ${baseMappings[0].formCode}`) - continue - } - - const registerDetail: Register = await registerResponse.json() - - // ✅ MAP_ATT에서 허용된 ATT_ID 목록 추출 - const allowedAttIds = new Set<string>() - if (Array.isArray(registerDetail.MAP_ATT)) { - for (const mapAttr of registerDetail.MAP_ATT) { - if (mapAttr.ATT_ID) { - allowedAttIds.add(mapAttr.ATT_ID) - } - } - } - - - // 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; // 이 항목은 건너뜀 - } - - const attributes: Record<string, string> = {} - if (Array.isArray(entry.ATTRIBUTES)) { - for (const attr of entry.ATTRIBUTES) { - // MAP_ATT에 정의된 ATT_ID만 포함 - if (attr.ATT_ID && allowedAttIds.has(attr.ATT_ID)) { - if (attr.VALUE !== null && attr.VALUE !== undefined) { - attributes[attr.ATT_ID] = String(attr.VALUE) - } - } - } - } - - - // 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(tagsPlant).values({ - projectCode, - packageCode, - formId: formId, - tagIdx: entry.TAG_IDX, - tagNo: entry.TAG_NO || entry.TAG_IDX, - tagType: tagType?.description || entry.TAG_TYPE_ID, - tagClassId: tagClass?.id, - class: tagClass?.label || entry.CLS_ID, - description: entry.TAG_DESC, - attributes: attributes, // JSONB로 저장 - }).onConflictDoUpdate({ - target: [tagsPlant.projectCode, tagsPlant.packageCode, tagsPlant.tagIdx], - set: { - formId: formId, - tagNo: entry.TAG_NO || entry.TAG_IDX, - tagType: tagType?.description || entry.TAG_TYPE_ID, - class: tagClass?.label || entry.CLS_ID, - description: entry.TAG_DESC, - attributes: attributes, // JSONB 업데이트 - 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.formEntriesPlant.findFirst({ - where: and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) - ) - }); - - 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(formEntriesPlant) - .set({ - data: finalData, - updatedAt: new Date() - }) - .where(eq(formEntriesPlant.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(formEntriesPlant).values({ - formCode: formCode, - projectCode, - packageCode, - 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<any> { - 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'}`); - } -}
\ No newline at end of file diff --git a/lib/sedp/sync-form.ts b/lib/sedp/sync-form.ts index a6d473ad..904d27ba 100644 --- a/lib/sedp/sync-form.ts +++ b/lib/sedp/sync-form.ts @@ -94,7 +94,7 @@ interface Register { SEQ: number; CMPLX_YN: boolean; CMPL_SETT: any | null; - MAP_ATT: MapAttribute2[]; + MAP_ATT: any[]; MAP_CLS_ID: string[]; MAP_OPER: any | null; LNK_ATT: LinkAttribute[]; @@ -157,13 +157,6 @@ interface MapAttribute { INOUT: string | null; } -interface MapAttribute2 { - ATT_ID: string; - VALUE: string; - IS_PARA: boolean; - OPER: string | null; -} - interface Attribute { PROJ_NO: string; ATT_ID: string; diff --git a/lib/tags-plant/column-builder.service.ts b/lib/tags-plant/column-builder.service.ts deleted file mode 100644 index 9a552d6e..00000000 --- a/lib/tags-plant/column-builder.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -// lib/vendor-data-plant/column-builder.service.ts -import { ColumnDef } from "@tanstack/react-table" -import { Tag } from "@/db/schema/vendorData" - -/** - * 동적 속성 컬럼 생성 (ATT_ID만 사용, 라벨 없음) - */ -export function createDynamicAttributeColumns( - attributeKeys: string[] -): ColumnDef<Tag>[] { - return attributeKeys.map(key => ({ - id: `attr_${key}`, - accessorFn: (row: Tag) => { - if (row.attributes && typeof row.attributes === 'object') { - return (row.attributes as Record<string, string>)[key] || '' - } - return '' - }, - header: key, // 단순 문자열로 반환 - cell: ({ getValue }) => { - const value = getValue() - return value as string || "-" - }, - meta: { - excelHeader: key - }, - enableSorting: true, - enableColumnFilter: true, - filterFn: "includesString", - enableResizing: true, - minSize: 100, - size: 150, - })) -}
\ No newline at end of file diff --git a/lib/tags-plant/queries.ts b/lib/tags-plant/queries.ts deleted file mode 100644 index a0d28b1e..00000000 --- a/lib/tags-plant/queries.ts +++ /dev/null @@ -1,68 +0,0 @@ -// lib/vendor-data-plant/queries.ts -"use server" - -import db from "@/db/db" - -import { tagsPlant } from "@/db/schema/vendorData" -import { eq, and } from "drizzle-orm" - -/** - * 모든 태그 가져오기 (클라이언트 렌더링용) - */ -export async function getAllTagsPlant( - projectCode: string, - packageCode: string -) { - try { - const tags = await db - .select() - .from(tagsPlant) - .where( - and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode) - ) - ) - .orderBy(tagsPlant.createdAt) - - return tags - } catch (error) { - console.error("Error fetching all tags:", error) - return [] - } -} - -/** - * 고유 속성 키 추출 - */ -export async function getUniqueAttributeKeys( - projectCode: string, - packageCode: string -): Promise<string[]> { - try { - const result = await db - .select({ - attributes: tagsPlant.attributes - }) - .from(tagsPlant) - .where( - and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.packageCode, packageCode) - ) - ) - - const allKeys = new Set<string>() - - for (const row of result) { - if (row.attributes && typeof row.attributes === 'object') { - Object.keys(row.attributes).forEach(key => allKeys.add(key)) - } - } - - return Array.from(allKeys).sort() - } catch (error) { - console.error("Error getting unique attribute keys:", error) - return [] - } -}
\ No newline at end of file diff --git a/lib/tags-plant/repository.ts b/lib/tags-plant/repository.ts index bbe36f66..b5d48335 100644 --- a/lib/tags-plant/repository.ts +++ b/lib/tags-plant/repository.ts @@ -1,5 +1,5 @@ import db from "@/db/db"; -import { NewTag, tags, tagsPlant } from "@/db/schema/vendorData"; +import { NewTag, tags } from "@/db/schema/vendorData"; import { eq, inArray, @@ -69,43 +69,3 @@ export async function deleteTagsByIds( ) { return tx.delete(tags).where(inArray(tags.id, ids)); } - - -export async function selectTagsPlant( - tx: PgTransaction<any, any, any>, - params: { - where?: any; // drizzle-orm의 조건식 (and, eq...) 등 - orderBy?: (ReturnType<typeof asc> | ReturnType<typeof desc>)[]; - offset?: number; - limit?: number; - } -) { - const { where, orderBy, offset = 0, limit = 10 } = params; - - return tx - .select() - .from(tagsPlant) - .where(where) - .orderBy(...(orderBy ?? [])) - .offset(offset) - .limit(limit); -} -/** 총 개수 count */ -export async function countTagsPlant( - tx: PgTransaction<any, any, any>, - where?: any -) { - const res = await tx.select({ count: count() }).from(tagsPlant).where(where); - return res[0]?.count ?? 0; -} - -export async function insertTagPlant( - tx: PgTransaction<any, any, any>, - data: NewTag // DB와 동일한 insert 가능한 타입 - ) { - // returning() 사용 시 배열로 돌아오므로 [0]만 리턴 - return tx - .insert(tagsPlant) - .values(data) - .returning({ id: tagsPlant.id, createdAt: tagsPlant.createdAt }); - }
\ No newline at end of file diff --git a/lib/tags-plant/service.ts b/lib/tags-plant/service.ts index 02bd33be..778ab89d 100644 --- a/lib/tags-plant/service.ts +++ b/lib/tags-plant/service.ts @@ -1,14 +1,14 @@ "use server" import db from "@/db/db" -import { formEntries, forms,items,formsPlant, tagClasses, tags, tagsPlant, tagSubfieldOptions, tagSubfields, tagTypes,formEntriesPlant } from "@/db/schema" +import { formEntries, forms, tagClasses, tags, tagSubfieldOptions, tagSubfields, tagTypes } from "@/db/schema/vendorData" // import { eq } from "drizzle-orm" import { createTagSchema, GetTagsSchema, updateTagSchema, UpdateTagSchema, type CreateTagSchema } from "./validations" import { revalidateTag, unstable_noStore } from "next/cache"; import { filterColumns } from "@/lib/filter-columns"; import { unstable_cache } from "@/lib/unstable-cache"; import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq, sql, ne, count, isNull } from "drizzle-orm"; -import { countTags, insertTag, selectTags, selectTagsPlant, countTagsPlant,insertTagPlant } from "./repository"; +import { countTags, insertTag, selectTags } from "./repository"; import { getErrorMessage } from "../handle-error"; import { getFormMappingsByTagType } from './form-mapping-service'; import { contractItems, contracts } from "@/db/schema/contract"; @@ -32,8 +32,7 @@ function generateTagIdx(): string { return randomBytes(12).toString('hex'); // 12바이트 = 24자리 16진수 } - -export async function getTagsPlant(input: GetTagsSchema, projectCode: string,packageCode: string ) { +export async function getTags(input: GetTagsSchema, packagesId: number) { // return unstable_cache( // async () => { @@ -42,7 +41,7 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac // (1) advancedWhere const advancedWhere = filterColumns({ - table: tagsPlant, + table: tags, filters: input.filters, joinOperator: input.joinOperator, }); @@ -52,31 +51,31 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac if (input.search) { const s = `%${input.search}%`; globalWhere = or( - ilike(tagsPlant.tagNo, s), - ilike(tagsPlant.tagType, s), - ilike(tagsPlant.description, s) + ilike(tags.tagNo, s), + ilike(tags.tagType, s), + ilike(tags.description, s) ); } - // (4) 최종 projectCode - const finalWhere = and(advancedWhere, globalWhere, eq(tagsPlant.projectCode, projectCode), eq(tagsPlant.packageCode, packageCode)); + // (4) 최종 where + const finalWhere = and(advancedWhere, globalWhere, eq(tags.contractItemId, packagesId)); // (5) 정렬 const orderBy = input.sort.length > 0 ? input.sort.map((item) => - item.desc ? desc(tagsPlant[item.id]) : asc(tagsPlant[item.id]) + item.desc ? desc(tags[item.id]) : asc(tags[item.id]) ) - : [asc(tagsPlant.createdAt)]; + : [asc(tags.createdAt)]; // 트랜잭션 내부에서 Repository 호출 const { data, total } = await db.transaction(async (tx) => { - const data = await selectTagsPlant(tx, { + const data = await selectTags(tx, { where: finalWhere, orderBy, offset, limit: input.perPage, }); - const total = await countTagsPlant(tx, finalWhere); + const total = await countTags(tx, finalWhere); return { data, total }; @@ -102,10 +101,9 @@ export async function getTagsPlant(input: GetTagsSchema, projectCode: string,pac export async function createTag( formData: CreateTagSchema, - projectCode: string, - packageCode: string, + selectedPackageId: number | null ) { - if (!projectCode) { + if (!selectedPackageId) { return { error: "No selectedPackageId provided" } } @@ -121,23 +119,33 @@ export async function createTag( try { // 하나의 트랜잭션에서 모든 작업 수행 return await db.transaction(async (tx) => { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 1) 선택된 contractItem의 contractId 가져오기 + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1) - const projectId = project.id + if (contractItemResult.length === 0) { + return { error: "Contract item not found" } + } + + const contractId = contractItemResult[0].contractId + const projectId = contractItemResult[0].projectId // 2) 해당 계약 내에서 같은 tagNo를 가진 태그가 있는지 확인 const duplicateCheck = await tx .select({ count: sql<number>`count(*)` }) - .from(tagsPlant) + .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where( and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.tagNo, validated.data.tagNo) + eq(contractItems.contractId, contractId), + eq(tags.tagNo, validated.data.tagNo) ) ) @@ -174,16 +182,16 @@ export async function createTag( const createdOrExistingForms: CreatedOrExistingForm[] = [] if (formMappings && formMappings.length > 0) { + console.log(selectedPackageId, formMappings) for (const formMapping of formMappings) { // 4-1) 이미 존재하는 폼인지 확인 const existingForm = await tx - .select({ id: formsPlant.id, im: formsPlant.im, eng: formsPlant.eng }) // eng 필드도 추가로 조회 - .from(formsPlant) + .select({ id: forms.id, im: forms.im, eng: forms.eng }) // eng 필드도 추가로 조회 + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1) @@ -211,9 +219,9 @@ export async function createTag( if (shouldUpdate) { await tx - .update(formsPlant) + .update(forms) .set(updateValues) - .where(eq(formsPlant.id, formId)) + .where(eq(forms.id, formId)) console.log(`Form ${formId} updated with:`, updateValues) } @@ -227,8 +235,7 @@ export async function createTag( } else { // 존재하지 않으면 새로 생성 const insertValues: any = { - projectCode: projectCode, - packageCode: packageCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, im: true, @@ -240,9 +247,9 @@ export async function createTag( } const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values(insertValues) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }) + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }) console.log("insertResult:", insertResult) formId = insertResult[0].id @@ -266,9 +273,8 @@ export async function createTag( console.log(`[CREATE TAG] Generated tagIdx: ${generatedTagIdx}`); // 5) 새 Tag 생성 (tagIdx 추가) - const [newTag] = await insertTagPlant(tx, { - packageCode:packageCode, - projectCode:projectCode, + const [newTag] = await insertTag(tx, { + contractItemId: selectedPackageId, formId: primaryFormId, tagIdx: generatedTagIdx, // 🆕 생성된 16진수 24자리 추가 tagNo: validated.data.tagNo, @@ -277,6 +283,7 @@ export async function createTag( description: validated.data.description ?? null, }) + console.log(`tags-${selectedPackageId}`, "create", newTag) // 6) 생성된 각 form에 대해 formEntries에 데이터 추가 (TAG_IDX 포함) for (const form of createdOrExistingForms) { @@ -284,9 +291,8 @@ export async function createTag( // 기존 formEntry 가져오기 const existingEntry = await tx.query.formEntries.findFirst({ where: and( - eq(formEntriesPlant.formCode, form.formCode), - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode) + eq(formEntries.formCode, form.formCode), + eq(formEntries.contractItemId, selectedPackageId) ) }); @@ -323,12 +329,12 @@ export async function createTag( const updatedData = [...existingData, newTagData]; await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date() }) - .where(eq(formEntriesPlant.id, existingEntry.id)); + .where(eq(formEntries.id, existingEntry.id)); console.log(`[CREATE TAG] Added tag ${validated.data.tagNo} with tagIdx ${generatedTagIdx} to existing formEntry for form ${form.formCode}`); } else { @@ -336,10 +342,9 @@ export async function createTag( } } else { // formEntry가 없는 경우 새로 생성 (TAG_IDX 포함) - await tx.insert(formEntriesPlant).values({ + await tx.insert(formEntries).values({ formCode: form.formCode, - projectCode: projectCode, - packageCode: packageCode, + contractItemId: selectedPackageId, data: [newTagData], createdAt: new Date(), updatedAt: new Date(), @@ -353,6 +358,16 @@ export async function createTag( } } + // 7) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시) + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}-ENG`) + revalidateTag("tags") + + // 생성된 각 form의 캐시도 무효화 + createdOrExistingForms.forEach(form => { + revalidateTag(`form-data-${form.formCode}-${selectedPackageId}`) + }) + // 8) 성공 시 반환 (tagIdx 추가) return { success: true, @@ -651,11 +666,10 @@ export async function createTagInForm( export async function updateTag( formData: UpdateTagSchema & { id: number }, - projectCode: string, - packageCode: string, + selectedPackageId: number | null ) { - if (!projectCode) { - return { error: "No projectCode provided" } + if (!selectedPackageId) { + return { error: "No selectedPackageId provided" } } if (!formData.id) { @@ -687,25 +701,35 @@ export async function updateTag( const originalTag = existingTag[0] - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 2) 선택된 contractItem의 contractId 가져오기 + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1) + + if (contractItemResult.length === 0) { + return { error: "Contract item not found" } + } - const projectId = project.id + const contractId = contractItemResult[0].contractId + const projectId = contractItemResult[0].projectId // 3) 태그 번호가 변경되었고, 해당 계약 내에서 같은 tagNo를 가진 다른 태그가 있는지 확인 if (originalTag.tagNo !== validated.data.tagNo) { const duplicateCheck = await tx .select({ count: sql<number>`count(*)` }) - .from(tagsPlant) + .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where( and( - eq(tagsPlant.projectCode, projectCode), - eq(tagsPlant.tagNo, validated.data.tagNo), - ne(tagsPlant.id, formData.id) // 자기 자신은 제외 + eq(contractItems.contractId, contractId), + eq(tags.tagNo, validated.data.tagNo), + ne(tags.id, formData.id) // 자기 자신은 제외 ) ) @@ -750,12 +774,11 @@ export async function updateTag( // 이미 존재하는 폼인지 확인 const existingForm = await tx .select({ id: forms.id }) - .from(formsPlant) + .from(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1) @@ -773,14 +796,13 @@ export async function updateTag( } else { // 존재하지 않으면 새로 생성 const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values({ - projectCode, - packageCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, }) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }) + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }) formId = insertResult[0].id createdOrExistingForms.push({ @@ -801,10 +823,9 @@ export async function updateTag( // 5) 태그 업데이트 const [updatedTag] = await tx - .update(tagsPlant) + .update(tags) .set({ - projectCode, - packageCode, + contractItemId: selectedPackageId, formId: primaryFormId, tagNo: validated.data.tagNo, class: validated.data.class, @@ -812,9 +833,12 @@ export async function updateTag( description: validated.data.description ?? null, updatedAt: new Date(), }) - .where(eq(tagsPlant.id, formData.id)) + .where(eq(tags.id, formData.id)) .returning() + // 6) 캐시 무효화 (React 서버 액션에서 캐싱 사용 시) + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}`) revalidateTag("tags") // 7) 성공 시 반환 @@ -843,8 +867,7 @@ export interface TagInputData { // 새로운 서버 액션 export async function bulkCreateTags( tagsfromExcel: TagInputData[], - projectCode: string, - packageCode: string + selectedPackageId: number ) { unstable_noStore(); @@ -856,22 +879,31 @@ export async function bulkCreateTags( // 단일 트랜잭션으로 모든 작업 처리 return await db.transaction(async (tx) => { // 1. 컨트랙트 ID 및 프로젝트 ID 조회 (한 번만) - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + const contractItemResult = await tx + .select({ + contractId: contractItems.contractId, + projectId: contracts.projectId // projectId 추가 + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) // contracts 테이블 조인 + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + + if (contractItemResult.length === 0) { + return { error: "Contract item not found" }; + } - const projectId = project.id + const contractId = contractItemResult[0].contractId; + const projectId = contractItemResult[0].projectId; // projectId 추출 // 2. 모든 태그 번호 중복 검사 (한 번에) const tagNos = tagsfromExcel.map(tag => tag.tagNo); const duplicateCheck = await tx .select({ tagNo: tags.tagNo }) .from(tags) + .innerJoin(contractItems, eq(tags.contractItemId, contractItems.id)) .where(and( - eq(tags.projectCode, projectCode), + eq(contractItems.contractId, contractId), inArray(tags.tagNo, tagNos) )); @@ -937,13 +969,12 @@ export async function bulkCreateTags( for (const formMapping of formMappings) { // 해당 폼이 이미 존재하는지 확인 const existingForm = await tx - .select({ id: formsPlant.id, im: formsPlant.im }) - .from(formsPlant) + .select({ id: forms.id, im: forms.im }) + .from(forms) .where( and( - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) .limit(1); @@ -956,9 +987,9 @@ export async function bulkCreateTags( // im 필드 업데이트 (필요한 경우) if (existingForm[0].im !== true) { await tx - .update(formsPlant) + .update(forms) .set({ im: true }) - .where(eq(formsPlant.id, formId)); + .where(eq(forms.id, formId)); } createdOrExistingForms.push({ @@ -970,15 +1001,14 @@ export async function bulkCreateTags( } else { // 존재하지 않으면 새로 생성 const insertResult = await tx - .insert(formsPlant) + .insert(forms) .values({ - packageCode:packageCode, - projectCode:projectCode, + contractItemId: selectedPackageId, formCode: formMapping.formCode, formName: formMapping.formName, im: true }) - .returning({ id: formsPlant.id, formCode: formsPlant.formCode, formName: formsPlant.formName }); + .returning({ id: forms.id, formCode: forms.formCode, formName: forms.formName }); formId = insertResult[0].id; createdOrExistingForms.push({ @@ -1018,9 +1048,8 @@ export async function bulkCreateTags( } // 태그 생성 - const [newTag] = await insertTagPlant(tx, { - packageCode:packageCode, - projectCode:projectCode, + const [newTag] = await insertTag(tx, { + contractItemId: selectedPackageId, formId: primaryFormId, tagNo: tagData.tagNo, class: tagData.class || "", @@ -1038,15 +1067,14 @@ export async function bulkCreateTags( }); } - // 4. formEntriesPlant 업데이트 처리 + // 4. formEntries 업데이트 처리 for (const [formCode, newTagsData] of tagsByFormCode.entries()) { try { // 기존 formEntry 가져오기 - const existingEntry = await tx.query.formEntriesPlant.findFirst({ + const existingEntry = await tx.query.formEntries.findFirst({ where: and( - eq(formEntriesPlant.formCode, formCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.projectCode, projectCode) + eq(formEntries.formCode, formCode), + eq(formEntries.contractItemId, selectedPackageId) ) }); @@ -1075,12 +1103,12 @@ export async function bulkCreateTags( const updatedData = [...existingData, ...newUniqueTagsData]; await tx - .update(formEntriesPlant) + .update(formEntries) .set({ data: updatedData, updatedAt: new Date() }) - .where(eq(formEntriesPlant.id, existingEntry.id)); + .where(eq(formEntries.id, existingEntry.id)); console.log(`[BULK CREATE] Added ${newUniqueTagsData.length} tags to existing formEntry for form ${formCode}`); } else { @@ -1088,10 +1116,9 @@ export async function bulkCreateTags( } } else { // formEntry가 없는 경우 새로 생성 - await tx.insert(formEntriesPlant).values({ + await tx.insert(formEntries).values({ formCode: formCode, - projectCode:projectCode, - packageCode:packageCode, + contractItemId: selectedPackageId, data: newTagsData, createdAt: new Date(), updatedAt: new Date(), @@ -1105,6 +1132,16 @@ export async function bulkCreateTags( } } + // 5. 캐시 무효화 (한 번만) + revalidateTag(`tags-${selectedPackageId}`); + revalidateTag(`forms-${selectedPackageId}`); + revalidateTag("tags"); + + // 업데이트된 모든 form의 캐시도 무효화 + for (const formCode of tagsByFormCode.keys()) { + revalidateTag(`form-data-${formCode}-${selectedPackageId}`); + } + return { success: true, data: { @@ -1123,8 +1160,7 @@ export async function bulkCreateTags( /** 복수 삭제 */ interface RemoveTagsInput { ids: number[]; - projectCode: string; - packageCode: string; + selectedPackageId: number; } @@ -1142,29 +1178,36 @@ function removeTagFromDataJson( export async function removeTags(input: RemoveTagsInput) { unstable_noStore() // React 서버 액션 무상태 함수 - const { ids, projectCode, packageCode } = input + const { ids, selectedPackageId } = input try { await db.transaction(async (tx) => { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); - const projectId = project.id; + const packageInfo = await tx + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); + } + + const projectId = packageInfo[0].projectId; // 1) 삭제 대상 tag들을 미리 조회 const tagsToDelete = await tx .select({ - id: tagsPlant.id, - tagNo: tagsPlant.tagNo, - tagType: tagsPlant.tagType, - class: tagsPlant.class, + id: tags.id, + tagNo: tags.tagNo, + tagType: tags.tagType, + class: tags.class, }) - .from(tagsPlant) - .where(inArray(tagsPlant.id, ids)) + .from(tags) + .where(inArray(tags.id, ids)) // 2) 태그 타입과 클래스의 고유 조합 추출 const uniqueTypeClassCombinations = [...new Set( @@ -1179,14 +1222,13 @@ export async function removeTags(input: RemoveTagsInput) { // 3-1) 삭제 중인 태그들 외에, 동일한 태그 타입/클래스를 가진 다른 태그가 있는지 확인 const otherTagsWithSameTypeClass = await tx .select({ count: count() }) - .from(tagsPlant) + .from(tags) .where( and( - eq(tagsPlant.tagType, tagType), - classValue ? eq(tagsPlant.class, classValue) : isNull(tagsPlant.class), - not(inArray(tagsPlant.id, ids)), - eq(tags.packageCode, packageCode), - eq(tags.projectCode, projectCode) // 같은 contractItemId 내에서만 확인 + eq(tags.tagType, tagType), + classValue ? eq(tags.class, classValue) : isNull(tags.class), + not(inArray(tags.id, ids)), // 현재 삭제 중인 태그들은 제외 + eq(tags.contractItemId, selectedPackageId) // 같은 contractItemId 내에서만 확인 ) ) @@ -1207,23 +1249,21 @@ export async function removeTags(input: RemoveTagsInput) { if (otherTagsWithSameTypeClass[0].count === 0) { // 폼 삭제 await tx - .delete(formsPlant) + .delete(forms) .where( and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.formCode, formMapping.formCode) + eq(forms.contractItemId, selectedPackageId), + eq(forms.formCode, formMapping.formCode) ) ) // formEntries 테이블에서도 해당 formCode 관련 데이터 삭제 await tx - .delete(formEntriesPlant) + .delete(formEntries) .where( and( - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.formCode, formMapping.formCode) + eq(formEntries.contractItemId, selectedPackageId), + eq(formEntries.formCode, formMapping.formCode) ) ) } @@ -1231,15 +1271,14 @@ export async function removeTags(input: RemoveTagsInput) { else if (relevantTagNos.length > 0) { const formEntryRecords = await tx .select({ - id: formEntriesPlant.id, - data: formEntriesPlant.data, + id: formEntries.id, + data: formEntries.data, }) - .from(formEntriesPlant) + .from(formEntries) .where( and( - eq(formEntriesPlant.projectCode, projectCode), - eq(formEntriesPlant.packageCode, packageCode), - eq(formEntriesPlant.formCode, formMapping.formCode) + eq(formEntries.contractItemId, selectedPackageId), + eq(formEntries.formCode, formMapping.formCode) ) ) @@ -1266,6 +1305,9 @@ export async function removeTags(input: RemoveTagsInput) { await tx.delete(tags).where(inArray(tags.id, ids)) }) + // 5) 캐시 무효화 + revalidateTag(`tags-${selectedPackageId}`) + revalidateTag(`forms-${selectedPackageId}`) return { data: null, error: null } } catch (err) { @@ -1286,26 +1328,25 @@ export interface ClassOption { * Class 옵션 목록을 가져오는 함수 * 이제 각 클래스는 연결된 tagTypeCode와 tagTypeDescription을 포함 */ -export async function getClassOptions( - packageCode: string, - projectCode: string -): Promise<UpdatedClassOption[]> { +export async function getClassOptions(selectedPackageId: number): Promise<UpdatedClassOption[]> { try { - // 1. 프로젝트 정보 조회 - const projectInfo = await db - .select() - .from(projects) - .where(eq(projects.code, projectCode)) + // 1. 먼저 contractItems에서 projectId 조회 + const packageInfo = await db + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) .limit(1); - if (projectInfo.length === 0) { - throw new Error(`Project with code ${projectCode} not found`); + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); } - const projectId = projectInfo[0].id; - + const projectId = packageInfo[0].projectId; - // 3. 태그 클래스들을 서브클래스 정보와 함께 조회 + // 2. 태그 클래스들을 서브클래스 정보와 함께 조회 const tagClassesWithSubclasses = await db .select({ id: tagClasses.id, @@ -1319,8 +1360,8 @@ export async function getClassOptions( .where(eq(tagClasses.projectId, projectId)) .orderBy(tagClasses.code); - // 4. 태그 타입 정보도 함께 조회 (description을 위해) - const tagTypesMap = new Map<string, string>(); + // 3. 태그 타입 정보도 함께 조회 (description을 위해) + const tagTypesMap = new Map(); const tagTypesList = await db .select({ code: tagTypes.code, @@ -1329,24 +1370,21 @@ export async function getClassOptions( .from(tagTypes) .where(eq(tagTypes.projectId, projectId)); - tagTypesList.forEach((tagType) => { + tagTypesList.forEach(tagType => { tagTypesMap.set(tagType.code, tagType.description); }); - // 5. 클래스 옵션으로 변환 - const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map( - (cls) => ({ - value: cls.code, - label: cls.label, - code: cls.code, - description: cls.label, - tagTypeCode: cls.tagTypeCode, - tagTypeDescription: - tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode, - subclasses: cls.subclasses || [], - subclassRemark: cls.subclassRemark || {}, - }) - ); + // 4. 클래스 옵션으로 변환 + const classOptions: UpdatedClassOption[] = tagClassesWithSubclasses.map(cls => ({ + value: cls.code, + label: cls.label, + code: cls.code, + description: cls.label, + tagTypeCode: cls.tagTypeCode, + tagTypeDescription: tagTypesMap.get(cls.tagTypeCode) || cls.tagTypeCode, + subclasses: cls.subclasses || [], + subclassRemark: cls.subclassRemark || {}, + })); return classOptions; } catch (error) { @@ -1354,8 +1392,6 @@ export async function getClassOptions( throw new Error("Failed to fetch class options"); } } - - interface SubFieldDef { name: string label: string @@ -1367,20 +1403,26 @@ interface SubFieldDef { export async function getSubfieldsByTagType( tagTypeCode: string, - projectCode: string, + selectedPackageId: number, subclassRemark: string = "", subclass: string = "", ) { try { - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), - columns: { - id: true - } - }); + // 1. 먼저 contractItems에서 projectId 조회 + const packageInfo = await db + .select({ + projectId: contracts.projectId + }) + .from(contractItems) + .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) + .where(eq(contractItems.id, selectedPackageId)) + .limit(1); + if (packageInfo.length === 0) { + throw new Error(`Contract item with ID ${selectedPackageId} not found`); + } - const projectId = project.id + const projectId = packageInfo[0].projectId; // 2. 올바른 projectId를 사용하여 tagSubfields 조회 const rows = await db @@ -1581,314 +1623,29 @@ export interface TagTypeOption { label: string; // tagTypes.description 값 } -export async function getProjectIdFromContractItemId( - projectCode: string -): Promise<number | null> { +export async function getProjectIdFromContractItemId(contractItemId: number): Promise<number | null> { try { // First get the contractId from contractItems - const project = await db.query.projects.findFirst({ - where: eq(projects.code, projectCode), + const contractItem = await db.query.contractItems.findFirst({ + where: eq(contractItems.id, contractItemId), columns: { - id: true + contractId: true } }); - if (!project) return null; - - return project?.id || null; - } catch (error) { - console.error("Error fetching projectId:", error); - return null; - } -} - -const SEDP_API_BASE_URL = process.env.SEDP_API_BASE_URL || 'http://sedpwebapi.ship.samsung.co.kr/api'; - + if (!contractItem) return null; -/** - * Engineering 폼 목록 가져오기 - */ -export async function getEngineeringForms( - projectCode: string, - packageCode: string -): Promise<FormInfo[]> { - try { - // 1. DB에서 eng=true인 폼 조회 - const existingForms = await db - .select({ - formCode: formsPlant.formCode, - formName: formsPlant.formName, - }) - .from(formsPlant) - .where( - and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.eng, true) - ) - ) - - // DB에 데이터가 있으면 반환 - if (existingForms.length > 0) { - return existingForms - } - - // 2. DB에 없으면 SEDP API에서 가져오기 - const apiKey = await getSEDPToken() - - // 2-1. GetByToolID로 레지스터 매핑 정보 가져오기 - const mappingResponse = 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 (!mappingResponse.ok) { - throw new Error( - `레지스터 매핑 요청 실패: ${mappingResponse.status} ${mappingResponse.statusText}` - ) - } - - const mappingData = await mappingResponse.json() - const registers: NewRegister[] = Array.isArray(mappingData) - ? mappingData - : [mappingData] - - // 2-2. packageCode가 SCOPES에 포함된 레지스터 필터링 - const matchingRegisters = registers.filter(register => - register.SCOPES.includes(packageCode) - ) - - if (matchingRegisters.length === 0) { - console.log(`패키지 ${packageCode}에 해당하는 레지스터가 없습니다.`) - return [] - } - - // 2-3. 각 레지스터의 상세 정보 가져오기 - const formInfos: FormInfo[] = [] - const formsToInsert: typeof formsPlant.$inferInsert[] = [] - - for (const register of matchingRegisters) { - try { - const detailResponse = 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: register.REG_TYPE_ID, - ContainDeleted: false - }) - } - ) - - if (!detailResponse.ok) { - console.error( - `레지스터 상세 정보 요청 실패 (${register.REG_TYPE_ID}): ${detailResponse.status}` - ) - continue - } - - const detail: RegisterDetail = await detailResponse.json() - - // DELETED가 true이거나 DESC가 없으면 스킵 - if (detail.DELETED || !detail.DESC) { - continue - } - - formInfos.push({ - formCode: detail.TYPE_ID, - formName: detail.DESC - }) - - // DB 삽입용 데이터 준비 - formsToInsert.push({ - projectCode: projectCode, - packageCode: packageCode, - formCode: detail.TYPE_ID, - formName: detail.DESC, - eng: true, - im: false - }) - } catch (error) { - console.error( - `레지스터 ${register.REG_TYPE_ID} 상세 정보 가져오기 실패:`, - error - ) - continue - } - } - - // 2-4. DB에 저장 - if (formsToInsert.length > 0) { - await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing() - console.log(`${formsToInsert.length}개의 Engineering 폼을 DB에 저장했습니다.`) - } - - return formInfos - } catch (error) { - console.error("Engineering 폼 가져오기 실패:", error) - throw new Error("Failed to fetch engineering forms") - } -} - -/** - * IM 폼 목록 가져오기 - */ -export async function getIMForms( - projectCode: string, - packageCode: string -): Promise<FormInfo[]> { - try { - // 1. DB에서 im=true인 폼 조회 - const existingForms = await db - .select({ - formCode: formsPlant.formCode, - formName: formsPlant.formName, - }) - .from(formsPlant) - .where( - and( - eq(formsPlant.projectCode, projectCode), - eq(formsPlant.packageCode, packageCode), - eq(formsPlant.im, true) - ) - ) - - // DB에 데이터가 있으면 반환 - if (existingForms.length > 0) { - return existingForms - } - - // 2. DB에 없으면 SEDP API에서 가져오기 - const apiKey = await getSEDPToken() - - // 2-1. GetByToolID로 레지스터 매핑 정보 가져오기 - const mappingResponse = 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 (!mappingResponse.ok) { - throw new Error( - `레지스터 매핑 요청 실패: ${mappingResponse.status} ${mappingResponse.statusText}` - ) - } - - const mappingData = await mappingResponse.json() - const registers: NewRegister[] = Array.isArray(mappingData) - ? mappingData - : [mappingData] - - // 2-2. packageCode가 SCOPES에 포함된 레지스터 필터링 - const matchingRegisters = registers.filter(register => - register.SCOPES.includes(packageCode) - ) - - if (matchingRegisters.length === 0) { - console.log(`패키지 ${packageCode}에 해당하는 레지스터가 없습니다.`) - return [] - } - - // 2-3. 각 레지스터의 상세 정보 가져오기 - const formInfos: FormInfo[] = [] - const formsToInsert: typeof formsPlant.$inferInsert[] = [] - - for (const register of matchingRegisters) { - try { - const detailResponse = 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: register.REG_TYPE_ID, - ContainDeleted: false - }) - } - ) - - if (!detailResponse.ok) { - console.error( - `레지스터 상세 정보 요청 실패 (${register.REG_TYPE_ID}): ${detailResponse.status}` - ) - continue - } - - const detail: RegisterDetail = await detailResponse.json() - - // DELETED가 true이거나 DESC가 없으면 스킵 - if (detail.DELETED || !detail.DESC) { - continue - } - - formInfos.push({ - formCode: detail.TYPE_ID, - formName: detail.DESC - }) - - // DB 삽입용 데이터 준비 - formsToInsert.push({ - projectCode: projectCode, - packageCode: packageCode, - formCode: detail.TYPE_ID, - formName: detail.DESC, - eng: false, - im: true - }) - } catch (error) { - console.error( - `레지스터 ${register.REG_TYPE_ID} 상세 정보 가져오기 실패:`, - error - ) - continue + // Then get the projectId from contracts + const contract = await db.query.contracts.findFirst({ + where: eq(contracts.id, contractItem.contractId), + columns: { + projectId: true } - } - - // 2-4. DB에 저장 - if (formsToInsert.length > 0) { - await db.insert(formsPlant).values(formsToInsert).onConflictDoNothing() - console.log(`${formsToInsert.length}개의 IM 폼을 DB에 저장했습니다.`) - } + }); - return formInfos + return contract?.projectId || null; } catch (error) { - console.error("IM 폼 가져오기 실패:", error) - throw new Error("Failed to fetch IM forms") + console.error("Error fetching projectId:", error); + return null; } }
\ No newline at end of file diff --git a/lib/tags-plant/table/add-tag-dialog.tsx b/lib/tags-plant/table/add-tag-dialog.tsx index 41731f63..9c82bf1a 100644 --- a/lib/tags-plant/table/add-tag-dialog.tsx +++ b/lib/tags-plant/table/add-tag-dialog.tsx @@ -61,7 +61,7 @@ import { getClassOptions, type ClassOption, TagTypeOption, -} from "@/lib/tags-plant/service" +} from "@/lib/tags/service" import { ScrollArea } from "@/components/ui/scroll-area" // Updated to support multiple rows and subclass @@ -98,11 +98,10 @@ interface UpdatedClassOption extends ClassOption { } interface AddTagDialogProps { - projectCode: string - packageCode: string + selectedPackageId: number } -export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { +export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { const router = useRouter() const params = useParams() const lng = (params?.lng as string) || "ko" @@ -126,6 +125,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { const fieldIdsRef = React.useRef<Record<string, string>>({}) const classOptionIdsRef = React.useRef<Record<string, string>>({}) + console.log(selectedPackageId, "tag") // --------------- // Load Class Options (서브클래스 정보 포함) @@ -135,7 +135,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { setIsLoadingClasses(true) try { // getClassOptions 함수가 서브클래스 정보도 포함하도록 수정되었다고 가정 - const result = await getClassOptions(packageCode, projectCode) + const result = await getClassOptions(selectedPackageId) setClassOptions(result) } catch (err) { toast.error(t("toast.classOptionsLoadFailed")) @@ -147,7 +147,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { if (open) { loadClassOptions() } - }, [open, projectCode, packageCode]) + }, [open, selectedPackageId]) // --------------- // react-hook-form with fieldArray support for multiple rows @@ -176,7 +176,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { setIsLoadingSubFields(true) try { // 수정된 getSubfieldsByTagType 함수 호출 (subclassRemark 파라미터 추가) - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, subclassRemark, subclass) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId, subclassRemark, subclass) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, @@ -313,7 +313,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { // Submit handler for multiple tags (서브클래스 정보 포함) // --------------- async function onSubmit(data: MultiTagFormValues) { - if (!projectCode) { + if (!selectedPackageId) { toast.error(t("toast.noSelectedPackageId")); return; } @@ -343,7 +343,7 @@ export function AddTagDialog({ projectCode, packageCode }: AddTagDialogProps) { }; try { - const res = await createTag(tagData, projectCode, packageCode); + const res = await createTag(tagData, selectedPackageId); if ("error" in res) { console.log(res.error) failedTags.push({ tag: row.tagNo, error: res.error }); diff --git a/lib/tags-plant/table/delete-tags-dialog.tsx b/lib/tags-plant/table/delete-tags-dialog.tsx index 69a4f4a6..6a024cda 100644 --- a/lib/tags-plant/table/delete-tags-dialog.tsx +++ b/lib/tags-plant/table/delete-tags-dialog.tsx @@ -4,6 +4,7 @@ import * as React from "react" import { type Row } from "@tanstack/react-table" import { Loader, Trash } from "lucide-react" import { toast } from "sonner" + import { useMediaQuery } from "@/hooks/use-media-query" import { Button } from "@/components/ui/button" import { @@ -26,15 +27,15 @@ import { DrawerTitle, DrawerTrigger, } from "@/components/ui/drawer" -import { removeTags } from "@/lib//tags-plant/service" + +import { removeTags } from "@/lib//tags/service" import { Tag } from "@/db/schema/vendorData" interface DeleteTasksDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> { tags: Row<Tag>["original"][] showTrigger?: boolean - projectCode: string - packageCode: string + selectedPackageId: number onSuccess?: () => void } @@ -42,8 +43,7 @@ export function DeleteTagsDialog({ tags, showTrigger = true, onSuccess, - projectCode, - packageCode, + selectedPackageId, ...props }: DeleteTasksDialogProps) { const [isDeletePending, startDeleteTransition] = React.useTransition() @@ -52,7 +52,7 @@ export function DeleteTagsDialog({ function onDelete() { startDeleteTransition(async () => { const { error } = await removeTags({ - ids: tags.map((tag) => tag.id),projectCode, packageCode + ids: tags.map((tag) => tag.id),selectedPackageId }) if (error) { diff --git a/lib/tags-plant/table/tag-table.tsx b/lib/tags-plant/table/tag-table.tsx index 2fdcd5fc..1986d933 100644 --- a/lib/tags-plant/table/tag-table.tsx +++ b/lib/tags-plant/table/tag-table.tsx @@ -1,4 +1,3 @@ -// components/vendor-data-plant/tags-table.tsx "use client" import * as React from "react" @@ -7,177 +6,40 @@ import type { DataTableFilterField, DataTableRowAction, } from "@/types/table" -import { useRouter } from "next/navigation" -import { toast } from "sonner" -import { Trash2, Download, Upload, Loader2, RefreshCcw, Plus } from "lucide-react" -import ExcelJS from "exceljs" -import type { Table as TanstackTable } from "@tanstack/react-table" -import { ClientDataTable } from "@/components/client-data-table/data-table" +import { toSentenceCase } from "@/lib/utils" +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" + import { getColumns } from "./tag-table-column" import { Tag } from "@/db/schema/vendorData" import { DeleteTagsDialog } from "./delete-tags-dialog" +import { TagsTableToolbarActions } from "./tags-table-toolbar-actions" +import { TagsTableFloatingBar } from "./tags-table-floating-bar" +import { getTags } from "../service" import { UpdateTagSheet } from "./update-tag-sheet" -import { AddTagDialog } from "./add-tag-dialog" import { useAtomValue } from 'jotai' import { selectedModeAtom } from '@/atoms' -import { Skeleton } from "@/components/ui/skeleton" -import type { ColumnDef } from "@tanstack/react-table" -import { createDynamicAttributeColumns } from "../column-builder.service" -import { getAllTagsPlant, getUniqueAttributeKeys } from "../queries" -import { Button } from "@/components/ui/button" -import { exportTagsToExcel } from "./tags-export" -import { - bulkCreateTags, - getClassOptions, - getProjectIdFromContractItemId, - getSubfieldsByTagType -} from "../service" -import { decryptWithServerAction } from "@/components/drm/drmUtils" +// 여기서 받은 `promises`로부터 태그 목록을 가져와 상태를 세팅 +// 예: "selectedPackageId"는 props로 전달 interface TagsTableProps { - projectCode: string - packageCode: string -} - -// 태그 넘버링 룰 인터페이스 (Import용) -interface TagNumberingRule { - attributesId: string; - attributesDescription: string; - expression: string | null; - delimiter: string | null; - sortOrder: number; -} - -interface ClassOption { - code: string; - label: string; - tagTypeCode: string; - tagTypeDescription: string; -} - -interface SubFieldDef { - name: string; - label: string; - type: "select" | "text"; - options?: { value: string; label: string }[]; - expression?: string; - delimiter?: string; + promises: Promise< [ Awaited<ReturnType<typeof getTags>> ] > + selectedPackageId: number } -export function TagsTable({ - projectCode, - packageCode, -}: TagsTableProps) { - const router = useRouter() +export function TagsTable({ promises, selectedPackageId }: TagsTableProps) { + // 1) 데이터를 가져옴 (server component -> use(...) pattern) + const [{ data, pageCount }] = React.use(promises) const selectedMode = useAtomValue(selectedModeAtom) - // 상태 관리 - const [tableData, setTableData] = React.useState<Tag[]>([]) - const [columns, setColumns] = React.useState<ColumnDef<Tag>[]>([]) - const [isLoading, setIsLoading] = React.useState(true) const [rowAction, setRowAction] = React.useState<DataTableRowAction<Tag> | null>(null) - - // 선택된 행 관리 - const [selectedRowsData, setSelectedRowsData] = React.useState<Tag[]>([]) - const [clearSelection, setClearSelection] = React.useState(false) - - // 다이얼로그 상태 - const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) - const [deleteTarget, setDeleteTarget] = React.useState<Tag[]>([]) - const [addTagDialogOpen, setAddTagDialogOpen] = React.useState(false) - - // Import/Export 상태 - const [isPending, setIsPending] = React.useState(false) - const [isExporting, setIsExporting] = React.useState(false) - const fileInputRef = React.useRef<HTMLInputElement>(null) - - // Sync 상태 - const [isSyncing, setIsSyncing] = React.useState(false) - const [syncId, setSyncId] = React.useState<string | null>(null) - const pollingRef = React.useRef<NodeJS.Timeout | null>(null) - - // Table ref for export - const tableRef = React.useRef<TanstackTable<Tag> | null>(null) - - // Cache for validation - const [classOptions, setClassOptions] = React.useState<ClassOption[]>([]) - const [subfieldCache, setSubfieldCache] = React.useState<Record<string, SubFieldDef[]>>({}) - const [projectId, setProjectId] = React.useState<number | null>(null) - // Load project ID - React.useEffect(() => { - const fetchProjectId = async () => { - if (packageCode && projectCode) { - try { - const pid = await getProjectIdFromContractItemId(projectCode) - setProjectId(pid) - } catch (error) { - console.error("Failed to fetch project ID:", error) - } - } - } - fetchProjectId() - }, [projectCode]) - - // Load class options - React.useEffect(() => { - const loadClassOptions = async () => { - try { - const options = await getClassOptions(packageCode, projectCode) - setClassOptions(options) - } catch (error) { - console.error("Failed to load class options:", error) - } - } - loadClassOptions() - }, [packageCode, projectCode]) - - // 데이터 및 컬럼 로드 - React.useEffect(() => { - async function loadTableData() { - try { - setIsLoading(true) - - const [tagsData, attributeKeys] = await Promise.all([ - getAllTagsPlant(projectCode, packageCode), - getUniqueAttributeKeys(projectCode, packageCode), - ]) - - const baseColumns = getColumns({ - setRowAction, - onDeleteClick: handleDeleteRow - }) - - let dynamicColumns: ColumnDef<Tag>[] = [] - if (attributeKeys.length > 0) { - dynamicColumns = createDynamicAttributeColumns(attributeKeys) - } - - const actionsColumn = baseColumns.pop() - const finalColumns = [ - ...baseColumns, - ...dynamicColumns, - actionsColumn - ].filter(Boolean) as ColumnDef<Tag>[] - - setTableData(tagsData) - setColumns(finalColumns) - } catch (error) { - console.error("Error loading table data:", error) - toast.error("Failed to load table data") - setTableData([]) - setColumns(getColumns({ - setRowAction, - onDeleteClick: handleDeleteRow - })) - } finally { - setIsLoading(false) - } - } - - loadTableData() - }, [projectCode, packageCode]) + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) // Filter fields const filterFields: DataTableFilterField<Tag>[] = [ @@ -205,11 +67,6 @@ export function TagsTable({ type: "text", }, { - id: "class", - label: "Class", - type: "text", - }, - { id: "createdAt", label: "Created at", type: "date", @@ -221,562 +78,78 @@ export function TagsTable({ }, ] - // 선택된 행 개수 - const selectedRowCount = React.useMemo(() => { - return selectedRowsData.length - }, [selectedRowsData]) - - // 개별 행 삭제 - const handleDeleteRow = React.useCallback((rowData: Tag) => { - setDeleteTarget([rowData]) - setDeleteDialogOpen(true) - }, []) - - // 배치 삭제 - const handleBatchDelete = React.useCallback(() => { - if (selectedRowsData.length === 0) { - toast.error("삭제할 항목을 선택해주세요.") - return - } - setDeleteTarget(selectedRowsData) - setDeleteDialogOpen(true) - }, [selectedRowsData]) - - // 삭제 성공 후 처리 - const handleDeleteSuccess = React.useCallback(() => { - const tagNosToDelete = deleteTarget - .map(item => item.tagNo) - .filter(Boolean) - - setTableData(prev => - prev.filter(item => !tagNosToDelete.includes(item.tagNo)) - ) - - setSelectedRowsData([]) - setClearSelection(prev => !prev) - setDeleteTarget([]) - - toast.success("삭제되었습니다.") - }, [deleteTarget]) - - // 클래스 라벨로 태그 타입 코드 찾기 - const getTagTypeCodeByClassLabel = React.useCallback((classLabel: string): string | null => { - const classOption = classOptions.find(opt => opt.label === classLabel) - return classOption?.tagTypeCode || null - }, [classOptions]) - - // 태그 타입에 따른 서브필드 가져오기 - const fetchSubfieldsByTagType = React.useCallback(async (tagTypeCode: string): Promise<SubFieldDef[]> => { - if (subfieldCache[tagTypeCode]) { - return subfieldCache[tagTypeCode] - } - - try { - const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode, "", "") - const formattedSubFields: SubFieldDef[] = subFields.map(field => ({ - name: field.name, - label: field.label, - type: field.type, - options: field.options || [], - expression: field.expression ?? undefined, - delimiter: field.delimiter ?? undefined, - })) - - setSubfieldCache(prev => ({ - ...prev, - [tagTypeCode]: formattedSubFields - })) - - return formattedSubFields - } catch (error) { - console.error(`Error fetching subfields for tagType ${tagTypeCode}:`, error) - return [] - } - }, [subfieldCache, projectCode]) - - // Class 기반 태그 번호 형식 검증 - const validateTagNumberByClass = React.useCallback(async ( - tagNo: string, - classLabel: string - ): Promise<string> => { - if (!tagNo) return "Tag number is empty." - if (!classLabel) return "Class is empty." - - try { - const tagTypeCode = getTagTypeCodeByClassLabel(classLabel) - if (!tagTypeCode) { - return `No tag type found for class '${classLabel}'.` - } - - const subfields = await fetchSubfieldsByTagType(tagTypeCode) - if (!subfields || subfields.length === 0) { - return `No subfields found for tag type code '${tagTypeCode}'.` - } - - let remainingTagNo = tagNo - - for (const field of subfields) { - const delimiter = field.delimiter || "" - let nextDelimiterPos - - if (delimiter && remainingTagNo.includes(delimiter)) { - nextDelimiterPos = remainingTagNo.indexOf(delimiter) - } else { - nextDelimiterPos = remainingTagNo.length - } - - const part = remainingTagNo.substring(0, nextDelimiterPos) - - if (!part) { - return `Empty part for field '${field.label}'.` - } - - if (field.expression) { - try { - let cleanPattern = field.expression.replace(/^\^/, '').replace(/\$$/, '') - const regex = new RegExp(`^${cleanPattern}$`) - - if (!regex.test(part)) { - return `Part '${part}' for field '${field.label}' does not match the pattern '${field.expression}'.` - } - } catch (error) { - console.error(`Invalid regex pattern: ${field.expression}`, error) - return `Invalid pattern for field '${field.label}': ${field.expression}` - } - } - - if (field.type === "select" && field.options && field.options.length > 0) { - const validValues = field.options.map(opt => opt.value) - if (!validValues.includes(part)) { - return `'${part}' is not a valid value for field '${field.label}'. Valid options: ${validValues.join(", ")}.` - } - } - - if (delimiter && nextDelimiterPos < remainingTagNo.length) { - remainingTagNo = remainingTagNo.substring(nextDelimiterPos + delimiter.length) - } else { - remainingTagNo = "" - break - } - } - - if (remainingTagNo) { - return `Tag number has extra parts: '${remainingTagNo}'.` - } - - return "" - } catch (error) { - console.error("Error validating tag number by class:", error) - return "Error validating tag number format." - } - }, [getTagTypeCodeByClassLabel, fetchSubfieldsByTagType]) - - // Import 파일 선택 - const handleImportClick = () => { - fileInputRef.current?.click() - } - - // Import 파일 처리 - const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { - const file = e.target.files?.[0] - if (!file) return - - e.target.value = "" - setIsPending(true) - - try { - const workbook = new ExcelJS.Workbook() - const arrayBuffer = await decryptWithServerAction(file) - await workbook.xlsx.load(arrayBuffer) - - const worksheet = workbook.worksheets[0] - const lastColIndex = worksheet.columnCount + 1 - worksheet.getRow(1).getCell(lastColIndex).value = "Error" - - const headerRowValues = worksheet.getRow(1).values as ExcelJS.CellValue[] - - // Excel header to accessor mapping - const excelHeaderToAccessor: Record<string, string> = {} - for (const col of columns) { - const meta = col.meta as { excelHeader?: string } | undefined - if (meta?.excelHeader) { - const accessor = col.id as string - excelHeaderToAccessor[meta.excelHeader] = accessor - } - } - - const accessorIndexMap: Record<string, number> = {} - for (let i = 1; i < headerRowValues.length; i++) { - const cellVal = String(headerRowValues[i] ?? "").trim() - if (!cellVal) continue - const accessor = excelHeaderToAccessor[cellVal] - if (accessor) { - accessorIndexMap[accessor] = i - } - } - - let errorCount = 0 - const importedRows: Tag[] = [] - const fileTagNos = new Set<string>() - const lastRow = worksheet.lastRow?.number || 1 - - for (let rowNum = 2; rowNum <= lastRow; rowNum++) { - const row = worksheet.getRow(rowNum) - const rowVals = row.values as ExcelJS.CellValue[] - if (!rowVals || rowVals.length <= 1) continue - - let errorMsg = "" - - const tagNoIndex = accessorIndexMap["tagNo"] - const classIndex = accessorIndexMap["class"] - - const tagNo = tagNoIndex ? String(rowVals[tagNoIndex] ?? "").trim() : "" - const classVal = classIndex ? String(rowVals[classIndex] ?? "").trim() : "" - - if (!tagNo) { - errorMsg += `Tag No is empty. ` - } - if (!classVal) { - errorMsg += `Class is empty. ` - } - - if (tagNo) { - const dup = tableData.find(t => t.tagNo === tagNo) - if (dup) { - errorMsg += `TagNo '${tagNo}' already exists. ` - } - - if (fileTagNos.has(tagNo)) { - errorMsg += `TagNo '${tagNo}' is duplicated within this file. ` - } else { - fileTagNos.add(tagNo) - } - } - - if (tagNo && classVal && !errorMsg) { - const classValidationError = await validateTagNumberByClass(tagNo, classVal) - if (classValidationError) { - errorMsg += classValidationError + " " - } - } - - if (errorMsg) { - row.getCell(lastColIndex).value = errorMsg.trim() - errorCount++ - } else { - const finalTagType = getTagTypeCodeByClassLabel(classVal) ?? "" - - importedRows.push({ - id: 0, - packageCode: packageCode, - projectCode: projectCode, - formId: null, - tagNo, - tagType: finalTagType, - class: classVal, - description: String(rowVals[accessorIndexMap["description"] ?? 0] ?? "").trim(), - createdAt: new Date(), - updatedAt: new Date(), - }) - } - } - - if (errorCount > 0) { - const outBuf = await workbook.xlsx.writeBuffer() - const errorFile = new Blob([outBuf]) - const url = URL.createObjectURL(errorFile) - const link = document.createElement("a") - link.href = url - link.download = "tag_import_errors.xlsx" - link.click() - URL.revokeObjectURL(url) - - toast.error(`There are ${errorCount} error row(s). Please see downloaded file.`) - return - } - - if (importedRows.length > 0) { - const result = await bulkCreateTags(importedRows, projectCode, packageCode) - if ("error" in result) { - toast.error(result.error) - } else { - toast.success(`${result.data.createdCount}개의 태그가 성공적으로 생성되었습니다.`) - router.refresh() - } - } - } catch (err) { - console.error(err) - toast.error("파일 업로드 중 오류가 발생했습니다.") - } finally { - setIsPending(false) - } - } - - // Export 함수 - const handleExport = async () => { - if (!tableRef.current) { - toast.error("테이블이 준비되지 않았습니다.") - return - } - - try { - setIsExporting(true) - await exportTagsToExcel(tableRef.current, packageCode, projectCode, { - filename: `Tags_${packageCode}_${projectCode}`, - excludeColumns: ["select", "actions", "createdAt", "updatedAt"], - }) - toast.success("태그 목록이 성공적으로 내보내졌습니다.") - } catch (error) { - console.error("Export error:", error) - toast.error("태그 목록 내보내기 중 오류가 발생했습니다.") - } finally { - setIsExporting(false) - } - } - - // Sync 함수 - const startGetTags = async () => { - try { - setIsSyncing(true) - - const response = await fetch('/api/cron/tags-plant/start', { - method: 'POST', - body: JSON.stringify({ - projectCode: projectCode, - packageCode: packageCode, - mode: selectedMode - }) - }) - - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error || 'Failed to start tag import') - } - - const data = await response.json() - - if (data.syncId) { - setSyncId(data.syncId) - toast.info('Tag import started. This may take a while...') - startPolling(data.syncId) - } else { - throw new Error('No import ID returned from server') - } - } catch (error) { - console.error('Error starting tag import:', error) - toast.error( - error instanceof Error - ? error.message - : 'An error occurred while starting tag import' - ) - setIsSyncing(false) - } - } - - const startPolling = (id: string) => { - if (pollingRef.current) { - clearInterval(pollingRef.current) - } - - pollingRef.current = setInterval(async () => { - try { - const response = await fetch(`/api/cron/tags-plant/status?id=${id}`) - - if (!response.ok) { - throw new Error('Failed to get tag import status') - } - - const data = await response.json() - - if (data.status === 'completed') { - if (pollingRef.current) { - clearInterval(pollingRef.current) - pollingRef.current = null - } - - router.refresh() - setIsSyncing(false) - setSyncId(null) + // 3) useDataTable 훅으로 react-table 구성 + const { table } = useDataTable({ + data: data, // <-- 여기서 tableData 사용 + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + // sorting: [{ id: "createdAt", desc: true }], + columnPinning: { right: ["actions"] }, + }, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + columnResizeMode: "onEnd", - toast.success( - `Tags imported successfully! ${data.result?.processedCount || 0} items processed.` - ) - } else if (data.status === 'failed') { - if (pollingRef.current) { - clearInterval(pollingRef.current) - pollingRef.current = null - } + }) - setIsSyncing(false) - setSyncId(null) - toast.error(data.error || 'Import failed') - } - } catch (error) { - console.error('Error checking importing status:', error) - } - }, 5000) - } + const [isCompact, setIsCompact] = React.useState<boolean>(false) - // rowAction 처리 - React.useEffect(() => { - if (rowAction?.type === "delete") { - handleDeleteRow(rowAction.row.original) - setRowAction(null) - } - }, [rowAction, handleDeleteRow]) - // Cleanup - React.useEffect(() => { - return () => { - if (pollingRef.current) { - clearInterval(pollingRef.current) - } - } + const handleCompactChange = React.useCallback((compact: boolean) => { + setIsCompact(compact) }, []) - - // 로딩 중 - if (isLoading) { - return ( - <div className="space-y-4"> - <Skeleton className="h-10 w-full" /> - <Skeleton className="h-[500px] w-full" /> - <Skeleton className="h-10 w-full" /> - </div> - ) - } - + + return ( <> - <ClientDataTable - data={tableData} - columns={columns} - advancedFilterFields={advancedFilterFields} - autoSizeColumns - onSelectedRowsChange={setSelectedRowsData} - clearSelection={clearSelection} - onTableReady={(table) => { - tableRef.current = table - }} - > - <div className="flex items-center gap-2"> - {/* 삭제 버튼 - 선택된 항목이 있을 때만 */} - {selectedRowCount > 0 && ( - <Button - variant="destructive" - size="sm" - onClick={handleBatchDelete} - > - <Trash2 className="mr-2 size-4" /> - Delete ({selectedRowCount}) - </Button> - )} - - {/* Get Tags 버튼 */} - <Button - variant="samsung" - size="sm" - onClick={startGetTags} - disabled={isSyncing} - > - <RefreshCcw className={`size-4 mr-2 ${isSyncing ? 'animate-spin' : ''}`} /> - <span className="hidden sm:inline"> - {isSyncing ? 'Syncing...' : 'Get Tags'} - </span> - </Button> - - {/* Add Tag 버튼 */} - <AddTagDialog - projectCode={projectCode} - packageCode={packageCode}/> + <DataTable + table={table} + compact={isCompact} - {/* Import 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleImportClick} - disabled={isPending || isExporting} - > - {isPending ? ( - <Loader2 className="size-4 mr-2 animate-spin" /> - ) : ( - <Upload className="size-4 mr-2" /> - )} - <span className="hidden sm:inline">Import</span> - </Button> - - {/* Export 버튼 */} - <Button - variant="outline" - size="sm" - onClick={handleExport} - disabled={isPending || isExporting || !tableRef.current} - > - {isExporting ? ( - <Loader2 className="size-4 mr-2 animate-spin" /> - ) : ( - <Download className="size-4 mr-2" /> - )} - <span className="hidden sm:inline">Export</span> - </Button> - </div> - </ClientDataTable> - - {/* Hidden file input */} - <input - ref={fileInputRef} - type="file" - accept=".xlsx,.xls" - className="hidden" - onChange={handleFileChange} - /> + floatingBar={<TagsTableFloatingBar table={table} selectedPackageId={selectedPackageId}/>} + > + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + enableCompactToggle={true} + compactStorageKey="tagTableCompact" + onCompactChange={handleCompactChange} + > + {/* + 4) ToolbarActions에 tableData, setTableData 넘겨서 + import 시 상태 병합 + */} + <TagsTableToolbarActions + table={table} + selectedPackageId={selectedPackageId} + tableData={data} // <-- pass current data + selectedMode={selectedMode} + /> + </DataTableAdvancedToolbar> + </DataTable> - {/* Update Sheet */} <UpdateTagSheet open={rowAction?.type === "update"} - onOpenChange={(open) => { - if (!open) setRowAction(null) - }} + onOpenChange={() => setRowAction(null)} tag={rowAction?.row.original ?? null} - packageCode={packageCode} - projectCode={projectCode} - onUpdateSuccess={(updatedValues) => { - if (rowAction?.row.original?.tagNo) { - const tagNo = rowAction.row.original.tagNo - setTableData(prev => - prev.map(item => - item.tagNo === tagNo ? updatedValues : item - ) - ) - } - }} + selectedPackageId={selectedPackageId} /> - {/* Delete Dialog */} + <DeleteTagsDialog - tags={deleteTarget} - packageCode={packageCode} - projectCode={projectCode} - open={deleteDialogOpen} - onOpenChange={(open) => { - if (!open) { - setDeleteDialogOpen(false) - setDeleteTarget([]) - } - }} - onSuccess={handleDeleteSuccess} + open={rowAction?.type === "delete"} + onOpenChange={() => setRowAction(null)} + tags={rowAction?.row.original ? [rowAction?.row.original] : []} showTrigger={false} + onSuccess={() => rowAction?.row.toggleSelected(false)} + selectedPackageId={selectedPackageId} /> - - {/* Add Tag Dialog */} - {/* <AddTagDialog - projectCode={projectCode} - packageCode={packageCode} - open={addTagDialogOpen} - onOpenChange={setAddTagDialogOpen} - onSuccess={() => { - router.refresh() - }} - /> */} </> ) }
\ No newline at end of file diff --git a/lib/tags-plant/table/tags-export.tsx b/lib/tags-plant/table/tags-export.tsx index a3255a0b..fa85148d 100644 --- a/lib/tags-plant/table/tags-export.tsx +++ b/lib/tags-plant/table/tags-export.tsx @@ -15,8 +15,7 @@ import { getClassOptions } from "../service" */ export async function exportTagsToExcel( table: Table<Tag>, - packageCode: string, - projectCode: string, + selectedPackageId: number, { filename = "Tags", excludeColumns = ["select", "actions", "createdAt", "updatedAt"], @@ -43,7 +42,7 @@ export async function exportTagsToExcel( const worksheet = workbook.addWorksheet("Tags") // 3. Tag Class 옵션 가져오기 - const classOptions = await getClassOptions(packageCode, projectCode) + const classOptions = await getClassOptions(selectedPackageId) // 4. 유효성 검사 시트 생성 const validationSheet = workbook.addWorksheet("ValidationData") diff --git a/lib/tags-plant/table/tags-table-floating-bar.tsx b/lib/tags-plant/table/tags-table-floating-bar.tsx index eadbfb12..8d55b7ac 100644 --- a/lib/tags-plant/table/tags-table-floating-bar.tsx +++ b/lib/tags-plant/table/tags-table-floating-bar.tsx @@ -36,13 +36,12 @@ import { Tag } from "@/db/schema/vendorData" interface TagsTableFloatingBarProps { table: Table<Tag> - packageCode: string - projectCode: string + selectedPackageId: number } -export function TagsTableFloatingBar({ table, packageCode, projectCode}: TagsTableFloatingBarProps) { +export function TagsTableFloatingBar({ table, selectedPackageId }: TagsTableFloatingBarProps) { const rows = table.getFilteredSelectedRowModel().rows const [isPending, startTransition] = React.useTransition() diff --git a/lib/tags-plant/table/tags-table-toolbar-actions.tsx b/lib/tags-plant/table/tags-table-toolbar-actions.tsx index c80a600e..cc2d82b4 100644 --- a/lib/tags-plant/table/tags-table-toolbar-actions.tsx +++ b/lib/tags-plant/table/tags-table-toolbar-actions.tsx @@ -52,8 +52,7 @@ interface TagsTableToolbarActionsProps { /** react-table 객체 */ table: Table<Tag> /** 현재 선택된 패키지 ID */ - packageCode: string - projectCode: string + selectedPackageId: number /** 현재 태그 목록(상태) */ tableData: Tag[] /** 태그 목록을 갱신하는 setState */ @@ -69,8 +68,7 @@ interface TagsTableToolbarActionsProps { */ export function TagsTableToolbarActions({ table, - packageCode, - projectCode, + selectedPackageId, tableData, selectedMode }: TagsTableToolbarActionsProps) { @@ -96,7 +94,7 @@ export function TagsTableToolbarActions({ React.useEffect(() => { const loadClassOptions = async () => { try { - const options = await getClassOptions(packageCode, projectCode) + const options = await getClassOptions(selectedPackageId) setClassOptions(options) } catch (error) { console.error("Failed to load class options:", error) @@ -104,7 +102,7 @@ export function TagsTableToolbarActions({ } loadClassOptions() - }, [packageCode, projectCode]) + }, [selectedPackageId]) // 숨겨진 <input>을 클릭 function handleImportClick() { @@ -137,11 +135,12 @@ export function TagsTableToolbarActions({ const [projectId, setProjectId] = React.useState<number | null>(null); + // Add useEffect to fetch projectId when selectedPackageId changes React.useEffect(() => { const fetchProjectId = async () => { - if (packageCode && projectCode) { + if (selectedPackageId) { try { - const pid = await getProjectIdFromContractItemId(projectCode ); + const pid = await getProjectIdFromContractItemId(selectedPackageId); setProjectId(pid); } catch (error) { console.error("Failed to fetch project ID:", error); @@ -151,7 +150,7 @@ export function TagsTableToolbarActions({ }; fetchProjectId(); - }, [projectCode]); + }, [selectedPackageId]); // 특정 attributesId에 대한 옵션 가져오기 const fetchOptions = React.useCallback(async (attributesId: string): Promise<TagOption[]> => { @@ -196,7 +195,7 @@ export function TagsTableToolbarActions({ } try { - const { subFields } = await getSubfieldsByTagType(tagTypeCode, projectCode) + const { subFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) // API 응답을 SubFieldDef 형식으로 변환 const formattedSubFields: SubFieldDef[] = subFields.map(field => ({ @@ -479,7 +478,7 @@ export function TagsTableToolbarActions({ if (tagNo) { // 이미 tableData 내 존재 여부 const dup = tableData.find( - (t) => t.tagNo === tagNo + (t) => t.contractItemId === selectedPackageId && t.tagNo === tagNo ) if (dup) { errorMsg += `TagNo '${tagNo}' already exists. ` @@ -524,8 +523,7 @@ export function TagsTableToolbarActions({ // 정상 행을 importedRows에 추가 importedRows.push({ id: 0, // 임시 - packageCode: packageCode, - projectCode: projectCode, + contractItemId: selectedPackageId, formId: null, tagNo, tagType: finalTagType, // ← 코드로 저장할지, Description으로 저장할지 결정 @@ -554,7 +552,7 @@ export function TagsTableToolbarActions({ // 정상 행이 있으면 태그 생성 요청 if (importedRows.length > 0) { - const result = await bulkCreateTags(importedRows, projectCode, packageCode); + const result = await bulkCreateTags(importedRows, selectedPackageId); if ("error" in result) { toast.error(result.error); } else { @@ -577,8 +575,8 @@ export function TagsTableToolbarActions({ setIsExporting(true) // 유효성 검사가 포함된 새로운 엑셀 내보내기 함수 호출 - await exportTagsToExcel(table, packageCode,projectCode, { - filename: `Tags_${packageCode}_${projectCode}`, + await exportTagsToExcel(table, selectedPackageId, { + filename: `Tags_${selectedPackageId}`, excludeColumns: ["select", "actions", "createdAt", "updatedAt"], }) @@ -596,11 +594,10 @@ export function TagsTableToolbarActions({ setIsLoading(true) // API 엔드포인트 호출 - 작업 시작만 요청 - const response = await fetch('/api/cron/tags-plant/start', { + const response = await fetch('/api/cron/tags/start', { method: 'POST', body: JSON.stringify({ - projectCode: projectCode, - packageCode: packageCode, + packageId: selectedPackageId, mode: selectedMode // 모드 정보 추가 }) }) @@ -641,7 +638,7 @@ export function TagsTableToolbarActions({ // 5초마다 상태 확인 pollingRef.current = setInterval(async () => { try { - const response = await fetch(`/api/cron/tags-plant/status?id=${id}`) + const response = await fetch(`/api/cron/tags/status?id=${id}`) if (!response.ok) { throw new Error('Failed to get tag import status') @@ -702,8 +699,7 @@ export function TagsTableToolbarActions({ .getFilteredSelectedRowModel() .rows.map((row) => row.original)} onSuccess={() => table.toggleAllRowsSelected(false)} - projectCode={projectCode} - packageCode={packageCode} + selectedPackageId={selectedPackageId} /> ) : null} <Button @@ -719,7 +715,7 @@ export function TagsTableToolbarActions({ </span> </Button> - <AddTagDialog projectCode={projectCode} packageCode={packageCode} /> + <AddTagDialog selectedPackageId={selectedPackageId} /> {/* Import */} <Button diff --git a/lib/tags-plant/table/update-tag-sheet.tsx b/lib/tags-plant/table/update-tag-sheet.tsx index 2be1e732..613abaa9 100644 --- a/lib/tags-plant/table/update-tag-sheet.tsx +++ b/lib/tags-plant/table/update-tag-sheet.tsx @@ -50,7 +50,7 @@ import { Badge } from "@/components/ui/badge" import { cn } from "@/lib/utils" import { Tag } from "@/db/schema/vendorData" -import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags-plant/service" +import { updateTag, getSubfieldsByTagType, getClassOptions, TagTypeOption } from "@/lib/tags/service" // SubFieldDef 인터페이스 interface SubFieldDef { @@ -84,11 +84,10 @@ type UpdateTagSchema = z.infer<typeof updateTagSchema> & Record<string, string> interface UpdateTagSheetProps extends React.ComponentPropsWithRef<typeof Sheet> { tag: Tag | null - packageCode: string - projectCode: string + selectedPackageId: number } -export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: UpdateTagSheetProps) { +export function UpdateTagSheet({ tag, selectedPackageId, ...props }: UpdateTagSheetProps) { const [isUpdatePending, startUpdateTransition] = React.useTransition() const [tagTypeList, setTagTypeList] = React.useState<TagTypeOption[]>([]) const [selectedTagTypeCode, setSelectedTagTypeCode] = React.useState<string | null>(null) @@ -111,7 +110,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat setIsLoadingClasses(true) try { - const result = await getClassOptions(packageCode, projectCode) + const result = await getClassOptions(selectedPackageId) setClassOptions(result) } catch (err) { toast.error("클래스 옵션을 불러오는데 실패했습니다.") @@ -165,7 +164,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat async function loadSubFieldsByTagTypeCode(tagTypeCode: string) { setIsLoadingSubFields(true) try { - const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, projectCode) + const { subFields: apiSubFields } = await getSubfieldsByTagType(tagTypeCode, selectedPackageId) const formattedSubFields: SubFieldDef[] = apiSubFields.map(field => ({ name: field.name, label: field.label, @@ -222,7 +221,7 @@ export function UpdateTagSheet({ tag, packageCode, projectCode,...props }: Updat ), } - const result = await updateTag(tagData, projectCode,packageCode ) + const result = await updateTag(tagData, selectedPackageId) if ("error" in result) { toast.error(result.error) diff --git a/lib/vendor-data/services.ts b/lib/vendor-data/services.ts index fe4e56ae..8c8b21d2 100644 --- a/lib/vendor-data/services.ts +++ b/lib/vendor-data/services.ts @@ -62,8 +62,6 @@ export async function getVendorProjectsAndContracts( itemId: contractItems.id, itemName: items.itemName, - packageCode: items.packageCode, - packageName: items.description, }) .from(contracts) .innerJoin(projects, eq(contracts.projectId, projects.id)) @@ -128,94 +126,3 @@ export async function getVendorProjectsAndContracts( return Array.from(projectMap.values()) } -interface ProjectWithPackages { - projectId: number - projectCode: string - projectName: string - projectType: "ship" | "plant" - packages: { - packageCode: string - packageName: string | null - }[] -} - -export async function getVendorProjectsWithPackages( - vendorId?: number, - projectType?: "ship" | "plant" -): Promise<ProjectWithPackages[]> { - // 세션에서 도메인 정보 가져오기 - const session = await getServerSession(authOptions) - - // EVCP 도메인일 때만 전체 조회 - const isEvcpDomain = session?.user?.domain === "evcp" - - // where 조건들을 배열로 관리 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const whereConditions: any[] = [] - - // vendorId 조건 추가 - if (!isEvcpDomain && vendorId) { - whereConditions.push(eq(contracts.vendorId, vendorId)) - } - - // projectType 조건 추가 - if (projectType) { - whereConditions.push(eq(projects.type, projectType)) - } - - const query = db - .select({ - projectId: projects.id, - projectCode: projects.code, - projectName: projects.name, - projectType: projects.type, - - packageCode: items.packageCode, - packageName: items.description, - }) - .from(contracts) - .innerJoin(projects, eq(contracts.projectId, projects.id)) - .innerJoin(contractItems, eq(contractItems.contractId, contracts.id)) - .innerJoin(items, eq(contractItems.itemId, items.id)) - - // 조건이 있으면 where 절 추가 - if (whereConditions.length > 0) { - query.where(and(...whereConditions)) - } - - const rows = await query - - const projectMap = new Map<number, ProjectWithPackages>() - - for (const row of rows) { - // 1) 프로젝트 그룹 찾기 - let projectEntry = projectMap.get(row.projectId) - if (!projectEntry) { - // 새 프로젝트 항목 생성 - projectEntry = { - projectId: row.projectId, - projectCode: row.projectCode, - projectName: row.projectName, - projectType: row.projectType, - packages: [], - } - projectMap.set(row.projectId, projectEntry) - } - - // 2) 프로젝트의 packages 배열에 패키지 추가 (중복 체크) - // packageCode가 같은 항목이 이미 존재하는지 확인 - const existingPackage = projectEntry.packages.find( - (pkg) => pkg.packageCode === row.packageCode - ) - - // 같은 packageCode가 없는 경우에만 추가 - if (!existingPackage) { - projectEntry.packages.push({ - packageCode: row.packageCode, - packageName: row.packageName, - }) - } - } - - return Array.from(projectMap.values()) -}
\ No newline at end of file diff --git a/lib/vendor-document/service.ts b/lib/vendor-document/service.ts index 48e3fa3f..bf2b0b7a 100644 --- a/lib/vendor-document/service.ts +++ b/lib/vendor-document/service.ts @@ -2,14 +2,14 @@ import { eq, SQL } from "drizzle-orm" import db from "@/db/db" -import { stageSubmissions, stageDocuments, stageIssueStages,documentAttachments, documents, issueStages, revisions, stageDocumentsView,vendorDocumentsView ,stageSubmissionAttachments, StageIssueStage, StageDocumentsView, StageDocument,} from "@/db/schema/vendorDocu" +import { documentAttachments, documents, issueStages, revisions, vendorDocumentsView } from "@/db/schema/vendorDocu" import { GetVendorDcoumentsSchema } from "./validations" import { unstable_cache } from "@/lib/unstable-cache"; import { filterColumns } from "@/lib/filter-columns"; import { getErrorMessage } from "@/lib/handle-error"; import { asc, desc, ilike, inArray, and, gte, lte, not, or , isNotNull, isNull} from "drizzle-orm"; import { countVendorDocuments, selectVendorDocuments } from "./repository" -import { contractItems, projects, items,contracts } from "@/db/schema" +import { contractItems } from "@/db/schema" import { saveFile } from "../file-stroage" import path from "path" @@ -494,706 +494,4 @@ export async function fetchRevisionsByStageParams( console.error("Error fetching revisions:", error); return []; } -} - -// 타입 정의 -type SubmissionInfo = { - id: number; - revisionNumber: number; - revisionCode: string; - revisionType: string; - submissionStatus: string; - submittedBy: string; - submittedAt: Date; - reviewStatus: string | null; - buyerSystemStatus: string | null; - syncStatus: string; -}; - -type AttachmentInfo = { - id: number; - fileName: string; - originalFileName: string; - fileSize: number; - fileType: string | null; - storageUrl: string | null; - syncStatus: string; - buyerSystemStatus: string | null; - uploadedAt: Date; -}; - -// Server Action: Fetch documents by projectCode and packageCode -export async function fetchDocumentsByProjectAndPackage( - projectCode: string, - packageCode: string -): Promise<StageDocument[]> { - try { - // First, find the project by code - const projectResult = await db - .select({ id: projects.id }) - .from(projects) - .where(eq(projects.code, projectCode)) - .limit(1); - - if (!projectResult.length) { - return []; - } - - const projectId = projectResult[0].id; - - // Find contract through contractItems joined with items table - const contractItemResult = await db - .select({ - contractId: contractItems.contractId - }) - .from(contractItems) - .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) - .innerJoin(items, eq(contractItems.itemId, items.id)) - .where( - and( - eq(contracts.projectId, projectId), - eq(items.packageCode, packageCode) - ) - ) - .limit(1); - - if (!contractItemResult.length) { - return []; - } - - const contractId = contractItemResult[0].contractId; - - // Get stage documents - const docsResult = await db - .select({ - id: stageDocuments.id, - docNumber: stageDocuments.docNumber, - title: stageDocuments.title, - vendorDocNumber: stageDocuments.vendorDocNumber, - status: stageDocuments.status, - issuedDate: stageDocuments.issuedDate, - docClass: stageDocuments.docClass, - projectId: stageDocuments.projectId, - vendorId: stageDocuments.vendorId, - contractId: stageDocuments.contractId, - buyerSystemStatus: stageDocuments.buyerSystemStatus, - buyerSystemComment: stageDocuments.buyerSystemComment, - lastSyncedAt: stageDocuments.lastSyncedAt, - syncStatus: stageDocuments.syncStatus, - syncError: stageDocuments.syncError, - syncVersion: stageDocuments.syncVersion, - lastModifiedBy: stageDocuments.lastModifiedBy, - createdAt: stageDocuments.createdAt, - updatedAt: stageDocuments.updatedAt, - }) - .from(stageDocuments) - .where( - and( - eq(stageDocuments.projectId, projectId), - eq(stageDocuments.contractId, contractId), - eq(stageDocuments.status, "ACTIVE") - ) - ) - .orderBy(stageDocuments.docNumber); - - return docsResult; - } catch (error) { - console.error("Error fetching documents:", error); - return []; - } -} - -// Server Action: Fetch stages by documentId -export async function fetchStagesByDocumentIdPlant( - documentId: number -): Promise<StageIssueStage[]> { - try { - const stagesResult = await db - .select({ - id: stageIssueStages.id, - documentId: stageIssueStages.documentId, - stageName: stageIssueStages.stageName, - planDate: stageIssueStages.planDate, - actualDate: stageIssueStages.actualDate, - stageStatus: stageIssueStages.stageStatus, - stageOrder: stageIssueStages.stageOrder, - priority: stageIssueStages.priority, - assigneeId: stageIssueStages.assigneeId, - assigneeName: stageIssueStages.assigneeName, - reminderDays: stageIssueStages.reminderDays, - description: stageIssueStages.description, - notes: stageIssueStages.notes, - createdAt: stageIssueStages.createdAt, - updatedAt: stageIssueStages.updatedAt, - }) - .from(stageIssueStages) - .where(eq(stageIssueStages.documentId, documentId)) - .orderBy(stageIssueStages.stageOrder, stageIssueStages.stageName); - - return stagesResult; - } catch (error) { - console.error("Error fetching stages:", error); - return []; - } -} - -// Server Action: Fetch submissions (revisions) by documentId and stageName -export async function fetchSubmissionsByStageParams( - documentId: number, - stageName: string -): Promise<SubmissionInfo[]> { - try { - // First, find the stageId - const stageResult = await db - .select({ id: stageIssueStages.id }) - .from(stageIssueStages) - .where( - and( - eq(stageIssueStages.documentId, documentId), - eq(stageIssueStages.stageName, stageName) - ) - ) - .limit(1); - - if (!stageResult.length) { - return []; - } - - const stageId = stageResult[0].id; - - // Then, get submissions for this stage - const submissionsResult = await db - .select({ - id: stageSubmissions.id, - revisionNumber: stageSubmissions.revisionNumber, - revisionCode: stageSubmissions.revisionCode, - revisionType: stageSubmissions.revisionType, - submissionStatus: stageSubmissions.submissionStatus, - submittedBy: stageSubmissions.submittedBy, - submittedAt: stageSubmissions.submittedAt, - reviewStatus: stageSubmissions.reviewStatus, - buyerSystemStatus: stageSubmissions.buyerSystemStatus, - syncStatus: stageSubmissions.syncStatus, - }) - .from(stageSubmissions) - .where(eq(stageSubmissions.stageId, stageId)) - .orderBy(stageSubmissions.revisionNumber); - - return submissionsResult; - } catch (error) { - console.error("Error fetching submissions:", error); - return []; - } -} - -// View를 활용한 더 효율적인 조회 -export async function fetchDocumentsViewByProjectAndPackage( - projectCode: string, - packageCode: string -): Promise<StageDocumentsView[]> { - try { - // First, find the project by code - const projectResult = await db - .select({ id: projects.id }) - .from(projects) - .where(eq(projects.code, projectCode)) - .limit(1); - - if (!projectResult.length) { - return []; - } - - const projectId = projectResult[0].id; - - // Find contract through contractItems joined with items - const contractItemResult = await db - .select({ - contractId: contractItems.contractId - }) - .from(contractItems) - .innerJoin(contracts, eq(contractItems.contractId, contracts.id)) - .innerJoin(items, eq(contractItems.itemId, items.id)) - .where( - and( - eq(contracts.projectId, projectId), - eq(items.packageCode, packageCode) - ) - ) - .limit(1); - - if (!contractItemResult.length) { - return []; - } - - const contractId = contractItemResult[0].contractId; - - // Use the view for enriched data (includes progress, current stage, etc.) - const documentsViewResult = await db - .select() - .from(stageDocumentsView) - .where( - and( - eq(stageDocumentsView.projectId, projectId), - eq(stageDocumentsView.contractId, contractId), - eq(stageDocumentsView.status, "ACTIVE") - ) - ) - .orderBy(stageDocumentsView.docNumber); - - return documentsViewResult; - } catch (error) { - console.error("Error fetching documents view:", error); - return []; - } -} - -// Server Action: Fetch submission attachments by submissionId -export async function fetchAttachmentsBySubmissionId( - submissionId: number -): Promise<AttachmentInfo[]> { - try { - const attachmentsResult = await db - .select({ - id: stageSubmissionAttachments.id, - fileName: stageSubmissionAttachments.fileName, - originalFileName: stageSubmissionAttachments.originalFileName, - fileSize: stageSubmissionAttachments.fileSize, - fileType: stageSubmissionAttachments.fileType, - storageUrl: stageSubmissionAttachments.storageUrl, - syncStatus: stageSubmissionAttachments.syncStatus, - buyerSystemStatus: stageSubmissionAttachments.buyerSystemStatus, - uploadedAt: stageSubmissionAttachments.uploadedAt, - }) - .from(stageSubmissionAttachments) - .where( - and( - eq(stageSubmissionAttachments.submissionId, submissionId), - eq(stageSubmissionAttachments.status, "ACTIVE") - ) - ) - .orderBy(stageSubmissionAttachments.uploadedAt); - - return attachmentsResult; - } catch (error) { - console.error("Error fetching attachments:", error); - return []; - } -} - -// 추가 헬퍼: 특정 제출의 상세 정보 (첨부파일 포함) -export async function getSubmissionWithAttachments(submissionId: number) { - try { - const [submission] = await db - .select({ - id: stageSubmissions.id, - stageId: stageSubmissions.stageId, - documentId: stageSubmissions.documentId, - revisionNumber: stageSubmissions.revisionNumber, - revisionCode: stageSubmissions.revisionCode, - revisionType: stageSubmissions.revisionType, - submissionStatus: stageSubmissions.submissionStatus, - submittedBy: stageSubmissions.submittedBy, - submittedByEmail: stageSubmissions.submittedByEmail, - submittedAt: stageSubmissions.submittedAt, - reviewedBy: stageSubmissions.reviewedBy, - reviewedAt: stageSubmissions.reviewedAt, - submissionTitle: stageSubmissions.submissionTitle, - submissionDescription: stageSubmissions.submissionDescription, - reviewStatus: stageSubmissions.reviewStatus, - reviewComments: stageSubmissions.reviewComments, - vendorId: stageSubmissions.vendorId, - totalFiles: stageSubmissions.totalFiles, - buyerSystemStatus: stageSubmissions.buyerSystemStatus, - syncStatus: stageSubmissions.syncStatus, - createdAt: stageSubmissions.createdAt, - updatedAt: stageSubmissions.updatedAt, - }) - .from(stageSubmissions) - .where(eq(stageSubmissions.id, submissionId)) - .limit(1); - - if (!submission) { - return null; - } - - const attachments = await fetchAttachmentsBySubmissionId(submissionId); - - return { - ...submission, - attachments, - }; - } catch (error) { - console.error("Error getting submission with attachments:", error); - return null; - } -} - - -interface CreateSubmissionResult { - success: boolean; - error?: string; - submissionId?: number; -} - -export async function createSubmissionAction( - formData: FormData -): Promise<CreateSubmissionResult> { - try { - // Extract form data - const documentId = formData.get("documentId") as string; - const stageName = formData.get("stageName") as string; - const revisionCode = formData.get("revisionCode") as string; - const customFileName = formData.get("customFileName") as string; - const submittedBy = formData.get("submittedBy") as string; - const submittedByEmail = formData.get("submittedByEmail") as string | null; - const submissionTitle = formData.get("submissionTitle") as string | null; - const submissionDescription = formData.get("submissionDescription") as string | null; - const vendorId = formData.get("vendorId") as string; - const attachment = formData.get("attachment") as File | null; - - // Validate required fields - if (!documentId || !stageName || !revisionCode || !submittedBy || !vendorId) { - return { - success: false, - error: "Missing required fields", - }; - } - - const parsedDocumentId = parseInt(documentId, 10); - const parsedVendorId = parseInt(vendorId, 10); - - // Validate parsed numbers - if (isNaN(parsedDocumentId) || isNaN(parsedVendorId)) { - return { - success: false, - error: "Invalid documentId or vendorId", - }; - } - - // Find the document - const [document] = await db - .select() - .from(stageDocuments) - .where(eq(stageDocuments.id, parsedDocumentId)) - .limit(1); - - if (!document) { - return { - success: false, - error: "Document not found", - }; - } - - // Find the stage - const [stage] = await db - .select() - .from(stageIssueStages) - .where( - and( - eq(stageIssueStages.documentId, parsedDocumentId), - eq(stageIssueStages.stageName, stageName) - ) - ) - .limit(1); - - if (!stage) { - return { - success: false, - error: `Stage "${stageName}" not found for this document`, - }; - } - - const stageId = stage.id; - - // Get the latest revision number for this stage - const existingSubmissions = await db - .select({ - revisionNumber: stageSubmissions.revisionNumber, - }) - .from(stageSubmissions) - .where(eq(stageSubmissions.stageId, stageId)) - .orderBy(desc(stageSubmissions.revisionNumber)) - .limit(1); - - const nextRevisionNumber = existingSubmissions.length > 0 - ? existingSubmissions[0].revisionNumber + 1 - : 1; - - // Check if revision code already exists for this stage - const [existingRevisionCode] = await db - .select() - .from(stageSubmissions) - .where( - and( - eq(stageSubmissions.stageId, stageId), - eq(stageSubmissions.revisionCode, revisionCode) - ) - ) - .limit(1); - - if (existingRevisionCode) { - return { - success: false, - error: `Revision code "${revisionCode}" already exists for this stage`, - }; - } - - // Get vendor code from vendors table - const [vendor] = await db - .select({ vendorCode: vendors.vendorCode }) - .from(vendors) - .where(eq(vendors.id, parsedVendorId)) - .limit(1); - - const vendorCode = vendor?.vendorCode || parsedVendorId.toString(); - - // Determine revision type - const revisionType = nextRevisionNumber === 1 ? "INITIAL" : "RESUBMISSION"; - - // Create the submission - const [newSubmission] = await db - .insert(stageSubmissions) - .values({ - stageId, - documentId: parsedDocumentId, - revisionNumber: nextRevisionNumber, - revisionCode, - revisionType, - submissionStatus: "SUBMITTED", - submittedBy, - submittedByEmail: submittedByEmail || undefined, - submittedAt: new Date(), - submissionTitle: submissionTitle || undefined, - submissionDescription: submissionDescription || undefined, - vendorId: parsedVendorId, - vendorCode, - totalFiles: attachment ? 1 : 0, - totalFileSize: attachment ? attachment.size : 0, - syncStatus: "pending", - syncVersion: 0, - lastModifiedBy: "EVCP", - totalFilesToSync: attachment ? 1 : 0, - syncedFilesCount: 0, - failedFilesCount: 0, - }) - .returning(); - - if (!newSubmission) { - return { - success: false, - error: "Failed to create submission", - }; - } - - // Upload attachment if provided - if (attachment) { - try { - // Generate unique filename - const fileExtension = customFileName.split(".").pop() || "docx"; - const timestamp = Date.now(); - const randomString = crypto.randomBytes(8).toString("hex"); - const uniqueFileName = `submissions/${parsedDocumentId}/${stageId}/${timestamp}_${randomString}.${fileExtension}`; - - // Calculate checksum - const buffer = await attachment.arrayBuffer(); - const checksum = crypto - .createHash("md5") - .update(Buffer.from(buffer)) - .digest("hex"); - - // Upload to Vercel Blob (or your storage solution) - const blob = await put(uniqueFileName, attachment, { - access: "public", - contentType: attachment.type || "application/octet-stream", - }); - - // Create attachment record - await db.insert(stageSubmissionAttachments).values({ - submissionId: newSubmission.id, - fileName: uniqueFileName, - originalFileName: customFileName, - fileType: attachment.type || "application/octet-stream", - fileExtension, - fileSize: attachment.size, - storageType: "S3", - storagePath: blob.url, - storageUrl: blob.url, - mimeType: attachment.type || "application/octet-stream", - checksum, - documentType: "DOCUMENT", - uploadedBy: submittedBy, - uploadedAt: new Date(), - status: "ACTIVE", - syncStatus: "pending", - syncVersion: 0, - lastModifiedBy: "EVCP", - isPublic: false, - }); - - // Update submission with file info - await db - .update(stageSubmissions) - .set({ - totalFiles: 1, - totalFileSize: attachment.size, - totalFilesToSync: 1, - updatedAt: new Date(), - }) - .where(eq(stageSubmissions.id, newSubmission.id)); - } catch (uploadError) { - console.error("Error uploading attachment:", uploadError); - - // Rollback: Delete the submission if file upload fails - await db - .delete(stageSubmissions) - .where(eq(stageSubmissions.id, newSubmission.id)); - - return { - success: false, - error: uploadError instanceof Error - ? `File upload failed: ${uploadError.message}` - : "File upload failed", - }; - } - } - - // Update stage status to SUBMITTED - await db - .update(stageIssueStages) - .set({ - stageStatus: "SUBMITTED", - updatedAt: new Date(), - }) - .where(eq(stageIssueStages.id, stageId)); - - // Update document's last modified info - await db - .update(stageDocuments) - .set({ - lastModifiedBy: "EVCP", - syncVersion: document.syncVersion + 1, - updatedAt: new Date(), - }) - .where(eq(stageDocuments.id, parsedDocumentId)); - - // Revalidate relevant paths - revalidatePath(`/projects/${document.projectId}/documents`); - revalidatePath(`/vendor/documents`); - revalidatePath(`/vendor/submissions`); - - return { - success: true, - submissionId: newSubmission.id, - }; - } catch (error) { - console.error("Error creating submission:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error occurred", - }; - } -} - -// Additional helper: Update submission status -export async function updateSubmissionStatus( - submissionId: number, - status: string, - reviewedBy?: string, - reviewComments?: string -): Promise<CreateSubmissionResult> { - try { - const reviewStatus = - status === "APPROVED" ? "APPROVED" : - status === "REJECTED" ? "REJECTED" : - "PENDING"; - - await db - .update(stageSubmissions) - .set({ - submissionStatus: status, - reviewStatus, - reviewComments: reviewComments || undefined, - reviewedBy: reviewedBy || undefined, - reviewedAt: new Date(), - updatedAt: new Date(), - }) - .where(eq(stageSubmissions.id, submissionId)); - - // If approved, update stage status - if (status === "APPROVED") { - const [submission] = await db - .select({ stageId: stageSubmissions.stageId }) - .from(stageSubmissions) - .where(eq(stageSubmissions.id, submissionId)) - .limit(1); - - if (submission) { - await db - .update(stageIssueStages) - .set({ - stageStatus: "APPROVED", - actualDate: new Date().toISOString().split('T')[0], - updatedAt: new Date(), - }) - .where(eq(stageIssueStages.id, submission.stageId)); - } - } - - return { success: true }; - } catch (error) { - console.error("Error updating submission status:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Failed to update submission status" - }; - } -} - -// Helper: Delete submission -export async function deleteSubmissionAction( - submissionId: number -): Promise<CreateSubmissionResult> { - try { - // Get submission info first - const [submission] = await db - .select() - .from(stageSubmissions) - .where(eq(stageSubmissions.id, submissionId)) - .limit(1); - - if (!submission) { - return { - success: false, - error: "Submission not found", - }; - } - - // Delete attachments from storage - const attachments = await db - .select() - .from(stageSubmissionAttachments) - .where(eq(stageSubmissionAttachments.submissionId, submissionId)); - - // TODO: Delete files from blob storage - // for (const attachment of attachments) { - // await del(attachment.storageUrl); - // } - - // Delete submission (cascade will delete attachments) - await db - .delete(stageSubmissions) - .where(eq(stageSubmissions.id, submissionId)); - - // Revalidate paths - revalidatePath(`/vendor/documents`); - revalidatePath(`/vendor/submissions`); - - return { success: true }; - } catch (error) { - console.error("Error deleting submission:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Failed to delete submission", - }; - } }
\ No newline at end of file |
