Files
vexplor_dev/backend-node/src/routes/dataflow/node-flows.ts
kjs 3f60f9ca3e fix(flow): 제어 실행 시 writer와 company_code 자동 입력 기능 추가
🐛 문제:
- 제어(플로우) 실행으로 데이터 INSERT 시 writer, company_code 컬럼이 비어있는 문제
- 플로우 실행 API에 인증이 없어 사용자 정보를 사용할 수 없었음

 해결:
1. 플로우 실행 API에 authenticateToken 미들웨어 추가
2. 사용자 정보(userId, userName, companyCode)를 contextData에 포함
3. INSERT 노드 실행 시 writer와 company_code 자동 추가
   - 필드 매핑에 없는 경우에만 자동 추가
   - writer: 현재 로그인한 사용자 ID
   - company_code: 현재 사용자의 회사 코드
   - 최고 관리자(companyCode = '*')는 제외

4. 플로우 제어 자동 감지 개선
   - flowConfig가 있으면 controlMode 없이도 플로우 모드로 인식
   - 데이터 미선택 시 명확한 오류 메시지 표시

🎯 영향:
- 입고처리, 출고처리 등 제어 기반 데이터 생성 시 멀티테넌시 보장
- 데이터 추적성 향상 (누가 생성했는지 자동 기록)

📝 수정 파일:
- frontend/lib/utils/buttonActions.ts
- backend-node/src/routes/dataflow/node-flows.ts
- backend-node/src/services/nodeFlowExecutionService.ts
2025-11-25 09:33:36 +09:00

264 lines
6.3 KiB
TypeScript

/**
* 노드 기반 데이터 플로우 API
*/
import { Router, Request, Response } from "express";
import { query, queryOne } from "../../database/db";
import { logger } from "../../utils/logger";
import { NodeFlowExecutionService } from "../../services/nodeFlowExecutionService";
import { AuthenticatedRequest } from "../../types/auth";
import { authenticateToken } from "../../middleware/authMiddleware";
const router = Router();
/**
* 플로우 목록 조회
*/
router.get("/", async (req: AuthenticatedRequest, res: Response) => {
try {
const userCompanyCode = req.user?.companyCode;
let sqlQuery = `
SELECT
flow_id as "flowId",
flow_name as "flowName",
flow_description as "flowDescription",
company_code as "companyCode",
created_at as "createdAt",
updated_at as "updatedAt"
FROM node_flows
`;
const params: any[] = [];
// 슈퍼 관리자가 아니면 회사별 필터링
if (userCompanyCode && userCompanyCode !== "*") {
sqlQuery += ` WHERE company_code = $1`;
params.push(userCompanyCode);
}
sqlQuery += ` ORDER BY updated_at DESC`;
const flows = await query(sqlQuery, params);
return res.json({
success: true,
data: flows,
});
} catch (error) {
logger.error("플로우 목록 조회 실패:", error);
return res.status(500).json({
success: false,
message: "플로우 목록을 조회하지 못했습니다.",
});
}
});
/**
* 플로우 상세 조회
*/
router.get("/:flowId", async (req: Request, res: Response) => {
try {
const { flowId } = req.params;
const flow = await queryOne(
`
SELECT
flow_id as "flowId",
flow_name as "flowName",
flow_description as "flowDescription",
flow_data as "flowData",
created_at as "createdAt",
updated_at as "updatedAt"
FROM node_flows
WHERE flow_id = $1
`,
[flowId]
);
if (!flow) {
return res.status(404).json({
success: false,
message: "플로우를 찾을 수 없습니다.",
});
}
return res.json({
success: true,
data: flow,
});
} catch (error) {
logger.error("플로우 조회 실패:", error);
return res.status(500).json({
success: false,
message: "플로우를 조회하지 못했습니다.",
});
}
});
/**
* 플로우 저장 (신규)
*/
router.post("/", async (req: AuthenticatedRequest, res: Response) => {
try {
const { flowName, flowDescription, flowData } = req.body;
const userCompanyCode = req.user?.companyCode || "*";
if (!flowName || !flowData) {
return res.status(400).json({
success: false,
message: "플로우 이름과 데이터는 필수입니다.",
});
}
const result = await queryOne(
`
INSERT INTO node_flows (flow_name, flow_description, flow_data, company_code)
VALUES ($1, $2, $3, $4)
RETURNING flow_id as "flowId"
`,
[flowName, flowDescription || "", flowData, userCompanyCode]
);
logger.info(
`플로우 저장 성공: ${result.flowId} (회사: ${userCompanyCode})`
);
return res.json({
success: true,
message: "플로우가 저장되었습니다.",
data: {
flowId: result.flowId,
},
});
} catch (error) {
logger.error("플로우 저장 실패:", error);
return res.status(500).json({
success: false,
message: "플로우를 저장하지 못했습니다.",
});
}
});
/**
* 플로우 수정
*/
router.put("/", async (req: Request, res: Response) => {
try {
const { flowId, flowName, flowDescription, flowData } = req.body;
if (!flowId || !flowName || !flowData) {
return res.status(400).json({
success: false,
message: "플로우 ID, 이름, 데이터는 필수입니다.",
});
}
await query(
`
UPDATE node_flows
SET flow_name = $1,
flow_description = $2,
flow_data = $3,
updated_at = NOW()
WHERE flow_id = $4
`,
[flowName, flowDescription || "", flowData, flowId]
);
logger.info(`플로우 수정 성공: ${flowId}`);
return res.json({
success: true,
message: "플로우가 수정되었습니다.",
data: {
flowId,
},
});
} catch (error) {
logger.error("플로우 수정 실패:", error);
return res.status(500).json({
success: false,
message: "플로우를 수정하지 못했습니다.",
});
}
});
/**
* 플로우 삭제
*/
router.delete("/:flowId", async (req: Request, res: Response) => {
try {
const { flowId } = req.params;
await query(
`
DELETE FROM node_flows
WHERE flow_id = $1
`,
[flowId]
);
logger.info(`플로우 삭제 성공: ${flowId}`);
return res.json({
success: true,
message: "플로우가 삭제되었습니다.",
});
} catch (error) {
logger.error("플로우 삭제 실패:", error);
return res.status(500).json({
success: false,
message: "플로우를 삭제하지 못했습니다.",
});
}
});
/**
* 플로우 실행
* POST /api/dataflow/node-flows/:flowId/execute
*/
router.post("/:flowId/execute", authenticateToken, async (req: AuthenticatedRequest, res: Response) => {
try {
const { flowId } = req.params;
const contextData = req.body;
logger.info(`🚀 플로우 실행 요청: flowId=${flowId}`, {
contextDataKeys: Object.keys(contextData),
userId: req.user?.userId,
companyCode: req.user?.companyCode,
});
// 사용자 정보를 contextData에 추가
const enrichedContextData = {
...contextData,
userId: req.user?.userId,
userName: req.user?.userName,
companyCode: req.user?.companyCode,
};
// 플로우 실행
const result = await NodeFlowExecutionService.executeFlow(
parseInt(flowId, 10),
enrichedContextData
);
return res.json({
success: result.success,
message: result.message,
data: result,
});
} catch (error) {
logger.error("플로우 실행 실패:", error);
return res.status(500).json({
success: false,
message:
error instanceof Error
? error.message
: "플로우 실행 중 오류가 발생했습니다.",
});
}
});
export default router;