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/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 85e1f361..68a686b8 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,9 @@ 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 {}; @@ -176,22 +182,44 @@ export const SplitPanelLayoutComponent: React.FC return values; }, [leftFilters]); - // 🔄 컬럼 가시성 처리 + // 🔄 컬럼 가시성 및 순서 처리 const visibleLeftColumns = useMemo(() => { const displayColumns = componentConfig.leftPanel?.columns || []; + console.log("🔍 [분할패널] visibleLeftColumns 계산:", { + displayColumns: displayColumns.length, + leftColumnVisibility: leftColumnVisibility.length, + leftColumnOrder: leftColumnOrder.length, + }); + if (displayColumns.length === 0) return []; + let columns = displayColumns; + // columnVisibility가 있으면 가시성 적용 if (leftColumnVisibility.length > 0) { const visibilityMap = new Map(leftColumnVisibility.map(cv => [cv.columnName, cv.visible])); - return displayColumns.filter((col: any) => { + columns = columns.filter((col: any) => { const colName = typeof col === 'string' ? col : (col.name || col.columnName); return visibilityMap.get(colName) !== false; }); + console.log("✅ [분할패널] 가시성 적용 후:", columns.length); } - return displayColumns; - }, [componentConfig.leftPanel?.columns, leftColumnVisibility]); + // 🔧 컬럼 순서 적용 + 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; + }); + console.log("✅ [분할패널] 순서 적용 후:", columns.map((c: any) => typeof c === 'string' ? c : (c.name || c.columnName))); + } + + return columns; + }, [componentConfig.leftPanel?.columns, leftColumnVisibility, leftColumnOrder]); // 🔄 데이터 그룹화 const groupedLeftData = useMemo(() => { @@ -227,13 +255,26 @@ export const SplitPanelLayoutComponent: React.FC setIsLoadingLeft(true); try { - // 🎯 필터 조건을 API에 전달 + // 🎯 필터 조건을 API에 전달 (entityJoinApi 사용) const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; - const result = await dataApi.getTableData(leftTableName, { + console.log("📡 [분할패널] API 호출 시작:", { + tableName: leftTableName, + filters, + searchValues, + }); + + const result = await entityJoinApi.getTableDataWithJoins(leftTableName, { page: 1, size: 100, search: filters, // 필터 조건 전달 + enableEntityJoin: true, // 엔티티 조인 활성화 + }); + + console.log("📡 [분할패널] API 응답:", { + success: result.success, + dataLength: result.data?.length || 0, + totalItems: result.totalItems, }); // 가나다순 정렬 (좌측 패널의 표시 컬럼 기준) @@ -346,6 +387,29 @@ 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; @@ -379,10 +443,12 @@ export const SplitPanelLayoutComponent: React.FC onFilterChange: setLeftFilters, onGroupChange: setLeftGrouping, onColumnVisibilityChange: setLeftColumnVisibility, + onColumnOrderChange: setLeftColumnOrder, // 🔧 컬럼 순서 변경 콜백 추가 + getColumnUniqueValues: getLeftColumnUniqueValues, // 🔧 고유값 가져오기 함수 추가 }); return () => unregisterTable(leftTableId); - }, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, leftColumnLabels, component.title, isDesignMode]); + }, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, leftColumnLabels, component.title, isDesignMode, getLeftColumnUniqueValues]); // 우측 테이블은 검색 컴포넌트 등록 제외 (좌측 마스터 테이블만 검색 가능) // useEffect(() => { @@ -858,6 +924,51 @@ 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; + console.log("🔍 [분할패널] 컬럼 가시성 변경 감지:", { + leftColumnVisibility: leftColumnVisibility.length, + leftTableName, + currentUserId, + visibility: leftColumnVisibility, + }); + + if (leftColumnVisibility.length > 0 && leftTableName && currentUserId) { + // 순서 업데이트 + const newOrder = leftColumnVisibility + .map((cv) => cv.columnName) + .filter((name) => name !== "__checkbox__"); // 체크박스 제외 + + console.log("✅ [분할패널] 컬럼 순서 업데이트:", newOrder); + setLeftColumnOrder(newOrder); + + // localStorage에 저장 + const storageKey = `table_column_visibility_${leftTableName}_${currentUserId}`; + localStorage.setItem(storageKey, JSON.stringify(leftColumnVisibility)); + console.log("💾 [분할패널] localStorage 저장:", storageKey); + } + }, [leftColumnVisibility, componentConfig.leftPanel?.tableName, currentUserId]); + // 초기 데이터 로드 useEffect(() => { if (!isDesignMode && componentConfig.autoLoad !== false) { @@ -868,7 +979,16 @@ export const SplitPanelLayoutComponent: React.FC // 🔄 필터 변경 시 데이터 다시 로드 useEffect(() => { + console.log("🔍 [분할패널] 필터 변경 감지:", { + leftFilters: leftFilters.length, + filters: leftFilters, + isDesignMode, + autoLoad: componentConfig.autoLoad, + searchValues, + }); + if (!isDesignMode && componentConfig.autoLoad !== false) { + console.log("✅ [분할패널] loadLeftData 호출 (필터 변경)"); loadLeftData(); } // eslint-disable-next-line react-hooks/exhaustive-deps