From 12e936c0b45ffa1c8f3c02ff77961212767be9a7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 26 Aug 2025 01:17:56 +0000 Subject: (대표님) 가입, 기본계약, 벤더 (최겸) 기술영업 아이템 관련 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/items-tech/service.ts | 96 +++++++++ lib/items-tech/table/hull/import-item-handler.tsx | 119 ++++++++--- lib/items-tech/table/import-excel-button.tsx | 31 ++- lib/items-tech/table/ship/import-item-handler.tsx | 112 +++++++--- lib/items-tech/table/top/import-item-handler.tsx | 112 +++++++--- lib/items-tech/utils/import-utils.ts | 240 ++++++++++++++++++++++ 6 files changed, 620 insertions(+), 90 deletions(-) create mode 100644 lib/items-tech/utils/import-utils.ts (limited to 'lib/items-tech') diff --git a/lib/items-tech/service.ts b/lib/items-tech/service.ts index e0896144..59aa7c6e 100644 --- a/lib/items-tech/service.ts +++ b/lib/items-tech/service.ts @@ -1460,6 +1460,102 @@ export async function getShipTypes() { } } +/** + * 조선 아이템에서 IM으로 시작하는 최대 itemCode 번호 조회 + */ +export async function getMaxShipbuildingIMCode(): Promise { + unstable_noStore(); + + try { + const result = await db + .select({ itemCode: itemShipbuilding.itemCode }) + .from(itemShipbuilding) + .where(sql`${itemShipbuilding.itemCode} LIKE 'IM%'`) + .orderBy(desc(itemShipbuilding.itemCode)) + .limit(1); + + if (result.length === 0) { + return 0; // IM 코드가 없으면 0부터 시작 + } + + const lastCode = result[0].itemCode; + const match = lastCode?.match(/^IM(\d+)$/); + + if (match) { + return parseInt(match[1], 10); + } + + return 0; + } catch (err) { + console.error("조선 IM 코드 조회 오류:", err); + return 0; + } +} + +/** + * 해양 TOP 아이템에서 IM으로 시작하는 최대 itemCode 번호 조회 + */ +export async function getMaxOffshoreTopIMCode(): Promise { + unstable_noStore(); + + try { + const result = await db + .select({ itemCode: itemOffshoreTop.itemCode }) + .from(itemOffshoreTop) + .where(sql`${itemOffshoreTop.itemCode} LIKE 'IM%'`) + .orderBy(desc(itemOffshoreTop.itemCode)) + .limit(1); + + if (result.length === 0) { + return 0; // IM 코드가 없으면 0부터 시작 + } + + const lastCode = result[0].itemCode; + const match = lastCode?.match(/^IM(\d+)$/); + + if (match) { + return parseInt(match[1], 10); + } + + return 0; + } catch (err) { + console.error("해양 TOP IM 코드 조회 오류:", err); + return 0; + } +} + +/** + * 해양 HULL 아이템에서 IM으로 시작하는 최대 itemCode 번호 조회 + */ +export async function getMaxOffshoreHullIMCode(): Promise { + unstable_noStore(); + + try { + const result = await db + .select({ itemCode: itemOffshoreHull.itemCode }) + .from(itemOffshoreHull) + .where(sql`${itemOffshoreHull.itemCode} LIKE 'IM%'`) + .orderBy(desc(itemOffshoreHull.itemCode)) + .limit(1); + + if (result.length === 0) { + return 0; // IM 코드가 없으면 0부터 시작 + } + + const lastCode = result[0].itemCode; + const match = lastCode?.match(/^IM(\d+)$/); + + if (match) { + return parseInt(match[1], 10); + } + + return 0; + } catch (err) { + console.error("해양 HULL IM 코드 조회 오류:", err); + return 0; + } +} + // ----------------------------------------------------------- // 기술영업을 위한 로직 끝 // ----------------------------------------------------------- \ No newline at end of file diff --git a/lib/items-tech/table/hull/import-item-handler.tsx b/lib/items-tech/table/hull/import-item-handler.tsx index 9090dab1..90ff47ae 100644 --- a/lib/items-tech/table/hull/import-item-handler.tsx +++ b/lib/items-tech/table/hull/import-item-handler.tsx @@ -1,7 +1,8 @@ "use client" import { z } from "zod" -import { createOffshoreHullItem } from "../../service" +import { createOffshoreHullItem, getMaxOffshoreHullIMCode } from "../../service" +import { splitItemCodes, createErrorExcelFile, ExtendedProcessResult, processEmptyItemCodes } from "../../utils/import-utils" // 해양 HULL 기능(공종) 유형 enum const HULL_WORK_TYPES = ["HA", "HE", "HH", "HM", "HO", "HP", "NC"] as const; @@ -16,23 +17,25 @@ const itemSchema = z.object({ subItemList: z.string().nullable().optional(), }); -interface ProcessResult { - successCount: number; - errorCount: number; - errors: Array<{ row: number; message: string }>; -} - /** * Excel 파일에서 가져온 해양 HULL 아이템 데이터 처리하는 함수 */ export async function processHullFileImport( jsonData: Record[], progressCallback?: (current: number, total: number) => void -): Promise { +): Promise { // 결과 카운터 초기화 let successCount = 0; let errorCount = 0; - const errors: Array<{ row: number; message: string }> = []; + const errors: Array<{ + row: number; + message: string; + itemCode?: string; + workType?: string; + originalData?: Record; + }> = []; + const processedItemCodes: string[] = []; + const duplicateItemCodes: string[] = []; // 빈 행 등 필터링 const dataRows = jsonData.filter(row => { @@ -45,9 +48,19 @@ export async function processHullFileImport( // 데이터 행이 없으면 빈 결과 반환 if (dataRows.length === 0) { - return { successCount: 0, errorCount: 0, errors: [] }; + return { + successCount: 0, + errorCount: 0, + errors: [], + processedItemCodes: [], + duplicateItemCodes: [] + }; } + // 기존 IM 코드의 최대 번호 조회 + const maxIMCode = await getMaxOffshoreHullIMCode(); + let nextIMNumber = maxIMCode + 1; + // 각 행에 대해 처리 for (let i = 0; i < dataRows.length; i++) { const row = dataRows[i]; @@ -81,33 +94,76 @@ export async function processHullFileImport( err => `${err.path.join('.')}: ${err.message}` ).join(', '); - errors.push({ row: rowIndex, message: errorMessage }); + errors.push({ + row: rowIndex, + message: errorMessage, + originalData: cleanedRow + }); errorCount++; continue; } - // 해양 HULL 아이템 생성 - const result = await createOffshoreHullItem({ - itemCode: cleanedRow.itemCode, - workType: cleanedRow.workType as "HA" | "HE" | "HH" | "HM" | "HO" | "HP" | "NC", - itemList: cleanedRow.itemList, - subItemList: cleanedRow.subItemList, - }); + // itemCode 분할 처리 + const rawItemCodes = splitItemCodes(cleanedRow.itemCode); - if (result.success) { - successCount++; - } else { - errors.push({ - row: rowIndex, - message: result.message || result.error || "알 수 없는 오류" - }); - errorCount++; + // 빈 itemCode 처리 (임시 코드 생성) + const { codes: itemCodes, nextNumber } = processEmptyItemCodes(rawItemCodes, nextIMNumber); + nextIMNumber = nextNumber; + + // 각 itemCode에 대해 개별 처리 + let rowSuccessCount = 0; + let rowErrorCount = 0; + + for (const singleItemCode of itemCodes) { + try { + // 해양 HULL 아이템 생성 + const result = await createOffshoreHullItem({ + itemCode: singleItemCode, + workType: cleanedRow.workType as "HA" | "HE" | "HH" | "HM" | "HO" | "HP" | "NC", + itemList: cleanedRow.itemList, + subItemList: cleanedRow.subItemList, + }); + + if (result.success) { + rowSuccessCount++; + processedItemCodes.push(singleItemCode); + } else { + rowErrorCount++; + if (result.message?.includes('중복') || result.error?.includes('중복')) { + duplicateItemCodes.push(singleItemCode); + } + + errors.push({ + row: rowIndex, + message: `${singleItemCode}: ${result.message || result.error || "알 수 없는 오류"}`, + itemCode: singleItemCode, + workType: cleanedRow.workType, + originalData: cleanedRow + }); + } + } catch (error) { + rowErrorCount++; + console.error(`${rowIndex}행 ${singleItemCode} 처리 오류:`, error); + errors.push({ + row: rowIndex, + message: `${singleItemCode}: ${error instanceof Error ? error.message : "알 수 없는 오류"}`, + itemCode: singleItemCode, + workType: cleanedRow.workType, + originalData: cleanedRow + }); + } } + + // 행별 성공/실패 카운트 업데이트 + successCount += rowSuccessCount; + errorCount += rowErrorCount; + } catch (error) { console.error(`${rowIndex}행 처리 오류:`, error); errors.push({ row: rowIndex, - message: error instanceof Error ? error.message : "알 수 없는 오류" + message: error instanceof Error ? error.message : "알 수 없는 오류", + originalData: row as Record }); errorCount++; } @@ -118,10 +174,17 @@ export async function processHullFileImport( } } + // 에러가 있으면 Excel 파일 생성 + if (errors.length > 0) { + await createErrorExcelFile(errors, 'hull'); + } + // 처리 결과 반환 return { successCount, errorCount, - errors: errors.length > 0 ? errors : [] + errors, + processedItemCodes, + duplicateItemCodes }; } diff --git a/lib/items-tech/table/import-excel-button.tsx b/lib/items-tech/table/import-excel-button.tsx index f8ba9f6d..c0c37b75 100644 --- a/lib/items-tech/table/import-excel-button.tsx +++ b/lib/items-tech/table/import-excel-button.tsx @@ -19,7 +19,7 @@ import { processFileImport } from "./ship/import-item-handler" import { processTopFileImport } from "./top/import-item-handler" import { processHullFileImport } from "./hull/import-item-handler" import { decryptWithServerAction } from "@/components/drm/drmUtils" - +import { ExtendedProcessResult } from "../utils/import-utils" // 선박 아이템 타입 type ItemType = "ship" | "top" | "hull"; @@ -58,7 +58,6 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) setError(null); }; - // 데이터 가져오기 처리 const handleImport = async () => { if (!file) { @@ -84,6 +83,7 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) // 복호화 실패 시 원본 파일 사용 arrayBuffer = await file.arrayBuffer(); } + // ExcelJS 워크북 로드 const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(arrayBuffer); @@ -93,12 +93,12 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) if (!worksheet) { throw new Error("Excel 파일에 워크시트가 없습니다."); } + // 헤더 행 찾기 let headerRowIndex = 1; let headerRow: ExcelJS.Row | undefined; let headerValues: (string | null)[] = []; - worksheet.eachRow((row, rowNumber) => { const values = row.values as (string | null)[]; if (!headerRow && values.some(v => v === "자재 그룹" || v === "자재 코드" || v === "itemCode" || v === "item_code")) { @@ -172,7 +172,7 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) }; // 선택된 타입에 따라 적절한 프로세스 함수 호출 - let result: { successCount: number; errorCount: number; errors?: Array<{ row: number; message: string }> }; + let result: ExtendedProcessResult; if (itemType === "top") { result = await processTopFileImport(dataRows, updateProgress); } else if (itemType === "hull") { @@ -181,15 +181,26 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) result = await processFileImport(dataRows, updateProgress); } - toast.success(`${result.successCount}개의 ${ITEM_TYPE_NAMES[itemType]}이(가) 성공적으로 가져와졌습니다.`); + // 성공 메시지 표시 + const successMessage = `${result.successCount}개의 ${ITEM_TYPE_NAMES[itemType]}이(가) 성공적으로 가져와졌습니다.`; + if (result.processedItemCodes.length > 0) { + toast.success(successMessage); + } + // 에러 처리 및 상세 정보 표시 if (result.errorCount > 0) { - const errorDetails = result.errors?.map((error: { row: number; message: string; itemCode?: string; workType?: string }) => + const errorDetails = result.errors?.map((error) => `행 ${error.row}: ${error.itemCode || '알 수 없음'} (${error.workType || '알 수 없음'}) - ${error.message}` ).join('\n') || '오류 정보를 가져올 수 없습니다.'; console.error('Import 오류 상세:', errorDetails); - toast.error(`${result.errorCount}개의 항목 처리 실패. 콘솔에서 상세 내용을 확인하세요.`); + + // 중복된 아이템코드가 있는 경우 별도 메시지 + if (result.duplicateItemCodes.length > 0) { + toast.error(`${result.duplicateItemCodes.length}개의 중복 아이템코드가 발견되었습니다. 오류 Excel 파일을 확인하세요.`); + } else { + toast.error(`${result.errorCount}개의 항목 처리 실패. 오류 Excel 파일을 확인하세요.`); + } } // 상태 초기화 및 다이얼로그 닫기 @@ -208,8 +219,6 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) } }; - - // 다이얼로그 열기/닫기 핸들러 const handleOpenChange = (newOpen: boolean) => { if (!newOpen) { @@ -245,6 +254,10 @@ export function ImportItemButton({ itemType, onSuccess }: ImportItemButtonProps) {ITEM_TYPE_NAMES[itemType]}을 Excel 파일에서 가져옵니다.
올바른 형식의 Excel 파일(.xlsx)을 업로드하세요. +
+ 참고: 아이템코드는 공백이나 콤마로 구분하여 여러 개를 입력할 수 있습니다. +
+ 자동 코드 생성: 아이템코드가 비어있으면 IM0001부터 자동으로 생성됩니다. diff --git a/lib/items-tech/table/ship/import-item-handler.tsx b/lib/items-tech/table/ship/import-item-handler.tsx index b0f475ff..e95d1987 100644 --- a/lib/items-tech/table/ship/import-item-handler.tsx +++ b/lib/items-tech/table/ship/import-item-handler.tsx @@ -1,7 +1,8 @@ "use client" import { z } from "zod" -import { createShipbuildingImportItem } from "../../service" // 아이템 생성 서버 액션 +import { createShipbuildingImportItem, getMaxShipbuildingIMCode } from "../../service" // 아이템 생성 서버 액션 +import { splitItemCodes, createErrorExcelFile, ExtendedProcessResult, processEmptyItemCodes } from "../../utils/import-utils" // 아이템 데이터 검증을 위한 Zod 스키마 const itemSchema = z.object({ @@ -13,23 +14,25 @@ const itemSchema = z.object({ itemList: z.string().nullable().optional(), }); -interface ProcessResult { - successCount: number; - errorCount: number; - errors: Array<{ row: number; message: string }>; -} - /** * Excel 파일에서 가져온 조선 아이템 데이터 처리하는 함수 */ export async function processFileImport( jsonData: Record[], progressCallback?: (current: number, total: number) => void -): Promise { +): Promise { // 결과 카운터 초기화 let successCount = 0; let errorCount = 0; - const errors: Array<{ row: number; message: string }> = []; + const errors: Array<{ + row: number; + message: string; + itemCode?: string; + workType?: string; + originalData?: Record; + }> = []; + const processedItemCodes: string[] = []; + const duplicateItemCodes: string[] = []; // 빈 행 등 필터링 const dataRows = jsonData.filter(row => { @@ -42,9 +45,19 @@ export async function processFileImport( // 데이터 행이 없으면 빈 결과 반환 if (dataRows.length === 0) { - return { successCount: 0, errorCount: 0, errors: [] }; + return { + successCount: 0, + errorCount: 0, + errors: [], + processedItemCodes: [], + duplicateItemCodes: [] + }; } + // 기존 IM 코드의 최대 번호 조회 + const maxIMCode = await getMaxShipbuildingIMCode(); + let nextIMNumber = maxIMCode + 1; + // 각 행에 대해 처리 for (let i = 0; i < dataRows.length; i++) { const row = dataRows[i]; @@ -81,35 +94,73 @@ export async function processFileImport( errors.push({ row: rowIndex, message: errorMessage, + originalData: cleanedRow }); errorCount++; continue; } + + // itemCode 분할 처리 + const rawItemCodes = splitItemCodes(cleanedRow.itemCode); - // 아이템 생성 - const result = await createShipbuildingImportItem({ - itemCode: cleanedRow.itemCode, - workType: cleanedRow.workType as "기장" | "전장" | "선실" | "배관" | "철의" | "선체", - shipTypes: cleanedRow.shipTypes, - itemList: cleanedRow.itemList, - }); + // 빈 itemCode 처리 (임시 코드 생성) + const { codes: itemCodes, nextNumber } = processEmptyItemCodes(rawItemCodes, nextIMNumber); + nextIMNumber = nextNumber; + + // 각 itemCode에 대해 개별 처리 + let rowSuccessCount = 0; + let rowErrorCount = 0; - if (result.success || !result.error) { - successCount++; - } else { - errors.push({ - row: rowIndex, - message: result.message || result.error || "알 수 없는 오류", - }); - errorCount++; + for (const singleItemCode of itemCodes) { + try { + // 아이템 생성 + const result = await createShipbuildingImportItem({ + itemCode: singleItemCode, + workType: cleanedRow.workType as "기장" | "전장" | "선실" | "배관" | "철의" | "선체", + shipTypes: cleanedRow.shipTypes, + itemList: cleanedRow.itemList, + }); + + if (result.success || !result.error) { + rowSuccessCount++; + processedItemCodes.push(singleItemCode); + } else { + rowErrorCount++; + if (result.message?.includes('중복') || result.error?.includes('중복')) { + duplicateItemCodes.push(singleItemCode); + } + + errors.push({ + row: rowIndex, + message: `${singleItemCode}: ${result.message || result.error || "알 수 없는 오류"}`, + itemCode: singleItemCode, + workType: cleanedRow.workType, + originalData: cleanedRow + }); + } + } catch (error) { + rowErrorCount++; + console.error(`${rowIndex}행 ${singleItemCode} 처리 오류:`, error); + errors.push({ + row: rowIndex, + message: `${singleItemCode}: ${error instanceof Error ? error.message : "알 수 없는 오류"}`, + itemCode: singleItemCode, + workType: cleanedRow.workType, + originalData: cleanedRow + }); + } } + // 행별 성공/실패 카운트 업데이트 + successCount += rowSuccessCount; + errorCount += rowErrorCount; + } catch (error) { console.error(`${rowIndex}행 처리 오류:`, error); - errors.push({ row: rowIndex, message: error instanceof Error ? error.message : "알 수 없는 오류", + originalData: row as Record }); errorCount++; } @@ -120,10 +171,17 @@ export async function processFileImport( } } + // 에러가 있으면 Excel 파일 생성 + if (errors.length > 0) { + await createErrorExcelFile(errors, 'ship'); + } + // 처리 결과 반환 return { successCount, errorCount, - errors + errors, + processedItemCodes, + duplicateItemCodes }; } \ No newline at end of file diff --git a/lib/items-tech/table/top/import-item-handler.tsx b/lib/items-tech/table/top/import-item-handler.tsx index 0197d826..19a2e29a 100644 --- a/lib/items-tech/table/top/import-item-handler.tsx +++ b/lib/items-tech/table/top/import-item-handler.tsx @@ -1,7 +1,8 @@ "use client" import { z } from "zod" -import { createOffshoreTopItem } from "../../service" +import { createOffshoreTopItem, getMaxOffshoreTopIMCode } from "../../service" +import { splitItemCodes, createErrorExcelFile, ExtendedProcessResult, processEmptyItemCodes } from "../../utils/import-utils" // 해양 TOP 기능(공종) 유형 enum const TOP_WORK_TYPES = ["TM", "TS", "TE", "TP", "TA"] as const; @@ -16,23 +17,25 @@ const itemSchema = z.object({ subItemList: z.string().nullable().optional(), }); -interface ProcessResult { - successCount: number; - errorCount: number; - errors: Array<{ row: number; message: string }>; -} - /** * Excel 파일에서 가져온 해양 TOP 아이템 데이터 처리하는 함수 */ export async function processTopFileImport( jsonData: Record[], progressCallback?: (current: number, total: number) => void -): Promise { +): Promise { // 결과 카운터 초기화 let successCount = 0; let errorCount = 0; - const errors: Array<{ row: number; message: string }> = []; + const errors: Array<{ + row: number; + message: string; + itemCode?: string; + workType?: string; + originalData?: Record; + }> = []; + const processedItemCodes: string[] = []; + const duplicateItemCodes: string[] = []; // 빈 행 등 필터링 const dataRows = jsonData.filter(row => { @@ -45,9 +48,19 @@ export async function processTopFileImport( // 데이터 행이 없으면 빈 결과 반환 if (dataRows.length === 0) { - return { successCount: 0, errorCount: 0, errors: [] }; + return { + successCount: 0, + errorCount: 0, + errors: [], + processedItemCodes: [], + duplicateItemCodes: [] + }; } + // 기존 IM 코드의 최대 번호 조회 + const maxIMCode = await getMaxOffshoreTopIMCode(); + let nextIMNumber = maxIMCode + 1; + // 각 행에 대해 처리 for (let i = 0; i < dataRows.length; i++) { const row = dataRows[i]; @@ -84,33 +97,73 @@ export async function processTopFileImport( errors.push({ row: rowIndex, message: errorMessage, + originalData: cleanedRow }); errorCount++; continue; } - // 해양 TOP 아이템 생성 - const result = await createOffshoreTopItem({ - itemCode: cleanedRow.itemCode, - workType: cleanedRow.workType as "TM" | "TS" | "TE" | "TP" | "TA", - itemList: cleanedRow.itemList, - subItemList: cleanedRow.subItemList, - }); + // itemCode 분할 처리 + const rawItemCodes = splitItemCodes(cleanedRow.itemCode); - if (result.success) { - successCount++; - } else { - errors.push({ - row: rowIndex, - message: result.message || result.error || "알 수 없는 오류", - }); - errorCount++; + // 빈 itemCode 처리 (임시 코드 생성) + const { codes: itemCodes, nextNumber } = processEmptyItemCodes(rawItemCodes, nextIMNumber); + nextIMNumber = nextNumber; + + // 각 itemCode에 대해 개별 처리 + let rowSuccessCount = 0; + let rowErrorCount = 0; + + for (const singleItemCode of itemCodes) { + try { + // 해양 TOP 아이템 생성 + const result = await createOffshoreTopItem({ + itemCode: singleItemCode, + workType: cleanedRow.workType as "TM" | "TS" | "TE" | "TP" | "TA", + itemList: cleanedRow.itemList, + subItemList: cleanedRow.subItemList, + }); + + if (result.success) { + rowSuccessCount++; + processedItemCodes.push(singleItemCode); + } else { + rowErrorCount++; + if (result.message?.includes('중복') || result.error?.includes('중복')) { + duplicateItemCodes.push(singleItemCode); + } + + errors.push({ + row: rowIndex, + message: `${singleItemCode}: ${result.message || result.error || "알 수 없는 오류"}`, + itemCode: singleItemCode, + workType: cleanedRow.workType, + originalData: cleanedRow + }); + } + } catch (error) { + rowErrorCount++; + console.error(`${rowIndex}행 ${singleItemCode} 처리 오류:`, error); + errors.push({ + row: rowIndex, + message: `${singleItemCode}: ${error instanceof Error ? error.message : "알 수 없는 오류"}`, + itemCode: singleItemCode, + workType: cleanedRow.workType, + originalData: cleanedRow + }); + } } + + // 행별 성공/실패 카운트 업데이트 + successCount += rowSuccessCount; + errorCount += rowErrorCount; + } catch (error) { console.error(`${rowIndex}행 처리 오류:`, error); errors.push({ row: rowIndex, message: error instanceof Error ? error.message : "알 수 없는 오류", + originalData: row as Record }); errorCount++; } @@ -121,10 +174,17 @@ export async function processTopFileImport( } } + // 에러가 있으면 Excel 파일 생성 + if (errors.length > 0) { + await createErrorExcelFile(errors, 'top'); + } + // 처리 결과 반환 return { successCount, errorCount, - errors + errors, + processedItemCodes, + duplicateItemCodes }; } diff --git a/lib/items-tech/utils/import-utils.ts b/lib/items-tech/utils/import-utils.ts new file mode 100644 index 00000000..e8a0d7a5 --- /dev/null +++ b/lib/items-tech/utils/import-utils.ts @@ -0,0 +1,240 @@ +import * as ExcelJS from 'exceljs'; +import { saveAs } from 'file-saver'; + +/** + * itemCode 문자열을 분할하여 배열로 반환 + * 공백이나 콤마로 구분된 여러 itemCode를 처리 + * 빈 itemCode의 경우 빈 문자열 하나를 포함한 배열 반환 + */ +export function splitItemCodes(itemCode: string): string[] { + if (!itemCode || typeof itemCode !== 'string') { + return [""]; // 빈 itemCode의 경우 빈 문자열 하나 반환 + } + + const trimmedCode = itemCode.trim(); + if (trimmedCode === '') { + return [""]; // 공백만 있는 경우도 빈 문자열 하나 반환 + } + + // 공백과 콤마로 분할하고, trim 처리 (빈 문자열도 유지) + return trimmedCode + .split(/[\s,]+/) + .map(code => code.trim()); +} + +/** + * 임시 IM 코드 생성 (4자리 숫자 형식) + */ +export function generateTempIMCode(startNumber: number): string { + return `IM${startNumber.toString().padStart(4, '0')}`; +} + +/** + * itemCode가 비어있거나 유효하지 않을 때 임시 코드 생성 + * @param itemCodes 분할된 itemCode 배열 + * @param startNumber 시작 번호 + * @returns 처리된 itemCode 배열 + */ +export function processEmptyItemCodes(itemCodes: string[], startNumber: number): { codes: string[], nextNumber: number } { + const processedCodes: string[] = []; + let currentNumber = startNumber; + + for (const code of itemCodes) { + if (!code || code.trim() === '') { + // 빈 코드인 경우 임시 코드 생성 + processedCodes.push(generateTempIMCode(currentNumber)); + currentNumber++; + } else { + // 유효한 코드인 경우 그대로 사용 + processedCodes.push(code); + } + } + + return { + codes: processedCodes, + nextNumber: currentNumber + }; +} + +/** + * 에러 정보를 포함한 Excel 파일 생성 및 다운로드 + */ +export async function createErrorExcelFile( + errors: Array<{ + row: number; + message: string; + itemCode?: string; + workType?: string; + originalData?: Record; + }>, + itemType: 'top' | 'hull' | 'ship' +): Promise { + try { + const workbook = new ExcelJS.Workbook(); + + // 에러 시트 생성 + const errorSheet = workbook.addWorksheet('Import 오류 목록'); + + // 헤더 설정 + const headers = [ + '행 번호', + '아이템코드', + '기능(공종)', + '자재명', + '자재명(상세)', + '선종', // 조선 아이템의 경우 + '오류 내용', + '해결 방법' + ]; + + errorSheet.addRow(headers); + + // 헤더 스타일 + const headerRow = errorSheet.getRow(1); + headerRow.eachCell((cell) => { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFF6B6B' } + }; + cell.font = { + bold: true, + color: { argb: 'FFFFFFFF' } + }; + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + + // 에러 데이터 추가 + errors.forEach((error) => { + const originalData = error.originalData || {}; + const errorRow = errorSheet.addRow([ + error.row, + error.itemCode || originalData.itemCode || '', + error.workType || originalData.workType || '', + originalData.itemList || '', + originalData.subItemList || '', + originalData.shipTypes || '', // 조선 아이템의 경우 + error.message, + getSolutionMessage(error.message) + ]); + + // 에러 행 스타일 + errorRow.eachCell((cell, colNumber) => { + if (colNumber === 7) { // 오류 내용 컬럼 + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFE0E0' } + }; + } + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + }); + + // 안내 시트 생성 + const instructionSheet = workbook.addWorksheet('오류 해결 가이드'); + + const instructions = [ + ['📌 오류 해결 방법 안내', ''], + ['', ''], + ['🔍 중복 아이템코드 오류', ''], + ['• 원인: 이미 존재하는 아이템코드입니다.', ''], + ['• 해결: 다른 아이템코드를 사용하거나 기존 아이템을 수정하세요.', ''], + ['', ''], + ['🔍 필수 필드 누락 오류', ''], + ['• 원인: 기능(공종) 등 필수 필드가 비어있습니다.', ''], + ['• 해결: 모든 필수 필드를 입력하세요.', ''], + ['', ''], + ['🔍 데이터 형식 오류', ''], + ['• 원인: 데이터 형식이 올바르지 않습니다.', ''], + ['• 해결: 올바른 형식으로 데이터를 입력하세요.', ''], + ['', ''], + ['📞 추가 문의: 시스템 관리자', ''] + ]; + + instructions.forEach((rowData, index) => { + const row = instructionSheet.addRow(rowData); + if (index === 0) { + row.getCell(1).font = { bold: true, size: 14, color: { argb: 'FF1F4E79' } }; + } else if (rowData[0]?.includes('📌') || rowData[0]?.includes('🔍')) { + row.getCell(1).font = { bold: true, color: { argb: 'FF1F4E79' } }; + } else if (rowData[0]?.includes(':')) { + row.getCell(1).font = { bold: true, color: { argb: 'FF1F4E79' } }; + } + }); + + instructionSheet.getColumn(1).width = 60; + + // 기본 컬럼 너비 설정 + errorSheet.getColumn(1).width = 10; // 행 번호 + errorSheet.getColumn(2).width = 20; // 아이템코드 + errorSheet.getColumn(3).width = 15; // 기능(공종) + errorSheet.getColumn(4).width = 30; // 자재명 + errorSheet.getColumn(5).width = 30; // 자재명(상세) + errorSheet.getColumn(6).width = 20; // 선종 + errorSheet.getColumn(7).width = 60; // 오류 내용 + errorSheet.getColumn(8).width = 40; // 해결 방법 + + // 파일 생성 및 다운로드 + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + }); + + const itemTypeNames = { + top: '해양TOP', + hull: '해양HULL', + ship: '조선' + }; + + const fileName = `${itemTypeNames[itemType]}_Import_오류_${new Date().toISOString().split('T')[0]}_${Date.now()}.xlsx`; + saveAs(blob, fileName); + + return fileName; + } catch (error) { + console.error("오류 파일 생성 중 오류:", error); + return ''; + } +} + +/** + * 오류 메시지에 따른 해결 방법 반환 + */ +function getSolutionMessage(errorMessage: string): string { + if (errorMessage.includes('중복')) { + return '다른 아이템코드를 사용하거나 기존 아이템을 수정하세요.'; + } else if (errorMessage.includes('필수')) { + return '모든 필수 필드를 입력하세요.'; + } else if (errorMessage.includes('형식')) { + return '올바른 형식으로 데이터를 입력하세요.'; + } else { + return '데이터를 확인하고 다시 시도하세요.'; + } +} + +/** + * 확장된 ProcessResult 인터페이스 + */ +export interface ExtendedProcessResult { + successCount: number; + errorCount: number; + errors: Array<{ + row: number; + message: string; + itemCode?: string; + workType?: string; + originalData?: Record; + }>; + processedItemCodes: string[]; // 성공적으로 처리된 아이템코드들 + duplicateItemCodes: string[]; // 중복된 아이템코드들 +} -- cgit v1.2.3