ERP-node/버튼_제어관리_기능_통합_계획서.md

1775 lines
49 KiB
Markdown
Raw Normal View History

2025-09-18 10:05:50 +09:00
# 🔧 버튼 제어관리 기능 통합 계획서
## 📋 프로젝트 개요
현재 구축되어 있는 **데이터 흐름 제어관리 시스템(DataFlow Management)**을 화면관리 시스템의 **버튼 컴포넌트**에 통합하여, 버튼 클릭 시 데이터 흐름을 제어할 수 있는 고급 기능을 제공합니다.
### 🎯 목표
- 버튼 액션 실행 시 조건부 데이터 제어 기능 제공
- 기존 제어관리 시스템의 조건부 연결 로직을 버튼 액션에 적용
- 복잡한 비즈니스 로직을 GUI로 설정 가능한 시스템 구축
## 🔍 현재 상황 분석
### 제어관리 시스템 (DataFlow Diagrams) 분석
#### 데이터베이스 구조
```sql
CREATE TABLE dataflow_diagrams (
diagram_id SERIAL PRIMARY KEY,
diagram_name VARCHAR(255),
relationships JSONB, -- 테이블 관계 정보
company_code VARCHAR(50),
created_at TIMESTAMP,
updated_at TIMESTAMP,
created_by VARCHAR(100),
updated_by VARCHAR(100),
node_positions JSONB, -- 시각적 위치 정보
control JSONB, -- 🔥 조건 설정 정보
plan JSONB, -- 🔥 실행 계획 정보
category JSON -- 🔥 연결 타입 정보
);
```
#### 핵심 데이터 구조
**1. control (조건 설정)**
```json
{
"id": "rel-1758010445208",
"triggerType": "insert",
"conditions": [
{
"id": "cond_1758010388399_65jnzabvv",
"type": "group-start",
"groupId": "group_1758010388399_x4uhh1ztz",
"groupLevel": 0
},
{
"id": "cond_1758010388969_rs2y93llp",
"type": "condition",
"field": "target_type",
"value": "1",
"dataType": "string",
"operator": "=",
"logicalOperator": "AND"
}
// ... 추가 조건들
]
}
```
**2. plan (실행 계획)**
```json
{
"id": "rel-1758010445208",
"sourceTable": "approval_kind",
"actions": [
{
"id": "action_1",
"name": "액션 1",
"actionType": "insert",
"conditions": [...],
"fieldMappings": [
{
"sourceField": "",
"sourceTable": "",
"targetField": "target_type",
"targetTable": "approval_kind",
"defaultValue": "123123"
}
],
"splitConfig": {
"delimiter": "",
"sourceField": "",
"targetField": ""
}
}
]
}
```
**3. category (연결 타입)**
```json
[
{
"id": "rel-1758010379858",
"category": "simple-key"
},
{
"id": "rel-1758010445208",
"category": "data-save"
}
]
```
### 현재 버튼 시스템 분석
#### ButtonTypeConfig 인터페이스
```typescript
export interface ButtonTypeConfig {
actionType: ButtonActionType; // 기본 액션 타입
variant?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "ghost"
| "link";
icon?: string;
confirmMessage?: string;
// 모달 관련 설정
popupTitle?: string;
popupContent?: string;
popupScreenId?: number;
// 네비게이션 관련 설정
navigateType?: "url" | "screen";
navigateUrl?: string;
navigateScreenId?: number;
navigateTarget?: "_self" | "_blank";
// 커스텀 액션 설정
customAction?: string;
// 스타일 설정
backgroundColor?: string;
textColor?: string;
borderColor?: string;
}
```
#### ButtonActionType
```typescript
export type ButtonActionType =
| "save"
| "delete"
| "edit"
| "add"
| "search"
| "reset"
| "submit"
| "close"
| "popup"
| "modal"
| "newWindow"
| "navigate";
```
## 🚀 구현 계획
### Phase 1: 기본 구조 확장
#### 1.1 ButtonTypeConfig 인터페이스 확장 (기존 액션 타입 유지)
```typescript
export interface ButtonTypeConfig {
actionType: ButtonActionType; // 기존 액션 타입 그대로 유지
variant?:
| "default"
| "destructive"
| "outline"
| "secondary"
| "ghost"
| "link";
icon?: string;
confirmMessage?: string;
// 모달 관련 설정
popupTitle?: string;
popupContent?: string;
popupScreenId?: number;
// 네비게이션 관련 설정
navigateType?: "url" | "screen";
navigateUrl?: string;
navigateScreenId?: number;
navigateTarget?: "_self" | "_blank";
// 커스텀 액션 설정
customAction?: string;
// 🔥 NEW: 모든 액션에 제어관리 옵션 추가
enableDataflowControl?: boolean; // 제어관리 활성화 여부
dataflowConfig?: ButtonDataflowConfig; // 제어관리 설정
dataflowTiming?: "before" | "after" | "replace"; // 언제 실행할지
// 스타일 설정
backgroundColor?: string;
textColor?: string;
borderColor?: string;
}
export interface ButtonDataflowConfig {
// 제어 방식 선택
controlMode: "simple" | "advanced";
// Simple 모드: 기존 관계도 선택
selectedDiagramId?: number;
selectedRelationshipId?: string;
// Advanced 모드: 직접 조건 설정
directControl?: {
sourceTable: string;
triggerType: "insert" | "update" | "delete";
conditions: DataflowCondition[];
actions: DataflowAction[];
};
// 실행 옵션
executionOptions?: {
rollbackOnError?: boolean;
enableLogging?: boolean;
maxRetryCount?: number;
asyncExecution?: boolean;
};
}
// 실행 타이밍 옵션 설명
// - "before": 기존 액션 실행 전에 제어관리 실행
// - "after": 기존 액션 실행 후에 제어관리 실행
// - "replace": 기존 액션 대신 제어관리만 실행
```
#### 1.3 데이터 구조 정의
```typescript
export interface DataflowCondition {
id: string;
type: "condition" | "group-start" | "group-end";
field?: string;
operator?: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value?: any;
dataType?: "string" | "number" | "boolean" | "date";
logicalOperator?: "AND" | "OR";
groupId?: string;
groupLevel?: number;
}
export interface DataflowAction {
id: string;
name: string;
actionType: "insert" | "update" | "delete" | "upsert";
targetTable: string;
conditions?: DataflowCondition[];
fieldMappings: DataflowFieldMapping[];
splitConfig?: {
sourceField: string;
delimiter: string;
targetField: string;
};
}
export interface DataflowFieldMapping {
sourceTable?: string;
sourceField: string;
targetTable?: string;
targetField: string;
defaultValue?: string;
transformFunction?: string;
}
```
### Phase 2: UI 컴포넌트 개발
#### 2.1 ButtonDataflowConfigPanel 컴포넌트 (기존 액션별 제어관리 옵션)
```typescript
interface ButtonDataflowConfigPanelProps {
component: ComponentData;
onUpdateProperty: (path: string, value: any) => void;
}
export const ButtonDataflowConfigPanel: React.FC<
ButtonDataflowConfigPanelProps
> = ({ component, onUpdateProperty }) => {
const config = component.webTypeConfig || {};
const dataflowConfig = config.dataflowConfig || {};
return (
<div className="space-y-6">
{/* 제어관리 활성화 스위치 */}
<div className="flex items-center justify-between">
<Label>제어관리 기능 활성화</Label>
<Switch
checked={config.enableDataflowControl || false}
onCheckedChange={(checked) =>
onUpdateProperty("webTypeConfig.enableDataflowControl", checked)
}
/>
</div>
{/* 제어관리가 활성화된 경우에만 설정 표시 */}
{config.enableDataflowControl && (
<>
{/* 실행 타이밍 선택 */}
<div>
<Label>실행 타이밍</Label>
<Select
value={config.dataflowTiming || "after"}
onValueChange={(value) =>
onUpdateProperty("webTypeConfig.dataflowTiming", value)
}
>
<SelectContent>
<SelectItem value="before">
기존 액션 실행 전 (사전 검증/준비)
</SelectItem>
<SelectItem value="after">
기존 액션 실행 후 (후속 처리)
</SelectItem>
<SelectItem value="replace">
기존 액션 대신 (완전 대체)
</SelectItem>
</SelectContent>
</Select>
<p className="mt-1 text-xs text-gray-500">
{config.dataflowTiming === "before" &&
"예: 저장 전 데이터 검증, 삭제 전 권한 확인"}
{config.dataflowTiming === "after" &&
"예: 저장 후 알림 발송, 삭제 후 관련 데이터 정리"}
{config.dataflowTiming === "replace" &&
"예: 복잡한 비즈니스 로직으로 기본 동작 완전 대체"}
</p>
</div>
{/* 제어 모드 선택 */}
<div>
<Label>제어 모드</Label>
<Select
value={dataflowConfig.controlMode || "simple"}
onValueChange={(value) =>
onUpdateProperty(
"webTypeConfig.dataflowConfig.controlMode",
value
)
}
>
<SelectContent>
<SelectItem value="simple">
간편 모드 (기존 관계도 선택)
</SelectItem>
<SelectItem value="advanced">고급 모드 (직접 설정)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 간편 모드 UI */}
{dataflowConfig.controlMode === "simple" && (
<SimpleModePanel
config={dataflowConfig}
onUpdateProperty={onUpdateProperty}
/>
)}
{/* 고급 모드 UI */}
{dataflowConfig.controlMode === "advanced" && (
<AdvancedModePanel
config={dataflowConfig}
onUpdateProperty={onUpdateProperty}
/>
)}
{/* 실행 옵션 */}
<ExecutionOptionsPanel
config={dataflowConfig}
onUpdateProperty={onUpdateProperty}
/>
</>
)}
</div>
);
};
```
#### 2.2 SimpleModePanel - 기존 관계도 선택
```typescript
const SimpleModePanel: React.FC<{
config: ButtonDataflowConfig;
onUpdateProperty: (path: string, value: any) => void;
}> = ({ config, onUpdateProperty }) => {
const [diagrams, setDiagrams] = useState<DataFlowDiagram[]>([]);
const [relationships, setRelationships] = useState<JsonRelationship[]>([]);
return (
<div className="space-y-4">
{/* 관계도 선택 */}
<div>
<Label>관계도 선택</Label>
<DiagramSelector
value={config.selectedDiagramId}
onSelect={(diagramId) => {
onUpdateProperty(
"webTypeConfig.dataflowConfig.selectedDiagramId",
diagramId
);
// 관계도 선택 시 관련 관계들 로드
loadRelationships(diagramId);
}}
/>
</div>
{/* 관계 선택 */}
{config.selectedDiagramId && (
<div>
<Label>제어할 관계 선택</Label>
<RelationshipSelector
diagramId={config.selectedDiagramId}
value={config.selectedRelationshipId}
onSelect={(relationshipId) =>
onUpdateProperty(
"webTypeConfig.dataflowConfig.selectedRelationshipId",
relationshipId
)
}
/>
</div>
)}
{/* 선택된 관계 미리보기 */}
{config.selectedRelationshipId && (
<RelationshipPreview
diagramId={config.selectedDiagramId}
relationshipId={config.selectedRelationshipId}
/>
)}
</div>
);
};
```
#### 2.3 AdvancedModePanel - 직접 조건 설정
```typescript
const AdvancedModePanel: React.FC<{
config: ButtonDataflowConfig;
onUpdateProperty: (path: string, value: any) => void;
}> = ({ config, onUpdateProperty }) => {
return (
<div className="space-y-6">
{/* 소스 테이블 선택 */}
<div>
<Label>소스 테이블</Label>
<TableSelector
value={config.directControl?.sourceTable}
onSelect={(table) =>
onUpdateProperty(
"webTypeConfig.dataflowConfig.directControl.sourceTable",
table
)
}
/>
</div>
{/* 트리거 타입 선택 */}
<div>
<Label>트리거 타입</Label>
<Select
value={config.directControl?.triggerType || "insert"}
onValueChange={(value) =>
onUpdateProperty(
"webTypeConfig.dataflowConfig.directControl.triggerType",
value
)
}
>
<SelectContent>
<SelectItem value="insert">데이터 삽입 시</SelectItem>
<SelectItem value="update">데이터 수정 시</SelectItem>
<SelectItem value="delete">데이터 삭제 시</SelectItem>
</SelectContent>
</Select>
</div>
{/* 조건 설정 */}
<div>
<Label>실행 조건</Label>
<ConditionBuilder
conditions={config.directControl?.conditions || []}
onUpdate={(conditions) =>
onUpdateProperty(
"webTypeConfig.dataflowConfig.directControl.conditions",
conditions
)
}
sourceTable={config.directControl?.sourceTable}
/>
</div>
{/* 액션 설정 */}
<div>
<Label>실행 액션</Label>
<ActionBuilder
actions={config.directControl?.actions || []}
onUpdate={(actions) =>
onUpdateProperty(
"webTypeConfig.dataflowConfig.directControl.actions",
actions
)
}
sourceTable={config.directControl?.sourceTable}
/>
</div>
</div>
);
};
```
### Phase 3: 서비스 계층 개발 (성능 최적화 적용)
#### 3.1 OptimizedButtonDataflowService (즉시 응답 + 백그라운드 실행)
```typescript
// 🔥 성능 최적화: 캐싱 시스템
class DataflowConfigCache {
private memoryCache = new Map<string, ButtonDataflowConfig>();
private readonly TTL = 5 * 60 * 1000; // 5분 TTL
async getConfig(buttonId: string): Promise<ButtonDataflowConfig | null> {
const cacheKey = `button_dataflow_${buttonId}`;
// L1: 메모리 캐시 확인 (1ms)
if (this.memoryCache.has(cacheKey)) {
console.log("⚡ Cache hit:", buttonId);
return this.memoryCache.get(cacheKey)!;
}
// L2: 서버에서 로드 (100-300ms)
console.log("🌐 Loading from server:", buttonId);
const serverConfig = await this.loadFromServer(buttonId);
// 캐시에 저장
this.memoryCache.set(cacheKey, serverConfig);
// TTL 후 캐시 제거
setTimeout(() => {
this.memoryCache.delete(cacheKey);
}, this.TTL);
return serverConfig;
}
private async loadFromServer(buttonId: string): Promise<ButtonDataflowConfig> {
// 실제 서버 호출 로직
return {} as ButtonDataflowConfig;
}
}
// 🔥 성능 최적화: 작업 큐 시스템
class DataflowJobQueue {
private queue: Array<{
id: string;
buttonId: string;
actionType: ButtonActionType;
config: ButtonTypeConfig;
contextData: Record<string, any>;
companyCode: string;
priority: "high" | "normal" | "low";
}> = [];
private processing = false;
// 🔥 즉시 반환하는 작업 큐잉
enqueue(
buttonId: string,
actionType: ButtonActionType,
config: ButtonTypeConfig,
contextData: Record<string, any>,
companyCode: string,
priority: "high" | "normal" | "low" = "normal"
): string {
const jobId = `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.queue.push({
id: jobId,
buttonId,
actionType,
config,
contextData,
companyCode,
priority,
});
// 우선순위 정렬
this.queue.sort((a, b) => {
const weights = { high: 3, normal: 2, low: 1 };
return weights[b.priority] - weights[a.priority];
});
// 비동기 처리 시작
this.processQueue();
return jobId; // 🔥 즉시 반환
}
private async processQueue(): Promise<void> {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
try {
// 배치 처리 (최대 3개 동시)
const batch = this.queue.splice(0, 3);
const promises = batch.map(job => this.executeJob(job));
await Promise.allSettled(promises);
} finally {
this.processing = false;
if (this.queue.length > 0) {
setTimeout(() => this.processQueue(), 10);
}
}
}
private async executeJob(job: any): Promise<void> {
const startTime = performance.now();
try {
await OptimizedButtonDataflowService.executeJobInternal(job);
const executionTime = performance.now() - startTime;
console.log(`⚡ Job ${job.id} completed in ${executionTime.toFixed(2)}ms`);
} catch (error) {
console.error(`❌ Job ${job.id} failed:`, error);
}
}
}
// 전역 인스턴스
const configCache = new DataflowConfigCache();
const jobQueue = new DataflowJobQueue();
export class OptimizedButtonDataflowService {
/**
* 🔥 메인 엔트리포인트: 즉시 응답 + 백그라운드 실행
*/
static async executeButtonWithDataflow(
actionType: ButtonActionType,
buttonConfig: ButtonTypeConfig,
contextData: Record<string, any>,
companyCode: string,
buttonId: string
): Promise<{ jobId: string; immediateResult?: any }> {
const { enableDataflowControl, dataflowTiming } = buttonConfig;
// 🔥 제어관리가 비활성화된 경우: 즉시 실행
if (!enableDataflowControl) {
const result = await this.executeOriginalAction(actionType, buttonConfig, contextData);
return { jobId: "immediate", immediateResult: result };
}
// 🔥 타이밍별 즉시 응답 전략
switch (dataflowTiming) {
case "before":
// before는 동기 처리 필요 (검증 목적)
return await this.executeBeforeTiming(actionType, buttonConfig, contextData, companyCode);
case "after":
// after는 백그라운드 처리 가능
return await this.executeAfterTiming(actionType, buttonConfig, contextData, companyCode, buttonId);
case "replace":
// replace는 상황에 따라 동기/비동기 선택
return await this.executeReplaceTiming(actionType, buttonConfig, contextData, companyCode, buttonId);
default:
return await this.executeAfterTiming(actionType, buttonConfig, contextData, companyCode, buttonId);
}
}
/**
* 🔥 After 타이밍: 즉시 기존 액션 + 백그라운드 제어관리
*/
private static async executeAfterTiming(
actionType: ButtonActionType,
buttonConfig: ButtonTypeConfig,
contextData: Record<string, any>,
companyCode: string,
buttonId: string
): Promise<{ jobId: string; immediateResult: any }> {
// 🔥 Step 1: 기존 액션 즉시 실행 (50-200ms)
const immediateResult = await this.executeOriginalAction(
actionType,
buttonConfig,
contextData
);
// 🔥 Step 2: 제어관리는 백그라운드에서 실행 (즉시 반환)
const jobId = jobQueue.enqueue(
buttonId,
actionType,
buttonConfig,
{ ...contextData, originalActionResult: immediateResult },
companyCode,
"normal"
);
return {
jobId,
immediateResult
};
}
/**
* 🔥 Before 타이밍: 빠른 제어관리 + 기존 액션
*/
private static async executeBeforeTiming(
actionType: ButtonActionType,
buttonConfig: ButtonTypeConfig,
contextData: Record<string, any>,
companyCode: string
): Promise<{ jobId: string; immediateResult: any }> {
// 간단한 조건만 즉시 검증 (복잡한 것은 에러)
const isSimpleValidation = await this.isSimpleValidationOnly(buttonConfig.dataflowConfig);
if (isSimpleValidation) {
// 🔥 간단한 검증: 메모리에서 즉시 처리 (1-10ms)
const validationResult = await this.executeQuickValidation(
buttonConfig.dataflowConfig!,
contextData
);
if (!validationResult.success) {
return {
jobId: "validation_failed",
immediateResult: { success: false, message: validationResult.message }
};
}
// 검증 통과 시 기존 액션 실행
const actionResult = await this.executeOriginalAction(actionType, buttonConfig, contextData);
return { jobId: "immediate", immediateResult: actionResult };
} else {
// 🔥 복잡한 검증: 사용자에게 알림 후 백그라운드 처리
const jobId = jobQueue.enqueue(
buttonConfig.buttonId || "unknown",
actionType,
buttonConfig,
contextData,
companyCode,
"high" // 높은 우선순위
);
return {
jobId,
immediateResult: {
success: true,
message: "검증 중입니다. 잠시만 기다려주세요.",
processing: true
}
};
}
}
/**
* 🔥 간단한 조건인지 판단 (메모리에서 즉시 처리 가능한지)
*/
private static async isSimpleValidationOnly(config?: ButtonDataflowConfig): Promise<boolean> {
if (!config || config.controlMode !== "advanced") return true;
const conditions = config.directControl?.conditions || [];
// 조건이 5개 이하이고 모두 단순 비교 연산자면 간단한 검증
return conditions.length <= 5 &&
conditions.every(c =>
c.type === "condition" &&
["=", "!=", ">", "<", ">=", "<="].includes(c.operator || "")
);
}
/**
* 🔥 빠른 검증 (메모리에서 즉시 처리)
*/
private static async executeQuickValidation(
config: ButtonDataflowConfig,
data: Record<string, any>
): Promise<{ success: boolean; message?: string }> {
if (config.controlMode === "simple") {
// 간편 모드는 일단 통과 (실제 검증은 백그라운드에서)
return { success: true };
}
const conditions = config.directControl?.conditions || [];
for (const condition of conditions) {
if (condition.type === "condition") {
const fieldValue = data[condition.field!];
const isValid = this.evaluateSimpleCondition(
fieldValue,
condition.operator!,
condition.value
);
if (!isValid) {
return {
success: false,
message: `조건 불만족: ${condition.field} ${condition.operator} ${condition.value}`
};
}
}
}
return { success: true };
}
/**
* 🔥 단순 조건 평가 (메모리에서 즉시)
*/
private static evaluateSimpleCondition(
fieldValue: any,
operator: string,
conditionValue: any
): boolean {
switch (operator) {
case "=": return fieldValue === conditionValue;
case "!=": return fieldValue !== conditionValue;
case ">": return fieldValue > conditionValue;
case "<": return fieldValue < conditionValue;
case ">=": return fieldValue >= conditionValue;
case "<=": return fieldValue <= conditionValue;
default: return true;
}
}
/**
* 🔥 기존 액션 실행 (최적화)
*/
private static async executeOriginalAction(
actionType: ButtonActionType,
buttonConfig: ButtonTypeConfig,
contextData: Record<string, any>
): Promise<any> {
const startTime = performance.now();
try {
switch (actionType) {
case "save":
return await this.executeSaveAction(buttonConfig, contextData);
case "delete":
return await this.executeDeleteAction(buttonConfig, contextData);
case "search":
return await this.executeSearchAction(buttonConfig, contextData);
default:
return { success: true, message: `${actionType} 액션 실행됨` };
}
} finally {
const executionTime = performance.now() - startTime;
if (executionTime > 200) {
console.warn(`🐌 Slow action: ${actionType} took ${executionTime.toFixed(2)}ms`);
}
}
}
/**
* 🔥 내부 작업 실행 (큐에서 호출)
*/
static async executeJobInternal(job: any): Promise<void> {
// 실제 제어관리 로직 실행
const dataflowResult = await this.executeDataflowLogic(
job.config.dataflowConfig,
job.contextData,
job.companyCode
);
// 결과를 클라이언트에 전송 (WebSocket, Server-Sent Events 등)
this.notifyClient(job.id, dataflowResult);
}
private static async executeDataflowLogic(
config: ButtonDataflowConfig,
contextData: Record<string, any>,
companyCode: string
): Promise<ExecutionResult> {
// 기존 제어관리 로직 활용
if (config.controlMode === "simple") {
return await this.executeSimpleMode(config, contextData, companyCode);
} else {
return await this.executeAdvancedMode(config, contextData, companyCode);
}
}
private static notifyClient(jobId: string, result: ExecutionResult): void {
// WebSocket이나 Server-Sent Events로 결과 전송
console.log(`📤 Notifying client: Job ${jobId} completed`, result);
}
}
/**
* 간편 모드 실행 - 기존 관계도 활용
*/
private static async executeSimpleMode(
config: ButtonDataflowConfig,
contextData: Record<string, any>,
companyCode: string
): Promise<ExecutionResult> {
// 1. 선택된 관계도와 관계 정보 조회
const diagram = await this.getDiagramById(
config.selectedDiagramId,
companyCode
);
const relationship = this.findRelationshipById(
diagram,
config.selectedRelationshipId
);
// 2. 기존 EventTriggerService 활용
return await EventTriggerService.executeSpecificRelationship(
relationship,
contextData,
companyCode
);
}
/**
* 고급 모드 실행 - 직접 설정 조건 활용
*/
private static async executeAdvancedMode(
config: ButtonDataflowConfig,
contextData: Record<string, any>,
companyCode: string
): Promise<ExecutionResult> {
const { directControl } = config;
if (!directControl) {
throw new Error("고급 모드 설정이 없습니다.");
}
// 1. 조건 검증
const conditionsMet = await this.evaluateConditions(
directControl.conditions,
contextData
);
if (!conditionsMet) {
return {
success: true,
executedActions: 0,
message: "조건을 만족하지 않아 실행되지 않았습니다.",
};
}
// 2. 액션 실행
return await this.executeActions(
directControl.actions,
contextData,
companyCode
);
}
/**
* 조건 평가
*/
private static async evaluateConditions(
conditions: DataflowCondition[],
data: Record<string, any>
): Promise<boolean> {
// 기존 EventTriggerService의 조건 평가 로직 재활용
return await ConditionEvaluator.evaluate(conditions, data);
}
/**
* 액션 실행
*/
private static async executeActions(
actions: DataflowAction[],
contextData: Record<string, any>,
companyCode: string
): Promise<ExecutionResult> {
// 기존 EventTriggerService의 액션 실행 로직 재활용
return await ActionExecutor.execute(actions, contextData, companyCode);
}
}
```
#### 3.2 기존 EventTriggerService 확장
```typescript
export class EventTriggerService {
// ... 기존 메서드들
/**
* 🔥 NEW: 특정 관계 실행 (버튼에서 호출)
*/
static async executeSpecificRelationship(
relationship: JsonRelationship,
contextData: Record<string, any>,
companyCode: string
): Promise<ExecutionResult> {
// 관계에 해당하는 제어 조건 및 실행 계획 추출
const control = this.extractControlFromRelationship(relationship);
const plan = this.extractPlanFromRelationship(relationship);
// 조건 검증
const conditionsMet = await this.evaluateConditions(
control.conditions,
contextData
);
if (!conditionsMet) {
return {
success: true,
executedActions: 0,
message: "조건을 만족하지 않아 실행되지 않았습니다.",
};
}
// 액션 실행
return await this.executePlan(plan, contextData, companyCode);
}
/**
* 🔥 NEW: 버튼 컨텍스트에서 데이터플로우 실행
*/
static async executeFromButtonContext(
buttonId: string,
screenId: number,
formData: Record<string, any>,
companyCode: string
): Promise<ExecutionResult> {
// 1. 버튼 설정 조회
const buttonConfig = await this.getButtonDataflowConfig(buttonId, screenId);
// 2. 컨텍스트 데이터 준비
const contextData = {
...formData,
buttonId,
screenId,
timestamp: new Date().toISOString(),
userContext: await this.getUserContext(),
};
// 3. 데이터플로우 실행
return await ButtonDataflowService.executeButtonDataflow(
buttonConfig,
contextData,
companyCode
);
}
}
```
### Phase 4: API 엔드포인트 개발
#### 4.1 ButtonDataflowController
```typescript
// backend-node/src/controllers/buttonDataflowController.ts
export async function executeButtonDataflow(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { buttonId, screenId, formData } = req.body;
const companyCode = req.user?.company_code;
const result = await EventTriggerService.executeFromButtonContext(
buttonId,
screenId,
formData,
companyCode
);
res.json({
success: true,
data: result,
});
} catch (error) {
logger.error("Button dataflow execution failed:", error);
res.status(500).json({
success: false,
message: "데이터플로우 실행 중 오류가 발생했습니다.",
});
}
}
export async function getAvailableDiagrams(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const companyCode = req.user?.company_code;
const diagrams = await DataFlowAPI.getJsonDataFlowDiagrams(companyCode);
res.json({
success: true,
data: diagrams,
});
} catch (error) {
logger.error("Failed to get available diagrams:", error);
res.status(500).json({
success: false,
message: "관계도 목록 조회 중 오류가 발생했습니다.",
});
}
}
export async function getDiagramRelationships(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { diagramId } = req.params;
const companyCode = req.user?.company_code;
const diagram = await DataFlowAPI.getJsonDataFlowDiagramById(
parseInt(diagramId),
companyCode
);
const relationships = diagram.relationships?.relationships || [];
res.json({
success: true,
data: relationships,
});
} catch (error) {
logger.error("Failed to get diagram relationships:", error);
res.status(500).json({
success: false,
message: "관계 목록 조회 중 오류가 발생했습니다.",
});
}
}
```
#### 4.2 라우팅 설정
```typescript
// backend-node/src/routes/buttonDataflowRoutes.ts
import express from "express";
import {
executeButtonDataflow,
getAvailableDiagrams,
getDiagramRelationships,
} from "../controllers/buttonDataflowController";
import { authenticateToken } from "../middleware/authMiddleware";
const router = express.Router();
// 모든 라우트에 인증 미들웨어 적용
router.use(authenticateToken);
// 버튼 데이터플로우 실행
router.post("/execute", executeButtonDataflow);
// 사용 가능한 관계도 목록 조회
router.get("/diagrams", getAvailableDiagrams);
// 특정 관계도의 관계 목록 조회
router.get("/diagrams/:diagramId/relationships", getDiagramRelationships);
export default router;
```
### Phase 5: 프론트엔드 통합
#### 5.1 ButtonConfigPanel 수정 (모든 액션에 제어관리 옵션 추가)
```typescript
// frontend/components/screen/config-panels/ButtonConfigPanel.tsx 수정
export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({
component,
onUpdateProperty,
}) => {
const config = component.webTypeConfig || {};
return (
<div className="space-y-4">
{/* 기존 액션 타입 선택 (변경 없음) */}
<div>
<Label htmlFor="action-type">액션 타입</Label>
<Select
value={config.actionType || "save"}
onValueChange={(value) =>
onUpdateProperty("webTypeConfig.actionType", value)
}
>
<SelectContent>
<SelectItem value="save">저장</SelectItem>
<SelectItem value="delete">삭제</SelectItem>
<SelectItem value="edit">수정</SelectItem>
<SelectItem value="add">추가</SelectItem>
<SelectItem value="search">검색</SelectItem>
<SelectItem value="reset">초기화</SelectItem>
<SelectItem value="submit">제출</SelectItem>
<SelectItem value="close">닫기</SelectItem>
<SelectItem value="popup">팝업 열기</SelectItem>
<SelectItem value="navigate">페이지 이동</SelectItem>
</SelectContent>
</Select>
</div>
{/* 기존 액션별 설정들 (variant, icon, confirmMessage 등) */}
{/* ... 기존 UI 컴포넌트들 ... */}
{/* 🔥 NEW: 모든 액션에 제어관리 옵션 추가 */}
<div className="mt-6 border-t pt-6">
<div className="mb-4 flex items-center space-x-2">
<Switch
id="enable-dataflow"
checked={config.enableDataflowControl || false}
onCheckedChange={(checked) =>
onUpdateProperty("webTypeConfig.enableDataflowControl", checked)
}
/>
<Label htmlFor="enable-dataflow" className="text-sm font-medium">
📊 제어관리 기능 추가
</Label>
</div>
{config.enableDataflowControl && (
<div className="ml-6 space-y-4 rounded-lg border bg-gray-50 p-4">
<div className="text-sm text-gray-600">
<strong>{getActionDisplayName(config.actionType)}</strong> 액션과
함께 데이터 흐름 제어 기능이 실행됩니다.
</div>
<ButtonDataflowConfigPanel
component={component}
onUpdateProperty={onUpdateProperty}
/>
</div>
)}
</div>
</div>
);
};
// 액션 타입별 표시명 헬퍼 함수
function getActionDisplayName(actionType: string): string {
const displayNames = {
save: "저장",
delete: "삭제",
edit: "수정",
add: "추가",
search: "검색",
reset: "초기화",
submit: "제출",
close: "닫기",
popup: "팝업",
navigate: "페이지 이동",
};
return displayNames[actionType] || actionType;
}
```
#### 5.2 프론트엔드 최적화 (즉시 응답 UI)
```typescript
// frontend/components/screen/OptimizedButtonComponent.tsx
import React, { useState, useCallback } from "react";
import { useDebouncedCallback } from "use-debounce";
import { toast } from "react-hot-toast";
interface OptimizedButtonProps {
component: ComponentData;
onDataflowComplete?: (result: any) => void;
}
export const OptimizedButtonComponent: React.FC<OptimizedButtonProps> = ({
component,
onDataflowComplete,
}) => {
const [isExecuting, setIsExecuting] = useState(false);
const [executionTime, setExecutionTime] = useState<number | null>(null);
const [backgroundJobs, setBackgroundJobs] = useState<Set<string>>(new Set());
const config = component.webTypeConfig;
// 🔥 디바운싱으로 중복 클릭 방지
const handleClick = useDebouncedCallback(async () => {
if (isExecuting) return;
setIsExecuting(true);
const startTime = performance.now();
try {
// 🔥 현재 폼 데이터 수집
const formData = collectFormData();
if (config?.enableDataflowControl && config?.dataflowConfig) {
// 🔥 최적화된 버튼 실행 (즉시 응답)
await executeOptimizedButtonAction(component, formData);
} else {
// 🔥 기존 액션만 실행
await executeOriginalAction(config?.actionType || "save", formData);
}
} catch (error) {
console.error("Button execution failed:", error);
toast.error("버튼 실행 중 오류가 발생했습니다.");
} finally {
const endTime = performance.now();
setExecutionTime(endTime - startTime);
setIsExecuting(false);
}
}, 300); // 300ms 디바운싱
/**
* 🔥 최적화된 버튼 액션 실행
*/
const executeOptimizedButtonAction = async (
component: ComponentData,
formData: Record<string, any>
) => {
const config = component.webTypeConfig!;
// 🔥 API 호출 (즉시 응답)
const response = await apiClient.post(
"/api/button-dataflow/execute-optimized",
{
actionType: config.actionType,
buttonConfig: config,
buttonId: component.id,
formData: formData,
}
);
const { jobId, immediateResult } = response.data;
// 🔥 즉시 결과 처리
if (immediateResult) {
handleImmediateResult(config.actionType, immediateResult);
// 사용자에게 즉시 피드백
toast.success(
getSuccessMessage(config.actionType, config.dataflowTiming)
);
}
// 🔥 백그라운드 작업 추적
if (jobId && jobId !== "immediate") {
setBackgroundJobs((prev) => new Set([...prev, jobId]));
// 백그라운드 작업 완료 대기 (선택적)
if (config.dataflowTiming === "before") {
// before 타이밍은 결과를 기다려야 함
await waitForBackgroundJob(jobId);
} else {
// after/replace 타이밍은 백그라운드에서 조용히 처리
trackBackgroundJob(jobId);
}
}
};
/**
* 🔥 즉시 결과 처리
*/
const handleImmediateResult = (actionType: string, result: any) => {
switch (actionType) {
case "save":
if (result.success) {
// 폼 초기화 또는 목록 새로고침
refreshDataList?.();
}
break;
case "delete":
if (result.success) {
// 목록에서 제거
removeFromList?.(result.deletedId);
}
break;
case "search":
if (result.success) {
// 검색 결과 표시
displaySearchResults?.(result.data);
}
break;
default:
console.log(`${actionType} 액션 완료:`, result);
}
};
/**
* 🔥 성공 메시지 생성
*/
const getSuccessMessage = (actionType: string, timing?: string): string => {
const actionName = getActionDisplayName(actionType);
switch (timing) {
case "before":
return `${actionName} 작업을 처리 중입니다...`;
case "after":
return `${actionName}이 완료되었습니다. 추가 처리를 진행 중입니다.`;
case "replace":
return `사용자 정의 작업을 처리 중입니다...`;
default:
return `${actionName}이 완료되었습니다.`;
}
};
/**
* 🔥 백그라운드 작업 추적
*/
const trackBackgroundJob = (jobId: string) => {
// WebSocket이나 polling으로 작업 상태 확인
const pollJobStatus = async () => {
try {
const statusResponse = await apiClient.get(
`/api/button-dataflow/job-status/${jobId}`
);
const { status, result } = statusResponse.data;
if (status === "completed") {
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
// 백그라운드 작업 완료 알림 (조용하게)
if (result.executedActions > 0) {
toast.success(
`추가 처리가 완료되었습니다. (${result.executedActions}개 액션)`,
{ duration: 2000 }
);
}
onDataflowComplete?.(result);
} else if (status === "failed") {
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
console.error("Background job failed:", result);
} else {
// 아직 진행 중 - 1초 후 다시 확인
setTimeout(pollJobStatus, 1000);
}
} catch (error) {
console.error("Failed to check job status:", error);
}
};
// 즉시 상태 확인 시작
setTimeout(pollJobStatus, 500);
};
/**
* 🔥 백그라운드 작업 완료 대기 (before 타이밍용)
*/
const waitForBackgroundJob = async (jobId: string): Promise<void> => {
return new Promise((resolve, reject) => {
const checkStatus = async () => {
try {
const response = await apiClient.get(
`/api/button-dataflow/job-status/${jobId}`
);
const { status, result } = response.data;
if (status === "completed") {
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
toast.success("모든 처리가 완료되었습니다.");
onDataflowComplete?.(result);
resolve();
} else if (status === "failed") {
setBackgroundJobs((prev) => {
const newSet = new Set(prev);
newSet.delete(jobId);
return newSet;
});
toast.error("처리 중 오류가 발생했습니다.");
reject(new Error(result.error));
} else {
// 진행 중 - 500ms 후 다시 확인
setTimeout(checkStatus, 500);
}
} catch (error) {
reject(error);
}
};
checkStatus();
});
};
return (
<Button
onClick={handleClick}
disabled={isExecuting}
className={`
relative transition-all duration-200
${isExecuting ? "opacity-75 cursor-wait" : ""}
${backgroundJobs.size > 0 ? "bg-blue-50 border-blue-200" : ""}
`}
>
{/* 메인 버튼 내용 */}
{isExecuting ? (
<div className="flex items-center space-x-2">
<Spinner size="sm" />
<span>처리중...</span>
</div>
) : (
component.label || "버튼"
)}
{/* 백그라운드 작업 표시 */}
{backgroundJobs.size > 0 && !isExecuting && (
<div className="absolute -top-1 -right-1 h-3 w-3">
<div className="animate-pulse h-full w-full bg-blue-500 rounded-full"></div>
</div>
)}
{/* 개발 모드에서 성능 정보 표시 */}
{process.env.NODE_ENV === "development" && executionTime && (
<span className="ml-2 text-xs opacity-60">
{executionTime.toFixed(0)}ms
</span>
)}
</Button>
);
};
/**
* 🔥 액션 타입별 표시명
*/
function getActionDisplayName(actionType: string): string {
const displayNames = {
save: "저장",
delete: "삭제",
edit: "수정",
add: "추가",
search: "검색",
reset: "초기화",
submit: "제출",
close: "닫기",
popup: "팝업",
navigate: "페이지 이동",
};
return displayNames[actionType] || actionType;
}
/**
* 🔥 기존 액션 실행 (제어관리 없음)
*/
const executeOriginalAction = async (
actionType: string,
formData: Record<string, any>
): Promise<any> => {
const startTime = performance.now();
try {
const response = await apiClient.post(`/api/actions/${actionType}`, {
formData,
});
const executionTime = performance.now() - startTime;
console.log(`⚡ ${actionType} completed in ${executionTime.toFixed(2)}ms`);
return response.data;
} catch (error) {
console.error(`❌ ${actionType} failed:`, error);
throw error;
}
};
```
## 🔄 사용 시나리오
### 시나리오 1: 저장 + 승인 프로세스 (after 타이밍)
1. **설정 단계**
- 버튼 액션 타입: "save" (저장)
- 제어관리 활성화: ✅
- 실행 타이밍: "after" (저장 후)
- 제어 모드: "간편 모드"
- 관계도 선택: "승인 프로세스 관계도"
- 관계 선택: "문서 저장 → 결재 데이터 자동 생성"
2. **실행 단계**
- 사용자가 저장 버튼 클릭
- **1단계**: 문서 저장 실행 (기존 save 액션)
- **2단계**: 저장 성공 후 제어관리 실행
- 조건 검증: 문서 상태, 작성자 권한 등
- 조건 만족 시 결재 테이블에 데이터 자동 삽입
- 관련 승인자에게 알림 발송
### 시나리오 2: 삭제 + 관련 데이터 정리 (before 타이밍)
1. **설정 단계**
- 버튼 액션 타입: "delete" (삭제)
- 제어관리 활성화: ✅
- 실행 타이밍: "before" (삭제 전)
- 제어 모드: "고급 모드"
- 소스 테이블: "order_master"
- 조건 설정: `status != 'completed' AND created_date > 30일전`
- 액션 설정: 관련 order_items, payment_info 테이블 사전 정리
2. **실행 단계**
- 주문 삭제 버튼 클릭
- **1단계**: 삭제 전 제어관리 실행
- 조건 검증: 삭제 가능 상태인지 확인
- 관련 테이블 데이터 사전 정리
- **2단계**: 메인 주문 데이터 삭제 실행
### 시나리오 3: 복잡한 비즈니스 로직 (replace 타이밍)
1. **설정 단계**
- 버튼 액션 타입: "submit" (제출)
- 제어관리 활성화: ✅
- 실행 타이밍: "replace" (기존 액션 대신)
- 제어 모드: "고급 모드"
- 복잡한 다단계 프로세스 설정:
- 재고 확인 → 가격 계산 → 할인 적용 → 주문 생성 → 결제 처리
2. **실행 단계**
- 주문 제출 버튼 클릭
- 기존 submit 액션은 실행되지 않음
- 제어관리에서 정의한 복잡한 비즈니스 로직만 실행
- 다단계 프로세스를 통한 주문 처리
## 🎯 기대 효과
### 개발자 관점
- 복잡한 비즈니스 로직을 코드 없이 GUI로 설정 가능
- 기존 제어관리 시스템의 재사용으로 개발 시간 단축
- 버튼 액션과 데이터 제어의 통합으로 일관된 UX 제공
### 사용자 관점
- 직관적인 버튼 클릭으로 복합적인 데이터 처리 가능
- 실시간 조건 검증으로 오류 방지
- 자동화된 데이터 흐름으로 업무 효율성 향상
### 시스템 관점
- 기존 인프라 활용으로 안정성 확보
- 모듈화된 설계로 유지보수성 향상
- 확장 가능한 아키텍처로 미래 요구사항 대응
## 📝 성능 최적화 중심 구현 우선순위
### 🚀 Phase 1: 즉시 효과 (1-2주) - 성능 기반
1.**즉시 응답 패턴** 구현
- OptimizedButtonComponent 개발
- 기존 액션 + 백그라운드 제어관리 분리
- 디바운싱 및 중복 클릭 방지
2.**기본 캐싱 시스템**
- DataflowConfigCache 구현 (메모리 캐시)
- 버튼별 설정 5분 TTL 캐싱
- 캐시 히트율 모니터링
3.**데이터베이스 최적화**
- 버튼별 제어관리 조회 인덱스 추가
- 전체 스캔 제거, 직접 조회로 변경
- 간단한 조건 메모리 평가
4.**간편 모드만 구현**
- 기존 관계도 선택 방식
- "after" 타이밍만 지원 (리스크 최소화)
- 복잡한 고급 모드는 2차에서
### 🔧 Phase 2: 고급 기능 (3-4주) - 안정성 확보
1. 🔄 **작업 큐 시스템**
- DataflowJobQueue 구현
- 배치 처리 (최대 3개 동시)
- 우선순위 기반 작업 처리
2. 🔄 **고급 모드 추가**
- ConditionBuilder, ActionBuilder 컴포넌트
- "before", "replace" 타이밍 지원
- 복잡한 조건 설정 UI
3. 🔄 **실시간 상태 추적**
- WebSocket 또는 polling 기반 작업 상태 확인
- 백그라운드 작업 진행률 표시
- 실패 시 자동 재시도 로직
4. 🔄 **성능 모니터링**
- 실시간 성능 지표 수집
- 느린 쿼리 감지 및 알림
- 캐시 효율성 분석
### ⚡ Phase 3: 고도화 (5-6주) - 사용자 경험 최적화
1.**프리로딩 시스템**
- 사용자 패턴 분석 기반 설정 미리 로드
- 예측적 캐싱 (자주 사용되는 관계도 우선)
- 브라우저 유휴 시간 활용 백그라운드 로딩
2.**고급 캐싱 전략**
- 다층 캐싱 (L1: 메모리, L2: 브라우저 저장소, L3: 서버)
- 캐시 무효화 전략 고도화
- 분산 캐싱 (여러 탭 간 공유)
3.**성능 대시보드**
- 관리자용 성능 모니터링 대시보드
- 버튼별 사용 빈도 및 성능 지표
- 자동 최적화 추천 시스템
4.**AI 기반 최적화**
- 사용자 패턴 학습
- 자동 설정 추천
- 성능 병목점 자동 감지
## 🎯 성능 목표 달성 지표
| Phase | 목표 응답 시간 | 사용자 체감 | 구현 내용 |
| ------- | -------------- | ----------- | -------------------- |
| Phase 1 | 50-200ms | 즉각 반응 | 즉시 응답 + 캐싱 |
| Phase 2 | 100-300ms | 빠른 응답 | 큐 시스템 + 최적화 |
| Phase 3 | 10-100ms | 초고속 | 프리로딩 + AI 최적화 |
## 💡 주요 성능 최적화 포인트
### 🔥 Critical Path 최적화
```
사용자 클릭 → 즉시 UI 응답 (0ms) → 기존 액션 (50-200ms) → 백그라운드 제어관리
```
### 🔥 Smart Caching Strategy
```
L1: 메모리 (1ms) → L2: 브라우저 저장소 (5-10ms) → L3: 서버 (100-300ms)
```
### 🔥 Database Optimization
```
기존: 전체 관계도 스캔 (500ms+)
새로운: 버튼별 직접 조회 (10-50ms)
```
이렇게 성능을 중심으로 단계적으로 구현하면, 사용자는 기존과 동일한 속도감을 유지하면서 강력한 제어관리 기능을 점진적으로 활용할 수 있게 됩니다!