"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Check, ChevronsUpDown, Plus, X, Settings, Columns, MousePointerClick } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import type { SplitPanelLayout2Config, ColumnConfig, DataTransferField, JoinTableConfig, ColumnDisplayConfig, ActionButtonConfig, SearchColumnConfig, GroupingConfig, TabConfig, ButtonDataTransferConfig } from "./types"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ColumnConfigModal } from "./ColumnConfigModal"; import { ActionButtonConfigModal } from "./ActionButtonConfigModal"; // lodash set 대체 함수 const setPath = (obj: any, path: string, value: any): any => { const keys = path.split("."); const result = { ...obj }; let current = result; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; current[key] = current[key] ? { ...current[key] } : {}; current = current[key]; } current[keys[keys.length - 1]] = value; return result; }; interface SplitPanelLayout2ConfigPanelProps { config: SplitPanelLayout2Config; onChange: (config: SplitPanelLayout2Config) => void; } interface TableInfo { table_name: string; table_comment?: string; } interface ColumnInfo { column_name: string; data_type: string; column_comment?: string; } interface ScreenInfo { screen_id: number; screen_name: string; screen_code: string; } export const SplitPanelLayout2ConfigPanel: React.FC = ({ config, onChange }) => { // updateConfig 헬퍼 함수: 경로 기반으로 config를 업데이트 const updateConfig = useCallback( (path: string, value: any) => { console.log(`[SplitPanelLayout2ConfigPanel] updateConfig: ${path} =`, value); const newConfig = setPath(config, path, value); console.log("[SplitPanelLayout2ConfigPanel] newConfig:", newConfig); onChange(newConfig); }, [config, onChange], ); // 상태 const [tables, setTables] = useState([]); const [leftColumns, setLeftColumns] = useState([]); const [rightColumns, setRightColumns] = useState([]); const [screens, setScreens] = useState([]); const [tablesLoading, setTablesLoading] = useState(false); const [screensLoading, setScreensLoading] = useState(false); // Popover 상태 const [leftTableOpen, setLeftTableOpen] = useState(false); const [rightTableOpen, setRightTableOpen] = useState(false); const [leftModalOpen, setLeftModalOpen] = useState(false); const [rightModalOpen, setRightModalOpen] = useState(false); // 개별 수정 모달 화면 선택 Popover 상태 const [leftEditModalOpen, setLeftEditModalOpen] = useState(false); const [rightEditModalOpen, setRightEditModalOpen] = useState(false); // 컬럼 세부설정 모달 상태 (기존 - 하위 호환성) const [columnConfigModalOpen, setColumnConfigModalOpen] = useState(false); const [editingColumnIndex, setEditingColumnIndex] = useState(null); const [editingColumnConfig, setEditingColumnConfig] = useState({ displayType: "text", aggregate: { enabled: false, function: "DISTINCT" }, }); // 새로운 컬럼 설정 모달 상태 const [leftColumnModalOpen, setLeftColumnModalOpen] = useState(false); const [rightColumnModalOpen, setRightColumnModalOpen] = useState(false); // 액션 버튼 설정 모달 상태 const [leftActionButtonModalOpen, setLeftActionButtonModalOpen] = useState(false); const [rightActionButtonModalOpen, setRightActionButtonModalOpen] = useState(false); // 데이터 전달 설정 모달 상태 const [dataTransferModalOpen, setDataTransferModalOpen] = useState(false); const [editingTransferIndex, setEditingTransferIndex] = useState(null); // 테이블 목록 로드 const loadTables = useCallback(async () => { setTablesLoading(true); try { 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); } }, []); // 화면 목록 로드 const loadScreens = useCallback(async () => { setScreensLoading(true); try { // size를 크게 설정하여 모든 화면 가져오기 const response = await apiClient.get("/screen-management/screens?size=1000"); console.log("[loadScreens] API 응답:", response.data); // 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.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); } }, []); // 컬럼 목록 로드 const loadColumns = useCallback(async (tableName: string, side: "left" | "right") => { if (!tableName) return; try { 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(transformedColumns); } else { 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([]); } } }, []); // 초기 로드 useEffect(() => { loadTables(); loadScreens(); }, [loadTables, loadScreens]); // 테이블 변경 시 컬럼 로드 useEffect(() => { if (config.leftPanel?.tableName) { loadColumns(config.leftPanel.tableName, "left"); } }, [config.leftPanel?.tableName, loadColumns]); useEffect(() => { if (config.rightPanel?.tableName) { loadColumns(config.rightPanel.tableName, "right"); } }, [config.rightPanel?.tableName, loadColumns]); // 조인 테이블 컬럼도 우측 컬럼 목록에 추가 useEffect(() => { 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_name: `${jt.joinTable}.${col.column_name}`, 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; onValueChange: (value: string) => void; placeholder: string; open: boolean; onOpenChange: (open: boolean) => void; }> = ({ value, onValueChange, placeholder, open, onOpenChange }) => { const selectedTable = tables.find((t) => t.table_name === value); return ( {tables.length === 0 ? "테이블 목록을 불러오는 중..." : "검색 결과가 없습니다"} {tables.map((table, index) => ( { onValueChange(table.table_name); onOpenChange(false); }} > {table.table_comment || table.table_name} {table.table_name} ))} ); }; // 화면 선택 컴포넌트 const ScreenSelect: React.FC<{ value: number | undefined; onValueChange: (value: number | undefined) => void; placeholder: string; open: boolean; onOpenChange: (open: boolean) => void; }> = ({ 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<{ columns: ColumnInfo[]; value: string; onValueChange: (value: string) => void; placeholder: string; 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 ( ); }; // 검색 가능한 컬럼 선택 컴포넌트 (Combobox 패턴) const SearchableColumnSelect: React.FC<{ columns: ColumnInfo[]; value: string; onValueChange: (value: string) => void; placeholder: string; disabled?: boolean; }> = ({ columns, value, onValueChange, placeholder, disabled = false }) => { const [open, setOpen] = useState(false); // 현재 선택된 값의 라벨 찾기 const selectedColumn = columns.find((col) => col.column_name === value); const displayValue = selectedColumn ? `${selectedColumn.column_comment || selectedColumn.column_name} (${selectedColumn.column_name})` : ""; return ( 검색 결과가 없습니다 {columns.map((col, index) => ( { onValueChange(col.column_name); setOpen(false); }} className="text-xs" >
{col.column_comment || col.column_name} {col.column_name}
))}
); }; // 조인 테이블 아이템 컴포넌트 const JoinTableItem: React.FC<{ index: number; joinTable: JoinTableConfig; tables: TableInfo[]; mainTableColumns: ColumnInfo[]; onUpdate: (field: keyof JoinTableConfig | Partial, value?: any) => void; onRemove: () => void; }> = ({ index, joinTable, tables, mainTableColumns, onUpdate, onRemove }) => { const [joinTableColumns, setJoinTableColumns] = useState([]); const [joinTableOpen, setJoinTableOpen] = useState(false); // 조인 테이블 선택 시 해당 테이블의 컬럼 로드 useEffect(() => { const loadJoinTableColumns = async () => { if (!joinTable.joinTable) { setJoinTableColumns([]); return; } try { const response = await apiClient.get(`/table-management/tables/${joinTable.joinTable}/columns?size=200`); 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; } 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 ?? "", })); setJoinTableColumns(transformedColumns); } catch (error) { console.error("조인 테이블 컬럼 로드 실패:", error); setJoinTableColumns([]); } }; loadJoinTableColumns(); }, [joinTable.joinTable]); const selectedTable = tables.find((t) => t.table_name === joinTable.joinTable); return (
조인 {index + 1}
{/* 조인 테이블 선택 */}
검색 결과가 없습니다 {tables.map((table) => ( { // cmdk가 value를 소문자로 변환하므로 직접 table.table_name 사용 // 여러 필드를 한 번에 업데이트 (연속 호출 시 덮어쓰기 방지) onUpdate({ joinTable: table.table_name, selectColumns: [], // 테이블 변경 시 선택 컬럼 초기화 }); setJoinTableOpen(false); }} className="text-xs" > {table.table_comment || table.table_name} {table.table_name} ))}
{/* 조인 타입 */}
{/* 조인 조건 */}
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"; const currentColumns = side === "left" ? config.leftPanel?.displayColumns || [] : config.rightPanel?.displayColumns || []; // 기본 테이블 설정 (메인 테이블) const defaultTable = side === "left" ? config.leftPanel?.tableName : config.rightPanel?.tableName; updateConfig(path, [...currentColumns, { name: "", label: "", sourceTable: defaultTable || "" }]); }; // 표시 컬럼 삭제 const removeDisplayColumn = (side: "left" | "right", index: number) => { const path = side === "left" ? "leftPanel.displayColumns" : "rightPanel.displayColumns"; const currentColumns = side === "left" ? config.leftPanel?.displayColumns || [] : config.rightPanel?.displayColumns || []; updateConfig( path, currentColumns.filter((_, i) => i !== index), ); }; // 표시 컬럼 업데이트 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]) { if (typeof fieldOrPartial === "object") { // 여러 필드를 한 번에 업데이트 currentColumns[index] = { ...currentColumns[index], ...fieldOrPartial }; } else { // 단일 필드 업데이트 currentColumns[index] = { ...currentColumns[index], [fieldOrPartial]: value }; } updateConfig(path, currentColumns); } }; // 컬럼 세부설정 저장 const handleSaveColumnConfig = () => { if (editingColumnIndex === null) return; const currentColumns = [...(config.leftPanel?.displayColumns || [])]; if (currentColumns[editingColumnIndex]) { currentColumns[editingColumnIndex] = { ...currentColumns[editingColumnIndex], displayConfig: editingColumnConfig, }; updateConfig("leftPanel.displayColumns", currentColumns); } setColumnConfigModalOpen(false); setEditingColumnIndex(null); }; // 좌측 컬럼 설정 모달 저장 핸들러 const handleLeftColumnConfigSave = (columnConfig: { displayColumns: ColumnConfig[]; searchColumns: SearchColumnConfig[]; grouping: GroupingConfig; showSearch: boolean; }) => { // 모든 변경사항을 한 번에 적용 (개별 updateConfig 호출 시 마지막 값만 적용되는 문제 방지) const newLeftPanel = { ...config.leftPanel, displayColumns: columnConfig.displayColumns, searchColumns: columnConfig.searchColumns, grouping: columnConfig.grouping, showSearch: columnConfig.showSearch, }; onChange({ ...config, leftPanel: newLeftPanel }); }; // 좌측 액션 버튼 설정 모달 저장 핸들러 const handleLeftActionButtonSave = (buttons: ActionButtonConfig[]) => { updateConfig("leftPanel.actionButtons", buttons); }; // 우측 컬럼 설정 모달 저장 핸들러 const handleRightColumnConfigSave = (columnConfig: { displayColumns: ColumnConfig[]; searchColumns: SearchColumnConfig[]; grouping: GroupingConfig; showSearch: boolean; }) => { // 모든 변경사항을 한 번에 적용 (개별 updateConfig 호출 시 마지막 값만 적용되는 문제 방지) const newRightPanel = { ...config.rightPanel, displayColumns: columnConfig.displayColumns, searchColumns: columnConfig.searchColumns, showSearch: columnConfig.showSearch, }; onChange({ ...config, rightPanel: newRightPanel }); }; // 우측 액션 버튼 설정 모달 저장 핸들러 const handleRightActionButtonSave = (buttons: ActionButtonConfig[]) => { updateConfig("rightPanel.actionButtons", buttons); }; return (
{/* 좌측 패널 설정 */}

좌측 패널 설정 (마스터)

updateConfig("leftPanel.title", e.target.value)} placeholder="부서" className="h-9 text-sm" />
updateConfig("leftPanel.tableName", value)} placeholder="테이블 선택" open={leftTableOpen} onOpenChange={setLeftTableOpen} />
{/* 표시 모드 설정 */}
{/* 컬럼 설정 버튼 */}

표시 컬럼 {(config.leftPanel?.displayColumns || []).length}개 {config.leftPanel?.grouping?.enabled && " | 그룹핑 사용"} {config.leftPanel?.showSearch && " | 검색 사용"}

{/* 액션 버튼 설정 */}

{(config.leftPanel?.actionButtons || []).length > 0 ? `${(config.leftPanel?.actionButtons || []).length}개 버튼` : config.leftPanel?.showAddButton ? "기본 추가 버튼" : "없음"}

{/* 개별 수정/삭제 버튼 (좌측) */}

각 행에 표시되는 수정/삭제 버튼

updateConfig("leftPanel.showEditButton", checked)} />
{/* 수정 버튼이 켜져 있을 때 모달 화면 선택 */} {config.leftPanel?.showEditButton && (
updateConfig("leftPanel.editModalScreenId", value)} placeholder="수정 모달 화면 선택" open={leftEditModalOpen} onOpenChange={setLeftEditModalOpen} />
)}
updateConfig("leftPanel.showDeleteButton", checked)} />
{(config.leftPanel?.showEditButton || config.leftPanel?.showDeleteButton) && (
updateConfig("leftPanel.primaryKeyColumn", value)} placeholder="기본키 컬럼 선택 (기본: id)" />

수정/삭제 시 레코드 식별에 사용

)}
{/* 탭 설정 (좌측) */}
{ if (checked) { updateConfig("leftPanel.tabConfig", { enabled: true, mode: "manual", showCount: true, }); } else { updateConfig("leftPanel.tabConfig.enabled", false); } }} />
{config.leftPanel?.tabConfig?.enabled && (
{/* 그룹핑이 설정되어 있지 않으면 안내 메시지 */} {!config.leftPanel?.grouping?.enabled ? (

탭 기능을 사용하려면 컬럼 설정에서 그룹핑을 먼저 활성화해주세요.

) : ( <>

그룹핑/집계된 컬럼 중에서 선택

updateConfig("leftPanel.tabConfig.showCount", checked)} />
)}
)}
{/* 추가 조인 테이블 설정 (좌측) */}

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

