# ๐Ÿ”ง ์ œ์–ด๊ด€๋ฆฌ ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ํ†ตํ•ฉ ๊ฐœ์„  ๊ณ„ํš์„œ ## ๐Ÿ“‹ ํ”„๋กœ์ ํŠธ ๊ฐœ์š” ### ๋ชฉ์  ํ˜„์žฌ ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ์—์„œ ๊ด€๋ฆฌ๋˜๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ปค๋„ฅ์…˜ ์ •๋ณด๋ฅผ ์ œ์–ด๊ด€๋ฆฌ์˜ ๋ฐ์ดํ„ฐ ์ €์žฅ ์•ก์…˜์—์„œ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ†ตํ•ฉํ•˜์—ฌ, ์‚ฌ์šฉ์ž๊ฐ€ ๋‹ค์–‘ํ•œ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ### ํ˜„์žฌ ์ƒํ™ฉ ๋ถ„์„ #### ๊ธฐ์กด ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ๊ด€๋ฆฌ - **ํ…Œ์ด๋ธ”**: `external_db_connections` - **์ง€์› DB**: MySQL, PostgreSQL, Oracle, SQL Server, SQLite, MariaDB - **๊ด€๋ฆฌ ๊ธฐ๋Šฅ**: ์—ฐ๊ฒฐ ์ •๋ณด CRUD, ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ, ์•”ํ˜ธํ™” ์ €์žฅ - **API**: `/api/external-db-connections/*` ์—”๋“œํฌ์ธํŠธ #### ๊ธฐ์กด ์ œ์–ด๊ด€๋ฆฌ ์‹œ์Šคํ…œ - **์—ฐ๊ฒฐ ์ข…๋ฅ˜**: ํ˜„์žฌ "๋ฐ์ดํ„ฐ ์ €์žฅ" ํƒ€์ž… ์ง€์› - **์•ก์…˜ ํƒ€์ž…**: INSERT, UPDATE, DELETE - **๋งคํ•‘**: FROM ํ…Œ์ด๋ธ” โ†’ TO ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋งคํ•‘ - **์ œ์•ฝ**: ํ˜„์žฌ๋Š” ๋ฉ”์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋‚ด์—์„œ๋งŒ ๋™์ž‘ ### ๋ณ€๊ฒฝ ์š”๊ตฌ์‚ฌํ•ญ 1. **์ปค๋„ฅ์…˜ ์„ ํƒ ๊ธฐ๋Šฅ ์ถ”๊ฐ€** - INSERT ์•ก์…˜ ํƒ€์ž… ์„ ํƒ ์‹œ ์ปค๋„ฅ์…˜ ์„ ํƒ ๋‹จ๊ณ„ ์ถ”๊ฐ€ - FROM/TO ํ…Œ์ด๋ธ” ๊ฐ๊ฐ์— ๋Œ€ํ•ด ๋…๋ฆฝ์ ์ธ ์ปค๋„ฅ์…˜ ์„ค์ • 2. **ํ…Œ์ด๋ธ” ์„ ํƒ ๊ธฐ๋Šฅ ๊ฐœ์„ ** - ์„ ํƒํ•œ ์ปค๋„ฅ์…˜์— ์žˆ๋Š” ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋™์  ๋กœ๋”ฉ - FROM ์ปค๋„ฅ์…˜์˜ ํ…Œ์ด๋ธ”๊ณผ TO ์ปค๋„ฅ์…˜์˜ ํ…Œ์ด๋ธ” ๋…๋ฆฝ ์„ ํƒ 3. **์ปฌ๋Ÿผ ๋งคํ•‘ ๊ทœ์น™ ์œ ์ง€** - FROM ํ…Œ์ด๋ธ”์˜ 1๊ฐœ ์ปฌ๋Ÿผ โ†’ TO ํ…Œ์ด๋ธ”์˜ 2๊ฐœ ์ด์ƒ ์ปฌ๋Ÿผ ๋งคํ•‘ ๊ฐ€๋Šฅ - FROM ํ…Œ์ด๋ธ”์˜ 2๊ฐœ ์ด์ƒ ์ปฌ๋Ÿผ โ†’ TO ํ…Œ์ด๋ธ”์˜ 1๊ฐœ ์ปฌ๋Ÿผ ๋งคํ•‘ **๋ถˆ๊ฐ€** - ๊ธฐ์กด UI ๊ตฌ์กฐ ์ตœ๋Œ€ํ•œ ์œ ์ง€ ## ๐Ÿ—๏ธ ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„ ### 1. ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ํ™•์žฅ #### ๊ธฐ์กด DataSaveSettings ๊ตฌ์กฐ ```typescript interface DataSaveSettings { connectionType: "data-save"; actions: Array<{ actionType: "insert" | "update" | "delete"; targetTable: string; fieldMappings: FieldMapping[]; }>; } ``` #### ๊ฐœ์„ ๋œ DataSaveSettings ๊ตฌ์กฐ ```typescript interface EnhancedDataSaveSettings { connectionType: "data-save"; actions: Array<{ actionType: "insert" | "update" | "delete"; // ๐Ÿ†• ์ปค๋„ฅ์…˜ ์ •๋ณด ์ถ”๊ฐ€ fromConnection?: { connectionId?: number; connectionName?: string; dbType?: string; }; toConnection?: { connectionId?: number; connectionName?: string; dbType?: string; }; // ๊ธฐ์กด ํ•„๋“œ๋“ค targetTable: string; fromTable?: string; // ๐Ÿ†• ๋ช…์‹œ์ ์œผ๋กœ ์ถ”๊ฐ€ fieldMappings: EnhancedFieldMapping[]; }>; } interface EnhancedFieldMapping { sourceTable: string; sourceField: string; targetTable: string; targetField: string; defaultValue?: string; transformFunction?: string; // ๐Ÿ†• ์ปค๋„ฅ์…˜ ์ •๋ณด ์ถ”๊ฐ€ sourceConnectionId?: number; targetConnectionId?: number; } ``` ### 2. UI ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ๊ฐœ์„  #### ๋‹จ๊ณ„๋ณ„ ์„ค์ • ํ”Œ๋กœ์šฐ ``` 1. ์•ก์…˜ ํƒ€์ž… ์„ ํƒ (INSERT/UPDATE/DELETE) โ†“ 2. [๋ชจ๋“  ์•ก์…˜ ํƒ€์ž…] ์ปค๋„ฅ์…˜ ์„ค์ • ๋‹จ๊ณ„ โ”œโ”€ FROM ์ปค๋„ฅ์…˜ ์„ ํƒ (๋ฐ์ดํ„ฐ ์†Œ์Šค) โ””โ”€ TO ์ปค๋„ฅ์…˜ ์„ ํƒ (๋ฐ์ดํ„ฐ ๋Œ€์ƒ) โ†“ 3. ํ…Œ์ด๋ธ” ์„ ํƒ ๋‹จ๊ณ„ โ”œโ”€ FROM ํ…Œ์ด๋ธ” ์„ ํƒ (์„ ํƒํ•œ FROM ์ปค๋„ฅ์…˜์˜ ํ…Œ์ด๋ธ”๋“ค) โ””โ”€ TO ํ…Œ์ด๋ธ” ์„ ํƒ (์„ ํƒํ•œ TO ์ปค๋„ฅ์…˜์˜ ํ…Œ์ด๋ธ”๋“ค) โ†“ 4. ์ปฌ๋Ÿผ ๋งคํ•‘ ๋‹จ๊ณ„ (์•ก์…˜ ํƒ€์ž…๋ณ„ UI) โ”œโ”€ INSERT: InsertFieldMappingPanel โ”œโ”€ UPDATE: UpdateFieldMappingPanel โ””โ”€ DELETE: DeleteConditionPanel ``` #### ์ƒˆ๋กœ์šด ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ```typescript // 1. ์ปค๋„ฅ์…˜ ์„ ํƒ ์ปดํฌ๋„ŒํŠธ (์‹ ๊ทœ) interface ConnectionSelectionPanelProps { fromConnectionId?: number; toConnectionId?: number; onFromConnectionChange: (connectionId: number) => void; onToConnectionChange: (connectionId: number) => void; availableConnections: ExternalDbConnection[]; actionType: "insert" | "update" | "delete"; // ๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ์ง€์› allowSameConnection?: boolean; currentConnectionId?: number; // ํ˜„์žฌ ๋ฉ”์ธ DB ์ปค๋„ฅ์…˜ } // 2. ํ…Œ์ด๋ธ” ์„ ํƒ ์ปดํฌ๋„ŒํŠธ (ํ™•์žฅ) interface TableSelectionPanelProps { fromConnectionId?: number; toConnectionId?: number; selectedFromTable?: string; selectedToTable?: string; onFromTableChange: (tableName: string) => void; onToTableChange: (tableName: string) => void; actionType: "insert" | "update" | "delete"; // ๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ์ง€์› allowSameTable?: boolean; showSameTableWarning?: boolean; } // 3. ์•ก์…˜ ํƒ€์ž…๋ณ„ ๋งคํ•‘ ์ปดํฌ๋„ŒํŠธ (ํ™•์žฅ) interface InsertFieldMappingPanelProps { // INSERT: FROM โ†’ TO ๋งคํ•‘ } interface UpdateFieldMappingPanelProps { // UPDATE: FROM ์กฐ๊ฑด + TO ์—…๋ฐ์ดํŠธ ํ•„๋“œ fromTableColumns: ColumnInfo[]; toTableColumns: ColumnInfo[]; updateConditions: UpdateCondition[]; updateFields: UpdateFieldMapping[]; onConditionsChange: (conditions: UpdateCondition[]) => void; onFieldsChange: (fields: UpdateFieldMapping[]) => void; } interface DeleteConditionPanelProps { // DELETE: FROM ์กฐ๊ฑด + TO ์‚ญ์ œ ์กฐ๊ฑด fromTableColumns: ColumnInfo[]; toTableColumns: ColumnInfo[]; deleteConditions: DeleteCondition[]; onConditionsChange: (conditions: DeleteCondition[]) => void; } ``` ## ๐Ÿ”ง ๊ตฌํ˜„ ์„ธ๋ถ€ ๊ณ„ํš ### Phase 1: ๋ฐฑ์—”๋“œ ์ธํ”„๋ผ ๊ตฌ์ถ• (2์ฃผ) #### 1.1 ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ ์กฐํšŒ API ํ™•์žฅ ```typescript // ๊ธฐ์กด API ํ™•์žฅ GET / api / external - db - connections / active; // ์‘๋‹ต: ํ™œ์„ฑํ™”๋œ ๋ชจ๋“  ์ปค๋„ฅ์…˜ ๋ชฉ๋ก GET / api / external - db - connections / { connectionId } / tables; // ์‘๋‹ต: ํŠน์ • ์ปค๋„ฅ์…˜์˜ ํ…Œ์ด๋ธ” ๋ชฉ๋ก GET / api / external - db - connections / { connectionId } / tables / { tableName } / columns; // ์‘๋‹ต: ํŠน์ • ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ ์ •๋ณด ``` #### 1.2 ๋‹ค์ค‘ ์ปค๋„ฅ์…˜ ์ฟผ๋ฆฌ ์‹คํ–‰ ์„œ๋น„์Šค ```typescript export class MultiConnectionQueryService { // ์†Œ์Šค ์ปค๋„ฅ์…˜์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ async fetchDataFromConnection( connectionId: number, tableName: string, conditions?: Record ): Promise[]>; // ๋Œ€์ƒ ์ปค๋„ฅ์…˜์— ๋ฐ์ดํ„ฐ ์‚ฝ์ž… async insertDataToConnection( connectionId: number, tableName: string, data: Record ): Promise; // ๐Ÿ†• ๋Œ€์ƒ ์ปค๋„ฅ์…˜์— ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ async updateDataToConnection( connectionId: number, tableName: string, data: Record, conditions: Record ): Promise; // ๐Ÿ†• ๋Œ€์ƒ ์ปค๋„ฅ์…˜์—์„œ ๋ฐ์ดํ„ฐ ์‚ญ์ œ async deleteDataFromConnection( connectionId: number, tableName: string, conditions: Record ): Promise; // ์ปค๋„ฅ์…˜๋ณ„ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ์กฐํšŒ async getTablesFromConnection(connectionId: number): Promise; // ์ปค๋„ฅ์…˜๋ณ„ ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ async getColumnsFromConnection( connectionId: number, tableName: string ): Promise; // ๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ์ „์šฉ ๋ฉ”์„œ๋“œ๋“ค async validateSelfTableOperation( tableName: string, operation: "update" | "delete", conditions: any[] ): Promise; // ๐Ÿ†• ๋ฉ”์ธ DB ์ž‘์—… (connectionId = 0์ธ ๊ฒฝ์šฐ) async executeOnMainDatabase( operation: "select" | "insert" | "update" | "delete", tableName: string, data?: Record, conditions?: Record ): Promise; } ``` #### 1.3 ์ œ์–ด๊ด€๋ฆฌ ์„œ๋น„์Šค ํ™•์žฅ ```typescript export class EnhancedDataflowControlService { // ๊ธฐ์กด ๋ฉ”์„œ๋“œ ํ™•์žฅ async executeDataflowControl( diagramId: number, relationshipId: string, triggerType: "insert" | "update" | "delete", sourceData: Record, tableName: string, // ๐Ÿ†• ์ถ”๊ฐ€ ๋งค๊ฐœ๋ณ€์ˆ˜ sourceConnectionId?: number, targetConnectionId?: number ): Promise<{ success: boolean; message: string; executedActions?: any[]; errors?: string[]; }>; // ๐Ÿ†• ๋‹ค์ค‘ ์ปค๋„ฅ์…˜ INSERT ์‹คํ–‰ private async executeMultiConnectionInsert( action: ControlAction, sourceData: Record, sourceConnectionId?: number, targetConnectionId?: number ): Promise; // ๐Ÿ†• ๋‹ค์ค‘ ์ปค๋„ฅ์…˜ UPDATE ์‹คํ–‰ private async executeMultiConnectionUpdate( action: ControlAction, sourceData: Record, sourceConnectionId?: number, targetConnectionId?: number ): Promise; // ๐Ÿ†• ๋‹ค์ค‘ ์ปค๋„ฅ์…˜ DELETE ์‹คํ–‰ private async executeMultiConnectionDelete( action: ControlAction, sourceData: Record, sourceConnectionId?: number, targetConnectionId?: number ): Promise; } ``` ### Phase 2: ํ”„๋ก ํŠธ์—”๋“œ UI ๊ฐœ์„  (3์ฃผ) #### 2.1 ConnectionSelectionPanel ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋ฐœ ```typescript export const ConnectionSelectionPanel: React.FC< ConnectionSelectionPanelProps > = ({ fromConnectionId, toConnectionId, onFromConnectionChange, onToConnectionChange, availableConnections, actionType, }) => { const getConnectionLabels = () => { switch (actionType) { case "insert": return { from: { title: "์†Œ์Šค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ", desc: "๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์„ ํƒํ•˜์„ธ์š”", }, to: { title: "๋Œ€์ƒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ", desc: "๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์„ ํƒํ•˜์„ธ์š”", }, }; case "update": return { from: { title: "์กฐ๊ฑด ํ™•์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค", desc: "์—…๋ฐ์ดํŠธ ์กฐ๊ฑด์„ ํ™•์ธํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์„ ํƒํ•˜์„ธ์š” (์ž๊ธฐ ์ž์‹  ๊ฐ€๋Šฅ)", }, to: { title: "์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค", desc: "๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์„ ํƒํ•˜์„ธ์š” (์ž๊ธฐ ์ž์‹  ๊ฐ€๋Šฅ)", }, }; case "delete": return { from: { title: "์กฐ๊ฑด ํ™•์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค", desc: "์‚ญ์ œ ์กฐ๊ฑด์„ ํ™•์ธํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์„ ํƒํ•˜์„ธ์š” (์ž๊ธฐ ์ž์‹  ๊ฐ€๋Šฅ)", }, to: { title: "์‚ญ์ œ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค", desc: "๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์„ ์„ ํƒํ•˜์„ธ์š” (์ž๊ธฐ ์ž์‹  ๊ฐ€๋Šฅ)", }, }; } }; // ๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ์‹œ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ const getSameConnectionWarning = () => { if (fromConnectionId === toConnectionId && fromConnectionId) { switch (actionType) { case "update": return "โš ๏ธ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ UPDATE ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์กฐ๊ฑด์„ ์‹ ์ค‘ํžˆ ์„ค์ •ํ•˜์„ธ์š”."; case "delete": return "๐Ÿšจ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ DELETE ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ์†์‹ค์— ์ฃผ์˜ํ•˜์„ธ์š”."; } } return null; }; const labels = getConnectionLabels(); const warningMessage = getSameConnectionWarning(); return (
{/* FROM ์ปค๋„ฅ์…˜ ์„ ํƒ */} {labels.from.title} {labels.from.desc} {/* TO ์ปค๋„ฅ์…˜ ์„ ํƒ */} {labels.to.title} {labels.to.desc}
{/* ๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ์‹œ ๊ฒฝ๊ณ  */} {warningMessage && ( ์ฃผ์˜์‚ฌํ•ญ {warningMessage} )}
); }; ``` #### 2.2 TableSelectionPanel ์ปดํฌ๋„ŒํŠธ ํ™•์žฅ ```typescript export const TableSelectionPanel: React.FC = ({ fromConnectionId, toConnectionId, selectedFromTable, selectedToTable, onFromTableChange, onToTableChange, }) => { const [fromTables, setFromTables] = useState([]); const [toTables, setToTables] = useState([]); const [loading, setLoading] = useState(false); // ์ปค๋„ฅ์…˜ ๋ณ€๊ฒฝ ์‹œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋”ฉ useEffect(() => { if (fromConnectionId) { loadTablesFromConnection(fromConnectionId, setFromTables); } }, [fromConnectionId]); useEffect(() => { if (toConnectionId) { loadTablesFromConnection(toConnectionId, setToTables); } }, [toConnectionId]); return (
{/* FROM ํ…Œ์ด๋ธ” ์„ ํƒ */} {/* TO ํ…Œ์ด๋ธ” ์„ ํƒ */}
); }; ``` #### 2.3 InsertFieldMappingPanel ํ™•์žฅ ```typescript // ๊ธฐ์กด ์ปดํฌ๋„ŒํŠธ์— ์ปค๋„ฅ์…˜ ์ •๋ณด ์ถ”๊ฐ€ interface EnhancedInsertFieldMappingPanelProps extends InsertFieldMappingPanelProps { fromConnectionId?: number; toConnectionId?: number; fromConnectionName?: string; toConnectionName?: string; } // ์ปฌ๋Ÿผ ๋กœ๋”ฉ ๋กœ์ง ์ˆ˜์ • useEffect(() => { if (fromConnectionId && fromTableName) { loadColumnsFromConnection(fromConnectionId, fromTableName).then( setFromTableColumns ); } }, [fromConnectionId, fromTableName]); useEffect(() => { if (toConnectionId && toTableName) { loadColumnsFromConnection(toConnectionId, toTableName).then( setToTableColumns ); } }, [toConnectionId, toTableName]); ``` ### Phase 3: ํ†ตํ•ฉ ๋ฐ ํ…Œ์ŠคํŠธ (1์ฃผ) #### 3.1 ActionFieldMappings ์ปดํฌ๋„ŒํŠธ ํ†ตํ•ฉ ```typescript export const ActionFieldMappings: React.FC = ({ action, actionIndex, settings, onSettingsChange, // ... ๊ธฐ์กด props }) => { const renderActionSpecificUI = () => { // ๊ณตํ†ต ๋‹จ๊ณ„: ์ปค๋„ฅ์…˜ ์„ ํƒ๊ณผ ํ…Œ์ด๋ธ” ์„ ํƒ const commonSteps = ( <> {/* 1๋‹จ๊ณ„: ์ปค๋„ฅ์…˜ ์„ ํƒ */} {/* 2๋‹จ๊ณ„: ํ…Œ์ด๋ธ” ์„ ํƒ */} {hasConnectionsSelected && ( )} ); // 3๋‹จ๊ณ„: ์•ก์…˜ ํƒ€์ž…๋ณ„ ๋งคํ•‘/์กฐ๊ฑด ์„ค์ • let specificPanel = null; if (hasTablesSelected) { switch (action.actionType) { case "insert": specificPanel = ( ); break; case "update": specificPanel = ( ); break; case "delete": specificPanel = ( ); break; } } return (
{commonSteps} {specificPanel}
); }; return renderActionSpecificUI(); }; ``` ## ๐Ÿ”„ ์•ก์…˜ ํƒ€์ž…๋ณ„ ์ƒ์„ธ ๊ตฌํ˜„ ### 1. UPDATE ์•ก์…˜ ๊ตฌํ˜„ #### UpdateFieldMappingPanel ์ปดํฌ๋„ŒํŠธ ```typescript export const UpdateFieldMappingPanel: React.FC< UpdateFieldMappingPanelProps > = ({ action, actionIndex, settings, onSettingsChange, fromTableColumns, toTableColumns, fromConnectionId, toConnectionId, }) => { const [updateConditions, setUpdateConditions] = useState( [] ); const [updateFields, setUpdateFields] = useState([]); return (
{/* UPDATE ์กฐ๊ฑด ์„ค์ • */} ๐Ÿ” ์—…๋ฐ์ดํŠธ ์กฐ๊ฑด ์„ค์ • FROM ํ…Œ์ด๋ธ”์—์„œ ์–ด๋–ค ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ TO ํ…Œ์ด๋ธ”์„ ์—…๋ฐ์ดํŠธํ• ์ง€ ์„ค์ •ํ•˜์„ธ์š” {/* UPDATE ํ•„๋“œ ๋งคํ•‘ */} ๐Ÿ“ ์—…๋ฐ์ดํŠธ ํ•„๋“œ ๋งคํ•‘ FROM ํ…Œ์ด๋ธ”์˜ ๊ฐ’์„ TO ํ…Œ์ด๋ธ”์˜ ์–ด๋–ค ํ•„๋“œ์— ์—…๋ฐ์ดํŠธํ• ์ง€ ์„ค์ •ํ•˜์„ธ์š” {/* WHERE ์กฐ๊ฑด ์„ค์ • */} ๐ŸŽฏ ์—…๋ฐ์ดํŠธ ๋Œ€์ƒ ์กฐ๊ฑด TO ํ…Œ์ด๋ธ”์—์„œ ์–ด๋–ค ๋ ˆ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ• ์ง€ WHERE ์กฐ๊ฑด์„ ์„ค์ •ํ•˜์„ธ์š”
); }; ``` #### UPDATE ๋ฐ์ดํ„ฐ ํƒ€์ž… ์ •์˜ ```typescript interface UpdateCondition { id: string; fromColumn: string; operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN"; value: string | string[]; logicalOperator?: "AND" | "OR"; } interface UpdateFieldMapping { id: string; fromColumn: string; toColumn: string; transformFunction?: string; defaultValue?: string; } interface WhereCondition { id: string; toColumn: string; operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN"; valueSource: "from_column" | "static" | "current_timestamp"; fromColumn?: string; // valueSource๊ฐ€ "from_column"์ธ ๊ฒฝ์šฐ staticValue?: string; // valueSource๊ฐ€ "static"์ธ ๊ฒฝ์šฐ logicalOperator?: "AND" | "OR"; } ``` ### 2. DELETE ์•ก์…˜ ๊ตฌํ˜„ #### DeleteConditionPanel ์ปดํฌ๋„ŒํŠธ ```typescript export const DeleteConditionPanel: React.FC = ({ action, actionIndex, settings, onSettingsChange, fromTableColumns, toTableColumns, fromConnectionId, toConnectionId, }) => { const [deleteConditions, setDeleteConditions] = useState( [] ); const [whereConditions, setWhereConditions] = useState([]); return (
{/* DELETE ํŠธ๋ฆฌ๊ฑฐ ์กฐ๊ฑด ์„ค์ • */} ๐Ÿ”ฅ ์‚ญ์ œ ํŠธ๋ฆฌ๊ฑฐ ์กฐ๊ฑด FROM ํ…Œ์ด๋ธ”์—์„œ ์–ด๋–ค ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์„ ๋•Œ TO ํ…Œ์ด๋ธ”์—์„œ ์‚ญ์ œ๋ฅผ ์‹คํ–‰ํ• ์ง€ ์„ค์ •ํ•˜์„ธ์š” {/* DELETE WHERE ์กฐ๊ฑด ์„ค์ • */} ๐ŸŽฏ ์‚ญ์ œ ๋Œ€์ƒ ์กฐ๊ฑด TO ํ…Œ์ด๋ธ”์—์„œ ์–ด๋–ค ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ• ์ง€ WHERE ์กฐ๊ฑด์„ ์„ค์ •ํ•˜์„ธ์š” {/* ์•ˆ์ „์žฅ์น˜ ์„ค์ • */} ๐Ÿ›ก๏ธ ์‚ญ์ œ ์•ˆ์ „์žฅ์น˜ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋Œ€๋Ÿ‰ ์‚ญ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์•ˆ์ „์žฅ์น˜๋ฅผ ์„ค์ •ํ•˜์„ธ์š”
); }; ``` #### DELETE ๋ฐ์ดํ„ฐ ํƒ€์ž… ์ •์˜ ```typescript interface DeleteCondition { id: string; fromColumn: string; operator: | "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN" | "EXISTS" | "NOT EXISTS"; value: string | string[]; logicalOperator?: "AND" | "OR"; } interface DeleteWhereCondition { id: string; toColumn: string; operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN"; valueSource: "from_column" | "static" | "condition_result"; fromColumn?: string; staticValue?: string; logicalOperator?: "AND" | "OR"; } interface DeleteSafetySettings { maxDeleteCount: number; requireConfirmation: boolean; dryRunFirst: boolean; logAllDeletes: boolean; } ``` ## ๐Ÿ”’ ๋งคํ•‘ ๊ทœ์น™ ๊ตฌํ˜„ ### 1. INSERT: FROM โ†’ TO ์ปฌ๋Ÿผ ๋งคํ•‘ ์ œ์•ฝ์‚ฌํ•ญ #### ํ—ˆ์šฉ๋˜๋Š” ๋งคํ•‘ (๊ธฐ์กด๊ณผ ๋™์ผ) ```typescript // โœ… 1:1 ๋งคํ•‘ FROM.column1 โ†’ TO.column1 // โœ… 1:N ๋งคํ•‘ (ํ•˜๋‚˜์˜ FROM ์ปฌ๋Ÿผ์ด ์—ฌ๋Ÿฌ TO ์ปฌ๋Ÿผ์— ๋งคํ•‘) FROM.column1 โ†’ TO.column1 FROM.column1 โ†’ TO.column2 FROM.column1 โ†’ TO.column3 ``` #### ๊ธˆ์ง€๋˜๋Š” ๋งคํ•‘ (์‹ ๊ทœ ๊ฒ€์ฆ ๋กœ์ง) ```typescript // โŒ N:1 ๋งคํ•‘ (์—ฌ๋Ÿฌ FROM ์ปฌ๋Ÿผ์ด ํ•˜๋‚˜์˜ TO ์ปฌ๋Ÿผ์— ๋งคํ•‘) FROM.column1 โ†’ TO.column1 FROM.column2 โ†’ TO.column1 // ์ด๋ฏธ ๋งคํ•‘๋œ TO.column1์— ์ถ”๊ฐ€ ๋งคํ•‘ ์‹œ๋„ ``` ### 2. UPDATE: ์กฐ๊ฑด ๋ฐ ํ•„๋“œ ๋งคํ•‘ ์ œ์•ฝ์‚ฌํ•ญ #### ํ—ˆ์šฉ๋˜๋Š” UPDATE ํŒจํ„ด ```typescript // โœ… ์กฐ๊ฑด๋ถ€ ์—…๋ฐ์ดํŠธ IF (FROM.status = 'completed') THEN UPDATE TO.table SET status = FROM.new_status WHERE TO.id = FROM.ref_id // โœ… ๋‹ค์ค‘ ํ•„๋“œ ์—…๋ฐ์ดํŠธ UPDATE TO.table SET column1 = FROM.value1, column2 = FROM.value2, updated_at = CURRENT_TIMESTAMP WHERE TO.id = FROM.ref_id // โœ… ์กฐ๊ฑด๋ถ€ ํ•„๋“œ ๋งคํ•‘ IF (FROM.priority > 5) THEN TO.urgent_flag = 'Y' ELSE TO.urgent_flag = 'N' ``` #### UPDATE ์ œ์•ฝ์‚ฌํ•ญ ```typescript // โŒ WHERE ์กฐ๊ฑด ์—†๋Š” ์ „์ฒด ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ (์•ˆ์ „์žฅ์น˜) // โŒ PRIMARY KEY ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ // โš ๏ธ ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ (ํ—ˆ์šฉํ•˜๋˜ ํŠน๋ณ„ํ•œ ์ฃผ์˜์‚ฌํ•ญ) // ๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” UPDATE ์‹œ ์•ˆ์ „์žฅ์น˜ const validateSelfTableUpdate = ( fromTable: string, toTable: string, updateConditions: UpdateCondition[], whereConditions: WhereCondition[] ): ValidationResult => { if (fromTable === toTable) { // 1. WHERE ์กฐ๊ฑด ํ•„์ˆ˜ if (!whereConditions.length) { return { isValid: false, error: "์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ ์‹œ WHERE ์กฐ๊ฑด์ด ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.", }; } // 2. ์—…๋ฐ์ดํŠธ ์กฐ๊ฑด๊ณผ WHERE ์กฐ๊ฑด์ด ๊ฒน์น˜์ง€ ์•Š๋„๋ก ์ฒดํฌ const conditionColumns = updateConditions.map((c) => c.fromColumn); const whereColumns = whereConditions.map((c) => c.toColumn); const overlap = conditionColumns.filter((col) => whereColumns.includes(col) ); if (overlap.length > 0) { return { isValid: false, error: `์—…๋ฐ์ดํŠธ ์กฐ๊ฑด๊ณผ WHERE ์กฐ๊ฑด์—์„œ ๊ฐ™์€ ์ปฌ๋Ÿผ(${overlap.join( ", " )})์„ ์‚ฌ์šฉํ•˜๋ฉด ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.`, }; } // 3. ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ ์ฒดํฌ const hasInfiniteLoopRisk = updateConditions.some((condition) => whereConditions.some( (where) => where.fromColumn === condition.toColumn && where.toColumn === condition.fromColumn ) ); if (hasInfiniteLoopRisk) { return { isValid: false, error: "์ž๊ธฐ ์ฐธ์กฐ ์—…๋ฐ์ดํŠธ๋กœ ์ธํ•œ ๋ฌดํ•œ ๋ฃจํ”„ ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.", }; } } return { isValid: true }; }; ``` ### 3. DELETE: ์กฐ๊ฑด ๋ฐ ์•ˆ์ „์žฅ์น˜ ์ œ์•ฝ์‚ฌํ•ญ #### ํ—ˆ์šฉ๋˜๋Š” DELETE ํŒจํ„ด ```typescript // โœ… ์กฐ๊ฑด๋ถ€ ์‚ญ์ œ IF (FROM.is_expired = 'Y') THEN DELETE FROM TO.table WHERE TO.ref_id = FROM.id // โœ… ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ IF (FROM.status = 'cancelled') THEN DELETE FROM TO.order_items WHERE TO.order_id = FROM.order_id // โœ… ์นด์Šค์ผ€์ด๋“œ ์‚ญ์ œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ DELETE FROM TO.child_table WHERE TO.parent_id = FROM.deleted_id ``` #### DELETE ์ œ์•ฝ์‚ฌํ•ญ ๋ฐ ์•ˆ์ „์žฅ์น˜ ```typescript // โŒ WHERE ์กฐ๊ฑด ์—†๋Š” ์ „์ฒด ํ…Œ์ด๋ธ” ์‚ญ์ œ (๊ฐ•๋ ฅํ•œ ์•ˆ์ „์žฅ์น˜) // โŒ ์ผ์ • ๊ฐœ์ˆ˜ ์ด์ƒ์˜ ๋Œ€๋Ÿ‰ ์‚ญ์ œ (maxDeleteCount ์ œํ•œ) // โš ๏ธ ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ์œ„๋ฐ˜ ๊ฐ€๋Šฅ์„ฑ ์ฒดํฌ // โš ๏ธ ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์‚ญ์ œ (ํ—ˆ์šฉํ•˜๋˜ ํŠน๋ณ„ํ•œ ์ฃผ์˜์‚ฌํ•ญ) const validateDeleteSafety = ( fromTable: string, toTable: string, deleteConditions: DeleteCondition[], whereConditions: WhereCondition[], safetySettings: DeleteSafetySettings ): ValidationResult => { // 1. WHERE ์กฐ๊ฑด ํ•„์ˆ˜ ์ฒดํฌ if (!whereConditions.length) { return { isValid: false, error: "DELETE ์ž‘์—…์—๋Š” ๋ฐ˜๋“œ์‹œ WHERE ์กฐ๊ฑด์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", }; } // 2. ๋Œ€๋Ÿ‰ ์‚ญ์ œ ์ œํ•œ ์ฒดํฌ if (safetySettings.maxDeleteCount < 1) { return { isValid: false, error: "์ตœ๋Œ€ ์‚ญ์ œ ๊ฐœ์ˆ˜๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.", }; } // ๐Ÿ†• 3. ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์‚ญ์ œ ์‹œ ์ถ”๊ฐ€ ์•ˆ์ „์žฅ์น˜ if (fromTable === toTable) { // ๊ฐ•ํ™”๋œ ์•ˆ์ „์žฅ์น˜: ๋” ์—„๊ฒฉํ•œ ์ œํ•œ const selfDeleteMaxCount = Math.min(safetySettings.maxDeleteCount, 10); if (safetySettings.maxDeleteCount > selfDeleteMaxCount) { return { isValid: false, error: `์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์‚ญ์ œ ์‹œ ์ตœ๋Œ€ ${selfDeleteMaxCount}๊ฐœ๊นŒ์ง€๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค.`, }; } // ์‚ญ์ œ ์กฐ๊ฑด์ด ๋„ˆ๋ฌด ๊ด‘๋ฒ”์œ„ํ•œ์ง€ ์ฒดํฌ const hasBroadCondition = deleteConditions.some( (condition) => condition.operator === "!=" || condition.operator === "NOT IN" || condition.operator === "NOT EXISTS" ); if (hasBroadCondition) { return { isValid: false, error: "์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์‚ญ์ œ ์‹œ ๋ถ€์ • ์กฐ๊ฑด(!=, NOT IN, NOT EXISTS)์€ ์œ„ํ—˜ํ•ฉ๋‹ˆ๋‹ค.", }; } // WHERE ์กฐ๊ฑด์ด ์ถฉ๋ถ„ํžˆ ๊ตฌ์ฒด์ ์ธ์ง€ ์ฒดํฌ if (whereConditions.length < 2) { return { isValid: false, error: "์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์‚ญ์ œ ์‹œ WHERE ์กฐ๊ฑด์„ 2๊ฐœ ์ด์ƒ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.", }; } } return { isValid: true }; }; // ๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ์‹œ ์‹ค์ œ ์‚ฌ์šฉ ์˜ˆ์‹œ const exampleSelfTableOperations = { // โœ… ์•ˆ์ „ํ•œ ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” UPDATE safeUpdate: ` UPDATE user_info SET last_login = NOW(), login_count = login_count + 1 WHERE user_id = 'specific_user' AND status = 'active' `, // โœ… ์•ˆ์ „ํ•œ ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” DELETE safeDelete: ` DELETE FROM temp_data WHERE created_at < NOW() - INTERVAL '7 days' AND status = 'processed' AND batch_id = 'specific_batch' LIMIT 10 `, // โŒ ์œ„ํ—˜ํ•œ ์ž‘์—…๋“ค dangerousOperations: [ "UPDATE table SET column = value (WHERE ์กฐ๊ฑด ์—†์Œ)", "DELETE FROM table WHERE status != 'active' (๋ถ€์ • ์กฐ๊ฑด์œผ๋กœ ์˜ˆ์ƒ๋ณด๋‹ค ๋งŽ์ด ์‚ญ์ œ๋  ์ˆ˜ ์žˆ์Œ)", "UPDATE table SET id = new_id WHERE id = old_id (ํ‚ค ๊ฐ’ ๋ณ€๊ฒฝ์œผ๋กœ ์ฐธ์กฐ ๋ฌด๊ฒฐ์„ฑ ์œ„ํ—˜)", ], }; ``` ### 4. ๊ณตํ†ต ๊ฒ€์ฆ ๋กœ์ง #### ๋งคํ•‘ ์ œ์•ฝ์‚ฌํ•ญ ํ†ตํ•ฉ ๊ฒ€์ฆ ```typescript const validateMappingConstraints = ( actionType: "insert" | "update" | "delete", newMapping: ColumnMapping, existingMappings: ColumnMapping[] ): ValidationResult => { switch (actionType) { case "insert": return validateInsertMapping(newMapping, existingMappings); case "update": return validateUpdateMapping(newMapping, existingMappings); case "delete": return validateDeleteConditions(newMapping, existingMappings); } }; const validateInsertMapping = ( newMapping: ColumnMapping, existingMappings: ColumnMapping[] ): ValidationResult => { // TO ์ปฌ๋Ÿผ์ด ์ด๋ฏธ ๋‹ค๋ฅธ FROM ์ปฌ๋Ÿผ๊ณผ ๋งคํ•‘๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ const existingToMapping = existingMappings.find( (mapping) => mapping.toColumnName === newMapping.toColumnName ); if ( existingToMapping && existingToMapping.fromColumnName && existingToMapping.fromColumnName !== newMapping.fromColumnName ) { return { isValid: false, error: `๋Œ€์ƒ ์ปฌ๋Ÿผ '${newMapping.toColumnName}'์€ ์ด๋ฏธ '${existingToMapping.fromColumnName}'๊ณผ ๋งคํ•‘๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.`, }; } return { isValid: true }; }; ``` ### 2. UI์—์„œ์˜ ์ œ์•ฝ์‚ฌํ•ญ ํ‘œ์‹œ #### ์ปฌ๋Ÿผ ์„ ํƒ ์‹œ ๋น„ํ™œ์„ฑํ™” ๋กœ์ง ```typescript const isToColumnClickable = (toColumn: ColumnInfo) => { const currentMapping = columnMappings.find( (m) => m.toColumnName === toColumn.columnName ); // ์ด๋ฏธ ๋‹ค๋ฅธ FROM ์ปฌ๋Ÿผ๊ณผ ๋งคํ•‘๋œ ๊ฒฝ์šฐ ํด๋ฆญ ๋ถˆ๊ฐ€ if (currentMapping?.fromColumnName) { return false; } // ๊ธฐ๋ณธ๊ฐ’์ด ์„ค์ •๋œ ๊ฒฝ์šฐ ํด๋ฆญ ๋ถˆ๊ฐ€ if (currentMapping?.defaultValue && currentMapping.defaultValue.trim()) { return false; } // ๋ฐ์ดํ„ฐ ํƒ€์ž… ํ˜ธํ™˜์„ฑ ์ฒดํฌ if (!selectedFromColumn) return true; const fromColumn = fromTableColumns.find( (col) => col.columnName === selectedFromColumn ); if (!fromColumn) return true; return fromColumn.dataType === toColumn.dataType; }; ``` #### ์‹œ๊ฐ์  ํ”ผ๋“œ๋ฐฑ ```typescript // TO ์ปฌ๋Ÿผ ๋ Œ๋”๋ง ์‹œ ์ƒํƒœ ํ‘œ์‹œ const getToColumnStatus = (toColumn: ColumnInfo) => { const mapping = columnMappings.find( (m) => m.toColumnName === toColumn.columnName ); if (mapping?.fromColumnName) { return { status: "mapped", color: "bg-green-100 border-green-300", icon: "๐Ÿ”—", label: `โ† ${mapping.fromColumnName}`, }; } if (mapping?.defaultValue) { return { status: "default", color: "bg-blue-100 border-blue-300", icon: "๐Ÿ“", label: `๊ธฐ๋ณธ๊ฐ’: ${mapping.defaultValue}`, }; } return { status: "unmapped", color: "bg-gray-100 border-gray-300", icon: "โšช", label: "๋ฏธ์„ค์ •", }; }; ``` ## ๐Ÿ“Š ๋ฐ์ดํ„ฐ ํ”Œ๋กœ์šฐ ### 1. ์„ค์ • ์ €์žฅ ํ”Œ๋กœ์šฐ ``` ์‚ฌ์šฉ์ž ์„ค์ • ์ž…๋ ฅ โ†“ ConnectionSelectionPanel โ†’ ์ปค๋„ฅ์…˜ ID ์ €์žฅ โ†“ TableSelectionPanel โ†’ ํ…Œ์ด๋ธ”๋ช… ์ €์žฅ โ†“ InsertFieldMappingPanel โ†’ ํ•„๋“œ ๋งคํ•‘ ์ €์žฅ โ†“ DataSaveSettings ์—…๋ฐ์ดํŠธ โ†“ dataflow_diagrams.plan ํ•„๋“œ์— JSON ์ €์žฅ ``` ### 2. ์‹คํ–‰ ํ”Œ๋กœ์šฐ #### INSERT ์‹คํ–‰ ํ”Œ๋กœ์šฐ ``` ์ œ์–ด๊ด€๋ฆฌ ํŠธ๋ฆฌ๊ฑฐ ๋ฐœ์ƒ (INSERT) โ†“ EnhancedDataflowControlService.executeDataflowControl() โ†“ ์†Œ์Šค ์ปค๋„ฅ์…˜์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ (MultiConnectionQueryService.fetchDataFromConnection) โ†“ ํ•„๋“œ ๋งคํ•‘ ๊ทœ์น™ ์ ์šฉ (1:N ๋งคํ•‘ ์ง€์›) โ†“ ๋Œ€์ƒ ์ปค๋„ฅ์…˜์— ๋ฐ์ดํ„ฐ ์‚ฝ์ž… (MultiConnectionQueryService.insertDataToConnection) โ†“ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ``` #### UPDATE ์‹คํ–‰ ํ”Œ๋กœ์šฐ ``` ์ œ์–ด๊ด€๋ฆฌ ํŠธ๋ฆฌ๊ฑฐ ๋ฐœ์ƒ (UPDATE) โ†“ EnhancedDataflowControlService.executeDataflowControl() โ†“ ์†Œ์Šค ์ปค๋„ฅ์…˜์—์„œ ์กฐ๊ฑด ๋ฐ์ดํ„ฐ ์กฐํšŒ (UPDATE ์กฐ๊ฑด ํ™•์ธ) โ†“ ์กฐ๊ฑด ๋งŒ์กฑ ์‹œ FROM ๋ฐ์ดํ„ฐ ์ถ”์ถœ โ†“ ํ•„๋“œ ๋งคํ•‘ ๊ทœ์น™ ์ ์šฉ (FROM โ†’ TO ํ•„๋“œ ๋งคํ•‘) โ†“ WHERE ์กฐ๊ฑด ์ƒ์„ฑ (TO ํ…Œ์ด๋ธ” ๋Œ€์ƒ ๋ ˆ์ฝ”๋“œ ์‹๋ณ„) โ†“ ๋Œ€์ƒ ์ปค๋„ฅ์…˜์—์„œ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ (MultiConnectionQueryService.updateDataToConnection) โ†“ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ``` #### DELETE ์‹คํ–‰ ํ”Œ๋กœ์šฐ ``` ์ œ์–ด๊ด€๋ฆฌ ํŠธ๋ฆฌ๊ฑฐ ๋ฐœ์ƒ (DELETE) โ†“ EnhancedDataflowControlService.executeDataflowControl() โ†“ ์†Œ์Šค ์ปค๋„ฅ์…˜์—์„œ ์‚ญ์ œ ํŠธ๋ฆฌ๊ฑฐ ์กฐ๊ฑด ํ™•์ธ โ†“ ์กฐ๊ฑด ๋งŒ์กฑ ์‹œ ์‚ญ์ œ ๋Œ€์ƒ ์‹๋ณ„ โ†“ ์•ˆ์ „์žฅ์น˜ ๊ฒ€์ฆ (maxDeleteCount, WHERE ์กฐ๊ฑด ํ•„์ˆ˜) โ†“ WHERE ์กฐ๊ฑด ์ƒ์„ฑ (TO ํ…Œ์ด๋ธ” ์‚ญ์ œ ๋Œ€์ƒ ๋ ˆ์ฝ”๋“œ) โ†“ [dryRunFirst=true์ธ ๊ฒฝ์šฐ] ์‚ญ์ œ ์˜ˆ์ƒ ๊ฐœ์ˆ˜ ํ™•์ธ โ†“ ๋Œ€์ƒ ์ปค๋„ฅ์…˜์—์„œ ๋ฐ์ดํ„ฐ ์‚ญ์ œ (MultiConnectionQueryService.deleteDataFromConnection) โ†“ ์‚ญ์ œ ๋กœ๊ทธ ๊ธฐ๋ก (logAllDeletes=true์ธ ๊ฒฝ์šฐ) โ†“ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ ``` ## ๐Ÿ› ๏ธ ๊ธฐ์ˆ ์  ๊ณ ๋ ค์‚ฌํ•ญ ### 1. ์„ฑ๋Šฅ ์ตœ์ ํ™” - **์ปค๋„ฅ์…˜ ํ’€๋ง**: ์™ธ๋ถ€ DB๋ณ„ ์ปค๋„ฅ์…˜ ํ’€ ๊ด€๋ฆฌ - **์บ์‹ฑ**: ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ์ •๋ณด ์บ์‹ฑ (Redis ํ™œ์šฉ) - **๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ**: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์‹œ ํ์ž‰ ์‹œ์Šคํ…œ ํ™œ์šฉ ### 2. ๋ณด์•ˆ ๊ฐ•ํ™” - **์ปค๋„ฅ์…˜ ์ •๋ณด ์•”ํ˜ธํ™”**: ๊ธฐ์กด ์‹œ์Šคํ…œ๊ณผ ๋™์ผํ•œ ์ˆ˜์ค€ ์œ ์ง€ - **์ ‘๊ทผ ๊ถŒํ•œ ๊ด€๋ฆฌ**: ํšŒ์‚ฌ๋ณ„ ์ปค๋„ฅ์…˜ ์ ‘๊ทผ ์ œ์–ด - **๊ฐ์‚ฌ ๋กœ๊น…**: ๋ชจ๋“  ์™ธ๋ถ€ DB ์ ‘๊ทผ ๊ธฐ๋ก ### 3. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ```typescript export class ConnectionError extends Error { constructor( message: string, public connectionId: number, public originalError?: Error ) { super(message); this.name = "ConnectionError"; } } export class MappingValidationError extends Error { constructor(message: string, public mappingErrors: ValidationError[]) { super(message); this.name = "MappingValidationError"; } } ``` ### 4. ํ˜ธํ™˜์„ฑ ์œ ์ง€ - **๊ธฐ์กด ์„ค์ • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜**: ๊ธฐ์กด ์ œ์–ด๊ด€๋ฆฌ ์„ค์ •์„ ์ƒˆ ๊ตฌ์กฐ๋กœ ์ž๋™ ๋ณ€ํ™˜ - **์ ์ง„์  ์ „ํ™˜**: ๊ธฐ์กด ๊ธฐ๋Šฅ ์œ ์ง€ํ•˜๋ฉด์„œ ์ƒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ - **๋กค๋ฐฑ ๊ณ„ํš**: ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์ด์ „ ๋ฒ„์ „์œผ๋กœ ๋ณต์› ๊ฐ€๋Šฅ ## ๐Ÿ“… ์ผ์ • ๊ณ„ํš ### Week 1-2: ๋ฐฑ์—”๋“œ ์ธํ”„๋ผ - [ ] MultiConnectionQueryService ๊ฐœ๋ฐœ - [ ] ์™ธ๋ถ€ ์ปค๋„ฅ์…˜ API ํ™•์žฅ - [ ] EnhancedDataflowControlService ๊ฐœ๋ฐœ ### Week 3-4: ํ”„๋ก ํŠธ์—”๋“œ UI - [ ] ConnectionSelectionPanel ๊ฐœ๋ฐœ (์•ก์…˜ ํƒ€์ž…๋ณ„ ๋ผ๋ฒจ๋ง) - [ ] TableSelectionPanel ๊ฐœ๋ฐœ (์•ก์…˜ ํƒ€์ž… ์ง€์›) - [ ] InsertFieldMappingPanel ํ™•์žฅ - [ ] UpdateFieldMappingPanel ๊ฐœ๋ฐœ - [ ] DeleteConditionPanel ๊ฐœ๋ฐœ ### Week 5: ํ†ตํ•ฉ ๋ฐ ํ…Œ์ŠคํŠธ - [ ] ์ปดํฌ๋„ŒํŠธ ํ†ตํ•ฉ - [ ] ๋งคํ•‘ ์ œ์•ฝ์‚ฌํ•ญ ๊ฒ€์ฆ ๋กœ์ง - [ ] ์ข…ํ•ฉ ํ…Œ์ŠคํŠธ ### Week 6: ๋ฌธ์„œํ™” ๋ฐ ๋ฐฐํฌ - [ ] ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ ์ž‘์„ฑ - [ ] ๊ฐœ๋ฐœ์ž ๋ฌธ์„œ ์—…๋ฐ์ดํŠธ - [ ] ๋ฐฐํฌ ๋ฐ ์‚ฌ์šฉ์ž ๊ต์œก ## ๐ŸŽฏ ์„ฑ๊ณต ์ง€ํ‘œ ### ๊ธฐ๋Šฅ์  ์ง€ํ‘œ - โœ… ๋‹ค์–‘ํ•œ ์™ธ๋ถ€ DB์— INSERT/UPDATE/DELETE ์„ฑ๊ณต๋ฅ  > 95% - โœ… ๋งคํ•‘ ์ œ์•ฝ์‚ฌํ•ญ ๊ฒ€์ฆ ์ •ํ™•๋„ 100% - โœ… DELETE ์•ˆ์ „์žฅ์น˜ ๋™์ž‘๋ฅ  100% - โœ… ๊ธฐ์กด ์ œ์–ด๊ด€๋ฆฌ ๊ธฐ๋Šฅ ํ˜ธํ™˜์„ฑ 100% ### ์„ฑ๋Šฅ ์ง€ํ‘œ - โœ… ์ปค๋„ฅ์…˜ ์„ค์ • UI ์‘๋‹ต ์‹œ๊ฐ„ < 2์ดˆ - โœ… ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ ๋กœ๋”ฉ ์‹œ๊ฐ„ < 3์ดˆ - โœ… INSERT/UPDATE/DELETE ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ < 5์ดˆ - โœ… ๋Œ€๋Ÿ‰ DELETE ๊ฒ€์ฆ ์‹œ๊ฐ„ < 3์ดˆ ### ์‚ฌ์šฉ์„ฑ ์ง€ํ‘œ - โœ… ์„ค์ • ์™„๋ฃŒ๊นŒ์ง€ ํ•„์š”ํ•œ ํด๋ฆญ ์ˆ˜ < 10ํšŒ - โœ… ๋งคํ•‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋ช…ํ™•ํ•œ ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ์ œ๊ณต - โœ… ๊ธฐ์กด ์‚ฌ์šฉ์ž์˜ ํ•™์Šต ๋น„์šฉ ์ตœ์†Œํ™” ## ๐Ÿ’ก ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ์‹ค์ œ ์‚ฌ์šฉ ์ผ€์ด์Šค ### 1. UPDATE ์‚ฌ์šฉ ์ผ€์ด์Šค #### ์ผ€์ด์Šค 1: ์‚ฌ์šฉ์ž ๋กœ๊ทธ์ธ ์ •๋ณด ์—…๋ฐ์ดํŠธ ```sql -- ํŠธ๋ฆฌ๊ฑฐ: ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•  ๋•Œ -- FROM: login_logs ํ…Œ์ด๋ธ”์—์„œ ์ตœ๊ทผ ๋กœ๊ทธ์ธ ๊ธฐ๋ก ํ™•์ธ -- TO: user_info ํ…Œ์ด๋ธ”์˜ last_login, login_count ์—…๋ฐ์ดํŠธ IF (login_logs.status = 'success' AND login_logs.created_at > NOW() - INTERVAL '1 minute') THEN UPDATE user_info SET last_login = login_logs.created_at, login_count = login_count + 1, updated_at = NOW() WHERE user_info.user_id = login_logs.user_id ``` #### ์ผ€์ด์Šค 2: ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ```sql -- ํŠธ๋ฆฌ๊ฑฐ: ์ฃผ๋ฌธ์ด ์™„๋ฃŒ๋  ๋•Œ -- FROM: order_items ํ…Œ์ด๋ธ”์—์„œ ์ฃผ๋ฌธ ์ˆ˜๋Ÿ‰ ํ™•์ธ -- TO: product_inventory ํ…Œ์ด๋ธ”์˜ ์žฌ๊ณ  ์ˆ˜๋Ÿ‰ ์ฐจ๊ฐ IF (order_items.status = 'confirmed') THEN UPDATE product_inventory SET current_stock = current_stock - order_items.quantity, last_updated = NOW() WHERE product_inventory.product_id = order_items.product_id ``` ### 2. DELETE ์‚ฌ์šฉ ์ผ€์ด์Šค #### ์ผ€์ด์Šค 1: ์ž„์‹œ ๋ฐ์ดํ„ฐ ์ž๋™ ์ •๋ฆฌ ```sql -- ํŠธ๋ฆฌ๊ฑฐ: ๋ฐฐ์น˜ ์ž‘์—… ์™„๋ฃŒ ์‹œ -- FROM: batch_jobs ํ…Œ์ด๋ธ”์—์„œ ์™„๋ฃŒ๋œ ์ž‘์—… ํ™•์ธ -- TO: temp_processing_data ํ…Œ์ด๋ธ”์˜ ์ž„์‹œ ๋ฐ์ดํ„ฐ ์‚ญ์ œ IF (batch_jobs.status = 'completed' AND batch_jobs.completed_at < NOW() - INTERVAL '1 hour') THEN DELETE FROM temp_processing_data WHERE temp_processing_data.batch_id = batch_jobs.batch_id AND temp_processing_data.status = 'processed' LIMIT 100 ``` #### ์ผ€์ด์Šค 2: ๋งŒ๋ฃŒ๋œ ์„ธ์…˜ ์ •๋ฆฌ ```sql -- ํŠธ๋ฆฌ๊ฑฐ: ์‹œ์Šคํ…œ ์ •๋ฆฌ ์ž‘์—… ์‹œ -- FROM: user_sessions ํ…Œ์ด๋ธ”์—์„œ ๋งŒ๋ฃŒ๋œ ์„ธ์…˜ ํ™•์ธ -- TO: user_sessions ํ…Œ์ด๋ธ”์—์„œ ๋งŒ๋ฃŒ๋œ ์„ธ์…˜ ์‚ญ์ œ IF (user_sessions.last_activity < NOW() - INTERVAL '24 hours') THEN DELETE FROM user_sessions WHERE user_sessions.last_activity < NOW() - INTERVAL '24 hours' AND user_sessions.status = 'inactive' LIMIT 50 ``` ### 3. ๋ณตํ•ฉ ์‹œ๋‚˜๋ฆฌ์˜ค #### ์ผ€์ด์Šค 3: ์ฃผ๋ฌธ ์ƒํƒœ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ์—ฐ์‡„ ์—…๋ฐ์ดํŠธ ```sql -- 1๋‹จ๊ณ„: ์ฃผ๋ฌธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ UPDATE orders SET status = 'shipped', shipped_at = NOW() WHERE order_id = 'ORD001' AND status = 'processing' -- 2๋‹จ๊ณ„: ๋ฐฐ์†ก ์ •๋ณด ์ƒ์„ฑ (INSERT) INSERT INTO shipping_info (order_id, tracking_number, created_at) VALUES ('ORD001', 'TRACK001', NOW()) -- 3๋‹จ๊ณ„: ๊ณ ๊ฐ ์ฃผ๋ฌธ ์ด๋ ฅ ์—…๋ฐ์ดํŠธ UPDATE customer_stats SET total_orders = total_orders + 1, last_order_date = NOW() WHERE customer_id = (SELECT customer_id FROM orders WHERE order_id = 'ORD001') ``` ## ๐Ÿš€ ํ–ฅํ›„ ํ™•์žฅ ๊ณ„ํš ### Phase 4: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ - **๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ ํ•จ์ˆ˜**: ํ•„๋“œ ๋งคํ•‘ ์‹œ ์ปค์Šคํ…€ ๋ณ€ํ™˜ ๋กœ์ง ์ง€์› - **๋ฐฐ์น˜ ์ฒ˜๋ฆฌ**: ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ ์ผ๊ด„ ์ฒ˜๋ฆฌ - **์Šค์ผ€์ค„๋ง**: ์ •๊ธฐ์  ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” - **๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ํŠธ๋žœ์žญ์…˜**: ๋ณต์žกํ•œ ์ž๊ธฐ ์ฐธ์กฐ ์ž‘์—…์˜ ์›์ž์„ฑ ๋ณด์žฅ ### Phase 5: ๋ชจ๋‹ˆํ„ฐ๋ง - **์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง**: ์™ธ๋ถ€ DB ์—ฐ๊ฒฐ ์ƒํƒœ ์‹ค์‹œ๊ฐ„ ์ถ”์  - **์„ฑ๋Šฅ ๋ถ„์„**: ์ฟผ๋ฆฌ ์‹คํ–‰ ์‹œ๊ฐ„ ๋ฐ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰ ๋ถ„์„ - **์•Œ๋ฆผ ์‹œ์Šคํ…œ**: ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์ž๋™ ์•Œ๋ฆผ - **๐Ÿ†• ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ๊ฐ์‹œ**: ์œ„ํ—˜ํ•œ ์ž๊ธฐ ์ฐธ์กฐ ์ž‘์—… ๋ชจ๋‹ˆํ„ฐ๋ง ### Phase 6: ์•ˆ์ „์„ฑ ๊ฐ•ํ™” - **๐Ÿ†• Dry Run ๋ชจ๋“œ**: ์‹ค์ œ ์‹คํ–‰ ์ „ ๊ฒฐ๊ณผ ์˜ˆ์ธก - **๐Ÿ†• ๋กค๋ฐฑ ์‹œ์Šคํ…œ**: ์ž๊ธฐ ์ž์‹  ํ…Œ์ด๋ธ” ์ž‘์—… ์‹œ ์ž๋™ ๋ฐฑ์—… ๋ฐ ๋ณต์› - **๐Ÿ†• ๋‹จ๊ณ„๋ณ„ ์Šน์ธ**: ์œ„ํ—˜ํ•œ ์ž๊ธฐ ์ฐธ์กฐ ์ž‘์—…์— ๋Œ€ํ•œ ๊ด€๋ฆฌ์ž ์Šน์ธ ํ”„๋กœ์„ธ์Šค ์ด ๊ณ„ํš์„œ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์ฒด๊ณ„์ ์ด๊ณ  ์•ˆ์ „ํ•œ ์ œ์–ด๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๊ฐœ์„ ์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.