"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 } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import type { SplitPanelLayout2Config, ColumnConfig, DataTransferField } from "./types"; // 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); // 테이블 목록 로드 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]); // 테이블 선택 컴포넌트 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(selectedValue); 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; }> = ({ 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") => { const path = side === "left" ? "leftPanel.displayColumns" : "rightPanel.displayColumns"; const currentColumns = side === "left" ? config.leftPanel?.displayColumns || [] : config.rightPanel?.displayColumns || []; updateConfig(path, [...currentColumns, { name: "", label: "" }]); }; // 표시 컬럼 삭제 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, field: keyof ColumnConfig, 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 }; updateConfig(path, currentColumns); } }; // 데이터 전달 필드 추가 const addDataTransferField = () => { const currentFields = config.dataTransferFields || []; updateConfig("dataTransferFields", [...currentFields, { sourceColumn: "", targetColumn: "" }]); }; // 데이터 전달 필드 삭제 const removeDataTransferField = (index: number) => { const currentFields = config.dataTransferFields || []; updateConfig("dataTransferFields", currentFields.filter((_, i) => i !== index)); }; // 데이터 전달 필드 업데이트 const updateDataTransferField = (index: number, field: keyof DataTransferField, value: string) => { const currentFields = [...(config.dataTransferFields || [])]; if (currentFields[index]) { currentFields[index] = { ...currentFields[index], [field]: value }; updateConfig("dataTransferFields", currentFields); } }; 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 || []).map((col, index) => (
컬럼 {index + 1}
updateDisplayColumn("left", index, "name", value)} placeholder="컬럼 선택" />
))} {(config.leftPanel?.displayColumns || []).length === 0 && (
표시할 컬럼을 추가하세요
)}
updateConfig("leftPanel.showSearch", checked)} />
{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 && (
검색할 컬럼을 추가하세요
)}
)}
updateConfig("leftPanel.showAddButton", checked)} />
{config.leftPanel?.showAddButton && ( <>
updateConfig("leftPanel.addButtonLabel", e.target.value)} placeholder="추가" className="h-9 text-sm" />
updateConfig("leftPanel.addModalScreenId", value)} placeholder="모달 화면 선택" open={leftModalOpen} onOpenChange={setLeftModalOpen} />
)}
{/* 우측 패널 설정 */}

우측 패널 설정 (상세)

updateConfig("rightPanel.title", e.target.value)} placeholder="사원" className="h-9 text-sm" />
updateConfig("rightPanel.tableName", value)} placeholder="테이블 선택" open={rightTableOpen} onOpenChange={setRightTableOpen} />
{/* 표시 컬럼 */}
{(config.rightPanel?.displayColumns || []).map((col, index) => (
컬럼 {index + 1}
updateDisplayColumn("right", index, "name", value)} placeholder="컬럼 선택" />
))} {(config.rightPanel?.displayColumns || []).length === 0 && (
표시할 컬럼을 추가하세요
)}
updateConfig("rightPanel.showSearch", checked)} />
{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 && (
검색할 컬럼을 추가하세요
)}
)}
updateConfig("rightPanel.showAddButton", checked)} />
{config.rightPanel?.showAddButton && ( <>
updateConfig("rightPanel.addButtonLabel", e.target.value)} placeholder="추가" className="h-9 text-sm" />
updateConfig("rightPanel.addModalScreenId", value)} placeholder="모달 화면 선택" open={rightModalOpen} onOpenChange={setRightModalOpen} />
)}
{/* 연결 설정 */}

연결 설정 (조인)

{/* 설명 */}

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

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

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

updateConfig("joinConfig.leftColumn", value)} placeholder="조인 컬럼 선택" />
updateConfig("joinConfig.rightColumn", value)} placeholder="조인 컬럼 선택" />
{/* 데이터 전달 설정 */}

데이터 전달 설정

{/* 설명 */}

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

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

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

{(config.dataTransferFields || []).map((field, index) => (
필드 {index + 1}
updateDataTransferField(index, "sourceColumn", value)} placeholder="소스 컬럼" />
updateDataTransferField(index, "targetColumn", e.target.value)} placeholder="모달에서 사용할 필드명" className="h-9 text-sm" />
))} {(config.dataTransferFields || []).length === 0 && (
전달할 필드를 추가하세요
)}
{/* 레이아웃 설정 */}

레이아웃 설정

updateConfig("splitRatio", parseInt(e.target.value) || 30)} min={10} max={90} className="h-9 text-sm" />
updateConfig("resizable", checked)} />
updateConfig("autoLoad", checked)} />
); }; export default SplitPanelLayout2ConfigPanel;