플로우 구현
This commit is contained in:
463
frontend/lib/api/flow.ts
Normal file
463
frontend/lib/api/flow.ts
Normal file
@@ -0,0 +1,463 @@
|
||||
/**
|
||||
* 플로우 관리 API 클라이언트
|
||||
*/
|
||||
|
||||
import {
|
||||
FlowDefinition,
|
||||
CreateFlowDefinitionRequest,
|
||||
UpdateFlowDefinitionRequest,
|
||||
FlowStep,
|
||||
CreateFlowStepRequest,
|
||||
UpdateFlowStepRequest,
|
||||
FlowStepConnection,
|
||||
CreateFlowConnectionRequest,
|
||||
FlowStepDataCount,
|
||||
FlowStepDataList,
|
||||
MoveDataRequest,
|
||||
MoveBatchDataRequest,
|
||||
FlowAuditLog,
|
||||
ApiResponse,
|
||||
} from "@/types/flow";
|
||||
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "/api";
|
||||
|
||||
// ============================================
|
||||
// 플로우 정의 API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 플로우 정의 목록 조회
|
||||
*/
|
||||
export async function getFlowDefinitions(params?: {
|
||||
tableName?: string;
|
||||
isActive?: boolean;
|
||||
}): Promise<ApiResponse<FlowDefinition[]>> {
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.tableName) queryParams.append("tableName", params.tableName);
|
||||
if (params?.isActive !== undefined) queryParams.append("isActive", String(params.isActive));
|
||||
|
||||
const url = `${API_BASE}/flow/definitions${queryParams.toString() ? `?${queryParams.toString()}` : ""}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 정의 상세 조회
|
||||
*/
|
||||
export async function getFlowDefinition(id: number): Promise<ApiResponse<FlowDefinition>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/definitions/${id}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 정의 상세 조회 (별칭)
|
||||
*/
|
||||
export const getFlowById = getFlowDefinition;
|
||||
|
||||
/**
|
||||
* 플로우 정의 생성
|
||||
*/
|
||||
export async function createFlowDefinition(data: CreateFlowDefinitionRequest): Promise<ApiResponse<FlowDefinition>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/definitions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 정의 수정
|
||||
*/
|
||||
export async function updateFlowDefinition(
|
||||
id: number,
|
||||
data: UpdateFlowDefinitionRequest,
|
||||
): Promise<ApiResponse<FlowDefinition>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/definitions/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 정의 삭제
|
||||
*/
|
||||
export async function deleteFlowDefinition(id: number): Promise<ApiResponse<{ success: boolean }>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/definitions/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 플로우 단계 API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 플로우 단계 목록 조회
|
||||
*/
|
||||
export async function getFlowSteps(flowId: number): Promise<ApiResponse<FlowStep[]>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/definitions/${flowId}/steps`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 단계 생성
|
||||
*/
|
||||
export async function createFlowStep(flowId: number, data: CreateFlowStepRequest): Promise<ApiResponse<FlowStep>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/definitions/${flowId}/steps`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 단계 수정
|
||||
*/
|
||||
export async function updateFlowStep(stepId: number, data: UpdateFlowStepRequest): Promise<ApiResponse<FlowStep>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/steps/${stepId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 단계 삭제
|
||||
*/
|
||||
export async function deleteFlowStep(stepId: number): Promise<ApiResponse<{ success: boolean }>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/steps/${stepId}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 플로우 연결 API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 플로우 연결 목록 조회
|
||||
*/
|
||||
export async function getFlowConnections(flowId: number): Promise<ApiResponse<FlowStepConnection[]>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/connections/${flowId}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 연결 생성
|
||||
*/
|
||||
export async function createFlowConnection(
|
||||
data: CreateFlowConnectionRequest,
|
||||
): Promise<ApiResponse<FlowStepConnection>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/connections`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 연결 삭제
|
||||
*/
|
||||
export async function deleteFlowConnection(connectionId: number): Promise<ApiResponse<{ success: boolean }>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/connections/${connectionId}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 플로우 실행 API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 특정 단계의 데이터 카운트 조회
|
||||
*/
|
||||
export async function getStepDataCount(flowId: number, stepId: number): Promise<ApiResponse<FlowStepDataCount>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/${flowId}/step/${stepId}/count`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 단계의 데이터 리스트 조회 (페이징)
|
||||
*/
|
||||
export async function getStepDataList(
|
||||
flowId: number,
|
||||
stepId: number,
|
||||
page: number = 1,
|
||||
pageSize: number = 20,
|
||||
): Promise<ApiResponse<FlowStepDataList>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/${flowId}/step/${stepId}/list?page=${page}&pageSize=${pageSize}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 단계의 데이터 카운트 조회
|
||||
*/
|
||||
export async function getAllStepCounts(flowId: number): Promise<ApiResponse<FlowStepDataCount[]>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/${flowId}/steps/counts`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터 이동
|
||||
*/
|
||||
export async function moveData(data: MoveDataRequest): Promise<ApiResponse<{ success: boolean }>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/move`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 데이터를 다음 스텝으로 이동 (편의 함수)
|
||||
*/
|
||||
export async function moveDataToNextStep(
|
||||
flowId: number,
|
||||
currentStepId: number,
|
||||
dataId: number,
|
||||
): Promise<ApiResponse<{ success: boolean }>> {
|
||||
return moveData({
|
||||
flowId,
|
||||
currentStepId,
|
||||
dataId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 배치 데이터 이동
|
||||
*/
|
||||
export async function moveBatchData(
|
||||
data: MoveBatchDataRequest,
|
||||
): Promise<ApiResponse<{ success: boolean; results: any[] }>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/move/batch`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 오딧 로그 API
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* 특정 레코드의 오딧 로그 조회
|
||||
*/
|
||||
export async function getAuditLogs(flowId: number, recordId: string): Promise<ApiResponse<FlowAuditLog[]>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/audit/${flowId}/${recordId}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 플로우 전체 오딧 로그 조회
|
||||
*/
|
||||
export async function getFlowAuditLogs(flowId: number, limit: number = 100): Promise<ApiResponse<FlowAuditLog[]>> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/flow/audit/${flowId}?limit=${limit}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||
import { FlowWidgetDefinition } from "./index";
|
||||
import { FlowWidget } from "@/components/screen/widgets/FlowWidget";
|
||||
|
||||
/**
|
||||
* FlowWidget 렌더러
|
||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
||||
*/
|
||||
export class FlowWidgetRenderer extends AutoRegisteringComponentRenderer {
|
||||
static componentDefinition = FlowWidgetDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
return <FlowWidget component={this.props.component as any} />;
|
||||
}
|
||||
}
|
||||
|
||||
// 자동 등록 실행
|
||||
FlowWidgetRenderer.registerSelf();
|
||||
|
||||
// Hot Reload 지원 (개발 모드)
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
FlowWidgetRenderer.enableHotReload();
|
||||
}
|
||||
|
||||
console.log("✅ FlowWidget 컴포넌트 등록 완료");
|
||||
40
frontend/lib/registry/components/flow-widget/index.ts
Normal file
40
frontend/lib/registry/components/flow-widget/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { createComponentDefinition } from "../../utils/createComponentDefinition";
|
||||
import { ComponentCategory } from "@/types/component";
|
||||
import { FlowWidget } from "@/components/screen/widgets/FlowWidget";
|
||||
import { FlowWidgetConfigPanel } from "@/components/screen/config-panels/FlowWidgetConfigPanel";
|
||||
|
||||
/**
|
||||
* FlowWidget 컴포넌트 정의
|
||||
* 플로우 관리 시스템의 플로우를 화면에 표시
|
||||
*/
|
||||
export const FlowWidgetDefinition = createComponentDefinition({
|
||||
id: "flow-widget",
|
||||
name: "플로우 위젯",
|
||||
nameEng: "Flow Widget",
|
||||
description: "플로우 관리 시스템의 플로우를 화면에 표시합니다",
|
||||
category: ComponentCategory.DISPLAY,
|
||||
webType: "text", // 기본 웹타입 (필수)
|
||||
component: FlowWidget,
|
||||
defaultConfig: {
|
||||
flowId: undefined,
|
||||
flowName: undefined,
|
||||
showStepCount: true,
|
||||
allowDataMove: false,
|
||||
displayMode: "horizontal",
|
||||
},
|
||||
defaultSize: {
|
||||
width: 1200,
|
||||
height: 120,
|
||||
gridColumnSpan: "full", // 전체 너비 사용
|
||||
},
|
||||
configPanel: FlowWidgetConfigPanel,
|
||||
icon: "Workflow",
|
||||
tags: ["플로우", "워크플로우", "프로세스", "상태"],
|
||||
version: "1.0.0",
|
||||
author: "개발팀",
|
||||
documentation: "",
|
||||
});
|
||||
|
||||
// 컴포넌트는 FlowWidgetRenderer에서 자동 등록됩니다
|
||||
@@ -40,6 +40,7 @@ import "./card-display/CardDisplayRenderer";
|
||||
import "./split-panel-layout/SplitPanelLayoutRenderer";
|
||||
import "./map/MapRenderer";
|
||||
import "./repeater-field-group/RepeaterFieldGroupRenderer";
|
||||
import "./flow-widget/FlowWidgetRenderer";
|
||||
|
||||
/**
|
||||
* 컴포넌트 초기화 함수
|
||||
|
||||
@@ -25,6 +25,7 @@ const CONFIG_PANEL_MAP: Record<string, () => Promise<any>> = {
|
||||
"card-display": () => import("@/lib/registry/components/card-display/CardDisplayConfigPanel"),
|
||||
"split-panel-layout": () => import("@/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel"),
|
||||
"repeater-field-group": () => import("@/components/webtypes/config/RepeaterConfigPanel"),
|
||||
"flow-widget": () => import("@/components/screen/config-panels/FlowWidgetConfigPanel"),
|
||||
};
|
||||
|
||||
// ConfigPanel 컴포넌트 캐시
|
||||
@@ -54,6 +55,7 @@ export async function getComponentConfigPanel(componentId: string): Promise<Reac
|
||||
const ConfigPanelComponent =
|
||||
module[`${toPascalCase(componentId)}ConfigPanel`] ||
|
||||
module.RepeaterConfigPanel || // repeater-field-group의 export명
|
||||
module.FlowWidgetConfigPanel || // flow-widget의 export명
|
||||
module.default;
|
||||
|
||||
if (!ConfigPanelComponent) {
|
||||
|
||||
Reference in New Issue
Block a user