2025-09-12 16:47:02 +09:00
|
|
|
|
import express from "express";
|
|
|
|
|
|
import { dataService } from "../services/dataService";
|
2026-01-09 11:21:16 +09:00
|
|
|
|
import { masterDetailExcelService } from "../services/masterDetailExcelService";
|
2025-09-12 16:47:02 +09:00
|
|
|
|
import { authenticateToken } from "../middleware/authMiddleware";
|
|
|
|
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
|
|
|
|
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
|
2026-01-09 11:21:16 +09:00
|
|
|
|
// ================================
|
|
|
|
|
|
// 마스터-디테일 엑셀 API
|
|
|
|
|
|
// ================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 마스터-디테일 관계 정보 조회
|
|
|
|
|
|
* GET /api/data/master-detail/relation/:screenId
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.get(
|
|
|
|
|
|
"/master-detail/relation/:screenId",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { screenId } = req.params;
|
|
|
|
|
|
|
|
|
|
|
|
if (!screenId || isNaN(parseInt(screenId))) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효한 screenId가 필요합니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🔍 마스터-디테일 관계 조회: screenId=${screenId}`);
|
|
|
|
|
|
|
|
|
|
|
|
const relation = await masterDetailExcelService.getMasterDetailRelation(
|
|
|
|
|
|
parseInt(screenId)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!relation) {
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: null,
|
|
|
|
|
|
message: "마스터-디테일 구조가 아닙니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 마스터-디테일 관계 발견:`, {
|
|
|
|
|
|
masterTable: relation.masterTable,
|
|
|
|
|
|
detailTable: relation.detailTable,
|
|
|
|
|
|
joinKey: relation.masterKeyColumn,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: relation,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error("마스터-디테일 관계 조회 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "마스터-디테일 관계 조회 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 마스터-디테일 엑셀 다운로드 데이터 조회
|
|
|
|
|
|
* POST /api/data/master-detail/download
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.post(
|
|
|
|
|
|
"/master-detail/download",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { screenId, filters } = req.body;
|
|
|
|
|
|
const companyCode = req.user?.companyCode || "*";
|
|
|
|
|
|
|
|
|
|
|
|
if (!screenId) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "screenId가 필요합니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📥 마스터-디테일 엑셀 다운로드: screenId=${screenId}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 마스터-디테일 관계 조회
|
|
|
|
|
|
const relation = await masterDetailExcelService.getMasterDetailRelation(
|
|
|
|
|
|
parseInt(screenId)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!relation) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "마스터-디테일 구조가 아닙니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. JOIN 데이터 조회
|
|
|
|
|
|
const data = await masterDetailExcelService.getJoinedData(
|
|
|
|
|
|
relation,
|
|
|
|
|
|
companyCode,
|
|
|
|
|
|
filters
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 마스터-디테일 데이터 조회 완료: ${data.data.length}행`);
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error("마스터-디테일 다운로드 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "마스터-디테일 다운로드 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 마스터-디테일 엑셀 업로드
|
|
|
|
|
|
* POST /api/data/master-detail/upload
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.post(
|
|
|
|
|
|
"/master-detail/upload",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { screenId, data } = req.body;
|
|
|
|
|
|
const companyCode = req.user?.companyCode || "*";
|
|
|
|
|
|
const userId = req.user?.userId;
|
|
|
|
|
|
|
|
|
|
|
|
if (!screenId || !data || !Array.isArray(data)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "screenId와 data 배열이 필요합니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📤 마스터-디테일 엑셀 업로드: screenId=${screenId}, rows=${data.length}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 마스터-디테일 관계 조회
|
|
|
|
|
|
const relation = await masterDetailExcelService.getMasterDetailRelation(
|
|
|
|
|
|
parseInt(screenId)
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!relation) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "마스터-디테일 구조가 아닙니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 데이터 업로드
|
|
|
|
|
|
const result = await masterDetailExcelService.uploadJoinedData(
|
|
|
|
|
|
relation,
|
|
|
|
|
|
data,
|
|
|
|
|
|
companyCode,
|
|
|
|
|
|
userId
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 마스터-디테일 업로드 완료:`, {
|
|
|
|
|
|
masterInserted: result.masterInserted,
|
|
|
|
|
|
masterUpdated: result.masterUpdated,
|
|
|
|
|
|
detailInserted: result.detailInserted,
|
|
|
|
|
|
errors: result.errors.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: result.success,
|
|
|
|
|
|
data: result,
|
|
|
|
|
|
message: result.success
|
|
|
|
|
|
? `마스터 ${result.masterInserted + result.masterUpdated}건, 디테일 ${result.detailInserted}건 처리되었습니다.`
|
|
|
|
|
|
: "업로드 중 오류가 발생했습니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error("마스터-디테일 업로드 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "마스터-디테일 업로드 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-01-09 15:32:02 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 마스터-디테일 간단 모드 엑셀 업로드
|
|
|
|
|
|
* - 마스터 정보는 UI에서 선택
|
|
|
|
|
|
* - 디테일 정보만 엑셀에서 업로드
|
|
|
|
|
|
* - 채번 규칙을 통해 마스터 키 자동 생성
|
|
|
|
|
|
*
|
|
|
|
|
|
* POST /api/data/master-detail/upload-simple
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.post(
|
|
|
|
|
|
"/master-detail/upload-simple",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
2026-01-09 15:46:09 +09:00
|
|
|
|
const { screenId, detailData, masterFieldValues, numberingRuleId, afterUploadFlowId, afterUploadFlows } = req.body;
|
2026-01-09 15:32:02 +09:00
|
|
|
|
const companyCode = req.user?.companyCode || "*";
|
|
|
|
|
|
const userId = req.user?.userId || "system";
|
|
|
|
|
|
|
|
|
|
|
|
if (!screenId || !detailData || !Array.isArray(detailData)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "screenId와 detailData 배열이 필요합니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📤 마스터-디테일 간단 모드 업로드: screenId=${screenId}, rows=${detailData.length}`);
|
|
|
|
|
|
console.log(` 마스터 필드 값:`, masterFieldValues);
|
|
|
|
|
|
console.log(` 채번 규칙 ID:`, numberingRuleId);
|
2026-01-09 15:46:09 +09:00
|
|
|
|
console.log(` 업로드 후 제어:`, afterUploadFlows?.length > 0 ? `${afterUploadFlows.length}개` : afterUploadFlowId || "없음");
|
2026-01-09 15:32:02 +09:00
|
|
|
|
|
|
|
|
|
|
// 업로드 실행
|
|
|
|
|
|
const result = await masterDetailExcelService.uploadSimple(
|
|
|
|
|
|
parseInt(screenId),
|
|
|
|
|
|
detailData,
|
|
|
|
|
|
masterFieldValues || {},
|
|
|
|
|
|
numberingRuleId,
|
|
|
|
|
|
companyCode,
|
2026-01-09 15:46:09 +09:00
|
|
|
|
userId,
|
|
|
|
|
|
afterUploadFlowId, // 업로드 후 제어 실행 (단일, 하위 호환성)
|
|
|
|
|
|
afterUploadFlows // 업로드 후 제어 실행 (다중)
|
2026-01-09 15:32:02 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 마스터-디테일 간단 모드 업로드 완료:`, {
|
|
|
|
|
|
masterInserted: result.masterInserted,
|
|
|
|
|
|
detailInserted: result.detailInserted,
|
|
|
|
|
|
generatedKey: result.generatedKey,
|
|
|
|
|
|
errors: result.errors.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: result.success,
|
|
|
|
|
|
data: result,
|
|
|
|
|
|
message: result.success
|
|
|
|
|
|
? `마스터 1건(${result.generatedKey}), 디테일 ${result.detailInserted}건 처리되었습니다.`
|
|
|
|
|
|
: "업로드 중 오류가 발생했습니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error("마스터-디테일 간단 모드 업로드 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "마스터-디테일 업로드 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-01-09 11:21:16 +09:00
|
|
|
|
// ================================
|
|
|
|
|
|
// 기존 데이터 API
|
|
|
|
|
|
// ================================
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 조인 데이터 조회 API (다른 라우트보다 먼저 정의)
|
|
|
|
|
|
* GET /api/data/join?leftTable=...&rightTable=...&leftColumn=...&rightColumn=...&leftValue=...
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.get(
|
|
|
|
|
|
"/join",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
2025-11-21 03:40:41 +09:00
|
|
|
|
const {
|
|
|
|
|
|
leftTable,
|
|
|
|
|
|
rightTable,
|
|
|
|
|
|
leftColumn,
|
|
|
|
|
|
rightColumn,
|
|
|
|
|
|
leftValue,
|
|
|
|
|
|
dataFilter,
|
|
|
|
|
|
enableEntityJoin,
|
|
|
|
|
|
displayColumns,
|
|
|
|
|
|
deduplication,
|
|
|
|
|
|
} = req.query;
|
2025-10-15 17:25:38 +09:00
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!leftTable || !rightTable || !leftColumn || !rightColumn) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message:
|
|
|
|
|
|
"필수 파라미터가 누락되었습니다 (leftTable, rightTable, leftColumn, rightColumn).",
|
|
|
|
|
|
error: "MISSING_PARAMETERS",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-13 17:06:41 +09:00
|
|
|
|
// dataFilter 파싱 (JSON 문자열로 전달됨)
|
|
|
|
|
|
let parsedDataFilter = undefined;
|
|
|
|
|
|
if (dataFilter && typeof dataFilter === "string") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
parsedDataFilter = JSON.parse(dataFilter);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("dataFilter 파싱 오류:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 10:23:54 +09:00
|
|
|
|
// 🆕 enableEntityJoin 파싱
|
2025-11-21 03:40:41 +09:00
|
|
|
|
const enableEntityJoinFlag =
|
|
|
|
|
|
enableEntityJoin === "true" ||
|
|
|
|
|
|
(typeof enableEntityJoin === "boolean" && enableEntityJoin);
|
2025-11-20 10:23:54 +09:00
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
// SQL 인젝션 방지를 위한 검증
|
|
|
|
|
|
const tables = [leftTable as string, rightTable as string];
|
|
|
|
|
|
const columns = [leftColumn as string, rightColumn as string];
|
|
|
|
|
|
|
|
|
|
|
|
for (const table of tables) {
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: `유효하지 않은 테이블명입니다: ${table}`,
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (const column of columns) {
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(column)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: `유효하지 않은 컬럼명입니다: ${column}`,
|
|
|
|
|
|
error: "INVALID_COLUMN_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-07 11:51:44 +09:00
|
|
|
|
// 회사 코드 추출 (멀티테넌시 필터링)
|
|
|
|
|
|
const userCompany = req.user?.companyCode;
|
|
|
|
|
|
|
2025-11-20 10:23:54 +09:00
|
|
|
|
// displayColumns 파싱 (item_info.item_name 등)
|
2025-11-21 03:40:41 +09:00
|
|
|
|
let parsedDisplayColumns:
|
|
|
|
|
|
| Array<{ name: string; label?: string }>
|
|
|
|
|
|
| undefined;
|
2025-11-20 10:23:54 +09:00
|
|
|
|
if (displayColumns) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
parsedDisplayColumns = JSON.parse(displayColumns as string);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error("displayColumns 파싱 실패:", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🆕 deduplication 파싱
|
2025-11-21 03:40:41 +09:00
|
|
|
|
let parsedDeduplication:
|
|
|
|
|
|
| {
|
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
|
groupByColumn: string;
|
|
|
|
|
|
keepStrategy: "latest" | "earliest" | "base_price" | "current_date";
|
|
|
|
|
|
sortColumn?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
| undefined;
|
2025-11-20 10:23:54 +09:00
|
|
|
|
if (deduplication) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
parsedDeduplication = JSON.parse(deduplication as string);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error("deduplication 파싱 실패:", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
console.log(`🔗 조인 데이터 조회:`, {
|
|
|
|
|
|
leftTable,
|
|
|
|
|
|
rightTable,
|
|
|
|
|
|
leftColumn,
|
|
|
|
|
|
rightColumn,
|
|
|
|
|
|
leftValue,
|
2025-11-07 11:51:44 +09:00
|
|
|
|
userCompany,
|
2025-11-20 10:23:54 +09:00
|
|
|
|
dataFilter: parsedDataFilter,
|
|
|
|
|
|
enableEntityJoin: enableEntityJoinFlag,
|
|
|
|
|
|
displayColumns: parsedDisplayColumns, // 🆕 표시 컬럼 로그
|
|
|
|
|
|
deduplication: parsedDeduplication, // 🆕 중복 제거 로그
|
2025-10-15 17:25:38 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-20 10:23:54 +09:00
|
|
|
|
// 조인 데이터 조회 (회사 코드 + 데이터 필터 + Entity 조인 + 표시 컬럼 + 중복 제거 전달)
|
2025-10-15 17:25:38 +09:00
|
|
|
|
const result = await dataService.getJoinedData(
|
|
|
|
|
|
leftTable as string,
|
|
|
|
|
|
rightTable as string,
|
|
|
|
|
|
leftColumn as string,
|
|
|
|
|
|
rightColumn as string,
|
2025-11-07 11:51:44 +09:00
|
|
|
|
leftValue as string,
|
2025-11-13 17:06:41 +09:00
|
|
|
|
userCompany,
|
2025-11-20 10:23:54 +09:00
|
|
|
|
parsedDataFilter,
|
|
|
|
|
|
enableEntityJoinFlag,
|
|
|
|
|
|
parsedDisplayColumns, // 🆕 표시 컬럼 전달
|
|
|
|
|
|
parsedDeduplication // 🆕 중복 제거 설정 전달
|
2025-10-15 17:25:38 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`✅ 조인 데이터 조회 성공: ${result.data?.length || 0}개 항목`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: result.data,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("조인 데이터 조회 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "조인 데이터 조회 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 동적 테이블 데이터 조회 API
|
|
|
|
|
|
* GET /api/data/{tableName}
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.get(
|
|
|
|
|
|
"/:tableName",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tableName } = req.params;
|
2025-10-15 17:25:38 +09:00
|
|
|
|
const {
|
|
|
|
|
|
limit,
|
|
|
|
|
|
offset,
|
|
|
|
|
|
page,
|
|
|
|
|
|
size,
|
|
|
|
|
|
orderBy,
|
|
|
|
|
|
searchTerm,
|
|
|
|
|
|
sortBy,
|
|
|
|
|
|
sortOrder,
|
|
|
|
|
|
userLang,
|
|
|
|
|
|
...filters
|
|
|
|
|
|
} = req.query;
|
2025-09-12 16:47:02 +09:00
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!tableName || typeof tableName !== "string") {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "테이블명이 필요합니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SQL 인젝션 방지를 위한 테이블명 검증
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
// page/size 또는 limit/offset 방식 지원
|
|
|
|
|
|
let finalLimit = 100;
|
|
|
|
|
|
let finalOffset = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (page && size) {
|
|
|
|
|
|
// page/size 방식
|
|
|
|
|
|
const pageNum = parseInt(page as string) || 1;
|
|
|
|
|
|
const sizeNum = parseInt(size as string) || 100;
|
|
|
|
|
|
finalLimit = sizeNum;
|
|
|
|
|
|
finalOffset = (pageNum - 1) * sizeNum;
|
|
|
|
|
|
} else if (limit || offset) {
|
|
|
|
|
|
// limit/offset 방식
|
|
|
|
|
|
finalLimit = parseInt(limit as string) || 10;
|
|
|
|
|
|
finalOffset = parseInt(offset as string) || 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
|
console.log(`📊 데이터 조회 요청: ${tableName}`, {
|
2025-10-15 17:25:38 +09:00
|
|
|
|
limit: finalLimit,
|
|
|
|
|
|
offset: finalOffset,
|
|
|
|
|
|
orderBy: orderBy || sortBy,
|
|
|
|
|
|
searchTerm,
|
2025-09-12 16:47:02 +09:00
|
|
|
|
filters,
|
|
|
|
|
|
user: req.user?.userId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
// filters에서 searchTerm과 sortOrder 제거 (이미 별도로 처리됨)
|
|
|
|
|
|
const cleanFilters = { ...filters };
|
|
|
|
|
|
delete cleanFilters.searchTerm;
|
|
|
|
|
|
delete cleanFilters.sortOrder;
|
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
|
// 데이터 조회
|
|
|
|
|
|
const result = await dataService.getTableData({
|
|
|
|
|
|
tableName,
|
2025-10-15 17:25:38 +09:00
|
|
|
|
limit: finalLimit,
|
|
|
|
|
|
offset: finalOffset,
|
|
|
|
|
|
orderBy: (orderBy || sortBy) as string,
|
|
|
|
|
|
filters: cleanFilters as Record<string, string>,
|
2025-09-12 16:47:02 +09:00
|
|
|
|
userCompany: req.user?.companyCode,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`✅ 데이터 조회 성공: ${tableName}, ${result.data?.length || 0}개 항목`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
// 페이징 정보 포함하여 반환
|
|
|
|
|
|
const total = result.data?.length || 0;
|
|
|
|
|
|
const responsePage =
|
|
|
|
|
|
finalLimit > 0 ? Math.floor(finalOffset / finalLimit) + 1 : 1;
|
|
|
|
|
|
const responseSize = finalLimit;
|
|
|
|
|
|
const totalPages = responseSize > 0 ? Math.ceil(total / responseSize) : 1;
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: result.data,
|
|
|
|
|
|
total,
|
|
|
|
|
|
page: responsePage,
|
|
|
|
|
|
size: responseSize,
|
|
|
|
|
|
totalPages,
|
|
|
|
|
|
});
|
2025-09-12 16:47:02 +09:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("데이터 조회 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "데이터 조회 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 테이블 컬럼 정보 조회 API
|
|
|
|
|
|
* GET /api/data/{tableName}/columns
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.get(
|
|
|
|
|
|
"/:tableName/columns",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tableName } = req.params;
|
|
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!tableName || typeof tableName !== "string") {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "테이블명이 필요합니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SQL 인젝션 방지를 위한 테이블명 검증
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`📋 컬럼 정보 조회: ${tableName}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 정보 조회
|
|
|
|
|
|
const result = await dataService.getTableColumns(tableName);
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`✅ 컬럼 정보 조회 성공: ${tableName}, ${result.data?.length || 0}개 컬럼`
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return res.json(result);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("컬럼 정보 조회 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "컬럼 정보 조회 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 레코드 상세 조회 API
|
|
|
|
|
|
* GET /api/data/{tableName}/{id}
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.get(
|
|
|
|
|
|
"/:tableName/:id",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tableName, id } = req.params;
|
|
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!tableName || typeof tableName !== "string") {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "테이블명이 필요합니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 10:32:37 +09:00
|
|
|
|
const { enableEntityJoin, groupByColumns, primaryKeyColumn } = req.query;
|
2025-11-21 03:40:41 +09:00
|
|
|
|
const enableEntityJoinFlag =
|
|
|
|
|
|
enableEntityJoin === "true" ||
|
|
|
|
|
|
(typeof enableEntityJoin === "boolean" && enableEntityJoin);
|
|
|
|
|
|
|
2025-11-20 10:23:54 +09:00
|
|
|
|
// groupByColumns 파싱 (JSON 문자열 또는 쉼표 구분)
|
|
|
|
|
|
let groupByColumnsArray: string[] = [];
|
|
|
|
|
|
if (groupByColumns) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (typeof groupByColumns === "string") {
|
|
|
|
|
|
// JSON 형식이면 파싱, 아니면 쉼표로 분리
|
2025-11-21 03:40:41 +09:00
|
|
|
|
groupByColumnsArray = groupByColumns.startsWith("[")
|
|
|
|
|
|
? JSON.parse(groupByColumns)
|
|
|
|
|
|
: groupByColumns.split(",").map((c) => c.trim());
|
2025-11-20 10:23:54 +09:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn("groupByColumns 파싱 실패:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 10:32:37 +09:00
|
|
|
|
// 🆕 primaryKeyColumn 파싱
|
|
|
|
|
|
const primaryKeyColumnStr = typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined;
|
|
|
|
|
|
|
2025-11-21 03:40:41 +09:00
|
|
|
|
console.log(`🔍 레코드 상세 조회: ${tableName}/${id}`, {
|
2025-11-20 10:23:54 +09:00
|
|
|
|
enableEntityJoin: enableEntityJoinFlag,
|
2025-11-21 03:40:41 +09:00
|
|
|
|
groupByColumns: groupByColumnsArray,
|
2026-01-21 10:32:37 +09:00
|
|
|
|
primaryKeyColumn: primaryKeyColumnStr,
|
2025-11-20 10:23:54 +09:00
|
|
|
|
});
|
2025-10-15 17:25:38 +09:00
|
|
|
|
|
2026-01-21 10:32:37 +09:00
|
|
|
|
// 레코드 상세 조회 (Entity Join 옵션 + 그룹핑 옵션 + Primary Key 컬럼 포함)
|
2025-11-21 03:40:41 +09:00
|
|
|
|
const result = await dataService.getRecordDetail(
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
id,
|
|
|
|
|
|
enableEntityJoinFlag,
|
2026-01-21 10:32:37 +09:00
|
|
|
|
groupByColumnsArray,
|
|
|
|
|
|
primaryKeyColumnStr // 🆕 Primary Key 컬럼명 전달
|
2025-11-21 03:40:41 +09:00
|
|
|
|
);
|
2025-10-15 17:25:38 +09:00
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.data) {
|
|
|
|
|
|
return res.status(404).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "레코드를 찾을 수 없습니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 레코드 조회 성공: ${tableName}/${id}`);
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: result.data,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("레코드 상세 조회 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "레코드 조회 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-20 11:58:43 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 그룹화된 데이터 UPSERT API
|
|
|
|
|
|
* POST /api/data/upsert-grouped
|
2025-11-21 03:40:41 +09:00
|
|
|
|
*
|
2025-11-20 11:58:43 +09:00
|
|
|
|
* 요청 본문:
|
|
|
|
|
|
* {
|
|
|
|
|
|
* tableName: string,
|
|
|
|
|
|
* parentKeys: { customer_id: "CUST-0002", item_id: "SLI-2025-0002" },
|
|
|
|
|
|
* records: [ { customer_item_code: "84-44", start_date: "2025-11-18", ... }, ... ]
|
|
|
|
|
|
* }
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.post(
|
|
|
|
|
|
"/upsert-grouped",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
2026-02-09 15:37:28 +09:00
|
|
|
|
const { tableName, parentKeys, records, deleteOrphans = true } = req.body;
|
2025-11-20 11:58:43 +09:00
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!tableName || !parentKeys || !records || !Array.isArray(records)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
2025-11-21 03:40:41 +09:00
|
|
|
|
message:
|
|
|
|
|
|
"필수 파라미터가 누락되었습니다 (tableName, parentKeys, records).",
|
2025-11-20 11:58:43 +09:00
|
|
|
|
error: "MISSING_PARAMETERS",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블명 검증
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🔄 그룹화된 데이터 UPSERT: ${tableName}`, {
|
|
|
|
|
|
parentKeys,
|
|
|
|
|
|
recordCount: records.length,
|
|
|
|
|
|
userCompany: req.user?.companyCode,
|
|
|
|
|
|
userId: req.user?.userId,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// UPSERT 수행
|
|
|
|
|
|
const result = await dataService.upsertGroupedRecords(
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
parentKeys,
|
|
|
|
|
|
records,
|
|
|
|
|
|
req.user?.companyCode,
|
2026-02-09 15:37:28 +09:00
|
|
|
|
req.user?.userId,
|
|
|
|
|
|
deleteOrphans
|
2025-11-20 11:58:43 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 그룹화된 데이터 UPSERT 성공: ${tableName}`, {
|
2025-11-21 03:40:41 +09:00
|
|
|
|
inserted: result.data?.inserted || 0,
|
|
|
|
|
|
updated: result.data?.updated || 0,
|
|
|
|
|
|
deleted: result.data?.deleted || 0,
|
2025-11-20 11:58:43 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: "데이터가 저장되었습니다.",
|
2025-11-21 03:40:41 +09:00
|
|
|
|
inserted: result.data?.inserted || 0,
|
|
|
|
|
|
updated: result.data?.updated || 0,
|
|
|
|
|
|
deleted: result.data?.deleted || 0,
|
2025-11-20 11:58:43 +09:00
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("그룹화된 데이터 UPSERT 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "데이터 저장 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 레코드 생성 API
|
|
|
|
|
|
* POST /api/data/{tableName}
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.post(
|
|
|
|
|
|
"/:tableName",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tableName } = req.params;
|
|
|
|
|
|
const data = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!tableName || typeof tableName !== "string") {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "테이블명이 필요합니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`➕ 레코드 생성: ${tableName}`, data);
|
|
|
|
|
|
|
2025-11-07 14:22:23 +09:00
|
|
|
|
// company_code와 company_name 자동 추가 (멀티테넌시)
|
|
|
|
|
|
const enrichedData = { ...data };
|
2025-11-21 03:40:41 +09:00
|
|
|
|
|
2025-11-07 14:22:23 +09:00
|
|
|
|
// 테이블에 company_code 컬럼이 있는지 확인하고 자동으로 추가
|
2025-11-21 03:40:41 +09:00
|
|
|
|
const hasCompanyCode = await dataService.checkColumnExists(
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
"company_code"
|
|
|
|
|
|
);
|
2025-11-07 14:22:23 +09:00
|
|
|
|
if (hasCompanyCode && req.user?.companyCode) {
|
|
|
|
|
|
enrichedData.company_code = req.user.companyCode;
|
|
|
|
|
|
console.log(`🏢 company_code 자동 추가: ${req.user.companyCode}`);
|
|
|
|
|
|
}
|
2025-11-21 03:40:41 +09:00
|
|
|
|
|
2025-11-07 14:22:23 +09:00
|
|
|
|
// 테이블에 company_name 컬럼이 있는지 확인하고 자동으로 추가
|
2025-11-21 03:40:41 +09:00
|
|
|
|
const hasCompanyName = await dataService.checkColumnExists(
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
"company_name"
|
|
|
|
|
|
);
|
2025-11-07 14:22:23 +09:00
|
|
|
|
if (hasCompanyName && req.user?.companyName) {
|
|
|
|
|
|
enrichedData.company_name = req.user.companyName;
|
|
|
|
|
|
console.log(`🏢 company_name 자동 추가: ${req.user.companyName}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
// 레코드 생성
|
2025-11-07 14:22:23 +09:00
|
|
|
|
const result = await dataService.createRecord(tableName, enrichedData);
|
2025-10-15 17:25:38 +09:00
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 레코드 생성 성공: ${tableName}`);
|
|
|
|
|
|
|
|
|
|
|
|
return res.status(201).json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: result.data,
|
|
|
|
|
|
message: "레코드가 생성되었습니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("레코드 생성 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "레코드 생성 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 레코드 수정 API
|
|
|
|
|
|
* PUT /api/data/{tableName}/{id}
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.put(
|
|
|
|
|
|
"/:tableName/:id",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tableName, id } = req.params;
|
|
|
|
|
|
const data = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!tableName || typeof tableName !== "string") {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "테이블명이 필요합니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✏️ 레코드 수정: ${tableName}/${id}`, data);
|
|
|
|
|
|
|
|
|
|
|
|
// 레코드 수정
|
|
|
|
|
|
const result = await dataService.updateRecord(tableName, id, data);
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 레코드 수정 성공: ${tableName}/${id}`);
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
data: result.data,
|
|
|
|
|
|
message: "레코드가 수정되었습니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("레코드 수정 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "레코드 수정 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 레코드 삭제 API
|
|
|
|
|
|
* DELETE /api/data/{tableName}/{id}
|
|
|
|
|
|
*/
|
2025-11-07 18:20:24 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 복합키 레코드 삭제 API (POST)
|
|
|
|
|
|
* POST /api/data/:tableName/delete
|
|
|
|
|
|
* Body: { user_id: 'xxx', dept_code: 'yyy' }
|
|
|
|
|
|
*/
|
|
|
|
|
|
router.post(
|
|
|
|
|
|
"/:tableName/delete",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tableName } = req.params;
|
|
|
|
|
|
const compositeKey = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!tableName || typeof tableName !== "string") {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "테이블명이 필요합니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🗑️ 복합키 레코드 삭제: ${tableName}`, compositeKey);
|
|
|
|
|
|
|
|
|
|
|
|
// 레코드 삭제 (복합키 객체 전달)
|
|
|
|
|
|
const result = await dataService.deleteRecord(tableName, compositeKey);
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 레코드 삭제 성공: ${tableName}`);
|
|
|
|
|
|
return res.json(result);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error(`레코드 삭제 오류 (${req.params.tableName}):`, error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "레코드 삭제 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-11-20 10:23:54 +09:00
|
|
|
|
/**
|
2025-11-20 11:58:43 +09:00
|
|
|
|
* 그룹 삭제 API
|
|
|
|
|
|
* POST /api/data/:tableName/delete-group
|
2025-11-20 10:23:54 +09:00
|
|
|
|
*/
|
|
|
|
|
|
router.post(
|
2025-11-20 11:58:43 +09:00
|
|
|
|
"/:tableName/delete-group",
|
2025-11-20 10:23:54 +09:00
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
2025-11-20 11:58:43 +09:00
|
|
|
|
const { tableName } = req.params;
|
|
|
|
|
|
const filterConditions = req.body;
|
2026-01-08 14:13:19 +09:00
|
|
|
|
const userCompany = req.user?.companyCode;
|
2025-11-20 10:23:54 +09:00
|
|
|
|
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 14:13:19 +09:00
|
|
|
|
console.log(`🗑️ 그룹 삭제:`, { tableName, filterConditions, userCompany });
|
2025-11-20 10:23:54 +09:00
|
|
|
|
|
2025-11-21 03:40:41 +09:00
|
|
|
|
const result = await dataService.deleteGroupRecords(
|
|
|
|
|
|
tableName,
|
2026-01-08 14:13:19 +09:00
|
|
|
|
filterConditions,
|
|
|
|
|
|
userCompany // 회사 코드 전달
|
2025-11-21 03:40:41 +09:00
|
|
|
|
);
|
2025-11-20 10:23:54 +09:00
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 11:58:43 +09:00
|
|
|
|
console.log(`✅ 그룹 삭제: ${result.data?.deleted}개`);
|
|
|
|
|
|
return res.json(result);
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error("그룹 삭제 오류:", error);
|
2025-11-20 10:23:54 +09:00
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
2025-11-20 11:58:43 +09:00
|
|
|
|
message: "그룹 삭제 실패",
|
|
|
|
|
|
error: error.message,
|
2025-11-20 10:23:54 +09:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-10-15 17:25:38 +09:00
|
|
|
|
router.delete(
|
|
|
|
|
|
"/:tableName/:id",
|
|
|
|
|
|
authenticateToken,
|
|
|
|
|
|
async (req: AuthenticatedRequest, res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { tableName, id } = req.params;
|
|
|
|
|
|
|
|
|
|
|
|
// 입력값 검증
|
|
|
|
|
|
if (!tableName || typeof tableName !== "string") {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "테이블명이 필요합니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
|
|
|
|
return res.status(400).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "유효하지 않은 테이블명입니다.",
|
|
|
|
|
|
error: "INVALID_TABLE_NAME",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`🗑️ 레코드 삭제: ${tableName}/${id}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 레코드 삭제
|
|
|
|
|
|
const result = await dataService.deleteRecord(tableName, id);
|
|
|
|
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
|
return res.status(400).json(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`✅ 레코드 삭제 성공: ${tableName}/${id}`);
|
|
|
|
|
|
|
|
|
|
|
|
return res.json({
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
message: "레코드가 삭제되었습니다.",
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("레코드 삭제 오류:", error);
|
|
|
|
|
|
return res.status(500).json({
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
message: "레코드 삭제 중 오류가 발생했습니다.",
|
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-12 16:47:02 +09:00
|
|
|
|
export default router;
|