# πŸ”§ λ²„νŠΌ μ œμ–΄κ΄€λ¦¬ κΈ°λŠ₯ 톡합 κ³„νšμ„œ ## πŸ“‹ ν”„λ‘œμ νŠΈ κ°œμš” ν˜„μž¬ κ΅¬μΆ•λ˜μ–΄ μžˆλŠ” **데이터 흐름 μ œμ–΄κ΄€λ¦¬ μ‹œμŠ€ν…œ(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) ``` μ΄λ ‡κ²Œ μ„±λŠ₯을 μ€‘μ‹¬μœΌλ‘œ λ‹¨κ³„μ μœΌλ‘œ κ΅¬ν˜„ν•˜λ©΄, μ‚¬μš©μžλŠ” κΈ°μ‘΄κ³Ό λ™μΌν•œ 속도감을 μœ μ§€ν•˜λ©΄μ„œ κ°•λ ₯ν•œ μ œμ–΄κ΄€λ¦¬ κΈ°λŠ₯을 μ μ§„μ μœΌλ‘œ ν™œμš©ν•  수 있게 λ©λ‹ˆλ‹€!