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

2145 lines
95 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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
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,
// configType: typeof config,
// configSelectedTable: config?.selectedTable,
// configPagination: config?.pagination,
// paginationEnabled: config?.pagination?.enabled,
// paginationPageSize: config?.pagination?.pageSize,
// configKeys: typeof config === 'object' ? Object.keys(config || {}) : 'not object',
// screenTableName,
// tableColumns: tableColumns?.length,
// tableColumnsSample: tableColumns?.[0],
// });
const [availableTables, setAvailableTables] = useState<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);
// 🔄 외부에서 config가 변경될 때 내부 상태 동기화 (표의 페이지네이션 변경 감지)
useEffect(() => {
// console.log("🔄 TableListConfigPanel - 외부 config 변경 감지:", {
// configPagination: config?.pagination,
// configPageSize: config?.pagination?.pageSize,
// });
// 현재는 별도 내부 상태가 없어서 자동으로 UI가 업데이트됨
// 만약 내부 상태가 있다면 여기서 동기화 처리
}, [config]);
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
const [entityDisplayConfigs, setEntityDisplayConfigs] = useState<
Record<
string,
{
sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
selectedColumns: string[];
separator: string;
}
>
>({});
// 화면 테이블명이 있으면 자동으로 설정 (초기 한 번만)
useEffect(() => {
if (screenTableName && !config.selectedTable) {
// 기존 config의 모든 속성을 유지하면서 selectedTable만 추가/업데이트
const updatedConfig = {
...config,
selectedTable: screenTableName,
// 컬럼이 있으면 유지, 없으면 빈 배열
columns: config.columns || [],
};
onChange(updatedConfig);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenTableName]); // config.selectedTable이 없을 때만 실행되도록 의존성 최소화
// 테이블 목록 가져오기
useEffect(() => {
const fetchTables = async () => {
setLoadingTables(true);
try {
// API 클라이언트를 사용하여 올바른 포트로 호출
const response = await tableTypeApi.getTables();
setAvailableTables(
response.map((table: any) => ({
tableName: table.tableName,
displayName: table.displayName || table.tableName,
})),
);
} catch (error) {
console.error("테이블 목록 가져오기 실패:", error);
} finally {
setLoadingTables(false);
}
};
fetchTables();
}, []);
// 선택된 테이블의 컬럼 목록 설정
useEffect(() => {
console.log(
"🔍 useEffect 실행됨 - config.selectedTable:",
config.selectedTable,
"screenTableName:",
screenTableName,
);
// 컴포넌트에 명시적으로 테이블이 선택되었거나, 화면에 연결된 테이블이 있는 경우 컬럼 목록 표시
const shouldShowColumns = config.selectedTable || screenTableName;
if (!shouldShowColumns) {
console.log("🔧 컬럼 목록 숨김 - 테이블이 선택되지 않음");
setAvailableColumns([]);
return;
}
// tableColumns prop을 우선 사용
if (tableColumns && tableColumns.length > 0) {
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,
}));
setAvailableColumns(mappedColumns);
// selectedTable이 없으면 screenTableName으로 설정
if (!config.selectedTable && screenTableName) {
onChange({
...config,
selectedTable: screenTableName,
columns: config.columns || [],
});
}
} 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]);
// 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) => {
// console.log("🔧 TableListConfigPanel handleNestedChange:", {
// parentKey,
// childKey,
// value,
// parentValue: config[parentKey],
// hasOnChange: !!onChange,
// onChangeType: typeof onChange,
// });
const parentValue = config[parentKey] as any;
const newConfig = {
[parentKey]: {
...parentValue,
[childKey]: value,
},
};
// console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
onChange(newConfig);
};
// 컬럼 추가
const addColumn = (columnName: string) => {
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
if (existingColumn) return;
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
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]) => {
console.log("🔗 조인 컬럼 추가 요청:", {
joinColumn,
joinAlias: joinColumn.joinAlias,
columnLabel: joinColumn.columnLabel,
tableName: joinColumn.tableName,
columnName: joinColumn.columnName,
});
const existingColumn = config.columns?.find((col) => col.columnName === joinColumn.joinAlias);
if (existingColumn) {
console.warn("⚠️ 이미 존재하는 컬럼:", joinColumn.joinAlias);
return;
}
// 🎯 joinTables에서 sourceColumn 찾기
const joinTableInfo = entityJoinColumns.joinTables?.find((jt: any) => jt.tableName === joinColumn.tableName);
const sourceColumn = joinTableInfo?.joinConfig?.sourceColumn || "";
console.log("🔍 조인 정보 추출:", {
tableName: joinColumn.tableName,
foundJoinTable: !!joinTableInfo,
sourceColumn,
joinConfig: joinTableInfo?.joinConfig,
});
// 조인 탭에서 추가하는 컬럼들은 일반 컬럼으로 처리 (isEntityJoin: false)
const newColumn: ColumnConfig = {
columnName: joinColumn.joinAlias,
displayName: joinColumn.columnLabel,
visible: true,
sortable: true,
searchable: true,
align: "left",
format: "text",
order: config.columns?.length || 0,
isEntityJoin: false, // 조인 탭에서 추가하는 컬럼은 엔티티 타입이 아님
// 🎯 추가 조인 정보 저장
additionalJoinInfo: {
sourceTable: config.selectedTable || screenTableName || "", // 기준 테이블 (예: user_info)
sourceColumn: sourceColumn, // 기준 컬럼 (예: dept_code) - joinTables에서 추출
referenceTable: joinColumn.tableName, // 참조 테이블 (예: dept_info)
joinAlias: joinColumn.joinAlias, // 조인 별칭 (예: dept_code_company_name)
},
};
handleChange("columns", [...(config.columns || []), newColumn]);
console.log("✅ 조인 컬럼 추가 완료:", {
columnName: newColumn.columnName,
displayName: newColumn.displayName,
totalColumns: (config.columns?.length || 0) + 1,
});
};
// 컬럼 제거
const removeColumn = (columnName: string) => {
const updatedColumns = config.columns?.filter((col) => col.columnName !== columnName) || [];
handleChange("columns", updatedColumns);
};
// 컬럼 업데이트
const updateColumn = (columnName: string, updates: Partial<ColumnConfig>) => {
const updatedColumns =
config.columns?.map((col) => (col.columnName === columnName ? { ...col, ...updates } : col)) || [];
handleChange("columns", updatedColumns);
};
// 🎯 기존 컬럼들을 체크하여 엔티티 타입인 경우 isEntityJoin 플래그 설정
// useRef로 이전 컬럼 개수를 추적하여 새 컬럼 추가 시에만 실행
const prevColumnsLengthRef = React.useRef<number>(0);
useEffect(() => {
const currentLength = config.columns?.length || 0;
const prevLength = prevColumnsLengthRef.current;
console.log("🔍 엔티티 컬럼 감지 useEffect 실행:", {
hasColumns: !!config.columns,
columnsCount: currentLength,
prevColumnsCount: prevLength,
hasTableColumns: !!tableColumns,
tableColumnsCount: tableColumns?.length || 0,
selectedTable: config.selectedTable,
});
if (!config.columns || !tableColumns || config.columns.length === 0) {
console.log("⚠️ 컬럼 또는 테이블 컬럼 정보가 없어서 엔티티 감지 스킵");
prevColumnsLengthRef.current = currentLength;
return;
}
// 컬럼 개수가 변경되지 않았고, 이미 체크한 적이 있으면 스킵
if (currentLength === prevLength && prevLength > 0) {
console.log(" 컬럼 개수 변경 없음, 엔티티 감지 스킵");
return;
}
const updatedColumns = config.columns.map((column) => {
// 이미 isEntityJoin이 설정된 경우 스킵
if (column.isEntityJoin) {
console.log("✅ 이미 엔티티 플래그 설정됨:", column.columnName);
return column;
}
// 테이블 컬럼 정보에서 해당 컬럼 찾기
const tableColumn = tableColumns.find((tc) => tc.columnName === column.columnName);
console.log("🔍 컬럼 검색:", {
columnName: column.columnName,
found: !!tableColumn,
inputType: tableColumn?.input_type,
webType: tableColumn?.web_type,
});
// 엔티티 타입인 경우 isEntityJoin 플래그 설정 (input_type 또는 web_type 확인)
if (tableColumn && (tableColumn.input_type === "entity" || tableColumn.web_type === "entity")) {
console.log("🎯 엔티티 컬럼 감지 및 플래그 설정:", {
columnName: column.columnName,
referenceTable: tableColumn.reference_table,
referenceTableAlt: tableColumn.referenceTable,
allTableColumnKeys: Object.keys(tableColumn),
});
return {
...column,
isEntityJoin: true,
entityJoinInfo: {
sourceTable: config.selectedTable || screenTableName || "",
sourceColumn: column.columnName,
joinAlias: column.columnName,
},
entityDisplayConfig: {
displayColumns: [], // 빈 배열로 초기화
separator: " - ",
sourceTable: config.selectedTable || screenTableName || "",
joinTable: tableColumn.reference_table || tableColumn.referenceTable || "",
},
};
}
return column;
});
// 변경사항이 있는 경우에만 업데이트
const hasChanges = updatedColumns.some((col, index) => col.isEntityJoin !== config.columns![index].isEntityJoin);
if (hasChanges) {
console.log("🎯 엔티티 컬럼 플래그 업데이트:", updatedColumns);
handleChange("columns", updatedColumns);
} else {
console.log(" 엔티티 컬럼 변경사항 없음");
}
// 현재 컬럼 개수를 저장
prevColumnsLengthRef.current = currentLength;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config.columns?.length, tableColumns, config.selectedTable]); // 컬럼 개수 변경 시에만 실행
// 🎯 엔티티 컬럼의 표시 컬럼 정보 로드
const loadEntityDisplayConfig = async (column: ColumnConfig) => {
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);
// sourceTable을 결정: entityJoinInfo -> config.selectedTable -> screenTableName 순서
const initialSourceTable = column.entityJoinInfo?.sourceTable || config.selectedTable || screenTableName;
if (!initialSourceTable) {
console.warn("⚠️ sourceTable을 결정할 수 없어서 초기화 실패:", column.columnName);
return;
}
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === column.columnName) {
return {
...col,
entityDisplayConfig: {
displayColumns: [],
separator: " - ",
sourceTable: initialSourceTable,
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 결정 우선순위:
// 1. entityDisplayConfig.sourceTable
// 2. entityJoinInfo.sourceTable
// 3. config.selectedTable
// 4. screenTableName
let sourceTable =
column.entityDisplayConfig.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
let joinTable = column.entityDisplayConfig.joinTable;
// sourceTable이 여전히 비어있으면 에러
if (!sourceTable) {
console.error("❌ sourceTable이 비어있어서 처리 불가:", {
columnName: column.columnName,
entityDisplayConfig: column.entityDisplayConfig,
entityJoinInfo: column.entityJoinInfo,
configSelectedTable: config.selectedTable,
screenTableName,
});
return;
}
console.log("✅ sourceTable 결정됨:", sourceTable);
if (!joinTable && sourceTable) {
// 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);
}
} else {
console.warn("⚠️ tableTypeApi에서 조인 테이블 정보를 찾지 못함:", column.columnName);
}
} catch (error) {
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
console.log("❌ 조회 실패 상세:", { sourceTable, columnName: column.columnName });
}
} else if (!joinTable) {
console.warn("⚠️ sourceTable이 없어서 joinTable 조회 불가:", column.columnName);
}
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
const configKey = `${column.columnName}`;
// 이미 로드된 경우 스킵
if (entityDisplayConfigs[configKey]) return;
// joinTable이 비어있으면 tableTypeApi로 컬럼 정보를 다시 가져와서 referenceTable 정보를 찾기
let actualJoinTable = joinTable;
if (!actualJoinTable && sourceTable) {
try {
console.log("🔍 tableTypeApi로 컬럼 정보 다시 조회:", {
tableName: sourceTable,
columnName: column.columnName,
});
const columnList = await tableTypeApi.getColumns(sourceTable);
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
console.log("🔍 컬럼 정보 조회 결과:", {
columnInfo: columnInfo,
referenceTable: columnInfo?.reference_table || columnInfo?.referenceTable,
referenceColumn: columnInfo?.reference_column || columnInfo?.referenceColumn,
});
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
actualJoinTable = columnInfo.reference_table || columnInfo.referenceTable;
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", actualJoinTable);
// entityDisplayConfig 업데이트
const updatedConfig = {
...column.entityDisplayConfig,
joinTable: actualJoinTable,
};
// 컬럼 설정 업데이트
const updatedColumns = config.columns?.map((col) =>
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
);
if (updatedColumns) {
handleChange("columns", updatedColumns);
}
} else {
console.log("⚠️ tableTypeApi에서도 referenceTable을 찾을 수 없음:", {
columnName: column.columnName,
columnInfo: columnInfo,
});
}
} catch (error) {
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}`;
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn)
? localConfig.selectedColumns.filter((col) => col !== selectedColumn)
: [...localConfig.selectedColumns, selectedColumn];
// 로컬 상태 업데이트
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
selectedColumns: newSelectedColumns,
},
}));
// 실제 컬럼 설정도 업데이트
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === columnName && col.entityDisplayConfig) {
return {
...col,
entityDisplayConfig: {
...col.entityDisplayConfig,
displayColumns: newSelectedColumns,
},
};
}
return col;
});
if (updatedColumns) {
handleChange("columns", updatedColumns);
console.log("🎯 엔티티 표시 컬럼 설정 업데이트:", {
columnName,
selectedColumns: newSelectedColumns,
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
});
}
};
// 🎯 엔티티 표시 구분자 업데이트
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
const configKey = `${columnName}`;
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
// 로컬 상태 업데이트
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
separator,
},
}));
// 실제 컬럼 설정도 업데이트
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === columnName && col.entityDisplayConfig) {
return {
...col,
entityDisplayConfig: {
...col.entityDisplayConfig,
separator,
},
};
}
return col;
});
if (updatedColumns) {
handleChange("columns", updatedColumns);
console.log("🎯 엔티티 표시 구분자 설정 업데이트:", {
columnName,
separator,
updatedColumn: updatedColumns.find((col) => col.columnName === columnName),
});
}
};
// 컬럼 순서 변경
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-3">
<Label> </Label>
<RadioGroup
value={config.displayMode || "table"}
onValueChange={(value: "table" | "card") => handleChange("displayMode", value)}
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="table" id="table-mode" />
<Label htmlFor="table-mode" className="cursor-pointer">
()
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="card" id="card-mode" />
<Label htmlFor="card-mode" className="cursor-pointer">
</Label>
</div>
</RadioGroup>
</div>
{/* 카드 모드 설정 */}
{config.displayMode === "card" && (
<div className="space-y-4 border-t pt-4">
<div className="space-y-3">
<Label className="text-sm font-medium"> </Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cards-per-row"> </Label>
<Select
value={config.cardConfig?.cardsPerRow?.toString() || "3"}
onValueChange={(value) => handleNestedChange("cardConfig", "cardsPerRow", parseInt(value))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1</SelectItem>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="5">5</SelectItem>
<SelectItem value="6">6</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="card-spacing"> (px)</Label>
<Input
id="card-spacing"
type="number"
value={config.cardConfig?.cardSpacing || 16}
onChange={(e) => handleNestedChange("cardConfig", "cardSpacing", parseInt(e.target.value))}
min="0"
max="50"
/>
</div>
</div>
<div className="space-y-3">
<Label className="text-sm font-medium"> </Label>
<div className="grid grid-cols-1 gap-3">
<div className="space-y-2">
<Label htmlFor="id-column">ID ( )</Label>
<Select
value={config.cardConfig?.idColumn || ""}
onValueChange={(value) => handleNestedChange("cardConfig", "idColumn", value)}
>
<SelectTrigger>
<SelectValue placeholder="ID 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.label || column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="title-column"> ( )</Label>
<Select
value={config.cardConfig?.titleColumn || ""}
onValueChange={(value) => handleNestedChange("cardConfig", "titleColumn", value)}
>
<SelectTrigger>
<SelectValue placeholder="제목 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.label || column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="subtitle-column"> ( )</Label>
<Select
value={config.cardConfig?.subtitleColumn || ""}
onValueChange={(value) => handleNestedChange("cardConfig", "subtitleColumn", value)}
>
<SelectTrigger>
<SelectValue placeholder="서브 제목 컬럼 선택 (선택사항)" />
</SelectTrigger>
<SelectContent>
<SelectItem value=""> </SelectItem>
{availableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.label || column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="description-column"> </Label>
<Select
value={config.cardConfig?.descriptionColumn || ""}
onValueChange={(value) => handleNestedChange("cardConfig", "descriptionColumn", value)}
>
<SelectTrigger>
<SelectValue placeholder="설명 컬럼 선택 (선택사항)" />
</SelectTrigger>
<SelectContent>
<SelectItem value=""> </SelectItem>
{availableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.label || column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="show-card-actions"
checked={config.cardConfig?.showActions !== false}
onCheckedChange={(checked) =>
handleNestedChange("cardConfig", "showActions", checked as boolean)
}
/>
<Label htmlFor="show-card-actions" className="cursor-pointer">
</Label>
</div>
</div>
</div>
)}
</CardContent>
</Card>
<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
key={`pageSize-${config.pagination?.pageSize}`}
value={config.pagination?.pageSize?.toString() || "20"}
onValueChange={(value) => {
// console.log("🎯 상세설정에서 페이지 크기 변경:", {
// from: config.pagination?.pageSize,
// to: parseInt(value),
// currentConfigPageSize: config.pagination?.pageSize
// });
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={() => {
// sourceTable 정보가 있는지 확인
const hasSourceTable =
column.entityDisplayConfig?.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
if (!hasSourceTable) {
console.error("❌ sourceTable 정보를 찾을 수 없어서 컬럼 로드 불가:", {
columnName: column.columnName,
entityDisplayConfig: column.entityDisplayConfig,
entityJoinInfo: column.entityJoinInfo,
configSelectedTable: config.selectedTable,
screenTableName,
});
alert("컬럼 정보를 로드할 수 없습니다. 테이블 정보가 없습니다.");
return;
}
loadEntityDisplayConfig(column);
}}
disabled={
!column.entityDisplayConfig?.sourceTable &&
!column.entityJoinInfo?.sourceTable &&
!config.selectedTable &&
!screenTableName
}
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>
);
};