"use client";
import React, { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { TableListConfig, ColumnConfig } from "./types";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { tableTypeApi } from "@/lib/api/screen";
import { tableManagementApi } from "@/lib/api/tableManagement";
import {
Plus,
Trash2,
ArrowUp,
ArrowDown,
ChevronsUpDown,
Check,
Lock,
Unlock,
Database,
Table2,
Link2,
GripVertical,
X,
} from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { cn } from "@/lib/utils";
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core";
import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
/**
* 드래그 가능한 선택된 컬럼 행 (v2-split-panel-layout의 SortableColumnRow 동일 패턴)
*/
function SortableColumnRow({
id,
col,
index,
isEntityJoin,
onLabelChange,
onWidthChange,
onRemove,
}: {
id: string;
col: ColumnConfig;
index: number;
isEntityJoin?: boolean;
onLabelChange: (value: string) => void;
onWidthChange: (value: number) => void;
onRemove: () => void;
}) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
const style = { transform: CSS.Transform.toString(transform), transition };
return (
{isEntityJoin ? (
) : (
#{index + 1}
)}
onLabelChange(e.target.value)}
placeholder="표시명"
className="h-6 min-w-0 flex-1 text-xs"
/>
onWidthChange(parseInt(e.target.value) || 100)}
placeholder="너비"
className="h-6 w-14 shrink-0 text-xs"
/>
);
}
export interface TableListConfigPanelProps {
config: TableListConfig;
onChange: (config: Partial) => void;
screenTableName?: string; // 화면에 연결된 테이블명
tableColumns?: any[]; // 테이블 컬럼 정보
}
/**
* TableList 설정 패널
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
*/
export const TableListConfigPanel: React.FC = ({
config: configProp,
onChange,
screenTableName,
tableColumns,
}) => {
// config가 undefined인 경우 빈 객체로 초기화
const config = configProp || {};
// console.log("🔍 TableListConfigPanel props:", {
// config,
// configType: typeof config,
// configSelectedTable: config?.selectedTable,
// configPagination: config?.pagination,
// paginationEnabled: config?.pagination?.enabled,
// paginationPageSize: config?.pagination?.pageSize,
// configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object',
// screenTableName,
// tableColumns: tableColumns?.length,
// tableColumnsSample: tableColumns?.[0],
// });
const [availableTables, setAvailableTables] = useState>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // 테이블 Combobox 열림 상태
const [availableColumns, setAvailableColumns] = useState<
Array<{ columnName: string; dataType: string; label?: string; input_type?: string }>
>([]);
const [entityJoinColumns, setEntityJoinColumns] = useState<{
availableColumns: Array<{
tableName: string;
columnName: string;
columnLabel: string;
dataType: string;
joinAlias: string;
suggestedLabel: string;
}>;
joinTables: Array<{
tableName: string;
currentDisplayColumn: string;
availableColumns: Array<{
columnName: string;
columnLabel: string;
dataType: string;
description?: string;
}>;
}>;
}>({ availableColumns: [], joinTables: [] });
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
// 🆕 제외 필터용 참조 테이블 컬럼 목록
const [referenceTableColumns, setReferenceTableColumns] = useState<
Array<{ columnName: string; dataType: string; label?: string }>
>([]);
const [loadingReferenceColumns, setLoadingReferenceColumns] = useState(false);
// 🔄 외부에서 config가 변경될 때 내부 상태 동기화 (표의 페이지네이션 변경 감지)
useEffect(() => {
// console.log("🔄 TableListConfigPanel - 외부 config 변경 감지:", {
// configPagination: config?.pagination,
// configPageSize: config?.pagination?.pageSize,
// });
// 현재는 별도 내부 상태가 없어서 자동으로 UI가 업데이트됨
// 만약 내부 상태가 있다면 여기서 동기화 처리
}, [config]);
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
const [entityDisplayConfigs, setEntityDisplayConfigs] = useState<
Record<
string,
{
sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
selectedColumns: string[];
separator: string;
}
>
>({});
// 화면 테이블명이 있으면 자동으로 설정 (초기 한 번만)
useEffect(() => {
if (screenTableName && !config.selectedTable) {
// 기존 config의 모든 속성을 유지하면서 selectedTable만 추가/업데이트
const updatedConfig = {
...config,
selectedTable: screenTableName,
// 컬럼이 있으면 유지, 없으면 빈 배열
columns: config.columns || [],
};
onChange(updatedConfig);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenTableName]); // config.selectedTable이 없을 때만 실행되도록 의존성 최소화
// 테이블 목록 가져오기
useEffect(() => {
const fetchTables = async () => {
setLoadingTables(true);
try {
// API 클라이언트를 사용하여 올바른 포트로 호출
const response = await tableTypeApi.getTables();
setAvailableTables(
response.map((table: any) => ({
tableName: table.tableName,
displayName: table.displayName || table.tableName,
})),
);
} catch (error) {
console.error("테이블 목록 가져오기 실패:", error);
} finally {
setLoadingTables(false);
}
};
fetchTables();
}, []);
// 🆕 실제 사용할 테이블 이름 계산 (customTableName 우선)
const targetTableName = React.useMemo(() => {
if (config.useCustomTable && config.customTableName) {
return config.customTableName;
}
return config.selectedTable || screenTableName;
}, [config.useCustomTable, config.customTableName, config.selectedTable, screenTableName]);
// 선택된 테이블의 컬럼 목록 설정
useEffect(() => {
console.log(
"🔍 useEffect 실행됨 - targetTableName:",
targetTableName,
"config.useCustomTable:",
config.useCustomTable,
"config.customTableName:",
config.customTableName,
"config.selectedTable:",
config.selectedTable,
"screenTableName:",
screenTableName,
);
if (!targetTableName) {
console.log("🔧 컬럼 목록 숨김 - 테이블이 선택되지 않음");
setAvailableColumns([]);
return;
}
// tableColumns prop은 화면의 기본 테이블 컬럼이므로,
// 다른 테이블을 선택한 경우 반드시 API에서 가져오기
const isUsingDifferentTable = config.selectedTable && screenTableName && config.selectedTable !== screenTableName;
const shouldUseTableColumnsProp = !config.useCustomTable && !isUsingDifferentTable && tableColumns && tableColumns.length > 0;
if (shouldUseTableColumnsProp) {
const mappedColumns = tableColumns.map((column: any) => ({
columnName: column.columnName || column.name,
dataType: column.dataType || column.type || "text",
label: column.label || column.displayName || column.columnLabel || column.columnName || column.name,
input_type: column.input_type || column.inputType,
}));
setAvailableColumns(mappedColumns);
// selectedTable이 없으면 screenTableName으로 설정
if (!config.selectedTable && screenTableName) {
onChange({
...config,
selectedTable: screenTableName,
columns: config.columns || [],
});
}
} else {
// API에서 컬럼 정보 가져오기 - tableManagementApi 사용
const fetchColumns = async () => {
console.log("🔧 API에서 컬럼 정보 가져오기 (tableManagementApi):", targetTableName);
try {
const result = await tableManagementApi.getColumnList(targetTableName);
console.log("🔧 tableManagementApi 응답:", result);
if (result.success && result.data) {
// API 응답 구조: { columns: [...], total, page, ... }
const columns = Array.isArray(result.data) ? result.data : result.data.columns;
console.log("🔧 컬럼 배열:", columns);
if (columns && Array.isArray(columns)) {
setAvailableColumns(
columns.map((col: any) => ({
columnName: col.columnName,
dataType: col.dataType,
label: col.displayName || col.columnLabel || col.columnName,
input_type: col.input_type || col.inputType,
})),
);
} else {
console.error("🔧 컬럼 배열을 찾을 수 없음:", result.data);
setAvailableColumns([]);
}
} else {
console.error("🔧 컬럼 조회 실패:", result.message);
setAvailableColumns([]);
}
} catch (error) {
console.error("컬럼 목록 가져오기 실패:", error);
setAvailableColumns([]);
}
};
fetchColumns();
}
}, [targetTableName, config.useCustomTable, tableColumns]);
// Entity 조인 컬럼 정보 가져오기 - targetTableName 사용
useEffect(() => {
const fetchEntityJoinColumns = async () => {
if (!targetTableName) {
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
return;
}
setLoadingEntityJoins(true);
try {
console.log("🔗 Entity 조인 컬럼 정보 가져오기:", targetTableName);
const result = await entityJoinApi.getEntityJoinColumns(targetTableName);
console.log("✅ Entity 조인 컬럼 응답:", result);
setEntityJoinColumns({
availableColumns: result.availableColumns || [],
joinTables: result.joinTables || [],
});
} catch (error) {
console.error("❌ Entity 조인 컬럼 조회 오류:", error);
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
} finally {
setLoadingEntityJoins(false);
}
};
fetchEntityJoinColumns();
}, [targetTableName]);
// 🆕 제외 필터용 참조 테이블 컬럼 가져오기
useEffect(() => {
const fetchReferenceColumns = async () => {
const refTable = config.excludeFilter?.referenceTable;
if (!refTable) {
setReferenceTableColumns([]);
return;
}
setLoadingReferenceColumns(true);
try {
console.log("🔗 참조 테이블 컬럼 정보 가져오기:", refTable);
const result = await tableManagementApi.getColumnList(refTable);
if (result.success && result.data) {
// result.data는 { columns: [], total, page, size, totalPages } 형태
const columns = result.data.columns || [];
setReferenceTableColumns(
columns.map((col: any) => ({
columnName: col.columnName || col.column_name,
dataType: col.dataType || col.data_type || "text",
label: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
})),
);
console.log("✅ 참조 테이블 컬럼 로드 완료:", columns.length, "개");
}
} catch (error) {
console.error("❌ 참조 테이블 컬럼 조회 오류:", error);
setReferenceTableColumns([]);
} finally {
setLoadingReferenceColumns(false);
}
};
fetchReferenceColumns();
}, [config.excludeFilter?.referenceTable]);
// 🎯 엔티티 컬럼 자동 로드
useEffect(() => {
const entityColumns = config.columns?.filter((col) => col.isEntityJoin && col.entityDisplayConfig);
if (!entityColumns || entityColumns.length === 0) return;
// 각 엔티티 컬럼에 대해 자동으로 loadEntityDisplayConfig 호출
entityColumns.forEach((column) => {
// 이미 로드된 경우 스킵
if (entityDisplayConfigs[column.columnName]) {
return;
}
loadEntityDisplayConfig(column);
});
}, [config.columns]);
const handleChange = (key: keyof TableListConfig, value: any) => {
// 기존 config와 병합하여 전달 (다른 속성 손실 방지)
const newConfig = { ...config, [key]: value };
console.log("📊 TableListConfigPanel handleChange:", { key, value, newConfig });
onChange(newConfig);
};
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {
// console.log("🔧 TableListConfigPanel handleNestedChange:", {
// parentKey,
// childKey,
// value,
// parentValue: config[parentKey],
// hasOnChange: !!onChange,
// onChangeType: typeof onChange,
// });
const parentValue = config[parentKey] as any;
// 전체 config와 병합하여 다른 속성 유지
const newConfig = {
...config,
[parentKey]: {
...parentValue,
[childKey]: value,
},
};
// console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
onChange(newConfig);
};
// 컬럼 추가
const addColumn = (columnName: string) => {
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
if (existingColumn) return;
// tableColumns → availableColumns 순서로 한국어 라벨 찾기
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
const availableColumnInfo = availableColumns.find((col) => col.columnName === columnName);
const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName;
const newColumn: ColumnConfig = {
columnName,
displayName,
visible: true,
sortable: true,
searchable: true,
align: "left",
format: "text",
order: config.columns?.length || 0,
};
handleChange("columns", [...(config.columns || []), newColumn]);
};
// 🎯 조인 컬럼 추가 (조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리)
const addEntityColumn = (joinColumn: (typeof entityJoinColumns.availableColumns)[0]) => {
console.log("🔗 조인 컬럼 추가 요청:", {
joinColumn,
joinAlias: joinColumn.joinAlias,
columnLabel: joinColumn.columnLabel,
tableName: joinColumn.tableName,
columnName: joinColumn.columnName,
});
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
if (existingColumn) {
console.warn("⚠️ 이미 존재하는 컬럼:", joinColumn.joinAlias);
return;
}
// 🎯 joinTables에서 sourceColumn 찾기
const joinTableInfo = entityJoinColumns.joinTables?.find((jt: any) => jt.tableName === joinColumn.tableName);
const sourceColumn = joinTableInfo?.joinConfig?.sourceColumn || "";
console.log("🔍 조인 정보 추출:", {
tableName: joinColumn.tableName,
foundJoinTable: !!joinTableInfo,
sourceColumn,
joinConfig: joinTableInfo?.joinConfig,
});
// 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false)
const newColumn: ColumnConfig = {
columnName: joinColumn.joinAlias,
displayName: joinColumn.columnLabel,
visible: true,
sortable: true,
searchable: true,
align: "left",
format: "text",
order: config.columns?.length || 0,
isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님
// 🎯 추가 조인 정보 저장
additionalJoinInfo: {
sourceTable: config.selectedTable || screenTableName || "", // 기준 테이블 (예: user_info)
sourceColumn: sourceColumn, // 기준 컬럼 (예: dept_code) - joinTables에서 추출
referenceTable: joinColumn.tableName, // 참조 테이블 (예: dept_info)
joinAlias: joinColumn.joinAlias, // 조인 별칭 (예: dept_code_company_name)
},
};
handleChange("columns", [...(config.columns || []), newColumn]);
console.log("✅ 조인 컬럼 추가 완료:", {
columnName: newColumn.columnName,
displayName: newColumn.displayName,
totalColumns: (config.columns?.length || 0) + 1,
});
};
// 컬럼 제거
const removeColumn = (columnName: string) => {
const updatedColumns = config.columns?.filter((col) => col.columnName !== columnName) || [];
handleChange("columns", updatedColumns);
};
// 컬럼 업데이트
const updateColumn = (columnName: string, updates: Partial) => {
const updatedColumns =
config.columns?.map((col) => (col.columnName === columnName ? { ...col, ...updates } : col)) || [];
handleChange("columns", updatedColumns);
};
// 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정
// useRef로 이전 컬럼 개수를 추적하여 새 컬럼 추가 시에만 실행
const prevColumnsLengthRef = React.useRef(0);
useEffect(() => {
const currentLength = config.columns?.length || 0;
const prevLength = prevColumnsLengthRef.current;
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
hasColumns: !!config.columns,
columnsCount: currentLength,
prevColumnsCount: prevLength,
hasTableColumns: !!tableColumns,
tableColumnsCount: tableColumns?.length || 0,
selectedTable: config.selectedTable,
});
if (!config.columns || !tableColumns || config.columns.length === 0) {
console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵");
prevColumnsLengthRef.current = currentLength;
return;
}
// 컬럼 개수가 변경되지 않았고, 이미 체크한 적이 있으면 스킵
if (currentLength === prevLength && prevLength > 0) {
console.log("ℹ️ 컬럼 개수 변경 없음, 엔티티 감지 스킵");
return;
}
const updatedColumns = config.columns.map((column) => {
// 이미 isEntityJoin이 설정된 경우 스킵
if (column.isEntityJoin) {
console.log("✅ 이미 엔티티 플래그 설정됨:", column.columnName);
return column;
}
// 테이블 컬럼 정보에서 해당 컬럼 찾기
const tableColumn = tableColumns.find((tc) => tc.columnName === column.columnName);
console.log("🔍 컬럼 검색:", {
columnName: column.columnName,
found: !!tableColumn,
inputType: tableColumn?.input_type,
webType: tableColumn?.web_type,
});
// 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인)
if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) {
console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", {
columnName: column.columnName,
referenceTable: tableColumn.reference_table,
referenceTableAlt: tableColumn.referenceTable,
allTableColumnKeys: Object.keys(tableColumn),
});
return {
...column,
isEntityJoin: true,
entityJoinInfo: {
sourceTable: config.selectedTable || screenTableName || "",
sourceColumn: column.columnName,
joinAlias: column.columnName,
},
entityDisplayConfig: {
displayColumns: [], // 빈 배열로 초기화
separator: " - ",
sourceTable: config.selectedTable || screenTableName || "",
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
},
};
}
return column;
});
// 변경사항이 있는 경우에만 업데이트
const hasChanges = updatedColumns.some((col, index) => col.isEntityJoin !== config.columns![index].isEntityJoin);
if (hasChanges) {
console.log("🎯 엔티티 컬럼 플래그 업데이트:", updatedColumns);
handleChange("columns", updatedColumns);
} else {
console.log("ℹ️ 엔티티 컬럼 변경사항 없음");
}
// 현재 컬럼 개수를 저장
prevColumnsLengthRef.current = currentLength;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.columns?.length, tableColumns, config.selectedTable]); // 컬럼 개수 변경 시에만 실행
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
const loadEntityDisplayConfig = async (column: ColumnConfig) => {
const configKey = `${column.columnName}`;
// 이미 로드된 경우 스킵
if (entityDisplayConfigs[configKey]) return;
if (!column.isEntityJoin) {
// 엔티티 컬럼이 아니면 빈 상태로 설정하여 로딩 상태 해제
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
sourceColumns: [],
joinColumns: [],
selectedColumns: [],
separator: " - ",
},
}));
return;
}
// sourceTable 결정 우선순위:
// 1. entityDisplayConfig.sourceTable
// 2. entityJoinInfo.sourceTable
// 3. config.selectedTable
// 4. screenTableName
const sourceTable =
column.entityDisplayConfig?.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
// sourceTable이 비어있으면 빈 상태로 설정
if (!sourceTable) {
console.warn("⚠️ sourceTable을 찾을 수 없음:", column.columnName);
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
sourceColumns: [],
joinColumns: [],
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
},
}));
return;
}
let joinTable = column.entityDisplayConfig?.joinTable;
// joinTable이 없으면 tableTypeApi로 조회해서 설정
if (!joinTable) {
try {
console.log("🔍 tableTypeApi로 컬럼 정보 조회:", {
tableName: sourceTable,
columnName: column.columnName,
});
const columnList = await tableTypeApi.getColumns(sourceTable);
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
console.log("🔍 컬럼 정보 조회 결과:", {
columnInfo: columnInfo,
referenceTable: columnInfo?.reference_table || columnInfo?.referenceTable,
referenceColumn: columnInfo?.reference_column || columnInfo?.referenceColumn,
});
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
joinTable = columnInfo.reference_table || columnInfo.referenceTable;
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", joinTable);
// entityDisplayConfig 업데이트
const updatedConfig = {
...column.entityDisplayConfig,
sourceTable: sourceTable,
joinTable: joinTable,
displayColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
};
// 컬럼 설정 업데이트
const updatedColumns = config.columns?.map((col) =>
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
);
if (updatedColumns) {
handleChange("columns", updatedColumns);
}
} else {
console.warn("⚠️ tableTypeApi에서 조인 테이블 정보를 찾지 못함:", column.columnName);
}
} catch (error) {
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
}
}
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
try {
// 기본 테이블 컬럼 정보는 항상 로드
const sourceResult = await entityJoinApi.getReferenceTableColumns(sourceTable);
const sourceColumns = sourceResult.columns || [];
// joinTable이 있으면 조인 테이블 컬럼도 로드
let joinColumns: Array<{ columnName: string; displayName: string; dataType: string }> = [];
if (joinTable) {
try {
const joinResult = await entityJoinApi.getReferenceTableColumns(joinTable);
joinColumns = joinResult.columns || [];
} catch (joinError) {
console.warn("⚠️ 조인 테이블 컬럼 로드 실패:", joinTable, joinError);
// 조인 테이블 로드 실패해도 소스 테이블 컬럼은 표시
}
}
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
sourceColumns,
joinColumns,
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
},
}));
} catch (error) {
console.error("엔티티 표시 컬럼 정보 로드 실패:", error);
// 에러 발생 시에도 빈 상태로 설정하여 로딩 상태 해제
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
sourceColumns: [],
joinColumns: [],
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
},
}));
}
};
// 🎯 엔티티 표시 컬럼 선택 토글
const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => {
const configKey = `${columnName}`;
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn)
? localConfig.selectedColumns.filter((col) => col !== selectedColumn)
: [...localConfig.selectedColumns, selectedColumn];
// 로컬 상태 업데이트
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
selectedColumns: newSelectedColumns,
},
}));
// 실제 컬럼 설정도 업데이트
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === columnName && col.entityDisplayConfig) {
return {
...col,
entityDisplayConfig: {
...col.entityDisplayConfig,
displayColumns: newSelectedColumns,
},
};
}
return col;
});
if (updatedColumns) {
handleChange("columns", updatedColumns);
}
};
// 🎯 엔티티 표시 구분자 업데이트
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
const configKey = `${columnName}`;
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
// 로컬 상태 업데이트
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
separator,
},
}));
// 실제 컬럼 설정도 업데이트
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === columnName && col.entityDisplayConfig) {
return {
...col,
entityDisplayConfig: {
...col.entityDisplayConfig,
separator,
},
};
}
return col;
});
if (updatedColumns) {
handleChange("columns", updatedColumns);
}
};
// 컬럼 순서 변경
const moveColumn = (columnName: string, direction: "up" | "down") => {
const columns = [...(config.columns || [])];
const index = columns.findIndex((col) => col.columnName === columnName);
if (index === -1) return;
const targetIndex = direction === "up" ? index - 1 : index + 1;
if (targetIndex < 0 || targetIndex >= columns.length) return;
[columns[index], columns[targetIndex]] = [columns[targetIndex], columns[index]];
// order 값 재정렬
columns.forEach((col, idx) => {
col.order = idx;
});
handleChange("columns", columns);
};
// 테이블 변경 핸들러 - 테이블 변경 시 컬럼 설정 초기화
const handleTableChange = (newTableName: string) => {
if (newTableName === targetTableName) return;
const updatedConfig = {
...config,
selectedTable: newTableName,
// 테이블이 변경되면 컬럼 설정 초기화
columns: [],
};
onChange(updatedConfig);
setTableComboboxOpen(false);
};
return (
테이블 리스트 설정
{/* 테이블 선택 */}
데이터 소스
테이블을 선택하세요. 미선택 시 화면 메인 테이블을 사용합니다.
테이블을 찾을 수 없습니다.
{availableTables.map((table) => (
handleTableChange(table.tableName)}
className="text-xs"
>
{table.displayName}
{table.displayName !== table.tableName && (
{table.tableName}
)}
))}
{screenTableName && targetTableName !== screenTableName && (
화면 기본 테이블({screenTableName})과 다른 테이블을 사용 중
)}
{/* 툴바 버튼 설정 */}
툴바 버튼 설정
테이블 상단에 표시할 버튼을 선택합니다
handleNestedChange("toolbar", "showEditMode", checked)}
/>
handleNestedChange("toolbar", "showExcel", checked)}
/>
handleNestedChange("toolbar", "showPdf", checked)}
/>
handleNestedChange("toolbar", "showCopy", checked)}
/>
handleNestedChange("toolbar", "showSearch", checked)}
/>
handleNestedChange("toolbar", "showFilter", checked)}
/>
handleNestedChange("toolbar", "showRefresh", checked)}
/>
{/* 체크박스 설정 */}
체크박스 설정
handleNestedChange("checkbox", "enabled", checked)}
/>
{config.checkbox?.enabled && (
<>
handleNestedChange("checkbox", "selectAll", checked)}
/>
>
)}
{/* 기본 정렬 설정 */}
기본 정렬 설정
테이블 로드 시 기본 정렬 순서를 지정합니다
{config.defaultSort?.columnName && (
)}
{/* 가로 스크롤 및 컬럼 고정 */}
가로 스크롤 및 컬럼 고정
handleNestedChange("horizontalScroll", "enabled", checked)}
/>
{config.horizontalScroll?.enabled && (
)}
{/* 컬럼 설정 */}
{/* 🎯 엔티티 컬럼 표시 설정 섹션 */}
{config.columns?.some((col) => col.isEntityJoin) && (
{config.columns
?.filter((col) => col.isEntityJoin && col.entityDisplayConfig)
.map((column) => (
{column.displayName || column.columnName}
{entityDisplayConfigs[column.columnName] ? (
{/* 구분자 설정 */}
updateEntityDisplaySeparator(column.columnName, e.target.value)}
className="h-6 w-full text-xs"
style={{ fontSize: "12px" }}
placeholder=" - "
/>
{/* 표시 컬럼 선택 (다중 선택) */}
{entityDisplayConfigs[column.columnName].sourceColumns.length === 0 &&
entityDisplayConfigs[column.columnName].joinColumns.length === 0 ? (
표시 가능한 컬럼이 없습니다.
{!column.entityDisplayConfig?.joinTable && (
테이블 타입 관리에서 참조 테이블을 설정하면 더 많은 컬럼을 선택할 수 있습니다.
)}
) : (
컬럼을 찾을 수 없습니다.
{entityDisplayConfigs[column.columnName].sourceColumns.length > 0 && (
{entityDisplayConfigs[column.columnName].sourceColumns.map((col) => (
toggleEntityDisplayColumn(column.columnName, col.columnName)}
className="text-xs"
>
{col.displayName}
))}
)}
{entityDisplayConfigs[column.columnName].joinColumns.length > 0 && (
{entityDisplayConfigs[column.columnName].joinColumns.map((col) => (
toggleEntityDisplayColumn(column.columnName, col.columnName)}
className="text-xs"
>
{col.displayName}
))}
)}
)}
{/* 참조 테이블 미설정 안내 */}
{!column.entityDisplayConfig?.joinTable &&
entityDisplayConfigs[column.columnName].sourceColumns.length > 0 && (
현재 기본 테이블 컬럼만 표시됩니다. 테이블 타입 관리에서 참조 테이블을 설정하면 조인된
테이블의 컬럼도 선택할 수 있습니다.
)}
{/* 선택된 컬럼 미리보기 */}
{entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && (
{entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => (
{colName}
{idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && (
{entityDisplayConfigs[column.columnName].separator}
)}
))}
)}
) : (
컬럼 정보 로딩 중...
)}
))}
)}
{!targetTableName ? (
테이블이 선택되지 않았습니다.
위 데이터 소스에서 테이블을 선택하세요.
) : availableColumns.length === 0 ? (
컬럼을 추가하려면 먼저 컴포넌트에 테이블을 명시적으로 선택하거나
기본 설정 탭에서 테이블을 설정해주세요.
현재 화면 테이블: {screenTableName}
) : (
<>
{availableColumns.length > 0 ? (
{availableColumns.map((column) => {
const isAdded = config.columns?.some((c) => c.columnName === column.columnName);
return (
{
if (isAdded) {
// 컬럼 제거
handleChange(
"columns",
config.columns?.filter((c) => c.columnName !== column.columnName) || [],
);
} else {
// 컬럼 추가
addColumn(column.columnName);
}
}}
>
{
if (isAdded) {
handleChange(
"columns",
config.columns?.filter((c) => c.columnName !== column.columnName) || [],
);
} else {
addColumn(column.columnName);
}
}}
className="pointer-events-none h-3.5 w-3.5"
/>
{column.label || column.columnName}
{isAdded && (
)}
{column.input_type || column.dataType}
);
})}
) : (
컬럼 정보를 불러오는 중...
)}
{/* Entity 조인 컬럼 추가 */}
{entityJoinColumns.joinTables.length > 0 && (
Entity 조인 컬럼
연관 테이블의 컬럼을 선택하세요
{entityJoinColumns.joinTables.map((joinTable, tableIndex) => (
{joinTable.tableName}
{joinTable.currentDisplayColumn}
{joinTable.availableColumns.map((column, colIndex) => {
const matchingJoinColumn = entityJoinColumns.availableColumns.find(
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
);
const isAlreadyAdded = config.columns?.some(
(col) => col.columnName === matchingJoinColumn?.joinAlias,
);
if (!matchingJoinColumn) return null;
return (
{
if (isAlreadyAdded) {
// 컬럼 제거
handleChange(
"columns",
config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) || [],
);
} else {
// 컬럼 추가
addEntityColumn(matchingJoinColumn);
}
}}
>
{
if (isAlreadyAdded) {
handleChange(
"columns",
config.columns?.filter((c) => c.columnName !== matchingJoinColumn.joinAlias) ||
[],
);
} else {
addEntityColumn(matchingJoinColumn);
}
}}
className="pointer-events-none h-3.5 w-3.5"
/>
{column.columnLabel}
{column.inputType || column.dataType}
);
})}
))}
)}
>
)}
{/* 선택된 컬럼 순서 변경 (DnD) */}
{config.columns && config.columns.length > 0 && (
표시할 컬럼 ({config.columns.length}개 선택)
드래그하여 순서를 변경하거나 표시명/너비를 수정할 수 있습니다
{
const { active, over } = event;
if (!over || active.id === over.id) return;
const columns = [...(config.columns || [])];
const oldIndex = columns.findIndex((c) => c.columnName === active.id);
const newIndex = columns.findIndex((c) => c.columnName === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
const reordered = arrayMove(columns, oldIndex, newIndex);
reordered.forEach((col, idx) => { col.order = idx; });
handleChange("columns", reordered);
}
}}
>
c.columnName)}
strategy={verticalListSortingStrategy}
>
{(config.columns || []).map((column, idx) => {
// displayName이 columnName과 같으면 한국어 라벨 미설정 → availableColumns에서 찾기
const resolvedLabel =
column.displayName && column.displayName !== column.columnName
? column.displayName
: availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName;
const colWithLabel = { ...column, displayName: resolvedLabel };
return (
updateColumn(column.columnName, { displayName: value })}
onWidthChange={(value) => updateColumn(column.columnName, { width: value })}
onRemove={() => removeColumn(column.columnName)}
/>
);
})}
)}
{/* 🆕 데이터 필터링 설정 */}
데이터 필터링
특정 컬럼 값으로 데이터를 필터링합니다
({
columnName: col.columnName,
columnLabel: col.label || col.columnName,
dataType: col.dataType,
input_type: col.input_type, // 🆕 실제 input_type 전달
}) as any,
)}
config={config.dataFilter}
onConfigChange={(dataFilter) => handleChange("dataFilter", dataFilter)}
/>
);
};