엑셀 업로드 템플릿 기능 구현
This commit is contained in:
@@ -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); // 파일이 존재하지 않음
|
||||
|
||||
208
backend-node/src/controllers/excelMappingController.ts
Normal file
208
backend-node/src/controllers/excelMappingController.ts
Normal file
@@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
25
backend-node/src/routes/excelMappingRoutes.ts
Normal file
25
backend-node/src/routes/excelMappingRoutes.ts
Normal file
@@ -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;
|
||||
|
||||
283
backend-node/src/services/excelMappingService.ts
Normal file
283
backend-node/src/services/excelMappingService.ts
Normal file
@@ -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<string, string | null>; // { "엑셀컬럼": "시스템컬럼" }
|
||||
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<ExcelMappingTemplate | null> {
|
||||
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<string, string | null>,
|
||||
companyCode: string,
|
||||
userId?: string
|
||||
): Promise<ExcelMappingTemplate> {
|
||||
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<ExcelMappingTemplate[]> {
|
||||
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<boolean> {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user