Merge branch 'dev' of http://39.117.244.52:3000/kjs/ERP-node into feature/screen-management
This commit is contained in:
@@ -32,6 +32,7 @@ import dataRoutes from "./routes/dataRoutes";
|
||||
import testButtonDataflowRoutes from "./routes/testButtonDataflowRoutes";
|
||||
import externalDbConnectionRoutes from "./routes/externalDbConnectionRoutes";
|
||||
import ddlRoutes from "./routes/ddlRoutes";
|
||||
import entityReferenceRoutes from "./routes/entityReferenceRoutes";
|
||||
// import userRoutes from './routes/userRoutes';
|
||||
// import menuRoutes from './routes/menuRoutes';
|
||||
|
||||
@@ -127,6 +128,7 @@ app.use("/api/data", dataRoutes);
|
||||
app.use("/api/test-button-dataflow", testButtonDataflowRoutes);
|
||||
app.use("/api/external-db-connections", externalDbConnectionRoutes);
|
||||
app.use("/api/ddl", ddlRoutes);
|
||||
app.use("/api/entity-reference", entityReferenceRoutes);
|
||||
// app.use('/api/users', userRoutes);
|
||||
// app.use('/api/menus', menuRoutes);
|
||||
|
||||
|
||||
@@ -143,16 +143,36 @@ export const createDataflowDiagram = async (req: Request, res: Response) => {
|
||||
message: "관계도가 성공적으로 생성되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("관계도 생성 실패:", error);
|
||||
// 디버깅을 위한 에러 정보 출력
|
||||
logger.error("에러 디버깅:", {
|
||||
errorType: typeof error,
|
||||
errorCode: (error as any)?.code,
|
||||
errorMessage: error instanceof Error ? error.message : "Unknown error",
|
||||
errorName: (error as any)?.name,
|
||||
errorMeta: (error as any)?.meta,
|
||||
});
|
||||
|
||||
// 중복 이름 에러 처리
|
||||
if (error instanceof Error && error.message.includes("unique constraint")) {
|
||||
// 중복 이름 에러인지 먼저 확인 (로그 출력 전에)
|
||||
const isDuplicateError =
|
||||
(error && typeof error === "object" && (error as any).code === "P2002") || // Prisma unique constraint error code
|
||||
(error instanceof Error &&
|
||||
(error.message.includes("unique constraint") ||
|
||||
error.message.includes("Unique constraint") ||
|
||||
error.message.includes("duplicate key") ||
|
||||
error.message.includes("UNIQUE constraint failed") ||
|
||||
error.message.includes("unique_diagram_name_per_company")));
|
||||
|
||||
if (isDuplicateError) {
|
||||
// 중복 에러는 콘솔에 로그 출력하지 않음
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "이미 존재하는 관계도 이름입니다.",
|
||||
message: "중복된 이름입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 다른 에러만 로그 출력
|
||||
logger.error("관계도 생성 실패:", error);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "관계도 생성 중 오류가 발생했습니다.",
|
||||
@@ -214,6 +234,25 @@ export const updateDataflowDiagram = async (req: Request, res: Response) => {
|
||||
message: "관계도가 성공적으로 수정되었습니다.",
|
||||
});
|
||||
} catch (error) {
|
||||
// 중복 이름 에러인지 먼저 확인 (로그 출력 전에)
|
||||
const isDuplicateError =
|
||||
(error && typeof error === "object" && (error as any).code === "P2002") || // Prisma unique constraint error code
|
||||
(error instanceof Error &&
|
||||
(error.message.includes("unique constraint") ||
|
||||
error.message.includes("Unique constraint") ||
|
||||
error.message.includes("duplicate key") ||
|
||||
error.message.includes("UNIQUE constraint failed") ||
|
||||
error.message.includes("unique_diagram_name_per_company")));
|
||||
|
||||
if (isDuplicateError) {
|
||||
// 중복 에러는 콘솔에 로그 출력하지 않음
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "중복된 이름입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 다른 에러만 로그 출력
|
||||
logger.error("관계도 수정 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
|
||||
208
backend-node/src/controllers/entityReferenceController.ts
Normal file
208
backend-node/src/controllers/entityReferenceController.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { Request, Response } from "express";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface EntityReferenceOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface EntityReferenceData {
|
||||
options: EntityReferenceOption[];
|
||||
referenceInfo: {
|
||||
referenceTable: string;
|
||||
referenceColumn: string;
|
||||
displayColumn: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CodeReferenceData {
|
||||
options: EntityReferenceOption[];
|
||||
codeCategory: string;
|
||||
}
|
||||
|
||||
export class EntityReferenceController {
|
||||
/**
|
||||
* 엔티티 참조 데이터 조회
|
||||
* GET /api/entity-reference/:tableName/:columnName
|
||||
*/
|
||||
static async getEntityReferenceData(req: Request, res: Response) {
|
||||
try {
|
||||
const { tableName, columnName } = req.params;
|
||||
const { limit = 100, search } = req.query;
|
||||
|
||||
logger.info(`엔티티 참조 데이터 조회 요청: ${tableName}.${columnName}`, {
|
||||
limit,
|
||||
search,
|
||||
});
|
||||
|
||||
// 컬럼 정보 조회
|
||||
const columnInfo = await prisma.column_labels.findFirst({
|
||||
where: {
|
||||
table_name: tableName,
|
||||
column_name: columnName,
|
||||
},
|
||||
});
|
||||
|
||||
if (!columnInfo) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: `컬럼 정보를 찾을 수 없습니다: ${tableName}.${columnName}`,
|
||||
});
|
||||
}
|
||||
|
||||
// webType 확인
|
||||
if (columnInfo.web_type !== "entity") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `컬럼 '${tableName}.${columnName}'은 entity 타입이 아닙니다. webType: ${columnInfo.web_type}`,
|
||||
});
|
||||
}
|
||||
|
||||
// column_labels에서 직접 참조 정보 가져오기
|
||||
const referenceTable = columnInfo.reference_table;
|
||||
const referenceColumn = columnInfo.reference_column;
|
||||
const displayColumn = columnInfo.display_column || "name";
|
||||
|
||||
// entity 타입인데 참조 테이블 정보가 없으면 오류
|
||||
if (!referenceTable || !referenceColumn) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `Entity 타입 컬럼 '${tableName}.${columnName}'에 참조 테이블 정보가 설정되지 않았습니다. column_labels에서 reference_table과 reference_column을 확인해주세요.`,
|
||||
});
|
||||
}
|
||||
|
||||
// 참조 테이블이 실제로 존재하는지 확인
|
||||
try {
|
||||
await prisma.$queryRawUnsafe(`SELECT 1 FROM ${referenceTable} LIMIT 1`);
|
||||
logger.info(
|
||||
`Entity 참조 설정: ${tableName}.${columnName} -> ${referenceTable}.${referenceColumn} (display: ${displayColumn})`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`참조 테이블 '${referenceTable}'이 존재하지 않습니다:`,
|
||||
error
|
||||
);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `참조 테이블 '${referenceTable}'이 존재하지 않습니다. column_labels의 reference_table 설정을 확인해주세요.`,
|
||||
});
|
||||
}
|
||||
|
||||
// 동적 쿼리로 참조 데이터 조회
|
||||
let query = `SELECT ${referenceColumn}, ${displayColumn} as display_name FROM ${referenceTable}`;
|
||||
const queryParams: any[] = [];
|
||||
|
||||
// 검색 조건 추가
|
||||
if (search) {
|
||||
query += ` WHERE ${displayColumn} ILIKE $1`;
|
||||
queryParams.push(`%${search}%`);
|
||||
}
|
||||
|
||||
query += ` ORDER BY ${displayColumn} LIMIT $${queryParams.length + 1}`;
|
||||
queryParams.push(Number(limit));
|
||||
|
||||
logger.info(`실행할 쿼리: ${query}`, {
|
||||
queryParams,
|
||||
referenceTable,
|
||||
referenceColumn,
|
||||
displayColumn,
|
||||
});
|
||||
|
||||
const referenceData = await prisma.$queryRawUnsafe(query, ...queryParams);
|
||||
|
||||
// 옵션 형태로 변환
|
||||
const options: EntityReferenceOption[] = (referenceData as any[]).map(
|
||||
(row) => ({
|
||||
value: String(row[referenceColumn]),
|
||||
label: String(row.display_name || row[referenceColumn]),
|
||||
})
|
||||
);
|
||||
|
||||
logger.info(`엔티티 참조 데이터 조회 완료: ${options.length}개 항목`);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
options,
|
||||
referenceInfo: {
|
||||
referenceTable,
|
||||
referenceColumn,
|
||||
displayColumn,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("엔티티 참조 데이터 조회 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "엔티티 참조 데이터 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 코드 데이터 조회
|
||||
* GET /api/entity-reference/code/:codeCategory
|
||||
*/
|
||||
static async getCodeData(req: Request, res: Response) {
|
||||
try {
|
||||
const { codeCategory } = req.params;
|
||||
const { limit = 100, search } = req.query;
|
||||
|
||||
logger.info(`공통 코드 데이터 조회 요청: ${codeCategory}`, {
|
||||
limit,
|
||||
search,
|
||||
});
|
||||
|
||||
// code_info 테이블에서 코드 데이터 조회
|
||||
let whereCondition: any = {
|
||||
code_category: codeCategory,
|
||||
is_active: "Y",
|
||||
};
|
||||
|
||||
if (search) {
|
||||
whereCondition.code_name = {
|
||||
contains: String(search),
|
||||
mode: "insensitive",
|
||||
};
|
||||
}
|
||||
|
||||
const codeData = await prisma.code_info.findMany({
|
||||
where: whereCondition,
|
||||
select: {
|
||||
code_value: true,
|
||||
code_name: true,
|
||||
},
|
||||
orderBy: {
|
||||
code_name: "asc",
|
||||
},
|
||||
take: Number(limit),
|
||||
});
|
||||
|
||||
// 옵션 형태로 변환
|
||||
const options: EntityReferenceOption[] = codeData.map((code) => ({
|
||||
value: code.code_value,
|
||||
label: code.code_name,
|
||||
}));
|
||||
|
||||
logger.info(`공통 코드 데이터 조회 완료: ${options.length}개 항목`);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
options,
|
||||
codeCategory,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("공통 코드 데이터 조회 실패:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: "공통 코드 데이터 조회 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
27
backend-node/src/routes/entityReferenceRoutes.ts
Normal file
27
backend-node/src/routes/entityReferenceRoutes.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Router } from "express";
|
||||
import { EntityReferenceController } from "../controllers/entityReferenceController";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* GET /api/entity-reference/code/:codeCategory
|
||||
* 공통 코드 데이터 조회
|
||||
*/
|
||||
router.get(
|
||||
"/code/:codeCategory",
|
||||
authenticateToken,
|
||||
EntityReferenceController.getCodeData
|
||||
);
|
||||
|
||||
/**
|
||||
* GET /api/entity-reference/:tableName/:columnName
|
||||
* 엔티티 참조 데이터 조회
|
||||
*/
|
||||
router.get(
|
||||
"/:tableName/:columnName",
|
||||
authenticateToken,
|
||||
EntityReferenceController.getEntityReferenceData
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -19,6 +19,7 @@ export interface ControlAction {
|
||||
id: string;
|
||||
name: string;
|
||||
actionType: "insert" | "update" | "delete";
|
||||
logicalOperator?: "AND" | "OR"; // 액션 간 논리 연산자 (첫 번째 액션 제외)
|
||||
conditions: ControlCondition[];
|
||||
fieldMappings: {
|
||||
sourceField?: string;
|
||||
@@ -136,17 +137,41 @@ export class DataflowControlService {
|
||||
};
|
||||
}
|
||||
|
||||
// 액션 실행
|
||||
// 액션 실행 (논리 연산자 지원)
|
||||
const executedActions = [];
|
||||
const errors = [];
|
||||
let previousActionSuccess = false;
|
||||
let shouldSkipRemainingActions = false;
|
||||
|
||||
for (let i = 0; i < targetPlan.actions.length; i++) {
|
||||
const action = targetPlan.actions[i];
|
||||
|
||||
for (const action of targetPlan.actions) {
|
||||
try {
|
||||
// 논리 연산자에 따른 실행 여부 결정
|
||||
if (
|
||||
i > 0 &&
|
||||
action.logicalOperator === "OR" &&
|
||||
previousActionSuccess
|
||||
) {
|
||||
console.log(
|
||||
`⏭️ OR 조건으로 인해 액션 건너뛰기: ${action.name} (이전 액션 성공)`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldSkipRemainingActions && action.logicalOperator === "AND") {
|
||||
console.log(
|
||||
`⏭️ 이전 액션 실패로 인해 AND 체인 액션 건너뛰기: ${action.name}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`⚡ 액션 실행: ${action.name} (${action.actionType})`);
|
||||
console.log(`📋 액션 상세 정보:`, {
|
||||
actionId: action.id,
|
||||
actionName: action.name,
|
||||
actionType: action.actionType,
|
||||
logicalOperator: action.logicalOperator,
|
||||
conditions: action.conditions,
|
||||
fieldMappings: action.fieldMappings,
|
||||
});
|
||||
@@ -163,6 +188,10 @@ export class DataflowControlService {
|
||||
console.log(
|
||||
`⚠️ 액션 조건 미충족: ${actionConditionResult.reason}`
|
||||
);
|
||||
previousActionSuccess = false;
|
||||
if (action.logicalOperator === "AND") {
|
||||
shouldSkipRemainingActions = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -173,11 +202,19 @@ export class DataflowControlService {
|
||||
actionName: action.name,
|
||||
result: actionResult,
|
||||
});
|
||||
|
||||
previousActionSuccess = true;
|
||||
shouldSkipRemainingActions = false; // 성공했으므로 다시 실행 가능
|
||||
} catch (error) {
|
||||
console.error(`❌ 액션 실행 오류: ${action.name}`, error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
errors.push(`액션 '${action.name}' 실행 오류: ${errorMessage}`);
|
||||
|
||||
previousActionSuccess = false;
|
||||
if (action.logicalOperator === "AND") {
|
||||
shouldSkipRemainingActions = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user