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

1329 lines
53 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 { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { TableListConfig, ColumnConfig } from "./types";
import { entityJoinApi } from "@/lib/api/entityJoin";
import { tableTypeApi } from "@/lib/api/screen";
import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check } from "lucide-react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { cn } from "@/lib/utils";
import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel";
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; input_type?: string }>
>([]);
const [entityJoinColumns, setEntityJoinColumns] = useState<{
availableColumns: Array<{
tableName: string;
columnName: string;
columnLabel: string;
dataType: string;
joinAlias: string;
suggestedLabel: string;
}>;
joinTables: Array<{
tableName: string;
currentDisplayColumn: string;
availableColumns: Array<{
columnName: string;
columnLabel: string;
dataType: string;
description?: string;
}>;
}>;
}>({ availableColumns: [], joinTables: [] });
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
// 🔄 외부에서 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,
input_type: column.input_type || column.inputType, // 🆕 input_type 추가
}));
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,
input_type: col.input_type || col.inputType, // 🆕 input_type 추가
})),
);
}
}
} 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]);
// 🎯 엔티티 컬럼 자동 로드
useEffect(() => {
const entityColumns = config.columns?.filter((col) => col.isEntityJoin && col.entityDisplayConfig);
if (!entityColumns || entityColumns.length === 0) return;
// 각 엔티티 컬럼에 대해 자동으로 loadEntityDisplayConfig 호출
entityColumns.forEach((column) => {
// 이미 로드된 경우 스킵
if (entityDisplayConfigs[column.columnName]) {
return;
}
loadEntityDisplayConfig(column);
});
}, [config.columns]);
const handleChange = (key: keyof TableListConfig, value: any) => {
// 기존 config와 병합하여 전달 (다른 속성 손실 방지)
onChange({ ...config, [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;
// 전체 config와 병합하여 다른 속성 유지
const newConfig = {
...config,
[parentKey]: {
...parentValue,
[childKey]: value,
},
};
// console.log("📤 TableListConfigPanel onChange 호출:", newConfig);
onChange(newConfig);
};
// 컬럼 추가
const addColumn = (columnName: string) => {
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
if (existingColumn) return;
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
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) => {
if (!column.isEntityJoin || !column.entityJoinInfo) {
return;
}
// entityDisplayConfig가 없으면 초기화
if (!column.entityDisplayConfig) {
// sourceTable을 결정: entityJoinInfo -> config.selectedTable -> screenTableName 순서
const initialSourceTable = column.entityJoinInfo?.sourceTable || config.selectedTable || screenTableName;
if (!initialSourceTable) {
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) {
return loadEntityDisplayConfig(updatedColumn);
}
}
return;
}
// sourceTable 결정 우선순위:
// 1. entityDisplayConfig.sourceTable
// 2. entityJoinInfo.sourceTable
// 3. config.selectedTable
// 4. screenTableName
const sourceTable =
column.entityDisplayConfig.sourceTable ||
column.entityJoinInfo?.sourceTable ||
config.selectedTable ||
screenTableName;
let joinTable = column.entityDisplayConfig.joinTable;
// sourceTable이 여전히 비어있으면 에러
if (!sourceTable) {
return;
}
if (!joinTable && sourceTable) {
// joinTable이 없으면 tableTypeApi로 조회해서 설정
try {
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;
// 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);
}
}
} catch (error) {
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
}
}
// sourceTable과 joinTable이 모두 있어야 로드
if (!sourceTable || !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);
}
};
// 🎯 엔티티 표시 구분자 업데이트
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
const configKey = `${columnName}`;
const localConfig = entityDisplayConfigs[configKey];
if (!localConfig) return;
// 로컬 상태 업데이트
setEntityDisplayConfigs((prev) => ({
...prev,
[configKey]: {
...prev[configKey],
separator,
},
}));
// 실제 컬럼 설정도 업데이트
const updatedColumns = config.columns?.map((col) => {
if (col.columnName === columnName && col.entityDisplayConfig) {
return {
...col,
entityDisplayConfig: {
...col.entityDisplayConfig,
separator,
},
};
}
return col;
});
if (updatedColumns) {
handleChange("columns", updatedColumns);
}
};
// 컬럼 순서 변경
const moveColumn = (columnName: string, direction: "up" | "down") => {
const columns = [...(config.columns || [])];
const index = columns.findIndex((col) => col.columnName === columnName);
if (index === -1) return;
const targetIndex = direction === "up" ? index - 1 : index + 1;
if (targetIndex < 0 || targetIndex >= columns.length) return;
[columns[index], columns[targetIndex]] = [columns[targetIndex], columns[index]];
// order 값 재정렬
columns.forEach((col, idx) => {
col.order = idx;
});
handleChange("columns", columns);
};
return (
<div className="space-y-4">
<div className="text-sm font-medium"> </div>
<div className="space-y-6">
{/* 테이블 제목 설정 */}
<div className="space-y-3">
<div>
<h3 className="text-sm font-semibold"> </h3>
</div>
<hr className="border-border" />
<div className="space-y-1">
<Label htmlFor="tableTitle" className="text-xs">
( )
</Label>
<Input
id="tableTitle"
type="text"
value={config.title || ""}
onChange={(e) => handleChange("title", e.target.value)}
placeholder="테이블 제목 입력..."
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
우선순위: 사용자
</p>
</div>
</div>
{/* 체크박스 설정 */}
<div className="space-y-3">
<div>
<h3 className="text-sm font-semibold"> </h3>
</div>
<hr className="border-border" />
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="checkboxEnabled"
checked={config.checkbox?.enabled ?? true}
onCheckedChange={(checked) => handleNestedChange("checkbox", "enabled", checked)}
/>
<Label htmlFor="checkboxEnabled"> </Label>
</div>
{config.checkbox?.enabled && (
<>
<div className="flex items-center space-x-2">
<Checkbox
id="checkboxSelectAll"
checked={config.checkbox?.selectAll ?? true}
onCheckedChange={(checked) => handleNestedChange("checkbox", "selectAll", checked)}
/>
<Label htmlFor="checkboxSelectAll"> </Label>
</div>
<div className="space-y-1">
<Label htmlFor="checkboxPosition" className="text-xs">
</Label>
<select
id="checkboxPosition"
value={config.checkbox?.position || "left"}
onChange={(e) => handleNestedChange("checkbox", "position", e.target.value)}
className="w-full h-8 text-xs border rounded-md px-2"
>
<option value="left"></option>
<option value="right"></option>
</select>
</div>
</>
)}
</div>
</div>
{/* 가로 스크롤 및 컬럼 고정 */}
<div className="space-y-3">
<div>
<h3 className="text-sm font-semibold"> </h3>
</div>
<hr className="border-border" />
<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>
)}
</div>
{/* 컬럼 설정 */}
{/* 🎯 엔티티 컬럼 표시 설정 섹션 */}
{config.columns?.some((col) => col.isEntityJoin) && (
<div className="space-y-3">
{config.columns
?.filter((col) => col.isEntityJoin && col.entityDisplayConfig)
.map((column) => (
<div key={column.columnName} className="space-y-2">
<div className="mb-2">
<span className="truncate text-xs font-medium" style={{ fontSize: "12px" }}>
{column.displayName || column.columnName}
</span>
</div>
{entityDisplayConfigs[column.columnName] ? (
<div className="space-y-2">
{/* 구분자 설정 */}
<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-6 w-full text-xs"
style={{ fontSize: "12px" }}
placeholder=" - "
/>
</div>
{/* 표시 컬럼 선택 (다중 선택) */}
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="h-6 w-full justify-between text-xs"
style={{ fontSize: "12px" }}
>
{entityDisplayConfigs[column.columnName].selectedColumns.length > 0
? `${entityDisplayConfigs[column.columnName].selectedColumns.length}개 선택됨`
: "컬럼 선택"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="컬럼 검색..." className="text-xs" />
<CommandList>
<CommandEmpty className="text-xs"> .</CommandEmpty>
{entityDisplayConfigs[column.columnName].sourceColumns.length > 0 && (
<CommandGroup heading={`기본: ${column.entityDisplayConfig?.sourceTable}`}>
{entityDisplayConfigs[column.columnName].sourceColumns.map((col) => (
<CommandItem
key={`source-${col.columnName}`}
onSelect={() => toggleEntityDisplayColumn(column.columnName, col.columnName)}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
entityDisplayConfigs[column.columnName].selectedColumns.includes(
col.columnName,
)
? "opacity-100"
: "opacity-0",
)}
/>
{col.displayName}
</CommandItem>
))}
</CommandGroup>
)}
{entityDisplayConfigs[column.columnName].joinColumns.length > 0 && (
<CommandGroup heading={`조인: ${column.entityDisplayConfig?.joinTable}`}>
{entityDisplayConfigs[column.columnName].joinColumns.map((col) => (
<CommandItem
key={`join-${col.columnName}`}
onSelect={() => toggleEntityDisplayColumn(column.columnName, col.columnName)}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-4 w-4",
entityDisplayConfigs[column.columnName].selectedColumns.includes(
col.columnName,
)
? "opacity-100"
: "opacity-0",
)}
/>
{col.displayName}
</CommandItem>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
</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 className="py-4 text-center text-xs text-gray-400"> ...</div>
)}
</div>
))}
</div>
)}
{!screenTableName ? (
<div className="space-y-3">
<div className="text-center text-gray-500">
<p> .</p>
<p className="text-sm"> .</p>
</div>
</div>
) : availableColumns.length === 0 ? (
<div className="space-y-3">
<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>
</div>
) : (
<>
<div className="space-y-2">
<div>
<h3 className="text-sm font-semibold"> </h3>
</div>
<hr className="border-border" />
{availableColumns.length > 0 ? (
<div className="space-y-1">
{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="h-6 w-full justify-start px-2 text-xs"
style={{ fontSize: "12px" }}
>
<Plus className="mr-1 h-3 w-3 shrink-0" />
<span className="truncate">{column.label || column.columnName}</span>
</Button>
))}
</div>
) : (
<div className="text-muted-foreground py-2 text-center text-xs"> ...</div>
)}
</div>
{/* Entity 조인 컬럼 추가 */}
{entityJoinColumns.joinTables.length > 0 && (
<div className="space-y-2">
<div>
<h3 className="text-sm font-semibold">Entity </h3>
</div>
<hr className="border-border" />
<div className="space-y-3">
{entityJoinColumns.joinTables.map((joinTable, tableIndex) => (
<div key={tableIndex} className="space-y-1">
<div className="flex items-center gap-2 px-2">
<span className="text-xs font-medium">{joinTable.tableName}</span>
<Badge variant="outline" className="text-[10px]">
{joinTable.currentDisplayColumn}
</Badge>
</div>
{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 matchingJoinColumn && !isAlreadyAdded ? (
<Button
key={colIndex}
variant="outline"
size="sm"
onClick={() => addEntityColumn(matchingJoinColumn)}
className="h-6 w-full justify-start px-2 text-xs"
style={{ fontSize: "12px" }}
>
<Plus className="mr-1 h-3 w-3 shrink-0" />
<span className="truncate">{column.columnLabel}</span>
</Button>
) : null;
})}
</div>
))}
</div>
</div>
)}
</>
)}
{screenTableName && (
<div className="space-y-3">
<div>
<h3 className="text-sm font-semibold"> </h3>
<p className="text-muted-foreground text-xs"> </p>
</div>
<hr className="border-border" />
{/* 간결한 리스트 형식 컬럼 설정 */}
<div className="space-y-1">
{config.columns?.map((column, index) => (
<div
key={column.columnName}
className="hover:bg-muted/30 flex h-6 items-center justify-between rounded border px-2"
>
{/* 컬럼명 */}
<span className="flex-1 truncate text-xs" style={{ fontSize: "12px" }}>
{availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName}
</span>
{/* 필터 체크박스 + 순서 변경 + 삭제 버튼 */}
<div className="flex shrink-0 items-center gap-1">
<Checkbox
checked={config.filter?.filters?.some((f) => f.columnName === column.columnName) || false}
onCheckedChange={(checked) => {
const currentFilters = config.filter?.filters || [];
const columnLabel =
availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName;
if (checked) {
// 필터 추가
handleChange("filter", {
...config.filter,
enabled: true,
filters: [
...currentFilters,
{
columnName: column.columnName,
label: columnLabel,
type: "text",
},
],
});
} else {
// 필터 제거
handleChange("filter", {
...config.filter,
filters: currentFilters.filter((f) => f.columnName !== column.columnName),
});
}
}}
className="h-3 w-3"
/>
</div>
{/* 순서 변경 + 삭제 버튼 */}
<div className="flex shrink-0 items-center gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "up")}
disabled={index === 0}
className="h-6 w-6 p-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}
className="h-6 w-6 p-0"
>
<ArrowDown className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => removeColumn(column.columnName)}
className="h-6 w-6 p-0 text-red-500 hover:text-red-600"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
))}
</div>
</div>
)}
{/* 필터 간격 설정 */}
{config.filter?.enabled && (
<div className="space-y-3">
<div>
<h3 className="text-sm font-semibold"> </h3>
</div>
<hr className="border-border" />
<div className="space-y-1">
<Label htmlFor="filterBottomSpacing" className="text-xs">
(px)
</Label>
<Input
id="filterBottomSpacing"
type="number"
value={config.filter?.bottomSpacing ?? 40}
onChange={(e) => {
const value = Math.max(0, Math.min(200, parseInt(e.target.value) || 40));
handleChange("filter", {
...config.filter,
bottomSpacing: value,
});
}}
min={0}
max={200}
step={10}
placeholder="40"
className="h-8 text-xs"
/>
<p className="text-[10px] text-muted-foreground">
기본값: 40px (0-200px , 10px )
</p>
</div>
</div>
)}
{/* 🆕 데이터 필터링 설정 */}
<div className="space-y-3">
<div>
<h3 className="text-sm font-semibold"> </h3>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
<hr className="border-border" />
<DataFilterConfigPanel
tableName={config.selectedTable || screenTableName}
columns={availableColumns.map((col) => ({
columnName: col.columnName,
columnLabel: col.label || col.columnName,
dataType: col.dataType,
input_type: col.input_type, // 🆕 실제 input_type 전달
} as any))}
config={config.dataFilter}
onConfigChange={(dataFilter) => handleChange("dataFilter", dataFilter)}
/>
</div>
{/* 🆕 연결된 필터 설정 (셀렉트박스 등 다른 컴포넌트 값으로 필터링) */}
<div className="space-y-3">
<div>
<h3 className="text-sm font-semibold"> </h3>
<p className="text-xs text-muted-foreground mt-1">
</p>
</div>
<hr className="border-border" />
{/* 연결된 필터 목록 */}
<div className="space-y-2">
{(config.linkedFilters || []).map((filter, index) => (
<div key={index} className="flex items-center gap-2 rounded border p-2">
<div className="flex-1 space-y-1">
<div className="flex items-center gap-2">
<Input
placeholder="소스 컴포넌트 ID"
value={filter.sourceComponentId || ""}
onChange={(e) => {
const newFilters = [...(config.linkedFilters || [])];
newFilters[index] = { ...filter, sourceComponentId: e.target.value };
handleChange("linkedFilters", newFilters);
}}
className="h-7 text-xs flex-1"
/>
<span className="text-xs text-muted-foreground"></span>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-7 flex-1 justify-between text-xs"
>
{filter.targetColumn || "필터링할 컬럼 선택"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="컬럼 검색..." className="h-8 text-xs" />
<CommandList>
<CommandEmpty className="text-xs py-2"> </CommandEmpty>
<CommandGroup>
{availableColumns.map((col) => (
<CommandItem
key={col.columnName}
value={col.columnName}
onSelect={() => {
const newFilters = [...(config.linkedFilters || [])];
newFilters[index] = { ...filter, targetColumn: col.columnName };
handleChange("linkedFilters", newFilters);
}}
className="text-xs"
>
<Check
className={cn(
"mr-2 h-3 w-3",
filter.targetColumn === col.columnName ? "opacity-100" : "opacity-0"
)}
/>
{col.label || col.columnName}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => {
const newFilters = (config.linkedFilters || []).filter((_, i) => i !== index);
handleChange("linkedFilters", newFilters);
}}
className="h-7 w-7 p-0 text-red-500 hover:text-red-600"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
))}
{/* 연결된 필터 추가 버튼 */}
<Button
variant="outline"
size="sm"
onClick={() => {
const newFilters = [
...(config.linkedFilters || []),
{ sourceComponentId: "", targetColumn: "", operator: "equals" as const, enabled: true }
];
handleChange("linkedFilters", newFilters);
}}
className="h-7 w-full text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
<p className="text-[10px] text-muted-foreground">
: 셀렉트박스(ID: select-basic-123) inbound_type
</p>
</div>
</div>
</div>
</div>
);
};