summaryrefslogtreecommitdiff
path: root/lib/items-tech
diff options
context:
space:
mode:
Diffstat (limited to 'lib/items-tech')
-rw-r--r--lib/items-tech/service.ts96
-rw-r--r--lib/items-tech/table/hull/import-item-handler.tsx119
-rw-r--r--lib/items-tech/table/import-excel-button.tsx31
-rw-r--r--lib/items-tech/table/ship/import-item-handler.tsx112
-rw-r--r--lib/items-tech/table/top/import-item-handler.tsx112
-rw-r--r--lib/items-tech/utils/import-utils.ts240
6 files changed, 620 insertions, 90 deletions
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<number> {
+ 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<number> {
+ 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<number> {
+ 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<string, unknown>[],
progressCallback?: (current: number, total: number) => void
-): Promise<ProcessResult> {
+): Promise<ExtendedProcessResult> {
// 결과 카운터 초기화
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<string, any>;
+ }> = [];
+ 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<string, any>
});
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 파일에서 가져옵니다.
<br />
올바른 형식의 Excel 파일(.xlsx)을 업로드하세요.
+ <br />
+ <strong>참고:</strong> 아이템코드는 공백이나 콤마로 구분하여 여러 개를 입력할 수 있습니다.
+ <br />
+ <strong>자동 코드 생성:</strong> 아이템코드가 비어있으면 IM0001부터 자동으로 생성됩니다.
</DialogDescription>
</DialogHeader>
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<string, unknown>[],
progressCallback?: (current: number, total: number) => void
-): Promise<ProcessResult> {
+): Promise<ExtendedProcessResult> {
// 결과 카운터 초기화
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<string, any>;
+ }> = [];
+ 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<string, any>
});
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<string, unknown>[],
progressCallback?: (current: number, total: number) => void
-): Promise<ProcessResult> {
+): Promise<ExtendedProcessResult> {
// 결과 카운터 초기화
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<string, any>;
+ }> = [];
+ 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<string, any>
});
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<string, any>;
+ }>,
+ itemType: 'top' | 'hull' | 'ship'
+): Promise<string> {
+ 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<string, any>;
+ }>;
+ processedItemCodes: string[]; // 성공적으로 처리된 아이템코드들
+ duplicateItemCodes: string[]; // 중복된 아이템코드들
+}