ERP-node/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx

1833 lines
80 KiB
TypeScript
Raw Normal View History

2025-09-15 11:43:59 +09:00
"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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { TableListConfig, ColumnConfig } from "./types";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { tableTypeApi } from "@/lib/api/screen";
2025-09-18 15:14:14 +09:00
import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react";
2025-09-15 11:43:59 +09:00
export interface TableListConfigPanelProps {
config: TableListConfig;
onChange: (config: Partial<TableListConfig>) => void;
screenTableName?: string; // 화면에 연결된 테이블명
tableColumns?: any[]; // 테이블 컬럼 정보
}
/**
* TableList
* UI
*/
export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
config,
onChange,
screenTableName,
tableColumns,
}) => {
console.log("🔍 TableListConfigPanel props:", {
config: config?.selectedTable,
screenTableName,
tableColumns: tableColumns?.length,
tableColumnsSample: tableColumns?.[0],
});
2025-09-15 11:43:59 +09:00
const [availableTables, setAvailableTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [availableColumns, setAvailableColumns] = useState<
Array<{ columnName: string; dataType: string; label?: 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);
2025-09-15 11:43:59 +09:00
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
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;
}
>
>({});
2025-09-15 11:43:59 +09:00
// 화면 테이블명이 있으면 자동으로 설정
useEffect(() => {
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
console.log("🔄 화면 테이블명 자동 설정:", screenTableName);
onChange({ selectedTable: screenTableName });
}
}, [screenTableName, config.selectedTable, onChange]);
// 테이블 목록 가져오기
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,
})),
);
2025-09-15 11:43:59 +09:00
} catch (error) {
console.error("테이블 목록 가져오기 실패:", error);
} finally {
setLoadingTables(false);
}
};
fetchTables();
}, []);
2025-09-23 14:26:18 +09:00
// 선택된 테이블의 컬럼 목록 설정
2025-09-15 11:43:59 +09:00
useEffect(() => {
2025-09-23 14:26:18 +09:00
console.log(
"🔍 useEffect 실행됨 - config.selectedTable:",
config.selectedTable,
"screenTableName:",
screenTableName,
);
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우에만 컬럼 목록 표시
const shouldShowColumns = config.selectedTable || (screenTableName && config.columns && config.columns.length > 0);
if (!shouldShowColumns) {
console.log("🔧 컬럼 목록 숨김 - 명시적 테이블 선택 또는 설정된 컬럼이 없음");
setAvailableColumns([]);
return;
}
// tableColumns prop을 우선 사용하되, 컴포넌트가 명시적으로 설정되었을 때만
if (tableColumns && tableColumns.length > 0 && (config.selectedTable || config.columns?.length > 0)) {
2025-09-15 11:43:59 +09:00
console.log("🔧 tableColumns prop 사용:", tableColumns);
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,
}));
console.log("🏷️ availableColumns 설정됨:", mappedColumns);
setAvailableColumns(mappedColumns);
} else if (config.selectedTable || screenTableName) {
// API에서 컬럼 정보 가져오기
const fetchColumns = async () => {
const tableName = config.selectedTable || screenTableName;
if (!tableName) {
setAvailableColumns([]);
return;
}
console.log("🔧 API에서 컬럼 정보 가져오기:", tableName);
try {
const response = await fetch(`/api/tables/${tableName}/columns`);
if (response.ok) {
const result = await response.json();
if (result.success && result.data) {
console.log("🔧 API 응답 컬럼 데이터:", result.data);
setAvailableColumns(
result.data.map((col: any) => ({
columnName: col.columnName,
dataType: col.dataType,
label: col.displayName || col.columnName,
})),
);
}
}
} catch (error) {
console.error("컬럼 목록 가져오기 실패:", error);
}
};
fetchColumns();
} else {
setAvailableColumns([]);
}
2025-09-23 14:26:18 +09:00
}, [config.selectedTable, screenTableName, tableColumns, config.columns]);
2025-09-15 11:43:59 +09:00
// Entity 조인 컬럼 정보 가져오기
useEffect(() => {
const fetchEntityJoinColumns = async () => {
const tableName = config.selectedTable || screenTableName;
if (!tableName) {
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
return;
}
setLoadingEntityJoins(true);
try {
console.log("🔗 Entity 조인 컬럼 정보 가져오기:", tableName);
const result = await entityJoinApi.getEntityJoinColumns(tableName);
console.log("✅ Entity 조인 컬럼 응답:", result);
setEntityJoinColumns({
availableColumns: result.availableColumns || [],
joinTables: result.joinTables || [],
});
} catch (error) {
console.error("❌ Entity 조인 컬럼 조회 오류:", error);
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
} finally {
setLoadingEntityJoins(false);
}
};
fetchEntityJoinColumns();
}, [config.selectedTable, screenTableName]);
2025-09-15 11:43:59 +09:00
const handleChange = (key: keyof TableListConfig, value: any) => {
onChange({ [key]: value });
};
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {
const parentValue = config[parentKey] as any;
onChange({
[parentKey]: {
...parentValue,
[childKey]: value,
},
});
};
// 컬럼 추가
const addColumn = (columnName: string) => {
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
if (existingColumn) return;
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
// 라벨명 우선 사용, 없으면 컬럼명 사용
const displayName = columnInfo?.label || columnInfo?.displayName || 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]) => {
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
if (existingColumn) return;
// 기본 표시명으로 엔티티 컬럼 추가 (컬럼 설정 패널에서 나중에 표시 컬럼 조합 선택)
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: true,
entityJoinInfo: {
sourceTable: config.selectedTable || "",
sourceColumn: joinColumn.columnName,
joinAlias: joinColumn.joinAlias,
},
// 🎯 엔티티 표시 설정 (기본값으로 초기화, 컬럼 설정에서 수정 가능)
entityDisplayConfig: {
displayColumns: [], // 빈 배열로 초기화
separator: " - ",
sourceTable: config.selectedTable || "",
joinTable: joinColumn.tableName,
},
};
handleChange("columns", [...(config.columns || []), newColumn]);
console.log("🔗 엔티티 컬럼 추가됨 (표시 컬럼은 컬럼 설정에서 선택):", newColumn);
};
2025-09-15 11:43:59 +09:00
// 컬럼 제거
const removeColumn = (columnName: string) => {
const updatedColumns = config.columns?.filter((col) => col.columnName !== columnName) || [];
handleChange("columns", updatedColumns);
};
// 컬럼 업데이트
const updateColumn = (columnName: string, updates: Partial<ColumnConfig>) => {
const updatedColumns =
config.columns?.map((col) => (col.columnName === columnName ? { ...col, ...updates } : col)) || [];
handleChange("columns", updatedColumns);
};
// 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정
useEffect(() => {
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
hasColumns: !!config.columns,
columnsCount: config.columns?.length || 0,
hasTableColumns: !!tableColumns,
tableColumnsCount: tableColumns?.length || 0,
selectedTable: config.selectedTable,
});
if (!config.columns || !tableColumns) {
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 || "",
sourceColumn: column.columnName,
joinAlias: column.columnName,
},
entityDisplayConfig: {
displayColumns: [], // 빈 배열로 초기화
separator: " - ",
sourceTable: config.selectedTable || "",
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(" 엔티티 컬럼 변경사항 없음");
}
}, [config.columns, tableColumns, config.selectedTable]);
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
const loadEntityDisplayConfig = async (column: ColumnConfig) => {
2025-09-23 17:43:24 +09:00
console.log("🔍 loadEntityDisplayConfig 시작:", {
columnName: column.columnName,
isEntityJoin: column.isEntityJoin,
entityJoinInfo: column.entityJoinInfo,
entityDisplayConfig: column.entityDisplayConfig,
configSelectedTable: config.selectedTable,
});
if (!column.isEntityJoin || !column.entityJoinInfo) {
console.log("⚠️ 엔티티 컬럼 조건 불만족:", {
isEntityJoin: column.isEntityJoin,
entityJoinInfo: column.entityJoinInfo,
});
return;
}
// entityDisplayConfig가 없으면 초기화
if (!column.entityDisplayConfig) {
console.log("🔧 entityDisplayConfig 초기화:", column.columnName);
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === column.columnName) {
return {
...col,
entityDisplayConfig: {
displayColumns: [],
separator: " - ",
sourceTable: config.selectedTable || "",
joinTable: "",
},
};
}
return col;
});
if (updatedColumns) {
handleChange("columns", updatedColumns);
// 업데이트된 컬럼으로 다시 시도
const updatedColumn = updatedColumns.find((col) => col.columnName === column.columnName);
if (updatedColumn) {
console.log("🔄 업데이트된 컬럼으로 재시도:", updatedColumn.entityDisplayConfig);
return loadEntityDisplayConfig(updatedColumn);
}
}
return;
}
console.log("🔍 entityDisplayConfig 전체 구조:", column.entityDisplayConfig);
console.log("🔍 entityDisplayConfig 키들:", Object.keys(column.entityDisplayConfig));
// sourceTable과 joinTable이 없으면 entityJoinInfo에서 가져오기
let sourceTable = column.entityDisplayConfig.sourceTable;
let joinTable = column.entityDisplayConfig.joinTable;
if (!sourceTable && column.entityJoinInfo) {
sourceTable = column.entityJoinInfo.sourceTable;
}
if (!joinTable) {
// joinTable이 없으면 tableTypeApi로 조회해서 설정
try {
console.log("🔍 joinTable이 없어서 tableTypeApi로 조회:", sourceTable);
const columnList = await tableTypeApi.getColumns(sourceTable);
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
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,
};
// 컬럼 설정 업데이트
const updatedColumns = config.columns?.map((col) =>
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
);
if (updatedColumns) {
handleChange("columns", updatedColumns);
}
}
} catch (error) {
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
}
}
2025-09-23 17:43:24 +09:00
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
const configKey = `${column.columnName}`;
// 이미 로드된 경우 스킵
if (entityDisplayConfigs[configKey]) return;
2025-09-23 17:43:24 +09:00
// joinTable이 비어있으면 tableTypeApi로 컬럼 정보를 다시 가져와서 referenceTable 정보를 찾기
let actualJoinTable = joinTable;
if (!actualJoinTable && sourceTable) {
try {
2025-09-23 17:43:24 +09:00
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) {
actualJoinTable = columnInfo.reference_table || columnInfo.referenceTable;
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", actualJoinTable);
// entityDisplayConfig 업데이트
const updatedConfig = {
...column.entityDisplayConfig,
joinTable: actualJoinTable,
};
2025-09-23 17:43:24 +09:00
// 컬럼 설정 업데이트
const updatedColumns = config.columns?.map((col) =>
2025-09-23 17:43:24 +09:00
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
);
2025-09-23 17:43:24 +09:00
if (updatedColumns) {
handleChange("columns", updatedColumns);
}
2025-09-23 17:43:24 +09:00
} else {
console.log("⚠️ tableTypeApi에서도 referenceTable을 찾을 수 없음:", {
columnName: column.columnName,
columnInfo: columnInfo,
});
}
} catch (error) {
2025-09-23 17:43:24 +09:00
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
}
}
// sourceTable과 joinTable이 모두 있어야 로드
if (!sourceTable || !actualJoinTable) {
console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable: actualJoinTable });
return;
}
try {
// 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드
const [sourceResult, joinResult] = await Promise.all([
entityJoinApi.getReferenceTableColumns(sourceTable),
entityJoinApi.getReferenceTableColumns(actualJoinTable),
]);
const sourceColumns = sourceResult.columns || [];
const joinColumns = joinResult.columns || [];
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
sourceColumns,
joinColumns,
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
separator: column.entityDisplayConfig?.separator || " - ",
},
}));
} catch (error) {
console.error("엔티티 표시 컬럼 정보 로드 실패:", error);
}
};
// 🎯 엔티티 표시 컬럼 선택 토글
const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => {
const configKey = `${columnName}`;
2025-09-23 17:43:24 +09:00
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
2025-09-23 17:43:24 +09:00
const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn)
? localConfig.selectedColumns.filter((col) => col !== selectedColumn)
: [...localConfig.selectedColumns, selectedColumn];
2025-09-23 17:43:24 +09:00
// 로컬 상태 업데이트
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
selectedColumns: newSelectedColumns,
},
}));
2025-09-23 17:43:24 +09:00
// 실제 컬럼 설정도 업데이트
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);
console.log("🎯 엔티티 표시 컬럼 설정 업데이트:", {
columnName,
selectedColumns: newSelectedColumns,
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
});
}
// 컬럼 설정 업데이트
updateColumn(columnName, {
entityDisplayConfig: {
...config.entityDisplayConfig,
displayColumns: newSelectedColumns,
},
});
};
// 🎯 엔티티 표시 구분자 업데이트
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
const configKey = `${columnName}`;
2025-09-23 17:43:24 +09:00
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
2025-09-23 17:43:24 +09:00
// 로컬 상태 업데이트
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
separator,
},
}));
2025-09-23 17:43:24 +09:00
// 실제 컬럼 설정도 업데이트
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === columnName && col.entityDisplayConfig) {
return {
...col,
entityDisplayConfig: {
...col.entityDisplayConfig,
separator,
},
};
}
return col;
});
2025-09-23 17:43:24 +09:00
if (updatedColumns) {
handleChange("columns", updatedColumns);
console.log("🎯 엔티티 표시 구분자 설정 업데이트:", {
columnName,
separator,
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
});
}
};
2025-09-15 11:43:59 +09:00
// 컬럼 순서 변경
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);
};
2025-09-23 14:26:18 +09:00
// 필터 추가
const addFilter = (columnName: string) => {
const existingFilter = config.filter?.filters?.find((f) => f.columnName === columnName);
if (existingFilter) return;
const column = availableColumns.find((col) => col.columnName === columnName);
if (!column) return;
// tableColumns에서 해당 컬럼의 메타정보 찾기
const tableColumn = tableColumns?.find((tc) => tc.columnName === columnName);
2025-09-23 14:26:18 +09:00
// 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정
const inferWidgetType = (dataType: string, webType?: string): string => {
// 웹타입이 있으면 우선 사용
if (webType) {
return webType;
}
// 데이터 타입으로 추론
const type = dataType.toLowerCase();
if (type.includes("int") || type.includes("numeric") || type.includes("decimal")) return "number";
if (type.includes("date") || type.includes("timestamp")) return "date";
if (type.includes("bool")) return "boolean";
return "text";
};
const widgetType = inferWidgetType(column.dataType, tableColumn?.webType || tableColumn?.web_type);
const newFilter = {
columnName,
widgetType,
label: column.label || column.columnName,
gridColumns: 3,
numberFilterMode: "range" as const,
// 코드 타입인 경우 코드 카테고리 추가
...(widgetType === "code" && {
codeCategory: tableColumn?.codeCategory || tableColumn?.code_category,
}),
// 엔티티 타입인 경우 참조 정보 추가
...(widgetType === "entity" && {
referenceTable: tableColumn?.referenceTable || tableColumn?.reference_table,
referenceColumn: tableColumn?.referenceColumn || tableColumn?.reference_column,
displayColumn: tableColumn?.displayColumn || tableColumn?.display_column,
}),
};
console.log("🔍 필터 추가:", newFilter);
const currentFilters = config.filter?.filters || [];
handleNestedChange("filter", "filters", [...currentFilters, newFilter]);
};
// 필터 제거
const removeFilter = (index: number) => {
const currentFilters = config.filter?.filters || [];
const updatedFilters = currentFilters.filter((_, i) => i !== index);
handleNestedChange("filter", "filters", updatedFilters);
};
// 필터 업데이트
const updateFilter = (index: number, key: string, value: any) => {
const currentFilters = config.filter?.filters || [];
const updatedFilters = currentFilters.map((filter, i) => (i === index ? { ...filter, [key]: value } : filter));
handleNestedChange("filter", "filters", updatedFilters);
};
2025-09-15 11:43:59 +09:00
return (
<div className="space-y-4">
<div className="text-sm font-medium"> </div>
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-6">
2025-09-15 11:43:59 +09:00
<TabsTrigger value="basic" className="flex items-center gap-1">
<Settings className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="columns" className="flex items-center gap-1">
<Columns className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="join-columns" className="flex items-center gap-1">
<Plus className="h-3 w-3" />
</TabsTrigger>
2025-09-15 11:43:59 +09:00
<TabsTrigger value="filter" className="flex items-center gap-1">
<Filter className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="actions" className="flex items-center gap-1">
<MousePointer className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="style" className="flex items-center gap-1">
<Palette className="h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 기본 설정 탭 */}
<TabsContent value="basic" className="space-y-4">
2025-09-18 15:14:14 +09:00
<ScrollArea className="h-[600px] pr-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> </Label>
<div className="rounded-md bg-gray-50 p-3">
<div className="text-sm font-medium">
{screenTableName ? (
<span className="text-blue-600">{screenTableName}</span>
) : (
<span className="text-gray-500"> </span>
)}
</div>
{screenTableName && (
<div className="mt-1 text-xs text-gray-500"> </div>
2025-09-15 11:43:59 +09:00
)}
</div>
</div>
2025-09-18 15:14:14 +09:00
2025-09-15 11:43:59 +09:00
<div className="space-y-2">
2025-09-18 15:14:14 +09:00
<Label htmlFor="title"></Label>
2025-09-15 11:43:59 +09:00
<Input
2025-09-18 15:14:14 +09:00
id="title"
value={config.title || ""}
onChange={(e) => handleChange("title", e.target.value)}
placeholder="테이블 제목 (선택사항)"
2025-09-15 11:43:59 +09:00
/>
</div>
2025-09-18 15:14:14 +09:00
</CardContent>
</Card>
2025-09-15 11:43:59 +09:00
2025-09-18 15:14:14 +09:00
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="showHeader"
checked={config.showHeader}
onCheckedChange={(checked) => handleChange("showHeader", checked)}
/>
<Label htmlFor="showHeader"> </Label>
</div>
2025-09-15 11:43:59 +09:00
2025-09-18 15:14:14 +09:00
<div className="flex items-center space-x-2">
<Checkbox
id="showFooter"
checked={config.showFooter}
onCheckedChange={(checked) => handleChange("showFooter", checked)}
/>
<Label htmlFor="showFooter"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="autoLoad"
checked={config.autoLoad}
onCheckedChange={(checked) => handleChange("autoLoad", checked)}
/>
<Label htmlFor="autoLoad"> </Label>
</div>
</CardContent>
</Card>
2025-09-15 11:43:59 +09:00
<Card>
2025-09-18 15:14:14 +09:00
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> </Label>
<Select
value={config.height}
onValueChange={(value: "auto" | "fixed" | "viewport") => handleChange("height", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto"></SelectItem>
<SelectItem value="fixed"></SelectItem>
<SelectItem value="viewport"> </SelectItem>
</SelectContent>
</Select>
2025-09-15 11:43:59 +09:00
</div>
2025-09-18 15:14:14 +09:00
{config.height === "fixed" && (
<div className="space-y-2">
<Label htmlFor="fixedHeight"> (px)</Label>
<Input
id="fixedHeight"
type="number"
value={config.fixedHeight || 400}
onChange={(e) => handleChange("fixedHeight", parseInt(e.target.value) || 400)}
min={200}
max={1000}
/>
</div>
)}
2025-09-15 11:43:59 +09:00
</CardContent>
</Card>
2025-09-18 15:14:14 +09:00
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="paginationEnabled"
checked={config.pagination?.enabled}
onCheckedChange={(checked) => handleNestedChange("pagination", "enabled", checked)}
/>
<Label htmlFor="paginationEnabled"> </Label>
</div>
{config.pagination?.enabled && (
<>
<div className="space-y-2">
<Label htmlFor="pageSize"> </Label>
<Select
value={config.pagination?.pageSize?.toString() || "20"}
onValueChange={(value) => handleNestedChange("pagination", "pageSize", parseInt(value))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
2025-09-15 11:43:59 +09:00
</div>
2025-09-18 15:14:14 +09:00
<div className="flex items-center space-x-2">
<Checkbox
id="showSizeSelector"
checked={config.pagination?.showSizeSelector}
onCheckedChange={(checked) => handleNestedChange("pagination", "showSizeSelector", checked)}
/>
<Label htmlFor="showSizeSelector"> </Label>
2025-09-15 11:43:59 +09:00
</div>
2025-09-18 15:14:14 +09:00
<div className="flex items-center space-x-2">
<Checkbox
id="showPageInfo"
checked={config.pagination?.showPageInfo}
onCheckedChange={(checked) => handleNestedChange("pagination", "showPageInfo", checked)}
/>
<Label htmlFor="showPageInfo"> </Label>
</div>
</>
)}
</CardContent>
</Card>
2025-09-15 11:43:59 +09:00
<Card>
<CardHeader>
2025-09-18 15:14:14 +09:00
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
2025-09-15 11:43:59 +09:00
</CardHeader>
2025-09-18 15:14:14 +09:00
<CardContent className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="horizontalScrollEnabled"
checked={config.horizontalScroll?.enabled}
onCheckedChange={(checked) => handleNestedChange("horizontalScroll", "enabled", checked)}
/>
<Label htmlFor="horizontalScrollEnabled"> </Label>
</div>
{config.horizontalScroll?.enabled && (
2025-09-15 11:43:59 +09:00
<div className="space-y-3">
2025-09-18 15:14:14 +09:00
<div className="space-y-1">
<Label htmlFor="maxVisibleColumns" className="text-sm">
</Label>
<Input
id="maxVisibleColumns"
type="number"
value={config.horizontalScroll?.maxVisibleColumns || 8}
onChange={(e) =>
handleNestedChange("horizontalScroll", "maxVisibleColumns", parseInt(e.target.value) || 8)
}
min={3}
max={20}
placeholder="8"
className="h-8"
/>
<div className="text-xs text-gray-500"> </div>
</div>
2025-09-15 11:43:59 +09:00
2025-09-18 15:14:14 +09:00
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label htmlFor="minColumnWidth" className="text-sm">
(px)
</Label>
<Input
id="minColumnWidth"
type="number"
value={config.horizontalScroll?.minColumnWidth || 100}
onChange={(e) =>
handleNestedChange("horizontalScroll", "minColumnWidth", parseInt(e.target.value) || 100)
}
min={50}
max={500}
placeholder="100"
className="h-8"
/>
</div>
<div className="space-y-1">
<Label htmlFor="maxColumnWidth" className="text-sm">
(px)
</Label>
<Input
id="maxColumnWidth"
type="number"
value={config.horizontalScroll?.maxColumnWidth || 300}
onChange={(e) =>
handleNestedChange("horizontalScroll", "maxColumnWidth", parseInt(e.target.value) || 300)
}
min={100}
max={800}
placeholder="300"
className="h-8"
/>
</div>
</div>
</div>
2025-09-18 18:49:30 +09:00
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="checkboxEnabled"
checked={config.checkbox?.enabled}
onCheckedChange={(checked) => handleNestedChange("checkbox", "enabled", checked)}
/>
<Label htmlFor="checkboxEnabled"> </Label>
</div>
{config.checkbox?.enabled && (
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="checkboxMultiple"
checked={config.checkbox?.multiple}
onCheckedChange={(checked) => handleNestedChange("checkbox", "multiple", checked)}
/>
<Label htmlFor="checkboxMultiple"> ()</Label>
</div>
<div className="space-y-1">
<Label htmlFor="checkboxPosition" className="text-sm">
</Label>
<Select
value={config.checkbox?.position || "left"}
onValueChange={(value) => handleNestedChange("checkbox", "position", value)}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="위치 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="checkboxSelectAll"
checked={config.checkbox?.selectAll}
onCheckedChange={(checked) => handleNestedChange("checkbox", "selectAll", checked)}
/>
<Label htmlFor="checkboxSelectAll"> / </Label>
</div>
</div>
2025-09-18 15:14:14 +09:00
)}
</CardContent>
</Card>
</ScrollArea>
</TabsContent>
{/* 컬럼 설정 탭 */}
<TabsContent value="columns" className="space-y-4">
<ScrollArea className="h-[600px] pr-4">
{/* 🎯 엔티티 컬럼 표시 설정 섹션 - 컬럼 설정 패널 바깥으로 분리 */}
{config.columns?.some((col) => col.isEntityJoin) && (
<Card className="border-l-4 border-l-orange-500">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">🎯 </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{config.columns
?.filter((col) => col.isEntityJoin && col.entityDisplayConfig)
.map((column) => (
<div key={column.columnName} className="space-y-3 rounded-lg border bg-orange-50 p-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-orange-300 text-orange-600">
{column.columnName}
</Badge>
<span className="text-sm font-medium">{column.displayName}</span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => loadEntityDisplayConfig(column)}
className="h-6 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{entityDisplayConfigs[column.columnName] && (
<div className="space-y-3">
{/* 구분자 설정 */}
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={entityDisplayConfigs[column.columnName].separator}
onChange={(e) => updateEntityDisplaySeparator(column.columnName, e.target.value)}
className="h-7 text-xs"
placeholder=" - "
/>
</div>
{/* 기본 테이블 컬럼 */}
<div className="space-y-1">
<Label className="text-xs text-blue-600">
: {column.entityDisplayConfig?.sourceTable}
</Label>
<div className="grid max-h-20 grid-cols-2 gap-1 overflow-y-auto">
{entityDisplayConfigs[column.columnName].sourceColumns.map((col) => (
<div key={col.columnName} className="flex items-center space-x-1">
<Checkbox
id={`source-${column.columnName}-${col.columnName}`}
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(
col.columnName,
)}
onCheckedChange={() =>
toggleEntityDisplayColumn(column.columnName, col.columnName)
}
className="h-3 w-3"
/>
<Label
htmlFor={`source-${column.columnName}-${col.columnName}`}
className="flex-1 cursor-pointer text-xs"
>
{col.displayName}
</Label>
</div>
))}
</div>
</div>
{/* 조인 테이블 컬럼 */}
<div className="space-y-1">
<Label className="text-xs text-green-600">
: {column.entityDisplayConfig?.joinTable}
</Label>
<div className="grid max-h-20 grid-cols-2 gap-1 overflow-y-auto">
{entityDisplayConfigs[column.columnName].joinColumns.map((col) => (
<div key={col.columnName} className="flex items-center space-x-1">
<Checkbox
id={`join-${column.columnName}-${col.columnName}`}
checked={entityDisplayConfigs[column.columnName].selectedColumns.includes(
col.columnName,
)}
onCheckedChange={() =>
toggleEntityDisplayColumn(column.columnName, col.columnName)
}
className="h-3 w-3"
/>
<Label
htmlFor={`join-${column.columnName}-${col.columnName}`}
className="flex-1 cursor-pointer text-xs"
>
{col.displayName}
</Label>
</div>
))}
</div>
</div>
{/* 선택된 컬럼 미리보기 */}
{entityDisplayConfigs[column.columnName].selectedColumns.length > 0 && (
<div className="space-y-1">
<Label className="text-xs"></Label>
<div className="flex flex-wrap gap-1 rounded bg-gray-50 p-2 text-xs">
{entityDisplayConfigs[column.columnName].selectedColumns.map((colName, idx) => (
<React.Fragment key={colName}>
<Badge variant="secondary" className="text-xs">
{colName}
</Badge>
{idx < entityDisplayConfigs[column.columnName].selectedColumns.length - 1 && (
<span className="text-gray-400">
{entityDisplayConfigs[column.columnName].separator}
</span>
)}
</React.Fragment>
))}
</div>
</div>
)}
</div>
)}
</div>
))}
</CardContent>
</Card>
)}
2025-09-18 15:14:14 +09:00
{!screenTableName ? (
<Card>
<CardContent className="pt-6">
<div className="text-center text-gray-500">
<p> .</p>
<p className="text-sm"> .</p>
</div>
</CardContent>
</Card>
2025-09-23 14:26:18 +09:00
) : availableColumns.length === 0 ? (
<Card>
<CardContent className="pt-6">
<div className="text-center text-gray-500">
<p> </p>
<p className="text-sm"> .</p>
<p className="mt-2 text-xs text-blue-600"> : {screenTableName}</p>
</div>
</CardContent>
</Card>
2025-09-18 15:14:14 +09:00
) : (
<>
<Card>
<CardHeader>
<CardTitle className="text-base"> - {screenTableName}</CardTitle>
<CardDescription>
{availableColumns.length > 0
? `${availableColumns.length}개의 사용 가능한 컬럼에서 선택하세요`
: "컬럼 정보를 불러오는 중..."}
</CardDescription>
</CardHeader>
<CardContent>
{availableColumns.length > 0 ? (
<div className="flex flex-wrap gap-2">
{availableColumns
.filter((col) => !config.columns?.find((c) => c.columnName === col.columnName))
.map((column) => (
2025-09-15 11:43:59 +09:00
<Button
2025-09-18 15:14:14 +09:00
key={column.columnName}
variant="outline"
2025-09-15 11:43:59 +09:00
size="sm"
2025-09-18 15:14:14 +09:00
onClick={() => addColumn(column.columnName)}
className="flex items-center gap-1"
2025-09-15 11:43:59 +09:00
>
2025-09-18 15:14:14 +09:00
<Plus className="h-3 w-3" />
{column.label || column.columnName}
<Badge variant="secondary" className="text-xs">
{column.dataType}
</Badge>
2025-09-15 11:43:59 +09:00
</Button>
2025-09-18 15:14:14 +09:00
))}
</div>
) : (
<div className="py-4 text-center text-gray-500">
<p> ...</p>
</div>
)}
</CardContent>
</Card>
</>
)}
2025-09-15 11:43:59 +09:00
2025-09-18 15:14:14 +09:00
{screenTableName && (
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-96">
<div className="space-y-3">
{config.columns?.map((column, index) => (
<div key={column.columnName} className="space-y-3 rounded-lg border p-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
checked={column.visible}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { visible: checked as boolean })
2025-09-15 11:43:59 +09:00
}
/>
2025-09-18 15:14:14 +09:00
<span className="font-medium">
{availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName}
</span>
2025-09-15 11:43:59 +09:00
</div>
2025-09-18 15:14:14 +09:00
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "up")}
disabled={index === 0}
2025-09-15 11:43:59 +09:00
>
2025-09-18 15:14:14 +09:00
<ArrowUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "down")}
disabled={index === (config.columns?.length || 0) - 1}
2025-09-15 11:43:59 +09:00
>
2025-09-18 15:14:14 +09:00
<ArrowDown className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => removeColumn(column.columnName)}
className="text-red-500 hover:text-red-600"
>
<Trash2 className="h-3 w-3" />
</Button>
2025-09-15 11:43:59 +09:00
</div>
2025-09-18 15:14:14 +09:00
</div>
2025-09-15 11:43:59 +09:00
2025-09-18 15:14:14 +09:00
{column.visible && (
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={
availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName
2025-09-15 11:43:59 +09:00
}
2025-09-18 15:14:14 +09:00
onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })}
className="h-8"
2025-09-15 11:43:59 +09:00
/>
</div>
2025-09-18 15:14:14 +09:00
{/* 엔티티 타입 컬럼 표시 */}
{column.isEntityJoin && (
<div className="col-span-2">
<div className="flex items-center gap-2 rounded bg-orange-50 p-2">
<Badge variant="outline" className="border-orange-300 text-orange-600">
</Badge>
<span className="text-xs text-orange-600">
"🎯 엔티티 컬럼 표시 설정"
</span>
</div>
</div>
)}
2025-09-18 15:14:14 +09:00
<div className="space-y-1">
<Label className="text-xs"></Label>
<Select
value={column.align}
onValueChange={(value: "left" | "center" | "right") =>
updateColumn(column.columnName, { align: value })
2025-09-15 11:43:59 +09:00
}
2025-09-18 15:14:14 +09:00
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="center"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Select
value={column.format}
onValueChange={(value: "text" | "number" | "date" | "currency" | "boolean") =>
updateColumn(column.columnName, { format: value })
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="date"></SelectItem>
<SelectItem value="currency"></SelectItem>
<SelectItem value="boolean"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs"> (px)</Label>
<Input
type="number"
value={column.width || ""}
onChange={(e) =>
updateColumn(column.columnName, {
width: e.target.value ? parseInt(e.target.value) : undefined,
})
}
placeholder="자동"
className="h-8"
2025-09-15 11:43:59 +09:00
/>
2025-09-18 15:14:14 +09:00
</div>
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Select
value={column.fixed === false ? "none" : column.fixed || "none"}
onValueChange={(value: string) => {
const fixedValue = value === "none" ? false : (value as "left" | "right");
updateColumn(column.columnName, {
fixed: fixedValue,
fixedOrder: fixedValue ? column.fixedOrder || 0 : undefined,
});
}}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
<SelectItem value="left"> </SelectItem>
<SelectItem value="right"> </SelectItem>
</SelectContent>
</Select>
</div>
{(column.fixed === "left" || column.fixed === "right") && (
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
type="number"
value={column.fixedOrder || 0}
onChange={(e) =>
updateColumn(column.columnName, {
fixedOrder: parseInt(e.target.value) || 0,
})
}
placeholder="0"
className="h-8"
min="0"
/>
</div>
)}
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
<Checkbox
checked={column.sortable}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { sortable: checked as boolean })
}
/>
<Label className="text-xs"> </Label>
</div>
<div className="flex items-center space-x-1">
<Checkbox
checked={column.searchable}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { searchable: checked as boolean })
}
/>
<Label className="text-xs"> </Label>
</div>
2025-09-15 11:43:59 +09:00
</div>
</div>
2025-09-18 15:14:14 +09:00
)}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
)}
</ScrollArea>
2025-09-15 11:43:59 +09:00
</TabsContent>
{/* Entity 조인 컬럼 추가 탭 */}
<TabsContent value="join-columns" className="space-y-4">
2025-09-18 15:14:14 +09:00
<ScrollArea className="h-[600px] pr-4">
<Card>
<CardHeader>
<CardTitle className="text-base">Entity </CardTitle>
<CardDescription>Entity .</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<ScrollArea className="max-h-96 pr-4">
{loadingEntityJoins ? (
<div className="text-muted-foreground py-4 text-center"> ...</div>
) : entityJoinColumns.joinTables.length === 0 ? (
<div className="text-muted-foreground py-8 text-center">
<div className="text-sm">Entity .</div>
<div className="mt-1 text-xs">
'entity' .
</div>
</div>
2025-09-18 15:14:14 +09:00
) : (
<div className="space-y-4">
{/* 조인 테이블별 그룹 */}
{entityJoinColumns.joinTables.map((joinTable, tableIndex) => (
<Card key={tableIndex} className="border-l-4 border-l-blue-500">
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-sm">
📊 {joinTable.tableName}
<Badge variant="outline" className="text-xs">
: {joinTable.currentDisplayColumn}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
{joinTable.availableColumns.length === 0 ? (
<div className="text-muted-foreground py-2 text-sm"> .</div>
) : (
<div className="grid gap-2">
{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,
);
return (
<div
key={colIndex}
className="flex items-center justify-between rounded border p-2"
>
<div className="flex-1">
<div className="text-sm font-medium">{column.columnLabel}</div>
<div className="text-muted-foreground text-xs">
{column.columnName} ({column.dataType})
</div>
{column.description && (
<div className="text-muted-foreground mt-1 text-xs">{column.description}</div>
)}
</div>
<div className="flex items-center gap-2">
{isAlreadyAdded ? (
<Badge variant="secondary" className="text-xs">
</Badge>
) : (
matchingJoinColumn && (
<Button
size="sm"
variant="outline"
onClick={() => addEntityColumn(matchingJoinColumn)}
2025-09-18 15:14:14 +09:00
className="text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
)
)}
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
))}
{/* 전체 사용 가능한 컬럼 요약 */}
{entityJoinColumns.availableColumns.length > 0 && (
<Card className="bg-muted/30">
<CardHeader className="pb-3">
<CardTitle className="text-sm">📋 </CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="text-muted-foreground mb-2 text-sm">
{entityJoinColumns.availableColumns.length} .
</div>
<div className="flex flex-wrap gap-1">
{entityJoinColumns.availableColumns.map((column, index) => {
const isAlreadyAdded = config.columns?.some(
2025-09-18 15:14:14 +09:00
(col) => col.columnName === column.joinAlias,
);
return (
2025-09-18 15:14:14 +09:00
<Badge
key={index}
variant={isAlreadyAdded ? "secondary" : "outline"}
className="cursor-pointer text-xs"
onClick={() => !isAlreadyAdded && addEntityColumn(column)}
2025-09-18 15:14:14 +09:00
>
{column.columnLabel}
{!isAlreadyAdded && <Plus className="ml-1 h-2 w-2" />}
</Badge>
);
})}
</div>
2025-09-18 15:14:14 +09:00
</CardContent>
</Card>
)}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
</ScrollArea>
</TabsContent>
2025-09-15 11:43:59 +09:00
{/* 필터 설정 탭 */}
<TabsContent value="filter" className="space-y-4">
2025-09-18 15:14:14 +09:00
<ScrollArea className="h-[600px] pr-4">
2025-09-23 14:26:18 +09:00
{/* 필터 기능 활성화 */}
2025-09-18 15:14:14 +09:00
<Card>
<CardHeader>
2025-09-23 14:26:18 +09:00
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
2025-09-18 15:14:14 +09:00
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="filterEnabled"
2025-09-23 14:26:18 +09:00
checked={config.filter?.enabled || false}
2025-09-18 15:14:14 +09:00
onCheckedChange={(checked) => handleNestedChange("filter", "enabled", checked)}
/>
<Label htmlFor="filterEnabled"> </Label>
</div>
2025-09-23 14:26:18 +09:00
</CardContent>
</Card>
2025-09-15 11:43:59 +09:00
2025-09-23 14:26:18 +09:00
{/* 필터 목록 */}
{config.filter?.enabled && (
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* 필터 추가 버튼 */}
{availableColumns.length > 0 && (
<div className="flex flex-wrap gap-2">
{availableColumns
.filter((col) => !config.filter?.filters?.find((f) => f.columnName === col.columnName))
.map((column) => (
<Button
key={column.columnName}
variant="outline"
size="sm"
onClick={() => addFilter(column.columnName)}
className="flex items-center gap-1"
>
<Plus className="h-3 w-3" />
{column.label || column.columnName}
<Badge variant="secondary" className="text-xs">
{column.dataType}
</Badge>
</Button>
))}
2025-09-15 11:43:59 +09:00
</div>
2025-09-23 14:26:18 +09:00
)}
2025-09-15 11:43:59 +09:00
2025-09-23 14:26:18 +09:00
{/* 설정된 필터 목록 */}
{config.filter?.filters && config.filter.filters.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-medium"> </h4>
{config.filter.filters.map((filter, index) => (
<div key={filter.columnName} className="space-y-3 rounded-lg border p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Badge variant="outline">{filter.widgetType}</Badge>
<span className="font-medium">{filter.label}</span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => removeFilter(index)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
2025-09-18 15:14:14 +09:00
2025-09-23 14:26:18 +09:00
<div className="grid grid-cols-2 gap-3">
<div>
<Label className="text-xs"></Label>
<Input
value={filter.label}
onChange={(e) => updateFilter(index, "label", e.target.value)}
placeholder="필터 라벨"
/>
</div>
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.gridColumns.toString()}
onValueChange={(value) => updateFilter(index, "gridColumns", parseInt(value))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="6">6</SelectItem>
</SelectContent>
</Select>
</div>
{/* 숫자 타입인 경우 검색 모드 선택 */}
{(filter.widgetType === "number" || filter.widgetType === "decimal") && (
<div>
<Label className="text-xs"> </Label>
<Select
value={filter.numberFilterMode || "range"}
onValueChange={(value) => updateFilter(index, "numberFilterMode", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="exact"> </SelectItem>
<SelectItem value="range"> </SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* 코드 타입인 경우 코드 카테고리 */}
{filter.widgetType === "code" && (
<div>
<Label className="text-xs"> </Label>
<Input
value={filter.codeCategory || ""}
onChange={(e) => updateFilter(index, "codeCategory", e.target.value)}
placeholder="코드 카테고리"
/>
</div>
)}
</div>
</div>
))}
2025-09-18 15:14:14 +09:00
</div>
2025-09-23 14:26:18 +09:00
)}
</CardContent>
</Card>
)}
2025-09-18 15:14:14 +09:00
</ScrollArea>
2025-09-15 11:43:59 +09:00
</TabsContent>
{/* 액션 설정 탭 */}
<TabsContent value="actions" className="space-y-4">
2025-09-18 15:14:14 +09:00
<ScrollArea className="h-[600px] pr-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="showActions"
checked={config.actions?.showActions}
onCheckedChange={(checked) => handleNestedChange("actions", "showActions", checked)}
/>
<Label htmlFor="showActions"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="bulkActions"
checked={config.actions?.bulkActions}
onCheckedChange={(checked) => handleNestedChange("actions", "bulkActions", checked)}
/>
<Label htmlFor="bulkActions"> </Label>
</div>
</CardContent>
</Card>
</ScrollArea>
2025-09-15 11:43:59 +09:00
</TabsContent>
{/* 스타일 설정 탭 */}
<TabsContent value="style" className="space-y-4">
2025-09-18 15:14:14 +09:00
<ScrollArea className="h-[600px] pr-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Select
value={config.tableStyle?.theme}
onValueChange={(value: "default" | "striped" | "bordered" | "minimal") =>
handleNestedChange("tableStyle", "theme", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"></SelectItem>
<SelectItem value="striped"></SelectItem>
<SelectItem value="bordered"></SelectItem>
<SelectItem value="minimal"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> </Label>
<Select
value={config.tableStyle?.rowHeight}
onValueChange={(value: "compact" | "normal" | "comfortable") =>
handleNestedChange("tableStyle", "rowHeight", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="compact"></SelectItem>
<SelectItem value="normal"></SelectItem>
<SelectItem value="comfortable"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="alternateRows"
checked={config.tableStyle?.alternateRows}
onCheckedChange={(checked) => handleNestedChange("tableStyle", "alternateRows", checked)}
/>
<Label htmlFor="alternateRows"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="hoverEffect"
checked={config.tableStyle?.hoverEffect}
onCheckedChange={(checked) => handleNestedChange("tableStyle", "hoverEffect", checked)}
/>
<Label htmlFor="hoverEffect"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="stickyHeader"
checked={config.stickyHeader}
onCheckedChange={(checked) => handleChange("stickyHeader", checked)}
/>
<Label htmlFor="stickyHeader"> </Label>
</div>
</CardContent>
</Card>
</ScrollArea>
2025-09-15 11:43:59 +09:00
</TabsContent>
</Tabs>
</div>
);
};