{(config.leftPanel?.joinTables || []).length > 0 && (
{(config.leftPanel?.joinTables || []).map((joinTable, index) => ( { const current = [...(config.leftPanel?.joinTables || [])]; if (typeof fieldOrPartial === "object") { current[index] = { ...current[index], ...fieldOrPartial }; } else { current[index] = { ...current[index], [fieldOrPartial]: value }; } updateConfig("leftPanel.joinTables", current); }} onRemove={() => { const current = config.leftPanel?.joinTables || []; updateConfig( "leftPanel.joinTables", current.filter((_, i) => i !== index), ); }} /> ))}
)}
{/* 우측 패널 설정 */}

우측 패널 설정 (상세)

updateConfig("rightPanel.title", e.target.value)} placeholder="사원" className="h-9 text-sm" />
updateConfig("rightPanel.tableName", value)} placeholder="테이블 선택" open={rightTableOpen} onOpenChange={setRightTableOpen} />
{/* 표시 모드 설정 */}
{/* 추가 조인 테이블 설정 */}

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

{(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), ); }} /> ))}
{/* 컬럼 설정 버튼 */}

표시 컬럼 {(config.rightPanel?.displayColumns || []).length}개 {config.rightPanel?.showSearch && " | 검색 사용"}

{/* 액션 버튼 설정 */}

