diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index 6f481198..40eada6e 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; }; // ๐Ÿ”ฅ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ „๋‹ฌ๋˜์—ˆ์œผ๋ฉด ์‚ฌ์šฉ, ์•„๋‹ˆ๋ฉด ๋…๋ฆฝ ํŠธ๋žœ์žญ์…˜ ์ƒ์„ฑ diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 389dd92d..74cea859 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -2392,6 +2392,44 @@ export const TableListComponent: React.FC = ({ 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) => { setHeaderFilters((prev) => { @@ -4548,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 6c5d5d36..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; } /** @@ -1252,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 ๋ˆ„๋ฝ)"); @@ -3644,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 }> = []; @@ -3752,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) => @@ -3766,7 +3835,8 @@ export class ButtonActionExecutor { console.log("๐Ÿ“ฆ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ์— ์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ:", { flowId, dataSourceType: controlDataSource, - sourceData, + sourceDataCount: sourceData.length, + sourceDataSample: sourceData[0], }); const result = await executeNodeFlow(flowId, {