From 026e99511cce366bb0e543868709792190db6996 Mon Sep 17 00:00:00 2001 From: DDD1542 Date: Fri, 27 Feb 2026 14:30:31 +0900 Subject: [PATCH] refactor: Enhance label display and drag-and-drop functionality in table configuration - Updated the InteractiveScreenViewer and InteractiveScreenViewerDynamic components to include label positioning and size adjustments based on horizontal label settings. - Improved the DynamicComponentRenderer to handle label display logic more robustly, allowing for string values in addition to boolean. - Introduced drag-and-drop functionality in the TableListConfigPanel for reordering selected columns, enhancing user experience and flexibility in column management. - Refactored the display name resolution logic to prioritize available column labels, ensuring accurate representation in the UI. --- .../screen/InteractiveScreenViewer.tsx | 11 +- .../screen/InteractiveScreenViewerDynamic.tsx | 17 +- .../lib/registry/DynamicComponentRenderer.tsx | 4 +- .../table-list/TableListConfigPanel.tsx | 228 +++++++++-------- .../v2-table-list/TableListConfigPanel.tsx | 229 +++++++++--------- 5 files changed, 255 insertions(+), 234 deletions(-) diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx index 1d64b597..7a9a3ff3 100644 --- a/frontend/components/screen/InteractiveScreenViewer.tsx +++ b/frontend/components/screen/InteractiveScreenViewer.tsx @@ -2233,8 +2233,17 @@ export const InteractiveScreenViewer: React.FC = ( ...component, style: { ...component.style, - labelDisplay: false, // 상위에서 라벨을 표시했으므로 컴포넌트 내부에서는 숨김 + labelDisplay: false, + labelPosition: "top" as const, + ...(isHorizontalLabel ? { width: "100%", height: "100%" } : {}), }, + ...(isHorizontalLabel ? { + size: { + ...component.size, + width: undefined as unknown as number, + height: undefined as unknown as number, + }, + } : {}), } : component; diff --git a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx index 253c886d..bcf9959c 100644 --- a/frontend/components/screen/InteractiveScreenViewerDynamic.tsx +++ b/frontend/components/screen/InteractiveScreenViewerDynamic.tsx @@ -1292,7 +1292,22 @@ export const InteractiveScreenViewerDynamic: React.FC = componentType === "modal-repeater-table" || componentType === "v2-input"; - // 🆕 v2-input 등의 라벨 표시 로직 (labelDisplay가 true일 때만 라벨 표시) + // 🆕 v2-input 등의 라벨 표시 로직 (labelDisplay가 true/"true"일 때만 라벨 표시) const labelDisplay = component.style?.labelDisplay ?? (component as any).labelDisplay; - const effectiveLabel = labelDisplay === true + const effectiveLabel = (labelDisplay === true || labelDisplay === "true") ? (component.style?.labelText || (component as any).label || component.componentConfig?.label) : undefined; diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index f3a28c4c..8526b0c9 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -10,11 +10,74 @@ import { TableListConfig, ColumnConfig } from "./types"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { tableTypeApi } from "@/lib/api/screen"; import { tableManagementApi } from "@/lib/api/tableManagement"; -import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check, Lock, Unlock, Database, Table2, Link2, GripVertical, Pencil } from "lucide-react"; +import { Plus, Trash2, ArrowUp, ArrowDown, ChevronsUpDown, Check, Lock, Unlock, Database, Table2, Link2, GripVertical, X } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel"; +import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +/** + * 드래그 가능한 선택된 컬럼 행 (v2-split-panel-layout의 SortableColumnRow 동일 패턴) + */ +function SortableColumnRow({ + id, + col, + index, + isEntityJoin, + onLabelChange, + onWidthChange, + onRemove, +}: { + id: string; + col: ColumnConfig; + index: number; + isEntityJoin?: boolean; + onLabelChange: (value: string) => void; + onWidthChange: (value: number) => void; + onRemove: () => void; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + const style = { transform: CSS.Transform.toString(transform), transition }; + + return ( +
+
+ +
+ {isEntityJoin ? ( + + ) : ( + #{index + 1} + )} + onLabelChange(e.target.value)} + placeholder="표시명" + className="h-6 min-w-0 flex-1 text-xs" + /> + onWidthChange(parseInt(e.target.value) || 100)} + placeholder="너비" + className="h-6 w-14 shrink-0 text-xs" + /> + +
+ ); +} export interface TableListConfigPanelProps { config: TableListConfig; @@ -348,11 +411,11 @@ export const TableListConfigPanel: React.FC = ({ const existingColumn = config.columns?.find((col) => col.columnName === columnName); if (existingColumn) return; - // tableColumns에서 해당 컬럼의 라벨 정보 찾기 + // tableColumns → availableColumns 순서로 한국어 라벨 찾기 const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName); + const availableColumnInfo = availableColumns.find((col) => col.columnName === columnName); - // 라벨명 우선 사용, 없으면 컬럼명 사용 - const displayName = columnInfo?.label || columnInfo?.displayName || columnName; + const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName; const newColumn: ColumnConfig = { columnName, @@ -1213,31 +1276,59 @@ export const TableListConfigPanel: React.FC = ({ )} - {/* 선택된 컬럼 순서 변경 */} + {/* 선택된 컬럼 순서 변경 (DnD) */} {config.columns && config.columns.length > 0 && (
-

컬럼 순서 / 설정

+

표시할 컬럼 ({config.columns.length}개 선택)

- 선택된 컬럼의 순서를 변경하거나 표시명을 수정할 수 있습니다 + 드래그하여 순서를 변경하거나 표시명/너비를 수정할 수 있습니다


-
- {[...(config.columns || [])] - .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) - .map((column, idx) => ( - moveColumn(column.columnName, direction)} - onRemove={() => removeColumn(column.columnName)} - onUpdate={(updates) => updateColumn(column.columnName, updates)} - /> - ))} -
+ { + const { active, over } = event; + if (!over || active.id === over.id) return; + const columns = [...(config.columns || [])]; + const oldIndex = columns.findIndex((c) => c.columnName === active.id); + const newIndex = columns.findIndex((c) => c.columnName === over.id); + if (oldIndex !== -1 && newIndex !== -1) { + const reordered = arrayMove(columns, oldIndex, newIndex); + reordered.forEach((col, idx) => { col.order = idx; }); + handleChange("columns", reordered); + } + }} + > + c.columnName)} + strategy={verticalListSortingStrategy} + > +
+ {(config.columns || []).map((column, idx) => { + const resolvedLabel = + column.displayName && column.displayName !== column.columnName + ? column.displayName + : availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName; + + const colWithLabel = { ...column, displayName: resolvedLabel }; + return ( + updateColumn(column.columnName, { displayName: value })} + onWidthChange={(value) => updateColumn(column.columnName, { width: value })} + onRemove={() => removeColumn(column.columnName)} + /> + ); + })} +
+
+
)} @@ -1269,96 +1360,3 @@ export const TableListConfigPanel: React.FC = ({ ); }; -/** - * 선택된 컬럼 항목 컴포넌트 - * 순서 이동, 삭제, 표시명 수정 기능 제공 - */ -const SelectedColumnItem: React.FC<{ - column: ColumnConfig; - index: number; - total: number; - onMove: (direction: "up" | "down") => void; - onRemove: () => void; - onUpdate: (updates: Partial) => void; -}> = ({ column, index, total, onMove, onRemove, onUpdate }) => { - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(column.displayName || column.columnName); - - const handleSave = () => { - const trimmed = editValue.trim(); - if (trimmed && trimmed !== column.displayName) { - onUpdate({ displayName: trimmed }); - } - setIsEditing(false); - }; - - return ( -
- - - {index + 1} - - {isEditing ? ( - setEditValue(e.target.value)} - onBlur={handleSave} - onKeyDown={(e) => { - if (e.key === "Enter") handleSave(); - if (e.key === "Escape") { - setEditValue(column.displayName || column.columnName); - setIsEditing(false); - } - }} - className="h-5 flex-1 px-1 text-xs" - autoFocus - /> - ) : ( - - )} - -
- - - -
-
- ); -}; diff --git a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx index ad250a16..7de8a533 100644 --- a/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/v2-table-list/TableListConfigPanel.tsx @@ -23,12 +23,75 @@ import { Table2, Link2, GripVertical, - Pencil, + X, } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { cn } from "@/lib/utils"; import { DataFilterConfigPanel } from "@/components/screen/config-panels/DataFilterConfigPanel"; +import { DndContext, closestCenter, type DragEndEvent } from "@dnd-kit/core"; +import { SortableContext, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +/** + * 드래그 가능한 선택된 컬럼 행 (v2-split-panel-layout의 SortableColumnRow 동일 패턴) + */ +function SortableColumnRow({ + id, + col, + index, + isEntityJoin, + onLabelChange, + onWidthChange, + onRemove, +}: { + id: string; + col: ColumnConfig; + index: number; + isEntityJoin?: boolean; + onLabelChange: (value: string) => void; + onWidthChange: (value: number) => void; + onRemove: () => void; +}) { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); + const style = { transform: CSS.Transform.toString(transform), transition }; + + return ( +
+
+ +
+ {isEntityJoin ? ( + + ) : ( + #{index + 1} + )} + onLabelChange(e.target.value)} + placeholder="표시명" + className="h-6 min-w-0 flex-1 text-xs" + /> + onWidthChange(parseInt(e.target.value) || 100)} + placeholder="너비" + className="h-6 w-14 shrink-0 text-xs" + /> + +
+ ); +} export interface TableListConfigPanelProps { config: TableListConfig; @@ -368,11 +431,11 @@ export const TableListConfigPanel: React.FC = ({ const existingColumn = config.columns?.find((col) => col.columnName === columnName); if (existingColumn) return; - // tableColumns에서 해당 컬럼의 라벨 정보 찾기 + // tableColumns → availableColumns 순서로 한국어 라벨 찾기 const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName); + const availableColumnInfo = availableColumns.find((col) => col.columnName === columnName); - // 라벨명 우선 사용, 없으면 컬럼명 사용 - const displayName = columnInfo?.label || columnInfo?.displayName || columnName; + const displayName = columnInfo?.label || columnInfo?.displayName || availableColumnInfo?.label || columnName; const newColumn: ColumnConfig = { columnName, @@ -1460,31 +1523,60 @@ export const TableListConfigPanel: React.FC = ({ )} - {/* 선택된 컬럼 순서 변경 */} + {/* 선택된 컬럼 순서 변경 (DnD) */} {config.columns && config.columns.length > 0 && (
-

컬럼 순서 / 설정

+

표시할 컬럼 ({config.columns.length}개 선택)

- 선택된 컬럼의 순서를 변경하거나 표시명을 수정할 수 있습니다 + 드래그하여 순서를 변경하거나 표시명/너비를 수정할 수 있습니다


-
- {[...(config.columns || [])] - .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) - .map((column, idx) => ( - moveColumn(column.columnName, direction)} - onRemove={() => removeColumn(column.columnName)} - onUpdate={(updates) => updateColumn(column.columnName, updates)} - /> - ))} -
+ { + const { active, over } = event; + if (!over || active.id === over.id) return; + const columns = [...(config.columns || [])]; + const oldIndex = columns.findIndex((c) => c.columnName === active.id); + const newIndex = columns.findIndex((c) => c.columnName === over.id); + if (oldIndex !== -1 && newIndex !== -1) { + const reordered = arrayMove(columns, oldIndex, newIndex); + reordered.forEach((col, idx) => { col.order = idx; }); + handleChange("columns", reordered); + } + }} + > + c.columnName)} + strategy={verticalListSortingStrategy} + > +
+ {(config.columns || []).map((column, idx) => { + // displayName이 columnName과 같으면 한국어 라벨 미설정 → availableColumns에서 찾기 + const resolvedLabel = + column.displayName && column.displayName !== column.columnName + ? column.displayName + : availableColumns.find((c) => c.columnName === column.columnName)?.label || column.displayName || column.columnName; + + const colWithLabel = { ...column, displayName: resolvedLabel }; + return ( + updateColumn(column.columnName, { displayName: value })} + onWidthChange={(value) => updateColumn(column.columnName, { width: value })} + onRemove={() => removeColumn(column.columnName)} + /> + ); + })} +
+
+
)} @@ -1515,96 +1607,3 @@ export const TableListConfigPanel: React.FC = ({ ); }; -/** - * 선택된 컬럼 항목 컴포넌트 - * 순서 이동, 삭제, 표시명 수정 기능 제공 - */ -const SelectedColumnItem: React.FC<{ - column: ColumnConfig; - index: number; - total: number; - onMove: (direction: "up" | "down") => void; - onRemove: () => void; - onUpdate: (updates: Partial) => void; -}> = ({ column, index, total, onMove, onRemove, onUpdate }) => { - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(column.displayName || column.columnName); - - const handleSave = () => { - const trimmed = editValue.trim(); - if (trimmed && trimmed !== column.displayName) { - onUpdate({ displayName: trimmed }); - } - setIsEditing(false); - }; - - return ( -
- - - {index + 1} - - {isEditing ? ( - setEditValue(e.target.value)} - onBlur={handleSave} - onKeyDown={(e) => { - if (e.key === "Enter") handleSave(); - if (e.key === "Escape") { - setEditValue(column.displayName || column.columnName); - setIsEditing(false); - } - }} - className="h-5 flex-1 px-1 text-xs" - autoFocus - /> - ) : ( - - )} - -
- - - -
-
- ); -};