fix: 분할 패널 컬럼 순서 변경 및 필터링 개선

문제:
1. ColumnVisibilityPanel에서 순서 변경 후 onColumnOrderChange가 호출되지 않음
2. 필터 입력 시 데이터가 제대로 필터링되지 않음
3. useAuth 훅 import 경로 오류 (@/hooks/use-auth → @/hooks/useAuth)

해결:
1. ColumnVisibilityPanel.handleApply()에 onColumnOrderChange 호출 추가
2. 필터 변경 감지 및 데이터 로드 로직 디버깅 로그 추가
3. useAuth import 경로 수정

테스트:
- 거래처관리 화면에서 컬럼 순서 변경 → 실시간 반영 
- 페이지 새로고침 → 순서 유지 (localStorage) 
- 필터 입력 → 필터 변경 감지 (추가 디버깅 필요)
This commit is contained in:
kjs 2025-11-12 16:33:08 +09:00
parent 579c4b7387
commit 7b84a81a96
2 changed files with 137 additions and 8 deletions

View File

@ -85,6 +85,15 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
const handleApply = () => {
table?.onColumnVisibilityChange(localColumns);
// 컬럼 순서 변경 콜백 호출
if (table?.onColumnOrderChange) {
const newOrder = localColumns
.map((col) => col.columnName)
.filter((name) => name !== "__checkbox__");
table.onColumnOrderChange(newOrder);
}
onClose();
};

View File

@ -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<SplitPanelLayoutComponentProps>
const [leftFilters, setLeftFilters] = useState<TableFilter[]>([]);
const [leftGrouping, setLeftGrouping] = useState<string[]>([]);
const [leftColumnVisibility, setLeftColumnVisibility] = useState<ColumnVisibility[]>([]);
const [leftColumnOrder, setLeftColumnOrder] = useState<string[]>([]); // 🔧 컬럼 순서
const [rightFilters, setRightFilters] = useState<TableFilter[]>([]);
const [rightGrouping, setRightGrouping] = useState<string[]>([]);
const [rightColumnVisibility, setRightColumnVisibility] = useState<ColumnVisibility[]>([]);
@ -160,6 +163,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
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<SplitPanelLayoutComponentProps>
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<SplitPanelLayoutComponentProps>
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<SplitPanelLayoutComponentProps>
[rightTableColumns],
);
// 🔧 컬럼의 고유값 가져오기 함수
const getLeftColumnUniqueValues = useCallback(async (columnName: string) => {
const leftTableName = componentConfig.leftPanel?.tableName;
if (!leftTableName || leftData.length === 0) return [];
// 현재 로드된 데이터에서 고유값 추출
const uniqueValues = new Set<string>();
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<SplitPanelLayoutComponentProps>
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<SplitPanelLayoutComponentProps>
}
}, [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<SplitPanelLayoutComponentProps>
// 🔄 필터 변경 시 데이터 다시 로드
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