diff --git a/backend-node/src/app.ts b/backend-node/src/app.ts index e928f96c..80e406b9 100644 --- a/backend-node/src/app.ts +++ b/backend-node/src/app.ts @@ -58,6 +58,7 @@ import riskAlertRoutes from "./routes/riskAlertRoutes"; // 리스크/알림 관 import todoRoutes from "./routes/todoRoutes"; // To-Do 관리 import bookingRoutes from "./routes/bookingRoutes"; // 예약 요청 관리 import mapDataRoutes from "./routes/mapDataRoutes"; // 지도 데이터 관리 +import excelMappingRoutes from "./routes/excelMappingRoutes"; // 엑셀 매핑 템플릿 import yardLayoutRoutes from "./routes/yardLayoutRoutes"; // 3D 필드 //import materialRoutes from "./routes/materialRoutes"; // 자재 관리 import digitalTwinRoutes from "./routes/digitalTwinRoutes"; // 디지털 트윈 (야드 관제) @@ -220,6 +221,7 @@ app.use("/api/external-rest-api-connections", externalRestApiConnectionRoutes); app.use("/api/multi-connection", multiConnectionRoutes); app.use("/api/screen-files", screenFileRoutes); app.use("/api/batch-configs", batchRoutes); +app.use("/api/excel-mapping", excelMappingRoutes); // 엑셀 매핑 템플릿 app.use("/api/batch-management", batchManagementRoutes); app.use("/api/batch-execution-logs", batchExecutionLogRoutes); // app.use("/api/db-type-categories", dbTypeCategoryRoutes); // 파일이 존재하지 않음 diff --git a/backend-node/src/controllers/excelMappingController.ts b/backend-node/src/controllers/excelMappingController.ts new file mode 100644 index 00000000..e29d4fe2 --- /dev/null +++ b/backend-node/src/controllers/excelMappingController.ts @@ -0,0 +1,208 @@ +import { Response } from "express"; +import { AuthenticatedRequest } from "../middleware/authMiddleware"; +import excelMappingService from "../services/excelMappingService"; +import { logger } from "../utils/logger"; + +/** + * 엑셀 컬럼 구조로 매핑 템플릿 조회 + * POST /api/excel-mapping/find + */ +export async function findMappingByColumns( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName, excelColumns } = req.body; + const companyCode = req.user?.companyCode || "*"; + + if (!tableName || !excelColumns || !Array.isArray(excelColumns)) { + res.status(400).json({ + success: false, + message: "tableName과 excelColumns(배열)가 필요합니다.", + }); + return; + } + + logger.info("엑셀 매핑 템플릿 조회 요청", { + tableName, + excelColumns, + companyCode, + userId: req.user?.userId, + }); + + const template = await excelMappingService.findMappingByColumns( + tableName, + excelColumns, + companyCode + ); + + if (template) { + res.json({ + success: true, + data: template, + message: "기존 매핑 템플릿을 찾았습니다.", + }); + } else { + res.json({ + success: true, + data: null, + message: "일치하는 매핑 템플릿이 없습니다.", + }); + } + } catch (error: any) { + logger.error("매핑 템플릿 조회 실패", { error: error.message }); + res.status(500).json({ + success: false, + message: "매핑 템플릿 조회 중 오류가 발생했습니다.", + error: error.message, + }); + } +} + +/** + * 매핑 템플릿 저장 (UPSERT) + * POST /api/excel-mapping/save + */ +export async function saveMappingTemplate( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName, excelColumns, columnMappings } = req.body; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId; + + if (!tableName || !excelColumns || !columnMappings) { + res.status(400).json({ + success: false, + message: "tableName, excelColumns, columnMappings가 필요합니다.", + }); + return; + } + + logger.info("엑셀 매핑 템플릿 저장 요청", { + tableName, + excelColumns, + columnMappings, + companyCode, + userId, + }); + + const template = await excelMappingService.saveMappingTemplate( + tableName, + excelColumns, + columnMappings, + companyCode, + userId + ); + + res.json({ + success: true, + data: template, + message: "매핑 템플릿이 저장되었습니다.", + }); + } catch (error: any) { + logger.error("매핑 템플릿 저장 실패", { error: error.message }); + res.status(500).json({ + success: false, + message: "매핑 템플릿 저장 중 오류가 발생했습니다.", + error: error.message, + }); + } +} + +/** + * 테이블의 매핑 템플릿 목록 조회 + * GET /api/excel-mapping/list/:tableName + */ +export async function getMappingTemplates( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { tableName } = req.params; + const companyCode = req.user?.companyCode || "*"; + + if (!tableName) { + res.status(400).json({ + success: false, + message: "tableName이 필요합니다.", + }); + return; + } + + logger.info("매핑 템플릿 목록 조회 요청", { + tableName, + companyCode, + }); + + const templates = await excelMappingService.getMappingTemplates( + tableName, + companyCode + ); + + res.json({ + success: true, + data: templates, + }); + } catch (error: any) { + logger.error("매핑 템플릿 목록 조회 실패", { error: error.message }); + res.status(500).json({ + success: false, + message: "매핑 템플릿 목록 조회 중 오류가 발생했습니다.", + error: error.message, + }); + } +} + +/** + * 매핑 템플릿 삭제 + * DELETE /api/excel-mapping/:id + */ +export async function deleteMappingTemplate( + req: AuthenticatedRequest, + res: Response +): Promise { + try { + const { id } = req.params; + const companyCode = req.user?.companyCode || "*"; + + if (!id) { + res.status(400).json({ + success: false, + message: "id가 필요합니다.", + }); + return; + } + + logger.info("매핑 템플릿 삭제 요청", { + id, + companyCode, + }); + + const deleted = await excelMappingService.deleteMappingTemplate( + parseInt(id), + companyCode + ); + + if (deleted) { + res.json({ + success: true, + message: "매핑 템플릿이 삭제되었습니다.", + }); + } else { + res.status(404).json({ + success: false, + message: "삭제할 매핑 템플릿을 찾을 수 없습니다.", + }); + } + } catch (error: any) { + logger.error("매핑 템플릿 삭제 실패", { error: error.message }); + res.status(500).json({ + success: false, + message: "매핑 템플릿 삭제 중 오류가 발생했습니다.", + error: error.message, + }); + } +} + diff --git a/backend-node/src/routes/excelMappingRoutes.ts b/backend-node/src/routes/excelMappingRoutes.ts new file mode 100644 index 00000000..cbcecc15 --- /dev/null +++ b/backend-node/src/routes/excelMappingRoutes.ts @@ -0,0 +1,25 @@ +import { Router } from "express"; +import { authenticateToken } from "../middleware/authMiddleware"; +import { + findMappingByColumns, + saveMappingTemplate, + getMappingTemplates, + deleteMappingTemplate, +} from "../controllers/excelMappingController"; + +const router = Router(); + +// 엑셀 컬럼 구조로 매핑 템플릿 조회 +router.post("/find", authenticateToken, findMappingByColumns); + +// 매핑 템플릿 저장 (UPSERT) +router.post("/save", authenticateToken, saveMappingTemplate); + +// 테이블의 매핑 템플릿 목록 조회 +router.get("/list/:tableName", authenticateToken, getMappingTemplates); + +// 매핑 템플릿 삭제 +router.delete("/:id", authenticateToken, deleteMappingTemplate); + +export default router; + diff --git a/backend-node/src/services/excelMappingService.ts b/backend-node/src/services/excelMappingService.ts new file mode 100644 index 00000000..a63a027b --- /dev/null +++ b/backend-node/src/services/excelMappingService.ts @@ -0,0 +1,283 @@ +import { getPool } from "../database/db"; +import { logger } from "../utils/logger"; +import crypto from "crypto"; + +export interface ExcelMappingTemplate { + id?: number; + tableName: string; + excelColumns: string[]; + excelColumnsHash: string; + columnMappings: Record; // { "엑셀컬럼": "시스템컬럼" } + companyCode: string; + createdDate?: Date; + updatedDate?: Date; +} + +class ExcelMappingService { + /** + * 엑셀 컬럼 목록으로 해시 생성 + * 정렬 후 MD5 해시 생성하여 동일한 컬럼 구조 식별 + */ + generateColumnsHash(columns: string[]): string { + // 컬럼 목록을 정렬하여 순서와 무관하게 동일한 해시 생성 + const sortedColumns = [...columns].sort(); + const columnsString = sortedColumns.join("|"); + return crypto.createHash("md5").update(columnsString).digest("hex"); + } + + /** + * 엑셀 컬럼 구조로 매핑 템플릿 조회 + * 동일한 컬럼 구조가 있으면 기존 매핑 반환 + */ + async findMappingByColumns( + tableName: string, + excelColumns: string[], + companyCode: string + ): Promise { + try { + const hash = this.generateColumnsHash(excelColumns); + + logger.info("엑셀 매핑 템플릿 조회", { + tableName, + excelColumns, + hash, + companyCode, + }); + + const pool = getPool(); + + // 회사별 매핑 먼저 조회, 없으면 공통(*) 매핑 조회 + let query: string; + let params: any[]; + + if (companyCode === "*") { + query = ` + SELECT + id, + table_name as "tableName", + excel_columns as "excelColumns", + excel_columns_hash as "excelColumnsHash", + column_mappings as "columnMappings", + company_code as "companyCode", + created_date as "createdDate", + updated_date as "updatedDate" + FROM excel_mapping_template + WHERE table_name = $1 + AND excel_columns_hash = $2 + ORDER BY updated_date DESC + LIMIT 1 + `; + params = [tableName, hash]; + } else { + query = ` + SELECT + id, + table_name as "tableName", + excel_columns as "excelColumns", + excel_columns_hash as "excelColumnsHash", + column_mappings as "columnMappings", + company_code as "companyCode", + created_date as "createdDate", + updated_date as "updatedDate" + FROM excel_mapping_template + WHERE table_name = $1 + AND excel_columns_hash = $2 + AND (company_code = $3 OR company_code = '*') + ORDER BY + CASE WHEN company_code = $3 THEN 0 ELSE 1 END, + updated_date DESC + LIMIT 1 + `; + params = [tableName, hash, companyCode]; + } + + const result = await pool.query(query, params); + + if (result.rows.length > 0) { + logger.info("기존 매핑 템플릿 발견", { + id: result.rows[0].id, + tableName, + }); + return result.rows[0]; + } + + logger.info("매핑 템플릿 없음 - 새 구조", { tableName, hash }); + return null; + } catch (error: any) { + logger.error(`매핑 템플릿 조회 실패: ${error.message}`, { error }); + throw error; + } + } + + /** + * 매핑 템플릿 저장 (UPSERT) + * 동일한 테이블+컬럼구조+회사코드가 있으면 업데이트, 없으면 삽입 + */ + async saveMappingTemplate( + tableName: string, + excelColumns: string[], + columnMappings: Record, + companyCode: string, + userId?: string + ): Promise { + try { + const hash = this.generateColumnsHash(excelColumns); + + logger.info("엑셀 매핑 템플릿 저장 (UPSERT)", { + tableName, + excelColumns, + hash, + columnMappings, + companyCode, + }); + + const pool = getPool(); + + const query = ` + INSERT INTO excel_mapping_template ( + table_name, + excel_columns, + excel_columns_hash, + column_mappings, + company_code, + created_date, + updated_date + ) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) + ON CONFLICT (table_name, excel_columns_hash, company_code) + DO UPDATE SET + column_mappings = EXCLUDED.column_mappings, + updated_date = NOW() + RETURNING + id, + table_name as "tableName", + excel_columns as "excelColumns", + excel_columns_hash as "excelColumnsHash", + column_mappings as "columnMappings", + company_code as "companyCode", + created_date as "createdDate", + updated_date as "updatedDate" + `; + + const result = await pool.query(query, [ + tableName, + excelColumns, + hash, + JSON.stringify(columnMappings), + companyCode, + ]); + + logger.info("매핑 템플릿 저장 완료", { + id: result.rows[0].id, + tableName, + hash, + }); + + return result.rows[0]; + } catch (error: any) { + logger.error(`매핑 템플릿 저장 실패: ${error.message}`, { error }); + throw error; + } + } + + /** + * 테이블의 모든 매핑 템플릿 조회 + */ + async getMappingTemplates( + tableName: string, + companyCode: string + ): Promise { + try { + logger.info("테이블 매핑 템플릿 목록 조회", { tableName, companyCode }); + + const pool = getPool(); + + let query: string; + let params: any[]; + + if (companyCode === "*") { + query = ` + SELECT + id, + table_name as "tableName", + excel_columns as "excelColumns", + excel_columns_hash as "excelColumnsHash", + column_mappings as "columnMappings", + company_code as "companyCode", + created_date as "createdDate", + updated_date as "updatedDate" + FROM excel_mapping_template + WHERE table_name = $1 + ORDER BY updated_date DESC + `; + params = [tableName]; + } else { + query = ` + SELECT + id, + table_name as "tableName", + excel_columns as "excelColumns", + excel_columns_hash as "excelColumnsHash", + column_mappings as "columnMappings", + company_code as "companyCode", + created_date as "createdDate", + updated_date as "updatedDate" + FROM excel_mapping_template + WHERE table_name = $1 + AND (company_code = $2 OR company_code = '*') + ORDER BY updated_date DESC + `; + params = [tableName, companyCode]; + } + + const result = await pool.query(query, params); + + logger.info(`매핑 템플릿 ${result.rows.length}개 조회`, { tableName }); + + return result.rows; + } catch (error: any) { + logger.error(`매핑 템플릿 목록 조회 실패: ${error.message}`, { error }); + throw error; + } + } + + /** + * 매핑 템플릿 삭제 + */ + async deleteMappingTemplate( + id: number, + companyCode: string + ): Promise { + try { + logger.info("매핑 템플릿 삭제", { id, companyCode }); + + const pool = getPool(); + + let query: string; + let params: any[]; + + if (companyCode === "*") { + query = `DELETE FROM excel_mapping_template WHERE id = $1`; + params = [id]; + } else { + query = `DELETE FROM excel_mapping_template WHERE id = $1 AND company_code = $2`; + params = [id, companyCode]; + } + + const result = await pool.query(query, params); + + if (result.rowCount && result.rowCount > 0) { + logger.info("매핑 템플릿 삭제 완료", { id }); + return true; + } + + logger.warn("삭제할 매핑 템플릿 없음", { id, companyCode }); + return false; + } catch (error: any) { + logger.error(`매핑 템플릿 삭제 실패: ${error.message}`, { error }); + throw error; + } + } +} + +export default new ExcelMappingService(); + diff --git a/frontend/components/common/ExcelUploadModal.tsx b/frontend/components/common/ExcelUploadModal.tsx index 28be5688..97214a2a 100644 --- a/frontend/components/common/ExcelUploadModal.tsx +++ b/frontend/components/common/ExcelUploadModal.tsx @@ -19,7 +19,6 @@ import { SelectValue, } from "@/components/ui/select"; import { Input } from "@/components/ui/input"; -import { Checkbox } from "@/components/ui/checkbox"; import { toast } from "sonner"; import { Upload, @@ -35,6 +34,7 @@ import { importFromExcel, getExcelSheetNames } from "@/lib/utils/excelExport"; import { DynamicFormApi } from "@/lib/api/dynamicForm"; import { getTableSchema, TableColumn } from "@/lib/api/tableSchema"; import { cn } from "@/lib/utils"; +import { findMappingByColumns, saveMappingTemplate } from "@/lib/api/excelMapping"; export interface ExcelUploadModalProps { open: boolean; @@ -66,12 +66,14 @@ export const ExcelUploadModal: React.FC = ({ const [file, setFile] = useState(null); const [sheetNames, setSheetNames] = useState([]); const [selectedSheet, setSelectedSheet] = useState(""); + const [isDragOver, setIsDragOver] = useState(false); const fileInputRef = useRef(null); // 2단계: 범위 지정 - const [autoCreateColumn, setAutoCreateColumn] = useState(false); - const [selectedCompany, setSelectedCompany] = useState(""); - const [selectedDataType, setSelectedDataType] = useState(""); + // (더 이상 사용하지 않는 상태들 - 3단계로 이동) + + // 3단계: 컬럼 매핑 + 매핑 템플릿 자동 적용 + const [isAutoMappingLoaded, setIsAutoMappingLoaded] = useState(false); const [detectedRange, setDetectedRange] = useState(""); const [previewData, setPreviewData] = useState[]>([]); const [allData, setAllData] = useState[]>([]); @@ -89,7 +91,11 @@ export const ExcelUploadModal: React.FC = ({ const handleFileChange = async (e: React.ChangeEvent) => { const selectedFile = e.target.files?.[0]; if (!selectedFile) return; + await processFile(selectedFile); + }; + // 파일 처리 공통 함수 (파일 선택 및 드래그 앤 드롭에서 공유) + const processFile = async (selectedFile: File) => { const fileExtension = selectedFile.name.split(".").pop()?.toLowerCase(); if (!["xlsx", "xls", "csv"].includes(fileExtension || "")) { toast.error("엑셀 파일만 업로드 가능합니다. (.xlsx, .xls, .csv)"); @@ -105,7 +111,7 @@ export const ExcelUploadModal: React.FC = ({ const data = await importFromExcel(selectedFile, sheets[0]); setAllData(data); - setDisplayData(data); // 전체 데이터를 미리보기에 표시 (스크롤 가능) + setDisplayData(data); if (data.length > 0) { const columns = Object.keys(data[0]); @@ -122,6 +128,30 @@ export const ExcelUploadModal: React.FC = ({ } }; + // 드래그 앤 드롭 핸들러 + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + + const droppedFile = e.dataTransfer.files?.[0]; + if (droppedFile) { + await processFile(droppedFile); + } + }; + // 시트 변경 핸들러 const handleSheetChange = async (sheetName: string) => { setSelectedSheet(sheetName); @@ -201,6 +231,15 @@ export const ExcelUploadModal: React.FC = ({ } }, [currentStep, tableName]); + // 테이블 생성 시 자동 생성되는 시스템 컬럼 (매핑에서 제외) + const AUTO_GENERATED_COLUMNS = [ + "id", // ID + "created_date", // 생성일시 + "updated_date", // 수정일시 + "writer", // 작성자 + "company_code", // 회사코드 + ]; + const loadTableSchema = async () => { try { console.log("🔍 테이블 스키마 로드 시작:", { tableName }); @@ -210,14 +249,41 @@ export const ExcelUploadModal: React.FC = ({ console.log("📊 테이블 스키마 응답:", response); if (response.success && response.data) { - console.log("✅ 시스템 컬럼 로드 완료:", response.data.columns); - setSystemColumns(response.data.columns); + // 자동 생성 컬럼 제외 + const filteredColumns = response.data.columns.filter( + (col) => !AUTO_GENERATED_COLUMNS.includes(col.name.toLowerCase()) + ); + console.log("✅ 시스템 컬럼 로드 완료 (자동 생성 컬럼 제외):", filteredColumns); + setSystemColumns(filteredColumns); - const initialMappings: ColumnMapping[] = excelColumns.map((col) => ({ - excelColumn: col, - systemColumn: null, - })); - setColumnMappings(initialMappings); + // 기존 매핑 템플릿 조회 + console.log("🔍 매핑 템플릿 조회 중...", { tableName, excelColumns }); + const mappingResponse = await findMappingByColumns(tableName, excelColumns); + + if (mappingResponse.success && mappingResponse.data) { + // 저장된 매핑 템플릿이 있으면 자동 적용 + console.log("✅ 기존 매핑 템플릿 발견:", mappingResponse.data); + const savedMappings = mappingResponse.data.columnMappings; + + const appliedMappings: ColumnMapping[] = excelColumns.map((col) => ({ + excelColumn: col, + systemColumn: savedMappings[col] || null, + })); + setColumnMappings(appliedMappings); + setIsAutoMappingLoaded(true); + + const matchedCount = appliedMappings.filter((m) => m.systemColumn).length; + toast.success(`이전 매핑 템플릿이 적용되었습니다. (${matchedCount}개 컬럼)`); + } else { + // 매핑 템플릿이 없으면 초기 상태로 설정 + console.log("ℹ️ 매핑 템플릿 없음 - 새 엑셀 구조"); + const initialMappings: ColumnMapping[] = excelColumns.map((col) => ({ + excelColumn: col, + systemColumn: null, + })); + setColumnMappings(initialMappings); + setIsAutoMappingLoaded(false); + } } else { console.error("❌ 테이블 스키마 로드 실패:", response); } @@ -343,6 +409,27 @@ export const ExcelUploadModal: React.FC = ({ toast.success( `${successCount}개 행이 업로드되었습니다.${failCount > 0 ? ` (실패: ${failCount}개)` : ""}` ); + + // 매핑 템플릿 저장 (UPSERT - 자동 저장) + try { + const mappingsToSave: Record = {}; + columnMappings.forEach((mapping) => { + mappingsToSave[mapping.excelColumn] = mapping.systemColumn; + }); + + console.log("💾 매핑 템플릿 저장 중...", { tableName, excelColumns, mappingsToSave }); + const saveResult = await saveMappingTemplate(tableName, excelColumns, mappingsToSave); + + if (saveResult.success) { + console.log("✅ 매핑 템플릿 저장 완료:", saveResult.data); + } else { + console.warn("⚠️ 매핑 템플릿 저장 실패:", saveResult.error); + } + } catch (error) { + console.warn("⚠️ 매핑 템플릿 저장 중 오류:", error); + // 매핑 템플릿 저장 실패해도 업로드는 성공이므로 에러 표시 안함 + } + onSuccess?.(); } else { toast.error("업로드에 실패했습니다."); @@ -362,9 +449,7 @@ export const ExcelUploadModal: React.FC = ({ setFile(null); setSheetNames([]); setSelectedSheet(""); - setAutoCreateColumn(false); - setSelectedCompany(""); - setSelectedDataType(""); + setIsAutoMappingLoaded(false); setDetectedRange(""); setPreviewData([]); setAllData([]); @@ -456,16 +541,46 @@ export const ExcelUploadModal: React.FC = ({ -
- + {/* 드래그 앤 드롭 영역 */} +
fileInputRef.current?.click()} + className={cn( + "mt-2 flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-6 transition-colors", + isDragOver + ? "border-primary bg-primary/5" + : file + ? "border-green-500 bg-green-50" + : "border-muted-foreground/25 hover:border-primary hover:bg-muted/50" + )} + > + {file ? ( + <> + +

{file.name}

+

+ 클릭하여 다른 파일 선택 +

+ + ) : ( + <> + +

+ {isDragOver ? "파일을 놓으세요" : "파일을 드래그하거나 클릭하여 선택"} +

+

+ 지원 형식: .xlsx, .xls, .csv +

+ + )} = ({ className="hidden" />
-

- 지원 형식: .xlsx, .xls, .csv -

{sheetNames.length > 0 && ( @@ -510,67 +622,22 @@ export const ExcelUploadModal: React.FC = ({ {/* 2단계: 범위 지정 */} {currentStep === 2 && (
- {/* 상단: 3개 드롭다운 가로 배치 */} -
- - - - - -
- - {/* 중간: 체크박스 + 버튼들 한 줄 배치 */} -
-
- setAutoCreateColumn(checked as boolean)} - /> - + {/* 상단: 시트 선택 + 버튼들 */} +
+
+ +
@@ -751,6 +818,35 @@ export const ExcelUploadModal: React.FC = ({ ))}
+ + {/* 매핑 자동 저장 안내 */} + {isAutoMappingLoaded ? ( +
+
+ +
+

이전 매핑이 자동 적용됨

+

+ 동일한 엑셀 구조가 감지되어 이전에 저장된 매핑이 적용되었습니다. + 필요시 수정하면 업로드 시 자동으로 저장됩니다. +

+
+
+
+ ) : ( +
+
+ +
+

새로운 엑셀 구조

+

+ 이 엑셀 구조는 처음입니다. 컬럼 매핑을 설정하면 업로드 시 자동으로 저장되어 + 다음에 같은 구조의 엑셀을 업로드할 때 자동 적용됩니다. +

+
+
+
+ )}
)} diff --git a/frontend/lib/api/excelMapping.ts b/frontend/lib/api/excelMapping.ts new file mode 100644 index 00000000..50b046ed --- /dev/null +++ b/frontend/lib/api/excelMapping.ts @@ -0,0 +1,106 @@ +import { apiClient } from "./client"; + +export interface ExcelMappingTemplate { + id?: number; + tableName: string; + excelColumns: string[]; + excelColumnsHash: string; + columnMappings: Record; // { "엑셀컬럼": "시스템컬럼" } + companyCode: string; + createdDate?: string; + updatedDate?: string; +} + +export interface ApiResponse { + success: boolean; + data?: T; + message?: string; + error?: string; +} + +/** + * 엑셀 컬럼 구조로 매핑 템플릿 조회 + * 동일한 엑셀 컬럼 구조가 있으면 기존 매핑 반환 + */ +export async function findMappingByColumns( + tableName: string, + excelColumns: string[] +): Promise> { + try { + const response = await apiClient.post("/excel-mapping/find", { + tableName, + excelColumns, + }); + return response.data; + } catch (error: any) { + console.error("매핑 템플릿 조회 실패:", error); + return { + success: false, + error: error.message || "매핑 템플릿 조회 실패", + }; + } +} + +/** + * 매핑 템플릿 저장 (UPSERT) + * 동일한 테이블+컬럼구조가 있으면 업데이트, 없으면 삽입 + */ +export async function saveMappingTemplate( + tableName: string, + excelColumns: string[], + columnMappings: Record +): Promise> { + try { + const response = await apiClient.post("/excel-mapping/save", { + tableName, + excelColumns, + columnMappings, + }); + return response.data; + } catch (error: any) { + console.error("매핑 템플릿 저장 실패:", error); + return { + success: false, + error: error.message || "매핑 템플릿 저장 실패", + }; + } +} + +/** + * 테이블의 매핑 템플릿 목록 조회 + */ +export async function getMappingTemplates( + tableName: string +): Promise> { + try { + const response = await apiClient.get( + `/excel-mapping/list/${encodeURIComponent(tableName)}` + ); + return response.data; + } catch (error: any) { + console.error("매핑 템플릿 목록 조회 실패:", error); + return { + success: false, + error: error.message || "매핑 템플릿 목록 조회 실패", + }; + } +} + +/** + * 매핑 템플릿 삭제 + */ +export async function deleteMappingTemplate( + id: number +): Promise> { + try { + const response = await apiClient.delete(`/excel-mapping/${id}`); + return response.data; + } catch (error: any) { + console.error("매핑 템플릿 삭제 실패:", error); + return { + success: false, + error: error.message || "매핑 템플릿 삭제 실패", + }; + } +} +