diff --git a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx index 0dd00543..e8400c49 100644 --- a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx +++ b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx @@ -7,6 +7,7 @@ import { ColumnConfig, DataTransferField, ActionButtonConfig, + JoinTableConfig, } from "./types"; import { defaultConfig } from "./config"; import { cn } from "@/lib/utils"; @@ -128,6 +129,94 @@ export const SplitPanelLayout2Component: React.FC> => { + const resultMap = new Map(); + if (!joinConfig.joinTable || !joinConfig.mainColumn || !joinConfig.joinColumn || mainData.length === 0) { + return resultMap; + } + + // 메인 데이터에서 조인할 키 값들 추출 + const joinKeys = [...new Set(mainData.map((item) => item[joinConfig.mainColumn]).filter(Boolean))]; + if (joinKeys.length === 0) return resultMap; + + try { + console.log(`[SplitPanelLayout2] 조인 테이블 로드: ${joinConfig.joinTable}, 키: ${joinKeys.length}개`); + + const response = await apiClient.post(`/table-management/tables/${joinConfig.joinTable}/data`, { + page: 1, + size: 1000, + // 조인 키 값들로 필터링 + dataFilter: { + enabled: true, + matchType: "any", // OR 조건으로 여러 키 매칭 + filters: joinKeys.map((key, idx) => ({ + id: `join_key_${idx}`, + columnName: joinConfig.joinColumn, + operator: "equals", + value: String(key), + valueType: "static", + })), + }, + autoFilter: { + enabled: true, + filterColumn: "company_code", + filterType: "company", + }, + }); + + if (response.data.success) { + const joinData = response.data.data?.data || []; + // 조인 컬럼 값을 키로 하는 Map 생성 + joinData.forEach((item: any) => { + const key = item[joinConfig.joinColumn]; + if (key) { + resultMap.set(String(key), item); + } + }); + console.log(`[SplitPanelLayout2] 조인 테이블 로드 완료: ${joinData.length}건`); + } + } catch (error) { + console.error(`[SplitPanelLayout2] 조인 테이블 로드 실패 (${joinConfig.joinTable}):`, error); + } + + return resultMap; + }, []); + + // 메인 데이터에 조인 테이블 데이터 병합 + const mergeJoinData = useCallback(( + mainData: any[], + joinConfig: JoinTableConfig, + joinDataMap: Map + ): any[] => { + return mainData.map((item) => { + const joinKey = item[joinConfig.mainColumn]; + const joinRow = joinDataMap.get(String(joinKey)); + + if (joinRow && joinConfig.selectColumns) { + // 선택된 컬럼만 병합 + const mergedItem = { ...item }; + joinConfig.selectColumns.forEach((col) => { + // alias가 있으면 alias_컬럼명, 없으면 그냥 컬럼명 + const targetKey = joinConfig.alias ? `${joinConfig.alias}_${col}` : col; + // 메인 테이블에 같은 컬럼이 없으면 추가 + if (!(col in mergedItem)) { + mergedItem[col] = joinRow[col]; + } else if (joinConfig.alias) { + // 메인 테이블에 같은 컬럼이 있으면 alias로 추가 + mergedItem[targetKey] = joinRow[col]; + } + }); + return mergedItem; + } + + return item; + }); + }, []); + // 우측 데이터 로드 (좌측 선택 항목 기반) const loadRightData = useCallback(async (selectedItem: any) => { if (!config.rightPanel?.tableName || !config.joinConfig?.leftColumn || !config.joinConfig?.rightColumn || !selectedItem) { @@ -173,7 +262,24 @@ export const SplitPanelLayout2Component: React.FC 0 && data.length > 0) { + console.log(`[SplitPanelLayout2] 조인 테이블 처리 시작: ${joinTables.length}개`); + + for (const joinTableConfig of joinTables) { + const joinDataMap = await loadJoinTableData(joinTableConfig, data); + if (joinDataMap.size > 0) { + data = mergeJoinData(data, joinTableConfig, joinDataMap); + } + } + + console.log(`[SplitPanelLayout2] 조인 데이터 병합 완료`); + } + setRightData(data); console.log(`[SplitPanelLayout2] 우측 데이터 로드 완료: ${data.length}건`); } else { @@ -196,7 +302,7 @@ export const SplitPanelLayout2Component: React.FC { diff --git a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx index da520d92..1a32f2ca 100644 --- a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx @@ -28,7 +28,7 @@ import { import { Check, ChevronsUpDown, Plus, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; -import type { SplitPanelLayout2Config, ColumnConfig, DataTransferField } from "./types"; +import type { SplitPanelLayout2Config, ColumnConfig, DataTransferField, JoinTableConfig } from "./types"; // lodash set 대체 함수 const setPath = (obj: any, path: string, value: any): any => { @@ -245,6 +245,68 @@ export const SplitPanelLayout2ConfigPanel: React.FC { + const loadJoinTableColumns = async () => { + const joinTables = config.rightPanel?.joinTables || []; + if (joinTables.length === 0 || !config.rightPanel?.tableName) return; + + // 메인 테이블 컬럼 먼저 로드 + try { + const mainResponse = await apiClient.get(`/table-management/tables/${config.rightPanel.tableName}/columns?size=200`); + let mainColumns: ColumnInfo[] = []; + + if (mainResponse.data?.success) { + const columnList = mainResponse.data.data?.columns || mainResponse.data.data || []; + mainColumns = columnList.map((c: any) => ({ + column_name: c.columnName ?? c.column_name ?? c.name ?? "", + data_type: c.dataType ?? c.data_type ?? c.type ?? "", + column_comment: c.displayName ?? c.column_comment ?? c.label ?? "", + })); + } + + // 조인 테이블들의 선택된 컬럼 추가 + const joinColumns: ColumnInfo[] = []; + for (const jt of joinTables) { + if (jt.joinTable && jt.selectColumns && jt.selectColumns.length > 0) { + try { + const joinResponse = await apiClient.get(`/table-management/tables/${jt.joinTable}/columns?size=200`); + if (joinResponse.data?.success) { + const columnList = joinResponse.data.data?.columns || joinResponse.data.data || []; + const transformedColumns = columnList.map((c: any) => ({ + column_name: c.columnName ?? c.column_name ?? c.name ?? "", + data_type: c.dataType ?? c.data_type ?? c.type ?? "", + column_comment: c.displayName ?? c.column_comment ?? c.label ?? "", + })); + + // 선택된 컬럼 추가 (테이블명으로 구분) + jt.selectColumns.forEach((selCol) => { + const col = transformedColumns.find((c: ColumnInfo) => c.column_name === selCol); + if (col) { + joinColumns.push({ + ...col, + column_comment: col.column_comment ? `${col.column_comment} (${jt.joinTable})` : `${col.column_name} (${jt.joinTable})`, + }); + } + }); + } + } catch (error) { + console.error(`조인 테이블 ${jt.joinTable} 컬럼 로드 실패:`, error); + } + } + } + + // 메인 + 조인 컬럼 합치기 + setRightColumns([...mainColumns, ...joinColumns]); + console.log(`[loadJoinTableColumns] 우측 컬럼 로드 완료: 메인 ${mainColumns.length}개 + 조인 ${joinColumns.length}개`); + } catch (error) { + console.error("조인 테이블 컬럼 로드 실패:", error); + } + }; + + loadJoinTableColumns(); + }, [config.rightPanel?.tableName, config.rightPanel?.joinTables]); + // 테이블 선택 컴포넌트 const TableSelect: React.FC<{ value: string; @@ -388,13 +450,28 @@ export const SplitPanelLayout2ConfigPanel: React.FC void; placeholder: string; - }> = ({ columns, value, onValueChange, placeholder }) => { + showTableName?: boolean; // 테이블명 표시 여부 + tableName?: string; // 메인 테이블명 (조인 컬럼과 구분용) + }> = ({ columns, value, onValueChange, placeholder, showTableName = false, tableName }) => { // 현재 선택된 값의 라벨 찾기 const selectedColumn = columns.find((col) => col.column_name === value); const displayValue = selectedColumn ? selectedColumn.column_comment || selectedColumn.column_name : value || ""; + // 컬럼이 조인 테이블에서 온 것인지 확인 (column_comment에 괄호가 있으면 조인 테이블) + const isJoinColumn = (col: ColumnInfo) => col.column_comment?.includes("(") && col.column_comment?.includes(")"); + + // 컬럼 표시 텍스트 생성 + const getColumnDisplayText = (col: ColumnInfo) => { + const label = col.column_comment || col.column_name; + if (showTableName && tableName && !isJoinColumn(col)) { + // 메인 테이블 컬럼에 테이블명 추가 + return `${label} (${tableName})`; + } + return label; + }; + return ( onUpdate("joinType", value)} + > + + + + + LEFT JOIN (데이터 없어도 표시) + INNER JOIN (데이터 있어야만 표시) + + + + + {/* 조인 조건 */} +
+ +
+
+ + onUpdate("mainColumn", value)} + placeholder="메인 테이블 컬럼" + /> +
+
=
+
+ + onUpdate("joinColumn", value)} + placeholder="조인 테이블 컬럼" + /> +
+
+
+ + {/* 가져올 컬럼 선택 */} +
+
+ + +
+

