feat: Enhance SplitPanelLayoutComponent with improved data loading and filtering logic

- Updated loadRightData function to support loading all data when no leftItem is selected, applying data filters as needed.
- Enhanced loadTabData function to handle data loading for tabs, including support for data filters and entity joins.
- Improved comments for clarity on data loading behavior based on leftItem selection.
- Refactored UI components in SplitPanelLayoutConfigPanel for better styling and organization, including updates to table selection and display settings.
This commit is contained in:
DDD1542
2026-02-11 10:46:47 +09:00
parent 9785f098d8
commit ced25c9a54
2 changed files with 461 additions and 403 deletions

View File

@@ -328,7 +328,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
<AccordionItem
key={tab.tabId}
value={tab.tabId}
className="rounded-lg border bg-gray-50"
className="rounded-lg border bg-card"
>
<AccordionTrigger className="px-3 py-2 hover:no-underline">
<div className="flex flex-1 items-center gap-2">
@@ -341,11 +341,11 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
)}
</div>
</AccordionTrigger>
<AccordionContent className="px-3 pb-3">
<div className="space-y-4">
<AccordionContent className="space-y-4 px-3 pb-3">
{/* ===== 1. 기본 정보 ===== */}
<div className="space-y-3 rounded-lg border bg-white p-3">
<Label className="text-xs font-semibold text-blue-600"> </Label>
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs"> </Label>
@@ -366,123 +366,120 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
type="number"
value={tab.panelHeaderHeight ?? 48}
onChange={(e) => updateTab({ panelHeaderHeight: parseInt(e.target.value) || 48 })}
placeholder="48"
className="h-8 w-24 text-xs"
/>
</div>
</div>
{/* ===== 2. 테이블 선택 ===== */}
<div className="space-y-3 rounded-lg border bg-white p-3">
<Label className="text-xs font-semibold text-blue-600"> </Label>
<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"
>
{tab.tableName || "테이블을 선택하세요"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{availableRightTables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.displayName || ""} ${table.tableName}`}
onSelect={() => updateTab({ tableName: table.tableName, columns: [] })}
>
<Check
className={cn(
"mr-2 h-4 w-4",
tab.tableName === table.tableName ? "opacity-100" : "opacity-0"
)}
/>
{table.displayName || table.tableName}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className="h-8 w-full justify-between text-xs"
>
{tab.tableName || "테이블을 선택하세요"}
<ChevronsUpDown className="ml-2 h-3 w-3 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="테이블 검색..." className="text-xs" />
<CommandEmpty> .</CommandEmpty>
<CommandGroup className="max-h-[200px] overflow-auto">
{availableRightTables.map((table) => (
<CommandItem
key={table.tableName}
value={`${table.displayName || ""} ${table.tableName}`}
onSelect={() => updateTab({ tableName: table.tableName, columns: [] })}
>
<Check
className={cn(
"mr-2 h-4 w-4",
tab.tableName === table.tableName ? "opacity-100" : "opacity-0"
)}
/>
{table.displayName || table.tableName}
{table.displayName && <span className="ml-2 text-xs text-gray-500">({table.tableName})</span>}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</div>
{/* ===== 3. 표시 모드 ===== */}
<div className="space-y-3 rounded-lg border bg-white p-3">
<Label className="text-xs font-semibold text-blue-600"> </Label>
<div className="space-y-1">
{/* ===== 3. 표시 모드 + 요약 설정 ===== */}
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={tab.displayMode || "list"}
onValueChange={(value: "list" | "table") => updateTab({ displayMode: value })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
<SelectTrigger className="h-8 bg-white text-xs">
<SelectValue>
{(tab.displayMode || "list") === "list" ? "목록 (LIST)" : "테이블 (TABLE)"}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="list"> ()</SelectItem>
<SelectItem value="table"></SelectItem>
<SelectItem value="list">
<div className="flex flex-col py-1">
<span className="text-sm font-medium"> (LIST)</span>
<span className="text-xs text-gray-500"> ()</span>
</div>
</SelectItem>
<SelectItem value="table">
<div className="flex flex-col py-1">
<span className="text-sm font-medium"> (TABLE)</span>
<span className="text-xs text-gray-500"> </span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{/* 요약 설정 (목록 모드) */}
{tab.displayMode === "list" && (
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs"> </Label>
{(tab.displayMode || "list") === "list" && (
<div className="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3">
<Label className="text-sm font-semibold"> </Label>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
type="number"
type="number" min="1" max="10"
value={tab.summaryColumnCount ?? 3}
onChange={(e) => updateTab({ summaryColumnCount: parseInt(e.target.value) || 3 })}
min={1}
max={10}
className="h-8 text-xs"
className="bg-white"
/>
<p className="text-xs text-gray-500"> (기본: 3개)</p>
</div>
<div className="flex items-center gap-2 pt-5">
<div className="flex items-center justify-between space-x-2">
<div className="flex-1">
<Label className="text-xs"> </Label>
<p className="text-xs text-gray-500"> </p>
</div>
<Checkbox
id={`tab-${tabIndex}-summary-label`}
checked={tab.summaryShowLabel ?? true}
onCheckedChange={(checked) => updateTab({ summaryShowLabel: !!checked })}
onCheckedChange={(checked) => updateTab({ summaryShowLabel: checked as boolean })}
/>
<label htmlFor={`tab-${tabIndex}-summary-label`} className="text-xs"> </label>
</div>
</div>
)}
</div>
{/* ===== 4. 컬럼 매핑 (연결 키) ===== */}
<div className="space-y-3 rounded-lg border bg-white p-3">
<Label className="text-xs font-semibold text-blue-600"> ( )</Label>
<p className="text-[10px] text-gray-500">
</p>
<div className="mt-2 grid grid-cols-2 gap-2">
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> ( )</h3>
<p className="text-muted-foreground text-[10px]"> </p>
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Select
value={tab.relation?.keys?.[0]?.leftColumn || tab.relation?.leftColumn || "__none__"}
onValueChange={(value) => {
if (value === "__none__") {
// 선택 안 함 - 조인 키 제거
updateTab({
relation: undefined,
});
updateTab({ relation: undefined });
} else {
updateTab({
relation: {
@@ -494,17 +491,13 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
}
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">
<span className="text-muted-foreground"> ( )</span>
</SelectItem>
<SelectItem value="__none__"><span className="text-muted-foreground"> ( )</span></SelectItem>
{leftTableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
<SelectItem key={col.columnName} value={col.columnName}>{col.columnLabel || col.columnName}</SelectItem>
))}
</SelectContent>
</Select>
@@ -515,10 +508,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
value={tab.relation?.keys?.[0]?.rightColumn || tab.relation?.foreignKey || "__none__"}
onValueChange={(value) => {
if (value === "__none__") {
// 선택 안 함 - 조인 키 제거
updateTab({
relation: undefined,
});
updateTab({ relation: undefined });
} else {
updateTab({
relation: {
@@ -530,17 +520,13 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
}
}}
>
<SelectTrigger className="h-7 text-xs">
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">
<span className="text-muted-foreground"> ( )</span>
</SelectItem>
<SelectItem value="__none__"><span className="text-muted-foreground"> ( )</span></SelectItem>
{tabColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
<SelectItem key={col.columnName} value={col.columnName}>{col.columnLabel || col.columnName}</SelectItem>
))}
</SelectContent>
</Select>
@@ -549,215 +535,202 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
</div>
{/* ===== 5. 기능 버튼 ===== */}
<div className="space-y-3 rounded-lg border bg-white p-3">
<Label className="text-xs font-semibold text-blue-600"> </Label>
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<div className="grid grid-cols-4 gap-2">
<div className="flex items-center gap-1">
<Checkbox
id={`tab-${tabIndex}-search`}
checked={tab.showSearch}
onCheckedChange={(checked) => updateTab({ showSearch: !!checked })}
/>
<Checkbox id={`tab-${tabIndex}-search`} checked={tab.showSearch} onCheckedChange={(checked) => updateTab({ showSearch: !!checked })} />
<label htmlFor={`tab-${tabIndex}-search`} className="text-xs"></label>
</div>
<div className="flex items-center gap-1">
<Checkbox
id={`tab-${tabIndex}-add`}
checked={tab.showAdd}
onCheckedChange={(checked) => updateTab({ showAdd: !!checked })}
/>
<Checkbox id={`tab-${tabIndex}-add`} checked={tab.showAdd} onCheckedChange={(checked) => updateTab({ showAdd: !!checked })} />
<label htmlFor={`tab-${tabIndex}-add`} className="text-xs"></label>
</div>
<div className="flex items-center gap-1">
<Checkbox
id={`tab-${tabIndex}-edit`}
checked={tab.showEdit}
onCheckedChange={(checked) => updateTab({ showEdit: !!checked })}
/>
<Checkbox id={`tab-${tabIndex}-edit`} checked={tab.showEdit} onCheckedChange={(checked) => updateTab({ showEdit: !!checked })} />
<label htmlFor={`tab-${tabIndex}-edit`} className="text-xs"></label>
</div>
<div className="flex items-center gap-1">
<Checkbox
id={`tab-${tabIndex}-delete`}
checked={tab.showDelete}
onCheckedChange={(checked) => updateTab({ showDelete: !!checked })}
/>
<Checkbox id={`tab-${tabIndex}-delete`} checked={tab.showDelete} onCheckedChange={(checked) => updateTab({ showDelete: !!checked })} />
<label htmlFor={`tab-${tabIndex}-delete`} className="text-xs"></label>
</div>
</div>
</div>
{/* ===== 6. 표시 컬럼 설정 ===== */}
<div className="space-y-3 rounded-lg border border-green-200 bg-green-50 p-3">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold text-green-700"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const currentColumns = tab.columns || [];
const newColumns = [...currentColumns, { name: "", label: "", width: 100 }];
updateTab({ columns: newColumns });
}}
className="h-7 text-xs"
disabled={!tab.tableName || loadingTabColumns}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
<p className="text-[10px] text-gray-600">
. .
</p>
{/* ===== 6. 표시 컬럼 - DnD + Entity 조인 통합 ===== */}
{(() => {
const selectedColumns = tab.columns || [];
const filteredTabCols = tabColumns.filter((c) => !["company_code", "company_name"].includes(c.columnName));
const unselectedCols = filteredTabCols.filter((c) => !selectedColumns.some((sc) => sc.name === c.columnName));
const dbNumericTypes = ["numeric", "decimal", "integer", "bigint", "double precision", "real", "smallint", "int4", "int8", "float4", "float8"];
const inputNumericTypes = ["number", "decimal", "currency", "integer"];
{/* 테이블 미선택 상태 */}
{!tab.tableName && (
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> </p>
</div>
)}
const handleTabDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (over && active.id !== over.id) {
const oldIndex = selectedColumns.findIndex((c) => c.name === active.id);
const newIndex = selectedColumns.findIndex((c) => c.name === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
updateTab({ columns: arrayMove([...selectedColumns], oldIndex, newIndex) });
}
}
};
{/* 테이블 선택됨 - 컬럼 목록 */}
{tab.tableName && (
<div className="space-y-2">
{/* 로딩 상태 */}
{loadingTabColumns && (
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> ...</p>
</div>
)}
return (
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> ({selectedColumns.length} )</h3>
<div className="max-h-[400px] overflow-y-auto rounded-md border bg-white p-2">
{!tab.tableName ? (
<p className="text-muted-foreground py-2 text-center text-xs"> </p>
) : loadingTabColumns ? (
<p className="text-muted-foreground py-2 text-center text-xs"> ...</p>
) : (
<>
{selectedColumns.length > 0 && (
<DndContext collisionDetection={closestCenter} onDragEnd={handleTabDragEnd}>
<SortableContext items={selectedColumns.map((c) => c.name)} strategy={verticalListSortingStrategy}>
<div className="space-y-1">
{selectedColumns.map((col, index) => {
const colInfo = tabColumns.find((c) => c.columnName === col.name);
const isNumeric = colInfo && (
dbNumericTypes.includes(colInfo.dataType?.toLowerCase() || "") ||
inputNumericTypes.includes(colInfo.input_type?.toLowerCase() || "") ||
inputNumericTypes.includes(colInfo.webType?.toLowerCase() || "")
);
return (
<SortableColumnRow
key={col.name}
id={col.name}
col={col}
index={index}
isNumeric={!!isNumeric}
isEntityJoin={!!(col as any).isEntityJoin}
onLabelChange={(value) => {
const newColumns = [...selectedColumns];
newColumns[index] = { ...newColumns[index], label: value };
updateTab({ columns: newColumns });
}}
onWidthChange={(value) => {
const newColumns = [...selectedColumns];
newColumns[index] = { ...newColumns[index], width: value };
updateTab({ columns: newColumns });
}}
onFormatChange={(checked) => {
const newColumns = [...selectedColumns];
newColumns[index] = { ...newColumns[index], format: { ...newColumns[index].format, type: "number", thousandSeparator: checked } };
updateTab({ columns: newColumns });
}}
onRemove={() => updateTab({ columns: selectedColumns.filter((_, i) => i !== index) })}
/>
);
})}
</div>
</SortableContext>
</DndContext>
)}
{/* 설정된 컬럼이 없을 때 */}
{!loadingTabColumns && (tab.columns || []).length === 0 && (
<div className="rounded-md border border-dashed border-gray-300 bg-white p-3 text-center">
<p className="text-xs text-gray-500"> </p>
<p className="mt-1 text-[10px] text-gray-400"> </p>
</div>
)}
{selectedColumns.length > 0 && unselectedCols.length > 0 && (
<div className="border-border/60 my-2 flex items-center gap-2 border-t pt-2">
<span className="text-muted-foreground text-[10px]"> </span>
</div>
)}
{/* 설정된 컬럼 목록 */}
{!loadingTabColumns && (tab.columns || []).length > 0 && (
(tab.columns || []).map((col, colIndex) => (
<div key={colIndex} className="space-y-2 rounded-md border bg-white p-3">
{/* 상단: 순서 변경 + 삭제 버튼 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<Button
size="sm"
variant="ghost"
<div className="space-y-0.5">
{unselectedCols.map((column) => (
<div
key={column.columnName}
className="hover:bg-muted/50 flex cursor-pointer items-center gap-2 rounded px-2 py-1.5"
onClick={() => {
if (colIndex === 0) return;
const newColumns = [...(tab.columns || [])];
[newColumns[colIndex - 1], newColumns[colIndex]] = [newColumns[colIndex], newColumns[colIndex - 1]];
updateTab({ columns: newColumns });
updateTab({ columns: [...selectedColumns, { name: column.columnName, label: column.columnLabel || column.columnName, width: 100 }] });
}}
disabled={colIndex === 0}
className="h-6 w-6 p-0"
>
<ArrowUp className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => {
const columns = tab.columns || [];
if (colIndex === columns.length - 1) return;
const newColumns = [...columns];
[newColumns[colIndex], newColumns[colIndex + 1]] = [newColumns[colIndex + 1], newColumns[colIndex]];
updateTab({ columns: newColumns });
}}
disabled={colIndex === (tab.columns || []).length - 1}
className="h-6 w-6 p-0"
>
<ArrowDown className="h-3 w-3" />
</Button>
<span className="text-[10px] text-gray-400">#{colIndex + 1}</span>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => {
const newColumns = (tab.columns || []).filter((_, i) => i !== colIndex);
updateTab({ columns: newColumns });
}}
className="h-6 w-6 p-0 text-red-500 hover:bg-red-50 hover:text-red-600"
>
<X className="h-3 w-3" />
</Button>
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
<span className="text-muted-foreground truncate text-xs">{column.columnLabel || column.columnName}</span>
</div>
))}
</div>
{/* 컬럼 선택 */}
<div className="space-y-1">
<Label className="text-[10px] text-gray-500"></Label>
<Select
value={col.name}
onValueChange={(value) => {
const selectedCol = tabColumns.find((c) => c.columnName === value);
const newColumns = [...(tab.columns || [])];
newColumns[colIndex] = {
...col,
name: value,
label: selectedCol?.columnLabel || value,
};
updateTab({ columns: newColumns });
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼을 선택하세요" />
</SelectTrigger>
<SelectContent>
{tabColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnLabel || column.columnName}
<span className="ml-1 text-[10px] text-gray-400">({column.columnName})</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */}
{(() => {
const joinData = tab.tableName ? entityJoinColumnsMap?.[tab.tableName] : null;
if (!joinData || joinData.joinTables.length === 0) return null;
{/* 라벨 + 너비 */}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-[10px] text-gray-500"></Label>
<Input
value={col.label}
onChange={(e) => {
const newColumns = [...(tab.columns || [])];
newColumns[colIndex] = { ...col, label: e.target.value };
updateTab({ columns: newColumns });
}}
placeholder="표시 라벨"
className="h-8 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-[10px] text-gray-500"> (px)</Label>
<Input
type="number"
value={col.width || 100}
onChange={(e) => {
const newColumns = [...(tab.columns || [])];
newColumns[colIndex] = { ...col, width: parseInt(e.target.value) || 100 };
updateTab({ columns: newColumns });
}}
placeholder="100"
className="h-8 text-xs"
/>
</div>
</div>
</div>
))
)}
return joinData.joinTables.map((joinTable, tableIndex) => {
const joinColumnsToShow = joinTable.availableColumns.filter((column) => {
const matchingJoinColumn = joinData.availableColumns.find(
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
);
if (!matchingJoinColumn) return false;
return !selectedColumns.some((c) => c.name === matchingJoinColumn.joinAlias);
});
const addedCount = joinTable.availableColumns.length - joinColumnsToShow.length;
if (joinColumnsToShow.length === 0 && addedCount === 0) return null;
return (
<details key={`tab-join-${tableIndex}`} className="group">
<summary className="border-border/60 my-2 flex cursor-pointer list-none items-center gap-2 border-t pt-2 select-none">
<ChevronRight className="h-3 w-3 shrink-0 text-blue-500 transition-transform group-open:rotate-90" />
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
<span className="text-[10px] font-medium text-blue-600">{joinTable.tableName}</span>
{addedCount > 0 && (
<span className="rounded-full bg-blue-100 px-1.5 text-[9px] font-medium text-blue-600">{addedCount} </span>
)}
<span className="text-[9px] text-gray-400">{joinColumnsToShow.length} </span>
</summary>
<div className="space-y-0.5 pt-1">
{joinColumnsToShow.map((column, colIndex) => {
const matchingJoinColumn = joinData.availableColumns.find(
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
);
if (!matchingJoinColumn) return null;
return (
<div
key={colIndex}
className="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 hover:bg-blue-50/60"
onClick={() => {
updateTab({
columns: [...selectedColumns, {
name: matchingJoinColumn.joinAlias,
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
width: 100,
isEntityJoin: true,
joinInfo: {
sourceTable: tab.tableName!,
sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "",
referenceTable: matchingJoinColumn.tableName,
joinAlias: matchingJoinColumn.joinAlias,
},
}],
});
}}
>
<Checkbox checked={false} className="pointer-events-none h-3.5 w-3.5 shrink-0" />
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
<span className="truncate text-xs text-blue-700">{column.columnLabel || column.columnName}</span>
</div>
);
})}
{joinColumnsToShow.length === 0 && (
<p className="px-2 py-1 text-[10px] text-gray-400"> </p>
)}
</div>
</details>
);
});
})()}
</>
)}
</div>
</div>
)}
</div>
);
})()}
{/* ===== 7. 추가 모달 컬럼 설정 (showAdd일 때) ===== */}
{tab.showAdd && (
<div className="space-y-3 rounded-lg border border-purple-200 bg-purple-50 p-3">
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold text-purple-700"> </Label>
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<Button
size="sm"
variant="outline"
@@ -845,76 +818,11 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
</div>
)}
{/* ===== 7.5 Entity 조인 컬럼 ===== */}
{(() => {
const joinData = tab.tableName ? entityJoinColumnsMap?.[tab.tableName] : null;
if (!joinData || joinData.joinTables.length === 0) return null;
return (
<div className="space-y-2 rounded-lg border bg-white p-3">
<Label className="text-xs font-semibold text-blue-600">Entity </Label>
<p className="text-muted-foreground text-[10px]"> </p>
{joinData.joinTables.map((joinTable, tableIndex) => (
<div key={tableIndex} className="space-y-1">
<div className="mb-1 flex items-center gap-2 text-[10px] font-medium text-blue-600">
<Link2 className="h-3 w-3" />
<span>{joinTable.tableName}</span>
<Badge variant="outline" className="text-[10px]">{joinTable.currentDisplayColumn}</Badge>
</div>
<div className="max-h-32 space-y-0.5 overflow-y-auto rounded-md border p-2">
{joinTable.availableColumns.map((column, colIndex) => {
const matchingJoinColumn = joinData.availableColumns.find(
(jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName,
);
if (!matchingJoinColumn) return null;
const tabColumns2 = tab.columns || [];
const isAdded = tabColumns2.some((c) => c.name === matchingJoinColumn.joinAlias);
return (
<div
key={colIndex}
className={cn(
"flex cursor-pointer items-center gap-2 rounded px-2 py-1 hover:bg-blue-50",
isAdded && "bg-blue-50",
)}
onClick={() => {
if (isAdded) {
updateTab({ columns: tabColumns2.filter((c) => c.name !== matchingJoinColumn.joinAlias) });
} else {
updateTab({
columns: [...tabColumns2, {
name: matchingJoinColumn.joinAlias,
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
width: 100,
isEntityJoin: true,
joinInfo: {
sourceTable: tab.tableName!,
sourceColumn: (joinTable as any).joinConfig?.sourceColumn || "",
referenceTable: matchingJoinColumn.tableName,
joinAlias: matchingJoinColumn.joinAlias,
},
}],
});
}
}}
>
<Checkbox checked={isAdded} className="pointer-events-none h-3.5 w-3.5" />
<Link2 className="h-3 w-3 shrink-0 text-blue-500" />
<span className="truncate text-xs">{column.columnLabel}</span>
<span className="ml-auto text-[10px] text-blue-400">{column.dataType}</span>
</div>
);
})}
</div>
</div>
))}
</div>
);
})()}
{/* Entity 조인 컬럼은 표시 컬럼 목록에 통합됨 */}
{/* ===== 8. 데이터 필터링 ===== */}
<div className="space-y-3 rounded-lg border bg-white p-3">
<Label className="text-xs font-semibold text-blue-600"> </Label>
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<DataFilterConfigPanel
tableName={tab.tableName}
columns={tabColumns}
@@ -925,9 +833,9 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
</div>
{/* ===== 9. 중복 데이터 제거 ===== */}
<div className="space-y-3 rounded-lg border bg-white p-3">
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<div className="flex items-center justify-between">
<Label className="text-xs font-semibold text-blue-600"> </Label>
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<Switch
checked={tab.deduplication?.enabled ?? false}
onCheckedChange={(checked) => {
@@ -1019,8 +927,8 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
{/* ===== 10. 수정 버튼 설정 ===== */}
{tab.showEdit && (
<div className="space-y-3 rounded-lg border border-blue-200 bg-blue-50 p-3">
<Label className="text-xs font-semibold text-blue-700"> </Label>
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<div className="space-y-2">
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
@@ -1125,8 +1033,8 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
{/* ===== 11. 삭제 버튼 설정 ===== */}
{tab.showDelete && (
<div className="space-y-3 rounded-lg border border-red-200 bg-red-50 p-3">
<Label className="text-xs font-semibold text-red-700"> </Label>
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-3">
<h3 className="border-l-2 border-l-primary/40 pl-2 text-xs font-semibold"> </h3>
<div className="space-y-2">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
@@ -1196,7 +1104,7 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
</Button>
</div>
</div>
</AccordionContent>
</AccordionItem>
);