From 8a865ac1f487185677bc7a38f9d1a6fc47ccde27 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 14:29:19 +0900 Subject: [PATCH 01/15] =?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 02eee979ea306061a118bb0c491e6e013e2ce3f0 Mon Sep 17 00:00:00 2001 From: leeheejin Date: Fri, 16 Jan 2026 15:17:49 +0900 Subject: [PATCH 02/15] =?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 03/15] =?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 04/15] 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 05/15] =?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 06/15] =?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 07/15] =?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 08/15] =?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 09/15] =?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 10/15] =?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 11/15] =?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 12/15] 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 13/15] =?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 14/15] =?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 ( From 447bf937de54a181e8b0ce67d0a1ee40639c4b94 Mon Sep 17 00:00:00 2001 From: hjjeong Date: Tue, 20 Jan 2026 14:13:09 +0900 Subject: [PATCH 15/15] =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B2=84=ED=8A=BC?= =?UTF-8?q?=20=ED=95=B8=EB=93=A4=EB=9F=AC=20Primary=20Key=20=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SplitPanelLayoutComponent.tsx | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 0113a9a8..33997fc7 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1418,7 +1418,7 @@ export const SplitPanelLayoutComponent: React.FC // ์ˆ˜์ • ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ const handleEditClick = useCallback( - (panel: "left" | "right", item: any) => { + async (panel: "left" | "right", item: any) => { // ๐Ÿ†• ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ • ๋ฒ„ํŠผ ์„ค์ • ํ™•์ธ if (panel === "right" && componentConfig.rightPanel?.editButton?.mode === "modal") { const modalScreenId = componentConfig.rightPanel?.editButton?.modalScreenId; @@ -1427,18 +1427,40 @@ export const SplitPanelLayoutComponent: React.FC // ์ปค์Šคํ…€ ๋ชจ๋‹ฌ ํ™”๋ฉด ์—ด๊ธฐ const rightTableName = componentConfig.rightPanel?.tableName || ""; - // Primary Key ์ฐพ๊ธฐ (์šฐ์„ ์ˆœ์œ„: id > ID > ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ) + // Primary Key ์ฐพ๊ธฐ: ํ…Œ์ด๋ธ” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ์‹ค์ œ PK ์ปฌ๋Ÿผ ์กฐํšŒ let primaryKeyName = "id"; let primaryKeyValue: any; - if (item.id !== undefined && item.id !== null) { + // 1. ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ์‹ค์ œ PK ์ฐพ๊ธฐ + let pkColumn = rightTableColumns.find( + (col) => col.isPrimaryKey === true || col.is_primary_key === true || col.is_primary_key === "YES" + ); + + // 2. rightTableColumns๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด API๋กœ ์ง์ ‘ ์กฐํšŒ + if (!pkColumn && rightTableColumns.length === 0 && rightTableName) { + try { + const columnsResponse = await tableTypeApi.getColumns(rightTableName); + pkColumn = columnsResponse?.find( + (col: any) => col.isPrimaryKey === true || col.is_primary_key === true || col.is_primary_key === "YES" + ); + } catch (error) { + console.error("PK ์ปฌ๋Ÿผ ์กฐํšŒ ์‹คํŒจ:", error); + } + } + + if (pkColumn) { + const pkName = pkColumn.columnName || pkColumn.column_name; + primaryKeyName = pkName; + primaryKeyValue = item[pkName]; + } else if (item.id !== undefined && item.id !== null) { + // 3. ํด๋ฐฑ: id ์ปฌ๋Ÿผ primaryKeyName = "id"; primaryKeyValue = item.id; } else if (item.ID !== undefined && item.ID !== null) { primaryKeyName = "ID"; primaryKeyValue = item.ID; } else { - // ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ๋ฅผ Primary Key๋กœ ๊ฐ„์ฃผ + // 4. ์ตœํ›„์˜ ํด๋ฐฑ: ์ฒซ ๋ฒˆ์งธ ํ•„๋“œ const firstKey = Object.keys(item)[0]; primaryKeyName = firstKey; primaryKeyValue = item[firstKey];