# ๐Ÿ”„ ์ œ์–ด๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ฐœ์„  ๊ณ„ํš์„œ ## ๐Ÿ“‹ ๊ฐœ์š” ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์‹œ์Šคํ…œ์ด ์ถ”๊ฐ€๋˜๋ฉด์„œ ๊ธฐ์กด ์ œ์–ด๊ด€๋ฆฌ ๋กœ์ง๊ณผ ๋ฒ„ํŠผ ์—ฐ๋™ ๋ฐฉ์‹์˜ ๊ฐœ์„ ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ## ๐ŸŽฏ ์ฃผ์š” ๊ฐœ์„ ์‚ฌํ•ญ ### 1. ๋ช…์นญ ๋ณ€๊ฒฝ: "๊ด€๊ณ„๋„" โ†’ "๊ด€๊ณ„" #### 1.1 ๋ณ€๊ฒฝ ์ด์œ  - **๊ธฐ์กด**: "๊ด€๊ณ„๋„"๋Š” ๋‹ค์ด์–ด๊ทธ๋žจ ์ „์ฒด๋ฅผ ์˜๋ฏธํ•˜๋Š” ์šฉ์–ด - **ํ˜„์žฌ**: ์‹ค์ œ๋กœ๋Š” ๊ฐœ๋ณ„ "๊ด€๊ณ„"๋ฅผ ์„ค์ •ํ•˜๊ณ  ๊ด€๋ฆฌ - **๊ฐœ์„ **: ์‚ฌ์šฉ์ž ์ดํ•ด๋„ ํ–ฅ์ƒ ๋ฐ ์šฉ์–ด ์ผ๊ด€์„ฑ ํ™•๋ณด #### 1.2 ๋ณ€๊ฒฝ ๋Œ€์ƒ ํŒŒ์ผ๋“ค ```typescript // UI ์ปดํฌ๋„ŒํŠธ๋“ค frontend / components / screen / config - panels / ButtonDataflowConfigPanel.tsx; frontend / components / dataflow / DataFlowDesigner.tsx; frontend / components / dataflow / SaveDiagramModal.tsx; frontend / components / dataflow / RelationshipListModal.tsx; frontend / components / dataflow / connection / redesigned / RightPanel / ConnectionStep.tsx; // API ๋ฐ ์„œ๋น„์Šค frontend / lib / api / dataflow.ts; frontend / hooks / useDataFlowDesigner.ts; // ํƒ€์ž… ์ •์˜ frontend / types / control - management.ts; ``` ### 2. ๋ฒ„ํŠผ ์ œ์–ด๊ด€๋ฆฌ ๋กœ์ง ๊ฐœ์„  #### 2.1 ํ˜„์žฌ ๋ฌธ์ œ์  ```typescript // ๐Ÿ”ด ๊ธฐ์กด: ๋ณต์žกํ•œ ๊ด€๊ณ„๋„ ์„ ํƒ ๋ฐฉ์‹ interface ButtonDataflowConfig { controlMode: "simple" | "advanced"; selectedDiagramId?: number; // ๊ด€๊ณ„๋„ ์ „์ฒด ์„ ํƒ selectedRelationshipId?: string; // ๊ฐœ๋ณ„ ๊ด€๊ณ„ ์„ ํƒ // ... } ``` #### 2.2 ๊ฐœ์„  ๋ฐฉํ–ฅ ```typescript // ๐ŸŸข ๊ฐœ์„ : ๋‹จ์ˆœํ™”๋œ ๊ด€๊ณ„ ์ง์ ‘ ์„ ํƒ interface ButtonDataflowConfig { controlMode: "relationship" | "external_call" | "custom"; // ๊ด€๊ณ„ ๊ธฐ๋ฐ˜ ์ œ์–ด relationshipConfig?: { relationshipId: string; // ๊ด€๊ณ„ ์ง์ ‘ ์„ ํƒ relationshipName: string; // ๊ด€๊ณ„๋ช… ํ‘œ์‹œ executionTiming: "before" | "after" | "replace"; contextData?: Record; // ์‹คํ–‰ ์‹œ ์ „๋‹ฌํ•  ์ปจํ…์ŠคํŠธ }; // ์™ธ๋ถ€ํ˜ธ์ถœ ์ œ์–ด externalCallConfig?: { configId: string; // external_call_configs ID configName: string; // ์„ค์ •๋ช… ํ‘œ์‹œ executionTiming: "before" | "after" | "replace"; dataMappingEnabled: boolean; // ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์‚ฌ์šฉ ์—ฌ๋ถ€ }; // ์ปค์Šคํ…€ ์ œ์–ด customConfig?: { actionType: string; parameters: Record; }; } ``` ### 3. ์™ธ๋ถ€ํ˜ธ์ถœ ์—ฐ๋™ ๊ฐœ์„  #### 3.1 ํ˜„์žฌ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ๋ฐฉ์‹ ```typescript // ๐Ÿ”ด ํ˜„์žฌ: ๋ณต์žกํ•œ ์„ค์ • ๊ตฌ์กฐ interface ExternalCallConfig { callType: "rest-api"; restApiSettings: { apiUrl: string; httpMethod: string; // ... ๋งŽ์€ ์„ค์ •๋“ค }; } ``` #### 3.2 ๊ฐœ์„ ๋œ ์—ฐ๋™ ๋ฐฉ์‹ ```typescript // ๐ŸŸข ๊ฐœ์„ : ๋‹จ์ˆœํ™”๋œ ์ฐธ์กฐ ๊ตฌ์กฐ interface ButtonExternalCallConfig { // 1๋‹จ๊ณ„: ์ €์žฅ๋œ ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์„ ํƒ externalCallConfigId: string; // external_call_configs ํ…Œ์ด๋ธ” ID configName: string; // ์„ค์ •๋ช… (UI ํ‘œ์‹œ์šฉ) // 2๋‹จ๊ณ„: ์‹คํ–‰ ์‹œ์  ์„ค์ • executionTiming: "before" | "after" | "replace"; // 3๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋ฐฉ์‹ dataMapping: { enabled: boolean; // ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์‚ฌ์šฉ ์—ฌ๋ถ€ sourceMode: "form" | "table" | "custom"; // ๋ฐ์ดํ„ฐ ์†Œ์Šค sourceConfig?: { tableName?: string; // table ๋ชจ๋“œ์šฉ customData?: Record; // custom ๋ชจ๋“œ์šฉ }; }; // 4๋‹จ๊ณ„: ์‹คํ–‰ ์˜ต์…˜ executionOptions: { rollbackOnError: boolean; // ์‹คํŒจ ์‹œ ๋กค๋ฐฑ showLoadingIndicator: boolean; // ๋กœ๋”ฉ ํ‘œ์‹œ successMessage?: string; // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ errorMessage?: string; // ์‹คํŒจ ๋ฉ”์‹œ์ง€ }; } ``` ### 4. ๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰ ๋กœ์ง ๊ฐœ์„  #### 4.1 ํ˜„์žฌ ์‹คํ–‰ ํ”Œ๋กœ์šฐ ```mermaid graph TD A[๋ฒ„ํŠผ ํด๋ฆญ] --> B[๊ธฐ์กด ์•ก์…˜ ์‹คํ–‰] B --> C[์ œ์–ด๊ด€๋ฆฌ ํ™•์ธ] C --> D[๊ด€๊ณ„๋„ ์กฐํšŒ] D --> E[๊ด€๊ณ„ ์ฐพ๊ธฐ] E --> F[์กฐ๊ฑด ๊ฒ€์ฆ] F --> G[์•ก์…˜ ์‹คํ–‰] ``` #### 4.2 ๊ฐœ์„ ๋œ ์‹คํ–‰ ํ”Œ๋กœ์šฐ ```mermaid graph TD A[๋ฒ„ํŠผ ํด๋ฆญ] --> B[์ œ์–ด ์„ค์ • ํ™•์ธ] B --> C{์ œ์–ด ํƒ€์ž…} C -->|๊ด€๊ณ„| D[๊ด€๊ณ„ ์ง์ ‘ ์‹คํ–‰] C -->|์™ธ๋ถ€ํ˜ธ์ถœ| E[์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰] C -->|์—†์Œ| F[๊ธฐ์กด ์•ก์…˜๋งŒ ์‹คํ–‰] D --> G[์กฐ๊ฑด ๊ฒ€์ฆ] G --> H[๊ด€๊ณ„ ์•ก์…˜ ์‹คํ–‰] E --> I[๋ฐ์ดํ„ฐ ๋งคํ•‘] I --> J[์™ธ๋ถ€ API ํ˜ธ์ถœ] J --> K[์‘๋‹ต ์ฒ˜๋ฆฌ] H --> L[์™„๋ฃŒ] K --> L F --> L ``` #### 4.3 ๊ฐœ์„ ๋œ ButtonActionExecutor ```typescript export class ButtonActionExecutor { /** * ๐Ÿ”ฅ ๊ฐœ์„ ๋œ ๋ฒ„ํŠผ ์•ก์…˜ ์‹คํ–‰ */ static async executeButtonAction( buttonConfig: ExtendedButtonTypeConfig, formData: Record, context: ButtonExecutionContext ): Promise { const executionPlan = this.createExecutionPlan(buttonConfig); const results: ExecutionResult[] = []; try { // 1. Before ํƒ€์ด๋ฐ ์ œ์–ด ์‹คํ–‰ if (executionPlan.beforeControls.length > 0) { const beforeResults = await this.executeControls( executionPlan.beforeControls, formData, context ); results.push(...beforeResults); } // 2. ๋ฉ”์ธ ์•ก์…˜ ์‹คํ–‰ (replace๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ) if (!executionPlan.hasReplaceControl) { const mainResult = await this.executeMainAction( buttonConfig, formData, context ); results.push(mainResult); } // 3. After ํƒ€์ด๋ฐ ์ œ์–ด ์‹คํ–‰ if (executionPlan.afterControls.length > 0) { const afterResults = await this.executeControls( executionPlan.afterControls, formData, context ); results.push(...afterResults); } return { success: true, results, executionTime: Date.now() - context.startTime, }; } catch (error) { // ๋กค๋ฐฑ ์ฒ˜๋ฆฌ await this.handleExecutionError(error, results, buttonConfig); throw error; } } /** * ๐Ÿ”ฅ ์ œ์–ด ์‹คํ–‰ (๊ด€๊ณ„ ๋˜๋Š” ์™ธ๋ถ€ํ˜ธ์ถœ) */ private static async executeControls( controls: ControlConfig[], formData: Record, context: ButtonExecutionContext ): Promise { const results: ExecutionResult[] = []; for (const control of controls) { switch (control.type) { case "relationship": const relationshipResult = await this.executeRelationship( control.relationshipConfig!, formData, context ); results.push(relationshipResult); break; case "external_call": const externalCallResult = await this.executeExternalCall( control.externalCallConfig!, formData, context ); results.push(externalCallResult); break; } } return results; } /** * ๐Ÿ”ฅ ๊ด€๊ณ„ ์‹คํ–‰ */ private static async executeRelationship( config: RelationshipConfig, formData: Record, context: ButtonExecutionContext ): Promise { // 1. ๊ด€๊ณ„ ์ •๋ณด ์กฐํšŒ const relationship = await RelationshipAPI.getRelationshipById( config.relationshipId ); // 2. ์ปจํ…์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ค€๋น„ const contextData = { ...formData, ...config.contextData, buttonId: context.buttonId, screenId: context.screenId, userId: context.userId, companyCode: context.companyCode, }; // 3. ๊ด€๊ณ„ ์‹คํ–‰ return await EventTriggerService.executeSpecificRelationship( relationship, contextData, context.companyCode ); } /** * ๐Ÿ”ฅ ์™ธ๋ถ€ํ˜ธ์ถœ ์‹คํ–‰ */ private static async executeExternalCall( config: ExternalCallConfig, formData: Record, context: ButtonExecutionContext ): Promise { // 1. ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์กฐํšŒ const externalCallConfig = await ExternalCallConfigAPI.getConfigById( config.configId ); // 2. ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ฒ˜๋ฆฌ let mappedData = formData; if (config.dataMappingEnabled && externalCallConfig.dataMappingConfig) { mappedData = await DataMappingService.processOutboundData( externalCallConfig.dataMappingConfig.outboundMapping, formData ); } // 3. ์™ธ๋ถ€ API ํ˜ธ์ถœ const callResult = await ExternalCallService.executeWithDataMapping( externalCallConfig.configData, externalCallConfig.dataMappingConfig, mappedData ); // 4. ์‘๋‹ต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ (Inbound ๋งคํ•‘) if ( callResult.success && config.dataMappingEnabled && externalCallConfig.dataMappingConfig?.direction === "inbound" ) { await DataMappingService.processInboundData( callResult.response, externalCallConfig.dataMappingConfig.inboundMapping! ); } return { success: callResult.success, message: callResult.success ? "์™ธ๋ถ€ํ˜ธ์ถœ ์„ฑ๊ณต" : callResult.error, executionTime: callResult.executionTime, data: callResult, }; } } ``` ### 5. UI/UX ๊ฐœ์„  ์‚ฌํ•ญ #### 5.1 ๋ฒ„ํŠผ ์„ค์ • ํŒจ๋„ ๊ฐœ์„  ```typescript // ๐ŸŸข ๋‹จ์ˆœํ™”๋œ ์ œ์–ด ์„ค์ • UI const ButtonControlConfigPanel = () => { return ( ๋ฒ„ํŠผ ์ œ์–ด ์„ค์ • ์ œ์–ด ์—†์Œ ๊ด€๊ณ„ ์‹คํ–‰ ์™ธ๋ถ€ ํ˜ธ์ถœ ); }; ``` #### 5.2 ๊ด€๊ณ„ ์„ ํƒ ์ปดํฌ๋„ŒํŠธ ```typescript const RelationshipSelector = ({ onSelect }) => { const [relationships, setRelationships] = useState([]); useEffect(() => { // ์ „์ฒด ๊ด€๊ณ„ ๋ชฉ๋ก ๋กœ๋“œ (๊ด€๊ณ„๋„๋ณ„ ๊ตฌ๋ถ„ ์—†์ด) loadAllRelationships(); }, []); return (
); }; ``` ### 6. ๊ตฌํ˜„ ์šฐ์„ ์ˆœ์œ„ #### Phase 1: ๋ช…์นญ ๋ณ€๊ฒฝ (1์ผ) 1. **UI ํ…์ŠคํŠธ ๋ณ€๊ฒฝ**: "๊ด€๊ณ„๋„" โ†’ "๊ด€๊ณ„" 2. **๋ณ€์ˆ˜๋ช… ์ •๋ฆฌ**: `diagram` โ†’ `relationship` ๊ด€๋ จ 3. **API ์—”๋“œํฌ์ธํŠธ ์ •๋ฆฌ**: ์ผ๊ด€์„ฑ ์žˆ๋Š” ๋ช…๋ช… #### Phase 2: ๋ฒ„ํŠผ ์ œ์–ด ๋กœ์ง ๊ฐœ์„  (2-3์ผ) 1. **ButtonDataflowConfig ํƒ€์ž… ๊ฐœ์„ ** 2. **RelationshipSelector ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ** 3. **ExternalCallSelector ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ** 4. **ButtonActionExecutor ๋กœ์ง ๊ฐœ์„ ** #### Phase 3: ์™ธ๋ถ€ํ˜ธ์ถœ ํ†ตํ•ฉ (1-2์ผ) 1. **์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ • ์ฐธ์กฐ ๋ฐฉ์‹ ๊ฐœ์„ ** 2. **๋ฐ์ดํ„ฐ ๋งคํ•‘ ํ†ตํ•ฉ** 3. **์‹คํ–‰ ํ”Œ๋กœ์šฐ ์ตœ์ ํ™”** #### Phase 4: ํ…Œ์ŠคํŠธ ๋ฐ ์ตœ์ ํ™” (1์ผ) 1. **์ „์ฒด ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ** 2. **์„ฑ๋Šฅ ์ตœ์ ํ™”** 3. **์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ ์—…๋ฐ์ดํŠธ** ### 7. ๊ธฐ๋Œ€ ํšจ๊ณผ #### 7.1 ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„  - **์ง๊ด€์ ์ธ ์šฉ์–ด**: "๊ด€๊ณ„๋„" โ†’ "๊ด€๊ณ„"๋กœ ์ดํ•ด๋„ ํ–ฅ์ƒ - **๋‹จ์ˆœํ™”๋œ ์„ค์ •**: ๋ณต์žกํ•œ ๊ด€๊ณ„๋„ ํƒ์ƒ‰ โ†’ ์ง์ ‘ ๊ด€๊ณ„ ์„ ํƒ - **ํ†ตํ•ฉ๋œ ์ œ์–ด**: ๊ด€๊ณ„ ์‹คํ–‰๊ณผ ์™ธ๋ถ€ํ˜ธ์ถœ์„ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ด€๋ฆฌ #### 7.2 ๊ฐœ๋ฐœ ํšจ์œจ์„ฑ ํ–ฅ์ƒ - **๋ช…ํ™•ํ•œ ์ฑ…์ž„ ๋ถ„๋ฆฌ**: ๊ด€๊ณ„ ๊ด€๋ฆฌ์™€ ์™ธ๋ถ€ํ˜ธ์ถœ ๊ด€๋ฆฌ ๋ถ„๋ฆฌ - **์žฌ์‚ฌ์šฉ์„ฑ ์ฆ๋Œ€**: ์™ธ๋ถ€ํ˜ธ์ถœ ์„ค์ •์˜ ์žฌ์‚ฌ์šฉ์„ฑ ํ–ฅ์ƒ - **์œ ์ง€๋ณด์ˆ˜์„ฑ ๊ฐœ์„ **: ๋‹จ์ˆœํ™”๋œ ๋กœ์ง์œผ๋กœ ๋””๋ฒ„๊น… ์šฉ์ด #### 7.3 ์‹œ์Šคํ…œ ํ™•์žฅ์„ฑ - **์ƒˆ๋กœ์šด ์ œ์–ด ํƒ€์ž… ์ถ”๊ฐ€ ์šฉ์ด**: ํ”Œ๋Ÿฌ๊ทธ์ธ ๋ฐฉ์‹์œผ๋กœ ํ™•์žฅ ๊ฐ€๋Šฅ - **๋ฐ์ดํ„ฐ ๋งคํ•‘ ์‹œ์Šคํ…œ ์™„์ „ ํ™œ์šฉ**: ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ ์œ ์—ฐํ•œ ์—ฐ๋™ - **๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋กœ๊น… ๊ฐ•ํ™”**: ๊ฐ ๋‹จ๊ณ„๋ณ„ ์ƒ์„ธํ•œ ์‹คํ–‰ ๋กœ๊ทธ ์ด ๊ฐœ์„  ๊ณ„ํš์„ ํ†ตํ•ด ์ œ์–ด๊ด€๋ฆฌ ์‹œ์Šคํ…œ์ด ๋”์šฑ ์ง๊ด€์ ์ด๊ณ  ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.