플로우 구현

This commit is contained in:
kjs
2025-10-20 10:55:33 +09:00
parent 6603ff81fe
commit f9c171c513
37 changed files with 6881 additions and 238 deletions

463
frontend/lib/api/flow.ts Normal file
View 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,
};
}
}

View File

@@ -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 컴포넌트 등록 완료");

View 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에서 자동 등록됩니다

View File

@@ -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";
/**
* 컴포넌트 초기화 함수

View File

@@ -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) {