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

1659 lines
74 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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";
import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react";
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],
});
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);
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
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 !== 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,
})),
);
} catch (error) {
console.error("테이블 목록 가져오기 실패:", error);
} finally {
setLoadingTables(false);
}
};
fetchTables();
}, []);
// 선택된 테이블의 컬럼 목록 설정
useEffect(() => {
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)) {
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([]);
}
}, [config.selectedTable, screenTableName, tableColumns, config.columns]);
// 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]);
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);
};
// 컬럼 제거
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) => {
if (!column.isEntityJoin || !column.entityJoinInfo || !column.entityDisplayConfig) return;
const { sourceTable, joinTable } = column.entityDisplayConfig;
const configKey = `${column.columnName}`;
// 이미 로드된 경우 스킵
if (entityDisplayConfigs[configKey]) return;
// sourceTable과 joinTable이 모두 있어야 로드
if (!sourceTable || !joinTable) {
console.log("⚠️ sourceTable 또는 joinTable이 비어있어서 로드 스킵:", { sourceTable, joinTable });
return;
}
try {
// 기본 테이블과 조인 테이블의 컬럼 정보를 병렬로 로드
const [sourceResult, joinResult] = await Promise.all([
entityJoinApi.getReferenceTableColumns(sourceTable),
entityJoinApi.getReferenceTableColumns(joinTable),
]);
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}`;
const config = entityDisplayConfigs[configKey];
if (!config) return;
const newSelectedColumns = config.selectedColumns.includes(selectedColumn)
? config.selectedColumns.filter((col) => col !== selectedColumn)
: [...config.selectedColumns, selectedColumn];
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
selectedColumns: newSelectedColumns,
},
}));
// 컬럼 설정 업데이트
updateColumn(columnName, {
entityDisplayConfig: {
...config.entityDisplayConfig,
displayColumns: newSelectedColumns,
},
});
};
// 🎯 엔티티 표시 구분자 업데이트
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
const configKey = `${columnName}`;
const config = entityDisplayConfigs[configKey];
if (!config) return;
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
separator,
},
}));
// 컬럼 설정 업데이트
updateColumn(columnName, {
entityDisplayConfig: {
...config.entityDisplayConfig,
separator,
},
});
};
// 컬럼 순서 변경
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 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);
// 컬럼의 데이터 타입과 웹타입에 따라 위젯 타입 결정
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);
};
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">
<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>
<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">
<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>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title"></Label>
<Input
id="title"
value={config.title || ""}
onChange={(e) => handleChange("title", e.target.value)}
placeholder="테이블 제목 (선택사항)"
/>
</div>
</CardContent>
</Card>
<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>
<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>
<Card>
<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>
</div>
{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>
)}
</CardContent>
</Card>
<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>
</div>
<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>
</div>
<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>
<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="horizontalScrollEnabled"
checked={config.horizontalScroll?.enabled}
onCheckedChange={(checked) => handleNestedChange("horizontalScroll", "enabled", checked)}
/>
<Label htmlFor="horizontalScrollEnabled"> </Label>
</div>
{config.horizontalScroll?.enabled && (
<div className="space-y-3">
<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>
<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>
)}
</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>
)}
</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>
)}
{!screenTableName ? (
<Card>
<CardContent className="pt-6">
<div className="text-center text-gray-500">
<p> .</p>
<p className="text-sm"> .</p>
</div>
</CardContent>
</Card>
) : 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>
) : (
<>
<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) => (
<Button
key={column.columnName}
variant="outline"
size="sm"
onClick={() => addColumn(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>
))}
</div>
) : (
<div className="py-4 text-center text-gray-500">
<p> ...</p>
</div>
)}
</CardContent>
</Card>
</>
)}
{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 })
}
/>
<span className="font-medium">
{availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName}
</span>
</div>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "up")}
disabled={index === 0}
>
<ArrowUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "down")}
disabled={index === (config.columns?.length || 0) - 1}
>
<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>
</div>
</div>
{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
}
onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })}
className="h-8"
/>
</div>
{/* 엔티티 타입 컬럼 표시 */}
{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>
)}
<div className="space-y-1">
<Label className="text-xs"></Label>
<Select
value={column.align}
onValueChange={(value: "left" | "center" | "right") =>
updateColumn(column.columnName, { align: value })
}
>
<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"
/>
</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>
</div>
</div>
)}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
)}
</ScrollArea>
</TabsContent>
{/* Entity 조인 컬럼 추가 탭 */}
<TabsContent value="join-columns" className="space-y-4">
<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>
) : (
<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)}
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(
(col) => col.columnName === column.joinAlias,
);
return (
<Badge
key={index}
variant={isAlreadyAdded ? "secondary" : "outline"}
className="cursor-pointer text-xs"
onClick={() => !isAlreadyAdded && addEntityColumn(column)}
>
{column.columnLabel}
{!isAlreadyAdded && <Plus className="ml-1 h-2 w-2" />}
</Badge>
);
})}
</div>
</CardContent>
</Card>
)}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
</ScrollArea>
</TabsContent>
{/* 필터 설정 탭 */}
<TabsContent value="filter" className="space-y-4">
<ScrollArea className="h-[600px] pr-4">
{/* 필터 기능 활성화 */}
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="filterEnabled"
checked={config.filter?.enabled || false}
onCheckedChange={(checked) => handleNestedChange("filter", "enabled", checked)}
/>
<Label htmlFor="filterEnabled"> </Label>
</div>
</CardContent>
</Card>
{/* 필터 목록 */}
{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>
))}
</div>
)}
{/* 설정된 필터 목록 */}
{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>
<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>
))}
</div>
)}
</CardContent>
</Card>
)}
</ScrollArea>
</TabsContent>
{/* 액션 설정 탭 */}
<TabsContent value="actions" className="space-y-4">
<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>
</TabsContent>
{/* 스타일 설정 탭 */}
<TabsContent value="style" className="space-y-4">
<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>
</TabsContent>
</Tabs>
</div>
);
};