엑셀 업로드 템플릿 기능 구현

This commit is contained in:
kjs 2026-01-08 11:45:39 +09:00
parent d90a403ed9
commit 5321ea5b80
6 changed files with 809 additions and 89 deletions

View File

@ -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); // 파일이 존재하지 않음

View 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,
});
}
}

View 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;

View 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();

View File

@ -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<ExcelUploadModalProps> = ({
const [file, setFile] = useState<File | null>(null);
const [sheetNames, setSheetNames] = useState<string[]>([]);
const [selectedSheet, setSelectedSheet] = useState<string>("");
const [isDragOver, setIsDragOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// 2단계: 범위 지정
const [autoCreateColumn, setAutoCreateColumn] = useState(false);
const [selectedCompany, setSelectedCompany] = useState<string>("");
const [selectedDataType, setSelectedDataType] = useState<string>("");
// (더 이상 사용하지 않는 상태들 - 3단계로 이동)
// 3단계: 컬럼 매핑 + 매핑 템플릿 자동 적용
const [isAutoMappingLoaded, setIsAutoMappingLoaded] = useState(false);
const [detectedRange, setDetectedRange] = useState<string>("");
const [previewData, setPreviewData] = useState<Record<string, any>[]>([]);
const [allData, setAllData] = useState<Record<string, any>[]>([]);
@ -89,7 +91,11 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
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<ExcelUploadModalProps> = ({
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<ExcelUploadModalProps> = ({
}
};
// 드래그 앤 드롭 핸들러
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(true);
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
setIsDragOver(false);
};
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
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<ExcelUploadModalProps> = ({
}
}, [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<ExcelUploadModalProps> = ({
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<ExcelUploadModalProps> = ({
toast.success(
`${successCount}개 행이 업로드되었습니다.${failCount > 0 ? ` (실패: ${failCount}개)` : ""}`
);
// 매핑 템플릿 저장 (UPSERT - 자동 저장)
try {
const mappingsToSave: Record<string, string | null> = {};
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<ExcelUploadModalProps> = ({
setFile(null);
setSheetNames([]);
setSelectedSheet("");
setAutoCreateColumn(false);
setSelectedCompany("");
setSelectedDataType("");
setIsAutoMappingLoaded(false);
setDetectedRange("");
setPreviewData([]);
setAllData([]);
@ -456,16 +541,46 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
<Label htmlFor="file-upload" className="text-xs sm:text-sm">
*
</Label>
<div className="mt-2 flex items-center gap-2">
<Button
type="button"
variant="outline"
onClick={() => fileInputRef.current?.click()}
className="h-8 flex-1 text-xs sm:h-10 sm:text-sm"
>
<Upload className="mr-2 h-4 w-4" />
{file ? file.name : "파일 선택"}
</Button>
{/* 드래그 앤 드롭 영역 */}
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => 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 ? (
<>
<FileSpreadsheet className="mb-2 h-10 w-10 text-green-600" />
<p className="text-sm font-medium text-green-700">{file.name}</p>
<p className="mt-1 text-xs text-muted-foreground">
</p>
</>
) : (
<>
<Upload className={cn(
"mb-2 h-10 w-10",
isDragOver ? "text-primary" : "text-muted-foreground"
)} />
<p className={cn(
"text-sm font-medium",
isDragOver ? "text-primary" : "text-muted-foreground"
)}>
{isDragOver ? "파일을 놓으세요" : "파일을 드래그하거나 클릭하여 선택"}
</p>
<p className="mt-1 text-xs text-muted-foreground">
형식: .xlsx, .xls, .csv
</p>
</>
)}
<input
ref={fileInputRef}
id="file-upload"
@ -475,9 +590,6 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
className="hidden"
/>
</div>
<p className="mt-1 text-[10px] text-muted-foreground sm:text-xs">
형식: .xlsx, .xls, .csv
</p>
</div>
{sheetNames.length > 0 && (
@ -510,67 +622,22 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
{/* 2단계: 범위 지정 */}
{currentStep === 2 && (
<div className="space-y-3">
{/* 상단: 3개 드롭다운 가로 배치 */}
<div className="grid grid-cols-3 gap-3">
<Select value={selectedSheet} onValueChange={handleSheetChange}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="Sheet1" />
</SelectTrigger>
<SelectContent>
{sheetNames.map((sheetName) => (
<SelectItem key={sheetName} value={sheetName} className="text-xs sm:text-sm">
{sheetName}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedCompany} onValueChange={setSelectedCompany}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="거래처 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="company1" className="text-xs sm:text-sm">
ABC
</SelectItem>
<SelectItem value="company2" className="text-xs sm:text-sm">
XYZ
</SelectItem>
<SelectItem value="company3" className="text-xs sm:text-sm">
</SelectItem>
</SelectContent>
</Select>
<Select value={selectedDataType} onValueChange={setSelectedDataType}>
<SelectTrigger className="h-8 text-xs sm:h-10 sm:text-sm">
<SelectValue placeholder="유형 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="type1" className="text-xs sm:text-sm">
1
</SelectItem>
<SelectItem value="type2" className="text-xs sm:text-sm">
2
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 중간: 체크박스 + 버튼들 한 줄 배치 */}
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center space-x-2">
<Checkbox
id="auto-create"
checked={autoCreateColumn}
onCheckedChange={(checked) => setAutoCreateColumn(checked as boolean)}
/>
<label
htmlFor="auto-create"
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 sm:text-sm"
>
</label>
{/* 상단: 시트 선택 + 버튼들 */}
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<Label className="text-xs text-muted-foreground sm:text-sm">:</Label>
<Select value={selectedSheet} onValueChange={handleSheetChange}>
<SelectTrigger className="h-8 w-[140px] text-xs sm:h-10 sm:w-[180px] sm:text-sm">
<SelectValue placeholder="Sheet1" />
</SelectTrigger>
<SelectContent>
{sheetNames.map((sheetName) => (
<SelectItem key={sheetName} value={sheetName} className="text-xs sm:text-sm">
{sheetName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="ml-auto flex flex-wrap gap-2">
@ -751,6 +818,35 @@ export const ExcelUploadModal: React.FC<ExcelUploadModalProps> = ({
))}
</div>
</div>
{/* 매핑 자동 저장 안내 */}
{isAutoMappingLoaded ? (
<div className="mt-4 rounded-md border border-success bg-success/10 p-3">
<div className="flex items-start gap-2">
<CheckCircle2 className="mt-0.5 h-4 w-4 text-success" />
<div className="text-[10px] text-success sm:text-xs">
<p className="font-medium"> </p>
<p className="mt-1">
.
.
</p>
</div>
</div>
</div>
) : (
<div className="mt-4 rounded-md border border-muted bg-muted/30 p-3">
<div className="flex items-start gap-2">
<Zap className="mt-0.5 h-4 w-4 text-muted-foreground" />
<div className="text-[10px] text-muted-foreground sm:text-xs">
<p className="font-medium"> </p>
<p className="mt-1">
.
.
</p>
</div>
</div>
</div>
)}
</div>
)}

View File

@ -0,0 +1,106 @@
import { apiClient } from "./client";
export interface ExcelMappingTemplate {
id?: number;
tableName: string;
excelColumns: string[];
excelColumnsHash: string;
columnMappings: Record<string, string | null>; // { "엑셀컬럼": "시스템컬럼" }
companyCode: string;
createdDate?: string;
updatedDate?: string;
}
export interface ApiResponse<T> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
/**
* 릿
*
*/
export async function findMappingByColumns(
tableName: string,
excelColumns: string[]
): Promise<ApiResponse<ExcelMappingTemplate | null>> {
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<string, string | null>
): Promise<ApiResponse<ExcelMappingTemplate>> {
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<ApiResponse<ExcelMappingTemplate[]>> {
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<ApiResponse<void>> {
try {
const response = await apiClient.delete(`/excel-mapping/${id}`);
return response.data;
} catch (error: any) {
console.error("매핑 템플릿 삭제 실패:", error);
return {
success: false,
error: error.message || "매핑 템플릿 삭제 실패",
};
}
}