{(config.rightPanel?.actionButtons || []).length > 0 ? `${(config.rightPanel?.actionButtons || []).length}개 버튼` : config.rightPanel?.showAddButton ? "기본 추가 버튼" : "없음"}

{/* 개별 수정/삭제 버튼 */}

각 행에 표시되는 수정/삭제 버튼

updateConfig("rightPanel.showEditButton", checked)} />
{/* 수정 버튼이 켜져 있을 때 모달 화면 선택 */} {config.rightPanel?.showEditButton && (
updateConfig("rightPanel.editModalScreenId", value)} placeholder="수정 모달 화면 선택" open={rightEditModalOpen} onOpenChange={setRightEditModalOpen} />
)}
updateConfig("rightPanel.showDeleteButton", checked)} />
{(config.rightPanel?.showEditButton || config.rightPanel?.showDeleteButton) && (
updateConfig("rightPanel.primaryKeyColumn", value)} placeholder="기본키 컬럼 선택 (기본: id)" />

수정/삭제 시 레코드 식별에 사용

)} {/* 수정 시 메인 테이블 조회 설정 */} {config.rightPanel?.showEditButton && (
{ if (checked) { updateConfig("rightPanel.mainTableForEdit", { tableName: "", linkColumn: { mainColumn: "", subColumn: "" }, }); } else { updateConfig("rightPanel.mainTableForEdit", undefined); } }} />

우측 패널이 서브 테이블일 때, 수정 모달에 메인 테이블 데이터도 함께 전달

{config.rightPanel?.mainTableForEdit && (
updateConfig("rightPanel.mainTableForEdit.tableName", e.target.value)} placeholder="예: user_info" className="h-7 text-xs mt-1" />
updateConfig("rightPanel.mainTableForEdit.linkColumn.mainColumn", e.target.value)} placeholder="예: user_id" className="h-7 text-xs mt-1" />
updateConfig("rightPanel.mainTableForEdit.linkColumn.subColumn", e.target.value)} placeholder="예: user_id" className="h-7 text-xs mt-1" />
)}
)}
{/* 탭 설정 (우측) */}
{ if (checked) { updateConfig("rightPanel.tabConfig", { enabled: true, mode: "manual", showCount: true, }); } else { updateConfig("rightPanel.tabConfig.enabled", false); } }} />
{config.rightPanel?.tabConfig?.enabled && (
updateConfig("rightPanel.tabConfig.tabSourceColumn", value)} placeholder="컬럼 선택..." disabled={!config.rightPanel?.tableName} />

선택한 컬럼의 고유값으로 탭이 생성됩니다

updateConfig("rightPanel.tabConfig.showCount", checked)} />
)}
{/* 기타 옵션들 - 접힌 상태로 표시 */}
추가 옵션
{/* 카드 모드 전용 옵션 */} {(config.rightPanel?.displayMode || "card") === "card" && (

라벨: 값 형식으로 표시

updateConfig("rightPanel.showLabels", checked)} />
)} {/* 체크박스 표시 */}

