1696 lines
72 KiB
TypeScript
1696 lines
72 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { TableListConfig, ColumnConfig } from "./types";
|
||
import { entityJoinApi } from "@/lib/api/entityJoin";
|
||
import { tableTypeApi } from "@/lib/api/screen";
|
||
import { tableManagementApi } from "@/lib/api/tableManagement";
|
||
import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check, Lock, Unlock } 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);
|
||
|
||
// 🆕 제외 필터용 참조 테이블 컬럼 목록
|
||
const [referenceTableColumns, setReferenceTableColumns] = useState<
|
||
Array<{ columnName: string; dataType: string; label?: string }>
|
||
>([]);
|
||
const [loadingReferenceColumns, setLoadingReferenceColumns] = useState(false);
|
||
|
||
// 🔄 외부에서 config가 변경될 때 내부 상태 동기화 (표의 페이지네이션 변경 감지)
|
||
useEffect(() => {
|
||
// console.log("🔄 TableListConfigPanel - 외부 config 변경 감지:", {
|
||
// configPagination: config?.pagination,
|
||
// configPageSize: config?.pagination?.pageSize,
|
||
// });
|
||
// 현재는 별도 내부 상태가 없어서 자동으로 UI가 업데이트됨
|
||
// 만약 내부 상태가 있다면 여기서 동기화 처리
|
||
}, [config]);
|
||
|
||
// 🎯 엔티티 컬럼 표시 설정을 위한 상태
|
||
const [entityDisplayConfigs, setEntityDisplayConfigs] = useState<
|
||
Record<
|
||
string,
|
||
{
|
||
sourceColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
|
||
joinColumns: Array<{ columnName: string; displayName: string; dataType: string }>;
|
||
selectedColumns: string[];
|
||
separator: string;
|
||
}
|
||
>
|
||
>({});
|
||
|
||
// 화면 테이블명이 있으면 자동으로 설정 (초기 한 번만)
|
||
useEffect(() => {
|
||
if (screenTableName && !config.selectedTable) {
|
||
// 기존 config의 모든 속성을 유지하면서 selectedTable만 추가/업데이트
|
||
const updatedConfig = {
|
||
...config,
|
||
selectedTable: screenTableName,
|
||
// 컬럼이 있으면 유지, 없으면 빈 배열
|
||
columns: config.columns || [],
|
||
};
|
||
onChange(updatedConfig);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [screenTableName]); // config.selectedTable이 없을 때만 실행되도록 의존성 최소화
|
||
|
||
// 테이블 목록 가져오기
|
||
useEffect(() => {
|
||
const fetchTables = async () => {
|
||
setLoadingTables(true);
|
||
try {
|
||
// API 클라이언트를 사용하여 올바른 포트로 호출
|
||
const response = await tableTypeApi.getTables();
|
||
setAvailableTables(
|
||
response.map((table: any) => ({
|
||
tableName: table.tableName,
|
||
displayName: table.displayName || table.tableName,
|
||
})),
|
||
);
|
||
} catch (error) {
|
||
console.error("테이블 목록 가져오기 실패:", error);
|
||
} finally {
|
||
setLoadingTables(false);
|
||
}
|
||
};
|
||
|
||
fetchTables();
|
||
}, []);
|
||
|
||
// 선택된 테이블의 컬럼 목록 설정
|
||
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 fetchReferenceColumns = async () => {
|
||
const refTable = config.excludeFilter?.referenceTable;
|
||
if (!refTable) {
|
||
setReferenceTableColumns([]);
|
||
return;
|
||
}
|
||
|
||
setLoadingReferenceColumns(true);
|
||
try {
|
||
console.log("🔗 참조 테이블 컬럼 정보 가져오기:", refTable);
|
||
const result = await tableManagementApi.getColumnList(refTable);
|
||
if (result.success && result.data) {
|
||
// result.data는 { columns: [], total, page, size, totalPages } 형태
|
||
const columns = result.data.columns || [];
|
||
setReferenceTableColumns(
|
||
columns.map((col: any) => ({
|
||
columnName: col.columnName || col.column_name,
|
||
dataType: col.dataType || col.data_type || "text",
|
||
label: col.displayName || col.columnLabel || col.column_label || col.columnName || col.column_name,
|
||
}))
|
||
);
|
||
console.log("✅ 참조 테이블 컬럼 로드 완료:", columns.length, "개");
|
||
}
|
||
} catch (error) {
|
||
console.error("❌ 참조 테이블 컬럼 조회 오류:", error);
|
||
setReferenceTableColumns([]);
|
||
} finally {
|
||
setLoadingReferenceColumns(false);
|
||
}
|
||
};
|
||
|
||
fetchReferenceColumns();
|
||
}, [config.excludeFilter?.referenceTable]);
|
||
|
||
// 🎯 엔티티 컬럼 자동 로드
|
||
useEffect(() => {
|
||
const entityColumns = config.columns?.filter((col) => col.isEntityJoin && col.entityDisplayConfig);
|
||
|
||
if (!entityColumns || entityColumns.length === 0) return;
|
||
|
||
// 각 엔티티 컬럼에 대해 자동으로 loadEntityDisplayConfig 호출
|
||
entityColumns.forEach((column) => {
|
||
// 이미 로드된 경우 스킵
|
||
if (entityDisplayConfigs[column.columnName]) {
|
||
return;
|
||
}
|
||
|
||
loadEntityDisplayConfig(column);
|
||
});
|
||
}, [config.columns]);
|
||
|
||
const handleChange = (key: keyof TableListConfig, value: any) => {
|
||
// 기존 config와 병합하여 전달 (다른 속성 손실 방지)
|
||
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) => {
|
||
const configKey = `${column.columnName}`;
|
||
|
||
// 이미 로드된 경우 스킵
|
||
if (entityDisplayConfigs[configKey]) return;
|
||
|
||
if (!column.isEntityJoin) {
|
||
// 엔티티 컬럼이 아니면 빈 상태로 설정하여 로딩 상태 해제
|
||
setEntityDisplayConfigs((prev) => ({
|
||
...prev,
|
||
[configKey]: {
|
||
sourceColumns: [],
|
||
joinColumns: [],
|
||
selectedColumns: [],
|
||
separator: " - ",
|
||
},
|
||
}));
|
||
return;
|
||
}
|
||
|
||
// sourceTable 결정 우선순위:
|
||
// 1. entityDisplayConfig.sourceTable
|
||
// 2. entityJoinInfo.sourceTable
|
||
// 3. config.selectedTable
|
||
// 4. screenTableName
|
||
const sourceTable =
|
||
column.entityDisplayConfig?.sourceTable ||
|
||
column.entityJoinInfo?.sourceTable ||
|
||
config.selectedTable ||
|
||
screenTableName;
|
||
|
||
// sourceTable이 비어있으면 빈 상태로 설정
|
||
if (!sourceTable) {
|
||
console.warn("⚠️ sourceTable을 찾을 수 없음:", column.columnName);
|
||
setEntityDisplayConfigs((prev) => ({
|
||
...prev,
|
||
[configKey]: {
|
||
sourceColumns: [],
|
||
joinColumns: [],
|
||
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
|
||
separator: column.entityDisplayConfig?.separator || " - ",
|
||
},
|
||
}));
|
||
return;
|
||
}
|
||
|
||
let joinTable = column.entityDisplayConfig?.joinTable;
|
||
|
||
// joinTable이 없으면 tableTypeApi로 조회해서 설정
|
||
if (!joinTable) {
|
||
try {
|
||
console.log("🔍 tableTypeApi로 컬럼 정보 조회:", {
|
||
tableName: sourceTable,
|
||
columnName: column.columnName,
|
||
});
|
||
|
||
const columnList = await tableTypeApi.getColumns(sourceTable);
|
||
const columnInfo = columnList.find((col: any) => (col.column_name || col.columnName) === column.columnName);
|
||
|
||
console.log("🔍 컬럼 정보 조회 결과:", {
|
||
columnInfo: columnInfo,
|
||
referenceTable: columnInfo?.reference_table || columnInfo?.referenceTable,
|
||
referenceColumn: columnInfo?.reference_column || columnInfo?.referenceColumn,
|
||
});
|
||
|
||
if (columnInfo?.reference_table || columnInfo?.referenceTable) {
|
||
joinTable = columnInfo.reference_table || columnInfo.referenceTable;
|
||
console.log("✅ tableTypeApi에서 조인 테이블 정보 찾음:", joinTable);
|
||
|
||
// entityDisplayConfig 업데이트
|
||
const updatedConfig = {
|
||
...column.entityDisplayConfig,
|
||
sourceTable: sourceTable,
|
||
joinTable: joinTable,
|
||
displayColumns: column.entityDisplayConfig?.displayColumns || [],
|
||
separator: column.entityDisplayConfig?.separator || " - ",
|
||
};
|
||
|
||
// 컬럼 설정 업데이트
|
||
const updatedColumns = config.columns?.map((col) =>
|
||
col.columnName === column.columnName ? { ...col, entityDisplayConfig: updatedConfig } : col,
|
||
);
|
||
|
||
if (updatedColumns) {
|
||
handleChange("columns", updatedColumns);
|
||
}
|
||
} else {
|
||
console.warn("⚠️ tableTypeApi에서 조인 테이블 정보를 찾지 못함:", column.columnName);
|
||
}
|
||
} catch (error) {
|
||
console.error("tableTypeApi 컬럼 정보 조회 실패:", error);
|
||
}
|
||
}
|
||
|
||
console.log("🔍 최종 추출한 값:", { sourceTable, joinTable });
|
||
|
||
try {
|
||
// 기본 테이블 컬럼 정보는 항상 로드
|
||
const sourceResult = await entityJoinApi.getReferenceTableColumns(sourceTable);
|
||
const sourceColumns = sourceResult.columns || [];
|
||
|
||
// joinTable이 있으면 조인 테이블 컬럼도 로드
|
||
let joinColumns: Array<{ columnName: string; displayName: string; dataType: string }> = [];
|
||
if (joinTable) {
|
||
try {
|
||
const joinResult = await entityJoinApi.getReferenceTableColumns(joinTable);
|
||
joinColumns = joinResult.columns || [];
|
||
} catch (joinError) {
|
||
console.warn("⚠️ 조인 테이블 컬럼 로드 실패:", joinTable, joinError);
|
||
// 조인 테이블 로드 실패해도 소스 테이블 컬럼은 표시
|
||
}
|
||
}
|
||
|
||
setEntityDisplayConfigs((prev) => ({
|
||
...prev,
|
||
[configKey]: {
|
||
sourceColumns,
|
||
joinColumns,
|
||
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
|
||
separator: column.entityDisplayConfig?.separator || " - ",
|
||
},
|
||
}));
|
||
} catch (error) {
|
||
console.error("엔티티 표시 컬럼 정보 로드 실패:", error);
|
||
// 에러 발생 시에도 빈 상태로 설정하여 로딩 상태 해제
|
||
setEntityDisplayConfigs((prev) => ({
|
||
...prev,
|
||
[configKey]: {
|
||
sourceColumns: [],
|
||
joinColumns: [],
|
||
selectedColumns: column.entityDisplayConfig?.displayColumns || [],
|
||
separator: column.entityDisplayConfig?.separator || " - ",
|
||
},
|
||
}));
|
||
}
|
||
};
|
||
|
||
// 🎯 엔티티 표시 컬럼 선택 토글
|
||
const toggleEntityDisplayColumn = (columnName: string, selectedColumn: string) => {
|
||
const configKey = `${columnName}`;
|
||
const localConfig = entityDisplayConfigs[configKey];
|
||
if (!localConfig) return;
|
||
|
||
const newSelectedColumns = localConfig.selectedColumns.includes(selectedColumn)
|
||
? localConfig.selectedColumns.filter((col) => col !== selectedColumn)
|
||
: [...localConfig.selectedColumns, selectedColumn];
|
||
|
||
// 로컬 상태 업데이트
|
||
setEntityDisplayConfigs((prev) => ({
|
||
...prev,
|
||
[configKey]: {
|
||
...prev[configKey],
|
||
selectedColumns: newSelectedColumns,
|
||
},
|
||
}));
|
||
|
||
// 실제 컬럼 설정도 업데이트
|
||
const updatedColumns = config.columns?.map((col) => {
|
||
if (col.columnName === columnName && col.entityDisplayConfig) {
|
||
return {
|
||
...col,
|
||
entityDisplayConfig: {
|
||
...col.entityDisplayConfig,
|
||
displayColumns: newSelectedColumns,
|
||
},
|
||
};
|
||
}
|
||
return col;
|
||
});
|
||
|
||
if (updatedColumns) {
|
||
handleChange("columns", updatedColumns);
|
||
}
|
||
};
|
||
|
||
// 🎯 엔티티 표시 구분자 업데이트
|
||
const updateEntityDisplaySeparator = (columnName: string, separator: string) => {
|
||
const configKey = `${columnName}`;
|
||
const localConfig = entityDisplayConfigs[configKey];
|
||
if (!localConfig) return;
|
||
|
||
// 로컬 상태 업데이트
|
||
setEntityDisplayConfigs((prev) => ({
|
||
...prev,
|
||
[configKey]: {
|
||
...prev[configKey],
|
||
separator,
|
||
},
|
||
}));
|
||
|
||
// 실제 컬럼 설정도 업데이트
|
||
const updatedColumns = config.columns?.map((col) => {
|
||
if (col.columnName === columnName && col.entityDisplayConfig) {
|
||
return {
|
||
...col,
|
||
entityDisplayConfig: {
|
||
...col.entityDisplayConfig,
|
||
separator,
|
||
},
|
||
};
|
||
}
|
||
return col;
|
||
});
|
||
|
||
if (updatedColumns) {
|
||
handleChange("columns", updatedColumns);
|
||
}
|
||
};
|
||
|
||
// 컬럼 순서 변경
|
||
const moveColumn = (columnName: string, direction: "up" | "down") => {
|
||
const columns = [...(config.columns || [])];
|
||
const index = columns.findIndex((col) => col.columnName === columnName);
|
||
|
||
if (index === -1) return;
|
||
|
||
const targetIndex = direction === "up" ? index - 1 : index + 1;
|
||
if (targetIndex < 0 || targetIndex >= columns.length) return;
|
||
|
||
[columns[index], columns[targetIndex]] = [columns[targetIndex], columns[index]];
|
||
|
||
// order 값 재정렬
|
||
columns.forEach((col, idx) => {
|
||
col.order = idx;
|
||
});
|
||
|
||
handleChange("columns", columns);
|
||
};
|
||
|
||
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>
|
||
{entityDisplayConfigs[column.columnName].sourceColumns.length === 0 &&
|
||
entityDisplayConfigs[column.columnName].joinColumns.length === 0 ? (
|
||
<div className="py-2 text-center text-xs text-gray-400">
|
||
표시 가능한 컬럼이 없습니다.
|
||
{!column.entityDisplayConfig?.joinTable && (
|
||
<p className="mt-1 text-[10px]">
|
||
테이블 타입 관리에서 참조 테이블을 설정하면 더 많은 컬럼을 선택할 수 있습니다.
|
||
</p>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<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 || config.selectedTable || screenTableName}`}>
|
||
{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>
|
||
|
||
{/* 참조 테이블 미설정 안내 */}
|
||
{!column.entityDisplayConfig?.joinTable && entityDisplayConfigs[column.columnName].sourceColumns.length > 0 && (
|
||
<div className="rounded bg-blue-50 p-2 text-[10px] text-blue-600">
|
||
현재 기본 테이블 컬럼만 표시됩니다. 테이블 타입 관리에서 참조 테이블을 설정하면 조인된 테이블의 컬럼도 선택할 수 있습니다.
|
||
</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) => {
|
||
// 해당 컬럼의 input_type 확인
|
||
const columnInfo = availableColumns.find((col) => col.columnName === column.columnName);
|
||
const isNumberType = columnInfo?.input_type === "number" || columnInfo?.input_type === "decimal";
|
||
|
||
return (
|
||
<div
|
||
key={column.columnName}
|
||
className="hover:bg-muted/30 flex items-center justify-between rounded border px-2 py-1"
|
||
style={{ minHeight: "28px" }}
|
||
>
|
||
<div className="flex flex-1 flex-col gap-0.5 overflow-hidden">
|
||
{/* 컬럼명 */}
|
||
<span className="truncate text-xs" style={{ fontSize: "12px" }}>
|
||
{columnInfo?.label || column.displayName || column.columnName}
|
||
</span>
|
||
|
||
{/* 숫자 타입인 경우 천단위 구분자 설정 */}
|
||
{isNumberType && (
|
||
<div className="flex items-center gap-1">
|
||
<Checkbox
|
||
id={`thousand-sep-${column.columnName}`}
|
||
checked={column.thousandSeparator !== false}
|
||
onCheckedChange={(checked) => {
|
||
updateColumn(column.columnName, { thousandSeparator: checked as boolean });
|
||
}}
|
||
className="h-3 w-3"
|
||
/>
|
||
<Label
|
||
htmlFor={`thousand-sep-${column.columnName}`}
|
||
className="text-[10px] text-muted-foreground cursor-pointer"
|
||
>
|
||
천단위 구분자
|
||
</Label>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 편집 가능 여부 + 필터 체크박스 */}
|
||
<div className="flex shrink-0 items-center gap-1">
|
||
{/* 🆕 편집 가능 여부 토글 */}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => {
|
||
updateColumn(column.columnName, {
|
||
editable: column.editable === false ? true : false
|
||
});
|
||
}}
|
||
className="h-6 w-6 p-0"
|
||
title={column.editable === false ? "편집 불가 (클릭하여 편집 허용)" : "편집 가능 (클릭하여 편집 잠금)"}
|
||
>
|
||
{column.editable === false ? (
|
||
<Lock className="h-3 w-3 text-destructive" />
|
||
) : (
|
||
<Unlock className="h-3 w-3 text-muted-foreground" />
|
||
)}
|
||
</Button>
|
||
|
||
{/* 필터 체크박스 */}
|
||
<Checkbox
|
||
checked={config.filter?.filters?.some((f) => f.columnName === column.columnName) || false}
|
||
onCheckedChange={(checked) => {
|
||
const currentFilters = config.filter?.filters || [];
|
||
const columnLabel =
|
||
columnInfo?.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"
|
||
title="필터에 추가"
|
||
/>
|
||
</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 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="flex items-center gap-2">
|
||
<Checkbox
|
||
id="excludeFilter-enabled"
|
||
checked={config.excludeFilter?.enabled || false}
|
||
onCheckedChange={(checked) => {
|
||
handleChange("excludeFilter", {
|
||
...config.excludeFilter,
|
||
enabled: checked as boolean,
|
||
});
|
||
}}
|
||
/>
|
||
<Label htmlFor="excludeFilter-enabled" className="text-xs">
|
||
제외 필터 활성화
|
||
</Label>
|
||
</div>
|
||
|
||
{config.excludeFilter?.enabled && (
|
||
<div className="space-y-3 rounded border p-3">
|
||
{/* 참조 테이블 선택 */}
|
||
<div className="space-y-1">
|
||
<Label className="text-xs font-medium">참조 테이블 (매핑 테이블)</Label>
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
role="combobox"
|
||
className="h-8 w-full justify-between text-xs"
|
||
>
|
||
{config.excludeFilter?.referenceTable || "테이블 선택..."}
|
||
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="w-[300px] p-0">
|
||
<Command>
|
||
<CommandInput placeholder="테이블 검색..." className="h-8 text-xs" />
|
||
<CommandList>
|
||
<CommandEmpty className="text-xs py-2">테이블을 찾을 수 없습니다</CommandEmpty>
|
||
<CommandGroup>
|
||
{availableTables.map((table) => (
|
||
<CommandItem
|
||
key={table.tableName}
|
||
value={table.tableName}
|
||
onSelect={() => {
|
||
handleChange("excludeFilter", {
|
||
...config.excludeFilter,
|
||
referenceTable: table.tableName,
|
||
referenceColumn: undefined,
|
||
sourceColumn: undefined,
|
||
filterColumn: undefined,
|
||
filterValueField: undefined,
|
||
});
|
||
}}
|
||
className="text-xs"
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-3 w-3",
|
||
config.excludeFilter?.referenceTable === table.tableName ? "opacity-100" : "opacity-0"
|
||
)}
|
||
/>
|
||
{table.displayName || table.tableName}
|
||
</CommandItem>
|
||
))}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
|
||
{config.excludeFilter?.referenceTable && (
|
||
<>
|
||
{/* 비교 컬럼 설정 - 한 줄에 두 개 */}
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{/* 참조 컬럼 (매핑 테이블) */}
|
||
<div className="space-y-1">
|
||
<Label className="text-xs">비교 컬럼 (매핑)</Label>
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
role="combobox"
|
||
disabled={loadingReferenceColumns}
|
||
className="h-8 w-full justify-between text-xs"
|
||
>
|
||
{loadingReferenceColumns
|
||
? "..."
|
||
: config.excludeFilter?.referenceColumn || "선택"}
|
||
<ChevronsUpDown className="ml-1 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>
|
||
{referenceTableColumns.map((col) => (
|
||
<CommandItem
|
||
key={col.columnName}
|
||
value={col.columnName}
|
||
onSelect={() => {
|
||
handleChange("excludeFilter", {
|
||
...config.excludeFilter,
|
||
referenceColumn: col.columnName,
|
||
});
|
||
}}
|
||
className="text-xs"
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-3 w-3",
|
||
config.excludeFilter?.referenceColumn === col.columnName ? "opacity-100" : "opacity-0"
|
||
)}
|
||
/>
|
||
{col.label || col.columnName}
|
||
</CommandItem>
|
||
))}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
|
||
{/* 소스 컬럼 (현재 테이블) */}
|
||
<div className="space-y-1">
|
||
<Label className="text-xs">비교 컬럼 (현재)</Label>
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
role="combobox"
|
||
className="h-8 w-full justify-between text-xs"
|
||
>
|
||
{config.excludeFilter?.sourceColumn || "선택"}
|
||
<ChevronsUpDown className="ml-1 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={() => {
|
||
handleChange("excludeFilter", {
|
||
...config.excludeFilter,
|
||
sourceColumn: col.columnName,
|
||
});
|
||
}}
|
||
className="text-xs"
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-3 w-3",
|
||
config.excludeFilter?.sourceColumn === col.columnName ? "opacity-100" : "opacity-0"
|
||
)}
|
||
/>
|
||
{col.label || col.columnName}
|
||
</CommandItem>
|
||
))}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 조건 필터 - 특정 조건의 데이터만 제외 */}
|
||
<div className="space-y-1">
|
||
<Label className="text-xs">조건 필터 (선택사항)</Label>
|
||
<p className="text-[10px] text-muted-foreground mb-1">
|
||
특정 조건의 데이터만 제외하려면 설정하세요 (예: 특정 거래처의 품목만)
|
||
</p>
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{/* 필터 컬럼 (매핑 테이블) */}
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
role="combobox"
|
||
disabled={loadingReferenceColumns}
|
||
className="h-8 w-full justify-between text-xs"
|
||
>
|
||
{loadingReferenceColumns
|
||
? "..."
|
||
: config.excludeFilter?.filterColumn
|
||
? `매핑: ${config.excludeFilter.filterColumn}`
|
||
: "매핑 테이블 컬럼"}
|
||
<ChevronsUpDown className="ml-1 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>
|
||
<CommandItem
|
||
value=""
|
||
onSelect={() => {
|
||
handleChange("excludeFilter", {
|
||
...config.excludeFilter,
|
||
filterColumn: undefined,
|
||
filterValueField: undefined,
|
||
});
|
||
}}
|
||
className="text-xs text-muted-foreground"
|
||
>
|
||
<Check className={cn("mr-2 h-3 w-3", !config.excludeFilter?.filterColumn ? "opacity-100" : "opacity-0")} />
|
||
사용 안함
|
||
</CommandItem>
|
||
{referenceTableColumns.map((col) => (
|
||
<CommandItem
|
||
key={col.columnName}
|
||
value={col.columnName}
|
||
onSelect={() => {
|
||
// 필터 컬럼 선택 시 같은 이름의 필드를 자동으로 설정
|
||
handleChange("excludeFilter", {
|
||
...config.excludeFilter,
|
||
filterColumn: col.columnName,
|
||
filterValueField: col.columnName, // 같은 이름으로 자동 설정
|
||
filterValueSource: "url",
|
||
});
|
||
}}
|
||
className="text-xs"
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-3 w-3",
|
||
config.excludeFilter?.filterColumn === col.columnName ? "opacity-100" : "opacity-0"
|
||
)}
|
||
/>
|
||
{col.label || col.columnName}
|
||
</CommandItem>
|
||
))}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
{/* 필터 값 필드명 (부모 화면에서 전달받는 필드) */}
|
||
<Input
|
||
placeholder="예: customer_code"
|
||
value={config.excludeFilter?.filterValueField || ""}
|
||
onChange={(e) => {
|
||
handleChange("excludeFilter", {
|
||
...config.excludeFilter,
|
||
filterValueField: e.target.value,
|
||
});
|
||
}}
|
||
disabled={!config.excludeFilter?.filterColumn}
|
||
className="h-8 text-xs"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* 설정 요약 */}
|
||
{config.excludeFilter?.referenceTable && config.excludeFilter?.referenceColumn && config.excludeFilter?.sourceColumn && (
|
||
<div className="rounded bg-muted/50 p-2 text-[10px] text-muted-foreground">
|
||
<strong>설정 요약:</strong> {config.selectedTable || screenTableName}.{config.excludeFilter.sourceColumn} 가
|
||
{" "}{config.excludeFilter.referenceTable}.{config.excludeFilter.referenceColumn} 에
|
||
{config.excludeFilter.filterColumn && config.excludeFilter.filterValueField && (
|
||
<> ({config.excludeFilter.filterColumn}=URL의 {config.excludeFilter.filterValueField}일 때)</>
|
||
)}
|
||
{" "}이미 있으면 제외
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|