From 2a52f25c10ce792c12fb91759e73e4cc5402b20c Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 13 Nov 2025 17:42:20 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8B=AC=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=ED=9B=84=20=EB=B6=80=EB=AA=A8=20=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=9E=90=EB=8F=99=20=EC=83=88?= =?UTF-8?q?=EB=A1=9C=EA=B3=A0=EC=B9=A8=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScreenModal에 onRefresh 콜백 추가하여 refreshTable 이벤트 발송 - InteractiveScreenViewerDynamic에 onRefresh, onFlowRefresh prop 추가 및 하위 컴포넌트로 전달 - TableListComponent에 refreshTable 이벤트 리스너 추가 - SplitPanelLayoutComponent에 refreshTable 이벤트 리스너 추가하여 좌/우측 패널 모두 새로고침 - 모달에서 데이터 저장 시 부모 화면의 모든 테이블 컴포넌트가 자동으로 새로고침되도록 개선 변경된 파일: - frontend/components/common/ScreenModal.tsx - frontend/components/screen/InteractiveScreenViewerDynamic.tsx - frontend/lib/registry/components/table-list/TableListComponent.tsx - frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx --- frontend/components/common/ScreenModal.tsx | 5 +++++ .../screen/InteractiveScreenViewerDynamic.tsx | 12 ++++++++--- .../SplitPanelLayoutComponent.tsx | 20 +++++++++++++++++++ .../table-list/TableListComponent.tsx | 16 +++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 609c2b43..4bbb913e 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -414,6 +414,11 @@ export const ScreenModal: React.FC = ({ className }) => { return newFormData; }); }} + onRefresh={() => { + // 부모 화면의 테이블 새로고침 이벤트 발송 + console.log("🔄 모달에서 부모 화면 테이블 새로고침 이벤트 발송"); + window.dispatchEvent(new CustomEvent("refreshTable")); + }} screenInfo={{ id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 639ffa0a..a2ab1522 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -40,6 +40,8 @@ interface InteractiveScreenViewerProps { tableName?: string; }; onSave?: () => Promise; + onRefresh?: () => void; + onFlowRefresh?: () => void; } export const InteractiveScreenViewerDynamic: React.FC = ({ @@ -50,6 +52,8 @@ export const InteractiveScreenViewerDynamic: React.FC { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { userName, user } = useAuth(); @@ -324,9 +328,11 @@ export const InteractiveScreenViewerDynamic: React.FC { - // 테이블 컴포넌트는 자체적으로 loadData 호출 - }} + onRefresh={onRefresh || (() => { + // 부모로부터 전달받은 onRefresh 또는 기본 동작 + console.log("🔄 InteractiveScreenViewerDynamic onRefresh 호출"); + })} + onFlowRefresh={onFlowRefresh} onClose={() => { // buttonActions.ts가 이미 처리함 }} diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index ced60e9e..955bae86 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -962,6 +962,26 @@ export const SplitPanelLayoutComponent: React.FC // eslint-disable-next-line react-hooks/exhaustive-deps }, [leftFilters]); + // 🆕 전역 테이블 새로고침 이벤트 리스너 + useEffect(() => { + const handleRefreshTable = () => { + if (!isDesignMode) { + console.log("🔄 [SplitPanel] refreshTable 이벤트 수신 - 데이터 새로고침"); + loadLeftData(); + // 선택된 항목이 있으면 우측 패널도 새로고침 + if (selectedLeftItem) { + loadRightData(selectedLeftItem); + } + } + }; + + window.addEventListener("refreshTable", handleRefreshTable); + + return () => { + window.removeEventListener("refreshTable", handleRefreshTable); + }; + }, [isDesignMode, loadLeftData, loadRightData, selectedLeftItem]); + // 리사이저 드래그 핸들러 const handleMouseDown = (e: React.MouseEvent) => { if (!resizable) return; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 16734059..8b397881 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -1766,6 +1766,22 @@ export const TableListComponent: React.FC = ({ } }, [tableConfig.refreshInterval, isDesignMode]); + // 🆕 전역 테이블 새로고침 이벤트 리스너 + useEffect(() => { + const handleRefreshTable = () => { + if (tableConfig.selectedTable && !isDesignMode) { + console.log("🔄 [TableList] refreshTable 이벤트 수신 - 데이터 새로고침"); + setRefreshTrigger((prev) => prev + 1); + } + }; + + window.addEventListener("refreshTable", handleRefreshTable); + + return () => { + window.removeEventListener("refreshTable", handleRefreshTable); + }; + }, [tableConfig.selectedTable, isDesignMode]); + // 초기 컬럼 너비 측정 (한 번만) useEffect(() => { if (!hasInitializedWidths.current && visibleColumns.length > 0) { From 702b506665b7d1a5f41076bbf13f5b1ec9f2290a Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 13 Nov 2025 17:52:33 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20=EB=B6=84=ED=95=A0=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EA=B0=92=EC=9D=84=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=EB=A1=9C=20=ED=91=9C=EC=8B=9C=ED=95=98?= =?UTF-8?q?=EB=8A=94=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 - 좌측/우측 패널 테이블의 카테고리 타입 컬럼 매핑 로드 - formatCellValue 함수 추가하여 카테고리 코드를 라벨+배지로 변환 - 좌측 패널 테이블 렌더링 부분(그룹화/일반)에 formatCellValue 적용 - 우측 패널 테이블 렌더링 부분에 formatCellValue 적용 - Badge 컴포넌트 import 및 카테고리별 색상 표시 변경 사항: - 카테고리 매핑 상태 추가 (leftCategoryMappings, rightCategoryMappings) - API 클라이언트 import 추가 - 카테고리 값 조회 API 호출 useEffect 2개 추가 - 셀 값 포맷팅 함수 formatCellValue 추가 - 모든 테이블 td 렌더링에서 String(value) → formatCellValue() 적용 --- .../SplitPanelLayoutComponent.tsx | 155 +++++++++++++++++- 1 file changed, 146 insertions(+), 9 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 955bae86..a7e7e874 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -6,11 +6,13 @@ import { SplitPanelLayoutConfig } from "./types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { useToast } from "@/hooks/use-toast"; import { tableTypeApi } from "@/lib/api/screen"; +import { apiClient } from "@/lib/api/client"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { useTableOptions } from "@/contexts/TableOptionsContext"; @@ -64,6 +66,8 @@ export const SplitPanelLayoutComponent: React.FC const [expandedItems, setExpandedItems] = useState>(new Set()); // 펼쳐진 항목들 const [leftColumnLabels, setLeftColumnLabels] = useState>({}); // 좌측 컬럼 라벨 const [rightColumnLabels, setRightColumnLabels] = useState>({}); // 우측 컬럼 라벨 + const [leftCategoryMappings, setLeftCategoryMappings] = useState>>({}); // 좌측 카테고리 매핑 + const [rightCategoryMappings, setRightCategoryMappings] = useState>>({}); // 우측 카테고리 매핑 const { toast } = useToast(); // 추가 모달 상태 @@ -241,6 +245,39 @@ export const SplitPanelLayoutComponent: React.FC })); }, [leftData, leftGrouping]); + // 셀 값 포맷팅 함수 (카테고리 타입 처리) + const formatCellValue = useCallback(( + columnName: string, + value: any, + categoryMappings: Record> + ) => { + if (value === null || value === undefined) return "-"; + + // 카테고리 매핑이 있는지 확인 + const mapping = categoryMappings[columnName]; + if (mapping && mapping[String(value)]) { + const categoryData = mapping[String(value)]; + const displayLabel = categoryData.label || String(value); + const displayColor = categoryData.color || "#64748b"; + + // 배지로 표시 + return ( + + {displayLabel} + + ); + } + + // 일반 값 + return String(value); + }, []); + // 좌측 데이터 로드 const loadLeftData = useCallback(async () => { const leftTableName = componentConfig.leftPanel?.tableName; @@ -523,6 +560,112 @@ export const SplitPanelLayoutComponent: React.FC loadRightTableColumns(); }, [componentConfig.rightPanel?.tableName, isDesignMode]); + // 좌측 테이블 카테고리 매핑 로드 + useEffect(() => { + const loadLeftCategoryMappings = async () => { + const leftTableName = componentConfig.leftPanel?.tableName; + if (!leftTableName || isDesignMode) return; + + try { + // 1. 컬럼 메타 정보 조회 + const columnsResponse = await tableTypeApi.getColumns(leftTableName); + const categoryColumns = columnsResponse.filter( + (col: any) => col.inputType === "category" + ); + + if (categoryColumns.length === 0) { + setLeftCategoryMappings({}); + return; + } + + // 2. 각 카테고리 컬럼에 대한 값 조회 + const mappings: Record> = {}; + + for (const col of categoryColumns) { + const columnName = col.columnName || col.column_name; + try { + const response = await apiClient.get( + `/table-type/category-values/${leftTableName}/${columnName}` + ); + + if (response.data.success && response.data.data) { + const valueMap: Record = {}; + response.data.data.forEach((item: any) => { + valueMap[item.value_code || item.valueCode] = { + label: item.value_label || item.valueLabel, + color: item.color, + }; + }); + mappings[columnName] = valueMap; + console.log(`✅ 좌측 카테고리 매핑 로드 [${columnName}]:`, valueMap); + } + } catch (error) { + console.error(`좌측 카테고리 값 조회 실패 [${columnName}]:`, error); + } + } + + setLeftCategoryMappings(mappings); + } catch (error) { + console.error("좌측 카테고리 매핑 로드 실패:", error); + } + }; + + loadLeftCategoryMappings(); + }, [componentConfig.leftPanel?.tableName, isDesignMode]); + + // 우측 테이블 카테고리 매핑 로드 + useEffect(() => { + const loadRightCategoryMappings = async () => { + const rightTableName = componentConfig.rightPanel?.tableName; + if (!rightTableName || isDesignMode) return; + + try { + // 1. 컬럼 메타 정보 조회 + const columnsResponse = await tableTypeApi.getColumns(rightTableName); + const categoryColumns = columnsResponse.filter( + (col: any) => col.inputType === "category" + ); + + if (categoryColumns.length === 0) { + setRightCategoryMappings({}); + return; + } + + // 2. 각 카테고리 컬럼에 대한 값 조회 + const mappings: Record> = {}; + + for (const col of categoryColumns) { + const columnName = col.columnName || col.column_name; + try { + const response = await apiClient.get( + `/table-type/category-values/${rightTableName}/${columnName}` + ); + + if (response.data.success && response.data.data) { + const valueMap: Record = {}; + response.data.data.forEach((item: any) => { + valueMap[item.value_code || item.valueCode] = { + label: item.value_label || item.valueLabel, + color: item.color, + }; + }); + mappings[columnName] = valueMap; + console.log(`✅ 우측 카테고리 매핑 로드 [${columnName}]:`, valueMap); + } + } catch (error) { + console.error(`우측 카테고리 값 조회 실패 [${columnName}]:`, error); + } + } + + setRightCategoryMappings(mappings); + } catch (error) { + console.error("우측 카테고리 매핑 로드 실패:", error); + } + }; + + loadRightCategoryMappings(); + }, [componentConfig.rightPanel?.tableName, isDesignMode]); + // 항목 펼치기/접기 토글 const toggleExpand = useCallback((itemId: any) => { setExpandedItems(prev => { @@ -1193,9 +1336,7 @@ export const SplitPanelLayoutComponent: React.FC className="whitespace-nowrap px-3 py-2 text-sm text-gray-900" style={{ textAlign: col.align || "left" }} > - {item[col.name] !== null && item[col.name] !== undefined - ? String(item[col.name]) - : "-"} + {formatCellValue(col.name, item[col.name], leftCategoryMappings)} ))} @@ -1246,9 +1387,7 @@ export const SplitPanelLayoutComponent: React.FC className="whitespace-nowrap px-3 py-2 text-sm text-gray-900" style={{ textAlign: col.align || "left" }} > - {item[col.name] !== null && item[col.name] !== undefined - ? String(item[col.name]) - : "-"} + {formatCellValue(col.name, item[col.name], leftCategoryMappings)} ))} @@ -1619,9 +1758,7 @@ export const SplitPanelLayoutComponent: React.FC className="whitespace-nowrap px-3 py-2 text-sm text-gray-900" style={{ textAlign: col.align || "left" }} > - {item[col.name] !== null && item[col.name] !== undefined - ? String(item[col.name]) - : "-"} + {formatCellValue(col.name, item[col.name], rightCategoryMappings)} ))} {!isDesignMode && ( From 075869c89c75774cd1267b577afa66590a232971 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 13 Nov 2025 17:55:10 +0900 Subject: [PATCH 03/12] =?UTF-8?q?fix:=20=EB=B6=84=ED=95=A0=20=ED=8C=A8?= =?UTF-8?q?=EB=84=90=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20API=20?= =?UTF-8?q?=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 잘못된 API 경로 수정 - 이전: /api/table-type/category-values/{tableName}/{columnName} - 수정: /api/table-categories/{tableName}/{columnName}/values - 백엔드 라우트와 일치하도록 변경 (app.ts에서 /api/table-categories로 마운트됨) - 좌측/우측 패널 카테고리 매핑 로드 API 호출 경로 수정 --- .../SplitPanelLayoutComponent.tsx | 1256 +++++++++-------- 1 file changed, 669 insertions(+), 587 deletions(-) diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index a7e7e874..b7364a4b 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -7,13 +7,31 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; -import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react"; +import { + Plus, + Search, + GripVertical, + Loader2, + ChevronDown, + ChevronUp, + Save, + ChevronRight, + Pencil, + Trash2, +} from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { useToast } from "@/hooks/use-toast"; import { tableTypeApi } from "@/lib/api/screen"; import { apiClient } from "@/lib/api/client"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import { TableFilter, ColumnVisibility } from "@/types/table-options"; @@ -66,21 +84,25 @@ export const SplitPanelLayoutComponent: React.FC const [expandedItems, setExpandedItems] = useState>(new Set()); // 펼쳐진 항목들 const [leftColumnLabels, setLeftColumnLabels] = useState>({}); // 좌측 컬럼 라벨 const [rightColumnLabels, setRightColumnLabels] = useState>({}); // 우측 컬럼 라벨 - const [leftCategoryMappings, setLeftCategoryMappings] = useState>>({}); // 좌측 카테고리 매핑 - const [rightCategoryMappings, setRightCategoryMappings] = useState>>({}); // 우측 카테고리 매핑 + const [leftCategoryMappings, setLeftCategoryMappings] = useState< + Record> + >({}); // 좌측 카테고리 매핑 + const [rightCategoryMappings, setRightCategoryMappings] = useState< + Record> + >({}); // 우측 카테고리 매핑 const { toast } = useToast(); // 추가 모달 상태 const [showAddModal, setShowAddModal] = useState(false); const [addModalPanel, setAddModalPanel] = useState<"left" | "right" | "left-item" | null>(null); const [addModalFormData, setAddModalFormData] = useState>({}); - + // 수정 모달 상태 const [showEditModal, setShowEditModal] = useState(false); const [editModalPanel, setEditModalPanel] = useState<"left" | "right" | null>(null); const [editModalItem, setEditModalItem] = useState(null); const [editModalFormData, setEditModalFormData] = useState>({}); - + // 삭제 확인 모달 상태 const [showDeleteModal, setShowDeleteModal] = useState(false); const [deleteModalPanel, setDeleteModalPanel] = useState<"left" | "right" | null>(null); @@ -121,51 +143,54 @@ export const SplitPanelLayoutComponent: React.FC }; // 계층 구조 빌드 함수 (트리 구조 유지) - const buildHierarchy = useCallback((items: any[]): any[] => { - if (!items || items.length === 0) return []; - - const itemAddConfig = componentConfig.leftPanel?.itemAddConfig; - if (!itemAddConfig) return items.map(item => ({ ...item, children: [] })); // 계층 설정이 없으면 평면 목록 - - const { sourceColumn, parentColumn } = itemAddConfig; - if (!sourceColumn || !parentColumn) return items.map(item => ({ ...item, children: [] })); + const buildHierarchy = useCallback( + (items: any[]): any[] => { + if (!items || items.length === 0) return []; - // ID를 키로 하는 맵 생성 - const itemMap = new Map(); - const rootItems: any[] = []; + const itemAddConfig = componentConfig.leftPanel?.itemAddConfig; + if (!itemAddConfig) return items.map((item) => ({ ...item, children: [] })); // 계층 설정이 없으면 평면 목록 - // 모든 항목을 맵에 추가하고 children 배열 초기화 - items.forEach(item => { - const id = item[sourceColumn]; - itemMap.set(id, { ...item, children: [], level: 0 }); - }); + const { sourceColumn, parentColumn } = itemAddConfig; + if (!sourceColumn || !parentColumn) return items.map((item) => ({ ...item, children: [] })); - // 부모-자식 관계 설정 - items.forEach(item => { - const id = item[sourceColumn]; - const parentId = item[parentColumn]; - const currentItem = itemMap.get(id); + // ID를 키로 하는 맵 생성 + const itemMap = new Map(); + const rootItems: any[] = []; - if (!currentItem) return; + // 모든 항목을 맵에 추가하고 children 배열 초기화 + items.forEach((item) => { + const id = item[sourceColumn]; + itemMap.set(id, { ...item, children: [], level: 0 }); + }); - if (!parentId || parentId === null || parentId === '') { - // 최상위 항목 - rootItems.push(currentItem); - } else { - // 부모가 있는 항목 - const parentItem = itemMap.get(parentId); - if (parentItem) { - currentItem.level = parentItem.level + 1; - parentItem.children.push(currentItem); - } else { - // 부모를 찾을 수 없으면 최상위로 처리 + // 부모-자식 관계 설정 + items.forEach((item) => { + const id = item[sourceColumn]; + const parentId = item[parentColumn]; + const currentItem = itemMap.get(id); + + if (!currentItem) return; + + if (!parentId || parentId === null || parentId === "") { + // 최상위 항목 rootItems.push(currentItem); + } else { + // 부모가 있는 항목 + const parentItem = itemMap.get(parentId); + if (parentItem) { + currentItem.level = parentItem.level + 1; + parentItem.children.push(currentItem); + } else { + // 부모를 찾을 수 없으면 최상위로 처리 + rootItems.push(currentItem); + } } - } - }); + }); - return rootItems; - }, [componentConfig.leftPanel?.itemAddConfig]); + return rootItems; + }, + [componentConfig.leftPanel?.itemAddConfig], + ); // 🔧 사용자 ID 가져오기 const { userId: currentUserId } = useAuth(); @@ -173,13 +198,13 @@ export const SplitPanelLayoutComponent: React.FC // 🔄 필터를 searchValues 형식으로 변환 const searchValues = useMemo(() => { if (!leftFilters || leftFilters.length === 0) return {}; - + const values: Record = {}; - leftFilters.forEach(filter => { - if (filter.value !== undefined && filter.value !== null && filter.value !== '') { + leftFilters.forEach((filter) => { + if (filter.value !== undefined && filter.value !== null && filter.value !== "") { values[filter.columnName] = { value: filter.value, - operator: filter.operator || 'contains', + operator: filter.operator || "contains", }; } }); @@ -189,55 +214,57 @@ export const SplitPanelLayoutComponent: React.FC // 🔄 컬럼 가시성 및 순서 처리 const visibleLeftColumns = useMemo(() => { const displayColumns = componentConfig.leftPanel?.columns || []; - + if (displayColumns.length === 0) return []; - + let columns = displayColumns; - + // columnVisibility가 있으면 가시성 적용 if (leftColumnVisibility.length > 0) { - const visibilityMap = new Map(leftColumnVisibility.map(cv => [cv.columnName, cv.visible])); + const visibilityMap = new Map(leftColumnVisibility.map((cv) => [cv.columnName, cv.visible])); columns = columns.filter((col: any) => { - const colName = typeof col === 'string' ? col : (col.name || col.columnName); + const colName = typeof col === "string" ? col : col.name || col.columnName; return visibilityMap.get(colName) !== false; }); } - + // 🔧 컬럼 순서 적용 if (leftColumnOrder.length > 0) { const orderMap = new Map(leftColumnOrder.map((name, index) => [name, index])); columns = [...columns].sort((a, b) => { - const aName = typeof a === 'string' ? a : (a.name || a.columnName); - const bName = typeof b === 'string' ? b : (b.name || b.columnName); + const aName = typeof a === "string" ? a : a.name || a.columnName; + const bName = typeof b === "string" ? b : b.name || b.columnName; const aIndex = orderMap.get(aName) ?? 999; const bIndex = orderMap.get(bName) ?? 999; return aIndex - bIndex; }); } - + return columns; }, [componentConfig.leftPanel?.columns, leftColumnVisibility, leftColumnOrder]); // 🔄 데이터 그룹화 const groupedLeftData = useMemo(() => { if (!leftGrouping || leftGrouping.length === 0 || leftData.length === 0) return []; - + const grouped = new Map(); - + leftData.forEach((item) => { // 각 그룹 컬럼의 값을 조합하여 그룹 키 생성 - const groupKey = leftGrouping.map(col => { - const value = item[col]; - // null/undefined 처리 - return value === null || value === undefined ? "(비어있음)" : String(value); - }).join(" > "); - + const groupKey = leftGrouping + .map((col) => { + const value = item[col]; + // null/undefined 처리 + return value === null || value === undefined ? "(비어있음)" : String(value); + }) + .join(" > "); + if (!grouped.has(groupKey)) { grouped.set(groupKey, []); } grouped.get(groupKey)!.push(item); }); - + return Array.from(grouped.entries()).map(([key, items]) => ({ groupKey: key, items, @@ -246,37 +273,40 @@ export const SplitPanelLayoutComponent: React.FC }, [leftData, leftGrouping]); // 셀 값 포맷팅 함수 (카테고리 타입 처리) - const formatCellValue = useCallback(( - columnName: string, - value: any, - categoryMappings: Record> - ) => { - if (value === null || value === undefined) return "-"; + const formatCellValue = useCallback( + ( + columnName: string, + value: any, + categoryMappings: Record>, + ) => { + if (value === null || value === undefined) return "-"; - // 카테고리 매핑이 있는지 확인 - const mapping = categoryMappings[columnName]; - if (mapping && mapping[String(value)]) { - const categoryData = mapping[String(value)]; - const displayLabel = categoryData.label || String(value); - const displayColor = categoryData.color || "#64748b"; + // 카테고리 매핑이 있는지 확인 + const mapping = categoryMappings[columnName]; + if (mapping && mapping[String(value)]) { + const categoryData = mapping[String(value)]; + const displayLabel = categoryData.label || String(value); + const displayColor = categoryData.color || "#64748b"; - // 배지로 표시 - return ( - - {displayLabel} - - ); - } + // 배지로 표시 + return ( + + {displayLabel} + + ); + } - // 일반 값 - return String(value); - }, []); + // 일반 값 + return String(value); + }, + [], + ); // 좌측 데이터 로드 const loadLeftData = useCallback(async () => { @@ -287,8 +317,7 @@ export const SplitPanelLayoutComponent: React.FC try { // 🎯 필터 조건을 API에 전달 (entityJoinApi 사용) const filters = Object.keys(searchValues).length > 0 ? searchValues : undefined; - - + const result = await entityJoinApi.getTableDataWithJoins(leftTableName, { page: 1, size: 100, @@ -296,18 +325,17 @@ export const SplitPanelLayoutComponent: React.FC enableEntityJoin: true, // 엔티티 조인 활성화 dataFilter: componentConfig.leftPanel?.dataFilter, // 🆕 데이터 필터 전달 }); - - + // 가나다순 정렬 (좌측 패널의 표시 컬럼 기준) const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; if (leftColumn && result.data.length > 0) { result.data.sort((a, b) => { - const aValue = String(a[leftColumn] || ''); - const bValue = String(b[leftColumn] || ''); - return aValue.localeCompare(bValue, 'ko-KR'); + const aValue = String(a[leftColumn] || ""); + const bValue = String(b[leftColumn] || ""); + return aValue.localeCompare(bValue, "ko-KR"); }); } - + // 계층 구조 빌드 const hierarchicalData = buildHierarchy(result.data); setLeftData(hierarchicalData); @@ -321,7 +349,14 @@ export const SplitPanelLayoutComponent: React.FC } finally { setIsLoadingLeft(false); } - }, [componentConfig.leftPanel?.tableName, componentConfig.rightPanel?.relation?.leftColumn, isDesignMode, toast, buildHierarchy, searchValues]); + }, [ + componentConfig.leftPanel?.tableName, + componentConfig.rightPanel?.relation?.leftColumn, + isDesignMode, + toast, + buildHierarchy, + searchValues, + ]); // 우측 데이터 로드 const loadRightData = useCallback( @@ -410,27 +445,30 @@ export const SplitPanelLayoutComponent: React.FC ); // 🔧 컬럼의 고유값 가져오기 함수 - const getLeftColumnUniqueValues = useCallback(async (columnName: string) => { - const leftTableName = componentConfig.leftPanel?.tableName; - if (!leftTableName || leftData.length === 0) return []; + const getLeftColumnUniqueValues = useCallback( + async (columnName: string) => { + const leftTableName = componentConfig.leftPanel?.tableName; + if (!leftTableName || leftData.length === 0) return []; - // 현재 로드된 데이터에서 고유값 추출 - const uniqueValues = new Set(); - - leftData.forEach((item) => { - const value = item[columnName]; - if (value !== null && value !== undefined && value !== '') { - // _name 필드 우선 사용 (category/entity type) - const displayValue = item[`${columnName}_name`] || value; - uniqueValues.add(String(displayValue)); - } - }); - - return Array.from(uniqueValues).map(value => ({ - value: value, - label: value, - })); - }, [componentConfig.leftPanel?.tableName, leftData]); + // 현재 로드된 데이터에서 고유값 추출 + const uniqueValues = new Set(); + + leftData.forEach((item) => { + const value = item[columnName]; + if (value !== null && value !== undefined && value !== "") { + // _name 필드 우선 사용 (category/entity type) + const displayValue = item[`${columnName}_name`] || value; + uniqueValues.add(String(displayValue)); + } + }); + + return Array.from(uniqueValues).map((value) => ({ + value: value, + label: value, + })); + }, + [componentConfig.leftPanel?.tableName, leftData], + ); // 좌측 테이블 등록 (Context에 등록) useEffect(() => { @@ -440,11 +478,13 @@ export const SplitPanelLayoutComponent: React.FC const leftTableId = `split-panel-left-${component.id}`; // 🔧 화면에 표시되는 컬럼 사용 (columns 속성) const configuredColumns = componentConfig.leftPanel?.columns || []; - const displayColumns = configuredColumns.map((col: any) => { - if (typeof col === 'string') return col; - return col.columnName || col.name || col; - }).filter(Boolean); - + const displayColumns = configuredColumns + .map((col: any) => { + if (typeof col === "string") return col; + return col.columnName || col.name || col; + }) + .filter(Boolean); + // 화면에 설정된 컬럼이 없으면 등록하지 않음 if (displayColumns.length === 0) return; @@ -470,7 +510,15 @@ export const SplitPanelLayoutComponent: React.FC }); return () => unregisterTable(leftTableId); - }, [component.id, componentConfig.leftPanel?.tableName, componentConfig.leftPanel?.columns, leftColumnLabels, component.title, isDesignMode, getLeftColumnUniqueValues]); + }, [ + component.id, + componentConfig.leftPanel?.tableName, + componentConfig.leftPanel?.columns, + leftColumnLabels, + component.title, + isDesignMode, + getLeftColumnUniqueValues, + ]); // 우측 테이블은 검색 컴포넌트 등록 제외 (좌측 마스터 테이블만 검색 가능) // useEffect(() => { @@ -540,7 +588,7 @@ export const SplitPanelLayoutComponent: React.FC try { const columnsResponse = await tableTypeApi.getColumns(rightTableName); setRightTableColumns(columnsResponse || []); - + // 우측 컬럼 라벨도 함께 로드 const labels: Record = {}; columnsResponse.forEach((col: any) => { @@ -569,9 +617,7 @@ export const SplitPanelLayoutComponent: React.FC try { // 1. 컬럼 메타 정보 조회 const columnsResponse = await tableTypeApi.getColumns(leftTableName); - const categoryColumns = columnsResponse.filter( - (col: any) => col.inputType === "category" - ); + const categoryColumns = columnsResponse.filter((col: any) => col.inputType === "category"); if (categoryColumns.length === 0) { setLeftCategoryMappings({}); @@ -584,9 +630,7 @@ export const SplitPanelLayoutComponent: React.FC for (const col of categoryColumns) { const columnName = col.columnName || col.column_name; try { - const response = await apiClient.get( - `/table-type/category-values/${leftTableName}/${columnName}` - ); + const response = await apiClient.get(`/table-categories/${leftTableName}/${columnName}/values`); if (response.data.success && response.data.data) { const valueMap: Record = {}; @@ -622,9 +666,7 @@ export const SplitPanelLayoutComponent: React.FC try { // 1. 컬럼 메타 정보 조회 const columnsResponse = await tableTypeApi.getColumns(rightTableName); - const categoryColumns = columnsResponse.filter( - (col: any) => col.inputType === "category" - ); + const categoryColumns = columnsResponse.filter((col: any) => col.inputType === "category"); if (categoryColumns.length === 0) { setRightCategoryMappings({}); @@ -637,9 +679,7 @@ export const SplitPanelLayoutComponent: React.FC for (const col of categoryColumns) { const columnName = col.columnName || col.column_name; try { - const response = await apiClient.get( - `/table-type/category-values/${rightTableName}/${columnName}` - ); + const response = await apiClient.get(`/table-categories/${rightTableName}/${columnName}/values`); if (response.data.success && response.data.data) { const valueMap: Record = {}; @@ -668,7 +708,7 @@ export const SplitPanelLayoutComponent: React.FC // 항목 펼치기/접기 토글 const toggleExpand = useCallback((itemId: any) => { - setExpandedItems(prev => { + setExpandedItems((prev) => { const newSet = new Set(prev); if (newSet.has(itemId)) { newSet.delete(itemId); @@ -680,21 +720,29 @@ export const SplitPanelLayoutComponent: React.FC }, []); // 추가 버튼 핸들러 - const handleAddClick = useCallback((panel: "left" | "right") => { - setAddModalPanel(panel); - - // 우측 패널 추가 시, 좌측에서 선택된 항목의 조인 컬럼 값을 자동으로 채움 - if (panel === "right" && selectedLeftItem && componentConfig.leftPanel?.leftColumn && componentConfig.rightPanel?.rightColumn) { - const leftColumnValue = selectedLeftItem[componentConfig.leftPanel.leftColumn]; - setAddModalFormData({ - [componentConfig.rightPanel.rightColumn]: leftColumnValue - }); - } else { - setAddModalFormData({}); - } - - setShowAddModal(true); - }, [selectedLeftItem, componentConfig]); + const handleAddClick = useCallback( + (panel: "left" | "right") => { + setAddModalPanel(panel); + + // 우측 패널 추가 시, 좌측에서 선택된 항목의 조인 컬럼 값을 자동으로 채움 + if ( + panel === "right" && + selectedLeftItem && + componentConfig.leftPanel?.leftColumn && + componentConfig.rightPanel?.rightColumn + ) { + const leftColumnValue = selectedLeftItem[componentConfig.leftPanel.leftColumn]; + setAddModalFormData({ + [componentConfig.rightPanel.rightColumn]: leftColumnValue, + }); + } else { + setAddModalFormData({}); + } + + setShowAddModal(true); + }, + [selectedLeftItem, componentConfig], + ); // 수정 버튼 핸들러 const handleEditClick = useCallback((panel: "left" | "right", item: any) => { @@ -706,11 +754,10 @@ export const SplitPanelLayoutComponent: React.FC // 수정 모달 저장 const handleEditModalSave = useCallback(async () => { - const tableName = editModalPanel === "left" - ? componentConfig.leftPanel?.tableName - : componentConfig.rightPanel?.tableName; - - const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + const tableName = + editModalPanel === "left" ? componentConfig.leftPanel?.tableName : componentConfig.rightPanel?.tableName; + + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id"; const primaryKey = editModalItem[sourceColumn] || editModalItem.id || editModalItem.ID; if (!tableName || !primaryKey) { @@ -724,15 +771,15 @@ export const SplitPanelLayoutComponent: React.FC try { console.log("📝 데이터 수정:", { tableName, primaryKey, data: editModalFormData }); - + // 프론트엔드 전용 필드 제거 (children, level 등) const cleanData = { ...editModalFormData }; delete cleanData.children; delete cleanData.level; - + // 좌측 패널 수정 시, 조인 관계 정보 포함 - let updatePayload: any = cleanData; - + const updatePayload: any = cleanData; + if (editModalPanel === "left" && componentConfig.rightPanel?.relation?.type === "join") { // 조인 관계가 있는 경우, 관계 정보를 페이로드에 추가 updatePayload._relationInfo = { @@ -743,7 +790,7 @@ export const SplitPanelLayoutComponent: React.FC }; console.log("🔗 조인 관계 정보 추가:", updatePayload._relationInfo); } - + const result = await dataApi.updateRecord(tableName, primaryKey, updatePayload); if (result.success) { @@ -751,12 +798,12 @@ export const SplitPanelLayoutComponent: React.FC title: "성공", description: "데이터가 성공적으로 수정되었습니다.", }); - + // 모달 닫기 setShowEditModal(false); setEditModalFormData({}); setEditModalItem(null); - + // 데이터 새로고침 if (editModalPanel === "left") { loadLeftData(); @@ -782,7 +829,16 @@ export const SplitPanelLayoutComponent: React.FC variant: "destructive", }); } - }, [editModalPanel, componentConfig, editModalItem, editModalFormData, toast, selectedLeftItem, loadLeftData, loadRightData]); + }, [ + editModalPanel, + componentConfig, + editModalItem, + editModalFormData, + toast, + selectedLeftItem, + loadLeftData, + loadRightData, + ]); // 삭제 버튼 핸들러 const handleDeleteClick = useCallback((panel: "left" | "right", item: any) => { @@ -794,21 +850,20 @@ export const SplitPanelLayoutComponent: React.FC // 삭제 확인 const handleDeleteConfirm = useCallback(async () => { // 우측 패널 삭제 시 중계 테이블 확인 - let tableName = deleteModalPanel === "left" - ? componentConfig.leftPanel?.tableName - : componentConfig.rightPanel?.tableName; - + let tableName = + deleteModalPanel === "left" ? componentConfig.leftPanel?.tableName : componentConfig.rightPanel?.tableName; + // 우측 패널 + 중계 테이블 모드인 경우 if (deleteModalPanel === "right" && componentConfig.rightPanel?.addConfig?.targetTable) { tableName = componentConfig.rightPanel.addConfig.targetTable; console.log("🔗 중계 테이블 모드: 삭제 대상 테이블 =", tableName); } - - const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id"; let primaryKey: any = deleteModalItem[sourceColumn] || deleteModalItem.id || deleteModalItem.ID; // 복합키 처리: deleteModalItem 전체를 전달 (백엔드에서 복합키 자동 처리) - if (deleteModalItem && typeof deleteModalItem === 'object') { + if (deleteModalItem && typeof deleteModalItem === "object") { primaryKey = deleteModalItem; console.log("🔑 복합키 가능성: 전체 객체 전달", primaryKey); } @@ -824,7 +879,7 @@ export const SplitPanelLayoutComponent: React.FC try { console.log("🗑️ 데이터 삭제:", { tableName, primaryKey }); - + const result = await dataApi.deleteRecord(tableName, primaryKey); if (result.success) { @@ -832,11 +887,11 @@ export const SplitPanelLayoutComponent: React.FC title: "성공", description: "데이터가 성공적으로 삭제되었습니다.", }); - + // 모달 닫기 setShowDeleteModal(false); setDeleteModalItem(null); - + // 데이터 새로고침 if (deleteModalPanel === "left") { loadLeftData(); @@ -857,13 +912,13 @@ export const SplitPanelLayoutComponent: React.FC } } catch (error: any) { console.error("데이터 삭제 오류:", error); - + // 외래키 제약조건 에러 처리 let errorMessage = "데이터 삭제 중 오류가 발생했습니다."; if (error?.response?.data?.error?.includes("foreign key")) { errorMessage = "이 데이터를 참조하는 다른 데이터가 있어 삭제할 수 없습니다."; } - + toast({ title: "오류", description: errorMessage, @@ -873,73 +928,76 @@ export const SplitPanelLayoutComponent: React.FC }, [deleteModalPanel, componentConfig, deleteModalItem, toast, selectedLeftItem, loadLeftData, loadRightData]); // 항목별 추가 버튼 핸들러 (좌측 항목의 + 버튼 - 하위 항목 추가) - const handleItemAddClick = useCallback((item: any) => { - const itemAddConfig = componentConfig.leftPanel?.itemAddConfig; - - if (!itemAddConfig) { - toast({ - title: "설정 오류", - description: "하위 항목 추가 설정이 없습니다.", - variant: "destructive", - }); - return; - } + const handleItemAddClick = useCallback( + (item: any) => { + const itemAddConfig = componentConfig.leftPanel?.itemAddConfig; - const { sourceColumn, parentColumn } = itemAddConfig; - - if (!sourceColumn || !parentColumn) { - toast({ - title: "설정 오류", - description: "현재 항목 ID 컬럼과 상위 항목 저장 컬럼을 설정해주세요.", - variant: "destructive", - }); - return; - } + if (!itemAddConfig) { + toast({ + title: "설정 오류", + description: "하위 항목 추가 설정이 없습니다.", + variant: "destructive", + }); + return; + } - // 선택된 항목의 sourceColumn 값을 가져와서 parentColumn에 매핑 - const sourceValue = item[sourceColumn]; - - if (!sourceValue) { - toast({ - title: "데이터 오류", - description: `선택한 항목의 ${sourceColumn} 값이 없습니다.`, - variant: "destructive", - }); - return; - } + const { sourceColumn, parentColumn } = itemAddConfig; - // 좌측 패널 추가 모달 열기 (parentColumn 값 미리 채우기) - setAddModalPanel("left-item"); - setAddModalFormData({ [parentColumn]: sourceValue }); - setShowAddModal(true); - }, [componentConfig, toast]); + if (!sourceColumn || !parentColumn) { + toast({ + title: "설정 오류", + description: "현재 항목 ID 컬럼과 상위 항목 저장 컬럼을 설정해주세요.", + variant: "destructive", + }); + return; + } + + // 선택된 항목의 sourceColumn 값을 가져와서 parentColumn에 매핑 + const sourceValue = item[sourceColumn]; + + if (!sourceValue) { + toast({ + title: "데이터 오류", + description: `선택한 항목의 ${sourceColumn} 값이 없습니다.`, + variant: "destructive", + }); + return; + } + + // 좌측 패널 추가 모달 열기 (parentColumn 값 미리 채우기) + setAddModalPanel("left-item"); + setAddModalFormData({ [parentColumn]: sourceValue }); + setShowAddModal(true); + }, + [componentConfig, toast], + ); // 추가 모달 저장 const handleAddModalSave = useCallback(async () => { // 테이블명과 모달 컬럼 결정 let tableName: string | undefined; let modalColumns: Array<{ name: string; label: string; required?: boolean }> | undefined; - let finalData = { ...addModalFormData }; - + const finalData = { ...addModalFormData }; + if (addModalPanel === "left") { tableName = componentConfig.leftPanel?.tableName; modalColumns = componentConfig.leftPanel?.addModalColumns; } else if (addModalPanel === "right") { // 우측 패널: 중계 테이블 설정이 있는지 확인 const addConfig = componentConfig.rightPanel?.addConfig; - + if (addConfig?.targetTable) { // 중계 테이블 모드 tableName = addConfig.targetTable; modalColumns = componentConfig.rightPanel?.addModalColumns; - + // 좌측 패널에서 선택된 값 자동 채우기 if (addConfig.leftPanelColumn && addConfig.targetColumn && selectedLeftItem) { const leftValue = selectedLeftItem[addConfig.leftPanelColumn]; finalData[addConfig.targetColumn] = leftValue; console.log(`🔗 좌측 패널 값 자동 채움: ${addConfig.targetColumn} = ${leftValue}`); } - + // 자동 채움 컬럼 추가 if (addConfig.autoFillColumns) { Object.entries(addConfig.autoFillColumns).forEach(([key, value]) => { @@ -968,7 +1026,7 @@ export const SplitPanelLayoutComponent: React.FC } // 필수 필드 검증 - const requiredFields = (modalColumns || []).filter(col => col.required); + const requiredFields = (modalColumns || []).filter((col) => col.required); for (const field of requiredFields) { if (!addModalFormData[field.name]) { toast({ @@ -982,19 +1040,19 @@ export const SplitPanelLayoutComponent: React.FC try { console.log("📝 데이터 추가:", { tableName, data: finalData }); - + const result = await dataApi.createRecord(tableName, finalData); - + if (result.success) { toast({ title: "성공", description: "데이터가 성공적으로 추가되었습니다.", }); - + // 모달 닫기 setShowAddModal(false); setAddModalFormData({}); - + // 데이터 새로고침 if (addModalPanel === "left" || addModalPanel === "left-item") { // 좌측 패널 데이터 새로고침 (일반 추가 또는 하위 항목 추가) @@ -1012,19 +1070,19 @@ export const SplitPanelLayoutComponent: React.FC } } catch (error: any) { console.error("데이터 추가 오류:", error); - + // 에러 메시지 추출 let errorMessage = "데이터 추가 중 오류가 발생했습니다."; - + if (error?.response?.data) { const responseData = error.response.data; - + // 백엔드에서 반환한 에러 메시지 확인 if (responseData.error) { // 중복 키 에러 처리 if (responseData.error.includes("duplicate key")) { errorMessage = "이미 존재하는 값입니다. 다른 값을 입력해주세요."; - } + } // NOT NULL 제약조건 에러 else if (responseData.error.includes("null value")) { const match = responseData.error.match(/column "(\w+)"/); @@ -1043,7 +1101,7 @@ export const SplitPanelLayoutComponent: React.FC errorMessage = responseData.message; } } - + toast({ title: "오류", description: errorMessage, @@ -1059,7 +1117,7 @@ export const SplitPanelLayoutComponent: React.FC // localStorage에서 저장된 설정 불러오기 const storageKey = `table_column_visibility_${leftTableName}_${currentUserId}`; const savedSettings = localStorage.getItem(storageKey); - + if (savedSettings) { try { const parsed = JSON.parse(savedSettings) as ColumnVisibility[]; @@ -1074,15 +1132,13 @@ export const SplitPanelLayoutComponent: React.FC // 🔧 컬럼 가시성 변경 시 localStorage에 저장 및 순서 업데이트 useEffect(() => { const leftTableName = componentConfig.leftPanel?.tableName; - + if (leftColumnVisibility.length > 0 && leftTableName && currentUserId) { // 순서 업데이트 - const newOrder = leftColumnVisibility - .map((cv) => cv.columnName) - .filter((name) => name !== "__checkbox__"); // 체크박스 제외 - + const newOrder = leftColumnVisibility.map((cv) => cv.columnName).filter((name) => name !== "__checkbox__"); // 체크박스 제외 + setLeftColumnOrder(newOrder); - + // localStorage에 저장 const storageKey = `table_column_visibility_${leftTableName}_${currentUserId}`; localStorage.setItem(storageKey, JSON.stringify(leftColumnVisibility)); @@ -1205,11 +1261,7 @@ export const SplitPanelLayoutComponent: React.FC {componentConfig.leftPanel?.title || "좌측 패널"} {!isDesignMode && componentConfig.leftPanel?.showAdd && ( - @@ -1217,7 +1269,7 @@ export const SplitPanelLayoutComponent: React.FC {componentConfig.leftPanel?.showSearch && (
- + - - 데이터 1-1 - 데이터 1-2 - 데이터 1-3 + + 데이터 1-1 + 데이터 1-2 + 데이터 1-3 - - 데이터 2-1 - 데이터 2-2 - 데이터 2-3 + + 데이터 2-1 + 데이터 2-2 + 데이터 2-3 @@ -1276,22 +1328,27 @@ export const SplitPanelLayoutComponent: React.FC : leftData; // 🔧 가시성 처리된 컬럼 사용 - const columnsToShow = visibleLeftColumns.length > 0 - ? visibleLeftColumns.map((col: any) => { - const colName = typeof col === 'string' ? col : (col.name || col.columnName); - return { - name: colName, - label: leftColumnLabels[colName] || (typeof col === 'object' ? col.label : null) || colName, - width: typeof col === 'object' ? col.width : 150, - align: (typeof col === 'object' ? col.align : "left") as "left" | "center" | "right" - }; - }) - : Object.keys(filteredData[0] || {}).filter(key => key !== 'children' && key !== 'level').slice(0, 5).map(key => ({ - name: key, - label: leftColumnLabels[key] || key, - width: 150, - align: "left" as const - })); + const columnsToShow = + visibleLeftColumns.length > 0 + ? visibleLeftColumns.map((col: any) => { + const colName = typeof col === "string" ? col : col.name || col.columnName; + return { + name: colName, + label: + leftColumnLabels[colName] || (typeof col === "object" ? col.label : null) || colName, + width: typeof col === "object" ? col.width : 150, + align: (typeof col === "object" ? col.align : "left") as "left" | "center" | "right", + }; + }) + : Object.keys(filteredData[0] || {}) + .filter((key) => key !== "children" && key !== "level") + .slice(0, 5) + .map((key) => ({ + name: key, + label: leftColumnLabels[key] || key, + width: 150, + align: "left" as const, + })); // 🔧 그룹화된 데이터 렌더링 if (groupedLeftData.length > 0) { @@ -1299,7 +1356,7 @@ export const SplitPanelLayoutComponent: React.FC
{groupedLeftData.map((group, groupIdx) => (
-
+
{group.groupKey} ({group.count}개)
@@ -1308,8 +1365,11 @@ export const SplitPanelLayoutComponent: React.FC {columnsToShow.map((col, idx) => ( @@ -1318,10 +1378,12 @@ export const SplitPanelLayoutComponent: React.FC {group.items.map((item, idx) => { - const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id"; const itemId = item[sourceColumn] || item.id || item.ID || idx; - const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); - + const isSelected = + selectedLeftItem && + (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); + return ( {columnsToShow.map((col, colIdx) => (
{col.label}
{formatCellValue(col.name, item[col.name], leftCategoryMappings)} @@ -1354,13 +1416,16 @@ export const SplitPanelLayoutComponent: React.FC return (
- + {columnsToShow.map((col, idx) => ( @@ -1369,10 +1434,12 @@ export const SplitPanelLayoutComponent: React.FC {filteredData.map((item, idx) => { - const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id"; const itemId = item[sourceColumn] || item.id || item.ID || idx; - const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); - + const isSelected = + selectedLeftItem && + (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); + return ( {columnsToShow.map((col, colIdx) => (
{col.label}
{formatCellValue(col.name, item[col.name], leftCategoryMappings)} @@ -1441,30 +1508,31 @@ export const SplitPanelLayoutComponent: React.FC 데이터를 불러오는 중... ) : ( - (() => { - // 검색 필터링 (클라이언트 사이드) - const filteredLeftData = leftSearchQuery - ? leftData.filter((item) => { - const searchLower = leftSearchQuery.toLowerCase(); - return Object.entries(item).some(([key, value]) => { - if (value === null || value === undefined) return false; - return String(value).toLowerCase().includes(searchLower); - }); - }) - : leftData; + (() => { + // 검색 필터링 (클라이언트 사이드) + const filteredLeftData = leftSearchQuery + ? leftData.filter((item) => { + const searchLower = leftSearchQuery.toLowerCase(); + return Object.entries(item).some(([key, value]) => { + if (value === null || value === undefined) return false; + return String(value).toLowerCase().includes(searchLower); + }); + }) + : leftData; - // 재귀 렌더링 함수 - const renderTreeItem = (item: any, index: number): React.ReactNode => { - const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; - const itemId = item[sourceColumn] || item.id || item.ID || index; - const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); - const hasChildren = item.children && item.children.length > 0; - const isExpanded = expandedItems.has(itemId); - const level = item.level || 0; + // 재귀 렌더링 함수 + const renderTreeItem = (item: any, index: number): React.ReactNode => { + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || "id"; + const itemId = item[sourceColumn] || item.id || item.ID || index; + const isSelected = + selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); + const hasChildren = item.children && item.children.length > 0; + const isExpanded = expandedItems.has(itemId); + const level = item.level || 0; - // 조인에 사용하는 leftColumn을 필수로 표시 - const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; - let displayFields: { label: string; value: any }[] = []; + // 조인에 사용하는 leftColumn을 필수로 표시 + const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; + let displayFields: { label: string; value: any }[] = []; // 디버그 로그 if (index === 0) { @@ -1482,10 +1550,13 @@ export const SplitPanelLayoutComponent: React.FC // 추가로 다른 의미있는 필드 1-2개 표시 (name, title 등) const additionalKeys = Object.keys(item).filter( - (k) => k !== "id" && k !== "ID" && k !== leftColumn && - (k.includes("name") || k.includes("title") || k.includes("desc")) + (k) => + k !== "id" && + k !== "ID" && + k !== leftColumn && + (k.includes("name") || k.includes("title") || k.includes("desc")), ); - + if (additionalKeys.length > 0) { displayFields.push({ label: additionalKeys[0], @@ -1509,115 +1580,119 @@ export const SplitPanelLayoutComponent: React.FC } } - const displayTitle = displayFields[0]?.value || item.name || item.title || `항목 ${index + 1}`; - const displaySubtitle = displayFields[1]?.value || null; + const displayTitle = displayFields[0]?.value || item.name || item.title || `항목 ${index + 1}`; + const displaySubtitle = displayFields[1]?.value || null; - return ( - - {/* 현재 항목 */} -
-
{ - handleLeftItemSelect(item); - if (hasChildren) { - toggleExpand(itemId); - } - }} + return ( + + {/* 현재 항목 */} +
- {/* 펼치기/접기 아이콘 */} - {hasChildren ? ( -
- {isExpanded ? ( - - ) : ( - +
{ + handleLeftItemSelect(item); + if (hasChildren) { + toggleExpand(itemId); + } + }} + > + {/* 펼치기/접기 아이콘 */} + {hasChildren ? ( +
+ {isExpanded ? ( + + ) : ( + + )} +
+ ) : ( +
+ )} + + {/* 항목 내용 */} +
+
{displayTitle}
+ {displaySubtitle && ( +
{displaySubtitle}
)}
- ) : ( -
- )} - - {/* 항목 내용 */} -
-
{displayTitle}
- {displaySubtitle &&
{displaySubtitle}
} -
- - {/* 항목별 버튼들 */} - {!isDesignMode && ( -
- {/* 수정 버튼 */} - - - {/* 삭제 버튼 */} - - - {/* 항목별 추가 버튼 */} - {componentConfig.leftPanel?.showItemAddButton && ( - - )} -
- )} -
-
- - {/* 자식 항목들 (접혀있으면 표시 안함) */} - {hasChildren && isExpanded && item.children.map((child: any, childIndex: number) => renderTreeItem(child, childIndex))} - - ); - }; - return filteredLeftData.length > 0 ? ( - // 실제 데이터 표시 - filteredLeftData.map((item, index) => renderTreeItem(item, index)) - ) : ( - // 검색 결과 없음 -
- {leftSearchQuery ? ( - <> -

검색 결과가 없습니다.

-

다른 검색어를 입력해보세요.

- - ) : ( - "데이터가 없습니다." - )} -
- ); - })() - )} + {/* 항목별 버튼들 */} + {!isDesignMode && ( +
+ {/* 수정 버튼 */} + + + {/* 삭제 버튼 */} + + + {/* 항목별 추가 버튼 */} + {componentConfig.leftPanel?.showItemAddButton && ( + + )} +
+ )} +
+
+ + {/* 자식 항목들 (접혀있으면 표시 안함) */} + {hasChildren && + isExpanded && + item.children.map((child: any, childIndex: number) => renderTreeItem(child, childIndex))} + + ); + }; + + return filteredLeftData.length > 0 ? ( + // 실제 데이터 표시 + filteredLeftData.map((item, index) => renderTreeItem(item, index)) + ) : ( + // 검색 결과 없음 +
+ {leftSearchQuery ? ( + <> +

검색 결과가 없습니다.

+

다른 검색어를 입력해보세요.

+ + ) : ( + "데이터가 없습니다." + )} +
+ ); + })() + )}
)} @@ -1628,9 +1703,9 @@ export const SplitPanelLayoutComponent: React.FC {resizable && (
- + className="group bg-border hover:bg-primary flex w-1 cursor-col-resize items-center justify-center transition-colors" + > +
)} @@ -1648,11 +1723,7 @@ export const SplitPanelLayoutComponent: React.FC {!isDesignMode && (
{componentConfig.rightPanel?.showAdd && ( - @@ -1663,7 +1734,7 @@ export const SplitPanelLayoutComponent: React.FC
{componentConfig.rightPanel?.showSearch && (
- + // 로딩 중
- -

데이터를 불러오는 중...

+ +

데이터를 불러오는 중...

) : rightData ? ( @@ -1701,75 +1772,81 @@ export const SplitPanelLayoutComponent: React.FC // 테이블 모드 체크 const isTableMode = componentConfig.rightPanel?.displayMode === "table"; - + if (isTableMode) { // 테이블 모드 렌더링 const displayColumns = componentConfig.rightPanel?.columns || []; - const columnsToShow = displayColumns.length > 0 - ? displayColumns.map(col => ({ - ...col, - label: rightColumnLabels[col.name] || col.label || col.name - })) - : Object.keys(filteredData[0] || {}).filter(key => !key.toLowerCase().includes("password")).slice(0, 5).map(key => ({ - name: key, - label: rightColumnLabels[key] || key, - width: 150, - align: "left" as const - })); + const columnsToShow = + displayColumns.length > 0 + ? displayColumns.map((col) => ({ + ...col, + label: rightColumnLabels[col.name] || col.label || col.name, + })) + : Object.keys(filteredData[0] || {}) + .filter((key) => !key.toLowerCase().includes("password")) + .slice(0, 5) + .map((key) => ({ + name: key, + label: rightColumnLabels[key] || key, + width: 150, + align: "left" as const, + })); return (
-
+
{filteredData.length}개의 관련 데이터 {rightSearchQuery && filteredData.length !== rightData.length && ( - (전체 {rightData.length}개 중) + (전체 {rightData.length}개 중) )}
- + {columnsToShow.map((col, idx) => ( ))} {!isDesignMode && ( - + )} {filteredData.map((item, idx) => { const itemId = item.id || item.ID || idx; - + return ( - + {columnsToShow.map((col, colIdx) => ( ))} {!isDesignMode && ( -
{col.label} 작업 + 작업 +
{formatCellValue(col.name, item[col.name], rightCategoryMappings)} +
@@ -1919,13 +2001,13 @@ export const SplitPanelLayoutComponent: React.FC
전체 상세 정보
- + {allValues.map(([key, value]) => ( - - + ))} @@ -1938,11 +2020,11 @@ export const SplitPanelLayoutComponent: React.FC })} ) : ( -
+
{rightSearchQuery ? ( <>

검색 결과가 없습니다.

-

다른 검색어를 입력해보세요.

+

다른 검색어를 입력해보세요.

) : ( "관련 데이터가 없습니다." @@ -1961,13 +2043,16 @@ export const SplitPanelLayoutComponent: React.FC displayEntries = rightColumns .map((col) => [col.name, rightData[col.name]] as [string, any]) .filter(([_, value]) => value !== null && value !== undefined && value !== ""); - + console.log("🔍 상세 모드 표시 로직:"); - console.log(" ✅ 설정된 컬럼 사용:", rightColumns.map(c => c.name)); + console.log( + " ✅ 설정된 컬럼 사용:", + rightColumns.map((c) => c.name), + ); } else { // 설정 없으면 모든 컬럼 표시 displayEntries = Object.entries(rightData).filter( - ([_, value]) => value !== null && value !== undefined && value !== "" + ([_, value]) => value !== null && value !== undefined && value !== "", ); console.log(" ⚠️ 컬럼 설정 없음, 모든 컬럼 표시"); } @@ -2010,7 +2095,7 @@ export const SplitPanelLayoutComponent: React.FC ) : ( // 선택 없음
-
+

좌측에서 항목을 선택하세요

선택한 항목의 상세 정보가 여기에 표시됩니다

@@ -2025,14 +2110,14 @@ export const SplitPanelLayoutComponent: React.FC - {addModalPanel === "left" + {addModalPanel === "left" ? `${componentConfig.leftPanel?.title} 추가` : addModalPanel === "right" - ? `${componentConfig.rightPanel?.title} 추가` - : `하위 ${componentConfig.leftPanel?.title} 추가`} + ? `${componentConfig.rightPanel?.title} 추가` + : `하위 ${componentConfig.leftPanel?.title} 추가`} - {addModalPanel === "left-item" + {addModalPanel === "left-item" ? "선택한 항목의 하위 항목을 추가합니다. 필수 항목을 입력해주세요." : "새로운 데이터를 추가합니다. 필수 항목을 입력해주세요."} @@ -2042,7 +2127,7 @@ export const SplitPanelLayoutComponent: React.FC {(() => { // 어떤 컬럼들을 표시할지 결정 let modalColumns: Array<{ name: string; label: string; required?: boolean }> | undefined; - + if (addModalPanel === "left") { modalColumns = componentConfig.leftPanel?.addModalColumns; } else if (addModalPanel === "right") { @@ -2050,42 +2135,44 @@ export const SplitPanelLayoutComponent: React.FC } else if (addModalPanel === "left-item") { modalColumns = componentConfig.leftPanel?.itemAddConfig?.addModalColumns; } - + return modalColumns?.map((col, index) => { - // 항목별 추가 버튼으로 열렸을 때, parentColumn은 미리 채워져 있고 수정 불가 - const isItemAddPreFilled = addModalPanel === "left-item" - && componentConfig.leftPanel?.itemAddConfig?.parentColumn === col.name - && addModalFormData[col.name]; - - // 우측 패널 추가 시, 조인 컬럼(rightColumn)은 미리 채워져 있고 수정 불가 - const isRightJoinPreFilled = addModalPanel === "right" - && componentConfig.rightPanel?.rightColumn === col.name - && addModalFormData[col.name]; - - const isPreFilled = isItemAddPreFilled || isRightJoinPreFilled; - - return ( -
- - { - setAddModalFormData(prev => ({ - ...prev, - [col.name]: e.target.value - })); - }} - placeholder={`${col.label} 입력`} - className="h-8 text-xs sm:h-10 sm:text-sm" - required={col.required} - disabled={isPreFilled} - /> -
- ); + // 항목별 추가 버튼으로 열렸을 때, parentColumn은 미리 채워져 있고 수정 불가 + const isItemAddPreFilled = + addModalPanel === "left-item" && + componentConfig.leftPanel?.itemAddConfig?.parentColumn === col.name && + addModalFormData[col.name]; + + // 우측 패널 추가 시, 조인 컬럼(rightColumn)은 미리 채워져 있고 수정 불가 + const isRightJoinPreFilled = + addModalPanel === "right" && + componentConfig.rightPanel?.rightColumn === col.name && + addModalFormData[col.name]; + + const isPreFilled = isItemAddPreFilled || isRightJoinPreFilled; + + return ( +
+ + { + setAddModalFormData((prev) => ({ + ...prev, + [col.name]: e.target.value, + })); + }} + placeholder={`${col.label} 입력`} + className="h-8 text-xs sm:h-10 sm:text-sm" + required={col.required} + disabled={isPreFilled} + /> +
+ ); }); })()}
@@ -2098,10 +2185,7 @@ export const SplitPanelLayoutComponent: React.FC > 취소 - @@ -2114,7 +2198,7 @@ export const SplitPanelLayoutComponent: React.FC - {editModalPanel === "left" + {editModalPanel === "left" ? `${componentConfig.leftPanel?.title} 수정` : `${componentConfig.rightPanel?.title} 수정`} @@ -2124,90 +2208,91 @@ export const SplitPanelLayoutComponent: React.FC
- {editModalItem && (() => { - // 좌측 패널 수정: leftColumn만 수정 가능 - if (editModalPanel === "left") { - const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; - - // leftColumn만 표시 - if (!leftColumn || editModalFormData[leftColumn] === undefined) { - return

수정 가능한 컬럼이 없습니다.

; - } + {editModalItem && + (() => { + // 좌측 패널 수정: leftColumn만 수정 가능 + if (editModalPanel === "left") { + const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; - return ( -
- - { - setEditModalFormData(prev => ({ - ...prev, - [leftColumn]: e.target.value - })); - }} - placeholder={`${leftColumn} 입력`} - className="h-8 text-xs sm:h-10 sm:text-sm" - /> -
- ); - } - - // 우측 패널 수정: 우측 패널에 설정된 표시 컬럼들만 - if (editModalPanel === "right") { - const rightColumns = componentConfig.rightPanel?.columns; - - if (rightColumns && rightColumns.length > 0) { - // 설정된 컬럼만 표시 - return rightColumns.map((col) => ( -
-
+ {getColumnLabel(key)} {String(value)}{String(value)}