항목 선택 기능 활성화

updateConfig("rightPanel.showCheckbox", checked)} />
{/* 연결 설정 */}

연결 설정 (조인)

{/* 설명 */}

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

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

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

복합키: 여러 컬럼으로 조인 (예: item_code + lot_number)

{/* 복합키가 설정된 경우 */} {(config.joinConfig?.keys || []).length > 0 ? ( <> {(config.joinConfig?.keys || []).map((key, index) => (
조인 키 {index + 1}
{ const newKeys = [...(config.joinConfig?.keys || [])]; newKeys[index] = { ...newKeys[index], leftColumn: value }; updateConfig("joinConfig.keys", newKeys); }} placeholder="좌측 컬럼" />
{ const newKeys = [...(config.joinConfig?.keys || [])]; newKeys[index] = { ...newKeys[index], rightColumn: value }; updateConfig("joinConfig.keys", newKeys); }} placeholder="우측 컬럼" />
))} ) : ( /* 단일키 (하위 호환성) */ <>
updateConfig("joinConfig.leftColumn", value)} placeholder="조인 컬럼 선택" />
updateConfig("joinConfig.rightColumn", value)} placeholder="조인 컬럼 선택" />
)}
{/* 데이터 전달 설정 */}

데이터 전달 설정

버튼 클릭 시 모달에 전달할 데이터를 설정합니다.

