1775 lines
49 KiB
Markdown
1775 lines
49 KiB
Markdown
|
|
# 🔧 버튼 제어관리 기능 통합 계획서
|
||
|
|
|
||
|
|
## 📋 프로젝트 개요
|
||
|
|
|
||
|
|
현재 구축되어 있는 **데이터 흐름 제어관리 시스템(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)
|
||
|
|
```
|
||
|
|
|
||
|
|
이렇게 성능을 중심으로 단계적으로 구현하면, 사용자는 기존과 동일한 속도감을 유지하면서 강력한 제어관리 기능을 점진적으로 활용할 수 있게 됩니다!
|