+ 조인 테이블에서 표시할 컬럼들을 선택하세요 +

+
+ {(joinTable.selectColumns || []).map((col, colIndex) => ( +
+ { + const current = [...(joinTable.selectColumns || [])]; + current[colIndex] = value; + onUpdate("selectColumns", current); + }} + placeholder="컬럼 선택" + /> + +
+ ))} + {(joinTable.selectColumns || []).length === 0 && ( +
+ 가져올 컬럼을 추가하세요 +
+ )} +
+
+ + ); + }; + // 표시 컬럼 추가 const addDisplayColumn = (side: "left" | "right") => { const path = side === "left" ? "leftPanel.displayColumns" : "rightPanel.displayColumns"; @@ -440,14 +742,25 @@ export const SplitPanelLayout2ConfigPanel: React.FC { + const updateDisplayColumn = ( + side: "left" | "right", + index: number, + fieldOrPartial: keyof ColumnConfig | Partial, + value?: any + ) => { const path = side === "left" ? "leftPanel.displayColumns" : "rightPanel.displayColumns"; const currentColumns = side === "left" ? [...(config.leftPanel?.displayColumns || [])] : [...(config.rightPanel?.displayColumns || [])]; if (currentColumns[index]) { - currentColumns[index] = { ...currentColumns[index], [field]: value }; + if (typeof fieldOrPartial === "object") { + // 여러 필드를 한 번에 업데이트 + currentColumns[index] = { ...currentColumns[index], ...fieldOrPartial }; + } else { + // 단일 필드 업데이트 + currentColumns[index] = { ...currentColumns[index], [fieldOrPartial]: value }; + } updateConfig(path, currentColumns); } }; @@ -687,6 +1000,66 @@ export const SplitPanelLayout2ConfigPanel: React.FC + {/* 추가 조인 테이블 설정 */} +
+
+ + +
+