{/* 좌측 패널 버튼 */} {(config.leftPanel?.actionButtons || []).length > 0 && (
{(config.leftPanel?.actionButtons || []).map((btn) => { // 이 버튼에 대한 데이터 전달 설정 찾기 const transferConfig = (config.buttonDataTransfers || []).find( (t) => t.targetPanel === "left" && t.targetButtonId === btn.id ); const transferIndex = transferConfig ? (config.buttonDataTransfers || []).findIndex((t) => t.id === transferConfig.id) : -1; return (
{btn.label} ({btn.action || "add"})
{transferConfig && transferConfig.fields.length > 0 && (
{transferConfig.fields.length}개 필드: {transferConfig.fields.map((f) => f.sourceColumn).join(", ")}
)}
); })}
)} {/* 우측 패널 버튼 */} {(config.rightPanel?.actionButtons || []).length > 0 && (
{(config.rightPanel?.actionButtons || []).map((btn) => { // 이 버튼에 대한 데이터 전달 설정 찾기 const transferConfig = (config.buttonDataTransfers || []).find( (t) => t.targetPanel === "right" && t.targetButtonId === btn.id ); const transferIndex = transferConfig ? (config.buttonDataTransfers || []).findIndex((t) => t.id === transferConfig.id) : -1; return (
{btn.label} ({btn.action || "add"})
{transferConfig && transferConfig.fields.length > 0 && (
{transferConfig.fields.length}개 필드: {transferConfig.fields.map((f) => f.sourceColumn).join(", ")}
)}
); })}
)} {/* 버튼이 없을 때 */} {(config.leftPanel?.actionButtons || []).length === 0 && (config.rightPanel?.actionButtons || []).length === 0 && (
설정된 버튼이 없습니다. 먼저 좌측/우측 패널에서 액션 버튼을 추가하세요.
)}
{/* 데이터 전달 상세 설정 모달 */} { if (editingTransferIndex !== null) { const current = [...(config.buttonDataTransfers || [])]; current[editingTransferIndex] = { ...current[editingTransferIndex], fields }; updateConfig("buttonDataTransfers", current); } setDataTransferModalOpen(false); setEditingTransferIndex(null); }} /> {/* 레이아웃 설정 */}

