# π§ λ²νΌ μ μ΄κ΄λ¦¬ κΈ°λ₯ ν΅ν© κ³νμ
## π νλ‘μ νΈ κ°μ
νμ¬ κ΅¬μΆλμ΄ μλ **λ°μ΄ν° νλ¦ μ μ΄κ΄λ¦¬ μμ€ν
(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 (
{/* μ μ΄κ΄λ¦¬ νμ±ν μ€μμΉ */}
onUpdateProperty("webTypeConfig.enableDataflowControl", checked)
}
/>
{/* μ μ΄κ΄λ¦¬κ° νμ±νλ κ²½μ°μλ§ μ€μ νμ */}
{config.enableDataflowControl && (
<>
{/* μ€ν νμ΄λ° μ ν */}
{config.dataflowTiming === "before" &&
"μ: μ μ₯ μ λ°μ΄ν° κ²μ¦, μμ μ κΆν νμΈ"}
{config.dataflowTiming === "after" &&
"μ: μ μ₯ ν μλ¦Ό λ°μ‘, μμ ν κ΄λ ¨ λ°μ΄ν° μ 리"}
{config.dataflowTiming === "replace" &&
"μ: 볡μ‘ν λΉμ¦λμ€ λ‘μ§μΌλ‘ κΈ°λ³Έ λμ μμ λ체"}
{/* μ μ΄ λͺ¨λ μ ν */}
{/* κ°νΈ λͺ¨λ UI */}
{dataflowConfig.controlMode === "simple" && (
)}
{/* κ³ κΈ λͺ¨λ UI */}
{dataflowConfig.controlMode === "advanced" && (
)}
{/* μ€ν μ΅μ
*/}
>
)}
);
};
```
#### 2.2 SimpleModePanel - κΈ°μ‘΄ κ΄κ³λ μ ν
```typescript
const SimpleModePanel: React.FC<{
config: ButtonDataflowConfig;
onUpdateProperty: (path: string, value: any) => void;
}> = ({ config, onUpdateProperty }) => {
const [diagrams, setDiagrams] = useState([]);
const [relationships, setRelationships] = useState([]);
return (
{/* κ΄κ³λ μ ν */}
{
onUpdateProperty(
"webTypeConfig.dataflowConfig.selectedDiagramId",
diagramId
);
// κ΄κ³λ μ ν μ κ΄λ ¨ κ΄κ³λ€ λ‘λ
loadRelationships(diagramId);
}}
/>
{/* κ΄κ³ μ ν */}
{config.selectedDiagramId && (
onUpdateProperty(
"webTypeConfig.dataflowConfig.selectedRelationshipId",
relationshipId
)
}
/>
)}
{/* μ νλ κ΄κ³ 미리보기 */}
{config.selectedRelationshipId && (
)}
);
};
```
#### 2.3 AdvancedModePanel - μ§μ 쑰건 μ€μ
```typescript
const AdvancedModePanel: React.FC<{
config: ButtonDataflowConfig;
onUpdateProperty: (path: string, value: any) => void;
}> = ({ config, onUpdateProperty }) => {
return (
{/* μμ€ ν
μ΄λΈ μ ν */}
onUpdateProperty(
"webTypeConfig.dataflowConfig.directControl.sourceTable",
table
)
}
/>
{/* νΈλ¦¬κ±° νμ
μ ν */}
{/* 쑰건 μ€μ */}
onUpdateProperty(
"webTypeConfig.dataflowConfig.directControl.conditions",
conditions
)
}
sourceTable={config.directControl?.sourceTable}
/>
{/* μ‘μ
μ€μ */}
onUpdateProperty(
"webTypeConfig.dataflowConfig.directControl.actions",
actions
)
}
sourceTable={config.directControl?.sourceTable}
/>
);
};
```
### Phase 3: μλΉμ€ κ³μΈ΅ κ°λ° (μ±λ₯ μ΅μ ν μ μ©)
#### 3.1 OptimizedButtonDataflowService (μ¦μ μλ΅ + λ°±κ·ΈλΌμ΄λ μ€ν)
```typescript
// π₯ μ±λ₯ μ΅μ ν: μΊμ± μμ€ν
class DataflowConfigCache {
private memoryCache = new Map();
private readonly TTL = 5 * 60 * 1000; // 5λΆ TTL
async getConfig(buttonId: string): Promise {
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 {
// μ€μ μλ² νΈμΆ λ‘μ§
return {} as ButtonDataflowConfig;
}
}
// π₯ μ±λ₯ μ΅μ ν: μμ
ν μμ€ν
class DataflowJobQueue {
private queue: Array<{
id: string;
buttonId: string;
actionType: ButtonActionType;
config: ButtonTypeConfig;
contextData: Record;
companyCode: string;
priority: "high" | "normal" | "low";
}> = [];
private processing = false;
// π₯ μ¦μ λ°ννλ μμ
νμ
enqueue(
buttonId: string,
actionType: ButtonActionType,
config: ButtonTypeConfig,
contextData: Record,
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 {
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 {
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,
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,
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,
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 {
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
): 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
): Promise {
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 {
// μ€μ μ μ΄κ΄λ¦¬ λ‘μ§ μ€ν
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,
companyCode: string
): Promise {
// κΈ°μ‘΄ μ μ΄κ΄λ¦¬ λ‘μ§ νμ©
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,
companyCode: string
): Promise {
// 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,
companyCode: string
): Promise {
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
): Promise {
// κΈ°μ‘΄ EventTriggerServiceμ 쑰건 νκ° λ‘μ§ μ¬νμ©
return await ConditionEvaluator.evaluate(conditions, data);
}
/**
* μ‘μ
μ€ν
*/
private static async executeActions(
actions: DataflowAction[],
contextData: Record,
companyCode: string
): Promise {
// κΈ°μ‘΄ EventTriggerServiceμ μ‘μ
μ€ν λ‘μ§ μ¬νμ©
return await ActionExecutor.execute(actions, contextData, companyCode);
}
}
```
#### 3.2 κΈ°μ‘΄ EventTriggerService νμ₯
```typescript
export class EventTriggerService {
// ... κΈ°μ‘΄ λ©μλλ€
/**
* π₯ NEW: νΉμ κ΄κ³ μ€ν (λ²νΌμμ νΈμΆ)
*/
static async executeSpecificRelationship(
relationship: JsonRelationship,
contextData: Record,
companyCode: string
): Promise {
// κ΄κ³μ ν΄λΉνλ μ μ΄ μ‘°κ±΄ λ° μ€ν κ³ν μΆμΆ
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,
companyCode: string
): Promise {
// 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 {
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 {
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 {
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 = ({
component,
onUpdateProperty,
}) => {
const config = component.webTypeConfig || {};
return (
{/* κΈ°μ‘΄ μ‘μ
νμ
μ ν (λ³κ²½ μμ) */}
{/* κΈ°μ‘΄ μ‘μ
λ³ μ€μ λ€ (variant, icon, confirmMessage λ±) */}
{/* ... κΈ°μ‘΄ UI μ»΄ν¬λνΈλ€ ... */}
{/* π₯ NEW: λͺ¨λ μ‘μ
μ μ μ΄κ΄λ¦¬ μ΅μ
μΆκ° */}
onUpdateProperty("webTypeConfig.enableDataflowControl", checked)
}
/>
{config.enableDataflowControl && (
{getActionDisplayName(config.actionType)} μ‘μ
κ³Ό
ν¨κ» λ°μ΄ν° νλ¦ μ μ΄ κΈ°λ₯μ΄ μ€νλ©λλ€.
)}
);
};
// μ‘μ
νμ
λ³ νμλͺ
ν¬νΌ ν¨μ
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 = ({
component,
onDataflowComplete,
}) => {
const [isExecuting, setIsExecuting] = useState(false);
const [executionTime, setExecutionTime] = useState(null);
const [backgroundJobs, setBackgroundJobs] = useState>(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
) => {
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 => {
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 (
);
};
/**
* π₯ μ‘μ
νμ
λ³ νμλͺ
*/
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
): Promise => {
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)
```
μ΄λ κ² μ±λ₯μ μ€μ¬μΌλ‘ λ¨κ³μ μΌλ‘ ꡬννλ©΄, μ¬μ©μλ κΈ°μ‘΄κ³Ό λμΌν μλκ°μ μ μ§νλ©΄μ κ°λ ₯ν μ μ΄κ΄λ¦¬ κΈ°λ₯μ μ μ§μ μΌλ‘ νμ©ν μ μκ² λ©λλ€!