From 294c61e0e3eec7c26be0fd66fa7ca6a8a594b579 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Wed, 3 Dec 2025 18:43:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(split-panel-layout2):=20=EB=B3=B5=EC=88=98?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EC=BB=AC=EB=9F=BC=20=EC=A7=80=EC=9B=90?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SearchColumnConfig 타입 추가 (types.ts) - 좌측/우측 패널 모두 여러 검색 컬럼 설정 가능 - ConfigPanel에 검색 컬럼 추가/삭제 UI 구현 - 검색 시 OR 조건으로 여러 컬럼 동시 검색 - 기존 searchColumn 단일 설정과 하위 호환성 유지 --- .../SplitPanelLayout2Component.tsx | 185 ++++-- .../SplitPanelLayout2ConfigPanel.tsx | 621 +++++++++++++----- .../components/split-panel-layout2/types.ts | 15 +- 3 files changed, 587 insertions(+), 234 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx index be14038f..8a9d73a7 100644 --- a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx +++ b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2Component.tsx @@ -317,13 +317,20 @@ export const SplitPanelLayout2Component: React.FC { if (!leftSearchTerm) return leftData; - const searchColumn = config.leftPanel?.searchColumn; - if (!searchColumn) return leftData; + // 복수 검색 컬럼 지원 (searchColumns 우선, 없으면 searchColumn 사용) + const searchColumns = config.leftPanel?.searchColumns?.map((c) => c.columnName).filter(Boolean) || []; + const legacyColumn = config.leftPanel?.searchColumn; + const columnsToSearch = searchColumns.length > 0 ? searchColumns : legacyColumn ? [legacyColumn] : []; + + if (columnsToSearch.length === 0) return leftData; const filterRecursive = (items: any[]): any[] => { return items.filter((item) => { - const value = String(item[searchColumn] || "").toLowerCase(); - const matches = value.includes(leftSearchTerm.toLowerCase()); + // 여러 컬럼 중 하나라도 매칭되면 포함 + const matches = columnsToSearch.some((col) => { + const value = String(item[col] || "").toLowerCase(); + return value.includes(leftSearchTerm.toLowerCase()); + }); if (item.children?.length > 0) { const filteredChildren = filterRecursive(item.children); @@ -338,19 +345,26 @@ export const SplitPanelLayout2Component: React.FC { if (!rightSearchTerm) return rightData; - const searchColumn = config.rightPanel?.searchColumn; - if (!searchColumn) return rightData; + // 복수 검색 컬럼 지원 (searchColumns 우선, 없으면 searchColumn 사용) + const searchColumns = config.rightPanel?.searchColumns?.map((c) => c.columnName).filter(Boolean) || []; + const legacyColumn = config.rightPanel?.searchColumn; + const columnsToSearch = searchColumns.length > 0 ? searchColumns : legacyColumn ? [legacyColumn] : []; + + if (columnsToSearch.length === 0) return rightData; return rightData.filter((item) => { - const value = String(item[searchColumn] || "").toLowerCase(); - return value.includes(rightSearchTerm.toLowerCase()); + // 여러 컬럼 중 하나라도 매칭되면 포함 + return columnsToSearch.some((col) => { + const value = String(item[col] || "").toLowerCase(); + return value.includes(rightSearchTerm.toLowerCase()); + }); }); - }, [rightData, rightSearchTerm, config.rightPanel?.searchColumn]); + }, [rightData, rightSearchTerm, config.rightPanel?.searchColumns, config.rightPanel?.searchColumn]); // 리사이즈 핸들러 const handleResizeStart = useCallback((e: React.MouseEvent) => { @@ -451,15 +465,19 @@ export const SplitPanelLayout2Component: React.FC + col.displayRow === "name" || (!col.displayRow && idx === 0) + ); + const infoRowColumns = displayColumns.filter((col, idx) => + col.displayRow === "info" || (!col.displayRow && idx > 0) + ); - const primaryValue = primaryColumn - ? item[primaryColumn.name] + // 이름 행의 첫 번째 값 (주요 표시 값) + const primaryValue = nameRowColumns[0] + ? item[nameRowColumns[0].name] : Object.values(item).find((v) => typeof v === "string" && v.length > 0); - const secondaryValue = secondaryColumn ? item[secondaryColumn.name] : null; return (
@@ -496,12 +514,38 @@ export const SplitPanelLayout2Component: React.FC -
- {primaryValue || "이름 없음"} + {/* 이름 행 (Name Row) */} +
+ + {primaryValue || "이름 없음"} + + {/* 이름 행의 추가 컬럼들 (배지 스타일) */} + {nameRowColumns.slice(1).map((col, idx) => { + const value = item[col.name]; + if (!value) return null; + return ( + + {formatValue(value, col.format)} + + ); + })}
- {secondaryValue && ( -
- {secondaryValue} + {/* 정보 행 (Info Row) */} + {infoRowColumns.length > 0 && ( +
+ {infoRowColumns.map((col, idx) => { + const value = item[col.name]; + if (!value) return null; + return ( + + {formatValue(value, col.format)} + + ); + }).filter(Boolean).reduce((acc: React.ReactNode[], curr, idx) => { + if (idx > 0) acc.push(|); + acc.push(curr); + return acc; + }, [])}
)}
@@ -521,53 +565,72 @@ export const SplitPanelLayout2Component: React.FC { const displayColumns = config.rightPanel?.displayColumns || []; - // 첫 번째 컬럼을 이름으로 사용 - const nameColumn = displayColumns[0]; - const name = nameColumn ? item[nameColumn.name] : "이름 없음"; - - // 나머지 컬럼들 - const otherColumns = displayColumns.slice(1); + // displayRow 설정에 따라 컬럼 분류 + // displayRow가 "name"이면 이름 행, "info"이면 정보 행 (기본값: 첫 번째는 name, 나머지는 info) + const nameRowColumns = displayColumns.filter((col, idx) => + col.displayRow === "name" || (!col.displayRow && idx === 0) + ); + const infoRowColumns = displayColumns.filter((col, idx) => + col.displayRow === "info" || (!col.displayRow && idx > 0) + ); return ( - - + +
- {/* 이름 */} -
- {name} - {otherColumns[0] && ( - - {item[otherColumns[0].name]} - - )} -
+ {/* 이름 행 (Name Row) */} + {nameRowColumns.length > 0 && ( +
+ {nameRowColumns.map((col, idx) => { + const value = item[col.name]; + if (!value && idx > 0) return null; + + // 첫 번째 컬럼은 굵게 표시 + if (idx === 0) { + return ( + + {formatValue(value, col.format) || "이름 없음"} + + ); + } + // 나머지는 배지 스타일 + return ( + + {formatValue(value, col.format)} + + ); + })} +
+ )} - {/* 상세 정보 */} -
- {otherColumns.slice(1).map((col, idx) => { - const value = item[col.name]; - if (!value) return null; + {/* 정보 행 (Info Row) */} + {infoRowColumns.length > 0 && ( +
+ {infoRowColumns.map((col, idx) => { + const value = item[col.name]; + if (!value) return null; - // 아이콘 결정 - let icon = null; - const colName = col.name.toLowerCase(); - if (colName.includes("tel") || colName.includes("phone")) { - icon = tel; - } else if (colName.includes("email")) { - icon = @; - } else if (colName.includes("sabun") || colName.includes("id")) { - icon = ID; - } + // 아이콘 결정 + let icon = null; + const colName = col.name.toLowerCase(); + if (colName.includes("tel") || colName.includes("phone")) { + icon = tel; + } else if (colName.includes("email")) { + icon = @; + } else if (colName.includes("sabun") || colName.includes("id")) { + icon = ID; + } - return ( - - {icon} - {formatValue(value, col.format)} - - ); - })} -
+ return ( + + {icon} + {formatValue(value, col.format)} + + ); + })} +
+ )}
{/* 액션 버튼 */} diff --git a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx index 878ddb12..db3638cb 100644 --- a/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout2/SplitPanelLayout2ConfigPanel.tsx @@ -98,13 +98,35 @@ export const SplitPanelLayout2ConfigPanel: React.FC { setTablesLoading(true); try { - const response = await apiClient.get("/table/list?userLang=KR"); - const tableList = response.data?.data || response.data || []; - if (Array.isArray(tableList)) { - setTables(tableList); + const response = await apiClient.get("/table-management/tables"); + console.log("[loadTables] API 응답:", response.data); + + let tableList: any[] = []; + if (response.data?.success && Array.isArray(response.data?.data)) { + tableList = response.data.data; + } else if (Array.isArray(response.data?.data)) { + tableList = response.data.data; + } else if (Array.isArray(response.data)) { + tableList = response.data; + } + + console.log("[loadTables] 추출된 테이블 목록:", tableList); + + if (tableList.length > 0) { + // 백엔드에서 카멜케이스(tableName)로 반환하므로 둘 다 처리 + const transformedTables = tableList.map((t: any) => ({ + table_name: t.tableName ?? t.table_name ?? t.name ?? "", + table_comment: t.displayName ?? t.table_comment ?? t.description ?? "", + })); + console.log("[loadTables] 변환된 테이블 목록:", transformedTables); + setTables(transformedTables); + } else { + console.warn("[loadTables] 테이블 목록이 비어있습니다"); + setTables([]); } } catch (error) { console.error("테이블 목록 로드 실패:", error); + setTables([]); } finally { setTablesLoading(false); } @@ -114,20 +136,38 @@ export const SplitPanelLayout2ConfigPanel: React.FC { setScreensLoading(true); try { - const response = await apiClient.get("/screen/list"); + // size를 크게 설정하여 모든 화면 가져오기 + const response = await apiClient.get("/screen-management/screens?size=1000"); console.log("[loadScreens] API 응답:", response.data); - const screenList = response.data?.data || response.data || []; - if (Array.isArray(screenList)) { + + // API 응답 구조: { success, data: [...], total, page, size } + let screenList: any[] = []; + if (response.data?.success && Array.isArray(response.data?.data)) { + screenList = response.data.data; + } else if (Array.isArray(response.data?.data)) { + screenList = response.data.data; + } else if (Array.isArray(response.data)) { + screenList = response.data; + } + + console.log("[loadScreens] 추출된 화면 목록:", screenList); + + if (screenList.length > 0) { + // 백엔드에서 카멜케이스(screenId, screenName)로 반환하므로 둘 다 처리 const transformedScreens = screenList.map((s: any) => ({ - screen_id: s.screen_id || s.id, - screen_name: s.screen_name || s.name, - screen_code: s.screen_code || s.code || "", + screen_id: s.screenId ?? s.screen_id ?? s.id, + screen_name: s.screenName ?? s.screen_name ?? s.name ?? `화면 ${s.screenId || s.screen_id || s.id}`, + screen_code: s.screenCode ?? s.screen_code ?? s.code ?? "", })); console.log("[loadScreens] 변환된 화면 목록:", transformedScreens); setScreens(transformedScreens); + } else { + console.warn("[loadScreens] 화면 목록이 비어있습니다"); + setScreens([]); } } catch (error) { console.error("화면 목록 로드 실패:", error); + setScreens([]); } finally { setScreensLoading(false); } @@ -137,17 +177,52 @@ export const SplitPanelLayout2ConfigPanel: React.FC { if (!tableName) return; try { - const response = await apiClient.get(`/table/${tableName}/columns`); - const columnList = response.data?.data || response.data || []; - if (Array.isArray(columnList)) { + const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=200`); + console.log(`[loadColumns] ${side} API 응답:`, response.data); + + // API 응답 구조: { success, data: { columns: [...], total, page, totalPages } } + let columnList: any[] = []; + if (response.data?.success && response.data?.data?.columns) { + columnList = response.data.data.columns; + } else if (Array.isArray(response.data?.data?.columns)) { + columnList = response.data.data.columns; + } else if (Array.isArray(response.data?.data)) { + columnList = response.data.data; + } else if (Array.isArray(response.data)) { + columnList = response.data; + } + + console.log(`[loadColumns] ${side} 추출된 컬럼 목록:`, columnList); + + if (columnList.length > 0) { + // 백엔드에서 카멜케이스(columnName)로 반환하므로 둘 다 처리 + 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 ?? "", + })); + console.log(`[loadColumns] ${side} 변환된 컬럼 목록:`, transformedColumns); + if (side === "left") { - setLeftColumns(columnList); + setLeftColumns(transformedColumns); } else { - setRightColumns(columnList); + setRightColumns(transformedColumns); + } + } else { + console.warn(`[loadColumns] ${side} 컬럼 목록이 비어있습니다`); + if (side === "left") { + setLeftColumns([]); + } else { + setRightColumns([]); } } } catch (error) { console.error(`${side} 컬럼 목록 로드 실패:`, error); + if (side === "left") { + setLeftColumns([]); + } else { + setRightColumns([]); + } } }, []); @@ -177,59 +252,63 @@ export const SplitPanelLayout2ConfigPanel: React.FC void; - }> = ({ value, onValueChange, placeholder, open, onOpenChange }) => ( - - - - - - - - - 테이블이 없습니다 - - {tables.map((table) => ( - { - onValueChange(selectedValue); - onOpenChange(false); - }} - > - - - {table.table_comment || table.table_name} - {table.table_name} - - - ))} - - - - - - ); + }> = ({ value, onValueChange, placeholder, open, onOpenChange }) => { + const selectedTable = tables.find((t) => t.table_name === value); + + return ( + + + + + + + + + + {tables.length === 0 ? "테이블 목록을 불러오는 중..." : "검색 결과가 없습니다"} + + + {tables.map((table, index) => ( + { + onValueChange(selectedValue); + onOpenChange(false); + }} + > + + + {table.table_comment || table.table_name} + {table.table_name} + + + ))} + + + + + + ); + }; // 화면 선택 컴포넌트 const ScreenSelect: React.FC<{ @@ -238,64 +317,70 @@ export const SplitPanelLayout2ConfigPanel: React.FC void; - }> = ({ value, onValueChange, placeholder, open, onOpenChange }) => ( - - - - - - - - - 화면이 없습니다 - - {screens.map((screen, index) => ( - { - const screenId = parseInt(selectedValue.split("-")[0]); - console.log("[ScreenSelect] onSelect:", { selectedValue, screenId, screen }); - onValueChange(screenId); - onOpenChange(false); - }} - className="flex items-center" - > -
- - - {screen.screen_name} - {screen.screen_code} - -
-
- ))} -
-
-
-
-
- ); + }> = ({ value, onValueChange, placeholder, open, onOpenChange }) => { + const selectedScreen = screens.find((s) => s.screen_id === value); + + return ( + + + + + + + + + + {screens.length === 0 ? "화면 목록을 불러오는 중..." : "검색 결과가 없습니다"} + + + {screens.map((screen, index) => ( + { + const screenId = parseInt(selectedValue.split("-")[0]); + console.log("[ScreenSelect] onSelect:", { selectedValue, screenId, screen }); + onValueChange(isNaN(screenId) ? undefined : screenId); + onOpenChange(false); + }} + className="flex items-center" + > +
+ + + {screen.screen_name} + {screen.screen_code} + +
+
+ ))} +
+
+
+
+
+ ); + }; // 컬럼 선택 컴포넌트 const ColumnSelect: React.FC<{ @@ -303,20 +388,36 @@ export const SplitPanelLayout2ConfigPanel: React.FC void; placeholder: string; - }> = ({ columns, value, onValueChange, placeholder }) => ( - - ); + }> = ({ columns, value, onValueChange, placeholder }) => { + // 현재 선택된 값의 라벨 찾기 + const selectedColumn = columns.find((col) => col.column_name === value); + const displayValue = selectedColumn + ? selectedColumn.column_comment || selectedColumn.column_name + : value || ""; + + return ( + + ); + }; // 표시 컬럼 추가 const addDisplayColumn = (side: "left" | "right") => { @@ -405,30 +506,52 @@ export const SplitPanelLayout2ConfigPanel: React.FC
-
+
{(config.leftPanel?.displayColumns || []).map((col, index) => ( -
+
+
+ 컬럼 {index + 1} + +
updateDisplayColumn("left", index, "name", value)} - placeholder="컬럼" + placeholder="컬럼 선택" /> - updateDisplayColumn("left", index, "label", e.target.value)} - placeholder="라벨" - className="h-9 text-sm flex-1" - /> - +
+ + +
))} + {(config.leftPanel?.displayColumns || []).length === 0 && ( +
+ 표시할 컬럼을 추가하세요 +
+ )}
@@ -440,6 +563,61 @@ export const SplitPanelLayout2ConfigPanel: React.FC
+ {config.leftPanel?.showSearch && ( +
+
+ + +
+
+ {(config.leftPanel?.searchColumns || []).map((searchCol, index) => ( +
+ { + const current = [...(config.leftPanel?.searchColumns || [])]; + current[index] = { ...current[index], columnName: value }; + updateConfig("leftPanel.searchColumns", current); + }} + placeholder="컬럼 선택" + /> + +
+ ))} + {(config.leftPanel?.searchColumns || []).length === 0 && ( +
+ 검색할 컬럼을 추가하세요 +
+ )} +
+
+ )} +
-
+
{(config.rightPanel?.displayColumns || []).map((col, index) => ( -
+
+
+ 컬럼 {index + 1} + +
updateDisplayColumn("right", index, "name", value)} - placeholder="컬럼" + placeholder="컬럼 선택" /> - updateDisplayColumn("right", index, "label", e.target.value)} - placeholder="라벨" - className="h-9 text-sm flex-1" - /> - +
+ + +
))} + {(config.rightPanel?.displayColumns || []).length === 0 && ( +
+ 표시할 컬럼을 추가하세요 +
+ )}
@@ -540,6 +740,61 @@ export const SplitPanelLayout2ConfigPanel: React.FC
+ {config.rightPanel?.showSearch && ( +
+
+ + +
+
+ {(config.rightPanel?.searchColumns || []).map((searchCol, index) => ( +
+ { + const current = [...(config.rightPanel?.searchColumns || [])]; + current[index] = { ...current[index], columnName: value }; + updateConfig("rightPanel.searchColumns", current); + }} + placeholder="컬럼 선택" + /> + +
+ ))} + {(config.rightPanel?.searchColumns || []).length === 0 && ( +
+ 검색할 컬럼을 추가하세요 +
+ )} +
+
+ )} +
-

연결 설정 (조인)

+

연결 설정 (조인)

+ + {/* 설명 */} +
+

좌측 패널 선택 시 우측 패널 데이터 필터링

+

좌측에서 항목을 선택하면 좌측 조인 컬럼의 값으로 우측 테이블을 필터링합니다.

+

예: 부서(dept_code) 선택 시 해당 부서의 사원만 표시

+
@@ -604,19 +866,31 @@ export const SplitPanelLayout2ConfigPanel: React.FC
-

데이터 전달 설정

+

데이터 전달 설정

+ {/* 설명 */} +
+

우측 패널 추가 버튼 클릭 시 모달로 데이터 전달

+

좌측에서 선택한 항목의 값을 모달 폼에 자동으로 채워줍니다.

+

예: dept_code를 모달의 dept_code 필드에 자동 입력

+
+
{(config.dataTransferFields || []).map((field, index) => ( -
+
필드 {index + 1} -
@@ -640,6 +914,11 @@ export const SplitPanelLayout2ConfigPanel: React.FC
))} + {(config.dataTransferFields || []).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 ec0f61b5..a5813600 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; // 표시 라벨 + displayRow?: "name" | "info"; // 표시 위치 (name: 이름 행, info: 정보 행) width?: number; // 너비 (px) bold?: boolean; // 굵게 표시 format?: { @@ -30,6 +31,14 @@ export interface DataTransferField { label?: string; // 표시용 라벨 } +/** + * 검색 컬럼 설정 + */ +export interface SearchColumnConfig { + columnName: string; // 검색 대상 컬럼명 + label?: string; // 표시 라벨 (없으면 컬럼명 사용) +} + /** * 좌측 패널 설정 */ @@ -37,7 +46,8 @@ export interface LeftPanelConfig { title?: string; // 패널 제목 tableName: string; // 테이블명 displayColumns: ColumnConfig[]; // 표시할 컬럼들 - searchColumn?: string; // 검색 대상 컬럼 + searchColumn?: string; // 검색 대상 컬럼 (단일, 하위 호환성) + searchColumns?: SearchColumnConfig[]; // 검색 대상 컬럼들 (복수) showSearch?: boolean; // 검색 표시 여부 showAddButton?: boolean; // 추가 버튼 표시 addButtonLabel?: string; // 추가 버튼 라벨 @@ -57,7 +67,8 @@ export interface RightPanelConfig { title?: string; // 패널 제목 tableName: string; // 테이블명 displayColumns: ColumnConfig[]; // 표시할 컬럼들 - searchColumn?: string; // 검색 대상 컬럼 + searchColumn?: string; // 검색 대상 컬럼 (단일, 하위 호환성) + searchColumns?: SearchColumnConfig[]; // 검색 대상 컬럼들 (복수) showSearch?: boolean; // 검색 표시 여부 showAddButton?: boolean; // 추가 버튼 표시 addButtonLabel?: string; // 추가 버튼 라벨