diff --git a/backend-node/src/services/ddlExecutionService.ts b/backend-node/src/services/ddlExecutionService.ts index 2ed01231..c7a611d3 100644 --- a/backend-node/src/services/ddlExecutionService.ts +++ b/backend-node/src/services/ddlExecutionService.ts @@ -104,7 +104,7 @@ export class DDLExecutionService { await this.saveTableMetadata(client, tableName, description); // 5-3. 컬럼 메타데이터 저장 - await this.saveColumnMetadata(client, tableName, columns); + await this.saveColumnMetadata(client, tableName, columns, userCompanyCode); }); // 6. 성공 로그 기록 @@ -272,7 +272,7 @@ export class DDLExecutionService { await client.query(ddlQuery); // 6-2. 컬럼 메타데이터 저장 - await this.saveColumnMetadata(client, tableName, [column]); + await this.saveColumnMetadata(client, tableName, [column], userCompanyCode); }); // 7. 성공 로그 기록 @@ -446,7 +446,8 @@ CREATE TABLE "${tableName}" (${baseColumns}, private async saveColumnMetadata( client: any, tableName: string, - columns: CreateColumnDefinition[] + columns: CreateColumnDefinition[], + companyCode: string ): Promise { // 먼저 table_labels에 테이블 정보가 있는지 확인하고 없으면 생성 await client.query( @@ -508,19 +509,19 @@ CREATE TABLE "${tableName}" (${baseColumns}, await client.query( ` INSERT INTO table_type_columns ( - table_name, column_name, input_type, detail_settings, + table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date ) VALUES ( - $1, $2, $3, '{}', - 'Y', $4, now(), now() + $1, $2, $3, $4, '{}', + 'Y', $5, now(), now() ) - ON CONFLICT (table_name, column_name) + ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET - input_type = $3, - display_order = $4, + input_type = $4, + display_order = $5, updated_date = now() `, - [tableName, defaultCol.name, defaultCol.inputType, defaultCol.order] + [tableName, defaultCol.name, companyCode, defaultCol.inputType, defaultCol.order] ); } @@ -535,20 +536,20 @@ CREATE TABLE "${tableName}" (${baseColumns}, await client.query( ` INSERT INTO table_type_columns ( - table_name, column_name, input_type, detail_settings, + table_name, column_name, company_code, input_type, detail_settings, is_nullable, display_order, created_date, updated_date ) VALUES ( - $1, $2, $3, $4, - 'Y', $5, now(), now() + $1, $2, $3, $4, $5, + 'Y', $6, now(), now() ) - ON CONFLICT (table_name, column_name) + ON CONFLICT (table_name, column_name, company_code) DO UPDATE SET - input_type = $3, - detail_settings = $4, - display_order = $5, + input_type = $4, + detail_settings = $5, + display_order = $6, updated_date = now() `, - [tableName, column.name, inputType, detailSettings, i] + [tableName, column.name, companyCode, inputType, detailSettings, i] ); } diff --git a/backend-node/src/services/menuService.ts b/backend-node/src/services/menuService.ts index b22beb88..86df579c 100644 --- a/backend-node/src/services/menuService.ts +++ b/backend-node/src/services/menuService.ts @@ -36,29 +36,61 @@ export async function getSiblingMenuObjids(menuObjid: number): Promise try { logger.debug("메뉴 스코프 조회 시작", { menuObjid }); - // 1. 현재 메뉴 자신을 포함 - const menuObjids = [menuObjid]; + // 1. 현재 메뉴 정보 조회 (부모 ID 확인) + const currentMenuQuery = ` + SELECT parent_obj_id FROM menu_info + WHERE objid = $1 + `; + const currentMenuResult = await pool.query(currentMenuQuery, [menuObjid]); - // 2. 현재 메뉴의 자식 메뉴들 조회 - const childrenQuery = ` + if (currentMenuResult.rows.length === 0) { + logger.warn("메뉴를 찾을 수 없음, 자기 자신만 반환", { menuObjid }); + return [menuObjid]; + } + + const parentObjId = Number(currentMenuResult.rows[0].parent_obj_id); + + // 2. 최상위 메뉴(parent_obj_id = 0)는 자기 자신만 반환 + if (parentObjId === 0) { + logger.debug("최상위 메뉴, 자기 자신만 반환", { menuObjid }); + return [menuObjid]; + } + + // 3. 형제 메뉴들 조회 (같은 부모를 가진 메뉴들) + const siblingsQuery = ` SELECT objid FROM menu_info WHERE parent_obj_id = $1 ORDER BY objid `; - const childrenResult = await pool.query(childrenQuery, [menuObjid]); + const siblingsResult = await pool.query(siblingsQuery, [parentObjId]); - const childObjids = childrenResult.rows.map((row) => Number(row.objid)); + const siblingObjids = siblingsResult.rows.map((row) => Number(row.objid)); - // 3. 자신 + 자식을 합쳐서 정렬 - const allObjids = Array.from(new Set([...menuObjids, ...childObjids])).sort((a, b) => a - b); + // 4. 각 형제 메뉴(자기 자신 포함)의 자식 메뉴들도 조회 + const allObjids = [...siblingObjids]; + + for (const siblingObjid of siblingObjids) { + const childrenQuery = ` + SELECT objid FROM menu_info + WHERE parent_obj_id = $1 + ORDER BY objid + `; + const childrenResult = await pool.query(childrenQuery, [siblingObjid]); + const childObjids = childrenResult.rows.map((row) => Number(row.objid)); + allObjids.push(...childObjids); + } + + // 5. 중복 제거 및 정렬 + const uniqueObjids = Array.from(new Set(allObjids)).sort((a, b) => a - b); logger.debug("메뉴 스코프 조회 완료", { - menuObjid, - childCount: childObjids.length, - totalCount: allObjids.length + menuObjid, + parentObjId, + siblingCount: siblingObjids.length, + totalCount: uniqueObjids.length }); - return allObjids; + return uniqueObjids; } catch (error: any) { logger.error("메뉴 스코프 조회 실패", { menuObjid, diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index 29cad453..c5d51db5 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -179,7 +179,8 @@ class TableCategoryValueService { } else { // 일반 회사: 자신의 카테고리 값만 조회 if (menuObjid && siblingObjids.length > 0) { - // 메뉴 스코프 적용 + // 메뉴 스코프 적용 + created_menu_objid 필터링 + // 현재 메뉴 스코프(형제 메뉴)에서 생성된 값만 표시 query = ` SELECT value_id AS "valueId", @@ -197,6 +198,7 @@ class TableCategoryValueService { is_default AS "isDefault", company_code AS "companyCode", menu_objid AS "menuObjid", + created_menu_objid AS "createdMenuObjid", created_at AS "createdAt", updated_at AS "updatedAt", created_by AS "createdBy", @@ -206,6 +208,10 @@ class TableCategoryValueService { AND column_name = $2 AND menu_objid = ANY($3) AND company_code = $4 + AND ( + created_menu_objid = ANY($3) -- 형제 메뉴에서 생성된 값만 + OR created_menu_objid IS NULL -- 레거시 데이터 (모든 메뉴에서 보임) + ) `; params = [tableName, columnName, siblingObjids, companyCode]; } else { @@ -331,8 +337,8 @@ class TableCategoryValueService { INSERT INTO table_column_category_values ( table_name, column_name, value_code, value_label, value_order, parent_value_id, depth, description, color, icon, - is_active, is_default, company_code, menu_objid, created_by - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + is_active, is_default, company_code, menu_objid, created_menu_objid, created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING value_id AS "valueId", table_name AS "tableName", @@ -349,6 +355,7 @@ class TableCategoryValueService { is_default AS "isDefault", company_code AS "companyCode", menu_objid AS "menuObjid", + created_menu_objid AS "createdMenuObjid", created_at AS "createdAt", created_by AS "createdBy" `; @@ -368,6 +375,7 @@ class TableCategoryValueService { value.isDefault || false, companyCode, menuObjid, // ← 메뉴 OBJID 저장 + menuObjid, // ← 🆕 생성 메뉴 OBJID 저장 (같은 값) userId, ]); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 8bcec704..ac8b62fd 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -1069,12 +1069,28 @@ export class TableManagementService { paramCount: number; } | null> { try { + // 🔧 {value, operator} 형태의 필터 객체 처리 + let actualValue = value; + let operator = "contains"; // 기본값 + + if (typeof value === "object" && value !== null && "value" in value) { + actualValue = value.value; + operator = value.operator || "contains"; + + logger.info("🔍 필터 객체 처리:", { + columnName, + originalValue: value, + actualValue, + operator, + }); + } + // "__ALL__" 값이거나 빈 값이면 필터 조건을 적용하지 않음 if ( - value === "__ALL__" || - value === "" || - value === null || - value === undefined + actualValue === "__ALL__" || + actualValue === "" || + actualValue === null || + actualValue === undefined ) { return null; } @@ -1083,12 +1099,22 @@ export class TableManagementService { const columnInfo = await this.getColumnWebTypeInfo(tableName, columnName); if (!columnInfo) { - // 컬럼 정보가 없으면 기본 문자열 검색 - return { - whereClause: `${columnName}::text ILIKE $${paramIndex}`, - values: [`%${value}%`], - paramCount: 1, - }; + // 컬럼 정보가 없으면 operator에 따른 기본 검색 + switch (operator) { + case "equals": + return { + whereClause: `${columnName}::text = $${paramIndex}`, + values: [actualValue], + paramCount: 1, + }; + case "contains": + default: + return { + whereClause: `${columnName}::text ILIKE $${paramIndex}`, + values: [`%${actualValue}%`], + paramCount: 1, + }; + } } const webType = columnInfo.webType; @@ -1097,17 +1123,17 @@ export class TableManagementService { switch (webType) { case "date": case "datetime": - return this.buildDateRangeCondition(columnName, value, paramIndex); + return this.buildDateRangeCondition(columnName, actualValue, paramIndex); case "number": case "decimal": - return this.buildNumberRangeCondition(columnName, value, paramIndex); + return this.buildNumberRangeCondition(columnName, actualValue, paramIndex); case "code": return await this.buildCodeSearchCondition( tableName, columnName, - value, + actualValue, paramIndex ); @@ -1115,15 +1141,15 @@ export class TableManagementService { return await this.buildEntitySearchCondition( tableName, columnName, - value, + actualValue, paramIndex ); default: - // 기본 문자열 검색 + // 기본 문자열 검색 (actualValue 사용) return { whereClause: `${columnName}::text ILIKE $${paramIndex}`, - values: [`%${value}%`], + values: [`%${actualValue}%`], paramCount: 1, }; } @@ -1133,9 +1159,14 @@ export class TableManagementService { error ); // 오류 시 기본 검색으로 폴백 + let fallbackValue = value; + if (typeof value === "object" && value !== null && "value" in value) { + fallbackValue = value.value; + } + return { whereClause: `${columnName}::text ILIKE $${paramIndex}`, - values: [`%${value}%`], + values: [`%${fallbackValue}%`], paramCount: 1, }; } diff --git a/frontend/components/screen/table-options/ColumnVisibilityPanel.tsx b/frontend/components/screen/table-options/ColumnVisibilityPanel.tsx index 2373aa0a..c03dac58 100644 --- a/frontend/components/screen/table-options/ColumnVisibilityPanel.tsx +++ b/frontend/components/screen/table-options/ColumnVisibilityPanel.tsx @@ -85,6 +85,15 @@ export const ColumnVisibilityPanel: React.FC = ({ const handleApply = () => { table?.onColumnVisibilityChange(localColumns); + + // 컬럼 순서 변경 콜백 호출 + if (table?.onColumnOrderChange) { + const newOrder = localColumns + .map((col) => col.columnName) + .filter((name) => name !== "__checkbox__"); + table.onColumnOrderChange(newOrder); + } + onClose(); }; diff --git a/frontend/components/screen/widgets/CategoryWidget.tsx b/frontend/components/screen/widgets/CategoryWidget.tsx index 2974ed60..a4e93256 100644 --- a/frontend/components/screen/widgets/CategoryWidget.tsx +++ b/frontend/components/screen/widgets/CategoryWidget.tsx @@ -49,6 +49,7 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p const effectiveMenuObjid = menuObjid || props.menuObjid; const [selectedColumn, setSelectedColumn] = useState<{ + uniqueKey: string; // 테이블명.컬럼명 형식 columnName: string; columnLabel: string; tableName: string; @@ -98,10 +99,12 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
- setSelectedColumn({ columnName, columnLabel, tableName }) - } + selectedColumn={selectedColumn?.uniqueKey || null} + onColumnSelect={(uniqueKey, columnLabel, tableName) => { + // uniqueKey는 "테이블명.컬럼명" 형식 + const columnName = uniqueKey.split('.')[1]; + setSelectedColumn({ uniqueKey, columnName, columnLabel, tableName }); + }} menuObjid={effectiveMenuObjid} />
@@ -118,6 +121,7 @@ export function CategoryWidget({ widgetId, tableName, menuObjid, component, ...p
{selectedColumn ? ( {columns.map((column) => { const uniqueKey = `${column.tableName}.${column.columnName}`; + const isSelected = selectedColumn === uniqueKey; // 테이블명.컬럼명으로 비교 return (
onColumnSelect(column.columnName, column.columnLabel || column.columnName, column.tableName)} + onClick={() => onColumnSelect(uniqueKey, column.columnLabel || column.columnName, column.tableName)} className={`cursor-pointer rounded-lg border px-4 py-2 transition-all ${ - selectedColumn === column.columnName ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50" + isSelected ? "border-primary bg-primary/10 shadow-sm" : "hover:bg-muted/50" }`} >

{column.columnLabel || column.columnName}

diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 483fc393..91947094 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useCallback, useEffect } from "react"; +import React, { useState, useCallback, useEffect, useMemo } from "react"; import { ComponentRendererProps } from "../../types"; import { SplitPanelLayoutConfig } from "./types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -8,12 +8,14 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react"; import { dataApi } from "@/lib/api/data"; +import { entityJoinApi } from "@/lib/api/entityJoin"; import { useToast } from "@/hooks/use-toast"; import { tableTypeApi } from "@/lib/api/screen"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility } from "@/types/table-options"; +import { useAuth } from "@/hooks/useAuth"; export interface SplitPanelLayoutComponentProps extends ComponentRendererProps { // 추가 props @@ -44,6 +46,7 @@ export const SplitPanelLayoutComponent: React.FC const [leftFilters, setLeftFilters] = useState([]); const [leftGrouping, setLeftGrouping] = useState([]); const [leftColumnVisibility, setLeftColumnVisibility] = useState([]); + const [leftColumnOrder, setLeftColumnOrder] = useState([]); // 🔧 컬럼 순서 const [rightFilters, setRightFilters] = useState([]); const [rightGrouping, setRightGrouping] = useState([]); const [rightColumnVisibility, setRightColumnVisibility] = useState([]); @@ -160,6 +163,84 @@ export const SplitPanelLayoutComponent: React.FC return rootItems; }, [componentConfig.leftPanel?.itemAddConfig]); + // 🔧 사용자 ID 가져오기 + const { userId: currentUserId } = useAuth(); + + // 🔄 필터를 searchValues 형식으로 변환 + const searchValues = useMemo(() => { + if (!leftFilters || leftFilters.length === 0) return {}; + + const values: Record = {}; + leftFilters.forEach(filter => { + if (filter.value !== undefined && filter.value !== null && filter.value !== '') { + values[filter.columnName] = { + value: filter.value, + operator: filter.operator || 'contains', + }; + } + }); + return values; + }, [leftFilters]); + + // 🔄 컬럼 가시성 및 순서 처리 + const visibleLeftColumns = useMemo(() => { + const displayColumns = componentConfig.leftPanel?.columns || []; + + if (displayColumns.length === 0) return []; + + let columns = displayColumns; + + // columnVisibility가 있으면 가시성 적용 + if (leftColumnVisibility.length > 0) { + const visibilityMap = new Map(leftColumnVisibility.map(cv => [cv.columnName, cv.visible])); + columns = columns.filter((col: any) => { + const colName = typeof col === 'string' ? col : (col.name || col.columnName); + return visibilityMap.get(colName) !== false; + }); + } + + // 🔧 컬럼 순서 적용 + if (leftColumnOrder.length > 0) { + const orderMap = new Map(leftColumnOrder.map((name, index) => [name, index])); + columns = [...columns].sort((a, b) => { + const aName = typeof a === 'string' ? a : (a.name || a.columnName); + const bName = typeof b === 'string' ? b : (b.name || b.columnName); + const aIndex = orderMap.get(aName) ?? 999; + const bIndex = orderMap.get(bName) ?? 999; + return aIndex - bIndex; + }); + } + + return columns; + }, [componentConfig.leftPanel?.columns, leftColumnVisibility, leftColumnOrder]); + + // 🔄 데이터 그룹화 + const groupedLeftData = useMemo(() => { + if (!leftGrouping || leftGrouping.length === 0 || leftData.length === 0) return []; + + const grouped = new Map(); + + leftData.forEach((item) => { + // 각 그룹 컬럼의 값을 조합하여 그룹 키 생성 + const groupKey = leftGrouping.map(col => { + const value = item[col]; + // null/undefined 처리 + return value === null || value === undefined ? "(비어있음)" : String(value); + }).join(" > "); + + if (!grouped.has(groupKey)) { + grouped.set(groupKey, []); + } + grouped.get(groupKey)!.push(item); + }); + + return Array.from(grouped.entries()).map(([key, items]) => ({ + groupKey: key, + items, + count: items.length, + })); + }, [leftData, leftGrouping]); + // 좌측 데이터 로드 const loadLeftData = useCallback(async () => { const leftTableName = componentConfig.leftPanel?.tableName; @@ -167,12 +248,18 @@ export const SplitPanelLayoutComponent: React.FC setIsLoadingLeft(true); try { - const result = await dataApi.getTableData(leftTableName, { + // 🎯 필터 조건을 API에 전달 (entityJoinApi 사용) + const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; + + + const result = await entityJoinApi.getTableDataWithJoins(leftTableName, { page: 1, size: 100, - // searchTerm 제거 - 클라이언트 사이드에서 필터링 + search: filters, // 필터 조건 전달 + enableEntityJoin: true, // 엔티티 조인 활성화 }); + // 가나다순 정렬 (좌측 패널의 표시 컬럼 기준) const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; if (leftColumn && result.data.length > 0) { @@ -196,7 +283,7 @@ export const SplitPanelLayoutComponent: React.FC } finally { setIsLoadingLeft(false); } - }, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, buildHierarchy]); + }, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, buildHierarchy, searchValues]); // 우측 데이터 로드 const loadRightData = useCallback( @@ -283,67 +370,101 @@ export const SplitPanelLayoutComponent: React.FC [rightTableColumns], ); + // 🔧 컬럼의 고유값 가져오기 함수 + const getLeftColumnUniqueValues = useCallback(async (columnName: string) => { + const leftTableName = componentConfig.leftPanel?.tableName; + if (!leftTableName || leftData.length === 0) return []; + + // 현재 로드된 데이터에서 고유값 추출 + const uniqueValues = new Set(); + + leftData.forEach((item) => { + const value = item[columnName]; + if (value !== null && value !== undefined && value !== '') { + // _name 필드 우선 사용 (category/entity type) + const displayValue = item[`${columnName}_name`] || value; + uniqueValues.add(String(displayValue)); + } + }); + + return Array.from(uniqueValues).map(value => ({ + value: value, + label: value, + })); + }, [componentConfig.leftPanel?.tableName, leftData]); + // 좌측 테이블 등록 (Context에 등록) useEffect(() => { const leftTableName = componentConfig.leftPanel?.tableName; if (!leftTableName || isDesignMode) return; const leftTableId = `split-panel-left-${component.id}`; - const leftColumns = componentConfig.leftPanel?.displayColumns || []; + // 🔧 화면에 표시되는 컬럼 사용 (columns 속성) + const configuredColumns = componentConfig.leftPanel?.columns || []; + const displayColumns = configuredColumns.map((col: any) => { + if (typeof col === 'string') return col; + return col.columnName || col.name || col; + }).filter(Boolean); + + // 화면에 설정된 컬럼이 없으면 등록하지 않음 + if (displayColumns.length === 0) return; - if (leftColumns.length > 0) { - registerTable({ - tableId: leftTableId, - label: `${component.title || "분할 패널"} (좌측)`, - tableName: leftTableName, - columns: leftColumns.map((col: string) => ({ - columnName: col, - columnLabel: leftColumnLabels[col] || col, - inputType: "text", - visible: true, - width: 150, - sortable: true, - filterable: true, - })), - onFilterChange: setLeftFilters, - onGroupChange: setLeftGrouping, - onColumnVisibilityChange: setLeftColumnVisibility, - }); + // 테이블명이 있으면 등록 + registerTable({ + tableId: leftTableId, + label: `${component.title || "분할 패널"} (좌측)`, + tableName: leftTableName, + columns: displayColumns.map((col: string) => ({ + columnName: col, + columnLabel: leftColumnLabels[col] || col, + inputType: "text", + visible: true, + width: 150, + sortable: true, + filterable: true, + })), + onFilterChange: setLeftFilters, + onGroupChange: setLeftGrouping, + onColumnVisibilityChange: setLeftColumnVisibility, + onColumnOrderChange: setLeftColumnOrder, // 🔧 컬럼 순서 변경 콜백 추가 + getColumnUniqueValues: getLeftColumnUniqueValues, // 🔧 고유값 가져오기 함수 추가 + }); - return () => unregisterTable(leftTableId); - } - }, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.displayColumns, leftColumnLabels, component.title, isDesignMode]); + return () => unregisterTable(leftTableId); + }, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, leftColumnLabels, component.title, isDesignMode, getLeftColumnUniqueValues]); - // 우측 테이블 등록 (Context에 등록) - useEffect(() => { - const rightTableName = componentConfig.rightPanel?.tableName; - if (!rightTableName || isDesignMode) return; - - const rightTableId = `split-panel-right-${component.id}`; - const rightColumns = rightTableColumns.map((col: any) => col.columnName || col.column_name).filter(Boolean); - - if (rightColumns.length > 0) { - registerTable({ - tableId: rightTableId, - label: `${component.title || "분할 패널"} (우측)`, - tableName: rightTableName, - columns: rightColumns.map((col: string) => ({ - columnName: col, - columnLabel: rightColumnLabels[col] || col, - inputType: "text", - visible: true, - width: 150, - sortable: true, - filterable: true, - })), - onFilterChange: setRightFilters, - onGroupChange: setRightGrouping, - onColumnVisibilityChange: setRightColumnVisibility, - }); - - return () => unregisterTable(rightTableId); - } - }, [component.id, componentConfig.rightPanel?.tableName, rightTableColumns, rightColumnLabels, component.title, isDesignMode]); + // 우측 테이블은 검색 컴포넌트 등록 제외 (좌측 마스터 테이블만 검색 가능) + // useEffect(() => { + // const rightTableName = componentConfig.rightPanel?.tableName; + // if (!rightTableName || isDesignMode) return; + // + // const rightTableId = `split-panel-right-${component.id}`; + // // 🔧 화면에 표시되는 컬럼만 등록 (displayColumns 또는 columns) + // const displayColumns = componentConfig.rightPanel?.columns || []; + // const rightColumns = displayColumns.map((col: any) => col.columnName || col.name || col).filter(Boolean); + // + // if (rightColumns.length > 0) { + // registerTable({ + // tableId: rightTableId, + // label: `${component.title || "분할 패널"} (우측)`, + // tableName: rightTableName, + // columns: rightColumns.map((col: string) => ({ + // columnName: col, + // columnLabel: rightColumnLabels[col] || col, + // inputType: "text", + // visible: true, + // width: 150, + // sortable: true, + // filterable: true, + // })), + // onFilterChange: setRightFilters, + // onGroupChange: setRightGrouping, + // onColumnVisibilityChange: setRightColumnVisibility, + // }); + // + // return () => unregisterTable(rightTableId); + // } + // }, [component.id, componentConfig.rightPanel?.tableName, componentConfig.rightPanel?.columns, rightColumnLabels, component.title, isDesignMode]); // 좌측 테이블 컬럼 라벨 로드 useEffect(() => { @@ -786,6 +907,43 @@ export const SplitPanelLayoutComponent: React.FC } }, [addModalPanel, componentConfig, addModalFormData, toast, selectedLeftItem, loadLeftData, loadRightData]); + // 🔧 좌측 컬럼 가시성 설정 저장 및 불러오기 + useEffect(() => { + const leftTableName = componentConfig.leftPanel?.tableName; + if (leftTableName && currentUserId) { + // localStorage에서 저장된 설정 불러오기 + const storageKey = `table_column_visibility_${leftTableName}_${currentUserId}`; + const savedSettings = localStorage.getItem(storageKey); + + if (savedSettings) { + try { + const parsed = JSON.parse(savedSettings) as ColumnVisibility[]; + setLeftColumnVisibility(parsed); + } catch (error) { + console.error("저장된 컬럼 설정 불러오기 실패:", error); + } + } + } + }, [componentConfig.leftPanel?.tableName, currentUserId]); + + // 🔧 컬럼 가시성 변경 시 localStorage에 저장 및 순서 업데이트 + useEffect(() => { + const leftTableName = componentConfig.leftPanel?.tableName; + + if (leftColumnVisibility.length > 0 && leftTableName && currentUserId) { + // 순서 업데이트 + const newOrder = leftColumnVisibility + .map((cv) => cv.columnName) + .filter((name) => name !== "__checkbox__"); // 체크박스 제외 + + setLeftColumnOrder(newOrder); + + // localStorage에 저장 + const storageKey = `table_column_visibility_${leftTableName}_${currentUserId}`; + localStorage.setItem(storageKey, JSON.stringify(leftColumnVisibility)); + } + }, [leftColumnVisibility, componentConfig.leftPanel?.tableName, currentUserId]); + // 초기 데이터 로드 useEffect(() => { if (!isDesignMode && componentConfig.autoLoad !== false) { @@ -794,6 +952,14 @@ export const SplitPanelLayoutComponent: React.FC // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDesignMode, componentConfig.autoLoad]); + // 🔄 필터 변경 시 데이터 다시 로드 + useEffect(() => { + if (!isDesignMode && componentConfig.autoLoad !== false) { + loadLeftData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [leftFilters]); + // 리사이저 드래그 핸들러 const handleMouseDown = (e: React.MouseEvent) => { if (!resizable) return; @@ -933,6 +1099,7 @@ export const SplitPanelLayoutComponent: React.FC
) : ( (() => { + // 🔧 로컬 검색 필터 적용 const filteredData = leftSearchQuery ? leftData.filter((item) => { const searchLower = leftSearchQuery.toLowerCase(); @@ -943,12 +1110,17 @@ export const SplitPanelLayoutComponent: React.FC }) : leftData; - const displayColumns = componentConfig.leftPanel?.columns || []; - const columnsToShow = displayColumns.length > 0 - ? displayColumns.map(col => ({ - ...col, - label: leftColumnLabels[col.name] || col.label || col.name - })) + // 🔧 가시성 처리된 컬럼 사용 + const columnsToShow = visibleLeftColumns.length > 0 + ? visibleLeftColumns.map((col: any) => { + const colName = typeof col === 'string' ? col : (col.name || col.columnName); + return { + name: colName, + label: leftColumnLabels[colName] || (typeof col === 'object' ? col.label : null) || colName, + width: typeof col === 'object' ? col.width : 150, + align: (typeof col === 'object' ? col.align : "left") as "left" | "center" | "right" + }; + }) : Object.keys(filteredData[0] || {}).filter(key => key !== 'children' && key !== 'level').slice(0, 5).map(key => ({ name: key, label: leftColumnLabels[key] || key, @@ -956,6 +1128,66 @@ export const SplitPanelLayoutComponent: React.FC align: "left" as const })); + // 🔧 그룹화된 데이터 렌더링 + if (groupedLeftData.length > 0) { + return ( +
+ {groupedLeftData.map((group, groupIdx) => ( +
+
+ {group.groupKey} ({group.count}개) +
+ + + + {columnsToShow.map((col, idx) => ( + + ))} + + + + {group.items.map((item, idx) => { + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + const itemId = item[sourceColumn] || item.id || item.ID || idx; + const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); + + return ( + handleLeftItemSelect(item)} + className={`hover:bg-accent cursor-pointer transition-colors ${ + isSelected ? "bg-primary/10" : "" + }`} + > + {columnsToShow.map((col, colIdx) => ( + + ))} + + ); + })} + +
+ {col.label} +
+ {item[col.name] !== null && item[col.name] !== undefined + ? String(item[col.name]) + : "-"} +
+
+ ))} +
+ ); + } + + // 🔧 일반 테이블 렌더링 (그룹화 없음) return (
diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 6344f3e8..f5fecd34 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -274,6 +274,11 @@ export const TableListComponent: React.FC = ({ setCurrentPage(1); // 필터 변경 시 첫 페이지로 }, [filters]); + // grouping이 변경되면 groupByColumns 업데이트 + useEffect(() => { + setGroupByColumns(grouping); + }, [grouping]); + // 초기 로드 시 localStorage에서 저장된 설정 불러오기 useEffect(() => { if (tableConfig.selectedTable && currentUserId) { @@ -1652,9 +1657,20 @@ export const TableListComponent: React.FC = ({ data.forEach((item) => { // 그룹 키 생성: "통화:KRW > 단위:EA" const keyParts = groupByColumns.map((col) => { - const value = item[col]; + // 카테고리/엔티티 타입인 경우 _name 필드 사용 + const inputType = columnMeta?.[col]?.inputType; + let displayValue = item[col]; + + if (inputType === 'category' || inputType === 'entity' || inputType === 'code') { + // _name 필드가 있으면 사용 (예: division_name, writer_name) + const nameField = `${col}_name`; + if (item[nameField] !== undefined && item[nameField] !== null) { + displayValue = item[nameField]; + } + } + const label = columnLabels[col] || col; - return `${label}:${value !== null && value !== undefined ? value : "-"}`; + return `${label}:${displayValue !== null && displayValue !== undefined ? displayValue : "-"}`; }); const groupKey = keyParts.join(" > "); @@ -1677,7 +1693,7 @@ export const TableListComponent: React.FC = ({ count: items.length, }; }); - }, [data, groupByColumns, columnLabels]); + }, [data, groupByColumns, columnLabels, columnMeta]); // 저장된 그룹 설정 불러오기 useEffect(() => { @@ -1860,124 +1876,7 @@ export const TableListComponent: React.FC = ({ if (tableConfig.stickyHeader && !isDesignMode) { return (
- {tableConfig.filter?.enabled && ( -
-
-
- -
-
- {/* 전체 개수 */} -
- 전체 {totalItems.toLocaleString()}개 -
- - - - - - - - -
-
-

그룹 설정

-

- 데이터를 그룹화할 컬럼을 선택하세요 -

-
- - {/* 컬럼 목록 */} -
- {visibleColumns - .filter((col) => col.columnName !== "__checkbox__") - .map((col) => ( -
- toggleGroupColumn(col.columnName)} - /> - -
- ))} -
- - {/* 선택된 그룹 안내 */} - {groupByColumns.length > 0 && ( -
- - {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")} - -
- )} - - {/* 초기화 버튼 */} - {groupByColumns.length > 0 && ( - - )} -
-
-
-
-
-
- )} + {/* 필터 헤더는 TableSearchWidget으로 이동 */} {/* 그룹 표시 배지 */} {groupByColumns.length > 0 && ( @@ -2040,125 +1939,7 @@ export const TableListComponent: React.FC = ({ return ( <>
- {/* 필터 */} - {tableConfig.filter?.enabled && ( -
-
-
- -
-
- {/* 전체 개수 */} -
- 전체 {totalItems.toLocaleString()}개 -
- - - - - - - - -
-
-

그룹 설정

-

- 데이터를 그룹화할 컬럼을 선택하세요 -

-
- - {/* 컬럼 목록 */} -
- {visibleColumns - .filter((col) => col.columnName !== "__checkbox__") - .map((col) => ( -
- toggleGroupColumn(col.columnName)} - /> - -
- ))} -
- - {/* 선택된 그룹 안내 */} - {groupByColumns.length > 0 && ( -
- - {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")} - -
- )} - - {/* 초기화 버튼 */} - {groupByColumns.length > 0 && ( - - )} -
-
-
-
-
-
- )} + {/* 필터 헤더는 TableSearchWidget으로 이동 */} {/* 그룹 표시 배지 */} {groupByColumns.length > 0 && ( diff --git a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx index 2b37e2d6..34b3044c 100644 --- a/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx +++ b/frontend/lib/registry/components/table-search-widget/TableSearchWidget.tsx @@ -76,7 +76,15 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table useEffect(() => { const tables = Array.from(registeredTables.values()); + console.log("🔍 [TableSearchWidget] 테이블 감지:", { + tablesCount: tables.length, + tableIds: tables.map(t => t.tableId), + selectedTableId, + autoSelectFirstTable, + }); + if (autoSelectFirstTable && tables.length > 0 && !selectedTableId) { + console.log("✅ [TableSearchWidget] 첫 번째 테이블 자동 선택:", tables[0].tableId); setSelectedTableId(tables[0].tableId); } }, [registeredTables, selectedTableId, autoSelectFirstTable, setSelectedTableId]);