ERP-node/backend-node/src/routes/dataRoutes.ts

577 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import express from "express";
import { dataService } from "../services/dataService";
import { authenticateToken } from "../middleware/authMiddleware";
import { AuthenticatedRequest } from "../types/auth";
const router = express.Router();
/**
* 조인 데이터 조회 API (다른 라우트보다 먼저 정의)
* GET /api/data/join?leftTable=...&rightTable=...&leftColumn=...&rightColumn=...&leftValue=...
*/
router.get(
"/join",
authenticateToken,
async (req: AuthenticatedRequest, res) => {
try {
const { leftTable, rightTable, leftColumn, rightColumn, leftValue, dataFilter } =
req.query;
// 입력값 검증
if (!leftTable || !rightTable || !leftColumn || !rightColumn) {
return res.status(400).json({
success: false,
message:
"필수 파라미터가 누락되었습니다 (leftTable, rightTable, leftColumn, rightColumn).",
error: "MISSING_PARAMETERS",
});
}
// dataFilter 파싱 (JSON 문자열로 전달됨)
let parsedDataFilter = undefined;
if (dataFilter && typeof dataFilter === "string") {
try {
parsedDataFilter = JSON.parse(dataFilter);
} catch (error) {
console.error("dataFilter 파싱 오류:", error);
}
}
// 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",
});
}
}
// 회사 코드 추출 (멀티테넌시 필터링)
const userCompany = req.user?.companyCode;
console.log(`🔗 조인 데이터 조회:`, {
leftTable,
rightTable,
leftColumn,
rightColumn,
leftValue,
userCompany,
dataFilter: parsedDataFilter, // 🆕 데이터 필터 로그
});
// 조인 데이터 조회 (회사 코드 + 데이터 필터 전달)
const result = await dataService.getJoinedData(
leftTable as string,
rightTable as string,
leftColumn as string,
rightColumn as string,
leftValue as string,
userCompany,
parsedDataFilter // 🆕 데이터 필터 전달
);
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",
});
}
}
);
/**
* 동적 테이블 데이터 조회 API
* GET /api/data/{tableName}
*/
router.get(
"/:tableName",
authenticateToken,
async (req: AuthenticatedRequest, res) => {
try {
const { tableName } = req.params;
const {
limit,
offset,
page,
size,
orderBy,
searchTerm,
sortBy,
sortOrder,
userLang,
...filters
} = req.query;
// 입력값 검증
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",
});
}
// 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;
}
console.log(`📊 데이터 조회 요청: ${tableName}`, {
limit: finalLimit,
offset: finalOffset,
orderBy: orderBy || sortBy,
searchTerm,
filters,
user: req.user?.userId,
});
// filters에서 searchTerm과 sortOrder 제거 (이미 별도로 처리됨)
const cleanFilters = { ...filters };
delete cleanFilters.searchTerm;
delete cleanFilters.sortOrder;
// 데이터 조회
const result = await dataService.getTableData({
tableName,
limit: finalLimit,
offset: finalOffset,
orderBy: (orderBy || sortBy) as string,
filters: cleanFilters as Record<string, string>,
userCompany: req.user?.companyCode,
});
if (!result.success) {
return res.status(400).json(result);
}
console.log(
`✅ 데이터 조회 성공: ${tableName}, ${result.data?.length || 0}개 항목`
);
// 페이징 정보 포함하여 반환
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,
});
} 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",
});
}
}
);
/**
* 레코드 상세 조회 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",
});
}
console.log(`🔍 레코드 상세 조회: ${tableName}/${id}`);
// 레코드 상세 조회
const result = await dataService.getRecordDetail(tableName, id);
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",
});
}
}
);
/**
* 레코드 생성 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);
// company_code와 company_name 자동 추가 (멀티테넌시)
const enrichedData = { ...data };
// 테이블에 company_code 컬럼이 있는지 확인하고 자동으로 추가
const hasCompanyCode = await dataService.checkColumnExists(tableName, "company_code");
if (hasCompanyCode && req.user?.companyCode) {
enrichedData.company_code = req.user.companyCode;
console.log(`🏢 company_code 자동 추가: ${req.user.companyCode}`);
}
// 테이블에 company_name 컬럼이 있는지 확인하고 자동으로 추가
const hasCompanyName = await dataService.checkColumnExists(tableName, "company_name");
if (hasCompanyName && req.user?.companyName) {
enrichedData.company_name = req.user.companyName;
console.log(`🏢 company_name 자동 추가: ${req.user.companyName}`);
}
// 레코드 생성
const result = await dataService.createRecord(tableName, enrichedData);
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}
*/
/**
* 복합키 레코드 삭제 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,
});
}
}
);
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",
});
}
}
);
export default router;