+ 다른 테이블을 조인하면 표시할 컬럼에서 해당 테이블의 컬럼도 선택할 수 있습니다. +

+
+ {(config.rightPanel?.joinTables || []).map((joinTable, index) => ( + { + const current = [...(config.rightPanel?.joinTables || [])]; + if (typeof fieldOrPartial === "object") { + // 여러 필드를 한 번에 업데이트 + current[index] = { ...current[index], ...fieldOrPartial }; + } else { + // 단일 필드 업데이트 + current[index] = { ...current[index], [fieldOrPartial]: value }; + } + updateConfig("rightPanel.joinTables", current); + }} + onRemove={() => { + const current = config.rightPanel?.joinTables || []; + updateConfig( + "rightPanel.joinTables", + current.filter((_, i) => i !== index) + ); + }} + /> + ))} +
+
+ {/* 표시 컬럼 */}
@@ -696,52 +1069,144 @@ export const SplitPanelLayout2ConfigPanel: React.FC
+

+ 테이블을 선택한 후 해당 테이블의 컬럼을 선택하세요. +

- {(config.rightPanel?.displayColumns || []).map((col, index) => ( -
-
- 컬럼 {index + 1} - + {(config.rightPanel?.displayColumns || []).map((col, index) => { + // 선택 가능한 테이블 목록: 메인 테이블 + 조인 테이블들 + const availableTables = [ + config.rightPanel?.tableName, + ...(config.rightPanel?.joinTables || []).map((jt) => jt.joinTable), + ].filter(Boolean) as string[]; + + // 선택된 테이블의 컬럼만 필터링 + const selectedSourceTable = col.sourceTable || config.rightPanel?.tableName; + const filteredColumns = rightColumns.filter((c) => { + // 조인 테이블 컬럼인지 확인 (column_comment에 테이블명 포함) + const isJoinColumn = c.column_comment?.includes("(") && c.column_comment?.includes(")"); + + if (selectedSourceTable === config.rightPanel?.tableName) { + // 메인 테이블 선택 시: 조인 컬럼 아닌 것만 + return !isJoinColumn; + } else { + // 조인 테이블 선택 시: 해당 테이블 컬럼만 + return c.column_comment?.includes(`(${selectedSourceTable})`); + } + }); + + // 테이블 라벨 가져오기 + const getTableLabel = (tableName: string) => { + const table = tables.find((t) => t.table_name === tableName); + return table?.table_comment || tableName; + }; + + return ( +
+
+ 컬럼 {index + 1} + +
+ + {/* 테이블 선택 */} +
+ + +
+ + {/* 컬럼 선택 */} +
+ + +
+ + {/* 표시 라벨 */} +
+ + updateDisplayColumn("right", index, "label", e.target.value)} + placeholder="라벨명 (미입력 시 컬럼명 사용)" + className="h-8 text-xs" + /> +
+ + {/* 표시 위치 */} +
+ + +
- updateDisplayColumn("right", index, "name", value)} - placeholder="컬럼 선택" - /> -
- - updateDisplayColumn("right", index, "label", e.target.value)} - placeholder="라벨명 (미입력 시 컬럼명 사용)" - className="h-8 text-xs" - /> -
-
- - -
-
- ))} + ); + })} {(config.rightPanel?.displayColumns || []).length === 0 && (
표시할 컬럼을 추가하세요 diff --git a/frontend/lib/registry/components/split-panel-layout2/types.ts b/frontend/lib/registry/components/split-panel-layout2/types.ts index 872563df..4c9f7cae 100644 --- a/frontend/lib/registry/components/split-panel-layout2/types.ts +++ b/frontend/lib/registry/components/split-panel-layout2/types.ts @@ -9,6 +9,7 @@ export interface ColumnConfig { name: string; // 컬럼명 label: string; // 표시 라벨 + sourceTable?: string; // 소스 테이블명 (메인 테이블 또는 조인 테이블) displayRow?: "name" | "info"; // 표시 위치 (name: 이름 행, info: 정보 행) width?: number; // 너비 (px) bold?: boolean; // 굵게 표시 @@ -94,6 +95,17 @@ export interface RightPanelConfig { actionButtons?: ActionButtonConfig[]; // 복수 액션 버튼 배열 primaryKeyColumn?: string; // 기본키 컬럼명 (수정/삭제용, 기본: id) emptyMessage?: string; // 데이터 없을 때 메시지 + + /** + * 추가 조인 테이블 설정 + * 메인 테이블에 다른 테이블을 JOIN하여 추가 정보를 함께 표시합니다. + * + * 사용 예시: + * - 메인 테이블: user_dept (부서-사용자 관계) + * - 조인 테이블: user_info (사용자 개인정보) + * - 결과: 부서별 사원 목록에 이메일, 전화번호 등 개인정보 함께 표시 + */ + joinTables?: JoinTableConfig[]; } /** @@ -104,6 +116,27 @@ export interface JoinConfig { rightColumn: string; // 우측 테이블의 조인 컬럼 } +/** + * 추가 조인 테이블 설정 + * 우측 패널의 메인 테이블에 다른 테이블을 JOIN하여 추가 컬럼을 가져옵니다. + * + * 예시: user_dept (메인) + user_info (조인) → 부서관계 + 개인정보 함께 표시 + * + * - joinTable: 조인할 테이블명 (예: user_info) + * - joinType: 조인 방식 (LEFT JOIN 권장) + * - mainColumn: 메인 테이블의 조인 컬럼 (예: user_id) + * - joinColumn: 조인 테이블의 조인 컬럼 (예: user_id) + * - selectColumns: 조인 테이블에서 가져올 컬럼들 (예: email, cell_phone) + */ +export interface JoinTableConfig { + joinTable: string; // 조인할 테이블명 + joinType: "LEFT" | "INNER"; // 조인 타입 (LEFT: 없어도 표시, INNER: 있어야만 표시) + mainColumn: string; // 메인 테이블의 조인 컬럼 + joinColumn: string; // 조인 테이블의 조인 컬럼 + selectColumns: string[]; // 조인 테이블에서 가져올 컬럼들 + alias?: string; // 테이블 별칭 (중복 컬럼명 구분용) +} + /** * 메인 설정 */