feat: Implement company code validation in flow management
- Enhanced the FlowController to include user company code validation for flow definitions, ensuring that users can only access and modify flows belonging to their company. - Updated the FlowDefinitionService to accept company code as a parameter for create, update, and delete operations, enforcing ownership checks. - Introduced sanitization methods in FlowConditionParser to prevent SQL injection for column and table names. - Modified the FlowDataMoveService to validate table names and column names during data movement operations, enhancing security. - Updated the frontend components to support batch data movement with proper validation and error handling.
This commit is contained in:
parent
e2d88f01e3
commit
fd5c61b12a
|
|
@ -144,8 +144,9 @@ export class FlowController {
|
|||
try {
|
||||
const { id } = req.params;
|
||||
const flowId = parseInt(id);
|
||||
const userCompanyCode = (req as any).user?.companyCode;
|
||||
|
||||
const definition = await this.flowDefinitionService.findById(flowId);
|
||||
const definition = await this.flowDefinitionService.findById(flowId, userCompanyCode);
|
||||
if (!definition) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
|
|
@ -182,12 +183,13 @@ export class FlowController {
|
|||
const { id } = req.params;
|
||||
const flowId = parseInt(id);
|
||||
const { name, description, isActive } = req.body;
|
||||
const userCompanyCode = (req as any).user?.companyCode;
|
||||
|
||||
const flowDef = await this.flowDefinitionService.update(flowId, {
|
||||
name,
|
||||
description,
|
||||
isActive,
|
||||
});
|
||||
}, userCompanyCode);
|
||||
|
||||
if (!flowDef) {
|
||||
res.status(404).json({
|
||||
|
|
@ -217,8 +219,9 @@ export class FlowController {
|
|||
try {
|
||||
const { id } = req.params;
|
||||
const flowId = parseInt(id);
|
||||
const userCompanyCode = (req as any).user?.companyCode;
|
||||
|
||||
const success = await this.flowDefinitionService.delete(flowId);
|
||||
const success = await this.flowDefinitionService.delete(flowId, userCompanyCode);
|
||||
|
||||
if (!success) {
|
||||
res.status(404).json({
|
||||
|
|
@ -275,6 +278,7 @@ export class FlowController {
|
|||
try {
|
||||
const { flowId } = req.params;
|
||||
const flowDefinitionId = parseInt(flowId);
|
||||
const userCompanyCode = (req as any).user?.companyCode;
|
||||
const {
|
||||
stepName,
|
||||
stepOrder,
|
||||
|
|
@ -293,6 +297,16 @@ export class FlowController {
|
|||
return;
|
||||
}
|
||||
|
||||
// 플로우 소유권 검증
|
||||
const flowDef = await this.flowDefinitionService.findById(flowDefinitionId, userCompanyCode);
|
||||
if (!flowDef) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "Flow definition not found or access denied",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const step = await this.flowStepService.create({
|
||||
flowDefinitionId,
|
||||
stepName,
|
||||
|
|
@ -324,6 +338,7 @@ export class FlowController {
|
|||
try {
|
||||
const { stepId } = req.params;
|
||||
const id = parseInt(stepId);
|
||||
const userCompanyCode = (req as any).user?.companyCode;
|
||||
const {
|
||||
stepName,
|
||||
stepOrder,
|
||||
|
|
@ -342,6 +357,19 @@ export class FlowController {
|
|||
displayConfig,
|
||||
} = req.body;
|
||||
|
||||
// 스텝 소유권 검증: 스텝이 속한 플로우가 사용자 회사 소유인지 확인
|
||||
const existingStep = await this.flowStepService.findById(id);
|
||||
if (existingStep) {
|
||||
const flowDef = await this.flowDefinitionService.findById(existingStep.flowDefinitionId, userCompanyCode);
|
||||
if (!flowDef) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: "Access denied: flow does not belong to your company",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const step = await this.flowStepService.update(id, {
|
||||
stepName,
|
||||
stepOrder,
|
||||
|
|
@ -388,6 +416,20 @@ export class FlowController {
|
|||
try {
|
||||
const { stepId } = req.params;
|
||||
const id = parseInt(stepId);
|
||||
const userCompanyCode = (req as any).user?.companyCode;
|
||||
|
||||
// 스텝 소유권 검증
|
||||
const existingStep = await this.flowStepService.findById(id);
|
||||
if (existingStep) {
|
||||
const flowDef = await this.flowDefinitionService.findById(existingStep.flowDefinitionId, userCompanyCode);
|
||||
if (!flowDef) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: "Access denied: flow does not belong to your company",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const success = await this.flowStepService.delete(id);
|
||||
|
||||
|
|
@ -446,6 +488,7 @@ export class FlowController {
|
|||
createConnection = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { flowDefinitionId, fromStepId, toStepId, label } = req.body;
|
||||
const userCompanyCode = (req as any).user?.companyCode;
|
||||
|
||||
if (!flowDefinitionId || !fromStepId || !toStepId) {
|
||||
res.status(400).json({
|
||||
|
|
@ -455,6 +498,28 @@ export class FlowController {
|
|||
return;
|
||||
}
|
||||
|
||||
// 플로우 소유권 검증
|
||||
const flowDef = await this.flowDefinitionService.findById(flowDefinitionId, userCompanyCode);
|
||||
if (!flowDef) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: "Flow definition not found or access denied",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// fromStepId, toStepId가 해당 flow에 속하는지 검증
|
||||
const fromStep = await this.flowStepService.findById(fromStepId);
|
||||
const toStep = await this.flowStepService.findById(toStepId);
|
||||
if (!fromStep || fromStep.flowDefinitionId !== flowDefinitionId ||
|
||||
!toStep || toStep.flowDefinitionId !== flowDefinitionId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "fromStepId and toStepId must belong to the specified flow",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = await this.flowConnectionService.create({
|
||||
flowDefinitionId,
|
||||
fromStepId,
|
||||
|
|
@ -482,6 +547,20 @@ export class FlowController {
|
|||
try {
|
||||
const { connectionId } = req.params;
|
||||
const id = parseInt(connectionId);
|
||||
const userCompanyCode = (req as any).user?.companyCode;
|
||||
|
||||
// 연결 소유권 검증
|
||||
const existingConn = await this.flowConnectionService.findById(id);
|
||||
if (existingConn) {
|
||||
const flowDef = await this.flowDefinitionService.findById(existingConn.flowDefinitionId, userCompanyCode);
|
||||
if (!flowDef) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
message: "Access denied: flow does not belong to your company",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const success = await this.flowConnectionService.delete(id);
|
||||
|
||||
|
|
@ -670,23 +749,24 @@ export class FlowController {
|
|||
*/
|
||||
moveData = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { flowId, recordId, toStepId, note } = req.body;
|
||||
const { flowId, fromStepId, recordId, toStepId, note } = req.body;
|
||||
const userId = (req as any).user?.userId || "system";
|
||||
|
||||
if (!flowId || !recordId || !toStepId) {
|
||||
if (!flowId || !fromStepId || !recordId || !toStepId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: "flowId, recordId, and toStepId are required",
|
||||
message: "flowId, fromStepId, recordId, and toStepId are required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await this.flowDataMoveService.moveDataToStep(
|
||||
flowId,
|
||||
recordId,
|
||||
fromStepId,
|
||||
toStepId,
|
||||
recordId,
|
||||
userId,
|
||||
note
|
||||
note ? { note } : undefined
|
||||
);
|
||||
|
||||
res.json({
|
||||
|
|
|
|||
|
|
@ -132,14 +132,23 @@ export class FlowConditionParser {
|
|||
/**
|
||||
* SQL 인젝션 방지를 위한 컬럼명 검증
|
||||
*/
|
||||
private static sanitizeColumnName(columnName: string): string {
|
||||
// 알파벳, 숫자, 언더스코어, 점(.)만 허용 (테이블명.컬럼명 형태 지원)
|
||||
static sanitizeColumnName(columnName: string): string {
|
||||
if (!/^[a-zA-Z0-9_.]+$/.test(columnName)) {
|
||||
throw new Error(`Invalid column name: ${columnName}`);
|
||||
}
|
||||
return columnName;
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL 인젝션 방지를 위한 테이블명 검증
|
||||
*/
|
||||
static sanitizeTableName(tableName: string): string {
|
||||
if (!/^[a-zA-Z0-9_.]+$/.test(tableName)) {
|
||||
throw new Error(`Invalid table name: ${tableName}`);
|
||||
}
|
||||
return tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 조건 검증
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
buildInsertQuery,
|
||||
buildSelectQuery,
|
||||
} from "./dbQueryBuilder";
|
||||
import { FlowConditionParser } from "./flowConditionParser";
|
||||
|
||||
export class FlowDataMoveService {
|
||||
private flowDefinitionService: FlowDefinitionService;
|
||||
|
|
@ -236,18 +237,19 @@ export class FlowDataMoveService {
|
|||
);
|
||||
}
|
||||
|
||||
const statusColumn = toStep.statusColumn;
|
||||
const tableName = fromStep.tableName;
|
||||
const statusColumn = FlowConditionParser.sanitizeColumnName(toStep.statusColumn);
|
||||
const tableName = FlowConditionParser.sanitizeTableName(fromStep.tableName);
|
||||
|
||||
// 추가 필드 업데이트 준비
|
||||
const updates: string[] = [`${statusColumn} = $2`, `updated_at = NOW()`];
|
||||
const values: any[] = [dataId, toStep.statusValue];
|
||||
let paramIndex = 3;
|
||||
|
||||
// 추가 데이터가 있으면 함께 업데이트
|
||||
// 추가 데이터가 있으면 함께 업데이트 (키 검증 포함)
|
||||
if (additionalData) {
|
||||
for (const [key, value] of Object.entries(additionalData)) {
|
||||
updates.push(`${key} = $${paramIndex}`);
|
||||
const safeKey = FlowConditionParser.sanitizeColumnName(key);
|
||||
updates.push(`${safeKey} = $${paramIndex}`);
|
||||
values.push(value);
|
||||
paramIndex++;
|
||||
}
|
||||
|
|
@ -276,33 +278,38 @@ export class FlowDataMoveService {
|
|||
dataId: any,
|
||||
additionalData?: Record<string, any>
|
||||
): Promise<any> {
|
||||
const sourceTable = fromStep.tableName;
|
||||
const targetTable = toStep.targetTable || toStep.tableName;
|
||||
const sourceTable = FlowConditionParser.sanitizeTableName(fromStep.tableName);
|
||||
const targetTable = FlowConditionParser.sanitizeTableName(toStep.targetTable || toStep.tableName);
|
||||
const fieldMappings = toStep.fieldMappings || {};
|
||||
|
||||
// 1. 소스 데이터 조회
|
||||
const selectQuery = `SELECT * FROM ${sourceTable} WHERE id = $1`;
|
||||
const sourceResult = await client.query(selectQuery, [dataId]);
|
||||
|
||||
if (sourceResult.length === 0) {
|
||||
if (sourceResult.rows.length === 0) {
|
||||
throw new Error(`소스 데이터를 찾을 수 없습니다: ${dataId}`);
|
||||
}
|
||||
|
||||
const sourceData = sourceResult[0];
|
||||
const sourceData = sourceResult.rows[0];
|
||||
|
||||
// 2. 필드 매핑 적용
|
||||
const mappedData: Record<string, any> = {};
|
||||
|
||||
// 매핑 정의가 있으면 적용
|
||||
// 매핑 정의가 있으면 적용 (컬럼명 검증)
|
||||
for (const [sourceField, targetField] of Object.entries(fieldMappings)) {
|
||||
FlowConditionParser.sanitizeColumnName(sourceField);
|
||||
FlowConditionParser.sanitizeColumnName(targetField as string);
|
||||
if (sourceData[sourceField] !== undefined) {
|
||||
mappedData[targetField as string] = sourceData[sourceField];
|
||||
}
|
||||
}
|
||||
|
||||
// 추가 데이터 병합
|
||||
// 추가 데이터 병합 (키 검증)
|
||||
if (additionalData) {
|
||||
Object.assign(mappedData, additionalData);
|
||||
for (const [key, value] of Object.entries(additionalData)) {
|
||||
const safeKey = FlowConditionParser.sanitizeColumnName(key);
|
||||
mappedData[safeKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 타겟 테이블에 데이터 삽입
|
||||
|
|
@ -321,7 +328,7 @@ export class FlowDataMoveService {
|
|||
`;
|
||||
|
||||
const insertResult = await client.query(insertQuery, values);
|
||||
return insertResult[0].id;
|
||||
return insertResult.rows[0].id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -349,12 +356,12 @@ export class FlowDataMoveService {
|
|||
]);
|
||||
|
||||
const stepDataMap: Record<string, string> =
|
||||
mappingResult.length > 0 ? mappingResult[0].step_data_map : {};
|
||||
mappingResult.rows.length > 0 ? mappingResult.rows[0].step_data_map : {};
|
||||
|
||||
// 새 단계 데이터 추가
|
||||
stepDataMap[String(currentStepId)] = String(targetDataId);
|
||||
|
||||
if (mappingResult.length > 0) {
|
||||
if (mappingResult.rows.length > 0) {
|
||||
// 기존 매핑 업데이트
|
||||
const updateQuery = `
|
||||
UPDATE flow_data_mapping
|
||||
|
|
@ -366,7 +373,7 @@ export class FlowDataMoveService {
|
|||
await client.query(updateQuery, [
|
||||
currentStepId,
|
||||
JSON.stringify(stepDataMap),
|
||||
mappingResult[0].id,
|
||||
mappingResult.rows[0].id,
|
||||
]);
|
||||
} else {
|
||||
// 새 매핑 생성
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ export class FlowDefinitionService {
|
|||
userId: string,
|
||||
userCompanyCode?: string
|
||||
): Promise<FlowDefinition> {
|
||||
const companyCode = request.companyCode || userCompanyCode || "*";
|
||||
// 클라이언트 입력(request.companyCode) 무시 - 인증된 사용자의 회사 코드만 사용
|
||||
const companyCode = userCompanyCode || "*";
|
||||
|
||||
console.log("🔥 flowDefinitionService.create called with:", {
|
||||
name: request.name,
|
||||
|
|
@ -118,10 +119,21 @@ export class FlowDefinitionService {
|
|||
|
||||
/**
|
||||
* 플로우 정의 단일 조회
|
||||
* companyCode가 전달되면 해당 회사 소유 플로우만 반환
|
||||
*/
|
||||
async findById(id: number): Promise<FlowDefinition | null> {
|
||||
const query = "SELECT * FROM flow_definition WHERE id = $1";
|
||||
const result = await db.query(query, [id]);
|
||||
async findById(id: number, companyCode?: string): Promise<FlowDefinition | null> {
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode && companyCode !== "*") {
|
||||
query = "SELECT * FROM flow_definition WHERE id = $1 AND company_code = $2";
|
||||
params = [id, companyCode];
|
||||
} else {
|
||||
query = "SELECT * FROM flow_definition WHERE id = $1";
|
||||
params = [id];
|
||||
}
|
||||
|
||||
const result = await db.query(query, params);
|
||||
|
||||
if (result.length === 0) {
|
||||
return null;
|
||||
|
|
@ -132,10 +144,12 @@ export class FlowDefinitionService {
|
|||
|
||||
/**
|
||||
* 플로우 정의 수정
|
||||
* companyCode가 전달되면 해당 회사 소유 플로우만 수정 가능
|
||||
*/
|
||||
async update(
|
||||
id: number,
|
||||
request: UpdateFlowDefinitionRequest
|
||||
request: UpdateFlowDefinitionRequest,
|
||||
companyCode?: string
|
||||
): Promise<FlowDefinition | null> {
|
||||
const fields: string[] = [];
|
||||
const params: any[] = [];
|
||||
|
|
@ -160,18 +174,27 @@ export class FlowDefinitionService {
|
|||
}
|
||||
|
||||
if (fields.length === 0) {
|
||||
return this.findById(id);
|
||||
return this.findById(id, companyCode);
|
||||
}
|
||||
|
||||
fields.push(`updated_at = NOW()`);
|
||||
|
||||
let whereClause = `WHERE id = $${paramIndex}`;
|
||||
params.push(id);
|
||||
paramIndex++;
|
||||
|
||||
if (companyCode && companyCode !== "*") {
|
||||
whereClause += ` AND company_code = $${paramIndex}`;
|
||||
params.push(companyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const query = `
|
||||
UPDATE flow_definition
|
||||
SET ${fields.join(", ")}
|
||||
WHERE id = $${paramIndex}
|
||||
${whereClause}
|
||||
RETURNING *
|
||||
`;
|
||||
params.push(id);
|
||||
|
||||
const result = await db.query(query, params);
|
||||
|
||||
|
|
@ -184,10 +207,21 @@ export class FlowDefinitionService {
|
|||
|
||||
/**
|
||||
* 플로우 정의 삭제
|
||||
* companyCode가 전달되면 해당 회사 소유 플로우만 삭제 가능
|
||||
*/
|
||||
async delete(id: number): Promise<boolean> {
|
||||
const query = "DELETE FROM flow_definition WHERE id = $1 RETURNING id";
|
||||
const result = await db.query(query, [id]);
|
||||
async delete(id: number, companyCode?: string): Promise<boolean> {
|
||||
let query: string;
|
||||
let params: any[];
|
||||
|
||||
if (companyCode && companyCode !== "*") {
|
||||
query = "DELETE FROM flow_definition WHERE id = $1 AND company_code = $2 RETURNING id";
|
||||
params = [id, companyCode];
|
||||
} else {
|
||||
query = "DELETE FROM flow_definition WHERE id = $1 RETURNING id";
|
||||
params = [id];
|
||||
}
|
||||
|
||||
const result = await db.query(query, params);
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { FlowStepService } from "./flowStepService";
|
|||
import { FlowConditionParser } from "./flowConditionParser";
|
||||
import { executeExternalQuery } from "./externalDbHelper";
|
||||
import { getPlaceholder, buildPaginationClause } from "./dbQueryBuilder";
|
||||
import { FlowConditionParser } from "./flowConditionParser";
|
||||
|
||||
export class FlowExecutionService {
|
||||
private flowDefinitionService: FlowDefinitionService;
|
||||
|
|
@ -42,7 +43,7 @@ export class FlowExecutionService {
|
|||
}
|
||||
|
||||
// 3. 테이블명 결정: 단계에 지정된 테이블이 있으면 사용, 없으면 플로우의 기본 테이블 사용
|
||||
const tableName = step.tableName || flowDef.tableName;
|
||||
const tableName = FlowConditionParser.sanitizeTableName(step.tableName || flowDef.tableName);
|
||||
|
||||
// 4. 조건 JSON을 SQL WHERE절로 변환
|
||||
const { where, params } = FlowConditionParser.toSqlWhere(
|
||||
|
|
@ -96,7 +97,7 @@ export class FlowExecutionService {
|
|||
}
|
||||
|
||||
// 3. 테이블명 결정: 단계에 지정된 테이블이 있으면 사용, 없으면 플로우의 기본 테이블 사용
|
||||
const tableName = step.tableName || flowDef.tableName;
|
||||
const tableName = FlowConditionParser.sanitizeTableName(step.tableName || flowDef.tableName);
|
||||
|
||||
// 4. 조건 JSON을 SQL WHERE절로 변환
|
||||
const { where, params } = FlowConditionParser.toSqlWhere(
|
||||
|
|
@ -267,11 +268,12 @@ export class FlowExecutionService {
|
|||
throw new Error(`Flow step not found: ${stepId}`);
|
||||
}
|
||||
|
||||
// 3. 테이블명 결정
|
||||
const tableName = step.tableName || flowDef.tableName;
|
||||
if (!tableName) {
|
||||
// 3. 테이블명 결정 (SQL 인젝션 방지)
|
||||
const rawTableName = step.tableName || flowDef.tableName;
|
||||
if (!rawTableName) {
|
||||
throw new Error("Table name not found");
|
||||
}
|
||||
const tableName = FlowConditionParser.sanitizeTableName(rawTableName);
|
||||
|
||||
// 4. Primary Key 컬럼 결정 (기본값: id)
|
||||
const primaryKeyColumn = flowDef.primaryKey || "id";
|
||||
|
|
@ -280,8 +282,10 @@ export class FlowExecutionService {
|
|||
`🔍 [updateStepData] Updating table: ${tableName}, PK: ${primaryKeyColumn}=${recordId}`
|
||||
);
|
||||
|
||||
// 5. SET 절 생성
|
||||
const updateColumns = Object.keys(updateData);
|
||||
// 5. SET 절 생성 (컬럼명 SQL 인젝션 방지)
|
||||
const updateColumns = Object.keys(updateData).map((col) =>
|
||||
FlowConditionParser.sanitizeColumnName(col)
|
||||
);
|
||||
if (updateColumns.length === 0) {
|
||||
throw new Error("No columns to update");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,6 +260,7 @@ export interface FlowStepDataList {
|
|||
// 데이터 이동 요청
|
||||
export interface MoveDataRequest {
|
||||
flowId: number;
|
||||
fromStepId: number;
|
||||
recordId: string;
|
||||
toStepId: number;
|
||||
note?: string;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Loader2, AlertCircle, ArrowRight } from "lucide-react";
|
||||
import { getStepDataList, moveDataToNextStep } from "@/lib/api/flow";
|
||||
import { getStepDataList, moveBatchData, getFlowConnections } from "@/lib/api/flow";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface FlowDataListModalProps {
|
||||
|
|
@ -102,15 +102,28 @@ export function FlowDataListModal({
|
|||
try {
|
||||
setMovingData(true);
|
||||
|
||||
// 선택된 행의 ID 추출 (가정: 각 행에 'id' 필드가 있음)
|
||||
const selectedDataIds = Array.from(selectedRows).map((index) => data[index].id);
|
||||
// 다음 스텝 결정 (연결 정보에서 조회)
|
||||
const connResponse = await getFlowConnections(flowId);
|
||||
if (!connResponse.success || !connResponse.data) {
|
||||
throw new Error("플로우 연결 정보를 가져올 수 없습니다");
|
||||
}
|
||||
const nextConn = connResponse.data.find((c: any) => c.fromStepId === stepId);
|
||||
if (!nextConn) {
|
||||
throw new Error("다음 단계가 연결되어 있지 않습니다");
|
||||
}
|
||||
|
||||
// 데이터 이동 API 호출
|
||||
for (const dataId of selectedDataIds) {
|
||||
const response = await moveDataToNextStep(flowId, stepId, dataId);
|
||||
if (!response.success) {
|
||||
throw new Error(`데이터 이동 실패: ${response.message}`);
|
||||
}
|
||||
// 선택된 행의 ID 추출
|
||||
const selectedDataIds = Array.from(selectedRows).map((index) => String(data[index].id));
|
||||
|
||||
// 배치 이동 API 호출
|
||||
const response = await moveBatchData({
|
||||
flowId,
|
||||
fromStepId: stepId,
|
||||
toStepId: nextConn.toStepId,
|
||||
dataIds: selectedDataIds,
|
||||
});
|
||||
if (!response.success) {
|
||||
throw new Error(`데이터 이동 실패: ${response.error || "알 수 없는 오류"}`);
|
||||
}
|
||||
|
||||
toast.success(`${selectedRows.size}건의 데이터를 다음 단계로 이동했습니다`);
|
||||
|
|
|
|||
|
|
@ -451,13 +451,15 @@ export async function moveData(data: MoveDataRequest): Promise<ApiResponse<{ suc
|
|||
*/
|
||||
export async function moveDataToNextStep(
|
||||
flowId: number,
|
||||
currentStepId: number,
|
||||
dataId: number,
|
||||
fromStepId: number,
|
||||
toStepId: number,
|
||||
recordId: string | number,
|
||||
): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return moveData({
|
||||
flowId,
|
||||
currentStepId,
|
||||
dataId,
|
||||
fromStepId,
|
||||
recordId: String(recordId),
|
||||
toStepId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@ export interface FlowStepDataList {
|
|||
|
||||
export interface MoveDataRequest {
|
||||
flowId: number;
|
||||
fromStepId: number;
|
||||
recordId: string;
|
||||
toStepId: number;
|
||||
note?: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue