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:
DDD1542
2026-02-10 10:51:23 +09:00
parent 9e1a54c738
commit 3c8c2ebcf4
5 changed files with 392 additions and 374 deletions

View File

@@ -957,6 +957,67 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
[formatDateValue, formatNumberValue],
);
// 🆕 패널 config의 columns에서 additionalJoinColumns 추출하는 헬퍼
const extractAdditionalJoinColumns = useCallback((columns: any[] | undefined, tableName: string) => {
if (!columns || columns.length === 0) return undefined;
const joinColumns: Array<{
sourceTable: string;
sourceColumn: string;
referenceTable: string;
joinAlias: string;
}> = [];
columns.forEach((col: any) => {
// 방법 1: isEntityJoin 플래그가 있는 경우 (설정 패널에서 Entity 조인 컬럼으로 추가한 경우)
if (col.isEntityJoin && col.joinInfo) {
const existing = joinColumns.find(
(j) => j.referenceTable === col.joinInfo.referenceTable && j.joinAlias === col.joinInfo.joinAlias
);
if (!existing) {
joinColumns.push({
sourceTable: col.joinInfo.sourceTable || tableName,
sourceColumn: col.joinInfo.sourceColumn,
referenceTable: col.joinInfo.referenceTable,
joinAlias: col.joinInfo.joinAlias,
});
}
return;
}
// 방법 2: "테이블명.컬럼명" 형식 (기존 좌측 패널 방식)
const colName = typeof col === "string" ? col : col.name || col.columnName;
if (colName && colName.includes(".")) {
const [refTable, refColumn] = colName.split(".");
const inferredSourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id");
const existing = joinColumns.find(
(j) => j.referenceTable === refTable && j.sourceColumn === inferredSourceColumn
);
if (!existing) {
joinColumns.push({
sourceTable: tableName,
sourceColumn: inferredSourceColumn,
referenceTable: refTable,
joinAlias: `${inferredSourceColumn}_${refColumn}`,
});
} else {
// 이미 추가된 테이블이면 별칭만 추가
const newAlias = `${inferredSourceColumn}_${refColumn}`;
if (!joinColumns.find((j) => j.joinAlias === newAlias)) {
joinColumns.push({
sourceTable: tableName,
sourceColumn: inferredSourceColumn,
referenceTable: refTable,
joinAlias: newAlias,
});
}
}
}
});
return joinColumns.length > 0 ? joinColumns : undefined;
}, []);
// 좌측 데이터 로드
const loadLeftData = useCallback(async () => {
const leftTableName = componentConfig.leftPanel?.tableName;
@@ -967,74 +1028,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 🎯 필터 조건을 API에 전달 (entityJoinApi 사용)
const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined;
// 🆕 "테이블명.컬럼명" 형식의 조인 컬럼들을 additionalJoinColumns로 변환
const configuredColumns = componentConfig.leftPanel?.columns || [];
const additionalJoinColumns: Array<{
sourceTable: string;
sourceColumn: string;
referenceTable: string;
joinAlias: string;
}> = [];
// 🆕 좌측 패널 config의 Entity 조인 컬럼 추출 (헬퍼 함수 사용)
const leftJoinColumns = extractAdditionalJoinColumns(
componentConfig.leftPanel?.columns,
leftTableName,
);
// 소스 컬럼 매핑 (item_info → item_code, warehouse_info → warehouse_id 등)
const sourceColumnMap: Record<string, string> = {};
configuredColumns.forEach((col: any) => {
const colName = typeof col === "string" ? col : col.name || col.columnName;
if (colName && colName.includes(".")) {
const [refTable, refColumn] = colName.split(".");
// 소스 컬럼 추론 (item_info → item_code 또는 warehouse_info → warehouse_id)
// 기본: _info → _code, 백업: _info → _id
const primarySourceColumn = refTable.replace("_info", "_code").replace("_mng", "_id");
const secondarySourceColumn = refTable.replace("_info", "_id").replace("_mng", "_id");
// 실제 존재하는 소스 컬럼은 백엔드에서 결정 (프론트엔드는 두 패턴 모두 전달)
const inferredSourceColumn = primarySourceColumn;
// 이미 추가된 조인인지 확인 (동일 테이블, 동일 소스컬럼)
const existingJoin = additionalJoinColumns.find(
(j) => j.referenceTable === refTable && j.sourceColumn === inferredSourceColumn,
);
if (!existingJoin) {
// 새로운 조인 추가 (첫 번째 컬럼)
additionalJoinColumns.push({
sourceTable: leftTableName,
sourceColumn: inferredSourceColumn,
referenceTable: refTable,
joinAlias: `${inferredSourceColumn}_${refColumn}`,
});
sourceColumnMap[refTable] = inferredSourceColumn;
}
// 추가 컬럼도 별도로 요청 (item_code_standard, item_code_unit 등)
// 단, 첫 번째 컬럼과 다른 경우만
const existingAliases = additionalJoinColumns
.filter((j) => j.referenceTable === refTable)
.map((j) => j.joinAlias);
const newAlias = `${sourceColumnMap[refTable] || inferredSourceColumn}_${refColumn}`;
if (!existingAliases.includes(newAlias)) {
additionalJoinColumns.push({
sourceTable: leftTableName,
sourceColumn: sourceColumnMap[refTable] || inferredSourceColumn,
referenceTable: refTable,
joinAlias: newAlias,
});
}
}
});
console.log("🔗 [분할패널] additionalJoinColumns:", additionalJoinColumns);
console.log("🔗 [분할패널] configuredColumns:", configuredColumns);
console.log("🔗 [분할패널] 좌측 additionalJoinColumns:", leftJoinColumns);
const result = await entityJoinApi.getTableDataWithJoins(leftTableName, {
page: 1,
size: 100,
search: filters, // 필터 조건 전달
enableEntityJoin: true, // 엔티티 조인 활성화
dataFilter: componentConfig.leftPanel?.dataFilter, // 🆕 데이터 필터 전달
additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 🆕 추가 조인 컬럼
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
search: filters,
enableEntityJoin: true,
dataFilter: componentConfig.leftPanel?.dataFilter,
additionalJoinColumns: leftJoinColumns,
companyCodeOverride: companyCode,
});
// 🔍 디버깅: API 응답 데이터의 키 확인
@@ -1093,11 +1102,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
// 🆕 엔티티 조인 API 사용
const { entityJoinApi } = await import("@/lib/api/entityJoin");
const rightDetailJoinColumns = extractAdditionalJoinColumns(
componentConfig.rightPanel?.columns,
rightTableName,
);
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
search: { id: primaryKey },
enableEntityJoin: true, // 엔티티 조인 활성화
enableEntityJoin: true,
size: 1,
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
companyCodeOverride: companyCode,
additionalJoinColumns: rightDetailJoinColumns, // 🆕 Entity 조인 컬럼 전달
});
const detail = result.items && result.items.length > 0 ? result.items[0] : null;
@@ -1141,6 +1155,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const { entityJoinApi } = await import("@/lib/api/entityJoin");
const allResults: any[] = [];
// 🆕 우측 패널 Entity 조인 컬럼 추출 (그룹 합산용)
const rightJoinColumnsForGroup = extractAdditionalJoinColumns(
componentConfig.rightPanel?.columns,
rightTableName,
);
// 각 원본 항목에 대해 조회
for (const originalItem of leftItem._originalItems) {
const searchConditions: Record<string, any> = {};
@@ -1155,7 +1175,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
search: searchConditions,
enableEntityJoin: true,
size: 1000,
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
companyCodeOverride: companyCode,
additionalJoinColumns: rightJoinColumnsForGroup, // 🆕 Entity 조인 컬럼 전달
});
if (result.data) {
allResults.push(...result.data);
@@ -1185,12 +1206,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
console.log("🔗 [분할패널] 복합키 조건:", searchConditions);
// 🆕 우측 패널 config의 Entity 조인 컬럼 추출
const rightJoinColumns = extractAdditionalJoinColumns(
componentConfig.rightPanel?.columns,
rightTableName,
);
if (rightJoinColumns) {
console.log("🔗 [분할패널] 우측 패널 additionalJoinColumns:", rightJoinColumns);
}
// 엔티티 조인 API로 데이터 조회
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
search: searchConditions,
enableEntityJoin: true,
size: 1000,
companyCodeOverride: companyCode, // 🆕 프리뷰용 회사 코드 오버라이드
companyCodeOverride: companyCode,
additionalJoinColumns: rightJoinColumns, // 🆕 Entity 조인 컬럼 전달
});
console.log("🔗 [분할패널] 복합키 조회 결과:", result);
@@ -1275,6 +1306,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const leftColumn = tabConfig.relation?.leftColumn || keys?.[0]?.leftColumn;
const rightColumn = tabConfig.relation?.foreignKey || keys?.[0]?.rightColumn;
// 🆕 탭 config의 Entity 조인 컬럼 추출
const tabJoinColumns = extractAdditionalJoinColumns(tabConfig.columns, tabTableName);
if (tabJoinColumns) {
console.log(`🔗 [분할패널] 탭 ${tabIndex} additionalJoinColumns:`, tabJoinColumns);
}
let resultData: any[] = [];
if (leftColumn && rightColumn) {
@@ -1303,12 +1340,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
search: searchConditions,
enableEntityJoin: true,
size: 1000,
additionalJoinColumns: tabJoinColumns, // 🆕 Entity 조인 컬럼 전달
});
resultData = result.data || [];
} else {
const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
enableEntityJoin: true,
size: 1000,
additionalJoinColumns: tabJoinColumns, // 🆕 Entity 조인 컬럼 전달
});
resultData = result.data || [];
}

View File

@@ -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">