From ab3a493abbd11ab95e7d5a43d410f27be9ad1a83 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 14:03:07 +0900 Subject: [PATCH 01/16] =?UTF-8?q?=ED=94=BC=EB=B2=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridComponent.tsx | 92 +++++++++---------- .../pivot-grid/PivotGridRenderer.tsx | 4 +- 2 files changed, 46 insertions(+), 50 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index b55907a4..bdc00019 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -184,7 +184,7 @@ const DataCell: React.FC = ({ onClick={onClick} onDoubleClick={onDoubleClick} > - - + 0 ); } @@ -222,7 +222,7 @@ const DataCell: React.FC = ({ )} {icon && {icon}} - {values[0].formattedValue} + {values[0].formattedValue || (values[0].value === 0 ? '0' : values[0].formattedValue)} ); @@ -257,7 +257,7 @@ const DataCell: React.FC = ({ )} {icon && {icon}} - {val.formattedValue} + {val.formattedValue || (val.value === 0 ? '0' : val.formattedValue)} ))} @@ -530,14 +530,22 @@ export const PivotGridComponent: React.FC = ({ // ๐Ÿ†• ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ์ฒซ ๋ ˆ๋ฒจ ์ž๋™ ํ™•์žฅ useEffect(() => { - if (!isInitialExpanded && pivotResult && pivotResult.flatRows.length > 0) { + if (pivotResult && pivotResult.flatRows.length > 0) { + console.log("๐Ÿ”ถ ํ”ผ๋ฒ— ๊ฒฐ๊ณผ ์ƒ์„ฑ๋จ:", { + flatRowsCount: pivotResult.flatRows.length, + expandedRowPaths: pivotState.expandedRowPaths.length, + isInitialExpanded, + }); + // ์ฒซ ๋ ˆ๋ฒจ ํ–‰๋“ค์˜ ๊ฒฝ๋กœ ์ˆ˜์ง‘ (level 0์ธ ํ–‰๋“ค) - const firstLevelPaths = pivotResult.flatRows - .filter(row => row.level === 0 && row.hasChildren) - .map(row => row.path); + const firstLevelRows = pivotResult.flatRows.filter(row => row.level === 0 && row.hasChildren); - if (firstLevelPaths.length > 0) { - console.log("๐Ÿ”ถ ์ดˆ๊ธฐ ์ž๋™ ํ™•์žฅ:", firstLevelPaths); + console.log("๐Ÿ”ถ ์ฒซ ๋ ˆ๋ฒจ ํ–‰ (level 0, hasChildren):", firstLevelRows.map(r => ({ path: r.path, caption: r.caption }))); + + // ์ดˆ๊ธฐ ํ™•์žฅ์ด ์•ˆ ๋˜์–ด ์žˆ๊ณ , ์ฒซ ๋ ˆ๋ฒจ ํ–‰์ด ์žˆ์œผ๋ฉด ์ž๋™ ํ™•์žฅ + if (!isInitialExpanded && firstLevelRows.length > 0) { + const firstLevelPaths = firstLevelRows.map(row => row.path); + console.log("๐Ÿ”ถ ์ดˆ๊ธฐ ์ž๋™ ํ™•์žฅ ์‹คํ–‰:", firstLevelPaths); setPivotState(prev => ({ ...prev, expandedRowPaths: firstLevelPaths, @@ -545,7 +553,7 @@ export const PivotGridComponent: React.FC = ({ setIsInitialExpanded(true); } } - }, [pivotResult, isInitialExpanded]); + }, [pivotResult, isInitialExpanded, pivotState.expandedRowPaths.length]); // ์กฐ๊ฑด๋ถ€ ์„œ์‹์šฉ ์ „์ฒด ๊ฐ’ ์ˆ˜์ง‘ const allCellValues = useMemo(() => { @@ -1607,13 +1615,13 @@ export const PivotGridComponent: React.FC = ({ {/* ์—ด ํ—ค๋” */} - + {/* ์ขŒ์ƒ๋‹จ ์ฝ”๋„ˆ (ํ–‰ ํ•„๋“œ ๋ผ๋ฒจ + ํ•„ํ„ฐ) */} ))} + + {/* ํ–‰ ์ด๊ณ„ ํ—ค๋” */} + {totals?.showRowGrandTotals && ( + + )} - {/* ์—ด ํ•„๋“œ ํ•„ํ„ฐ (ํ—ค๋” ์™ผ์ชฝ์— ํ‘œ์‹œ) */} + {/* ์—ด ํ•„๋“œ ํ•„ํ„ฐ (ํ—ค๋” ์˜ค๋ฅธ์ชฝ ๋์— ํ‘œ์‹œ) */} {columnFields.length > 0 && ( )} - - {/* ํ–‰ ์ด๊ณ„ ํ—ค๋” */} - {totals?.showRowGrandTotals && ( - - )} {/* ๋ฐ์ดํ„ฐ ํ•„๋“œ ๋ผ๋ฒจ (๋‹ค์ค‘ ๋ฐ์ดํ„ฐ ํ•„๋“œ์ธ ๊ฒฝ์šฐ) */} {dataFields.length > 1 && ( - + {flatColumns.map((col, colIdx) => ( {dataFields.map((df, dfIdx) => ( @@ -1747,7 +1756,7 @@ export const PivotGridComponent: React.FC = ({ key={`${colIdx}-${dfIdx}`} className={cn( "border-r border-b border-border", - "px-2 py-1 text-center text-xs font-normal", + "px-2 py-0.5 text-center text-xs font-normal", "text-muted-foreground cursor-pointer hover:bg-accent/50" )} onClick={() => handleSort(df.field)} @@ -1760,19 +1769,6 @@ export const PivotGridComponent: React.FC = ({ ))} ))} - {totals?.showRowGrandTotals && - dataFields.map((df, dfIdx) => ( - - ))} )} diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx index 08b21369..191f3610 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx @@ -210,7 +210,7 @@ const PivotGridWrapper: React.FC = (props) => { fieldChooser={componentConfig.fieldChooser || props.fieldChooser} chart={componentConfig.chart || props.chart} allowExpandAll={componentConfig.allowExpandAll !== false} - height={componentConfig.height || props.height || "400px"} + height="100%" maxHeight={componentConfig.maxHeight || props.maxHeight} exportConfig={componentConfig.exportConfig || props.exportConfig || { excel: true }} onCellClick={props.onCellClick} @@ -339,7 +339,7 @@ export class PivotGridRenderer extends AutoRegisteringComponentRenderer { fieldChooser={componentConfig.fieldChooser || props.fieldChooser} chart={componentConfig.chart || props.chart} allowExpandAll={componentConfig.allowExpandAll !== false} - height={componentConfig.height || props.height || "400px"} + height="100%" maxHeight={componentConfig.maxHeight || props.maxHeight} exportConfig={componentConfig.exportConfig || props.exportConfig || { excel: true }} onCellClick={props.onCellClick} From 8a865ac1f487185677bc7a38f9d1a6fc47ccde27 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 14:29:19 +0900 Subject: [PATCH 02/16] =?UTF-8?q?=EC=9D=B4=EC=83=81=ED=95=9C=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=20=EC=88=98=EC=A0=95=20=ED=94=BC=EB=B2=97=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridComponent.tsx | 77 ++++++++++++++----- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index bdc00019..ccfbde88 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -526,11 +526,16 @@ export const PivotGridComponent: React.FC = ({ }); return result; - }, [filteredData, fields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths]); + }, [ + filteredData, + fields, + JSON.stringify(pivotState.expandedRowPaths), + JSON.stringify(pivotState.expandedColumnPaths) + ]); // ๐Ÿ†• ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ์ฒซ ๋ ˆ๋ฒจ ์ž๋™ ํ™•์žฅ useEffect(() => { - if (pivotResult && pivotResult.flatRows.length > 0) { + if (pivotResult && pivotResult.flatRows.length > 0 && !isInitialExpanded) { console.log("๐Ÿ”ถ ํ”ผ๋ฒ— ๊ฒฐ๊ณผ ์ƒ์„ฑ๋จ:", { flatRowsCount: pivotResult.flatRows.length, expandedRowPaths: pivotState.expandedRowPaths.length, @@ -542,10 +547,10 @@ export const PivotGridComponent: React.FC = ({ console.log("๐Ÿ”ถ ์ฒซ ๋ ˆ๋ฒจ ํ–‰ (level 0, hasChildren):", firstLevelRows.map(r => ({ path: r.path, caption: r.caption }))); - // ์ดˆ๊ธฐ ํ™•์žฅ์ด ์•ˆ ๋˜์–ด ์žˆ๊ณ , ์ฒซ ๋ ˆ๋ฒจ ํ–‰์ด ์žˆ์œผ๋ฉด ์ž๋™ ํ™•์žฅ - if (!isInitialExpanded && firstLevelRows.length > 0) { + // ์ฒซ ๋ ˆ๋ฒจ ํ–‰์ด ์žˆ์œผ๋ฉด ์ž๋™ ํ™•์žฅ + if (firstLevelRows.length > 0) { const firstLevelPaths = firstLevelRows.map(row => row.path); - console.log("๐Ÿ”ถ ์ดˆ๊ธฐ ์ž๋™ ํ™•์žฅ ์‹คํ–‰:", firstLevelPaths); + console.log("๐Ÿ”ถ ์ดˆ๊ธฐ ์ž๋™ ํ™•์žฅ ์‹คํ–‰ (ํ•œ ๋ฒˆ๋งŒ):", firstLevelPaths); setPivotState(prev => ({ ...prev, expandedRowPaths: firstLevelPaths, @@ -553,7 +558,7 @@ export const PivotGridComponent: React.FC = ({ setIsInitialExpanded(true); } } - }, [pivotResult, isInitialExpanded, pivotState.expandedRowPaths.length]); + }, [pivotResult, isInitialExpanded]); // ์กฐ๊ฑด๋ถ€ ์„œ์‹์šฉ ์ „์ฒด ๊ฐ’ ์ˆ˜์ง‘ const allCellValues = useMemo(() => { @@ -749,31 +754,61 @@ export const PivotGridComponent: React.FC = ({ [onExpandChange] ); - // ์ „์ฒด ํ™•์žฅ + // ์ „์ฒด ํ™•์žฅ (์žฌ๊ท€์ ์œผ๋กœ ๋ชจ๋“  ๋ ˆ๋ฒจ ํ™•์žฅ) const handleExpandAll = useCallback(() => { - if (!pivotResult) return; + if (!pivotResult) { + console.log("โŒ [handleExpandAll] pivotResult๊ฐ€ ์—†์Œ"); + return; + } + // ๐Ÿ†• ์žฌ๊ท€์ ์œผ๋กœ ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ ์ƒ์„ฑ const allRowPaths: string[][] = []; - pivotResult.flatRows.forEach((row) => { - if (row.hasChildren) { - allRowPaths.push(row.path); + const rowFields = fields.filter((f) => f.area === "row" && f.visible !== false); + + // ๋ฐ์ดํ„ฐ์—์„œ ๋ชจ๋“  ๊ณ ์œ ํ•œ ๊ฒฝ๋กœ ์ถ”์ถœ + const pathSet = new Set(); + filteredData.forEach((item) => { + for (let depth = 1; depth <= rowFields.length; depth++) { + const path = rowFields.slice(0, depth).map((f) => String(item[f.field] ?? "")); + const pathKey = JSON.stringify(path); + pathSet.add(pathKey); } }); + // Set์„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ + pathSet.forEach((pathKey) => { + allRowPaths.push(JSON.parse(pathKey)); + }); + + console.log("๐Ÿ”ท [handleExpandAll] ํ™•์žฅํ•  ํ–‰:", { + totalRows: pivotResult.flatRows.length, + rowsWithChildren: allRowPaths.length, + paths: allRowPaths.slice(0, 5), // ์ฒ˜์Œ 5๊ฐœ๋งŒ ๋กœ๊ทธ + }); + setPivotState((prev) => ({ ...prev, expandedRowPaths: allRowPaths, expandedColumnPaths: [], })); - }, [pivotResult]); + }, [pivotResult, fields, filteredData]); // ์ „์ฒด ์ถ•์†Œ const handleCollapseAll = useCallback(() => { - setPivotState((prev) => ({ - ...prev, - expandedRowPaths: [], - expandedColumnPaths: [], - })); + console.log("๐Ÿ”ท [handleCollapseAll] ์ „์ฒด ์ถ•์†Œ ์‹คํ–‰"); + + setPivotState((prev) => { + console.log("๐Ÿ”ท [handleCollapseAll] ์ด์ „ ์ƒํƒœ:", { + expandedRowPaths: prev.expandedRowPaths.length, + expandedColumnPaths: prev.expandedColumnPaths.length, + }); + + return { + ...prev, + expandedRowPaths: [], + expandedColumnPaths: [], + }; + }); }, []); // ์…€ ํด๋ฆญ @@ -1391,8 +1426,8 @@ export const PivotGridComponent: React.FC = ({ variant="ghost" size="sm" className="h-7 px-2" - onClick={handleExpandAll} - title="์ „์ฒด ํ™•์žฅ" + onClick={handleCollapseAll} + title="์ „์ฒด ์ถ•์†Œ" > @@ -1401,8 +1436,8 @@ export const PivotGridComponent: React.FC = ({ variant="ghost" size="sm" className="h-7 px-2" - onClick={handleCollapseAll} - title="์ „์ฒด ์ถ•์†Œ" + onClick={handleExpandAll} + title="์ „์ฒด ํ™•์žฅ" > From ab52c49492f8a7c57a615df60ea43fc20f3b66fe Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 16 Jan 2026 14:48:15 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=ED=99=94=EB=A9=B4=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20(?= =?UTF-8?q?=EB=B3=B5=EC=A0=9C/=EC=82=AD=EC=A0=9C/=EA=B7=B8=EB=A3=B9=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC/=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ํ™”๋ฉด ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์˜ ๋ณต์ œ, ์‚ญ์ œ, ์ˆ˜์ • ๋ฐ ํ…Œ์ด๋ธ” ์„ค์ • ๊ธฐ๋Šฅ์„ ์ „๋ฉด ๊ฐœ์„  - ๊ทธ๋ฃน ์‚ญ์ œ ์‹œ ํ•˜์œ„ ๊ทธ๋ฃน๊ณผ์˜ ์—ฐ๊ด€์„ฑ ์ •๋ฆฌ ๋ฐ ๋กœ๋”ฉ ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ์ถ”๊ฐ€ - ํ™”๋ฉด ์ˆ˜์ • ๊ธฐ๋Šฅ ์ถ”๊ฐ€: ์ด๋ฆ„, ๊ทธ๋ฃน, ์—ญํ• , ์ •๋ ฌ ์ˆœ์„œ ๋ณ€๊ฒฝ - ํ…Œ์ด๋ธ” ์„ค์ • ๋ชจ๋‹ฌ์— ๊ด€๋ จ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๋ฐ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€ - ๋ฉ”๋‰ด-ํ™”๋ฉด ๊ทธ๋ฃน ๋™๊ธฐํ™” API ์ถ”๊ฐ€ ๋ฐ ๊ด€๋ จ ์ƒํƒœ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๊ตฌํ˜„ - ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ๋ง ๋กœ์ง ๊ฐœ์„ : ๋‹ค์ค‘ ํ‚ค์›Œ๋“œ ์ง€์› - ๊ด€๋ จ ํŒŒ์ผ ๋ฐ ์ง„ํ–‰ ์ƒํƒœ ์—…๋ฐ์ดํŠธ --- PLAN.MD | 59 +- .../src/controllers/screenGroupController.ts | 240 ++++- backend-node/src/routes/screenGroupRoutes.ts | 17 + .../src/services/menuScreenSyncService.ts | 939 ++++++++++++++++++ .../admin/screenMng/screenMngList/page.tsx | 14 +- .../components/screen/CopyScreenModal.tsx | 2 +- .../components/screen/ScreenGroupTreeView.tsx | 589 ++++++++++- .../screen/panels/DataFlowPanel.tsx | 1 + .../screen/panels/FieldJoinPanel.tsx | 1 + frontend/lib/api/screenGroup.ts | 94 ++ 10 files changed, 1906 insertions(+), 50 deletions(-) create mode 100644 backend-node/src/services/menuScreenSyncService.ts diff --git a/PLAN.MD b/PLAN.MD index 0ca6521d..271d0af1 100644 --- a/PLAN.MD +++ b/PLAN.MD @@ -1,7 +1,7 @@ -# ํ”„๋กœ์ ํŠธ: ํ™”๋ฉด ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๊ฐœ์„  (๋ณต์ œ/์‚ญ์ œ/๊ทธ๋ฃน ๊ด€๋ฆฌ) +# ํ”„๋กœ์ ํŠธ: ํ™”๋ฉด ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ๊ฐœ์„  (๋ณต์ œ/์‚ญ์ œ/๊ทธ๋ฃน ๊ด€๋ฆฌ/ํ…Œ์ด๋ธ” ์„ค์ •) ## ๊ฐœ์š” -ํ™”๋ฉด ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์˜ ๋ณต์ œ ๋ฐ ์‚ญ์ œ ๊ธฐ๋Šฅ์„ ์ „๋ฉด ๊ฐœ์„ ํ•˜์—ฌ, ๋‹จ์ผ ํ™”๋ฉด ๋ณต์ œ, ๊ทธ๋ฃน(ํด๋”) ์ „์ฒด ๋ณต์ œ, ์ •๋ ฌ ์ˆœ์„œ ์œ ์ง€, ์ผ๊ด„ ์ด๋ฆ„ ๋ณ€๊ฒฝ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. +ํ™”๋ฉด ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์˜ ๋ณต์ œ, ์‚ญ์ œ, ์ˆ˜์ •, ํ…Œ์ด๋ธ” ์„ค์ • ๊ธฐ๋Šฅ์„ ์ „๋ฉด ๊ฐœ์„ ํ•˜์—ฌ ํšจ์œจ์ ์ธ ํ™”๋ฉด ๊ด€๋ฆฌ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ## ํ•ต์‹ฌ ๊ธฐ๋Šฅ @@ -15,47 +15,54 @@ ### 2. ๊ทธ๋ฃน(ํด๋”) ์ „์ฒด ๋ณต์ œ - [x] ๋Œ€๋ถ„๋ฅ˜ ํด๋” ๋ณต์ œ ์‹œ ๋ชจ๋“  ํ•˜์œ„ ํด๋” + ํ™”๋ฉด ์žฌ๊ท€์  ๋ณต์ œ - [x] ์ •๋ ฌ ์ˆœ์„œ(display_order) ์œ ์ง€ - - ๊ทธ๋ฃน ์ƒ์„ฑ ์‹œ ์›๋ณธ display_order ์ „๋‹ฌ - - ํ™”๋ฉด ์ถ”๊ฐ€ ์‹œ ์›๋ณธ display_order ์œ ์ง€ - - ํ•˜์œ„ ๊ทธ๋ฃน๋“ค display_order ์ˆœ์œผ๋กœ ์ •๋ ฌ ํ›„ ๋ณต์ œ - [x] ๋Œ€๋ถ„๋ฅ˜(์ตœ์ƒ์œ„ ๊ทธ๋ฃน) ๋ณต์ œ ์‹œ ๊ฒฝ๊ณ  ๋ฌธ๊ตฌ ํ‘œ์‹œ -- [x] ์ •๋ ฌ ์ˆœ์„œ ์ž…๋ ฅ ํ•„๋“œ ์ถ”๊ฐ€ (์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์ˆ˜์ • ๊ฐ€๋Šฅ) -- [x] ์›๋ณธ ๊ทธ๋ฃน ์ •๋ณด ํ‘œ์‹œ ๊ฐœ์„  - - ์ง์ ‘ ํฌํ•จ ํ™”๋ฉด ์ˆ˜ - - ํ•˜์œ„ ๊ทธ๋ฃน ์ˆ˜ - - ๋ณต์ œ๋  ์ด ํ™”๋ฉด ์ˆ˜ (ํ•˜์œ„ ๊ทธ๋ฃน ํฌํ•จ) +- [x] ์ •๋ ฌ ์ˆœ์„œ ์ž…๋ ฅ ํ•„๋“œ ์ถ”๊ฐ€ +- [x] ๋ณต์ œ ๋ชจ๋“œ ์„ ํƒ: ์ „์ฒด(ํด๋”+ํ™”๋ฉด), ํด๋”๋งŒ, ํ™”๋ฉด๋งŒ +- [x] ๋ชจ๋‹ฌ ์Šคํฌ๋กค ์ง€์› (max-h-[90vh] overflow-y-auto) ### 3. ๊ณ ๊ธ‰ ์˜ต์…˜: ์ด๋ฆ„ ์ผ๊ด„ ๋ณ€๊ฒฝ -- [x] ์‚ญ์ œํ•  ํ…์ŠคํŠธ ์ง€์ • (๋ชจ๋“  ํด๋”/ํ™”๋ฉด ์ด๋ฆ„์—์„œ ์ œ๊ฑฐ) -- [x] ์ถ”๊ฐ€ํ•  ์ ‘๋ฏธ์‚ฌ ์ง€์ • (๊ธฐ๋ณธ๊ฐ’: " (๋ณต์ œ)") +- [x] ์ฐพ์„ ํ…์ŠคํŠธ / ๋Œ€์ฒดํ•  ํ…์ŠคํŠธ (Find & Replace) - [x] ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ธฐ๋Šฅ ### 4. ์‚ญ์ œ ๊ธฐ๋Šฅ - [x] ๋‹จ์ผ ํ™”๋ฉด ์‚ญ์ œ (ํœด์ง€ํ†ต์œผ๋กœ ์ด๋™) -- [x] ๊ทธ๋ฃน ์‚ญ์ œ ์‹œ ์˜ต์…˜ ์„ ํƒ - - "ํ™”๋ฉด๋„ ํ•จ๊ป˜ ์‚ญ์ œ" ์ฒดํฌ๋ฐ•์Šค - - ์ฒดํฌ ์‹œ: ๊ทธ๋ฃน + ํฌํ•จ๋œ ํ™”๋ฉด ๋ชจ๋‘ ์‚ญ์ œ - - ๋ฏธ์ฒดํฌ ์‹œ: ํ™”๋ฉด์€ "๋ฏธ๋ถ„๋ฅ˜"๋กœ ์ด๋™ +- [x] ๊ทธ๋ฃน ์‚ญ์ œ (ํ™”๋ฉด ํ•จ๊ป˜ ์‚ญ์ œ ์˜ต์…˜) +- [x] ์‚ญ์ œ ์‹œ ๋กœ๋”ฉ ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ํ‘œ์‹œ -### 5. ํšŒ์‚ฌ ์ฝ”๋“œ ์ง€์› (์ตœ๊ณ  ๊ด€๋ฆฌ์ž) +### 5. ํ™”๋ฉด ์ˆ˜์ • ๊ธฐ๋Šฅ +- [x] ์šฐํด๋ฆญ "์ˆ˜์ •" ๋ฉ”๋‰ด๋กœ ํ™”๋ฉด ์ด๋ฆ„/๊ทธ๋ฃน/์—ญํ• /์ •๋ ฌ ์ˆœ์„œ ๋ณ€๊ฒฝ +- [x] ๊ทธ๋ฃน ์ถ”๊ฐ€/์ˆ˜์ • ์‹œ ์ƒ์œ„ ๊ทธ๋ฃน ๊ธฐ๋ฐ˜ ์ž๋™ ํšŒ์‚ฌ ์ฝ”๋“œ ์„ค์ • + +### 6. ํ…Œ์ด๋ธ” ์„ค์ • ๊ธฐ๋Šฅ (TableSettingModal) +- [x] ํ™”๋ฉด ์„ค์ • ๋ชจ๋‹ฌ์— "ํ…Œ์ด๋ธ” ์„ค์ •" ํƒญ ์ถ”๊ฐ€ +- [x] ์ž…๋ ฅ ํƒ€์ž… ๋ณ€๊ฒฝ ์‹œ ๊ด€๋ จ ์ฐธ์กฐ ํ•„๋“œ ์ž๋™ ์ดˆ๊ธฐํ™” + - ์—”ํ‹ฐํ‹ฐโ†’ํ…์ŠคํŠธ: referenceTable, referenceColumn, displayColumn ์ดˆ๊ธฐํ™” + - ์ฝ”๋“œโ†’๋‹ค๋ฅธ ํƒ€์ž…: codeCategory, codeValue ์ดˆ๊ธฐํ™” +- [x] ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ์œ ์ง€ (inputType โ†” referenceTable ์—ฐ๋™) +- [x] ์กฐ์ธ ๋ฐฐ์ง€ ๋‹จ์ผํ™” (FK ๋ฐฐ์ง€ ์ œ๊ฑฐ, ์กฐ์ธ ๋ฐฐ์ง€๋งŒ ํ‘œ์‹œ) + +### 7. ํšŒ์‚ฌ ์ฝ”๋“œ ์ง€์› (์ตœ๊ณ  ๊ด€๋ฆฌ์ž) - [x] ๋Œ€์ƒ ํšŒ์‚ฌ ์„ ํƒ ๊ฐ€๋Šฅ -- [x] ๋ณต์ œ๋œ ๊ทธ๋ฃน/ํ™”๋ฉด์— ์„ ํƒํ•œ ํšŒ์‚ฌ ์ฝ”๋“œ ์ ์šฉ +- [x] ์ƒ์œ„ ๊ทธ๋ฃน ์„ ํƒ ์‹œ ์ž๋™ ํšŒ์‚ฌ ์ฝ”๋“œ ์„ค์ • ## ๊ด€๋ จ ํŒŒ์ผ -- `frontend/components/screen/CopyScreenModal.tsx` - ๋ณต์ œ ๋ชจ๋‹ฌ (ํ™”๋ฉด/๊ทธ๋ฃน ํ†ตํ•ฉ) +- `frontend/components/screen/CopyScreenModal.tsx` - ๋ณต์ œ ๋ชจ๋‹ฌ - `frontend/components/screen/ScreenGroupTreeView.tsx` - ํŠธ๋ฆฌ ๋ทฐ + ์ปจํ…์ŠคํŠธ ๋ฉ”๋‰ด -- `frontend/lib/api/screen.ts` - ํ™”๋ฉด API (๋ณต์ œ, ์‚ญ์ œ) +- `frontend/components/screen/TableSettingModal.tsx` - ํ…Œ์ด๋ธ” ์„ค์ • ๋ชจ๋‹ฌ +- `frontend/components/screen/ScreenSettingModal.tsx` - ํ™”๋ฉด ์„ค์ • ๋ชจ๋‹ฌ (ํ…Œ์ด๋ธ” ์„ค์ • ํƒญ ํฌํ•จ) +- `frontend/lib/api/screen.ts` - ํ™”๋ฉด API - `frontend/lib/api/screenGroup.ts` - ๊ทธ๋ฃน API +- `frontend/lib/api/tableManagement.ts` - ํ…Œ์ด๋ธ” ๊ด€๋ฆฌ API ## ์ง„ํ–‰ ์ƒํƒœ - [์™„๋ฃŒ] ๋‹จ์ผ ํ™”๋ฉด ๋ณต์ œ + ์ƒˆ๋กœ๊ณ ์นจ - [์™„๋ฃŒ] ๊ทธ๋ฃน ์ „์ฒด ๋ณต์ œ (์žฌ๊ท€์ ) -- [์™„๋ฃŒ] ์ •๋ ฌ ์ˆœ์„œ(display_order) ์œ ์ง€ -- [์™„๋ฃŒ] ๋Œ€๋ถ„๋ฅ˜ ๊ฒฝ๊ณ  ๋ฌธ๊ตฌ -- [์™„๋ฃŒ] ์ •๋ ฌ ์ˆœ์„œ ์ž…๋ ฅ ํ•„๋“œ -- [์™„๋ฃŒ] ๊ณ ๊ธ‰ ์˜ต์…˜: ์ด๋ฆ„ ์ผ๊ด„ ๋ณ€๊ฒฝ -- [์™„๋ฃŒ] ๋‹จ์ผ ํ™”๋ฉด ์‚ญ์ œ -- [์™„๋ฃŒ] ๊ทธ๋ฃน ์‚ญ์ œ (ํ™”๋ฉด ํ•จ๊ป˜ ์‚ญ์ œ ์˜ต์…˜) +- [์™„๋ฃŒ] ๊ณ ๊ธ‰ ์˜ต์…˜: ์ด๋ฆ„ ์ผ๊ด„ ๋ณ€๊ฒฝ (Find & Replace) +- [์™„๋ฃŒ] ๋‹จ์ผ ํ™”๋ฉด/๊ทธ๋ฃน ์‚ญ์ œ + ๋กœ๋”ฉ ํ”„๋กœ๊ทธ๋ ˆ์Šค +- [์™„๋ฃŒ] ํ™”๋ฉด ์ˆ˜์ • (์ด๋ฆ„/๊ทธ๋ฃน/์—ญํ• /์ˆœ์„œ) +- [์™„๋ฃŒ] ํ…Œ์ด๋ธ” ์„ค์ • ํƒญ ์ถ”๊ฐ€ +- [์™„๋ฃŒ] ์ž…๋ ฅ ํƒ€์ž… ๋ณ€๊ฒฝ ์‹œ ๊ด€๋ จ ํ•„๋“œ ์ดˆ๊ธฐํ™” +- [์™„๋ฃŒ] ๊ทธ๋ฃน ๋ณต์ œ ๋ชจ๋‹ฌ ์Šคํฌ๋กค ๋ฌธ์ œ ์ˆ˜์ • --- diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index 569fe793..b89ef902 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -1,6 +1,12 @@ import { Request, Response } from "express"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; +import { + syncScreenGroupsToMenu, + syncMenuToScreenGroups, + getSyncStatus, + syncAllCompanies, +} from "../services/menuScreenSyncService"; // pool ์ธ์Šคํ„ด์Šค ๊ฐ€์ ธ์˜ค๊ธฐ const pool = getPool(); @@ -294,10 +300,35 @@ export const updateScreenGroup = async (req: Request, res: Response) => { // ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ export const deleteScreenGroup = async (req: Request, res: Response) => { + const client = await pool.connect(); try { const { id } = req.params; const companyCode = (req.user as any).companyCode; + await client.query('BEGIN'); + + // 1. ์‚ญ์ œํ•  ๊ทธ๋ฃน๊ณผ ํ•˜์œ„ ๊ทธ๋ฃน ID ์ˆ˜์ง‘ (CASCADE ์‚ญ์ œ ๋Œ€์ƒ) + const childGroupsResult = await client.query(` + WITH RECURSIVE child_groups AS ( + SELECT id FROM screen_groups WHERE id = $1 + UNION ALL + SELECT sg.id FROM screen_groups sg + JOIN child_groups cg ON sg.parent_group_id = cg.id + ) + SELECT id FROM child_groups + `, [id]); + const groupIdsToDelete = childGroupsResult.rows.map((r: any) => r.id); + + // 2. menu_info์—์„œ ์‚ญ์ œ๋  screen_group ์ฐธ์กฐ๋ฅผ NULL๋กœ ์ •๋ฆฌ + if (groupIdsToDelete.length > 0) { + await client.query(` + UPDATE menu_info + SET screen_group_id = NULL + WHERE screen_group_id = ANY($1::int[]) + `, [groupIdsToDelete]); + } + + // 3. screen_groups ์‚ญ์ œ let query = `DELETE FROM screen_groups WHERE id = $1`; const params: any[] = [id]; @@ -308,18 +339,24 @@ export const deleteScreenGroup = async (req: Request, res: Response) => { query += " RETURNING id"; - const result = await pool.query(query, params); + const result = await client.query(query, params); if (result.rows.length === 0) { + await client.query('ROLLBACK'); return res.status(404).json({ success: false, message: "ํ™”๋ฉด ๊ทธ๋ฃน์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค." }); } - logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ", { companyCode, groupId: id }); + await client.query('COMMIT'); + + logger.info("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ", { companyCode, groupId: id, cleanedRefs: groupIdsToDelete.length }); res.json({ success: true, message: "ํ™”๋ฉด ๊ทธ๋ฃน์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }); } catch (error: any) { + await client.query('ROLLBACK'); logger.error("ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ ์‹คํŒจ:", error); res.status(500).json({ success: false, message: "ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", error: error.message }); + } finally { + client.release(); } }; @@ -2014,3 +2051,202 @@ export const getScreenSubTables = async (req: Request, res: Response) => { } }; + +// ============================================================ +// ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” API +// ============================================================ + +/** + * ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” + * screen_groups๋ฅผ menu_info๋กœ ๋™๊ธฐํ™” + */ +export const syncScreenGroupsToMenuController = async (req: Request, res: Response) => { + try { + const userCompanyCode = (req.user as any).companyCode; + const userId = (req.user as any).userId; + const { targetCompanyCode } = req.body; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ + let companyCode = userCompanyCode; + if (userCompanyCode === "*" && targetCompanyCode) { + companyCode = targetCompanyCode; + } + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(*)๋Š” ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•ด์•ผ ํ•จ + if (companyCode === "*") { + return res.status(400).json({ + success: false, + message: "๋™๊ธฐํ™”ํ•  ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", + }); + } + + logger.info("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์š”์ฒญ", { companyCode, userId }); + + const result = await syncScreenGroupsToMenu(companyCode, userId); + + if (!result.success) { + return res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + errors: result.errors, + }); + } + + res.json({ + success: true, + message: `๋™๊ธฐํ™” ์™„๋ฃŒ: ์ƒ์„ฑ ${result.created}๊ฐœ, ์—ฐ๊ฒฐ ${result.linked}๊ฐœ, ์Šคํ‚ต ${result.skipped}๊ฐœ`, + data: result, + }); + } catch (error: any) { + logger.error("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + +/** + * ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” + * menu_info๋ฅผ screen_groups๋กœ ๋™๊ธฐํ™” + */ +export const syncMenuToScreenGroupsController = async (req: Request, res: Response) => { + try { + const userCompanyCode = (req.user as any).companyCode; + const userId = (req.user as any).userId; + const { targetCompanyCode } = req.body; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ + let companyCode = userCompanyCode; + if (userCompanyCode === "*" && targetCompanyCode) { + companyCode = targetCompanyCode; + } + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(*)๋Š” ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•ด์•ผ ํ•จ + if (companyCode === "*") { + return res.status(400).json({ + success: false, + message: "๋™๊ธฐํ™”ํ•  ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", + }); + } + + logger.info("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์š”์ฒญ", { companyCode, userId }); + + const result = await syncMenuToScreenGroups(companyCode, userId); + + if (!result.success) { + return res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + errors: result.errors, + }); + } + + res.json({ + success: true, + message: `๋™๊ธฐํ™” ์™„๋ฃŒ: ์ƒ์„ฑ ${result.created}๊ฐœ, ์—ฐ๊ฒฐ ${result.linked}๊ฐœ, ์Šคํ‚ต ${result.skipped}๊ฐœ`, + data: result, + }); + } catch (error: any) { + logger.error("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + +/** + * ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ + */ +export const getSyncStatusController = async (req: Request, res: Response) => { + try { + const userCompanyCode = (req.user as any).companyCode; + const { targetCompanyCode } = req.query; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ + let companyCode = userCompanyCode; + if (userCompanyCode === "*" && targetCompanyCode) { + companyCode = targetCompanyCode as string; + } + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(*)๋Š” ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•ด์•ผ ํ•จ + if (companyCode === "*") { + return res.status(400).json({ + success: false, + message: "์กฐํšŒํ•  ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”.", + }); + } + + const status = await getSyncStatus(companyCode); + + res.json({ + success: true, + data: status, + }); + } catch (error: any) { + logger.error("๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + +/** + * ์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” + * ๋ชจ๋“  ํšŒ์‚ฌ์— ๋Œ€ํ•ด ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) + */ +export const syncAllCompaniesController = async (req: Request, res: Response) => { + try { + const userCompanyCode = (req.user as any).companyCode; + const userId = (req.user as any).userId; + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ „์ฒด ๋™๊ธฐํ™” ๊ฐ€๋Šฅ + if (userCompanyCode !== "*") { + return res.status(403).json({ + success: false, + message: "์ „์ฒด ๋™๊ธฐํ™”๋Š” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + logger.info("์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” ์š”์ฒญ", { userId }); + + const result = await syncAllCompanies(userId); + + if (!result.success) { + return res.status(500).json({ + success: false, + message: "์ „์ฒด ๋™๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + }); + } + + // ๊ฒฐ๊ณผ ์š”์•ฝ + const totalCreated = result.results.reduce((sum, r) => sum + r.created, 0); + const totalLinked = result.results.reduce((sum, r) => sum + r.linked, 0); + + res.json({ + success: true, + message: `์ „์ฒด ๋™๊ธฐํ™” ์™„๋ฃŒ: ${result.totalCompanies}๊ฐœ ํšŒ์‚ฌ ์ค‘ ${result.successCount}๊ฐœ ์„ฑ๊ณต`, + data: { + totalCompanies: result.totalCompanies, + successCount: result.successCount, + failedCount: result.failedCount, + totalCreated, + totalLinked, + details: result.results, + }, + }); + } catch (error: any) { + logger.error("์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” ์‹คํŒจ:", error); + res.status(500).json({ + success: false, + message: "์ „์ฒด ๋™๊ธฐํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } +}; + diff --git a/backend-node/src/routes/screenGroupRoutes.ts b/backend-node/src/routes/screenGroupRoutes.ts index d4980fe8..614e6d61 100644 --- a/backend-node/src/routes/screenGroupRoutes.ts +++ b/backend-node/src/routes/screenGroupRoutes.ts @@ -31,6 +31,11 @@ import { getMultipleScreenLayoutSummary, // ํ™”๋ฉด ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๊ด€๊ณ„ getScreenSubTables, + // ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” + syncScreenGroupsToMenuController, + syncMenuToScreenGroupsController, + getSyncStatusController, + syncAllCompaniesController, } from "../controllers/screenGroupController"; const router = Router(); @@ -89,6 +94,18 @@ router.post("/layout-summary/batch", getMultipleScreenLayoutSummary); // ============================================================ router.post("/sub-tables/batch", getScreenSubTables); +// ============================================================ +// ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” +// ============================================================ +// ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ +router.get("/sync/status", getSyncStatusController); +// ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” +router.post("/sync/screen-to-menu", syncScreenGroupsToMenuController); +// ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” +router.post("/sync/menu-to-screen", syncMenuToScreenGroupsController); +// ์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) +router.post("/sync/all", syncAllCompaniesController); + export default router; diff --git a/backend-node/src/services/menuScreenSyncService.ts b/backend-node/src/services/menuScreenSyncService.ts new file mode 100644 index 00000000..13c77ed6 --- /dev/null +++ b/backend-node/src/services/menuScreenSyncService.ts @@ -0,0 +1,939 @@ +import { getPool } from "../database/db"; +import { logger } from "../utils/logger"; + +const pool = getPool(); + +/** + * ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” ์„œ๋น„์Šค + * + * ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™”: + * 1. screen_groups โ†’ menu_info: ํ™”๋ฉด๊ด€๋ฆฌ ํด๋” ๊ตฌ์กฐ๋ฅผ ๋ฉ”๋‰ด๋กœ ๋™๊ธฐํ™” + * 2. menu_info โ†’ screen_groups: ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด๋ฅผ ํ™”๋ฉด๊ด€๋ฆฌ ํด๋”๋กœ ๋™๊ธฐํ™” + */ + +// ============================================================ +// ํƒ€์ž… ์ •์˜ +// ============================================================ + +interface SyncResult { + success: boolean; + created: number; + linked: number; + skipped: number; + errors: string[]; + details: SyncDetail[]; +} + +interface SyncDetail { + action: 'created' | 'linked' | 'skipped' | 'error'; + sourceName: string; + sourceId: number | string; + targetId?: number | string; + reason?: string; +} + +// ============================================================ +// ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” +// ============================================================ + +/** + * screen_groups๋ฅผ menu_info๋กœ ๋™๊ธฐํ™” + * + * ๋กœ์ง: + * 1. ํ•ด๋‹น ํšŒ์‚ฌ์˜ screen_groups ์กฐํšŒ (ํด๋” ๊ตฌ์กฐ) + * 2. ์ด๋ฏธ menu_objid๊ฐ€ ์—ฐ๊ฒฐ๋œ ๊ฒƒ์€ ์ œ์™ธ + * 3. ์ด๋ฆ„์œผ๋กœ ๊ธฐ์กด menu_info ๋งค์นญ ์‹œ๋„ + * - ๋งค์นญ๋˜๋ฉด: ์–‘์ชฝ์— ์—ฐ๊ฒฐ ID ์—…๋ฐ์ดํŠธ + * - ๋งค์นญ ์•ˆ๋˜๋ฉด: menu_info์— ์ƒˆ๋กœ ์ƒ์„ฑ + * 4. ๊ณ„์ธต ๊ตฌ์กฐ(parent) ์œ ์ง€ + */ +export async function syncScreenGroupsToMenu( + companyCode: string, + userId: string +): Promise { + const result: SyncResult = { + success: true, + created: 0, + linked: 0, + skipped: 0, + errors: [], + details: [], + }; + + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + logger.info("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์‹œ์ž‘", { companyCode, userId }); + + // 1. ํ•ด๋‹น ํšŒ์‚ฌ์˜ screen_groups ์กฐํšŒ (์•„์ง menu_objid๊ฐ€ ์—†๋Š” ๊ฒƒ) + const screenGroupsQuery = ` + SELECT + sg.id, + sg.group_name, + sg.group_code, + sg.parent_group_id, + sg.group_level, + sg.display_order, + sg.description, + sg.icon, + sg.menu_objid, + -- ๋ถ€๋ชจ ๊ทธ๋ฃน์˜ menu_objid๋„ ์กฐํšŒ (๊ณ„์ธต ์—ฐ๊ฒฐ์šฉ) + parent.menu_objid as parent_menu_objid + FROM screen_groups sg + LEFT JOIN screen_groups parent ON sg.parent_group_id = parent.id + WHERE sg.company_code = $1 + ORDER BY sg.group_level ASC, sg.display_order ASC + `; + const screenGroupsResult = await client.query(screenGroupsQuery, [companyCode]); + + // 2. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ๊ธฐ์กด menu_info ์กฐํšŒ (์‚ฌ์šฉ์ž ๋ฉ”๋‰ด, menu_type=1) + // ๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ๋งค์นญ์„ ์œ„ํ•ด ๋ถ€๋ชจ ์ด๋ฆ„๋„ ์กฐํšŒ + const existingMenusQuery = ` + SELECT + m.objid, + m.menu_name_kor, + m.parent_obj_id, + m.screen_group_id, + p.menu_name_kor as parent_name + FROM menu_info m + LEFT JOIN menu_info p ON m.parent_obj_id = p.objid + WHERE m.company_code = $1 AND m.menu_type = 1 + `; + const existingMenusResult = await client.query(existingMenusQuery, [companyCode]); + + // ๊ฒฝ๋กœ(๋ถ€๋ชจ์ด๋ฆ„ > ์ด๋ฆ„) โ†’ ๋ฉ”๋‰ด ๋งคํ•‘ (screen_group_id๊ฐ€ ์—†๋Š” ๊ฒƒ๋งŒ) + // ๋‹จ์ˆœ ์ด๋ฆ„ ๋งค์นญ๋„ ์œ ์ง€ (ํ•˜์œ„ ํ˜ธํ™˜) + const menuByPath: Map = new Map(); + const menuByName: Map = new Map(); + existingMenusResult.rows.forEach((menu: any) => { + if (!menu.screen_group_id) { + const menuName = menu.menu_name_kor?.trim().toLowerCase() || ''; + const parentName = menu.parent_name?.trim().toLowerCase() || ''; + const pathKey = parentName ? `${parentName}>${menuName}` : menuName; + + menuByPath.set(pathKey, menu); + // ๋‹จ์ˆœ ์ด๋ฆ„ ๋งคํ•‘์€ ์ฒซ ๋ฒˆ์งธ ๊ฒƒ๋งŒ (์ค‘๋ณต ๋ฐฉ์ง€) + if (!menuByName.has(menuName)) { + menuByName.set(menuName, menu); + } + } + }); + + // ๋ชจ๋“  ๋ฉ”๋‰ด์˜ objid ์ง‘ํ•ฉ (์‚ญ์ œ ํ™•์ธ์šฉ) + const existingMenuObjids = new Set(existingMenusResult.rows.map((m: any) => Number(m.objid))); + + // 3. ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด์˜ ๋ฃจํŠธ ์ฐพ๊ธฐ (parent_obj_id = 0์ธ ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด) + // ์—†์œผ๋ฉด ์ƒ์„ฑ + let userMenuRootObjid: number | null = null; + const rootMenuQuery = ` + SELECT objid FROM menu_info + WHERE company_code = $1 AND menu_type = 1 AND parent_obj_id = 0 + ORDER BY seq ASC + LIMIT 1 + `; + const rootMenuResult = await client.query(rootMenuQuery, [companyCode]); + + if (rootMenuResult.rows.length > 0) { + userMenuRootObjid = Number(rootMenuResult.rows[0].objid); + } else { + // ๋ฃจํŠธ ๋ฉ”๋‰ด๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ + const newObjid = Date.now(); + const createRootQuery = ` + INSERT INTO menu_info (objid, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_type, company_code, writer, regdate, status) + VALUES ($1, 0, '์‚ฌ์šฉ์ž', 'User', 1, 1, $2, $3, NOW(), 'Y') + RETURNING objid + `; + const createRootResult = await client.query(createRootQuery, [newObjid, companyCode, userId]); + userMenuRootObjid = Number(createRootResult.rows[0].objid); + logger.info("์‚ฌ์šฉ์ž ๋ฉ”๋‰ด ๋ฃจํŠธ ์ƒ์„ฑ", { companyCode, objid: userMenuRootObjid }); + } + + // 4. screen_groups ID โ†’ menu_objid ๋งคํ•‘ (์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด) + const groupToMenuMap: Map = new Map(); + + // screen_groups์˜ ๋ถ€๋ชจ ์ด๋ฆ„ ์กฐํšŒ๋ฅผ ์œ„ํ•œ ๋งคํ•‘ + const groupIdToName: Map = new Map(); + screenGroupsResult.rows.forEach((g: any) => { + groupIdToName.set(g.id, g.group_name?.trim().toLowerCase() || ''); + }); + + // 5. ๊ฐ screen_group ์ฒ˜๋ฆฌ + for (const group of screenGroupsResult.rows) { + const groupId = group.id; + const groupName = group.group_name?.trim(); + const groupNameLower = groupName?.toLowerCase() || ''; + + // ์ด๋ฏธ ์—ฐ๊ฒฐ๋œ ๊ฒฝ์šฐ - ์‹ค์ œ๋กœ ๋ฉ”๋‰ด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + if (group.menu_objid) { + const menuExists = existingMenuObjids.has(Number(group.menu_objid)); + + if (menuExists) { + // ๋ฉ”๋‰ด๊ฐ€ ์กด์žฌํ•˜๋ฉด ์Šคํ‚ต + result.skipped++; + result.details.push({ + action: 'skipped', + sourceName: groupName, + sourceId: groupId, + targetId: group.menu_objid, + reason: '์ด๋ฏธ ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ๋จ', + }); + groupToMenuMap.set(groupId, Number(group.menu_objid)); + continue; + } else { + // ๋ฉ”๋‰ด๊ฐ€ ์‚ญ์ œ๋˜์—ˆ์œผ๋ฉด ์—ฐ๊ฒฐ ํ•ด์ œํ•˜๊ณ  ์žฌ์ƒ์„ฑ + logger.info("์‚ญ์ œ๋œ ๋ฉ”๋‰ด ์—ฐ๊ฒฐ ํ•ด์ œ", { groupId, deletedMenuObjid: group.menu_objid }); + await client.query( + `UPDATE screen_groups SET menu_objid = NULL, updated_date = NOW() WHERE id = $1`, + [groupId] + ); + // ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ ์žฌ์ƒ์„ฑ ๋˜๋Š” ์žฌ์—ฐ๊ฒฐ + } + } + + // ๋ถ€๋ชจ ๊ทธ๋ฃน ์ด๋ฆ„ ์กฐํšŒ (๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ๋งค์นญ์šฉ) + const parentGroupName = group.parent_group_id ? groupIdToName.get(group.parent_group_id) : ''; + const pathKey = parentGroupName ? `${parentGroupName}>${groupNameLower}` : groupNameLower; + + // ๊ฒฝ๋กœ๋กœ ๊ธฐ์กด ๋ฉ”๋‰ด ๋งค์นญ ์‹œ๋„ (์šฐ์„ ์ˆœ์œ„: ๊ฒฝ๋กœ ๋งค์นญ > ์ด๋ฆ„ ๋งค์นญ) + let matchedMenu = menuByPath.get(pathKey); + if (!matchedMenu) { + // ๊ฒฝ๋กœ ๋งค์นญ ์‹คํŒจ์‹œ ์ด๋ฆ„์œผ๋กœ ์‹œ๋„ (ํ•˜์œ„ ํ˜ธํ™˜) + matchedMenu = menuByName.get(groupNameLower); + } + + if (matchedMenu) { + // ๋งค์นญ๋œ ๋ฉ”๋‰ด์™€ ์—ฐ๊ฒฐ + const menuObjid = Number(matchedMenu.objid); + + // screen_groups์— menu_objid ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE screen_groups SET menu_objid = $1, updated_date = NOW() WHERE id = $2`, + [menuObjid, groupId] + ); + + // menu_info์— screen_group_id ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE menu_info SET screen_group_id = $1 WHERE objid = $2`, + [groupId, menuObjid] + ); + + groupToMenuMap.set(groupId, menuObjid); + result.linked++; + result.details.push({ + action: 'linked', + sourceName: groupName, + sourceId: groupId, + targetId: menuObjid, + }); + + // ๋งค์นญ๋œ ๋ฉ”๋‰ด๋Š” Map์—์„œ ์ œ๊ฑฐ (์ค‘๋ณต ๋งค์นญ ๋ฐฉ์ง€) + menuByPath.delete(pathKey); + menuByName.delete(groupNameLower); + + } else { + // ์ƒˆ ๋ฉ”๋‰ด ์ƒ์„ฑ + const newObjid = Date.now() + groupId; // ๊ณ ์œ  ID ๋ณด์žฅ + + // ๋ถ€๋ชจ ๋ฉ”๋‰ด objid ๊ฒฐ์ • + let parentMenuObjid = userMenuRootObjid; + if (group.parent_group_id && group.parent_menu_objid) { + parentMenuObjid = Number(group.parent_menu_objid); + } else if (group.parent_group_id && groupToMenuMap.has(group.parent_group_id)) { + parentMenuObjid = groupToMenuMap.get(group.parent_group_id)!; + } + + // ๊ฐ™์€ ๋ถ€๋ชจ ์•„๋ž˜์—์„œ ๊ฐ€์žฅ ๋†’์€ seq ์กฐํšŒ ํ›„ +1 + let nextSeq = 1; + const maxSeqQuery = ` + SELECT COALESCE(MAX(seq), 0) + 1 as next_seq + FROM menu_info + WHERE parent_obj_id = $1 AND company_code = $2 AND menu_type = 1 + `; + const maxSeqResult = await client.query(maxSeqQuery, [parentMenuObjid, companyCode]); + if (maxSeqResult.rows.length > 0) { + nextSeq = parseInt(maxSeqResult.rows[0].next_seq) || 1; + } + + // menu_info์— ์‚ฝ์ž… + const insertMenuQuery = ` + INSERT INTO menu_info ( + objid, parent_obj_id, menu_name_kor, menu_name_eng, + seq, menu_type, company_code, writer, regdate, status, screen_group_id, menu_desc + ) VALUES ($1, $2, $3, $4, $5, 1, $6, $7, NOW(), 'Y', $8, $9) + RETURNING objid + `; + await client.query(insertMenuQuery, [ + newObjid, + parentMenuObjid, + groupName, + group.group_code || groupName, + nextSeq, + companyCode, + userId, + groupId, + group.description || null, + ]); + + // screen_groups์— menu_objid ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE screen_groups SET menu_objid = $1, updated_date = NOW() WHERE id = $2`, + [newObjid, groupId] + ); + + groupToMenuMap.set(groupId, newObjid); + result.created++; + result.details.push({ + action: 'created', + sourceName: groupName, + sourceId: groupId, + targetId: newObjid, + }); + } + } + + await client.query('COMMIT'); + + logger.info("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์™„๋ฃŒ", { + companyCode, + created: result.created, + linked: result.linked, + skipped: result.skipped + }); + + return result; + + } catch (error: any) { + await client.query('ROLLBACK'); + logger.error("ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์‹คํŒจ", { companyCode, error: error.message }); + result.success = false; + result.errors.push(error.message); + return result; + } finally { + client.release(); + } +} + + +// ============================================================ +// ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” +// ============================================================ + +/** + * menu_info๋ฅผ screen_groups๋กœ ๋™๊ธฐํ™” + * + * ๋กœ์ง: + * 1. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด(menu_type=1) ์กฐํšŒ + * 2. ์ด๋ฏธ screen_group_id๊ฐ€ ์—ฐ๊ฒฐ๋œ ๊ฒƒ์€ ์ œ์™ธ + * 3. ์ด๋ฆ„์œผ๋กœ ๊ธฐ์กด screen_groups ๋งค์นญ ์‹œ๋„ + * - ๋งค์นญ๋˜๋ฉด: ์–‘์ชฝ์— ์—ฐ๊ฒฐ ID ์—…๋ฐ์ดํŠธ + * - ๋งค์นญ ์•ˆ๋˜๋ฉด: screen_groups์— ์ƒˆ๋กœ ์ƒ์„ฑ (ํด๋”๋กœ) + * 4. ๊ณ„์ธต ๊ตฌ์กฐ(parent) ์œ ์ง€ + */ +export async function syncMenuToScreenGroups( + companyCode: string, + userId: string +): Promise { + const result: SyncResult = { + success: true, + created: 0, + linked: 0, + skipped: 0, + errors: [], + details: [], + }; + + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + logger.info("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์‹œ์ž‘", { companyCode, userId }); + + // 0. ํšŒ์‚ฌ ์ด๋ฆ„ ์กฐํšŒ (ํšŒ์‚ฌ ํด๋” ์ฐพ๊ธฐ/์ƒ์„ฑ์šฉ) + const companyNameQuery = `SELECT company_name FROM company_mng WHERE company_code = $1`; + const companyNameResult = await client.query(companyNameQuery, [companyCode]); + const companyName = companyNameResult.rows[0]?.company_name || companyCode; + + // 1. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ์‚ฌ์šฉ์ž ๋ฉ”๋‰ด ์กฐํšŒ (menu_type=1) + const menusQuery = ` + SELECT + m.objid, + m.menu_name_kor, + m.menu_name_eng, + m.parent_obj_id, + m.seq, + m.menu_url, + m.menu_desc, + m.screen_group_id, + -- ๋ถ€๋ชจ ๋ฉ”๋‰ด์˜ screen_group_id๋„ ์กฐํšŒ (๊ณ„์ธต ์—ฐ๊ฒฐ์šฉ) + parent.screen_group_id as parent_screen_group_id + FROM menu_info m + LEFT JOIN menu_info parent ON m.parent_obj_id = parent.objid + WHERE m.company_code = $1 AND m.menu_type = 1 + ORDER BY + CASE WHEN m.parent_obj_id = 0 THEN 0 ELSE 1 END, + m.parent_obj_id, + m.seq + `; + const menusResult = await client.query(menusQuery, [companyCode]); + + // 2. ํ•ด๋‹น ํšŒ์‚ฌ์˜ ๊ธฐ์กด screen_groups ์กฐํšŒ (๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ๋งค์นญ์„ ์œ„ํ•ด ๋ถ€๋ชจ ์ด๋ฆ„๋„ ์กฐํšŒ) + const existingGroupsQuery = ` + SELECT + g.id, + g.group_name, + g.menu_objid, + g.parent_group_id, + p.group_name as parent_name + FROM screen_groups g + LEFT JOIN screen_groups p ON g.parent_group_id = p.id + WHERE g.company_code = $1 + `; + const existingGroupsResult = await client.query(existingGroupsQuery, [companyCode]); + + // ๊ฒฝ๋กœ(๋ถ€๋ชจ์ด๋ฆ„ > ์ด๋ฆ„) โ†’ ๊ทธ๋ฃน ๋งคํ•‘ (menu_objid๊ฐ€ ์—†๋Š” ๊ฒƒ๋งŒ) + // ๋‹จ์ˆœ ์ด๋ฆ„ ๋งค์นญ๋„ ์œ ์ง€ (ํ•˜์œ„ ํ˜ธํ™˜) + const groupByPath: Map = new Map(); + const groupByName: Map = new Map(); + existingGroupsResult.rows.forEach((group: any) => { + if (!group.menu_objid) { + const groupName = group.group_name?.trim().toLowerCase() || ''; + const parentName = group.parent_name?.trim().toLowerCase() || ''; + const pathKey = parentName ? `${parentName}>${groupName}` : groupName; + + groupByPath.set(pathKey, group); + // ๋‹จ์ˆœ ์ด๋ฆ„ ๋งคํ•‘์€ ์ฒซ ๋ฒˆ์งธ ๊ฒƒ๋งŒ (์ค‘๋ณต ๋ฐฉ์ง€) + if (!groupByName.has(groupName)) { + groupByName.set(groupName, group); + } + } + }); + + // ๋ชจ๋“  ๊ทธ๋ฃน์˜ id ์ง‘ํ•ฉ (์‚ญ์ œ ํ™•์ธ์šฉ) + const existingGroupIds = new Set(existingGroupsResult.rows.map((g: any) => Number(g.id))); + + // 3. ํšŒ์‚ฌ ํด๋” ์ฐพ๊ธฐ ๋˜๋Š” ์ƒ์„ฑ (๋ฃจํŠธ ๋ ˆ๋ฒจ์— ํšŒ์‚ฌ๋ช…์œผ๋กœ ๋œ ํด๋”) + let companyFolderId: number | null = null; + const companyFolderQuery = ` + SELECT id FROM screen_groups + WHERE company_code = $1 AND parent_group_id IS NULL AND group_level = 0 + ORDER BY id ASC + LIMIT 1 + `; + const companyFolderResult = await client.query(companyFolderQuery, [companyCode]); + + if (companyFolderResult.rows.length > 0) { + companyFolderId = companyFolderResult.rows[0].id; + logger.info("ํšŒ์‚ฌ ํด๋” ๋ฐœ๊ฒฌ", { companyCode, companyFolderId, companyName }); + } else { + // ํšŒ์‚ฌ ํด๋”๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ + // ๋ฃจํŠธ ๋ ˆ๋ฒจ์—์„œ ๊ฐ€์žฅ ๋†’์€ display_order ์กฐํšŒ ํ›„ +1 + let nextRootOrder = 1; + const maxRootOrderQuery = ` + SELECT COALESCE(MAX(display_order), 0) + 1 as next_order + FROM screen_groups + WHERE parent_group_id IS NULL + `; + const maxRootOrderResult = await client.query(maxRootOrderQuery); + if (maxRootOrderResult.rows.length > 0) { + nextRootOrder = parseInt(maxRootOrderResult.rows[0].next_order) || 1; + } + + const createFolderQuery = ` + INSERT INTO screen_groups ( + group_name, group_code, parent_group_id, group_level, + display_order, company_code, writer, hierarchy_path + ) VALUES ($1, $2, NULL, 0, $3, $4, $5, '/') + RETURNING id + `; + const createFolderResult = await client.query(createFolderQuery, [ + companyName, + companyCode.toLowerCase(), + nextRootOrder, + companyCode, + userId, + ]); + companyFolderId = createFolderResult.rows[0].id; + + // hierarchy_path ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE screen_groups SET hierarchy_path = $1 WHERE id = $2`, + [`/${companyFolderId}/`, companyFolderId] + ); + + logger.info("ํšŒ์‚ฌ ํด๋” ์ƒ์„ฑ", { companyCode, companyFolderId, companyName }); + } + + // 4. menu_objid โ†’ screen_group_id ๋งคํ•‘ (์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด) + const menuToGroupMap: Map = new Map(); + + // ๋ถ€๋ชจ ๋ฉ”๋‰ด ์ค‘ ์ด๋ฏธ screen_group_id๊ฐ€ ์žˆ๋Š” ๊ฒƒ ๋“ฑ๋ก + menusResult.rows.forEach((menu: any) => { + if (menu.screen_group_id) { + menuToGroupMap.set(Number(menu.objid), Number(menu.screen_group_id)); + } + }); + + // ๋ฃจํŠธ ๋ฉ”๋‰ด(parent_obj_id = 0)์˜ objid ์ฐพ๊ธฐ โ†’ ํšŒ์‚ฌ ํด๋”์™€ ๋งคํ•‘ + let rootMenuObjid: number | null = null; + for (const menu of menusResult.rows) { + if (Number(menu.parent_obj_id) === 0) { + rootMenuObjid = Number(menu.objid); + // ๋ฃจํŠธ ๋ฉ”๋‰ด๋Š” ํšŒ์‚ฌ ํด๋”์™€ ์—ฐ๊ฒฐ + if (companyFolderId) { + menuToGroupMap.set(rootMenuObjid, companyFolderId); + } + break; + } + } + + // 5. ๊ฐ ๋ฉ”๋‰ด ์ฒ˜๋ฆฌ + for (const menu of menusResult.rows) { + const menuObjid = Number(menu.objid); + const menuName = menu.menu_name_kor?.trim(); + + // ๋ฃจํŠธ ๋ฉ”๋‰ด(parent_obj_id = 0)๋Š” ์Šคํ‚ต (์ด๋ฏธ ํšŒ์‚ฌ ํด๋”์™€ ๋งคํ•‘๋จ) + if (Number(menu.parent_obj_id) === 0) { + result.skipped++; + result.details.push({ + action: 'skipped', + sourceName: menuName, + sourceId: menuObjid, + targetId: companyFolderId || undefined, + reason: '๋ฃจํŠธ ๋ฉ”๋‰ด โ†’ ํšŒ์‚ฌ ํด๋”์™€ ๋งคํ•‘๋จ', + }); + continue; + } + + // ์ด๋ฏธ ์—ฐ๊ฒฐ๋œ ๊ฒฝ์šฐ - ์‹ค์ œ๋กœ ๊ทธ๋ฃน์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + if (menu.screen_group_id) { + const groupExists = existingGroupIds.has(Number(menu.screen_group_id)); + + if (groupExists) { + // ๊ทธ๋ฃน์ด ์กด์žฌํ•˜๋ฉด ์Šคํ‚ต + result.skipped++; + result.details.push({ + action: 'skipped', + sourceName: menuName, + sourceId: menuObjid, + targetId: menu.screen_group_id, + reason: '์ด๋ฏธ ํ™”๋ฉด๊ทธ๋ฃน๊ณผ ์—ฐ๊ฒฐ๋จ', + }); + menuToGroupMap.set(menuObjid, Number(menu.screen_group_id)); + continue; + } else { + // ๊ทธ๋ฃน์ด ์‚ญ์ œ๋˜์—ˆ์œผ๋ฉด ์—ฐ๊ฒฐ ํ•ด์ œํ•˜๊ณ  ์žฌ์ƒ์„ฑ + logger.info("์‚ญ์ œ๋œ ๊ทธ๋ฃน ์—ฐ๊ฒฐ ํ•ด์ œ", { menuObjid, deletedGroupId: menu.screen_group_id }); + await client.query( + `UPDATE menu_info SET screen_group_id = NULL WHERE objid = $1`, + [menuObjid] + ); + // ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ ์žฌ์ƒ์„ฑ ๋˜๋Š” ์žฌ์—ฐ๊ฒฐ + } + } + + const menuNameLower = menuName?.toLowerCase() || ''; + + // ๋ถ€๋ชจ ๋ฉ”๋‰ด ์ด๋ฆ„ ์กฐํšŒ (๊ฒฝ๋กœ ๊ธฐ๋ฐ˜ ๋งค์นญ์šฉ) + const parentMenu = menusResult.rows.find((m: any) => Number(m.objid) === Number(menu.parent_obj_id)); + const parentMenuName = parentMenu?.menu_name_kor?.trim().toLowerCase() || ''; + const pathKey = parentMenuName ? `${parentMenuName}>${menuNameLower}` : menuNameLower; + + // ๊ฒฝ๋กœ๋กœ ๊ธฐ์กด ๊ทธ๋ฃน ๋งค์นญ ์‹œ๋„ (์šฐ์„ ์ˆœ์œ„: ๊ฒฝ๋กœ ๋งค์นญ > ์ด๋ฆ„ ๋งค์นญ) + let matchedGroup = groupByPath.get(pathKey); + if (!matchedGroup) { + // ๊ฒฝ๋กœ ๋งค์นญ ์‹คํŒจ์‹œ ์ด๋ฆ„์œผ๋กœ ์‹œ๋„ (ํ•˜์œ„ ํ˜ธํ™˜) + matchedGroup = groupByName.get(menuNameLower); + } + + if (matchedGroup) { + // ๋งค์นญ๋œ ๊ทธ๋ฃน๊ณผ ์—ฐ๊ฒฐ + const groupId = Number(matchedGroup.id); + + try { + // menu_info์— screen_group_id ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE menu_info SET screen_group_id = $1 WHERE objid = $2`, + [groupId, menuObjid] + ); + + // screen_groups์— menu_objid ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE screen_groups SET menu_objid = $1, updated_date = NOW() WHERE id = $2`, + [menuObjid, groupId] + ); + + menuToGroupMap.set(menuObjid, groupId); + result.linked++; + result.details.push({ + action: 'linked', + sourceName: menuName, + sourceId: menuObjid, + targetId: groupId, + }); + + // ๋งค์นญ๋œ ๊ทธ๋ฃน์€ Map์—์„œ ์ œ๊ฑฐ (์ค‘๋ณต ๋งค์นญ ๋ฐฉ์ง€) + groupByPath.delete(pathKey); + groupByName.delete(menuNameLower); + } catch (linkError: any) { + logger.error("๊ทธ๋ฃน ์—ฐ๊ฒฐ ์ค‘ ์—๋Ÿฌ", { menuName, menuObjid, groupId, error: linkError.message, stack: linkError.stack }); + throw linkError; + } + + } else { + // ์ƒˆ screen_group ์ƒ์„ฑ + // ๋ถ€๋ชจ ๊ทธ๋ฃน ID ๊ฒฐ์ • + let parentGroupId: number | null = null; + let groupLevel = 1; // ๊ธฐ๋ณธ๊ฐ’์€ 1 (ํšŒ์‚ฌ ํด๋” ์•„๋ž˜) + + // ์šฐ์„ ์ˆœ์œ„ 1: menuToGroupMap์—์„œ ๋ถ€๋ชจ ๋ฉ”๋‰ด์˜ ์ƒˆ ๊ทธ๋ฃน ID ์กฐํšŒ (๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒ์„ฑ๋œ ๊ฒƒ) + if (menuToGroupMap.has(Number(menu.parent_obj_id))) { + parentGroupId = menuToGroupMap.get(Number(menu.parent_obj_id))!; + } + // ์šฐ์„ ์ˆœ์œ„ 2: ๋ถ€๋ชจ ๋ฉ”๋‰ด๊ฐ€ ๋ฃจํŠธ ๋ฉ”๋‰ด๋ฉด ํšŒ์‚ฌ ํด๋” ์‚ฌ์šฉ + else if (Number(menu.parent_obj_id) === rootMenuObjid) { + parentGroupId = companyFolderId; + } + // ์šฐ์„ ์ˆœ์œ„ 3: ๋ถ€๋ชจ ๋ฉ”๋‰ด์˜ screen_group_id๊ฐ€ ์žˆ๊ณ , ํ•ด๋‹น ๊ทธ๋ฃน์ด ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋ฉด ์‚ฌ์šฉ + else if (menu.parent_screen_group_id && existingGroupIds.has(Number(menu.parent_screen_group_id))) { + parentGroupId = Number(menu.parent_screen_group_id); + } + + // ๋ถ€๋ชจ ๊ทธ๋ฃน์˜ ๋ ˆ๋ฒจ ์กฐํšŒ + if (parentGroupId) { + const parentLevelQuery = `SELECT group_level FROM screen_groups WHERE id = $1`; + const parentLevelResult = await client.query(parentLevelQuery, [parentGroupId]); + if (parentLevelResult.rows.length > 0) { + groupLevel = (parentLevelResult.rows[0].group_level || 0) + 1; + } + } + + // ๊ฐ™์€ ๋ถ€๋ชจ ์•„๋ž˜์—์„œ ๊ฐ€์žฅ ๋†’์€ display_order ์กฐํšŒ ํ›„ +1 + let nextDisplayOrder = 1; + const maxOrderQuery = parentGroupId + ? `SELECT COALESCE(MAX(display_order), 0) + 1 as next_order FROM screen_groups WHERE parent_group_id = $1 AND company_code = $2` + : `SELECT COALESCE(MAX(display_order), 0) + 1 as next_order FROM screen_groups WHERE parent_group_id IS NULL AND company_code = $1`; + const maxOrderParams = parentGroupId ? [parentGroupId, companyCode] : [companyCode]; + const maxOrderResult = await client.query(maxOrderQuery, maxOrderParams); + if (maxOrderResult.rows.length > 0) { + nextDisplayOrder = parseInt(maxOrderResult.rows[0].next_order) || 1; + } + + // group_code ์ƒ์„ฑ (์˜๋ฌธ๋ช… ๋˜๋Š” ์ด๋ฆ„ ๊ธฐ๋ฐ˜) + const groupCode = (menu.menu_name_eng || menuName || 'group') + .replace(/\s+/g, '_') + .toLowerCase() + .substring(0, 50); + + // screen_groups์— ์‚ฝ์ž… + const insertGroupQuery = ` + INSERT INTO screen_groups ( + group_name, group_code, parent_group_id, group_level, + display_order, company_code, writer, menu_objid, description + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING id + `; + + let newGroupId: number; + try { + logger.info("์ƒˆ ๊ทธ๋ฃน ์ƒ์„ฑ ์‹œ๋„", { + menuName, + menuObjid, + groupCode: groupCode + '_' + menuObjid, + parentGroupId, + groupLevel, + nextDisplayOrder, + companyCode, + }); + + const insertResult = await client.query(insertGroupQuery, [ + menuName, + groupCode + '_' + menuObjid, // ๊ณ ์œ ์„ฑ ๋ณด์žฅ + parentGroupId, + groupLevel, + nextDisplayOrder, + companyCode, + userId, + menuObjid, + menu.menu_desc || null, + ]); + + newGroupId = insertResult.rows[0].id; + } catch (insertError: any) { + logger.error("๊ทธ๋ฃน ์ƒ์„ฑ ์ค‘ ์—๋Ÿฌ", { + menuName, + menuObjid, + parentGroupId, + groupLevel, + error: insertError.message, + stack: insertError.stack, + code: insertError.code, + detail: insertError.detail, + }); + throw insertError; + } + + // hierarchy_path ์—…๋ฐ์ดํŠธ + let hierarchyPath = `/${newGroupId}/`; + if (parentGroupId) { + const parentPathQuery = `SELECT hierarchy_path FROM screen_groups WHERE id = $1`; + const parentPathResult = await client.query(parentPathQuery, [parentGroupId]); + if (parentPathResult.rows.length > 0 && parentPathResult.rows[0].hierarchy_path) { + hierarchyPath = `${parentPathResult.rows[0].hierarchy_path}${newGroupId}/`.replace('//', '/'); + } + } + await client.query( + `UPDATE screen_groups SET hierarchy_path = $1 WHERE id = $2`, + [hierarchyPath, newGroupId] + ); + + // menu_info์— screen_group_id ์—…๋ฐ์ดํŠธ + await client.query( + `UPDATE menu_info SET screen_group_id = $1 WHERE objid = $2`, + [newGroupId, menuObjid] + ); + + menuToGroupMap.set(menuObjid, newGroupId); + result.created++; + result.details.push({ + action: 'created', + sourceName: menuName, + sourceId: menuObjid, + targetId: newGroupId, + }); + } + } + + await client.query('COMMIT'); + + logger.info("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์™„๋ฃŒ", { + companyCode, + created: result.created, + linked: result.linked, + skipped: result.skipped + }); + + return result; + + } catch (error: any) { + await client.query('ROLLBACK'); + logger.error("๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์‹คํŒจ", { + companyCode, + error: error.message, + stack: error.stack, + code: error.code, + detail: error.detail, + }); + result.success = false; + result.errors.push(error.message); + return result; + } finally { + client.release(); + } +} + + +// ============================================================ +// ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ +// ============================================================ + +/** + * ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ + * + * - ์—ฐ๊ฒฐ๋œ ํ•ญ๋ชฉ ์ˆ˜ + * - ์—ฐ๊ฒฐ ์•ˆ ๋œ ํ•ญ๋ชฉ ์ˆ˜ + * - ์–‘๋ฐฉํ–ฅ ๋น„๊ต + */ +export async function getSyncStatus(companyCode: string): Promise<{ + screenGroups: { total: number; linked: number; unlinked: number }; + menuItems: { total: number; linked: number; unlinked: number }; + potentialMatches: Array<{ menuName: string; groupName: string; similarity: string }>; +}> { + // screen_groups ์ƒํƒœ + const sgQuery = ` + SELECT + COUNT(*) as total, + COUNT(menu_objid) as linked + FROM screen_groups + WHERE company_code = $1 + `; + const sgResult = await pool.query(sgQuery, [companyCode]); + + // menu_info ์ƒํƒœ (์‚ฌ์šฉ์ž ๋ฉ”๋‰ด๋งŒ, ๋ฃจํŠธ ์ œ์™ธ) + const menuQuery = ` + SELECT + COUNT(*) as total, + COUNT(screen_group_id) as linked + FROM menu_info + WHERE company_code = $1 AND menu_type = 1 AND parent_obj_id != 0 + `; + const menuResult = await pool.query(menuQuery, [companyCode]); + + // ์ด๋ฆ„์ด ๊ฐ™์€ ์ž ์žฌ์  ๋งค์นญ ํ›„๋ณด ์กฐํšŒ + const matchQuery = ` + SELECT + m.menu_name_kor as menu_name, + sg.group_name + FROM menu_info m + JOIN screen_groups sg ON LOWER(TRIM(m.menu_name_kor)) = LOWER(TRIM(sg.group_name)) + WHERE m.company_code = $1 + AND sg.company_code = $1 + AND m.menu_type = 1 + AND m.screen_group_id IS NULL + AND sg.menu_objid IS NULL + LIMIT 10 + `; + const matchResult = await pool.query(matchQuery, [companyCode]); + + const sgTotal = parseInt(sgResult.rows[0].total); + const sgLinked = parseInt(sgResult.rows[0].linked); + const menuTotal = parseInt(menuResult.rows[0].total); + const menuLinked = parseInt(menuResult.rows[0].linked); + + return { + screenGroups: { + total: sgTotal, + linked: sgLinked, + unlinked: sgTotal - sgLinked, + }, + menuItems: { + total: menuTotal, + linked: menuLinked, + unlinked: menuTotal - menuLinked, + }, + potentialMatches: matchResult.rows.map((row: any) => ({ + menuName: row.menu_name, + groupName: row.group_name, + similarity: 'exact', + })), + }; +} + + +// ============================================================ +// ์ „์ฒด ๋™๊ธฐํ™” (๋ชจ๋“  ํšŒ์‚ฌ) +// ============================================================ + +interface AllCompaniesSyncResult { + success: boolean; + totalCompanies: number; + successCount: number; + failedCount: number; + results: Array<{ + companyCode: string; + companyName: string; + direction: 'screens-to-menus' | 'menus-to-screens'; + created: number; + linked: number; + skipped: number; + success: boolean; + error?: string; + }>; +} + +/** + * ๋ชจ๋“  ํšŒ์‚ฌ์— ๋Œ€ํ•ด ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ + * + * ๋กœ์ง: + * 1. ๋ชจ๋“  ํšŒ์‚ฌ ์กฐํšŒ + * 2. ๊ฐ ํšŒ์‚ฌ๋ณ„๋กœ ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ + * - ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” + * - ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” + * 3. ๊ฒฐ๊ณผ ์ง‘๊ณ„ + */ +export async function syncAllCompanies( + userId: string +): Promise { + const result: AllCompaniesSyncResult = { + success: true, + totalCompanies: 0, + successCount: 0, + failedCount: 0, + results: [], + }; + + try { + logger.info("์ „์ฒด ๋™๊ธฐํ™” ์‹œ์ž‘", { userId }); + + // ๋ชจ๋“  ํšŒ์‚ฌ ์กฐํšŒ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž ์ „์šฉ ํšŒ์‚ฌ ์ œ์™ธ) + const companiesQuery = ` + SELECT company_code, company_name + FROM company_mng + WHERE company_code != '*' + ORDER BY company_name + `; + const companiesResult = await pool.query(companiesQuery); + + result.totalCompanies = companiesResult.rows.length; + + // ๊ฐ ํšŒ์‚ฌ๋ณ„๋กœ ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” + for (const company of companiesResult.rows) { + const companyCode = company.company_code; + const companyName = company.company_name; + + try { + // 1. ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” + const screensToMenusResult = await syncScreenGroupsToMenu(companyCode, userId); + result.results.push({ + companyCode, + companyName, + direction: 'screens-to-menus', + created: screensToMenusResult.created, + linked: screensToMenusResult.linked, + skipped: screensToMenusResult.skipped, + success: screensToMenusResult.success, + error: screensToMenusResult.errors.length > 0 ? screensToMenusResult.errors.join(', ') : undefined, + }); + + // 2. ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” + const menusToScreensResult = await syncMenuToScreenGroups(companyCode, userId); + result.results.push({ + companyCode, + companyName, + direction: 'menus-to-screens', + created: menusToScreensResult.created, + linked: menusToScreensResult.linked, + skipped: menusToScreensResult.skipped, + success: menusToScreensResult.success, + error: menusToScreensResult.errors.length > 0 ? menusToScreensResult.errors.join(', ') : undefined, + }); + + if (screensToMenusResult.success && menusToScreensResult.success) { + result.successCount++; + } else { + result.failedCount++; + } + + } catch (error: any) { + logger.error("ํšŒ์‚ฌ ๋™๊ธฐํ™” ์‹คํŒจ", { companyCode, companyName, error: error.message }); + result.results.push({ + companyCode, + companyName, + direction: 'screens-to-menus', + created: 0, + linked: 0, + skipped: 0, + success: false, + error: error.message, + }); + result.failedCount++; + } + } + + logger.info("์ „์ฒด ๋™๊ธฐํ™” ์™„๋ฃŒ", { + totalCompanies: result.totalCompanies, + successCount: result.successCount, + failedCount: result.failedCount, + }); + + return result; + + } catch (error: any) { + logger.error("์ „์ฒด ๋™๊ธฐํ™” ์‹คํŒจ", { error: error.message }); + result.success = false; + return result; + } +} + diff --git a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx index 106870eb..cbd74337 100644 --- a/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx +++ b/frontend/app/(main)/admin/screenMng/screenMngList/page.tsx @@ -111,10 +111,15 @@ export default function ScreenManagementPage() { }; // ๊ฒ€์ƒ‰์–ด๋กœ ํ•„ํ„ฐ๋ง๋œ ํ™”๋ฉด - const filteredScreens = screens.filter((screen) => - screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) || - screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase()) - ); + // ๊ฒ€์ƒ‰์–ด๊ฐ€ ์—ฌ๋Ÿฌ ํ‚ค์›Œ๋“œ(ํด๋” ๊ณ„์ธต ๊ฒ€์ƒ‰)์ด๋ฉด ํ™”๋ฉด ํ•„ํ„ฐ๋ง ์—†์ด ๋ชจ๋“  ํ™”๋ฉด ํ‘œ์‹œ + // ๋‹จ์ผ ํ‚ค์›Œ๋“œ๋ฉด ํ•ด๋‹น ํ‚ค์›Œ๋“œ๋กœ ํ™”๋ฉด ํ•„ํ„ฐ๋ง + const searchKeywords = searchTerm.toLowerCase().trim().split(/\s+/).filter(Boolean); + const filteredScreens = searchKeywords.length > 1 + ? screens // ํด๋” ๊ณ„์ธต ๊ฒ€์ƒ‰ ์‹œ์—๋Š” ํ™”๋ฉด ํ•„ํ„ฐ๋ง ์—†์Œ (ํด๋”์—์„œ ์ด๋ฏธ ํ•„ํ„ฐ๋ง๋จ) + : screens.filter((screen) => + screen.screenName.toLowerCase().includes(searchTerm.toLowerCase()) || + screen.screenCode.toLowerCase().includes(searchTerm.toLowerCase()) + ); // ํ™”๋ฉด ์„ค๊ณ„ ๋ชจ๋“œ์ผ ๋•Œ๋Š” ๋ ˆ์ด์•„์›ƒ ์—†์ด ์ „์ฒด ํ™”๋ฉด ์‚ฌ์šฉ if (isDesignMode) { @@ -183,6 +188,7 @@ export default function ScreenManagementPage() { selectedScreen={selectedScreen} onScreenSelect={handleScreenSelect} onScreenDesign={handleDesignScreen} + searchTerm={searchTerm} onGroupSelect={(group) => { setSelectedGroup(group); setSelectedScreen(null); // ํ™”๋ฉด ์„ ํƒ ํ•ด์ œ diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx index 5590cef4..a5207b96 100644 --- a/frontend/components/screen/CopyScreenModal.tsx +++ b/frontend/components/screen/CopyScreenModal.tsx @@ -927,7 +927,7 @@ export default function CopyScreenModal({ if (mode === "group") { return ( - + {/* ๋กœ๋”ฉ ์˜ค๋ฒ„๋ ˆ์ด */} {isCopying && (
diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index b171bbe1..edd36816 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { cn } from "@/lib/utils"; import { ChevronRight, @@ -16,6 +16,8 @@ import { Copy, FolderTree, Loader2, + RefreshCw, + Building2, } from "lucide-react"; import { ScreenDefinition } from "@/types/screen"; import { @@ -24,9 +26,17 @@ import { deleteScreenGroup, addScreenToGroup, removeScreenFromGroup, + getMenuScreenSyncStatus, + syncScreenGroupsToMenu, + syncMenuToScreenGroups, + syncAllCompanies, + SyncStatus, + AllCompaniesSyncResult, } from "@/lib/api/screenGroup"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { useAuth } from "@/hooks/useAuth"; +import { getCompanyList, Company } from "@/lib/api/company"; import { DropdownMenu, DropdownMenuContent, @@ -88,6 +98,7 @@ interface ScreenGroupTreeViewProps { onGroupSelect?: (group: { id: number; name: string; company_code?: string } | null) => void; onScreenSelectInGroup?: (group: { id: number; name: string; company_code?: string }, screenId: number) => void; companyCode?: string; + searchTerm?: string; // ๊ฒ€์ƒ‰์–ด (๋„์–ด์“ฐ๊ธฐ๋กœ ๊ตฌ๋ถ„๋œ ์—ฌ๋Ÿฌ ํ‚ค์›Œ๋“œ) } interface TreeNode { @@ -107,6 +118,7 @@ export function ScreenGroupTreeView({ onGroupSelect, onScreenSelectInGroup, companyCode, + searchTerm = "", }: ScreenGroupTreeViewProps) { const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(true); @@ -155,6 +167,24 @@ export function ScreenGroupTreeView({ const [contextMenuGroup, setContextMenuGroup] = useState(null); const [contextMenuGroupPosition, setContextMenuGroupPosition] = useState<{ x: number; y: number } | null>(null); + // ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” ์ƒํƒœ + const [isSyncDialogOpen, setIsSyncDialogOpen] = useState(false); + const [syncStatus, setSyncStatus] = useState(null); + const [isSyncing, setIsSyncing] = useState(false); + const [syncDirection, setSyncDirection] = useState<"screen-to-menu" | "menu-to-screen" | "all" | null>(null); + + // ํšŒ์‚ฌ ์„ ํƒ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž์šฉ) + const { user } = useAuth(); + const [companies, setCompanies] = useState([]); + const [selectedCompanyCode, setSelectedCompanyCode] = useState(""); + const [isSyncCompanySelectOpen, setIsSyncCompanySelectOpen] = useState(false); + + // ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ตœ๊ณ  ๊ด€๋ฆฌ์ž์ธ์ง€ ํ™•์ธ + const isSuperAdmin = user?.companyCode === "*"; + + // ์‹ค์ œ ์‚ฌ์šฉํ•  ํšŒ์‚ฌ ์ฝ”๋“œ (props โ†’ ์„ ํƒ โ†’ ์‚ฌ์šฉ์ž ๊ธฐ๋ณธ๊ฐ’) + const effectiveCompanyCode = companyCode || selectedCompanyCode || (isSuperAdmin ? "" : user?.companyCode) || ""; + // ๊ทธ๋ฃน ๋ชฉ๋ก ๋ฐ ๊ทธ๋ฃน๋ณ„ ํ™”๋ฉด ๋กœ๋“œ useEffect(() => { loadGroupsData(); @@ -242,6 +272,124 @@ export function ScreenGroupTreeView({ setIsGroupModalOpen(true); }; + // ๋™๊ธฐํ™” ๋‹ค์ด์–ผ๋กœ๊ทธ ์—ด๊ธฐ + const handleOpenSyncDialog = async () => { + setIsSyncDialogOpen(true); + setSyncStatus(null); + setSyncDirection(null); + setSelectedCompanyCode(""); + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž์ผ ๋•Œ ํšŒ์‚ฌ ๋ชฉ๋ก ๋กœ๋“œ + if (isSuperAdmin && companies.length === 0) { + try { + const companiesList = await getCompanyList(); + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž(*)์šฉ ํšŒ์‚ฌ๋Š” ์ œ์™ธ + const filteredCompanies = companiesList.filter(c => c.company_code !== "*"); + setCompanies(filteredCompanies); + } catch (error) { + console.error("ํšŒ์‚ฌ ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + } + } + + // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ์•„๋‹ˆ๋ฉด ๋ฐ”๋กœ ์ƒํƒœ ์กฐํšŒ + if (!isSuperAdmin && user?.companyCode) { + const response = await getMenuScreenSyncStatus(user.companyCode); + if (response.success && response.data) { + setSyncStatus(response.data); + } + } + }; + + // ํšŒ์‚ฌ ์„ ํƒ ์‹œ ์ƒํƒœ ์กฐํšŒ + const handleCompanySelect = async (companyCode: string) => { + setSelectedCompanyCode(companyCode); + setIsSyncCompanySelectOpen(false); + setSyncStatus(null); + + if (companyCode) { + const response = await getMenuScreenSyncStatus(companyCode); + if (response.success && response.data) { + setSyncStatus(response.data); + } else { + toast.error(response.error || "๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ ์‹คํŒจ"); + } + } + }; + + // ๋™๊ธฐํ™” ์‹คํ–‰ + const handleSync = async (direction: "screen-to-menu" | "menu-to-screen") => { + // ์‚ฌ์šฉํ•  ํšŒ์‚ฌ ์ฝ”๋“œ ๊ฒฐ์ • + const targetCompanyCode = isSuperAdmin ? selectedCompanyCode : user?.companyCode; + + if (!targetCompanyCode) { + toast.error("ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”."); + return; + } + + setIsSyncing(true); + setSyncDirection(direction); + + try { + const response = direction === "screen-to-menu" + ? await syncScreenGroupsToMenu(targetCompanyCode) + : await syncMenuToScreenGroups(targetCompanyCode); + + if (response.success) { + const data = response.data; + toast.success( + `๋™๊ธฐํ™” ์™„๋ฃŒ: ์ƒ์„ฑ ${data?.created || 0}๊ฐœ, ์—ฐ๊ฒฐ ${data?.linked || 0}๊ฐœ, ์Šคํ‚ต ${data?.skipped || 0}๊ฐœ` + ); + // ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ + await loadGroupsData(); + // ๋™๊ธฐํ™” ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ + const statusResponse = await getMenuScreenSyncStatus(targetCompanyCode); + if (statusResponse.success && statusResponse.data) { + setSyncStatus(statusResponse.data); + } + } else { + toast.error(`๋™๊ธฐํ™” ์‹คํŒจ: ${response.error || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); + } + } catch (error: any) { + toast.error(`๋™๊ธฐํ™” ์‹คํŒจ: ${error.message}`); + } finally { + setIsSyncing(false); + setSyncDirection(null); + } + }; + + // ์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) + const handleSyncAll = async () => { + if (!isSuperAdmin) { + toast.error("์ „์ฒด ๋™๊ธฐํ™”๋Š” ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."); + return; + } + + setIsSyncing(true); + setSyncDirection("all"); + + try { + const response = await syncAllCompanies(); + + if (response.success && response.data) { + const data = response.data; + toast.success( + `์ „์ฒด ๋™๊ธฐํ™” ์™„๋ฃŒ: ${data.totalCompanies}๊ฐœ ํšŒ์‚ฌ, ์ƒ์„ฑ ${data.totalCreated}๊ฐœ, ์—ฐ๊ฒฐ ${data.totalLinked}๊ฐœ` + ); + // ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ + await loadGroupsData(); + // ๋™๊ธฐํ™” ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ + setIsSyncDialogOpen(false); + } else { + toast.error(`์ „์ฒด ๋™๊ธฐํ™” ์‹คํŒจ: ${response.error || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); + } + } catch (error: any) { + toast.error(`์ „์ฒด ๋™๊ธฐํ™” ์‹คํŒจ: ${error.message}`); + } finally { + setIsSyncing(false); + setSyncDirection(null); + } + }; + // ๊ทธ๋ฃน ์ˆ˜์ • ๋ฒ„ํŠผ ํด๋ฆญ const handleEditGroup = (group: ScreenGroup, e: React.MouseEvent) => { e.stopPropagation(); @@ -596,6 +744,191 @@ export function ScreenGroupTreeView({ return result; }; + // ๊ฒ€์ƒ‰์–ด๋กœ ๊ทธ๋ฃน ํ•„ํ„ฐ๋ง (๋„์–ด์“ฐ๊ธฐ๋กœ ๊ตฌ๋ถ„๋œ ์—ฌ๋Ÿฌ ํ‚ค์›Œ๋“œ - ๊ณ„์ธต์  ๊ฒ€์ƒ‰) + const getFilteredGroups = useMemo(() => { + if (!searchTerm.trim()) { + return groups; // ๊ฒ€์ƒ‰์–ด๊ฐ€ ์—†์œผ๋ฉด ๋ชจ๋“  ๊ทธ๋ฃน ๋ฐ˜ํ™˜ + } + + // ๊ฒ€์ƒ‰์–ด๋ฅผ ๋„์–ด์“ฐ๊ธฐ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ  ๋นˆ ๋ฌธ์ž์—ด ์ œ๊ฑฐ + const keywords = searchTerm.toLowerCase().trim().split(/\s+/).filter(k => k.length > 0); + + if (keywords.length === 0) { + return groups; + } + + // ๊ทธ๋ฃน์˜ ์กฐ์ƒ ID๋“ค์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ + const getAncestorIds = (groupId: number): Set => { + const ancestors = new Set(); + let current = groups.find(g => g.id === groupId); + while (current?.parent_group_id) { + ancestors.add(current.parent_group_id); + current = groups.find(g => g.id === current!.parent_group_id); + } + return ancestors; + }; + + // ์ฒซ ๋ฒˆ์งธ ํ‚ค์›Œ๋“œ์™€ ์ผ์น˜ํ•˜๋Š” ๊ทธ๋ฃน ์ฐพ๊ธฐ + let currentMatchingIds = new Set(); + for (const group of groups) { + const groupName = group.group_name.toLowerCase(); + if (groupName.includes(keywords[0])) { + currentMatchingIds.add(group.id); + } + } + + // ์ผ์น˜ํ•˜๋Š” ๊ทธ๋ฃน์ด ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด ๋ฐ˜ํ™˜ + if (currentMatchingIds.size === 0) { + return []; + } + + // ๋‚˜๋จธ์ง€ ํ‚ค์›Œ๋“œ๋“ค์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฒ˜๋ฆฌ (๊ณ„์ธต์  ๊ฒ€์ƒ‰) + for (let i = 1; i < keywords.length; i++) { + const keyword = keywords[i]; + const nextMatchingIds = new Set(); + + for (const group of groups) { + const groupName = group.group_name.toLowerCase(); + if (groupName.includes(keyword)) { + // ์ด ๊ทธ๋ฃน์˜ ์กฐ์ƒ ์ค‘์— ์ด์ „ ํ‚ค์›Œ๋“œ์™€ ์ผ์น˜ํ•˜๋Š” ๊ทธ๋ฃน์ด ์žˆ๋Š”์ง€ ํ™•์ธ + const ancestors = getAncestorIds(group.id); + const hasMatchingAncestor = Array.from(currentMatchingIds).some(id => + ancestors.has(id) || id === group.id + ); + + if (hasMatchingAncestor) { + nextMatchingIds.add(group.id); + } + } + } + + // ๋งค์นญ๋˜๋Š” ๊ฒŒ ์žˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ, ์—†์œผ๋ฉด ์ด์ „ ๊ฒฐ๊ณผ ์œ ์ง€ + if (nextMatchingIds.size > 0) { + // ์ด์ „ ํ‚ค์›Œ๋“œ ๋งค์นญ๋„ ์œ ์ง€ (์ƒ์œ„ ํด๋” ํ‘œ์‹œ๋ฅผ ์œ„ํ•ด) + nextMatchingIds.forEach(id => currentMatchingIds.add(id)); + currentMatchingIds = nextMatchingIds; + } + } + + // ์ตœ์ข… ๋งค์นญ ๊ฒฐ๊ณผ + const finalMatchingIds = currentMatchingIds; + + // ํ‘œ์‹œํ•  ๊ทธ๋ฃน ID ์ง‘ํ•ฉ + const groupsToShow = new Set(); + + // ์ผ์น˜ํ•˜๋Š” ๊ทธ๋ฃน์˜ ์ƒ์œ„ ๊ทธ๋ฃน๋“ค๋„ ํฌํ•จ (๊ณ„์ธต ์œ ์ง€๋ฅผ ์œ„ํ•ด) + const addParents = (groupId: number) => { + const group = groups.find(g => g.id === groupId); + if (group) { + groupsToShow.add(group.id); + if (group.parent_group_id) { + addParents(group.parent_group_id); + } + } + }; + + // ํ•˜์œ„ ๊ทธ๋ฃน๋“ค์„ ์ถ”๊ฐ€ํ•˜๋Š” ํ•จ์ˆ˜ + const addChildren = (groupId: number) => { + const children = groups.filter(g => g.parent_group_id === groupId); + for (const child of children) { + groupsToShow.add(child.id); + addChildren(child.id); + } + }; + + // ์ตœ์ข… ๋งค์นญ ๊ทธ๋ฃน๋“ค์˜ ์ƒ์œ„ ์ถ”๊ฐ€ + for (const groupId of finalMatchingIds) { + addParents(groupId); + } + + // ๋งˆ์ง€๋ง‰ ํ‚ค์›Œ๋“œ์™€ ์ผ์น˜ํ•˜๋Š” ๊ทธ๋ฃน์˜ ํ•˜์œ„๋งŒ ์ถ”๊ฐ€ + for (const groupId of finalMatchingIds) { + addChildren(groupId); + } + + // ํ•„ํ„ฐ๋ง๋œ ๊ทธ๋ฃน๋งŒ ๋ฐ˜ํ™˜ + return groups.filter(g => groupsToShow.has(g.id)); + }, [groups, searchTerm]); + + // ๊ฒ€์ƒ‰ ์‹œ ํ•ด๋‹น ๊ทธ๋ฃน์ด ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธ (ํ•˜์ด๋ผ์ดํŠธ์šฉ) + const isGroupMatchingSearch = (groupName: string): boolean => { + if (!searchTerm.trim()) return false; + const keywords = searchTerm.toLowerCase().trim().split(/\s+/).filter(k => k.length > 0); + const name = groupName.toLowerCase(); + return keywords.some(keyword => name.includes(keyword)); + }; + + // ๊ฒ€์ƒ‰ ์‹œ ํ•ด๋‹น ๊ทธ๋ฃน์ด ์ž๋™์œผ๋กœ ํŽผ์ณ์ ธ์•ผ ํ•˜๋Š”์ง€ ํ™•์ธ + // (๊ฒ€์ƒ‰์–ด์™€ ์ผ์น˜ํ•˜๋Š” ๊ทธ๋ฃน์˜ ์ƒ์œ„ + ๋งˆ์ง€๋ง‰ ๊ฒ€์ƒ‰์–ด์™€ ์ผ์น˜ํ•˜๋Š” ๊ทธ๋ฃน๋„ ์ž๋™ ํŽผ์นจ) + const shouldAutoExpandForSearch = useMemo(() => { + if (!searchTerm.trim()) return new Set(); + + const keywords = searchTerm.toLowerCase().trim().split(/\s+/).filter(k => k.length > 0); + if (keywords.length === 0) return new Set(); + + // ๊ทธ๋ฃน์˜ ์กฐ์ƒ ID๋“ค์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ + const getAncestorIds = (groupId: number): Set => { + const ancestors = new Set(); + let current = groups.find(g => g.id === groupId); + while (current?.parent_group_id) { + ancestors.add(current.parent_group_id); + current = groups.find(g => g.id === current!.parent_group_id); + } + return ancestors; + }; + + // ๊ณ„์ธต์  ๊ฒ€์ƒ‰์œผ๋กœ ์ตœ์ข… ์ผ์น˜ ๊ทธ๋ฃน ์ฐพ๊ธฐ (getFilteredGroups์™€ ๋™์ผํ•œ ๋กœ์ง) + let currentMatchingIds = new Set(); + for (const group of groups) { + const groupName = group.group_name.toLowerCase(); + if (groupName.includes(keywords[0])) { + currentMatchingIds.add(group.id); + } + } + + for (let i = 1; i < keywords.length; i++) { + const keyword = keywords[i]; + const nextMatchingIds = new Set(); + + for (const group of groups) { + const groupName = group.group_name.toLowerCase(); + if (groupName.includes(keyword)) { + const ancestors = getAncestorIds(group.id); + const hasMatchingAncestor = Array.from(currentMatchingIds).some(id => + ancestors.has(id) || id === group.id + ); + + if (hasMatchingAncestor) { + nextMatchingIds.add(group.id); + } + } + } + + if (nextMatchingIds.size > 0) { + nextMatchingIds.forEach(id => currentMatchingIds.add(id)); + currentMatchingIds = nextMatchingIds; + } + } + + // ์ž๋™ ํŽผ์นจ ๋Œ€์ƒ: ์ผ์น˜ ๊ทธ๋ฃน์˜ ์ƒ์œ„ + ์ผ์น˜ ๊ทธ๋ฃน ์ž์ฒด + const autoExpandIds = new Set(); + + const addParents = (groupId: number) => { + const group = groups.find(g => g.id === groupId); + if (group?.parent_group_id) { + autoExpandIds.add(group.parent_group_id); + addParents(group.parent_group_id); + } + }; + + for (const groupId of currentMatchingIds) { + autoExpandIds.add(groupId); // ์ผ์น˜ํ•˜๋Š” ๊ทธ๋ฃน ์ž์ฒด๋„ ํŽผ์นจ (ํ™”๋ฉด ํ‘œ์‹œ๋ฅผ ์œ„ํ•ด) + addParents(groupId); + } + + return autoExpandIds; + }, [groups, searchTerm]); + // ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ const loadGroupsData = async () => { try { @@ -635,8 +968,8 @@ export function ScreenGroupTreeView({ return (
- {/* ๊ทธ๋ฃน ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} -
+ {/* ๊ทธ๋ฃน ์ถ”๊ฐ€ & ๋™๊ธฐํ™” ๋ฒ„ํŠผ */} +
+
{/* ํŠธ๋ฆฌ ๋ชฉ๋ก */}
+ {/* ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ ํ‘œ์‹œ */} + {searchTerm.trim() && getFilteredGroups.length === 0 && ( +
+ "{searchTerm}"์™€ ์ผ์น˜ํ•˜๋Š” ํด๋”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค +
+ )} + {/* ๊ทธ๋ฃนํ™”๋œ ํ™”๋ฉด๋“ค (๋Œ€๋ถ„๋ฅ˜๋งŒ ๋จผ์ € ๋ Œ๋”๋ง) */} - {groups + {getFilteredGroups .filter((g) => !(g as any).parent_group_id) // ๋Œ€๋ถ„๋ฅ˜๋งŒ (parent_group_id๊ฐ€ null) .map((group) => { const groupId = String(group.id); - const isExpanded = expandedGroups.has(groupId); + const isExpanded = expandedGroups.has(groupId) || shouldAutoExpandForSearch.has(group.id); // ๊ฒ€์ƒ‰ ์‹œ ์ƒ์œ„ ๊ทธ๋ฃน๋งŒ ์ž๋™ ํ™•์žฅ const groupScreens = getScreensInGroup(group.id); + const isMatching = isGroupMatchingSearch(group.group_name); // ๊ฒ€์ƒ‰์–ด ์ผ์น˜ ์—ฌ๋ถ€ - // ํ•˜์œ„ ๊ทธ๋ฃน๋“ค ์ฐพ๊ธฐ - const childGroups = groups.filter((g) => (g as any).parent_group_id === group.id); + // ํ•˜์œ„ ๊ทธ๋ฃน๋“ค ์ฐพ๊ธฐ (ํ•„ํ„ฐ๋ง๋œ ๊ทธ๋ฃน์—์„œ๋งŒ) + const childGroups = getFilteredGroups.filter((g) => (g as any).parent_group_id === group.id); return (
@@ -667,7 +1017,8 @@ export function ScreenGroupTreeView({
toggleGroup(groupId)} onContextMenu={(e) => handleGroupContextMenu(e, group)} @@ -682,7 +1033,7 @@ export function ScreenGroupTreeView({ ) : ( )} - {group.group_name} + {group.group_name} {groupScreens.length} @@ -719,11 +1070,12 @@ export function ScreenGroupTreeView({
{childGroups.map((childGroup) => { const childGroupId = String(childGroup.id); - const isChildExpanded = expandedGroups.has(childGroupId); + const isChildExpanded = expandedGroups.has(childGroupId) || shouldAutoExpandForSearch.has(childGroup.id); // ๊ฒ€์ƒ‰ ์‹œ ์ƒ์œ„ ๊ทธ๋ฃน๋งŒ ์ž๋™ ํ™•์žฅ const childScreens = getScreensInGroup(childGroup.id); + const isChildMatching = isGroupMatchingSearch(childGroup.group_name); - // ์†์ž ๊ทธ๋ฃน๋“ค (3๋‹จ๊ณ„) - const grandChildGroups = groups.filter((g) => (g as any).parent_group_id === childGroup.id); + // ์†์ž ๊ทธ๋ฃน๋“ค (3๋‹จ๊ณ„) - ํ•„ํ„ฐ๋ง๋œ ๊ทธ๋ฃน์—์„œ๋งŒ + const grandChildGroups = getFilteredGroups.filter((g) => (g as any).parent_group_id === childGroup.id); return (
@@ -731,7 +1083,8 @@ export function ScreenGroupTreeView({
toggleGroup(childGroupId)} onContextMenu={(e) => handleGroupContextMenu(e, childGroup)} @@ -746,7 +1099,7 @@ export function ScreenGroupTreeView({ ) : ( )} - {childGroup.group_name} + {childGroup.group_name} {childScreens.length} @@ -782,8 +1135,9 @@ export function ScreenGroupTreeView({
{grandChildGroups.map((grandChild) => { const grandChildId = String(grandChild.id); - const isGrandExpanded = expandedGroups.has(grandChildId); + const isGrandExpanded = expandedGroups.has(grandChildId) || shouldAutoExpandForSearch.has(grandChild.id); // ๊ฒ€์ƒ‰ ์‹œ ์ƒ์œ„ ๊ทธ๋ฃน๋งŒ ์ž๋™ ํ™•์žฅ const grandScreens = getScreensInGroup(grandChild.id); + const isGrandMatching = isGroupMatchingSearch(grandChild.group_name); return (
@@ -791,7 +1145,8 @@ export function ScreenGroupTreeView({
toggleGroup(grandChildId)} onContextMenu={(e) => handleGroupContextMenu(e, grandChild)} @@ -806,7 +1161,7 @@ export function ScreenGroupTreeView({ ) : ( )} - {grandChild.group_name} + {grandChild.group_name} {grandScreens.length} @@ -1459,6 +1814,206 @@ export function ScreenGroupTreeView({ )} + {/* ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” ๋‹ค์ด์–ผ๋กœ๊ทธ */} + + + + ๋ฉ”๋‰ด-ํ™”๋ฉด ๋™๊ธฐํ™” + + ํ™”๋ฉด๊ด€๋ฆฌ์˜ ํด๋” ๊ตฌ์กฐ์™€ ๋ฉ”๋‰ด๊ด€๋ฆฌ๋ฅผ ์—ฐ๋™ํ•ฉ๋‹ˆ๋‹ค. + + + + {/* ์ตœ๊ณ  ๊ด€๋ฆฌ์ž: ํšŒ์‚ฌ ์„ ํƒ */} + {isSuperAdmin && ( +
+ + + + + + + + + + ํšŒ์‚ฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {companies.map((company) => ( + handleCompanySelect(company.company_code)} + className="text-sm" + > + + {company.company_name} + + ))} + + + + + +
+ )} + + {/* ํ˜„์žฌ ์ƒํƒœ ํ‘œ์‹œ */} + {syncStatus ? ( +
+
+
+
ํ™”๋ฉด๊ด€๋ฆฌ
+
{syncStatus.screenGroups.total}๊ฐœ
+
+ ์—ฐ๊ฒฐ๋จ: {syncStatus.screenGroups.linked} / ๋ฏธ์—ฐ๊ฒฐ: {syncStatus.screenGroups.unlinked} +
+
+
+
์‚ฌ์šฉ์ž ๋ฉ”๋‰ด
+
{syncStatus.menuItems.total}๊ฐœ
+
+ ์—ฐ๊ฒฐ๋จ: {syncStatus.menuItems.linked} / ๋ฏธ์—ฐ๊ฒฐ: {syncStatus.menuItems.unlinked} +
+
+
+ + {syncStatus.potentialMatches.length > 0 && ( +
+
์ž๋™ ๋งค์นญ ๊ฐ€๋Šฅ ({syncStatus.potentialMatches.length}๊ฐœ)
+
+ {syncStatus.potentialMatches.slice(0, 5).map((match, i) => ( +
+ {match.menuName} = {match.groupName} +
+ ))} + {syncStatus.potentialMatches.length > 5 && ( +
...์™ธ {syncStatus.potentialMatches.length - 5}๊ฐœ
+ )} +
+
+ )} + + {/* ๋™๊ธฐํ™” ๋ฒ„ํŠผ */} +
+ + +
+ + {/* ์ „์ฒด ๋™๊ธฐํ™” (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) */} + {isSuperAdmin && ( +
+ +
+ )} +
+ ) : isSuperAdmin && !selectedCompanyCode ? ( +
+ +

+ ๊ฐœ๋ณ„ ํšŒ์‚ฌ ๋™๊ธฐํ™”๋ฅผ ํ•˜๋ ค๋ฉด ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•ด์ฃผ์„ธ์š”. +

+ + {/* ์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” ๋ฒ„ํŠผ (ํšŒ์‚ฌ ์„ ํƒ ์—†์ด๋„ ํ‘œ์‹œ) */} +
+ +
+
+ ) : ( +
+ +
+ )} + + + + +
+
+
); } \ No newline at end of file diff --git a/frontend/components/screen/panels/DataFlowPanel.tsx b/frontend/components/screen/panels/DataFlowPanel.tsx index 4b31d1a9..1fdf0ec8 100644 --- a/frontend/components/screen/panels/DataFlowPanel.tsx +++ b/frontend/components/screen/panels/DataFlowPanel.tsx @@ -462,3 +462,4 @@ export default function DataFlowPanel({ groupId, screenId, screens = [] }: DataF + diff --git a/frontend/components/screen/panels/FieldJoinPanel.tsx b/frontend/components/screen/panels/FieldJoinPanel.tsx index 884ac69b..29891228 100644 --- a/frontend/components/screen/panels/FieldJoinPanel.tsx +++ b/frontend/components/screen/panels/FieldJoinPanel.tsx @@ -414,3 +414,4 @@ export default function FieldJoinPanel({ screenId, componentId, layoutId }: Fiel + diff --git a/frontend/lib/api/screenGroup.ts b/frontend/lib/api/screenGroup.ts index 65294444..0a91f907 100644 --- a/frontend/lib/api/screenGroup.ts +++ b/frontend/lib/api/screenGroup.ts @@ -498,3 +498,97 @@ export async function getScreenSubTables( } } + +// ============================================================ +// ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” API +// ============================================================ + +export interface SyncDetail { + action: 'created' | 'linked' | 'skipped' | 'error'; + sourceName: string; + sourceId: number | string; + targetId?: number | string; + reason?: string; +} + +export interface SyncResult { + success: boolean; + created: number; + linked: number; + skipped: number; + errors: string[]; + details: SyncDetail[]; +} + +export interface SyncStatus { + screenGroups: { total: number; linked: number; unlinked: number }; + menuItems: { total: number; linked: number; unlinked: number }; + potentialMatches: Array<{ menuName: string; groupName: string; similarity: string }>; +} + +// ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ +export async function getMenuScreenSyncStatus( + targetCompanyCode?: string +): Promise> { + try { + const queryParams = targetCompanyCode ? `?targetCompanyCode=${targetCompanyCode}` : ''; + const response = await apiClient.get(`/screen-groups/sync/status${queryParams}`); + return response.data; + } catch (error: any) { + return { success: false, error: error.message }; + } +} + +// ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” +export async function syncScreenGroupsToMenu( + targetCompanyCode?: string +): Promise> { + try { + const response = await apiClient.post("/screen-groups/sync/screen-to-menu", { targetCompanyCode }); + return response.data; + } catch (error: any) { + return { success: false, error: error.message }; + } +} + +// ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” +export async function syncMenuToScreenGroups( + targetCompanyCode?: string +): Promise> { + try { + const response = await apiClient.post("/screen-groups/sync/menu-to-screen", { targetCompanyCode }); + return response.data; + } catch (error: any) { + return { success: false, error: error.message }; + } +} + +// ์ „์ฒด ๋™๊ธฐํ™” ๊ฒฐ๊ณผ ํƒ€์ž… +export interface AllCompaniesSyncResult { + totalCompanies: number; + successCount: number; + failedCount: number; + totalCreated: number; + totalLinked: number; + details: Array<{ + companyCode: string; + companyName: string; + direction: 'screens-to-menus' | 'menus-to-screens'; + created: number; + linked: number; + skipped: number; + success: boolean; + error?: string; + }>; +} + +// ์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) +export async function syncAllCompanies(): Promise> { + try { + const response = await apiClient.post("/screen-groups/sync/all"); + return response.data; + } catch (error: any) { + return { success: false, error: error.message }; + } +} + From 02eee979ea306061a118bb0c491e6e013e2ce3f0 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 15:17:49 +0900 Subject: [PATCH 04/16] =?UTF-8?q?=EA=B3=A0=EC=B9=98=EA=B8=B0=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridComponent.tsx | 53 ++++++++--- .../pivot-grid/components/FieldPanel.tsx | 88 ++++++++++++++++--- 2 files changed, 117 insertions(+), 24 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index ccfbde88..c30472d8 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -432,10 +432,20 @@ export const PivotGridComponent: React.FC = ({ // ํ•„ํ„ฐ ์˜์—ญ ํ•„๋“œ const filterFields = useMemo( - () => - fields + () => { + const result = fields .filter((f) => f.area === "filter" && f.visible !== false) - .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)), + .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)); + + console.log("๐Ÿ”ท [filterFields] ํ•„ํ„ฐ ํ•„๋“œ ๊ณ„์‚ฐ:", { + totalFields: fields.length, + filterFieldsCount: result.length, + filterFieldNames: result.map(f => f.field), + allFieldAreas: fields.map(f => ({ field: f.field, area: f.area, visible: f.visible })), + }); + + return result; + }, [fields] ); @@ -715,7 +725,15 @@ export const PivotGridComponent: React.FC = ({ // ํ•„๋“œ ๋ณ€๊ฒฝ const handleFieldsChange = useCallback( (newFields: PivotFieldConfig[]) => { + console.log("๐Ÿ”ท [handleFieldsChange] ํ•„๋“œ ๋ณ€๊ฒฝ:", { + totalFields: newFields.length, + filterFields: newFields.filter(f => f.area === "filter").length, + filterFieldNames: newFields.filter(f => f.area === "filter").map(f => f.field), + changedFields: newFields.filter(f => f.area === "filter"), + }); + console.log("๐Ÿ”ท [handleFieldsChange] setFields ํ˜ธ์ถœ ์ „"); setFields(newFields); + console.log("๐Ÿ”ท [handleFieldsChange] setFields ํ˜ธ์ถœ ํ›„"); }, [] ); @@ -1023,10 +1041,12 @@ export const PivotGridComponent: React.FC = ({ console.log("ํ”ผ๋ฒ— ์ƒํƒœ๊ฐ€ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); }, [saveStateToStorage]); - // ์ƒํƒœ ์ดˆ๊ธฐํ™” + // ์ƒํƒœ ์ดˆ๊ธฐํ™” (ํ™•์žฅ/์ถ•์†Œ, ์ •๋ ฌ, ํ•„ํ„ฐ๋งŒ ์ดˆ๊ธฐํ™”, ํ•„๋“œ ์„ค์ •์€ ์œ ์ง€) const handleResetState = useCallback(() => { + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ƒํƒœ ์ œ๊ฑฐ localStorage.removeItem(stateStorageKey); - setFields(initialFields); + + // ํ™•์žฅ/์ถ•์†Œ, ์ •๋ ฌ, ํ•„ํ„ฐ ์ƒํƒœ๋งŒ ์ดˆ๊ธฐํ™” setPivotState({ expandedRowPaths: [], expandedColumnPaths: [], @@ -1037,7 +1057,10 @@ export const PivotGridComponent: React.FC = ({ setColumnWidths({}); setSelectedCell(null); setSelectionRange(null); - }, [stateStorageKey, initialFields]); + + // ๐Ÿ†• ํ•„๋“œ ์„ค์ •์€ ์œ ์ง€ (initialFields๋กœ ๋˜๋Œ๋ฆฌ์ง€ ์•Š์Œ) + console.log("๐Ÿ”ท ํ”ผ๋ฒ— ์ƒํƒœ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค (ํ•„๋“œ ์„ค์ •์€ ์œ ์ง€)"); + }, [stateStorageKey]); // ํ•„๋“œ ์ˆจ๊ธฐ๊ธฐ/ํ‘œ์‹œ ์ƒํƒœ const [hiddenFields, setHiddenFields] = useState>(new Set()); @@ -1617,19 +1640,25 @@ export const PivotGridComponent: React.FC = ({ } /> diff --git a/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx b/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx index fed43afb..08dca70e 100644 --- a/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx +++ b/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx @@ -25,6 +25,7 @@ import { horizontalListSortingStrategy, useSortable, } from "@dnd-kit/sortable"; +import { useDroppable } from "@dnd-kit/core"; import { CSS } from "@dnd-kit/utilities"; import { cn } from "@/lib/utils"; import { PivotFieldConfig, PivotAreaType } from "../types"; @@ -244,22 +245,31 @@ const DroppableArea: React.FC = ({ const areaFields = fields.filter((f) => f.area === area && f.visible !== false); const fieldIds = areaFields.map((f) => `${area}-${f.field}`); + // ๐Ÿ†• ๋“œ๋กญ ๊ฐ€๋Šฅ ์˜์—ญ ์„ค์ • + const { setNodeRef, isOver: isOverDroppable } = useDroppable({ + id: area, // "filter", "column", "row", "data" + }); + + const finalIsOver = isOver || isOverDroppable; + return (
{/* ์˜์—ญ ํ—ค๋” */} -
+
{icon} {title} {areaFields.length > 0 && ( - + {areaFields.length} )} @@ -267,11 +277,16 @@ const DroppableArea: React.FC = ({ {/* ํ•„๋“œ ๋ชฉ๋ก */} -
+
{areaFields.length === 0 ? ( - - ํ•„๋“œ๋ฅผ ์—ฌ๊ธฐ๋กœ ๋“œ๋ž˜๊ทธ - +
+ + โ† ํ•„๋“œ๋ฅผ ์—ฌ๊ธฐ๋กœ ๋“œ๋ž˜๊ทธํ•˜์„ธ์š” + +
) : ( areaFields.map((field) => ( = ({ return; } - // ๋“œ๋กญ ์˜์—ญ ๊ฐ์ง€ + // ๋“œ๋กญ ์˜์—ญ ๊ฐ์ง€ (์˜์—ญ ์ž์ฒด์˜ ID๋ฅผ ์šฐ์„  ํ™•์ธ) const overId = over.id as string; + + // 1. overId๊ฐ€ ์˜์—ญ ์ž์ฒด์ธ ๊ฒฝ์šฐ (filter, column, row, data) + if (["filter", "column", "row", "data"].includes(overId)) { + setOverArea(overId as PivotAreaType); + console.log("๐Ÿ”ท [handleDragOver] ์˜์—ญ ๊ฐ์ง€:", overId); + return; + } + + // 2. overId๊ฐ€ ํ•„๋“œ์ธ ๊ฒฝ์šฐ (์˜ˆ: row-part_name) const targetArea = overId.split("-")[0] as PivotAreaType; if (["filter", "column", "row", "data"].includes(targetArea)) { setOverArea(targetArea); + console.log("๐Ÿ”ท [handleDragOver] ํ•„๋“œ ์˜์—ญ ๊ฐ์ง€:", targetArea); } }; // ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; + const currentOverArea = overArea; // handleDragOver์—์„œ ๊ฐ์ง€ํ•œ ์˜์—ญ ์ €์žฅ setActiveId(null); setOverArea(null); - if (!over) return; + if (!over) { + console.log("๐Ÿ”ท [FieldPanel] ๋“œ๋กญ ๋Œ€์ƒ ์—†์Œ"); + return; + } const activeId = active.id as string; const overId = over.id as string; + console.log("๐Ÿ”ท [FieldPanel] ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ:", { + activeId, + overId, + detectedOverArea: currentOverArea, + }); + // ํ•„๋“œ ์ •๋ณด ํŒŒ์‹ฑ const [sourceArea, sourceField] = activeId.split("-") as [ PivotAreaType, string ]; - const [targetArea] = overId.split("-") as [PivotAreaType, string]; + + // targetArea ๊ฒฐ์ •: handleDragOver์—์„œ ๊ฐ์ง€ํ•œ ์˜์—ญ ์šฐ์„  ์‚ฌ์šฉ + let targetArea: PivotAreaType; + if (currentOverArea) { + targetArea = currentOverArea; + } else if (["filter", "column", "row", "data"].includes(overId)) { + targetArea = overId as PivotAreaType; + } else { + targetArea = overId.split("-")[0] as PivotAreaType; + } + + console.log("๐Ÿ”ท [FieldPanel] ํŒŒ์‹ฑ ๊ฒฐ๊ณผ:", { + sourceArea, + sourceField, + targetArea, + usedOverArea: !!currentOverArea, + }); // ๊ฐ™์€ ์˜์—ญ ๋‚ด ์ •๋ ฌ if (sourceArea === targetArea) { @@ -396,6 +447,12 @@ export const FieldPanel: React.FC = ({ // ๋‹ค๋ฅธ ์˜์—ญ์œผ๋กœ ์ด๋™ if (["filter", "column", "row", "data"].includes(targetArea)) { + console.log("๐Ÿ”ท [FieldPanel] ์˜์—ญ ์ด๋™:", { + field: sourceField, + from: sourceArea, + to: targetArea, + }); + const newFields = fields.map((f) => { if (f.field === sourceField && f.area === sourceArea) { return { @@ -406,6 +463,13 @@ export const FieldPanel: React.FC = ({ } return f; }); + + console.log("๐Ÿ”ท [FieldPanel] ๋ณ€๊ฒฝ๋œ ํ•„๋“œ:", { + totalFields: newFields.length, + filterFields: newFields.filter(f => f.area === "filter").length, + changedField: newFields.find(f => f.field === sourceField), + }); + onFieldsChange(newFields); } }; From b97b0cc7d7316947906ba52b7c01d4a21925e6fd Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 16 Jan 2026 15:52:35 +0900 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B7=B8=EB=A3=B9=20=EA=B4=80=EB=A0=A8=20API=EC=97=90=EC=84=9C?= =?UTF-8?q?=20AuthenticatedRequest=20=ED=83=80=EC=9E=85=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ํ™”๋ฉด ๊ทธ๋ฃน ๋ชฉ๋ก ์กฐํšŒ, ์ƒ์„ธ ์กฐํšŒ, ์ƒ์„ฑ, ์ˆ˜์ •, ์‚ญ์ œ API์—์„œ Request ํƒ€์ž…์„ AuthenticatedRequest๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด๋ฅผ ๋ช…ํ™•ํžˆ ์ฒ˜๋ฆฌ - companyCode๋ฅผ req.user?.companyCode || "*"๋กœ ์„ค์ •ํ•˜์—ฌ ๊ธฐ๋ณธ๊ฐ’ ์ฒ˜๋ฆฌ ๊ฐœ์„  - ๊ด€๋ จ API์˜ ์ผ๊ด€์„ฑ ์žˆ๋Š” ํƒ€์ž… ์‚ฌ์šฉ์œผ๋กœ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ๋ฐ ์œ ์ง€๋ณด์ˆ˜์„ฑ ํ–ฅ์ƒ --- .../src/controllers/screenGroupController.ts | 119 +++++++++--------- 1 file changed, 60 insertions(+), 59 deletions(-) diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index b89ef902..43ccce32 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -1,6 +1,7 @@ import { Request, Response } from "express"; import { getPool } from "../database/db"; import { logger } from "../utils/logger"; +import { AuthenticatedRequest } from "../types/auth"; import { syncScreenGroupsToMenu, syncMenuToScreenGroups, @@ -16,9 +17,9 @@ const pool = getPool(); // ============================================================ // ํ™”๋ฉด ๊ทธ๋ฃน ๋ชฉ๋ก ์กฐํšŒ -export const getScreenGroups = async (req: Request, res: Response) => { +export const getScreenGroups = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { page = 1, size = 20, searchTerm } = req.query; const offset = (parseInt(page as string) - 1) * parseInt(size as string); @@ -90,10 +91,10 @@ export const getScreenGroups = async (req: Request, res: Response) => { }; // ํ™”๋ฉด ๊ทธ๋ฃน ์ƒ์„ธ ์กฐํšŒ -export const getScreenGroup = async (req: Request, res: Response) => { +export const getScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = ` SELECT sg.*, @@ -136,10 +137,10 @@ export const getScreenGroup = async (req: Request, res: Response) => { }; // ํ™”๋ฉด ๊ทธ๋ฃน ์ƒ์„ฑ -export const createScreenGroup = async (req: Request, res: Response) => { +export const createScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body; if (!group_name || !group_code) { @@ -210,10 +211,10 @@ export const createScreenGroup = async (req: Request, res: Response) => { }; // ํ™”๋ฉด ๊ทธ๋ฃน ์ˆ˜์ • -export const updateScreenGroup = async (req: Request, res: Response) => { +export const updateScreenGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const userCompanyCode = (req.user as any).companyCode; + const userCompanyCode = req.user?.companyCode || "*"; const { group_name, group_code, main_table_name, description, icon, display_order, is_active, parent_group_id, target_company_code } = req.body; // ํšŒ์‚ฌ ์ฝ”๋“œ ๊ฒฐ์ •: ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์„ ํƒํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ, ์•„๋‹ˆ๋ฉด ํ˜„์žฌ ๊ทธ๋ฃน์˜ ํšŒ์‚ฌ ์œ ์ง€ @@ -299,11 +300,11 @@ export const updateScreenGroup = async (req: Request, res: Response) => { }; // ํ™”๋ฉด ๊ทธ๋ฃน ์‚ญ์ œ -export const deleteScreenGroup = async (req: Request, res: Response) => { +export const deleteScreenGroup = async (req: AuthenticatedRequest, res: Response) => { const client = await pool.connect(); try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; await client.query('BEGIN'); @@ -366,10 +367,10 @@ export const deleteScreenGroup = async (req: Request, res: Response) => { // ============================================================ // ๊ทธ๋ฃน์— ํ™”๋ฉด ์ถ”๊ฐ€ -export const addScreenToGroup = async (req: Request, res: Response) => { +export const addScreenToGroup = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_id, screen_id, screen_role, display_order, is_default } = req.body; if (!group_id || !screen_id) { @@ -406,10 +407,10 @@ export const addScreenToGroup = async (req: Request, res: Response) => { }; // ๊ทธ๋ฃน์—์„œ ํ™”๋ฉด ์ œ๊ฑฐ -export const removeScreenFromGroup = async (req: Request, res: Response) => { +export const removeScreenFromGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_group_screens WHERE id = $1`; const params: any[] = [id]; @@ -437,10 +438,10 @@ export const removeScreenFromGroup = async (req: Request, res: Response) => { }; // ๊ทธ๋ฃน ๋‚ด ํ™”๋ฉด ์ˆœ์„œ/์—ญํ•  ์ˆ˜์ • -export const updateScreenInGroup = async (req: Request, res: Response) => { +export const updateScreenInGroup = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_role, display_order, is_default } = req.body; let query = ` @@ -476,9 +477,9 @@ export const updateScreenInGroup = async (req: Request, res: Response) => { // ============================================================ // ํ™”๋ฉด ํ•„๋“œ ์กฐ์ธ ๋ชฉ๋ก ์กฐํšŒ -export const getFieldJoins = async (req: Request, res: Response) => { +export const getFieldJoins = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_id } = req.query; let query = ` @@ -517,10 +518,10 @@ export const getFieldJoins = async (req: Request, res: Response) => { }; // ํ™”๋ฉด ํ•„๋“œ ์กฐ์ธ ์ƒ์„ฑ -export const createFieldJoin = async (req: Request, res: Response) => { +export const createFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { screen_id, layout_id, component_id, field_name, save_table, save_column, join_table, join_column, display_column, @@ -558,10 +559,10 @@ export const createFieldJoin = async (req: Request, res: Response) => { }; // ํ™”๋ฉด ํ•„๋“œ ์กฐ์ธ ์ˆ˜์ • -export const updateFieldJoin = async (req: Request, res: Response) => { +export const updateFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { layout_id, component_id, field_name, save_table, save_column, join_table, join_column, display_column, @@ -603,10 +604,10 @@ export const updateFieldJoin = async (req: Request, res: Response) => { }; // ํ™”๋ฉด ํ•„๋“œ ์กฐ์ธ ์‚ญ์ œ -export const deleteFieldJoin = async (req: Request, res: Response) => { +export const deleteFieldJoin = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_field_joins WHERE id = $1`; const params: any[] = [id]; @@ -637,9 +638,9 @@ export const deleteFieldJoin = async (req: Request, res: Response) => { // ============================================================ // ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๋ชฉ๋ก ์กฐํšŒ -export const getDataFlows = async (req: Request, res: Response) => { +export const getDataFlows = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, source_screen_id } = req.query; let query = ` @@ -687,10 +688,10 @@ export const getDataFlows = async (req: Request, res: Response) => { }; // ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ƒ์„ฑ -export const createDataFlow = async (req: Request, res: Response) => { +export const createDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_id, source_screen_id, source_action, target_screen_id, target_action, data_mapping, flow_type, flow_label, condition_expression, is_active @@ -726,10 +727,10 @@ export const createDataFlow = async (req: Request, res: Response) => { }; // ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์ˆ˜์ • -export const updateDataFlow = async (req: Request, res: Response) => { +export const updateDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, source_screen_id, source_action, target_screen_id, target_action, data_mapping, flow_type, flow_label, condition_expression, is_active @@ -769,10 +770,10 @@ export const updateDataFlow = async (req: Request, res: Response) => { }; // ๋ฐ์ดํ„ฐ ํ๋ฆ„ ์‚ญ์ œ -export const deleteDataFlow = async (req: Request, res: Response) => { +export const deleteDataFlow = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_data_flows WHERE id = $1`; const params: any[] = [id]; @@ -803,9 +804,9 @@ export const deleteDataFlow = async (req: Request, res: Response) => { // ============================================================ // ํ™”๋ฉด-ํ…Œ์ด๋ธ” ๊ด€๊ณ„ ๋ชฉ๋ก ์กฐํšŒ -export const getTableRelations = async (req: Request, res: Response) => { +export const getTableRelations = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { screen_id, group_id } = req.query; let query = ` @@ -852,10 +853,10 @@ export const getTableRelations = async (req: Request, res: Response) => { }; // ํ™”๋ฉด-ํ…Œ์ด๋ธ” ๊ด€๊ณ„ ์ƒ์„ฑ -export const createTableRelation = async (req: Request, res: Response) => { +export const createTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { - const companyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const companyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { group_id, screen_id, table_name, relation_type, crud_operations, description, is_active } = req.body; if (!screen_id || !table_name) { @@ -885,10 +886,10 @@ export const createTableRelation = async (req: Request, res: Response) => { }; // ํ™”๋ฉด-ํ…Œ์ด๋ธ” ๊ด€๊ณ„ ์ˆ˜์ • -export const updateTableRelation = async (req: Request, res: Response) => { +export const updateTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; const { group_id, table_name, relation_type, crud_operations, description, is_active } = req.body; let query = ` @@ -920,10 +921,10 @@ export const updateTableRelation = async (req: Request, res: Response) => { }; // ํ™”๋ฉด-ํ…Œ์ด๋ธ” ๊ด€๊ณ„ ์‚ญ์ œ -export const deleteTableRelation = async (req: Request, res: Response) => { +export const deleteTableRelation = async (req: AuthenticatedRequest, res: Response) => { try { const { id } = req.params; - const companyCode = (req.user as any).companyCode; + const companyCode = req.user?.companyCode || "*"; let query = `DELETE FROM screen_table_relations WHERE id = $1`; const params: any[] = [id]; @@ -953,7 +954,7 @@ export const deleteTableRelation = async (req: Request, res: Response) => { // ============================================================ // ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ ์š”์•ฝ ์กฐํšŒ (์œ„์ ฏ ํƒ€์ž…๋ณ„ ๊ฐœ์ˆ˜, ๋ผ๋ฒจ ๋ชฉ๋ก) -export const getScreenLayoutSummary = async (req: Request, res: Response) => { +export const getScreenLayoutSummary = async (req: AuthenticatedRequest, res: Response) => { try { const { screenId } = req.params; @@ -1021,7 +1022,7 @@ export const getScreenLayoutSummary = async (req: Request, res: Response) => { }; // ์—ฌ๋Ÿฌ ํ™”๋ฉด์˜ ๋ ˆ์ด์•„์›ƒ ์š”์•ฝ ์ผ๊ด„ ์กฐํšŒ (๋ฏธ๋‹ˆ์–ด์ฒ˜ ๋ Œ๋”๋ง์šฉ ์ขŒํ‘œ ํฌํ•จ) -export const getMultipleScreenLayoutSummary = async (req: Request, res: Response) => { +export const getMultipleScreenLayoutSummary = async (req: AuthenticatedRequest, res: Response) => { try { const { screenIds } = req.body; @@ -1221,7 +1222,7 @@ export const getMultipleScreenLayoutSummary = async (req: Request, res: Response // ============================================================ // ์—ฌ๋Ÿฌ ํ™”๋ฉด์˜ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ •๋ณด ์กฐํšŒ (๋ฉ”์ธ ํ…Œ์ด๋ธ” โ†’ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๊ด€๊ณ„) -export const getScreenSubTables = async (req: Request, res: Response) => { +export const getScreenSubTables = async (req: AuthenticatedRequest, res: Response) => { try { const { screenIds } = req.body; @@ -2060,10 +2061,10 @@ export const getScreenSubTables = async (req: Request, res: Response) => { * ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” * screen_groups๋ฅผ menu_info๋กœ ๋™๊ธฐํ™” */ -export const syncScreenGroupsToMenuController = async (req: Request, res: Response) => { +export const syncScreenGroupsToMenuController = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { targetCompanyCode } = req.body; // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ @@ -2111,10 +2112,10 @@ export const syncScreenGroupsToMenuController = async (req: Request, res: Respon * ๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” * menu_info๋ฅผ screen_groups๋กœ ๋™๊ธฐํ™” */ -export const syncMenuToScreenGroupsController = async (req: Request, res: Response) => { +export const syncMenuToScreenGroupsController = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; const { targetCompanyCode } = req.body; // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ @@ -2161,9 +2162,9 @@ export const syncMenuToScreenGroupsController = async (req: Request, res: Respon /** * ๋™๊ธฐํ™” ์ƒํƒœ ์กฐํšŒ */ -export const getSyncStatusController = async (req: Request, res: Response) => { +export const getSyncStatusController = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; + const userCompanyCode = req.user?.companyCode || "*"; const { targetCompanyCode } = req.query; // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๊ฐ€ ํŠน์ • ํšŒ์‚ฌ๋ฅผ ์ง€์ •ํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ํšŒ์‚ฌ๋กœ @@ -2200,10 +2201,10 @@ export const getSyncStatusController = async (req: Request, res: Response) => { * ์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” * ๋ชจ๋“  ํšŒ์‚ฌ์— ๋Œ€ํ•ด ์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์ˆ˜ํ–‰ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ) */ -export const syncAllCompaniesController = async (req: Request, res: Response) => { +export const syncAllCompaniesController = async (req: AuthenticatedRequest, res: Response) => { try { - const userCompanyCode = (req.user as any).companyCode; - const userId = (req.user as any).userId; + const userCompanyCode = req.user?.companyCode || "*"; + const userId = req.user?.userId || ""; // ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋งŒ ์ „์ฒด ๋™๊ธฐํ™” ๊ฐ€๋Šฅ if (userCompanyCode !== "*") { From 44979851049cae0a316c192e33089f53195765df Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 16:11:34 +0900 Subject: [PATCH 06/16] Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj From 351ecbb35d5c728449f7f056985287b31d2c260d Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 16:24:43 +0900 Subject: [PATCH 07/16] =?UTF-8?q?=EB=B0=B0=ED=8F=AC=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=95=88=EB=82=98=EA=B2=8C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/pivot-grid/PivotGridComponent.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index c30472d8..f7c03b7d 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -725,14 +725,18 @@ export const PivotGridComponent: React.FC = ({ // ํ•„๋“œ ๋ณ€๊ฒฝ const handleFieldsChange = useCallback( (newFields: PivotFieldConfig[]) => { + // ๐Ÿ†• visible: false ํ•„๋“œ ์ œ๊ฑฐ (FieldChooser์—์„œ "์‚ฌ์šฉ ์•ˆํ•จ"์œผ๋กœ ์„ค์ •ํ•œ ํ•„๋“œ) + const visibleFields = newFields.filter(f => f.visible !== false); + console.log("๐Ÿ”ท [handleFieldsChange] ํ•„๋“œ ๋ณ€๊ฒฝ:", { totalFields: newFields.length, - filterFields: newFields.filter(f => f.area === "filter").length, - filterFieldNames: newFields.filter(f => f.area === "filter").map(f => f.field), - changedFields: newFields.filter(f => f.area === "filter"), + visibleFields: visibleFields.length, + removedFields: newFields.length - visibleFields.length, + filterFields: visibleFields.filter(f => f.area === "filter").length, + filterFieldNames: visibleFields.filter(f => f.area === "filter").map(f => f.field), }); console.log("๐Ÿ”ท [handleFieldsChange] setFields ํ˜ธ์ถœ ์ „"); - setFields(newFields); + setFields(visibleFields); console.log("๐Ÿ”ท [handleFieldsChange] setFields ํ˜ธ์ถœ ํ›„"); }, [] From d1631d15ffe6002560aa6d4cf15dfca1b09060af Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 16:49:59 +0900 Subject: [PATCH 08/16] =?UTF-8?q?=EC=95=88=EB=8B=AB=ED=9E=88=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/components/FieldChooser.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx b/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx index 89fe5128..a948aba0 100644 --- a/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx +++ b/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx @@ -267,11 +267,13 @@ export const FieldChooser: React.FC = ({ const existingConfig = selectedFields.find((f) => f.field === field.field); if (area === "none") { - // ํ•„๋“œ ์ œ๊ฑฐ ๋˜๋Š” ์ˆจ๊ธฐ๊ธฐ + // ๐Ÿ†• ํ•„๋“œ ์™„์ „ ์ œ๊ฑฐ (visible: false ๋Œ€์‹  ๋ฐฐ์—ด์—์„œ ์ œ๊ฑฐ) if (existingConfig) { - const newFields = selectedFields.map((f) => - f.field === field.field ? { ...f, visible: false } : f - ); + const newFields = selectedFields.filter((f) => f.field !== field.field); + console.log("๐Ÿ”ท [FieldChooser] ํ•„๋“œ ์ œ๊ฑฐ:", { + removedField: field.field, + remainingFields: newFields.length, + }); onFieldsChange(newFields); } } else { @@ -282,6 +284,10 @@ export const FieldChooser: React.FC = ({ ? { ...f, area, visible: true } : f ); + console.log("๐Ÿ”ท [FieldChooser] ํ•„๋“œ ์˜์—ญ ๋ณ€๊ฒฝ:", { + field: field.field, + newArea: area, + }); onFieldsChange(newFields); } else { // ์ƒˆ ํ•„๋“œ ์ถ”๊ฐ€ @@ -294,6 +300,10 @@ export const FieldChooser: React.FC = ({ summaryType: area === "data" ? "sum" : undefined, areaIndex: selectedFields.filter((f) => f.area === area).length, }; + console.log("๐Ÿ”ท [FieldChooser] ํ•„๋“œ ์ถ”๊ฐ€:", { + field: field.field, + area, + }); onFieldsChange([...selectedFields, newField]); } } From 2a3cc7ba006ecf3ea92df4928624fd1057a45c32 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 17:39:35 +0900 Subject: [PATCH 09/16] =?UTF-8?q?=EB=B0=B0=ED=8F=AC=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EB=90=98=EA=B2=8C=20=EA=B3=A0=EC=B3=90=EB=86=93=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridComponent.tsx | 101 +++++++++--------- 1 file changed, 48 insertions(+), 53 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index f7c03b7d..9135231c 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -384,20 +384,36 @@ export const PivotGridComponent: React.FC = ({ localStorage.setItem(stateStorageKey, JSON.stringify(stateToSave)); }, [fields, pivotState, sortConfig, columnWidths, stateStorageKey]); - // ์ƒํƒœ ๋ณต์› (localStorage) + // ์ƒํƒœ ๋ณต์› (localStorage) - ํ”„๋กœ๋•์…˜ ์•ˆ์ „์„ฑ ๊ฐ•ํ™” useEffect(() => { if (typeof window === "undefined") return; - const savedState = localStorage.getItem(stateStorageKey); - if (savedState) { - try { - const parsed = JSON.parse(savedState); - if (parsed.fields) setFields(parsed.fields); - if (parsed.pivotState) setPivotState(parsed.pivotState); - if (parsed.sortConfig) setSortConfig(parsed.sortConfig); - if (parsed.columnWidths) setColumnWidths(parsed.columnWidths); - } catch (e) { - console.warn("ํ”ผ๋ฒ— ์ƒํƒœ ๋ณต์› ์‹คํŒจ:", e); + + try { + const savedState = localStorage.getItem(stateStorageKey); + if (!savedState) return; + + const parsed = JSON.parse(savedState); + + // ํ•„๋“œ ๋ณต์› ์‹œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (์ค‘์š”!) + if (parsed.fields && Array.isArray(parsed.fields) && parsed.fields.length > 0) { + // ์ €์žฅ๋œ ํ•„๋“œ๊ฐ€ ํ˜„์žฌ ๋ฐ์ดํ„ฐ์™€ ํ˜ธํ™˜๋˜๋Š”์ง€ ํ™•์ธ + const validFields = parsed.fields.filter((f: PivotFieldConfig) => + f && typeof f.field === "string" && typeof f.area === "string" + ); + + if (validFields.length > 0) { + setFields(validFields); + } } + + // ๋‚˜๋จธ์ง€ ์ƒํƒœ ๋ณต์› + if (parsed.pivotState) setPivotState(parsed.pivotState); + if (parsed.sortConfig) setSortConfig(parsed.sortConfig); + if (parsed.columnWidths) setColumnWidths(parsed.columnWidths); + } catch (e) { + console.warn("ํ”ผ๋ฒ— ์ƒํƒœ ๋ณต์› ์‹คํŒจ, localStorage ์ดˆ๊ธฐํ™”:", e); + // ์†์ƒ๋œ ์ƒํƒœ๋Š” ์ œ๊ฑฐ + localStorage.removeItem(stateStorageKey); } }, [stateStorageKey]); @@ -512,15 +528,15 @@ export const PivotGridComponent: React.FC = ({ return null; } - const visibleFields = fields.filter((f) => f.visible !== false); + // FieldChooser์—์„œ ์ด๋ฏธ ํ•„๋“œ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๋ฏ€๋กœ visible ํ•„ํ„ฐ๋ง ๋ถˆํ•„์š” // ํ–‰, ์—ด, ๋ฐ์ดํ„ฐ ์˜์—ญ์— ํ•„๋“œ๊ฐ€ ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜ (ํ•„ํ„ฐ๋Š” ์ œ์™ธ) - if (visibleFields.filter((f) => ["row", "column", "data"].includes(f.area)).length === 0) { + if (fields.filter((f) => ["row", "column", "data"].includes(f.area)).length === 0) { return null; } const result = processPivotData( filteredData, - visibleFields, + fields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths ); @@ -536,32 +552,18 @@ export const PivotGridComponent: React.FC = ({ }); return result; - }, [ - filteredData, - fields, - JSON.stringify(pivotState.expandedRowPaths), - JSON.stringify(pivotState.expandedColumnPaths) - ]); + }, [filteredData, fields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths]); - // ๐Ÿ†• ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ์ฒซ ๋ ˆ๋ฒจ ์ž๋™ ํ™•์žฅ + // ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ์ฒซ ๋ ˆ๋ฒจ ์ž๋™ ํ™•์žฅ useEffect(() => { if (pivotResult && pivotResult.flatRows.length > 0 && !isInitialExpanded) { - console.log("๐Ÿ”ถ ํ”ผ๋ฒ— ๊ฒฐ๊ณผ ์ƒ์„ฑ๋จ:", { - flatRowsCount: pivotResult.flatRows.length, - expandedRowPaths: pivotState.expandedRowPaths.length, - isInitialExpanded, - }); - // ์ฒซ ๋ ˆ๋ฒจ ํ–‰๋“ค์˜ ๊ฒฝ๋กœ ์ˆ˜์ง‘ (level 0์ธ ํ–‰๋“ค) - const firstLevelRows = pivotResult.flatRows.filter(row => row.level === 0 && row.hasChildren); - - console.log("๐Ÿ”ถ ์ฒซ ๋ ˆ๋ฒจ ํ–‰ (level 0, hasChildren):", firstLevelRows.map(r => ({ path: r.path, caption: r.caption }))); + const firstLevelRows = pivotResult.flatRows.filter((row) => row.level === 0 && row.hasChildren); // ์ฒซ ๋ ˆ๋ฒจ ํ–‰์ด ์žˆ์œผ๋ฉด ์ž๋™ ํ™•์žฅ if (firstLevelRows.length > 0) { - const firstLevelPaths = firstLevelRows.map(row => row.path); - console.log("๐Ÿ”ถ ์ดˆ๊ธฐ ์ž๋™ ํ™•์žฅ ์‹คํ–‰ (ํ•œ ๋ฒˆ๋งŒ):", firstLevelPaths); - setPivotState(prev => ({ + const firstLevelPaths = firstLevelRows.map((row) => row.path); + setPivotState((prev) => ({ ...prev, expandedRowPaths: firstLevelPaths, })); @@ -725,19 +727,16 @@ export const PivotGridComponent: React.FC = ({ // ํ•„๋“œ ๋ณ€๊ฒฝ const handleFieldsChange = useCallback( (newFields: PivotFieldConfig[]) => { - // ๐Ÿ†• visible: false ํ•„๋“œ ์ œ๊ฑฐ (FieldChooser์—์„œ "์‚ฌ์šฉ ์•ˆํ•จ"์œผ๋กœ ์„ค์ •ํ•œ ํ•„๋“œ) - const visibleFields = newFields.filter(f => f.visible !== false); - + // FieldChooser์—์„œ ์ด๋ฏธ ํ•„๋“œ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๋ฏ€๋กœ ์ถ”๊ฐ€ ํ•„ํ„ฐ๋ง ๋ถˆํ•„์š” console.log("๐Ÿ”ท [handleFieldsChange] ํ•„๋“œ ๋ณ€๊ฒฝ:", { totalFields: newFields.length, - visibleFields: visibleFields.length, - removedFields: newFields.length - visibleFields.length, - filterFields: visibleFields.filter(f => f.area === "filter").length, - filterFieldNames: visibleFields.filter(f => f.area === "filter").map(f => f.field), + filterFields: newFields.filter(f => f.area === "filter").length, + filterFieldNames: newFields.filter(f => f.area === "filter").map(f => f.field), + rowFields: newFields.filter(f => f.area === "row").length, + columnFields: newFields.filter(f => f.area === "column").length, + dataFields: newFields.filter(f => f.area === "data").length, }); - console.log("๐Ÿ”ท [handleFieldsChange] setFields ํ˜ธ์ถœ ์ „"); - setFields(visibleFields); - console.log("๐Ÿ”ท [handleFieldsChange] setFields ํ˜ธ์ถœ ํ›„"); + setFields(newFields); }, [] ); @@ -945,6 +944,8 @@ export const PivotGridComponent: React.FC = ({ // ์ธ์‡„ ๊ธฐ๋Šฅ (PDF ๋‚ด๋ณด๋‚ด๊ธฐ๋ณด๋‹ค ๋จผ์ € ์ •์˜ํ•ด์•ผ ํ•จ) const handlePrint = useCallback(() => { + if (typeof window === "undefined") return; + const printContent = tableRef.current; if (!printContent) return; @@ -1047,8 +1048,10 @@ export const PivotGridComponent: React.FC = ({ // ์ƒํƒœ ์ดˆ๊ธฐํ™” (ํ™•์žฅ/์ถ•์†Œ, ์ •๋ ฌ, ํ•„ํ„ฐ๋งŒ ์ดˆ๊ธฐํ™”, ํ•„๋“œ ์„ค์ •์€ ์œ ์ง€) const handleResetState = useCallback(() => { - // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ƒํƒœ ์ œ๊ฑฐ - localStorage.removeItem(stateStorageKey); + // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ƒํƒœ ์ œ๊ฑฐ (SSR ๋ณดํ˜ธ) + if (typeof window !== "undefined") { + localStorage.removeItem(stateStorageKey); + } // ํ™•์žฅ/์ถ•์†Œ, ์ •๋ ฌ, ํ•„ํ„ฐ ์ƒํƒœ๋งŒ ์ดˆ๊ธฐํ™” setPivotState({ @@ -1061,9 +1064,6 @@ export const PivotGridComponent: React.FC = ({ setColumnWidths({}); setSelectedCell(null); setSelectionRange(null); - - // ๐Ÿ†• ํ•„๋“œ ์„ค์ •์€ ์œ ์ง€ (initialFields๋กœ ๋˜๋Œ๋ฆฌ์ง€ ์•Š์Œ) - console.log("๐Ÿ”ท ํ”ผ๋ฒ— ์ƒํƒœ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค (ํ•„๋“œ ์„ค์ •์€ ์œ ์ง€)"); }, [stateStorageKey]); // ํ•„๋“œ ์ˆจ๊ธฐ๊ธฐ/ํ‘œ์‹œ ์ƒํƒœ @@ -1081,11 +1081,6 @@ export const PivotGridComponent: React.FC = ({ }); }, []); - // ์ˆจ๊ฒจ์ง„ ํ•„๋“œ ์ œ์™ธํ•œ ํ™œ์„ฑ ํ•„๋“œ๋“ค - const visibleFields = useMemo(() => { - return fields.filter((f) => !hiddenFields.has(f.field)); - }, [fields, hiddenFields]); - // ์ˆจ๊ฒจ์ง„ ํ•„๋“œ ๋ชฉ๋ก const hiddenFieldsList = useMemo(() => { return fields.filter((f) => hiddenFields.has(f.field)); From df8065503d94567701e035043ffd50cdaa8bad8f Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 16 Jan 2026 17:41:19 +0900 Subject: [PATCH 10/16] =?UTF-8?q?feat:=20=EB=A9=94=EB=89=B4=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=B0=8F=20=EB=8F=99=EA=B8=B0=ED=99=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๋ฉ”๋‰ด ์‚ญ์ œ ์‹œ ํ•˜์œ„ ๋ฉ”๋‰ด๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘ํ•˜์—ฌ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ - ๋ฉ”๋‰ด ์‚ญ์ œ ์„ฑ๊ณต ์‹œ ์‚ญ์ œ๋œ ๋ฉ”๋‰ด์™€ ํ•˜์œ„ ๋ฉ”๋‰ด ์ˆ˜๋ฅผ ํฌํ•จํ•œ ์‘๋‹ต ๋ฉ”์‹œ์ง€ ๊ฐœ์„  - ๋ฉ”๋‰ด ๋ณต์ œ ์‹œ ํ•ญ์ƒ ํ™œ์„ฑํ™” ์ƒํƒœ๋กœ ์„ค์ • - ํ™”๋ฉด-๋ฉ”๋‰ด ๋™๊ธฐํ™” ์ง„ํ–‰ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•œ ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ - ์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋”๋Š” ๋ฉ”๋‰ด๋กœ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ์Šคํ‚ตํ•˜๋Š” ๋กœ์ง ์ถ”๊ฐ€ - ๋™๊ธฐํ™” ์ง„ํ–‰ ์ค‘ ์˜ค๋ฒ„๋ ˆ์ด UI ๊ฐœ์„  --- .../src/controllers/adminController.ts | 201 +++++++++++++----- backend-node/src/services/menuCopyService.ts | 2 +- .../src/services/menuScreenSyncService.ts | 42 +++- .../components/screen/ScreenGroupTreeView.tsx | 79 +++++-- 4 files changed, 246 insertions(+), 78 deletions(-) diff --git a/backend-node/src/controllers/adminController.ts b/backend-node/src/controllers/adminController.ts index ce7b9c7f..c86b0064 100644 --- a/backend-node/src/controllers/adminController.ts +++ b/backend-node/src/controllers/adminController.ts @@ -1417,6 +1417,75 @@ export async function updateMenu( } } +/** + * ์žฌ๊ท€์ ์œผ๋กœ ๋ชจ๋“  ํ•˜์œ„ ๋ฉ”๋‰ด ID๋ฅผ ์ˆ˜์ง‘ํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ + */ +async function collectAllChildMenuIds(parentObjid: number): Promise { + const allIds: number[] = []; + + // ์ง์ ‘ ์ž์‹ ๋ฉ”๋‰ด๋“ค ์กฐํšŒ + const children = await query( + `SELECT objid FROM menu_info WHERE parent_obj_id = $1`, + [parentObjid] + ); + + for (const child of children) { + allIds.push(child.objid); + // ์ž์‹์˜ ์ž์‹๋“ค๋„ ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘ + const grandChildren = await collectAllChildMenuIds(child.objid); + allIds.push(...grandChildren); + } + + return allIds; +} + +/** + * ๋ฉ”๋‰ด ๋ฐ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ํ—ฌํผ ํ•จ์ˆ˜ + */ +async function cleanupMenuRelatedData(menuObjid: number): Promise { + // 1. category_column_mapping์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • + await query( + `UPDATE category_column_mapping SET menu_objid = NULL WHERE menu_objid = $1`, + [menuObjid] + ); + + // 2. code_category์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • + await query( + `UPDATE code_category SET menu_objid = NULL WHERE menu_objid = $1`, + [menuObjid] + ); + + // 3. code_info์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • + await query( + `UPDATE code_info SET menu_objid = NULL WHERE menu_objid = $1`, + [menuObjid] + ); + + // 4. numbering_rules์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • + await query( + `UPDATE numbering_rules SET menu_objid = NULL WHERE menu_objid = $1`, + [menuObjid] + ); + + // 5. rel_menu_auth์—์„œ ๊ด€๋ จ ๊ถŒํ•œ ์‚ญ์ œ + await query( + `DELETE FROM rel_menu_auth WHERE menu_objid = $1`, + [menuObjid] + ); + + // 6. screen_menu_assignments์—์„œ ๊ด€๋ จ ํ• ๋‹น ์‚ญ์ œ + await query( + `DELETE FROM screen_menu_assignments WHERE menu_objid = $1`, + [menuObjid] + ); + + // 7. screen_groups์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • + await query( + `UPDATE screen_groups SET menu_objid = NULL WHERE menu_objid = $1`, + [menuObjid] + ); +} + /** * ๋ฉ”๋‰ด ์‚ญ์ œ */ @@ -1443,7 +1512,7 @@ export async function deleteMenu( // ์‚ญ์ œํ•˜๋ ค๋Š” ๋ฉ”๋‰ด ์กฐํšŒ const currentMenu = await queryOne( - `SELECT objid, company_code FROM menu_info WHERE objid = $1`, + `SELECT objid, company_code, menu_name_kor FROM menu_info WHERE objid = $1`, [Number(menuId)] ); @@ -1478,67 +1547,50 @@ export async function deleteMenu( } } - // ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด์ด ์žˆ๋Š” ๊ด€๋ จ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋จผ์ € ์ •๋ฆฌ const menuObjid = Number(menuId); - // 1. category_column_mapping์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • - await query( - `UPDATE category_column_mapping SET menu_objid = NULL WHERE menu_objid = $1`, - [menuObjid] - ); + // ํ•˜์œ„ ๋ฉ”๋‰ด๋“ค ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘ + const childMenuIds = await collectAllChildMenuIds(menuObjid); + const allMenuIdsToDelete = [menuObjid, ...childMenuIds]; - // 2. code_category์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • - await query( - `UPDATE code_category SET menu_objid = NULL WHERE menu_objid = $1`, - [menuObjid] - ); - - // 3. code_info์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • - await query( - `UPDATE code_info SET menu_objid = NULL WHERE menu_objid = $1`, - [menuObjid] - ); - - // 4. numbering_rules์—์„œ menu_objid๋ฅผ NULL๋กœ ์„ค์ • - await query( - `UPDATE numbering_rules SET menu_objid = NULL WHERE menu_objid = $1`, - [menuObjid] - ); - - // 5. rel_menu_auth์—์„œ ๊ด€๋ จ ๊ถŒํ•œ ์‚ญ์ œ - await query( - `DELETE FROM rel_menu_auth WHERE menu_objid = $1`, - [menuObjid] - ); - - // 6. screen_menu_assignments์—์„œ ๊ด€๋ จ ํ• ๋‹น ์‚ญ์ œ - await query( - `DELETE FROM screen_menu_assignments WHERE menu_objid = $1`, - [menuObjid] - ); + logger.info(`๋ฉ”๋‰ด ์‚ญ์ œ ๋Œ€์ƒ: ๋ณธ์ธ(${menuObjid}) + ํ•˜์œ„ ๋ฉ”๋‰ด ${childMenuIds.length}๊ฐœ`, { + menuName: currentMenu.menu_name_kor, + totalCount: allMenuIdsToDelete.length, + childMenuIds, + }); - logger.info("๋ฉ”๋‰ด ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ์™„๋ฃŒ", { menuObjid }); + // ๋ชจ๋“  ์‚ญ์ œ ๋Œ€์ƒ ๋ฉ”๋‰ด์— ๋Œ€ํ•ด ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ + for (const objid of allMenuIdsToDelete) { + await cleanupMenuRelatedData(objid); + } - // Raw Query๋ฅผ ์‚ฌ์šฉํ•œ ๋ฉ”๋‰ด ์‚ญ์ œ - const [deletedMenu] = await query( - `DELETE FROM menu_info WHERE objid = $1 RETURNING *`, - [menuObjid] - ); + logger.info("๋ฉ”๋‰ด ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ์™„๋ฃŒ", { + menuObjid, + totalCleaned: allMenuIdsToDelete.length + }); - logger.info("๋ฉ”๋‰ด ์‚ญ์ œ ์„ฑ๊ณต", { deletedMenu }); + // ํ•˜์œ„ ๋ฉ”๋‰ด๋ถ€ํ„ฐ ์—ญ์ˆœ์œผ๋กœ ์‚ญ์ œ (์™ธ๋ž˜ํ‚ค ์ œ์•ฝ ํšŒํ”ผ) + // ๊ฐ€์žฅ ๊นŠ์€ ํ•˜์œ„๋ถ€ํ„ฐ ์‚ญ์ œํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์—ญ์ˆœ์œผ๋กœ + const reversedIds = [...allMenuIdsToDelete].reverse(); + + for (const objid of reversedIds) { + await query(`DELETE FROM menu_info WHERE objid = $1`, [objid]); + } + + logger.info("๋ฉ”๋‰ด ์‚ญ์ œ ์„ฑ๊ณต", { + deletedMenuObjid: menuObjid, + deletedMenuName: currentMenu.menu_name_kor, + totalDeleted: allMenuIdsToDelete.length, + }); const response: ApiResponse = { success: true, - message: "๋ฉ”๋‰ด๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + message: `๋ฉ”๋‰ด๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. (ํ•˜์œ„ ๋ฉ”๋‰ด ${childMenuIds.length}๊ฐœ ํฌํ•จ)`, data: { - objid: deletedMenu.objid.toString(), - menuNameKor: deletedMenu.menu_name_kor, - menuNameEng: deletedMenu.menu_name_eng, - menuUrl: deletedMenu.menu_url, - menuDesc: deletedMenu.menu_desc, - status: deletedMenu.status, - writer: deletedMenu.writer, - regdate: new Date(deletedMenu.regdate).toISOString(), + objid: menuObjid.toString(), + menuNameKor: currentMenu.menu_name_kor, + deletedCount: allMenuIdsToDelete.length, + deletedChildCount: childMenuIds.length, }, }; @@ -1623,18 +1675,49 @@ export async function deleteMenusBatch( } } + // ๋ชจ๋“  ์‚ญ์ œ ๋Œ€์ƒ ๋ฉ”๋‰ด ID ์ˆ˜์ง‘ (ํ•˜์œ„ ๋ฉ”๋‰ด ํฌํ•จ) + const allMenuIdsToDelete = new Set(); + + for (const menuId of menuIds) { + const objid = Number(menuId); + allMenuIdsToDelete.add(objid); + + // ํ•˜์œ„ ๋ฉ”๋‰ด๋“ค ์žฌ๊ท€์ ์œผ๋กœ ์ˆ˜์ง‘ + const childMenuIds = await collectAllChildMenuIds(objid); + childMenuIds.forEach(id => allMenuIdsToDelete.add(Number(id))); + } + + const allIdsArray = Array.from(allMenuIdsToDelete); + + logger.info(`๋ฉ”๋‰ด ์ผ๊ด„ ์‚ญ์ œ ๋Œ€์ƒ: ์„ ํƒ ${menuIds.length}๊ฐœ + ํ•˜์œ„ ๋ฉ”๋‰ด ํฌํ•จ ์ด ${allIdsArray.length}๊ฐœ`, { + selectedMenuIds: menuIds, + totalWithChildren: allIdsArray.length, + }); + + // ๋ชจ๋“  ์‚ญ์ œ ๋Œ€์ƒ ๋ฉ”๋‰ด์— ๋Œ€ํ•ด ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ + for (const objid of allIdsArray) { + await cleanupMenuRelatedData(objid); + } + + logger.info("๋ฉ”๋‰ด ๊ด€๋ จ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ ์™„๋ฃŒ", { + totalCleaned: allIdsArray.length + }); + // Raw Query๋ฅผ ์‚ฌ์šฉํ•œ ๋ฉ”๋‰ด ์ผ๊ด„ ์‚ญ์ œ let deletedCount = 0; let failedCount = 0; const deletedMenus: any[] = []; const failedMenuIds: string[] = []; + // ํ•˜์œ„ ๋ฉ”๋‰ด๋ถ€ํ„ฐ ์‚ญ์ œํ•˜๊ธฐ ์œ„ํ•ด ์—ญ์ˆœ์œผ๋กœ ์ •๋ ฌ + const reversedIds = [...allIdsArray].reverse(); + // ๊ฐ ๋ฉ”๋‰ด ID์— ๋Œ€ํ•ด ์‚ญ์ œ ์‹œ๋„ - for (const menuId of menuIds) { + for (const menuObjid of reversedIds) { try { const result = await query( `DELETE FROM menu_info WHERE objid = $1 RETURNING *`, - [Number(menuId)] + [menuObjid] ); if (result.length > 0) { @@ -1645,20 +1728,20 @@ export async function deleteMenusBatch( }); } else { failedCount++; - failedMenuIds.push(menuId); + failedMenuIds.push(String(menuObjid)); } } catch (error) { - logger.error(`๋ฉ”๋‰ด ์‚ญ์ œ ์‹คํŒจ (ID: ${menuId}):`, error); + logger.error(`๋ฉ”๋‰ด ์‚ญ์ œ ์‹คํŒจ (ID: ${menuObjid}):`, error); failedCount++; - failedMenuIds.push(menuId); + failedMenuIds.push(String(menuObjid)); } } logger.info("๋ฉ”๋‰ด ์ผ๊ด„ ์‚ญ์ œ ์™„๋ฃŒ", { - total: menuIds.length, + requested: menuIds.length, + totalWithChildren: allIdsArray.length, deletedCount, failedCount, - deletedMenus, failedMenuIds, }); diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index a163f30c..f8b808d3 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -2090,7 +2090,7 @@ export class MenuCopyService { menu.menu_url, menu.menu_desc, userId, - menu.status, + 'active', // ๋ณต์ œ๋œ ๋ฉ”๋‰ด๋Š” ํ•ญ์ƒ ํ™œ์„ฑํ™” ์ƒํƒœ menu.system_name, targetCompanyCode, // ์ƒˆ ํšŒ์‚ฌ ์ฝ”๋“œ menu.lang_key, diff --git a/backend-node/src/services/menuScreenSyncService.ts b/backend-node/src/services/menuScreenSyncService.ts index 13c77ed6..d6f27e07 100644 --- a/backend-node/src/services/menuScreenSyncService.ts +++ b/backend-node/src/services/menuScreenSyncService.ts @@ -142,7 +142,7 @@ export async function syncScreenGroupsToMenu( const newObjid = Date.now(); const createRootQuery = ` INSERT INTO menu_info (objid, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_type, company_code, writer, regdate, status) - VALUES ($1, 0, '์‚ฌ์šฉ์ž', 'User', 1, 1, $2, $3, NOW(), 'Y') + VALUES ($1, 0, '์‚ฌ์šฉ์ž', 'User', 1, 1, $2, $3, NOW(), 'active') RETURNING objid `; const createRootResult = await client.query(createRootQuery, [newObjid, companyCode, userId]); @@ -159,12 +159,36 @@ export async function syncScreenGroupsToMenu( groupIdToName.set(g.id, g.group_name?.trim().toLowerCase() || ''); }); - // 5. ๊ฐ screen_group ์ฒ˜๋ฆฌ + // 5. ์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋” ID ์ฐพ๊ธฐ (level 0, parent_group_id IS NULL) + // ์ด ํด๋”๋Š” ๋ฉ”๋‰ด๋กœ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ , ํ•˜์œ„ ํด๋”๋“ค์„ ์‚ฌ์šฉ์ž ๋ฃจํŠธ ๋ฐ”๋กœ ์•„๋ž˜์— ๋ฐฐ์น˜ + const topLevelCompanyFolderIds = new Set(); + for (const group of screenGroupsResult.rows) { + if (group.group_level === 0 && group.parent_group_id === null) { + topLevelCompanyFolderIds.add(group.id); + // ์ตœ์ƒ์œ„ ํด๋” โ†’ ์‚ฌ์šฉ์ž ๋ฃจํŠธ์— ๋งคํ•‘ (ํ•˜์œ„ ํด๋”์˜ ๋ถ€๋ชจ๋กœ ์‚ฌ์šฉ) + groupToMenuMap.set(group.id, userMenuRootObjid!); + logger.info("์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋” ์Šคํ‚ต", { groupId: group.id, groupName: group.group_name }); + } + } + + // 6. ๊ฐ screen_group ์ฒ˜๋ฆฌ for (const group of screenGroupsResult.rows) { const groupId = group.id; const groupName = group.group_name?.trim(); const groupNameLower = groupName?.toLowerCase() || ''; + // ์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋”๋Š” ๋ฉ”๋‰ด๋กœ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ  ์Šคํ‚ต + if (topLevelCompanyFolderIds.has(groupId)) { + result.skipped++; + result.details.push({ + action: 'skipped', + sourceName: groupName, + sourceId: groupId, + reason: '์ตœ์ƒ์œ„ ํšŒ์‚ฌ ํด๋” (๋ฉ”๋‰ด ์ƒ์„ฑ ์Šคํ‚ต)', + }); + continue; + } + // ์ด๋ฏธ ์—ฐ๊ฒฐ๋œ ๊ฒฝ์šฐ - ์‹ค์ œ๋กœ ๋ฉ”๋‰ด๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ if (group.menu_objid) { const menuExists = existingMenuObjids.has(Number(group.menu_objid)); @@ -237,11 +261,17 @@ export async function syncScreenGroupsToMenu( const newObjid = Date.now() + groupId; // ๊ณ ์œ  ID ๋ณด์žฅ // ๋ถ€๋ชจ ๋ฉ”๋‰ด objid ๊ฒฐ์ • + // ์šฐ์„ ์ˆœ์œ„: groupToMenuMap > parent_menu_objid (์กด์žฌ ํ™•์ธ ํ•„์ˆ˜) let parentMenuObjid = userMenuRootObjid; - if (group.parent_group_id && group.parent_menu_objid) { - parentMenuObjid = Number(group.parent_menu_objid); - } else if (group.parent_group_id && groupToMenuMap.has(group.parent_group_id)) { + if (group.parent_group_id && groupToMenuMap.has(group.parent_group_id)) { + // ํ˜„์žฌ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒ์„ฑ๋œ ๋ถ€๋ชจ ๋ฉ”๋‰ด ์‚ฌ์šฉ parentMenuObjid = groupToMenuMap.get(group.parent_group_id)!; + } else if (group.parent_group_id && group.parent_menu_objid) { + // ๊ธฐ์กด parent_menu_objid๊ฐ€ ์‹ค์ œ๋กœ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + const parentMenuExists = existingMenuObjids.has(Number(group.parent_menu_objid)); + if (parentMenuExists) { + parentMenuObjid = Number(group.parent_menu_objid); + } } // ๊ฐ™์€ ๋ถ€๋ชจ ์•„๋ž˜์—์„œ ๊ฐ€์žฅ ๋†’์€ seq ์กฐํšŒ ํ›„ +1 @@ -261,7 +291,7 @@ export async function syncScreenGroupsToMenu( INSERT INTO menu_info ( objid, parent_obj_id, menu_name_kor, menu_name_eng, seq, menu_type, company_code, writer, regdate, status, screen_group_id, menu_desc - ) VALUES ($1, $2, $3, $4, $5, 1, $6, $7, NOW(), 'Y', $8, $9) + ) VALUES ($1, $2, $3, $4, $5, 1, $6, $7, NOW(), 'active', $8, $9) RETURNING objid `; await client.query(insertMenuQuery, [ diff --git a/frontend/components/screen/ScreenGroupTreeView.tsx b/frontend/components/screen/ScreenGroupTreeView.tsx index edd36816..7cd1310f 100644 --- a/frontend/components/screen/ScreenGroupTreeView.tsx +++ b/frontend/components/screen/ScreenGroupTreeView.tsx @@ -172,6 +172,7 @@ export function ScreenGroupTreeView({ const [syncStatus, setSyncStatus] = useState(null); const [isSyncing, setIsSyncing] = useState(false); const [syncDirection, setSyncDirection] = useState<"screen-to-menu" | "menu-to-screen" | "all" | null>(null); + const [syncProgress, setSyncProgress] = useState<{ message: string; detail?: string } | null>(null); // ํšŒ์‚ฌ ์„ ํƒ (์ตœ๊ณ  ๊ด€๋ฆฌ์ž์šฉ) const { user } = useAuth(); @@ -328,14 +329,31 @@ export function ScreenGroupTreeView({ setIsSyncing(true); setSyncDirection(direction); + setSyncProgress({ + message: direction === "screen-to-menu" + ? "ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์ค‘..." + : "๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์ค‘...", + detail: "๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..." + }); try { + setSyncProgress({ + message: direction === "screen-to-menu" + ? "ํ™”๋ฉด๊ด€๋ฆฌ โ†’ ๋ฉ”๋‰ด ๋™๊ธฐํ™” ์ค‘..." + : "๋ฉ”๋‰ด โ†’ ํ™”๋ฉด๊ด€๋ฆฌ ๋™๊ธฐํ™” ์ค‘...", + detail: "๋™๊ธฐํ™” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..." + }); + const response = direction === "screen-to-menu" ? await syncScreenGroupsToMenu(targetCompanyCode) : await syncMenuToScreenGroups(targetCompanyCode); if (response.success) { const data = response.data; + setSyncProgress({ + message: "๋™๊ธฐํ™” ์™„๋ฃŒ!", + detail: `์ƒ์„ฑ ${data?.created || 0}๊ฐœ, ์—ฐ๊ฒฐ ${data?.linked || 0}๊ฐœ, ์Šคํ‚ต ${data?.skipped || 0}๊ฐœ` + }); toast.success( `๋™๊ธฐํ™” ์™„๋ฃŒ: ์ƒ์„ฑ ${data?.created || 0}๊ฐœ, ์—ฐ๊ฒฐ ${data?.linked || 0}๊ฐœ, ์Šคํ‚ต ${data?.skipped || 0}๊ฐœ` ); @@ -347,13 +365,17 @@ export function ScreenGroupTreeView({ setSyncStatus(statusResponse.data); } } else { + setSyncProgress(null); toast.error(`๋™๊ธฐํ™” ์‹คํŒจ: ${response.error || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); } } catch (error: any) { + setSyncProgress(null); toast.error(`๋™๊ธฐํ™” ์‹คํŒจ: ${error.message}`); } finally { setIsSyncing(false); setSyncDirection(null); + // 3์ดˆ ํ›„ ์ง„ํ–‰ ๋ฉ”์‹œ์ง€ ์ดˆ๊ธฐํ™” + setTimeout(() => setSyncProgress(null), 3000); } }; @@ -366,27 +388,42 @@ export function ScreenGroupTreeView({ setIsSyncing(true); setSyncDirection("all"); + setSyncProgress({ + message: "์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” ์ค‘...", + detail: "๋ชจ๋“  ํšŒ์‚ฌ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..." + }); try { + setSyncProgress({ + message: "์ „์ฒด ํšŒ์‚ฌ ๋™๊ธฐํ™” ์ค‘...", + detail: "์–‘๋ฐฉํ–ฅ ๋™๊ธฐํ™” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค..." + }); + const response = await syncAllCompanies(); if (response.success && response.data) { const data = response.data; + setSyncProgress({ + message: "์ „์ฒด ๋™๊ธฐํ™” ์™„๋ฃŒ!", + detail: `${data.totalCompanies}๊ฐœ ํšŒ์‚ฌ, ์ƒ์„ฑ ${data.totalCreated}๊ฐœ, ์—ฐ๊ฒฐ ${data.totalLinked}๊ฐœ` + }); toast.success( `์ „์ฒด ๋™๊ธฐํ™” ์™„๋ฃŒ: ${data.totalCompanies}๊ฐœ ํšŒ์‚ฌ, ์ƒ์„ฑ ${data.totalCreated}๊ฐœ, ์—ฐ๊ฒฐ ${data.totalLinked}๊ฐœ` ); // ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ await loadGroupsData(); - // ๋™๊ธฐํ™” ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ - setIsSyncDialogOpen(false); } else { + setSyncProgress(null); toast.error(`์ „์ฒด ๋™๊ธฐํ™” ์‹คํŒจ: ${response.error || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜"}`); } } catch (error: any) { + setSyncProgress(null); toast.error(`์ „์ฒด ๋™๊ธฐํ™” ์‹คํŒจ: ${error.message}`); } finally { setIsSyncing(false); setSyncDirection(null); + // 3์ดˆ ํ›„ ์ง„ํ–‰ ๋ฉ”์‹œ์ง€ ์ดˆ๊ธฐํ™” + setTimeout(() => setSyncProgress(null), 3000); } }; @@ -979,15 +1016,17 @@ export function ScreenGroupTreeView({ ๊ทธ๋ฃน ์ถ”๊ฐ€ - +{isSuperAdmin && ( + + )}
{/* ํŠธ๋ฆฌ ๋ชฉ๋ก */} @@ -1816,7 +1855,23 @@ export function ScreenGroupTreeView({ {/* ๋ฉ”๋‰ด-ํ™”๋ฉด๊ทธ๋ฃน ๋™๊ธฐํ™” ๋‹ค์ด์–ผ๋กœ๊ทธ */} - + + {/* ๋™๊ธฐํ™” ์ง„ํ–‰ ์ค‘ ์˜ค๋ฒ„๋ ˆ์ด (์‚ญ์ œ์™€ ๋™์ผํ•œ ์Šคํƒ€์ผ) */} + {isSyncing && ( +
+ +

{syncProgress?.message || "๋™๊ธฐํ™” ์ค‘..."}

+ {syncProgress?.detail && ( +

{syncProgress.detail}

+ )} +
+
+
+
+ )} ๋ฉ”๋‰ด-ํ™”๋ฉด ๋™๊ธฐํ™” From 49f67451eb3c137092bbd2a9f94021cd68a3d85d Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 18:14:55 +0900 Subject: [PATCH 11/16] =?UTF-8?q?=ED=94=BC=EB=B2=97=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=98=A4=EB=8A=98=EC=B5=9C=EC=A2=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridComponent.tsx | 233 ++++++++---------- .../pivot-grid/PivotGridRenderer.tsx | 147 ++++++----- .../pivot-grid/components/FieldChooser.tsx | 14 +- .../pivot-grid/components/FieldPanel.tsx | 28 --- .../pivot-grid/utils/pivotEngine.ts | 12 +- 5 files changed, 200 insertions(+), 234 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 9135231c..feda2167 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -296,24 +296,6 @@ export const PivotGridComponent: React.FC = ({ onFieldDrop, onExpandChange, }) => { - // ๋””๋ฒ„๊น… ๋กœ๊ทธ - console.log("๐Ÿ”ถ PivotGridComponent props:", { - title, - hasExternalData: !!externalData, - externalDataLength: externalData?.length, - initialFieldsLength: initialFields?.length, - }); - - // ๐Ÿ†• ๋ฐ์ดํ„ฐ ์ƒ˜ํ”Œ ํ™•์ธ - if (externalData && externalData.length > 0) { - console.log("๐Ÿ”ถ ์ฒซ ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ ์ƒ˜ํ”Œ:", externalData[0]); - console.log("๐Ÿ”ถ ์ „์ฒด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜:", externalData.length); - } - - // ๐Ÿ†• ํ•„๋“œ ์„ค์ • ํ™•์ธ - if (initialFields && initialFields.length > 0) { - console.log("๐Ÿ”ถ ํ•„๋“œ ์„ค์ •:", initialFields); - } // ==================== ์ƒํƒœ ==================== const [fields, setFields] = useState(initialFields); @@ -406,10 +388,31 @@ export const PivotGridComponent: React.FC = ({ } } - // ๋‚˜๋จธ์ง€ ์ƒํƒœ ๋ณต์› - if (parsed.pivotState) setPivotState(parsed.pivotState); + // pivotState ๋ณต์› ์‹œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (ํ™•์žฅ ๊ฒฝ๋กœ ๊ฒ€์ฆ) + if (parsed.pivotState && typeof parsed.pivotState === "object") { + const restoredState: PivotGridState = { + // expandedRowPaths๋Š” ๋ฐฐ์—ด์˜ ๋ฐฐ์—ด์ด์–ด์•ผ ํ•จ + expandedRowPaths: Array.isArray(parsed.pivotState.expandedRowPaths) + ? parsed.pivotState.expandedRowPaths.filter( + (p: unknown) => Array.isArray(p) && p.every(item => typeof item === "string") + ) + : [], + // expandedColumnPaths๋„ ๋™์ผํ•˜๊ฒŒ ๊ฒ€์ฆ + expandedColumnPaths: Array.isArray(parsed.pivotState.expandedColumnPaths) + ? parsed.pivotState.expandedColumnPaths.filter( + (p: unknown) => Array.isArray(p) && p.every(item => typeof item === "string") + ) + : [], + sortConfig: parsed.pivotState.sortConfig || null, + filterConfig: parsed.pivotState.filterConfig || {}, + }; + setPivotState(restoredState); + } + if (parsed.sortConfig) setSortConfig(parsed.sortConfig); - if (parsed.columnWidths) setColumnWidths(parsed.columnWidths); + if (parsed.columnWidths && typeof parsed.columnWidths === "object") { + setColumnWidths(parsed.columnWidths); + } } catch (e) { console.warn("ํ”ผ๋ฒ— ์ƒํƒœ ๋ณต์› ์‹คํŒจ, localStorage ์ดˆ๊ธฐํ™”:", e); // ์†์ƒ๋œ ์ƒํƒœ๋Š” ์ œ๊ฑฐ @@ -452,14 +455,6 @@ export const PivotGridComponent: React.FC = ({ const result = fields .filter((f) => f.area === "filter" && f.visible !== false) .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)); - - console.log("๐Ÿ”ท [filterFields] ํ•„ํ„ฐ ํ•„๋“œ ๊ณ„์‚ฐ:", { - totalFields: fields.length, - filterFieldsCount: result.length, - filterFieldNames: result.map(f => f.field), - allFieldAreas: fields.map(f => ({ field: f.field, area: f.area, visible: f.visible })), - }); - return result; }, [fields] @@ -524,51 +519,54 @@ export const PivotGridComponent: React.FC = ({ // ==================== ํ”ผ๋ฒ— ์ฒ˜๋ฆฌ ==================== const pivotResult = useMemo(() => { - if (!filteredData || filteredData.length === 0 || fields.length === 0) { + try { + if (!filteredData || filteredData.length === 0 || fields.length === 0) { + return null; + } + + // FieldChooser์—์„œ ์ด๋ฏธ ํ•„๋“œ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๋ฏ€๋กœ visible ํ•„ํ„ฐ๋ง ๋ถˆํ•„์š” + // ํ–‰, ์—ด, ๋ฐ์ดํ„ฐ ์˜์—ญ์— ํ•„๋“œ๊ฐ€ ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜ (ํ•„ํ„ฐ๋Š” ์ œ์™ธ) + if (fields.filter((f) => ["row", "column", "data"].includes(f.area)).length === 0) { + return null; + } + + const result = processPivotData( + filteredData, + fields, + pivotState.expandedRowPaths, + pivotState.expandedColumnPaths + ); + + return result; + } catch (error) { + console.error("โŒ [pivotResult] ํ”ผ๋ฒ— ์ฒ˜๋ฆฌ ์—๋Ÿฌ:", error); return null; } - - // FieldChooser์—์„œ ์ด๋ฏธ ํ•„๋“œ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๋ฏ€๋กœ visible ํ•„ํ„ฐ๋ง ๋ถˆํ•„์š” - // ํ–‰, ์—ด, ๋ฐ์ดํ„ฐ ์˜์—ญ์— ํ•„๋“œ๊ฐ€ ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜ (ํ•„ํ„ฐ๋Š” ์ œ์™ธ) - if (fields.filter((f) => ["row", "column", "data"].includes(f.area)).length === 0) { - return null; - } - - const result = processPivotData( - filteredData, - fields, - pivotState.expandedRowPaths, - pivotState.expandedColumnPaths - ); - - // ๐Ÿ†• ํ”ผ๋ฒ— ๊ฒฐ๊ณผ ํ™•์ธ - console.log("๐Ÿ”ถ ํ”ผ๋ฒ— ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ:", { - hasResult: !!result, - flatRowsCount: result?.flatRows?.length, - flatColumnsCount: result?.flatColumns?.length, - dataMatrixSize: result?.dataMatrix?.size, - expandedRowPaths: pivotState.expandedRowPaths.length, - expandedColumnPaths: pivotState.expandedColumnPaths.length, - }); - - return result; }, [filteredData, fields, pivotState.expandedRowPaths, pivotState.expandedColumnPaths]); // ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ ์ฒซ ๋ ˆ๋ฒจ ์ž๋™ ํ™•์žฅ useEffect(() => { - if (pivotResult && pivotResult.flatRows.length > 0 && !isInitialExpanded) { - // ์ฒซ ๋ ˆ๋ฒจ ํ–‰๋“ค์˜ ๊ฒฝ๋กœ ์ˆ˜์ง‘ (level 0์ธ ํ–‰๋“ค) - const firstLevelRows = pivotResult.flatRows.filter((row) => row.level === 0 && row.hasChildren); + try { + if (pivotResult && pivotResult.flatRows && pivotResult.flatRows.length > 0 && !isInitialExpanded) { + // ์ฒซ ๋ ˆ๋ฒจ ํ–‰๋“ค์˜ ๊ฒฝ๋กœ ์ˆ˜์ง‘ (level 0์ธ ํ–‰๋“ค) + const firstLevelRows = pivotResult.flatRows.filter((row) => row.level === 0 && row.hasChildren); - // ์ฒซ ๋ ˆ๋ฒจ ํ–‰์ด ์žˆ์œผ๋ฉด ์ž๋™ ํ™•์žฅ - if (firstLevelRows.length > 0) { - const firstLevelPaths = firstLevelRows.map((row) => row.path); - setPivotState((prev) => ({ - ...prev, - expandedRowPaths: firstLevelPaths, - })); - setIsInitialExpanded(true); + // ์ฒซ ๋ ˆ๋ฒจ ํ–‰์ด ์žˆ์œผ๋ฉด ์ž๋™ ํ™•์žฅ + if (firstLevelRows.length > 0 && firstLevelRows.length < 100) { + const firstLevelPaths = firstLevelRows.map((row) => row.path); + setPivotState((prev) => ({ + ...prev, + expandedRowPaths: firstLevelPaths, + })); + setIsInitialExpanded(true); + } else { + // ํ–‰์ด ๋„ˆ๋ฌด ๋งŽ์œผ๋ฉด ์ž๋™ ํ™•์žฅ ๊ฑด๋„ˆ๋›ฐ๊ธฐ + setIsInitialExpanded(true); + } } + } catch (error) { + console.error("โŒ [์ดˆ๊ธฐ ํ™•์žฅ] ์—๋Ÿฌ:", error); + setIsInitialExpanded(true); } }, [pivotResult, isInitialExpanded]); @@ -727,15 +725,6 @@ export const PivotGridComponent: React.FC = ({ // ํ•„๋“œ ๋ณ€๊ฒฝ const handleFieldsChange = useCallback( (newFields: PivotFieldConfig[]) => { - // FieldChooser์—์„œ ์ด๋ฏธ ํ•„๋“œ๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๋ฏ€๋กœ ์ถ”๊ฐ€ ํ•„ํ„ฐ๋ง ๋ถˆํ•„์š” - console.log("๐Ÿ”ท [handleFieldsChange] ํ•„๋“œ ๋ณ€๊ฒฝ:", { - totalFields: newFields.length, - filterFields: newFields.filter(f => f.area === "filter").length, - filterFieldNames: newFields.filter(f => f.area === "filter").map(f => f.field), - rowFields: newFields.filter(f => f.area === "row").length, - columnFields: newFields.filter(f => f.area === "column").length, - dataFields: newFields.filter(f => f.area === "data").length, - }); setFields(newFields); }, [] @@ -744,8 +733,6 @@ export const PivotGridComponent: React.FC = ({ // ํ–‰ ํ™•์žฅ/์ถ•์†Œ const handleToggleRowExpand = useCallback( (path: string[]) => { - console.log("๐Ÿ”ถ ํ–‰ ํ™•์žฅ/์ถ•์†Œ ํด๋ฆญ:", path); - setPivotState((prev) => { const pathKey = pathToKey(path); const existingIndex = prev.expandedRowPaths.findIndex( @@ -754,16 +741,13 @@ export const PivotGridComponent: React.FC = ({ let newPaths: string[][]; if (existingIndex >= 0) { - console.log("๐Ÿ”ถ ํ–‰ ์ถ•์†Œ:", path); newPaths = prev.expandedRowPaths.filter( (_, i) => i !== existingIndex ); } else { - console.log("๐Ÿ”ถ ํ–‰ ํ™•์žฅ:", path); newPaths = [...prev.expandedRowPaths, path]; } - console.log("๐Ÿ”ถ ์ƒˆ๋กœ์šด ํ™•์žฅ ๊ฒฝ๋กœ:", newPaths); onExpandChange?.(newPaths); return { @@ -777,59 +761,58 @@ export const PivotGridComponent: React.FC = ({ // ์ „์ฒด ํ™•์žฅ (์žฌ๊ท€์ ์œผ๋กœ ๋ชจ๋“  ๋ ˆ๋ฒจ ํ™•์žฅ) const handleExpandAll = useCallback(() => { - if (!pivotResult) { - console.log("โŒ [handleExpandAll] pivotResult๊ฐ€ ์—†์Œ"); - return; - } - - // ๐Ÿ†• ์žฌ๊ท€์ ์œผ๋กœ ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ ์ƒ์„ฑ - const allRowPaths: string[][] = []; - const rowFields = fields.filter((f) => f.area === "row" && f.visible !== false); - - // ๋ฐ์ดํ„ฐ์—์„œ ๋ชจ๋“  ๊ณ ์œ ํ•œ ๊ฒฝ๋กœ ์ถ”์ถœ - const pathSet = new Set(); - filteredData.forEach((item) => { - for (let depth = 1; depth <= rowFields.length; depth++) { - const path = rowFields.slice(0, depth).map((f) => String(item[f.field] ?? "")); - const pathKey = JSON.stringify(path); - pathSet.add(pathKey); + try { + if (!pivotResult) { + return; } - }); - // Set์„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ - pathSet.forEach((pathKey) => { - allRowPaths.push(JSON.parse(pathKey)); - }); + // ์žฌ๊ท€์ ์œผ๋กœ ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ๊ฒฝ๋กœ ์ƒ์„ฑ + const allRowPaths: string[][] = []; + const rowFields = fields.filter((f) => f.area === "row" && f.visible !== false); + + // ํ–‰ ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฉด ์ข…๋ฃŒ + if (rowFields.length === 0) { + return; + } + + // ๋ฐ์ดํ„ฐ์—์„œ ๋ชจ๋“  ๊ณ ์œ ํ•œ ๊ฒฝ๋กœ ์ถ”์ถœ + const pathSet = new Set(); + filteredData.forEach((item) => { + // ๋งˆ์ง€๋ง‰ ๋ ˆ๋ฒจ์€ ์ œ์™ธ (ํ™•์žฅํ•  ์ž์‹์ด ์—†์œผ๋ฏ€๋กœ) + for (let depth = 1; depth < rowFields.length; depth++) { + const path = rowFields.slice(0, depth).map((f) => String(item[f.field] ?? "")); + const pathKey = JSON.stringify(path); + pathSet.add(pathKey); + } + }); - console.log("๐Ÿ”ท [handleExpandAll] ํ™•์žฅํ•  ํ–‰:", { - totalRows: pivotResult.flatRows.length, - rowsWithChildren: allRowPaths.length, - paths: allRowPaths.slice(0, 5), // ์ฒ˜์Œ 5๊ฐœ๋งŒ ๋กœ๊ทธ - }); + // Set์„ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ (์ตœ๋Œ€ 1000๊ฐœ๋กœ ์ œํ•œํ•˜์—ฌ ์„ฑ๋Šฅ ๋ณดํ˜ธ) + const MAX_PATHS = 1000; + let count = 0; + pathSet.forEach((pathKey) => { + if (count < MAX_PATHS) { + allRowPaths.push(JSON.parse(pathKey)); + count++; + } + }); - setPivotState((prev) => ({ - ...prev, - expandedRowPaths: allRowPaths, - expandedColumnPaths: [], - })); + setPivotState((prev) => ({ + ...prev, + expandedRowPaths: allRowPaths, + expandedColumnPaths: [], + })); + } catch (error) { + console.error("โŒ [handleExpandAll] ์—๋Ÿฌ:", error); + } }, [pivotResult, fields, filteredData]); // ์ „์ฒด ์ถ•์†Œ const handleCollapseAll = useCallback(() => { - console.log("๐Ÿ”ท [handleCollapseAll] ์ „์ฒด ์ถ•์†Œ ์‹คํ–‰"); - - setPivotState((prev) => { - console.log("๐Ÿ”ท [handleCollapseAll] ์ด์ „ ์ƒํƒœ:", { - expandedRowPaths: prev.expandedRowPaths.length, - expandedColumnPaths: prev.expandedColumnPaths.length, - }); - - return { - ...prev, - expandedRowPaths: [], - expandedColumnPaths: [], - }; - }); + setPivotState((prev) => ({ + ...prev, + expandedRowPaths: [], + expandedColumnPaths: [], + })); }, []); // ์…€ ํด๋ฆญ diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx index 191f3610..61ebacd7 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridRenderer.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, Component, ErrorInfo, ReactNode } from "react"; import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer"; import { createComponentDefinition } from "../../utils/createComponentDefinition"; import { ComponentCategory } from "@/types/component"; @@ -8,6 +8,66 @@ import { PivotGridComponent } from "./PivotGridComponent"; import { PivotGridConfigPanel } from "./PivotGridConfigPanel"; import { PivotFieldConfig } from "./types"; import { dataApi } from "@/lib/api/data"; +import { AlertCircle, RefreshCw } from "lucide-react"; +import { Button } from "@/components/ui/button"; + +// ==================== ์—๋Ÿฌ ๊ฒฝ๊ณ„ ==================== + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; +} + +class PivotGridErrorBoundary extends Component< + { children: ReactNode; onReset?: () => void }, + ErrorBoundaryState +> { + constructor(props: { children: ReactNode; onReset?: () => void }) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error("๐Ÿ”ด [PivotGrid] ๋ Œ๋”๋ง ์—๋Ÿฌ:", error); + console.error("๐Ÿ”ด [PivotGrid] ์—๋Ÿฌ ์ •๋ณด:", errorInfo); + } + + handleReset = () => { + this.setState({ hasError: false, error: undefined }); + this.props.onReset?.(); + }; + + render() { + if (this.state.hasError) { + return ( +
+ +

+ ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ ์˜ค๋ฅ˜ +

+

+ {this.state.error?.message || "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."} +

+ +
+ ); + } + + return this.props.children; + } +} // ==================== ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ (๋ฏธ๋ฆฌ๋ณด๊ธฐ์šฉ) ==================== @@ -111,19 +171,14 @@ const PivotGridWrapper: React.FC = (props) => { setIsLoading(true); try { - console.log("๐Ÿ”ท [PivotGrid] ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ์ž‘:", tableName); - const response = await dataApi.getTableData(tableName, { page: 1, - size: 10000, // ํ”ผ๋ฒ— ๋ถ„์„์šฉ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ (pageSize โ†’ size) + size: 10000, // ํ”ผ๋ฒ— ๋ถ„์„์šฉ ๋Œ€๋Ÿ‰ ๋ฐ์ดํ„ฐ }); - console.log("๐Ÿ”ท [PivotGrid] API ์‘๋‹ต:", response); - // dataApi.getTableData๋Š” { data, total, page, size, totalPages } ๊ตฌ์กฐ if (response.data && Array.isArray(response.data)) { setLoadedData(response.data); - console.log("โœ… [PivotGrid] ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์™„๋ฃŒ:", response.data.length, "๊ฑด"); } else { console.error("โŒ [PivotGrid] ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹คํŒจ: ์‘๋‹ต์— data ๋ฐฐ์—ด์ด ์—†์Œ"); setLoadedData([]); @@ -137,21 +192,6 @@ const PivotGridWrapper: React.FC = (props) => { loadTableData(); }, [componentConfig.dataSource?.tableName, configData, props.isDesignMode]); - - // ๋””๋ฒ„๊น… ๋กœ๊ทธ - console.log("๐Ÿ”ท PivotGridWrapper props:", { - isDesignMode: props.isDesignMode, - isInteractive: props.isInteractive, - hasComponentConfig: !!props.componentConfig, - hasConfig: !!props.config, - hasData: !!configData, - dataLength: configData?.length, - hasLoadedData: loadedData.length > 0, - loadedDataLength: loadedData.length, - hasFields: !!configFields, - fieldsLength: configFields?.length, - isLoading, - }); // ๋””์ž์ธ ๋ชจ๋“œ ํŒ๋‹จ: // 1. isDesignMode === true @@ -173,13 +213,6 @@ const PivotGridWrapper: React.FC = (props) => { ? (componentConfig.title || props.title || "ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ") + " (๋ฏธ๋ฆฌ๋ณด๊ธฐ)" : (componentConfig.title || props.title); - console.log("๐Ÿ”ท PivotGridWrapper final:", { - isDesignMode, - usePreviewData, - finalDataLength: finalData?.length, - finalFieldsLength: finalFields?.length, - }); - // ์ด๊ณ„ ์„ค์ • const totalsConfig = componentConfig.totals || props.totals || { showRowGrandTotals: true, @@ -200,24 +233,27 @@ const PivotGridWrapper: React.FC = (props) => { ); } + // ์—๋Ÿฌ ๊ฒฝ๊ณ„๋กœ ๊ฐ์‹ธ์„œ ๋ Œ๋”๋ง ์—๋Ÿฌ ์‹œ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์™„์ „ํžˆ ์‚ฌ๋ผ์ง€์ง€ ์•Š๋„๋ก ํ•จ return ( - + + + ); }; @@ -283,18 +319,6 @@ export class PivotGridRenderer extends AutoRegisteringComponentRenderer { const componentConfig = props.componentConfig || props.config || {}; const configFields = componentConfig.fields || props.fields; const configData = props.data; - - // ๋””๋ฒ„๊น… ๋กœ๊ทธ - console.log("๐Ÿ”ท PivotGridRenderer props:", { - isDesignMode: props.isDesignMode, - isInteractive: props.isInteractive, - hasComponentConfig: !!props.componentConfig, - hasConfig: !!props.config, - hasData: !!configData, - dataLength: configData?.length, - hasFields: !!configFields, - fieldsLength: configFields?.length, - }); // ๋””์ž์ธ ๋ชจ๋“œ ํŒ๋‹จ: // 1. isDesignMode === true @@ -314,13 +338,6 @@ export class PivotGridRenderer extends AutoRegisteringComponentRenderer { ? (componentConfig.title || props.title || "ํ”ผ๋ฒ— ๊ทธ๋ฆฌ๋“œ") + " (๋ฏธ๋ฆฌ๋ณด๊ธฐ)" : (componentConfig.title || props.title); - console.log("๐Ÿ”ท PivotGridRenderer final:", { - isDesignMode, - usePreviewData, - finalDataLength: finalData?.length, - finalFieldsLength: finalFields?.length, - }); - // ์ด๊ณ„ ์„ค์ • const totalsConfig = componentConfig.totals || props.totals || { showRowGrandTotals: true, diff --git a/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx b/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx index a948aba0..fba64e65 100644 --- a/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx +++ b/frontend/lib/registry/components/pivot-grid/components/FieldChooser.tsx @@ -267,13 +267,9 @@ export const FieldChooser: React.FC = ({ const existingConfig = selectedFields.find((f) => f.field === field.field); if (area === "none") { - // ๐Ÿ†• ํ•„๋“œ ์™„์ „ ์ œ๊ฑฐ (visible: false ๋Œ€์‹  ๋ฐฐ์—ด์—์„œ ์ œ๊ฑฐ) + // ํ•„๋“œ ์™„์ „ ์ œ๊ฑฐ (visible: false ๋Œ€์‹  ๋ฐฐ์—ด์—์„œ ์ œ๊ฑฐ) if (existingConfig) { const newFields = selectedFields.filter((f) => f.field !== field.field); - console.log("๐Ÿ”ท [FieldChooser] ํ•„๋“œ ์ œ๊ฑฐ:", { - removedField: field.field, - remainingFields: newFields.length, - }); onFieldsChange(newFields); } } else { @@ -284,10 +280,6 @@ export const FieldChooser: React.FC = ({ ? { ...f, area, visible: true } : f ); - console.log("๐Ÿ”ท [FieldChooser] ํ•„๋“œ ์˜์—ญ ๋ณ€๊ฒฝ:", { - field: field.field, - newArea: area, - }); onFieldsChange(newFields); } else { // ์ƒˆ ํ•„๋“œ ์ถ”๊ฐ€ @@ -300,10 +292,6 @@ export const FieldChooser: React.FC = ({ summaryType: area === "data" ? "sum" : undefined, areaIndex: selectedFields.filter((f) => f.area === area).length, }; - console.log("๐Ÿ”ท [FieldChooser] ํ•„๋“œ ์ถ”๊ฐ€:", { - field: field.field, - area, - }); onFieldsChange([...selectedFields, newField]); } } diff --git a/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx b/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx index 08dca70e..967afd08 100644 --- a/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx +++ b/frontend/lib/registry/components/pivot-grid/components/FieldPanel.tsx @@ -360,7 +360,6 @@ export const FieldPanel: React.FC = ({ // 1. overId๊ฐ€ ์˜์—ญ ์ž์ฒด์ธ ๊ฒฝ์šฐ (filter, column, row, data) if (["filter", "column", "row", "data"].includes(overId)) { setOverArea(overId as PivotAreaType); - console.log("๐Ÿ”ท [handleDragOver] ์˜์—ญ ๊ฐ์ง€:", overId); return; } @@ -368,7 +367,6 @@ export const FieldPanel: React.FC = ({ const targetArea = overId.split("-")[0] as PivotAreaType; if (["filter", "column", "row", "data"].includes(targetArea)) { setOverArea(targetArea); - console.log("๐Ÿ”ท [handleDragOver] ํ•„๋“œ ์˜์—ญ ๊ฐ์ง€:", targetArea); } }; @@ -380,19 +378,12 @@ export const FieldPanel: React.FC = ({ setOverArea(null); if (!over) { - console.log("๐Ÿ”ท [FieldPanel] ๋“œ๋กญ ๋Œ€์ƒ ์—†์Œ"); return; } const activeId = active.id as string; const overId = over.id as string; - console.log("๐Ÿ”ท [FieldPanel] ๋“œ๋ž˜๊ทธ ์ข…๋ฃŒ:", { - activeId, - overId, - detectedOverArea: currentOverArea, - }); - // ํ•„๋“œ ์ •๋ณด ํŒŒ์‹ฑ const [sourceArea, sourceField] = activeId.split("-") as [ PivotAreaType, @@ -409,13 +400,6 @@ export const FieldPanel: React.FC = ({ targetArea = overId.split("-")[0] as PivotAreaType; } - console.log("๐Ÿ”ท [FieldPanel] ํŒŒ์‹ฑ ๊ฒฐ๊ณผ:", { - sourceArea, - sourceField, - targetArea, - usedOverArea: !!currentOverArea, - }); - // ๊ฐ™์€ ์˜์—ญ ๋‚ด ์ •๋ ฌ if (sourceArea === targetArea) { const areaFields = fields.filter((f) => f.area === sourceArea); @@ -447,12 +431,6 @@ export const FieldPanel: React.FC = ({ // ๋‹ค๋ฅธ ์˜์—ญ์œผ๋กœ ์ด๋™ if (["filter", "column", "row", "data"].includes(targetArea)) { - console.log("๐Ÿ”ท [FieldPanel] ์˜์—ญ ์ด๋™:", { - field: sourceField, - from: sourceArea, - to: targetArea, - }); - const newFields = fields.map((f) => { if (f.field === sourceField && f.area === sourceArea) { return { @@ -464,12 +442,6 @@ export const FieldPanel: React.FC = ({ return f; }); - console.log("๐Ÿ”ท [FieldPanel] ๋ณ€๊ฒฝ๋œ ํ•„๋“œ:", { - totalFields: newFields.length, - filterFields: newFields.filter(f => f.area === "filter").length, - changedField: newFields.find(f => f.field === sourceField), - }); - onFieldsChange(newFields); } }; diff --git a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts index 4d3fecfd..702a13e5 100644 --- a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts +++ b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts @@ -728,9 +728,15 @@ export function processPivotData( } } - // ํ™•์žฅ ๊ฒฝ๋กœ Set ๋ณ€ํ™˜ - const expandedRowSet = new Set(expandedRowPaths.map(pathToKey)); - const expandedColSet = new Set(expandedColumnPaths.map(pathToKey)); + // ํ™•์žฅ ๊ฒฝ๋กœ Set ๋ณ€ํ™˜ (์ž˜๋ชป๋œ ํ˜•์‹ ํ•„ํ„ฐ๋ง) + const validRowPaths = (expandedRowPaths || []).filter( + (p): p is string[] => Array.isArray(p) && p.length > 0 && p.every(item => typeof item === "string") + ); + const validColPaths = (expandedColumnPaths || []).filter( + (p): p is string[] => Array.isArray(p) && p.length > 0 && p.every(item => typeof item === "string") + ); + const expandedRowSet = new Set(validRowPaths.map(pathToKey)); + const expandedColSet = new Set(validColPaths.map(pathToKey)); // ๊ธฐ๋ณธ ํ™•์žฅ: ์ฒซ ๋ฒˆ์งธ ๋ ˆ๋ฒจ ๋ชจ๋‘ ํ™•์žฅ if (expandedRowPaths.length === 0 && rowFields.length > 0) { From 8603fddbcb8d41e56443295d140978ccad5dc246 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 19 Jan 2026 09:50:25 +0900 Subject: [PATCH 12/16] =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EB=8B=A4=EC=8B=9C..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pivot-grid/PivotGridComponent.tsx | 46 ++++++++++++++----- .../pivot-grid/hooks/useVirtualScroll.ts | 8 +++- .../pivot-grid/utils/pivotEngine.ts | 20 ++------ 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index feda2167..57bc2e8a 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -376,6 +376,12 @@ export const PivotGridComponent: React.FC = ({ const parsed = JSON.parse(savedState); + // ๋ฒ„์ „ ์ฒดํฌ - ๋ฒ„์ „์ด ๋‹ค๋ฅด๋ฉด ์ด์ „ ์ƒํƒœ ๋ฌด์‹œ + if (parsed.version !== PIVOT_STATE_VERSION) { + localStorage.removeItem(stateStorageKey); + return; + } + // ํ•„๋“œ ๋ณต์› ์‹œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ (์ค‘์š”!) if (parsed.fields && Array.isArray(parsed.fields) && parsed.fields.length > 0) { // ์ €์žฅ๋œ ํ•„๋“œ๊ฐ€ ํ˜„์žฌ ๋ฐ์ดํ„ฐ์™€ ํ˜ธํ™˜๋˜๋Š”์ง€ ํ™•์ธ @@ -501,19 +507,31 @@ export const PivotGridComponent: React.FC = ({ if (activeFilters.length === 0) return data; - return data.filter((row) => { + const result = data.filter((row) => { return activeFilters.every((filter) => { - const value = row[filter.field]; + const rawValue = row[filter.field]; const filterValues = filter.filterValues || []; const filterType = filter.filterType || "include"; + // ํƒ€์ž… ์•ˆ์ „ํ•œ ๋น„๊ต: ๊ฐ’์„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ๋น„๊ต + const value = rawValue === null || rawValue === undefined + ? "(๋นˆ ๊ฐ’)" + : String(rawValue); + if (filterType === "include") { - return filterValues.includes(value); + return filterValues.some((fv) => String(fv) === value); } else { - return !filterValues.includes(value); + return filterValues.every((fv) => String(fv) !== value); } }); }); + + // ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„ํ„ฐ๋ง๋˜๋ฉด ๊ฒฝ๊ณ  (๋””๋ฒ„๊น…์šฉ) + if (result.length === 0 && data.length > 0) { + console.warn("โš ๏ธ [PivotGrid] ํ•„ํ„ฐ๋กœ ์ธํ•ด ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์ œ๊ฑฐ๋จ"); + } + + return result; }, [data, fields]); // ==================== ํ”ผ๋ฒ— ์ฒ˜๋ฆฌ ==================== @@ -1654,7 +1672,10 @@ export const PivotGridComponent: React.FC = ({
0 ? containerHeight : undefined, + minHeight: 100 // ์ตœ์†Œ ๋†’์ด ๋ณด์žฅ - ๋ธ”๋ผ์ธ๋“œ ํšจ๊ณผ ๋ฐฉ์ง€ + }} tabIndex={0} onKeyDown={handleKeyDown} > @@ -1929,12 +1950,15 @@ export const PivotGridComponent: React.FC = ({ }); })()} - {/* ๊ฐ€์ƒ ์Šคํฌ๋กค ํ•˜๋‹จ ์—ฌ๋ฐฑ */} - {enableVirtualScroll && ( -
- - )} + {/* ๊ฐ€์ƒ ์Šคํฌ๋กค ํ•˜๋‹จ ์—ฌ๋ฐฑ - ์Œ์ˆ˜ ๋ฐฉ์ง€ */} + {enableVirtualScroll && (() => { + const bottomPadding = Math.max(0, virtualScroll.totalHeight - virtualScroll.offsetTop - (visibleFlatRows.length * ROW_HEIGHT)); + return bottomPadding > 0 ? ( + + + ) : null; + })()} {/* ์—ด ์ด๊ณ„ ํ–‰ (ํ•˜๋‹จ ์œ„์น˜ - ๊ธฐ๋ณธ๊ฐ’) */} {totals?.showColumnGrandTotals && totals?.rowGrandTotalPosition !== "top" && ( diff --git a/frontend/lib/registry/components/pivot-grid/hooks/useVirtualScroll.ts b/frontend/lib/registry/components/pivot-grid/hooks/useVirtualScroll.ts index 152cb2df..6557dee3 100644 --- a/frontend/lib/registry/components/pivot-grid/hooks/useVirtualScroll.ts +++ b/frontend/lib/registry/components/pivot-grid/hooks/useVirtualScroll.ts @@ -51,14 +51,18 @@ export function useVirtualScroll(options: VirtualScrollOptions): VirtualScrollRe // ๋ณด์ด๋Š” ์•„์ดํ…œ ์ˆ˜ const visibleCount = Math.ceil(containerHeight / itemHeight); - // ์‹œ์ž‘/๋ ์ธ๋ฑ์Šค ๊ณ„์‚ฐ + // ์‹œ์ž‘/๋ ์ธ๋ฑ์Šค ๊ณ„์‚ฐ (์Œ์ˆ˜ ๋ฐฉ์ง€) const { startIndex, endIndex } = useMemo(() => { + // itemCount๊ฐ€ 0์ด๋ฉด ๋นˆ ๋ฐฐ์—ด + if (itemCount === 0) { + return { startIndex: 0, endIndex: -1 }; + } const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan); const end = Math.min( itemCount - 1, Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan ); - return { startIndex: start, endIndex: end }; + return { startIndex: start, endIndex: Math.max(start, end) }; // end๊ฐ€ start๋ณด๋‹ค ์ž‘์ง€ ์•Š๋„๋ก }, [scrollTop, itemHeight, containerHeight, itemCount, overscan]); // ์ „์ฒด ๋†’์ด diff --git a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts index 702a13e5..02dd4608 100644 --- a/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts +++ b/frontend/lib/registry/components/pivot-grid/utils/pivotEngine.ts @@ -710,23 +710,9 @@ export function processPivotData( .filter((f) => f.area === "data" && f.visible !== false) .sort((a, b) => (a.areaIndex || 0) - (b.areaIndex || 0)); - const filterFields = fields.filter( - (f) => f.area === "filter" && f.visible !== false - ); - - // ํ•„ํ„ฐ ์ ์šฉ - let filteredData = data; - for (const filterField of filterFields) { - if (filterField.filterValues && filterField.filterValues.length > 0) { - filteredData = filteredData.filter((row) => { - const value = getFieldValue(row, filterField); - if (filterField.filterType === "exclude") { - return !filterField.filterValues!.includes(value); - } - return filterField.filterValues!.includes(value); - }); - } - } + // ์ฐธ๊ณ : ํ•„ํ„ฐ๋ง์€ PivotGridComponent์—์„œ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋จ + // ์—ฌ๊ธฐ์„œ๋Š” ์ถ”๊ฐ€ ํ•„ํ„ฐ๋ง ์—†์ด ์ „๋‹ฌ๋ฐ›์€ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ + const filteredData = data; // ํ™•์žฅ ๊ฒฝ๋กœ Set ๋ณ€ํ™˜ (์ž˜๋ชป๋œ ํ˜•์‹ ํ•„ํ„ฐ๋ง) const validRowPaths = (expandedRowPaths || []).filter( From f2ab4f11bda249fe76d4f0a4394eb5ec2d36202d Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 19 Jan 2026 10:14:20 +0900 Subject: [PATCH 13/16] =?UTF-8?q?=EC=A7=84=EC=A7=9C=20=ED=95=B4=EA=B2=B0?= =?UTF-8?q?=ED=96=88=EC=9D=8C=20=EC=A7=84=EC=A7=9C=EC=A7=84=EC=A7=9C?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../registry/components/pivot-grid/PivotGridComponent.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx index 57bc2e8a..53ad204d 100644 --- a/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx +++ b/frontend/lib/registry/components/pivot-grid/PivotGridComponent.tsx @@ -1674,7 +1674,11 @@ export const PivotGridComponent: React.FC = ({ className="flex-1 overflow-auto focus:outline-none" style={{ maxHeight: enableVirtualScroll && containerHeight > 0 ? containerHeight : undefined, - minHeight: 100 // ์ตœ์†Œ ๋†’์ด ๋ณด์žฅ - ๋ธ”๋ผ์ธ๋“œ ํšจ๊ณผ ๋ฐฉ์ง€ + // ์ตœ์†Œ 200px ๋ณด์žฅ + ๋ฐ์ดํ„ฐ์— ๋งž๊ฒŒ ์กฐ์ • (์ตœ๋Œ€ 400px) + minHeight: Math.max( + 200, // ์ ˆ๋Œ€ ์ตœ์†Œ๊ฐ’ - ๋ธ”๋ผ์ธ๋“œ ํšจ๊ณผ ๋ฐฉ์ง€ + Math.min(400, (sortedFlatRows.length + 3) * ROW_HEIGHT + 50) + ) }} tabIndex={0} onKeyDown={handleKeyDown} From c282d5c611bfa0bac9e43a7f478dbffa1ccdd1a3 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 19 Jan 2026 10:14:48 +0900 Subject: [PATCH 14/16] Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into lhj From e4667cce5f32c93f00d233e5fd29b5e814118850 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 19 Jan 2026 12:07:29 +0900 Subject: [PATCH 15/16] =?UTF-8?q?refactor:=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=BF=BC=EB=A6=AC=20=EB=B0=8F=20=EB=A1=9C=EA=B9=85?= =?UTF-8?q?=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ๋‹ค์ค‘ ๊ฐ’ ๋ฐฐ์—ด ๊ฒ€์ƒ‰ ์‹œ ์กฐ๊ฑด ์ฒ˜๋ฆฌ ๋กœ์ง ๊ฐœ์„  - ์ฟผ๋ฆฌ์—์„œ main. ์ ‘๋‘์‚ฌ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ช…ํ™•ํ•œ ํ…Œ์ด๋ธ” ์ฐธ์กฐ ๋ณด์žฅ - ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ ์ œ๊ฑฐ ๋ฐ ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ - ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ๋กœ๊น… ๊ฐœ์„ ์œผ๋กœ ๋””๋ฒ„๊น… ์šฉ์ด์„ฑ ์ฆ๊ฐ€ - ์ƒˆ๋กœ์šด ์ˆ˜์ฃผ๊ด€๋ฆฌ ๋ฐ ๊ฑฐ๋ž˜์ฒ˜ ํ…Œ์ด๋ธ” ์ถ”๊ฐ€๋กœ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ์ง€์› ๊ฐ•ํ™” --- .cursor/rules/multilang-component-guide.mdc | 2 +- .../src/services/tableManagementService.ts | 113 ++++++++++++------ .../SplitPanelLayoutComponent.tsx | 7 +- 3 files changed, 85 insertions(+), 37 deletions(-) diff --git a/.cursor/rules/multilang-component-guide.mdc b/.cursor/rules/multilang-component-guide.mdc index 60bdc0ec..97140312 100644 --- a/.cursor/rules/multilang-component-guide.mdc +++ b/.cursor/rules/multilang-component-guide.mdc @@ -140,7 +140,7 @@ if (comp.componentType === "my-new-component") { if (config?.title) { addLabel({ id: `${comp.id}_title`, - componentId: `${comp.id}_title`, + componentId: `${comp.id}_title`,- label: config.title, type: "title", parentType: "my-new-component", diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 2e67040a..9dad459c 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1314,7 +1314,7 @@ export class TableManagementService { // ๊ฐ ๊ฐ’์„ LIKE ๋˜๋Š” = ์กฐ๊ฑด์œผ๋กœ ์ฒ˜๋ฆฌ const conditions: string[] = []; const values: any[] = []; - + value.forEach((v: any, idx: number) => { const safeValue = String(v).trim(); // ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜๊ฑฐ๋‚˜, ์ฝค๋งˆ๋กœ ๊ตฌ๋ถ„๋œ ๊ฐ’ ์ค‘ ํ•˜๋‚˜๋กœ ํฌํ•จ @@ -1323,17 +1323,24 @@ export class TableManagementService { // - "2," ๋กœ ์‹œ์ž‘ // - ",2" ๋กœ ๋๋‚จ // - ",2," ์ค‘๊ฐ„์— ํฌํ•จ - const paramBase = paramIndex + (idx * 4); + const paramBase = paramIndex + idx * 4; conditions.push(`( ${columnName}::text = $${paramBase} OR ${columnName}::text LIKE $${paramBase + 1} OR ${columnName}::text LIKE $${paramBase + 2} OR ${columnName}::text LIKE $${paramBase + 3} )`); - values.push(safeValue, `${safeValue},%`, `%,${safeValue}`, `%,${safeValue},%`); + values.push( + safeValue, + `${safeValue},%`, + `%,${safeValue}`, + `%,${safeValue},%` + ); }); - logger.info(`๐Ÿ” ๋‹ค์ค‘ ๊ฐ’ ๋ฐฐ์—ด ๊ฒ€์ƒ‰: ${columnName} IN [${value.join(", ")}]`); + logger.info( + `๐Ÿ” ๋‹ค์ค‘ ๊ฐ’ ๋ฐฐ์—ด ๊ฒ€์ƒ‰: ${columnName} IN [${value.join(", ")}]` + ); return { whereClause: `(${conditions.join(" OR ")})`, values, @@ -1772,21 +1779,29 @@ export class TableManagementService { // contains ์—ฐ์‚ฐ์ž (๊ธฐ๋ณธ): ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ์œผ๋กœ ๊ฒ€์ƒ‰ const referenceColumn = entityTypeInfo.referenceColumn || "id"; const referenceTable = entityTypeInfo.referenceTable; - + // displayColumn์ด ๋น„์–ด์žˆ๊ฑฐ๋‚˜ "none"์ด๋ฉด ์ฐธ์กฐ ํ…Œ์ด๋ธ”์—์„œ ์ž๋™ ๊ฐ์ง€ (entityJoinService์™€ ๋™์ผํ•œ ๋กœ์ง) let displayColumn = entityTypeInfo.displayColumn; - if (!displayColumn || displayColumn === "none" || displayColumn === "") { - displayColumn = await this.findDisplayColumnForTable(referenceTable, referenceColumn); + if ( + !displayColumn || + displayColumn === "none" || + displayColumn === "" + ) { + displayColumn = await this.findDisplayColumnForTable( + referenceTable, + referenceColumn + ); logger.info( `๐Ÿ” [buildEntitySearchCondition] displayColumn ์ž๋™ ๊ฐ์ง€: ${referenceTable} -> ${displayColumn}` ); } // ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ์œผ๋กœ ๊ฒ€์ƒ‰ + // ๐Ÿ”ง main. ์ ‘๋‘์‚ฌ ์ถ”๊ฐ€: EXISTS ์„œ๋ธŒ์ฟผ๋ฆฌ์—์„œ ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์ฐธ์กฐ ์‹œ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ • return { whereClause: `EXISTS ( SELECT 1 FROM ${referenceTable} ref - WHERE ref.${referenceColumn} = ${columnName} + WHERE ref.${referenceColumn} = main.${columnName} AND ref.${displayColumn} ILIKE $${paramIndex} )`, values: [`%${value}%`], @@ -2150,14 +2165,14 @@ export class TableManagementService { // ์•ˆ์ „ํ•œ ํ…Œ์ด๋ธ”๋ช… ๊ฒ€์ฆ const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, ""); - // ์ „์ฒด ๊ฐœ์ˆ˜ ์กฐํšŒ - const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`; + // ์ „์ฒด ๊ฐœ์ˆ˜ ์กฐํšŒ (main ๋ณ„์นญ ์ถ”๊ฐ€ - buildWhereClause๊ฐ€ main. ์ ‘๋‘์‚ฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ํ•„์š”) + const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} main ${whereClause}`; const countResult = await query(countQuery, searchValues); const total = parseInt(countResult[0].count); - // ๋ฐ์ดํ„ฐ ์กฐํšŒ + // ๋ฐ์ดํ„ฐ ์กฐํšŒ (main ๋ณ„์นญ ์ถ”๊ฐ€) const dataQuery = ` - SELECT * FROM ${safeTableName} + SELECT main.* FROM ${safeTableName} main ${whereClause} ${orderClause} LIMIT $${paramIndex} OFFSET $${paramIndex + 1} @@ -2494,7 +2509,7 @@ export class TableManagementService { skippedColumns.push(column); return; } - + const dataType = columnTypeMap.get(column) || "text"; setConditions.push( `"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}` @@ -2506,7 +2521,9 @@ export class TableManagementService { }); if (skippedColumns.length > 0) { - logger.info(`โš ๏ธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปฌ๋Ÿผ ์Šคํ‚ต: ${skippedColumns.join(", ")}`); + logger.info( + `โš ๏ธ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ปฌ๋Ÿผ ์Šคํ‚ต: ${skippedColumns.join(", ")}` + ); } // WHERE ์กฐ๊ฑด ์ƒ์„ฑ (PRIMARY KEY ์šฐ์„ , ์—†์œผ๋ฉด ๋ชจ๋“  ์›๋ณธ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ) @@ -2776,10 +2793,14 @@ export class TableManagementService { // ์‹ค์ œ ์†Œ์Šค ์ปฌ๋Ÿผ์ด partner_id์ธ๋ฐ ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ customer_id๋กœ ์ถ”๋ก ํ•˜๋Š” ๊ฒฝ์šฐ ๋Œ€์‘ if (!baseJoinConfig && (additionalColumn as any).referenceTable) { baseJoinConfig = joinConfigs.find( - (config) => config.referenceTable === (additionalColumn as any).referenceTable + (config) => + config.referenceTable === + (additionalColumn as any).referenceTable ); if (baseJoinConfig) { - logger.info(`๐Ÿ”„ referenceTable๋กœ ์กฐ์ธ ์„ค์ • ์ฐพ์Œ: ${(additionalColumn as any).referenceTable} โ†’ ${baseJoinConfig.sourceColumn}`); + logger.info( + `๐Ÿ”„ referenceTable๋กœ ์กฐ์ธ ์„ค์ • ์ฐพ์Œ: ${(additionalColumn as any).referenceTable} โ†’ ${baseJoinConfig.sourceColumn}` + ); } } @@ -2787,25 +2808,31 @@ export class TableManagementService { // joinAlias์—์„œ ์‹ค์ œ ์ปฌ๋Ÿผ๋ช… ์ถ”์ถœ const sourceColumn = baseJoinConfig.sourceColumn; // ์‹ค์ œ ์†Œ์Šค ์ปฌ๋Ÿผ (์˜ˆ: partner_id) const originalJoinAlias = additionalColumn.joinAlias; // ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๋ณด๋‚ธ ๋ณ„์นญ (์˜ˆ: customer_id_customer_name) - + // ๐Ÿ”„ ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์ž˜๋ชป๋œ ์†Œ์Šค ์ปฌ๋Ÿผ์œผ๋กœ ์ถ”๋ก ํ•œ ๊ฒฝ์šฐ ์ฒ˜๋ฆฌ // customer_id_customer_name โ†’ customer_name ์ถ”์ถœ (customer_id_ ๋ถ€๋ถ„ ์ œ๊ฑฐ) // ๋˜๋Š” partner_id_customer_name โ†’ customer_name ์ถ”์ถœ (partner_id_ ๋ถ€๋ถ„ ์ œ๊ฑฐ) let actualColumnName: string; - + // ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๋ณด๋‚ธ joinAlias์—์„œ ์‹ค์ œ ์ปฌ๋Ÿผ๋ช… ์ถ”์ถœ const frontendSourceColumn = additionalColumn.sourceColumn; // ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์ถ”๋ก ํ•œ ์†Œ์Šค ์ปฌ๋Ÿผ (customer_id) if (originalJoinAlias.startsWith(`${frontendSourceColumn}_`)) { // ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ์ถ”๋ก ํ•œ ์†Œ์Šค ์ปฌ๋Ÿผ์œผ๋กœ ์‹œ์ž‘ํ•˜๋ฉด ๊ทธ ๋ถ€๋ถ„ ์ œ๊ฑฐ - actualColumnName = originalJoinAlias.replace(`${frontendSourceColumn}_`, ""); + actualColumnName = originalJoinAlias.replace( + `${frontendSourceColumn}_`, + "" + ); } else if (originalJoinAlias.startsWith(`${sourceColumn}_`)) { // ์‹ค์ œ ์†Œ์Šค ์ปฌ๋Ÿผ์œผ๋กœ ์‹œ์ž‘ํ•˜๋ฉด ๊ทธ ๋ถ€๋ถ„ ์ œ๊ฑฐ - actualColumnName = originalJoinAlias.replace(`${sourceColumn}_`, ""); + actualColumnName = originalJoinAlias.replace( + `${sourceColumn}_`, + "" + ); } else { // ์–ด๋А ๊ฒƒ๋„ ์•„๋‹ˆ๋ฉด ์›๋ณธ ์‚ฌ์šฉ actualColumnName = originalJoinAlias; } - + // ๐Ÿ†• ์˜ฌ๋ฐ”๋ฅธ joinAlias ์žฌ์ƒ์„ฑ (์‹ค์ œ ์†Œ์Šค ์ปฌ๋Ÿผ ๊ธฐ๋ฐ˜) const correctedJoinAlias = `${sourceColumn}_${actualColumnName}`; @@ -3199,8 +3226,10 @@ export class TableManagementService { } // Entity ์กฐ์ธ ์ปฌ๋Ÿผ ๊ฒ€์ƒ‰์ด ์žˆ๋Š”์ง€ ํ™•์ธ (๊ธฐ๋ณธ ์กฐ์ธ + ์ถ”๊ฐ€ ์กฐ์ธ ์ปฌ๋Ÿผ ๋ชจ๋‘ ํฌํ•จ) + // ๐Ÿ”ง sourceColumn๋„ ํฌํ•จ: search={"order_no":"..."} ํ˜•ํƒœ๋„ Entity ๊ฒ€์ƒ‰์œผ๋กœ ์ธ์‹ const allEntityColumns = [ ...joinConfigs.map((config) => config.aliasColumn), + ...joinConfigs.map((config) => config.sourceColumn), // ๐Ÿ”ง ์†Œ์Šค ์ปฌ๋Ÿผ๋„ ํฌํ•จ // ์ถ”๊ฐ€ ์กฐ์ธ ์ปฌ๋Ÿผ๋“ค๋„ ํฌํ•จ (writer_dept_code, company_code_status ๋“ฑ) ...joinConfigs.flatMap((config) => { const additionalColumns = []; @@ -3606,8 +3635,10 @@ export class TableManagementService { }); // main. ์ ‘๋‘์‚ฌ ์ถ”๊ฐ€ (์กฐ์ธ ์ฟผ๋ฆฌ์šฉ) + // ๐Ÿ”ง ์ด๋ฏธ ์ ‘๋‘์‚ฌ(. ์•ž)๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ๋Š” ๊ต์ฒดํ•˜์ง€ ์•Š์Œ (ref.column, main.column ๋“ฑ) + // Negative lookbehind (?> { + ): Promise< + Array<{ + leftColumn: string; + rightColumn: string; + direction: "left_to_right" | "right_to_left"; + inputType: string; + displayColumn?: string; + }> + > { try { - logger.info(`๋‘ ํ…Œ์ด๋ธ” ๊ฐ„ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์‹œ์ž‘: ${leftTable} <-> ${rightTable}`); - + logger.info( + `๋‘ ํ…Œ์ด๋ธ” ๊ฐ„ ์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์‹œ์ž‘: ${leftTable} <-> ${rightTable}` + ); + const relations: Array<{ leftColumn: string; rightColumn: string; @@ -4806,12 +4844,17 @@ export class TableManagementService { logger.info(`์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์™„๋ฃŒ: ${relations.length}๊ฐœ ๋ฐœ๊ฒฌ`); relations.forEach((rel, idx) => { - logger.info(` ${idx + 1}. ${leftTable}.${rel.leftColumn} <-> ${rightTable}.${rel.rightColumn} (${rel.direction})`); + logger.info( + ` ${idx + 1}. ${leftTable}.${rel.leftColumn} <-> ${rightTable}.${rel.rightColumn} (${rel.direction})` + ); }); return relations; } catch (error) { - logger.error(`์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์‹คํŒจ: ${leftTable} <-> ${rightTable}`, error); + logger.error( + `์—”ํ‹ฐํ‹ฐ ๊ด€๊ณ„ ๊ฐ์ง€ ์‹คํŒจ: ${leftTable} <-> ${rightTable}`, + error + ); return []; } } diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 50f7c41b..0113a9a8 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -917,10 +917,15 @@ export const SplitPanelLayoutComponent: React.FC const { entityJoinApi } = await import("@/lib/api/entityJoin"); // ๋ณตํ•ฉํ‚ค ์กฐ๊ฑด ์ƒ์„ฑ + // ๐Ÿ”ง ๊ด€๊ณ„ ํ•„ํ„ฐ๋ง์€ ์ •ํ™•ํ•œ ๊ฐ’ ๋งค์นญ์ด ํ•„์š”ํ•˜๋ฏ€๋กœ equals ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ + // (entity ํƒ€์ž… ์ปฌ๋Ÿผ์˜ ๊ฒฝ์šฐ ๊ธฐ๋ณธ contains ์—ฐ์‚ฐ์ž๊ฐ€ ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ํ‘œ์‹œ ์ปฌ๋Ÿผ์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜์—ฌ ์‹คํŒจํ•จ) const searchConditions: Record = {}; keys.forEach((key) => { if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) { - searchConditions[key.rightColumn] = leftItem[key.leftColumn]; + searchConditions[key.rightColumn] = { + value: leftItem[key.leftColumn], + operator: "equals", + }; } }); From d09a6977f7083b394d0837aa248d699fa00c8799 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Mon, 19 Jan 2026 17:25:12 +0900 Subject: [PATCH 16/16] =?UTF-8?q?=EA=B2=80=EC=83=89=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=97=85=EA=B7=B8=EB=A0=88=EC=9D=B4=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../table-list/TableListComponent.tsx | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 78abf111..366aa05b 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -41,7 +41,7 @@ import { Lock, } from "lucide-react"; import * as XLSX from "xlsx"; -import { FileText, ChevronRightIcon } from "lucide-react"; +import { FileText, ChevronRightIcon, Search } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; @@ -455,6 +455,7 @@ export const TableListComponent: React.FC = ({ // ๐Ÿ†• ์ปฌ๋Ÿผ ํ—ค๋” ํ•„ํ„ฐ ์ƒํƒœ (์ƒ๋‹จ์—์„œ ์„ ์–ธ) const [headerFilters, setHeaderFilters] = useState>>({}); + const [headerLikeFilters, setHeaderLikeFilters] = useState>({}); // LIKE ๊ฒ€์ƒ‰์šฉ const [openFilterColumn, setOpenFilterColumn] = useState(null); // ๐Ÿ†• Filter Builder (๊ณ ๊ธ‰ ํ•„ํ„ฐ) ๊ด€๋ จ ์ƒํƒœ - filteredData๋ณด๋‹ค ๋จผ์ € ์ •์˜ํ•ด์•ผ ํ•จ @@ -488,6 +489,22 @@ export const TableListComponent: React.FC = ({ }); } + // 2-1. ๐Ÿ†• LIKE ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ ์šฉ + if (Object.keys(headerLikeFilters).length > 0) { + result = result.filter((row) => { + return Object.entries(headerLikeFilters).every(([columnName, searchText]) => { + if (!searchText || searchText.trim() === "") return true; + + // ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ๋ช… ์‹œ๋„ + const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()]; + const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue).toLowerCase() : ""; + + // LIKE ๊ฒ€์ƒ‰ (๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ) + return cellStr.includes(searchText.toLowerCase()); + }); + }); + } + // 3. ๐Ÿ†• Filter Builder ์ ์šฉ if (filterGroups.length > 0) { result = result.filter((row) => { @@ -541,7 +558,7 @@ export const TableListComponent: React.FC = ({ } return result; - }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]); + }, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, headerLikeFilters, filterGroups]); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); @@ -2935,6 +2952,7 @@ export const TableListComponent: React.FC = ({ headerFilters: Object.fromEntries( Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set)]), ), + headerLikeFilters, // LIKE ๊ฒ€์ƒ‰ ํ•„ํ„ฐ ์ €์žฅ pageSize: localPageSize, timestamp: Date.now(), }; @@ -2955,6 +2973,7 @@ export const TableListComponent: React.FC = ({ frozenColumnCount, showGridLines, headerFilters, + headerLikeFilters, localPageSize, ]); @@ -2991,6 +3010,9 @@ export const TableListComponent: React.FC = ({ }); setHeaderFilters(filters); } + if (state.headerLikeFilters) { + setHeaderLikeFilters(state.headerLikeFilters); + } } catch (error) { console.error("โŒ ํ…Œ์ด๋ธ” ์ƒํƒœ ๋ณต์› ์‹คํŒจ:", error); } @@ -5737,7 +5759,7 @@ export const TableListComponent: React.FC = ({ }} className={cn( "hover:bg-primary/20 ml-1 rounded p-0.5 transition-colors", - headerFilters[column.columnName]?.size > 0 && "text-primary bg-primary/10", + (headerFilters[column.columnName]?.size > 0 || headerLikeFilters[column.columnName]) && "text-primary bg-primary/10", )} title="ํ•„ํ„ฐ" > @@ -5745,7 +5767,7 @@ export const TableListComponent: React.FC = ({ e.stopPropagation()} > @@ -5754,16 +5776,42 @@ export const TableListComponent: React.FC = ({ ํ•„ํ„ฐ: {columnLabels[column.columnName] || column.displayName} - {headerFilters[column.columnName]?.size > 0 && ( + {(headerFilters[column.columnName]?.size > 0 || headerLikeFilters[column.columnName]) && ( )} -
+ {/* LIKE ๊ฒ€์ƒ‰ ์ž…๋ ฅ ํ•„๋“œ */} +
+ + { + setHeaderLikeFilters((prev) => ({ + ...prev, + [column.columnName]: e.target.value, + })); + }} + className="border-input bg-background placeholder:text-muted-foreground h-7 w-full rounded-md border pl-7 pr-2 text-xs focus:outline-none focus:ring-1 focus:ring-primary" + onClick={(e) => e.stopPropagation()} + /> +
+ {/* ๊ตฌ๋ถ„์„  */} +
๋˜๋Š” ๊ฐ’ ์„ ํƒ:
+
{columnUniqueValues[column.columnName]?.slice(0, 50).map((val) => { const isSelected = headerFilters[column.columnName]?.has(val); return (
0 ? 2 : 1} > @@ -1657,8 +1665,8 @@ export const PivotGridComponent: React.FC = ({ key={idx} className={cn( "border-r border-b border-border relative group", - "px-2 py-1.5 text-center text-xs font-medium", - "bg-muted/70 sticky top-0 z-10", + "px-2 py-1 text-center text-xs font-medium", + "bg-background sticky top-0 z-10", dataFields.length === 1 && "cursor-pointer hover:bg-accent/50" )} colSpan={dataFields.length || 1} @@ -1680,16 +1688,31 @@ export const PivotGridComponent: React.FC = ({ /> 1 ? 2 : 1} + > + ์ด๊ณ„ + 0 ? 2 : 1} + rowSpan={dataFields.length > 1 ? 2 : 1} >
{columnFields.map((f) => ( @@ -1721,25 +1744,11 @@ export const PivotGridComponent: React.FC = ({
- ์ด๊ณ„ -
- {df.caption} -
-
+