레이아웃 설정

updateConfig("splitRatio", parseInt(e.target.value) || 30)} min={10} max={90} className="h-9 text-sm" />
updateConfig("resizable", checked)} />
updateConfig("autoLoad", checked)} />
{/* 컬럼 세부설정 모달 */} 컬럼 세부설정 {editingColumnIndex !== null && config.leftPanel?.displayColumns?.[editingColumnIndex] && `${config.leftPanel.displayColumns[editingColumnIndex].label || config.leftPanel.displayColumns[editingColumnIndex].name} 컬럼의 표시 방식을 설정합니다.`}
{/* 표시 방식 */}

배지는 여러 값을 태그 형태로 나란히 표시합니다

{/* 집계 설정 */}

그룹핑 시 값을 집계합니다

{ setEditingColumnConfig((prev) => ({ ...prev, aggregate: { enabled: checked, function: prev.aggregate?.function || "DISTINCT", }, })); }} />
{editingColumnConfig.aggregate?.enabled && (

{editingColumnConfig.aggregate?.function === "DISTINCT" ? "중복을 제거하고 고유한 값들만 배열로 표시합니다" : "값의 개수를 숫자로 표시합니다"}

)}
{/* 좌측 패널 컬럼 설정 모달 */} {/* 좌측 패널 액션 버튼 설정 모달 */} {/* 우측 패널 컬럼 설정 모달 */} {/* 우측 패널 액션 버튼 설정 모달 */}
); }; // 데이터 전달 상세 설정 모달 컴포넌트 interface DataTransferDetailModalProps { open: boolean; onOpenChange: (open: boolean) => void; config: SplitPanelLayout2Config; editingIndex: number | null; leftColumns: ColumnInfo[]; onSave: (fields: DataTransferField[]) => void; } const DataTransferDetailModal: React.FC = ({ open, onOpenChange, config, editingIndex, leftColumns, onSave, }) => { const [fields, setFields] = useState([]); // 모달 열릴 때 현재 설정 로드 useEffect(() => { if (open && editingIndex !== null) { const transfer = config.buttonDataTransfers?.[editingIndex]; setFields(transfer?.fields || []); } }, [open, editingIndex, config.buttonDataTransfers]); const transfer = editingIndex !== null ? config.buttonDataTransfers?.[editingIndex] : null; // 소스 컬럼: 대상 패널에 따라 반대 패널의 컬럼 사용 // left 패널 버튼이면 -> left 패널 데이터를 모달에 전달 (좌측 컬럼이 소스) // right 패널 버튼이면 -> left 패널 데이터를 모달에 전달 (좌측 컬럼이 소스, 또는 right 컬럼) const sourceColumns = transfer?.targetPanel === "right" ? leftColumns : leftColumns; const addField = () => { setFields([...fields, { sourceColumn: "", targetColumn: "" }]); }; const removeField = (index: number) => { setFields(fields.filter((_, i) => i !== index)); }; const updateField = (index: number, key: keyof DataTransferField, value: string) => { const newFields = [...fields]; newFields[index] = { ...newFields[index], [key]: value }; setFields(newFields); }; const targetButton = transfer?.targetPanel === "left" ? config.leftPanel?.actionButtons?.find((btn) => btn.id === transfer.targetButtonId) : config.rightPanel?.actionButtons?.find((btn) => btn.id === transfer?.targetButtonId); return ( 데이터 전달 상세 설정 {transfer?.targetPanel === "left" ? "좌측" : "우측"} 패널의 "{targetButton?.label || "버튼"}" 클릭 시 모달에 전달할 데이터를 설정합니다.

소스 컬럼: 선택된 항목에서 가져올 컬럼

타겟 컬럼: 모달 폼에 전달할 필드명

{fields.map((field, index) => (
->
updateField(index, "targetColumn", e.target.value)} placeholder="타겟 컬럼" className="h-8 text-xs" />
))} {fields.length === 0 && (
전달할 필드가 없습니다. 추가 버튼을 클릭하세요.
)}
); }; export default SplitPanelLayout2ConfigPanel;