diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index 6f481198..7221ce16 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -2282,6 +2282,7 @@ export class NodeFlowExecutionService { UPDATE ${targetTable} SET ${setClauses.join(", ")} WHERE ${updateWhereConditions} + RETURNING * `; logger.info(`๐Ÿ”„ UPDATE ์‹คํ–‰:`, { @@ -2292,8 +2293,14 @@ export class NodeFlowExecutionService { values: updateValues, }); - await txClient.query(updateSql, updateValues); + const updateResult = await txClient.query(updateSql, updateValues); updatedCount++; + + // ๐Ÿ†• UPDATE ๊ฒฐ๊ณผ๋ฅผ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ์— ๋ณ‘ํ•ฉ (๋‹ค์Œ ๋…ธ๋“œ์—์„œ id ๋“ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ) + if (updateResult.rows && updateResult.rows[0]) { + Object.assign(data, updateResult.rows[0]); + logger.info(` ๐Ÿ“ฆ UPDATE ๊ฒฐ๊ณผ ๋ณ‘ํ•ฉ: id=${updateResult.rows[0].id}`); + } } else { // 3-B. ์—†์œผ๋ฉด INSERT const columns: string[] = []; @@ -2340,6 +2347,7 @@ export class NodeFlowExecutionService { const insertSql = ` INSERT INTO ${targetTable} (${columns.join(", ")}) VALUES (${placeholders}) + RETURNING * `; logger.info(`โž• INSERT ์‹คํ–‰:`, { @@ -2348,8 +2356,14 @@ export class NodeFlowExecutionService { conflictKeyValues, }); - await txClient.query(insertSql, values); + const insertResult = await txClient.query(insertSql, values); insertedCount++; + + // ๐Ÿ†• INSERT ๊ฒฐ๊ณผ๋ฅผ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ์— ๋ณ‘ํ•ฉ (๋‹ค์Œ ๋…ธ๋“œ์—์„œ id ๋“ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ) + if (insertResult.rows && insertResult.rows[0]) { + Object.assign(data, insertResult.rows[0]); + logger.info(` ๐Ÿ“ฆ INSERT ๊ฒฐ๊ณผ ๋ณ‘ํ•ฉ: id=${insertResult.rows[0].id}`); + } } } @@ -2357,11 +2371,10 @@ export class NodeFlowExecutionService { `โœ… UPSERT ์™„๋ฃŒ (๋‚ด๋ถ€ DB): ${targetTable}, INSERT ${insertedCount}๊ฑด, UPDATE ${updatedCount}๊ฑด` ); - return { - insertedCount, - updatedCount, - totalCount: insertedCount + updatedCount, - }; + // ๐Ÿ”ฅ ๋‹ค์Œ ๋…ธ๋“œ์— ์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ + // dataArray์—๋Š” Object.assign์œผ๋กœ UPSERT ๊ฒฐ๊ณผ(id ๋“ฑ)๊ฐ€ ์ด๋ฏธ ๋ณ‘ํ•ฉ๋˜์–ด ์žˆ์Œ + // ์นด์šดํŠธ ์ •๋ณด๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•˜์—ฌ ๊ธฐ์กด ํ˜ธํ™˜์„ฑ ์œ ์ง€ + return dataArray; }; // ๐Ÿ”ฅ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ „๋‹ฌ๋˜์—ˆ์œผ๋ฉด ์‚ฌ์šฉ, ์•„๋‹ˆ๋ฉด ๋…๋ฆฝ ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ @@ -2707,28 +2720,48 @@ export class NodeFlowExecutionService { const trueData: any[] = []; const falseData: any[] = []; - inputData.forEach((item: any) => { - const results = conditions.map((condition: any) => { + // ๋ฐฐ์—ด์˜ ๊ฐ ํ•ญ๋ชฉ์— ๋Œ€ํ•ด ์กฐ๊ฑด ํ‰๊ฐ€ (EXISTS ์กฐ๊ฑด์€ ๋น„๋™๊ธฐ) + for (const item of inputData) { + const results: boolean[] = []; + + for (const condition of conditions) { const fieldValue = item[condition.field]; - let compareValue = condition.value; - if (condition.valueType === "field") { - compareValue = item[condition.value]; + // EXISTS ๊ณ„์—ด ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ + if ( + condition.operator === "EXISTS_IN" || + condition.operator === "NOT_EXISTS_IN" + ) { + const existsResult = await this.evaluateExistsCondition( + fieldValue, + condition.operator, + condition.lookupTable, + condition.lookupField, + context.companyCode + ); + results.push(existsResult); logger.info( - `๐Ÿ”„ ํ•„๋“œ ์ฐธ์กฐ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` + `๐Ÿ” EXISTS ์กฐ๊ฑด: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}` ); } else { - logger.info( - `๐Ÿ“Š ๊ณ ์ •๊ฐ’ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${compareValue}` + // ์ผ๋ฐ˜ ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ + let compareValue = condition.value; + if (condition.valueType === "field") { + compareValue = item[condition.value]; + logger.info( + `๐Ÿ”„ ํ•„๋“œ ์ฐธ์กฐ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` + ); + } else { + logger.info( + `๐Ÿ“Š ๊ณ ์ •๊ฐ’ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${compareValue}` + ); + } + + results.push( + this.evaluateCondition(fieldValue, condition.operator, compareValue) ); } - - return this.evaluateCondition( - fieldValue, - condition.operator, - compareValue - ); - }); + } const result = logic === "OR" @@ -2740,7 +2773,7 @@ export class NodeFlowExecutionService { } else { falseData.push(item); } - }); + } logger.info( `๐Ÿ” ์กฐ๊ฑด ํ•„ํ„ฐ๋ง ๊ฒฐ๊ณผ: TRUE ${trueData.length}๊ฑด / FALSE ${falseData.length}๊ฑด (${logic} ๋กœ์ง)` @@ -2755,27 +2788,46 @@ export class NodeFlowExecutionService { } // ๋‹จ์ผ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ - const results = conditions.map((condition: any) => { + const results: boolean[] = []; + + for (const condition of conditions) { const fieldValue = inputData[condition.field]; - let compareValue = condition.value; - if (condition.valueType === "field") { - compareValue = inputData[condition.value]; + // EXISTS ๊ณ„์—ด ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ + if ( + condition.operator === "EXISTS_IN" || + condition.operator === "NOT_EXISTS_IN" + ) { + const existsResult = await this.evaluateExistsCondition( + fieldValue, + condition.operator, + condition.lookupTable, + condition.lookupField, + context.companyCode + ); + results.push(existsResult); logger.info( - `๐Ÿ”„ ํ•„๋“œ ์ฐธ์กฐ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` + `๐Ÿ” EXISTS ์กฐ๊ฑด: ${condition.field} (${fieldValue}) ${condition.operator} ${condition.lookupTable}.${condition.lookupField} => ${existsResult}` ); } else { - logger.info( - `๐Ÿ“Š ๊ณ ์ •๊ฐ’ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${compareValue}` + // ์ผ๋ฐ˜ ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ + let compareValue = condition.value; + if (condition.valueType === "field") { + compareValue = inputData[condition.value]; + logger.info( + `๐Ÿ”„ ํ•„๋“œ ์ฐธ์กฐ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${condition.value} (${compareValue})` + ); + } else { + logger.info( + `๐Ÿ“Š ๊ณ ์ •๊ฐ’ ๋น„๊ต: ${condition.field} (${fieldValue}) vs ${compareValue}` + ); + } + + results.push( + this.evaluateCondition(fieldValue, condition.operator, compareValue) ); } - - return this.evaluateCondition( - fieldValue, - condition.operator, - compareValue - ); - }); + } const result = logic === "OR" @@ -2784,7 +2836,7 @@ export class NodeFlowExecutionService { logger.info(`๐Ÿ” ์กฐ๊ฑด ํ‰๊ฐ€ ๊ฒฐ๊ณผ: ${result} (${logic} ๋กœ์ง)`); - // โš ๏ธ ์กฐ๊ฑด ๋…ธ๋“œ๋Š” TRUE/FALSE ๋ธŒ๋žœ์น˜๋ฅผ ์œ„ํ•œ ํŠน๋ณ„ํ•œ ์ฒ˜๋ฆฌ ํ•„์š” + // ์กฐ๊ฑด ๋…ธ๋“œ๋Š” TRUE/FALSE ๋ธŒ๋žœ์น˜๋ฅผ ์œ„ํ•œ ํŠน๋ณ„ํ•œ ์ฒ˜๋ฆฌ ํ•„์š” // ์กฐ๊ฑด ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•˜๊ณ , ์›๋ณธ ๋ฐ์ดํ„ฐ๋Š” ํ•ญ์ƒ ๋ฐ˜ํ™˜ // ๋‹ค์Œ ๋…ธ๋“œ์—์„œ sourceHandle์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•„ํ„ฐ๋ง๋จ return { @@ -2795,6 +2847,68 @@ export class NodeFlowExecutionService { }; } + /** + * EXISTS_IN / NOT_EXISTS_IN ์กฐ๊ฑด ํ‰๊ฐ€ + * ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ๊ฐ’์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + */ + private static async evaluateExistsCondition( + fieldValue: any, + operator: string, + lookupTable: string, + lookupField: string, + companyCode?: string + ): Promise { + if (!lookupTable || !lookupField) { + logger.warn("โš ๏ธ EXISTS ์กฐ๊ฑด: lookupTable ๋˜๋Š” lookupField๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"); + return false; + } + + if (fieldValue === null || fieldValue === undefined || fieldValue === "") { + logger.info( + `โš ๏ธ EXISTS ์กฐ๊ฑด: ํ•„๋“œ๊ฐ’์ด ๋น„์–ด์žˆ์–ด ${operator === "NOT_EXISTS_IN" ? "TRUE" : "FALSE"} ๋ฐ˜ํ™˜` + ); + // ๊ฐ’์ด ๋น„์–ด์žˆ์œผ๋ฉด: EXISTS_IN์€ false, NOT_EXISTS_IN์€ true + return operator === "NOT_EXISTS_IN"; + } + + try { + // ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: company_code ํ•„ํ„ฐ ์ ์šฉ ์—ฌ๋ถ€ ํ™•์ธ + // company_mng ํ…Œ์ด๋ธ”์€ ์ œ์™ธ + const hasCompanyCode = lookupTable !== "company_mng" && companyCode; + + let sql: string; + let params: any[]; + + if (hasCompanyCode) { + sql = `SELECT EXISTS(SELECT 1 FROM "${lookupTable}" WHERE "${lookupField}" = $1 AND company_code = $2) as exists_result`; + params = [fieldValue, companyCode]; + } else { + sql = `SELECT EXISTS(SELECT 1 FROM "${lookupTable}" WHERE "${lookupField}" = $1) as exists_result`; + params = [fieldValue]; + } + + logger.info(`๐Ÿ” EXISTS ์ฟผ๋ฆฌ: ${sql}, params: ${JSON.stringify(params)}`); + + const result = await query(sql, params); + const existsInTable = result[0]?.exists_result === true; + + logger.info( + `๐Ÿ” EXISTS ๊ฒฐ๊ณผ: ${fieldValue}์ด(๊ฐ€) ${lookupTable}.${lookupField}์— ${existsInTable ? "์กด์žฌํ•จ" : "์กด์žฌํ•˜์ง€ ์•Š์Œ"}` + ); + + // EXISTS_IN: ์กด์žฌํ•˜๋ฉด true + // NOT_EXISTS_IN: ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด true + if (operator === "EXISTS_IN") { + return existsInTable; + } else { + return !existsInTable; + } + } catch (error: any) { + logger.error(`โŒ EXISTS ์กฐ๊ฑด ํ‰๊ฐ€ ์‹คํŒจ: ${error.message}`); + return false; + } + } + /** * WHERE ์ ˆ ์ƒ์„ฑ */ diff --git a/frontend/components/dataflow/node-editor/nodes/ConditionNode.tsx b/frontend/components/dataflow/node-editor/nodes/ConditionNode.tsx index 5418fcab..4cf5e32d 100644 --- a/frontend/components/dataflow/node-editor/nodes/ConditionNode.tsx +++ b/frontend/components/dataflow/node-editor/nodes/ConditionNode.tsx @@ -22,6 +22,13 @@ const OPERATOR_LABELS: Record = { NOT_IN: "NOT IN", IS_NULL: "NULL", IS_NOT_NULL: "NOT NULL", + EXISTS_IN: "EXISTS IN", + NOT_EXISTS_IN: "NOT EXISTS IN", +}; + +// EXISTS ๊ณ„์—ด ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธ +const isExistsOperator = (operator: string): boolean => { + return operator === "EXISTS_IN" || operator === "NOT_EXISTS_IN"; }; export const ConditionNode = memo(({ data, selected }: NodeProps) => { @@ -54,15 +61,31 @@ export const ConditionNode = memo(({ data, selected }: NodeProps 0 && (
{data.logic}
)} -
+
{condition.field} - + {OPERATOR_LABELS[condition.operator] || condition.operator} - {condition.value !== null && condition.value !== undefined && ( - - {typeof condition.value === "string" ? `"${condition.value}"` : String(condition.value)} + {/* EXISTS ์—ฐ์‚ฐ์ž์ธ ๊ฒฝ์šฐ ํ…Œ์ด๋ธ”.ํ•„๋“œ ํ‘œ์‹œ */} + {isExistsOperator(condition.operator) ? ( + + {(condition as any).lookupTableLabel || (condition as any).lookupTable || "..."} + {(condition as any).lookupField && `.${(condition as any).lookupFieldLabel || (condition as any).lookupField}`} + ) : ( + // ์ผ๋ฐ˜ ์—ฐ์‚ฐ์ž์ธ ๊ฒฝ์šฐ ๊ฐ’ ํ‘œ์‹œ + condition.value !== null && + condition.value !== undefined && ( + + {typeof condition.value === "string" ? `"${condition.value}"` : String(condition.value)} + + ) )}
diff --git a/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx b/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx index 87f7f771..a2d060d4 100644 --- a/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx +++ b/frontend/components/dataflow/node-editor/panels/properties/ConditionProperties.tsx @@ -4,14 +4,18 @@ * ์กฐ๊ฑด ๋ถ„๊ธฐ ๋…ธ๋“œ ์†์„ฑ ํŽธ์ง‘ */ -import { useEffect, useState } from "react"; -import { Plus, Trash2 } from "lucide-react"; +import { useEffect, useState, useCallback } from "react"; +import { Plus, Trash2, Database, Search, Check, ChevronsUpDown } from "lucide-react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { useFlowEditorStore } from "@/lib/stores/flowEditorStore"; -import type { ConditionNodeData } from "@/types/node-editor"; +import type { ConditionNodeData, ConditionOperator } from "@/types/node-editor"; +import { tableManagementApi } from "@/lib/api/tableManagement"; +import { cn } from "@/lib/utils"; // ํ•„๋“œ ์ •์˜ interface FieldDefinition { @@ -20,6 +24,19 @@ interface FieldDefinition { type?: string; } +// ํ…Œ์ด๋ธ” ์ •๋ณด +interface TableInfo { + tableName: string; + tableLabel: string; +} + +// ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด +interface ColumnInfo { + columnName: string; + columnLabel: string; + dataType: string; +} + interface ConditionPropertiesProps { nodeId: string; data: ConditionNodeData; @@ -38,8 +55,194 @@ const OPERATORS = [ { value: "NOT_IN", label: "NOT IN" }, { value: "IS_NULL", label: "NULL" }, { value: "IS_NOT_NULL", label: "NOT NULL" }, + { value: "EXISTS_IN", label: "๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•จ" }, + { value: "NOT_EXISTS_IN", label: "๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜์ง€ ์•Š์Œ" }, ] as const; +// EXISTS ๊ณ„์—ด ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธ +const isExistsOperator = (operator: string): boolean => { + return operator === "EXISTS_IN" || operator === "NOT_EXISTS_IN"; +}; + +// ํ…Œ์ด๋ธ” ์„ ํƒ์šฉ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ Combobox +function TableCombobox({ + tables, + value, + onSelect, + placeholder = "ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰...", +}: { + tables: TableInfo[]; + value: string; + onSelect: (value: string) => void; + placeholder?: string; +}) { + const [open, setOpen] = useState(false); + + const selectedTable = tables.find((t) => t.tableName === value); + + return ( + + + + + + + + + ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {tables.map((table) => ( + { + onSelect(table.tableName); + setOpen(false); + }} + className="text-xs" + > + +
+ {table.tableLabel} + {table.tableName} +
+
+ ))} +
+
+
+
+
+ ); +} + +// ์ปฌ๋Ÿผ ์„ ํƒ์šฉ ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ Combobox +function ColumnCombobox({ + columns, + value, + onSelect, + placeholder = "์ปฌ๋Ÿผ ๊ฒ€์ƒ‰...", +}: { + columns: ColumnInfo[]; + value: string; + onSelect: (value: string) => void; + placeholder?: string; +}) { + const [open, setOpen] = useState(false); + + const selectedColumn = columns.find((c) => c.columnName === value); + + return ( + + + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {columns.map((col) => ( + { + onSelect(col.columnName); + setOpen(false); + }} + className="text-xs" + > + + {col.columnLabel} + ({col.columnName}) + + ))} + + + + + + ); +} + +// ์ปฌ๋Ÿผ ์„ ํƒ ์„น์…˜ (์ž๋™ ๋กœ๋“œ ํฌํ•จ) +function ColumnSelectSection({ + lookupTable, + lookupField, + tableColumnsCache, + loadingColumns, + loadTableColumns, + onSelect, +}: { + lookupTable: string; + lookupField: string; + tableColumnsCache: Record; + loadingColumns: Record; + loadTableColumns: (tableName: string) => Promise; + onSelect: (value: string) => void; +}) { + // ์บ์‹œ์— ์—†๊ณ  ๋กœ๋”ฉ ์ค‘์ด ์•„๋‹ˆ๋ฉด ์ž๋™์œผ๋กœ ๋กœ๋“œ + useEffect(() => { + if (lookupTable && !tableColumnsCache[lookupTable] && !loadingColumns[lookupTable]) { + loadTableColumns(lookupTable); + } + }, [lookupTable, tableColumnsCache, loadingColumns, loadTableColumns]); + + const isLoading = loadingColumns[lookupTable]; + const columns = tableColumnsCache[lookupTable]; + + return ( +
+ + {isLoading ? ( +
+ ์ปฌ๋Ÿผ ๋ชฉ๋ก ๋กœ๋”ฉ ์ค‘... +
+ ) : columns && columns.length > 0 ? ( + + ) : ( +
+ ์ปฌ๋Ÿผ ๋ชฉ๋ก์„ ๋กœ๋“œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค +
+ )} +
+ ); +} + export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) { const { updateNode, nodes, edges } = useFlowEditorStore(); @@ -48,6 +251,12 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) const [logic, setLogic] = useState<"AND" | "OR">(data.logic || "AND"); const [availableFields, setAvailableFields] = useState([]); + // EXISTS ์—ฐ์‚ฐ์ž์šฉ ์ƒํƒœ + const [allTables, setAllTables] = useState([]); + const [tableColumnsCache, setTableColumnsCache] = useState>({}); + const [loadingTables, setLoadingTables] = useState(false); + const [loadingColumns, setLoadingColumns] = useState>({}); + // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‹œ ๋กœ์ปฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ useEffect(() => { setDisplayName(data.displayName || "์กฐ๊ฑด ๋ถ„๊ธฐ"); @@ -55,6 +264,100 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) setLogic(data.logic || "AND"); }, [data]); + // ์ „์ฒด ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ (EXISTS ์—ฐ์‚ฐ์ž์šฉ) + useEffect(() => { + const loadAllTables = async () => { + // ์ด๋ฏธ EXISTS ์—ฐ์‚ฐ์ž๊ฐ€ ์žˆ๊ฑฐ๋‚˜ ๋กœ๋“œ๋œ ์ ์ด ์žˆ์œผ๋ฉด ์Šคํ‚ต + if (allTables.length > 0) return; + + // EXISTS ์—ฐ์‚ฐ์ž๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ + const hasExistsOperator = conditions.some((c) => isExistsOperator(c.operator)); + if (!hasExistsOperator) return; + + setLoadingTables(true); + try { + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { + setAllTables( + response.data.map((t: any) => ({ + tableName: t.tableName, + tableLabel: t.tableLabel || t.tableName, + })) + ); + } + } catch (error) { + console.error("ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + } finally { + setLoadingTables(false); + } + }; + + loadAllTables(); + }, [conditions, allTables.length]); + + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ํ•จ์ˆ˜ + const loadTableColumns = useCallback( + async (tableName: string): Promise => { + // ์บ์‹œ์— ์žˆ์œผ๋ฉด ๋ฐ˜ํ™˜ + if (tableColumnsCache[tableName]) { + return tableColumnsCache[tableName]; + } + + // ์ด๋ฏธ ๋กœ๋”ฉ ์ค‘์ด๋ฉด ์Šคํ‚ต + if (loadingColumns[tableName]) { + return []; + } + + // ๋กœ๋”ฉ ์ƒํƒœ ์„ค์ • + setLoadingColumns((prev) => ({ ...prev, [tableName]: true })); + + try { + // getColumnList ๋ฐ˜ํ™˜: { success, data: { columns, total, ... } } + const response = await tableManagementApi.getColumnList(tableName); + if (response.success && response.data && response.data.columns) { + const columns = response.data.columns.map((c: any) => ({ + columnName: c.columnName, + columnLabel: c.columnLabel || c.columnName, + dataType: c.dataType, + })); + setTableColumnsCache((prev) => ({ ...prev, [tableName]: columns })); + console.log(`โœ… ํ…Œ์ด๋ธ” ${tableName} ์ปฌ๋Ÿผ ๋กœ๋“œ ์™„๋ฃŒ:`, columns.length, "๊ฐœ"); + return columns; + } else { + console.warn(`โš ๏ธ ํ…Œ์ด๋ธ” ${tableName} ์ปฌ๋Ÿผ ์กฐํšŒ ์‹คํŒจ:`, response); + } + } catch (error) { + console.error(`โŒ ํ…Œ์ด๋ธ” ${tableName} ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:`, error); + } finally { + setLoadingColumns((prev) => ({ ...prev, [tableName]: false })); + } + return []; + }, + [tableColumnsCache, loadingColumns] + ); + + // EXISTS ์—ฐ์‚ฐ์ž ์„ ํƒ ์‹œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๊ฐ•์ œ ๋กœ๋“œ + const ensureTablesLoaded = useCallback(async () => { + if (allTables.length > 0) return; + + setLoadingTables(true); + try { + const response = await tableManagementApi.getTableList(); + if (response.success && response.data) { + setAllTables( + response.data.map((t: any) => ({ + tableName: t.tableName, + tableLabel: t.tableLabel || t.tableName, + })) + ); + } + } catch (error) { + console.error("ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + } finally { + setLoadingTables(false); + } + }, [allTables.length]); + // ๐Ÿ”ฅ ์—ฐ๊ฒฐ๋œ ์†Œ์Šค ๋…ธ๋“œ์˜ ํ•„๋“œ๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘ useEffect(() => { const getAllSourceFields = (currentNodeId: string, visited: Set = new Set()): FieldDefinition[] => { @@ -170,15 +473,18 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) }, [nodeId, nodes, edges]); const handleAddCondition = () => { - setConditions([ - ...conditions, - { - field: "", - operator: "EQUALS", - value: "", - valueType: "static", // "static" (๊ณ ์ •๊ฐ’) ๋˜๋Š” "field" (ํ•„๋“œ ์ฐธ์กฐ) - }, - ]); + const newCondition = { + field: "", + operator: "EQUALS" as ConditionOperator, + value: "", + valueType: "static" as "static" | "field", + // EXISTS ์—ฐ์‚ฐ์ž์šฉ ํ•„๋“œ๋Š” ์ดˆ๊ธฐ๊ฐ’ ์—†์Œ + lookupTable: undefined, + lookupTableLabel: undefined, + lookupField: undefined, + lookupFieldLabel: undefined, + }; + setConditions([...conditions, newCondition]); }; const handleRemoveCondition = (index: number) => { @@ -196,9 +502,50 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) }); }; - const handleConditionChange = (index: number, field: string, value: any) => { + const handleConditionChange = async (index: number, field: string, value: any) => { const newConditions = [...conditions]; newConditions[index] = { ...newConditions[index], [field]: value }; + + // EXISTS ์—ฐ์‚ฐ์ž๋กœ ๋ณ€๊ฒฝ ์‹œ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋“œ ๋ฐ ๊ธฐ์กด value/valueType ์ดˆ๊ธฐํ™” + if (field === "operator" && isExistsOperator(value)) { + await ensureTablesLoaded(); + // EXISTS ์—ฐ์‚ฐ์ž์—์„œ๋Š” value, valueType์ด ํ•„์š” ์—†์œผ๋ฏ€๋กœ ์ดˆ๊ธฐํ™” + newConditions[index].value = ""; + newConditions[index].valueType = undefined; + } + + // EXISTS ์—ฐ์‚ฐ์ž์—์„œ ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ž๋กœ ๋ณ€๊ฒฝ ์‹œ lookup ํ•„๋“œ๋“ค ์ดˆ๊ธฐํ™” + if (field === "operator" && !isExistsOperator(value)) { + newConditions[index].lookupTable = undefined; + newConditions[index].lookupTableLabel = undefined; + newConditions[index].lookupField = undefined; + newConditions[index].lookupFieldLabel = undefined; + } + + // lookupTable ๋ณ€๊ฒฝ ์‹œ ์ปฌ๋Ÿผ ๋ชฉ๋ก ๋กœ๋“œ ๋ฐ ๋ผ๋ฒจ ์„ค์ • + if (field === "lookupTable" && value) { + const tableInfo = allTables.find((t) => t.tableName === value); + if (tableInfo) { + newConditions[index].lookupTableLabel = tableInfo.tableLabel; + } + // ํ…Œ์ด๋ธ” ๋ณ€๊ฒฝ ์‹œ ํ•„๋“œ ์ดˆ๊ธฐํ™” + newConditions[index].lookupField = undefined; + newConditions[index].lookupFieldLabel = undefined; + // ์ปฌ๋Ÿผ ๋ชฉ๋ก ๋ฏธ๋ฆฌ ๋กœ๋“œ + await loadTableColumns(value); + } + + // lookupField ๋ณ€๊ฒฝ ์‹œ ๋ผ๋ฒจ ์„ค์ • + if (field === "lookupField" && value) { + const tableName = newConditions[index].lookupTable; + if (tableName && tableColumnsCache[tableName]) { + const columnInfo = tableColumnsCache[tableName].find((c) => c.columnName === value); + if (columnInfo) { + newConditions[index].lookupFieldLabel = columnInfo.columnLabel; + } + } + } + setConditions(newConditions); updateNode(nodeId, { conditions: newConditions, @@ -329,64 +676,114 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) - {condition.operator !== "IS_NULL" && condition.operator !== "IS_NOT_NULL" && ( + {/* EXISTS ์—ฐ์‚ฐ์ž์ธ ๊ฒฝ์šฐ: ํ…Œ์ด๋ธ”/ํ•„๋“œ ์„ ํƒ UI (๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ Combobox) */} + {isExistsOperator(condition.operator) && ( <>
- - + + {loadingTables ? ( +
+ ํ…Œ์ด๋ธ” ๋ชฉ๋ก ๋กœ๋”ฉ ์ค‘... +
+ ) : allTables.length > 0 ? ( + handleConditionChange(index, "lookupTable", value)} + placeholder="ํ…Œ์ด๋ธ” ๊ฒ€์ƒ‰..." + /> + ) : ( +
+ ํ…Œ์ด๋ธ” ๋ชฉ๋ก์„ ๋กœ๋“œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค +
+ )}
-
- - {(condition as any).valueType === "field" ? ( - // ํ•„๋“œ ์ฐธ์กฐ: ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ์„ ํƒ - availableFields.length > 0 ? ( - - ) : ( -
- ์†Œ์Šค ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•˜์„ธ์š” -
- ) - ) : ( - // ๊ณ ์ •๊ฐ’: ์ง์ ‘ ์ž…๋ ฅ - handleConditionChange(index, "value", e.target.value)} - placeholder="๋น„๊ตํ•  ๊ฐ’" - className="mt-1 h-8 text-xs" - /> - )} + {(condition as any).lookupTable && ( + handleConditionChange(index, "lookupField", value)} + /> + )} + +
+ {condition.operator === "EXISTS_IN" + ? `์†Œ์Šค์˜ "${condition.field || "..."}" ๊ฐ’์ด "${(condition as any).lookupTableLabel || "..."}" ํ…Œ์ด๋ธ”์˜ "${(condition as any).lookupFieldLabel || "..."}" ์ปฌ๋Ÿผ์— ์กด์žฌํ•˜๋ฉด TRUE` + : `์†Œ์Šค์˜ "${condition.field || "..."}" ๊ฐ’์ด "${(condition as any).lookupTableLabel || "..."}" ํ…Œ์ด๋ธ”์˜ "${(condition as any).lookupFieldLabel || "..."}" ์ปฌ๋Ÿผ์— ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด TRUE`}
)} + + {/* ์ผ๋ฐ˜ ์—ฐ์‚ฐ์ž์ธ ๊ฒฝ์šฐ: ๊ธฐ์กด ๋น„๊ต๊ฐ’ UI */} + {condition.operator !== "IS_NULL" && + condition.operator !== "IS_NOT_NULL" && + !isExistsOperator(condition.operator) && ( + <> +
+ + +
+ +
+ + {(condition as any).valueType === "field" ? ( + // ํ•„๋“œ ์ฐธ์กฐ: ๋“œ๋กญ๋‹ค์šด์œผ๋กœ ์„ ํƒ + availableFields.length > 0 ? ( + + ) : ( +
+ ์†Œ์Šค ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•˜์„ธ์š” +
+ ) + ) : ( + // ๊ณ ์ •๊ฐ’: ์ง์ ‘ ์ž…๋ ฅ + handleConditionChange(index, "value", e.target.value)} + placeholder="๋น„๊ตํ•  ๊ฐ’" + className="mt-1 h-8 text-xs" + /> + )} +
+ + )}
))} @@ -402,20 +799,28 @@ export function ConditionProperties({ nodeId, data }: ConditionPropertiesProps) {/* ์•ˆ๋‚ด */}
- ๐Ÿ”Œ ์†Œ์Šค ๋…ธ๋“œ ์—ฐ๊ฒฐ: ํ…Œ์ด๋ธ”/์™ธ๋ถ€DB ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•˜๋ฉด ์ž๋™์œผ๋กœ ํ•„๋“œ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. + ์†Œ์Šค ๋…ธ๋“œ ์—ฐ๊ฒฐ: ํ…Œ์ด๋ธ”/์™ธ๋ถ€DB ๋…ธ๋“œ๋ฅผ ์—ฐ๊ฒฐํ•˜๋ฉด ์ž๋™์œผ๋กœ ํ•„๋“œ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.
- ๐Ÿ”„ ๋น„๊ต ๊ฐ’ ํƒ€์ž…:
โ€ข ๊ณ ์ •๊ฐ’: ์ง์ ‘ ์ž…๋ ฅํ•œ ๊ฐ’๊ณผ ๋น„๊ต (์˜ˆ: age > 30) -
โ€ข ํ•„๋“œ ์ฐธ์กฐ: ๋‹ค๋ฅธ ํ•„๋“œ์˜ ๊ฐ’๊ณผ ๋น„๊ต (์˜ˆ: ์ฃผ๋ฌธ์ˆ˜๋Ÿ‰ > ์žฌ๊ณ ์ˆ˜๋Ÿ‰) + ๋น„๊ต ๊ฐ’ ํƒ€์ž…:
+ - ๊ณ ์ •๊ฐ’: ์ง์ ‘ ์ž…๋ ฅํ•œ ๊ฐ’๊ณผ ๋น„๊ต (์˜ˆ: age > 30) +
- ํ•„๋“œ ์ฐธ์กฐ: ๋‹ค๋ฅธ ํ•„๋“œ์˜ ๊ฐ’๊ณผ ๋น„๊ต (์˜ˆ: ์ฃผ๋ฌธ์ˆ˜๋Ÿ‰ > ์žฌ๊ณ ์ˆ˜๋Ÿ‰) +
+
+ ํ…Œ์ด๋ธ” ์กด์žฌ ์—ฌ๋ถ€ ๊ฒ€์‚ฌ:
+ - ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•จ: ๊ฐ’์ด ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์žˆ์œผ๋ฉด TRUE +
- ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜์ง€ ์•Š์Œ: ๊ฐ’์ด ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์—†์œผ๋ฉด TRUE +
+ (์˜ˆ: ํ’ˆ๋ช…์ด ํ’ˆ๋ชฉ์ •๋ณด ํ…Œ์ด๋ธ”์— ์—†์œผ๋ฉด ์ž๋™ ๋“ฑ๋ก)
- ๐Ÿ’ก AND: ๋ชจ๋“  ์กฐ๊ฑด์ด ์ฐธ์ด์–ด์•ผ TRUE ์ถœ๋ ฅ + AND: ๋ชจ๋“  ์กฐ๊ฑด์ด ์ฐธ์ด์–ด์•ผ TRUE ์ถœ๋ ฅ
- ๐Ÿ’ก OR: ํ•˜๋‚˜๋ผ๋„ ์ฐธ์ด๋ฉด TRUE ์ถœ๋ ฅ + OR: ํ•˜๋‚˜๋ผ๋„ ์ฐธ์ด๋ฉด TRUE ์ถœ๋ ฅ
- โšก TRUE ์ถœ๋ ฅ์€ ์˜ค๋ฅธ์ชฝ ์œ„, FALSE ์ถœ๋ ฅ์€ ์˜ค๋ฅธ์ชฝ ์•„๋ž˜์ž…๋‹ˆ๋‹ค. + TRUE ์ถœ๋ ฅ์€ ์˜ค๋ฅธ์ชฝ ์œ„, FALSE ์ถœ๋ ฅ์€ ์˜ค๋ฅธ์ชฝ ์•„๋ž˜์ž…๋‹ˆ๋‹ค.
diff --git a/frontend/components/screen-embedding/EmbeddedScreen.tsx b/frontend/components/screen-embedding/EmbeddedScreen.tsx index 3bfb7a77..0b32830e 100644 --- a/frontend/components/screen-embedding/EmbeddedScreen.tsx +++ b/frontend/components/screen-embedding/EmbeddedScreen.tsx @@ -27,13 +27,14 @@ interface EmbeddedScreenProps { onSelectionChanged?: (selectedRows: any[]) => void; position?: SplitPanelPosition; // ๋ถ„ํ•  ํŒจ๋„ ๋‚ด ์œ„์น˜ (left/right) initialFormData?: Record; // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ์—์„œ ์ „๋‹ฌ๋˜๋Š” ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ + groupedData?: Record[]; // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ ์›๋ณธ ๋ฐ์ดํ„ฐ ์ถ”์ ์šฉ) } /** * ์ž„๋ฒ ๋“œ๋œ ํ™”๋ฉด ์ปดํฌ๋„ŒํŠธ */ export const EmbeddedScreen = forwardRef( - ({ embedding, onSelectionChanged, position, initialFormData }, ref) => { + ({ embedding, onSelectionChanged, position, initialFormData, groupedData }, ref) => { const [layout, setLayout] = useState([]); const [selectedRows, setSelectedRows] = useState([]); const [loading, setLoading] = useState(true); @@ -430,6 +431,8 @@ export const EmbeddedScreen = forwardRef ); diff --git a/frontend/components/screen-embedding/ScreenSplitPanel.tsx b/frontend/components/screen-embedding/ScreenSplitPanel.tsx index f457e851..2f30a4ec 100644 --- a/frontend/components/screen-embedding/ScreenSplitPanel.tsx +++ b/frontend/components/screen-embedding/ScreenSplitPanel.tsx @@ -17,13 +17,14 @@ interface ScreenSplitPanelProps { screenId?: number; config?: any; // ์„ค์ • ํŒจ๋„์—์„œ ์˜ค๋Š” config (leftScreenId, rightScreenId, splitRatio, resizable) initialFormData?: Record; // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ์—์„œ ์ „๋‹ฌ๋˜๋Š” ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ + groupedData?: Record[]; // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ ์›๋ณธ ๋ฐ์ดํ„ฐ ์ถ”์ ์šฉ) } /** * ๋ถ„ํ•  ํŒจ๋„ ์ปดํฌ๋„ŒํŠธ * ์ˆœ์ˆ˜ํ•˜๊ฒŒ ํ™”๋ฉด ๋ถ„ํ•  ๊ธฐ๋Šฅ๋งŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. */ -export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSplitPanelProps) { +export function ScreenSplitPanel({ screenId, config, initialFormData, groupedData }: ScreenSplitPanelProps) { // config์—์„œ splitRatio ์ถ”์ถœ (๊ธฐ๋ณธ๊ฐ’ 50) const configSplitRatio = config?.splitRatio ?? 50; @@ -117,7 +118,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp {/* ์ขŒ์ธก ํŒจ๋„ */}
{hasLeftScreen ? ( - + ) : (

์ขŒ์ธก ํ™”๋ฉด์„ ์„ ํƒํ•˜์„ธ์š”

@@ -157,7 +158,7 @@ export function ScreenSplitPanel({ screenId, config, initialFormData }: ScreenSp {/* ์šฐ์ธก ํŒจ๋„ */}
{hasRightScreen ? ( - + ) : (

์šฐ์ธก ํ™”๋ฉด์„ ์„ ํƒํ•˜์„ธ์š”

diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 4763507e..1dfdba14 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -365,6 +365,7 @@ export const InteractiveScreenViewerDynamic: React.FC = ({ editModalTitle: String(config.action?.editModalTitle || ""), editModalDescription: String(config.action?.editModalDescription || ""), targetUrl: String(config.action?.targetUrl || ""), + groupByColumn: String(config.action?.groupByColumns?.[0] || ""), }); const [screens, setScreens] = useState([]); @@ -97,6 +98,11 @@ export const ButtonConfigPanel: React.FC = ({ const [modalTargetColumns, setModalTargetColumns] = useState>([]); const [modalSourcePopoverOpen, setModalSourcePopoverOpen] = useState>({}); const [modalTargetPopoverOpen, setModalTargetPopoverOpen] = useState>({}); + + // ๐Ÿ†• ๊ทธ๋ฃนํ™” ์ปฌ๋Ÿผ ์„ ํƒ์šฉ ์ƒํƒœ + const [currentTableColumns, setCurrentTableColumns] = useState>([]); + const [groupByColumnOpen, setGroupByColumnOpen] = useState(false); + const [groupByColumnSearch, setGroupByColumnSearch] = useState(""); const [modalSourceSearch, setModalSourceSearch] = useState>({}); const [modalTargetSearch, setModalTargetSearch] = useState>({}); @@ -130,6 +136,7 @@ export const ButtonConfigPanel: React.FC = ({ editModalTitle: String(latestAction.editModalTitle || ""), editModalDescription: String(latestAction.editModalDescription || ""), targetUrl: String(latestAction.targetUrl || ""), + groupByColumn: String(latestAction.groupByColumns?.[0] || ""), }); // ๐Ÿ†• ์ œ๋ชฉ ๋ธ”๋ก ์ดˆ๊ธฐํ™” @@ -327,6 +334,35 @@ export const ButtonConfigPanel: React.FC = ({ loadColumns(); }, [config.action?.dataTransfer?.sourceTable, config.action?.dataTransfer?.targetTable]); + // ๐Ÿ†• ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ (๊ทธ๋ฃนํ™” ์ปฌ๋Ÿผ ์„ ํƒ์šฉ) + useEffect(() => { + if (!currentTableName) return; + + const loadCurrentTableColumns = async () => { + try { + const response = await apiClient.get(`/table-management/tables/${currentTableName}/columns`); + if (response.data.success) { + let columnData = response.data.data; + if (!Array.isArray(columnData) && columnData?.columns) columnData = columnData.columns; + if (!Array.isArray(columnData) && columnData?.data) columnData = columnData.data; + + if (Array.isArray(columnData)) { + const columns = columnData.map((col: any) => ({ + name: col.name || col.columnName, + label: col.displayName || col.label || col.columnLabel || col.name || col.columnName, + })); + setCurrentTableColumns(columns); + console.log(`โœ… ํ˜„์žฌ ํ…Œ์ด๋ธ” ${currentTableName} ์ปฌ๋Ÿผ ๋กœ๋“œ ์„ฑ๊ณต:`, columns.length, "๊ฐœ"); + } + } + } catch (error) { + console.error("ํ˜„์žฌ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์‹คํŒจ:", error); + } + }; + + loadCurrentTableColumns(); + }, [currentTableName]); + // ๐Ÿ†• openModalWithData ์†Œ์Šค/ํƒ€๊ฒŸ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ useEffect(() => { const actionType = config.action?.type; @@ -1529,6 +1565,106 @@ export const ButtonConfigPanel: React.FC = ({
)} + +
+ + + + + + +
+
+ + setGroupByColumnSearch(e.target.value)} + className="border-0 p-0 focus-visible:ring-0" + /> +
+
+ {currentTableColumns.length === 0 ? ( +
+ {currentTableName ? "์ปฌ๋Ÿผ์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘..." : "ํ…Œ์ด๋ธ”์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค"} +
+ ) : ( + <> + {/* ์„ ํƒ ํ•ด์ œ ์˜ต์…˜ */} +
{ + setLocalInputs((prev) => ({ ...prev, groupByColumn: "" })); + onUpdateProperty("componentConfig.action.groupByColumns", undefined); + setGroupByColumnOpen(false); + setGroupByColumnSearch(""); + }} + > + + ์„ ํƒ ์•ˆ ํ•จ +
+ {/* ์ปฌ๋Ÿผ ๋ชฉ๋ก */} + {currentTableColumns + .filter((col) => { + if (!groupByColumnSearch) return true; + const search = groupByColumnSearch.toLowerCase(); + return ( + col.name.toLowerCase().includes(search) || + col.label.toLowerCase().includes(search) + ); + }) + .map((col) => ( +
{ + setLocalInputs((prev) => ({ ...prev, groupByColumn: col.name })); + onUpdateProperty("componentConfig.action.groupByColumns", [col.name]); + setGroupByColumnOpen(false); + setGroupByColumnSearch(""); + }} + > + +
+ {col.name} + {col.label !== col.name && ( + {col.label} + )} +
+
+ ))} + + )} +
+
+
+
+

+ ์—ฌ๋Ÿฌ ํ–‰์„ ํ•˜๋‚˜์˜ ๊ทธ๋ฃน์œผ๋กœ ๋ฌถ์–ด์„œ ์ˆ˜์ •ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค +

+
)} diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index f9d7a7d5..6166317f 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -413,10 +413,12 @@ export const DynamicComponentRenderer: React.FC = groupedData: props.groupedData, // โœ… ์–ธ๋”์Šค์ฝ”์–ด ์ œ๊ฑฐํ•˜์—ฌ ์ง์ ‘ ์ „๋‹ฌ _groupedData: props.groupedData, // ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€ // ๐Ÿ†• UniversalFormModal์šฉ initialData ์ „๋‹ฌ - // originalData๊ฐ€ ๋น„์–ด์žˆ์ง€ ์•Š์œผ๋ฉด originalData ์‚ฌ์šฉ, ์•„๋‹ˆ๋ฉด formData ์‚ฌ์šฉ - // ์ƒ์„ฑ ๋ชจ๋“œ์—์„œ๋Š” originalData๊ฐ€ ๋นˆ ๊ฐ์ฒด์ด๋ฏ€๋กœ formData๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•จ - _initialData: (originalData && Object.keys(originalData).length > 0) ? originalData : formData, + // ์šฐ์„ ์ˆœ์œ„: props.initialData > originalData > formData + // ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ์—์„œ ์ „๋‹ฌ๋œ initialData๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„ ์‚ฌ์šฉ + _initialData: props.initialData || ((originalData && Object.keys(originalData).length > 0) ? originalData : formData), _originalData: originalData, + // ๐Ÿ†• initialData๋„ ์ง์ ‘ ์ „๋‹ฌ (์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ โ†’ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ) + initialData: props.initialData, // ๐Ÿ†• ํƒญ ๊ด€๋ จ ์ •๋ณด ์ „๋‹ฌ (ํƒญ ๋‚ด๋ถ€์˜ ํ…Œ์ด๋ธ” ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ) parentTabId: props.parentTabId, parentTabsComponentId: props.parentTabsComponentId, diff --git a/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx b/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx index db3fde4c..e3d84d43 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalContainerComponent.tsx @@ -42,7 +42,16 @@ export function ConditionalContainerComponent({ className, groupedData, // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ onSave, // ๐Ÿ†• EditModal์˜ handleSave ์ฝœ๋ฐฑ + initialData, // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ (๋ฐœ์ฃผ์ผ, ๋‹ด๋‹น์ž, ๋ฉ”๋ชจ ๋“ฑ) }: ConditionalContainerProps) { + // ๐Ÿ” ๋””๋ฒ„๊ทธ: initialData ์ˆ˜์‹  ํ™•์ธ + React.useEffect(() => { + console.log("[ConditionalContainer] initialData ์ˆ˜์‹ :", { + hasInitialData: !!initialData, + initialDataKeys: initialData ? Object.keys(initialData) : [], + initialData, + }); + }, [initialData]); // ํ™”๋ฉด ์ปจํ…์ŠคํŠธ (๋ฐ์ดํ„ฐ ์ œ๊ณต์ž๋กœ ๋“ฑ๋ก) const screenContext = useScreenContextOptional(); @@ -221,6 +230,7 @@ export function ConditionalContainerComponent({ onSave={onSave} controlField={controlField} selectedCondition={selectedValue} + initialData={initialData} /> ))}
@@ -244,6 +254,7 @@ export function ConditionalContainerComponent({ onSave={onSave} controlField={controlField} selectedCondition={selectedValue} + initialData={initialData} /> ) : null ) diff --git a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx index 59c82421..1338f40b 100644 --- a/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx +++ b/frontend/lib/registry/components/conditional-container/ConditionalSectionViewer.tsx @@ -29,7 +29,17 @@ export function ConditionalSectionViewer({ onSave, // ๐Ÿ†• EditModal์˜ handleSave ์ฝœ๋ฐฑ controlField, // ๐Ÿ†• ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ์˜ ์ œ์–ด ํ•„๋“œ๋ช… selectedCondition, // ๐Ÿ†• ํ˜„์žฌ ์„ ํƒ๋œ ์กฐ๊ฑด ๊ฐ’ + initialData, // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ (๋ฐœ์ฃผ์ผ, ๋‹ด๋‹น์ž, ๋ฉ”๋ชจ ๋“ฑ) }: ConditionalSectionViewerProps) { + // ๐Ÿ” ๋””๋ฒ„๊ทธ: initialData ์ˆ˜์‹  ํ™•์ธ + React.useEffect(() => { + console.log("[ConditionalSectionViewer] initialData ์ˆ˜์‹ :", { + sectionId, + hasInitialData: !!initialData, + initialDataKeys: initialData ? Object.keys(initialData) : [], + initialData, + }); + }, [initialData, sectionId]); const { userId, userName, user } = useAuth(); const [isLoading, setIsLoading] = useState(false); const [components, setComponents] = useState([]); @@ -191,6 +201,7 @@ export function ConditionalSectionViewer({ onFormDataChange={onFormDataChange} groupedData={groupedData} onSave={hasUniversalFormModal ? undefined : onSave} + initialData={initialData} />
); diff --git a/frontend/lib/registry/components/conditional-container/types.ts b/frontend/lib/registry/components/conditional-container/types.ts index 284e0855..24ba2033 100644 --- a/frontend/lib/registry/components/conditional-container/types.ts +++ b/frontend/lib/registry/components/conditional-container/types.ts @@ -47,6 +47,7 @@ export interface ConditionalContainerProps { onFormDataChange?: (fieldName: string, value: any) => void; groupedData?: Record[]; // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ (EditModal โ†’ ModalRepeaterTable) onSave?: () => Promise; // ๐Ÿ†• EditModal์˜ handleSave ์ฝœ๋ฐฑ + initialData?: Record; // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ (๋ฐœ์ฃผ์ผ, ๋‹ด๋‹น์ž, ๋ฉ”๋ชจ ๋“ฑ) // ํ™”๋ฉด ํŽธ์ง‘๊ธฐ ๊ด€๋ จ isDesignMode?: boolean; // ๋””์ž์ธ ๋ชจ๋“œ ์—ฌ๋ถ€ @@ -82,5 +83,7 @@ export interface ConditionalSectionViewerProps { // ๐Ÿ†• ์กฐ๊ฑด๋ถ€ ์ปจํ…Œ์ด๋„ˆ ์ •๋ณด (์ž์‹ ํ™”๋ฉด์— ์ „๋‹ฌ) controlField?: string; // ์ œ์–ด ํ•„๋“œ๋ช… (์˜ˆ: "inbound_type") selectedCondition?: string; // ํ˜„์žฌ ์„ ํƒ๋œ ์กฐ๊ฑด ๊ฐ’ (์˜ˆ: "PURCHASE_IN") + // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (๋ฐœ์ฃผ์ผ, ๋‹ด๋‹น์ž, ๋ฉ”๋ชจ ๋“ฑ) + initialData?: Record; } diff --git a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx index 8bdd5758..70785171 100644 --- a/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx +++ b/frontend/lib/registry/components/entity-search-input/EntitySearchInputComponent.tsx @@ -52,7 +52,8 @@ export function EntitySearchInputComponent({ // ์—ฐ์‡„๊ด€๊ณ„ ์„ค์ • ์ถ”์ถœ (webTypeConfig ๋˜๋Š” component.componentConfig์—์„œ) const config = component?.componentConfig || {}; const effectiveCascadingRelationCode = cascadingRelationCode || config.cascadingRelationCode; - const effectiveParentFieldId = parentFieldId || config.parentFieldId; + // cascadingParentField: ConfigPanel์—์„œ ์ €์žฅ๋˜๋Š” ํ•„๋“œ๋ช… + const effectiveParentFieldId = parentFieldId || config.cascadingParentField || config.parentFieldId; const effectiveCascadingRole = config.cascadingRole; // "parent" | "child" | undefined // ๋ถ€๋ชจ ์—ญํ• ์ด๋ฉด ์—ฐ์‡„๊ด€๊ณ„ ๋กœ์ง ์ ์šฉ ์•ˆํ•จ (์ž์‹๋งŒ ๋ถ€๋ชจ ๊ฐ’์— ๋”ฐ๋ผ ํ•„ํ„ฐ๋ง๋จ) diff --git a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx index 1eca9fab..7bf7a81d 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ItemSelectionModal.tsx @@ -17,6 +17,7 @@ import { Search, Loader2 } from "lucide-react"; import { useEntitySearch } from "../entity-search-input/useEntitySearch"; import { ItemSelectionModalProps, ModalFilterConfig } from "./types"; import { apiClient } from "@/lib/api/client"; +import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue"; export function ItemSelectionModal({ open, @@ -99,13 +100,44 @@ export function ItemSelectionModal({ } } - // ์ •๋ ฌ ํ›„ ์˜ต์…˜์œผ๋กœ ๋ณ€ํ™˜ + // ๐Ÿ†• CATEGORY_ ์ฝ”๋“œ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ๋ผ๋ฒจ ์กฐํšŒ + const allCodes = new Set(); + for (const val of uniqueValues) { + // ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’๋„ ์ฒ˜๋ฆฌ + const codes = val.split(",").map(c => c.trim()); + codes.forEach(code => { + if (code.startsWith("CATEGORY_")) { + allCodes.add(code); + } + }); + } + + // CATEGORY_ ์ฝ”๋“œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ผ๋ฒจ ์กฐํšŒ + let labelMap: Record = {}; + if (allCodes.size > 0) { + try { + const labelResponse = await getCategoryLabelsByCodes(Array.from(allCodes)); + if (labelResponse.success && labelResponse.data) { + labelMap = labelResponse.data; + } + } catch (labelError) { + console.error("์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ์กฐํšŒ ์‹คํŒจ:", labelError); + } + } + + // ์ •๋ ฌ ํ›„ ์˜ต์…˜์œผ๋กœ ๋ณ€ํ™˜ (๋ผ๋ฒจ ์ ์šฉ) const options = Array.from(uniqueValues) .sort() - .map((val) => ({ - value: val, - label: val, - })); + .map((val) => { + // ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ + if (val.includes(",")) { + const codes = val.split(",").map(c => c.trim()); + const labels = codes.map(code => labelMap[code] || code); + return { value: val, label: labels.join(", ") }; + } + // ๋‹จ์ผ ๊ฐ’ + return { value: val, label: labelMap[val] || val }; + }); setCategoryOptions((prev) => ({ ...prev, diff --git a/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelRenderer.tsx b/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelRenderer.tsx index 5dc1830c..adeb9e20 100644 --- a/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelRenderer.tsx +++ b/frontend/lib/registry/components/screen-split-panel/ScreenSplitPanelRenderer.tsx @@ -66,7 +66,7 @@ class ScreenSplitPanelRenderer extends AutoRegisteringComponentRenderer { }; render() { - const { component, style = {}, componentConfig, config, screenId, formData } = this.props as any; + const { component, style = {}, componentConfig, config, screenId, formData, groupedData } = this.props as any; // componentConfig ๋˜๋Š” config ๋˜๋Š” component.componentConfig ์‚ฌ์šฉ const finalConfig = componentConfig || config || component?.componentConfig || {}; @@ -77,6 +77,7 @@ class ScreenSplitPanelRenderer extends AutoRegisteringComponentRenderer { screenId={screenId || finalConfig.screenId} config={finalConfig} initialFormData={formData} // ๐Ÿ†• ์ˆ˜์ • ๋ฐ์ดํ„ฐ ์ „๋‹ฌ + groupedData={groupedData} // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ ์›๋ณธ ๋ฐ์ดํ„ฐ ์ถ”์ ์šฉ) /> ); diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 24a93af8..74cea859 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -6,6 +6,7 @@ import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; +import { getCategoryLabelsByCodes } from "@/lib/api/tableCategoryValue"; import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; import { getFullImageUrl } from "@/lib/api/client"; import { Button } from "@/components/ui/button"; @@ -471,6 +472,7 @@ export const TableListComponent: React.FC = ({ } // 2. ํ—ค๋” ํ•„ํ„ฐ ์ ์šฉ (joinColumnMapping ์‚ฌ์šฉ ์•ˆ ํ•จ - ์ง์ ‘ ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ) + // ๐Ÿ†• ๋‹ค์ค‘ ๊ฐ’ ์ง€์›: ์…€ ๊ฐ’์ด "A,B,C" ํ˜•ํƒœ์ผ ๋•Œ, ํ•„ํ„ฐ์—์„œ "A"๋ฅผ ์„ ํƒํ•˜๋ฉด ํ•ด๋‹น ํ–‰๋„ ํ‘œ์‹œ if (Object.keys(headerFilters).length > 0) { result = result.filter((row) => { return Object.entries(headerFilters).every(([columnName, values]) => { @@ -480,7 +482,16 @@ export const TableListComponent: React.FC = ({ const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : ""; - return values.has(cellStr); + // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๋Š” ๊ฒฝ์šฐ + if (values.has(cellStr)) return true; + + // ๋‹ค์ค‘ ๊ฐ’์ธ ๊ฒฝ์šฐ: ์ฝค๋งˆ๋กœ ๋ถ„๋ฆฌํ•ด์„œ ํ•˜๋‚˜๋ผ๋„ ํฌํ•จ๋˜๋ฉด true + if (cellStr.includes(",")) { + const cellValues = cellStr.split(",").map(v => v.trim()); + return cellValues.some(v => values.has(v)); + } + + return false; }); }); } @@ -2248,12 +2259,18 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ํŽธ์ง‘ ๋ชจ๋“œ ์ง„์ž… placeholder (์‹ค์ œ ๊ตฌํ˜„์€ visibleColumns ์ •์˜ ํ›„) const startEditingRef = useRef<() => void>(() => {}); + // ๐Ÿ†• ์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ๋งคํ•‘ (API์—์„œ ๊ฐ€์ ธ์˜จ ๊ฒƒ) + const [categoryLabelCache, setCategoryLabelCache] = useState>({}); + // ๐Ÿ†• ๊ฐ ์ปฌ๋Ÿผ์˜ ๊ณ ์œ ๊ฐ’ ๋ชฉ๋ก ๊ณ„์‚ฐ (๋ผ๋ฒจ ํฌํ•จ) const columnUniqueValues = useMemo(() => { const result: Record> = {}; if (data.length === 0) return result; + // ๐Ÿ†• ์ „์ฒด ๋ฐ์ดํ„ฐ์—์„œ ๊ฐœ๋ณ„ ๊ฐ’ -> ๋ผ๋ฒจ ๋งคํ•‘ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• (๋‹ค์ค‘ ๊ฐ’ ์ฒ˜๋ฆฌ์šฉ) + const globalLabelMap: Record> = {}; + (tableConfig.columns || []).forEach((column: { columnName: string }) => { if (column.columnName === "__checkbox__") return; @@ -2265,23 +2282,70 @@ export const TableListComponent: React.FC = ({ `${column.columnName}_value_label`, // ์˜ˆ: division_value_label ]; const valuesMap = new Map(); // value -> label + const singleValueLabelMap = new Map(); // ๊ฐœ๋ณ„ ๊ฐ’ -> ๋ผ๋ฒจ (๋‹ค์ค‘๊ฐ’ ์ฒ˜๋ฆฌ์šฉ) + // 1์ฐจ: ๋ชจ๋“  ๋ฐ์ดํ„ฐ์—์„œ ๊ฐœ๋ณ„ ๊ฐ’ -> ๋ผ๋ฒจ ๋งคํ•‘ ์ˆ˜์ง‘ (๋‹จ์ผ๊ฐ’ + ๋‹ค์ค‘๊ฐ’ ๋ชจ๋‘) data.forEach((row) => { const val = row[mappedColumnName]; if (val !== null && val !== undefined && val !== "") { const valueStr = String(val); - // ๋ผ๋ฒจ ์ปฌ๋Ÿผ ํ›„๋ณด๋“ค ์ค‘ ๊ฐ’์ด ์žˆ๋Š” ๊ฒƒ ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์›๋ณธ ๊ฐ’ ์‚ฌ์šฉ - let label = valueStr; + + // ๋ผ๋ฒจ ์ปฌ๋Ÿผ์—์„œ ๋ผ๋ฒจ ์ฐพ๊ธฐ + let labelStr = ""; for (const labelCol of labelColumnCandidates) { if (row[labelCol] && row[labelCol] !== "") { - label = String(row[labelCol]); + labelStr = String(row[labelCol]); break; } } - valuesMap.set(valueStr, label); + + // ๋‹จ์ผ ๊ฐ’์ธ ๊ฒฝ์šฐ + if (!valueStr.includes(",")) { + if (labelStr) { + singleValueLabelMap.set(valueStr, labelStr); + } + } else { + // ๋‹ค์ค‘ ๊ฐ’์ธ ๊ฒฝ์šฐ: ๊ฐ’๊ณผ ๋ผ๋ฒจ์„ ๊ฐ๊ฐ ๋ถ„๋ฆฌํ•ด์„œ ๋งคํ•‘ + const individualValues = valueStr.split(",").map(v => v.trim()); + const individualLabels = labelStr ? labelStr.split(",").map(l => l.trim()) : []; + + // ๊ฐ’๊ณผ ๋ผ๋ฒจ ๊ฐœ์ˆ˜๊ฐ€ ๊ฐ™์œผ๋ฉด 1:1 ๋งคํ•‘ + if (individualValues.length === individualLabels.length) { + individualValues.forEach((v, idx) => { + if (individualLabels[idx] && !singleValueLabelMap.has(v)) { + singleValueLabelMap.set(v, individualLabels[idx]); + } + }); + } + } } }); + // 2์ฐจ: ๋ชจ๋“  ๊ฐ’ ์ฒ˜๋ฆฌ (๋‹ค์ค‘ ๊ฐ’ ํฌํ•จ) - ํ•„ํ„ฐ ๋ชฉ๋ก์šฉ + data.forEach((row) => { + const val = row[mappedColumnName]; + if (val !== null && val !== undefined && val !== "") { + const valueStr = String(val); + + // ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’์ธ์ง€ ํ™•์ธ + if (valueStr.includes(",")) { + // ๋‹ค์ค‘ ๊ฐ’: ๊ฐ๊ฐ ๋ถ„๋ฆฌํ•ด์„œ ๊ฐœ๋ณ„ ๋ผ๋ฒจ ์ฐพ๊ธฐ + const individualValues = valueStr.split(",").map(v => v.trim()); + // ๐Ÿ†• singleValueLabelMap โ†’ categoryLabelCache ์ˆœ์œผ๋กœ ๋ผ๋ฒจ ์ฐพ๊ธฐ + const individualLabels = individualValues.map(v => + singleValueLabelMap.get(v) || categoryLabelCache[v] || v + ); + valuesMap.set(valueStr, individualLabels.join(", ")); + } else { + // ๋‹จ์ผ ๊ฐ’: ๋งคํ•‘์—์„œ ์ฐพ๊ฑฐ๋‚˜ ์บ์‹œ์—์„œ ์ฐพ๊ฑฐ๋‚˜ ์›๋ณธ ์‚ฌ์šฉ + const label = singleValueLabelMap.get(valueStr) || categoryLabelCache[valueStr] || valueStr; + valuesMap.set(valueStr, label); + } + } + }); + + globalLabelMap[column.columnName] = singleValueLabelMap; + // value-label ์Œ์œผ๋กœ ์ €์žฅํ•˜๊ณ  ๋ผ๋ฒจ ๊ธฐ์ค€ ์ •๋ ฌ result[column.columnName] = Array.from(valuesMap.entries()) .map(([value, label]) => ({ value, label })) @@ -2289,7 +2353,82 @@ export const TableListComponent: React.FC = ({ }); return result; - }, [data, tableConfig.columns, joinColumnMapping]); + }, [data, tableConfig.columns, joinColumnMapping, categoryLabelCache]); + + // ๐Ÿ†• ๋ผ๋ฒจ์„ ๋ชป ์ฐพ์€ CATEGORY_ ์ฝ”๋“œ๋“ค์„ API๋กœ ์กฐํšŒ + useEffect(() => { + const unlabeledCodes = new Set(); + + // columnUniqueValues์—์„œ ๋ผ๋ฒจ์ด ์ฝ”๋“œ ๊ทธ๋Œ€๋กœ์ธ ํ•ญ๋ชฉ ์ฐพ๊ธฐ + Object.values(columnUniqueValues).forEach(items => { + items.forEach(item => { + // ๋ผ๋ฒจ์— CATEGORY_๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด ๋ผ๋ฒจ์„ ๋ชป ์ฐพ์€ ๊ฒƒ + if (item.label.includes("CATEGORY_")) { + // ์ฝค๋งˆ๋กœ ๋ถ„๋ฆฌํ•ด์„œ ๊ฐœ๋ณ„ ์ฝ”๋“œ ์ถ”์ถœ + const codes = item.label.split(",").map(c => c.trim()); + codes.forEach(code => { + if (code.startsWith("CATEGORY_") && !categoryLabelCache[code]) { + unlabeledCodes.add(code); + } + }); + } + }); + }); + + if (unlabeledCodes.size === 0) return; + + // API๋กœ ๋ผ๋ฒจ ์กฐํšŒ + const fetchLabels = async () => { + try { + const response = await getCategoryLabelsByCodes(Array.from(unlabeledCodes)); + if (response.success && response.data) { + setCategoryLabelCache(prev => ({ ...prev, ...response.data })); + } + } catch (error) { + console.error("์นดํ…Œ๊ณ ๋ฆฌ ๋ผ๋ฒจ ์กฐํšŒ ์‹คํŒจ:", error); + } + }; + + fetchLabels(); + }, [columnUniqueValues, categoryLabelCache]); + + // ๐Ÿ†• ๋ฐ์ดํ„ฐ์—์„œ CATEGORY_ ์ฝ”๋“œ๋ฅผ ์ฐพ์•„ ๋ผ๋ฒจ ๋ฏธ๋ฆฌ ๋กœ๋“œ (ํ…Œ์ด๋ธ” ์…€ ๋ Œ๋”๋ง์šฉ) + useEffect(() => { + if (data.length === 0) return; + + const categoryCodesToFetch = new Set(); + + // ๋ชจ๋“  ๋ฐ์ดํ„ฐ ํ–‰์—์„œ CATEGORY_ ์ฝ”๋“œ ์ˆ˜์ง‘ + data.forEach((row) => { + Object.entries(row).forEach(([key, value]) => { + if (value && typeof value === "string") { + // ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๋‹ค์ค‘ ๊ฐ’๋„ ์ฒ˜๋ฆฌ + const codes = value.split(",").map((v) => v.trim()); + codes.forEach((code) => { + if (code.startsWith("CATEGORY_") && !categoryLabelCache[code]) { + categoryCodesToFetch.add(code); + } + }); + } + }); + }); + + if (categoryCodesToFetch.size === 0) return; + + // API๋กœ ๋ผ๋ฒจ ์กฐํšŒ + const fetchLabels = async () => { + try { + const response = await getCategoryLabelsByCodes(Array.from(categoryCodesToFetch)); + if (response.success && response.data && Object.keys(response.data).length > 0) { + setCategoryLabelCache((prev) => ({ ...prev, ...response.data })); + } + } catch (error) { + console.error("CATEGORY_ ๋ผ๋ฒจ ์กฐํšŒ ์‹คํŒจ:", error); + } + }; + + fetchLabels(); + }, [data, categoryLabelCache]); // ๐Ÿ†• ํ—ค๋” ํ•„ํ„ฐ ํ† ๊ธ€ const toggleHeaderFilter = useCallback((columnName: string, value: string) => { @@ -4447,10 +4586,36 @@ export const TableListComponent: React.FC = ({ case "boolean": return value ? "์˜ˆ" : "์•„๋‹ˆ์˜ค"; default: - return String(value); + // ๐Ÿ†• CATEGORY_ ์ฝ”๋“œ ์ž๋™ ๋ณ€ํ™˜ (inputType์ด category๊ฐ€ ์•„๋‹ˆ์–ด๋„) + const strValue = String(value); + if (strValue.startsWith("CATEGORY_")) { + // rowData์—์„œ _label ํ•„๋“œ ์ฐพ๊ธฐ + if (rowData) { + const labelFieldCandidates = [ + `${column.columnName}_label`, + `${column.columnName}_name`, + `${column.columnName}_value_label`, + ]; + for (const labelField of labelFieldCandidates) { + if (rowData[labelField] && rowData[labelField] !== "") { + return String(rowData[labelField]); + } + } + } + // categoryMappings์—์„œ ์ฐพ๊ธฐ + const mapping = categoryMappings[column.columnName]; + if (mapping && mapping[strValue]) { + return mapping[strValue].label; + } + // categoryLabelCache์—์„œ ์ฐพ๊ธฐ (ํ•„ํ„ฐ์šฉ ์บ์‹œ) + if (categoryLabelCache[strValue]) { + return categoryLabelCache[strValue]; + } + } + return strValue; } }, - [columnMeta, joinedColumnMeta, optimizedConvertCode, categoryMappings], + [columnMeta, joinedColumnMeta, optimizedConvertCode, categoryMappings, categoryLabelCache], ); // ======================================== diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 6daf17e9..4c9d638b 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -304,6 +304,9 @@ export interface ButtonActionContext { selectedLeftData?: Record; refreshRightPanel?: () => void; }; + + // ๐Ÿ†• ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ (์ €์žฅ ํ›„ ์ œ์–ด ์‹คํ–‰ ์‹œ ํ”Œ๋กœ์šฐ์— ์ „๋‹ฌ) + savedData?: any; } /** @@ -1036,10 +1039,11 @@ export class ButtonActionExecutor { } // ๐Ÿ†• ๊ณตํ†ต ํ•„๋“œ ๋ณ‘ํ•ฉ + ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ - // ๊ณตํ†ต ํ•„๋“œ๋ฅผ ๋จผ์ € ๋„ฃ๊ณ , ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ๋ฐ์ดํ„ฐ๋กœ ๋ฎ์–ด์”€ (๊ฐœ๋ณ„ ํ•ญ๋ชฉ์ด ์šฐ์„ ) + // ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋จผ์ € ๋„ฃ๊ณ , ๊ณตํ†ต ํ•„๋“œ๋กœ ๋ฎ์–ด์”€ (๊ณตํ†ต ํ•„๋“œ๊ฐ€ ์šฐ์„ ) + // ์ด์œ : ์‚ฌ์šฉ์ž๊ฐ€ ๊ณตํ†ต ํ•„๋“œ(์ถœ๊ณ ์ƒํƒœ ๋“ฑ)๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ชจ๋“  ํ•ญ๋ชฉ์— ์ ์šฉ๋˜์–ด์•ผ ํ•จ const dataWithMeta: Record = { - ...commonFields, // ๋ฒ”์šฉ ํผ ๋ชจ๋‹ฌ์˜ ๊ณตํ†ต ํ•„๋“œ (order_no, manager_id ๋“ฑ) ...dataToSave, // RepeaterFieldGroup์˜ ๊ฐœ๋ณ„ ํ•ญ๋ชฉ ๋ฐ์ดํ„ฐ + ...commonFields, // ๋ฒ”์šฉ ํผ ๋ชจ๋‹ฌ์˜ ๊ณตํ†ต ํ•„๋“œ (outbound_status ๋“ฑ) - ๊ณตํ†ต ํ•„๋“œ๊ฐ€ ์šฐ์„ ! created_by: context.userId, updated_by: context.userId, company_code: context.companyCode, @@ -1251,7 +1255,49 @@ export class ButtonActionExecutor { // ๐Ÿ”ฅ ์ €์žฅ ์„ฑ๊ณต ํ›„ ์—ฐ๊ฒฐ๋œ ์ œ์–ด ์‹คํ–‰ (dataflowTiming์ด 'after'์ธ ๊ฒฝ์šฐ) if (config.enableDataflowControl && config.dataflowConfig) { console.log("๐ŸŽฏ ์ €์žฅ ํ›„ ์ œ์–ด ์‹คํ–‰ ์‹œ์ž‘:", config.dataflowConfig); - await this.executeAfterSaveControl(config, context); + + // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ (comp_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ์— JSON ๋ฐฐ์—ด์ด ์žˆ๋Š” ๊ฒฝ์šฐ) + // ์ž…๊ณ  ํ™”๋ฉด ๋“ฑ์—์„œ ํ’ˆ๋ชฉ ๋ชฉ๋ก์ด comp_xxx ํ•„๋“œ์— JSON ๋ฌธ์ž์—ด๋กœ ์ €์žฅ๋จ + const formData: Record = (saveResult.data || context.formData || {}) as Record; + let parsedSectionData: any[] = []; + + // comp_๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•„๋“œ์—์„œ ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ + const compFieldKey = Object.keys(formData).find(key => + key.startsWith("comp_") && typeof formData[key] === "string" + ); + + if (compFieldKey) { + try { + const sectionData = JSON.parse(formData[compFieldKey]); + if (Array.isArray(sectionData) && sectionData.length > 0) { + // ๊ณตํ†ต ํ•„๋“œ์™€ ์„น์…˜ ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ + parsedSectionData = sectionData.map((item: any) => { + // ์„น์…˜ ๋ฐ์ดํ„ฐ์—์„œ ๋ถˆํ•„์š”ํ•œ ๋‚ด๋ถ€ ํ•„๋“œ ์ œ๊ฑฐ + const { _isNewItem, _targetTable, _existingRecord, ...cleanItem } = item; + // ๊ณตํ†ต ํ•„๋“œ(comp_ ํ•„๋“œ ์ œ์™ธ) + ์„น์…˜ ์•„์ดํ…œ ๋ณ‘ํ•ฉ + const commonFields: Record = {}; + Object.keys(formData).forEach(key => { + if (!key.startsWith("comp_") && !key.endsWith("_numberingRuleId")) { + commonFields[key] = formData[key]; + } + }); + return { ...commonFields, ...cleanItem }; + }); + console.log(`๐Ÿ“ฆ [handleSave] ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์™„๋ฃŒ: ${parsedSectionData.length}๊ฑด`, parsedSectionData[0]); + } + } catch (parseError) { + console.warn("โš ๏ธ [handleSave] ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ ์‹คํŒจ:", parseError); + } + } + + // ์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ context์— ์ถ”๊ฐ€ํ•˜์—ฌ ํ”Œ๋กœ์šฐ์— ์ „๋‹ฌ + const contextWithSavedData = { + ...context, + savedData: formData, + // ํŒŒ์‹ฑ๋œ ์„น์…˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด selectedRowsData๋กœ ์ „๋‹ฌ + selectedRowsData: parsedSectionData.length > 0 ? parsedSectionData : context.selectedRowsData, + }; + await this.executeAfterSaveControl(config, contextWithSavedData); } } else { throw new Error("์ €์žฅ์— ํ•„์š”ํ•œ ์ •๋ณด๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. (ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” ํ™”๋ฉดID ๋ˆ„๋ฝ)"); @@ -3643,8 +3689,20 @@ export class ButtonActionExecutor { // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ API const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); - // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„ - const sourceData: any = context.formData || {}; + // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„: context-data ๋ชจ๋“œ๋Š” ๋ฐฐ์—ด์„ ๊ธฐ๋Œ€ํ•จ + // ์šฐ์„ ์ˆœ์œ„: selectedRowsData > savedData > formData + // - selectedRowsData: ํ…Œ์ด๋ธ” ์„น์…˜์—์„œ ์ €์žฅ๋œ ํ•˜์œ„ ํ•ญ๋ชฉ๋“ค (item_code, inbound_qty ๋“ฑ ํฌํ•จ) + // - savedData: ์ €์žฅ API ์‘๋‹ต ๋ฐ์ดํ„ฐ + // - formData: ํผ์— ์ž…๋ ฅ๋œ ๋ฐ์ดํ„ฐ + let sourceData: any[]; + if (context.selectedRowsData && context.selectedRowsData.length > 0) { + sourceData = context.selectedRowsData; + console.log("๐Ÿ“ฆ [๋‹ค์ค‘์ œ์–ด] selectedRowsData ์‚ฌ์šฉ:", sourceData.length, "๊ฑด"); + } else { + const savedData = context.savedData || context.formData || {}; + sourceData = Array.isArray(savedData) ? savedData : [savedData]; + console.log("๐Ÿ“ฆ [๋‹ค์ค‘์ œ์–ด] savedData/formData ์‚ฌ์šฉ:", sourceData.length, "๊ฑด"); + } let allSuccess = true; const results: Array<{ flowId: number; flowName: string; success: boolean; message?: string }> = []; @@ -3751,8 +3809,20 @@ export class ButtonActionExecutor { // ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์‹คํ–‰ API ํ˜ธ์ถœ const { executeNodeFlow } = await import("@/lib/api/nodeFlows"); - // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„ - const sourceData: any = context.formData || {}; + // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ค€๋น„: context-data ๋ชจ๋“œ๋Š” ๋ฐฐ์—ด์„ ๊ธฐ๋Œ€ํ•จ + // ์šฐ์„ ์ˆœ์œ„: selectedRowsData > savedData > formData + // - selectedRowsData: ํ…Œ์ด๋ธ” ์„น์…˜์—์„œ ์ €์žฅ๋œ ํ•˜์œ„ ํ•ญ๋ชฉ๋“ค (item_code, inbound_qty ๋“ฑ ํฌํ•จ) + // - savedData: ์ €์žฅ API ์‘๋‹ต ๋ฐ์ดํ„ฐ + // - formData: ํผ์— ์ž…๋ ฅ๋œ ๋ฐ์ดํ„ฐ + let sourceData: any[]; + if (context.selectedRowsData && context.selectedRowsData.length > 0) { + sourceData = context.selectedRowsData; + console.log("๐Ÿ“ฆ [๋‹จ์ผ์ œ์–ด] selectedRowsData ์‚ฌ์šฉ:", sourceData.length, "๊ฑด"); + } else { + const savedData = context.savedData || context.formData || {}; + sourceData = Array.isArray(savedData) ? savedData : [savedData]; + console.log("๐Ÿ“ฆ [๋‹จ์ผ์ œ์–ด] savedData/formData ์‚ฌ์šฉ:", sourceData.length, "๊ฑด"); + } // repeat-screen-modal ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ณ‘ํ•ฉ const repeatScreenModalKeys = Object.keys(context.formData || {}).filter((key) => @@ -3765,7 +3835,8 @@ export class ButtonActionExecutor { console.log("๐Ÿ“ฆ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ์— ์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ:", { flowId, dataSourceType: controlDataSource, - sourceData, + sourceDataCount: sourceData.length, + sourceDataSample: sourceData[0], }); const result = await executeNodeFlow(flowId, { diff --git a/frontend/types/node-editor.ts b/frontend/types/node-editor.ts index 55c8f67e..6eb1bb1c 100644 --- a/frontend/types/node-editor.ts +++ b/frontend/types/node-editor.ts @@ -95,24 +95,35 @@ export interface RestAPISourceNodeData { displayName?: string; } +// ์กฐ๊ฑด ์—ฐ์‚ฐ์ž ํƒ€์ž… +export type ConditionOperator = + | "EQUALS" + | "NOT_EQUALS" + | "GREATER_THAN" + | "LESS_THAN" + | "GREATER_THAN_OR_EQUAL" + | "LESS_THAN_OR_EQUAL" + | "LIKE" + | "NOT_LIKE" + | "IN" + | "NOT_IN" + | "IS_NULL" + | "IS_NOT_NULL" + | "EXISTS_IN" // ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•จ + | "NOT_EXISTS_IN"; // ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜์ง€ ์•Š์Œ + // ์กฐ๊ฑด ๋ถ„๊ธฐ ๋…ธ๋“œ export interface ConditionNodeData { conditions: Array<{ field: string; - operator: - | "EQUALS" - | "NOT_EQUALS" - | "GREATER_THAN" - | "LESS_THAN" - | "GREATER_THAN_OR_EQUAL" - | "LESS_THAN_OR_EQUAL" - | "LIKE" - | "NOT_LIKE" - | "IN" - | "NOT_IN" - | "IS_NULL" - | "IS_NOT_NULL"; + operator: ConditionOperator; value: any; + valueType?: "static" | "field"; // ๋น„๊ต ๊ฐ’ ํƒ€์ž… + // EXISTS_IN / NOT_EXISTS_IN ์ „์šฉ ํ•„๋“œ + lookupTable?: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ”๋ช… + lookupTableLabel?: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ” ๋ผ๋ฒจ + lookupField?: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ”์˜ ๋น„๊ต ํ•„๋“œ + lookupFieldLabel?: string; // ์กฐํšŒํ•  ํ…Œ์ด๋ธ”์˜ ๋น„๊ต ํ•„๋“œ ๋ผ๋ฒจ }>; logic: "AND" | "OR"; displayName?: string;