feat: Enhance entity join functionality with company code support
- Updated the EntityJoinController to log the company code during entity join configuration retrieval. - Modified the entityJoinService to accept company code as a parameter, allowing for company-specific entity join detection. - Enhanced the TableManagementService to pass the company code when detecting entity joins and retrieving reference table columns. - Implemented a helper function in the SplitPanelLayoutComponent to extract additional join columns based on the entity join configuration. - Improved the SplitPanelLayoutConfigPanel to display entity join columns dynamically, enhancing user experience and functionality.
This commit is contained in:
@@ -28,12 +28,13 @@ import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
// 드래그 가능한 컬럼 아이템
|
||||
function SortableColumnRow({
|
||||
id, col, index, isNumeric, onLabelChange, onWidthChange, onFormatChange, onRemove,
|
||||
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove,
|
||||
}: {
|
||||
id: string;
|
||||
col: { name: string; label: string; width?: number; format?: any };
|
||||
index: number;
|
||||
isNumeric: boolean;
|
||||
isEntityJoin?: boolean;
|
||||
onLabelChange: (value: string) => void;
|
||||
onWidthChange: (value: number) => void;
|
||||
onFormatChange: (checked: boolean) => void;
|
||||
@@ -49,12 +50,17 @@ function SortableColumnRow({
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-md border bg-card px-2 py-1.5",
|
||||
isDragging && "z-50 opacity-50 shadow-md",
|
||||
isEntityJoin && "border-blue-200 bg-blue-50/30",
|
||||
)}
|
||||
>
|
||||
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
||||
<GripVertical className="h-3 w-3" />
|
||||
</div>
|
||||
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
||||
{isEntityJoin ? (
|
||||
<Link2 className="h-3 w-3 shrink-0 text-blue-500" title="Entity 조인 컬럼" />
|
||||
) : (
|
||||
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
||||
)}
|
||||
<Input
|
||||
value={col.label}
|
||||
onChange={(e) => onLabelChange(e.target.value)}
|
||||
@@ -1975,6 +1981,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
col={col}
|
||||
index={index}
|
||||
isNumeric={!!isNumeric}
|
||||
isEntityJoin={!!(col as any).isEntityJoin}
|
||||
onLabelChange={(value) => {
|
||||
const newColumns = [...selectedColumns];
|
||||
newColumns[index] = { ...newColumns[index], label: value };
|
||||
@@ -2021,6 +2028,78 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 좌측 패널 - Entity 조인 컬럼 아코디언 */}
|
||||
{(() => {
|
||||
const leftTable = config.leftPanel?.tableName || screenTableName;
|
||||
const joinData = leftTable ? entityJoinColumns[leftTable] : null;
|
||||
if (!joinData || joinData.joinTables.length === 0) return null;
|
||||
|
||||
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={`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={() => {
|
||||
updateLeftPanel({
|
||||
columns: [...selectedColumns, {
|
||||
name: matchingJoinColumn.joinAlias,
|
||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||
width: 100,
|
||||
isEntityJoin: true,
|
||||
joinInfo: {
|
||||
sourceTable: leftTable!,
|
||||
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>
|
||||
@@ -2029,76 +2108,6 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 좌측 패널 Entity 조인 컬럼 */}
|
||||
{(() => {
|
||||
const leftTable = config.leftPanel?.tableName || screenTableName;
|
||||
const joinData = leftTable ? entityJoinColumns[leftTable] : null;
|
||||
if (!joinData || joinData.joinTables.length === 0) return null;
|
||||
const selectedColumns = config.leftPanel?.columns || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-4">
|
||||
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">Entity 조인 컬럼</h3>
|
||||
<p className="text-muted-foreground text-[10px]">연관 테이블의 컬럼을 표시 컬럼에 추가합니다</p>
|
||||
<div className="space-y-3">
|
||||
{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 isAdded = selectedColumns.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) {
|
||||
updateLeftPanel({ columns: selectedColumns.filter((c) => c.name !== matchingJoinColumn.joinAlias) });
|
||||
} else {
|
||||
updateLeftPanel({
|
||||
columns: [...selectedColumns, {
|
||||
name: matchingJoinColumn.joinAlias,
|
||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||
width: 100,
|
||||
isEntityJoin: true,
|
||||
joinInfo: {
|
||||
sourceTable: leftTable!,
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* 좌측 패널 데이터 필터링 */}
|
||||
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
||||
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">좌측 패널 데이터 필터링</h3>
|
||||
@@ -2351,64 +2360,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 엔티티 설정 선택 - 조건 필터 모드에서만 표시 */}
|
||||
{relationshipType !== "detail" && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium">필터 연결 컬럼</Label>
|
||||
<p className="text-muted-foreground text-[10px]">
|
||||
우측 테이블에서 좌측 테이블을 참조하는 컬럼을 선택하세요
|
||||
</p>
|
||||
<Select
|
||||
value={config.rightPanel?.relation?.foreignKey || ""}
|
||||
onValueChange={(value) => {
|
||||
// 선택된 엔티티 컬럼 정보 찾기
|
||||
const entityColumn = rightTableColumns.find((col) => col.columnName === value);
|
||||
if (entityColumn) {
|
||||
updateRightPanel({
|
||||
relation: {
|
||||
...config.rightPanel?.relation,
|
||||
foreignKey: value,
|
||||
// 참조 테이블과 컬럼은 엔티티 설정에서 자동으로 가져옴
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="엔티티 컬럼 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{rightTableColumns
|
||||
.filter((col) => {
|
||||
// 엔티티 타입 컬럼만 표시 (input_type이 entity인 경우)
|
||||
const inputType = col.input_type?.toLowerCase() || col.webType?.toLowerCase() || "";
|
||||
return inputType === "entity" || inputType === "code";
|
||||
})
|
||||
.map((column) => (
|
||||
<SelectItem key={column.columnName} value={column.columnName}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{column.columnLabel || column.columnName}</span>
|
||||
<span className="text-muted-foreground text-[10px]">({column.columnName})</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
{rightTableColumns.filter((col) => {
|
||||
const inputType = col.input_type?.toLowerCase() || col.webType?.toLowerCase() || "";
|
||||
return inputType === "entity" || inputType === "code";
|
||||
}).length === 0 && (
|
||||
<div className="text-muted-foreground px-2 py-4 text-center text-xs">
|
||||
엔티티 타입 컬럼이 없습니다.
|
||||
<br />
|
||||
테이블 타입관리에서 엔티티 설정을 추가하세요.
|
||||
</div>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{config.rightPanel?.relation?.foreignKey && (
|
||||
<p className="text-muted-foreground text-[10px]">선택된 컬럼의 엔티티 설정이 자동으로 적용됩니다.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* 필터 연결 컬럼 제거됨 - Entity 조인이 자동으로 관계를 처리 */}
|
||||
|
||||
{/* 우측 패널 표시 컬럼 설정 - 드래그앤드롭 */}
|
||||
{(() => {
|
||||
@@ -2455,6 +2407,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
col={col}
|
||||
index={index}
|
||||
isNumeric={!!isNumeric}
|
||||
isEntityJoin={!!(col as any).isEntityJoin}
|
||||
onLabelChange={(value) => {
|
||||
const newColumns = [...selectedColumns];
|
||||
newColumns[index] = { ...newColumns[index], label: value };
|
||||
@@ -2499,6 +2452,78 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */}
|
||||
{(() => {
|
||||
const rightTable = config.rightPanel?.tableName;
|
||||
const joinData = rightTable ? entityJoinColumns[rightTable] : null;
|
||||
if (!joinData || joinData.joinTables.length === 0) return null;
|
||||
|
||||
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={`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={() => {
|
||||
updateRightPanel({
|
||||
columns: [...selectedColumns, {
|
||||
name: matchingJoinColumn.joinAlias,
|
||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||
width: 100,
|
||||
isEntityJoin: true,
|
||||
joinInfo: {
|
||||
sourceTable: rightTable!,
|
||||
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>
|
||||
@@ -2507,75 +2532,7 @@ export const SplitPanelLayoutConfigPanel: React.FC<SplitPanelLayoutConfigPanelPr
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* 우측 패널 Entity 조인 컬럼 */}
|
||||
{(() => {
|
||||
const rightTable = config.rightPanel?.tableName;
|
||||
const joinData = rightTable ? entityJoinColumns[rightTable] : null;
|
||||
if (!joinData || joinData.joinTables.length === 0) return null;
|
||||
const selectedColumns = config.rightPanel?.columns || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-3 rounded-lg border border-border/50 bg-muted/40 p-4">
|
||||
<h3 className="border-l-2 border-l-primary/40 pl-2 text-sm font-semibold">Entity 조인 컬럼</h3>
|
||||
<p className="text-muted-foreground text-[10px]">연관 테이블의 컬럼을 표시 컬럼에 추가합니다</p>
|
||||
<div className="space-y-3">
|
||||
{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 isAdded = selectedColumns.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) {
|
||||
updateRightPanel({ columns: selectedColumns.filter((c) => c.name !== matchingJoinColumn.joinAlias) });
|
||||
} else {
|
||||
updateRightPanel({
|
||||
columns: [...selectedColumns, {
|
||||
name: matchingJoinColumn.joinAlias,
|
||||
label: matchingJoinColumn.suggestedLabel || matchingJoinColumn.columnLabel,
|
||||
width: 100,
|
||||
isEntityJoin: true,
|
||||
joinInfo: {
|
||||
sourceTable: rightTable!,
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{/* 우측 패널 Entity 조인 컬럼은 표시 컬럼 목록에 통합됨 */}
|
||||
|
||||
{/* 우측 패널 데이터 필터링 */}
|
||||
<div className="space-y-4 rounded-lg border border-border/50 bg-muted/40 p-4">
|
||||
|
||||
Reference in New Issue
Block a user